mirror of
https://github.com/apache/superset.git
synced 2024-09-12 00:29:39 -04:00
feat: Drill by open in Explore (#23575)
This commit is contained in:
parent
9d2f43d312
commit
117360cd57
@ -27,14 +27,17 @@ import {
|
||||
} from './Operator';
|
||||
import { TimeGranularity } from '../../time-format';
|
||||
|
||||
interface BaseSimpleAdhocFilter {
|
||||
expressionType: 'SIMPLE';
|
||||
interface BaseAdhocFilter {
|
||||
clause: 'WHERE' | 'HAVING';
|
||||
subject: string;
|
||||
timeGrain?: TimeGranularity;
|
||||
isExtra?: boolean;
|
||||
}
|
||||
|
||||
interface BaseSimpleAdhocFilter extends BaseAdhocFilter {
|
||||
expressionType: 'SIMPLE';
|
||||
subject: string;
|
||||
}
|
||||
|
||||
export type UnaryAdhocFilter = BaseSimpleAdhocFilter & {
|
||||
operator: UnaryOperator;
|
||||
};
|
||||
@ -54,9 +57,8 @@ export type SimpleAdhocFilter =
|
||||
| BinaryAdhocFilter
|
||||
| SetAdhocFilter;
|
||||
|
||||
export interface FreeFormAdhocFilter {
|
||||
export interface FreeFormAdhocFilter extends BaseAdhocFilter {
|
||||
expressionType: 'SQL';
|
||||
clause: 'WHERE' | 'HAVING';
|
||||
sqlExpression: string;
|
||||
}
|
||||
|
||||
|
@ -40,30 +40,10 @@ const fetchWithNoData = () => {
|
||||
});
|
||||
};
|
||||
|
||||
const setup = (overrides: Record<string, any> = {}) => {
|
||||
const props = {
|
||||
column: { column_name: 'state' },
|
||||
formData: { ...chart.form_data, viz_type: 'pie' },
|
||||
groupbyFieldName: 'groupby',
|
||||
...overrides,
|
||||
};
|
||||
return render(
|
||||
<DrillByChart
|
||||
filters={[
|
||||
{
|
||||
col: 'gender',
|
||||
op: '==',
|
||||
val: 'boy',
|
||||
formattedVal: 'boy',
|
||||
},
|
||||
]}
|
||||
{...props}
|
||||
/>,
|
||||
{
|
||||
const setup = (overrides: Record<string, any> = {}) =>
|
||||
render(<DrillByChart formData={{ ...chart.form_data, ...overrides }} />, {
|
||||
useRedux: true,
|
||||
},
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
const waitForRender = (overrides: Record<string, any> = {}) =>
|
||||
waitFor(() => setup(overrides));
|
||||
|
@ -17,52 +17,20 @@
|
||||
* under the License.
|
||||
*/
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import {
|
||||
Behavior,
|
||||
BinaryQueryObjectFilterClause,
|
||||
Column,
|
||||
css,
|
||||
SuperChart,
|
||||
} from '@superset-ui/core';
|
||||
import { simpleFilterToAdhoc } from 'src/utils/simpleFilterToAdhoc';
|
||||
import { BaseFormData, Behavior, css, SuperChart } from '@superset-ui/core';
|
||||
import { getChartDataRequest } from 'src/components/Chart/chartAction';
|
||||
import Loading from 'src/components/Loading';
|
||||
|
||||
interface DrillByChartProps {
|
||||
column?: Column;
|
||||
filters?: BinaryQueryObjectFilterClause[];
|
||||
formData: { [key: string]: any; viz_type: string };
|
||||
groupbyFieldName?: string;
|
||||
formData: BaseFormData & { [key: string]: any };
|
||||
}
|
||||
|
||||
export default function DrillByChart({
|
||||
column,
|
||||
filters,
|
||||
formData,
|
||||
groupbyFieldName = 'groupby',
|
||||
}: DrillByChartProps) {
|
||||
let updatedFormData = formData;
|
||||
let groupbyField: any = [];
|
||||
export default function DrillByChart({ formData }: DrillByChartProps) {
|
||||
const [chartDataResult, setChartDataResult] = useState();
|
||||
|
||||
if (column) {
|
||||
groupbyField = Array.isArray(formData[groupbyFieldName])
|
||||
? [column.column_name]
|
||||
: column.column_name;
|
||||
}
|
||||
|
||||
if (filters) {
|
||||
const adhocFilters = filters.map(filter => simpleFilterToAdhoc(filter));
|
||||
updatedFormData = {
|
||||
...formData,
|
||||
adhoc_filters: [...formData.adhoc_filters, ...adhocFilters],
|
||||
[groupbyFieldName]: groupbyField,
|
||||
};
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
getChartDataRequest({
|
||||
formData: updatedFormData,
|
||||
formData,
|
||||
}).then(({ json }) => {
|
||||
setChartDataResult(json.result);
|
||||
});
|
||||
@ -81,7 +49,7 @@ export default function DrillByChart({
|
||||
behaviors={[Behavior.INTERACTIVE_CHART]}
|
||||
chartType={formData.viz_type}
|
||||
enableNoResults
|
||||
formData={updatedFormData}
|
||||
formData={formData}
|
||||
queriesData={chartDataResult}
|
||||
height="100%"
|
||||
width="100%"
|
||||
|
@ -18,23 +18,26 @@
|
||||
*/
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import fetchMock from 'fetch-mock';
|
||||
import { omit, isUndefined, omitBy } from 'lodash';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { waitFor } from '@testing-library/react';
|
||||
import { render, screen } from 'spec/helpers/testing-library';
|
||||
import chartQueries, { sliceId } from 'spec/fixtures/mockChartQueries';
|
||||
import mockState from 'spec/fixtures/mockState';
|
||||
import fetchMock from 'fetch-mock';
|
||||
import { DashboardPageIdContext } from 'src/dashboard/containers/DashboardPage';
|
||||
import DrillByModal from './DrillByModal';
|
||||
|
||||
const CHART_DATA_ENDPOINT =
|
||||
'glob:*api/v1/chart/data?form_data=%7B%22slice_id%22%3A18%7D';
|
||||
|
||||
fetchMock.post(CHART_DATA_ENDPOINT, { body: {} }, {});
|
||||
const CHART_DATA_ENDPOINT = 'glob:*/api/v1/chart/data*';
|
||||
const FORM_DATA_KEY_ENDPOINT = 'glob:*/api/v1/explore/form_data';
|
||||
|
||||
const { form_data: formData } = chartQueries[sliceId];
|
||||
const { slice_name: chartName } = formData;
|
||||
const drillByModalState = {
|
||||
...mockState,
|
||||
dashboardLayout: {
|
||||
past: [],
|
||||
present: {
|
||||
CHART_ID: {
|
||||
id: 'CHART_ID',
|
||||
meta: {
|
||||
@ -43,6 +46,8 @@ const drillByModalState = {
|
||||
},
|
||||
},
|
||||
},
|
||||
future: [],
|
||||
},
|
||||
};
|
||||
const dataset = {
|
||||
changed_on_humanized: '01-01-2001',
|
||||
@ -56,12 +61,13 @@ const dataset = {
|
||||
},
|
||||
],
|
||||
};
|
||||
const renderModal = async (state?: object) => {
|
||||
|
||||
const renderModal = async () => {
|
||||
const DrillByModalWrapper = () => {
|
||||
const [showModal, setShowModal] = useState(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
<DashboardPageIdContext.Provider value="1">
|
||||
<button type="button" onClick={() => setShowModal(true)}>
|
||||
Show modal
|
||||
</button>
|
||||
@ -71,23 +77,29 @@ const renderModal = async (state?: object) => {
|
||||
onHideModal={() => setShowModal(false)}
|
||||
dataset={dataset}
|
||||
/>
|
||||
</>
|
||||
</DashboardPageIdContext.Provider>
|
||||
);
|
||||
};
|
||||
render(<DrillByModalWrapper />, {
|
||||
useDnd: true,
|
||||
useRedux: true,
|
||||
useRouter: true,
|
||||
initialState: state,
|
||||
initialState: drillByModalState,
|
||||
});
|
||||
|
||||
userEvent.click(screen.getByRole('button', { name: 'Show modal' }));
|
||||
await screen.findByRole('dialog', { name: `Drill by: ${chartName}` });
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
fetchMock
|
||||
.post(CHART_DATA_ENDPOINT, { body: {} }, {})
|
||||
.post(FORM_DATA_KEY_ENDPOINT, { key: '123' });
|
||||
});
|
||||
afterEach(fetchMock.restore);
|
||||
|
||||
test('should render the title', async () => {
|
||||
await renderModal(drillByModalState);
|
||||
await renderModal();
|
||||
expect(screen.getByText(`Drill by: ${chartName}`)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
@ -105,3 +117,30 @@ test('should close the modal', async () => {
|
||||
userEvent.click(screen.getAllByRole('button', { name: 'Close' })[1]);
|
||||
expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should generate Explore url', async () => {
|
||||
await renderModal();
|
||||
await waitFor(() => fetchMock.called(FORM_DATA_KEY_ENDPOINT));
|
||||
const expectedRequestPayload = {
|
||||
form_data: {
|
||||
...omitBy(
|
||||
omit(formData, ['slice_id', 'slice_name', 'dashboards']),
|
||||
isUndefined,
|
||||
),
|
||||
slice_id: 0,
|
||||
},
|
||||
datasource_id: Number(formData.datasource.split('__')[0]),
|
||||
datasource_type: formData.datasource.split('__')[1],
|
||||
};
|
||||
|
||||
const parsedRequestPayload = JSON.parse(
|
||||
fetchMock.lastCall()?.[1]?.body as string,
|
||||
);
|
||||
parsedRequestPayload.form_data = JSON.parse(parsedRequestPayload.form_data);
|
||||
|
||||
expect(parsedRequestPayload).toEqual(expectedRequestPayload);
|
||||
|
||||
expect(
|
||||
await screen.findByRole('link', { name: 'Edit chart' }),
|
||||
).toHaveAttribute('href', '/explore/?form_data_key=123&dashboard_page_id=1');
|
||||
});
|
||||
|
@ -17,31 +17,62 @@
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import React, { useContext, useEffect, useMemo, useState } from 'react';
|
||||
import {
|
||||
BaseFormData,
|
||||
BinaryQueryObjectFilterClause,
|
||||
Column,
|
||||
css,
|
||||
ensureIsArray,
|
||||
t,
|
||||
useTheme,
|
||||
} from '@superset-ui/core';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { Link } from 'react-router-dom';
|
||||
import Modal from 'src/components/Modal';
|
||||
import Button from 'src/components/Button';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { DashboardLayout, RootState } from 'src/dashboard/types';
|
||||
import { DashboardPageIdContext } from 'src/dashboard/containers/DashboardPage';
|
||||
import { postFormData } from 'src/explore/exploreUtils/formData';
|
||||
import { noOp } from 'src/utils/common';
|
||||
import { simpleFilterToAdhoc } from 'src/utils/simpleFilterToAdhoc';
|
||||
import { useDatasetMetadataBar } from 'src/features/datasets/metadataBar/useDatasetMetadataBar';
|
||||
import { Dataset } from '../types';
|
||||
import DrillByChart from './DrillByChart';
|
||||
|
||||
interface ModalFooterProps {
|
||||
exploreChart: () => void;
|
||||
formData: BaseFormData;
|
||||
closeModal?: () => void;
|
||||
}
|
||||
|
||||
const ModalFooter = ({ exploreChart, closeModal }: ModalFooterProps) => (
|
||||
const ModalFooter = ({ formData, closeModal }: ModalFooterProps) => {
|
||||
const [url, setUrl] = useState('');
|
||||
const dashboardPageId = useContext(DashboardPageIdContext);
|
||||
const [datasource_id, datasource_type] = formData.datasource.split('__');
|
||||
useEffect(() => {
|
||||
postFormData(Number(datasource_id), datasource_type, formData, 0)
|
||||
.then(key => {
|
||||
setUrl(
|
||||
`/explore/?form_data_key=${key}&dashboard_page_id=${dashboardPageId}`,
|
||||
);
|
||||
})
|
||||
.catch(e => {
|
||||
console.log(e);
|
||||
});
|
||||
}, [dashboardPageId, datasource_id, datasource_type, formData]);
|
||||
return (
|
||||
<>
|
||||
<Button buttonStyle="secondary" buttonSize="small" onClick={exploreChart}>
|
||||
<Button buttonStyle="secondary" buttonSize="small" onClick={noOp}>
|
||||
<Link
|
||||
css={css`
|
||||
&:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
`}
|
||||
to={url}
|
||||
>
|
||||
{t('Edit chart')}
|
||||
</Link>
|
||||
</Button>
|
||||
<Button
|
||||
buttonStyle="primary"
|
||||
@ -52,12 +83,13 @@ const ModalFooter = ({ exploreChart, closeModal }: ModalFooterProps) => (
|
||||
{t('Close')}
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
);
|
||||
};
|
||||
|
||||
interface DrillByModalProps {
|
||||
column?: Column;
|
||||
filters?: BinaryQueryObjectFilterClause[];
|
||||
formData: { [key: string]: any; viz_type: string };
|
||||
formData: BaseFormData & { [key: string]: any };
|
||||
groupbyFieldName?: string;
|
||||
onHideModal: () => void;
|
||||
showModal: boolean;
|
||||
@ -68,7 +100,7 @@ export default function DrillByModal({
|
||||
column,
|
||||
filters,
|
||||
formData,
|
||||
groupbyFieldName,
|
||||
groupbyFieldName = 'groupby',
|
||||
onHideModal,
|
||||
showModal,
|
||||
dataset,
|
||||
@ -82,7 +114,32 @@ export default function DrillByModal({
|
||||
);
|
||||
const chartName =
|
||||
chartLayoutItem?.meta.sliceNameOverride || chartLayoutItem?.meta.sliceName;
|
||||
const exploreChart = () => {};
|
||||
|
||||
const updatedFormData = useMemo(() => {
|
||||
let updatedFormData = { ...formData };
|
||||
if (column) {
|
||||
updatedFormData[groupbyFieldName] = Array.isArray(
|
||||
formData[groupbyFieldName],
|
||||
)
|
||||
? [column.column_name]
|
||||
: column.column_name;
|
||||
}
|
||||
|
||||
if (filters) {
|
||||
const adhocFilters = filters.map(filter => simpleFilterToAdhoc(filter));
|
||||
updatedFormData = {
|
||||
...updatedFormData,
|
||||
adhoc_filters: [
|
||||
...ensureIsArray(formData.adhoc_filters),
|
||||
...adhocFilters,
|
||||
],
|
||||
};
|
||||
}
|
||||
updatedFormData.slice_id = 0;
|
||||
delete updatedFormData.slice_name;
|
||||
delete updatedFormData.dashboards;
|
||||
return updatedFormData;
|
||||
}, [column, filters, formData, groupbyFieldName]);
|
||||
|
||||
const { metadataBar } = useDatasetMetadataBar({ dataset });
|
||||
return (
|
||||
@ -95,7 +152,7 @@ export default function DrillByModal({
|
||||
show={showModal}
|
||||
onHide={onHideModal ?? (() => null)}
|
||||
title={t('Drill by: %s', chartName)}
|
||||
footer={<ModalFooter exploreChart={exploreChart} />}
|
||||
footer={<ModalFooter formData={updatedFormData} />}
|
||||
responsive
|
||||
resizable
|
||||
resizableConfig={{
|
||||
@ -118,12 +175,7 @@ export default function DrillByModal({
|
||||
`}
|
||||
>
|
||||
{metadataBar}
|
||||
<DrillByChart
|
||||
column={column}
|
||||
filters={filters}
|
||||
formData={formData}
|
||||
groupbyFieldName={groupbyFieldName}
|
||||
/>
|
||||
<DrillByChart formData={updatedFormData} />
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
|
@ -33,6 +33,17 @@ import {
|
||||
} from '@superset-ui/core';
|
||||
import { simpleFilterToAdhoc } from 'src/utils/simpleFilterToAdhoc';
|
||||
|
||||
const removeExtraFieldForNewCharts = (
|
||||
filters: AdhocFilter[],
|
||||
isNewChart: boolean,
|
||||
) =>
|
||||
filters.map(filter => {
|
||||
if (filter.isExtra) {
|
||||
return { ...filter, isExtra: !isNewChart };
|
||||
}
|
||||
return filter;
|
||||
});
|
||||
|
||||
const removeAdhocFilterDuplicates = (filters: AdhocFilter[]) => {
|
||||
const isDuplicate = (
|
||||
adhocFilter: AdhocFilter,
|
||||
@ -103,7 +114,6 @@ const mergeNativeFiltersToFormData = (
|
||||
) => {
|
||||
const nativeFiltersData: JsonObject = {};
|
||||
const extraFormData = dashboardFormData.extra_form_data || {};
|
||||
|
||||
Object.entries(EXTRA_FORM_DATA_OVERRIDE_REGULAR_MAPPINGS).forEach(
|
||||
([srcKey, targetKey]) => {
|
||||
const val = extraFormData[srcKey];
|
||||
@ -193,7 +203,8 @@ export const getFormDataWithDashboardContext = (
|
||||
.reduce(
|
||||
(acc, key) => ({
|
||||
...acc,
|
||||
[key]: applyTimeRangeFilters(
|
||||
[key]: removeExtraFieldForNewCharts(
|
||||
applyTimeRangeFilters(
|
||||
dashboardContextFormData,
|
||||
removeAdhocFilterDuplicates([
|
||||
...ensureIsArray(exploreFormData[key]),
|
||||
@ -201,6 +212,8 @@ export const getFormDataWithDashboardContext = (
|
||||
...ensureIsArray(nativeFiltersData[key]),
|
||||
]),
|
||||
),
|
||||
exploreFormData.slice_id === 0,
|
||||
),
|
||||
}),
|
||||
{},
|
||||
);
|
||||
|
Loading…
Reference in New Issue
Block a user