feat: Implement context menu for drill by (#23454)

This commit is contained in:
Kamil Gabryjelski 2023-03-29 15:01:51 +02:00 committed by GitHub
parent 542bf25729
commit 9fbfd1c1d8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
43 changed files with 860 additions and 129 deletions

View File

@ -31,6 +31,7 @@ export enum Behavior {
* when dimensions are right-clicked on.
*/
DRILL_TO_DETAIL = 'DRILL_TO_DETAIL',
DRILL_BY = 'DRILL_BY',
}
export interface ContextMenuFilters {
@ -39,6 +40,11 @@ export interface ContextMenuFilters {
isCurrentValueSelected?: boolean;
};
drillToDetail?: BinaryQueryObjectFilterClause[];
drillBy?: {
filters: BinaryQueryObjectFilterClause[];
groupbyFieldName: string;
adhocFilterFieldName?: string;
};
}
export enum AppSection {

View File

@ -172,6 +172,7 @@ function WorldMap(element, props) {
const val =
countryFieldtype === 'name' ? mapData[key]?.name : mapData[key]?.country;
let drillToDetailFilters;
let drillByFilters;
if (val) {
drillToDetailFilters = [
{
@ -181,10 +182,18 @@ function WorldMap(element, props) {
formattedVal: val,
},
];
drillByFilters = [
{
col: entity,
op: '==',
val,
},
];
}
onContextMenu(pointerEvent.clientX, pointerEvent.clientY, {
drillToDetail: drillToDetailFilters,
crossFilter: getCrossFilterDataMask(source),
drillBy: { filters: drillByFilters, groupbyFieldName: 'entity' },
});
};

View File

@ -45,7 +45,11 @@ const metadata = new ChartMetadata({
],
thumbnail,
useLegacyApi: true,
behaviors: [Behavior.INTERACTIVE_CHART, Behavior.DRILL_TO_DETAIL],
behaviors: [
Behavior.INTERACTIVE_CHART,
Behavior.DRILL_TO_DETAIL,
Behavior.DRILL_BY,
],
});
export default class WorldMapChartPlugin extends ChartPlugin {

View File

@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
import { t, ChartMetadata, ChartPlugin, Behavior } from '@superset-ui/core';
import { Behavior, ChartMetadata, ChartPlugin, t } from '@superset-ui/core';
import buildQuery from './buildQuery';
import controlPanel from './controlPanel';
import transformProps from './transformProps';
@ -44,7 +44,11 @@ export default class EchartsBoxPlotChartPlugin extends ChartPlugin<
controlPanel,
loadChart: () => import('./EchartsBoxPlot'),
metadata: new ChartMetadata({
behaviors: [Behavior.INTERACTIVE_CHART, Behavior.DRILL_TO_DETAIL],
behaviors: [
Behavior.INTERACTIVE_CHART,
Behavior.DRILL_TO_DETAIL,
Behavior.DRILL_BY,
],
category: t('Distribution'),
credits: ['https://echarts.apache.org'],
description: t(

View File

@ -44,7 +44,11 @@ export default class EchartsFunnelChartPlugin extends ChartPlugin<
controlPanel,
loadChart: () => import('./EchartsFunnel'),
metadata: new ChartMetadata({
behaviors: [Behavior.INTERACTIVE_CHART, Behavior.DRILL_TO_DETAIL],
behaviors: [
Behavior.INTERACTIVE_CHART,
Behavior.DRILL_TO_DETAIL,
Behavior.DRILL_BY,
],
category: t('KPI'),
credits: ['https://echarts.apache.org'],
description: t(

View File

@ -35,7 +35,11 @@ export default class EchartsGaugeChartPlugin extends ChartPlugin<
controlPanel,
loadChart: () => import('./EchartsGauge'),
metadata: new ChartMetadata({
behaviors: [Behavior.INTERACTIVE_CHART, Behavior.DRILL_TO_DETAIL],
behaviors: [
Behavior.INTERACTIVE_CHART,
Behavior.DRILL_TO_DETAIL,
Behavior.DRILL_BY,
],
category: t('KPI'),
credits: ['https://echarts.apache.org'],
description: t(

View File

@ -137,11 +137,16 @@ export default function EchartsGraph({
const data = (echartOptions as any).series[0].data as Data;
const drillToDetailFilters =
e.dataType === 'node' ? handleNodeClick(data) : handleEdgeClick(data);
const node = data.find(item => item.id === e.data.id);
onContextMenu(pointerEvent.clientX, pointerEvent.clientY, {
drillToDetail: drillToDetailFilters,
crossFilter: getCrossFilterDataMask(
data.find(item => item.id === e.data.id),
),
crossFilter: getCrossFilterDataMask(node),
drillBy: node && {
filters: [{ col: node.col, op: '==', val: node.name }],
groupbyFieldName:
node.col === formData.source ? 'source' : 'target',
},
});
}
},

View File

@ -48,7 +48,11 @@ export default class EchartsGraphChartPlugin extends ChartPlugin {
t('Transformable'),
],
thumbnail,
behaviors: [Behavior.INTERACTIVE_CHART, Behavior.DRILL_TO_DETAIL],
behaviors: [
Behavior.INTERACTIVE_CHART,
Behavior.DRILL_TO_DETAIL,
Behavior.DRILL_BY,
],
}),
transformProps,
});

View File

@ -131,42 +131,52 @@ export default function EchartsMixedTimeseries({
const { data, seriesName, seriesIndex } = eventParams;
const pointerEvent = eventParams.event.event;
const drillToDetailFilters: BinaryQueryObjectFilterClause[] = [];
if (data) {
const values = [
...(eventParams.name ? [eventParams.name] : []),
...(isFirstQuery(seriesIndex) ? labelMap : labelMapB)[
eventParams.seriesName
],
];
if (xAxis.type === AxisType.time) {
drillToDetailFilters.push({
col:
xAxis.label === DTTM_ALIAS
? formData.granularitySqla
: xAxis.label,
grain: formData.timeGrainSqla,
op: '==',
val: data[0],
formattedVal: xValueFormatter(data[0]),
});
}
[
...(xAxis.type === AxisType.category ? [xAxis.label] : []),
...(isFirstQuery(seriesIndex)
? formData.groupby
: formData.groupbyB),
].forEach((dimension, i) =>
drillToDetailFilters.push({
const drillByFilters: BinaryQueryObjectFilterClause[] = [];
const isFirst = isFirstQuery(seriesIndex);
const values = [
...(eventParams.name ? [eventParams.name] : []),
...(isFirst ? labelMap : labelMapB)[eventParams.seriesName],
];
if (data && xAxis.type === AxisType.time) {
drillToDetailFilters.push({
col:
xAxis.label === DTTM_ALIAS
? formData.granularitySqla
: xAxis.label,
grain: formData.timeGrainSqla,
op: '==',
val: data[0],
formattedVal: xValueFormatter(data[0]),
});
}
[
...(data && xAxis.type === AxisType.category ? [xAxis.label] : []),
...(isFirst ? formData.groupby : formData.groupbyB),
].forEach((dimension, i) =>
drillToDetailFilters.push({
col: dimension,
op: '==',
val: values[i],
formattedVal: String(values[i]),
}),
);
[...(isFirst ? formData.groupby : formData.groupbyB)].forEach(
(dimension, i) =>
drillByFilters.push({
col: dimension,
op: '==',
val: values[i],
formattedVal: String(values[i]),
}),
);
}
);
onContextMenu(pointerEvent.clientX, pointerEvent.clientY, {
drillToDetail: drillToDetailFilters,
crossFilter: getCrossFilterDataMask(seriesName, seriesIndex),
drillBy: {
filters: drillByFilters,
groupbyFieldName: isFirst ? 'groupby' : 'groupby_b',
adhocFilterFieldName: isFirst ? 'adhoc_filters' : 'adhoc_filters_b',
},
});
}
},

View File

@ -54,7 +54,11 @@ export default class EchartsTimeseriesChartPlugin extends ChartPlugin<
controlPanel,
loadChart: () => import('./EchartsMixedTimeseries'),
metadata: new ChartMetadata({
behaviors: [Behavior.INTERACTIVE_CHART, Behavior.DRILL_TO_DETAIL],
behaviors: [
Behavior.INTERACTIVE_CHART,
Behavior.DRILL_TO_DETAIL,
Behavior.DRILL_BY,
],
category: t('Evolution'),
credits: ['https://echarts.apache.org'],
description: hasGenericChartAxes

View File

@ -47,7 +47,11 @@ export default class EchartsPieChartPlugin extends ChartPlugin<
controlPanel,
loadChart: () => import('./EchartsPie'),
metadata: new ChartMetadata({
behaviors: [Behavior.INTERACTIVE_CHART, Behavior.DRILL_TO_DETAIL],
behaviors: [
Behavior.INTERACTIVE_CHART,
Behavior.DRILL_TO_DETAIL,
Behavior.DRILL_BY,
],
category: t('Part of a Whole'),
credits: ['https://echarts.apache.org'],
description:

View File

@ -46,7 +46,11 @@ export default class EchartsRadarChartPlugin extends ChartPlugin<
controlPanel,
loadChart: () => import('./EchartsRadar'),
metadata: new ChartMetadata({
behaviors: [Behavior.INTERACTIVE_CHART, Behavior.DRILL_TO_DETAIL],
behaviors: [
Behavior.INTERACTIVE_CHART,
Behavior.DRILL_TO_DETAIL,
Behavior.DRILL_BY,
],
category: t('Ranking'),
credits: ['https://echarts.apache.org'],
description: t(

View File

@ -40,7 +40,6 @@ export default function EchartsSunburst(props: SunburstTransformedProps) {
refs,
emitCrossFilters,
} = props;
const { columns } = formData;
const getCrossFilterDataMask = useCallback(
@ -62,7 +61,7 @@ export default function EchartsSunburst(props: SunburstTransformedProps) {
filters:
values.length === 0 || !columns
? []
: columns.map((col, idx) => {
: columns.slice(0, treePath.length).map((col, idx) => {
const val = labels.map(v => v[idx]);
if (val === null || val === undefined)
return {
@ -111,6 +110,7 @@ export default function EchartsSunburst(props: SunburstTransformedProps) {
const treePath = extractTreePathInfo(eventParams.treePathInfo);
const pointerEvent = eventParams.event.event;
const drillToDetailFilters: BinaryQueryObjectFilterClause[] = [];
const drillByFilters: BinaryQueryObjectFilterClause[] = [];
if (columns?.length) {
treePath.forEach((path, i) =>
drillToDetailFilters.push({
@ -120,10 +120,16 @@ export default function EchartsSunburst(props: SunburstTransformedProps) {
formattedVal: path,
}),
);
drillByFilters.push({
col: columns[treePath.length - 1],
op: '==',
val: treePath[treePath.length - 1],
});
}
onContextMenu(pointerEvent.clientX, pointerEvent.clientY, {
drillToDetail: drillToDetailFilters,
crossFilter: getCrossFilterDataMask(treePathInfo),
drillBy: { filters: drillByFilters, groupbyFieldName: 'columns' },
});
}
},

View File

@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
import { t, ChartMetadata, ChartPlugin, Behavior } from '@superset-ui/core';
import { Behavior, ChartMetadata, ChartPlugin, t } from '@superset-ui/core';
import transformProps from './transformProps';
import thumbnail from './images/thumbnail.png';
import controlPanel from './controlPanel';
@ -31,7 +31,11 @@ export default class EchartsSunburstChartPlugin extends ChartPlugin {
controlPanel,
loadChart: () => import('./EchartsSunburst'),
metadata: new ChartMetadata({
behaviors: [Behavior.INTERACTIVE_CHART, Behavior.DRILL_TO_DETAIL],
behaviors: [
Behavior.INTERACTIVE_CHART,
Behavior.DRILL_TO_DETAIL,
Behavior.DRILL_BY,
],
category: t('Part of a Whole'),
credits: ['https://echarts.apache.org'],
description: t(

View File

@ -50,7 +50,11 @@ export default class EchartsAreaChartPlugin extends ChartPlugin<
controlPanel,
loadChart: () => import('../EchartsTimeseries'),
metadata: new ChartMetadata({
behaviors: [Behavior.INTERACTIVE_CHART, Behavior.DRILL_TO_DETAIL],
behaviors: [
Behavior.INTERACTIVE_CHART,
Behavior.DRILL_TO_DETAIL,
Behavior.DRILL_BY,
],
category: t('Evolution'),
credits: ['https://echarts.apache.org'],
description: hasGenericChartAxes

View File

@ -201,40 +201,48 @@ export default function EchartsTimeseries({
eventParams.event.stop();
const { data, seriesName } = eventParams;
const drillToDetailFilters: BinaryQueryObjectFilterClause[] = [];
const drillByFilters: BinaryQueryObjectFilterClause[] = [];
const pointerEvent = eventParams.event.event;
const values = [
...(eventParams.name ? [eventParams.name] : []),
...labelMap[eventParams.seriesName],
...labelMap[seriesName],
];
if (data) {
if (xAxis.type === AxisType.time) {
drillToDetailFilters.push({
col:
// if the xAxis is '__timestamp', granularity_sqla will be the column of filter
xAxis.label === DTTM_ALIAS
? formData.granularitySqla
: xAxis.label,
grain: formData.timeGrainSqla,
op: '==',
val: data[0],
formattedVal: xValueFormatter(data[0]),
});
}
[
...(xAxis.type === AxisType.category ? [xAxis.label] : []),
...formData.groupby,
].forEach((dimension, i) =>
drillToDetailFilters.push({
col: dimension,
op: '==',
val: values[i],
formattedVal: String(values[i]),
}),
);
if (data && xAxis.type === AxisType.time) {
drillToDetailFilters.push({
col:
// if the xAxis is '__timestamp', granularity_sqla will be the column of filter
xAxis.label === DTTM_ALIAS
? formData.granularitySqla
: xAxis.label,
grain: formData.timeGrainSqla,
op: '==',
val: data[0],
formattedVal: xValueFormatter(data[0]),
});
}
[
...(xAxis.type === AxisType.category && data ? [xAxis.label] : []),
...formData.groupby,
].forEach((dimension, i) =>
drillToDetailFilters.push({
col: dimension,
op: '==',
val: values[i],
formattedVal: String(values[i]),
}),
);
formData.groupby.forEach((dimension, i) => {
drillByFilters.push({
col: dimension,
op: '==',
val: labelMap[seriesName][i],
});
});
onContextMenu(pointerEvent.clientX, pointerEvent.clientY, {
drillToDetail: drillToDetailFilters,
crossFilter: getCrossFilterDataMask(seriesName),
drillBy: { filters: drillByFilters, groupbyFieldName: 'groupby' },
});
}
},

View File

@ -56,7 +56,11 @@ export default class EchartsTimeseriesBarChartPlugin extends ChartPlugin<
controlPanel,
loadChart: () => import('../../EchartsTimeseries'),
metadata: new ChartMetadata({
behaviors: [Behavior.INTERACTIVE_CHART, Behavior.DRILL_TO_DETAIL],
behaviors: [
Behavior.INTERACTIVE_CHART,
Behavior.DRILL_TO_DETAIL,
Behavior.DRILL_BY,
],
category: t('Evolution'),
credits: ['https://echarts.apache.org'],
description: hasGenericChartAxes

View File

@ -55,7 +55,11 @@ export default class EchartsTimeseriesLineChartPlugin extends ChartPlugin<
controlPanel,
loadChart: () => import('../../EchartsTimeseries'),
metadata: new ChartMetadata({
behaviors: [Behavior.INTERACTIVE_CHART, Behavior.DRILL_TO_DETAIL],
behaviors: [
Behavior.INTERACTIVE_CHART,
Behavior.DRILL_TO_DETAIL,
Behavior.DRILL_BY,
],
category: t('Evolution'),
credits: ['https://echarts.apache.org'],
description: hasGenericChartAxes

View File

@ -54,7 +54,11 @@ export default class EchartsTimeseriesScatterChartPlugin extends ChartPlugin<
controlPanel,
loadChart: () => import('../../EchartsTimeseries'),
metadata: new ChartMetadata({
behaviors: [Behavior.INTERACTIVE_CHART, Behavior.DRILL_TO_DETAIL],
behaviors: [
Behavior.INTERACTIVE_CHART,
Behavior.DRILL_TO_DETAIL,
Behavior.DRILL_BY,
],
category: t('Evolution'),
credits: ['https://echarts.apache.org'],
description: hasGenericChartAxes

View File

@ -54,7 +54,11 @@ export default class EchartsTimeseriesSmoothLineChartPlugin extends ChartPlugin<
controlPanel,
loadChart: () => import('../../EchartsTimeseries'),
metadata: new ChartMetadata({
behaviors: [Behavior.INTERACTIVE_CHART, Behavior.DRILL_TO_DETAIL],
behaviors: [
Behavior.INTERACTIVE_CHART,
Behavior.DRILL_TO_DETAIL,
Behavior.DRILL_BY,
],
category: t('Evolution'),
credits: ['https://echarts.apache.org'],
description: hasGenericChartAxes

View File

@ -45,7 +45,11 @@ export default class EchartsTimeseriesStepChartPlugin extends ChartPlugin<
controlPanel,
loadChart: () => import('../EchartsTimeseries'),
metadata: new ChartMetadata({
behaviors: [Behavior.INTERACTIVE_CHART, Behavior.DRILL_TO_DETAIL],
behaviors: [
Behavior.INTERACTIVE_CHART,
Behavior.DRILL_TO_DETAIL,
Behavior.DRILL_BY,
],
category: t('Evolution'),
credits: ['https://echarts.apache.org'],
description: hasGenericChartAxes

View File

@ -44,7 +44,11 @@ export default class EchartsTimeseriesChartPlugin extends ChartPlugin<
controlPanel,
loadChart: () => import('./EchartsTimeseries'),
metadata: new ChartMetadata({
behaviors: [Behavior.INTERACTIVE_CHART, Behavior.DRILL_TO_DETAIL],
behaviors: [
Behavior.INTERACTIVE_CHART,
Behavior.DRILL_TO_DETAIL,
Behavior.DRILL_BY,
],
category: t('Evolution'),
credits: ['https://echarts.apache.org'],
description: hasGenericChartAxes

View File

@ -116,17 +116,25 @@ export default function EchartsTreemap({
if (treePath.length > 0) {
const pointerEvent = eventParams.event.event;
const drillToDetailFilters: BinaryQueryObjectFilterClause[] = [];
treePath.forEach((path, i) =>
const drillByFilters: BinaryQueryObjectFilterClause[] = [];
treePath.forEach((path, i) => {
const val = path === 'null' ? NULL_STRING : path;
drillToDetailFilters.push({
col: groupby[i],
op: '==',
val: path === 'null' ? NULL_STRING : path,
val,
formattedVal: path,
}),
);
});
drillByFilters.push({
col: groupby[i],
op: '==',
val,
});
});
onContextMenu(pointerEvent.clientX, pointerEvent.clientY, {
drillToDetail: drillToDetailFilters,
crossFilter: getCrossFilterDataMask(data, treePathInfo),
drillBy: { filters: drillByFilters, groupbyFieldName: 'groupby' },
});
}
}

View File

@ -46,7 +46,11 @@ export default class EchartsTreemapChartPlugin extends ChartPlugin<
controlPanel,
loadChart: () => import('./EchartsTreemap'),
metadata: new ChartMetadata({
behaviors: [Behavior.INTERACTIVE_CHART, Behavior.DRILL_TO_DETAIL],
behaviors: [
Behavior.INTERACTIVE_CHART,
Behavior.DRILL_TO_DETAIL,
Behavior.DRILL_BY,
],
category: t('Part of a Whole'),
credits: ['https://echarts.apache.org'],
description: t(

View File

@ -111,11 +111,11 @@ export const contextMenuEventHandler =
if (onContextMenu) {
e.event.stop();
const pointerEvent = e.event.event;
const drillToDetailFilters: BinaryQueryObjectFilterClause[] = [];
const drillFilters: BinaryQueryObjectFilterClause[] = [];
if (groupby.length > 0) {
const values = labelMap[e.name];
groupby.forEach((dimension, i) =>
drillToDetailFilters.push({
drillFilters.push({
col: dimension,
op: '==',
val: values[i],
@ -124,8 +124,9 @@ export const contextMenuEventHandler =
);
}
onContextMenu(pointerEvent.clientX, pointerEvent.clientY, {
drillToDetail: drillToDetailFilters,
drillToDetail: drillFilters,
crossFilter: getCrossFilterDataMask(e.name),
drillBy: { filters: drillFilters, groupbyFieldName: 'groupby' },
});
}
};

View File

@ -478,10 +478,27 @@ export default function PivotTableChart(props: PivotTableProps) {
onContextMenu(e.clientX, e.clientY, {
drillToDetail: drillToDetailFilters,
crossFilter: getCrossFilterDataMask(dataPoint),
drillBy: dataPoint && {
filters: [
{
col: Object.keys(dataPoint)[0],
op: '==',
val: Object.values(dataPoint)[0],
},
],
groupbyFieldName: rowKey ? 'groupbyRows' : 'groupbyColumns',
},
});
}
},
[cols, dateFormatters, onContextMenu, rows, timeGrainSqla],
[
cols,
dateFormatters,
getCrossFilterDataMask,
onContextMenu,
rows,
timeGrainSqla,
],
);
return (

View File

@ -17,12 +17,12 @@
* under the License.
*/
import {
t,
Behavior,
ChartMetadata,
ChartPlugin,
Behavior,
ChartProps,
QueryFormData,
t,
} from '@superset-ui/core';
import buildQuery from './buildQuery';
import controlPanel from './controlPanel';
@ -47,7 +47,11 @@ export default class PivotTableChartPlugin extends ChartPlugin<
*/
constructor() {
const metadata = new ChartMetadata({
behaviors: [Behavior.INTERACTIVE_CHART, Behavior.DRILL_TO_DETAIL],
behaviors: [
Behavior.INTERACTIVE_CHART,
Behavior.DRILL_TO_DETAIL,
Behavior.DRILL_BY,
],
category: t('Table'),
description: t(
'Used to summarize a set of data by grouping together multiple statistics along two axes. Examples: Sales numbers by region and month, tasks by status and assignee, active users by age and location. Not the most visually stunning visualization, but highly informative and versatile.',

View File

@ -391,6 +391,18 @@ export default function TableChart<D extends DataRecord = DataRecord>(
crossFilter: cellPoint.isMetric
? undefined
: getCrossFilterDataMask(cellPoint.key, cellPoint.value),
drillBy: cellPoint.isMetric
? undefined
: {
filters: [
{
col: cellPoint.key,
op: '==',
val: cellPoint.value as string | number | boolean,
},
],
groupbyFieldName: 'groupby',
},
});
}
: undefined;

View File

@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
import { t, ChartMetadata, ChartPlugin, Behavior } from '@superset-ui/core';
import { Behavior, ChartMetadata, ChartPlugin, t } from '@superset-ui/core';
import transformProps from './transformProps';
import thumbnail from './images/thumbnail.png';
import example1 from './images/Table.jpg';
@ -31,7 +31,11 @@ export { default as __hack__ } from './types';
export * from './types';
const metadata = new ChartMetadata({
behaviors: [Behavior.INTERACTIVE_CHART, Behavior.DRILL_TO_DETAIL],
behaviors: [
Behavior.INTERACTIVE_CHART,
Behavior.DRILL_TO_DETAIL,
Behavior.DRILL_BY,
],
category: t('Table'),
canBeAnnotationTypes: ['EVENT', 'INTERVAL'],
description: t(

View File

@ -44,6 +44,7 @@ import { DrillDetailMenuItems } from './DrillDetail';
import { getMenuAdjustedY } from './utils';
import { updateDataMask } from '../../dataMask/actions';
import { MenuItemTooltip } from './DisabledMenuItemTooltip';
import { DrillByMenuItems } from './DrillBy/DrillByMenuItems';
export interface ChartContextMenuProps {
id: number;
@ -84,17 +85,25 @@ const ChartContextMenu = (
const showDrillToDetail =
isFeatureEnabled(FeatureFlag.DRILL_TO_DETAIL) && canExplore;
const showDrillBy = isFeatureEnabled(FeatureFlag.DRILL_BY) && canExplore;
const showCrossFilters = isFeatureEnabled(
FeatureFlag.DASHBOARD_CROSS_FILTERS,
);
const isCrossFilteringSupportedByChart = getChartMetadataRegistry()
.get(formData.viz_type)
?.behaviors?.includes(Behavior.INTERACTIVE_CHART);
let itemsCount = 0;
if (isFeatureEnabled(FeatureFlag.DASHBOARD_CROSS_FILTERS)) {
if (showCrossFilters) {
itemsCount += 1;
}
if (showDrillToDetail) {
itemsCount += 2; // Drill to detail always has 2 top-level menu items
}
if (showDrillBy) {
itemsCount += 1;
}
if (itemsCount === 0) {
itemsCount = 1; // "No actions" appears if no actions in menu
}
@ -180,6 +189,25 @@ const ChartContextMenu = (
isContextMenu
contextMenuY={clientY}
onSelection={onSelection}
submenuIndex={showCrossFilters ? 2 : 1}
/>,
);
}
if (showDrillBy) {
let submenuIndex = 0;
if (showCrossFilters) {
submenuIndex += 1;
}
if (showDrillToDetail) {
submenuIndex += 2;
}
menuItems.push(
<DrillByMenuItems
filters={filters?.drillBy?.filters}
groupbyFieldName={filters?.drillBy?.groupbyFieldName}
formData={formData}
contextMenuY={clientY}
submenuIndex={submenuIndex}
/>,
);
}

View File

@ -0,0 +1,190 @@
/**
* 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 userEvent from '@testing-library/user-event';
import {
Behavior,
ChartMetadata,
getChartMetadataRegistry,
} from '@superset-ui/core';
import fetchMock from 'fetch-mock';
import { render, screen, within, waitFor } from 'spec/helpers/testing-library';
import chartQueries, { sliceId } from 'spec/fixtures/mockChartQueries';
import { Menu } from 'src/components/Menu';
import { supersetGetCache } from 'src/utils/cachedSupersetGet';
import { DrillByMenuItems, DrillByMenuItemsProps } from './DrillByMenuItems';
/* eslint jest/expect-expect: ["warn", { "assertFunctionNames": ["expect*"] }] */
const datasetEndpointMatcher = 'glob:*/api/v1/dataset/7';
const { form_data: defaultFormData } = chartQueries[sliceId];
const defaultColumns = [
{ column_name: 'col1', groupby: true },
{ column_name: 'col2', groupby: true },
{ column_name: 'col3', groupby: true },
{ column_name: 'col4', groupby: true },
{ column_name: 'col5', groupby: true },
{ column_name: 'col6', groupby: true },
{ column_name: 'col7', groupby: true },
{ column_name: 'col8', groupby: true },
{ column_name: 'col9', groupby: true },
{ column_name: 'col10', groupby: true },
{ column_name: 'col11', groupby: true },
];
const defaultFilters = [
{
col: 'filter_col',
op: '==' as const,
val: 'val',
},
];
const renderMenu = ({
formData = defaultFormData,
filters = defaultFilters,
}: Partial<DrillByMenuItemsProps>) =>
render(
<Menu>
<DrillByMenuItems
formData={formData ?? defaultFormData}
filters={filters}
groupbyFieldName="groupby"
/>
</Menu>,
{ useRouter: true, useRedux: true },
);
const expectDrillByDisabled = async (tooltipContent: string) => {
const drillByMenuItem = screen.getByRole('menuitem', {
name: 'Drill by',
});
expect(drillByMenuItem).toBeVisible();
expect(drillByMenuItem).toHaveAttribute('aria-disabled', 'true');
const tooltipTrigger = within(drillByMenuItem).getByTestId('tooltip-trigger');
userEvent.hover(tooltipTrigger as HTMLElement);
const tooltip = await screen.findByRole('tooltip', { name: tooltipContent });
expect(tooltip).toBeInTheDocument();
};
const expectDrillByEnabled = async () => {
const drillByMenuItem = screen.getByRole('menuitem', {
name: 'Drill by',
});
expect(drillByMenuItem).toBeInTheDocument();
await waitFor(() =>
expect(drillByMenuItem).not.toHaveAttribute('aria-disabled'),
);
const tooltipTrigger =
within(drillByMenuItem).queryByTestId('tooltip-trigger');
expect(tooltipTrigger).not.toBeInTheDocument();
userEvent.hover(
within(drillByMenuItem).getByRole('button', { name: 'Drill by' }),
);
expect(await screen.findByTestId('drill-by-submenu')).toBeInTheDocument();
};
getChartMetadataRegistry().registerValue(
'pie',
new ChartMetadata({
name: 'fake pie',
thumbnail: '.png',
useLegacyApi: false,
behaviors: [Behavior.DRILL_BY],
}),
);
describe('Drill by menu items', () => {
afterEach(() => {
supersetGetCache.clear();
fetchMock.restore();
});
test('render disabled menu item for unsupported chart', async () => {
renderMenu({
formData: { ...defaultFormData, viz_type: 'unsupported_viz' },
});
await expectDrillByDisabled(
'Drill by is not yet supported for this chart type',
);
});
test('render disabled menu item for supported chart, no filters', async () => {
renderMenu({ filters: [] });
await expectDrillByDisabled(
'Drill by is not available for this data point',
);
});
test('render disabled menu item for supported chart, no columns', async () => {
fetchMock.get(datasetEndpointMatcher, { result: { columns: [] } });
renderMenu({});
await waitFor(() => fetchMock.called(datasetEndpointMatcher));
await expectDrillByDisabled('No dimensions available for drill by');
});
test('render menu item with submenu without searchbox', async () => {
const slicedColumns = defaultColumns.slice(0, 9);
fetchMock.get(datasetEndpointMatcher, {
result: { columns: slicedColumns },
});
renderMenu({});
await waitFor(() => fetchMock.called(datasetEndpointMatcher));
await expectDrillByEnabled();
slicedColumns.forEach(column => {
expect(screen.getByText(column.column_name)).toBeInTheDocument();
});
expect(screen.queryByRole('textbox')).not.toBeInTheDocument();
});
test('render menu item with submenu and searchbox', async () => {
fetchMock.get(datasetEndpointMatcher, {
result: { columns: defaultColumns },
});
renderMenu({});
await waitFor(() => fetchMock.called(datasetEndpointMatcher));
await expectDrillByEnabled();
defaultColumns.forEach(column => {
expect(screen.getByText(column.column_name)).toBeInTheDocument();
});
const searchbox = screen.getByRole('textbox');
expect(searchbox).toBeInTheDocument();
userEvent.type(searchbox, 'col1');
await screen.findByText('col1');
const expectedFilteredColumnNames = ['col1', 'col10', 'col11'];
defaultColumns
.filter(col => !expectedFilteredColumnNames.includes(col.column_name))
.forEach(col => {
expect(screen.queryByText(col.column_name)).not.toBeInTheDocument();
});
expectedFilteredColumnNames.forEach(colName => {
expect(screen.getByText(colName)).toBeInTheDocument();
});
});
});

View File

@ -0,0 +1,221 @@
/**
* 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, {
ChangeEvent,
ReactNode,
useCallback,
useEffect,
useMemo,
useState,
} from 'react';
import { Menu } from 'src/components/Menu';
import {
BaseFormData,
Behavior,
BinaryQueryObjectFilterClause,
Column,
css,
ensureIsArray,
getChartMetadataRegistry,
t,
useTheme,
} from '@superset-ui/core';
import Icons from 'src/components/Icons';
import { Input } from 'src/components/Input';
import {
cachedSupersetGet,
supersetGetCache,
} from 'src/utils/cachedSupersetGet';
import { MenuItemTooltip } from '../DisabledMenuItemTooltip';
import { getSubmenuYOffset } from '../utils';
import { MenuItemWithTruncation } from '../MenuItemWithTruncation';
const MAX_SUBMENU_HEIGHT = 200;
const SHOW_COLUMNS_SEARCH_THRESHOLD = 10;
const SEARCH_INPUT_HEIGHT = 48;
export interface DrillByMenuItemsProps {
filters?: BinaryQueryObjectFilterClause[];
formData: BaseFormData & { [key: string]: any };
contextMenuY?: number;
submenuIndex?: number;
groupbyFieldName?: string;
}
export const DrillByMenuItems = ({
filters,
groupbyFieldName,
formData,
contextMenuY = 0,
submenuIndex = 0,
...rest
}: DrillByMenuItemsProps) => {
const theme = useTheme();
const [searchInput, setSearchInput] = useState('');
const [columns, setColumns] = useState<Column[]>([]);
useEffect(() => {
// Input is displayed only when columns.length > SHOW_COLUMNS_SEARCH_THRESHOLD
// Reset search input in case Input gets removed
setSearchInput('');
}, [columns.length]);
const hasDrillBy = ensureIsArray(filters).length && groupbyFieldName;
const handlesDimensionContextMenu = useMemo(
() =>
getChartMetadataRegistry()
.get(formData.viz_type)
?.behaviors.find(behavior => behavior === Behavior.DRILL_BY),
[formData.viz_type],
);
useEffect(() => {
if (handlesDimensionContextMenu && hasDrillBy) {
const datasetId = formData.datasource.split('__')[0];
cachedSupersetGet({
endpoint: `/api/v1/dataset/${datasetId}`,
})
.then(({ json: { result } }) => {
setColumns(
ensureIsArray(result.columns)
.filter(column => column.groupby)
.filter(
column =>
!ensureIsArray(formData[groupbyFieldName]).includes(
column.column_name,
),
),
);
})
.catch(() => {
supersetGetCache.delete(`/api/v1/dataset/${datasetId}`);
});
}
}, [formData, groupbyFieldName, handlesDimensionContextMenu, hasDrillBy]);
const handleInput = useCallback((e: ChangeEvent<HTMLInputElement>) => {
e.stopPropagation();
const input = e?.target?.value;
setSearchInput(input);
}, []);
const filteredColumns = useMemo(
() =>
columns.filter(column =>
(column.verbose_name || column.column_name)
.toLowerCase()
.includes(searchInput.toLowerCase()),
),
[columns, searchInput],
);
const submenuYOffset = useMemo(
() =>
getSubmenuYOffset(
contextMenuY,
filteredColumns.length || 1,
submenuIndex,
MAX_SUBMENU_HEIGHT,
columns.length > SHOW_COLUMNS_SEARCH_THRESHOLD
? SEARCH_INPUT_HEIGHT
: 0,
),
[contextMenuY, filteredColumns.length, submenuIndex, columns.length],
);
let tooltip: ReactNode;
if (!handlesDimensionContextMenu) {
tooltip = t('Drill by is not yet supported for this chart type');
} else if (!hasDrillBy) {
tooltip = t('Drill by is not available for this data point');
} else if (columns.length === 0) {
tooltip = t('No dimensions available for drill by');
}
if (!handlesDimensionContextMenu || !hasDrillBy || columns.length === 0) {
return (
<Menu.Item key="drill-by-disabled" disabled {...rest}>
<div>
{t('Drill by')}
<MenuItemTooltip title={tooltip} />
</div>
</Menu.Item>
);
}
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>
);
};

View File

@ -31,10 +31,10 @@ import {
} from '@superset-ui/core';
import { Menu } from 'src/components/Menu';
import DrillDetailModal from './DrillDetailModal';
import { getMenuAdjustedY, MENU_ITEM_HEIGHT } from '../utils';
import { getSubmenuYOffset } from '../utils';
import { MenuItemTooltip } from '../DisabledMenuItemTooltip';
import { MenuItemWithTruncation } from '../MenuItemWithTruncation';
const MENU_PADDING = 4;
const DRILL_TO_DETAIL_TEXT = t('Drill to detail by');
const DisabledMenuItem = ({ children, ...props }: { children: ReactNode }) => (
@ -65,6 +65,7 @@ export type DrillDetailMenuItemsProps = {
contextMenuY?: number;
onSelection?: () => void;
onClick?: (event: MouseEvent) => void;
submenuIndex?: number;
};
const DrillDetailMenuItems = ({
@ -75,6 +76,7 @@ const DrillDetailMenuItems = ({
contextMenuY = 0,
onSelection = () => null,
onClick = () => null,
submenuIndex = 0,
...props
}: DrillDetailMenuItemsProps) => {
const [modalFilters, setFilters] = useState<BinaryQueryObjectFilterClause[]>(
@ -162,31 +164,35 @@ const DrillDetailMenuItems = ({
}
// Ensure submenu doesn't appear offscreen
const submenuYOffset = useMemo(() => {
const itemsCount = filters.length > 1 ? filters.length + 1 : filters.length;
const submenuY =
contextMenuY + MENU_PADDING + MENU_ITEM_HEIGHT + MENU_PADDING;
return getMenuAdjustedY(submenuY, itemsCount) - submenuY;
}, [contextMenuY, filters.length]);
const submenuYOffset = useMemo(
() =>
getSubmenuYOffset(
contextMenuY,
filters.length > 1 ? filters.length + 1 : filters.length,
submenuIndex,
),
[contextMenuY, filters.length, submenuIndex],
);
if (handlesDimensionContextMenu && !noAggregations && filters?.length) {
drillToDetailByMenuItem = (
<Menu.SubMenu
{...props}
popupOffset={[0, submenuYOffset]}
popupClassName="chart-context-submenu"
title={DRILL_TO_DETAIL_TEXT}
>
<div data-test="drill-to-detail-by-submenu">
{filters.map((filter, i) => (
<Menu.Item
<MenuItemWithTruncation
{...props}
tooltipText={`${DRILL_TO_DETAIL_TEXT} ${filter.formattedVal}`}
key={`drill-detail-filter-${i}`}
onClick={openModal.bind(null, [filter])}
>
{`${DRILL_TO_DETAIL_TEXT} `}
<Filter>{filter.formattedVal}</Filter>
</Menu.Item>
</MenuItemWithTruncation>
))}
{filters.length > 1 && (
<Menu.Item
@ -194,8 +200,10 @@ const DrillDetailMenuItems = ({
key="drill-detail-filter-all"
onClick={openModal.bind(null, filters)}
>
{`${DRILL_TO_DETAIL_TEXT} `}
<Filter>{t('all')}</Filter>
<div>
{`${DRILL_TO_DETAIL_TEXT} `}
<Filter>{t('all')}</Filter>
</div>
</Menu.Item>
)}
</div>

View File

@ -0,0 +1,58 @@
/**
* 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, { ReactNode } from 'react';
import { css, truncationCSS, useCSSTextTruncation } from '@superset-ui/core';
import { Menu } from 'src/components/Menu';
import { Tooltip } from 'src/components/Tooltip';
export type MenuItemWithTruncationProps = {
tooltipText: ReactNode;
children: ReactNode;
onClick?: () => void;
};
export const MenuItemWithTruncation = ({
tooltipText,
children,
...props
}: MenuItemWithTruncationProps) => {
const [itemRef, itemIsTruncated] = useCSSTextTruncation<HTMLDivElement>();
return (
<Menu.Item
css={css`
display: flex;
`}
{...props}
>
<Tooltip title={itemIsTruncated ? tooltipText : null}>
<div
ref={itemRef}
css={css`
max-width: 100%;
${truncationCSS};
`}
>
{children}
</div>
</Tooltip>
</Menu.Item>
);
};

View File

@ -39,4 +39,7 @@ test('correctly positions at lower edge of screen', () => {
expect(getMenuAdjustedY(425, 1)).toEqual(425); // No adjustment
expect(getMenuAdjustedY(425, 2)).toEqual(404); // Adjustment
expect(getMenuAdjustedY(425, 3)).toEqual(372); // Adjustment
expect(getMenuAdjustedY(425, 8, 200)).toEqual(268);
expect(getMenuAdjustedY(425, 8, 200, 48)).toEqual(220);
});

View File

@ -18,6 +18,7 @@
*/
export const MENU_ITEM_HEIGHT = 32;
const MENU_PADDING = 4;
const MENU_VERTICAL_SPACING = 32;
/**
@ -27,14 +28,45 @@ const MENU_VERTICAL_SPACING = 32;
* @param clientY The original Y-offset
* @param itemsCount The number of menu items
*/
export function getMenuAdjustedY(clientY: number, itemsCount: number) {
export const getMenuAdjustedY = (
clientY: number,
itemsCount: number,
maxItemsContainerHeight = Number.MAX_SAFE_INTEGER,
additionalItemsHeight = 0,
) => {
// Viewport height
const vh = Math.max(
document.documentElement.clientHeight || 0,
window.innerHeight || 0,
);
const menuHeight = MENU_ITEM_HEIGHT * itemsCount + MENU_VERTICAL_SPACING;
const menuHeight =
Math.min(MENU_ITEM_HEIGHT * itemsCount, maxItemsContainerHeight) +
MENU_VERTICAL_SPACING +
additionalItemsHeight;
// Always show the context menu inside the viewport
return vh - clientY < menuHeight ? vh - menuHeight : clientY;
}
};
export const getSubmenuYOffset = (
contextMenuY: number,
itemsCount: number,
submenuIndex = 0,
maxItemsContainerHeight = Number.MAX_SAFE_INTEGER,
additionalItemsHeight = 0,
) => {
const submenuY =
contextMenuY +
MENU_PADDING +
MENU_ITEM_HEIGHT * submenuIndex +
MENU_PADDING;
return (
getMenuAdjustedY(
submenuY,
itemsCount,
maxItemsContainerHeight,
additionalItemsHeight,
) - submenuY
);
};

View File

@ -22,8 +22,8 @@ import { Column, ensureIsArray, t, useChangeEffect } from '@superset-ui/core';
import { Select, FormInstance } from 'src/components';
import { useToasts } from 'src/components/MessageToasts/withToasts';
import { getClientErrorObject } from 'src/utils/getClientErrorObject';
import { cachedSupersetGet } from 'src/utils/cachedSupersetGet';
import { NativeFiltersForm } from '../types';
import { cachedSupersetGet } from './utils';
interface ColumnSelectProps {
allowClear?: boolean;

View File

@ -24,7 +24,8 @@ import {
ClientErrorObject,
getClientErrorObject,
} from 'src/utils/getClientErrorObject';
import { cachedSupersetGet, datasetToSelectOption } from './utils';
import { cachedSupersetGet } from 'src/utils/cachedSupersetGet';
import { datasetToSelectOption } from './utils';
interface DatasetSelectProps {
onChange: (value: { label: string; value: number }) => void;

View File

@ -60,6 +60,7 @@ import { addDangerToast } from 'src/components/MessageToasts/actions';
import { Radio } from 'src/components/Radio';
import Tabs from 'src/components/Tabs';
import { Tooltip } from 'src/components/Tooltip';
import { cachedSupersetGet } from 'src/utils/cachedSupersetGet';
import {
Chart,
ChartsState,
@ -90,7 +91,6 @@ import getControlItemsMap from './getControlItemsMap';
import RemovedFilter from './RemovedFilter';
import { useBackendFormUpdate, useDefaultValue } from './state';
import {
cachedSupersetGet,
hasTemporalColumns,
mostUsedDataset,
setNativeFilterFieldValues,

View File

@ -20,14 +20,8 @@ import { flatMapDeep } from 'lodash';
import { FormInstance } from 'src/components';
import React from 'react';
import { CustomControlItem, Dataset } from '@superset-ui/chart-controls';
import {
Column,
ensureIsArray,
GenericDataType,
SupersetClient,
} from '@superset-ui/core';
import { Column, ensureIsArray, GenericDataType } from '@superset-ui/core';
import { DatasourcesState, ChartsState } from 'src/dashboard/types';
import { cacheWrapper } from 'src/utils/cacheWrapper';
import { FILTER_SUPPORTED_TYPES } from './constants';
const FILTERS_FIELD_NAME = 'filters';
@ -124,11 +118,3 @@ export const mostUsedDataset = (
return datasets[mostUsedDataset]?.id;
};
const localCache = new Map<string, any>();
export const cachedSupersetGet = cacheWrapper(
SupersetClient.get,
localCache,
({ endpoint }) => endpoint || '',
);

View File

@ -59,7 +59,11 @@ import { DashboardContextForExplore } from 'src/types/DashboardContextForExplore
import shortid from 'shortid';
import { RootState } from '../types';
import { getActiveFilters } from '../util/activeDashboardFilters';
import { filterCardPopoverStyle, headerStyles } from '../styles';
import {
chartContextMenuStyles,
filterCardPopoverStyle,
headerStyles,
} from '../styles';
export const DashboardPageIdContext = React.createContext('');
@ -279,7 +283,13 @@ export const DashboardPage: FC<PageProps> = ({ idOrSlug }: PageProps) => {
return (
<>
<Global styles={[filterCardPopoverStyle(theme), headerStyles(theme)]} />
<Global
styles={[
filterCardPopoverStyle(theme),
headerStyles(theme),
chartContextMenuStyles(theme),
]}
/>
<DashboardPageIdContext.Provider value={dashboardPageId}>
<DashboardContainer />
</DashboardPageIdContext.Provider>

View File

@ -87,3 +87,10 @@ export const filterCardPopoverStyle = (theme: SupersetTheme) => css`
}
}
`;
export const chartContextMenuStyles = (theme: SupersetTheme) => css`
.ant-dropdown-menu-submenu.chart-context-submenu {
max-width: ${theme.gridUnit * 60}px;
min-width: ${theme.gridUnit * 40}px;
}
`;

View File

@ -0,0 +1,29 @@
/**
* 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 { SupersetClient } from '@superset-ui/core';
import { cacheWrapper } from './cacheWrapper';
export const supersetGetCache = new Map<string, any>();
export const cachedSupersetGet = cacheWrapper(
SupersetClient.get,
supersetGetCache,
({ endpoint }) => endpoint || '',
);