feat: show search filtered total (#36083)

This commit is contained in:
Kaito Watanabe
2025-12-01 16:29:23 -05:00
committed by GitHub
parent db995ad5bf
commit 6e0960c3f5
5 changed files with 185 additions and 21 deletions

View File

@@ -26,6 +26,7 @@ import {
CSSProperties,
DragEvent,
useEffect,
useMemo,
} from 'react';
import { typedMemo, usePrevious } from '@superset-ui/core';
import {
@@ -88,6 +89,7 @@ export interface DataTableProps<D extends object> extends TableOptions<D> {
searchInputId?: string;
onSearchColChange: (searchCol: string) => void;
searchOptions: SearchOption[];
onFilteredDataChange?: (rows: Row<D>[], filterValue?: string) => void;
}
export interface RenderHTMLCellProps extends HTMLProps<HTMLTableCellElement> {
@@ -130,6 +132,7 @@ export default typedMemo(function DataTable<D extends object>({
searchInputId,
onSearchColChange,
searchOptions,
onFilteredDataChange,
...moreUseTableOptions
}: DataTableProps<D>): JSX.Element {
const tableHooks: PluginHook<D>[] = [
@@ -215,6 +218,7 @@ export default typedMemo(function DataTable<D extends object>({
wrapStickyTable,
setColumnOrder,
allColumns,
rows,
state: {
pageIndex,
pageSize,
@@ -237,6 +241,30 @@ export default typedMemo(function DataTable<D extends object>({
...tableHooks,
);
const rowSignature = useMemo(
// sort the rows by id to ensure the total is not recalculated when the rows are only reordered
() =>
rows
.map((row, index) => row.id ?? index)
.sort()
.join('|'),
[rows],
);
const rowsRef = useRef(rows);
rowsRef.current = rows;
useEffect(() => {
if (!onFilteredDataChange) {
return;
}
const searchText =
typeof filterValue === 'string' ? filterValue : undefined;
onFilteredDataChange(rowsRef.current, searchText);
}, [filterValue, onFilteredDataChange, rowSignature]);
const handleSearchChange = useCallback(
(query: string) => {
if (manualSearch && onSearchChange) {

View File

@@ -361,8 +361,14 @@ export default function TableChart<D extends DataRecord = DataRecord>(
comparisonColumns[0].key,
]);
const [hideComparisonKeys, setHideComparisonKeys] = useState<string[]>([]);
// recalculated totals to display when the search filter is applied (client-side pagination)
const [displayedTotals, setDisplayedTotals] = useState<D | undefined>(totals);
const theme = useTheme();
useEffect(() => {
setDisplayedTotals(totals);
}, [totals]);
// only take relevant page size options
const pageSizeOptions = useMemo(() => {
const getServerPagination = (n: number) => n <= rowCount;
@@ -1132,7 +1138,7 @@ export default function TableChart<D extends DataRecord = DataRecord>(
</th>
),
Footer: totals ? (
Footer: displayedTotals ? (
i === 0 ? (
<th key={`footer-summary-${i}`}>
<div
@@ -1157,7 +1163,9 @@ export default function TableChart<D extends DataRecord = DataRecord>(
</th>
) : (
<td key={`footer-total-${i}`} style={sharedStyle}>
<strong>{formatColumnValue(column, totals[key])[1]}</strong>
<strong>
{formatColumnValue(column, displayedTotals[key])[1]}
</strong>
</td>
)
) : undefined,
@@ -1177,7 +1185,7 @@ export default function TableChart<D extends DataRecord = DataRecord>(
getValueRange,
emitCrossFilters,
comparisonLabels,
totals,
displayedTotals,
theme,
sortDesc,
groupHeaderColumns,
@@ -1202,6 +1210,36 @@ export default function TableChart<D extends DataRecord = DataRecord>(
const [searchOptions, setSearchOptions] = useState<SearchOption[]>([]);
const handleFilteredDataChange = useCallback(
(rows: Row<D>[], searchText?: string) => {
if (!totals || serverPagination) {
return;
}
if (!searchText?.trim()) {
setDisplayedTotals(totals);
return;
}
const updatedTotals: Record<string, DataRecordValue> = { ...totals };
filteredColumnsMeta.forEach(column => {
if (column.isMetric || column.isPercentMetric) {
const aggregatedValue = rows.reduce<number>((acc, row) => {
const rawValue = row.original?.[column.key];
const numValue = Number(String(rawValue ?? '').replace(/,/g, ''));
return Number.isFinite(numValue) ? acc + numValue : acc;
}, 0);
updatedTotals[column.key] = aggregatedValue;
}
});
setDisplayedTotals(updatedTotals as D);
},
[filteredColumnsMeta, serverPagination, totals],
);
useEffect(() => {
const options = (
columns as unknown as ColumnWithLooseAccessor &
@@ -1356,6 +1394,7 @@ export default function TableChart<D extends DataRecord = DataRecord>(
manualSearch={serverPagination}
onSearchChange={debouncedSearch}
searchOptions={searchOptions}
onFilteredDataChange={handleFilteredDataChange}
/>
</Styles>
);

View File

@@ -271,6 +271,24 @@ const buildQuery: BuildQuery<TableChartFormData> = (
};
updateTableOwnState(options?.hooks?.setDataMask, modifiedOwnState);
}
if (formData.server_pagination) {
// Add search filter if search text exists
if (ownState.searchText && ownState?.searchColumn) {
queryObject = {
...queryObject,
filters: [
...(queryObject.filters || []),
{
col: ownState?.searchColumn,
op: 'ILIKE',
val: `${ownState.searchText}%`,
},
],
};
}
}
// Because we use same buildQuery for all table on the page we need split them by id
options?.hooks?.setCachedChanges({
[formData.slice_id]: queryObject.filters,
@@ -320,23 +338,6 @@ const buildQuery: BuildQuery<TableChartFormData> = (
];
}
if (formData.server_pagination) {
// Add search filter if search text exists
if (ownState.searchText && ownState?.searchColumn) {
queryObject = {
...queryObject,
filters: [
...(queryObject.filters || []),
{
col: ownState?.searchColumn,
op: 'ILIKE',
val: `${ownState.searchText}%`,
},
],
};
}
}
// Now since row limit control is always visible even
// in case of server pagination
// we must use row limit from form data