feat(ag-grid): Server Side Filtering for Column Level Filters (#35683)

This commit is contained in:
amaannawab923
2026-01-12 19:25:07 +05:30
committed by GitHub
parent 459b4cb23d
commit 4f444ae1d2
20 changed files with 4142 additions and 95 deletions

View File

@@ -19,9 +19,10 @@
* under the License.
*/
import { useRef, useState } from 'react';
import { useRef, useState, useEffect } from 'react';
import { t } from '@apache-superset/core';
import { ArrowDownOutlined, ArrowUpOutlined } from '@ant-design/icons';
import { Column } from '@superset-ui/core/components/ThemedAgGridReact';
import FilterIcon from './Filter';
import KebabMenu from './KebabMenu';
import {
@@ -29,6 +30,8 @@ import {
CustomHeaderParams,
SortState,
UserProvidedColDef,
FilterInputPosition,
AGGridFilterInstance,
} from '../../types';
import CustomPopover from './CustomPopover';
import {
@@ -39,6 +42,13 @@ import {
MenuContainer,
SortIconWrapper,
} from '../../styles';
import { GridApi } from 'ag-grid-community';
import {
FILTER_POPOVER_OPEN_DELAY,
FILTER_INPUT_POSITIONS,
FILTER_CONDITION_BODY_INDEX,
FILTER_INPUT_SELECTOR,
} from '../../consts';
const getSortIcon = (sortState: SortState[], colId: string | null) => {
if (!sortState?.length || !colId) return null;
@@ -53,6 +63,43 @@ const getSortIcon = (sortState: SortState[], colId: string | null) => {
return null;
};
// Auto-opens filter popover and focuses the correct input after server-side filtering
const autoOpenFilterAndFocus = async (
column: Column,
api: GridApi,
filterRef: React.RefObject<HTMLDivElement>,
setFilterVisible: (visible: boolean) => void,
lastFilteredInputPosition?: FilterInputPosition,
) => {
setFilterVisible(true);
const filterInstance = (await api.getColumnFilterInstance(
column,
)) as AGGridFilterInstance | null;
const filterEl = filterInstance?.eGui;
if (!filterEl || !filterRef.current) return;
filterRef.current.innerHTML = '';
filterRef.current.appendChild(filterEl);
if (filterInstance?.eConditionBodies) {
const conditionBodies = filterInstance.eConditionBodies;
const targetIndex =
lastFilteredInputPosition === FILTER_INPUT_POSITIONS.SECOND
? FILTER_CONDITION_BODY_INDEX.SECOND
: FILTER_CONDITION_BODY_INDEX.FIRST;
const targetBody = conditionBodies[targetIndex];
if (targetBody) {
const input = targetBody.querySelector(
FILTER_INPUT_SELECTOR,
) as HTMLInputElement | null;
input?.focus();
}
}
};
const CustomHeader: React.FC<CustomHeaderParams> = ({
displayName,
enableSorting,
@@ -61,7 +108,12 @@ const CustomHeader: React.FC<CustomHeaderParams> = ({
column,
api,
}) => {
const { initialSortState, onColumnHeaderClicked } = context;
const {
initialSortState,
onColumnHeaderClicked,
lastFilteredColumn,
lastFilteredInputPosition,
} = context;
const colId = column?.getColId();
const colDef = column?.getColDef() as CustomColDef;
const userColDef = column.getUserProvidedColDef() as UserProvidedColDef;
@@ -77,7 +129,6 @@ const CustomHeader: React.FC<CustomHeaderParams> = ({
const isTimeComparison = !isMain && userColDef?.timeComparisonKey;
const sortKey = isMain ? colId.replace('Main', '').trim() : colId;
// Sorting logic
const clearSort = () => {
onColumnHeaderClicked({ column: { colId: sortKey, sort: null } });
setSort(null, false);
@@ -106,7 +157,9 @@ const CustomHeader: React.FC<CustomHeaderParams> = ({
e.stopPropagation();
setFilterVisible(!isFilterVisible);
const filterInstance = await api.getColumnFilterInstance<any>(column);
const filterInstance = (await api.getColumnFilterInstance(
column,
)) as AGGridFilterInstance | null;
const filterEl = filterInstance?.eGui;
if (filterEl && filterRef.current) {
filterRef.current.innerHTML = '';
@@ -114,6 +167,25 @@ const CustomHeader: React.FC<CustomHeaderParams> = ({
}
};
// Re-open filter popover after server refresh (delay allows AG Grid to finish rendering)
useEffect(() => {
if (lastFilteredColumn === colId && !isFilterVisible) {
const timeoutId = setTimeout(
() =>
autoOpenFilterAndFocus(
column,
api,
filterRef,
setFilterVisible,
lastFilteredInputPosition,
),
FILTER_POPOVER_OPEN_DELAY,
);
return () => clearTimeout(timeoutId);
}
return undefined;
}, [lastFilteredColumn, colId, lastFilteredInputPosition]);
const handleMenuClick = (e: React.MouseEvent) => {
e.stopPropagation();
setMenuVisible(!isMenuVisible);

View File

@@ -55,7 +55,9 @@ import Pagination from './components/Pagination';
import SearchSelectDropdown from './components/SearchSelectDropdown';
import { SearchOption, SortByItem } from '../types';
import getInitialSortState, { shouldSort } from '../utils/getInitialSortState';
import getInitialFilterModel from '../utils/getInitialFilterModel';
import { PAGE_SIZE_OPTIONS } from '../consts';
import { getCompleteFilterState } from '../utils/filterStateManager';
export interface AgGridState extends Partial<GridState> {
timestamp?: number;
@@ -100,6 +102,8 @@ export interface AgGridTableProps {
showTotals: boolean;
width: number;
onColumnStateChange?: (state: AgGridChartStateWithMetadata) => void;
onFilterChanged?: (filterModel: Record<string, any>) => void;
metricColumns?: string[];
gridRef?: RefObject<AgGridReact>;
chartState?: AgGridChartState;
}
@@ -137,6 +141,8 @@ const AgGridDataTable: FunctionComponent<AgGridTableProps> = memo(
showTotals,
width,
onColumnStateChange,
onFilterChanged,
metricColumns = [],
chartState,
}) => {
const gridRef = useRef<AgGridReact>(null);
@@ -144,14 +150,27 @@ const AgGridDataTable: FunctionComponent<AgGridTableProps> = memo(
const rowData = useMemo(() => data, [data]);
const containerRef = useRef<HTMLDivElement>(null);
const lastCapturedStateRef = useRef<string | null>(null);
const filterOperationVersionRef = useRef(0);
const searchId = `search-${id}`;
const initialFilterModel = getInitialFilterModel(
chartState,
serverPaginationData,
serverPagination,
);
const gridInitialState: GridState = {
...(serverPagination && {
sort: {
sortModel: getInitialSortState(serverPaginationData?.sortBy || []),
},
}),
...(initialFilterModel && {
filter: {
filterModel: initialFilterModel,
},
}),
};
const defaultColDef = useMemo<ColDef>(
@@ -332,6 +351,56 @@ const AgGridDataTable: FunctionComponent<AgGridTableProps> = memo(
[onColumnStateChange],
);
const handleFilterChanged = useCallback(async () => {
filterOperationVersionRef.current += 1;
const currentVersion = filterOperationVersionRef.current;
const completeFilterState = await getCompleteFilterState(
gridRef,
metricColumns,
);
// Skip stale operations from rapid filter changes
if (currentVersion !== filterOperationVersionRef.current) {
return;
}
// Reject invalid filter states (e.g., text filter on numeric column)
if (completeFilterState.originalFilterModel) {
const filterModel = completeFilterState.originalFilterModel;
const hasInvalidFilterType = Object.entries(filterModel).some(
([colId, filter]: [string, any]) => {
if (
filter?.filterType === 'text' &&
metricColumns?.includes(colId)
) {
return true;
}
return false;
},
);
if (hasInvalidFilterType) {
return;
}
}
if (
!isEqual(
serverPaginationData?.agGridFilterModel,
completeFilterState.originalFilterModel,
)
) {
if (onFilterChanged) {
onFilterChanged(completeFilterState);
}
}
}, [
onFilterChanged,
metricColumns,
serverPaginationData?.agGridFilterModel,
]);
useEffect(() => {
if (
hasServerPageLengthChanged &&
@@ -356,19 +425,14 @@ const AgGridDataTable: FunctionComponent<AgGridTableProps> = memo(
// This will make columns fill the grid width
params.api.sizeColumnsToFit();
// Restore saved AG Grid state from permalink if available
if (chartState && params.api) {
// Restore saved column state from permalink if available
// Note: filterModel is now handled via gridInitialState for better performance
if (chartState?.columnState && params.api) {
try {
if (chartState.columnState) {
params.api.applyColumnState?.({
state: chartState.columnState as ColumnState[],
applyOrder: true,
});
}
if (chartState.filterModel) {
params.api.setFilterModel?.(chartState.filterModel);
}
params.api.applyColumnState?.({
state: chartState.columnState as ColumnState[],
applyOrder: true,
});
} catch {
// Silently fail if state restoration fails
}
@@ -429,6 +493,7 @@ const AgGridDataTable: FunctionComponent<AgGridTableProps> = memo(
rowSelection="multiple"
animateRows
onCellClicked={handleCrossFilter}
onFilterChanged={handleFilterChanged}
onStateUpdated={handleGridStateChange}
initialState={gridInitialState}
maintainColumnOrder
@@ -520,6 +585,9 @@ const AgGridDataTable: FunctionComponent<AgGridTableProps> = memo(
serverPaginationData?.sortBy || [],
),
isActiveFilterValue,
lastFilteredColumn: serverPaginationData?.lastFilteredColumn,
lastFilteredInputPosition:
serverPaginationData?.lastFilteredInputPosition,
}}
/>
{serverPagination && (