You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by di...@apache.org on 2023/02/27 11:55:18 UTC

[superset] branch master updated: feat: Cross Filters in FilterBar (#23138)

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

diegopucci 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 dcd3e00f39 feat: Cross Filters in FilterBar (#23138)
dcd3e00f39 is described below

commit dcd3e00f39ca16979c53d7bead865a54d23bc622
Author: Geido <60...@users.noreply.github.com>
AuthorDate: Mon Feb 27 12:55:09 2023 +0100

    feat: Cross Filters in FilterBar (#23138)
    
    Co-authored-by: Kamil Gabryjelski <ka...@gmail.com>
---
 .../DashboardBuilder/DashboardBuilder.tsx          |   6 +-
 .../DetailsPanel/DetailsPanel.test.tsx             |   2 +-
 .../components/FiltersBadge/DetailsPanel/index.tsx |   2 +-
 .../FilterIndicator/FilterIndicator.test.tsx       |   2 +-
 .../FiltersBadge/FilterIndicator/index.tsx         |   2 +-
 .../dashboard/components/FiltersBadge/index.tsx    |  14 +-
 .../FilterBar/CrossFilters/CrossFilter.test.tsx    |  82 ++++++++++
 .../FilterBar/CrossFilters/CrossFilter.tsx         | 114 +++++++++++++
 .../FilterBar/CrossFilters/CrossFilterTag.test.tsx |  61 +++++++
 .../FilterBar/CrossFilters/CrossFilterTag.tsx      |  93 +++++++++++
 .../CrossFilters/CrossFilterTitle.test.tsx         |  52 ++++++
 .../FilterBar/CrossFilters/CrossFilterTitle.tsx    |  90 +++++++++++
 .../FilterBar/CrossFilters/Vertical.tsx            |  46 ++++++
 .../CrossFilters/VerticalCollapse.test.tsx         | 107 ++++++++++++
 .../FilterBar/CrossFilters/VerticalCollapse.tsx    | 102 ++++++++++++
 .../FilterBar/CrossFilters/selectors.ts            |  53 ++++++
 .../nativeFilters/FilterBar/CrossFilters/styles.ts |  28 ++++
 .../FilterBar/FilterControls/FilterControls.tsx    | 110 ++++++++++---
 .../FilterBar/FiltersDropdownContent/index.tsx     |  17 ++
 .../FiltersOutOfScopeCollapsible/index.tsx         |  10 +-
 .../nativeFilters/FilterBar/Horizontal.tsx         |  32 +++-
 .../nativeFilters/FilterBar/Vertical.tsx           | 180 +++++++++++----------
 .../{FiltersBadge => nativeFilters}/selectors.ts   | 161 +++++++++++-------
 23 files changed, 1190 insertions(+), 176 deletions(-)

diff --git a/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardBuilder.tsx b/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardBuilder.tsx
index 9d6d3a51eb..99360801c5 100644
--- a/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardBuilder.tsx
+++ b/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardBuilder.tsx
@@ -445,6 +445,9 @@ const DashboardBuilder: FC<DashboardBuilderProps> = () => {
   const fullSizeChartId = useSelector<RootState, number | null>(
     state => state.dashboardState.fullSizeChartId,
   );
+  const crossFiltersEnabled = isFeatureEnabled(
+    FeatureFlag.DASHBOARD_CROSS_FILTERS,
+  );
   const filterBarOrientation = useSelector<RootState, FilterBarOrientation>(
     ({ dashboardInfo }) =>
       isFeatureEnabled(FeatureFlag.HORIZONTAL_FILTER_BAR)
@@ -523,7 +526,8 @@ const DashboardBuilder: FC<DashboardBuilderProps> = () => {
   const filterSetEnabled = isFeatureEnabled(
     FeatureFlag.DASHBOARD_NATIVE_FILTERS_SET,
   );
-  const showFilterBar = nativeFiltersEnabled && !editMode;
+  const showFilterBar =
+    (crossFiltersEnabled || nativeFiltersEnabled) && !editMode;
 
   const offset =
     FILTER_BAR_HEADER_HEIGHT +
diff --git a/superset-frontend/src/dashboard/components/FiltersBadge/DetailsPanel/DetailsPanel.test.tsx b/superset-frontend/src/dashboard/components/FiltersBadge/DetailsPanel/DetailsPanel.test.tsx
index 8d2b121ff6..3c3e25ee56 100644
--- a/superset-frontend/src/dashboard/components/FiltersBadge/DetailsPanel/DetailsPanel.test.tsx
+++ b/superset-frontend/src/dashboard/components/FiltersBadge/DetailsPanel/DetailsPanel.test.tsx
@@ -19,7 +19,7 @@
 import userEvent from '@testing-library/user-event';
 import React from 'react';
 import { render, screen } from 'spec/helpers/testing-library';
-import { Indicator } from 'src/dashboard/components/FiltersBadge/selectors';
+import { Indicator } from 'src/dashboard/components/nativeFilters/selectors';
 import DetailsPanel from '.';
 
 const createProps = () => ({
diff --git a/superset-frontend/src/dashboard/components/FiltersBadge/DetailsPanel/index.tsx b/superset-frontend/src/dashboard/components/FiltersBadge/DetailsPanel/index.tsx
index 3531ab1be9..022dab7968 100644
--- a/superset-frontend/src/dashboard/components/FiltersBadge/DetailsPanel/index.tsx
+++ b/superset-frontend/src/dashboard/components/FiltersBadge/DetailsPanel/index.tsx
@@ -29,7 +29,7 @@ import {
   Reset,
   Title,
 } from 'src/dashboard/components/FiltersBadge/Styles';
-import { Indicator } from 'src/dashboard/components/FiltersBadge/selectors';
+import { Indicator } from 'src/dashboard/components/nativeFilters/selectors';
 import FilterIndicator from 'src/dashboard/components/FiltersBadge/FilterIndicator';
 import { RootState } from 'src/dashboard/types';
 
diff --git a/superset-frontend/src/dashboard/components/FiltersBadge/FilterIndicator/FilterIndicator.test.tsx b/superset-frontend/src/dashboard/components/FiltersBadge/FilterIndicator/FilterIndicator.test.tsx
index fdded804b8..de49516a64 100644
--- a/superset-frontend/src/dashboard/components/FiltersBadge/FilterIndicator/FilterIndicator.test.tsx
+++ b/superset-frontend/src/dashboard/components/FiltersBadge/FilterIndicator/FilterIndicator.test.tsx
@@ -19,7 +19,7 @@
 import userEvent from '@testing-library/user-event';
 import React from 'react';
 import { render, screen } from 'spec/helpers/testing-library';
-import { Indicator } from 'src/dashboard/components/FiltersBadge/selectors';
+import { Indicator } from 'src/dashboard/components/nativeFilters/selectors';
 import FilterIndicator from '.';
 
 const createProps = () => ({
diff --git a/superset-frontend/src/dashboard/components/FiltersBadge/FilterIndicator/index.tsx b/superset-frontend/src/dashboard/components/FiltersBadge/FilterIndicator/index.tsx
index e7675a3ef0..2954474e64 100644
--- a/superset-frontend/src/dashboard/components/FiltersBadge/FilterIndicator/index.tsx
+++ b/superset-frontend/src/dashboard/components/FiltersBadge/FilterIndicator/index.tsx
@@ -28,7 +28,7 @@ import {
   ItemIcon,
   Title,
 } from 'src/dashboard/components/FiltersBadge/Styles';
-import { Indicator } from 'src/dashboard/components/FiltersBadge/selectors';
+import { Indicator } from 'src/dashboard/components/nativeFilters/selectors';
 
 export interface IndicatorProps {
   indicator: Indicator;
diff --git a/superset-frontend/src/dashboard/components/FiltersBadge/index.tsx b/superset-frontend/src/dashboard/components/FiltersBadge/index.tsx
index c7748849a8..fb0718bf54 100644
--- a/superset-frontend/src/dashboard/components/FiltersBadge/index.tsx
+++ b/superset-frontend/src/dashboard/components/FiltersBadge/index.tsx
@@ -24,12 +24,6 @@ import { DataMaskStateWithId, Filters } from '@superset-ui/core';
 import Icons from 'src/components/Icons';
 import { usePrevious } from 'src/hooks/usePrevious';
 import { setDirectPathToChild } from 'src/dashboard/actions/dashboardState';
-import {
-  ChartsState,
-  DashboardInfo,
-  DashboardLayout,
-  RootState,
-} from 'src/dashboard/types';
 import DetailsPanelPopover from './DetailsPanel';
 import { Pill } from './Styles';
 import {
@@ -37,7 +31,13 @@ import {
   IndicatorStatus,
   selectIndicatorsForChart,
   selectNativeIndicatorsForChart,
-} from './selectors';
+} from '../nativeFilters/selectors';
+import {
+  ChartsState,
+  DashboardInfo,
+  DashboardLayout,
+  RootState,
+} from '../../types';
 
 export interface FiltersBadgeProps {
   chartId: number;
diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/CrossFilters/CrossFilter.test.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/CrossFilters/CrossFilter.test.tsx
new file mode 100644
index 0000000000..85dc3bf705
--- /dev/null
+++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/CrossFilters/CrossFilter.test.tsx
@@ -0,0 +1,82 @@
+/**
+ * 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 React from 'react';
+import { render, screen } from 'spec/helpers/testing-library';
+import { FilterBarOrientation } from 'src/dashboard/types';
+import { IndicatorStatus } from '../../selectors';
+import CrossFilter from './CrossFilter';
+
+const mockedProps = {
+  filter: {
+    name: 'test',
+    emitterId: 1,
+    column: 'country_name',
+    value: 'Italy',
+    status: IndicatorStatus.CrossFilterApplied,
+    path: ['test-path'],
+  },
+  orientation: FilterBarOrientation.HORIZONTAL,
+  last: false,
+};
+
+const setup = (props: typeof mockedProps) =>
+  render(<CrossFilter {...props} />, {
+    useRedux: true,
+  });
+
+test('CrossFilter should render', () => {
+  const { container } = setup(mockedProps);
+  expect(container).toBeInTheDocument();
+});
+
+test('Title should render', () => {
+  setup(mockedProps);
+  expect(screen.getByText('test')).toBeInTheDocument();
+});
+
+test('Search icon should be visible', () => {
+  setup(mockedProps);
+  expect(
+    screen.getByTestId('cross-filters-highlight-emitter'),
+  ).toBeInTheDocument();
+});
+
+test('Column and value should be visible', () => {
+  setup(mockedProps);
+  expect(screen.getByText('country_name')).toBeInTheDocument();
+  expect(screen.getByText('Italy')).toBeInTheDocument();
+});
+
+test('Tag should be closable', () => {
+  setup(mockedProps);
+  expect(screen.getByRole('img', { name: 'close' })).toBeInTheDocument();
+});
+
+test('Divider should not be visible', () => {
+  setup(mockedProps);
+  expect(screen.queryByTestId('cross-filters-divider')).not.toBeInTheDocument();
+});
+
+test('Divider should be visible', () => {
+  setup({
+    ...mockedProps,
+    last: true,
+  });
+  expect(screen.getByTestId('cross-filters-divider')).toBeInTheDocument();
+});
diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/CrossFilters/CrossFilter.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/CrossFilters/CrossFilter.tsx
new file mode 100644
index 0000000000..f03e777b25
--- /dev/null
+++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/CrossFilters/CrossFilter.tsx
@@ -0,0 +1,114 @@
+/**
+ * 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 React, { useCallback } from 'react';
+import { css, useTheme } from '@superset-ui/core';
+import { CrossFilterIndicator } from 'src/dashboard/components/nativeFilters/selectors';
+import { useDispatch } from 'react-redux';
+import { setFocusedNativeFilter } from 'src/dashboard/actions/nativeFilters';
+import { FilterBarOrientation } from 'src/dashboard/types';
+import { updateDataMask } from 'src/dataMask/actions';
+import CrossFilterTag from './CrossFilterTag';
+import CrossFilterTitle from './CrossFilterTitle';
+
+const CrossFilter = (props: {
+  filter: CrossFilterIndicator;
+  orientation: FilterBarOrientation;
+  last?: boolean;
+}) => {
+  const { filter, orientation, last } = props;
+  const theme = useTheme();
+  const dispatch = useDispatch();
+
+  const handleHighlightFilterSource = useCallback(
+    (path?: string[]) => {
+      if (path) {
+        dispatch(setFocusedNativeFilter(path[0]));
+      }
+    },
+    [dispatch],
+  );
+
+  const handleRemoveCrossFilter = (chartId: number) => {
+    dispatch(
+      updateDataMask(chartId, {
+        extraFormData: {
+          filters: [],
+        },
+        filterState: {
+          value: null,
+          selectedValues: null,
+        },
+      }),
+    );
+  };
+
+  return (
+    <div
+      key={`${filter.name}${filter.emitterId}`}
+      css={css`
+        ${orientation === FilterBarOrientation.VERTICAL
+          ? `
+            display: block;
+            margin-bottom: ${theme.gridUnit * 4}px;
+          `
+          : `
+            display: flex;
+          `}
+      `}
+    >
+      <CrossFilterTitle
+        title={filter.name}
+        orientation={orientation || FilterBarOrientation.HORIZONTAL}
+        onHighlightFilterSource={() => handleHighlightFilterSource(filter.path)}
+      />
+      {(filter.column || filter.value) && (
+        <CrossFilterTag
+          filter={filter}
+          orientation={orientation}
+          removeCrossFilter={handleRemoveCrossFilter}
+        />
+      )}
+      {last && (
+        <span
+          data-test="cross-filters-divider"
+          css={css`
+            ${orientation === FilterBarOrientation.HORIZONTAL
+              ? `
+                width: 1px;
+                height: 22px;
+                margin-left: ${theme.gridUnit * 4}px;
+                margin-right: ${theme.gridUnit}px;
+              `
+              : `
+                width: 100%;
+                height: 1px;
+                display: block;
+                margin-bottom: ${theme.gridUnit * 4}px;
+                margin-top: ${theme.gridUnit * 4}px;
+            `}
+            background: ${theme.colors.grayscale.light2};
+          `}
+        />
+      )}
+    </div>
+  );
+};
+
+export default CrossFilter;
diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/CrossFilters/CrossFilterTag.test.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/CrossFilters/CrossFilterTag.test.tsx
new file mode 100644
index 0000000000..72f831dcfa
--- /dev/null
+++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/CrossFilters/CrossFilterTag.test.tsx
@@ -0,0 +1,61 @@
+/**
+ * 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 userEvent from '@testing-library/user-event';
+import React from 'react';
+import { render, screen } from 'spec/helpers/testing-library';
+import { FilterBarOrientation } from 'src/dashboard/types';
+import { IndicatorStatus } from '../../selectors';
+import CrossFilterTag from './CrossFilterTag';
+
+const mockedProps = {
+  filter: {
+    name: 'test',
+    emitterId: 1,
+    column: 'country_name',
+    value: 'Italy',
+    status: IndicatorStatus.CrossFilterApplied,
+    path: ['test-path'],
+  },
+  orientation: FilterBarOrientation.HORIZONTAL,
+  removeCrossFilter: jest.fn(),
+};
+
+const setup = (props: typeof mockedProps) =>
+  render(<CrossFilterTag {...props} />, {
+    useRedux: true,
+  });
+
+test('CrossFilterTag should render', () => {
+  const { container } = setup(mockedProps);
+  expect(container).toBeInTheDocument();
+});
+
+test('Column and value should be visible', () => {
+  setup(mockedProps);
+  expect(screen.getByText('country_name')).toBeInTheDocument();
+  expect(screen.getByText('Italy')).toBeInTheDocument();
+});
+
+test('Tag should be closable', () => {
+  setup(mockedProps);
+  const close = screen.getByRole('img', { name: 'close' });
+  expect(close).toBeInTheDocument();
+  userEvent.click(close);
+  expect(mockedProps.removeCrossFilter).toHaveBeenCalledWith(1);
+});
diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/CrossFilters/CrossFilterTag.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/CrossFilters/CrossFilterTag.tsx
new file mode 100644
index 0000000000..e59070df7a
--- /dev/null
+++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/CrossFilters/CrossFilterTag.tsx
@@ -0,0 +1,93 @@
+/**
+ * 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 React from 'react';
+import { styled, css, useTheme } from '@superset-ui/core';
+import { CrossFilterIndicator } from 'src/dashboard/components/nativeFilters/selectors';
+import { Tag } from 'src/components';
+import { Tooltip } from 'src/components/Tooltip';
+import useCSSTextTruncation from 'src/hooks/useTruncation/useCSSTextTruncation';
+import { FilterBarOrientation } from 'src/dashboard/types';
+import { ellipsisCss } from './styles';
+
+const StyledCrossFilterValue = styled.b`
+  ${({ theme }) => `
+    max-width: ${theme.gridUnit * 25}px;
+  `}
+  ${ellipsisCss}
+`;
+
+const StyledCrossFilterColumn = styled('span')`
+  ${({ theme }) => `
+    max-width: ${theme.gridUnit * 25}px;
+    padding-right: ${theme.gridUnit}px;
+  `}
+  ${ellipsisCss}
+`;
+
+const StyledTag = styled(Tag)`
+  ${({ theme }) => `
+    border: 1px solid ${theme.colors.grayscale.light3};
+    border-radius: 2px;
+    .anticon-close {
+      vertical-align: middle;
+    }
+  `}
+`;
+
+const CrossFilterTag = (props: {
+  filter: CrossFilterIndicator;
+  orientation: FilterBarOrientation;
+  removeCrossFilter: (filterId: number) => void;
+}) => {
+  const { filter, orientation, removeCrossFilter } = props;
+  const theme = useTheme();
+  const [columnRef, columnIsTruncated] =
+    useCSSTextTruncation<HTMLSpanElement>();
+  const [valueRef, valueIsTruncated] = useCSSTextTruncation<HTMLSpanElement>();
+
+  return (
+    <StyledTag
+      css={css`
+        ${orientation === FilterBarOrientation.VERTICAL
+          ? `
+            margin-top: ${theme.gridUnit * 2}px;
+          `
+          : `
+            margin-left: ${theme.gridUnit * 2}px;
+          `}
+      `}
+      closable
+      onClose={() => removeCrossFilter(filter.emitterId)}
+    >
+      <Tooltip title={columnIsTruncated ? filter.column : null}>
+        <StyledCrossFilterColumn ref={columnRef}>
+          {filter.column}
+        </StyledCrossFilterColumn>
+      </Tooltip>
+      <Tooltip title={valueIsTruncated ? filter.value : null}>
+        <StyledCrossFilterValue ref={valueRef}>
+          {filter.value}
+        </StyledCrossFilterValue>
+      </Tooltip>
+    </StyledTag>
+  );
+};
+
+export default CrossFilterTag;
diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/CrossFilters/CrossFilterTitle.test.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/CrossFilters/CrossFilterTitle.test.tsx
new file mode 100644
index 0000000000..16987440a2
--- /dev/null
+++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/CrossFilters/CrossFilterTitle.test.tsx
@@ -0,0 +1,52 @@
+/**
+ * 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 userEvent from '@testing-library/user-event';
+import React from 'react';
+import { render, screen } from 'spec/helpers/testing-library';
+import { FilterBarOrientation } from 'src/dashboard/types';
+import CrossFilterTitle from './CrossFilterTitle';
+
+const mockedProps = {
+  title: 'test-title',
+  orientation: FilterBarOrientation.HORIZONTAL,
+  onHighlightFilterSource: jest.fn(),
+};
+
+const setup = (props: typeof mockedProps) =>
+  render(<CrossFilterTitle {...props} />, {
+    useRedux: true,
+  });
+
+test('CrossFilterTitle should render', () => {
+  const { container } = setup(mockedProps);
+  expect(container).toBeInTheDocument();
+});
+
+test('Title should be visible', () => {
+  setup(mockedProps);
+  expect(screen.getByText('test-title')).toBeInTheDocument();
+});
+
+test('Search icon should highlight emitter', () => {
+  setup(mockedProps);
+  const search = screen.getByTestId('cross-filters-highlight-emitter');
+  expect(search).toBeInTheDocument();
+  userEvent.click(search);
+  expect(mockedProps.onHighlightFilterSource).toHaveBeenCalled();
+});
diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/CrossFilters/CrossFilterTitle.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/CrossFilters/CrossFilterTitle.tsx
new file mode 100644
index 0000000000..70f0ad2cd9
--- /dev/null
+++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/CrossFilters/CrossFilterTitle.tsx
@@ -0,0 +1,90 @@
+/**
+ * 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 React from 'react';
+import { t, css, styled, useTheme } from '@superset-ui/core';
+import { Tooltip } from 'src/components/Tooltip';
+import useCSSTextTruncation from 'src/hooks/useTruncation/useCSSTextTruncation';
+import { FilterBarOrientation } from 'src/dashboard/types';
+import Icons from 'src/components/Icons';
+import { ellipsisCss } from './styles';
+
+const StyledCrossFilterTitle = styled.div`
+  ${({ theme }) => `
+    display: flex;
+    font-size: ${theme.typography.sizes.s}px;
+    color: ${theme.colors.grayscale.base};
+    vertical-align: middle;
+    align-items: center;
+  `}
+`;
+
+const StyledIconSearch = styled(Icons.SearchOutlined)`
+  ${({ theme }) => `
+    & > span.anticon.anticon-search {
+      color: ${theme.colors.grayscale.light1};
+      margin-left: ${theme.gridUnit}px;
+      transition: 0.3s;
+      vertical-align: middle;
+      line-height: 0;
+      &:hover {
+        color: ${theme.colors.grayscale.base};
+      }
+    }
+  `}
+`;
+
+const CrossFilterChartTitle = (props: {
+  title: string;
+  orientation: FilterBarOrientation;
+  onHighlightFilterSource: () => void;
+}) => {
+  const { title, orientation, onHighlightFilterSource } = props;
+  const [titleRef, titleIsTruncated] = useCSSTextTruncation<HTMLSpanElement>();
+  const theme = useTheme();
+  return (
+    <StyledCrossFilterTitle>
+      <Tooltip title={titleIsTruncated ? title : null}>
+        <span
+          css={css`
+            max-width: ${orientation === FilterBarOrientation.VERTICAL
+              ? `${theme.gridUnit * 45}px`
+              : `${theme.gridUnit * 15}px`};
+            line-height: 1.4;
+            ${ellipsisCss}
+          `}
+          ref={titleRef}
+        >
+          {title}
+        </span>
+      </Tooltip>
+      <Tooltip title={t('Locate the chart')}>
+        <StyledIconSearch
+          iconSize="s"
+          data-test="cross-filters-highlight-emitter"
+          role="button"
+          tabIndex={0}
+          onClick={onHighlightFilterSource}
+        />
+      </Tooltip>
+    </StyledCrossFilterTitle>
+  );
+};
+
+export default CrossFilterChartTitle;
diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/CrossFilters/Vertical.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/CrossFilters/Vertical.tsx
new file mode 100644
index 0000000000..93fb649d68
--- /dev/null
+++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/CrossFilters/Vertical.tsx
@@ -0,0 +1,46 @@
+/**
+ * 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 React from 'react';
+import { DataMaskStateWithId } from '@superset-ui/core';
+import { useSelector } from 'react-redux';
+import { DashboardInfo, DashboardLayout, RootState } from 'src/dashboard/types';
+import crossFiltersSelector from './selectors';
+import VerticalCollapse from './VerticalCollapse';
+
+const CrossFiltersVertical = () => {
+  const dataMask = useSelector<RootState, DataMaskStateWithId>(
+    state => state.dataMask,
+  );
+  const dashboardInfo = useSelector<RootState, DashboardInfo>(
+    state => state.dashboardInfo,
+  );
+  const dashboardLayout = useSelector<RootState, DashboardLayout>(
+    state => state.dashboardLayout.present,
+  );
+  const selectedCrossFilters = crossFiltersSelector({
+    dataMask,
+    dashboardInfo,
+    dashboardLayout,
+  });
+
+  return <VerticalCollapse crossFilters={selectedCrossFilters} />;
+};
+
+export default CrossFiltersVertical;
diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/CrossFilters/VerticalCollapse.test.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/CrossFilters/VerticalCollapse.test.tsx
new file mode 100644
index 0000000000..92aaf8cfc6
--- /dev/null
+++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/CrossFilters/VerticalCollapse.test.tsx
@@ -0,0 +1,107 @@
+/**
+ * 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 React from 'react';
+import { render, screen } from 'spec/helpers/testing-library';
+import { IndicatorStatus } from '../../selectors';
+import VerticalCollapse from './VerticalCollapse';
+
+const mockedProps = {
+  crossFilters: [
+    {
+      name: 'test',
+      emitterId: 1,
+      column: 'country_name',
+      value: 'Italy',
+      status: IndicatorStatus.CrossFilterApplied,
+      path: ['test-path'],
+    },
+    {
+      name: 'test-b',
+      emitterId: 2,
+      column: 'country_code',
+      value: 'IT',
+      status: IndicatorStatus.CrossFilterApplied,
+      path: ['test-path-2'],
+    },
+  ],
+};
+
+const setup = (props: typeof mockedProps) =>
+  render(<VerticalCollapse {...props} />, {
+    useRedux: true,
+  });
+
+test('VerticalCollapse should render', () => {
+  const { container } = setup(mockedProps);
+  expect(container).toBeInTheDocument();
+});
+
+test('Collapse with title should render', () => {
+  setup(mockedProps);
+  expect(screen.getByText('Cross-filters')).toBeInTheDocument();
+});
+
+test('Collapse should not render when empty', () => {
+  setup({
+    crossFilters: [],
+  });
+  expect(screen.queryByText('Cross-filters')).not.toBeInTheDocument();
+  expect(screen.queryByText('test')).not.toBeInTheDocument();
+  expect(screen.queryByText('test-b')).not.toBeInTheDocument();
+  expect(
+    screen.queryByTestId('cross-filters-highlight-emitter'),
+  ).not.toBeInTheDocument();
+  expect(screen.queryByRole('img', { name: 'close' })).not.toBeInTheDocument();
+  expect(screen.queryByText('country_name')).not.toBeInTheDocument();
+  expect(screen.queryByText('Italy')).not.toBeInTheDocument();
+  expect(screen.queryByText('country_code')).not.toBeInTheDocument();
+  expect(screen.queryByText('IT')).not.toBeInTheDocument();
+  expect(screen.queryByTestId('cross-filters-divider')).not.toBeInTheDocument();
+});
+
+test('Titles should be visible', () => {
+  setup(mockedProps);
+  expect(screen.getByText('test')).toBeInTheDocument();
+  expect(screen.getByText('test-b')).toBeInTheDocument();
+});
+
+test('Search icons should be visible', () => {
+  setup(mockedProps);
+  expect(screen.getAllByTestId('cross-filters-highlight-emitter')).toHaveLength(
+    2,
+  );
+});
+
+test('Tags should be visible', () => {
+  setup(mockedProps);
+  expect(screen.getByText('country_name')).toBeInTheDocument();
+  expect(screen.getByText('Italy')).toBeInTheDocument();
+  expect(screen.getByText('country_code')).toBeInTheDocument();
+  expect(screen.getByText('IT')).toBeInTheDocument();
+});
+
+test('Tags should be closable', () => {
+  setup(mockedProps);
+  expect(screen.getAllByRole('img', { name: 'close' })).toHaveLength(2);
+});
+
+test('Divider should be visible', () => {
+  setup(mockedProps);
+  expect(screen.getByTestId('cross-filters-divider')).toBeInTheDocument();
+});
diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/CrossFilters/VerticalCollapse.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/CrossFilters/VerticalCollapse.tsx
new file mode 100644
index 0000000000..a748f9ed99
--- /dev/null
+++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/CrossFilters/VerticalCollapse.tsx
@@ -0,0 +1,102 @@
+/**
+ * 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 React, { useMemo } from 'react';
+import Collapse from 'src/components/Collapse';
+import { styled, t, useTheme, css } from '@superset-ui/core';
+import { FilterBarOrientation } from 'src/dashboard/types';
+import CrossFilter from './CrossFilter';
+import { CrossFilterIndicator } from '../../selectors';
+
+const StyledCollapse = styled(Collapse)`
+  ${({ theme }) => `
+    .ant-collapse-header {
+      margin-bottom: ${theme.gridUnit * 4}px;
+    }
+    .ant-collapse-item > .ant-collapse-header {
+      padding-bottom: 0;
+    }
+    .ant-collapse-item > .ant-collapse-header > .ant-collapse-arrow {
+      font-size: ${theme.typography.sizes.xs}px;
+      padding-top: ${theme.gridUnit * 3}px;
+    }
+    .ant-collapse-item > .ant-collapse-content > .ant-collapse-content-box {
+      padding-top: 0;
+    }
+  `}
+`;
+
+const StyledCrossFiltersTitle = styled.span`
+  ${({ theme }) => `
+    font-size: ${theme.typography.sizes.s}px;
+  `}
+`;
+
+const CrossFiltersVerticalCollapse = (props: {
+  crossFilters: CrossFilterIndicator[];
+}) => {
+  const { crossFilters } = props;
+  const theme = useTheme();
+  const crossFiltersIndicators = useMemo(
+    () =>
+      crossFilters.map(filter => (
+        <CrossFilter
+          key={filter.emitterId}
+          filter={filter}
+          orientation={FilterBarOrientation.VERTICAL}
+        />
+      )),
+    [crossFilters],
+  );
+
+  if (!crossFilters.length) {
+    return null;
+  }
+
+  return (
+    <StyledCollapse
+      ghost
+      defaultActiveKey="crossFilters"
+      expandIconPosition="right"
+    >
+      <Collapse.Panel
+        key="crossFilters"
+        header={
+          <StyledCrossFiltersTitle>
+            {t('Cross-filters')}
+          </StyledCrossFiltersTitle>
+        }
+      >
+        {crossFiltersIndicators}
+        <span
+          data-test="cross-filters-divider"
+          css={css`
+            width: 100%;
+            height: 1px;
+            display: block;
+            background: ${theme.colors.grayscale.light3};
+            margin: ${theme.gridUnit * 8}px auto 0 auto;
+          `}
+        />
+      </Collapse.Panel>
+    </StyledCollapse>
+  );
+};
+
+export default CrossFiltersVerticalCollapse;
diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/CrossFilters/selectors.ts b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/CrossFilters/selectors.ts
new file mode 100644
index 0000000000..c0f45af5b3
--- /dev/null
+++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/CrossFilters/selectors.ts
@@ -0,0 +1,53 @@
+/**
+ * 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 { DataMaskStateWithId } from '@superset-ui/core';
+import { DashboardInfo, DashboardLayout } from 'src/dashboard/types';
+import { CrossFilterIndicator, selectChartCrossFilters } from '../../selectors';
+
+export const crossFiltersSelector = (props: {
+  dataMask: DataMaskStateWithId;
+  dashboardInfo: DashboardInfo;
+  dashboardLayout: DashboardLayout;
+}): CrossFilterIndicator[] => {
+  const { dataMask, dashboardInfo, dashboardLayout } = props;
+  const chartConfiguration = dashboardInfo.metadata?.chart_configuration;
+  const chartsIds = Object.keys(chartConfiguration);
+  const shouldFilterEmitters = true;
+
+  let selectedCrossFilters: CrossFilterIndicator[] = [];
+
+  for (let i = 0; i < chartsIds.length; i += 1) {
+    const chartId = Number(chartsIds[i]);
+    const crossFilters = selectChartCrossFilters(
+      dataMask,
+      chartId,
+      dashboardLayout,
+      chartConfiguration,
+      shouldFilterEmitters,
+    );
+    selectedCrossFilters = [
+      ...selectedCrossFilters,
+      ...(crossFilters as CrossFilterIndicator[]),
+    ];
+  }
+  return selectedCrossFilters;
+};
+
+export default crossFiltersSelector;
diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/CrossFilters/styles.ts b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/CrossFilters/styles.ts
new file mode 100644
index 0000000000..0eee141c10
--- /dev/null
+++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/CrossFilters/styles.ts
@@ -0,0 +1,28 @@
+/**
+ * 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 { css } from '@superset-ui/core';
+
+export const ellipsisCss = css`
+  white-space: nowrap;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  display: inline-block;
+  vertical-align: middle;
+`;
diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterControls/FilterControls.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterControls/FilterControls.tsx
index fa0bfeac6f..d4ec83c1e2 100644
--- a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterControls/FilterControls.tsx
+++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterControls/FilterControls.tsx
@@ -46,7 +46,12 @@ import {
   useDashboardHasTabs,
   useSelectFiltersInScope,
 } from 'src/dashboard/components/nativeFilters/state';
-import { FilterBarOrientation, RootState } from 'src/dashboard/types';
+import {
+  DashboardInfo,
+  DashboardLayout,
+  FilterBarOrientation,
+  RootState,
+} from 'src/dashboard/types';
 import DropdownContainer, {
   Ref as DropdownContainerRef,
 } from 'src/components/DropdownContainer';
@@ -54,6 +59,8 @@ import Icons from 'src/components/Icons';
 import { FiltersOutOfScopeCollapsible } from '../FiltersOutOfScopeCollapsible';
 import { useFilterControlFactory } from '../useFilterControlFactory';
 import { FiltersDropdownContent } from '../FiltersDropdownContent';
+import crossFiltersSelector from '../CrossFilters/selectors';
+import CrossFilter from '../CrossFilters/CrossFilter';
 import { useFilterOutlined } from '../useFilterOutlined';
 
 type FilterControlsProps = {
@@ -77,6 +84,29 @@ const FilterControls: FC<FilterControlsProps> = ({
   const [overflowedIds, setOverflowedIds] = useState<string[]>([]);
   const popoverRef = useRef<DropdownContainerRef>(null);
 
+  const dataMask = useSelector<RootState, DataMaskStateWithId>(
+    state => state.dataMask,
+  );
+  const dashboardInfo = useSelector<RootState, DashboardInfo>(
+    state => state.dashboardInfo,
+  );
+  const dashboardLayout = useSelector<RootState, DashboardLayout>(
+    state => state.dashboardLayout.present,
+  );
+  const isCrossFiltersEnabled = isFeatureEnabled(
+    FeatureFlag.DASHBOARD_CROSS_FILTERS,
+  );
+  const selectedCrossFilters = useMemo(
+    () =>
+      isCrossFiltersEnabled
+        ? crossFiltersSelector({
+            dataMask,
+            dashboardInfo,
+            dashboardLayout,
+          })
+        : [],
+    [dashboardInfo, dashboardLayout, dataMask, isCrossFiltersEnabled],
+  );
   const { filterControlFactory, filtersWithValues } = useFilterControlFactory(
     dataMaskSelected,
     onFilterSelectionChange,
@@ -126,37 +156,68 @@ const FilterControls: FC<FilterControlsProps> = ({
     </>
   );
 
-  const items = useMemo(
-    () =>
-      filtersInScope.map((filter, index) => ({
-        id: filter.id,
-        element: (
-          <div
-            className="filter-item-wrapper"
-            css={css`
-              flex-shrink: 0;
-            `}
-          >
-            {renderer(filter, index)}
-          </div>
-        ),
-      })),
-    [filtersInScope, renderer],
-  );
-
   const overflowedFiltersInScope = useMemo(
     () => filtersInScope.filter(({ id }) => overflowedIds?.includes(id)),
     [filtersInScope, overflowedIds],
   );
 
-  const activeOverflowedFiltersInScope = useMemo(
+  const overflowedCrossFilters = useMemo(
     () =>
-      overflowedFiltersInScope.filter(filter =>
-        isNativeFilterWithDataMask(filter),
+      selectedCrossFilters.filter(({ emitterId, name }) =>
+        overflowedIds?.includes(`${name}${emitterId}`),
       ),
-    [overflowedFiltersInScope],
+    [overflowedIds, selectedCrossFilters],
+  );
+
+  const activeOverflowedFiltersInScope = useMemo(() => {
+    const activeOverflowedFilters = overflowedFiltersInScope.filter(filter =>
+      isNativeFilterWithDataMask(filter),
+    );
+    return [...activeOverflowedFilters, ...overflowedCrossFilters];
+  }, [overflowedCrossFilters, overflowedFiltersInScope]);
+
+  const rendererCrossFilter = useCallback(
+    (crossFilter, orientation, last) => (
+      <CrossFilter
+        filter={crossFilter}
+        orientation={orientation}
+        last={
+          filtersInScope.length > 0 &&
+          `${last.name}${last.emitterId}` ===
+            `${crossFilter.name}${crossFilter.emitterId}`
+        }
+      />
+    ),
+    [filtersInScope.length],
   );
 
+  const items = useMemo(() => {
+    const crossFilters = selectedCrossFilters.map(c => ({
+      // a combination of filter name and chart id to account
+      // for multiple cross filters from the same chart in the future
+      id: `${c.name}${c.emitterId}`,
+      element: rendererCrossFilter(
+        c,
+        FilterBarOrientation.HORIZONTAL,
+        selectedCrossFilters.at(-1),
+      ),
+    }));
+    const nativeFiltersInScope = filtersInScope.map((filter, index) => ({
+      id: filter.id,
+      element: (
+        <div
+          className="filter-item-wrapper"
+          css={css`
+            flex-shrink: 0;
+          `}
+        >
+          {renderer(filter, index)}
+        </div>
+      ),
+    }));
+    return [...crossFilters, ...nativeFiltersInScope];
+  }, [filtersInScope, renderer, rendererCrossFilter, selectedCrossFilters]);
+
   const renderHorizontalContent = () => (
     <div
       css={(theme: SupersetTheme) =>
@@ -193,12 +254,15 @@ const FilterControls: FC<FilterControlsProps> = ({
         }
         dropdownContent={
           overflowedFiltersInScope.length ||
+          overflowedCrossFilters.length ||
           (filtersOutOfScope.length && showCollapsePanel)
             ? () => (
                 <FiltersDropdownContent
+                  overflowedCrossFilters={overflowedCrossFilters}
                   filtersInScope={overflowedFiltersInScope}
                   filtersOutOfScope={filtersOutOfScope}
                   renderer={renderer}
+                  rendererCrossFilter={rendererCrossFilter}
                   showCollapsePanel={showCollapsePanel}
                 />
               )
diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FiltersDropdownContent/index.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FiltersDropdownContent/index.tsx
index 84710c94b5..c4c720710b 100644
--- a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FiltersDropdownContent/index.tsx
+++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FiltersDropdownContent/index.tsx
@@ -19,19 +19,29 @@
 
 import React, { ReactNode } from 'react';
 import { css, Divider, Filter, SupersetTheme } from '@superset-ui/core';
+import { FilterBarOrientation } from 'src/dashboard/types';
 import { FiltersOutOfScopeCollapsible } from '../FiltersOutOfScopeCollapsible';
+import { CrossFilterIndicator } from '../../selectors';
 
 export interface FiltersDropdownContentProps {
+  overflowedCrossFilters: CrossFilterIndicator[];
   filtersInScope: (Filter | Divider)[];
   filtersOutOfScope: (Filter | Divider)[];
   renderer: (filter: Filter | Divider, index: number) => ReactNode;
+  rendererCrossFilter: (
+    crossFilter: CrossFilterIndicator,
+    orientation: FilterBarOrientation.VERTICAL,
+    last: CrossFilterIndicator,
+  ) => ReactNode;
   showCollapsePanel?: boolean;
 }
 
 export const FiltersDropdownContent = ({
+  overflowedCrossFilters,
   filtersInScope,
   filtersOutOfScope,
   renderer,
+  rendererCrossFilter,
   showCollapsePanel,
 }: FiltersDropdownContentProps) => (
   <div
@@ -42,6 +52,13 @@ export const FiltersDropdownContent = ({
       `
     }
   >
+    {overflowedCrossFilters.map(crossFilter =>
+      rendererCrossFilter(
+        crossFilter,
+        FilterBarOrientation.VERTICAL,
+        overflowedCrossFilters.at(-1) as CrossFilterIndicator,
+      ),
+    )}
     {filtersInScope.map(renderer)}
     {showCollapsePanel && (
       <FiltersOutOfScopeCollapsible
diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FiltersOutOfScopeCollapsible/index.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FiltersOutOfScopeCollapsible/index.tsx
index a85f190732..898ca2768e 100644
--- a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FiltersOutOfScopeCollapsible/index.tsx
+++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FiltersOutOfScopeCollapsible/index.tsx
@@ -80,7 +80,15 @@ export const FiltersOutOfScopeCollapsible = ({
     }
   >
     <AntdCollapse.Panel
-      header={t('Filters out of scope (%d)', filtersOutOfScope.length)}
+      header={
+        <span
+          css={(theme: SupersetTheme) => css`
+            font-size: ${theme.typography.sizes.s}px;
+          `}
+        >
+          {t('Filters out of scope (%d)', filtersOutOfScope.length)}
+        </span>
+      }
       key="1"
     >
       {filtersOutOfScope.map(renderer)}
diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/Horizontal.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/Horizontal.tsx
index 9dc2d62d44..badff64295 100644
--- a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/Horizontal.tsx
+++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/Horizontal.tsx
@@ -18,14 +18,23 @@
  */
 
 import React from 'react';
-import { styled, t } from '@superset-ui/core';
+import {
+  DataMaskStateWithId,
+  FeatureFlag,
+  isFeatureEnabled,
+  styled,
+  t,
+} from '@superset-ui/core';
 import Icons from 'src/components/Icons';
 import Loading from 'src/components/Loading';
+import { DashboardInfo, DashboardLayout, RootState } from 'src/dashboard/types';
+import { useSelector } from 'react-redux';
 import FilterControls from './FilterControls/FilterControls';
 import { getFilterBarTestId } from './utils';
 import { HorizontalBarProps } from './types';
 import FilterBarSettings from './FilterBarSettings';
 import FilterConfigurationLink from './FilterConfigurationLink';
+import crossFiltersSelector from './CrossFilters/selectors';
 
 const HorizontalBar = styled.div`
   ${({ theme }) => `
@@ -95,7 +104,26 @@ const HorizontalFilterBar: React.FC<HorizontalBarProps> = ({
   isInitialized,
   onSelectionChange,
 }) => {
-  const hasFilters = filterValues.length > 0;
+  const dataMask = useSelector<RootState, DataMaskStateWithId>(
+    state => state.dataMask,
+  );
+  const dashboardInfo = useSelector<RootState, DashboardInfo>(
+    state => state.dashboardInfo,
+  );
+  const dashboardLayout = useSelector<RootState, DashboardLayout>(
+    state => state.dashboardLayout.present,
+  );
+  const isCrossFiltersEnabled = isFeatureEnabled(
+    FeatureFlag.DASHBOARD_CROSS_FILTERS,
+  );
+  const selectedCrossFilters = isCrossFiltersEnabled
+    ? crossFiltersSelector({
+        dataMask,
+        dashboardInfo,
+        dashboardLayout,
+      })
+    : [];
+  const hasFilters = filterValues.length > 0 || selectedCrossFilters.length > 0;
 
   return (
     <HorizontalBar {...getFilterBarTestId()}>
diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/Vertical.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/Vertical.tsx
index 3150ed5e90..265c5932ac 100644
--- a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/Vertical.tsx
+++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/Vertical.tsx
@@ -41,6 +41,7 @@ import { useFilterSets } from './state';
 import EditSection from './FilterSets/EditSection';
 import Header from './Header';
 import FilterControls from './FilterControls/FilterControls';
+import CrossFiltersVertical from './CrossFilters/Vertical';
 
 const BarWrapper = styled.div<{ width: number }>`
   width: ${({ theme }) => theme.gridUnit * 8}px;
@@ -190,6 +191,96 @@ const VerticalFilterBar: React.FC<VerticalBarProps> = ({
 
   const numberOfFilters = nativeFilterValues.length;
 
+  const filterControls = useMemo(
+    () =>
+      filterValues.length === 0 ? (
+        <FilterBarEmptyStateContainer>
+          <EmptyStateSmall
+            title={t('No global filters are currently added')}
+            image="filter.svg"
+            description={
+              canEdit &&
+              t(
+                'Click on "+Add/Edit Filters" button to create new dashboard filters',
+              )
+            }
+          />
+        </FilterBarEmptyStateContainer>
+      ) : (
+        <FilterControlsWrapper>
+          <FilterControls
+            dataMaskSelected={dataMaskSelected}
+            onFilterSelectionChange={onSelectionChange}
+          />
+        </FilterControlsWrapper>
+      ),
+    [canEdit, dataMaskSelected, filterValues.length, onSelectionChange],
+  );
+
+  const filterSetsTabs = useMemo(
+    () => (
+      <StyledTabs
+        centered
+        onChange={setTab as HandlerFunction}
+        defaultActiveKey={TabIds.AllFilters}
+        activeKey={editFilterSetId ? TabIds.AllFilters : undefined}
+      >
+        <AntdTabs.TabPane
+          tab={t('All filters (%(filterCount)d)', {
+            filterCount: numberOfFilters,
+          })}
+          key={TabIds.AllFilters}
+          css={tabPaneStyle}
+        >
+          {editFilterSetId && (
+            <EditSection
+              dataMaskSelected={dataMaskSelected}
+              disabled={!isDisabled}
+              onCancel={() => setEditFilterSetId(null)}
+              filterSetId={editFilterSetId}
+            />
+          )}
+          {filterControls}
+        </AntdTabs.TabPane>
+        <AntdTabs.TabPane
+          disabled={!!editFilterSetId}
+          tab={t('Filter sets (%(filterSetCount)d)', {
+            filterSetCount: filterSetFilterValues.length,
+          })}
+          key={TabIds.FilterSets}
+          css={tabPaneStyle}
+        >
+          <FilterSets
+            onEditFilterSet={setEditFilterSetId}
+            disabled={!isDisabled}
+            dataMaskSelected={dataMaskSelected}
+            tab={tab}
+            onFilterSelectionChange={onSelectionChange}
+          />
+        </AntdTabs.TabPane>
+      </StyledTabs>
+    ),
+    [
+      dataMaskSelected,
+      editFilterSetId,
+      filterControls,
+      filterSetFilterValues.length,
+      isDisabled,
+      numberOfFilters,
+      onSelectionChange,
+      tab,
+      tabPaneStyle,
+    ],
+  );
+
+  const crossFilters = useMemo(
+    () =>
+      isFeatureEnabled(FeatureFlag.DASHBOARD_CROSS_FILTERS) ? (
+        <CrossFiltersVertical />
+      ) : null,
+    [],
+  );
+
   return (
     <FilterBarScrollContext.Provider value={isScrolling}>
       <BarWrapper
@@ -219,89 +310,16 @@ const VerticalFilterBar: React.FC<VerticalBarProps> = ({
               <Loading />
             </div>
           ) : isFeatureEnabled(FeatureFlag.DASHBOARD_NATIVE_FILTERS_SET) ? (
-            <StyledTabs
-              centered
-              onChange={setTab as HandlerFunction}
-              defaultActiveKey={TabIds.AllFilters}
-              activeKey={editFilterSetId ? TabIds.AllFilters : undefined}
-            >
-              <AntdTabs.TabPane
-                tab={t('All filters (%(filterCount)d)', {
-                  filterCount: numberOfFilters,
-                })}
-                key={TabIds.AllFilters}
-                css={tabPaneStyle}
-              >
-                {editFilterSetId && (
-                  <EditSection
-                    dataMaskSelected={dataMaskSelected}
-                    disabled={!isDisabled}
-                    onCancel={() => setEditFilterSetId(null)}
-                    filterSetId={editFilterSetId}
-                  />
-                )}
-                {filterValues.length === 0 ? (
-                  <FilterBarEmptyStateContainer>
-                    <EmptyStateSmall
-                      title={t('No filters are currently added')}
-                      image="filter.svg"
-                      description={
-                        canEdit &&
-                        t(
-                          'Click the button above to add a filter to the dashboard',
-                        )
-                      }
-                    />
-                  </FilterBarEmptyStateContainer>
-                ) : (
-                  <FilterControlsWrapper>
-                    <FilterControls
-                      dataMaskSelected={dataMaskSelected}
-                      onFilterSelectionChange={onSelectionChange}
-                    />
-                  </FilterControlsWrapper>
-                )}
-              </AntdTabs.TabPane>
-              <AntdTabs.TabPane
-                disabled={!!editFilterSetId}
-                tab={t('Filter sets (%(filterSetCount)d)', {
-                  filterSetCount: filterSetFilterValues.length,
-                })}
-                key={TabIds.FilterSets}
-                css={tabPaneStyle}
-              >
-                <FilterSets
-                  onEditFilterSet={setEditFilterSetId}
-                  disabled={!isDisabled}
-                  dataMaskSelected={dataMaskSelected}
-                  tab={tab}
-                  onFilterSelectionChange={onSelectionChange}
-                />
-              </AntdTabs.TabPane>
-            </StyledTabs>
+            <>
+              {crossFilters}
+              {filterSetsTabs}
+            </>
           ) : (
             <div css={tabPaneStyle} onScroll={onScroll}>
-              {filterValues.length === 0 ? (
-                <FilterBarEmptyStateContainer>
-                  <EmptyStateSmall
-                    title={t('No filters are currently added')}
-                    image="filter.svg"
-                    description={
-                      canEdit &&
-                      t(
-                        'Click the button above to add a filter to the dashboard',
-                      )
-                    }
-                  />
-                </FilterBarEmptyStateContainer>
-              ) : (
-                <FilterControlsWrapper>
-                  <FilterControls
-                    dataMaskSelected={dataMaskSelected}
-                    onFilterSelectionChange={onSelectionChange}
-                  />
-                </FilterControlsWrapper>
-              )}
+              <>
+                {crossFilters}
+                {filterControls}
+              </>
             </div>
           )}
           {actions}
diff --git a/superset-frontend/src/dashboard/components/FiltersBadge/selectors.ts b/superset-frontend/src/dashboard/components/nativeFilters/selectors.ts
similarity index 80%
rename from superset-frontend/src/dashboard/components/FiltersBadge/selectors.ts
rename to superset-frontend/src/dashboard/components/nativeFilters/selectors.ts
index c0916b9960..871a8d402e 100644
--- a/superset-frontend/src/dashboard/components/FiltersBadge/selectors.ts
+++ b/superset-frontend/src/dashboard/components/nativeFilters/selectors.ts
@@ -60,7 +60,7 @@ type Filter = {
 };
 
 const extractLabel = (filter?: FilterState): string | null => {
-  if (filter?.label) {
+  if (filter?.label && !filter?.label?.includes(undefined)) {
     return filter.label;
   }
   if (filter?.value) {
@@ -158,6 +158,8 @@ export type Indicator = {
   path?: string[];
 };
 
+export type CrossFilterIndicator = Indicator & { emitterId: number };
+
 const cachedIndicatorsForChart = {};
 const cachedDashboardFilterDataForChart = {};
 // inspects redux state to find what the filter indicators should be shown for a given chart
@@ -214,9 +216,97 @@ export const selectIndicatorsForChart = (
   return indicators;
 };
 
+const getStatus = ({
+  label,
+  column,
+  type = DataMaskType.NativeFilters,
+  rejectedColumns,
+  appliedColumns,
+}: {
+  label: string | null;
+  column?: string;
+  type?: DataMaskType;
+  rejectedColumns?: Set<string>;
+  appliedColumns?: Set<string>;
+}): IndicatorStatus => {
+  // a filter is only considered unset if it's value is null
+  const hasValue = label !== null;
+  if (type === DataMaskType.CrossFilters && hasValue) {
+    return IndicatorStatus.CrossFilterApplied;
+  }
+  if (!column && hasValue) {
+    // Filter without datasource
+    return IndicatorStatus.Applied;
+  }
+  if (column && rejectedColumns?.has(column))
+    return IndicatorStatus.Incompatible;
+  if (column && appliedColumns?.has(column) && hasValue) {
+    return IndicatorStatus.Applied;
+  }
+  return IndicatorStatus.Unset;
+};
+
+const defaultChartConfig = {};
+export const selectChartCrossFilters = (
+  dataMask: DataMaskStateWithId,
+  chartId: number,
+  dashboardLayout: Layout,
+  chartConfiguration: ChartConfiguration = defaultChartConfig,
+  filterEmitter = false,
+): Indicator[] | CrossFilterIndicator[] => {
+  let crossFilterIndicators: any = [];
+  if (isFeatureEnabled(FeatureFlag.DASHBOARD_CROSS_FILTERS)) {
+    const dashboardLayoutValues = Object.values(dashboardLayout);
+    crossFilterIndicators = Object.values(chartConfiguration)
+      .filter(chartConfig => {
+        const inScope =
+          chartConfig.crossFilters?.chartsInScope?.includes(chartId);
+        if (!filterEmitter && inScope) {
+          return true;
+        }
+        if (filterEmitter && !inScope) {
+          return true;
+        }
+        return false;
+      })
+      .map(chartConfig => {
+        const filterState = dataMask[chartConfig.id]?.filterState;
+        const extraFormData = dataMask[chartConfig.id]?.extraFormData;
+        const label = extractLabel(filterState);
+        const filtersState = filterState?.filters;
+        const column =
+          extraFormData?.filters?.[0]?.col ||
+          (filtersState && Object.keys(filtersState)[0]);
+
+        const dashboardLayoutItem = dashboardLayoutValues.find(
+          layoutItem => layoutItem?.meta?.chartId === chartConfig.id,
+        );
+        const filterObject: Indicator = {
+          column,
+          name: dashboardLayoutItem?.meta?.sliceName as string,
+          path: [
+            ...(dashboardLayoutItem?.parents ?? []),
+            dashboardLayoutItem?.id || '',
+          ],
+          status: getStatus({
+            label,
+            type: DataMaskType.CrossFilters,
+          }),
+          value: label,
+        };
+        if (filterEmitter) {
+          (filterObject as CrossFilterIndicator).emitterId = chartId;
+        }
+        return filterObject;
+      })
+      .filter(filter => filter.status === IndicatorStatus.CrossFilterApplied);
+  }
+
+  return crossFilterIndicators;
+};
+
 const cachedNativeIndicatorsForChart = {};
 const cachedNativeFilterDataForChart: any = {};
-const defaultChartConfig = {};
 export const selectNativeIndicatorsForChart = (
   nativeFilters: Filters,
   dataMask: DataMaskStateWithId,
@@ -240,31 +330,6 @@ export const selectNativeIndicatorsForChart = (
   ) {
     return cachedNativeIndicatorsForChart[chartId];
   }
-  const getStatus = ({
-    label,
-    column,
-    type = DataMaskType.NativeFilters,
-  }: {
-    label: string | null;
-    column?: string;
-    type?: DataMaskType;
-  }): IndicatorStatus => {
-    // a filter is only considered unset if it's value is null
-    const hasValue = label !== null;
-    if (type === DataMaskType.CrossFilters && hasValue) {
-      return IndicatorStatus.CrossFilterApplied;
-    }
-    if (!column && hasValue) {
-      // Filter without datasource
-      return IndicatorStatus.Applied;
-    }
-    if (column && rejectedColumns.has(column))
-      return IndicatorStatus.Incompatible;
-    if (column && appliedColumns.has(column) && hasValue) {
-      return IndicatorStatus.Applied;
-    }
-    return IndicatorStatus.Unset;
-  };
 
   let nativeFilterIndicators: any = [];
   if (isFeatureEnabled(FeatureFlag.DASHBOARD_NATIVE_FILTERS)) {
@@ -284,7 +349,12 @@ export const selectNativeIndicatorsForChart = (
             column,
             name: nativeFilter.name,
             path: [nativeFilter.id],
-            status: getStatus({ label, column }),
+            status: getStatus({
+              label,
+              column,
+              rejectedColumns,
+              appliedColumns,
+            }),
             value: label,
           };
         });
@@ -292,35 +362,12 @@ export const selectNativeIndicatorsForChart = (
 
   let crossFilterIndicators: any = [];
   if (isFeatureEnabled(FeatureFlag.DASHBOARD_CROSS_FILTERS)) {
-    const dashboardLayoutValues = Object.values(dashboardLayout);
-    crossFilterIndicators = Object.values(chartConfiguration)
-      .filter(chartConfig =>
-        chartConfig.crossFilters?.chartsInScope?.includes(chartId),
-      )
-      .map(chartConfig => {
-        const filterState = dataMask[chartConfig.id]?.filterState;
-        const label = extractLabel(filterState);
-        const filtersState = filterState?.filters;
-        const column = filtersState && Object.keys(filtersState)[0];
-
-        const dashboardLayoutItem = dashboardLayoutValues.find(
-          layoutItem => layoutItem?.meta?.chartId === chartConfig.id,
-        );
-        return {
-          column,
-          name: dashboardLayoutItem?.meta?.sliceName as string,
-          path: [
-            ...(dashboardLayoutItem?.parents ?? []),
-            dashboardLayoutItem?.id,
-          ],
-          status: getStatus({
-            label,
-            type: DataMaskType.CrossFilters,
-          }),
-          value: label,
-        };
-      })
-      .filter(filter => filter.status === IndicatorStatus.CrossFilterApplied);
+    crossFilterIndicators = selectChartCrossFilters(
+      dataMask,
+      chartId,
+      dashboardLayout,
+      chartConfiguration,
+    );
   }
   const indicators = crossFilterIndicators.concat(nativeFilterIndicators);
   cachedNativeIndicatorsForChart[chartId] = indicators;