fix(ag-grid): fix conditional formatting theme colors and module extensibility (#35605)

This commit is contained in:
Gabriel Torres Ruiz
2025-10-22 21:39:00 -04:00
committed by GitHub
parent 79918a7939
commit 5e4a80e5d0
14 changed files with 243 additions and 82 deletions

View File

@@ -241,10 +241,21 @@ export const getColorFormatters = memoizeOne(
( (
columnConfig: ConditionalFormattingConfig[] | undefined, columnConfig: ConditionalFormattingConfig[] | undefined,
data: DataRecord[], data: DataRecord[],
theme?: Record<string, any>,
alpha?: boolean, alpha?: boolean,
) => ) =>
columnConfig?.reduce( columnConfig?.reduce(
(acc: ColorFormatters, config: ConditionalFormattingConfig) => { (acc: ColorFormatters, config: ConditionalFormattingConfig) => {
let resolvedColorScheme = config.colorScheme;
if (
theme &&
typeof config.colorScheme === 'string' &&
config.colorScheme.startsWith('color') &&
theme[config.colorScheme]
) {
resolvedColorScheme = theme[config.colorScheme] as string;
}
if ( if (
config?.column !== undefined && config?.column !== undefined &&
(config?.operator === Comparator.None || (config?.operator === Comparator.None ||
@@ -257,7 +268,7 @@ export const getColorFormatters = memoizeOne(
acc.push({ acc.push({
column: config?.column, column: config?.column,
getColorFromValue: getColorFunction( getColorFromValue: getColorFunction(
config, { ...config, colorScheme: resolvedColorScheme },
data.map(row => row[config.column!] as number), data.map(row => row[config.column!] as number),
alpha, alpha,
), ),

View File

@@ -218,3 +218,29 @@ test('handles missing theme gracefully', () => {
// Should still render without crashing // Should still render without crashing
expect(screen.getByTestId('ag-grid-react')).toBeInTheDocument(); expect(screen.getByTestId('ag-grid-react')).toBeInTheDocument();
}); });
test('merges theme overrides with default theme parameters', () => {
const themeOverrides = {
fontSize: 16,
headerBackgroundColor: '#custom-color',
};
render(
<ThemedAgGridReact
rowData={mockRowData}
columnDefs={mockColumnDefs}
themeOverrides={themeOverrides}
/>,
);
const agGrid = screen.getByTestId('ag-grid-react');
const theme = JSON.parse(agGrid.getAttribute('data-theme') || '{}');
// Custom overrides should be applied
expect(theme.fontSize).toBe(16);
expect(theme.headerBackgroundColor).toBe('#custom-color');
// Default theme parameters should still be present
expect(theme.foregroundColor).toBeDefined();
expect(theme.borderColor).toBeDefined();
});

View File

@@ -186,5 +186,5 @@ export {
// Re-export AgGridReact for ref types // Re-export AgGridReact for ref types
export { AgGridReact } from 'ag-grid-react'; export { AgGridReact } from 'ag-grid-react';
// Export the setup function for AG-Grid modules // Export the setup function and default modules for AG-Grid
export { setupAGGridModules } from './setupAGGridModules'; export { setupAGGridModules, defaultModules } from './setupAGGridModules';

View File

@@ -0,0 +1,110 @@
/**
* 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 { ModuleRegistry } from 'ag-grid-community';
import { setupAGGridModules, defaultModules } from './setupAGGridModules';
jest.mock('ag-grid-community', () => ({
ModuleRegistry: {
registerModules: jest.fn(),
},
ColumnAutoSizeModule: { moduleName: 'ColumnAutoSizeModule' },
ColumnHoverModule: { moduleName: 'ColumnHoverModule' },
RowAutoHeightModule: { moduleName: 'RowAutoHeightModule' },
RowStyleModule: { moduleName: 'RowStyleModule' },
PaginationModule: { moduleName: 'PaginationModule' },
CellStyleModule: { moduleName: 'CellStyleModule' },
TextFilterModule: { moduleName: 'TextFilterModule' },
NumberFilterModule: { moduleName: 'NumberFilterModule' },
DateFilterModule: { moduleName: 'DateFilterModule' },
ExternalFilterModule: { moduleName: 'ExternalFilterModule' },
CsvExportModule: { moduleName: 'CsvExportModule' },
ColumnApiModule: { moduleName: 'ColumnApiModule' },
RowApiModule: { moduleName: 'RowApiModule' },
CellApiModule: { moduleName: 'CellApiModule' },
RenderApiModule: { moduleName: 'RenderApiModule' },
ClientSideRowModelModule: { moduleName: 'ClientSideRowModelModule' },
CustomFilterModule: { moduleName: 'CustomFilterModule' },
}));
beforeEach(() => {
jest.clearAllMocks();
});
test('defaultModules exports an array of AG Grid modules', () => {
expect(Array.isArray(defaultModules)).toBe(true);
expect(defaultModules.length).toBeGreaterThan(0);
// Verify it contains expected modules
const moduleNames = defaultModules.map((m: any) => m.moduleName);
expect(moduleNames).toContain('ColumnAutoSizeModule');
expect(moduleNames).toContain('PaginationModule');
expect(moduleNames).toContain('ClientSideRowModelModule');
});
test('setupAGGridModules registers default modules when called without arguments', () => {
setupAGGridModules();
expect(ModuleRegistry.registerModules).toHaveBeenCalledTimes(1);
expect(ModuleRegistry.registerModules).toHaveBeenCalledWith(defaultModules);
});
test('setupAGGridModules registers default + additional modules when provided', () => {
const mockEnterpriseModule1 = { moduleName: 'MultiFilterModule' };
const mockEnterpriseModule2 = { moduleName: 'PivotModule' };
const additionalModules = [mockEnterpriseModule1, mockEnterpriseModule2];
setupAGGridModules(additionalModules);
expect(ModuleRegistry.registerModules).toHaveBeenCalledTimes(1);
const registeredModules = (ModuleRegistry.registerModules as jest.Mock).mock
.calls[0][0];
// Should contain all default modules
defaultModules.forEach(module => {
expect(registeredModules).toContain(module);
});
// Should contain additional modules
expect(registeredModules).toContain(mockEnterpriseModule1);
expect(registeredModules).toContain(mockEnterpriseModule2);
// Total length should be default + additional
expect(registeredModules.length).toBe(
defaultModules.length + additionalModules.length,
);
});
test('setupAGGridModules handles empty additional modules array', () => {
setupAGGridModules([]);
expect(ModuleRegistry.registerModules).toHaveBeenCalledTimes(1);
expect(ModuleRegistry.registerModules).toHaveBeenCalledWith(defaultModules);
});
test('setupAGGridModules does not mutate defaultModules array', () => {
const originalLength = defaultModules.length;
const mockEnterpriseModule = { moduleName: 'EnterpriseModule' };
setupAGGridModules([mockEnterpriseModule]);
// defaultModules should remain unchanged
expect(defaultModules.length).toBe(originalLength);
expect(defaultModules).not.toContain(mockEnterpriseModule);
});

View File

@@ -19,6 +19,7 @@
import { import {
ModuleRegistry, ModuleRegistry,
type Module,
ColumnAutoSizeModule, ColumnAutoSizeModule,
ColumnHoverModule, ColumnHoverModule,
RowAutoHeightModule, RowAutoHeightModule,
@@ -39,27 +40,29 @@ import {
} from 'ag-grid-community'; } from 'ag-grid-community';
/** /**
* Registers the AG-Grid modules required for Superset's table functionality. * Default AG Grid modules that are registered by default.
* This should be called once during application initialization. * These modules provide core AG Grid functionality.
*/ */
export const setupAGGridModules = () => { export const defaultModules: Module[] = [
ModuleRegistry.registerModules([ ColumnAutoSizeModule,
ColumnAutoSizeModule, ColumnHoverModule,
ColumnHoverModule, RowAutoHeightModule,
RowAutoHeightModule, RowStyleModule,
RowStyleModule, PaginationModule,
PaginationModule, CellStyleModule,
CellStyleModule, TextFilterModule,
TextFilterModule, NumberFilterModule,
NumberFilterModule, DateFilterModule,
DateFilterModule, ExternalFilterModule,
ExternalFilterModule, CsvExportModule,
CsvExportModule, ColumnApiModule,
ColumnApiModule, RowApiModule,
RowApiModule, CellApiModule,
CellApiModule, RenderApiModule,
RenderApiModule, ClientSideRowModelModule,
ClientSideRowModelModule, CustomFilterModule,
CustomFilterModule, ];
]);
export const setupAGGridModules = (additionalModules: Module[] = []) => {
ModuleRegistry.registerModules([...defaultModules, ...additionalModules]);
}; };

View File

@@ -181,6 +181,7 @@ export {
ThemedAgGridReact, ThemedAgGridReact,
type ThemedAgGridReactProps, type ThemedAgGridReactProps,
setupAGGridModules, setupAGGridModules,
defaultModules,
} from './ThemedAgGridReact'; } from './ThemedAgGridReact';
export { export {
CodeEditor, CodeEditor,

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

After

Width:  |  Height:  |  Size: 16 KiB

View File

@@ -74,6 +74,23 @@ function isPositiveNumber(value: string | number | null | undefined) {
); );
} }
const calculateDifferences = (
originalValue: number,
comparisonValue: number,
) => {
const valueDifference = originalValue - comparisonValue;
let percentDifferenceNum;
if (!originalValue && !comparisonValue) {
percentDifferenceNum = 0;
} else if (!originalValue || !comparisonValue) {
percentDifferenceNum = originalValue ? 1 : -1;
} else {
percentDifferenceNum =
(originalValue - comparisonValue) / Math.abs(comparisonValue);
}
return { valueDifference, percentDifferenceNum };
};
const processComparisonTotals = ( const processComparisonTotals = (
comparisonSuffix: string, comparisonSuffix: string,
totals?: DataRecord[], totals?: DataRecord[],
@@ -149,23 +166,6 @@ const getComparisonColFormatter = (
return formatter; return formatter;
}; };
const calculateDifferences = (
originalValue: number,
comparisonValue: number,
) => {
const valueDifference = originalValue - comparisonValue;
let percentDifferenceNum;
if (!originalValue && !comparisonValue) {
percentDifferenceNum = 0;
} else if (!originalValue || !comparisonValue) {
percentDifferenceNum = originalValue ? 1 : -1;
} else {
percentDifferenceNum =
(originalValue - comparisonValue) / Math.abs(comparisonValue);
}
return { valueDifference, percentDifferenceNum };
};
const processComparisonDataRecords = memoizeOne( const processComparisonDataRecords = memoizeOne(
function processComparisonDataRecords( function processComparisonDataRecords(
originalData: DataRecord[] | undefined, originalData: DataRecord[] | undefined,
@@ -471,6 +471,7 @@ const transformProps = (
filterState, filterState,
hooks: { setDataMask = () => {} }, hooks: { setDataMask = () => {} },
emitCrossFilters, emitCrossFilters,
theme,
} = chartProps; } = chartProps;
const { const {
@@ -494,6 +495,36 @@ const transformProps = (
const allowRearrangeColumns = true; const allowRearrangeColumns = true;
// Calculate time comparison settings early since they're used in multiple places
const isUsingTimeComparison =
!isEmpty(time_compare) &&
queryMode === QueryMode.Aggregate &&
comparison_type === ComparisonType.Values &&
isFeatureEnabled(FeatureFlag.TableV2TimeComparisonEnabled);
const nonCustomNorInheritShifts = ensureIsArray(formData.time_compare).filter(
(shift: string) => shift !== 'custom' && shift !== 'inherit',
);
const customOrInheritShifts = ensureIsArray(formData.time_compare).filter(
(shift: string) => shift === 'custom' || shift === 'inherit',
);
let timeOffsets: string[] = [];
if (isUsingTimeComparison && !isEmpty(nonCustomNorInheritShifts)) {
timeOffsets = nonCustomNorInheritShifts;
}
// Shifts for custom or inherit time comparison
if (isUsingTimeComparison && !isEmpty(customOrInheritShifts)) {
if (customOrInheritShifts.includes('custom')) {
timeOffsets = timeOffsets.concat([formData.start_date_offset]);
}
if (customOrInheritShifts.includes('inherit')) {
timeOffsets = timeOffsets.concat(['inherit']);
}
}
const calculateBasicStyle = ( const calculateBasicStyle = (
percentDifferenceNum: number, percentDifferenceNum: number,
colorOption: ColorSchemeEnum, colorOption: ColorSchemeEnum,
@@ -606,12 +637,6 @@ const transformProps = (
: undefined; : undefined;
}; };
const isUsingTimeComparison =
!isEmpty(time_compare) &&
queryMode === QueryMode.Aggregate &&
comparison_type === ComparisonType.Values &&
isFeatureEnabled(FeatureFlag.TableV2TimeComparisonEnabled);
let hasServerPageLengthChanged = false; let hasServerPageLengthChanged = false;
const pageLengthFromMap = serverPageLengthMap.get(slice_id); const pageLengthFromMap = serverPageLengthMap.get(slice_id);
@@ -624,28 +649,6 @@ const transformProps = (
const timeGrain = extractTimegrain(formData); const timeGrain = extractTimegrain(formData);
const nonCustomNorInheritShifts = ensureIsArray(formData.time_compare).filter(
(shift: string) => shift !== 'custom' && shift !== 'inherit',
);
const customOrInheritShifts = ensureIsArray(formData.time_compare).filter(
(shift: string) => shift === 'custom' || shift === 'inherit',
);
let timeOffsets: string[] = [];
if (isUsingTimeComparison && !isEmpty(nonCustomNorInheritShifts)) {
timeOffsets = nonCustomNorInheritShifts;
}
// Shifts for custom or inherit time comparison
if (isUsingTimeComparison && !isEmpty(customOrInheritShifts)) {
if (customOrInheritShifts.includes('custom')) {
timeOffsets = timeOffsets.concat([formData.start_date_offset]);
}
if (customOrInheritShifts.includes('inherit')) {
timeOffsets = timeOffsets.concat(['inherit']);
}
}
const comparisonSuffix = isUsingTimeComparison const comparisonSuffix = isUsingTimeComparison
? ensureIsArray(timeOffsets)[0] ? ensureIsArray(timeOffsets)[0]
: ''; : '';
@@ -685,7 +688,7 @@ const transformProps = (
const basicColorFormatters = const basicColorFormatters =
comparisonColorEnabled && getBasicColorFormatter(baseQuery?.data, columns); comparisonColorEnabled && getBasicColorFormatter(baseQuery?.data, columns);
const columnColorFormatters = const columnColorFormatters =
getColorFormatters(conditionalFormatting, passedData) ?? []; getColorFormatters(conditionalFormatting, passedData, theme) ?? [];
const basicColorColumnFormatters = getBasicColorFormatterForColumn( const basicColorColumnFormatters = getBasicColorFormatterForColumn(
baseQuery?.data, baseQuery?.data,

View File

@@ -43,6 +43,7 @@ export default function transformProps(
rawFormData, rawFormData,
hooks, hooks,
datasource: { currencyFormats = {}, columnFormats = {} }, datasource: { currencyFormats = {}, columnFormats = {} },
theme,
} = chartProps; } = chartProps;
const { const {
metricNameFontSize, metricNameFontSize,
@@ -105,7 +106,7 @@ export default function transformProps(
const defaultColorFormatters = [] as ColorFormatters; const defaultColorFormatters = [] as ColorFormatters;
const colorThresholdFormatters = const colorThresholdFormatters =
getColorFormatters(conditionalFormatting, data, false) ?? getColorFormatters(conditionalFormatting, data, theme, false) ??
defaultColorFormatters; defaultColorFormatters;
return { return {
width, width,

View File

@@ -81,6 +81,7 @@ export default function transformProps(chartProps: ChartProps<QueryFormData>) {
filterState, filterState,
datasource: { verboseMap = {}, columnFormats = {}, currencyFormats = {} }, datasource: { verboseMap = {}, columnFormats = {}, currencyFormats = {} },
emitCrossFilters, emitCrossFilters,
theme,
} = chartProps; } = chartProps;
const { data, colnames, coltypes } = queriesData[0]; const { data, colnames, coltypes } = queriesData[0];
const { const {
@@ -141,7 +142,11 @@ export default function transformProps(chartProps: ChartProps<QueryFormData>) {
}, },
{}, {},
); );
const metricColorFormatters = getColorFormatters(conditionalFormatting, data); const metricColorFormatters = getColorFormatters(
conditionalFormatting,
data,
theme,
);
return { return {
width, width,

View File

@@ -469,6 +469,7 @@ const transformProps = (
onContextMenu, onContextMenu,
}, },
emitCrossFilters, emitCrossFilters,
theme,
} = chartProps; } = chartProps;
const formData = merge( const formData = merge(
@@ -682,7 +683,7 @@ const transformProps = (
const basicColorFormatters = const basicColorFormatters =
comparisonColorEnabled && getBasicColorFormatter(baseQuery?.data, columns); comparisonColorEnabled && getBasicColorFormatter(baseQuery?.data, columns);
const columnColorFormatters = const columnColorFormatters =
getColorFormatters(conditionalFormatting, passedData) ?? getColorFormatters(conditionalFormatting, passedData, theme) ??
defaultColorFormatters; defaultColorFormatters;
const basicColorColumnFormatters = getBasicColorFormatterForColumn( const basicColorColumnFormatters = getBasicColorFormatterForColumn(

View File

@@ -76,7 +76,7 @@ export default memo(function ColumnConfigItem({
alignItems: 'center', alignItems: 'center',
position: 'absolute', position: 'absolute',
right: 3 * sizeUnit, right: 3 * sizeUnit,
top: 3 * sizeUnit, top: 4 * sizeUnit,
transform: 'translateY(-50%)', transform: 'translateY(-50%)',
gap: sizeUnit, gap: sizeUnit,
color: theme.colorTextSecondary, color: theme.colorTextSecondary,

View File

@@ -17,7 +17,7 @@
* under the License. * under the License.
*/ */
import { useMemo, useState, useEffect } from 'react'; import { useMemo, useState, useEffect } from 'react';
import { styled, SupersetTheme, t, useTheme } from '@superset-ui/core'; import { styled, t } from '@superset-ui/core';
import { GenericDataType } from '@apache-superset/core/api/core'; import { GenericDataType } from '@apache-superset/core/api/core';
import { import {
Comparator, Comparator,
@@ -56,10 +56,11 @@ const JustifyEnd = styled.div`
justify-content: flex-end; justify-content: flex-end;
`; `;
const colorSchemeOptions = (theme: SupersetTheme) => [ // Use theme token names instead of hex values to support theme switching
{ value: theme.colorSuccessBg, label: t('success') }, const colorSchemeOptions = () => [
{ value: theme.colorWarningBg, label: t('alert') }, { value: 'colorSuccessBg', label: t('success') },
{ value: theme.colorErrorBg, label: t('error') }, { value: 'colorWarningBg', label: t('alert') },
{ value: 'colorErrorBg', label: t('error') },
]; ];
const operatorOptions = [ const operatorOptions = [
@@ -238,9 +239,8 @@ export const FormattingPopoverContent = ({
columns: { label: string; value: string; dataType: GenericDataType }[]; columns: { label: string; value: string; dataType: GenericDataType }[];
extraColorChoices?: { label: string; value: string }[]; extraColorChoices?: { label: string; value: string }[];
}) => { }) => {
const theme = useTheme();
const [form] = Form.useForm(); const [form] = Form.useForm();
const colorScheme = colorSchemeOptions(theme); const colorScheme = colorSchemeOptions();
const [showOperatorFields, setShowOperatorFields] = useState( const [showOperatorFields, setShowOperatorFields] = useState(
config === undefined || config === undefined ||
(config?.colorScheme !== ColorSchemeEnum.Green && (config?.colorScheme !== ColorSchemeEnum.Green &&