mirror of
https://github.com/apache/superset.git
synced 2024-09-12 00:29:39 -04:00
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,
|
supersetGetCache,
|
||||||
} from 'src/utils/cachedSupersetGet';
|
} from 'src/utils/cachedSupersetGet';
|
||||||
import { MenuItemTooltip } from '../DisabledMenuItemTooltip';
|
import { MenuItemTooltip } from '../DisabledMenuItemTooltip';
|
||||||
|
import DrillByModal from './DrillByModal';
|
||||||
import { getSubmenuYOffset } from '../utils';
|
import { getSubmenuYOffset } from '../utils';
|
||||||
import { MenuItemWithTruncation } from '../MenuItemWithTruncation';
|
import { MenuItemWithTruncation } from '../MenuItemWithTruncation';
|
||||||
|
|
||||||
@ -69,6 +70,17 @@ export const DrillByMenuItems = ({
|
|||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const [searchInput, setSearchInput] = useState('');
|
const [searchInput, setSearchInput] = useState('');
|
||||||
const [columns, setColumns] = useState<Column[]>([]);
|
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(() => {
|
useEffect(() => {
|
||||||
// Input is displayed only when columns.length > SHOW_COLUMNS_SEARCH_THRESHOLD
|
// Input is displayed only when columns.length > SHOW_COLUMNS_SEARCH_THRESHOLD
|
||||||
// Reset search input in case Input gets removed
|
// Reset search input in case Input gets removed
|
||||||
@ -161,61 +173,71 @@ export const DrillByMenuItems = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Menu.SubMenu
|
<>
|
||||||
title={t('Drill by')}
|
<Menu.SubMenu
|
||||||
key="drill-by-submenu"
|
title={t('Drill by')}
|
||||||
popupClassName="chart-context-submenu"
|
key="drill-by-submenu"
|
||||||
popupOffset={[0, submenuYOffset]}
|
popupClassName="chart-context-submenu"
|
||||||
{...rest}
|
popupOffset={[0, submenuYOffset]}
|
||||||
>
|
{...rest}
|
||||||
<div data-test="drill-by-submenu">
|
>
|
||||||
{columns.length > SHOW_COLUMNS_SEARCH_THRESHOLD && (
|
<div data-test="drill-by-submenu">
|
||||||
<Input
|
{columns.length > SHOW_COLUMNS_SEARCH_THRESHOLD && (
|
||||||
prefix={
|
<Input
|
||||||
<Icons.Search
|
prefix={
|
||||||
iconSize="l"
|
<Icons.Search
|
||||||
iconColor={theme.colors.grayscale.light1}
|
iconSize="l"
|
||||||
/>
|
iconColor={theme.colors.grayscale.light1}
|
||||||
}
|
/>
|
||||||
onChange={handleInput}
|
}
|
||||||
placeholder={t('Search columns')}
|
onChange={handleInput}
|
||||||
value={searchInput}
|
placeholder={t('Search columns')}
|
||||||
onClick={e => {
|
value={searchInput}
|
||||||
// prevent closing menu when clicking on input
|
onClick={e => {
|
||||||
e.nativeEvent.stopImmediatePropagation();
|
// prevent closing menu when clicking on input
|
||||||
}}
|
e.nativeEvent.stopImmediatePropagation();
|
||||||
allowClear
|
}}
|
||||||
css={css`
|
allowClear
|
||||||
width: auto;
|
css={css`
|
||||||
max-width: 100%;
|
width: auto;
|
||||||
margin: ${theme.gridUnit * 2}px ${theme.gridUnit * 3}px;
|
max-width: 100%;
|
||||||
box-shadow: none;
|
margin: ${theme.gridUnit * 2}px ${theme.gridUnit * 3}px;
|
||||||
`}
|
box-shadow: none;
|
||||||
/>
|
`}
|
||||||
)}
|
/>
|
||||||
{filteredColumns.length ? (
|
)}
|
||||||
<div
|
{filteredColumns.length ? (
|
||||||
css={css`
|
<div
|
||||||
max-height: ${MAX_SUBMENU_HEIGHT}px;
|
css={css`
|
||||||
overflow: auto;
|
max-height: ${MAX_SUBMENU_HEIGHT}px;
|
||||||
`}
|
overflow: auto;
|
||||||
>
|
`}
|
||||||
{filteredColumns.map(column => (
|
>
|
||||||
<MenuItemWithTruncation
|
{filteredColumns.map(column => (
|
||||||
key={`drill-by-item-${column.column_name}`}
|
<MenuItemWithTruncation
|
||||||
tooltipText={column.verbose_name || column.column_name}
|
key={`drill-by-item-${column.column_name}`}
|
||||||
{...rest}
|
tooltipText={column.verbose_name || column.column_name}
|
||||||
>
|
{...rest}
|
||||||
{column.verbose_name || column.column_name}
|
onClick={() => openModal(column)}
|
||||||
</MenuItemWithTruncation>
|
>
|
||||||
))}
|
{column.verbose_name || column.column_name}
|
||||||
</div>
|
</MenuItemWithTruncation>
|
||||||
) : (
|
))}
|
||||||
<Menu.Item disabled key="no-drill-by-columns-found" {...rest}>
|
</div>
|
||||||
{t('No columns found')}
|
) : (
|
||||||
</Menu.Item>
|
<Menu.Item disabled key="no-drill-by-columns-found" {...rest}>
|
||||||
)}
|
{t('No columns found')}
|
||||||
</div>
|
</Menu.Item>
|
||||||
</Menu.SubMenu>
|
)}
|
||||||
|
</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();
|
||||||
|
});
|
108
superset-frontend/src/components/Chart/DrillBy/DrillByModal.tsx
Normal file
108
superset-frontend/src/components/Chart/DrillBy/DrillByModal.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user