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/03/29 20:14:03 UTC

[superset] branch master updated: feat: drill by modal (#23458)

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 97b5cdd588 feat: drill by modal (#23458)
97b5cdd588 is described below

commit 97b5cdd588ceb2702098ca0f569750f7f16f2bbb
Author: Lily Kuang <li...@preset.io>
AuthorDate: Wed Mar 29 13:13:52 2023 -0700

    feat: drill by modal (#23458)
    
    Co-authored-by: Kamil Gabryjelski <ka...@gmail.com>
---
 .../components/Chart/DrillBy/DrillByMenuItems.tsx  | 134 ++++++++++++---------
 .../components/Chart/DrillBy/DrillByModal.test.tsx |  88 ++++++++++++++
 .../src/components/Chart/DrillBy/DrillByModal.tsx  | 108 +++++++++++++++++
 3 files changed, 274 insertions(+), 56 deletions(-)

diff --git a/superset-frontend/src/components/Chart/DrillBy/DrillByMenuItems.tsx b/superset-frontend/src/components/Chart/DrillBy/DrillByMenuItems.tsx
index 1da50a412f..07b00c4be3 100644
--- a/superset-frontend/src/components/Chart/DrillBy/DrillByMenuItems.tsx
+++ b/superset-frontend/src/components/Chart/DrillBy/DrillByMenuItems.tsx
@@ -44,6 +44,7 @@ import {
   supersetGetCache,
 } from 'src/utils/cachedSupersetGet';
 import { MenuItemTooltip } from '../DisabledMenuItemTooltip';
+import DrillByModal from './DrillByModal';
 import { getSubmenuYOffset } from '../utils';
 import { MenuItemWithTruncation } from '../MenuItemWithTruncation';
 
@@ -69,6 +70,17 @@ export const DrillByMenuItems = ({
   const theme = useTheme();
   const [searchInput, setSearchInput] = useState('');
   const [columns, setColumns] = useState<Column[]>([]);
+  const [showModal, setShowModal] = useState(false);
+  const [currentColumn, setCurrentColumn] = useState();
+
+  const openModal = useCallback(column => {
+    setCurrentColumn(column);
+    setShowModal(true);
+  }, []);
+  const closeModal = useCallback(() => {
+    setShowModal(false);
+  }, []);
+
   useEffect(() => {
     // Input is displayed only when columns.length > SHOW_COLUMNS_SEARCH_THRESHOLD
     // Reset search input in case Input gets removed
@@ -161,61 +173,71 @@ export const DrillByMenuItems = ({
   }
 
   return (
-    <Menu.SubMenu
-      title={t('Drill by')}
-      key="drill-by-submenu"
-      popupClassName="chart-context-submenu"
-      popupOffset={[0, submenuYOffset]}
-      {...rest}
-    >
-      <div data-test="drill-by-submenu">
-        {columns.length > SHOW_COLUMNS_SEARCH_THRESHOLD && (
-          <Input
-            prefix={
-              <Icons.Search
-                iconSize="l"
-                iconColor={theme.colors.grayscale.light1}
-              />
-            }
-            onChange={handleInput}
-            placeholder={t('Search columns')}
-            value={searchInput}
-            onClick={e => {
-              // prevent closing menu when clicking on input
-              e.nativeEvent.stopImmediatePropagation();
-            }}
-            allowClear
-            css={css`
-              width: auto;
-              max-width: 100%;
-              margin: ${theme.gridUnit * 2}px ${theme.gridUnit * 3}px;
-              box-shadow: none;
-            `}
-          />
-        )}
-        {filteredColumns.length ? (
-          <div
-            css={css`
-              max-height: ${MAX_SUBMENU_HEIGHT}px;
-              overflow: auto;
-            `}
-          >
-            {filteredColumns.map(column => (
-              <MenuItemWithTruncation
-                key={`drill-by-item-${column.column_name}`}
-                tooltipText={column.verbose_name || column.column_name}
-                {...rest}
-              >
-                {column.verbose_name || column.column_name}
-              </MenuItemWithTruncation>
-            ))}
-          </div>
-        ) : (
-          <Menu.Item disabled key="no-drill-by-columns-found" {...rest}>
-            {t('No columns found')}
-          </Menu.Item>
-        )}
-      </div>
-    </Menu.SubMenu>
+    <>
+      <Menu.SubMenu
+        title={t('Drill by')}
+        key="drill-by-submenu"
+        popupClassName="chart-context-submenu"
+        popupOffset={[0, submenuYOffset]}
+        {...rest}
+      >
+        <div data-test="drill-by-submenu">
+          {columns.length > SHOW_COLUMNS_SEARCH_THRESHOLD && (
+            <Input
+              prefix={
+                <Icons.Search
+                  iconSize="l"
+                  iconColor={theme.colors.grayscale.light1}
+                />
+              }
+              onChange={handleInput}
+              placeholder={t('Search columns')}
+              value={searchInput}
+              onClick={e => {
+                // prevent closing menu when clicking on input
+                e.nativeEvent.stopImmediatePropagation();
+              }}
+              allowClear
+              css={css`
+                width: auto;
+                max-width: 100%;
+                margin: ${theme.gridUnit * 2}px ${theme.gridUnit * 3}px;
+                box-shadow: none;
+              `}
+            />
+          )}
+          {filteredColumns.length ? (
+            <div
+              css={css`
+                max-height: ${MAX_SUBMENU_HEIGHT}px;
+                overflow: auto;
+              `}
+            >
+              {filteredColumns.map(column => (
+                <MenuItemWithTruncation
+                  key={`drill-by-item-${column.column_name}`}
+                  tooltipText={column.verbose_name || column.column_name}
+                  {...rest}
+                  onClick={() => openModal(column)}
+                >
+                  {column.verbose_name || column.column_name}
+                </MenuItemWithTruncation>
+              ))}
+            </div>
+          ) : (
+            <Menu.Item disabled key="no-drill-by-columns-found" {...rest}>
+              {t('No columns found')}
+            </Menu.Item>
+          )}
+        </div>
+      </Menu.SubMenu>
+      <DrillByModal
+        column={currentColumn}
+        filters={filters}
+        formData={formData}
+        onHideModal={closeModal}
+        showModal={showModal}
+      />
+    </>
   );
 };
diff --git a/superset-frontend/src/components/Chart/DrillBy/DrillByModal.test.tsx b/superset-frontend/src/components/Chart/DrillBy/DrillByModal.test.tsx
new file mode 100644
index 0000000000..10d9e1af83
--- /dev/null
+++ b/superset-frontend/src/components/Chart/DrillBy/DrillByModal.test.tsx
@@ -0,0 +1,88 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import React, { useState } from 'react';
+import userEvent from '@testing-library/user-event';
+import { render, screen } from 'spec/helpers/testing-library';
+import chartQueries, { sliceId } from 'spec/fixtures/mockChartQueries';
+import mockState from 'spec/fixtures/mockState';
+import DrillByModal from './DrillByModal';
+
+const { form_data: formData } = chartQueries[sliceId];
+const { slice_name: chartName } = formData;
+const drillByModalState = {
+  ...mockState,
+  dashboardLayout: {
+    CHART_ID: {
+      id: 'CHART_ID',
+      meta: {
+        chartId: formData.slice_id,
+        sliceName: chartName,
+      },
+    },
+  },
+};
+const renderModal = async (state?: object) => {
+  const DrillByModalWrapper = () => {
+    const [showModal, setShowModal] = useState(false);
+    return (
+      <>
+        <button type="button" onClick={() => setShowModal(true)}>
+          Show modal
+        </button>
+        <DrillByModal
+          formData={formData}
+          filters={[]}
+          showModal={showModal}
+          onHideModal={() => setShowModal(false)}
+        />
+      </>
+    );
+  };
+
+  render(<DrillByModalWrapper />, {
+    useDnd: true,
+    useRedux: true,
+    useRouter: true,
+    initialState: state,
+  });
+
+  userEvent.click(screen.getByRole('button', { name: 'Show modal' }));
+  await screen.findByRole('dialog', { name: `Drill by: ${chartName}` });
+};
+
+test('should render the title', async () => {
+  await renderModal(drillByModalState);
+  expect(screen.getByText(`Drill by: ${chartName}`)).toBeInTheDocument();
+});
+
+test('should render the button', async () => {
+  await renderModal();
+  expect(
+    screen.getByRole('button', { name: 'Edit chart' }),
+  ).toBeInTheDocument();
+  expect(screen.getAllByRole('button', { name: 'Close' })).toHaveLength(2);
+});
+
+test('should close the modal', async () => {
+  await renderModal();
+  expect(screen.getByRole('dialog')).toBeInTheDocument();
+  userEvent.click(screen.getAllByRole('button', { name: 'Close' })[1]);
+  expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
+});
diff --git a/superset-frontend/src/components/Chart/DrillBy/DrillByModal.tsx b/superset-frontend/src/components/Chart/DrillBy/DrillByModal.tsx
new file mode 100644
index 0000000000..527284ebe5
--- /dev/null
+++ b/superset-frontend/src/components/Chart/DrillBy/DrillByModal.tsx
@@ -0,0 +1,108 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import React from 'react';
+import {
+  BinaryQueryObjectFilterClause,
+  Column,
+  css,
+  t,
+  useTheme,
+} from '@superset-ui/core';
+import Modal from 'src/components/Modal';
+import Button from 'src/components/Button';
+import { useSelector } from 'react-redux';
+import { DashboardLayout, RootState } from 'src/dashboard/types';
+
+interface ModalFooterProps {
+  exploreChart: () => void;
+  closeModal?: () => void;
+}
+
+const ModalFooter = ({ exploreChart, closeModal }: ModalFooterProps) => (
+  <>
+    <Button buttonStyle="secondary" buttonSize="small" onClick={exploreChart}>
+      {t('Edit chart')}
+    </Button>
+    <Button
+      buttonStyle="primary"
+      buttonSize="small"
+      onClick={closeModal}
+      data-test="close-drillby-modal"
+    >
+      {t('Close')}
+    </Button>
+  </>
+);
+
+interface DrillByModalProps {
+  column?: Column;
+  filters?: BinaryQueryObjectFilterClause[];
+  formData: { [key: string]: any; viz_type: string };
+  onHideModal: () => void;
+  showModal: boolean;
+}
+
+export default function DrillByModal({
+  column,
+  formData,
+  filters,
+  onHideModal,
+  showModal,
+}: DrillByModalProps) {
+  const theme = useTheme();
+  const dashboardLayout = useSelector<RootState, DashboardLayout>(
+    state => state.dashboardLayout.present,
+  );
+  const chartLayoutItem = Object.values(dashboardLayout).find(
+    layoutItem => layoutItem.meta?.chartId === formData.slice_id,
+  );
+  const chartName =
+    chartLayoutItem?.meta.sliceNameOverride || chartLayoutItem?.meta.sliceName;
+  const exploreChart = () => {};
+
+  return (
+    <Modal
+      css={css`
+        .ant-modal-footer {
+          border-top: none;
+        }
+      `}
+      show={showModal}
+      onHide={onHideModal ?? (() => null)}
+      title={t('Drill by: %s', chartName)}
+      footer={<ModalFooter exploreChart={exploreChart} />}
+      responsive
+      resizable
+      resizableConfig={{
+        minHeight: theme.gridUnit * 128,
+        minWidth: theme.gridUnit * 128,
+        defaultSize: {
+          width: 'auto',
+          height: '75vh',
+        },
+      }}
+      draggable
+      destroyOnClose
+      maskClosable={false}
+    >
+      {}
+    </Modal>
+  );
+}