This is an automated email from the ASF dual-hosted git repository.
rusackas pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/superset.git
The following commit(s) were added to refs/heads/master by this push:
new 067a356b6f0 fix(chart): include echart_options in Matrixify
render-trigger allowlist (#43946)
067a356b6f0 is described below
commit 067a356b6f0d4ee619c9dcf93831a7f7957bf187
Author: Evan Rusackas <[email protected]>
AuthorDate: Tue Sep 8 13:24:13 2026 -0700
fix(chart): include echart_options in Matrixify render-trigger allowlist
(#43946)
Co-authored-by: Claude Opus 4.8 <[email protected]>
---
.../src/chart/components/StatefulChart.test.tsx | 98 ++++++++++++++++++++++
.../src/chart/components/StatefulChart.tsx | 30 +++++++
2 files changed, 128 insertions(+)
diff --git
a/superset-frontend/packages/superset-ui-core/src/chart/components/StatefulChart.test.tsx
b/superset-frontend/packages/superset-ui-core/src/chart/components/StatefulChart.test.tsx
index 7d671f3ff5e..ff40b69c343 100644
---
a/superset-frontend/packages/superset-ui-core/src/chart/components/StatefulChart.test.tsx
+++
b/superset-frontend/packages/superset-ui-core/src/chart/components/StatefulChart.test.tsx
@@ -566,6 +566,104 @@ test('should NOT refetch data when other string-based
renderTrigger controls cha
});
});
+test('should NOT refetch data when echart_options (string-based renderTrigger
control) changes', async () => {
+ // Matches how the Timeseries/MixedTimeseries control panels reference this
+ // shared control: a bare string, e.g. ['echart_options'].
+ const controlPanelConfig = {
+ controlPanelSections: [
+ {
+ controlSetRows: [['echart_options']],
+ },
+ ],
+ };
+
+ jest.mocked(getChartControlPanelRegistry).mockReturnValue({
+ get: jest.fn().mockReturnValue(controlPanelConfig),
+ } as unknown as ReturnType<typeof getChartControlPanelRegistry>);
+
+ const formDataWithEchartOptions = {
+ ...mockFormData,
+ echart_options: '{}',
+ };
+
+ const { rerender, getByTestId } = render(
+ <StatefulChart
+ formData={formDataWithEchartOptions}
+ chartType="test_chart"
+ />,
+ );
+
+ await waitFor(() => {
+ expect(mockChartClient.client.post).toHaveBeenCalledTimes(1);
+ });
+
+ // Edit the ECharts Options field (e.g. from the Customize tab while the
+ // chart is part of a Matrixify grid cell).
+ const updatedFormData = {
+ ...formDataWithEchartOptions,
+ echart_options: '{"title": {"text": "My Chart"}}',
+ };
+
+ rerender(<StatefulChart formData={updatedFormData} chartType="test_chart"
/>);
+
+ await waitFor(() => {
+ // Should NOT refetch data - echart_options is a renderTrigger control
+ expect(mockChartClient.client.post).toHaveBeenCalledTimes(1);
+ // But should re-render with the new formData
+ expect(getByTestId('super-chart')).toHaveTextContent(
+ JSON.stringify(updatedFormData),
+ );
+ });
+});
+
+test('should refetch when a chart overrides a shared renderTrigger control to
renderTrigger: false', async () => {
+ // Matches Country Map's controlPanel.controlOverrides, which sets
+ // linear_color_scheme to renderTrigger: false because it drives the
+ // choropleth data query rather than just styling.
+ const controlPanelConfig = {
+ controlPanelSections: [
+ {
+ controlSetRows: [['linear_color_scheme']],
+ },
+ ],
+ controlOverrides: {
+ linear_color_scheme: {
+ renderTrigger: false,
+ },
+ },
+ };
+
+ jest.mocked(getChartControlPanelRegistry).mockReturnValue({
+ get: jest.fn().mockReturnValue(controlPanelConfig),
+ } as unknown as ReturnType<typeof getChartControlPanelRegistry>);
+
+ const formDataWithColorScheme = {
+ ...mockFormData,
+ linear_color_scheme: 'schemeA',
+ };
+
+ const { rerender } = render(
+ <StatefulChart formData={formDataWithColorScheme} chartType="test_chart"
/>,
+ );
+
+ await waitFor(() => {
+ expect(mockChartClient.client.post).toHaveBeenCalledTimes(1);
+ });
+
+ const updatedFormData = {
+ ...formDataWithColorScheme,
+ linear_color_scheme: 'schemeB',
+ };
+
+ rerender(<StatefulChart formData={updatedFormData} chartType="test_chart"
/>);
+
+ await waitFor(() => {
+ // Should refetch because this chart's controlOverrides mark the control
+ // as data-affecting, overriding the shared-control fallback.
+ expect(mockChartClient.client.post).toHaveBeenCalledTimes(2);
+ });
+});
+
test('should refetch when string control is NOT in
RENDER_TRIGGER_SHARED_CONTROLS', async () => {
// Control panel with a string control that is NOT in the renderTrigger set
const controlPanelConfig = {
diff --git
a/superset-frontend/packages/superset-ui-core/src/chart/components/StatefulChart.tsx
b/superset-frontend/packages/superset-ui-core/src/chart/components/StatefulChart.tsx
index e6c9103594b..723b33f7d31 100644
---
a/superset-frontend/packages/superset-ui-core/src/chart/components/StatefulChart.tsx
+++
b/superset-frontend/packages/superset-ui-core/src/chart/components/StatefulChart.tsx
@@ -50,6 +50,9 @@ type LoadingState = 'uninitialized' | 'loading' | 'loaded' |
'error';
* This list is needed because string-based control references (e.g.,
['zoomable'])
* cannot be introspected for their renderTrigger property without importing
* sharedControls, which would create a circular dependency.
+ *
+ * Keep this list in sync with the `renderTrigger: true` entries in
+ * @superset-ui/chart-controls's sharedControls.tsx.
*/
const RENDER_TRIGGER_SHARED_CONTROLS = new Set([
'zoomable',
@@ -57,6 +60,11 @@ const RENDER_TRIGGER_SHARED_CONTROLS = new Set([
'time_shift_color',
'y_axis_format',
'currency_format',
+ 'color_picker',
+ 'linear_color_scheme',
+ 'x_axis_time_format',
+ 'x_axis_number_format',
+ 'echart_options',
]);
/**
@@ -111,6 +119,28 @@ function shouldRefetchData(
}
});
+ // Individual chart types can override a shared control's renderTrigger
+ // behavior (e.g., Country Map sets `linear_color_scheme` to
+ // renderTrigger: false because it drives the choropleth query, not just
+ // styling). Apply those overrides on top of the shared-control fallback
+ // so such controls still trigger a refetch for that chart type.
+ const { controlOverrides } = controlPanel;
+ if (controlOverrides) {
+ Object.entries(controlOverrides).forEach(([controlName, override]) => {
+ if (
+ override &&
+ typeof override === 'object' &&
+ 'renderTrigger' in override
+ ) {
+ if ((override as { renderTrigger?: boolean }).renderTrigger) {
+ renderTriggerControls.add(controlName);
+ } else {
+ renderTriggerControls.delete(controlName);
+ }
+ }
+ });
+ }
+
// Check which fields changed
const changedFields = Object.keys(nextFormData).filter(
key =>