diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/Bar/controlPanel.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/Bar/controlPanel.tsx index 267520975cd..2986919303b 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/Bar/controlPanel.tsx +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/Bar/controlPanel.tsx @@ -35,7 +35,7 @@ import { minorTicks, richTooltipSection, seriesOrderSection, - showValueSection, + showValueSectionWithoutStream, truncateXAxis, xAxisBounds, xAxisLabelRotation, @@ -327,7 +327,7 @@ const config: ControlPanelConfig = { ...seriesOrderSection, ['color_scheme'], ['time_shift_color'], - ...showValueSection, + ...showValueSectionWithoutStream, [ { name: 'stackDimension', @@ -375,11 +375,18 @@ const config: ControlPanelConfig = { ], }, ], - formDataOverrides: formData => ({ - ...formData, - metrics: getStandardizedControls().popAllMetrics(), - groupby: getStandardizedControls().popAllColumns(), - }), + formDataOverrides: formData => { + // Reset stack to null if it's Stream when switching to Bar chart + const formDataWithStack = formData as Record; + return { + ...formData, + metrics: getStandardizedControls().popAllMetrics(), + groupby: getStandardizedControls().popAllColumns(), + ...(formDataWithStack.stack === StackControlsValue.Stream && { + stack: null, + }), + }; + }, }; export default config; diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/constants.ts b/superset-frontend/plugins/plugin-chart-echarts/src/constants.ts index 718ca01a3b2..036f4324757 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/constants.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/constants.ts @@ -76,6 +76,14 @@ export const AreaChartStackControlOptions: [ Exclude, ][] = [...StackControlOptions, [StackControlsValue.Expand, t('Expand')]]; +export const StackControlOptionsWithoutStream: [ + JsonValue, + Exclude, +][] = [ + [null, t('None')], + [StackControlsValue.Stack, t('Stack')], +]; + export const TIMEGRAIN_TO_TIMESTAMP = { [TimeGranularity.HOUR]: 3600 * 1000, [TimeGranularity.DAY]: 3600 * 1000 * 24, diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/controls.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/controls.tsx index 1d407c6a607..22db2ffed7c 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/controls.tsx +++ b/superset-frontend/plugins/plugin-chart-echarts/src/controls.tsx @@ -28,7 +28,11 @@ import { SORT_SERIES_CHOICES, sharedControls, } from '@superset-ui/chart-controls'; -import { DEFAULT_LEGEND_FORM_DATA, StackControlOptions } from './constants'; +import { + DEFAULT_LEGEND_FORM_DATA, + StackControlOptions, + StackControlOptionsWithoutStream, +} from './constants'; import { DEFAULT_FORM_DATA } from './Timeseries/constants'; import { defaultXAxis } from './defaults'; @@ -148,6 +152,14 @@ export const stackControl: ControlSetItem = { }, }; +export const stackControlWithoutStream: ControlSetItem = { + ...stackControl, + config: { + ...stackControl.config, + choices: StackControlOptionsWithoutStream, + }, +}; + export const onlyTotalControl: ControlSetItem = { name: 'only_total', config: { @@ -193,6 +205,13 @@ export const showValueSectionWithoutStack: ControlSetRow[] = [ [onlyTotalControl], ]; +export const showValueSectionWithoutStream: ControlSetRow[] = [ + [showValueControl], + [stackControlWithoutStream], + [onlyTotalControl], + [percentageThresholdControl], +]; + const richTooltipControl: ControlSetItem = { name: 'rich_tooltip', config: { diff --git a/superset-frontend/plugins/plugin-chart-echarts/test/Timeseries/Bar/controlPanel.test.ts b/superset-frontend/plugins/plugin-chart-echarts/test/Timeseries/Bar/controlPanel.test.ts index 1128af3edb6..2b56d6ef0ad 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/test/Timeseries/Bar/controlPanel.test.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/test/Timeseries/Bar/controlPanel.test.ts @@ -17,188 +17,204 @@ * under the License. */ import controlPanel from '../../../src/Timeseries/Regular/Bar/controlPanel'; +import { + StackControlOptionsWithoutStream, + StackControlsValue, +} from '../../../src/constants'; -describe('Bar Chart Control Panel', () => { - describe('x_axis_time_format control', () => { - test('should include x_axis_time_format control in the panel', () => { - const config = controlPanel; +const config = controlPanel; - // Look for x_axis_time_format control in all sections and rows - let foundTimeFormatControl = false; - - for (const section of config.controlPanelSections) { - if (section && section.controlSetRows) { - for (const row of section.controlSetRows) { - for (const control of row) { - if ( - typeof control === 'object' && - control !== null && - 'name' in control && - control.name === 'x_axis_time_format' - ) { - foundTimeFormatControl = true; - break; - } - } - if (foundTimeFormatControl) break; +const getControl = (controlName: string) => { + for (const section of config.controlPanelSections) { + if (section && section.controlSetRows) { + for (const row of section.controlSetRows) { + for (const control of row) { + if ( + typeof control === 'object' && + control !== null && + 'name' in control && + control.name === controlName + ) { + return control; } - if (foundTimeFormatControl) break; } } + } + } - expect(foundTimeFormatControl).toBe(true); - }); + return null; +}; - test('should have correct default value for x_axis_time_format', () => { - const config = controlPanel; - - // Find the x_axis_time_format control - let timeFormatControl: any = null; - - for (const section of config.controlPanelSections) { - if (section && section.controlSetRows) { - for (const row of section.controlSetRows) { - for (const control of row) { - if ( - typeof control === 'object' && - control !== null && - 'name' in control && - control.name === 'x_axis_time_format' - ) { - timeFormatControl = control; - break; - } - } - if (timeFormatControl) break; - } - if (timeFormatControl) break; - } - } - - expect(timeFormatControl).toBeDefined(); - expect(timeFormatControl.config).toBeDefined(); - expect(timeFormatControl.config.default).toBe('smart_date'); - }); - - test('should have visibility function for x_axis_time_format', () => { - const config = controlPanel; - - // Find the x_axis_time_format control - let timeFormatControl: any = null; - - for (const section of config.controlPanelSections) { - if (section && section.controlSetRows) { - for (const row of section.controlSetRows) { - for (const control of row) { - if ( - typeof control === 'object' && - control !== null && - 'name' in control && - control.name === 'x_axis_time_format' - ) { - timeFormatControl = control; - break; - } - } - if (timeFormatControl) break; - } - if (timeFormatControl) break; - } - } - - expect(timeFormatControl).toBeDefined(); - expect(timeFormatControl.config.visibility).toBeDefined(); - expect(typeof timeFormatControl.config.visibility).toBe('function'); - - // The visibility function exists - the exact logic is tested implicitly through UI behavior - // The important part is that the control has proper visibility configuration - }); - - test('should have proper control configuration', () => { - const config = controlPanel; - - // Find the x_axis_time_format control - let timeFormatControl: any = null; - - for (const section of config.controlPanelSections) { - if (section && section.controlSetRows) { - for (const row of section.controlSetRows) { - for (const control of row) { - if ( - typeof control === 'object' && - control !== null && - 'name' in control && - control.name === 'x_axis_time_format' - ) { - timeFormatControl = control; - break; - } - } - if (timeFormatControl) break; - } - if (timeFormatControl) break; - } - } - - expect(timeFormatControl).toBeDefined(); - expect(timeFormatControl.config).toMatchObject({ - default: 'smart_date', - disableStash: true, - resetOnHide: false, - }); - - // Should have a description that includes D3 time format docs - expect(timeFormatControl.config.description).toContain('D3'); - }); - }); - - describe('Control panel structure for bar charts', () => { - test('should have Chart Orientation section', () => { - const config = controlPanel; - - const orientationSection = config.controlPanelSections.find( - section => section && section.label === 'Chart Orientation', - ); - - expect(orientationSection).toBeDefined(); - expect(orientationSection!.expanded).toBe(true); - }); - - test('should have Chart Options section with X Axis controls', () => { - const config = controlPanel; - - const chartOptionsSection = config.controlPanelSections.find( - section => section && section.label === 'Chart Options', - ); - - expect(chartOptionsSection).toBeDefined(); - expect(chartOptionsSection!.expanded).toBe(true); - - // Should contain X Axis subsection header - this is sufficient proof - expect(chartOptionsSection!.controlSetRows).toBeDefined(); - expect(chartOptionsSection!.controlSetRows!.length).toBeGreaterThan(0); - }); - - test('should have proper form data overrides', () => { - const config = controlPanel; - - expect(config.formDataOverrides).toBeDefined(); - expect(typeof config.formDataOverrides).toBe('function'); - - // Test the form data override function - const mockFormData = { - datasource: '1__table', - viz_type: 'echarts_timeseries_bar', - metrics: ['test_metric'], - groupby: ['test_column'], - other_field: 'test', - }; - - const result = config.formDataOverrides!(mockFormData); - - expect(result).toHaveProperty('metrics'); - expect(result).toHaveProperty('groupby'); - expect(result).toHaveProperty('other_field', 'test'); - }); - }); +// Mock getStandardizedControls +jest.mock('@superset-ui/chart-controls', () => { + const actual = jest.requireActual('@superset-ui/chart-controls'); + return { + ...actual, + getStandardizedControls: jest.fn(() => ({ + popAllMetrics: jest.fn(() => []), + popAllColumns: jest.fn(() => []), + })), + }; +}); + +test('should include x_axis_time_format control in the panel', () => { + const timeFormatControl = getControl('x_axis_time_format'); + expect(timeFormatControl).toBeDefined(); +}); + +test('should have correct default value for x_axis_time_format', () => { + const timeFormatControl: any = getControl('x_axis_time_format'); + expect(timeFormatControl).toBeDefined(); + expect(timeFormatControl.config).toBeDefined(); + expect(timeFormatControl.config.default).toBe('smart_date'); +}); + +test('should have visibility function for x_axis_time_format', () => { + const timeFormatControl: any = getControl('x_axis_time_format'); + expect(timeFormatControl).toBeDefined(); + expect(timeFormatControl.config.visibility).toBeDefined(); + expect(typeof timeFormatControl.config.visibility).toBe('function'); +}); + +test('should have proper control configuration for x_axis_time_format', () => { + const timeFormatControl: any = getControl('x_axis_time_format'); + expect(timeFormatControl).toBeDefined(); + expect(timeFormatControl.config).toMatchObject({ + default: 'smart_date', + disableStash: true, + resetOnHide: false, + }); + expect(timeFormatControl.config.description).toContain('D3'); +}); + +test('should have Chart Orientation section', () => { + const orientationSection = config.controlPanelSections.find( + section => section && section.label === 'Chart Orientation', + ); + expect(orientationSection).toBeDefined(); + expect(orientationSection!.expanded).toBe(true); +}); + +test('should have Chart Options section with X Axis controls', () => { + const chartOptionsSection = config.controlPanelSections.find( + section => section && section.label === 'Chart Options', + ); + expect(chartOptionsSection).toBeDefined(); + expect(chartOptionsSection!.expanded).toBe(true); + expect(chartOptionsSection!.controlSetRows).toBeDefined(); + expect(chartOptionsSection!.controlSetRows!.length).toBeGreaterThan(0); +}); + +test('should have proper form data overrides', () => { + expect(config.formDataOverrides).toBeDefined(); + expect(typeof config.formDataOverrides).toBe('function'); + + const mockFormData = { + datasource: '1__table', + viz_type: 'echarts_timeseries_bar', + metrics: ['test_metric'], + groupby: ['test_column'], + other_field: 'test', + }; + + const result = config.formDataOverrides!(mockFormData); + + expect(result).toHaveProperty('metrics'); + expect(result).toHaveProperty('groupby'); + expect(result).toHaveProperty('other_field', 'test'); +}); + +test('should include stack control in the panel', () => { + const stackControl = getControl('stack'); + expect(stackControl).toBeDefined(); +}); + +test('should use StackControlOptionsWithoutStream for stack control', () => { + const stackControl: any = getControl('stack'); + expect(stackControl).toBeDefined(); + expect(stackControl.config).toBeDefined(); + expect(stackControl.config.choices).toBe(StackControlOptionsWithoutStream); +}); + +test('should not include Stream option in stack control choices', () => { + const stackControl: any = getControl('stack'); + expect(stackControl).toBeDefined(); + const { choices } = stackControl.config; + const streamOption = choices.find( + (choice: any[]) => choice[0] === StackControlsValue.Stream, + ); + expect(streamOption).toBeUndefined(); +}); + +test('should include None and Stack options in stack control choices', () => { + const stackControl: any = getControl('stack'); + expect(stackControl).toBeDefined(); + const { choices } = stackControl.config; + const noneOption = choices.find((choice: any[]) => choice[0] === null); + const stackOption = choices.find( + (choice: any[]) => choice[0] === StackControlsValue.Stack, + ); + expect(noneOption).toBeDefined(); + expect(stackOption).toBeDefined(); +}); + +test('should have correct default value for stack control', () => { + const stackControl: any = getControl('stack'); + expect(stackControl).toBeDefined(); + expect(stackControl.config.default).toBe(null); +}); + +test('should reset stack to null when formData has Stream value', () => { + const mockFormData = { + datasource: '1__table', + viz_type: 'echarts_timeseries_bar', + metrics: ['test_metric'], + groupby: ['test_column'], + stack: StackControlsValue.Stream, + }; + + const result = config.formDataOverrides!(mockFormData); + + expect(result.stack).toBe(null); +}); + +test('should preserve stack value when formData has Stack value', () => { + const mockFormData = { + datasource: '1__table', + viz_type: 'echarts_timeseries_bar', + metrics: ['test_metric'], + groupby: ['test_column'], + stack: StackControlsValue.Stack, + }; + + const result = config.formDataOverrides!(mockFormData); + + expect(result.stack).toBe(StackControlsValue.Stack); +}); + +test('should preserve stack value when formData has null value', () => { + const mockFormData = { + datasource: '1__table', + viz_type: 'echarts_timeseries_bar', + metrics: ['test_metric'], + groupby: ['test_column'], + stack: null, + }; + + const result = config.formDataOverrides!(mockFormData); + + expect(result.stack).toBe(null); +}); + +test('should preserve stack value when formData does not have stack property', () => { + const mockFormData = { + datasource: '1__table', + viz_type: 'echarts_timeseries_bar', + metrics: ['test_metric'], + groupby: ['test_column'], + }; + + const result = config.formDataOverrides!(mockFormData); + + expect(result).not.toHaveProperty('stack'); });