in superset-frontend/src/components/Chart/DrillBy/DrillByModal.tsx [160:487]
export default function DrillByModal({
column,
dataset,
drillByConfig,
formData,
onHideModal,
canDownload,
}: DrillByModalProps) {
const dispatch = useDispatch();
const theme = useTheme();
const { addDangerToast } = useToasts();
const [isChartDataLoading, setIsChartDataLoading] = useState(true);
const [drillByConfigs, setDrillByConfigs] = useState<DrillByConfigs>([
{ ...drillByConfig, column },
]);
useEffect(() => {
dispatch(
logEvent(LOG_ACTIONS_DRILL_BY_MODAL_OPENED, {
slice_id: formData.slice_id,
}),
);
}, [dispatch, formData.slice_id]);
const {
column: currentColumn,
groupbyFieldName = drillByConfig.groupbyFieldName,
} = drillByConfigs[drillByConfigs.length - 1] || {};
const initialGroupbyColumns = useMemo(
() =>
ensureIsArray(formData[groupbyFieldName])
.map(colName =>
dataset.columns?.find(col => col.column_name === colName),
)
.filter(isDefined),
[dataset.columns, formData, groupbyFieldName],
);
const { displayModeToggle, drillByDisplayMode } = useDisplayModeToggle();
const [chartDataResult, setChartDataResult] = useState<QueryData[]>();
const resultsTable = useResultsTableView(
chartDataResult,
formData.datasource,
canDownload,
);
const [currentFormData, setCurrentFormData] = useState(formData);
const [usedGroupbyColumns, setUsedGroupbyColumns] = useState<Column[]>(
[...initialGroupbyColumns, column].filter(isDefined),
);
const [breadcrumbsData, setBreadcrumbsData] = useState<DrillByBreadcrumb[]>([
{ groupby: initialGroupbyColumns, filters: drillByConfig.filters },
{ groupby: column || [] },
]);
const getNewGroupby = useCallback(
(groupbyCol: Column, fieldName = groupbyFieldName) =>
Array.isArray(formData[fieldName])
? [groupbyCol.column_name]
: groupbyCol.column_name,
[formData, groupbyFieldName],
);
const getFormDataChangesFromConfigs = useCallback(
(configs: DrillByConfigs) =>
configs.reduce<Record<string, any>>(
(acc, config) => {
if (config?.groupbyFieldName && config.column) {
acc.formData[config.groupbyFieldName] = getNewGroupby(
config.column,
config.groupbyFieldName,
);
acc.overriddenGroupbyFields.add(config.groupbyFieldName);
}
const adhocFilterFieldName =
config?.adhocFilterFieldName || DEFAULT_ADHOC_FILTER_FIELD_NAME;
acc.formData[adhocFilterFieldName] = [
...ensureIsArray(acc[adhocFilterFieldName]),
...ensureIsArray(config.filters).map(filter =>
simpleFilterToAdhoc(filter),
),
];
acc.overriddenAdhocFilterFields.add(adhocFilterFieldName);
return acc;
},
{
formData: {} as Record<string, string | string[] | Set<string>>,
overriddenGroupbyFields: new Set<string>(),
overriddenAdhocFilterFields: new Set<string>(),
},
),
[getNewGroupby],
);
const getFiltersFromConfigsByFieldName = useCallback(
() =>
drillByConfigs.reduce<Record<string, AdhocFilter[]>>((acc, config) => {
const adhocFilterFieldName =
config.adhocFilterFieldName || DEFAULT_ADHOC_FILTER_FIELD_NAME;
acc[adhocFilterFieldName] = [
...(acc[adhocFilterFieldName] || []),
...config.filters.map(filter => simpleFilterToAdhoc(filter)),
];
return acc;
}, {}),
[drillByConfigs],
);
const onBreadcrumbClick = useCallback(
(breadcrumb: DrillByBreadcrumb, index: number) => {
dispatch(
logEvent(LOG_ACTIONS_DRILL_BY_BREADCRUMB_CLICKED, {
slice_id: formData.slice_id,
}),
);
setDrillByConfigs(prevConfigs => prevConfigs.slice(0, index));
setBreadcrumbsData(prevBreadcrumbs => {
const newBreadcrumbs = prevBreadcrumbs.slice(0, index + 1);
delete newBreadcrumbs[newBreadcrumbs.length - 1].filters;
return newBreadcrumbs;
});
setUsedGroupbyColumns(prevUsedGroupbyColumns =>
prevUsedGroupbyColumns.slice(0, index),
);
setCurrentFormData(() => {
if (index === 0) {
return formData;
}
const { formData: overrideFormData, overriddenAdhocFilterFields } =
getFormDataChangesFromConfigs(drillByConfigs.slice(0, index));
const newFormData = {
...formData,
...overrideFormData,
};
overriddenAdhocFilterFields.forEach((adhocFilterField: string) => ({
...newFormData,
[adhocFilterField]: [
...formData[adhocFilterField],
...overrideFormData[adhocFilterField],
],
}));
return newFormData;
});
},
[dispatch, drillByConfigs, formData, getFormDataChangesFromConfigs],
);
const breadcrumbs = useDrillByBreadcrumbs(breadcrumbsData, onBreadcrumbClick);
const drilledFormData = useMemo(() => {
let updatedFormData = { ...currentFormData };
if (currentColumn && groupbyFieldName) {
updatedFormData[groupbyFieldName] = getNewGroupby(currentColumn);
}
const adhocFilters = getFiltersFromConfigsByFieldName();
Object.keys(adhocFilters).forEach(adhocFilterFieldName => {
updatedFormData = {
...updatedFormData,
[adhocFilterFieldName]: [
...ensureIsArray(formData[adhocFilterFieldName]),
...adhocFilters[adhocFilterFieldName],
],
};
});
updatedFormData.slice_id = 0;
delete updatedFormData.slice_name;
delete updatedFormData.dashboards;
return updatedFormData;
}, [
currentFormData,
currentColumn,
groupbyFieldName,
getFiltersFromConfigsByFieldName,
getNewGroupby,
formData,
]);
useEffect(() => {
setUsedGroupbyColumns(usedCols =>
!currentColumn ||
usedCols.some(
usedCol => usedCol.column_name === currentColumn.column_name,
)
? usedCols
: [...usedCols, currentColumn],
);
}, [currentColumn]);
const onSelection = useCallback(
(
newColumn: Column,
drillByConfig: Required<ContextMenuFilters>['drillBy'],
) => {
dispatch(
logEvent(LOG_ACTIONS_FURTHER_DRILL_BY, {
drill_depth: drillByConfigs.length + 1,
slice_id: formData.slice_id,
}),
);
setCurrentFormData(drilledFormData);
setDrillByConfigs(prevConfigs => [
...prevConfigs,
{ ...drillByConfig, column: newColumn },
]);
setBreadcrumbsData(prevBreadcrumbs => {
const newBreadcrumbs = [...prevBreadcrumbs, { groupby: newColumn }];
newBreadcrumbs[newBreadcrumbs.length - 2].filters =
drillByConfig.filters;
return newBreadcrumbs;
});
},
[dispatch, drillByConfigs.length, drilledFormData, formData.slice_id],
);
const additionalConfig = useMemo(
() => ({
drillBy: { excludedColumns: usedGroupbyColumns, openNewModal: false },
}),
[usedGroupbyColumns],
);
const { contextMenu, inContextMenu, onContextMenu } = useContextMenu(
0,
currentFormData,
onSelection,
ContextMenuItem.DrillBy,
additionalConfig,
);
const chartName = useSelector<RootState, string | undefined>(state => {
const chartLayoutItem = Object.values(state.dashboardLayout.present).find(
layoutItem => layoutItem.meta?.chartId === formData.slice_id,
);
return (
chartLayoutItem?.meta.sliceNameOverride || chartLayoutItem?.meta.sliceName
);
});
useEffect(() => {
if (drilledFormData) {
const [useLegacyApi] = getQuerySettings(drilledFormData);
setIsChartDataLoading(true);
setChartDataResult(undefined);
getChartDataRequest({
formData: drilledFormData,
})
.then(({ response, json }) =>
handleChartDataResponse(response, json, useLegacyApi),
)
.then(queriesResponse => {
setChartDataResult(queriesResponse);
})
.catch(() => {
addDangerToast(t('Failed to load chart data.'));
})
.finally(() => {
setIsChartDataLoading(false);
});
}
}, [addDangerToast, drilledFormData]);
const { metadataBar } = useDatasetMetadataBar({ dataset });
return (
<Modal
css={css`
.antd5-modal-footer {
border-top: none;
}
`}
show
onHide={onHideModal ?? (() => null)}
title={t('Drill by: %s', chartName)}
footer={<ModalFooter formData={drilledFormData} />}
responsive
resizable
resizableConfig={{
minHeight: theme.gridUnit * 128,
minWidth: theme.gridUnit * 128,
defaultSize: {
width: 'auto',
height: '80vh',
},
}}
draggable
destroyOnClose
maskClosable={false}
>
<div
css={css`
display: flex;
flex-direction: column;
height: 100%;
`}
>
{metadataBar}
{breadcrumbs}
{displayModeToggle}
{isChartDataLoading && <Loading />}
{!isChartDataLoading && !chartDataResult && (
<Alert
type="error"
message={t('There was an error loading the chart data')}
/>
)}
{drillByDisplayMode === DrillByType.Chart && chartDataResult && (
<DrillByChart
dataset={dataset}
formData={drilledFormData}
result={chartDataResult}
onContextMenu={onContextMenu}
inContextMenu={inContextMenu}
/>
)}
{drillByDisplayMode === DrillByType.Table &&
chartDataResult &&
resultsTable}
{contextMenu}
</div>
</Modal>
);
}