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

villebro 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 4d04565  feat(native-filters): apply scoping of native filters to 
dashboard (#12716)
4d04565 is described below

commit 4d04565c9af477d10b01a1d9156a63af38f381e0
Author: simchaNielsen <[email protected]>
AuthorDate: Tue Jan 26 09:29:49 2021 +0200

    feat(native-filters): apply scoping of native filters to dashboard (#12716)
    
    * feat: scoping native filters in dashboard
    
    * test: add tests / fix reducer
    
    * test: fix tests
    
    * chore: merge with master
    
    * fix: fix undefined cases
    
    * fix: fix code according cypress
    
    * refactor: fix mocks according CRs
    
    * chore: re run pipeline
---
 .../spec/fixtures/mockNativeFilters.ts             | 114 +++++++++++++++++++++
 .../dashboard/components/Dashboard_spec.jsx        |  28 +++++
 .../util/getFormDataWithExtraFilters_spec.ts       |  35 +++++--
 .../src/dashboard/components/Dashboard.jsx         |   8 +-
 .../src/dashboard/containers/Chart.jsx             |   2 +
 .../src/dashboard/containers/Dashboard.jsx         |  10 +-
 .../src/dashboard/reducers/nativeFilters.ts        |   5 +-
 superset-frontend/src/dashboard/types.ts           |  10 ++
 .../dashboard/util/activeDashboardNativeFilters.ts | 105 +++++++++++++++++++
 .../util/charts/getFormDataWithExtraFilters.ts     |  20 +++-
 10 files changed, 318 insertions(+), 19 deletions(-)

diff --git a/superset-frontend/spec/fixtures/mockNativeFilters.ts 
b/superset-frontend/spec/fixtures/mockNativeFilters.ts
index 9d14a07..9aa6e10 100644
--- a/superset-frontend/spec/fixtures/mockNativeFilters.ts
+++ b/superset-frontend/spec/fixtures/mockNativeFilters.ts
@@ -88,3 +88,117 @@ export const nativeFilters: NativeFiltersState = {
     },
   },
 };
+
+export const extraFormData = {
+  append_form_data: {
+    filters: [
+      {
+        col: 'ethnic_minority',
+        op: 'IN',
+        val: 'No, not an ethnic minority',
+      },
+    ],
+  },
+};
+
+export const NATIVE_FILTER_ID = 'NATIVE_FILTER-p4LImrSgA';
+
+export const singleNativeFiltersState = {
+  filters: {
+    [NATIVE_FILTER_ID]: {
+      id: [NATIVE_FILTER_ID],
+      name: 'eth',
+      type: 'text',
+      targets: [{ datasetId: 13, column: { name: 'ethnic_minority' } }],
+      defaultValue: null,
+      cascadeParentIds: [],
+      scope: { rootPath: ['ROOT_ID'], excluded: [227, 229] },
+      inverseSelection: false,
+      isInstant: true,
+      allowsMultipleValues: false,
+      isRequired: false,
+    },
+  },
+  filtersState: {
+    [NATIVE_FILTER_ID]: {
+      id: NATIVE_FILTER_ID,
+      extraFormData,
+    },
+  },
+};
+
+export const layoutForSingleNativeFilter = {
+  'CHART-ZHVS7YasaQ': {
+    children: [],
+    id: 'CHART-ZHVS7YasaQ',
+    meta: {
+      chartId: 230,
+      height: 50,
+      sliceName: 'Pie Chart',
+      uuid: '05ef6145-3950-4f59-891f-160852613eca',
+      width: 12,
+    },
+    parents: ['ROOT_ID', 'GRID_ID', 'ROW-NweUz7oC0'],
+    type: 'CHART',
+  },
+  'CHART-gsGu8NIKQT': {
+    children: [],
+    id: 'CHART-gsGu8NIKQT',
+    meta: {
+      chartId: 227,
+      height: 50,
+      sliceName: 'Another Chart',
+      uuid: 'ddb78f6c-7876-47fc-ae98-70183b05ba90',
+      width: 4,
+    },
+    parents: ['ROOT_ID', 'GRID_ID', 'ROW-QkiTjeZGs'],
+    type: 'CHART',
+  },
+  'CHART-hgYjD8axJX': {
+    children: [],
+    id: 'CHART-hgYjD8axJX',
+    meta: {
+      chartId: 229,
+      height: 47,
+      sliceName: 'Bar Chart',
+      uuid: 'e1501e54-d632-4fdc-ae16-07cafee31093',
+      width: 12,
+    },
+    parents: ['ROOT_ID', 'GRID_ID', 'ROW-mcdVZi0rL'],
+    type: 'CHART',
+  },
+  DASHBOARD_VERSION_KEY: 'v2',
+  GRID_ID: {
+    children: ['ROW-mcdVZi0rL', 'ROW-NweUz7oC0', 'ROW-QkiTjeZGs'],
+    id: 'GRID_ID',
+    parents: ['ROOT_ID'],
+    type: 'GRID',
+  },
+  HEADER_ID: {
+    id: 'HEADER_ID',
+    type: 'HEADER',
+    meta: { text: 'My Native Filter Dashboard' },
+  },
+  ROOT_ID: { children: ['GRID_ID'], id: 'ROOT_ID', type: 'ROOT' },
+  'ROW-NweUz7oC0': {
+    children: ['CHART-ZHVS7YasaQ'],
+    id: 'ROW-NweUz7oC0',
+    meta: { background: 'BACKGROUND_TRANSPARENT' },
+    parents: ['ROOT_ID', 'GRID_ID'],
+    type: 'ROW',
+  },
+  'ROW-QkiTjeZGs': {
+    children: ['CHART-gsGu8NIKQT'],
+    id: 'ROW-QkiTjeZGs',
+    meta: { background: 'BACKGROUND_TRANSPARENT' },
+    parents: ['ROOT_ID', 'GRID_ID'],
+    type: 'ROW',
+  },
+  'ROW-mcdVZi0rL': {
+    children: ['CHART-hgYjD8axJX'],
+    id: 'ROW-mcdVZi0rL',
+    meta: { '0': 'ROOT_ID', background: 'BACKGROUND_TRANSPARENT' },
+    parents: ['ROOT_ID', 'GRID_ID'],
+    type: 'ROW',
+  },
+};
diff --git 
a/superset-frontend/spec/javascripts/dashboard/components/Dashboard_spec.jsx 
b/superset-frontend/spec/javascripts/dashboard/components/Dashboard_spec.jsx
index 87f6c51..972498e 100644
--- a/superset-frontend/spec/javascripts/dashboard/components/Dashboard_spec.jsx
+++ b/superset-frontend/spec/javascripts/dashboard/components/Dashboard_spec.jsx
@@ -28,10 +28,17 @@ import newComponentFactory from 
'src/dashboard/util/newComponentFactory';
 // mock data
 import chartQueries from 'spec/fixtures/mockChartQueries';
 import datasources from 'spec/fixtures/mockDatasource';
