feat(Chart): Save Chart State globally (#35343)

This commit is contained in:
Alexandru Soare
2025-10-29 15:54:07 +02:00
committed by GitHub
parent 2db19008fb
commit 99b61143f6
36 changed files with 1772 additions and 38 deletions

View File

@@ -22,25 +22,33 @@ import {
useMemo,
useRef,
memo,
FunctionComponent,
useState,
ChangeEvent,
useEffect,
type RefObject,
} from 'react';
import { ThemedAgGridReact } from '@superset-ui/core/components';
import { Constants, ThemedAgGridReact } from '@superset-ui/core/components';
import {
AgGridReact,
AllCommunityModule,
ClientSideRowModelModule,
type ColDef,
type ColumnState,
ModuleRegistry,
GridReadyEvent,
GridState,
CellClickedEvent,
IMenuActionParams,
} from '@superset-ui/core/components/ThemedAgGridReact';
import { type FunctionComponent } from 'react';
import { JsonObject, DataRecordValue, DataRecord, t } from '@superset-ui/core';
import {
AgGridChartState,
DataRecordValue,
DataRecord,
JsonObject,
t,
} from '@superset-ui/core';
import { SearchOutlined } from '@ant-design/icons';
import { debounce, isEqual } from 'lodash';
import Pagination from './components/Pagination';
@@ -49,6 +57,17 @@ import { SearchOption, SortByItem } from '../types';
import getInitialSortState, { shouldSort } from '../utils/getInitialSortState';
import { PAGE_SIZE_OPTIONS } from '../consts';
export interface AgGridState extends Partial<GridState> {
timestamp?: number;
hasChanges?: boolean;
}
// AgGridChartState with optional metadata fields for state change events
export type AgGridChartStateWithMetadata = Partial<AgGridChartState> & {
timestamp?: number;
hasChanges?: boolean;
};
export interface AgGridTableProps {
gridTheme?: string;
isDarkMode?: boolean;
@@ -80,6 +99,9 @@ export interface AgGridTableProps {
cleanedTotals: DataRecord;
showTotals: boolean;
width: number;
onColumnStateChange?: (state: AgGridChartStateWithMetadata) => void;
gridRef?: RefObject<AgGridReact>;
chartState?: AgGridChartState;
}
ModuleRegistry.registerModules([AllCommunityModule, ClientSideRowModelModule]);
@@ -114,11 +136,14 @@ const AgGridDataTable: FunctionComponent<AgGridTableProps> = memo(
cleanedTotals,
showTotals,
width,
onColumnStateChange,
chartState,
}) => {
const gridRef = useRef<AgGridReact>(null);
const inputRef = useRef<HTMLInputElement>(null);
const rowData = useMemo(() => data, [data]);
const containerRef = useRef<HTMLDivElement>(null);
const lastCapturedStateRef = useRef<string | null>(null);
const searchId = `search-${id}`;
const gridInitialState: GridState = {
@@ -211,6 +236,34 @@ const AgGridDataTable: FunctionComponent<AgGridTableProps> = memo(
if (!isSortable) return;
if (serverPagination && gridRef.current?.api && onColumnStateChange) {
const { api } = gridRef.current;
if (sortDir == null) {
api.applyColumnState({
defaultState: { sort: null },
});
} else {
api.applyColumnState({
defaultState: { sort: null },
state: [{ colId, sort: sortDir as 'asc' | 'desc', sortIndex: 0 }],
});
}
const columnState = api.getColumnState?.() || [];
const filterModel = api.getFilterModel?.() || {};
const sortModel = sortDir
? [{ colId, sort: sortDir as 'asc' | 'desc', sortIndex: 0 }]
: [];
onColumnStateChange({
columnState,
sortModel,
filterModel,
timestamp: Date.now(),
});
}
if (sortDir == null) {
onSortChange([]);
return;
@@ -234,6 +287,51 @@ const AgGridDataTable: FunctionComponent<AgGridTableProps> = memo(
[serverPagination, gridInitialState, percentMetrics, onSortChange],
);
const handleGridStateChange = useCallback(
debounce(() => {
if (onColumnStateChange && gridRef.current?.api) {
try {
const { api } = gridRef.current;
const columnState = api.getColumnState ? api.getColumnState() : [];
const filterModel = api.getFilterModel ? api.getFilterModel() : {};
const sortModel = columnState
.filter(col => col.sort)
.map(col => ({
colId: col.colId,
sort: col.sort as 'asc' | 'desc',
sortIndex: col.sortIndex || 0,
}))
.sort((a, b) => (a.sortIndex || 0) - (b.sortIndex || 0));
const stateToSave = {
columnState,
sortModel,
filterModel,
timestamp: Date.now(),
};
const stateHash = JSON.stringify({
columnOrder: columnState.map(c => c.colId),
sorts: sortModel,
filters: filterModel,
});
if (stateHash !== lastCapturedStateRef.current) {
lastCapturedStateRef.current = stateHash;
onColumnStateChange(stateToSave);
}
} catch (error) {
console.warn('Error capturing AG Grid state:', error);
}
}
}, Constants.SLOW_DEBOUNCE),
[onColumnStateChange],
);
useEffect(() => {
if (
hasServerPageLengthChanged &&
@@ -257,6 +355,24 @@ const AgGridDataTable: FunctionComponent<AgGridTableProps> = memo(
const onGridReady = (params: GridReadyEvent) => {
// This will make columns fill the grid width
params.api.sizeColumnsToFit();
// Restore saved AG Grid state from permalink if available
if (chartState && params.api) {
try {
if (chartState.columnState) {
params.api.applyColumnState?.({
state: chartState.columnState as ColumnState[],
applyOrder: true,
});
}
if (chartState.filterModel) {
params.api.setFilterModel?.(chartState.filterModel);
}
} catch {
// Silently fail if state restoration fails
}
}
};
return (
@@ -313,7 +429,9 @@ const AgGridDataTable: FunctionComponent<AgGridTableProps> = memo(
rowSelection="multiple"
animateRows
onCellClicked={handleCrossFilter}
onStateUpdated={handleGridStateChange}
initialState={gridInitialState}
maintainColumnOrder
suppressAggFuncInHeader
enableCellTextSelection
quickFilterText={serverPagination ? '' : quickFilterText}