mirror of
https://github.com/apache/superset.git
synced 2026-04-19 16:14:52 +00:00
feat(dashboard): Add cross filter from context menu (#23141)
This commit is contained in:
committed by
GitHub
parent
95eb8d79d0
commit
ee1952e488
@@ -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>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user