+import {
+  extraFormData,
+  NATIVE_FILTER_ID,
+  layoutForSingleNativeFilter,
+  singleNativeFiltersState,
+} from 'spec/fixtures/mockNativeFilters';
 import dashboardInfo from 'spec/fixtures/mockDashboardInfo';
 import { dashboardLayout } from 'spec/fixtures/mockDashboardLayout';
 import dashboardState from 'spec/fixtures/mockDashboardState';
 import { sliceEntitiesForChart as sliceEntities } from 
'spec/fixtures/mockSliceEntities';
+import { getActiveNativeFilters } from 
'src/dashboard/util/activeDashboardNativeFilters';
 
 describe('Dashboard', () => {
   const props = {
@@ -141,6 +148,27 @@ describe('Dashboard', () => {
       expect(wrapper.instance().appliedFilters).toBe(OVERRIDE_FILTERS);
     });
 
+    it('should call refresh when native filters changed', () => {
+      wrapper.setProps({
+        activeFilters: {
+          ...OVERRIDE_FILTERS,
+          ...getActiveNativeFilters({
+            nativeFilters: singleNativeFiltersState,
+            layout: layoutForSingleNativeFilter,
+          }),
+        },
+      });
+      wrapper.instance().componentDidUpdate(prevProps);
+      expect(refreshSpy.callCount).toBe(1);
+      expect(wrapper.instance().appliedFilters).toEqual({
+        ...OVERRIDE_FILTERS,
+        [NATIVE_FILTER_ID]: {
+          scope: [230],
+          values: [extraFormData],
+        },
+      });
+    });
+
     it('should call refresh if a filter is added', () => {
       const newFilter = {
         gender: { values: ['boy', 'girl'], scope: [1] },
diff --git 
a/superset-frontend/spec/javascripts/dashboard/util/getFormDataWithExtraFilters_spec.ts
 
b/superset-frontend/spec/javascripts/dashboard/util/getFormDataWithExtraFilters_spec.ts
index d90bf81..5080f53 100644
--- 
a/superset-frontend/spec/javascripts/dashboard/util/getFormDataWithExtraFilters_spec.ts
+++ 
b/superset-frontend/spec/javascripts/dashboard/util/getFormDataWithExtraFilters_spec.ts
@@ -16,11 +16,18 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import getFormDataWithExtraFilters from 
'src/dashboard/util/charts/getFormDataWithExtraFilters';
+import getFormDataWithExtraFilters, {
+  GetFormDataWithExtraFiltersArguments,
+} from 'src/dashboard/util/charts/getFormDataWithExtraFilters';
+import { DASHBOARD_ROOT_ID } from 'src/dashboard/util/constants';
+import { Filter } from 'src/dashboard/components/nativeFilters/types';
+import { LayoutItem } from 'src/dashboard/types';
+import { dashboardLayout } from '../../../fixtures/mockDashboardLayout';
+import { sliceId as chartId } from '../../../fixtures/mockChartQueries';
 
 describe('getFormDataWithExtraFilters', () => {
-  const chartId = 8675309;
-  const mockArgs = {
+  const filterId = 'native-filter-1';
+  const mockArgs: GetFormDataWithExtraFiltersArguments = {
     chart: {
       id: chartId,
       formData: {
@@ -37,11 +44,27 @@ describe('getFormDataWithExtraFilters', () => {
       region: ['Spain'],
       color: ['pink', 'purple'],
     },
+    sliceId: chartId,
     nativeFilters: {
-      filters: {},
-      filtersState: {},
+      filters: {
+        [filterId]: ({
+          id: filterId,
+          scope: {
+            rootPath: [DASHBOARD_ROOT_ID],
+            excluded: [],
+          },
+        } as unknown) as Filter,
+      },
+      filtersState: {
+        [filterId]: {
+          id: filterId,
+          extraFormData: {},
+        },
+      },
+    },
+    layout: (dashboardLayout.present as unknown) as {
+      [key: string]: LayoutItem;
     },
-    sliceId: chartId,
   };
 
   it('should include filters from the passed filters', () => {
diff --git a/superset-frontend/src/dashboard/components/Dashboard.jsx 
b/superset-frontend/src/dashboard/components/Dashboard.jsx
index 9606fc8..3f3a792 100644
--- a/superset-frontend/src/dashboard/components/Dashboard.jsx
+++ b/superset-frontend/src/dashboard/components/Dashboard.jsx
@@ -144,15 +144,11 @@ class Dashboard extends React.PureComponent {
     }
   }
 
-  componentDidUpdate(prevProps) {
+  componentDidUpdate() {
     const { hasUnsavedChanges, editMode } = this.props.dashboardState;
 
     const { appliedFilters } = this;
-    const { activeFilters, nativeFilters } = this.props;
-    // do not apply filter when dashboard in edit mode
-    if (!areObjectsEqual(prevProps.nativeFilters, nativeFilters)) {
-      this.refreshCharts(this.getAllCharts().map(chart => chart.id));
-    }
+    const { activeFilters } = this.props;
     if (!editMode && !areObjectsEqual(appliedFilters, activeFilters)) {
       this.applyFilters();
     }
diff --git a/superset-frontend/src/dashboard/containers/Chart.jsx 
b/superset-frontend/src/dashboard/containers/Chart.jsx
index abd784a..9c48a89 100644
--- a/superset-frontend/src/dashboard/containers/Chart.jsx
+++ b/superset-frontend/src/dashboard/containers/Chart.jsx
@@ -43,6 +43,7 @@ function mapStateToProps(
     charts: chartQueries,
     dashboardInfo,
     dashboardState,
+    dashboardLayout,
     datasources,
     sliceEntities,
     nativeFilters,
@@ -57,6 +58,7 @@ function mapStateToProps(
 
   // note: this method caches filters if possible to prevent render cascades
   const formData = getFormDataWithExtraFilters({
+    layout: dashboardLayout.present,
     chart,
     filters: getAppliedFilterValues(id),
     colorScheme,
diff --git a/superset-frontend/src/dashboard/containers/Dashboard.jsx 
b/superset-frontend/src/dashboard/containers/Dashboard.jsx
index f644dc6..07fc924 100644
--- a/superset-frontend/src/dashboard/containers/Dashboard.jsx
+++ b/superset-frontend/src/dashboard/containers/Dashboard.jsx
@@ -28,6 +28,7 @@ import {
 import { triggerQuery } from '../../chart/chartAction';
 import { logEvent } from '../../logger/actions';
 import { getActiveFilters } from '../util/activeDashboardFilters';
+import { getActiveNativeFilters } from '../util/activeDashboardNativeFilters';
 
 function mapStateToProps(state) {
   const {
@@ -54,10 +55,15 @@ function mapStateToProps(state) {
     // When dashboard is first loaded into browser,
     // its value is from preselect_filters that dashboard owner saved in 
dashboard's meta data
     // When user start interacting with dashboard, it will be user picked 
values from all filter_box
-    activeFilters: getActiveFilters(),
+    activeFilters: {
+      ...getActiveFilters(),
+      ...getActiveNativeFilters({
+        nativeFilters,
+        layout: dashboardLayout.present,
+      }),
+    },
     slices: sliceEntities.slices,
     layout: dashboardLayout.present,
-    nativeFilters,
     impressionId,
   };
 }
diff --git a/superset-frontend/src/dashboard/reducers/nativeFilters.ts 
b/superset-frontend/src/dashboard/reducers/nativeFilters.ts
index b4c76a1..b69ccd5 100644
--- a/superset-frontend/src/dashboard/reducers/nativeFilters.ts
+++ b/superset-frontend/src/dashboard/reducers/nativeFilters.ts
@@ -36,6 +36,7 @@ export function getInitialFilterState(id: string): 
FilterState {
 
 export function getInitialState(
   filterConfig: FilterConfiguration,
+  prevFiltersState: { [filterId: string]: FilterState },
 ): NativeFiltersState {
   const filters = {};
   const filtersState = {};
@@ -43,7 +44,7 @@ export function getInitialState(
   filterConfig.forEach(filter => {
     const { id } = filter;
     filters[id] = filter;
-    filtersState[id] = getInitialFilterState(id);
+    filtersState[id] = prevFiltersState?.[id] || getInitialFilterState(id);
   });
   return state;
 }
@@ -67,7 +68,7 @@ export default function nativeFilterReducer(
       };
 
     case SET_FILTER_CONFIG_COMPLETE:
-      return getInitialState(action.filterConfig);
+      return getInitialState(action.filterConfig, filtersState);
 
     // TODO handle SET_FILTER_CONFIG_FAIL action
     default:
diff --git a/superset-frontend/src/dashboard/types.ts 
b/superset-frontend/src/dashboard/types.ts
index b66332a..86fa4bf 100644
--- a/superset-frontend/src/dashboard/types.ts
+++ b/superset-frontend/src/dashboard/types.ts
@@ -25,6 +25,7 @@ export type ChartReducerInitialState = typeof chart;
 // chart query built from initialState
 // Ref: 
https://github.com/apache/superset/blob/dcac860f3e5528ecbc39e58f045c7388adb5c3d0/superset-frontend/src/dashboard/reducers/getInitialState.js#L120
 export interface ChartQueryPayload extends Partial<ChartReducerInitialState> {
+  id: number;
   formData: ChartProps['formData'];
   form_data?: ChartProps['rawFormData'];
   [key: string]: unknown;
@@ -69,3 +70,12 @@ export type LayoutItem = {
     width: number;
   };
 };
+
+type ActiveFilter = {
+  scope: number[];
+  values: any[];
+};
+
+export type ActiveFilters = {
+  [key: string]: ActiveFilter;
+};
diff --git 
a/superset-frontend/src/dashboard/util/activeDashboardNativeFilters.ts 
b/superset-frontend/src/dashboard/util/activeDashboardNativeFilters.ts
new file mode 100644
index 0000000..e143164
--- /dev/null
+++ b/superset-frontend/src/dashboard/util/activeDashboardNativeFilters.ts
@@ -0,0 +1,105 @@
+/**
+ * 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 { CHART_TYPE } from './componentTypes';
+import { NativeFiltersState, Scope } from '../components/nativeFilters/types';
+import { ActiveFilters, LayoutItem } from '../types';
+
+// Looking for affected chart scopes and values
+export const findAffectedCharts = ({
+  child,
+  layout,
+  scope,
+  activeNativeFilters,
+  filterId,
+  extraFormData,
+}: {
+  child: string;
+  layout: { [key: string]: LayoutItem };
+  scope: Scope;
+  activeNativeFilters: ActiveFilters;
+  filterId: string;
+  extraFormData: any;
+}) => {
+  const chartId = layout[child]?.meta?.chartId;
+  if (layout[child].type === CHART_TYPE) {
+    // Ignore excluded charts
+    if (scope.excluded.includes(chartId)) {
+      return;
+    }
+    if (!activeNativeFilters[filterId]) {
+      // Small mutation but simplify logic
+      // eslint-disable-next-line no-param-reassign
+      activeNativeFilters[filterId] = {
+        scope: [],
+        values: [],
+      };
+    }
+    // Add not excluded chart scopes(to know what charts refresh) and 
values(refresh only if its value changed)
+    activeNativeFilters[filterId].scope.push(chartId);
+    activeNativeFilters[filterId].values.push(extraFormData);
+    return;
+  }
+  // If child is not chart, recursive iterate over its children
+  layout[child].children.forEach((child: string) =>
+    findAffectedCharts({
+      child,
+      layout,
+      scope,
+      activeNativeFilters,
+      filterId,
+      extraFormData,
+    }),
+  );
+};
+
+export const getActiveNativeFilters = ({
+  nativeFilters,
+  layout,
+}: {
+  nativeFilters: NativeFiltersState;
+  layout: { [key: string]: LayoutItem };
+}): ActiveFilters => {
+  const activeNativeFilters = {};
+  if (!nativeFilters?.filtersState) {
+    return activeNativeFilters;
+  }
+  Object.values(nativeFilters.filtersState).forEach(
+    ({ id: filterId, extraFormData }) => {
+      const scope = nativeFilters?.filters?.[filterId]?.scope;
+      if (!scope) {
+        return;
+      }
+      // Iterate over all roots to find all affected charts
+      scope.rootPath.forEach(layoutItemId => {
+        layout[layoutItemId].children.forEach((child: string) => {
+          // Need exclude from affected charts, charts that located in scope 
`excluded`
+          findAffectedCharts({
+            child,
+            layout,
+            scope,
+            activeNativeFilters,
+            filterId,
+            extraFormData,
+          });
+        });
+      });
+    },
+  );
+  return activeNativeFilters;
+};
diff --git 
a/superset-frontend/src/dashboard/util/charts/getFormDataWithExtraFilters.ts 
b/superset-frontend/src/dashboard/util/charts/getFormDataWithExtraFilters.ts
index 446e924..6d7e56b 100644
--- a/superset-frontend/src/dashboard/util/charts/getFormDataWithExtraFilters.ts
+++ b/superset-frontend/src/dashboard/util/charts/getFormDataWithExtraFilters.ts
@@ -21,19 +21,21 @@ import {
   CategoricalColorNamespace,
   DataRecordFilters,
 } from '@superset-ui/core';
-import { ChartQueryPayload } from 'src/dashboard/types';
+import { ChartQueryPayload, LayoutItem } from 'src/dashboard/types';
 import { NativeFiltersState } from 
'src/dashboard/components/nativeFilters/types';
 import { getExtraFormData } from 
'src/dashboard/components/nativeFilters/utils';
 import getEffectiveExtraFilters from './getEffectiveExtraFilters';
+import { getActiveNativeFilters } from '../activeDashboardNativeFilters';
 
 // We cache formData objects so that our connected container components don't 
always trigger
 // render cascades. we cannot leverage the reselect library because our cache 
size is >1
 const cachedFiltersByChart = {};
 const cachedFormdataByChart = {};
 
-interface GetFormDataWithExtraFiltersArguments {
+export interface GetFormDataWithExtraFiltersArguments {
   chart: ChartQueryPayload;
   filters: DataRecordFilters;
+  layout: { [key: string]: LayoutItem };
   colorScheme?: string;
   colorNamespace?: string;
   sliceId: number;
@@ -49,6 +51,7 @@ export default function getFormDataWithExtraFilters({
   colorScheme,
   colorNamespace,
   sliceId,
+  layout,
   nativeFilters,
 }: GetFormDataWithExtraFiltersArguments) {
   // Propagate color mapping to chart
@@ -68,12 +71,23 @@ export default function getFormDataWithExtraFilters({
     return cachedFormdataByChart[sliceId];
   }
 
+  let extraData = {};
+  const activeNativeFilters = getActiveNativeFilters({ nativeFilters, layout 
});
+  const isAffectedChart = Object.values(activeNativeFilters).some(({ scope }) 
=>
+    scope.includes(chart.id),
+  );
+  if (isAffectedChart) {
+    extraData = {
+      extra_form_data: getExtraFormData(nativeFilters),
+    };
+  }
+
   const formData = {
     ...chart.formData,
     ...(colorScheme && { color_scheme: colorScheme }),
     label_colors: labelColors,
     extra_filters: getEffectiveExtraFilters(filters),
-    extra_form_data: getExtraFormData(nativeFilters),
+    ...extraData,
   };
   cachedFiltersByChart[sliceId] = filters;
   cachedFormdataByChart[sliceId] = formData;

Reply via email to