feat(dashboard): Add cross filter from context menu (#23141)

This commit is contained in:
Kamil Gabryjelski
2023-02-23 17:05:41 +01:00
committed by GitHub
parent 95eb8d79d0
commit ee1952e488
26 changed files with 896 additions and 753 deletions

View File

@@ -220,8 +220,8 @@ class BigNumberVis extends React.PureComponent<BigNumberVizProps> {
const { data } = eventParams;
if (data) {
const pointerEvent = eventParams.event.event;
const filters: BinaryQueryObjectFilterClause[] = [];
filters.push({
const drillToDetailFilters: BinaryQueryObjectFilterClause[] = [];
drillToDetailFilters.push({
col: this.props.formData?.granularitySqla,
grain: this.props.formData?.timeGrainSqla,
op: '==',
@@ -231,7 +231,7 @@ class BigNumberVis extends React.PureComponent<BigNumberVizProps> {
this.props.onContextMenu(
pointerEvent.clientX,
pointerEvent.clientY,
filters,
{ drillToDetail: drillToDetailFilters },
);
}
}

View File

@@ -19,8 +19,8 @@
import { EChartsCoreOption } from 'echarts';
import {
BinaryQueryObjectFilterClause,
ChartDataResponseResult,
ContextMenuFilters,
DataRecordValue,
NumberFormatter,
QueryFormData,
@@ -89,7 +89,7 @@ export type BigNumberVizProps = {
onContextMenu?: (
clientX: number,
clientY: number,
filters?: BinaryQueryObjectFilterClause[],
filters?: ContextMenuFilters,
) => void;
xValueFormatter?: TimeFormatter;
formData?: BigNumberWithTrendlineFormData;

View File

@@ -16,60 +16,15 @@
* specific language governing permissions and limitations
* under the License.
*/
import React, { useCallback } from 'react';
import React from 'react';
import Echart from '../components/Echart';
import { allEventHandlers } from '../utils/eventHandlers';
import { BoxPlotChartTransformedProps } from './types';
export default function EchartsBoxPlot(props: BoxPlotChartTransformedProps) {
const {
height,
width,
echartOptions,
setDataMask,
labelMap,
groupby,
selectedValues,
refs,
emitCrossFilters,
} = props;
const handleChange = useCallback(
(values: string[]) => {
if (!emitCrossFilters) {
return;
}
const { height, width, echartOptions, selectedValues, refs } = props;
const groupbyValues = values.map(value => labelMap[value]);
setDataMask({
extraFormData: {
filters:
values.length === 0
? []
: groupby.map((col, idx) => {
const val = groupbyValues.map(v => v[idx]);
if (val === null || val === undefined)
return {
col,
op: 'IS NULL',
};
return {
col,
op: 'IN',
val: val as (string | number | boolean)[],
};
}),
},
filterState: {
value: groupbyValues.length ? groupbyValues : null,
selectedValues: values.length ? values : null,
},
});
},
[groupby, labelMap, setDataMask, selectedValues],
);
const eventHandlers = allEventHandlers(props, handleChange);
const eventHandlers = allEventHandlers(props);
return (
<Echart

View File

@@ -16,60 +16,15 @@
* specific language governing permissions and limitations
* under the License.
*/
import React, { useCallback } from 'react';
import React from 'react';
import { FunnelChartTransformedProps } from './types';
import Echart from '../components/Echart';
import { allEventHandlers } from '../utils/eventHandlers';
export default function EchartsFunnel(props: FunnelChartTransformedProps) {
const {
height,
width,
echartOptions,
setDataMask,
labelMap,
groupby,
selectedValues,
emitCrossFilters,
refs,
} = props;
const handleChange = useCallback(
(values: string[]) => {
if (!emitCrossFilters) {
return;
}
const { height, width, echartOptions, selectedValues, refs } = props;
const groupbyValues = values.map(value => labelMap[value]);
setDataMask({
extraFormData: {
filters:
values.length === 0
? []
: groupby.map((col, idx) => {
const val = groupbyValues.map(v => v[idx]);
if (val === null || val === undefined)
return {
col,
op: 'IS NULL',
};
return {
col,
op: 'IN',
val: val as (string | number | boolean)[],
};
}),
},
filterState: {
value: groupbyValues.length ? groupbyValues : null,
selectedValues: values.length ? values : null,
},
});
},
[groupby, labelMap, setDataMask, selectedValues],
);
const eventHandlers = allEventHandlers(props, handleChange);
const eventHandlers = allEventHandlers(props);
return (
<Echart

View File

@@ -16,60 +16,15 @@
* specific language governing permissions and limitations
* under the License.
*/
import React, { useCallback } from 'react';
import React from 'react';
import { GaugeChartTransformedProps } from './types';
import Echart from '../components/Echart';
import { allEventHandlers } from '../utils/eventHandlers';
export default function EchartsGauge(props: GaugeChartTransformedProps) {
const {
height,
width,
echartOptions,
setDataMask,
labelMap,
groupby,
selectedValues,
emitCrossFilters,
refs,
} = props;
const handleChange = useCallback(
(values: string[]) => {
if (!emitCrossFilters) {
return;
}
const { height, width, echartOptions, selectedValues, refs } = props;
const groupbyValues = values.map(value => labelMap[value]);
setDataMask({
extraFormData: {
filters:
values.length === 0
? []
: groupby.map((col, idx) => {
const val = groupbyValues.map(v => v[idx]);
if (val === null || val === undefined)
return {
col,
op: 'IS NULL',
};
return {
col,
op: 'IN',
val: val as (string | number | boolean)[],
};
}),
},
filterState: {
value: groupbyValues.length ? groupbyValues : null,
selectedValues: values.length ? values : null,
},
});
},
[groupby, labelMap, setDataMask, selectedValues],
);
const eventHandlers = allEventHandlers(props, handleChange);
const eventHandlers = allEventHandlers(props);
return (
<Echart

View File

@@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
import React, { useMemo } from 'react';
import React from 'react';
import { EventHandlers } from '../types';
import Echart from '../components/Echart';
import { GraphChartTransformedProps } from './types';
@@ -36,127 +36,123 @@ type Event = {
dataType: 'node' | 'edge';
};
const EchartsGraph = React.memo(
({
height,
width,
echartOptions,
formData,
onContextMenu,
setDataMask,
filterState,
refs,
emitCrossFilters,
}: GraphChartTransformedProps) => {
const eventHandlers: EventHandlers = useMemo(
() => ({
click: (e: Event) => {
if (!emitCrossFilters || !setDataMask) {
return;
}
e.event.stop();
const data = (echartOptions as any).series[0].data as Data;
const node = data.find(item => item.id === e.data.id);
const val = filterState?.value === node?.name ? null : node?.name;
if (node?.col) {
setDataMask({
extraFormData: {
filters: val
? [
{
col: node.col,
op: '==',
val,
},
]
: [],
},
filterState: {
value: val,
selectedValues: [val],
},
});
}
export default function EchartsGraph({
height,
width,
echartOptions,
formData,
onContextMenu,
setDataMask,
filterState,
emitCrossFilters,
refs,
}: GraphChartTransformedProps) {
const getCrossFilterDataMask = (node: DataRow | undefined) => {
if (!node?.name || !node?.col) {
return undefined;
}
const { name, col } = node;
const selected = Object.values(
filterState?.selectedValues || {},
) as string[];
let values: string[];
if (selected.includes(name)) {
values = selected.filter(v => v !== name);
} else {
values = [name];
}
return {
dataMask: {
extraFormData: {
filters: values.length
? [
{
col,
op: 'IN' as const,
val: values,
},
]
: [],
},
contextmenu: (e: Event) => {
const handleNodeClick = (data: Data) => {
const node = data.find(item => item.id === e.data.id);
if (node?.name) {
return [
{
col: node.col,
op: '==' as const,
val: node.name,
formattedVal: node.name,
},
];
}
return undefined;
};
const handleEdgeClick = (data: Data) => {
const sourceValue = data.find(
item => item.id === e.data.source,
)?.name;
const targetValue = data.find(
item => item.id === e.data.target,
)?.name;
if (sourceValue && targetValue) {
return [
{
col: formData.source,
op: '==' as const,
val: sourceValue,
formattedVal: sourceValue,
},
{
col: formData.target,
op: '==' as const,
val: targetValue,
formattedVal: targetValue,
},
];
}
return undefined;
};
if (onContextMenu) {
e.event.stop();
const pointerEvent = e.event.event;
const data = (echartOptions as any).series[0].data as Data;
const filters =
e.dataType === 'node'
? handleNodeClick(data)
: handleEdgeClick(data);
if (filters) {
onContextMenu(
pointerEvent.clientX,
pointerEvent.clientY,
filters,
);
}
}
filterState: {
value: values.length ? values : null,
selectedValues: values.length ? values : null,
},
}),
[
echartOptions,
emitCrossFilters,
filterState?.value,
formData.source,
formData.target,
onContextMenu,
setDataMask,
],
);
return (
<Echart
refs={refs}
height={height}
width={width}
echartOptions={echartOptions}
eventHandlers={eventHandlers}
/>
);
},
);
export default EchartsGraph;
},
isCurrentValueSelected: selected.includes(name),
};
};
const eventHandlers: EventHandlers = {
click: (e: Event) => {
if (!emitCrossFilters || !setDataMask) {
return;
}
e.event.stop();
const data = (echartOptions as any).series[0].data as Data;
const node = data.find(item => item.id === e.data.id);
const dataMask = getCrossFilterDataMask(node)?.dataMask;
if (dataMask) {
setDataMask(dataMask);
}
},
contextmenu: (e: Event) => {
const handleNodeClick = (data: Data) => {
const node = data.find(item => item.id === e.data.id);
if (node?.name) {
return [
{
col: node.col,
op: '==' as const,
val: node.name,
formattedVal: node.name,
},
];
}
return undefined;
};
const handleEdgeClick = (data: Data) => {
const sourceValue = data.find(item => item.id === e.data.source)?.name;
const targetValue = data.find(item => item.id === e.data.target)?.name;
if (sourceValue && targetValue) {
return [
{
col: formData.source,
op: '==' as const,
val: sourceValue,
formattedVal: sourceValue,
},
{
col: formData.target,
op: '==' as const,
val: targetValue,
formattedVal: targetValue,
},
];
}
return undefined;
};
if (onContextMenu) {
e.event.stop();
const pointerEvent = e.event.event;
const data = (echartOptions as any).series[0].data as Data;
const drillToDetailFilters =
e.dataType === 'node' ? handleNodeClick(data) : handleEdgeClick(data);
onContextMenu(pointerEvent.clientX, pointerEvent.clientY, {
drillToDetail: drillToDetailFilters,
crossFilter: getCrossFilterDataMask(
data.find(item => item.id === e.data.id),
),
});
}
},
};
return (
<Echart
refs={refs}
height={height}
width={width}
echartOptions={echartOptions}
eventHandlers={eventHandlers}
/>
);
}

View File

@@ -51,10 +51,14 @@ export default function EchartsMixedTimeseries({
[seriesBreakdown],
);
const handleChange = useCallback(
(values: string[], seriesIndex: number) => {
if (!emitCrossFilters) {
return;
const getCrossFilterDataMask = useCallback(
(seriesName, seriesIndex) => {
const selected: string[] = Object.values(selectedValues || {});
let values: string[];
if (selected.includes(seriesName)) {
values = selected.filter(v => v !== seriesName);
} else {
values = [seriesName];
}
const currentGroupBy = isFirstQuery(seriesIndex) ? groupby : groupbyB;
@@ -63,51 +67,57 @@ export default function EchartsMixedTimeseries({
.map(value => currentLabelMap?.[value])
.filter(value => !!value);
setDataMask({
extraFormData: {
// @ts-ignore
filters:
values.length === 0
? []
: [
...currentGroupBy.map((col, idx) => {
const val: DataRecordValue[] = groupbyValues.map(
v => v[idx],
);
if (val === null || val === undefined)
return {
dataMask: {
extraFormData: {
// @ts-ignore
filters:
values.length === 0
? []
: [
...currentGroupBy.map((col, idx) => {
const val: DataRecordValue[] = groupbyValues.map(
v => v[idx],
);
if (val === null || val === undefined)
return {
col,
op: 'IS NULL' as const,
};
return {
col,
op: 'IS NULL',
op: 'IN' as const,
val: val as (string | number | boolean)[],
};
return {
col,
op: 'IN',
val: val as (string | number | boolean)[],
};
}),
],
}),
],
},
filterState: {
value: !groupbyValues.length ? null : groupbyValues,
selectedValues: values.length ? values : null,
},
},
filterState: {
value: !groupbyValues.length ? null : groupbyValues,
selectedValues: values.length ? values : null,
},
});
isCurrentValueSelected: selected.includes(seriesName),
};
},
[groupby, groupbyB, labelMap, labelMapB, setDataMask, selectedValues],
[groupby, groupbyB, isFirstQuery, labelMap, labelMapB, selectedValues],
);
const handleChange = useCallback(
(seriesName: string, seriesIndex: number) => {
if (!emitCrossFilters) {
return;
}
setDataMask(getCrossFilterDataMask(seriesName, seriesIndex).dataMask);
},
[emitCrossFilters, setDataMask, getCrossFilterDataMask],
);
const eventHandlers: EventHandlers = {
click: props => {
const { seriesName, seriesIndex } = props;
const values: string[] = Object.values(selectedValues || {});
if (values.includes(seriesName)) {
handleChange(
values.filter(v => v !== seriesName),
seriesIndex,
);
} else {
handleChange([seriesName], seriesIndex);
}
handleChange(seriesName, seriesIndex);
},
mouseout: () => {
currentSeries.name = '';
@@ -118,18 +128,18 @@ export default function EchartsMixedTimeseries({
contextmenu: eventParams => {
if (onContextMenu) {
eventParams.event.stop();
const { data, seriesIndex } = eventParams;
const { data, seriesName, seriesIndex } = eventParams;
const pointerEvent = eventParams.event.event;
const drillToDetailFilters: BinaryQueryObjectFilterClause[] = [];
if (data) {
const pointerEvent = eventParams.event.event;
const values = [
...(eventParams.name ? [eventParams.name] : []),
...(isFirstQuery(seriesIndex) ? labelMap : labelMapB)[
eventParams.seriesName
],
];
const filters: BinaryQueryObjectFilterClause[] = [];
if (xAxis.type === AxisType.time) {
filters.push({
drillToDetailFilters.push({
col:
xAxis.label === DTTM_ALIAS
? formData.granularitySqla
@@ -146,15 +156,18 @@ export default function EchartsMixedTimeseries({
? formData.groupby
: formData.groupbyB),
].forEach((dimension, i) =>
filters.push({
drillToDetailFilters.push({
col: dimension,
op: '==',
val: values[i],
formattedVal: String(values[i]),
}),
);
onContextMenu(pointerEvent.clientX, pointerEvent.clientY, filters);
}
onContextMenu(pointerEvent.clientX, pointerEvent.clientY, {
drillToDetail: drillToDetailFilters,
crossFilter: getCrossFilterDataMask(seriesName, seriesIndex),
});
}
},
};

View File

@@ -16,60 +16,15 @@
* specific language governing permissions and limitations
* under the License.
*/
import React, { useCallback } from 'react';
import React from 'react';
import { PieChartTransformedProps } from './types';
import Echart from '../components/Echart';
import { allEventHandlers } from '../utils/eventHandlers';
export default function EchartsPie(props: PieChartTransformedProps) {
const {
height,
width,
echartOptions,
setDataMask,
labelMap,
groupby,
selectedValues,
refs,
emitCrossFilters,
} = props;
const handleChange = useCallback(
(values: string[]) => {
if (!emitCrossFilters) {
return;
}
const { height, width, echartOptions, selectedValues, refs } = props;
const groupbyValues = values.map(value => labelMap[value]);
setDataMask({
extraFormData: {
filters:
values.length === 0
? []
: groupby.map((col, idx) => {
const val = groupbyValues.map(v => v[idx]);
if (val === null || val === undefined)
return {
col,
op: 'IS NULL',
};
return {
col,
op: 'IN',
val: val as (string | number | boolean)[],
};
}),
},
filterState: {
value: groupbyValues.length ? groupbyValues : null,
selectedValues: values.length ? values : null,
},
});
},
[groupby, labelMap, setDataMask, selectedValues],
);
const eventHandlers = allEventHandlers(props, handleChange);
const eventHandlers = allEventHandlers(props);
return (
<Echart

View File

@@ -16,60 +16,14 @@
* specific language governing permissions and limitations
* under the License.
*/
import React, { useCallback } from 'react';
import React from 'react';
import { RadarChartTransformedProps } from './types';
import Echart from '../components/Echart';
import { allEventHandlers } from '../utils/eventHandlers';
export default function EchartsRadar(props: RadarChartTransformedProps) {
const {
height,
width,
echartOptions,
setDataMask,
labelMap,
groupby,
selectedValues,
emitCrossFilters,
refs,
} = props;
const handleChange = useCallback(
(values: string[]) => {
if (!emitCrossFilters) {
return;
}
const groupbyValues = values.map(value => labelMap[value]);
setDataMask({
extraFormData: {
filters:
values.length === 0
? []
: groupby.map((col, idx) => {
const val = groupbyValues.map(v => v[idx]);
if (val === null || val === undefined)
return {
col,
op: 'IS NULL',
};
return {
col,
op: 'IN',
val: val as (string | number | boolean)[],
};
}),
},
filterState: {
value: groupbyValues.length ? groupbyValues : null,
selectedValues: values.length ? values : null,
},
});
},
[groupby, labelMap, setDataMask, selectedValues],
);
const eventHandlers = allEventHandlers(props, handleChange);
const { height, width, echartOptions, selectedValues, refs } = props;
const eventHandlers = allEventHandlers(props);
return (
<Echart

View File

@@ -43,65 +43,77 @@ export default function EchartsSunburst(props: SunburstTransformedProps) {
const { columns } = formData;
const getCrossFilterDataMask = useCallback(
(treePathInfo: TreePathInfo[]) => {
const treePath = extractTreePathInfo(treePathInfo);
const name = treePath.join(',');
const selected = Object.values(selectedValues);
let values: string[];
if (selected.includes(name)) {
values = selected.filter(v => v !== name);
} else {
values = [name];
}
const labels = values.map(value => labelMap[value]);
return {
dataMask: {
extraFormData: {
filters:
values.length === 0 || !columns
? []
: columns.map((col, idx) => {
const val = labels.map(v => v[idx]);
if (val === null || val === undefined)
return {
col,
op: 'IS NULL' as const,
};
return {
col,
op: 'IN' as const,
val: val as (string | number | boolean)[],
};
}),
},
filterState: {
value: labels.length ? labels : null,
selectedValues: values.length ? values : null,
},
},
isCurrentValueSelected: selected.includes(name),
};
},
[columns, labelMap, selectedValues],
);
const handleChange = useCallback(
(values: string[]) => {
(treePathInfo: TreePathInfo[]) => {
if (!emitCrossFilters) {
return;
}
const labels = values.map(value => labelMap[value]);
setDataMask({
extraFormData: {
filters:
values.length === 0 || !columns
? []
: columns.map((col, idx) => {
const val = labels.map(v => v[idx]);
if (val === null || val === undefined)
return {
col,
op: 'IS NULL',
};
return {
col,
op: 'IN',
val: val as (string | number | boolean)[],
};
}),
},
filterState: {
value: labels.length ? labels : null,
selectedValues: values.length ? values : null,
},
});
setDataMask(getCrossFilterDataMask(treePathInfo).dataMask);
},
[emitCrossFilters, setDataMask, columns, labelMap],
[emitCrossFilters, setDataMask, getCrossFilterDataMask],
);
const eventHandlers: EventHandlers = {
click: props => {
const { treePathInfo } = props;
const treePath = extractTreePathInfo(treePathInfo);
const name = treePath.join(',');
const values = Object.values(selectedValues);
if (values.includes(name)) {
handleChange(values.filter(v => v !== name));
} else {
handleChange([name]);
}
handleChange(treePathInfo);
},
contextmenu: eventParams => {
if (onContextMenu) {
eventParams.event.stop();
const { data } = eventParams;
const { data, treePathInfo } = eventParams;
const { records } = data;
const treePath = extractTreePathInfo(eventParams.treePathInfo);
const pointerEvent = eventParams.event.event;
const filters: BinaryQueryObjectFilterClause[] = [];
const drillToDetailFilters: BinaryQueryObjectFilterClause[] = [];
if (columns?.length) {
treePath.forEach((path, i) =>
filters.push({
drillToDetailFilters.push({
col: columns[i],
op: '==',
val: records[i],
@@ -109,7 +121,10 @@ export default function EchartsSunburst(props: SunburstTransformedProps) {
}),
);
}
onContextMenu(pointerEvent.clientX, pointerEvent.clientY, filters);
onContextMenu(pointerEvent.clientX, pointerEvent.clientY, {
drillToDetail: drillToDetailFilters,
crossFilter: getCrossFilterDataMask(treePathInfo),
});
}
},
};

View File

@@ -108,40 +108,56 @@ export default function EchartsTimeseries({
return model;
};
const getCrossFilterDataMask = useCallback(
(value: string) => {
const selected: string[] = Object.values(selectedValues);
let values: string[];
if (selected.includes(value)) {
values = selected.filter(v => v !== value);
} else {
values = [value];
}
const groupbyValues = values.map(value => labelMap[value]);
return {
dataMask: {
extraFormData: {
filters:
values.length === 0
? []
: groupby.map((col, idx) => {
const val = groupbyValues.map(v => v[idx]);
if (val === null || val === undefined)
return {
col,
op: 'IS NULL' as const,
};
return {
col,
op: 'IN' as const,
val: val as (string | number | boolean)[],
};
}),
},
filterState: {
label: groupbyValues.length ? groupbyValues : undefined,
value: groupbyValues.length ? groupbyValues : null,
selectedValues: values.length ? values : null,
},
},
isCurrentValueSelected: selected.includes(value),
};
},
[groupby, labelMap, selectedValues],
);
const handleChange = useCallback(
(values: string[]) => {
(value: string) => {
if (!emitCrossFilters) {
return;
}
const groupbyValues = values.map(value => labelMap[value]);
setDataMask({
extraFormData: {
filters:
values.length === 0
? []
: groupby.map((col, idx) => {
const val = groupbyValues.map(v => v[idx]);
if (val === null || val === undefined)
return {
col,
op: 'IS NULL',
};
return {
col,
op: 'IN',
val: val as (string | number | boolean)[],
};
}),
},
filterState: {
label: groupbyValues.length ? groupbyValues : undefined,
value: groupbyValues.length ? groupbyValues : null,
selectedValues: values.length ? values : null,
},
});
setDataMask(getCrossFilterDataMask(value).dataMask);
},
[groupby, labelMap, setDataMask, emitCrossFilters],
[emitCrossFilters, setDataMask, getCrossFilterDataMask],
);
const eventHandlers: EventHandlers = {
@@ -152,12 +168,7 @@ export default function EchartsTimeseries({
// Ensure that double-click events do not trigger single click event. So we put it in the timer.
clickTimer.current = setTimeout(() => {
const { seriesName: name } = props;
const values = Object.values(selectedValues);
if (values.includes(name)) {
handleChange(values.filter(v => v !== name));
} else {
handleChange([name]);
}
handleChange(name);
}, TIMER_DURATION);
},
mouseout: () => {
@@ -188,16 +199,16 @@ export default function EchartsTimeseries({
contextmenu: eventParams => {
if (onContextMenu) {
eventParams.event.stop();
const { data } = eventParams;
const { data, seriesName } = eventParams;
const drillToDetailFilters: BinaryQueryObjectFilterClause[] = [];
const pointerEvent = eventParams.event.event;
const values = [
...(eventParams.name ? [eventParams.name] : []),
...labelMap[eventParams.seriesName],
];
if (data) {
const pointerEvent = eventParams.event.event;
const values = [
...(eventParams.name ? [eventParams.name] : []),
...labelMap[eventParams.seriesName],
];
const filters: BinaryQueryObjectFilterClause[] = [];
if (xAxis.type === AxisType.time) {
filters.push({
drillToDetailFilters.push({
col:
// if the xAxis is '__timestamp', granularity_sqla will be the column of filter
xAxis.label === DTTM_ALIAS
@@ -213,15 +224,18 @@ export default function EchartsTimeseries({
...(xAxis.type === AxisType.category ? [xAxis.label] : []),
...formData.groupby,
].forEach((dimension, i) =>
filters.push({
drillToDetailFilters.push({
col: dimension,
op: '==',
val: values[i],
formattedVal: String(values[i]),
}),
);
onContextMenu(pointerEvent.clientX, pointerEvent.clientY, filters);
}
onContextMenu(pointerEvent.clientX, pointerEvent.clientY, {
drillToDetail: drillToDetailFilters,
crossFilter: getCrossFilterDataMask(seriesName),
});
}
},
};

View File

@@ -39,74 +39,95 @@ export default function EchartsTreemap({
selectedValues,
width,
}: TreemapTransformedProps) {
const handleChange = useCallback(
(values: string[]) => {
if (!emitCrossFilters) {
return;
const getCrossFilterDataMask = useCallback(
(data, treePathInfo) => {
if (data?.children) {
return undefined;
}
const { treePath } = extractTreePathInfo(treePathInfo);
const name = treePath.join(',');
const selected = Object.values(selectedValues);
let values: string[];
if (selected.includes(name)) {
values = selected.filter(v => v !== name);
} else {
values = [name];
}
const groupbyValues = values.map(value => labelMap[value]);
setDataMask({
extraFormData: {
filters:
values.length === 0
? []
: groupby.map((col, idx) => {
const val: DataRecordValue[] = groupbyValues.map(v => v[idx]);
if (val === null || val === undefined)
return {
dataMask: {
extraFormData: {
filters:
values.length === 0
? []
: groupby.map((col, idx) => {
const val: DataRecordValue[] = groupbyValues.map(
v => v[idx],
);
if (val === null || val === undefined)
return {
col,
op: 'IS NULL' as const,
};
return {
col,
op: 'IS NULL',
op: 'IN' as const,
val: val as (string | number | boolean)[],
};
return {
col,
op: 'IN',
val: val as (string | number | boolean)[],
};
}),
}),
},
filterState: {
value: groupbyValues.length ? groupbyValues : null,
selectedValues: values.length ? values : null,
},
},
filterState: {
value: groupbyValues.length ? groupbyValues : null,
selectedValues: values.length ? values : null,
},
});
isCurrentValueSelected: selected.includes(name),
};
},
[groupby, labelMap, setDataMask, selectedValues],
[groupby, labelMap, selectedValues],
);
const handleChange = useCallback(
(data, treePathInfo) => {
if (!emitCrossFilters) {
return;
}
const dataMask = getCrossFilterDataMask(data, treePathInfo)?.dataMask;
if (dataMask) {
setDataMask(dataMask);
}
},
[emitCrossFilters, getCrossFilterDataMask, setDataMask],
);
const eventHandlers: EventHandlers = {
click: props => {
const { data, treePathInfo } = props;
// do nothing when clicking on the parent node
if (data?.children) {
return;
}
const { treePath } = extractTreePathInfo(treePathInfo);
const name = treePath.join(',');
const values = Object.values(selectedValues);
if (values.includes(name)) {
handleChange(values.filter(v => v !== name));
} else {
handleChange([name]);
}
handleChange(data, treePathInfo);
},
contextmenu: eventParams => {
if (onContextMenu) {
eventParams.event.stop();
const { treePath } = extractTreePathInfo(eventParams.treePathInfo);
const { data, treePathInfo } = eventParams;
const { treePath } = extractTreePathInfo(treePathInfo);
if (treePath.length > 0) {
const pointerEvent = eventParams.event.event;
const filters: BinaryQueryObjectFilterClause[] = [];
const drillToDetailFilters: BinaryQueryObjectFilterClause[] = [];
treePath.forEach((path, i) =>
filters.push({
drillToDetailFilters.push({
col: groupby[i],
op: '==',
val: path === 'null' ? NULL_STRING : path,
formattedVal: path,
}),
);
onContextMenu(pointerEvent.clientX, pointerEvent.clientY, filters);
onContextMenu(pointerEvent.clientX, pointerEvent.clientY, {
drillToDetail: drillToDetailFilters,
crossFilter: getCrossFilterDataMask(data, treePathInfo),
});
}
}
},

View File

@@ -18,9 +18,9 @@
*/
import React, { RefObject } from 'react';
import {
BinaryQueryObjectFilterClause,
ChartDataResponseResult,
ChartProps,
ContextMenuFilters,
FilterState,
HandlerFunction,
PlainObject,
@@ -124,7 +124,7 @@ export interface BaseTransformedProps<F> {
onContextMenu?: (
clientX: number,
clientY: number,
filters?: BinaryQueryObjectFilterClause[],
filters?: ContextMenuFilters,
) => void;
setDataMask?: SetDataMaskHook;
filterState?: FilterState;
@@ -146,7 +146,7 @@ export type ContextMenuTransformedProps = {
onContextMenu?: (
clientX: number,
clientY: number,
filters?: BinaryQueryObjectFilterClause[],
filters?: ContextMenuFilters,
) => void;
setDataMask?: SetDataMaskHook;
};

View File

@@ -16,7 +16,12 @@
* specific language governing permissions and limitations
* under the License.
*/
import { BinaryQueryObjectFilterClause } from '@superset-ui/core';
import {
BinaryQueryObjectFilterClause,
ContextMenuFilters,
DataMask,
QueryFormColumn,
} from '@superset-ui/core';
import {
BaseTransformedProps,
CrossFilterTransformedProps,
@@ -28,17 +33,67 @@ export type Event = {
event: { stop: () => void; event: PointerEvent };
};
export const clickEventHandler =
const getCrossFilterDataMask =
(
selectedValues: Record<number, string>,
handleChange: (values: string[]) => void,
groupby: QueryFormColumn[],
labelMap: Record<string, string[]>,
) =>
(value: string) => {
const selected = Object.values(selectedValues);
let values: string[];
if (selected.includes(value)) {
values = selected.filter(v => v !== value);
} else {
values = [value];
}
const groupbyValues = values.map(value => labelMap[value]);
return {
dataMask: {
extraFormData: {
filters:
values.length === 0
? []
: groupby.map((col, idx) => {
const val = groupbyValues.map(v => v[idx]);
if (val === null || val === undefined)
return {
col,
op: 'IS NULL' as const,
};
return {
col,
op: 'IN' as const,
val: val as (string | number | boolean)[],
};
}),
},
filterState: {
value: groupbyValues.length ? groupbyValues : null,
selectedValues: values.length ? values : null,
},
},
isCurrentValueSelected: selected.includes(value),
};
};
export const clickEventHandler =
(
getCrossFilterDataMask: (
value: string,
) => ContextMenuFilters['crossFilter'],
setDataMask: (dataMask: DataMask) => void,
emitCrossFilters?: boolean,
) =>
({ name }: { name: string }) => {
const values = Object.values(selectedValues);
if (values.includes(name)) {
handleChange(values.filter(v => v !== name));
} else {
handleChange([name]);
if (!emitCrossFilters) {
return;
}
const dataMask = getCrossFilterDataMask(name)?.dataMask;
if (dataMask) {
setDataMask(dataMask);
}
};
@@ -48,16 +103,19 @@ export const contextMenuEventHandler =
CrossFilterTransformedProps)['groupby'],
onContextMenu: BaseTransformedProps<any>['onContextMenu'],
labelMap: Record<string, string[]>,
getCrossFilterDataMask: (
value: string,
) => ContextMenuFilters['crossFilter'],
) =>
(e: Event) => {
if (onContextMenu) {
e.event.stop();
const pointerEvent = e.event.event;
const filters: BinaryQueryObjectFilterClause[] = [];
const drillToDetailFilters: BinaryQueryObjectFilterClause[] = [];
if (groupby.length > 0) {
const values = labelMap[e.name];
groupby.forEach((dimension, i) =>
filters.push({
drillToDetailFilters.push({
col: dimension,
op: '==',
val: values[i],
@@ -65,18 +123,36 @@ export const contextMenuEventHandler =
}),
);
}
onContextMenu(pointerEvent.clientX, pointerEvent.clientY, filters);
onContextMenu(pointerEvent.clientX, pointerEvent.clientY, {
drillToDetail: drillToDetailFilters,
crossFilter: getCrossFilterDataMask(e.name),
});
}
};
export const allEventHandlers = (
transformedProps: BaseTransformedProps<any> & CrossFilterTransformedProps,
handleChange: (values: string[]) => void,
) => {
const { groupby, selectedValues, onContextMenu, labelMap } = transformedProps;
const {
groupby,
onContextMenu,
setDataMask,
labelMap,
emitCrossFilters,
selectedValues,
} = transformedProps;
const eventHandlers: EventHandlers = {
click: clickEventHandler(selectedValues, handleChange),
contextmenu: contextMenuEventHandler(groupby, onContextMenu, labelMap),
click: clickEventHandler(
getCrossFilterDataMask(selectedValues, groupby, labelMap),
setDataMask,
emitCrossFilters,
),
contextmenu: contextMenuEventHandler(
groupby,
onContextMenu,
labelMap,
getCrossFilterDataMask(selectedValues, groupby, labelMap),
),
};
return eventHandlers;
};