diff --git a/superset-frontend/packages/superset-ui-chart-controls/src/types.ts b/superset-frontend/packages/superset-ui-chart-controls/src/types.ts index 69ab4f160570..cdf3e441d790 100644 --- a/superset-frontend/packages/superset-ui-chart-controls/src/types.ts +++ b/superset-frontend/packages/superset-ui-chart-controls/src/types.ts @@ -364,6 +364,10 @@ export interface ControlPanelConfig { standardizedFormData: StandardizedFormDataInterface; }, ) => QueryFormData; + updateStandardizedState?: ( + prevState: StandardizedState, + currState: StandardizedState, + ) => StandardizedState; } export type ControlOverrides = { diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberTotal/controlPanel.ts b/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberTotal/controlPanel.ts index c715c8f232b0..94dd45830556 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberTotal/controlPanel.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberTotal/controlPanel.ts @@ -100,4 +100,8 @@ export default { ...formData, metric: formData.standardizedFormData.standardizedState.metrics[0], }), + updateStandardizedState: (prevState, currState) => ({ + ...currState, + metrics: [currState.metrics[0], ...prevState.metrics.slice(1)], + }), } as ControlPanelConfig; diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberWithTrendline/controlPanel.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberWithTrendline/controlPanel.tsx index c61ec6d0462b..0ea0a96229e8 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberWithTrendline/controlPanel.tsx +++ b/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberWithTrendline/controlPanel.tsx @@ -274,6 +274,10 @@ const config: ControlPanelConfig = { ...formData, metric: formData.standardizedFormData.standardizedState.metrics[0], }), + updateStandardizedState: (prevState, currState) => ({ + ...currState, + metrics: [currState.metrics[0], ...prevState.metrics.slice(1)], + }), }; export default config; diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Funnel/controlPanel.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Funnel/controlPanel.tsx index 39ce57d498eb..b23cf2ed9793 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Funnel/controlPanel.tsx +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Funnel/controlPanel.tsx @@ -148,6 +148,10 @@ const config: ControlPanelConfig = { metric: formData.standardizedFormData.standardizedState.metrics[0], groupby: formData.standardizedFormData.standardizedState.columns, }), + updateStandardizedState: (prevState, currState) => ({ + ...currState, + metrics: [currState.metrics[0], ...prevState.metrics.slice(1)], + }), }; export default config; diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Gauge/controlPanel.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Gauge/controlPanel.tsx index d9dbb3025e8b..bb727888e59a 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Gauge/controlPanel.tsx +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Gauge/controlPanel.tsx @@ -313,6 +313,10 @@ const config: ControlPanelConfig = { metric: formData.standardizedFormData.standardizedState.metrics[0], groupby: formData.standardizedFormData.standardizedState.columns, }), + updateStandardizedState: (prevState, currState) => ({ + ...currState, + metrics: [currState.metrics[0], ...prevState.metrics.slice(1)], + }), }; export default config; diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Graph/controlPanel.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Graph/controlPanel.tsx index 39547f683a8d..218b1d0335ea 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Graph/controlPanel.tsx +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Graph/controlPanel.tsx @@ -324,6 +324,10 @@ const controlPanel: ControlPanelConfig = { ...formData, metric: formData.standardizedFormData.standardizedState.metrics[0], }), + updateStandardizedState: (prevState, currState) => ({ + ...currState, + metrics: [currState.metrics[0], ...prevState.metrics.slice(1)], + }), }; export default controlPanel; diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Pie/controlPanel.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Pie/controlPanel.tsx index 43e1ab6cba4d..7b948208a457 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Pie/controlPanel.tsx +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Pie/controlPanel.tsx @@ -260,6 +260,10 @@ const config: ControlPanelConfig = { row_limit: ensureIsInt(formData.row_limit, 100) >= 100 ? 100 : formData.row_limit, }), + updateStandardizedState: (prevState, currState) => ({ + ...currState, + metrics: [currState.metrics[0], ...prevState.metrics.slice(1)], + }), }; export default config; diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Tree/controlPanel.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Tree/controlPanel.tsx index 097a882fa3f2..c115f5c5c20d 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Tree/controlPanel.tsx +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Tree/controlPanel.tsx @@ -289,6 +289,10 @@ const controlPanel: ControlPanelConfig = { ...formData, metric: formData.standardizedFormData.standardizedState.metrics[0], }), + updateStandardizedState: (prevState, currState) => ({ + ...currState, + metrics: [currState.metrics[0], ...prevState.metrics.slice(1)], + }), }; export default controlPanel; diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Treemap/controlPanel.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Treemap/controlPanel.tsx index 8887377d2de5..e7cca1af263d 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Treemap/controlPanel.tsx +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Treemap/controlPanel.tsx @@ -142,6 +142,10 @@ const config: ControlPanelConfig = { metric: formData.standardizedFormData.standardizedState.metrics[0], groupby: formData.standardizedFormData.standardizedState.columns, }), + updateStandardizedState: (prevState, currState) => ({ + ...currState, + metrics: [currState.metrics[0], ...prevState.metrics.slice(1)], + }), }; export default config; diff --git a/superset-frontend/src/explore/controlUtils/standardizedFormData.test.tsx b/superset-frontend/src/explore/controlUtils/standardizedFormData.test.ts similarity index 74% rename from superset-frontend/src/explore/controlUtils/standardizedFormData.test.tsx rename to superset-frontend/src/explore/controlUtils/standardizedFormData.test.ts index e00bc58e8d0b..59333077ec25 100644 --- a/superset-frontend/src/explore/controlUtils/standardizedFormData.test.tsx +++ b/superset-frontend/src/explore/controlUtils/standardizedFormData.test.ts @@ -27,15 +27,46 @@ import { } from './standardizedFormData'; describe('should collect control values and create SFD', () => { - const sharedControlsFormData = {}; - Object.entries(sharedControls).forEach(([, names]) => { - names.forEach(name => { - sharedControlsFormData[name] = name; - }); - }); - const publicControlsFormData = Object.fromEntries( - publicControls.map((name, idx) => [[name], idx]), - ); + const sharedControlsFormData = { + // metrics + metric: 'm1', + metrics: ['m2'], + metric_2: 'm3', + // columns + groupby: ['c1'], + columns: ['c2'], + groupbyColumns: ['c3'], + groupbyRows: ['c4'], + }; + const publicControlsFormData = { + // time section + granularity_sqla: 'time_column', + time_grain_sqla: 'P1D', + time_range: '2000 : today', + // filters + adhoc_filters: [], + // subquery limit(series limit) + limit: 5, + // order by clause + timeseries_limit_metric: 'orderby_metric', + series_limit_metric: 'orderby_metric', + // desc or asc in order by clause + order_desc: true, + // outer query limit + row_limit: 100, + // x asxs column + x_axis: 'x_axis_column', + // advanced analytics - rolling window + rolling_type: 'sum', + rolling_periods: 1, + min_periods: 0, + // advanced analytics - time comparison + time_compare: '1 year ago', + comparison_type: 'values', + // advanced analytics - resample + resample_rule: '1D', + resample_method: 'zerofill', + }; const sourceMockFormData: QueryFormData = { ...sharedControlsFormData, ...publicControlsFormData, @@ -89,26 +120,45 @@ describe('should collect control values and create SFD', () => { }); }); - test('collect sharedControls', () => { - const sfd = new StandardizedFormData(sourceMockFormData); + test('should avoid to overlap', () => { + const sharedControlsSet = new Set(Object.keys(sharedControls)); + const publicControlsSet = new Set(publicControls); + expect( + [...sharedControlsSet].filter((x: string) => publicControlsSet.has(x)), + ).toEqual([]); + }); - expect(sfd.dumpSFD().standardizedState.metrics).toEqual( - sharedControls.metrics.map(controlName => controlName), - ); - expect(sfd.dumpSFD().standardizedState.columns).toEqual( - sharedControls.columns.map(controlName => controlName), + test('should collect all sharedControls', () => { + expect(Object.entries(sharedControlsFormData).length).toBe( + Object.entries(sharedControls).length, ); + const sfd = new StandardizedFormData(sourceMockFormData); + expect(sfd.serialize().standardizedState.metrics).toEqual([ + 'm1', + 'm2', + 'm3', + ]); + expect(sfd.serialize().standardizedState.columns).toEqual([ + 'c1', + 'c2', + 'c3', + 'c4', + ]); }); - test('should transform all publicControls', () => { + test('should transform all publicControls and sharedControls', () => { + expect(Object.entries(publicControlsFormData).length).toBe( + publicControls.length, + ); + const sfd = new StandardizedFormData(sourceMockFormData); const { formData } = sfd.transform('target_viz', sourceMockStore); - Object.entries(publicControlsFormData).forEach(([key]) => { + Object.entries(publicControlsFormData).forEach(([key, value]) => { expect(formData).toHaveProperty(key); + expect(value).toEqual(publicControlsFormData[key]); }); - Object.entries(sharedControls).forEach(([key, value]) => { - expect(formData[key]).toEqual(value); - }); + expect(formData.columns).toEqual(['c1', 'c2', 'c3', 'c4']); + expect(formData.metrics).toEqual(['m1', 'm2', 'm3']); }); test('should inherit standardizedFormData and memorizedFormData is LIFO', () => { @@ -156,6 +206,7 @@ describe('should transform form_data between table and bigNumberTotal', () => { const tableVizFormData = { datasource: '30__table', viz_type: 'table', + granularity_sqla: 'ds', time_grain_sqla: 'P1D', time_range: 'No filter', query_mode: 'aggregate', @@ -171,7 +222,6 @@ describe('should transform form_data between table and bigNumberTotal', () => { table_timestamp_format: 'smart_date', show_cell_bars: true, color_pn: true, - applied_time_extras: {}, url_params: { form_data_key: 'p3No_sqDW7k-kMTzlBPAPd9vwp1IXTf6stbyzjlrPPa0ninvdYUUiMC6F1iKit3Y', @@ -196,7 +246,9 @@ describe('should transform form_data between table and bigNumberTotal', () => { dataset_id: '30', }, }, - granularity_sqla: {}, + granularity_sqla: { + value: 'ds', + }, time_grain_sqla: { value: 'P1D', }, @@ -270,6 +322,22 @@ describe('should transform form_data between table and bigNumberTotal', () => { ); }); + test('get and has', () => { + // table -> bigNumberTotal + const sfd = new StandardizedFormData(tableVizFormData); + const { formData: bntFormData } = sfd.transform( + 'big_number_total', + tableVizStore, + ); + + // bigNumberTotal -> table + const sfd2 = new StandardizedFormData(bntFormData); + expect(sfd2.has('big_number_total')).toBeTruthy(); + expect(sfd2.has('table')).toBeTruthy(); + expect(sfd2.get('big_number_total').viz_type).toBe('big_number_total'); + expect(sfd2.get('table').viz_type).toBe('table'); + }); + test('transform', () => { // table -> bigNumberTotal const sfd = new StandardizedFormData(tableVizFormData); @@ -300,7 +368,7 @@ describe('should transform form_data between table and bigNumberTotal', () => { ); expect(tblFormData.viz_type).toBe('table'); expect(tblFormData.metrics).toEqual(['sum(sales)']); - expect(tblFormData.groupby).toEqual([]); + expect(tblFormData.groupby).toEqual(['name']); expect(tblFormData.time_range).toBe('2021 : 2022'); }); }); diff --git a/superset-frontend/src/explore/controlUtils/standardizedFormData.ts b/superset-frontend/src/explore/controlUtils/standardizedFormData.ts index c42ef95085f1..99b0220da334 100644 --- a/superset-frontend/src/explore/controlUtils/standardizedFormData.ts +++ b/superset-frontend/src/explore/controlUtils/standardizedFormData.ts @@ -16,6 +16,7 @@ * specific language governing permissions and limitations * under the License. */ +import { isEmpty, intersection } from 'lodash'; import { ensureIsArray, getChartControlPanelRegistry, @@ -29,38 +30,52 @@ import { import { getControlsState } from 'src/explore/store'; import { getFormDataFromControls } from './getFormDataFromControls'; -export const sharedControls: Record = { - metrics: ['metric', 'metrics', 'metric_2'], - columns: ['groupby', 'columns', 'groupbyColumns', 'groupbyRows'], +export const sharedControls: Record = { + // metrics + metric: 'metrics', // via sharedControls, scalar + metrics: 'metrics', // via sharedControls, array + metric_2: 'metrics', // via sharedControls, scalar + // columns + groupby: 'columns', // via sharedControls, array + columns: 'columns', // via sharedControls, array + groupbyColumns: 'columns', // via pivot table v2, array + groupbyRows: 'columns', // via pivot table v2, array }; +const sharedControlsMap: Record = { + metrics: [], + columns: [], +}; +Object.entries(sharedControls).forEach(([key, value]) => + sharedControlsMap[value].push(key), +); export const publicControls = [ // time section - 'granularity_sqla', - 'time_grain_sqla', - 'time_range', + 'granularity_sqla', // via sharedControls + 'time_grain_sqla', // via sharedControls + 'time_range', // via sharedControls // filters - 'adhoc_filters', + 'adhoc_filters', // via sharedControls // subquery limit(series limit) - 'limit', + 'limit', // via sharedControls // order by clause - 'timeseries_limit_metric', - 'series_limit_metric', + 'timeseries_limit_metric', // via sharedControls + 'series_limit_metric', // via sharedControls // desc or asc in order by clause - 'order_desc', + 'order_desc', // via sharedControls // outer query limit - 'row_limit', + 'row_limit', // via sharedControls // x asxs column - 'x_axis', + 'x_axis', // via sharedControls // advanced analytics - rolling window - 'rolling_type', - 'rolling_periods', - 'min_periods', + 'rolling_type', // via sections.advancedAnalytics + 'rolling_periods', // via sections.advancedAnalytics + 'min_periods', // via sections.advancedAnalytics // advanced analytics - time comparison - 'time_compare', - 'comparison_type', + 'time_compare', // via sections.advancedAnalytics + 'comparison_type', // via sections.advancedAnalytics // advanced analytics - resample - 'resample_rule', - 'resample_method', + 'resample_rule', // via sections.advancedAnalytics + 'resample_method', // via sections.advancedAnalytics ]; export class StandardizedFormData { @@ -70,20 +85,10 @@ export class StandardizedFormData { /* * Support form_data for smooth switching between different viz * */ - const standardizedState = { - metrics: [], - columns: [], - }; const formData = Object.freeze(sourceFormData); - const reversedMap = StandardizedFormData.getReversedMap(); - Object.entries(formData).forEach(([key, value]) => { - if (reversedMap.has(key)) { - standardizedState[reversedMap.get(key)].push(...ensureIsArray(value)); - } - }); - - const memorizedFormData = Array.isArray( + // generates an ordered map, the key is viz_type and the value is form_data. the last item is current viz + const memorizedFormData: Map = Array.isArray( formData?.standardizedFormData?.memorizedFormData, ) ? new Map(formData.standardizedFormData.memorizedFormData) @@ -93,25 +98,72 @@ export class StandardizedFormData { memorizedFormData.delete(vizType); } memorizedFormData.set(vizType, formData); + + // calculate sharedControls + const standardizedState = + StandardizedFormData.getStandardizedState(formData); + this.sfd = { standardizedState, memorizedFormData, }; } - static getReversedMap() { - const reversedMap = new Map(); - Object.entries(sharedControls).forEach(([key, names]) => { - names.forEach(name => { - reversedMap.set(name, key); - }); + static getStandardizedState(formData: QueryFormData): StandardizedState { + // 1. collect current sharedControls + let currState: StandardizedState = { + metrics: [], + columns: [], + }; + Object.entries(formData).forEach(([key, value]) => { + if (key in sharedControls) { + currState[sharedControls[key]].push(...ensureIsArray(value)); + } }); - return reversedMap; + + // 2. get previous StandardizedState + let prevState: StandardizedState = { + metrics: [], + columns: [], + }; + if ( + formData?.standardizedFormData?.standardizedState && + Array.isArray(formData.standardizedFormData.standardizedState.metrics) && + Array.isArray(formData.standardizedFormData.standardizedState.columns) + ) { + prevState = formData.standardizedFormData.standardizedState; + } + // the initial prevState should equal to currentState + if (isEmpty(prevState.metrics) && isEmpty(prevState.columns)) { + prevState = currState; + } + + // 3. inherit SS from previous state if current viz hasn't columns-like controls or metrics-like controls + Object.keys(sharedControlsMap).forEach(key => { + if ( + isEmpty(intersection(Object.keys(formData), sharedControlsMap[key])) + ) { + currState[key] = prevState[key]; + } + }); + + // 4. update hook + const controlPanel = getChartControlPanelRegistry().get(formData.viz_type); + if (controlPanel?.updateStandardizedState) { + currState = controlPanel.updateStandardizedState(prevState, currState); + } + + // 5. clear up + Object.entries(currState).forEach(([key, value]) => { + currState[key] = value.filter(Boolean); + }); + + return currState; } private getLatestFormData(vizType: string): QueryFormData { - if (this.sfd.memorizedFormData.has(vizType)) { - return this.sfd.memorizedFormData.get(vizType) as QueryFormData; + if (this.has(vizType)) { + return this.get(vizType); } return this.memorizedFormData.slice(-1)[0][1]; @@ -125,13 +177,21 @@ export class StandardizedFormData { return Array.from(this.sfd.memorizedFormData.entries()); } - dumpSFD() { + serialize() { return { standardizedState: this.standardizedState, memorizedFormData: this.memorizedFormData, }; } + has(vizType: string): boolean { + return this.sfd.memorizedFormData.has(vizType); + } + + get(vizType: string): QueryFormData { + return this.sfd.memorizedFormData.get(vizType) as QueryFormData; + } + transform( targetVizType: string, exploreState: Record, @@ -162,7 +222,7 @@ export class StandardizedFormData { }); const targetFormData = { ...getFormDataFromControls(targetControlsState), - standardizedFormData: this.dumpSFD(), + standardizedFormData: this.serialize(), }; const controlPanel = getChartControlPanelRegistry().get(targetVizType);