mirror of
https://github.com/apache/superset.git
synced 2026-04-18 23:55:00 +00:00
feat(table): Export table data with "Search box" enabled (#36281)
Co-authored-by: RebeccaH2003 <114100529+RebeccaH2003@users.noreply.github.com>
This commit is contained in:
@@ -90,6 +90,7 @@ export interface DataTableProps<D extends object> extends TableOptions<D> {
|
||||
onSearchColChange: (searchCol: string) => void;
|
||||
searchOptions: SearchOption[];
|
||||
onFilteredDataChange?: (rows: Row<D>[], filterValue?: string) => void;
|
||||
onFilteredRowsChange?: (rows: D[]) => void;
|
||||
}
|
||||
|
||||
export interface RenderHTMLCellProps extends HTMLProps<HTMLTableCellElement> {
|
||||
@@ -133,6 +134,7 @@ export default typedMemo(function DataTable<D extends object>({
|
||||
onSearchColChange,
|
||||
searchOptions,
|
||||
onFilteredDataChange,
|
||||
onFilteredRowsChange,
|
||||
...moreUseTableOptions
|
||||
}: DataTableProps<D>): JSX.Element {
|
||||
const tableHooks: PluginHook<D>[] = [
|
||||
@@ -204,6 +206,7 @@ export default typedMemo(function DataTable<D extends object>({
|
||||
);
|
||||
|
||||
const {
|
||||
rows, // filtered/sorted rows before pagination
|
||||
getTableProps,
|
||||
getTableBodyProps,
|
||||
prepareRow,
|
||||
@@ -218,7 +221,6 @@ export default typedMemo(function DataTable<D extends object>({
|
||||
wrapStickyTable,
|
||||
setColumnOrder,
|
||||
allColumns,
|
||||
rows,
|
||||
state: {
|
||||
pageIndex,
|
||||
pageSize,
|
||||
@@ -452,6 +454,83 @@ export default typedMemo(function DataTable<D extends object>({
|
||||
onServerPaginationChange(pageNumber, serverPageSize);
|
||||
}
|
||||
|
||||
// Emit filtered rows to parent in client-side mode (debounced via RAF)
|
||||
const isMountedRef = useRef(true);
|
||||
useEffect(() => {
|
||||
isMountedRef.current = true;
|
||||
return () => {
|
||||
isMountedRef.current = false;
|
||||
};
|
||||
}, []);
|
||||
|
||||
const rafRef = useRef<number | null>(null);
|
||||
const lastSigRef = useRef<string>('');
|
||||
|
||||
// Prefer a stable identifier from original row data; otherwise use a deterministic
|
||||
// concatenation of visible values (keys sorted so column order changes are detected).
|
||||
function stableRowKey<D extends object>(r: Row<D>): string {
|
||||
const orig = r.original as Record<string, unknown> | undefined;
|
||||
if (orig) {
|
||||
const idLike =
|
||||
(orig as any).id ??
|
||||
(orig as any).ID ??
|
||||
(orig as any).key ??
|
||||
(orig as any).uuid;
|
||||
if (idLike != null) return String(idLike);
|
||||
}
|
||||
|
||||
// Fallback: derive from row.values, but make it stable against column order changes.
|
||||
const v = r.values as Record<string, unknown>;
|
||||
const keys = Object.keys(v).sort(); // detect column order changes
|
||||
return keys.map(k => String(v[k] ?? '')).join('|');
|
||||
}
|
||||
|
||||
// Very small, fast hash for strings (no crypto dependency).
|
||||
function hashString(s: string): string {
|
||||
let h = 0;
|
||||
for (let i = 0; i < s.length; i += 1) {
|
||||
h = (h * 31 + s.charCodeAt(i)) | 0;
|
||||
}
|
||||
return String(h);
|
||||
}
|
||||
|
||||
function signatureOfRows<D extends object>(rs: Row<D>[]): string {
|
||||
const keys = rs.map(stableRowKey);
|
||||
const len = keys.length;
|
||||
const first = keys[0] ?? '';
|
||||
const last = keys[len - 1] ?? '';
|
||||
const digest = hashString(keys.join('\u0001')); // non-printable separator to avoid collisions
|
||||
return `${len}|${first}|${last}|${digest}`;
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (serverPagination || typeof onFilteredRowsChange !== 'function') {
|
||||
return;
|
||||
}
|
||||
|
||||
const sig = signatureOfRows(rows);
|
||||
|
||||
if (sig !== lastSigRef.current) {
|
||||
lastSigRef.current = sig;
|
||||
if (rafRef.current != null) {
|
||||
cancelAnimationFrame(rafRef.current);
|
||||
}
|
||||
rafRef.current = requestAnimationFrame(() => {
|
||||
if (isMountedRef.current) {
|
||||
// Only emit originals when the signature truly changed
|
||||
onFilteredRowsChange(rows.map(r => r.original as D));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (rafRef.current != null) {
|
||||
cancelAnimationFrame(rafRef.current);
|
||||
rafRef.current = null;
|
||||
}
|
||||
};
|
||||
}, [rows, serverPagination, onFilteredRowsChange]);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={wrapperRef}
|
||||
|
||||
@@ -122,4 +122,6 @@ interface TableOwnState {
|
||||
sortColumn?: string;
|
||||
sortOrder?: 'asc' | 'desc';
|
||||
searchText?: string;
|
||||
|
||||
clientView?: ClientViewSnapshot;
|
||||
}
|
||||
|
||||
@@ -17,25 +17,27 @@
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { SetDataMaskHook } from '@superset-ui/core';
|
||||
import { TableOwnState } from '../types/react-table';
|
||||
import type { SetDataMaskHook } from '@superset-ui/core';
|
||||
import type { TableOwnState } from '../types/react-table';
|
||||
|
||||
export const updateExternalFormData = (
|
||||
setDataMask: SetDataMaskHook = () => {},
|
||||
pageNumber: number,
|
||||
pageSize: number,
|
||||
) =>
|
||||
) => {
|
||||
setDataMask({
|
||||
ownState: {
|
||||
currentPage: pageNumber,
|
||||
pageSize,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const updateTableOwnState = (
|
||||
setDataMask: SetDataMaskHook = () => {},
|
||||
modifiedOwnState: TableOwnState,
|
||||
) =>
|
||||
) => {
|
||||
setDataMask({
|
||||
ownState: modifiedOwnState,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -25,6 +25,7 @@ import {
|
||||
MouseEvent,
|
||||
KeyboardEvent as ReactKeyboardEvent,
|
||||
useEffect,
|
||||
useRef,
|
||||
} from 'react';
|
||||
|
||||
import {
|
||||
@@ -1354,6 +1355,50 @@ export default function TableChart<D extends DataRecord = DataRecord>(
|
||||
}
|
||||
};
|
||||
|
||||
// collect client-side filtered rows for export & push snapshot to ownState (guarded)
|
||||
const [clientViewRows, setClientViewRows] = useState<DataRecord[]>([]);
|
||||
|
||||
const exportColumns = useMemo(
|
||||
() =>
|
||||
visibleColumnsMeta.map(col => ({
|
||||
key: col.key,
|
||||
label: col.config?.customColumnName || col.originalLabel || col.key,
|
||||
})),
|
||||
[visibleColumnsMeta],
|
||||
);
|
||||
|
||||
// Use a ref to store previous clientViewRows and exportColumns for robust change detection
|
||||
const prevClientViewRef = useRef<{
|
||||
rows: DataRecord[];
|
||||
columns: typeof exportColumns;
|
||||
} | null>(null);
|
||||
useEffect(() => {
|
||||
if (serverPagination) return; // only for client-side mode
|
||||
const prev = prevClientViewRef.current;
|
||||
const rowsChanged = !prev || !isEqual(prev.rows, clientViewRows);
|
||||
const columnsChanged = !prev || !isEqual(prev.columns, exportColumns);
|
||||
if (rowsChanged || columnsChanged) {
|
||||
prevClientViewRef.current = {
|
||||
rows: clientViewRows,
|
||||
columns: exportColumns,
|
||||
};
|
||||
updateTableOwnState(setDataMask, {
|
||||
...serverPaginationData,
|
||||
clientView: {
|
||||
rows: clientViewRows,
|
||||
columns: exportColumns,
|
||||
count: clientViewRows.length,
|
||||
},
|
||||
});
|
||||
}
|
||||
}, [
|
||||
clientViewRows,
|
||||
exportColumns,
|
||||
serverPagination,
|
||||
setDataMask,
|
||||
serverPaginationData,
|
||||
]);
|
||||
|
||||
return (
|
||||
<Styles>
|
||||
<DataTable<D>
|
||||
@@ -1391,6 +1436,7 @@ export default function TableChart<D extends DataRecord = DataRecord>(
|
||||
onSearchChange={debouncedSearch}
|
||||
searchOptions={searchOptions}
|
||||
onFilteredDataChange={handleFilteredDataChange}
|
||||
onFilteredRowsChange={setClientViewRows}
|
||||
/>
|
||||
</Styles>
|
||||
);
|
||||
|
||||
@@ -39,6 +39,7 @@ const metadata = new ChartMetadata({
|
||||
Behavior.InteractiveChart,
|
||||
Behavior.DrillToDetail,
|
||||
Behavior.DrillBy,
|
||||
'EXPORT_CURRENT_VIEW' as any,
|
||||
],
|
||||
category: t('Table'),
|
||||
canBeAnnotationTypes: ['EVENT', 'INTERVAL'],
|
||||
|
||||
Reference in New Issue
Block a user