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

jli 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 362948324c fix(Filters): Apply native & cross filters on common 
columns (#30438)
362948324c is described below

commit 362948324c7718e74c0a9655332249c0e1328703
Author: Geido <[email protected]>
AuthorDate: Wed Oct 16 02:13:05 2024 +0300

    fix(Filters): Apply native & cross filters on common columns (#30438)
---
 docs/static/resources/openapi.json                 |   6 +
 .../superset-ui-chart-controls/src/types.ts        |   1 +
 .../superset-ui-core/src/query/types/Dashboard.ts  |  36 +++
 .../test/query/types/Dashboard.test.ts             |  28 +++
 .../spec/fixtures/mockDashboardInfo.js             |   1 +
 .../spec/fixtures/mockNativeFilters.ts             |   1 +
 .../src/dashboard/actions/dashboardState.js        |  13 +-
 .../src/dashboard/components/Dashboard.jsx         |  31 ++-
 .../src/dashboard/components/Dashboard.test.jsx    |  29 +++
 .../OverwriteConfirmModal.test.tsx                 |   5 +
 .../src/dashboard/fixtures/mockNativeFilters.ts    |   1 +
 .../dashboard/util/activeAllDashboardFilters.ts    |   4 +
 .../src/dashboard/util/crossFilters.test.ts        |   4 +-
 .../src/dashboard/util/crossFilters.ts             |  19 +-
 .../src/dashboard/util/getRelatedCharts.test.ts    | 254 +++++++++++++++++++++
 .../src/dashboard/util/getRelatedCharts.ts         | 200 ++++++++++++++++
 .../util/useFilterFocusHighlightStyles.test.tsx    |  22 ++
 .../util/useFilterFocusHighlightStyles.ts          |  22 +-
 superset-frontend/src/types/Chart.ts               |   2 +
 superset/connectors/sqla/models.py                 |  14 +-
 superset/dashboards/schemas.py                     |   1 +
 21 files changed, 656 insertions(+), 38 deletions(-)

diff --git a/docs/static/resources/openapi.json 
b/docs/static/resources/openapi.json
index 43781ac965..c2cadfd1f5 100644
--- a/docs/static/resources/openapi.json
+++ b/docs/static/resources/openapi.json
@@ -3053,6 +3053,12 @@
             },
             "type": "array"
           },
