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

@@ -22,11 +22,13 @@ import React, {
useLayoutEffect,
useMemo,
useState,
MouseEvent,
} from 'react';
import {
ColumnInstance,
ColumnWithLooseAccessor,
DefaultSortTypes,
Row,
} from 'react-table';
import { extent as d3Extent, max as d3Max } from 'd3-array';
import { FaSort } from '@react-icons/all-files/fa/FaSort';
@@ -241,57 +243,6 @@ export default function TableChart<D extends DataRecord = DataRecord>(
// keep track of whether column order changed, so that column widths can too
const [columnOrderToggle, setColumnOrderToggle] = useState(false);
const handleChange = useCallback(
(filters: { [x: string]: DataRecordValue[] }) => {
if (!emitCrossFilters) {
return;
}
const groupBy = Object.keys(filters);
const groupByValues = Object.values(filters);
const labelElements: string[] = [];
groupBy.forEach(col => {
const isTimestamp = col === DTTM_ALIAS;
const filterValues = ensureIsArray(filters?.[col]);
if (filterValues.length) {
const valueLabels = filterValues.map(value =>
isTimestamp ? timestampFormatter(value) : value,
);
labelElements.push(`${valueLabels.join(', ')}`);
}
});
setDataMask({
extraFormData: {
filters:
groupBy.length === 0
? []
: groupBy.map(col => {
const val = ensureIsArray(filters?.[col]);
if (!val.length)
return {
col,
op: 'IS NULL',
};
return {
col,
op: 'IN',
val: val.map(el =>
el instanceof Date ? el.getTime() : el!,
),
grain: col === DTTM_ALIAS ? timeGrain : undefined,
};
}),
},
filterState: {
label: labelElements.join(', '),
value: groupByValues.length ? groupByValues : null,
filters: filters && Object.keys(filters).length ? filters : null,
},
});
},
[emitCrossFilters, setDataMask],
);
// only take relevant page size options
const pageSizeOptions = useMemo(() => {
const getServerPagination = (n: number) => n <= rowCount;
@@ -322,25 +273,80 @@ export default function TableChart<D extends DataRecord = DataRecord>(
[filters],
);
const getCrossFilterDataMask = (key: string, value: DataRecordValue) => {
let updatedFilters = { ...(filters || {}) };
if (filters && isActiveFilterValue(key, value)) {
updatedFilters = {};
} else {
updatedFilters = {
[key]: [value],
};
}
if (
Array.isArray(updatedFilters[key]) &&
updatedFilters[key].length === 0
) {
delete updatedFilters[key];
}
const groupBy = Object.keys(updatedFilters);
const groupByValues = Object.values(updatedFilters);
const labelElements: string[] = [];
groupBy.forEach(col => {
const isTimestamp = col === DTTM_ALIAS;
const filterValues = ensureIsArray(updatedFilters?.[col]);
if (filterValues.length) {
const valueLabels = filterValues.map(value =>
isTimestamp ? timestampFormatter(value) : value,
);
labelElements.push(`${valueLabels.join(', ')}`);
}
});
return {
dataMask: {
extraFormData: {
filters:
groupBy.length === 0
? []
: groupBy.map(col => {
const val = ensureIsArray(updatedFilters?.[col]);
if (!val.length)
return {
col,
op: 'IS NULL' as const,
};
return {
col,
op: 'IN' as const,
val: val.map(el =>
el instanceof Date ? el.getTime() : el!,
),
grain: col === DTTM_ALIAS ? timeGrain : undefined,
};
}),
},
filterState: {
label: labelElements.join(', '),
value: groupByValues.length ? groupByValues : null,
filters:
updatedFilters && Object.keys(updatedFilters).length
? updatedFilters
: null,
},
},
isCurrentValueSelected: isActiveFilterValue(key, value),
};
};
const toggleFilter = useCallback(
function toggleFilter(key: string, val: DataRecordValue) {
let updatedFilters = { ...(filters || {}) };
if (filters && isActiveFilterValue(key, val)) {
updatedFilters = {};
} else {
updatedFilters = {
[key]: [val],
};
if (!emitCrossFilters) {
return;
}
if (
Array.isArray(updatedFilters[key]) &&
updatedFilters[key].length === 0
) {
delete updatedFilters[key];
}
handleChange(updatedFilters);
setDataMask(getCrossFilterDataMask(key, val).dataMask);
},
[filters, handleChange, isActiveFilterValue],
[emitCrossFilters, getCrossFilterDataMask, setDataMask],
);
const getSharedStyle = (column: DataColumnMeta): CSSProperties => {
@@ -355,6 +361,39 @@ export default function TableChart<D extends DataRecord = DataRecord>(
};
};
const handleContextMenu =
onContextMenu && !isRawRecords
? (
value: D,
cellPoint: {
key: string;
value: DataRecordValue;
isMetric?: boolean;
},
clientX: number,
clientY: number,
) => {
const drillToDetailFilters: BinaryQueryObjectFilterClause[] = [];
columnsMeta.forEach(col => {
if (!col.isMetric) {
const dataRecordValue = value[col.key];
drillToDetailFilters.push({
col: col.key,
op: '==',
val: dataRecordValue as string | number | boolean,
formattedVal: formatColumnValue(col, dataRecordValue)[1],
});
}
});
onContextMenu(clientX, clientY, {
drillToDetail: drillToDetailFilters,
crossFilter: cellPoint.isMetric
? undefined
: getCrossFilterDataMask(cellPoint.key, cellPoint.value),
});
}
: undefined;
const getColumnConfigs = useCallback(
(column: DataColumnMeta, i: number): ColumnWithLooseAccessor<D> => {
const { key, label, isNumeric, dataType, isMetric, config = {} } = column;
@@ -390,7 +429,7 @@ export default function TableChart<D extends DataRecord = DataRecord>(
getValueRange(key, alignPositiveNegative);
let className = '';
if (emitCrossFilters) {
if (emitCrossFilters && !isMetric) {
className += ' dt-is-filter';
}
@@ -400,7 +439,7 @@ export default function TableChart<D extends DataRecord = DataRecord>(
// typing is incorrect in current version of `@types/react-table`
// so we ask TS not to check.
accessor: ((datum: D) => datum[key]) as never,
Cell: ({ value }: { value: DataRecordValue }) => {
Cell: ({ value, row }: { value: DataRecordValue; row: Row<D> }) => {
const [isHtml, text] = formatColumnValue(column, value);
const html = isHtml ? { __html: text } : undefined;
@@ -453,9 +492,21 @@ export default function TableChart<D extends DataRecord = DataRecord>(
// show raw number in title in case of numeric values
title: typeof value === 'number' ? String(value) : undefined,
onClick:
emitCrossFilters && !valueRange
emitCrossFilters && !valueRange && !isMetric
? () => toggleFilter(key, value)
: undefined,
onContextMenu: (e: MouseEvent) => {
if (handleContextMenu) {
e.preventDefault();
e.stopPropagation();
handleContextMenu(
row.original,
{ key, value, isMetric },
e.nativeEvent.clientX,
e.nativeEvent.clientY,
);
}
},
className: [
className,
value == null ? 'dt-is-null' : '',
@@ -621,25 +672,6 @@ export default function TableChart<D extends DataRecord = DataRecord>(
const { width: widthFromState, height: heightFromState } = tableSize;
const handleContextMenu =
onContextMenu && !isRawRecords
? (value: D, clientX: number, clientY: number) => {
const filters: BinaryQueryObjectFilterClause[] = [];
columnsMeta.forEach(col => {
if (!col.isMetric) {
const dataRecordValue = value[col.key];
filters.push({
col: col.key,
op: '==',
val: dataRecordValue as string | number | boolean,
formattedVal: formatColumnValue(col, dataRecordValue)[1],
});
}
});
onContextMenu(clientX, clientY, filters);
}
: undefined;
return (
<Styles>
<DataTable<D>
@@ -662,7 +694,6 @@ export default function TableChart<D extends DataRecord = DataRecord>(
selectPageSize={pageSize !== null && SelectPageSize}
// not in use in Superset, but needed for unit tests
sticky={sticky}
onContextMenu={handleContextMenu}
/>
</Styles>
);