From 3713a162838fbae307d4b3a50e4c86cd5c550283 Mon Sep 17 00:00:00 2001
From: Alexander Polyankin <alexander.polyankin@metabase.com>
Date: Thu, 23 May 2024 17:57:41 -0400
Subject: [PATCH] Optimize initial dashboard load (#42901)

---
 .../dashboard-filters-nested.cy.spec.js       |   7 +-
 .../metrics/metrics-dashboard.cy.spec.js      |  10 +-
 .../v1/parameters/utils/targets.ts            |   9 +-
 .../actions/components/ActionViz/Action.tsx   |   6 +-
 .../components/ActionViz/Action.unit.spec.tsx |   4 +
 .../FieldValuesWidget/FieldValuesWidget.tsx   |   1 +
 .../dashboard/actions/data-fetching-typed.ts  |  14 +-
 .../actions/data-fetching.unit.spec.js        |   4 +-
 .../metabase/dashboard/actions/metadata.js    |  16 +--
 .../components/DashCard/DashCard.tsx          |  94 ++++++++-----
 .../use-duplicate-dashcard.ts                 |   4 +-
 .../DashCard/DashCardMenu/DashCardMenu.tsx    |  10 +-
 .../DashCard/DashCardVisualization.tsx        |  28 +---
 .../DashCard/Dashcard.unit.spec.tsx           |  13 +-
 .../components/Dashboard/Dashboard.tsx        | 130 +-----------------
 .../dashboard/components/DashboardGrid.tsx    |  51 ++-----
 .../DashboardHeader/DashboardHeader.tsx       |  10 +-
 .../DashboardParameterList.tsx                |  62 +++++++++
 .../DashboardParameterList/index.ts           |   1 +
 .../DashboardParameterPanel.tsx               |  92 +++++++++++++
 .../DashboardParameterPanel/index.ts          |   1 +
 .../components/DashboardSidebars.jsx          |  12 +-
 .../containers/DashboardApp/DashboardApp.tsx  |  18 +--
 .../metabase/dashboard/hoc/DashboardData.jsx  |   4 +-
 .../dashboard/hoc/WithVizSettingsData.js      |  52 -------
 .../dashboard/hoc/useClickBehaviorData.js     |  57 ++++++++
 frontend/src/metabase/dashboard/selectors.ts  |  55 +++++++-
 frontend/src/metabase/dashboard/utils.ts      |   6 +-
 .../src/metabase/dashboard/utils.unit.spec.ts |   5 +-
 .../PublicDashboard/PublicDashboard.tsx       |   5 +-
 frontend/src/metabase/questions/actions.ts    |  27 ++--
 .../visualizations/Heading/Heading.tsx        |   6 +-
 .../Heading/Heading.unit.spec.tsx             |  14 +-
 .../visualizations/Text/Text.jsx              |   4 +-
 .../visualizations/Text/Text.unit.spec.tsx    |  20 ++-
 35 files changed, 477 insertions(+), 375 deletions(-)
 create mode 100644 frontend/src/metabase/dashboard/components/DashboardParameterList/DashboardParameterList.tsx
 create mode 100644 frontend/src/metabase/dashboard/components/DashboardParameterList/index.ts
 create mode 100644 frontend/src/metabase/dashboard/components/DashboardParameterPanel/DashboardParameterPanel.tsx
 create mode 100644 frontend/src/metabase/dashboard/components/DashboardParameterPanel/index.ts
 delete mode 100644 frontend/src/metabase/dashboard/hoc/WithVizSettingsData.js
 create mode 100644 frontend/src/metabase/dashboard/hoc/useClickBehaviorData.js

diff --git a/e2e/test/scenarios/dashboard-filters/dashboard-filters-nested.cy.spec.js b/e2e/test/scenarios/dashboard-filters/dashboard-filters-nested.cy.spec.js
index e4a454b33aa..42a03ca6c3e 100644
--- a/e2e/test/scenarios/dashboard-filters/dashboard-filters-nested.cy.spec.js
+++ b/e2e/test/scenarios/dashboard-filters/dashboard-filters-nested.cy.spec.js
@@ -78,8 +78,11 @@ describe("scenarios > dashboard > filters > nested questions", () => {
     // Add multiple values (metabase#18113)
     filterWidget().click();
     popover().within(() => {
-      cy.findByText("Gizmo").click();
-      cy.findByText("Gadget").click();
+      cy.findByPlaceholderText("Enter some text")
+        .type("Gizmo")
+        .blur()
+        .type("Gadget")
+        .blur();
     });
 
     cy.button("Add filter").click();
diff --git a/e2e/test/scenarios/metrics/metrics-dashboard.cy.spec.js b/e2e/test/scenarios/metrics/metrics-dashboard.cy.spec.js
index b73d7e21df1..26bafecf78b 100644
--- a/e2e/test/scenarios/metrics/metrics-dashboard.cy.spec.js
+++ b/e2e/test/scenarios/metrics/metrics-dashboard.cy.spec.js
@@ -278,12 +278,8 @@ function combineAndVerifyMetrics(metric1, metric2) {
       visitDashboard(dashboard.id);
     },
   );
-  cy.findByTestId("dashboard-header").within(() => {
-    cy.findByLabelText("Edit dashboard").click();
-    cy.findByLabelText("Add questions").click();
-  });
-  cy.findByTestId("add-card-sidebar").findByText(metric1.name).click();
-  getDashboardCard(1).realHover().findByTestId("add-series-button").click();
+  editDashboard();
+  getDashboardCard().realHover().findByTestId("add-series-button").click();
   modal().within(() => {
     cy.findByText(metric2.name).click();
     cy.findByLabelText("Legend").within(() => {
@@ -293,7 +289,7 @@ function combineAndVerifyMetrics(metric1, metric2) {
     cy.button("Done").click();
   });
   saveDashboard();
-  getDashboardCard(1).within(() => {
+  getDashboardCard().within(() => {
     cy.findByLabelText("Legend").within(() => {
       cy.findByText(metric1.name).should("be.visible");
       cy.findByText(metric2.name).should("be.visible");
diff --git a/frontend/src/metabase-lib/v1/parameters/utils/targets.ts b/frontend/src/metabase-lib/v1/parameters/utils/targets.ts
index 4bed89d0f04..d893c42c8f9 100644
--- a/frontend/src/metabase-lib/v1/parameters/utils/targets.ts
+++ b/frontend/src/metabase-lib/v1/parameters/utils/targets.ts
@@ -45,7 +45,6 @@ export function getParameterTargetField(
   }
 
   const fieldRef = target[1];
-  const query = question.query();
   const metadata = question.metadata();
 
   // native queries
@@ -60,8 +59,12 @@ export function getParameterTargetField(
 
   if (isConcreteFieldReference(fieldRef)) {
     const fieldId = fieldRef[1];
-    const tableId = Lib.sourceTableOrCardId(query);
-    return metadata.field(fieldId, tableId) ?? metadata.field(fieldId);
+    const resultMetadata = question.getResultMetadata();
+    const fieldMetadata = resultMetadata.find(field => field.id === fieldId);
+    return (
+      metadata.field(fieldId, fieldMetadata?.table_id) ??
+      metadata.field(fieldId)
+    );
   }
 
   return null;
diff --git a/frontend/src/metabase/actions/components/ActionViz/Action.tsx b/frontend/src/metabase/actions/components/ActionViz/Action.tsx
index 8b055c5a9e3..d70da5ab30d 100644
--- a/frontend/src/metabase/actions/components/ActionViz/Action.tsx
+++ b/frontend/src/metabase/actions/components/ActionViz/Action.tsx
@@ -8,7 +8,10 @@ import {
   executeRowAction,
   reloadDashboardCards,
 } from "metabase/dashboard/actions";
-import { getEditingDashcardId } from "metabase/dashboard/selectors";
+import {
+  getEditingDashcardId,
+  getParameterValues,
+} from "metabase/dashboard/selectors";
 import { getActionIsEnabledInDatabase } from "metabase/dashboard/utils";
 import type { VisualizationProps } from "metabase/visualizations/types";
 import type {
@@ -139,6 +142,7 @@ const ConnectedActionComponent = connect()(ActionComponent);
 
 function mapStateToProps(state: State, props: ActionProps) {
   return {
+    parameterValues: getParameterValues(state),
     isEditingDashcard: getEditingDashcardId(state) === props.dashcard.id,
   };
 }
diff --git a/frontend/src/metabase/actions/components/ActionViz/Action.unit.spec.tsx b/frontend/src/metabase/actions/components/ActionViz/Action.unit.spec.tsx
index 0f5938860c5..6e7469dd182 100644
--- a/frontend/src/metabase/actions/components/ActionViz/Action.unit.spec.tsx
+++ b/frontend/src/metabase/actions/components/ActionViz/Action.unit.spec.tsx
@@ -33,6 +33,7 @@ import {
   createMockStructuredDatasetQuery,
   createMockDatabase,
 } from "metabase-types/api/mocks";
+import { createMockDashboardState } from "metabase-types/store/mocks";
 
 import type { ActionProps } from "./Action";
 import Action from "./Action";
@@ -156,6 +157,9 @@ async function setup({
         entities: createMockEntitiesState({
           databases: [database],
         }),
+        dashboard: createMockDashboardState({
+          parameterValues,
+        }),
       },
     },
   );
diff --git a/frontend/src/metabase/components/FieldValuesWidget/FieldValuesWidget.tsx b/frontend/src/metabase/components/FieldValuesWidget/FieldValuesWidget.tsx
index 38703bbaeb5..13fc133b5fd 100644
--- a/frontend/src/metabase/components/FieldValuesWidget/FieldValuesWidget.tsx
+++ b/frontend/src/metabase/components/FieldValuesWidget/FieldValuesWidget.tsx
@@ -498,6 +498,7 @@ export function FieldValuesWidgetInner({
             }}
             onInputChange={onInputChange}
             parseFreeformValue={parseFreeformValue}
+            updateOnInputBlur
           />
         )}
       </div>
diff --git a/frontend/src/metabase/dashboard/actions/data-fetching-typed.ts b/frontend/src/metabase/dashboard/actions/data-fetching-typed.ts
index f052e723d1c..c971dca06a9 100644
--- a/frontend/src/metabase/dashboard/actions/data-fetching-typed.ts
+++ b/frontend/src/metabase/dashboard/actions/data-fetching-typed.ts
@@ -4,10 +4,8 @@ import { loadMetadataForDashboard } from "metabase/dashboard/actions/metadata";
 import {
   getDashboardById,
   getDashCardById,
-  getInitialSelectedTabId,
   getParameterValues,
   getQuestions,
-  getSelectedTabId,
 } from "metabase/dashboard/selectors";
 import {
   expandInlineDashboard,
@@ -127,17 +125,7 @@ export const fetchDashboard = createAsyncThunk(
       fetchDashboardCancellation = null;
 
       if (dashboardType === "normal" || dashboardType === "transient") {
-        const selectedTabId =
-          getSelectedTabId(getState()) ?? getInitialSelectedTabId(result);
-
-        const cards =
-          selectedTabId == null
-            ? result.dashcards
-            : result.dashcards.filter(
-                (c: DashboardCard) => c.dashboard_tab_id === selectedTabId,
-              );
-
-        await dispatch(loadMetadataForDashboard(cards));
+        await dispatch(loadMetadataForDashboard(result.dashcards));
       }
 
       const isUsingCachedResults = entities != null;
diff --git a/frontend/src/metabase/dashboard/actions/data-fetching.unit.spec.js b/frontend/src/metabase/dashboard/actions/data-fetching.unit.spec.js
index 1a3e6956c81..0922ed5ee3d 100644
--- a/frontend/src/metabase/dashboard/actions/data-fetching.unit.spec.js
+++ b/frontend/src/metabase/dashboard/actions/data-fetching.unit.spec.js
@@ -97,7 +97,7 @@ describe("fetchDashboard", () => {
     ).toHaveLength(1);
   });
 
-  it("should fetch metadata for cards on the first tab when there are tabs", async () => {
+  it("should fetch metadata for all cards when there are tabs", async () => {
     const dashboard = createMockDashboard({
       dashcards: [
         createMockDashboardCard({
@@ -147,7 +147,7 @@ describe("fetchDashboard", () => {
     ).toHaveLength(1);
     expect(
       fetchMock.calls(`path:/api/table/${ORDERS_ID}/query_metadata`),
-    ).toHaveLength(0);
+    ).toHaveLength(1);
   });
 
   it("should cancel previous dashboard fetch when a new one is initiated (metabase#35959)", async () => {
diff --git a/frontend/src/metabase/dashboard/actions/metadata.js b/frontend/src/metabase/dashboard/actions/metadata.js
index f4d7b6c6261..a4b40d037e0 100644
--- a/frontend/src/metabase/dashboard/actions/metadata.js
+++ b/frontend/src/metabase/dashboard/actions/metadata.js
@@ -1,6 +1,6 @@
 import Questions from "metabase/entities/questions";
 import { getLinkTargets } from "metabase/lib/click-behavior";
-import { loadMetadataForCard } from "metabase/questions/actions";
+import { loadMetadataForCards } from "metabase/questions/actions";
 
 import { isVirtualDashCard } from "../utils";
 
@@ -10,17 +10,15 @@ export const loadMetadataForDashboard = dashCards => async dispatch => {
     .flatMap(dc => [dc.card].concat(dc.series || []));
 
   await Promise.all([
-    dispatch(loadMetadataForCards(cards)),
+    dispatch(loadMetadataForAvailableCards(cards)),
     dispatch(loadMetadataForLinkedTargets(dashCards)),
   ]);
 };
 
-const loadMetadataForCards = cards => (dispatch, getState) => {
-  return Promise.all(
-    cards
-      .filter(card => card.dataset_query) // exclude queries without perms
-      .map(card => dispatch(loadMetadataForCard(card))),
-  );
+const loadMetadataForAvailableCards = cards => dispatch => {
+  // exclude queries without perms
+  const availableCards = cards.filter(card => card.dataset_query);
+  return dispatch(loadMetadataForCards(availableCards));
 };
 
 const loadMetadataForLinkedTargets =
@@ -43,5 +41,5 @@ const loadMetadataForLinkedTargets =
       )
       .filter(card => card != null);
 
-    await dispatch(loadMetadataForCards(cards));
+    await dispatch(loadMetadataForAvailableCards(cards));
   };
diff --git a/frontend/src/metabase/dashboard/components/DashCard/DashCard.tsx b/frontend/src/metabase/dashboard/components/DashCard/DashCard.tsx
index d67fda8b89f..7968728b7e4 100644
--- a/frontend/src/metabase/dashboard/components/DashCard/DashCard.tsx
+++ b/frontend/src/metabase/dashboard/components/DashCard/DashCard.tsx
@@ -1,7 +1,7 @@
 import cx from "classnames";
 import type { LocationDescriptor } from "history";
 import { getIn } from "icepick";
-import { useCallback, useMemo, useRef, useState } from "react";
+import { memo, useCallback, useMemo, useRef, useState } from "react";
 import { useMount } from "react-use";
 
 import ErrorBoundary from "metabase/ErrorBoundary";
@@ -9,28 +9,27 @@ import { isActionCard } from "metabase/actions/utils";
 import CS from "metabase/css/core/index.css";
 import DashboardS from "metabase/css/dashboard.module.css";
 import { DASHBOARD_SLOW_TIMEOUT } from "metabase/dashboard/constants";
+import { getDashcardData } from "metabase/dashboard/selectors";
 import {
   getDashcardResultsError,
   isDashcardLoading,
   isQuestionDashCard,
 } from "metabase/dashboard/utils";
+import { color } from "metabase/lib/colors";
+import { useSelector } from "metabase/lib/redux";
 import { isJWT } from "metabase/lib/utils";
+import { PLUGIN_COLLECTIONS } from "metabase/plugins";
 import EmbedFrameS from "metabase/public/components/EmbedFrame/EmbedFrame.module.css";
-import type { IconProps } from "metabase/ui";
 import type { Mode } from "metabase/visualizations/click-actions/Mode";
 import { mergeSettings } from "metabase/visualizations/lib/settings";
 import type Metadata from "metabase-lib/v1/metadata/Metadata";
-import { getParameterValuesBySlug } from "metabase-lib/v1/parameters/utils/parameter-values";
 import type {
   Card,
   CardId,
   Dashboard,
   DashboardCard,
   DashCardId,
-  ParameterId,
-  ParameterValueOrArray,
   VisualizationSettings,
-  DashCardDataMap,
   VirtualCard,
 } from "metabase-types/api";
 import type { StoreDashcard } from "metabase-types/store";
@@ -53,9 +52,7 @@ export interface DashCardProps {
   dashcard: StoreDashcard;
   gridItemWidth: number;
   totalNumGridCols: number;
-  dashcardData: DashCardDataMap;
   slowCards: Record<CardId, boolean>;
-  parameterValues: Record<ParameterId, ParameterValueOrArray>;
   metadata: Metadata;
   mode?: Mode;
 
@@ -69,28 +66,30 @@ export interface DashCardProps {
   isPublic?: boolean;
   isXray?: boolean;
 
-  headerIcon?: IconProps;
-
-  onAddSeries: () => void;
-  onReplaceCard: () => void;
-  onRemove: () => void;
+  onAddSeries: (dashcard: StoreDashcard) => void;
+  onReplaceCard: (dashcard: StoreDashcard) => void;
+  onRemove: (dashcard: StoreDashcard) => void;
   markNewCardSeen: (dashcardId: DashCardId) => void;
   navigateToNewCardFromDashboard?: (
     opts: NavigateToNewCardFromDashboardOpts,
   ) => void;
-  onReplaceAllVisualizationSettings: (settings: VisualizationSettings) => void;
-  onUpdateVisualizationSettings: (settings: VisualizationSettings) => void;
-  showClickBehaviorSidebar: (dashCardId: DashCardId | null) => void;
+  onReplaceAllVisualizationSettings: (
+    dashcardId: DashCardId,
+    settings: VisualizationSettings,
+  ) => void;
+  onUpdateVisualizationSettings: (
+    dashcardId: DashCardId,
+    settings: VisualizationSettings,
+  ) => void;
+  showClickBehaviorSidebar: (dashcardId: DashCardId | null) => void;
   onChangeLocation: (location: LocationDescriptor) => void;
 }
 
 function DashCardInner({
   dashcard,
-  dashcardData,
   dashboard,
   slowCards,
   metadata,
-  parameterValues,
   gridItemWidth,
   totalNumGridCols,
   mode,
@@ -102,7 +101,6 @@ function DashCardInner({
   isXray = false,
   isEditingParameter,
   clickBehaviorSidebarDashcard,
-  headerIcon,
   onAddSeries,
   onReplaceCard,
   onRemove,
@@ -113,6 +111,9 @@ function DashCardInner({
   onUpdateVisualizationSettings,
   onReplaceAllVisualizationSettings,
 }: DashCardProps) {
+  const dashcardData = useSelector(state =>
+    getDashcardData(state, dashcard.id),
+  );
   const [isPreviewingCard, setIsPreviewingCard] = useState(false);
   const cardRootRef = useRef<HTMLDivElement>(null);
 
@@ -159,13 +160,13 @@ function DashCardInner({
       }
 
       return {
-        ...getIn(dashcardData, [dashcard.id, card.id]),
+        ...getIn(dashcardData, [card.id]),
         card,
         isSlow,
         isUsuallyFast,
       };
     });
-  }, [cards, dashcard.id, dashcardData, slowCards]);
+  }, [cards, dashcardData, slowCards]);
 
   const isLoading = useMemo(
     () => isDashcardLoading(dashcard, dashcardData),
@@ -190,11 +191,6 @@ function DashCardInner({
   const error = useMemo(() => getDashcardResultsError(series), [series]);
   const hasError = !!error;
 
-  const parameterValuesBySlug = useMemo(
-    () => getParameterValuesBySlug(dashboard.parameters, parameterValues),
-    [dashboard.parameters, parameterValues],
-  );
-
   const gridSize = useMemo(
     () => ({ width: dashcard.size_x, height: dashcard.size_y }),
     [dashcard],
@@ -225,6 +221,30 @@ function DashCardInner({
     );
   }, [isEditing, isAction, mainCard]);
 
+  const headerIcon = useMemo(() => {
+    const { isRegularCollection } = PLUGIN_COLLECTIONS;
+    const isRegularQuestion = isRegularCollection({
+      authority_level: dashcard.collection_authority_level,
+    });
+    const isRegularDashboard = isRegularCollection({
+      authority_level: dashboard.collection_authority_level,
+    });
+    const authorityLevel = dashcard.collection_authority_level;
+    if (isRegularDashboard && !isRegularQuestion && authorityLevel) {
+      const opts = PLUGIN_COLLECTIONS.AUTHORITY_LEVEL[authorityLevel];
+      const iconSize = 14;
+      return {
+        name: opts.icon,
+        color: opts.color ? color(opts.color) : undefined,
+        tooltip: opts.tooltips?.belonging,
+        size: iconSize,
+
+        // Workaround: headerIcon on cards in a first column have incorrect offset out of the box
+        targetOffsetX: dashcard.col === 0 ? iconSize : 0,
+      };
+    }
+  }, [dashcard, dashboard.collection_authority_level]);
+
   const isEditingDashboardLayout =
     isEditing && !clickBehaviorSidebarDashcard && !isEditingParameter;
 
@@ -287,12 +307,14 @@ function DashCardInner({
             isLoading={isLoading}
             isPreviewing={isPreviewingCard}
             hasError={hasError}
-            onAddSeries={onAddSeries}
-            onRemove={onRemove}
-            onReplaceCard={onReplaceCard}
-            onUpdateVisualizationSettings={onUpdateVisualizationSettings}
-            onReplaceAllVisualizationSettings={
-              onReplaceAllVisualizationSettings
+            onAddSeries={() => onAddSeries(dashcard)}
+            onRemove={() => onRemove(dashcard)}
+            onReplaceCard={() => onReplaceCard(dashcard)}
+            onUpdateVisualizationSettings={settings =>
+              onUpdateVisualizationSettings(dashcard.id, settings)
+            }
+            onReplaceAllVisualizationSettings={settings =>
+              onReplaceAllVisualizationSettings(dashcard.id, settings)
             }
             showClickBehaviorSidebar={handleShowClickBehaviorSidebar}
             onPreviewToggle={handlePreviewToggle}
@@ -302,8 +324,6 @@ function DashCardInner({
           dashboard={dashboard}
           dashcard={dashcard}
           series={series}
-          parameterValues={parameterValues}
-          parameterValuesBySlug={parameterValuesBySlug}
           metadata={metadata}
           mode={mode}
           gridSize={gridSize}
@@ -327,7 +347,9 @@ function DashCardInner({
           isMobile={isMobile}
           isPublic={isPublic}
           showClickBehaviorSidebar={showClickBehaviorSidebar}
-          onUpdateVisualizationSettings={onUpdateVisualizationSettings}
+          onUpdateVisualizationSettings={settings =>
+            onUpdateVisualizationSettings(dashcard.id, settings)
+          }
           onChangeCardAndRun={changeCardAndRunHandler}
           onChangeLocation={onChangeLocation}
         />
@@ -336,6 +358,6 @@ function DashCardInner({
   );
 }
 
-export const DashCard = Object.assign(DashCardInner, {
+export const DashCard = Object.assign(memo(DashCardInner), {
   root: DashCardRoot,
 });
diff --git a/frontend/src/metabase/dashboard/components/DashCard/DashCardActionsPanel/use-duplicate-dashcard.ts b/frontend/src/metabase/dashboard/components/DashCard/DashCardActionsPanel/use-duplicate-dashcard.ts
index 25ede5a3fe8..4e8c5de19a7 100644
--- a/frontend/src/metabase/dashboard/components/DashCard/DashCardActionsPanel/use-duplicate-dashcard.ts
+++ b/frontend/src/metabase/dashboard/components/DashCard/DashCardActionsPanel/use-duplicate-dashcard.ts
@@ -7,7 +7,7 @@ import {
 import { getExistingDashCards } from "metabase/dashboard/actions/utils";
 import { trackDashcardDuplicated } from "metabase/dashboard/analytics";
 import {
-  getCardData,
+  getDashcardDataMap,
   getDashboards,
   getDashcards,
   getSelectedTabId,
@@ -31,7 +31,7 @@ export function useDuplicateDashCard({
   const dashboards = useSelector(getDashboards);
   const dashcards = useSelector(getDashcards);
   const selectedTabId = useSelector(getSelectedTabId);
-  const dashcardDataMap = useSelector(getCardData);
+  const dashcardDataMap = useSelector(getDashcardDataMap);
 
   return useCallback(() => {
     if (!dashcard) {
diff --git a/frontend/src/metabase/dashboard/components/DashCard/DashCardMenu/DashCardMenu.tsx b/frontend/src/metabase/dashboard/components/DashCard/DashCardMenu/DashCardMenu.tsx
index 1553fe05712..14fd3fdc7d7 100644
--- a/frontend/src/metabase/dashboard/components/DashCard/DashCardMenu/DashCardMenu.tsx
+++ b/frontend/src/metabase/dashboard/components/DashCard/DashCardMenu/DashCardMenu.tsx
@@ -6,6 +6,8 @@ import { t } from "ttag";
 
 import CS from "metabase/css/core/index.css";
 import { editQuestion } from "metabase/dashboard/actions";
+import { getParameterValuesBySlugMap } from "metabase/dashboard/selectors";
+import { useStore } from "metabase/lib/redux";
 import { PLUGIN_FEATURE_LEVEL_PERMISSIONS } from "metabase/plugins";
 import type { DownloadQueryResultsOpts } from "metabase/query_builder/actions";
 import { downloadQueryResults } from "metabase/query_builder/actions";
@@ -31,7 +33,6 @@ interface OwnProps {
   dashcardId?: DashCardId;
   uuid?: string;
   token?: string;
-  params?: Record<string, unknown>;
   visualizationSettings?: VisualizationSettings;
 }
 
@@ -59,12 +60,15 @@ const DashCardMenu = ({
   dashcardId,
   uuid,
   token,
-  params,
   onEditQuestion,
   onDownloadResults,
 }: DashCardMenuProps) => {
+  const store = useStore();
+
   const [{ loading }, handleDownload] = useAsyncFn(
     async (opts: { type: string; enableFormatting: boolean }) => {
+      const params = getParameterValuesBySlugMap(store.getState());
+
       await onDownloadResults({
         ...opts,
         question,
@@ -76,7 +80,7 @@ const DashCardMenu = ({
         params,
       });
     },
-    [question, result, dashboardId, dashcardId, uuid, token, params],
+    [store, question, result, dashboardId, dashcardId, uuid, token],
   );
 
   const handleMenuContent = useCallback(
diff --git a/frontend/src/metabase/dashboard/components/DashCard/DashCardVisualization.tsx b/frontend/src/metabase/dashboard/components/DashCard/DashCardVisualization.tsx
index a92b3d4dd14..12e785a733e 100644
--- a/frontend/src/metabase/dashboard/components/DashCard/DashCardVisualization.tsx
+++ b/frontend/src/metabase/dashboard/components/DashCard/DashCardVisualization.tsx
@@ -1,11 +1,10 @@
 import cx from "classnames";
 import type { LocationDescriptor } from "history";
 import { useCallback, useMemo } from "react";
-import { connect } from "react-redux";
 import { t } from "ttag";
 
 import CS from "metabase/css/core/index.css";
-import { WithVizSettingsData } from "metabase/dashboard/hoc/WithVizSettingsData";
+import { useClickBehaviorData } from "metabase/dashboard/hoc/useClickBehaviorData";
 import {
   getVirtualCardType,
   isQuestionCard,
@@ -22,13 +21,10 @@ import type {
   DashCardId,
   Dataset,
   Series,
-  ParameterId,
-  ParameterValueOrArray,
   VirtualCardDisplay,
   VisualizationSettings,
   DashboardCard,
 } from "metabase-types/api";
-import type { Dispatch } from "metabase-types/store";
 
 import { ClickBehaviorSidebarOverlay } from "./ClickBehaviorSidebarOverlay/ClickBehaviorSidebarOverlay";
 import {
@@ -47,8 +43,6 @@ interface DashCardVisualizationProps {
   dashboard: Dashboard;
   dashcard: DashboardCard;
   series: Series;
-  parameterValues: Record<ParameterId, ParameterValueOrArray>;
-  parameterValuesBySlug: Record<string, ParameterValueOrArray>;
   metadata: Metadata;
   mode?: Mode;
 
@@ -85,22 +79,13 @@ interface DashCardVisualizationProps {
   onChangeLocation: (location: LocationDescriptor) => void;
 }
 
-function mapDispatchToProps(dispatch: Dispatch) {
-  return { dispatch };
-}
-
 // This is done to add the `getExtraDataForClick` prop.
 // We need that to pass relevant data along with the clicked object.
-const WrappedVisualization = WithVizSettingsData(
-  connect(null, mapDispatchToProps)(Visualization),
-);
 
 export function DashCardVisualization({
   dashcard,
   dashboard,
   series,
-  parameterValues,
-  parameterValuesBySlug,
   mode,
   metadata,
   gridSize,
@@ -214,7 +199,6 @@ export function DashCardVisualization({
         dashcardId={dashcard.id}
         dashboardId={dashboard.id}
         token={isEmbed ? String(dashcard.dashboard_id) : undefined}
-        params={parameterValuesBySlug}
       />
     );
   }, [
@@ -227,11 +211,14 @@ export function DashCardVisualization({
     isEditing,
     isXray,
     dashboard.id,
-    parameterValuesBySlug,
   ]);
 
+  const { getExtraDataForClick } = useClickBehaviorData({
+    dashcardId: dashcard.id,
+  });
+
   return (
-    <WrappedVisualization
+    <Visualization
       className={cx(CS.flexFull, CS.overflowHidden, {
         [CS.pointerEventsNone]: isEditingDashboardLayout,
       })}
@@ -241,8 +228,6 @@ export function DashCardVisualization({
       dashboard={dashboard}
       dashcard={dashcard}
       rawSeries={series}
-      parameterValues={parameterValues}
-      parameterValuesBySlug={parameterValuesBySlug}
       metadata={metadata}
       mode={mode}
       gridSize={gridSize}
@@ -263,6 +248,7 @@ export function DashCardVisualization({
       isMobile={isMobile}
       actionButtons={renderActionButtons()}
       replacementContent={renderVisualizationOverlay()}
+      getExtraDataForClick={getExtraDataForClick}
       onUpdateVisualizationSettings={onUpdateVisualizationSettings}
       onChangeCardAndRun={onChangeCardAndRun}
       onChangeLocation={onChangeLocation}
diff --git a/frontend/src/metabase/dashboard/components/DashCard/Dashcard.unit.spec.tsx b/frontend/src/metabase/dashboard/components/DashCard/Dashcard.unit.spec.tsx
index f36bde700d4..560454135e2 100644
--- a/frontend/src/metabase/dashboard/components/DashCard/Dashcard.unit.spec.tsx
+++ b/frontend/src/metabase/dashboard/components/DashCard/Dashcard.unit.spec.tsx
@@ -1,6 +1,7 @@
 import { createMockMetadata } from "__support__/metadata";
 import { queryIcon, renderWithProviders, screen } from "__support__/ui";
 import registerVisualizations from "metabase/visualizations/register";
+import type { DashCardDataMap } from "metabase-types/api";
 import {
   createMockCard,
   createMockDashboard,
@@ -11,6 +12,7 @@ import {
   createMockLinkDashboardCard,
   createMockDataset,
 } from "metabase-types/api/mocks";
+import { createMockDashboardState } from "metabase-types/store/mocks";
 
 import type { DashCardProps } from "./DashCard";
 import { DashCard } from "./DashCard";
@@ -67,7 +69,7 @@ function setup({
   dashcard = tableDashcard,
   dashcardData = tableDashcardData,
   ...props
-}: Partial<DashCardProps> = {}) {
+}: Partial<DashCardProps> & { dashcardData?: DashCardDataMap } = {}) {
   const onReplaceCard = jest.fn();
 
   renderWithProviders(
@@ -76,9 +78,7 @@ function setup({
       dashcard={dashcard}
       gridItemWidth={4}
       totalNumGridCols={24}
-      dashcardData={dashcardData}
       slowCards={{}}
-      parameterValues={{}}
       metadata={metadata}
       isEditing={false}
       isEditingParameter={false}
@@ -93,6 +93,13 @@ function setup({
       showClickBehaviorSidebar={jest.fn()}
       onChangeLocation={jest.fn()}
     />,
+    {
+      storeInitialState: {
+        dashboard: createMockDashboardState({
+          dashcardData,
+        }),
+      },
+    },
   );
 
   return { onReplaceCard };
diff --git a/frontend/src/metabase/dashboard/components/Dashboard/Dashboard.tsx b/frontend/src/metabase/dashboard/components/Dashboard/Dashboard.tsx
index 82342c042c0..f0a3f22ce3f 100644
--- a/frontend/src/metabase/dashboard/components/Dashboard/Dashboard.tsx
+++ b/frontend/src/metabase/dashboard/components/Dashboard/Dashboard.tsx
@@ -22,15 +22,10 @@ import type {
   SuccessfulFetchDashboardResult,
 } from "metabase/dashboard/types";
 import Dashboards from "metabase/entities/dashboards";
-import { isSmallScreen, getMainElement } from "metabase/lib/dom";
+import { getMainElement } from "metabase/lib/dom";
 import { useDispatch } from "metabase/lib/redux";
-import { FilterApplyButton } from "metabase/parameters/components/FilterApplyButton";
-import SyncedParametersList from "metabase/parameters/components/SyncedParametersList/SyncedParametersList";
-import { getVisibleParameters } from "metabase/parameters/utils/ui";
 import type { EmbeddingParameterVisibility } from "metabase/public/lib/types";
 import type Metadata from "metabase-lib/v1/metadata/Metadata";
-import type { UiParameter } from "metabase-lib/v1/parameters/types";
-import { getValuePopulatedParameters } from "metabase-lib/v1/parameters/utils/parameter-values";
 import type {
   Dashboard as IDashboard,
   DashboardId,
@@ -38,7 +33,6 @@ import type {
   DashCardId,
   Database,
   DatabaseId,
-  Parameter,
   ParameterId,
   ParameterValueOrArray,
   CardId,
@@ -60,6 +54,7 @@ import type {
 
 import { DASHBOARD_PDF_EXPORT_ROOT_ID, SIDEBAR_NAME } from "../../constants";
 import { DashboardGridConnected } from "../DashboardGrid";
+import { DashboardParameterPanel } from "../DashboardParameterPanel";
 import { DashboardSidebars } from "../DashboardSidebars";
 
 import {
@@ -69,9 +64,6 @@ import {
   DashboardBody,
   DashboardHeaderContainer,
   ParametersAndCardsContainer,
-  ParametersWidgetContainer,
-  FixedWidthContainer,
-  ParametersFixedWidthContainer,
 } from "./Dashboard.styled";
 import {
   DashboardEmptyState,
@@ -94,8 +86,6 @@ type DashboardProps = {
   dashcardData: DashCardDataMap;
   slowCards: Record<DashCardId, boolean>;
   databases: Record<DatabaseId, Database>;
-  editingParameter?: Parameter | null;
-  parameters: UiParameter[];
   parameterValues: Record<ParameterId, ParameterValueOrArray>;
   draftParameterValues: Record<ParameterId, ParameterValueOrArray | null>;
   metadata: Metadata;
@@ -141,12 +131,8 @@ type DashboardProps = {
   setParameterName: (id: ParameterId, name: string) => void;
   setParameterType: (id: ParameterId, type: string) => void;
   navigateToNewCardFromDashboard: typeof navigateToNewCardFromDashboard;
-  setParameterIndex: (id: ParameterId, index: number) => void;
-  setParameterValue: (id: ParameterId, value: RowValue) => void;
   setParameterDefaultValue: (id: ParameterId, value: RowValue) => void;
-  setParameterValueToDefault: (id: ParameterId) => void;
   setParameterRequired: (id: ParameterId, value: boolean) => void;
-  setEditingParameter: (id: ParameterId) => void;
   setParameterIsMultiSelect: (id: ParameterId, isMultiSelect: boolean) => void;
   setParameterQueryType: (id: ParameterId, queryType: ValuesQueryType) => void;
   setParameterSourceType: (
@@ -192,14 +178,11 @@ function DashboardInner(props: DashboardProps) {
     closeNavbar,
     dashboard,
     dashboardId,
-    draftParameterValues,
     editingOnLoad,
-    editingParameter,
     fetchDashboard,
     fetchDashboardCardData,
     fetchDashboardCardMetadata,
     initialize,
-    isAutoApplyFilters,
     isEditing,
     isFullscreen,
     isNavigatingBackToDashboard,
@@ -209,15 +192,10 @@ function DashboardInner(props: DashboardProps) {
     location,
     onRefreshPeriodChange,
     parameterValues,
-    parameters,
     selectedTabId,
     setDashboardAttributes,
     setEditingDashboard,
-    setEditingParameter,
     setErrorPage,
-    setParameterIndex,
-    setParameterValue,
-    setParameterValueToDefault,
     setSharing,
     toggleSidebar,
   } = props;
@@ -245,33 +223,10 @@ function DashboardInner(props: DashboardProps) {
     );
   }, [dashboard, selectedTabId]);
 
-  const hiddenParameterSlugs = useMemo(() => {
-    if (isEditing) {
-      // All filters should be visible in edit mode
-      return undefined;
-    }
-
-    const currentTabParameterIds = currentTabDashcards.flatMap(
-      (dc: DashboardCard) =>
-        dc.parameter_mappings?.map(pm => pm.parameter_id) ?? [],
-    );
-    const hiddenParameters = parameters.filter(
-      parameter => !currentTabParameterIds.includes(parameter.id),
-    );
-
-    return hiddenParameters.map(p => p.slug).join(",");
-  }, [parameters, currentTabDashcards, isEditing]);
-
-  const visibleParameters = useMemo(
-    () => getVisibleParameters(parameters, hiddenParameterSlugs),
-    [parameters, hiddenParameterSlugs],
-  );
-
   const canWrite = Boolean(dashboard?.can_write);
   const canRestore = Boolean(dashboard?.can_restore);
   const tabHasCards = currentTabDashcards.length > 0;
   const dashboardHasCards = dashboard?.dashcards.length > 0;
-  const hasVisibleParameters = visibleParameters.length > 0;
 
   const shouldRenderAsNightMode = isNightMode && isFullscreen;
 
@@ -447,7 +402,6 @@ function DashboardInner(props: DashboardProps) {
         isFullscreen={props.isFullscreen}
         isEditingParameter={props.isEditingParameter}
         isEditing={props.isEditing}
-        parameterValues={props.parameterValues}
         dashcardData={props.dashcardData}
         dashboard={props.dashboard}
         slowCards={props.slowCards}
@@ -458,68 +412,6 @@ function DashboardInner(props: DashboardProps) {
     );
   };
 
-  const parametersWidget = (
-    <SyncedParametersList
-      parameters={getValuePopulatedParameters({
-        parameters,
-        values: isAutoApplyFilters ? parameterValues : draftParameterValues,
-      })}
-      editingParameter={editingParameter}
-      hideParameters={hiddenParameterSlugs}
-      dashboard={dashboard}
-      isFullscreen={isFullscreen}
-      isNightMode={shouldRenderAsNightMode}
-      isEditing={isEditing}
-      setParameterValue={setParameterValue}
-      setParameterIndex={setParameterIndex}
-      setEditingParameter={setEditingParameter}
-      setParameterValueToDefault={setParameterValueToDefault}
-      enableParameterRequiredBehavior
-    />
-  );
-
-  const renderParameterList = () => {
-    if (!hasVisibleParameters) {
-      return null;
-    }
-
-    if (isEditing) {
-      return (
-        <ParametersWidgetContainer
-          hasScroll
-          isSticky
-          isFullscreen={isFullscreen}
-          isNightMode={shouldRenderAsNightMode}
-          data-testid="edit-dashboard-parameters-widget-container"
-        >
-          <FixedWidthContainer
-            isFixedWidth={dashboard?.width === "fixed"}
-            data-testid="fixed-width-filters"
-          >
-            {parametersWidget}
-          </FixedWidthContainer>
-        </ParametersWidgetContainer>
-      );
-    }
-
-    return (
-      <ParametersWidgetContainer
-        hasScroll={hasScroll}
-        isFullscreen={isFullscreen}
-        isNightMode={shouldRenderAsNightMode}
-        isSticky={isParametersWidgetContainersSticky(visibleParameters.length)}
-        data-testid="dashboard-parameters-widget-container"
-      >
-        <ParametersFixedWidthContainer
-          isFixedWidth={dashboard?.width === "fixed"}
-          data-testid="fixed-width-filters"
-        >
-          {parametersWidget}
-          <FilterApplyButton />
-        </ParametersFixedWidthContainer>
-      </ParametersWidgetContainer>
-    );
-  };
   return (
     <DashboardLoadingAndErrorWrapper
       isFullHeight={isEditing || isSharing}
@@ -563,7 +455,6 @@ function DashboardInner(props: DashboardProps) {
               location={location}
               dashboard={dashboard}
               isNightMode={shouldRenderAsNightMode}
-              parametersWidget={parametersWidget}
               isFullscreen={isFullscreen}
               fetchDashboard={fetchDashboard}
               onEditingChange={handleSetEditing}
@@ -609,7 +500,10 @@ function DashboardInner(props: DashboardProps) {
                 !isFullscreen && (isEditing || isSharing)
               }
             >
-              {renderParameterList()}
+              <DashboardParameterPanel
+                isFullscreen={isFullscreen}
+                hasScroll={hasScroll}
+              />
               <CardsContainer data-element-id="dashboard-cards-container">
                 {renderContent()}
               </CardsContainer>
@@ -617,11 +511,9 @@ function DashboardInner(props: DashboardProps) {
 
             <DashboardSidebars
               dashboard={dashboard}
-              parameters={parameters}
               showAddParameterPopover={props.showAddParameterPopover}
               removeParameter={props.removeParameter}
               addCardToDashboard={props.addCardToDashboard}
-              editingParameter={props.editingParameter}
               clickBehaviorSidebarDashcard={props.clickBehaviorSidebarDashcard}
               onReplaceAllDashCardVisualizationSettings={
                 props.onReplaceAllDashCardVisualizationSettings
@@ -662,16 +554,6 @@ function DashboardInner(props: DashboardProps) {
   );
 }
 
-function isParametersWidgetContainersSticky(parameterCount: number) {
-  if (!isSmallScreen()) {
-    return true;
-  }
-
-  // Sticky header with more than 5 parameters
-  // takes too much space on small screens
-  return parameterCount <= 5;
-}
-
 function isSuccessfulFetchDashboardResult(
   result: FetchDashboardResult,
 ): result is SuccessfulFetchDashboardResult {
diff --git a/frontend/src/metabase/dashboard/components/DashboardGrid.tsx b/frontend/src/metabase/dashboard/components/DashboardGrid.tsx
index b879c78d3fb..15b7ed6ee06 100644
--- a/frontend/src/metabase/dashboard/components/DashboardGrid.tsx
+++ b/frontend/src/metabase/dashboard/components/DashboardGrid.tsx
@@ -22,7 +22,6 @@ import {
   getVisibleCardIds,
 } from "metabase/dashboard/utils";
 import * as MetabaseAnalytics from "metabase/lib/analytics";
-import { color } from "metabase/lib/colors";
 import {
   GRID_WIDTH,
   GRID_ASPECT_RATIO,
@@ -31,7 +30,6 @@ import {
   DEFAULT_CARD_SIZE,
   MIN_ROW_HEIGHT,
 } from "metabase/lib/dashboard_grid";
-import { PLUGIN_COLLECTIONS } from "metabase/plugins";
 import EmbedFrameS from "metabase/public/components/EmbedFrame/EmbedFrame.module.css";
 import { addUndo } from "metabase/redux/undo";
 import { getVisualizationRaw } from "metabase/visualizations";
@@ -49,8 +47,6 @@ import type {
   DashCardId,
   Dashboard,
   DashboardTabId,
-  ParameterId,
-  ParameterValueOrArray,
   DashboardCard,
 } from "metabase-types/api";
 
@@ -126,7 +122,6 @@ type OwnProps = {
   dashboard: Dashboard;
   dashcardData: DashCardDataMap;
   selectedTabId: DashboardTabId | null;
-  parameterValues: Record<ParameterId, ParameterValueOrArray>;
   slowCards: Record<DashCardId, boolean>;
   isEditing: boolean;
   isEditingParameter: boolean;
@@ -467,39 +462,14 @@ class DashboardGrid extends Component<DashboardGridProps, DashboardGridState> {
     MetabaseAnalytics.trackStructEvent("Dashboard", "Remove Card");
   };
 
-  onDashCardAddSeries(dc: BaseDashboardCard) {
+  onDashCardAddSeries = (dc: BaseDashboardCard) => {
     this.setState({ addSeriesModalDashCard: dc });
-  }
+  };
 
   onReplaceCard = (dashcard: BaseDashboardCard) => {
     this.setState({ replaceCardModalDashCard: dashcard });
   };
 
-  getDashboardCardIcon = (dashCard: BaseDashboardCard) => {
-    const { isRegularCollection } = PLUGIN_COLLECTIONS;
-    const { dashboard } = this.props;
-    const isRegularQuestion = isRegularCollection({
-      authority_level: dashCard.collection_authority_level,
-    });
-    const isRegularDashboard = isRegularCollection({
-      authority_level: dashboard.collection_authority_level,
-    });
-    const authorityLevel = dashCard.collection_authority_level;
-    if (isRegularDashboard && !isRegularQuestion && authorityLevel) {
-      const opts = PLUGIN_COLLECTIONS.AUTHORITY_LEVEL[authorityLevel];
-      const iconSize = 14;
-      return {
-        name: opts.icon,
-        color: opts.color ? color(opts.color) : undefined,
-        tooltip: opts.tooltips?.belonging,
-        size: iconSize,
-
-        // Workaround: headerIcon on cards in a first column have incorrect offset out of the box
-        targetOffsetX: dashCard.col === 0 ? iconSize : 0,
-      };
-    }
-  };
-
   renderDashCard(
     dc: DashboardCard,
     {
@@ -515,9 +485,6 @@ class DashboardGrid extends Component<DashboardGridProps, DashboardGridState> {
     return (
       <DashCard
         dashcard={dc}
-        headerIcon={this.getDashboardCardIcon(dc)}
-        dashcardData={this.props.dashcardData}
-        parameterValues={this.props.parameterValues}
         slowCards={this.props.slowCards}
         gridItemWidth={gridItemWidth}
         totalNumGridCols={totalNumGridCols}
@@ -529,14 +496,14 @@ class DashboardGrid extends Component<DashboardGridProps, DashboardGridState> {
         isMobile={isMobile}
         isPublic={this.props.isPublic}
         isXray={this.props.isXray}
-        onRemove={() => this.onDashCardRemove(dc)}
-        onAddSeries={() => this.onDashCardAddSeries(dc)}
-        onReplaceCard={() => this.onReplaceCard(dc)}
-        onUpdateVisualizationSettings={settings =>
-          this.props.onUpdateDashCardVisualizationSettings(dc.id, settings)
+        onRemove={this.onDashCardRemove}
+        onAddSeries={this.onDashCardAddSeries}
+        onReplaceCard={this.onReplaceCard}
+        onUpdateVisualizationSettings={
+          this.props.onUpdateDashCardVisualizationSettings
         }
-        onReplaceAllVisualizationSettings={settings =>
-          this.props.onReplaceAllDashCardVisualizationSettings(dc.id, settings)
+        onReplaceAllVisualizationSettings={
+          this.props.onReplaceAllDashCardVisualizationSettings
         }
         mode={this.props.mode}
         navigateToNewCardFromDashboard={
diff --git a/frontend/src/metabase/dashboard/components/DashboardHeader/DashboardHeader.tsx b/frontend/src/metabase/dashboard/components/DashboardHeader/DashboardHeader.tsx
index 5ba544eabdd..6eeef87810a 100644
--- a/frontend/src/metabase/dashboard/components/DashboardHeader/DashboardHeader.tsx
+++ b/frontend/src/metabase/dashboard/components/DashboardHeader/DashboardHeader.tsx
@@ -1,9 +1,8 @@
 import cx from "classnames";
 import type { Location } from "history";
-import { type MouseEvent, type ReactNode, useState, Fragment } from "react";
+import { type MouseEvent, useState, Fragment } from "react";
 import { useMount } from "react-use";
 import { msgid, ngettext, t } from "ttag";
-import _ from "underscore";
 
 import { isInstanceAnalyticsCollection } from "metabase/collections/utils";
 import {
@@ -66,6 +65,7 @@ import type {
 } from "metabase-types/store";
 
 import { DASHBOARD_PDF_EXPORT_ROOT_ID, SIDEBAR_NAME } from "../../constants";
+import { DashboardParameterList } from "../DashboardParameterList";
 import { ExtraEditButtonsMenu } from "../ExtraEditButtonsMenu/ExtraEditButtonsMenu";
 
 import {
@@ -93,7 +93,6 @@ interface DashboardHeaderProps {
   isAddParameterPopoverOpen: boolean;
   canManageSubscriptions: boolean;
   hasNightModeToggle: boolean;
-  parametersWidget?: ReactNode;
 
   addCardToDashboard: (opts: {
     dashId: DashboardId;
@@ -149,7 +148,6 @@ export const DashboardHeader = (props: DashboardHeaderProps) => {
     isEditing,
     location,
     dashboard,
-    parametersWidget,
     isFullscreen,
     onFullscreenChange,
     sidebar,
@@ -360,8 +358,8 @@ export const DashboardHeader = (props: DashboardHeaderProps) => {
     const buttons = [];
     const extraButtons = [];
 
-    if (isFullscreen && parametersWidget) {
-      buttons.push(parametersWidget);
+    if (isFullscreen) {
+      buttons.push(<DashboardParameterList isFullscreen={isFullscreen} />);
     }
 
     if (isEditing) {
diff --git a/frontend/src/metabase/dashboard/components/DashboardParameterList/DashboardParameterList.tsx b/frontend/src/metabase/dashboard/components/DashboardParameterList/DashboardParameterList.tsx
new file mode 100644
index 00000000000..0bb9fb1a7e6
--- /dev/null
+++ b/frontend/src/metabase/dashboard/components/DashboardParameterList/DashboardParameterList.tsx
@@ -0,0 +1,62 @@
+import {
+  setEditingParameter,
+  setParameterIndex,
+  setParameterValue,
+  setParameterValueToDefault,
+} from "metabase/dashboard/actions";
+import {
+  getDashboardComplete,
+  getDraftParameterValues,
+  getEditingParameter,
+  getHiddenParameterSlugs,
+  getIsAutoApplyFilters,
+  getIsEditing,
+  getIsNightMode,
+  getParameters,
+  getParameterValues,
+} from "metabase/dashboard/selectors";
+import { useDispatch, useSelector } from "metabase/lib/redux";
+import SyncedParametersList from "metabase/parameters/components/SyncedParametersList";
+import { getValuePopulatedParameters } from "metabase-lib/v1/parameters/utils/parameter-values";
+
+interface DashboardParameterListProps {
+  isFullscreen: boolean;
+}
+
+export function DashboardParameterList({
+  isFullscreen,
+}: DashboardParameterListProps) {
+  const dashboard = useSelector(getDashboardComplete);
+  const parameters = useSelector(getParameters);
+  const parameterValues = useSelector(getParameterValues);
+  const draftParameterValues = useSelector(getDraftParameterValues);
+  const editingParameter = useSelector(getEditingParameter);
+  const hiddenParameterSlugs = useSelector(getHiddenParameterSlugs);
+  const isEditing = useSelector(getIsEditing);
+  const isAutoApplyFilters = useSelector(getIsAutoApplyFilters);
+  const isNightMode = useSelector(getIsNightMode);
+  const shouldRenderAsNightMode = isNightMode && isFullscreen;
+  const dispatch = useDispatch();
+
+  return (
+    <SyncedParametersList
+      parameters={getValuePopulatedParameters({
+        parameters,
+        values: isAutoApplyFilters ? parameterValues : draftParameterValues,
+      })}
+      editingParameter={editingParameter}
+      hideParameters={hiddenParameterSlugs}
+      dashboard={dashboard}
+      isFullscreen={isFullscreen}
+      isNightMode={shouldRenderAsNightMode}
+      isEditing={isEditing}
+      setParameterValue={(id, value) => dispatch(setParameterValue(id, value))}
+      setParameterIndex={(id, index) => dispatch(setParameterIndex(id, index))}
+      setEditingParameter={id => dispatch(setEditingParameter(id))}
+      setParameterValueToDefault={id =>
+        dispatch(setParameterValueToDefault(id))
+      }
+      enableParameterRequiredBehavior
+    />
+  );
+}
diff --git a/frontend/src/metabase/dashboard/components/DashboardParameterList/index.ts b/frontend/src/metabase/dashboard/components/DashboardParameterList/index.ts
new file mode 100644
index 00000000000..67b0e879f3e
--- /dev/null
+++ b/frontend/src/metabase/dashboard/components/DashboardParameterList/index.ts
@@ -0,0 +1 @@
+export * from "./DashboardParameterList";
diff --git a/frontend/src/metabase/dashboard/components/DashboardParameterPanel/DashboardParameterPanel.tsx b/frontend/src/metabase/dashboard/components/DashboardParameterPanel/DashboardParameterPanel.tsx
new file mode 100644
index 00000000000..8fd6dbbb4de
--- /dev/null
+++ b/frontend/src/metabase/dashboard/components/DashboardParameterPanel/DashboardParameterPanel.tsx
@@ -0,0 +1,92 @@
+import {
+  getDashboardComplete,
+  getHiddenParameterSlugs,
+  getIsEditing,
+  getIsNightMode,
+  getParameters,
+} from "metabase/dashboard/selectors";
+import { isSmallScreen } from "metabase/lib/dom";
+import { useSelector } from "metabase/lib/redux";
+import { FilterApplyButton } from "metabase/parameters/components/FilterApplyButton";
+import { getVisibleParameters } from "metabase/parameters/utils/ui";
+
+import {
+  FixedWidthContainer,
+  ParametersFixedWidthContainer,
+  ParametersWidgetContainer,
+} from "../Dashboard/Dashboard.styled";
+import { DashboardParameterList } from "../DashboardParameterList";
+
+interface DashboardParameterPanelProps {
+  hasScroll: boolean;
+  isFullscreen: boolean;
+}
+
+export function DashboardParameterPanel({
+  hasScroll,
+  isFullscreen,
+}: DashboardParameterPanelProps) {
+  const dashboard = useSelector(getDashboardComplete);
+  const parameters = useSelector(getParameters);
+  const hiddenParameterSlugs = useSelector(getHiddenParameterSlugs);
+  const isEditing = useSelector(getIsEditing);
+  const isNightMode = useSelector(getIsNightMode);
+  const visibleParameters = getVisibleParameters(
+    parameters,
+    hiddenParameterSlugs,
+  );
+  const hasVisibleParameters = visibleParameters.length > 0;
+  const shouldRenderAsNightMode = isNightMode && isFullscreen;
+
+  if (!hasVisibleParameters) {
+    return null;
+  }
+
+  if (isEditing) {
+    return (
+      <ParametersWidgetContainer
+        hasScroll
+        isSticky
+        isFullscreen={isFullscreen}
+        isNightMode={shouldRenderAsNightMode}
+        data-testid="edit-dashboard-parameters-widget-container"
+      >
+        <FixedWidthContainer
+          isFixedWidth={dashboard?.width === "fixed"}
+          data-testid="fixed-width-filters"
+        >
+          {<DashboardParameterList isFullscreen={isFullscreen} />}
+        </FixedWidthContainer>
+      </ParametersWidgetContainer>
+    );
+  }
+
+  return (
+    <ParametersWidgetContainer
+      hasScroll={hasScroll}
+      isFullscreen={isFullscreen}
+      isNightMode={shouldRenderAsNightMode}
+      isSticky={isParametersWidgetContainersSticky(visibleParameters.length)}
+      data-testid="dashboard-parameters-widget-container"
+    >
+      <ParametersFixedWidthContainer
+        isFixedWidth={dashboard?.width === "fixed"}
+        data-testid="fixed-width-filters"
+      >
+        {<DashboardParameterList isFullscreen={isFullscreen} />}
+
+        <FilterApplyButton />
+      </ParametersFixedWidthContainer>
+    </ParametersWidgetContainer>
+  );
+}
+
+function isParametersWidgetContainersSticky(parameterCount: number) {
+  if (!isSmallScreen()) {
+    return true;
+  }
+
+  // Sticky header with more than 5 parameters
+  // takes too much space on small screens
+  return parameterCount <= 5;
+}
diff --git a/frontend/src/metabase/dashboard/components/DashboardParameterPanel/index.ts b/frontend/src/metabase/dashboard/components/DashboardParameterPanel/index.ts
new file mode 100644
index 00000000000..bd56c5b0e6f
--- /dev/null
+++ b/frontend/src/metabase/dashboard/components/DashboardParameterPanel/index.ts
@@ -0,0 +1 @@
+export * from "./DashboardParameterPanel";
diff --git a/frontend/src/metabase/dashboard/components/DashboardSidebars.jsx b/frontend/src/metabase/dashboard/components/DashboardSidebars.jsx
index ce870f8bbfd..ffb2cf15da1 100644
--- a/frontend/src/metabase/dashboard/components/DashboardSidebars.jsx
+++ b/frontend/src/metabase/dashboard/components/DashboardSidebars.jsx
@@ -3,7 +3,12 @@ import { useCallback } from "react";
 import _ from "underscore";
 
 import { SIDEBAR_NAME } from "metabase/dashboard/constants";
+import {
+  getEditingParameter,
+  getParameters,
+} from "metabase/dashboard/selectors";
 import * as MetabaseAnalytics from "metabase/lib/analytics";
+import { useSelector } from "metabase/lib/redux";
 import { ParameterSidebar } from "metabase/parameters/components/ParameterSidebar";
 import { hasMapping } from "metabase/parameters/utils/dashboards";
 import SharingSidebar from "metabase/sharing/components/SharingSidebar";
@@ -15,11 +20,9 @@ import { DashboardInfoSidebar } from "./DashboardInfoSidebar";
 
 DashboardSidebars.propTypes = {
   dashboard: PropTypes.object,
-  parameters: PropTypes.array,
   showAddParameterPopover: PropTypes.func.isRequired,
   removeParameter: PropTypes.func.isRequired,
   addCardToDashboard: PropTypes.func.isRequired,
-  editingParameter: PropTypes.object,
   clickBehaviorSidebarDashcard: PropTypes.object, // only defined when click-behavior sidebar is open
   onReplaceAllDashCardVisualizationSettings: PropTypes.func.isRequired,
   onUpdateDashCardVisualizationSettings: PropTypes.func.isRequired,
@@ -49,11 +52,9 @@ DashboardSidebars.propTypes = {
 
 export function DashboardSidebars({
   dashboard,
-  parameters,
   showAddParameterPopover,
   removeParameter,
   addCardToDashboard,
-  editingParameter,
   clickBehaviorSidebarDashcard,
   onReplaceAllDashCardVisualizationSettings,
   onUpdateDashCardVisualizationSettings,
@@ -77,6 +78,9 @@ export function DashboardSidebars({
   selectedTabId,
   getEmbeddedParameterVisibility,
 }) {
+  const parameters = useSelector(getParameters);
+  const editingParameter = useSelector(getEditingParameter);
+
   const handleAddCard = useCallback(
     cardId => {
       addCardToDashboard({
diff --git a/frontend/src/metabase/dashboard/containers/DashboardApp/DashboardApp.tsx b/frontend/src/metabase/dashboard/containers/DashboardApp/DashboardApp.tsx
index 5ef5558845a..6d0b03cc5ea 100644
--- a/frontend/src/metabase/dashboard/containers/DashboardApp/DashboardApp.tsx
+++ b/frontend/src/metabase/dashboard/containers/DashboardApp/DashboardApp.tsx
@@ -32,14 +32,12 @@ import {
 } from "metabase/selectors/user";
 import type Database from "metabase-lib/v1/metadata/Database";
 import type Metadata from "metabase-lib/v1/metadata/Metadata";
-import type { UiParameter } from "metabase-lib/v1/parameters/types";
 import type {
   Dashboard as IDashboard,
   DashboardId,
   DashCardDataMap,
   DashCardId,
   DatabaseId,
-  Parameter,
   ParameterId,
   ParameterValueOrArray,
 } from "metabase-types/api";
@@ -48,17 +46,14 @@ import type { SelectedTabId, State, StoreDashcard } from "metabase-types/store";
 import * as dashboardActions from "../../actions";
 import { DASHBOARD_SLOW_TIMEOUT } from "../../constants";
 import {
-  getCardData,
+  getDashcardDataMap,
   getClickBehaviorSidebarDashcard,
   getDashboardBeforeEditing,
   getDashboardComplete,
   getDocumentTitle,
-  getDraftParameterValues,
-  getEditingParameter,
   getFavicon,
   getIsAdditionalInfoVisible,
   getIsAddParameterPopoverOpen,
-  getIsAutoApplyFilters,
   getIsDirty,
   getIsEditing,
   getIsEditingParameter,
@@ -68,7 +63,6 @@ import {
   getIsRunning,
   getIsSharing,
   getLoadingStartTime,
-  getParameters,
   getParameterValues,
   getSelectedTabId,
   getSidebar,
@@ -96,10 +90,7 @@ type StateProps = {
   dashcardData: DashCardDataMap;
   slowCards: Record<DashCardId, unknown>;
   databases: Record<DatabaseId, Database>;
-  editingParameter?: Parameter | null;
-  parameters: UiParameter[];
   parameterValues: Record<ParameterId, ParameterValueOrArray>;
-  draftParameterValues: Record<ParameterId, ParameterValueOrArray | null>;
   metadata: Metadata;
   loadingStartTime: number | null;
   clickBehaviorSidebarDashcard: StoreDashcard | null;
@@ -112,7 +103,6 @@ type StateProps = {
   isHeaderVisible: boolean;
   isAdditionalInfoVisible: boolean;
   selectedTabId: SelectedTabId;
-  isAutoApplyFilters: boolean;
   isNavigatingBackToDashboard: boolean;
   getEmbeddedParameterVisibility: (
     slug: string,
@@ -139,13 +129,10 @@ const mapStateToProps = (state: State): StateProps => {
     isEditingParameter: getIsEditingParameter(state),
     isDirty: getIsDirty(state),
     dashboard: getDashboardComplete(state),
-    dashcardData: getCardData(state),
+    dashcardData: getDashcardDataMap(state),
     slowCards: getSlowCards(state),
     databases: metadata.databases,
-    editingParameter: getEditingParameter(state),
-    parameters: getParameters(state),
     parameterValues: getParameterValues(state),
-    draftParameterValues: getDraftParameterValues(state),
 
     metadata,
     loadingStartTime: getLoadingStartTime(state),
@@ -159,7 +146,6 @@ const mapStateToProps = (state: State): StateProps => {
     isHeaderVisible: getIsHeaderVisible(state),
     isAdditionalInfoVisible: getIsAdditionalInfoVisible(state),
     selectedTabId: getSelectedTabId(state),
-    isAutoApplyFilters: getIsAutoApplyFilters(state),
     isNavigatingBackToDashboard: getIsNavigatingBackToDashboard(state),
     getEmbeddedParameterVisibility: (slug: string) =>
       getEmbeddedParameterVisibility(state, slug),
diff --git a/frontend/src/metabase/dashboard/hoc/DashboardData.jsx b/frontend/src/metabase/dashboard/hoc/DashboardData.jsx
index 6764e24ab7c..eafa37a043d 100644
--- a/frontend/src/metabase/dashboard/hoc/DashboardData.jsx
+++ b/frontend/src/metabase/dashboard/hoc/DashboardData.jsx
@@ -7,7 +7,7 @@ import _ from "underscore";
 import * as dashboardActions from "metabase/dashboard/actions";
 import {
   getDashboardComplete,
-  getCardData,
+  getDashcardDataMap,
   getSlowCards,
   getParameters,
   getParameterValues,
@@ -19,7 +19,7 @@ import { setErrorPage } from "metabase/redux/app";
 const mapStateToProps = (state, props) => {
   return {
     dashboard: getDashboardComplete(state, props),
-    dashcardData: getCardData(state, props),
+    dashcardData: getDashcardDataMap(state, props),
     selectedTabId: getSelectedTabId(state),
     slowCards: getSlowCards(state, props),
     parameters: getParameters(state, props),
diff --git a/frontend/src/metabase/dashboard/hoc/WithVizSettingsData.js b/frontend/src/metabase/dashboard/hoc/WithVizSettingsData.js
deleted file mode 100644
index 46546710f61..00000000000
--- a/frontend/src/metabase/dashboard/hoc/WithVizSettingsData.js
+++ /dev/null
@@ -1,52 +0,0 @@
-import { Component } from "react";
-import { connect } from "react-redux";
-import { withRouter } from "react-router";
-import _ from "underscore";
-
-import { getLinkTargets } from "metabase/lib/click-behavior";
-import { getUserAttributes } from "metabase/selectors/user";
-
-/**
- * This HOC gives access to data referenced in viz settings.
- * @deprecated HOCs are deprecated
- */
-export const WithVizSettingsData = ComposedComponent => {
-  return withRouter(
-    connect(
-      (state, props) => ({
-        getExtraDataForClick: clicked => {
-          const entitiesByTypeAndId = _.chain(getLinkTargets(clicked.settings))
-            .groupBy(target => target.entity.name)
-            .mapObject(targets =>
-              _.chain(targets)
-                .map(({ entity, entityType, entityId }) =>
-                  entityType === "question"
-                    ? entity.selectors.getObject(state, { entityId })?.card()
-                    : entity.selectors.getObject(state, { entityId }),
-                )
-                .filter(object => object != null)
-                .indexBy(object => object.id)
-                .value(),
-            )
-            .value();
-          return {
-            ...entitiesByTypeAndId,
-            location: props.location,
-            routerParams: props.params,
-            parameterValuesBySlug: props.parameterValuesBySlug,
-            dashboard: props.dashboard,
-            dashcard: props.dashcard,
-            userAttributes: getUserAttributes(state, props),
-          };
-        },
-      }),
-      dispatch => ({ dispatch }),
-    )(
-      class WithVizSettingsData extends Component {
-        render() {
-          return <ComposedComponent {..._.omit(this.props, "dispatch")} />;
-        }
-      },
-    ),
-  );
-};
diff --git a/frontend/src/metabase/dashboard/hoc/useClickBehaviorData.js b/frontend/src/metabase/dashboard/hoc/useClickBehaviorData.js
new file mode 100644
index 00000000000..fba2d5e8f2c
--- /dev/null
+++ b/frontend/src/metabase/dashboard/hoc/useClickBehaviorData.js
@@ -0,0 +1,57 @@
+import { useMemo } from "react";
+import _ from "underscore";
+
+import {
+  getDashboardComplete,
+  getDashCardById,
+  getParameterValuesBySlugMap,
+} from "metabase/dashboard/selectors";
+import { getLinkTargets } from "metabase/lib/click-behavior";
+import { useStore } from "metabase/lib/redux";
+import { getUserAttributes } from "metabase/selectors/user";
+
+function createGetExtraDataForClick(store, dashcardId) {
+  return clicked => {
+    const state = store.getState();
+    const dashboard = getDashboardComplete(state);
+    const dashcard = getDashCardById(state, dashcardId);
+    const parameterValuesBySlug = getParameterValuesBySlugMap(state);
+    const userAttributes = getUserAttributes(state);
+
+    const entitiesByTypeAndId = _.chain(getLinkTargets(clicked.settings))
+      .groupBy(target => target.entity.name)
+      .mapObject(targets =>
+        _.chain(targets)
+          .map(({ entity, entityType, entityId }) =>
+            entityType === "question"
+              ? entity.selectors.getObject(state, { entityId })?.card()
+              : entity.selectors.getObject(state, { entityId }),
+          )
+          .filter(object => object != null)
+          .indexBy(object => object.id)
+          .value(),
+      )
+      .value();
+    return {
+      ...entitiesByTypeAndId,
+      parameterValuesBySlug,
+      dashboard,
+      dashcard,
+      userAttributes,
+    };
+  };
+}
+
+/**
+ * This hook gives access to data referenced in viz settings.
+ */
+export const useClickBehaviorData = ({ dashcardId }) => {
+  const store = useStore();
+
+  const getExtraDataForClick = useMemo(
+    () => createGetExtraDataForClick(store, dashcardId),
+    [store, dashcardId],
+  );
+
+  return { getExtraDataForClick };
+};
diff --git a/frontend/src/metabase/dashboard/selectors.ts b/frontend/src/metabase/dashboard/selectors.ts
index bc76f3396ad..65b0589fe4d 100644
--- a/frontend/src/metabase/dashboard/selectors.ts
+++ b/frontend/src/metabase/dashboard/selectors.ts
@@ -14,6 +14,7 @@ import type { EmbeddingParameterVisibility } from "metabase/public/lib/types";
 import { getEmbedOptions, getIsEmbedded } from "metabase/selectors/embed";
 import { getMetadata } from "metabase/selectors/metadata";
 import Question from "metabase-lib/v1/Question";
+import { getParameterValuesBySlug } from "metabase-lib/v1/parameters/utils/parameter-values";
 import type {
   Card,
   CardId,
@@ -62,7 +63,14 @@ export const getClickBehaviorSidebarDashcard = (state: State) => {
     : null;
 };
 export const getDashboards = (state: State) => state.dashboard.dashboards;
-export const getCardData = (state: State) => state.dashboard.dashcardData;
+export const getDashcardDataMap = (state: State) =>
+  state.dashboard.dashcardData;
+
+export function getDashcardData(state: State, dashcardId: DashCardId) {
+  const dashcardData = getDashcardDataMap(state);
+  return dashcardData[dashcardId];
+}
+
 export const getSlowCards = (state: State) => state.dashboard.slowCards;
 export const getParameterValues = (state: State) =>
   state.dashboard.parameterValues;
@@ -203,6 +211,16 @@ const getIsParameterValuesEmpty = createSelector(
   },
 );
 
+export const getParameterValuesBySlugMap = createSelector(
+  [getDashboardComplete, getParameterValues],
+  (dashboard, parameterValues) => {
+    if (!dashboard) {
+      return {};
+    }
+    return getParameterValuesBySlug(dashboard.parameters, parameterValues);
+  },
+);
+
 export const getCanShowAutoApplyFiltersToast = createSelector(
   [
     getDashboard,
@@ -436,6 +454,41 @@ export function getInitialSelectedTabId(dashboard: Dashboard | StoreDashboard) {
   return dashboard.tabs?.[0]?.id || null;
 }
 
+export const getCurrentTabDashcards = createSelector(
+  [getDashboardComplete, getSelectedTabId],
+  (dashboard, selectedTabId) => {
+    if (!dashboard || !Array.isArray(dashboard?.dashcards)) {
+      return [];
+    }
+    if (!selectedTabId) {
+      return dashboard.dashcards;
+    }
+    return dashboard.dashcards.filter(
+      (dc: DashboardCard) => dc.dashboard_tab_id === selectedTabId,
+    );
+  },
+);
+
+export const getHiddenParameterSlugs = createSelector(
+  [getParameters, getCurrentTabDashcards, getIsEditing],
+  (parameters, currentTabDashcards, isEditing) => {
+    if (isEditing) {
+      // All filters should be visible in edit mode
+      return undefined;
+    }
+
+    const currentTabParameterIds = currentTabDashcards.flatMap(
+      (dc: DashboardCard) =>
+        dc.parameter_mappings?.map(pm => pm.parameter_id) ?? [],
+    );
+    const hiddenParameters = parameters.filter(
+      parameter => !currentTabParameterIds.includes(parameter.id),
+    );
+
+    return hiddenParameters.map(p => p.slug).join(",");
+  },
+);
+
 export const getParameterMappingsBeforeEditing = createSelector(
   [getDashboardBeforeEditing],
   editingDashboard => {
diff --git a/frontend/src/metabase/dashboard/utils.ts b/frontend/src/metabase/dashboard/utils.ts
index f877bc04ee8..6b65eb3f51f 100644
--- a/frontend/src/metabase/dashboard/utils.ts
+++ b/frontend/src/metabase/dashboard/utils.ts
@@ -205,17 +205,17 @@ export async function fetchDataOrError<T>(dataPromise: Promise<T>) {
 
 export function isDashcardLoading(
   dashcard: BaseDashboardCard,
-  dashcardsData: DashCardDataMap,
+  dashcardsData: Record<CardId, Dataset | null | undefined>,
 ) {
   if (isVirtualDashCard(dashcard)) {
     return false;
   }
 
-  if (dashcardsData[dashcard.id] == null) {
+  if (dashcardsData == null) {
     return true;
   }
 
-  const cardData = Object.values(dashcardsData[dashcard.id]);
+  const cardData = Object.values(dashcardsData);
   return cardData.length === 0 || cardData.some(data => data == null);
 }
 
diff --git a/frontend/src/metabase/dashboard/utils.unit.spec.ts b/frontend/src/metabase/dashboard/utils.unit.spec.ts
index 0904eccca66..f48d1da2ca9 100644
--- a/frontend/src/metabase/dashboard/utils.unit.spec.ts
+++ b/frontend/src/metabase/dashboard/utils.unit.spec.ts
@@ -141,7 +141,7 @@ describe("Dashboard utils", () => {
     it("should return false for cards with loaded data", () => {
       expect(
         isDashcardLoading(createMockDashboardCard({ id: 1 }), {
-          1: { 2: createMockDataset() },
+          2: createMockDataset(),
         }),
       ).toBe(false);
     });
@@ -149,7 +149,8 @@ describe("Dashboard utils", () => {
     it("should return true when the dash card data is missing", () => {
       expect(
         isDashcardLoading(createMockDashboardCard({ id: 1 }), {
-          1: { 2: null, 3: createMockDataset() },
+          2: null,
+          3: createMockDataset(),
         }),
       ).toBe(true);
     });
diff --git a/frontend/src/metabase/public/containers/PublicDashboard/PublicDashboard.tsx b/frontend/src/metabase/public/containers/PublicDashboard/PublicDashboard.tsx
index 0104cc50a02..b7f04210bf7 100644
--- a/frontend/src/metabase/public/containers/PublicDashboard/PublicDashboard.tsx
+++ b/frontend/src/metabase/public/containers/PublicDashboard/PublicDashboard.tsx
@@ -29,7 +29,7 @@ import type {
 } from "metabase/dashboard/hoc/types";
 import {
   getDashboardComplete,
-  getCardData,
+  getDashcardDataMap,
   getSlowCards,
   getParameters,
   getParameterValues,
@@ -67,7 +67,7 @@ const mapStateToProps = (state: State, props: OwnProps) => {
     ),
     metadata: getMetadata(state),
     dashboard: getDashboardComplete(state),
-    dashcardData: getCardData(state),
+    dashcardData: getDashcardDataMap(state),
     slowCards: getSlowCards(state),
     parameters: getParameters(state),
     parameterValues: getParameterValues(state),
@@ -275,7 +275,6 @@ class PublicDashboardInner extends Component<PublicDashboardProps> {
                   navigateToNewCardFromDashboard={() => {}}
                   dashcardData={this.props.dashcardData}
                   selectedTabId={this.props.selectedTabId}
-                  parameterValues={this.props.parameterValues}
                   slowCards={this.props.slowCards}
                   isEditing={false}
                   isEditingParameter={false}
diff --git a/frontend/src/metabase/questions/actions.ts b/frontend/src/metabase/questions/actions.ts
index e0b9848dc7f..8707eaa1386 100644
--- a/frontend/src/metabase/questions/actions.ts
+++ b/frontend/src/metabase/questions/actions.ts
@@ -9,16 +9,27 @@ export interface LoadMetadataOptions {
   reload?: boolean;
 }
 
-export const loadMetadataForCard =
-  (card: Card, options?: LoadMetadataOptions) =>
+export const loadMetadataForCard = (
+  card: Card,
+  options?: LoadMetadataOptions,
+) => loadMetadataForCards([card], options);
+
+export const loadMetadataForCards =
+  (cards: Card[], options?: LoadMetadataOptions) =>
   async (dispatch: Dispatch, getState: GetState) => {
     const getDependencies = () => {
-      const question = new Question(card, getMetadata(getState()));
-      return Lib.dependentMetadata(
-        question.query(),
-        question.id(),
-        question.type(),
-      );
+      // it's important to create it once here for performance reasons
+      // MBQL lib attaches parsed metadata to the object
+      const metadata = getMetadata(getState());
+      return cards
+        .map(card => new Question(card, metadata))
+        .flatMap(question =>
+          Lib.dependentMetadata(
+            question.query(),
+            question.id(),
+            question.type(),
+          ),
+        );
     };
     await dispatch(loadMetadata(getDependencies, [], options));
   };
diff --git a/frontend/src/metabase/visualizations/visualizations/Heading/Heading.tsx b/frontend/src/metabase/visualizations/visualizations/Heading/Heading.tsx
index 286992c3674..605f89f2fec 100644
--- a/frontend/src/metabase/visualizations/visualizations/Heading/Heading.tsx
+++ b/frontend/src/metabase/visualizations/visualizations/Heading/Heading.tsx
@@ -2,13 +2,14 @@ import type { MouseEvent } from "react";
 import { useEffect, useMemo, useState } from "react";
 import { t } from "ttag";
 
+import { getParameterValues } from "metabase/dashboard/selectors";
 import { useToggle } from "metabase/hooks/use-toggle";
+import { useSelector } from "metabase/lib/redux";
 import { isEmpty } from "metabase/lib/validate";
 import { fillParametersInText } from "metabase/visualizations/shared/utils/parameter-substitution";
 import type {
   Dashboard,
   QuestionDashboardCard,
-  ParameterValueOrArray,
   VisualizationSettings,
 } from "metabase-types/api";
 
@@ -25,7 +26,6 @@ interface HeadingProps {
   dashcard: QuestionDashboardCard;
   settings: VisualizationSettings;
   dashboard: Dashboard;
-  parameterValues: { [id: string]: ParameterValueOrArray };
 }
 
 export function Heading({
@@ -34,8 +34,8 @@ export function Heading({
   onUpdateVisualizationSettings,
   dashcard,
   dashboard,
-  parameterValues,
 }: HeadingProps) {
+  const parameterValues = useSelector(getParameterValues);
   const justAdded = useMemo(() => dashcard?.justAdded || false, [dashcard]);
 
   const [isFocused, { turnOn: toggleFocusOn, turnOff: toggleFocusOff }] =
diff --git a/frontend/src/metabase/visualizations/visualizations/Heading/Heading.unit.spec.tsx b/frontend/src/metabase/visualizations/visualizations/Heading/Heading.unit.spec.tsx
index 724a70d1301..ddb6a5760f8 100644
--- a/frontend/src/metabase/visualizations/visualizations/Heading/Heading.unit.spec.tsx
+++ b/frontend/src/metabase/visualizations/visualizations/Heading/Heading.unit.spec.tsx
@@ -1,6 +1,7 @@
-import { render, screen } from "@testing-library/react";
+import { screen } from "@testing-library/react";
 import userEvent from "@testing-library/user-event";
 
+import { renderWithProviders } from "__support__/ui";
 import { color } from "metabase/lib/colors";
 import { buildTextTagTarget } from "metabase-lib/v1/parameters/utils/targets";
 import type {
@@ -16,6 +17,7 @@ import {
   createMockDashboard,
   createMockDashboardCard,
 } from "metabase-types/api/mocks";
+import { createMockDashboardState } from "metabase-types/store/mocks";
 
 import { Heading } from "../Heading";
 
@@ -45,8 +47,14 @@ const defaultProps = {
   parameterValues: {},
 };
 
-const setup = (options: Options) => {
-  render(<Heading {...defaultProps} {...options} />);
+const setup = ({ parameterValues, ...options }: Options) => {
+  renderWithProviders(<Heading {...defaultProps} {...options} />, {
+    storeInitialState: {
+      dashboard: createMockDashboardState({
+        parameterValues,
+      }),
+    },
+  });
 };
 
 describe("Text", () => {
diff --git a/frontend/src/metabase/visualizations/visualizations/Text/Text.jsx b/frontend/src/metabase/visualizations/visualizations/Text/Text.jsx
index f574e3ce682..a0e9eb027a9 100644
--- a/frontend/src/metabase/visualizations/visualizations/Text/Text.jsx
+++ b/frontend/src/metabase/visualizations/visualizations/Text/Text.jsx
@@ -7,7 +7,9 @@ import remarkGfm from "remark-gfm";
 import { t } from "ttag";
 
 import CS from "metabase/css/core/index.css";
+import { getParameterValues } from "metabase/dashboard/selectors";
 import { useToggle } from "metabase/hooks/use-toggle";
+import { useSelector } from "metabase/lib/redux";
 import { isEmpty } from "metabase/lib/validate";
 import { fillParametersInText } from "metabase/visualizations/shared/utils/parameter-substitution";
 
@@ -38,9 +40,9 @@ export function Text({
   gridSize,
   settings,
   isEditing,
-  parameterValues,
   isMobile,
 }) {
+  const parameterValues = useSelector(getParameterValues);
   const justAdded = useMemo(() => dashcard?.justAdded || false, [dashcard]);
   const [textValue, setTextValue] = useState(settings.text);
 
diff --git a/frontend/src/metabase/visualizations/visualizations/Text/Text.unit.spec.tsx b/frontend/src/metabase/visualizations/visualizations/Text/Text.unit.spec.tsx
index 81306e84ce0..ac2916ce9d1 100644
--- a/frontend/src/metabase/visualizations/visualizations/Text/Text.unit.spec.tsx
+++ b/frontend/src/metabase/visualizations/visualizations/Text/Text.unit.spec.tsx
@@ -1,7 +1,10 @@
-import { render, screen } from "@testing-library/react";
+import { screen } from "@testing-library/react";
 import userEvent from "@testing-library/user-event";
 
+import { renderWithProviders } from "__support__/ui";
 import { color } from "metabase/lib/colors";
+import type { ParameterValueOrArray } from "metabase-types/api";
+import { createMockDashboardState } from "metabase-types/store/mocks";
 
 import { Text } from "../Text";
 
@@ -21,8 +24,19 @@ const defaultProps = {
   isMobile: false,
 };
 
-const setup = (options = {}) => {
-  render(<Text {...defaultProps} {...options} />);
+interface SetupOpts {
+  settings?: Settings;
+  parameterValues?: Record<string, ParameterValueOrArray>;
+}
+
+const setup = ({ parameterValues, ...options }: SetupOpts = {}) => {
+  renderWithProviders(<Text {...defaultProps} {...options} />, {
+    storeInitialState: {
+      dashboard: createMockDashboardState({
+        parameterValues,
+      }),
+    },
+  });
 };
 
 describe("Text", () => {
-- 
GitLab