+          "column_names": {
+            "items": {
+              "type": "string"
+            },
+            "type": "array"
+          },
           "currency_formats": {
             "type": "object"
           },
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 d4170be17c..86e7b1c04e 100644
--- a/superset-frontend/packages/superset-ui-chart-controls/src/types.ts
+++ b/superset-frontend/packages/superset-ui-chart-controls/src/types.ts
@@ -83,6 +83,7 @@ export interface Dataset {
   owners?: Owner[];
   filter_select?: boolean;
   filter_select_enabled?: boolean;
+  column_names?: string[];
 }
 
 export interface ControlPanelState {
diff --git 
a/superset-frontend/packages/superset-ui-core/src/query/types/Dashboard.ts 
b/superset-frontend/packages/superset-ui-core/src/query/types/Dashboard.ts
index cb299c3ac9..ac4b19cae5 100644
--- a/superset-frontend/packages/superset-ui-core/src/query/types/Dashboard.ts
+++ b/superset-frontend/packages/superset-ui-core/src/query/types/Dashboard.ts
@@ -80,6 +80,24 @@ export type Filter = {
   description: string;
 };
 
+export type AppliedFilter = {
+  values: {
+    filters: Record<string, any>[];
+  } | null;
+};
+
+export type AppliedCrossFilterType = {
+  filterType: undefined;
+  targets: number[];
+  scope: number[];
+} & AppliedFilter;
+
+export type AppliedNativeFilterType = {
+  filterType: 'filter_select';
+  scope: number[];
+  targets: Partial<NativeFilterTarget>[];
+} & AppliedFilter;
+
 export type FilterWithDataMask = Filter & { dataMask: DataMaskWithId };
 
 export type Divider = Partial<Omit<Filter, 'id' | 'type'>> & {
@@ -89,6 +107,24 @@ export type Divider = Partial<Omit<Filter, 'id' | 'type'>> 
& {
   type: typeof NativeFilterType.Divider;
 };
 
+export function isAppliedCrossFilterType(
+  filterElement: AppliedCrossFilterType | AppliedNativeFilterType | Filter,
+): filterElement is AppliedCrossFilterType {
+  return (
+    filterElement.filterType === undefined &&
+    filterElement.hasOwnProperty('values')
+  );
+}
+
+export function isAppliedNativeFilterType(
+  filterElement: AppliedCrossFilterType | AppliedNativeFilterType | Filter,
+): filterElement is AppliedNativeFilterType {
+  return (
+    filterElement.filterType === 'filter_select' &&
+    filterElement.hasOwnProperty('values')
+  );
+}
+
 export function isNativeFilter(
   filterElement: Filter | Divider,
 ): filterElement is Filter {
diff --git 
a/superset-frontend/packages/superset-ui-core/test/query/types/Dashboard.test.ts
 
b/superset-frontend/packages/superset-ui-core/test/query/types/Dashboard.test.ts
index 79d7980940..c1c7143957 100644
--- 
a/superset-frontend/packages/superset-ui-core/test/query/types/Dashboard.test.ts
+++ 
b/superset-frontend/packages/superset-ui-core/test/query/types/Dashboard.test.ts
@@ -24,6 +24,10 @@ import {
   FilterWithDataMask,
   Divider,
   isNativeFilterWithDataMask,
+  isAppliedCrossFilterType,
+  isAppliedNativeFilterType,
+  AppliedCrossFilterType,
+  AppliedNativeFilterType,
 } from '@superset-ui/core';
 
 const filter: Filter = {
@@ -51,6 +55,20 @@ const filterDivider: Divider = {
   description: 'Divider description.',
 };
 
+const appliedCrossFilter: AppliedCrossFilterType = {
+  filterType: undefined,
+  targets: [1, 2],
+  scope: [1, 2],
+  values: null,
+};
+
+const appliedNativeFilter: AppliedNativeFilterType = {
+  filterType: 'filter_select',
+  scope: [1, 2],
+  targets: [{}],
+  values: null,
+};
+
 test('filter type guard', () => {
   expect(isNativeFilter(filter)).toBeTruthy();
   expect(isNativeFilter(filterWithDataMask)).toBeTruthy();
@@ -68,3 +86,13 @@ test('filter divider type guard', () => {
   expect(isFilterDivider(filterWithDataMask)).toBeFalsy();
   expect(isFilterDivider(filterDivider)).toBeTruthy();
 });
+
+test('applied cross filter type guard', () => {
+  expect(isAppliedCrossFilterType(appliedCrossFilter)).toBeTruthy();
+  expect(isAppliedCrossFilterType(appliedNativeFilter)).toBeFalsy();
+});
+
+test('applied native filter type guard', () => {
+  expect(isAppliedNativeFilterType(appliedNativeFilter)).toBeTruthy();
+  expect(isAppliedNativeFilterType(appliedCrossFilter)).toBeFalsy();
+});
diff --git a/superset-frontend/spec/fixtures/mockDashboardInfo.js 
b/superset-frontend/spec/fixtures/mockDashboardInfo.js
index 2f747fd07b..a046a554d0 100644
--- a/superset-frontend/spec/fixtures/mockDashboardInfo.js
+++ b/superset-frontend/spec/fixtures/mockDashboardInfo.js
@@ -26,6 +26,7 @@ export default {
       {
         id: 'DefaultsID',
         filterType: 'filter_select',
+        chartsInScope: [],
         targets: [{}],
         cascadeParentIds: [],
       },
diff --git a/superset-frontend/spec/fixtures/mockNativeFilters.ts 
b/superset-frontend/spec/fixtures/mockNativeFilters.ts
index 070f48ab06..b83cdcc8dc 100644
--- a/superset-frontend/spec/fixtures/mockNativeFilters.ts
+++ b/superset-frontend/spec/fixtures/mockNativeFilters.ts
@@ -133,6 +133,7 @@ export const singleNativeFiltersState = {
       id: [NATIVE_FILTER_ID],
       name: 'eth',
       type: 'text',
+      filterType: 'filter_select',
       targets: [{ datasetId: 13, column: { name: 'ethnic_minority' } }],
       defaultDataMask: {
         filterState: {
diff --git a/superset-frontend/src/dashboard/actions/dashboardState.js 
b/superset-frontend/src/dashboard/actions/dashboardState.js
index 9b0c181821..945dda32a3 100644
--- a/superset-frontend/src/dashboard/actions/dashboardState.js
+++ b/superset-frontend/src/dashboard/actions/dashboardState.js
@@ -61,7 +61,7 @@ import {
   dashboardInfoChanged,
   SAVE_CHART_CONFIG_COMPLETE,
 } from './dashboardInfo';
-import { fetchDatasourceMetadata } from './datasources';
+import { fetchDatasourceMetadata, setDatasources } from './datasources';
 import { updateDirectPathToFilter } from './dashboardFilters';
 import { SET_FILTER_CONFIG_COMPLETE } from './nativeFilters';
 import getOverwriteItems from '../util/getOverwriteItems';
@@ -341,6 +341,17 @@ export function saveDashboardRequest(data, id, saveType) {
             filterConfig: metadata.native_filter_configuration,
           });
         }
+
+        // fetch datasets to make sure they are up to date
+        SupersetClient.get({
+          endpoint: `/api/v1/dashboard/${id}/datasets`,
+          headers: { 'Content-Type': 'application/json' },
+        }).then(({ json }) => {
+          const datasources = json?.result ?? [];
+          if (datasources.length) {
+            dispatch(setDatasources(datasources));
+          }
+        });
       }
       if (lastModifiedTime) {
         dispatch(saveDashboardRequestSuccess(lastModifiedTime));
diff --git a/superset-frontend/src/dashboard/components/Dashboard.jsx 
b/superset-frontend/src/dashboard/components/Dashboard.jsx
index 8cc0d81030..1953889c5d 100644
--- a/superset-frontend/src/dashboard/components/Dashboard.jsx
+++ b/superset-frontend/src/dashboard/components/Dashboard.jsx
@@ -41,6 +41,7 @@ import { areObjectsEqual } from '../../reduxUtils';
 import getLocationHash from '../util/getLocationHash';
 import isDashboardEmpty from '../util/isDashboardEmpty';
 import { getAffectedOwnDataCharts } from '../util/charts/getOwnDataCharts';
+import { getRelatedCharts } from '../util/getRelatedCharts';
 
 const propTypes = {
   actions: PropTypes.shape({
@@ -211,9 +212,10 @@ class Dashboard extends PureComponent {
 
   applyFilters() {
     const { appliedFilters } = this;
-    const { activeFilters, ownDataCharts } = this.props;
+    const { activeFilters, ownDataCharts, datasources, slices } = this.props;
 
     // refresh charts if a filter was removed, added, or changed
+
     const currFilterKeys = Object.keys(activeFilters);
     const appliedFilterKeys = Object.keys(appliedFilters);
 
@@ -228,10 +230,24 @@ class Dashboard extends PureComponent {
         appliedFilterKeys.includes(filterKey)
       ) {
         // filterKey is removed?
-        affectedChartIds.push(...appliedFilters[filterKey].scope);
+        affectedChartIds.push(
+          ...getRelatedCharts(
+            appliedFilters,
+            activeFilters,
+            slices,
+            datasources,
+          )[filterKey],
+        );
       } else if (!appliedFilterKeys.includes(filterKey)) {
         // filterKey is newly added?
-        affectedChartIds.push(...activeFilters[filterKey].scope);
+        affectedChartIds.push(
+          ...getRelatedCharts(
+            activeFilters,
+            appliedFilters,
+            slices,
+            datasources,
+          )[filterKey],
+        );
       } else {
         // if filterKey changes value,
         // update charts in its scope
@@ -244,7 +260,14 @@ class Dashboard extends PureComponent {
             },
           )
         ) {
-          affectedChartIds.push(...activeFilters[filterKey].scope);
+          affectedChartIds.push(
+            ...getRelatedCharts(
+              activeFilters,
+              appliedFilters,
+              slices,
+              datasources,
+            )[filterKey],
+          );
         }
 
         // if filterKey changes scope,
diff --git a/superset-frontend/src/dashboard/components/Dashboard.test.jsx 
b/superset-frontend/src/dashboard/components/Dashboard.test.jsx
index b33fe9f638..cd76787a21 100644
--- a/superset-frontend/src/dashboard/components/Dashboard.test.jsx
+++ b/superset-frontend/src/dashboard/components/Dashboard.test.jsx
@@ -37,6 +37,9 @@ import { dashboardLayout } from 
'spec/fixtures/mockDashboardLayout';
 import dashboardState from 'spec/fixtures/mockDashboardState';
 import { sliceEntitiesForChart as sliceEntities } from 
'spec/fixtures/mockSliceEntities';
 import { getAllActiveFilters } from 
'src/dashboard/util/activeAllDashboardFilters';
+import { getRelatedCharts } from 'src/dashboard/util/getRelatedCharts';
+
+jest.mock('src/dashboard/util/getRelatedCharts');
 
 describe('Dashboard', () => {
   const props = {
@@ -130,6 +133,7 @@ describe('Dashboard', () => {
 
     afterEach(() => {
       refreshSpy.restore();
+      jest.clearAllMocks();
     });
 
     it('should not call refresh when is editMode', () => {
@@ -153,6 +157,9 @@ describe('Dashboard', () => {
     });
 
     it('should call refresh when native filters changed', () => {
+      getRelatedCharts.mockReturnValue({
+        [NATIVE_FILTER_ID]: [230],
+      });
       wrapper.setProps({
         activeFilters: {
           ...OVERRIDE_FILTERS,
@@ -170,11 +177,27 @@ describe('Dashboard', () => {
         [NATIVE_FILTER_ID]: {
           scope: [230],
           values: extraFormData,
+          filterType: 'filter_select',
+          targets: [
+            {
+              datasetId: 13,
+              column: {
+                name: 'ethnic_minority',
+              },
+            },
+          ],
         },
       });
     });
 
     it('should call refresh if a filter is added', () => {
+      getRelatedCharts.mockReturnValue({
+        '1_region': [1],
+        '2_country_name': [1, 2],
+        '3_region': [1],
+        '3_country_name': [],
+        gender: [1],
+      });
       const newFilter = {
         gender: { values: ['boy', 'girl'], scope: [1] },
       };
@@ -186,6 +209,12 @@ describe('Dashboard', () => {
     });
 
     it('should call refresh if a filter is removed', () => {
+      getRelatedCharts.mockReturnValue({
+        '1_region': [1],
+        '2_country_name': [1, 2],
+        '3_region': [1],
+        '3_country_name': [],
+      });
       wrapper.setProps({
         activeFilters: {},
       });
diff --git 
a/superset-frontend/src/dashboard/components/OverwriteConfirm/OverwriteConfirmModal.test.tsx
 
b/superset-frontend/src/dashboard/components/OverwriteConfirm/OverwriteConfirmModal.test.tsx
index 01e9e825bf..5d93fd6900 100644
--- 
a/superset-frontend/src/dashboard/components/OverwriteConfirm/OverwriteConfirmModal.test.tsx
+++ 
b/superset-frontend/src/dashboard/components/OverwriteConfirm/OverwriteConfirmModal.test.tsx
@@ -51,6 +51,11 @@ test('renders diff viewer when it contains 
overwriteConfirmMetadata', async () =
 
 test('requests update dashboard api when save button is clicked', async () => {
   const updateDashboardEndpoint = 
`glob:*/api/v1/dashboard/${overwriteConfirmMetadata.dashboardId}`;
+  const fetchDatasetsEndpoint = 
`glob:*/api/v1/dashboard/${overwriteConfirmMetadata.dashboardId}/datasets`;
+
+  // mock fetch datasets
+  fetchMock.get(fetchDatasetsEndpoint, []);
+
   fetchMock.put(updateDashboardEndpoint, {
     id: overwriteConfirmMetadata.dashboardId,
     last_modified_time: +new Date(),
diff --git a/superset-frontend/src/dashboard/fixtures/mockNativeFilters.ts 
b/superset-frontend/src/dashboard/fixtures/mockNativeFilters.ts
index d1d37cbf6a..32f54cbe26 100644
--- a/superset-frontend/src/dashboard/fixtures/mockNativeFilters.ts
+++ b/superset-frontend/src/dashboard/fixtures/mockNativeFilters.ts
@@ -39,6 +39,7 @@ export const nativeFiltersInfo: NativeFiltersState = {
       id: 'DefaultsID',
       name: 'test',
       filterType: 'filter_select',
+      chartsInScope: [],
       targets: [
         {
           datasetId: 0,
diff --git a/superset-frontend/src/dashboard/util/activeAllDashboardFilters.ts 
b/superset-frontend/src/dashboard/util/activeAllDashboardFilters.ts
index f9e98e8536..3bde1d2e6f 100644
--- a/superset-frontend/src/dashboard/util/activeAllDashboardFilters.ts
+++ b/superset-frontend/src/dashboard/util/activeAllDashboardFilters.ts
@@ -54,9 +54,13 @@ export const getAllActiveFilters = ({
       chartConfiguration?.[filterId]?.crossFilters?.chartsInScope ??
       allSliceIds ??
       [];
+    const filterType = nativeFilters?.[filterId]?.filterType;
+    const targets = nativeFilters?.[filterId]?.targets ?? scope;
     // Iterate over all roots to find all affected charts
     activeFilters[filterId] = {
       scope,
+      filterType,
+      targets,
       values: extraFormData,
     };
   });
diff --git a/superset-frontend/src/dashboard/util/crossFilters.test.ts 
b/superset-frontend/src/dashboard/util/crossFilters.test.ts
index e811856a4a..b88579cc84 100644
--- a/superset-frontend/src/dashboard/util/crossFilters.test.ts
+++ b/superset-frontend/src/dashboard/util/crossFilters.test.ts
@@ -294,14 +294,14 @@ test('Recalculate charts in global filter scope when 
charts change', () => {
         id: 2,
         crossFilters: {
           scope: 'global',
-          chartsInScope: [1],
+          chartsInScope: [1, 3],
         },
       },
       '3': {
         id: 3,
         crossFilters: {
           scope: 'global',
-          chartsInScope: [],
+          chartsInScope: [1, 2],
         },
       },
     },
diff --git a/superset-frontend/src/dashboard/util/crossFilters.ts 
b/superset-frontend/src/dashboard/util/crossFilters.ts
index 880b826870..903a1f74fb 100644
--- a/superset-frontend/src/dashboard/util/crossFilters.ts
+++ b/superset-frontend/src/dashboard/util/crossFilters.ts
@@ -52,20 +52,6 @@ export const getCrossFiltersConfiguration = (
     return undefined;
   }
 
-  const chartsByDataSource: Record<string, Set<number>> = Object.values(
-    charts,
-  ).reduce((acc: Record<string, Set<number>>, chart) => {
-    if (!chart.form_data) {
-      return acc;
-    }
-    const { datasource } = chart.form_data;
-    if (!acc[datasource]) {
-      acc[datasource] = new Set();
-    }
-    acc[datasource].add(chart.id);
-    return acc;
-  }, {});
-
   const globalChartConfiguration = metadata.global_chart_configuration?.scope
     ? {
         scope: metadata.global_chart_configuration.scope,
@@ -111,13 +97,10 @@ export const getCrossFiltersConfiguration = (
           },
         };
       }
-      const chartDataSource = charts[chartId].form_data.datasource;
       chartConfiguration[chartId].crossFilters.chartsInScope =
         
isCrossFilterScopeGlobal(chartConfiguration[chartId].crossFilters.scope)
           ? globalChartConfiguration.chartsInScope.filter(
-              id =>
-                id !== Number(chartId) &&
-                chartsByDataSource[chartDataSource]?.has(id),
+              id => id !== Number(chartId),
             )
           : getChartIdsInFilterScope(
               chartConfiguration[chartId].crossFilters.scope,
diff --git a/superset-frontend/src/dashboard/util/getRelatedCharts.test.ts 
b/superset-frontend/src/dashboard/util/getRelatedCharts.test.ts
new file mode 100644
index 0000000000..94762e8f78
--- /dev/null
+++ b/superset-frontend/src/dashboard/util/getRelatedCharts.test.ts
@@ -0,0 +1,254 @@
+/**
+ * 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 {
+  AppliedCrossFilterType,
+  DatasourceType,
+  Filter,
+  NativeFilterType,
+} from '@superset-ui/core';
+import { DatasourcesState } from '../types';
+import { getRelatedCharts } from './getRelatedCharts';
+
+const slices = {
+  '1': { datasource: 'ds1', slice_id: 1 },
+  '2': { datasource: 'ds2', slice_id: 2 },
+  '3': { datasource: 'ds1', slice_id: 3 },
+} as any;
+
+const datasources: DatasourcesState = {
+  ds1: {
+    uid: 'ds1',
+    datasource_name: 'ds1',
+    table_name: 'table1',
+    description: '',
+    id: 100,
+    columns: [{ column_name: 'column1' }, { column_name: 'column2' }],
+    column_names: ['column1', 'column2'],
+    column_types: [],
+    type: DatasourceType.Table,
+    metrics: [],
+    column_formats: {},
+    currency_formats: {},
+    verbose_map: {},
+    main_dttm_col: '',
+    filter_select_enabled: true,
+  },
+  ds2: {
+    uid: 'ds2',
+    datasource_name: 'ds2',
+    table_name: 'table2',
+    description: '',
+    id: 200,
+    columns: [{ column_name: 'column3' }, { column_name: 'column4' }],
+    column_names: ['column3', 'column4'],
+    column_types: [],
+    type: DatasourceType.Table,
+    metrics: [],
+    column_formats: {},
+    currency_formats: {},
+    verbose_map: {},
+    main_dttm_col: '',
+    filter_select_enabled: true,
+  },
+};
+
+test('Return chart ids matching the dataset id with native filter', () => {
+  const filters = {
+    filterKey1: {
+      filterType: 'filter_select',
+      chartsInScope: [1, 2],
+      scope: {
+        excluded: [],
+        rootPath: [],
+      },
+      targets: [
+        {
+          column: { name: 'column1' },
+          datasetId: 100,
+        },
+      ],
+      type: NativeFilterType.NativeFilter,
+    } as unknown as Filter,
+  };
+
+  const result = getRelatedCharts(filters, null, slices, datasources);
+  expect(result).toEqual({
+    filterKey1: [1],
+  });
+});
+
+test('Return chart ids matching the dataset id with cross filter', () => {
+  const filters = {
+    '3': {
+      filterType: undefined,
+      scope: [1, 2],
+      targets: [],
+      values: null,
+    } as AppliedCrossFilterType,
+  };
+
+  const result = getRelatedCharts(filters, null, slices, datasources);
+  expect(result).toEqual({
+    '3': [1],
+  });
+});
+
+test('Return chart ids matching the column name with native filter', () => {
+  const filters = {
+    filterKey1: {
+      filterType: 'filter_select',
+      chartsInScope: [1, 2],
+      scope: {
+        excluded: [],
+        rootPath: [],
+      },
+      targets: [
+        {
+          column: { name: 'column3' },
+          datasetId: 999,
+        },
+      ],
+      type: NativeFilterType.NativeFilter,
+    } as unknown as Filter,
+  };
+
+  const result = getRelatedCharts(filters, null, slices, datasources);
+  expect(result).toEqual({
+    filterKey1: [2],
+  });
+});
+
+test('Return chart ids matching the column name with cross filter', () => {
+  const filters = {
+    '1': {
+      filterType: undefined,
+      scope: [1, 2],
+      targets: [],
+      values: {
+        filters: [{ col: 'column3' }],
+      },
+    } as AppliedCrossFilterType,
+  };
+
+  const result = getRelatedCharts(filters, null, slices, datasources);
+  expect(result).toEqual({
+    '1': [2],
+  });
+});
+
+test('Return chart ids when column display name matches with native filter', 
() => {
+  const filters = {
+    filterKey1: {
+      filterType: 'filter_select',
+      chartsInScope: [1, 2],
+      scope: {
+        excluded: [],
+        rootPath: [],
+      },
+      targets: [
+        {
+          column: { name: 'column4', displayName: 'column4' },
+          datasetId: 999,
+        },
+      ],
+      type: NativeFilterType.NativeFilter,
+    } as unknown as Filter,
+  };
+
+  const result = getRelatedCharts(filters, null, slices, datasources);
+  expect(result).toEqual({
+    filterKey1: [2],
+  });
+});
+
+test('Return chart ids when column display name matches with cross filter', () 
=> {
+  const filters = {
+    '1': {
+      filterType: undefined,
+      scope: [1, 2],
+      targets: [],
+      values: {
+        filters: [{ col: 'column4' }],
+      },
+    } as AppliedCrossFilterType,
+  };
+
+  const result = getRelatedCharts(filters, null, slices, datasources);
+  expect(result).toEqual({
+    '1': [2],
+  });
+});
+
+test('Return scope when filterType is not filter_select', () => {
+  const filters = {
+    filterKey1: {
+      filterType: 'filter_time',
+      chartsInScope: [3, 4],
+    } as Filter,
+  };
+
+  const result = getRelatedCharts(filters, null, slices, datasources);
+  expect(result).toEqual({
+    filterKey1: [3, 4],
+  });
+});
+
+test('Return an empty array if no matching charts found with native filter', 
() => {
+  const filters = {
+    filterKey1: {
+      filterType: 'filter_select',
+      chartsInScope: [1, 2],
+      scope: {
+        excluded: [],
+        rootPath: [],
+      },
+      targets: [
+        {
+          column: { name: 'nonexistent_column' },
+          datasetId: 300,
+        },
+      ],
+      type: NativeFilterType.NativeFilter,
+    } as unknown as Filter,
+  };
+
+  const result = getRelatedCharts(filters, null, slices, datasources);
+  expect(result).toEqual({
+    filterKey1: [],
+  });
+});
+
+test('Return an empty array if no matching charts found with cross filter', () 
=> {
+  const filters = {
+    '1': {
+      filterType: undefined,
+      scope: [1, 2],
+      targets: [],
+      values: {
+        filters: [{ col: 'nonexistent_column' }],
+      },
+    } as AppliedCrossFilterType,
+  };
+
+  const result = getRelatedCharts(filters, null, slices, datasources);
+  expect(result).toEqual({
+    '1': [],
+  });
+});
diff --git a/superset-frontend/src/dashboard/util/getRelatedCharts.ts 
b/superset-frontend/src/dashboard/util/getRelatedCharts.ts
new file mode 100644
index 0000000000..aba56ba3b6
--- /dev/null
+++ b/superset-frontend/src/dashboard/util/getRelatedCharts.ts
@@ -0,0 +1,200 @@
+/**
+ * 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 {
+  AppliedCrossFilterType,
+  AppliedNativeFilterType,
+  ensureIsArray,
+  Filter,
+  isAppliedCrossFilterType,
+  isAppliedNativeFilterType,
+  isNativeFilter,
+} from '@superset-ui/core';
+import { Slice } from 'src/types/Chart';
+import { DatasourcesState } from '../types';
+
+function checkForExpression(formData?: Record<string, any>) {
+  const groupby = ensureIsArray(formData?.groupby) ?? [];
+  const allColumns = ensureIsArray(formData?.all_columns) ?? [];
+  const checkColumns = groupby.concat(allColumns);
+  return checkColumns.some(
+    (col: string | Record<string, any>) =>
+      typeof col !== 'string' && col.expressionType !== undefined,
+  );
+}
+
+function getRelatedChartsForSelectFilter(
+  nativeFilter: AppliedNativeFilterType | Filter,
+  slices: Record<string, Slice>,
+  chartsInScope: number[],
+  datasources: DatasourcesState,
+) {
+  return Object.values(slices)
+    .filter(slice => {
+      const { datasource, slice_id } = slice;
+      // not in scope, ignore
+      if (!chartsInScope.includes(slice_id)) return false;
+
+      const chartDatasource = datasource
+        ? datasources[datasource]
+        : Object.values(datasources).find(ds => ds.id === slice.datasource_id);
+
+      const { column, datasetId } = nativeFilter.targets?.[0] ?? {};
+      const datasourceColumnNames = chartDatasource?.column_names ?? [];
+
+      // same datasource, always apply
+      if (chartDatasource?.id === datasetId) return true;
+
+      // let backend deal with adhoc columns and jinja
+      const hasSqlExpression = checkForExpression(slice.form_data);
+      if (hasSqlExpression) {
+        return true;
+      }
+
+      return datasourceColumnNames.some(
+        col => col === column?.name || col === column?.displayName,
+      );
+    })
+    .map(slice => slice.slice_id);
+}
+function getRelatedChartsForCrossFilter(
+  filterKey: string,
+  crossFilter: AppliedCrossFilterType,
+  slices: Record<string, Slice>,
+  scope: number[],
+  datasources: DatasourcesState,
+): number[] {
+  const sourceSlice = slices[filterKey];
+  const filters = crossFilter?.values?.filters ?? [];
+
+  if (!sourceSlice) return [];
+
+  const sourceDatasource = sourceSlice.datasource
+    ? datasources[sourceSlice.datasource]
+    : Object.values(datasources).find(
+        ds => ds.id === sourceSlice.datasource_id,
+      );
+
+  return Object.values(slices)
+    .filter(slice => {
+      // cross-filter emitter
+      if (slice.slice_id === Number(filterKey)) return false;
+      // not in scope, ignore
+      if (!scope.includes(slice.slice_id)) return false;
+
+      const targetDatasource = slice.datasource
+        ? datasources[slice.datasource]
+        : Object.values(datasources).find(ds => ds.id === slice.datasource_id);
+
+      // same datasource, always apply
+      if (targetDatasource === sourceDatasource) return true;
+
+      const targetColumnNames = targetDatasource?.column_names ?? [];
+
+      // let backend deal with adhoc columns and jinja
+      const hasSqlExpression = checkForExpression(slice.form_data);
+      if (hasSqlExpression) {
+        return true;
+      }
+
+      for (const filter of filters) {
+        // let backend deal with adhoc columns
+        if (
+          typeof filter.col !== 'string' &&
+          filter.col.expressionType !== undefined
+        ) {
+          return true;
+        }
+        // shared column names, different datasources, apply filter
+        if (targetColumnNames.includes(filter.col)) return true;
+      }
+
+      return false;
+    })
+    .map(slice => slice.slice_id);
+}
+
+export function getRelatedCharts(
+  filters: Record<
+    string,
+    AppliedNativeFilterType | AppliedCrossFilterType | Filter
+  >,
+  checkFilters: Record<
+    string,
+    AppliedNativeFilterType | AppliedCrossFilterType | Filter
+  > | null,
+  slices: Record<string, Slice>,
+  datasources: DatasourcesState,
+) {
+  const related = Object.entries(filters).reduce((acc, [filterKey, filter]) => 
{
+    const isCrossFilter =
+      Object.keys(slices).includes(filterKey) &&
+      isAppliedCrossFilterType(filter);
+
+    const chartsInScope = Array.isArray(filter.scope)
+      ? filter.scope
+      : ((filter as Filter).chartsInScope ?? []);
+
+    if (isCrossFilter) {
+      const checkFilter = checkFilters?.[filterKey] as AppliedCrossFilterType;
+      const crossFilter = filter as AppliedCrossFilterType;
+      const wasRemoved = !!(
+        ((filter.values && filter.values.filters === undefined) ||
+          filter.values?.filters?.length === 0) &&
+        checkFilter?.values?.filters?.length
+      );
+      const actualCrossFilter = wasRemoved ? checkFilter : crossFilter;
+
+      return {
+        ...acc,
+        [filterKey]: getRelatedChartsForCrossFilter(
+          filterKey,
+          actualCrossFilter,
+          slices,
+          chartsInScope,
+          datasources,
+        ),
+      };
+    }
+
+    const nativeFilter = filter as AppliedNativeFilterType | Filter;
+    // on highlight, a standard native filter is passed
+    // on apply, an applied native filter is passed
+    if (
+      isAppliedNativeFilterType(nativeFilter) ||
+      isNativeFilter(nativeFilter)
+    ) {
+      return {
+        ...acc,
+        [filterKey]: getRelatedChartsForSelectFilter(
+          nativeFilter,
+          slices,
+          chartsInScope,
+          datasources,
+        ),
+      };
+    }
+    return {
+      ...acc,
+      [filterKey]: chartsInScope,
+    };
+  }, {});
+
+  return related;
+}
diff --git 
a/superset-frontend/src/dashboard/util/useFilterFocusHighlightStyles.test.tsx 
b/superset-frontend/src/dashboard/util/useFilterFocusHighlightStyles.test.tsx
index d3010376da..ca19c63d51 100644
--- 
a/superset-frontend/src/dashboard/util/useFilterFocusHighlightStyles.test.tsx
+++ 
b/superset-frontend/src/dashboard/util/useFilterFocusHighlightStyles.test.tsx
@@ -24,6 +24,9 @@ import reducerIndex from 'spec/helpers/reducerIndex';
 import { screen, render } from 'spec/helpers/testing-library';
 import { initialState } from 'src/SqlLab/fixtures';
 import useFilterFocusHighlightStyles from './useFilterFocusHighlightStyles';
+import { getRelatedCharts } from './getRelatedCharts';
+
+jest.mock('./getRelatedCharts');
 
 const TestComponent = ({ chartId }: { chartId: number }) => {
   const styles = useFilterFocusHighlightStyles(chartId);
@@ -38,6 +41,7 @@ describe('useFilterFocusHighlightStyles', () => {
       { ...mockState, ...(initialState as any), ...customState },
       compose(applyMiddleware(thunk)),
     );
+  const mockGetRelatedCharts = getRelatedCharts as jest.Mock;
 
   const renderWrapper = (chartId: number, store = createMockStore()) =>
     render(<TestComponent chartId={chartId} />, {
@@ -57,6 +61,9 @@ describe('useFilterFocusHighlightStyles', () => {
   });
 
   it('should return unfocused styles if chart is not in scope of focused 
native filter', async () => {
+    mockGetRelatedCharts.mockReturnValue({
+      'test-filter': [],
+    });
     const store = createMockStore({
       nativeFilters: {
         focusedFilterId: 'test-filter',
@@ -76,6 +83,9 @@ describe('useFilterFocusHighlightStyles', () => {
   });
 
   it('should return unfocused styles if chart is not in scope of hovered 
native filter', async () => {
+    mockGetRelatedCharts.mockReturnValue({
+      'test-filter': [],
+    });
     const store = createMockStore({
       nativeFilters: {
         hoveredFilterId: 'test-filter',
@@ -96,6 +106,9 @@ describe('useFilterFocusHighlightStyles', () => {
 
   it('should return focused styles if chart is in scope of focused native 
filter', async () => {
     const chartId = 18;
+    mockGetRelatedCharts.mockReturnValue({
+      testFilter: [chartId],
+    });
     const store = createMockStore({
       nativeFilters: {
         focusedFilterId: 'testFilter',
@@ -116,6 +129,9 @@ describe('useFilterFocusHighlightStyles', () => {
 
   it('should return focused styles if chart is in scope of hovered native 
filter', async () => {
     const chartId = 18;
+    mockGetRelatedCharts.mockReturnValue({
+      testFilter: [chartId],
+    });
     const store = createMockStore({
       nativeFilters: {
         hoveredFilterId: 'testFilter',
@@ -136,6 +152,9 @@ describe('useFilterFocusHighlightStyles', () => {
 
   it('should return unfocused styles if focusedFilterField is targeting a 
different chart', async () => {
     const chartId = 18;
+    mockGetRelatedCharts.mockReturnValue({
+      testFilter: [],
+    });
     const store = createMockStore({
       dashboardState: {
         focusedFilterField: {
@@ -159,6 +178,9 @@ describe('useFilterFocusHighlightStyles', () => {
 
   it('should return focused styles if focusedFilterField chart equals our 
own', async () => {
     const chartId = 18;
+    mockGetRelatedCharts.mockReturnValue({
+      testFilter: [chartId],
+    });
     const store = createMockStore({
       dashboardState: {
         focusedFilterField: {
diff --git 
a/superset-frontend/src/dashboard/util/useFilterFocusHighlightStyles.ts 
b/superset-frontend/src/dashboard/util/useFilterFocusHighlightStyles.ts
index f1f428240c..74d31f60d6 100644
--- a/superset-frontend/src/dashboard/util/useFilterFocusHighlightStyles.ts
+++ b/superset-frontend/src/dashboard/util/useFilterFocusHighlightStyles.ts
@@ -16,11 +16,12 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import { useTheme } from '@superset-ui/core';
+import { Filter, useTheme } from '@superset-ui/core';
 import { useSelector } from 'react-redux';
 
 import { getChartIdsInFilterScope } from 
'src/dashboard/util/activeDashboardFilters';
 import { DashboardState, RootState } from 'src/dashboard/types';
+import { getRelatedCharts } from './getRelatedCharts';
 
 const selectFocusedFilterScope = (
   dashboardState: DashboardState,
@@ -41,6 +42,7 @@ const useFilterFocusHighlightStyles = (chartId: number) => {
   const dashboardState = useSelector(
     (state: RootState) => state.dashboardState,
   );
+
   const dashboardFilters = useSelector(
     (state: RootState) => state.dashboardFilters,
   );
@@ -49,6 +51,18 @@ const useFilterFocusHighlightStyles = (chartId: number) => {
     dashboardFilters,
   );
 
+  const datasources =
+    useSelector((state: RootState) => state.datasources) || {};
+  const slices =
+    useSelector((state: RootState) => state.sliceEntities.slices) || {};
+
+  const relatedCharts = getRelatedCharts(
+    nativeFilters.filters as Record<string, Filter>,
+    null,
+    slices,
+    datasources,
+  );
+
   const highlightedFilterId =
     nativeFilters?.focusedFilterId || nativeFilters?.hoveredFilterId;
   if (!(focusedFilterScope || highlightedFilterId)) {
@@ -69,11 +83,7 @@ const useFilterFocusHighlightStyles = (chartId: number) => {
   };
 
   if (highlightedFilterId) {
-    if (
-      nativeFilters.filters[highlightedFilterId]?.chartsInScope?.includes(
-        chartId,
-      )
-    ) {
+    if (relatedCharts[highlightedFilterId]?.includes(chartId)) {
       return focusedChartStyles;
     }
   } else if (
diff --git a/superset-frontend/src/types/Chart.ts 
b/superset-frontend/src/types/Chart.ts
index f26525cd33..b7c439cb21 100644
--- a/superset-frontend/src/types/Chart.ts
+++ b/superset-frontend/src/types/Chart.ts
@@ -74,6 +74,8 @@ export type Slice = {
   query_context?: object;
   is_managed_externally: boolean;
   owners?: number[];
+  datasource?: string;
+  datasource_id?: number;
 };
 
 export default Chart;
diff --git a/superset/connectors/sqla/models.py 
b/superset/connectors/sqla/models.py
index 82980cef16..e8aa0d705b 100644
--- a/superset/connectors/sqla/models.py
+++ b/superset/connectors/sqla/models.py
@@ -489,6 +489,11 @@ class BaseDatasource(AuditMixinNullable, 
ImportExportMixin):  # pylint: disable=
         del data["description"]
         data.update({"metrics": filtered_metrics})
         data.update({"columns": filtered_columns})
+
+        all_columns = {
+            column_["column_name"]: column_["verbose_name"] or 
column_["column_name"]
+            for column_ in filtered_columns
+        }
         verbose_map = {"__timestamp": "Time"}
         verbose_map.update(
             {
@@ -496,14 +501,9 @@ class BaseDatasource(AuditMixinNullable, 
ImportExportMixin):  # pylint: disable=
                 for metric in filtered_metrics
             }
         )
-        verbose_map.update(
-            {
-                column_["column_name"]: column_["verbose_name"]
-                or column_["column_name"]
-                for column_ in filtered_columns
-            }
-        )
+        verbose_map.update(all_columns)
         data["verbose_map"] = verbose_map
+        data["column_names"] = set(all_columns.values()) | 
set(self.column_names)
 
         return data
 
diff --git a/superset/dashboards/schemas.py b/superset/dashboards/schemas.py
index b0a47aba41..d63e79336c 100644
--- a/superset/dashboards/schemas.py
+++ b/superset/dashboards/schemas.py
@@ -262,6 +262,7 @@ class DashboardDatasetSchema(Schema):
     owners = fields.List(fields.Dict())
     columns = fields.List(fields.Dict())
     column_types = fields.List(fields.Int())
+    column_names = fields.List(fields.Str())
     metrics = fields.List(fields.Dict())
     order_by_choices = fields.List(fields.List(fields.Str()))
     verbose_map = fields.Dict(fields.Str(), fields.Str())


Reply via email to