mirror of
https://github.com/apache/superset.git
synced 2026-04-22 17:45:21 +00:00
feat(ag-grid): Server Side Filtering for Column Level Filters (#35683)
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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 && (
|
||||
|
||||
Reference in New Issue
Block a user