feat: drill by modal (#23458)

Co-authored-by: Kamil Gabryjelski <kamil.gabryjelski@gmail.com>
This commit is contained in:
Lily Kuang 2023-03-29 13:13:52 -07:00 committed by GitHub
parent 4220d32f3d
commit 97b5cdd588
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 274 additions and 56 deletions

View File

@ -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}
/>
</>
);
};

View File

@ -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();
});

View File

@ -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>
);
}