mirror of https://github.com/apache/superset.git
feat: drill by modal (#23458)
Co-authored-by: Kamil Gabryjelski <kamil.gabryjelski@gmail.com>
This commit is contained in:
parent
4220d32f3d
commit
97b5cdd588
|
@ -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}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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();
|
||||
});
|
|
@ -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>
|
||||
);
|
||||
}
|
Loading…
Reference in New Issue