You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by li...@apache.org on 2023/04/11 14:59:12 UTC
[superset] branch master updated: feat: implement drill by table (#23603)
This is an automated email from the ASF dual-hosted git repository.
lilykuang pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/superset.git
The following commit(s) were added to refs/heads/master by this push:
new 02275587d1 feat: implement drill by table (#23603)
02275587d1 is described below
commit 02275587d1c1ed7d93c8d4de1fa132e157a991d2
Author: Lily Kuang <li...@preset.io>
AuthorDate: Tue Apr 11 07:59:03 2023 -0700
feat: implement drill by table (#23603)
---
.../components/Chart/DrillBy/DrillByChart.test.tsx | 41 +++-------
.../src/components/Chart/DrillBy/DrillByChart.tsx | 49 +++++-------
.../components/Chart/DrillBy/DrillByMenuItems.tsx | 19 ++---
.../components/Chart/DrillBy/DrillByModal.test.tsx | 44 ++++++++---
.../src/components/Chart/DrillBy/DrillByModal.tsx | 90 +++++++++++++++++++---
superset-frontend/src/components/Chart/types.ts | 5 ++
6 files changed, 162 insertions(+), 86 deletions(-)
diff --git a/superset-frontend/src/components/Chart/DrillBy/DrillByChart.test.tsx b/superset-frontend/src/components/Chart/DrillBy/DrillByChart.test.tsx
index 55dbd3dee3..02d1754939 100644
--- a/superset-frontend/src/components/Chart/DrillBy/DrillByChart.test.tsx
+++ b/superset-frontend/src/components/Chart/DrillBy/DrillByChart.test.tsx
@@ -22,28 +22,18 @@ import chartQueries, { sliceId } from 'spec/fixtures/mockChartQueries';
import fetchMock from 'fetch-mock';
import DrillByChart from './DrillByChart';
-const CHART_DATA_ENDPOINT =
- 'glob:*api/v1/chart/data?form_data=%7B%22slice_id%22%3A18%7D';
-
const chart = chartQueries[sliceId];
-const fetchWithNoData = () => {
- fetchMock.post(CHART_DATA_ENDPOINT, {
- result: [
- {
- total_count: 0,
- data: [],
- colnames: [],
- coltypes: [],
- },
- ],
- });
-};
-
-const setup = (overrides: Record<string, any> = {}) =>
- render(<DrillByChart formData={{ ...chart.form_data, ...overrides }} />, {
- useRedux: true,
- });
+const setup = (overrides: Record<string, any> = {}, result?: any) =>
+ render(
+ <DrillByChart
+ formData={{ ...chart.form_data, ...overrides }}
+ result={result}
+ />,
+ {
+ useRedux: true,
+ },
+ );
const waitForRender = (overrides: Record<string, any> = {}) =>
waitFor(() => setup(overrides));
@@ -51,20 +41,11 @@ const waitForRender = (overrides: Record<string, any> = {}) =>
afterEach(fetchMock.restore);
test('should render', async () => {
- fetchWithNoData();
const { container } = await waitForRender();
expect(container).toBeInTheDocument();
});
-test('should render loading indicator', async () => {
- setup();
- await waitFor(() =>
- expect(screen.getByLabelText('Loading')).toBeInTheDocument(),
- );
-});
-
test('should render the "No results" components', async () => {
- fetchWithNoData();
- setup();
+ setup({}, []);
expect(await screen.findByText('No Results')).toBeInTheDocument();
});
diff --git a/superset-frontend/src/components/Chart/DrillBy/DrillByChart.tsx b/superset-frontend/src/components/Chart/DrillBy/DrillByChart.tsx
index 5d1a3dec23..d19dbe9137 100644
--- a/superset-frontend/src/components/Chart/DrillBy/DrillByChart.tsx
+++ b/superset-frontend/src/components/Chart/DrillBy/DrillByChart.tsx
@@ -16,26 +16,21 @@
* specific language governing permissions and limitations
* under the License.
*/
-import React, { useEffect, useState } from 'react';
-import { BaseFormData, Behavior, css, SuperChart } from '@superset-ui/core';
-import { getChartDataRequest } from 'src/components/Chart/chartAction';
-import Loading from 'src/components/Loading';
+import React from 'react';
+import {
+ BaseFormData,
+ Behavior,
+ QueryData,
+ SuperChart,
+ css,
+} from '@superset-ui/core';
interface DrillByChartProps {
formData: BaseFormData & { [key: string]: any };
+ result: QueryData[];
}
-export default function DrillByChart({ formData }: DrillByChartProps) {
- const [chartDataResult, setChartDataResult] = useState();
-
- useEffect(() => {
- getChartDataRequest({
- formData,
- }).then(({ json }) => {
- setChartDataResult(json.result);
- });
- }, []);
-
+export default function DrillByChart({ formData, result }: DrillByChartProps) {
return (
<div
css={css`
@@ -43,20 +38,16 @@ export default function DrillByChart({ formData }: DrillByChartProps) {
height: 100%;
`}
>
- {chartDataResult ? (
- <SuperChart
- disableErrorBoundary
- behaviors={[Behavior.INTERACTIVE_CHART]}
- chartType={formData.viz_type}
- enableNoResults
- formData={formData}
- queriesData={chartDataResult}
- height="100%"
- width="100%"
- />
- ) : (
- <Loading />
- )}
+ <SuperChart
+ disableErrorBoundary
+ behaviors={[Behavior.INTERACTIVE_CHART]}
+ chartType={formData.viz_type}
+ enableNoResults
+ formData={formData}
+ queriesData={result}
+ height="100%"
+ width="100%"
+ />
</div>
);
}
diff --git a/superset-frontend/src/components/Chart/DrillBy/DrillByMenuItems.tsx b/superset-frontend/src/components/Chart/DrillBy/DrillByMenuItems.tsx
index d1a27d5ea9..4064006fac 100644
--- a/superset-frontend/src/components/Chart/DrillBy/DrillByMenuItems.tsx
+++ b/superset-frontend/src/components/Chart/DrillBy/DrillByMenuItems.tsx
@@ -244,15 +244,16 @@ export const DrillByMenuItems = ({
)}
</div>
</Menu.SubMenu>
- <DrillByModal
- column={currentColumn}
- filters={filters}
- formData={formData}
- groupbyFieldName={groupbyFieldName}
- onHideModal={closeModal}
- showModal={showModal}
- dataset={dataset!}
- />
+ {showModal && (
+ <DrillByModal
+ column={currentColumn}
+ filters={filters}
+ formData={formData}
+ groupbyFieldName={groupbyFieldName}
+ onHideModal={closeModal}
+ dataset={dataset!}
+ />
+ )}
</>
);
};
diff --git a/superset-frontend/src/components/Chart/DrillBy/DrillByModal.test.tsx b/superset-frontend/src/components/Chart/DrillBy/DrillByModal.test.tsx
index 54eb653028..f08a9701a8 100644
--- a/superset-frontend/src/components/Chart/DrillBy/DrillByModal.test.tsx
+++ b/superset-frontend/src/components/Chart/DrillBy/DrillByModal.test.tsx
@@ -71,12 +71,13 @@ const renderModal = async () => {
<button type="button" onClick={() => setShowModal(true)}>
Show modal
</button>
- <DrillByModal
- formData={formData}
- showModal={showModal}
- onHideModal={() => setShowModal(false)}
- dataset={dataset}
- />
+ {showModal && (
+ <DrillByModal
+ formData={formData}
+ onHideModal={() => setShowModal(false)}
+ dataset={dataset}
+ />
+ )}
</DashboardPageIdContext.Provider>
);
};
@@ -118,9 +119,16 @@ test('should close the modal', async () => {
expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
});
+test('should render loading indicator', async () => {
+ await renderModal();
+ await waitFor(() =>
+ expect(screen.getByLabelText('Loading')).toBeInTheDocument(),
+ );
+});
+
test('should generate Explore url', async () => {
await renderModal();
- await waitFor(() => fetchMock.called(FORM_DATA_KEY_ENDPOINT));
+ await waitFor(() => fetchMock.called(CHART_DATA_ENDPOINT));
const expectedRequestPayload = {
form_data: {
...omitBy(
@@ -128,6 +136,9 @@ test('should generate Explore url', async () => {
isUndefined,
),
slice_id: 0,
+ result_format: 'json',
+ result_type: 'full',
+ force: false,
},
datasource_id: Number(formData.datasource.split('__')[0]),
datasource_type: formData.datasource.split('__')[1],
@@ -136,11 +147,26 @@ test('should generate Explore url', async () => {
const parsedRequestPayload = JSON.parse(
fetchMock.lastCall()?.[1]?.body as string,
);
- parsedRequestPayload.form_data = JSON.parse(parsedRequestPayload.form_data);
- expect(parsedRequestPayload).toEqual(expectedRequestPayload);
+ expect(parsedRequestPayload.form_data).toEqual(
+ expectedRequestPayload.form_data,
+ );
expect(
await screen.findByRole('link', { name: 'Edit chart' }),
).toHaveAttribute('href', '/explore/?form_data_key=123&dashboard_page_id=1');
});
+
+test('should render radio buttons', async () => {
+ await renderModal();
+ const chartRadio = screen.getByRole('radio', { name: /chart/i });
+ const tableRadio = screen.getByRole('radio', { name: /table/i });
+
+ expect(chartRadio).toBeInTheDocument();
+ expect(tableRadio).toBeInTheDocument();
+ expect(chartRadio).toBeChecked();
+ expect(tableRadio).not.toBeChecked();
+ userEvent.click(tableRadio);
+ expect(chartRadio).not.toBeChecked();
+ expect(tableRadio).toBeChecked();
+});
diff --git a/superset-frontend/src/components/Chart/DrillBy/DrillByModal.tsx b/superset-frontend/src/components/Chart/DrillBy/DrillByModal.tsx
index 2c87ee0cab..776346705c 100644
--- a/superset-frontend/src/components/Chart/DrillBy/DrillByModal.tsx
+++ b/superset-frontend/src/components/Chart/DrillBy/DrillByModal.tsx
@@ -22,6 +22,7 @@ import {
BaseFormData,
BinaryQueryObjectFilterClause,
Column,
+ QueryData,
css,
ensureIsArray,
t,
@@ -30,19 +31,24 @@ import {
import { useSelector } from 'react-redux';
import { Link } from 'react-router-dom';
import Modal from 'src/components/Modal';
+import Loading from 'src/components/Loading';
import Button from 'src/components/Button';
+import { Radio } from 'src/components/Radio';
import { DashboardLayout, RootState } from 'src/dashboard/types';
import { DashboardPageIdContext } from 'src/dashboard/containers/DashboardPage';
import { postFormData } from 'src/explore/exploreUtils/formData';
import { noOp } from 'src/utils/common';
import { simpleFilterToAdhoc } from 'src/utils/simpleFilterToAdhoc';
import { useDatasetMetadataBar } from 'src/features/datasets/metadataBar/useDatasetMetadataBar';
-import { Dataset } from '../types';
+import { SingleQueryResultPane } from 'src/explore/components/DataTablesPane/components/SingleQueryResultPane';
+import { Dataset, DrillByType } from '../types';
import DrillByChart from './DrillByChart';
+import { getChartDataRequest } from '../chartAction';
+const DATA_SIZE = 15;
interface ModalFooterProps {
- formData: BaseFormData;
closeModal?: () => void;
+ formData: BaseFormData;
}
const ModalFooter = ({ formData, closeModal }: ModalFooterProps) => {
@@ -74,6 +80,7 @@ const ModalFooter = ({ formData, closeModal }: ModalFooterProps) => {
{t('Edit chart')}
</Link>
</Button>
+
<Button
buttonStyle="primary"
buttonSize="small"
@@ -88,24 +95,30 @@ const ModalFooter = ({ formData, closeModal }: ModalFooterProps) => {
interface DrillByModalProps {
column?: Column;
+ dataset: Dataset;
filters?: BinaryQueryObjectFilterClause[];
formData: BaseFormData & { [key: string]: any };
groupbyFieldName?: string;
onHideModal: () => void;
- showModal: boolean;
- dataset: Dataset;
}
export default function DrillByModal({
column,
+ dataset,
filters,
formData,
groupbyFieldName = 'groupby',
onHideModal,
- showModal,
- dataset,
}: DrillByModalProps) {
const theme = useTheme();
+ const [chartDataResult, setChartDataResult] = useState<QueryData[]>();
+ const [drillByDisplayMode, setDrillByDisplayMode] = useState<DrillByType>(
+ DrillByType.Chart,
+ );
+ const [datasourceId] = useMemo(
+ () => formData.datasource.split('__'),
+ [formData.datasource],
+ );
const dashboardLayout = useSelector<RootState, DashboardLayout>(
state => state.dashboardLayout.present,
);
@@ -141,7 +154,17 @@ export default function DrillByModal({
return updatedFormData;
}, [column, filters, formData, groupbyFieldName]);
+ useEffect(() => {
+ if (updatedFormData) {
+ getChartDataRequest({
+ formData: updatedFormData,
+ }).then(({ json }) => {
+ setChartDataResult(json.result);
+ });
+ }
+ }, [updatedFormData]);
const { metadataBar } = useDatasetMetadataBar({ dataset });
+
return (
<Modal
css={css`
@@ -149,7 +172,7 @@ export default function DrillByModal({
border-top: none;
}
`}
- show={showModal}
+ show
onHide={onHideModal ?? (() => null)}
title={t('Drill by: %s', chartName)}
footer={<ModalFooter formData={updatedFormData} />}
@@ -160,7 +183,7 @@ export default function DrillByModal({
minWidth: theme.gridUnit * 128,
defaultSize: {
width: 'auto',
- height: '75vh',
+ height: '80vh',
},
}}
draggable
@@ -175,7 +198,56 @@ export default function DrillByModal({
`}
>
{metadataBar}
- <DrillByChart formData={updatedFormData} />
+ <div
+ css={css`
+ margin-bottom: ${theme.gridUnit * 6}px;
+ .ant-radio-button-wrapper-checked:not(.ant-radio-button-wrapper-disabled):focus-within {
+ box-shadow: none;
+ }
+ `}
+ >
+ <Radio.Group
+ onChange={({ target: { value } }) => {
+ setDrillByDisplayMode(value);
+ }}
+ defaultValue={DrillByType.Chart}
+ >
+ <Radio.Button
+ value={DrillByType.Chart}
+ data-test="drill-by-chart-radio"
+ >
+ {t('Chart')}
+ </Radio.Button>
+ <Radio.Button
+ value={DrillByType.Table}
+ data-test="drill-by-table-radio"
+ >
+ {t('Table')}
+ </Radio.Button>
+ </Radio.Group>
+ </div>
+ {!chartDataResult && <Loading />}
+ {drillByDisplayMode === DrillByType.Chart && chartDataResult && (
+ <DrillByChart formData={updatedFormData} result={chartDataResult} />
+ )}
+ {drillByDisplayMode === DrillByType.Table && chartDataResult && (
+ <div
+ css={css`
+ .pagination-container {
+ bottom: ${-theme.gridUnit * 4}px;
+ }
+ `}
+ >
+ <SingleQueryResultPane
+ colnames={chartDataResult[0].colnames}
+ coltypes={chartDataResult[0].coltypes}
+ data={chartDataResult[0].data}
+ dataSize={DATA_SIZE}
+ datasourceId={datasourceId}
+ isVisible
+ />
+ </div>
+ )}
</div>
</Modal>
);
diff --git a/superset-frontend/src/components/Chart/types.ts b/superset-frontend/src/components/Chart/types.ts
index 83983df943..7dee034f34 100644
--- a/superset-frontend/src/components/Chart/types.ts
+++ b/superset-frontend/src/components/Chart/types.ts
@@ -17,6 +17,11 @@
* under the License.
*/
+export enum DrillByType {
+ Chart,
+ Table,
+}
+
export type Dataset = {
changed_by?: {
first_name: string;