Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ import {
ReactNode,
RefObject,
useCallback,
useEffect,
useImperativeHandle,
useMemo,
useState,
Expand All @@ -39,7 +38,6 @@ import {
getChartMetadataRegistry,
getExtensionsRegistry,
isFeatureEnabled,
logging,
QueryFormData,
t,
useTheme,
Expand All @@ -50,12 +48,8 @@ import { usePermissions } from 'src/hooks/usePermissions';
import { Dropdown } from '@superset-ui/core/components';
import { updateDataMask } from 'src/dataMask/actions';
import DrillByModal from 'src/components/Chart/DrillBy/DrillByModal';
import { useVerboseMap } from 'src/hooks/apiResources/datasets';
import { Dataset } from 'src/components/Chart/types';
import {
cachedSupersetGet,
supersetGetCache,
} from 'src/utils/cachedSupersetGet';
import { useDatasetDrillInfo } from 'src/hooks/apiResources/datasets';
import { ResourceStatus } from 'src/hooks/apiResources/apiResources';
import { DrillDetailMenuItems } from '../DrillDetail';
import { getMenuAdjustedY } from '../utils';
import { MenuItemTooltip } from '../DisabledMenuItemTooltip';
Expand Down Expand Up @@ -131,9 +125,6 @@ const ChartContextMenu = (
const [drillModalIsOpen, setDrillModalIsOpen] = useState(false);
const [drillByColumn, setDrillByColumn] = useState<Column>();
const [showDrillByModal, setShowDrillByModal] = useState(false);
const [dataset, setDataset] = useState<Dataset>();
const [isLoadingDataset, setIsLoadingDataset] = useState(false);
const verboseMap = useVerboseMap(dataset);

const closeContextMenu = useCallback(() => {
setVisible(false);
Expand Down Expand Up @@ -166,47 +157,27 @@ const ChartContextMenu = (
canDrillBy &&
isDisplayed(ContextMenuItem.DrillBy);

useEffect(() => {
async function fetchDataset() {
if (!visible || dataset || (!showDrillBy && !showDrillToDetail)) return;

const datasetId = Number(formData.datasource.split('__')[0]);
try {
setIsLoadingDataset(true);
let response;

if (loadDrillByOptionsExtension) {
response = await loadDrillByOptionsExtension(datasetId, formData);
} else {
const endpoint = `/api/v1/dataset/${datasetId}/drill_info/?q=(dashboard_id:${dashboardId})`;
response = await cachedSupersetGet({ endpoint });
}

const { json } = response;
const { result } = json;

setDataset(result);
} catch (error) {
logging.error('Failed to load dataset:', error);
supersetGetCache.delete(`/api/v1/dataset/${datasetId}/drill_info/`);
} finally {
setIsLoadingDataset(false);
}
}

fetchDataset();
}, [
visible,
showDrillBy,
showDrillToDetail,
const datasetResource = useDatasetDrillInfo(
formData.datasource,
loadDrillByOptionsExtension,
dashboardId,
]);
formData,
);

const isLoadingDataset = datasetResource.status === ResourceStatus.Loading;

// Compute filteredDataset with all columns returned + a filtered list of valid drillable options
const filteredDataset = useMemo(() => {
if (!dataset || !showDrillBy) return dataset;
// Short circuit if still loading
if (datasetResource.status !== ResourceStatus.Complete) {
return undefined;
}

// No need to filter the dataset if Drill By is not allowed
if (!showDrillBy) {
return datasetResource.result;
}

const dataset = datasetResource.result;

const filteredColumns = ensureIsArray(dataset.columns).filter(
column =>
Expand All @@ -226,7 +197,8 @@ const ChartContextMenu = (
drillable_columns: filteredColumns,
};
}, [
dataset,
datasetResource.status,
datasetResource.result,
showDrillBy,
filters?.drillBy?.groupbyFieldName,
formData.x_axis,
Expand Down Expand Up @@ -450,16 +422,19 @@ const ChartContextMenu = (
dataset={filteredDataset}
/>
)}
{showDrillByModal && drillByColumn && dataset && filters?.drillBy && (
<DrillByModal
column={drillByColumn}
drillByConfig={filters?.drillBy}
formData={formData}
onHideModal={handleCloseDrillByModal}
dataset={{ ...filteredDataset!, verbose_map: verboseMap }}
canDownload={canDownload}
/>
)}
{showDrillByModal &&
drillByColumn &&
filteredDataset &&
filters?.drillBy && (
<DrillByModal
column={drillByColumn}
drillByConfig={filters?.drillBy}
formData={formData}
onHideModal={handleCloseDrillByModal}
dataset={filteredDataset}
canDownload={canDownload}
/>
)}
</>,
document.body,
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -188,3 +188,49 @@ test('should render the error', async () => {
await waitForRender();
expect(screen.getByText('Error: Something went wrong')).toBeInTheDocument();
});

test('should use verbose_map for column headers when available', async () => {
jest.restoreAllMocks();

const datasetWithVerboseMap = {
...MOCKED_DATASET,
verbose_map: {
year: 'Year of Release',
na_sales: 'North America Sales',
},
};

fetchMock.post(SAMPLES_ENDPOINT, {
result: {
total_count: 1,
data: [
{
year: 1996,
na_sales: 11.27,
eu_sales: 8.89,
},
],
colnames: ['year', 'na_sales', 'eu_sales'],
coltypes: [0, 0, 0],
},
});

await waitForRender({ dataset: datasetWithVerboseMap });

expect(
screen.getByRole('columnheader', { name: 'Year of Release' }),
).toBeInTheDocument();
expect(
screen.getByRole('columnheader', { name: 'North America Sales' }),
).toBeInTheDocument();
expect(
screen.queryByRole('columnheader', { name: 'eu_sales' }),
).toBeInTheDocument();

expect(
screen.queryByRole('columnheader', { name: 'year' }),
).not.toBeInTheDocument();
expect(
screen.queryByRole('columnheader', { name: 'na_sales' }),
).not.toBeInTheDocument();
});
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ export default function DrillDetailPane({
title:
resultsPage?.colTypes[index] === GenericDataType.Temporal ? (
<HeaderWithRadioGroup
headerTitle={column}
headerTitle={dataset?.verbose_map?.[column] || column}
groupTitle={t('Formatting')}
groupOptions={[
{ label: t('Original value'), value: TimeFormatting.Original },
Expand All @@ -159,7 +159,7 @@ export default function DrillDetailPane({
}
/>
) : (
column
dataset?.verbose_map?.[column] || column
),
render: value => {
if (value === true || value === false) {
Expand All @@ -179,7 +179,12 @@ export default function DrillDetailPane({
},
width: 150,
})) || [],
[resultsPage?.colNames, resultsPage?.colTypes, timeFormatting],
[
resultsPage?.colNames,
resultsPage?.colTypes,
timeFormatting,
dataset?.verbose_map,
],
);

const data: DataType[] = useMemo(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ import { LOG_ACTIONS_CHART_DOWNLOAD_AS_IMAGE } from 'src/logger/LogUtils';
import { MenuKeys, RootState } from 'src/dashboard/types';
import DrillDetailModal from 'src/components/Chart/DrillDetail/DrillDetailModal';
import { usePermissions } from 'src/hooks/usePermissions';
import { useDatasetDrillInfo } from 'src/hooks/apiResources/datasets';
import { ResourceStatus } from 'src/hooks/apiResources/apiResources';
import { useCrossFiltersScopingModal } from '../nativeFilters/FilterBar/CrossFilters/ScopingModal/useCrossFiltersScopingModal';
import { ViewResultsModalTrigger } from './ViewResultsModalTrigger';

Expand Down Expand Up @@ -177,6 +179,18 @@ const SliceHeaderControls = (
?.behaviors?.includes(Behavior.InteractiveChart);
const canExplore = props.supersetCanExplore;
const { canDrillToDetail, canViewQuery, canViewTable } = usePermissions();

const datasetResource = useDatasetDrillInfo(
props.slice.datasource,
props.dashboardId,
props.formData,
);

const datasetWithVerboseMap =
datasetResource.status === ResourceStatus.Complete
? datasetResource.result
: undefined;

const refreshChart = () => {
if (props.updatedDttm) {
props.forceRefresh(props.slice.slice_id, props.dashboardId);
Expand Down Expand Up @@ -574,6 +588,7 @@ const SliceHeaderControls = (
}}
chartId={slice.slice_id}
showModal={drillModalIsOpen}
dataset={datasetWithVerboseMap}
/>

{canEditCrossFilters && scopingModal}
Expand Down
93 changes: 91 additions & 2 deletions superset-frontend/src/hooks/apiResources/datasets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,34 @@
* specific language governing permissions and limitations
* under the License.
*/
import { Column, Metric, ensureIsArray } from '@superset-ui/core';
import {
Column,
logging,
Metric,
ensureIsArray,
getExtensionsRegistry,
QueryFormData,
} from '@superset-ui/core';
import { useEffect, useState } from 'react';
import { Dataset } from 'src/components/Chart/types';
import {
cachedSupersetGet,
supersetGetCache,
} from 'src/utils/cachedSupersetGet';
import { Resource, ResourceStatus } from './apiResources';

export const useVerboseMap = (dataset?: Dataset) => {
/**
* Utility function to extract numeric dataset ID from datasource string
*/
export const getDatasetId = (datasetId: string | number): number =>
typeof datasetId === 'string'
? Number(datasetId.split('__')[0])
: Number(datasetId);

/**
* Helper function to create verbose_map from a dataset
*/
export const createVerboseMap = (dataset?: Dataset): Record<string, string> => {
const verbose_map: Record<string, string> = {};
ensureIsArray(dataset?.columns).forEach((column: Column) => {
verbose_map[column.column_name] = column.verbose_name || column.column_name;
Expand All @@ -30,3 +54,68 @@ export const useVerboseMap = (dataset?: Dataset) => {
});
return verbose_map;
};

/**
* Hook to fetch dataset drill info with extension support and verbose_map
* Handles both extension and standard API cases internally
*/
export const useDatasetDrillInfo = (
datasetId: string | number,
dashboardId: number,
formData?: QueryFormData,
): Resource<Dataset> => {
const [resource, setResource] = useState<Resource<Dataset>>({
status: ResourceStatus.Loading,
result: null,
error: null,
});

useEffect(() => {
const fetchDataset = async () => {
try {
const numericDatasetId = getDatasetId(datasetId);
const loadDrillByOptionsExtension = getExtensionsRegistry().get(
'load.drillby.options',
);
let result;

if (loadDrillByOptionsExtension && formData) {
const response = await loadDrillByOptionsExtension(
numericDatasetId,
formData,
);
result = response?.json?.result;
} else {
const endpoint = `/api/v1/dataset/${numericDatasetId}/drill_info/?q=(dashboard_id:${dashboardId})`;
try {
const { json } = await cachedSupersetGet({ endpoint });
const { result: datasetResult } = json;
result = datasetResult;
} catch (error) {
logging.error('Failed to load dataset: ', error);
supersetGetCache.delete(endpoint);
throw error;
}
}

const verbose_map = createVerboseMap(result);

setResource({
status: ResourceStatus.Complete,
result: { ...result, verbose_map },
error: null,
});
} catch (error) {
setResource({
status: ResourceStatus.Error,
result: null,
error: error instanceof Error ? error : new Error(String(error)),
});
}
};

fetchDataset();
}, [datasetId, dashboardId, formData]);

return resource;
};
Loading