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;