You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by mi...@apache.org on 2022/06/28 17:24:07 UTC

[superset] 16/19: feat(standardized form data): keep all columns and metrics (#20377)

This is an automated email from the ASF dual-hosted git repository.

michaelsmolina pushed a commit to branch 2.0
in repository https://gitbox.apache.org/repos/asf/superset.git

commit b8d9208b6e555181d7040161103535ca3ef500e0
Author: Yongjie Zhao <yo...@gmail.com>
AuthorDate: Fri Jun 24 21:46:25 2022 +0800

    feat(standardized form data): keep all columns and metrics (#20377)
    
    (cherry picked from commit bbbe102887a524b1ee0ffd4ada8481078dbe5496)
---
 .../superset-ui-chart-controls/src/types.ts        |   4 +
 .../src/BigNumber/BigNumberTotal/controlPanel.ts   |   4 +
 .../BigNumberWithTrendline/controlPanel.tsx        |   4 +
 .../src/Funnel/controlPanel.tsx                    |   4 +
 .../src/Gauge/controlPanel.tsx                     |   4 +
 .../src/Graph/controlPanel.tsx                     |   4 +
 .../plugin-chart-echarts/src/Pie/controlPanel.tsx  |   4 +
 .../plugin-chart-echarts/src/Tree/controlPanel.tsx |   4 +
 .../src/Treemap/controlPanel.tsx                   |   4 +
 ...mData.test.tsx => standardizedFormData.test.ts} | 116 ++++++++++++----
 .../explore/controlUtils/standardizedFormData.ts   | 146 +++++++++++++++------
 11 files changed, 231 insertions(+), 67 deletions(-)

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 431450f142..fac5548851 100644
--- a/superset-frontend/packages/superset-ui-chart-controls/src/types.ts
+++ b/superset-frontend/packages/superset-ui-chart-controls/src/types.ts
@@ -363,6 +363,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 c715c8f232..94dd458305 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 c61ec6d046..0ea0a96229 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 39ce57d498..b23cf2ed97 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 d9dbb3025e..bb727888e5 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 39547f683a..218b1d0335 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 43e1ab6cba..7b948208a4 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 097a882fa3..c115f5c5c2 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 8887377d2d..e7cca1af26 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 e048c04498..e7d1c758c4 100644
--- a/superset-frontend/src/explore/controlUtils/standardizedFormData.test.tsx
+++ b/superset-frontend/src/explore/controlUtils/standardizedFormData.test.ts
@@ -28,15 +28,46 @@ import {
 import { xAxisControl } from '../../../plugins/plugin-chart-echarts/src/controls';
 
 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,
@@ -90,26 +121,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', () => {
@@ -157,6 +207,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',
@@ -172,7 +223,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',
@@ -197,7 +247,9 @@ describe('should transform form_data between table and bigNumberTotal', () => {
           dataset_id: '30',
         },
       },
-      granularity_sqla: {},
+      granularity_sqla: {
+        value: 'ds',
+      },
       time_grain_sqla: {
         value: 'P1D',
       },
@@ -271,6 +323,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);
@@ -301,7 +369,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 c42ef95085..99b0220da3 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<keyof StandardizedState, string[]> = {
-  metrics: ['metric', 'metrics', 'metric_2'],
-  columns: ['groupby', 'columns', 'groupbyColumns', 'groupbyRows'],
+export const sharedControls: Record<string, keyof StandardizedState> = {
+  // 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<keyof StandardizedState, string[]> = {
+  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<string, QueryFormData> = 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<string, any>,
@@ -162,7 +222,7 @@ export class StandardizedFormData {
     });
     const targetFormData = {
       ...getFormDataFromControls(targetControlsState),
-      standardizedFormData: this.dumpSFD(),
+      standardizedFormData: this.serialize(),
     };
 
     const controlPanel = getChartControlPanelRegistry().get(targetVizType);