From 97b5cdd588ceb2702098ca0f569750f7f16f2bbb Mon Sep 17 00:00:00 2001 From: Lily Kuang Date: Wed, 29 Mar 2023 13:13:52 -0700 Subject: [PATCH] feat: drill by modal (#23458) Co-authored-by: Kamil Gabryjelski --- .../Chart/DrillBy/DrillByMenuItems.tsx | 134 ++++++++++-------- .../Chart/DrillBy/DrillByModal.test.tsx | 88 ++++++++++++ .../components/Chart/DrillBy/DrillByModal.tsx | 108 ++++++++++++++ 3 files changed, 274 insertions(+), 56 deletions(-) create mode 100644 superset-frontend/src/components/Chart/DrillBy/DrillByModal.test.tsx create mode 100644 superset-frontend/src/components/Chart/DrillBy/DrillByModal.tsx 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([]); + 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 ( - -
- {columns.length > SHOW_COLUMNS_SEARCH_THRESHOLD && ( - - } - 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 ? ( -
- {filteredColumns.map(column => ( - - {column.verbose_name || column.column_name} - - ))} -
- ) : ( - - {t('No columns found')} - - )} -
-
+ <> + +
+ {columns.length > SHOW_COLUMNS_SEARCH_THRESHOLD && ( + + } + 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 ? ( +
+ {filteredColumns.map(column => ( + openModal(column)} + > + {column.verbose_name || column.column_name} + + ))} +
+ ) : ( + + {t('No columns found')} + + )} +
+
+ + ); }; 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 ( + <> + + setShowModal(false)} + /> + + ); + }; + + render(, { + 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) => ( + <> + + + +); + +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( + 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 ( + null)} + title={t('Drill by: %s', chartName)} + footer={} + responsive + resizable + resizableConfig={{ + minHeight: theme.gridUnit * 128, + minWidth: theme.gridUnit * 128, + defaultSize: { + width: 'auto', + height: '75vh', + }, + }} + draggable + destroyOnClose + maskClosable={false} + > + {} + + ); +}