mirror of
https://github.com/apache/superset.git
synced 2026-08-12 11:11:01 +00:00
feat(Chart): Save Chart State globally (#35343)
This commit is contained in:
@@ -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}
|
||||
|
||||
@@ -82,6 +82,8 @@ export default function TableChart<D extends DataRecord = DataRecord>(
|
||||
columnColorFormatters,
|
||||
basicColorFormatters,
|
||||
width,
|
||||
onChartStateChange,
|
||||
chartState,
|
||||
} = props;
|
||||
|
||||
const [searchOptions, setSearchOptions] = useState<SearchOption[]>([]);
|
||||
@@ -110,6 +112,15 @@ export default function TableChart<D extends DataRecord = DataRecord>(
|
||||
comparisonColumns?.[0]?.key,
|
||||
]);
|
||||
|
||||
const handleColumnStateChange = useCallback(
|
||||
agGridState => {
|
||||
if (onChartStateChange) {
|
||||
onChartStateChange(agGridState);
|
||||
}
|
||||
},
|
||||
[onChartStateChange],
|
||||
);
|
||||
|
||||
const filteredColumns = useMemo(() => {
|
||||
if (!isUsingTimeComparison) {
|
||||
return columns;
|
||||
@@ -289,6 +300,8 @@ export default function TableChart<D extends DataRecord = DataRecord>(
|
||||
cleanedTotals={totals || {}}
|
||||
showTotals={showTotals}
|
||||
width={width}
|
||||
onColumnStateChange={handleColumnStateChange}
|
||||
chartState={chartState}
|
||||
/>
|
||||
</StyledChartContainer>
|
||||
);
|
||||
|
||||
@@ -20,8 +20,11 @@ import {
|
||||
AdhocColumn,
|
||||
buildQueryContext,
|
||||
ensureIsArray,
|
||||
getColumnLabel,
|
||||
getMetricLabel,
|
||||
isPhysicalColumn,
|
||||
QueryFormColumn,
|
||||
QueryFormMetric,
|
||||
QueryFormOrderBy,
|
||||
QueryMode,
|
||||
QueryObject,
|
||||
@@ -192,6 +195,7 @@ const buildQuery: BuildQuery<TableChartFormData> = (
|
||||
|
||||
const moreProps: Partial<QueryObject> = {};
|
||||
const ownState = options?.ownState ?? {};
|
||||
|
||||
// Build Query flag to check if its for either download as csv, excel or json
|
||||
const isDownloadQuery =
|
||||
['csv', 'xlsx'].includes(formData?.result_format || '') ||
|
||||
@@ -211,22 +215,133 @@ const buildQuery: BuildQuery<TableChartFormData> = (
|
||||
moreProps.row_offset = currentPage * pageSize;
|
||||
}
|
||||
|
||||
// getting sort by in case of server pagination from own state
|
||||
let sortByFromOwnState: QueryFormOrderBy[] | undefined;
|
||||
if (Array.isArray(ownState?.sortBy) && ownState?.sortBy.length > 0) {
|
||||
const sortByItem = ownState?.sortBy[0];
|
||||
sortByFromOwnState = [[sortByItem?.key, !sortByItem?.desc]];
|
||||
|
||||
const sortSource =
|
||||
isDownloadQuery && ownState?.sortModel
|
||||
? ownState.sortModel
|
||||
: ownState?.sortBy;
|
||||
|
||||
if (Array.isArray(sortSource) && sortSource.length > 0) {
|
||||
const mapColIdToIdentifier = (colId: string): string | undefined => {
|
||||
const matchingColumn = columns.find((col: QueryFormColumn) => {
|
||||
const colLabel = getColumnLabel(col);
|
||||
return colLabel === colId;
|
||||
});
|
||||
|
||||
if (matchingColumn) {
|
||||
if (
|
||||
typeof matchingColumn === 'object' &&
|
||||
'sqlExpression' in matchingColumn
|
||||
) {
|
||||
return matchingColumn.sqlExpression;
|
||||
}
|
||||
return getColumnLabel(matchingColumn);
|
||||
}
|
||||
|
||||
const matchingMetric = (metrics || []).find((met: QueryFormMetric) => {
|
||||
const metLabel = getMetricLabel(met);
|
||||
return metLabel === colId || `%${metLabel}` === colId;
|
||||
});
|
||||
|
||||
if (matchingMetric) {
|
||||
return getMetricLabel(matchingMetric);
|
||||
}
|
||||
|
||||
return colId;
|
||||
};
|
||||
|
||||
sortByFromOwnState = sortSource
|
||||
.map((sortItem: any) => {
|
||||
const colId = sortItem?.colId || sortItem?.key;
|
||||
const sortKey = mapColIdToIdentifier(colId);
|
||||
if (!sortKey) return null;
|
||||
const isDesc = sortItem?.sort === 'desc' || sortItem?.desc;
|
||||
return [sortKey, !isDesc] as QueryFormOrderBy;
|
||||
})
|
||||
.filter((item): item is QueryFormOrderBy => item !== null);
|
||||
|
||||
// Add secondary sort for stable ordering (matches AG Grid's stable sort behavior)
|
||||
if (sortByFromOwnState.length === 1 && isDownloadQuery && orderby) {
|
||||
const primarySort = sortByFromOwnState[0][0];
|
||||
orderby.forEach(orderItem => {
|
||||
if (orderItem[0] !== primarySort) {
|
||||
sortByFromOwnState!.push(orderItem);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Note: In Superset, "columns" are dimensions and "metrics" are measures,
|
||||
// but AG Grid treats them all as "columns" in the UI
|
||||
let orderedColumns = columns;
|
||||
let orderedMetrics = metrics;
|
||||
|
||||
if (
|
||||
isDownloadQuery &&
|
||||
ownState.columnOrder &&
|
||||
Array.isArray(ownState.columnOrder)
|
||||
) {
|
||||
type ColumnOrMetric = QueryFormColumn | QueryFormMetric;
|
||||
|
||||
const matchesColId = (item: ColumnOrMetric, colId: string): boolean => {
|
||||
if (typeof item === 'string') {
|
||||
return item === colId;
|
||||
}
|
||||
|
||||
// Check AdhocColumn properties
|
||||
if ('sqlExpression' in item || 'columnName' in item) {
|
||||
return (
|
||||
(item as AdhocColumn).sqlExpression === colId ||
|
||||
item.label === colId
|
||||
);
|
||||
}
|
||||
|
||||
// Check metric properties
|
||||
return getMetricLabel(item) === colId || item.label === colId;
|
||||
};
|
||||
|
||||
const reorderByColumnOrder = (
|
||||
items: ColumnOrMetric[],
|
||||
): ColumnOrMetric[] => {
|
||||
const ordered: ColumnOrMetric[] = [];
|
||||
const remaining = new Set(items);
|
||||
|
||||
ownState.columnOrder.forEach((colId: string) => {
|
||||
const match = items.find(
|
||||
item => remaining.has(item) && matchesColId(item, colId),
|
||||
);
|
||||
if (match) {
|
||||
ordered.push(match);
|
||||
remaining.delete(match);
|
||||
}
|
||||
});
|
||||
|
||||
remaining.forEach(item => ordered.push(item));
|
||||
return ordered;
|
||||
};
|
||||
|
||||
orderedColumns = reorderByColumnOrder(columns) as typeof columns;
|
||||
orderedMetrics = reorderByColumnOrder(metrics || []) as typeof metrics;
|
||||
}
|
||||
|
||||
let queryObject = {
|
||||
...baseQueryObject,
|
||||
columns,
|
||||
extras,
|
||||
columns: orderedColumns,
|
||||
extras: {
|
||||
...extras,
|
||||
// Pass column order to enable mixed column+metric ordering
|
||||
...(isDownloadQuery &&
|
||||
ownState.columnOrder &&
|
||||
Array.isArray(ownState.columnOrder)
|
||||
? { column_order: ownState.columnOrder }
|
||||
: {}),
|
||||
},
|
||||
orderby:
|
||||
formData.server_pagination && sortByFromOwnState
|
||||
(formData.server_pagination || isDownloadQuery) && sortByFromOwnState
|
||||
? sortByFromOwnState
|
||||
: orderby,
|
||||
metrics,
|
||||
metrics: orderedMetrics,
|
||||
post_processing: postProcessing,
|
||||
time_offsets: timeOffsets,
|
||||
...moreProps,
|
||||
@@ -275,6 +390,43 @@ const buildQuery: BuildQuery<TableChartFormData> = (
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to determine if a column is a metric (needs HAVING) or dimension (needs WHERE)
|
||||
*/
|
||||
const isMetricColumn = (colId: string): boolean => {
|
||||
const metricLabels = new Set(
|
||||
(metrics || []).map(m =>
|
||||
typeof m === 'string' ? m : getMetricLabel(m),
|
||||
),
|
||||
);
|
||||
return metricLabels.has(colId) || colId.startsWith('%');
|
||||
};
|
||||
|
||||
/**
|
||||
* Helper to classify SQL clauses into WHERE (for dimensions) and HAVING (for metrics)
|
||||
*/
|
||||
const classifySQLClauses = (
|
||||
sqlClauses: Record<string, string>,
|
||||
): { whereClause?: string; havingClause?: string } => {
|
||||
const whereClauses: string[] = [];
|
||||
const havingClauses: string[] = [];
|
||||
|
||||
Object.entries(sqlClauses).forEach(([colId, sqlClause]) => {
|
||||
if (isMetricColumn(colId)) {
|
||||
havingClauses.push(sqlClause);
|
||||
} else {
|
||||
whereClauses.push(sqlClause);
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
whereClause:
|
||||
whereClauses.length > 0 ? whereClauses.join(' AND ') : undefined,
|
||||
havingClause:
|
||||
havingClauses.length > 0 ? havingClauses.join(' AND ') : undefined,
|
||||
};
|
||||
};
|
||||
|
||||
if (formData.server_pagination) {
|
||||
// Add search filter if search text exists
|
||||
if (ownState.searchText && ownState?.searchColumn) {
|
||||
@@ -292,6 +444,39 @@ const buildQuery: BuildQuery<TableChartFormData> = (
|
||||
}
|
||||
}
|
||||
|
||||
if (isDownloadQuery) {
|
||||
// Apply any QueryFilterClause filters from ownState (e.g., server pagination search)
|
||||
if (ownState.filters?.length) {
|
||||
queryObject.filters = [
|
||||
...(queryObject.filters || []),
|
||||
...ownState.filters,
|
||||
];
|
||||
}
|
||||
|
||||
// Apply AG Grid filters converted to SQL WHERE/HAVING clauses
|
||||
if (ownState.sqlClauses) {
|
||||
const { whereClause, havingClause } = classifySQLClauses(
|
||||
ownState.sqlClauses as Record<string, string>,
|
||||
);
|
||||
|
||||
if (whereClause || havingClause) {
|
||||
queryObject.extras = {
|
||||
...queryObject.extras,
|
||||
...(whereClause && {
|
||||
where: queryObject.extras?.where
|
||||
? `${queryObject.extras.where} AND ${whereClause}`
|
||||
: whereClause,
|
||||
}),
|
||||
...(havingClause && {
|
||||
having: queryObject.extras?.having
|
||||
? `${queryObject.extras.having} AND ${havingClause}`
|
||||
: havingClause,
|
||||
}),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Now since row limit control is always visible even
|
||||
// in case of server pagination
|
||||
// we must use row limit from form data
|
||||
|
||||
@@ -33,6 +33,12 @@ import { TableChartFormData, TableChartProps } from './types';
|
||||
// must export something for the module to be exist in dev mode
|
||||
export { default as __hack__ } from './types';
|
||||
export * from './types';
|
||||
export {
|
||||
convertAgGridStateToOwnState,
|
||||
convertSortModel,
|
||||
convertColumnState,
|
||||
convertFilterModel,
|
||||
} from './stateConversion';
|
||||
|
||||
const metadata = new ChartMetadata({
|
||||
behaviors: [
|
||||
|
||||
@@ -0,0 +1,234 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import {
|
||||
BackendOwnState,
|
||||
QuerySortBy,
|
||||
type AgGridChartState,
|
||||
type AgGridSortModel,
|
||||
type AgGridFilterModel,
|
||||
type AgGridFilter,
|
||||
} from '@superset-ui/core';
|
||||
|
||||
/**
|
||||
* AG Grid text filter type to backend operator mapping
|
||||
*/
|
||||
const TEXT_FILTER_OPERATORS: Record<string, string> = {
|
||||
equals: '==',
|
||||
notEqual: '!=',
|
||||
contains: 'ILIKE',
|
||||
notContains: 'NOT ILIKE',
|
||||
startsWith: 'ILIKE',
|
||||
endsWith: 'ILIKE',
|
||||
};
|
||||
|
||||
/**
|
||||
* AG Grid number filter type to backend operator mapping
|
||||
*/
|
||||
const NUMBER_FILTER_OPERATORS: Record<string, string> = {
|
||||
equals: '==',
|
||||
notEqual: '!=',
|
||||
lessThan: '<',
|
||||
lessThanOrEqual: '<=',
|
||||
greaterThan: '>',
|
||||
greaterThanOrEqual: '>=',
|
||||
};
|
||||
|
||||
function getTextComparator(type: string, value: string): string {
|
||||
if (type === 'contains' || type === 'notContains') {
|
||||
return `%${value}%`;
|
||||
}
|
||||
if (type === 'startsWith') {
|
||||
return `${value}%`;
|
||||
}
|
||||
if (type === 'endsWith') {
|
||||
return `%${value}`;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts AG Grid sortModel to backend sortBy format
|
||||
*/
|
||||
export function convertSortModel(
|
||||
sortModel: AgGridSortModel[],
|
||||
): QuerySortBy[] | undefined {
|
||||
if (!sortModel || sortModel.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const sortItem = sortModel[0];
|
||||
return [
|
||||
{
|
||||
id: sortItem.colId,
|
||||
key: sortItem.colId,
|
||||
desc: sortItem.sort === 'desc',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts column order from AG Grid columnState
|
||||
*/
|
||||
export function convertColumnState(
|
||||
columnState: Array<{ colId: string }>,
|
||||
): string[] | undefined {
|
||||
if (!columnState || columnState.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return columnState.map(col => col.colId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts any AG Grid filter to a SQL WHERE/HAVING clause.
|
||||
* Recursively handles both simple filters (single condition) and complex filters (multiple conditions with AND/OR).
|
||||
*
|
||||
* Examples:
|
||||
* - Simple text: {filterType: 'text', type: 'contains', filter: 'abc'} → "column_name ILIKE '%abc%'"
|
||||
* - Simple number: {filterType: 'number', type: 'greaterThan', filter: 5} → "column_name > 5"
|
||||
* - Complex: {operator: 'AND', condition1: {type: 'greaterThan', filter: 1}, condition2: {type: 'lessThan', filter: 16}}
|
||||
* → "(column_name > 1 AND column_name < 16)"
|
||||
* - Set: {filterType: 'set', values: ['a', 'b']} → "column_name IN ('a', 'b')"
|
||||
*/
|
||||
function convertFilterToSQL(
|
||||
colId: string,
|
||||
filter: AgGridFilter,
|
||||
): string | null {
|
||||
// Complex filter: has operator and conditions
|
||||
if (
|
||||
filter.operator &&
|
||||
(filter.condition1 || filter.condition2 || filter.conditions)
|
||||
) {
|
||||
const conditions: string[] = [];
|
||||
|
||||
// Collect all conditions
|
||||
[filter.condition1, filter.condition2, ...(filter.conditions || [])]
|
||||
.filter(Boolean)
|
||||
.forEach(condition => {
|
||||
const sql = convertFilterToSQL(colId, condition!);
|
||||
if (sql) conditions.push(sql);
|
||||
});
|
||||
|
||||
if (conditions.length === 0) return null;
|
||||
if (conditions.length === 1) return conditions[0];
|
||||
|
||||
return `(${conditions.join(` ${filter.operator} `)})`;
|
||||
}
|
||||
|
||||
if (filter.filterType === 'text' && filter.filter && filter.type) {
|
||||
const op = TEXT_FILTER_OPERATORS[filter.type];
|
||||
const val = getTextComparator(filter.type, String(filter.filter));
|
||||
return op === 'ILIKE' || op === 'NOT ILIKE'
|
||||
? `${colId} ${op} '${val}'`
|
||||
: `${colId} ${op} '${filter.filter}'`;
|
||||
}
|
||||
|
||||
if (
|
||||
filter.filterType === 'number' &&
|
||||
filter.filter !== undefined &&
|
||||
filter.type
|
||||
) {
|
||||
const op = NUMBER_FILTER_OPERATORS[filter.type];
|
||||
return `${colId} ${op} ${filter.filter}`;
|
||||
}
|
||||
|
||||
if (filter.filterType === 'date' && filter.dateFrom && filter.type) {
|
||||
const op = NUMBER_FILTER_OPERATORS[filter.type];
|
||||
return `${colId} ${op} '${filter.dateFrom}'`;
|
||||
}
|
||||
|
||||
if (
|
||||
filter.filterType === 'set' &&
|
||||
Array.isArray(filter.values) &&
|
||||
filter.values.length > 0
|
||||
) {
|
||||
const values = filter.values.map((v: string) => `'${v}'`).join(', ');
|
||||
return `${colId} IN (${values})`;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts AG Grid filterModel to SQL WHERE/HAVING clauses.
|
||||
* All filters (simple and complex) are uniformly converted to SQL for consistent backend handling.
|
||||
*
|
||||
* Returns a map of column IDs to their SQL filter expressions.
|
||||
*/
|
||||
export function convertFilterModel(
|
||||
filterModel: AgGridFilterModel,
|
||||
): { sqlClauses?: Record<string, string> } | undefined {
|
||||
if (!filterModel || Object.keys(filterModel).length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const sqlClauses: Record<string, string> = {};
|
||||
|
||||
Object.entries(filterModel).forEach(([colId, filter]) => {
|
||||
const sqlClause = convertFilterToSQL(colId, filter);
|
||||
if (sqlClause) {
|
||||
sqlClauses[colId] = sqlClause;
|
||||
}
|
||||
});
|
||||
|
||||
if (Object.keys(sqlClauses).length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return { sqlClauses };
|
||||
}
|
||||
|
||||
/**
|
||||
* Base converter for AG Grid-based charts (table, pivot, etc.)
|
||||
* Converts AG Grid state to backend-compatible format.
|
||||
*
|
||||
* This can be extended by specific implementations (pivot) that need
|
||||
* additional conversion logic.
|
||||
*/
|
||||
export function convertAgGridStateToOwnState(
|
||||
agGridState: AgGridChartState,
|
||||
): Partial<BackendOwnState> {
|
||||
const ownState: Partial<BackendOwnState> = {};
|
||||
|
||||
const sortBy = convertSortModel(agGridState.sortModel);
|
||||
if (sortBy) {
|
||||
ownState.sortBy = sortBy;
|
||||
}
|
||||
|
||||
const columnOrder = convertColumnState(agGridState.columnState);
|
||||
if (columnOrder) {
|
||||
ownState.columnOrder = columnOrder;
|
||||
}
|
||||
|
||||
const filterConversion = convertFilterModel(agGridState.filterModel);
|
||||
if (filterConversion?.sqlClauses) {
|
||||
ownState.sqlClauses = filterConversion.sqlClauses;
|
||||
}
|
||||
|
||||
if (agGridState.pageSize !== undefined) {
|
||||
ownState.pageSize = agGridState.pageSize;
|
||||
}
|
||||
|
||||
if (agGridState.currentPage !== undefined) {
|
||||
ownState.currentPage = agGridState.currentPage;
|
||||
}
|
||||
|
||||
return ownState;
|
||||
}
|
||||
@@ -469,7 +469,7 @@ const transformProps = (
|
||||
queriesData = [],
|
||||
ownState: serverPaginationData,
|
||||
filterState,
|
||||
hooks: { setDataMask = () => {} },
|
||||
hooks: { setDataMask = () => {}, onChartStateChange },
|
||||
emitCrossFilters,
|
||||
theme,
|
||||
} = chartProps;
|
||||
@@ -737,6 +737,8 @@ const transformProps = (
|
||||
basicColorColumnFormatters,
|
||||
basicColorFormatters,
|
||||
formData,
|
||||
chartState: serverPaginationData?.chartState,
|
||||
onChartStateChange,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -34,6 +34,7 @@ import {
|
||||
Currency,
|
||||
JsonObject,
|
||||
Metric,
|
||||
AgGridChartState,
|
||||
} from '@superset-ui/core';
|
||||
import { GenericDataType } from '@apache-superset/core/api/core';
|
||||
import {
|
||||
@@ -176,6 +177,8 @@ export interface AgGridTableChartTransformedProps<
|
||||
basicColorFormatters?: { [Key: string]: BasicColorFormatterType }[];
|
||||
basicColorColumnFormatters?: { [Key: string]: BasicColorFormatterType }[];
|
||||
formData: TableChartFormData;
|
||||
onChartStateChange?: (chartState: JsonObject) => void;
|
||||
chartState?: AgGridChartState;
|
||||
}
|
||||
|
||||
export enum ColorSchemeEnum {
|
||||
|
||||
Reference in New Issue
Block a user