mirror of
https://github.com/apache/superset.git
synced 2024-09-12 00:29:39 -04:00
feat: Implement context menu for drill by (#23454)
This commit is contained in:
parent
542bf25729
commit
9fbfd1c1d8
@ -31,6 +31,7 @@ export enum Behavior {
|
|||||||
* when dimensions are right-clicked on.
|
* when dimensions are right-clicked on.
|
||||||
*/
|
*/
|
||||||
DRILL_TO_DETAIL = 'DRILL_TO_DETAIL',
|
DRILL_TO_DETAIL = 'DRILL_TO_DETAIL',
|
||||||
|
DRILL_BY = 'DRILL_BY',
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ContextMenuFilters {
|
export interface ContextMenuFilters {
|
||||||
@ -39,6 +40,11 @@ export interface ContextMenuFilters {
|
|||||||
isCurrentValueSelected?: boolean;
|
isCurrentValueSelected?: boolean;
|
||||||
};
|
};
|
||||||
drillToDetail?: BinaryQueryObjectFilterClause[];
|
drillToDetail?: BinaryQueryObjectFilterClause[];
|
||||||
|
drillBy?: {
|
||||||
|
filters: BinaryQueryObjectFilterClause[];
|
||||||
|
groupbyFieldName: string;
|
||||||
|
adhocFilterFieldName?: string;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum AppSection {
|
export enum AppSection {
|
||||||
|
@ -172,6 +172,7 @@ function WorldMap(element, props) {
|
|||||||
const val =
|
const val =
|
||||||
countryFieldtype === 'name' ? mapData[key]?.name : mapData[key]?.country;
|
countryFieldtype === 'name' ? mapData[key]?.name : mapData[key]?.country;
|
||||||
let drillToDetailFilters;
|
let drillToDetailFilters;
|
||||||
|
let drillByFilters;
|
||||||
if (val) {
|
if (val) {
|
||||||
drillToDetailFilters = [
|
drillToDetailFilters = [
|
||||||
{
|
{
|
||||||
@ -181,10 +182,18 @@ function WorldMap(element, props) {
|
|||||||
formattedVal: val,
|
formattedVal: val,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
drillByFilters = [
|
||||||
|
{
|
||||||
|
col: entity,
|
||||||
|
op: '==',
|
||||||
|
val,
|
||||||
|
},
|
||||||
|
];
|
||||||
}
|
}
|
||||||
onContextMenu(pointerEvent.clientX, pointerEvent.clientY, {
|
onContextMenu(pointerEvent.clientX, pointerEvent.clientY, {
|
||||||
drillToDetail: drillToDetailFilters,
|
drillToDetail: drillToDetailFilters,
|
||||||
crossFilter: getCrossFilterDataMask(source),
|
crossFilter: getCrossFilterDataMask(source),
|
||||||
|
drillBy: { filters: drillByFilters, groupbyFieldName: 'entity' },
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -45,7 +45,11 @@ const metadata = new ChartMetadata({
|
|||||||
],
|
],
|
||||||
thumbnail,
|
thumbnail,
|
||||||
useLegacyApi: true,
|
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 {
|
export default class WorldMapChartPlugin extends ChartPlugin {
|
||||||
|
@ -16,7 +16,7 @@
|
|||||||
* specific language governing permissions and limitations
|
* specific language governing permissions and limitations
|
||||||
* under the License.
|
* 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 buildQuery from './buildQuery';
|
||||||
import controlPanel from './controlPanel';
|
import controlPanel from './controlPanel';
|
||||||
import transformProps from './transformProps';
|
import transformProps from './transformProps';
|
||||||
@ -44,7 +44,11 @@ export default class EchartsBoxPlotChartPlugin extends ChartPlugin<
|
|||||||
controlPanel,
|
controlPanel,
|
||||||
loadChart: () => import('./EchartsBoxPlot'),
|
loadChart: () => import('./EchartsBoxPlot'),
|
||||||
metadata: new ChartMetadata({
|
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'),
|
category: t('Distribution'),
|
||||||
credits: ['https://echarts.apache.org'],
|
credits: ['https://echarts.apache.org'],
|
||||||
description: t(
|
description: t(
|
||||||
|
@ -44,7 +44,11 @@ export default class EchartsFunnelChartPlugin extends ChartPlugin<
|
|||||||
controlPanel,
|
controlPanel,
|
||||||
loadChart: () => import('./EchartsFunnel'),
|
loadChart: () => import('./EchartsFunnel'),
|
||||||
metadata: new ChartMetadata({
|
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'),
|
category: t('KPI'),
|
||||||
credits: ['https://echarts.apache.org'],
|
credits: ['https://echarts.apache.org'],
|
||||||
description: t(
|
description: t(
|
||||||
|
@ -35,7 +35,11 @@ export default class EchartsGaugeChartPlugin extends ChartPlugin<
|
|||||||
controlPanel,
|
controlPanel,
|
||||||
loadChart: () => import('./EchartsGauge'),
|
loadChart: () => import('./EchartsGauge'),
|
||||||
metadata: new ChartMetadata({
|
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'),
|
category: t('KPI'),
|
||||||
credits: ['https://echarts.apache.org'],
|
credits: ['https://echarts.apache.org'],
|
||||||
description: t(
|
description: t(
|
||||||
|
@ -137,11 +137,16 @@ export default function EchartsGraph({
|
|||||||
const data = (echartOptions as any).series[0].data as Data;
|
const data = (echartOptions as any).series[0].data as Data;
|
||||||
const drillToDetailFilters =
|
const drillToDetailFilters =
|
||||||
e.dataType === 'node' ? handleNodeClick(data) : handleEdgeClick(data);
|
e.dataType === 'node' ? handleNodeClick(data) : handleEdgeClick(data);
|
||||||
|
const node = data.find(item => item.id === e.data.id);
|
||||||
|
|
||||||
onContextMenu(pointerEvent.clientX, pointerEvent.clientY, {
|
onContextMenu(pointerEvent.clientX, pointerEvent.clientY, {
|
||||||
drillToDetail: drillToDetailFilters,
|
drillToDetail: drillToDetailFilters,
|
||||||
crossFilter: getCrossFilterDataMask(
|
crossFilter: getCrossFilterDataMask(node),
|
||||||
data.find(item => item.id === e.data.id),
|
drillBy: node && {
|
||||||
),
|
filters: [{ col: node.col, op: '==', val: node.name }],
|
||||||
|
groupbyFieldName:
|
||||||
|
node.col === formData.source ? 'source' : 'target',
|
||||||
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -48,7 +48,11 @@ export default class EchartsGraphChartPlugin extends ChartPlugin {
|
|||||||
t('Transformable'),
|
t('Transformable'),
|
||||||
],
|
],
|
||||||
thumbnail,
|
thumbnail,
|
||||||
behaviors: [Behavior.INTERACTIVE_CHART, Behavior.DRILL_TO_DETAIL],
|
behaviors: [
|
||||||
|
Behavior.INTERACTIVE_CHART,
|
||||||
|
Behavior.DRILL_TO_DETAIL,
|
||||||
|
Behavior.DRILL_BY,
|
||||||
|
],
|
||||||
}),
|
}),
|
||||||
transformProps,
|
transformProps,
|
||||||
});
|
});
|
||||||
|
@ -131,42 +131,52 @@ export default function EchartsMixedTimeseries({
|
|||||||
const { data, seriesName, seriesIndex } = eventParams;
|
const { data, seriesName, seriesIndex } = eventParams;
|
||||||
const pointerEvent = eventParams.event.event;
|
const pointerEvent = eventParams.event.event;
|
||||||
const drillToDetailFilters: BinaryQueryObjectFilterClause[] = [];
|
const drillToDetailFilters: BinaryQueryObjectFilterClause[] = [];
|
||||||
if (data) {
|
const drillByFilters: BinaryQueryObjectFilterClause[] = [];
|
||||||
const values = [
|
const isFirst = isFirstQuery(seriesIndex);
|
||||||
...(eventParams.name ? [eventParams.name] : []),
|
const values = [
|
||||||
...(isFirstQuery(seriesIndex) ? labelMap : labelMapB)[
|
...(eventParams.name ? [eventParams.name] : []),
|
||||||
eventParams.seriesName
|
...(isFirst ? labelMap : labelMapB)[eventParams.seriesName],
|
||||||
],
|
];
|
||||||
];
|
if (data && xAxis.type === AxisType.time) {
|
||||||
if (xAxis.type === AxisType.time) {
|
drillToDetailFilters.push({
|
||||||
drillToDetailFilters.push({
|
col:
|
||||||
col:
|
xAxis.label === DTTM_ALIAS
|
||||||
xAxis.label === DTTM_ALIAS
|
? formData.granularitySqla
|
||||||
? formData.granularitySqla
|
: xAxis.label,
|
||||||
: xAxis.label,
|
grain: formData.timeGrainSqla,
|
||||||
grain: formData.timeGrainSqla,
|
op: '==',
|
||||||
op: '==',
|
val: data[0],
|
||||||
val: data[0],
|
formattedVal: xValueFormatter(data[0]),
|
||||||
formattedVal: xValueFormatter(data[0]),
|
});
|
||||||
});
|
}
|
||||||
}
|
[
|
||||||
[
|
...(data && xAxis.type === AxisType.category ? [xAxis.label] : []),
|
||||||
...(xAxis.type === AxisType.category ? [xAxis.label] : []),
|
...(isFirst ? formData.groupby : formData.groupbyB),
|
||||||
...(isFirstQuery(seriesIndex)
|
].forEach((dimension, i) =>
|
||||||
? formData.groupby
|
drillToDetailFilters.push({
|
||||||
: formData.groupbyB),
|
col: dimension,
|
||||||
].forEach((dimension, i) =>
|
op: '==',
|
||||||
drillToDetailFilters.push({
|
val: values[i],
|
||||||
|
formattedVal: String(values[i]),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
[...(isFirst ? formData.groupby : formData.groupbyB)].forEach(
|
||||||
|
(dimension, i) =>
|
||||||
|
drillByFilters.push({
|
||||||
col: dimension,
|
col: dimension,
|
||||||
op: '==',
|
op: '==',
|
||||||
val: values[i],
|
val: values[i],
|
||||||
formattedVal: String(values[i]),
|
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
|
||||||
onContextMenu(pointerEvent.clientX, pointerEvent.clientY, {
|
onContextMenu(pointerEvent.clientX, pointerEvent.clientY, {
|
||||||
drillToDetail: drillToDetailFilters,
|
drillToDetail: drillToDetailFilters,
|
||||||
crossFilter: getCrossFilterDataMask(seriesName, seriesIndex),
|
crossFilter: getCrossFilterDataMask(seriesName, seriesIndex),
|
||||||
|
drillBy: {
|
||||||
|
filters: drillByFilters,
|
||||||
|
groupbyFieldName: isFirst ? 'groupby' : 'groupby_b',
|
||||||
|
adhocFilterFieldName: isFirst ? 'adhoc_filters' : 'adhoc_filters_b',
|
||||||
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -54,7 +54,11 @@ export default class EchartsTimeseriesChartPlugin extends ChartPlugin<
|
|||||||
controlPanel,
|
controlPanel,
|
||||||
loadChart: () => import('./EchartsMixedTimeseries'),
|
loadChart: () => import('./EchartsMixedTimeseries'),
|
||||||
metadata: new ChartMetadata({
|
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'),
|
category: t('Evolution'),
|
||||||
credits: ['https://echarts.apache.org'],
|
credits: ['https://echarts.apache.org'],
|
||||||
description: hasGenericChartAxes
|
description: hasGenericChartAxes
|
||||||
|
@ -47,7 +47,11 @@ export default class EchartsPieChartPlugin extends ChartPlugin<
|
|||||||
controlPanel,
|
controlPanel,
|
||||||
loadChart: () => import('./EchartsPie'),
|
loadChart: () => import('./EchartsPie'),
|
||||||
metadata: new ChartMetadata({
|
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'),
|
category: t('Part of a Whole'),
|
||||||
credits: ['https://echarts.apache.org'],
|
credits: ['https://echarts.apache.org'],
|
||||||
description:
|
description:
|
||||||
|
@ -46,7 +46,11 @@ export default class EchartsRadarChartPlugin extends ChartPlugin<
|
|||||||
controlPanel,
|
controlPanel,
|
||||||
loadChart: () => import('./EchartsRadar'),
|
loadChart: () => import('./EchartsRadar'),
|
||||||
metadata: new ChartMetadata({
|
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'),
|
category: t('Ranking'),
|
||||||
credits: ['https://echarts.apache.org'],
|
credits: ['https://echarts.apache.org'],
|
||||||
description: t(
|
description: t(
|
||||||
|
@ -40,7 +40,6 @@ export default function EchartsSunburst(props: SunburstTransformedProps) {
|
|||||||
refs,
|
refs,
|
||||||
emitCrossFilters,
|
emitCrossFilters,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const { columns } = formData;
|
const { columns } = formData;
|
||||||
|
|
||||||
const getCrossFilterDataMask = useCallback(
|
const getCrossFilterDataMask = useCallback(
|
||||||
@ -62,7 +61,7 @@ export default function EchartsSunburst(props: SunburstTransformedProps) {
|
|||||||
filters:
|
filters:
|
||||||
values.length === 0 || !columns
|
values.length === 0 || !columns
|
||||||
? []
|
? []
|
||||||
: columns.map((col, idx) => {
|
: columns.slice(0, treePath.length).map((col, idx) => {
|
||||||
const val = labels.map(v => v[idx]);
|
const val = labels.map(v => v[idx]);
|
||||||
if (val === null || val === undefined)
|
if (val === null || val === undefined)
|
||||||
return {
|
return {
|
||||||
@ -111,6 +110,7 @@ export default function EchartsSunburst(props: SunburstTransformedProps) {
|
|||||||
const treePath = extractTreePathInfo(eventParams.treePathInfo);
|
const treePath = extractTreePathInfo(eventParams.treePathInfo);
|
||||||
const pointerEvent = eventParams.event.event;
|
const pointerEvent = eventParams.event.event;
|
||||||
const drillToDetailFilters: BinaryQueryObjectFilterClause[] = [];
|
const drillToDetailFilters: BinaryQueryObjectFilterClause[] = [];
|
||||||
|
const drillByFilters: BinaryQueryObjectFilterClause[] = [];
|
||||||
if (columns?.length) {
|
if (columns?.length) {
|
||||||
treePath.forEach((path, i) =>
|
treePath.forEach((path, i) =>
|
||||||
drillToDetailFilters.push({
|
drillToDetailFilters.push({
|
||||||
@ -120,10 +120,16 @@ export default function EchartsSunburst(props: SunburstTransformedProps) {
|
|||||||
formattedVal: path,
|
formattedVal: path,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
drillByFilters.push({
|
||||||
|
col: columns[treePath.length - 1],
|
||||||
|
op: '==',
|
||||||
|
val: treePath[treePath.length - 1],
|
||||||
|
});
|
||||||
}
|
}
|
||||||
onContextMenu(pointerEvent.clientX, pointerEvent.clientY, {
|
onContextMenu(pointerEvent.clientX, pointerEvent.clientY, {
|
||||||
drillToDetail: drillToDetailFilters,
|
drillToDetail: drillToDetailFilters,
|
||||||
crossFilter: getCrossFilterDataMask(treePathInfo),
|
crossFilter: getCrossFilterDataMask(treePathInfo),
|
||||||
|
drillBy: { filters: drillByFilters, groupbyFieldName: 'columns' },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -16,7 +16,7 @@
|
|||||||
* specific language governing permissions and limitations
|
* specific language governing permissions and limitations
|
||||||
* under the License.
|
* 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 transformProps from './transformProps';
|
||||||
import thumbnail from './images/thumbnail.png';
|
import thumbnail from './images/thumbnail.png';
|
||||||
import controlPanel from './controlPanel';
|
import controlPanel from './controlPanel';
|
||||||
@ -31,7 +31,11 @@ export default class EchartsSunburstChartPlugin extends ChartPlugin {
|
|||||||
controlPanel,
|
controlPanel,
|
||||||
loadChart: () => import('./EchartsSunburst'),
|
loadChart: () => import('./EchartsSunburst'),
|
||||||
metadata: new ChartMetadata({
|
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'),
|
category: t('Part of a Whole'),
|
||||||
credits: ['https://echarts.apache.org'],
|
credits: ['https://echarts.apache.org'],
|
||||||
description: t(
|
description: t(
|
||||||
|
@ -50,7 +50,11 @@ export default class EchartsAreaChartPlugin extends ChartPlugin<
|
|||||||
controlPanel,
|
controlPanel,
|
||||||
loadChart: () => import('../EchartsTimeseries'),
|
loadChart: () => import('../EchartsTimeseries'),
|
||||||
metadata: new ChartMetadata({
|
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'),
|
category: t('Evolution'),
|
||||||
credits: ['https://echarts.apache.org'],
|
credits: ['https://echarts.apache.org'],
|
||||||
description: hasGenericChartAxes
|
description: hasGenericChartAxes
|
||||||
|
@ -201,40 +201,48 @@ export default function EchartsTimeseries({
|
|||||||
eventParams.event.stop();
|
eventParams.event.stop();
|
||||||
const { data, seriesName } = eventParams;
|
const { data, seriesName } = eventParams;
|
||||||
const drillToDetailFilters: BinaryQueryObjectFilterClause[] = [];
|
const drillToDetailFilters: BinaryQueryObjectFilterClause[] = [];
|
||||||
|
const drillByFilters: BinaryQueryObjectFilterClause[] = [];
|
||||||
const pointerEvent = eventParams.event.event;
|
const pointerEvent = eventParams.event.event;
|
||||||
const values = [
|
const values = [
|
||||||
...(eventParams.name ? [eventParams.name] : []),
|
...(eventParams.name ? [eventParams.name] : []),
|
||||||
...labelMap[eventParams.seriesName],
|
...labelMap[seriesName],
|
||||||
];
|
];
|
||||||
if (data) {
|
if (data && xAxis.type === AxisType.time) {
|
||||||
if (xAxis.type === AxisType.time) {
|
drillToDetailFilters.push({
|
||||||
drillToDetailFilters.push({
|
col:
|
||||||
col:
|
// if the xAxis is '__timestamp', granularity_sqla will be the column of filter
|
||||||
// if the xAxis is '__timestamp', granularity_sqla will be the column of filter
|
xAxis.label === DTTM_ALIAS
|
||||||
xAxis.label === DTTM_ALIAS
|
? formData.granularitySqla
|
||||||
? formData.granularitySqla
|
: xAxis.label,
|
||||||
: xAxis.label,
|
grain: formData.timeGrainSqla,
|
||||||
grain: formData.timeGrainSqla,
|
op: '==',
|
||||||
op: '==',
|
val: data[0],
|
||||||
val: data[0],
|
formattedVal: xValueFormatter(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]),
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
[
|
||||||
|
...(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, {
|
onContextMenu(pointerEvent.clientX, pointerEvent.clientY, {
|
||||||
drillToDetail: drillToDetailFilters,
|
drillToDetail: drillToDetailFilters,
|
||||||
crossFilter: getCrossFilterDataMask(seriesName),
|
crossFilter: getCrossFilterDataMask(seriesName),
|
||||||
|
drillBy: { filters: drillByFilters, groupbyFieldName: 'groupby' },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -56,7 +56,11 @@ export default class EchartsTimeseriesBarChartPlugin extends ChartPlugin<
|
|||||||
controlPanel,
|
controlPanel,
|
||||||
loadChart: () => import('../../EchartsTimeseries'),
|
loadChart: () => import('../../EchartsTimeseries'),
|
||||||
metadata: new ChartMetadata({
|
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'),
|
category: t('Evolution'),
|
||||||
credits: ['https://echarts.apache.org'],
|
credits: ['https://echarts.apache.org'],
|
||||||
description: hasGenericChartAxes
|
description: hasGenericChartAxes
|
||||||
|
@ -55,7 +55,11 @@ export default class EchartsTimeseriesLineChartPlugin extends ChartPlugin<
|
|||||||
controlPanel,
|
controlPanel,
|
||||||
loadChart: () => import('../../EchartsTimeseries'),
|
loadChart: () => import('../../EchartsTimeseries'),
|
||||||
metadata: new ChartMetadata({
|
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'),
|
category: t('Evolution'),
|
||||||
credits: ['https://echarts.apache.org'],
|
credits: ['https://echarts.apache.org'],
|
||||||
description: hasGenericChartAxes
|
description: hasGenericChartAxes
|
||||||
|
@ -54,7 +54,11 @@ export default class EchartsTimeseriesScatterChartPlugin extends ChartPlugin<
|
|||||||
controlPanel,
|
controlPanel,
|
||||||
loadChart: () => import('../../EchartsTimeseries'),
|
loadChart: () => import('../../EchartsTimeseries'),
|
||||||
metadata: new ChartMetadata({
|
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'),
|
category: t('Evolution'),
|
||||||
credits: ['https://echarts.apache.org'],
|
credits: ['https://echarts.apache.org'],
|
||||||
description: hasGenericChartAxes
|
description: hasGenericChartAxes
|
||||||
|
@ -54,7 +54,11 @@ export default class EchartsTimeseriesSmoothLineChartPlugin extends ChartPlugin<
|
|||||||
controlPanel,
|
controlPanel,
|
||||||
loadChart: () => import('../../EchartsTimeseries'),
|
loadChart: () => import('../../EchartsTimeseries'),
|
||||||
metadata: new ChartMetadata({
|
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'),
|
category: t('Evolution'),
|
||||||
credits: ['https://echarts.apache.org'],
|
credits: ['https://echarts.apache.org'],
|
||||||
description: hasGenericChartAxes
|
description: hasGenericChartAxes
|
||||||
|
@ -45,7 +45,11 @@ export default class EchartsTimeseriesStepChartPlugin extends ChartPlugin<
|
|||||||
controlPanel,
|
controlPanel,
|
||||||
loadChart: () => import('../EchartsTimeseries'),
|
loadChart: () => import('../EchartsTimeseries'),
|
||||||
metadata: new ChartMetadata({
|
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'),
|
category: t('Evolution'),
|
||||||
credits: ['https://echarts.apache.org'],
|
credits: ['https://echarts.apache.org'],
|
||||||
description: hasGenericChartAxes
|
description: hasGenericChartAxes
|
||||||
|
@ -44,7 +44,11 @@ export default class EchartsTimeseriesChartPlugin extends ChartPlugin<
|
|||||||
controlPanel,
|
controlPanel,
|
||||||
loadChart: () => import('./EchartsTimeseries'),
|
loadChart: () => import('./EchartsTimeseries'),
|
||||||
metadata: new ChartMetadata({
|
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'),
|
category: t('Evolution'),
|
||||||
credits: ['https://echarts.apache.org'],
|
credits: ['https://echarts.apache.org'],
|
||||||
description: hasGenericChartAxes
|
description: hasGenericChartAxes
|
||||||
|
@ -116,17 +116,25 @@ export default function EchartsTreemap({
|
|||||||
if (treePath.length > 0) {
|
if (treePath.length > 0) {
|
||||||
const pointerEvent = eventParams.event.event;
|
const pointerEvent = eventParams.event.event;
|
||||||
const drillToDetailFilters: BinaryQueryObjectFilterClause[] = [];
|
const drillToDetailFilters: BinaryQueryObjectFilterClause[] = [];
|
||||||
treePath.forEach((path, i) =>
|
const drillByFilters: BinaryQueryObjectFilterClause[] = [];
|
||||||
|
treePath.forEach((path, i) => {
|
||||||
|
const val = path === 'null' ? NULL_STRING : path;
|
||||||
drillToDetailFilters.push({
|
drillToDetailFilters.push({
|
||||||
col: groupby[i],
|
col: groupby[i],
|
||||||
op: '==',
|
op: '==',
|
||||||
val: path === 'null' ? NULL_STRING : path,
|
val,
|
||||||
formattedVal: path,
|
formattedVal: path,
|
||||||
}),
|
});
|
||||||
);
|
drillByFilters.push({
|
||||||
|
col: groupby[i],
|
||||||
|
op: '==',
|
||||||
|
val,
|
||||||
|
});
|
||||||
|
});
|
||||||
onContextMenu(pointerEvent.clientX, pointerEvent.clientY, {
|
onContextMenu(pointerEvent.clientX, pointerEvent.clientY, {
|
||||||
drillToDetail: drillToDetailFilters,
|
drillToDetail: drillToDetailFilters,
|
||||||
crossFilter: getCrossFilterDataMask(data, treePathInfo),
|
crossFilter: getCrossFilterDataMask(data, treePathInfo),
|
||||||
|
drillBy: { filters: drillByFilters, groupbyFieldName: 'groupby' },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -46,7 +46,11 @@ export default class EchartsTreemapChartPlugin extends ChartPlugin<
|
|||||||
controlPanel,
|
controlPanel,
|
||||||
loadChart: () => import('./EchartsTreemap'),
|
loadChart: () => import('./EchartsTreemap'),
|
||||||
metadata: new ChartMetadata({
|
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'),
|
category: t('Part of a Whole'),
|
||||||
credits: ['https://echarts.apache.org'],
|
credits: ['https://echarts.apache.org'],
|
||||||
description: t(
|
description: t(
|
||||||
|
@ -111,11 +111,11 @@ export const contextMenuEventHandler =
|
|||||||
if (onContextMenu) {
|
if (onContextMenu) {
|
||||||
e.event.stop();
|
e.event.stop();
|
||||||
const pointerEvent = e.event.event;
|
const pointerEvent = e.event.event;
|
||||||
const drillToDetailFilters: BinaryQueryObjectFilterClause[] = [];
|
const drillFilters: BinaryQueryObjectFilterClause[] = [];
|
||||||
if (groupby.length > 0) {
|
if (groupby.length > 0) {
|
||||||
const values = labelMap[e.name];
|
const values = labelMap[e.name];
|
||||||
groupby.forEach((dimension, i) =>
|
groupby.forEach((dimension, i) =>
|
||||||
drillToDetailFilters.push({
|
drillFilters.push({
|
||||||
col: dimension,
|
col: dimension,
|
||||||
op: '==',
|
op: '==',
|
||||||
val: values[i],
|
val: values[i],
|
||||||
@ -124,8 +124,9 @@ export const contextMenuEventHandler =
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
onContextMenu(pointerEvent.clientX, pointerEvent.clientY, {
|
onContextMenu(pointerEvent.clientX, pointerEvent.clientY, {
|
||||||
drillToDetail: drillToDetailFilters,
|
drillToDetail: drillFilters,
|
||||||
crossFilter: getCrossFilterDataMask(e.name),
|
crossFilter: getCrossFilterDataMask(e.name),
|
||||||
|
drillBy: { filters: drillFilters, groupbyFieldName: 'groupby' },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -478,10 +478,27 @@ export default function PivotTableChart(props: PivotTableProps) {
|
|||||||
onContextMenu(e.clientX, e.clientY, {
|
onContextMenu(e.clientX, e.clientY, {
|
||||||
drillToDetail: drillToDetailFilters,
|
drillToDetail: drillToDetailFilters,
|
||||||
crossFilter: getCrossFilterDataMask(dataPoint),
|
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 (
|
return (
|
||||||
|
@ -17,12 +17,12 @@
|
|||||||
* under the License.
|
* under the License.
|
||||||
*/
|
*/
|
||||||
import {
|
import {
|
||||||
t,
|
Behavior,
|
||||||
ChartMetadata,
|
ChartMetadata,
|
||||||
ChartPlugin,
|
ChartPlugin,
|
||||||
Behavior,
|
|
||||||
ChartProps,
|
ChartProps,
|
||||||
QueryFormData,
|
QueryFormData,
|
||||||
|
t,
|
||||||
} from '@superset-ui/core';
|
} from '@superset-ui/core';
|
||||||
import buildQuery from './buildQuery';
|
import buildQuery from './buildQuery';
|
||||||
import controlPanel from './controlPanel';
|
import controlPanel from './controlPanel';
|
||||||
@ -47,7 +47,11 @@ export default class PivotTableChartPlugin extends ChartPlugin<
|
|||||||
*/
|
*/
|
||||||
constructor() {
|
constructor() {
|
||||||
const metadata = new ChartMetadata({
|
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'),
|
category: t('Table'),
|
||||||
description: t(
|
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.',
|
'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.',
|
||||||
|
@ -391,6 +391,18 @@ export default function TableChart<D extends DataRecord = DataRecord>(
|
|||||||
crossFilter: cellPoint.isMetric
|
crossFilter: cellPoint.isMetric
|
||||||
? undefined
|
? undefined
|
||||||
: getCrossFilterDataMask(cellPoint.key, cellPoint.value),
|
: getCrossFilterDataMask(cellPoint.key, cellPoint.value),
|
||||||
|
drillBy: cellPoint.isMetric
|
||||||
|
? undefined
|
||||||
|
: {
|
||||||
|
filters: [
|
||||||
|
{
|
||||||
|
col: cellPoint.key,
|
||||||
|
op: '==',
|
||||||
|
val: cellPoint.value as string | number | boolean,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
groupbyFieldName: 'groupby',
|
||||||
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
: undefined;
|
: undefined;
|
||||||
|
@ -16,7 +16,7 @@
|
|||||||
* specific language governing permissions and limitations
|
* specific language governing permissions and limitations
|
||||||
* under the License.
|
* 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 transformProps from './transformProps';
|
||||||
import thumbnail from './images/thumbnail.png';
|
import thumbnail from './images/thumbnail.png';
|
||||||
import example1 from './images/Table.jpg';
|
import example1 from './images/Table.jpg';
|
||||||
@ -31,7 +31,11 @@ export { default as __hack__ } from './types';
|
|||||||
export * from './types';
|
export * from './types';
|
||||||
|
|
||||||
const metadata = new ChartMetadata({
|
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'),
|
category: t('Table'),
|
||||||
canBeAnnotationTypes: ['EVENT', 'INTERVAL'],
|
canBeAnnotationTypes: ['EVENT', 'INTERVAL'],
|
||||||
description: t(
|
description: t(
|
||||||
|
@ -44,6 +44,7 @@ import { DrillDetailMenuItems } from './DrillDetail';
|
|||||||
import { getMenuAdjustedY } from './utils';
|
import { getMenuAdjustedY } from './utils';
|
||||||
import { updateDataMask } from '../../dataMask/actions';
|
import { updateDataMask } from '../../dataMask/actions';
|
||||||
import { MenuItemTooltip } from './DisabledMenuItemTooltip';
|
import { MenuItemTooltip } from './DisabledMenuItemTooltip';
|
||||||
|
import { DrillByMenuItems } from './DrillBy/DrillByMenuItems';
|
||||||
|
|
||||||
export interface ChartContextMenuProps {
|
export interface ChartContextMenuProps {
|
||||||
id: number;
|
id: number;
|
||||||
@ -84,17 +85,25 @@ const ChartContextMenu = (
|
|||||||
const showDrillToDetail =
|
const showDrillToDetail =
|
||||||
isFeatureEnabled(FeatureFlag.DRILL_TO_DETAIL) && canExplore;
|
isFeatureEnabled(FeatureFlag.DRILL_TO_DETAIL) && canExplore;
|
||||||
|
|
||||||
|
const showDrillBy = isFeatureEnabled(FeatureFlag.DRILL_BY) && canExplore;
|
||||||
|
|
||||||
|
const showCrossFilters = isFeatureEnabled(
|
||||||
|
FeatureFlag.DASHBOARD_CROSS_FILTERS,
|
||||||
|
);
|
||||||
const isCrossFilteringSupportedByChart = getChartMetadataRegistry()
|
const isCrossFilteringSupportedByChart = getChartMetadataRegistry()
|
||||||
.get(formData.viz_type)
|
.get(formData.viz_type)
|
||||||
?.behaviors?.includes(Behavior.INTERACTIVE_CHART);
|
?.behaviors?.includes(Behavior.INTERACTIVE_CHART);
|
||||||
|
|
||||||
let itemsCount = 0;
|
let itemsCount = 0;
|
||||||
if (isFeatureEnabled(FeatureFlag.DASHBOARD_CROSS_FILTERS)) {
|
if (showCrossFilters) {
|
||||||
itemsCount += 1;
|
itemsCount += 1;
|
||||||
}
|
}
|
||||||
if (showDrillToDetail) {
|
if (showDrillToDetail) {
|
||||||
itemsCount += 2; // Drill to detail always has 2 top-level menu items
|
itemsCount += 2; // Drill to detail always has 2 top-level menu items
|
||||||
}
|
}
|
||||||
|
if (showDrillBy) {
|
||||||
|
itemsCount += 1;
|
||||||
|
}
|
||||||
if (itemsCount === 0) {
|
if (itemsCount === 0) {
|
||||||
itemsCount = 1; // "No actions" appears if no actions in menu
|
itemsCount = 1; // "No actions" appears if no actions in menu
|
||||||
}
|
}
|
||||||
@ -180,6 +189,25 @@ const ChartContextMenu = (
|
|||||||
isContextMenu
|
isContextMenu
|
||||||
contextMenuY={clientY}
|
contextMenuY={clientY}
|
||||||
onSelection={onSelection}
|
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}
|
||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
@ -31,10 +31,10 @@ import {
|
|||||||
} from '@superset-ui/core';
|
} from '@superset-ui/core';
|
||||||
import { Menu } from 'src/components/Menu';
|
import { Menu } from 'src/components/Menu';
|
||||||
import DrillDetailModal from './DrillDetailModal';
|
import DrillDetailModal from './DrillDetailModal';
|
||||||
import { getMenuAdjustedY, MENU_ITEM_HEIGHT } from '../utils';
|
import { getSubmenuYOffset } from '../utils';
|
||||||
import { MenuItemTooltip } from '../DisabledMenuItemTooltip';
|
import { MenuItemTooltip } from '../DisabledMenuItemTooltip';
|
||||||
|
import { MenuItemWithTruncation } from '../MenuItemWithTruncation';
|
||||||
|
|
||||||
const MENU_PADDING = 4;
|
|
||||||
const DRILL_TO_DETAIL_TEXT = t('Drill to detail by');
|
const DRILL_TO_DETAIL_TEXT = t('Drill to detail by');
|
||||||
|
|
||||||
const DisabledMenuItem = ({ children, ...props }: { children: ReactNode }) => (
|
const DisabledMenuItem = ({ children, ...props }: { children: ReactNode }) => (
|
||||||
@ -65,6 +65,7 @@ export type DrillDetailMenuItemsProps = {
|
|||||||
contextMenuY?: number;
|
contextMenuY?: number;
|
||||||
onSelection?: () => void;
|
onSelection?: () => void;
|
||||||
onClick?: (event: MouseEvent) => void;
|
onClick?: (event: MouseEvent) => void;
|
||||||
|
submenuIndex?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
const DrillDetailMenuItems = ({
|
const DrillDetailMenuItems = ({
|
||||||
@ -75,6 +76,7 @@ const DrillDetailMenuItems = ({
|
|||||||
contextMenuY = 0,
|
contextMenuY = 0,
|
||||||
onSelection = () => null,
|
onSelection = () => null,
|
||||||
onClick = () => null,
|
onClick = () => null,
|
||||||
|
submenuIndex = 0,
|
||||||
...props
|
...props
|
||||||
}: DrillDetailMenuItemsProps) => {
|
}: DrillDetailMenuItemsProps) => {
|
||||||
const [modalFilters, setFilters] = useState<BinaryQueryObjectFilterClause[]>(
|
const [modalFilters, setFilters] = useState<BinaryQueryObjectFilterClause[]>(
|
||||||
@ -162,31 +164,35 @@ const DrillDetailMenuItems = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Ensure submenu doesn't appear offscreen
|
// Ensure submenu doesn't appear offscreen
|
||||||
const submenuYOffset = useMemo(() => {
|
const submenuYOffset = useMemo(
|
||||||
const itemsCount = filters.length > 1 ? filters.length + 1 : filters.length;
|
() =>
|
||||||
const submenuY =
|
getSubmenuYOffset(
|
||||||
contextMenuY + MENU_PADDING + MENU_ITEM_HEIGHT + MENU_PADDING;
|
contextMenuY,
|
||||||
|
filters.length > 1 ? filters.length + 1 : filters.length,
|
||||||
return getMenuAdjustedY(submenuY, itemsCount) - submenuY;
|
submenuIndex,
|
||||||
}, [contextMenuY, filters.length]);
|
),
|
||||||
|
[contextMenuY, filters.length, submenuIndex],
|
||||||
|
);
|
||||||
|
|
||||||
if (handlesDimensionContextMenu && !noAggregations && filters?.length) {
|
if (handlesDimensionContextMenu && !noAggregations && filters?.length) {
|
||||||
drillToDetailByMenuItem = (
|
drillToDetailByMenuItem = (
|
||||||
<Menu.SubMenu
|
<Menu.SubMenu
|
||||||
{...props}
|
{...props}
|
||||||
popupOffset={[0, submenuYOffset]}
|
popupOffset={[0, submenuYOffset]}
|
||||||
|
popupClassName="chart-context-submenu"
|
||||||
title={DRILL_TO_DETAIL_TEXT}
|
title={DRILL_TO_DETAIL_TEXT}
|
||||||
>
|
>
|
||||||
<div data-test="drill-to-detail-by-submenu">
|
<div data-test="drill-to-detail-by-submenu">
|
||||||
{filters.map((filter, i) => (
|
{filters.map((filter, i) => (
|
||||||
<Menu.Item
|
<MenuItemWithTruncation
|
||||||
{...props}
|
{...props}
|
||||||
|
tooltipText={`${DRILL_TO_DETAIL_TEXT} ${filter.formattedVal}`}
|
||||||
key={`drill-detail-filter-${i}`}
|
key={`drill-detail-filter-${i}`}
|
||||||
onClick={openModal.bind(null, [filter])}
|
onClick={openModal.bind(null, [filter])}
|
||||||
>
|
>
|
||||||
{`${DRILL_TO_DETAIL_TEXT} `}
|
{`${DRILL_TO_DETAIL_TEXT} `}
|
||||||
<Filter>{filter.formattedVal}</Filter>
|
<Filter>{filter.formattedVal}</Filter>
|
||||||
</Menu.Item>
|
</MenuItemWithTruncation>
|
||||||
))}
|
))}
|
||||||
{filters.length > 1 && (
|
{filters.length > 1 && (
|
||||||
<Menu.Item
|
<Menu.Item
|
||||||
@ -194,8 +200,10 @@ const DrillDetailMenuItems = ({
|
|||||||
key="drill-detail-filter-all"
|
key="drill-detail-filter-all"
|
||||||
onClick={openModal.bind(null, filters)}
|
onClick={openModal.bind(null, filters)}
|
||||||
>
|
>
|
||||||
{`${DRILL_TO_DETAIL_TEXT} `}
|
<div>
|
||||||
<Filter>{t('all')}</Filter>
|
{`${DRILL_TO_DETAIL_TEXT} `}
|
||||||
|
<Filter>{t('all')}</Filter>
|
||||||
|
</div>
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
@ -39,4 +39,7 @@ test('correctly positions at lower edge of screen', () => {
|
|||||||
expect(getMenuAdjustedY(425, 1)).toEqual(425); // No adjustment
|
expect(getMenuAdjustedY(425, 1)).toEqual(425); // No adjustment
|
||||||
expect(getMenuAdjustedY(425, 2)).toEqual(404); // Adjustment
|
expect(getMenuAdjustedY(425, 2)).toEqual(404); // Adjustment
|
||||||
expect(getMenuAdjustedY(425, 3)).toEqual(372); // Adjustment
|
expect(getMenuAdjustedY(425, 3)).toEqual(372); // Adjustment
|
||||||
|
|
||||||
|
expect(getMenuAdjustedY(425, 8, 200)).toEqual(268);
|
||||||
|
expect(getMenuAdjustedY(425, 8, 200, 48)).toEqual(220);
|
||||||
});
|
});
|
||||||
|
@ -18,6 +18,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
export const MENU_ITEM_HEIGHT = 32;
|
export const MENU_ITEM_HEIGHT = 32;
|
||||||
|
const MENU_PADDING = 4;
|
||||||
const MENU_VERTICAL_SPACING = 32;
|
const MENU_VERTICAL_SPACING = 32;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -27,14 +28,45 @@ const MENU_VERTICAL_SPACING = 32;
|
|||||||
* @param clientY The original Y-offset
|
* @param clientY The original Y-offset
|
||||||
* @param itemsCount The number of menu items
|
* @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
|
// Viewport height
|
||||||
const vh = Math.max(
|
const vh = Math.max(
|
||||||
document.documentElement.clientHeight || 0,
|
document.documentElement.clientHeight || 0,
|
||||||
window.innerHeight || 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
|
// Always show the context menu inside the viewport
|
||||||
return vh - clientY < menuHeight ? vh - menuHeight : clientY;
|
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
|
||||||
|
);
|
||||||
|
};
|
||||||
|
@ -22,8 +22,8 @@ import { Column, ensureIsArray, t, useChangeEffect } from '@superset-ui/core';
|
|||||||
import { Select, FormInstance } from 'src/components';
|
import { Select, FormInstance } from 'src/components';
|
||||||
import { useToasts } from 'src/components/MessageToasts/withToasts';
|
import { useToasts } from 'src/components/MessageToasts/withToasts';
|
||||||
import { getClientErrorObject } from 'src/utils/getClientErrorObject';
|
import { getClientErrorObject } from 'src/utils/getClientErrorObject';
|
||||||
|
import { cachedSupersetGet } from 'src/utils/cachedSupersetGet';
|
||||||
import { NativeFiltersForm } from '../types';
|
import { NativeFiltersForm } from '../types';
|
||||||
import { cachedSupersetGet } from './utils';
|
|
||||||
|
|
||||||
interface ColumnSelectProps {
|
interface ColumnSelectProps {
|
||||||
allowClear?: boolean;
|
allowClear?: boolean;
|
||||||
|
@ -24,7 +24,8 @@ import {
|
|||||||
ClientErrorObject,
|
ClientErrorObject,
|
||||||
getClientErrorObject,
|
getClientErrorObject,
|
||||||
} from 'src/utils/getClientErrorObject';
|
} from 'src/utils/getClientErrorObject';
|
||||||
import { cachedSupersetGet, datasetToSelectOption } from './utils';
|
import { cachedSupersetGet } from 'src/utils/cachedSupersetGet';
|
||||||
|
import { datasetToSelectOption } from './utils';
|
||||||
|
|
||||||
interface DatasetSelectProps {
|
interface DatasetSelectProps {
|
||||||
onChange: (value: { label: string; value: number }) => void;
|
onChange: (value: { label: string; value: number }) => void;
|
||||||
|
@ -60,6 +60,7 @@ import { addDangerToast } from 'src/components/MessageToasts/actions';
|
|||||||
import { Radio } from 'src/components/Radio';
|
import { Radio } from 'src/components/Radio';
|
||||||
import Tabs from 'src/components/Tabs';
|
import Tabs from 'src/components/Tabs';
|
||||||
import { Tooltip } from 'src/components/Tooltip';
|
import { Tooltip } from 'src/components/Tooltip';
|
||||||
|
import { cachedSupersetGet } from 'src/utils/cachedSupersetGet';
|
||||||
import {
|
import {
|
||||||
Chart,
|
Chart,
|
||||||
ChartsState,
|
ChartsState,
|
||||||
@ -90,7 +91,6 @@ import getControlItemsMap from './getControlItemsMap';
|
|||||||
import RemovedFilter from './RemovedFilter';
|
import RemovedFilter from './RemovedFilter';
|
||||||
import { useBackendFormUpdate, useDefaultValue } from './state';
|
import { useBackendFormUpdate, useDefaultValue } from './state';
|
||||||
import {
|
import {
|
||||||
cachedSupersetGet,
|
|
||||||
hasTemporalColumns,
|
hasTemporalColumns,
|
||||||
mostUsedDataset,
|
mostUsedDataset,
|
||||||
setNativeFilterFieldValues,
|
setNativeFilterFieldValues,
|
||||||
|
@ -20,14 +20,8 @@ import { flatMapDeep } from 'lodash';
|
|||||||
import { FormInstance } from 'src/components';
|
import { FormInstance } from 'src/components';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { CustomControlItem, Dataset } from '@superset-ui/chart-controls';
|
import { CustomControlItem, Dataset } from '@superset-ui/chart-controls';
|
||||||
import {
|
import { Column, ensureIsArray, GenericDataType } from '@superset-ui/core';
|
||||||
Column,
|
|
||||||
ensureIsArray,
|
|
||||||
GenericDataType,
|
|
||||||
SupersetClient,
|
|
||||||
} from '@superset-ui/core';
|
|
||||||
import { DatasourcesState, ChartsState } from 'src/dashboard/types';
|
import { DatasourcesState, ChartsState } from 'src/dashboard/types';
|
||||||
import { cacheWrapper } from 'src/utils/cacheWrapper';
|
|
||||||
import { FILTER_SUPPORTED_TYPES } from './constants';
|
import { FILTER_SUPPORTED_TYPES } from './constants';
|
||||||
|
|
||||||
const FILTERS_FIELD_NAME = 'filters';
|
const FILTERS_FIELD_NAME = 'filters';
|
||||||
@ -124,11 +118,3 @@ export const mostUsedDataset = (
|
|||||||
|
|
||||||
return datasets[mostUsedDataset]?.id;
|
return datasets[mostUsedDataset]?.id;
|
||||||
};
|
};
|
||||||
|
|
||||||
const localCache = new Map<string, any>();
|
|
||||||
|
|
||||||
export const cachedSupersetGet = cacheWrapper(
|
|
||||||
SupersetClient.get,
|
|
||||||
localCache,
|
|
||||||
({ endpoint }) => endpoint || '',
|
|
||||||
);
|
|
||||||
|
@ -59,7 +59,11 @@ import { DashboardContextForExplore } from 'src/types/DashboardContextForExplore
|
|||||||
import shortid from 'shortid';
|
import shortid from 'shortid';
|
||||||
import { RootState } from '../types';
|
import { RootState } from '../types';
|
||||||
import { getActiveFilters } from '../util/activeDashboardFilters';
|
import { getActiveFilters } from '../util/activeDashboardFilters';
|
||||||
import { filterCardPopoverStyle, headerStyles } from '../styles';
|
import {
|
||||||
|
chartContextMenuStyles,
|
||||||
|
filterCardPopoverStyle,
|
||||||
|
headerStyles,
|
||||||
|
} from '../styles';
|
||||||
|
|
||||||
export const DashboardPageIdContext = React.createContext('');
|
export const DashboardPageIdContext = React.createContext('');
|
||||||
|
|
||||||
@ -279,7 +283,13 @@ export const DashboardPage: FC<PageProps> = ({ idOrSlug }: PageProps) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Global styles={[filterCardPopoverStyle(theme), headerStyles(theme)]} />
|
<Global
|
||||||
|
styles={[
|
||||||
|
filterCardPopoverStyle(theme),
|
||||||
|
headerStyles(theme),
|
||||||
|
chartContextMenuStyles(theme),
|
||||||
|
]}
|
||||||
|
/>
|
||||||
<DashboardPageIdContext.Provider value={dashboardPageId}>
|
<DashboardPageIdContext.Provider value={dashboardPageId}>
|
||||||
<DashboardContainer />
|
<DashboardContainer />
|
||||||
</DashboardPageIdContext.Provider>
|
</DashboardPageIdContext.Provider>
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
29
superset-frontend/src/utils/cachedSupersetGet.ts
Normal file
29
superset-frontend/src/utils/cachedSupersetGet.ts
Normal 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 || '',
|
||||||
|
);
|
Loading…
Reference in New Issue
Block a user