From 4fe8e0466c2cd422593e671d5a2874a359c7c8af Mon Sep 17 00:00:00 2001
From: Oisin Coveney <oisin@metabase.com>
Date: Thu, 9 May 2024 18:36:03 +0300
Subject: [PATCH] Convert PublicDashboard to TS (#41972)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Co-authored-by: Nicolò Pretto <info@npretto.com>
---
 .../src/metabase-types/store/dashboard.ts     |   2 +-
 .../dashboard/actions/data-fetching-typed.ts  | 190 ++++++++++
 .../dashboard/actions/data-fetching.js        | 182 +--------
 .../actions/data-fetching.unit.spec.js        |   2 +-
 .../src/metabase/dashboard/actions/index.ts   |   1 +
 .../metabase/dashboard/actions/revisions.js   |   6 +-
 .../src/metabase/dashboard/actions/save.js    |   5 +-
 frontend/src/metabase/dashboard/actions/ui.ts |   2 +-
 .../components/Dashboard/Dashboard.tsx        |  40 +-
 ...hboardActions.jsx => DashboardActions.tsx} |  77 ++--
 .../dashboard/components/DashboardGrid.tsx    |  70 ++--
 .../DashboardHeader/DashboardHeader.tsx       |   2 +-
 frontend/src/metabase/dashboard/hoc/types.ts  |  46 +++
 frontend/src/metabase/dashboard/types.ts      |   2 +-
 .../parameters/utils/parameter-values.ts      |   4 +-
 .../PublicDashboard/PublicDashboard.jsx       | 223 -----------
 .../PublicDashboard/PublicDashboard.tsx       | 348 ++++++++++++++++++
 17 files changed, 699 insertions(+), 503 deletions(-)
 create mode 100644 frontend/src/metabase/dashboard/actions/data-fetching-typed.ts
 rename frontend/src/metabase/dashboard/components/{DashboardActions.jsx => DashboardActions.tsx} (68%)
 create mode 100644 frontend/src/metabase/dashboard/hoc/types.ts
 delete mode 100644 frontend/src/metabase/public/containers/PublicDashboard/PublicDashboard.jsx
 create mode 100644 frontend/src/metabase/public/containers/PublicDashboard/PublicDashboard.tsx

diff --git a/frontend/src/metabase-types/store/dashboard.ts b/frontend/src/metabase-types/store/dashboard.ts
index ac5b4e3e30b..4c15ede47fc 100644
--- a/frontend/src/metabase-types/store/dashboard.ts
+++ b/frontend/src/metabase-types/store/dashboard.ts
@@ -101,7 +101,7 @@ export interface DashboardState {
   isAddParameterPopoverOpen: boolean;
   isNavigatingBackToDashboard: boolean;
 
-  slowCards: Record<DashCardId, unknown>;
+  slowCards: Record<DashCardId, boolean>;
 
   sidebar: DashboardSidebarState;
 
diff --git a/frontend/src/metabase/dashboard/actions/data-fetching-typed.ts b/frontend/src/metabase/dashboard/actions/data-fetching-typed.ts
new file mode 100644
index 00000000000..fd6f12a9926
--- /dev/null
+++ b/frontend/src/metabase/dashboard/actions/data-fetching-typed.ts
@@ -0,0 +1,190 @@
+import { denormalize, normalize, schema } from "normalizr";
+
+import { loadMetadataForDashboard } from "metabase/dashboard/actions/metadata";
+import {
+  getDashboardById,
+  getDashCardById,
+  getParameterValues,
+  getQuestions,
+  getSelectedTabId,
+} from "metabase/dashboard/selectors";
+import {
+  expandInlineDashboard,
+  getDashboardType,
+} from "metabase/dashboard/utils";
+import type { Deferred } from "metabase/lib/promise";
+import { defer } from "metabase/lib/promise";
+import { createAsyncThunk } from "metabase/lib/redux";
+import { getDashboardUiParameters } from "metabase/parameters/utils/dashboards";
+import { getParameterValuesByIdFromQueryParams } from "metabase/parameters/utils/parameter-values";
+import { addFields, addParamValues } from "metabase/redux/metadata";
+import { getMetadata } from "metabase/selectors/metadata";
+import { AutoApi, DashboardApi, EmbedApi, PublicApi } from "metabase/services";
+import type { DashboardCard } from "metabase-types/api";
+
+// normalizr schemas
+const dashcard = new schema.Entity("dashcard");
+const dashboard = new schema.Entity("dashboard", {
+  dashcards: [dashcard],
+});
+
+let fetchDashboardCancellation: Deferred | null;
+
+export const fetchDashboard = createAsyncThunk(
+  "metabase/dashboard/FETCH_DASHBOARD",
+  async (
+    {
+      dashId,
+      queryParams,
+      options: { preserveParameters = false, clearCache = true } = {},
+    }: {
+      dashId: string;
+      queryParams: Record<string, any>;
+      options?: { preserveParameters?: boolean; clearCache?: boolean };
+    },
+    { getState, dispatch, rejectWithValue },
+  ) => {
+    if (fetchDashboardCancellation) {
+      fetchDashboardCancellation.resolve();
+    }
+    fetchDashboardCancellation = defer();
+
+    try {
+      let entities;
+      let result;
+
+      const dashboardType = getDashboardType(dashId);
+      const loadedDashboard = getDashboardById(getState(), dashId);
+
+      if (!clearCache && loadedDashboard) {
+        entities = {
+          dashboard: { [dashId]: loadedDashboard },
+          dashcard: Object.fromEntries(
+            loadedDashboard.dashcards.map(id => [
+              id,
+              getDashCardById(getState(), id),
+            ]),
+          ),
+        };
+        result = denormalize(dashId, dashboard, entities);
+      } else if (dashboardType === "public") {
+        result = await PublicApi.dashboard(
+          { uuid: dashId },
+          { cancelled: fetchDashboardCancellation.promise },
+        );
+        result = {
+          ...result,
+          id: dashId,
+          dashcards: result.dashcards.map((dc: DashboardCard) => ({
+            ...dc,
+            dashboard_id: dashId,
+          })),
+        };
+      } else if (dashboardType === "embed") {
+        result = await EmbedApi.dashboard(
+          { token: dashId },
+          { cancelled: fetchDashboardCancellation.promise },
+        );
+        result = {
+          ...result,
+          id: dashId,
+          dashcards: result.dashcards.map((dc: DashboardCard) => ({
+            ...dc,
+            dashboard_id: dashId,
+          })),
+        };
+      } else if (dashboardType === "transient") {
+        const subPath = dashId.split("/").slice(3).join("/");
+        result = await AutoApi.dashboard(
+          { subPath },
+          { cancelled: fetchDashboardCancellation.promise },
+        );
+        result = {
+          ...result,
+          id: dashId,
+          dashcards: result.dashcards.map((dc: DashboardCard) => ({
+            ...dc,
+            dashboard_id: dashId,
+          })),
+        };
+      } else if (dashboardType === "inline") {
+        // HACK: this is horrible but the easiest way to get "inline" dashboards up and running
+        // pass the dashboard in as dashboardId, and replace the id with [object Object] because
+        // that's what it will be when cast to a string
+        // Adding ESLint ignore because this is a hack and we should fix it.
+        // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+        // @ts-expect-error
+        result = expandInlineDashboard(dashId);
+        dashId = result.id = String(dashId);
+      } else {
+        result = await DashboardApi.get(
+          { dashId: dashId },
+          { cancelled: fetchDashboardCancellation.promise },
+        );
+      }
+
+      fetchDashboardCancellation = null;
+
+      if (dashboardType === "normal" || dashboardType === "transient") {
+        const selectedTabId = getSelectedTabId(getState());
+
+        const cards =
+          selectedTabId === undefined
+            ? result.dashcards
+            : result.dashcards.filter(
+                (c: DashboardCard) => c.dashboard_tab_id === selectedTabId,
+              );
+
+        await dispatch(loadMetadataForDashboard(cards));
+      }
+
+      const isUsingCachedResults = entities != null;
+      if (!isUsingCachedResults) {
+        // copy over any virtual cards from the dashcard to the underlying card/question
+        result.dashcards.forEach((card: DashboardCard) => {
+          if (card.visualization_settings?.virtual_card) {
+            card.card = Object.assign(
+              card.card || {},
+              card.visualization_settings.virtual_card,
+            );
+          }
+        });
+      }
+
+      if (result.param_values) {
+        dispatch(addParamValues(result.param_values));
+      }
+      if (result.param_fields) {
+        dispatch(addFields(result.param_fields));
+      }
+
+      const metadata = getMetadata(getState());
+      const questions = getQuestions(getState());
+      const parameters = getDashboardUiParameters(
+        result.dashcards,
+        result.parameters,
+        metadata,
+        questions,
+      );
+
+      const parameterValuesById = preserveParameters
+        ? getParameterValues(getState())
+        : getParameterValuesByIdFromQueryParams(parameters, queryParams);
+
+      entities = entities ?? normalize(result, dashboard).entities;
+
+      return {
+        entities,
+        dashboard: result,
+        dashboardId: result.id,
+        parameterValues: parameterValuesById,
+        preserveParameters,
+      };
+    } catch (error) {
+      if (!(error as { isCancelled: boolean }).isCancelled) {
+        console.error(error);
+      }
+      return rejectWithValue(error);
+    }
+  },
+);
diff --git a/frontend/src/metabase/dashboard/actions/data-fetching.js b/frontend/src/metabase/dashboard/actions/data-fetching.js
index 04679e03f48..0615a1ed3b3 100644
--- a/frontend/src/metabase/dashboard/actions/data-fetching.js
+++ b/frontend/src/metabase/dashboard/actions/data-fetching.js
@@ -1,25 +1,15 @@
 import { getIn } from "icepick";
-import { denormalize, normalize, schema } from "normalizr";
 import { t } from "ttag";
 
 import { showAutoApplyFiltersToast } from "metabase/dashboard/actions/parameters";
 import { defer } from "metabase/lib/promise";
-import {
-  createAction,
-  createAsyncThunk,
-  createThunkAction,
-} from "metabase/lib/redux";
+import { createAction, createThunkAction } from "metabase/lib/redux";
 import { equals } from "metabase/lib/utils";
-import { getDashboardUiParameters } from "metabase/parameters/utils/dashboards";
-import { getParameterValuesByIdFromQueryParams } from "metabase/parameters/utils/parameter-values";
-import { addParamValues, addFields } from "metabase/redux/metadata";
-import { getMetadata } from "metabase/selectors/metadata";
 import {
   DashboardApi,
   CardApi,
   PublicApi,
   EmbedApi,
-  AutoApi,
   MetabaseApi,
   maybeUsePivotEndpoint,
 } from "metabase/services";
@@ -30,16 +20,12 @@ import { DASHBOARD_SLOW_TIMEOUT } from "../constants";
 import {
   getDashboardComplete,
   getDashCardBeforeEditing,
-  getParameterValues,
   getLoadingDashCards,
   getCanShowAutoApplyFiltersToast,
-  getDashboardById,
   getDashCardById,
   getSelectedTabId,
-  getQuestions,
 } from "../selectors";
 import {
-  expandInlineDashboard,
   isVirtualDashCard,
   getAllDashboardCards,
   getDashboardType,
@@ -49,12 +35,6 @@ import {
 
 import { loadMetadataForDashboard } from "./metadata";
 
-// normalizr schemas
-const dashcard = new schema.Entity("dashcard");
-const dashboard = new schema.Entity("dashboard", {
-  dashcards: [dashcard],
-});
-
 export const FETCH_DASHBOARD_CARD_DATA =
   "metabase/dashboard/FETCH_DASHBOARD_CARD_DATA";
 export const CANCEL_FETCH_DASHBOARD_CARD_DATA =
@@ -136,160 +116,6 @@ const loadingComplete = createThunkAction(
   },
 );
 
-let fetchDashboardCancellation;
-
-export const fetchDashboard = createAsyncThunk(
-  "metabase/dashboard/FETCH_DASHBOARD",
-  async (
-    {
-      dashId,
-      queryParams,
-      options: { preserveParameters = false, clearCache = true } = {},
-    },
-    { getState, dispatch, rejectWithValue },
-  ) => {
-    if (fetchDashboardCancellation) {
-      fetchDashboardCancellation.resolve();
-    }
-    fetchDashboardCancellation = defer();
-
-    try {
-      let entities;
-      let result;
-
-      const dashboardType = getDashboardType(dashId);
-      const loadedDashboard = getDashboardById(getState(), dashId);
-
-      if (!clearCache && loadedDashboard) {
-        entities = {
-          dashboard: { [dashId]: loadedDashboard },
-          dashcard: Object.fromEntries(
-            loadedDashboard.dashcards.map(id => [
-              id,
-              getDashCardById(getState(), id),
-            ]),
-          ),
-        };
-        result = denormalize(dashId, dashboard, entities);
-      } else if (dashboardType === "public") {
-        result = await PublicApi.dashboard(
-          { uuid: dashId },
-          { cancelled: fetchDashboardCancellation.promise },
-        );
-        result = {
-          ...result,
-          id: dashId,
-          dashcards: result.dashcards.map(dc => ({
-            ...dc,
-            dashboard_id: dashId,
-          })),
-        };
-      } else if (dashboardType === "embed") {
-        result = await EmbedApi.dashboard(
-          { token: dashId },
-          { cancelled: fetchDashboardCancellation.promise },
-        );
-        result = {
-          ...result,
-          id: dashId,
-          dashcards: result.dashcards.map(dc => ({
-            ...dc,
-            dashboard_id: dashId,
-          })),
-        };
-      } else if (dashboardType === "transient") {
-        const subPath = dashId.split("/").slice(3).join("/");
-        result = await AutoApi.dashboard(
-          { subPath },
-          { cancelled: fetchDashboardCancellation.promise },
-        );
-        result = {
-          ...result,
-          id: dashId,
-          dashcards: result.dashcards.map(dc => ({
-            ...dc,
-            dashboard_id: dashId,
-          })),
-        };
-      } else if (dashboardType === "inline") {
-        // HACK: this is horrible but the easiest way to get "inline" dashboards up and running
-        // pass the dashboard in as dashboardId, and replace the id with [object Object] because
-        // that's what it will be when cast to a string
-        result = expandInlineDashboard(dashId);
-        dashId = result.id = String(dashId);
-      } else {
-        result = await DashboardApi.get(
-          { dashId: dashId },
-          { cancelled: fetchDashboardCancellation.promise },
-        );
-      }
-
-      fetchDashboardCancellation = null;
-
-      if (dashboardType === "normal" || dashboardType === "transient") {
-        const selectedTabId = getSelectedTabId(getState());
-
-        const cards =
-          selectedTabId === undefined
-            ? result.dashcards
-            : result.dashcards.filter(
-                c => c.dashboard_tab_id === selectedTabId,
-              );
-
-        await dispatch(loadMetadataForDashboard(cards));
-      }
-
-      const isUsingCachedResults = entities != null;
-      if (!isUsingCachedResults) {
-        // copy over any virtual cards from the dashcard to the underlying card/question
-        result.dashcards.forEach(card => {
-          if (card.visualization_settings.virtual_card) {
-            card.card = Object.assign(
-              card.card || {},
-              card.visualization_settings.virtual_card,
-            );
-          }
-        });
-      }
-
-      if (result.param_values) {
-        dispatch(addParamValues(result.param_values));
-      }
-      if (result.param_fields) {
-        dispatch(addFields(result.param_fields));
-      }
-
-      const metadata = getMetadata(getState());
-      const questions = getQuestions(getState());
-      const parameters = getDashboardUiParameters(
-        result.dashcards,
-        result.parameters,
-        metadata,
-        questions,
-      );
-
-      const parameterValuesById = preserveParameters
-        ? getParameterValues(getState())
-        : getParameterValuesByIdFromQueryParams(parameters, queryParams);
-
-      entities = entities ?? normalize(result, dashboard).entities;
-
-      return {
-        entities,
-        dashboard: result,
-        dashboardId: result.id,
-        parameterValues: parameterValuesById,
-        preserveParameters,
-      };
-    } catch (error) {
-      if (!error.isCancelled) {
-        console.error(error);
-      }
-      return rejectWithValue(error);
-    }
-  },
-);
-
 export const fetchCardData = createThunkAction(
   FETCH_CARD_DATA,
   function (card, dashcard, { reload, clearCache, ignoreCache } = {}) {
@@ -480,7 +306,7 @@ export const fetchCardData = createThunkAction(
 );
 
 export const fetchDashboardCardData =
-  ({ isRefreshing, ...options } = {}) =>
+  ({ isRefreshing, reload = false, clearCache = false } = {}) =>
   (dispatch, getState) => {
     const dashboard = getDashboardComplete(getState());
     const selectedTabId = getSelectedTabId(getState());
@@ -530,7 +356,9 @@ export const fetchDashboardCardData =
     }
 
     const promises = nonVirtualDashcardsToFetch.map(({ card, dashcard }) => {
-      return dispatch(fetchCardData(card, dashcard, options)).then(() => {
+      return dispatch(
+        fetchCardData(card, dashcard, { reload, clearCache }),
+      ).then(() => {
         return dispatch(updateLoadingTitle(nonVirtualDashcardsToFetch.length));
       });
     });
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 ec07b685f59..c93db8ceb2a 100644
--- a/frontend/src/metabase/dashboard/actions/data-fetching.unit.spec.js
+++ b/frontend/src/metabase/dashboard/actions/data-fetching.unit.spec.js
@@ -11,7 +11,7 @@ import { createMockDashboardState } from "metabase-types/store/mocks";
 
 import { dashboardReducers } from "../reducers";
 
-import { fetchDashboard } from "./data-fetching";
+import { fetchDashboard } from "./data-fetching-typed";
 
 describe("fetchDashboard", () => {
   let store;
diff --git a/frontend/src/metabase/dashboard/actions/index.ts b/frontend/src/metabase/dashboard/actions/index.ts
index 5e3cb65e1c8..92c1894430a 100644
--- a/frontend/src/metabase/dashboard/actions/index.ts
+++ b/frontend/src/metabase/dashboard/actions/index.ts
@@ -2,6 +2,7 @@ export * from "./cards-typed";
 export * from "./cards";
 export * from "./core";
 export * from "./data-fetching";
+export * from "./data-fetching-typed";
 export * from "./navigation";
 export * from "./parameters";
 export * from "./revisions";
diff --git a/frontend/src/metabase/dashboard/actions/revisions.js b/frontend/src/metabase/dashboard/actions/revisions.js
index 09934b33c5d..edcb5b0d7ef 100644
--- a/frontend/src/metabase/dashboard/actions/revisions.js
+++ b/frontend/src/metabase/dashboard/actions/revisions.js
@@ -1,8 +1,10 @@
+import {
+  fetchDashboard,
+  fetchDashboardCardData,
+} from "metabase/dashboard/actions";
 import Revision from "metabase/entities/revisions";
 import { createThunkAction } from "metabase/lib/redux";
 
-import { fetchDashboard, fetchDashboardCardData } from "./data-fetching";
-
 export const REVERT_TO_REVISION = "metabase/dashboard/REVERT_TO_REVISION";
 export const revertToRevision = createThunkAction(
   REVERT_TO_REVISION,
diff --git a/frontend/src/metabase/dashboard/actions/save.js b/frontend/src/metabase/dashboard/actions/save.js
index 8b06ed46258..75b804a1e1d 100644
--- a/frontend/src/metabase/dashboard/actions/save.js
+++ b/frontend/src/metabase/dashboard/actions/save.js
@@ -1,6 +1,10 @@
 import { assocIn, dissocIn, getIn } from "icepick";
 import _ from "underscore";
 
+import {
+  fetchDashboard,
+  fetchDashboardCardData,
+} from "metabase/dashboard/actions";
 import Dashboards from "metabase/entities/dashboards";
 import { createThunkAction } from "metabase/lib/redux";
 import { CardApi } from "metabase/services";
@@ -10,7 +14,6 @@ import { trackDashboardSaved } from "../analytics";
 import { getDashboardBeforeEditing } from "../selectors";
 
 import { setEditingDashboard } from "./core";
-import { fetchDashboard, fetchDashboardCardData } from "./data-fetching";
 import { hasDashboardChanged, haveDashboardCardsChanged } from "./utils";
 
 export const UPDATE_DASHBOARD_AND_CARDS =
diff --git a/frontend/src/metabase/dashboard/actions/ui.ts b/frontend/src/metabase/dashboard/actions/ui.ts
index 92d4ef05bd6..feb5b92d21a 100644
--- a/frontend/src/metabase/dashboard/actions/ui.ts
+++ b/frontend/src/metabase/dashboard/actions/ui.ts
@@ -18,7 +18,7 @@ export const CLOSE_SIDEBAR = "metabase/dashboard/CLOSE_SIDEBAR";
 export const closeSidebar = createAction(CLOSE_SIDEBAR);
 
 export const showClickBehaviorSidebar =
-  (dashcardId: DashCardId) => (dispatch: Dispatch) => {
+  (dashcardId: DashCardId | null) => (dispatch: Dispatch) => {
     if (dashcardId != null) {
       dispatch(
         setSidebar({
diff --git a/frontend/src/metabase/dashboard/components/Dashboard/Dashboard.tsx b/frontend/src/metabase/dashboard/components/Dashboard/Dashboard.tsx
index 8f23cdceee8..3ace89e5480 100644
--- a/frontend/src/metabase/dashboard/components/Dashboard/Dashboard.tsx
+++ b/frontend/src/metabase/dashboard/components/Dashboard/Dashboard.tsx
@@ -11,6 +11,7 @@ import type {
 } from "metabase/dashboard/actions";
 import { DashboardHeader } from "metabase/dashboard/components/DashboardHeader";
 import { DashboardControls } from "metabase/dashboard/hoc/DashboardControls";
+import type { DashboardControlsPassedProps } from "metabase/dashboard/hoc/types";
 import type {
   FetchDashboardResult,
   SuccessfulFetchDashboardResult,
@@ -41,6 +42,7 @@ import type {
   ValuesQueryType,
   ValuesSourceType,
   ValuesSourceConfig,
+  DashboardCard,
 } from "metabase-types/api";
 import type {
   DashboardSidebarName,
@@ -69,8 +71,7 @@ import {
   DashboardEmptyStateWithoutAddPrompt,
 } from "./DashboardEmptyState/DashboardEmptyState";
 
-interface DashboardProps {
-  dashboardId: DashboardId;
+type DashboardProps = {
   route: Route;
   params: { slug: string };
   children?: ReactNode;
@@ -106,28 +107,10 @@ interface DashboardProps {
   isNavigatingBackToDashboard: boolean;
   addCardOnLoad?: DashCardId;
   editingOnLoad?: string | string[];
-  location: Location;
-  isNightMode: boolean;
-  isFullscreen: boolean;
-  hasNightModeToggle: boolean;
-  refreshPeriod: number | null;
 
   initialize: (opts?: { clearCache?: boolean }) => void;
-  fetchDashboard: (opts: {
-    dashId: DashboardId;
-    queryParams?: Record<string, unknown>;
-    options?: {
-      clearCache?: boolean;
-      preserveParameters?: boolean;
-    };
-  }) => Promise<FetchDashboardResult>;
-  fetchDashboardCardData: (opts?: {
-    reload?: boolean;
-    clearCache?: boolean;
-  }) => Promise<void>;
   fetchDashboardCardMetadata: () => Promise<void>;
   cancelFetchDashboardCardData: () => void;
-  loadDashboardParams: () => void;
   addCardToDashboard: (opts: {
     dashId: DashboardId;
     cardId: CardId;
@@ -138,7 +121,6 @@ interface DashboardProps {
   addLinkDashCardToDashboard: (opts: NewDashCardOpts) => void;
   archiveDashboard: (id: DashboardId) => Promise<void>;
 
-  onRefreshPeriodChange: (period: number | null) => void;
   setEditingDashboard: (dashboard: IDashboard | null) => void;
   setDashboardAttributes: (opts: SetDashboardAttributesOpts) => void;
   setSharing: (isSharing: boolean) => void;
@@ -189,15 +171,10 @@ interface DashboardProps {
     slug: string,
   ) => EmbeddingParameterVisibility | null;
   updateDashboardAndCards: () => void;
-  onFullscreenChange: (
-    isFullscreen: boolean,
-    browserFullscreen?: boolean,
-  ) => void;
 
-  onNightModeChange: () => void;
   setSidebar: (opts: { name: DashboardSidebarName }) => void;
   hideAddParameterPopover: () => void;
-}
+} & DashboardControlsPassedProps;
 
 function DashboardInner(props: DashboardProps) {
   const {
@@ -255,7 +232,7 @@ function DashboardInner(props: DashboardProps) {
       return dashboard.dashcards;
     }
     return dashboard.dashcards.filter(
-      dc => dc.dashboard_tab_id === selectedTabId,
+      (dc: DashboardCard) => dc.dashboard_tab_id === selectedTabId,
     );
   }, [dashboard, selectedTabId]);
 
@@ -266,7 +243,8 @@ function DashboardInner(props: DashboardProps) {
     }
 
     const currentTabParameterIds = currentTabDashcards.flatMap(
-      dc => dc.parameter_mappings?.map(pm => pm.parameter_id) ?? [],
+      (dc: DashboardCard) =>
+        dc.parameter_mappings?.map(pm => pm.parameter_id) ?? [],
     );
     const hiddenParameters = parameters.filter(
       parameter => !currentTabParameterIds.includes(parameter.id),
@@ -452,6 +430,10 @@ function DashboardInner(props: DashboardProps) {
       );
     }
     return (
+      // TODO: We should make these props explicit, keeping in mind the DashboardControls inject props as well.
+      // TODO: Check if onEditingChange is being used.
+      // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+      // @ts-expect-error
       <DashboardGridConnected
         {...props}
         isNightMode={shouldRenderAsNightMode}
diff --git a/frontend/src/metabase/dashboard/components/DashboardActions.jsx b/frontend/src/metabase/dashboard/components/DashboardActions.tsx
similarity index 68%
rename from frontend/src/metabase/dashboard/components/DashboardActions.jsx
rename to frontend/src/metabase/dashboard/components/DashboardActions.tsx
index 300ab357445..1b523071663 100644
--- a/frontend/src/metabase/dashboard/components/DashboardActions.jsx
+++ b/frontend/src/metabase/dashboard/components/DashboardActions.tsx
@@ -3,6 +3,7 @@ import { t } from "ttag";
 import Tooltip from "metabase/core/components/Tooltip";
 import { DashboardEmbedAction } from "metabase/dashboard/components/DashboardEmbedAction/DashboardEmbedAction";
 import { DashboardHeaderButton } from "metabase/dashboard/components/DashboardHeader/DashboardHeader.styled";
+import type { Dashboard, DashboardCard } from "metabase-types/api";
 
 import {
   FullScreenButtonIcon,
@@ -10,25 +11,46 @@ import {
   RefreshWidgetButton,
 } from "./DashboardActions.styled";
 
-export const getDashboardActions = props => {
-  const {
-    dashboard,
-    isAdmin,
-    canManageSubscriptions,
-    formInput,
-    isEditing = false,
-    isEmpty = false,
-    isFullscreen,
-    isNightMode,
-    isPublic = false,
-    onNightModeChange,
-    refreshPeriod,
-    setRefreshElapsedHook,
-    onRefreshPeriodChange,
-    onSharingClick,
-    onFullscreenChange,
-    hasNightModeToggle,
-  } = props;
+type GetDashboardActionsProps = {
+  canManageSubscriptions?: boolean;
+  dashboard: Dashboard | null;
+  formInput?: any;
+  hasNightModeToggle: boolean;
+  isAdmin?: boolean;
+  isEditing?: boolean;
+  isEmpty?: boolean;
+  isFullscreen: boolean;
+  isNightMode: boolean;
+  isPublic?: boolean;
+  onFullscreenChange: (
+    isFullscreen: boolean,
+    isBrowserFullscreen?: boolean,
+  ) => void;
+  onNightModeChange: (isNightMode: boolean) => void;
+  onRefreshPeriodChange: (period: number) => void;
+  onSharingClick?: () => void;
+  refreshPeriod: number | null;
+  setRefreshElapsedHook?: (hook: (elapsed: number) => void) => void;
+};
+
+export const getDashboardActions = ({
+  canManageSubscriptions = false,
+  dashboard,
+  formInput,
+  hasNightModeToggle,
+  isAdmin,
+  isEditing = false,
+  isEmpty = false,
+  isFullscreen,
+  isNightMode,
+  isPublic = false,
+  onFullscreenChange,
+  onNightModeChange,
+  onRefreshPeriodChange,
+  onSharingClick,
+  refreshPeriod,
+  setRefreshElapsedHook,
+}: GetDashboardActionsProps) => {
   const buttons = [];
 
   const isLoaded = !!dashboard;
@@ -38,7 +60,8 @@ export const getDashboardActions = props => {
   const hasDataCards =
     hasCards &&
     dashboard.dashcards.some(
-      dashCard => !["text", "heading"].includes(dashCard.card.display),
+      (dashCard: DashboardCard) =>
+        !["text", "heading"].includes(dashCard.card.display),
     );
 
   const canCreateSubscription = hasDataCards && canManageSubscriptions;
@@ -68,12 +91,14 @@ export const getDashboardActions = props => {
       );
     }
 
-    buttons.push(
-      <DashboardEmbedAction
-        key="dashboard-embed-action"
-        dashboard={dashboard}
-      />,
-    );
+    if (isLoaded) {
+      buttons.push(
+        <DashboardEmbedAction
+          key="dashboard-embed-action"
+          dashboard={dashboard}
+        />,
+      );
+    }
   }
 
   if (!isEditing && !isEmpty) {
diff --git a/frontend/src/metabase/dashboard/components/DashboardGrid.tsx b/frontend/src/metabase/dashboard/components/DashboardGrid.tsx
index 456f3a78c5c..944899c5f05 100644
--- a/frontend/src/metabase/dashboard/components/DashboardGrid.tsx
+++ b/frontend/src/metabase/dashboard/components/DashboardGrid.tsx
@@ -1,5 +1,6 @@
 import cx from "classnames";
 import type { LocationDescriptor } from "history";
+import type { ComponentType } from "react";
 import { Component } from "react";
 import type { ConnectedProps } from "react-redux";
 import { connect } from "react-redux";
@@ -53,6 +54,7 @@ import type {
   DashboardCard,
 } from "metabase-types/api";
 
+import type { SetDashCardAttributesOpts } from "../actions";
 import { removeCardFromDashboard } from "../actions";
 
 import { AddSeriesModal } from "./AddSeriesModal/AddSeriesModal";
@@ -78,12 +80,22 @@ type LayoutItem = {
   dashcard: BaseDashboardCard;
 };
 
-type DashboardChangeItem = {
-  id: DashCardId;
-  attributes: Partial<BaseDashboardCard>;
-};
+interface DashboardGridState {
+  visibleCardIds: Set<number>;
+  initialCardSizes: { [key: string]: { w: number; h: number } };
+  layouts: { desktop: LayoutItem[]; mobile: LayoutItem[] };
+  addSeriesModalDashCard: BaseDashboardCard | null;
+  replaceCardModalDashCard: BaseDashboardCard | null;
+  isDragging: boolean;
+  isAnimationPaused: boolean;
+}
+
+const mapDispatchToProps = { addUndo, removeCardFromDashboard };
+const connector = connect(null, mapDispatchToProps);
+
+type DashboardGridReduxProps = ConnectedProps<typeof connector>;
 
-type DashboardGridProps = ConnectedProps<typeof connector> & {
+type OwnProps = {
   dashboard: Dashboard;
   dashcardData: DashCardDataMap;
   selectedTabId: DashboardTabId;
@@ -91,7 +103,7 @@ type DashboardGridProps = ConnectedProps<typeof connector> & {
   slowCards: Record<CardId, boolean>;
   isEditing: boolean;
   isEditingParameter: boolean;
-  isPublic: boolean;
+  isPublic?: boolean;
   isXray: boolean;
   isFullscreen: boolean;
   isNightMode: boolean;
@@ -115,9 +127,9 @@ type DashboardGridProps = ConnectedProps<typeof connector> & {
   }) => void;
   markNewCardSeen: (dashcardId: DashCardId) => void;
 
-  setDashCardAttributes: (options: DashboardChangeItem) => void;
+  setDashCardAttributes: (options: SetDashCardAttributesOpts) => void;
   setMultipleDashCardAttributes: (changes: {
-    dashcards: Array<DashboardChangeItem>;
+    dashcards: Array<SetDashCardAttributesOpts>;
   }) => void;
 
   undoRemoveCardFromDashboard: (options: { dashcardId: DashCardId }) => void;
@@ -136,24 +148,10 @@ type DashboardGridProps = ConnectedProps<typeof connector> & {
 
   showClickBehaviorSidebar: (dashcardId: DashCardId | null) => void;
 
-  addUndo: (options: {
-    message: string;
-    undo: boolean;
-    action: () => void;
-  }) => void;
+  onEditingChange?: (dashboard: Dashboard | null) => void;
 };
 
-interface DashboardGridState {
-  visibleCardIds: Set<number>;
-  initialCardSizes: { [key: string]: { w: number; h: number } };
-  layouts: { desktop: LayoutItem[]; mobile: LayoutItem[] };
-  addSeriesModalDashCard: BaseDashboardCard | null;
-  replaceCardModalDashCard: BaseDashboardCard | null;
-  isDragging: boolean;
-  isAnimationPaused: boolean;
-}
-
-const mapDispatchToProps = { addUndo, removeCardFromDashboard };
+type DashboardGridProps = OwnProps & DashboardGridReduxProps;
 
 class DashboardGrid extends Component<DashboardGridProps, DashboardGridState> {
   static contextType = ContentViewportContext;
@@ -258,7 +256,7 @@ class DashboardGrid extends Component<DashboardGridProps, DashboardGridState> {
       return;
     }
 
-    const changes: DashboardChangeItem[] = [];
+    const changes: SetDashCardAttributesOpts[] = [];
 
     layout.forEach(layoutItem => {
       const dashboardCard = this.getVisibleCards().find(
@@ -539,17 +537,15 @@ class DashboardGrid extends Component<DashboardGridProps, DashboardGridState> {
         isMobile={isMobile}
         isPublic={this.props.isPublic}
         isXray={this.props.isXray}
-        onRemove={this.onDashCardRemove.bind(this, dc)}
-        onAddSeries={this.onDashCardAddSeries.bind(this, dc)}
+        onRemove={() => this.onDashCardRemove(dc)}
+        onAddSeries={() => this.onDashCardAddSeries(dc)}
         onReplaceCard={() => this.onReplaceCard(dc)}
-        onUpdateVisualizationSettings={this.props.onUpdateDashCardVisualizationSettings.bind(
-          this,
-          dc.id,
-        )}
-        onReplaceAllVisualizationSettings={this.props.onReplaceAllDashCardVisualizationSettings.bind(
-          this,
-          dc.id,
-        )}
+        onUpdateVisualizationSettings={settings =>
+          this.props.onUpdateDashCardVisualizationSettings(dc.id, settings)
+        }
+        onReplaceAllVisualizationSettings={settings =>
+          this.props.onReplaceAllDashCardVisualizationSettings(dc.id, settings)
+        }
         mode={this.props.mode}
         navigateToNewCardFromDashboard={
           this.props.navigateToNewCardFromDashboard
@@ -673,9 +669,7 @@ const getUndoReplaceCardMessage = ({ type }: Card) => {
   throw new Error(`Unknown card.type: ${type}`);
 };
 
-const connector = connect(null, mapDispatchToProps);
-
 export const DashboardGridConnected = _.compose(
   ExplicitSize(),
   connector,
-)(DashboardGrid);
+)(DashboardGrid) as ComponentType<OwnProps>;
diff --git a/frontend/src/metabase/dashboard/components/DashboardHeader/DashboardHeader.tsx b/frontend/src/metabase/dashboard/components/DashboardHeader/DashboardHeader.tsx
index be415e998c0..c0972552706 100644
--- a/frontend/src/metabase/dashboard/components/DashboardHeader/DashboardHeader.tsx
+++ b/frontend/src/metabase/dashboard/components/DashboardHeader/DashboardHeader.tsx
@@ -130,7 +130,7 @@ interface DashboardHeaderProps {
     browserFullscreen?: boolean,
   ) => void;
   onSharingClick: () => void;
-  onNightModeChange: () => void;
+  onNightModeChange: (isNightMode: boolean) => void;
 
   setSidebar: (opts: { name: DashboardSidebarName }) => void;
   closeSidebar: () => void;
diff --git a/frontend/src/metabase/dashboard/hoc/types.ts b/frontend/src/metabase/dashboard/hoc/types.ts
new file mode 100644
index 00000000000..88ada986c6c
--- /dev/null
+++ b/frontend/src/metabase/dashboard/hoc/types.ts
@@ -0,0 +1,46 @@
+import type { Location } from "history";
+import type { LocationAction } from "react-router-redux";
+
+import type { EmbeddingDisplayOptions } from "metabase/public/lib/types";
+import type { DashboardId } from "metabase-types/api";
+
+import type { FetchDashboardResult } from "../types";
+
+// passed via ...this.props
+export type DashboardControlsProps = {
+  location: Location;
+  fetchDashboard: (opts: {
+    dashId: DashboardId;
+    queryParams?: Record<string, unknown>;
+    options?: {
+      clearCache?: boolean;
+      preserveParameters?: boolean;
+    };
+  }) => Promise<FetchDashboardResult>;
+  dashboardId: DashboardId;
+  fetchDashboardCardData: (opts?: {
+    reload?: boolean;
+    clearCache?: boolean;
+  }) => Promise<void>;
+};
+
+// Passed via ...this.state
+export type DashboardControlsState = {
+  isFullscreen: boolean;
+  refreshPeriod: number | null;
+  theme: EmbeddingDisplayOptions["theme"];
+  hideParameters: string;
+};
+
+export type DashboardControlsPassedProps = {
+  // Passed via explicit props
+  replace: LocationAction;
+  isNightMode: boolean;
+  hasNightModeToggle: boolean;
+  setRefreshElapsedHook: (hook: (elapsed: number) => void) => void;
+  loadDashboardParams: () => void;
+  onNightModeChange: (isNightMode: boolean) => void;
+  onFullscreenChange: (isFullscreen: boolean) => void;
+  onRefreshPeriodChange: (refreshPeriod: number | null) => void;
+} & DashboardControlsProps &
+  DashboardControlsState;
diff --git a/frontend/src/metabase/dashboard/types.ts b/frontend/src/metabase/dashboard/types.ts
index 2f9faddb866..58ab6026c09 100644
--- a/frontend/src/metabase/dashboard/types.ts
+++ b/frontend/src/metabase/dashboard/types.ts
@@ -3,7 +3,7 @@ import type { Dashboard } from "metabase-types/api";
 export type SuccessfulFetchDashboardResult = {
   payload: { dashboard: Dashboard };
 };
-type FailedFetchDashboardResult = { error: unknown; payload: unknown };
+export type FailedFetchDashboardResult = { error: unknown; payload: unknown };
 
 export type FetchDashboardResult =
   | SuccessfulFetchDashboardResult
diff --git a/frontend/src/metabase/parameters/utils/parameter-values.ts b/frontend/src/metabase/parameters/utils/parameter-values.ts
index 06a18b1b264..72292618696 100644
--- a/frontend/src/metabase/parameters/utils/parameter-values.ts
+++ b/frontend/src/metabase/parameters/utils/parameter-values.ts
@@ -9,7 +9,7 @@ import type {
 
 export function getParameterValueFromQueryParams(
   parameter: Parameter,
-  queryParams: Record<string, string | string[] | undefined>,
+  queryParams: Record<string, string | string[] | null | undefined>,
 ) {
   queryParams = queryParams || {};
 
@@ -99,7 +99,7 @@ function normalizeParameterValueForWidget(
 
 export function getParameterValuesByIdFromQueryParams(
   parameters: Parameter[],
-  queryParams: Record<string, string | string[] | undefined>,
+  queryParams: Record<string, string | string[] | null | undefined>,
 ) {
   return Object.fromEntries(
     parameters.map(parameter => [
diff --git a/frontend/src/metabase/public/containers/PublicDashboard/PublicDashboard.jsx b/frontend/src/metabase/public/containers/PublicDashboard/PublicDashboard.jsx
deleted file mode 100644
index 87a103b3c36..00000000000
--- a/frontend/src/metabase/public/containers/PublicDashboard/PublicDashboard.jsx
+++ /dev/null
@@ -1,223 +0,0 @@
-/* eslint-disable react/prop-types */
-import cx from "classnames";
-import { assoc } from "icepick";
-import { Component } from "react";
-import { connect } from "react-redux";
-import { push } from "react-router-redux";
-import _ from "underscore";
-
-import LoadingAndErrorWrapper from "metabase/components/LoadingAndErrorWrapper";
-import ColorS from "metabase/css/core/colors.module.css";
-import CS from "metabase/css/core/index.css";
-import DashboardS from "metabase/css/dashboard.module.css";
-import * as dashboardActions from "metabase/dashboard/actions";
-import { getDashboardActions } from "metabase/dashboard/components/DashboardActions";
-import { DashboardGridConnected } from "metabase/dashboard/components/DashboardGrid";
-import { DashboardTabs } from "metabase/dashboard/components/DashboardTabs";
-import { DashboardControls } from "metabase/dashboard/hoc/DashboardControls";
-import {
-  getDashboardComplete,
-  getCardData,
-  getSlowCards,
-  getParameters,
-  getParameterValues,
-  getDraftParameterValues,
-  getSelectedTabId,
-} from "metabase/dashboard/selectors";
-import { isActionDashCard } from "metabase/dashboard/utils";
-import title from "metabase/hoc/Title";
-import { isWithinIframe } from "metabase/lib/dom";
-import ParametersS from "metabase/parameters/components/ParameterValueWidget.module.css";
-import { setErrorPage } from "metabase/redux/app";
-import { getMetadata } from "metabase/selectors/metadata";
-import {
-  setPublicDashboardEndpoints,
-  setEmbedDashboardEndpoints,
-} from "metabase/services";
-import { PublicMode } from "metabase/visualizations/click-actions/modes/PublicMode";
-
-import EmbedFrame from "../../components/EmbedFrame";
-
-import { DashboardContainer } from "./PublicDashboard.styled";
-
-const mapStateToProps = (state, props) => {
-  return {
-    metadata: getMetadata(state, props),
-    dashboardId:
-      props.params.dashboardId || props.params.uuid || props.params.token,
-    dashboard: getDashboardComplete(state, props),
-    dashcardData: getCardData(state, props),
-    slowCards: getSlowCards(state, props),
-    parameters: getParameters(state, props),
-    parameterValues: getParameterValues(state, props),
-    draftParameterValues: getDraftParameterValues(state, props),
-    selectedTabId: getSelectedTabId(state),
-  };
-};
-
-const mapDispatchToProps = {
-  ...dashboardActions,
-  setErrorPage,
-  onChangeLocation: push,
-};
-
-class PublicDashboardInner extends Component {
-  _initialize = async () => {
-    const {
-      initialize,
-      fetchDashboard,
-      fetchDashboardCardData,
-      setErrorPage,
-      location,
-      params: { uuid, token },
-    } = this.props;
-    if (uuid) {
-      setPublicDashboardEndpoints();
-    } else if (token) {
-      setEmbedDashboardEndpoints();
-    }
-
-    initialize();
-
-    const result = await fetchDashboard({
-      dashId: uuid || token,
-      queryParams: location.query,
-    });
-
-    if (result.error) {
-      setErrorPage(result.payload);
-      return;
-    }
-
-    try {
-      if (this.props.dashboard.tabs.length === 0) {
-        await fetchDashboardCardData({ reload: false, clearCache: true });
-      }
-    } catch (error) {
-      console.error(error);
-      setErrorPage(error);
-    }
-  };
-
-  async componentDidMount() {
-    this._initialize();
-  }
-
-  componentWillUnmount() {
-    this.props.cancelFetchDashboardCardData();
-  }
-
-  async componentDidUpdate(prevProps) {
-    if (this.props.dashboardId !== prevProps.dashboardId) {
-      return this._initialize();
-    }
-
-    if (!_.isEqual(prevProps.selectedTabId, this.props.selectedTabId)) {
-      this.props.fetchDashboardCardData();
-      this.props.fetchDashboardCardMetadata();
-      return;
-    }
-
-    if (!_.isEqual(this.props.parameterValues, prevProps.parameterValues)) {
-      this.props.fetchDashboardCardData({ reload: false, clearCache: true });
-    }
-  }
-
-  getCurrentTabDashcards = () => {
-    const { dashboard, selectedTabId } = this.props;
-    if (!Array.isArray(dashboard?.dashcards)) {
-      return [];
-    }
-    if (!selectedTabId) {
-      return dashboard.dashcards;
-    }
-    return dashboard.dashcards.filter(
-      dashcard => dashcard.dashboard_tab_id === selectedTabId,
-    );
-  };
-
-  getHiddenParameterSlugs = () => {
-    const { parameters } = this.props;
-    const currentTabParameterIds = this.getCurrentTabDashcards().flatMap(
-      dashcard =>
-        dashcard.parameter_mappings?.map(mapping => mapping.parameter_id) ?? [],
-    );
-    const hiddenParameters = parameters.filter(
-      parameter => !currentTabParameterIds.includes(parameter.id),
-    );
-    return hiddenParameters.map(parameter => parameter.slug).join(",");
-  };
-
-  render() {
-    const {
-      dashboard,
-      parameters,
-      parameterValues,
-      draftParameterValues,
-      isFullscreen,
-      isNightMode,
-      setParameterValueToDefault,
-    } = this.props;
-
-    const buttons = !isWithinIframe()
-      ? getDashboardActions({ ...this.props, isPublic: true })
-      : [];
-
-    const visibleDashcards = (dashboard?.dashcards ?? []).filter(
-      dashcard => !isActionDashCard(dashcard),
-    );
-
-    return (
-      <EmbedFrame
-        name={dashboard && dashboard.name}
-        description={dashboard && dashboard.description}
-        dashboard={dashboard}
-        parameters={parameters}
-        parameterValues={parameterValues}
-        draftParameterValues={draftParameterValues}
-        hiddenParameterSlugs={this.getHiddenParameterSlugs()}
-        setParameterValue={this.props.setParameterValue}
-        setParameterValueToDefault={setParameterValueToDefault}
-        enableParameterRequiredBehavior
-        actionButtons={
-          buttons.length > 0 && <div className={CS.flex}>{buttons}</div>
-        }
-        dashboardTabs={
-          dashboard?.tabs?.length > 1 && (
-            <DashboardTabs location={this.props.location} />
-          )
-        }
-      >
-        <LoadingAndErrorWrapper
-          className={cx({
-            [DashboardS.DashboardFullscreen]: isFullscreen,
-            [DashboardS.DashboardNight]: isNightMode,
-            [ParametersS.DashboardNight]: isNightMode,
-            [ColorS.DashboardNight]: isNightMode,
-          })}
-          loading={!dashboard}
-        >
-          {() => (
-            <DashboardContainer>
-              <DashboardGridConnected
-                {...this.props}
-                dashboard={assoc(dashboard, "dashcards", visibleDashcards)}
-                isPublic
-                className={CS.spread}
-                mode={PublicMode}
-                metadata={this.props.metadata}
-                navigateToNewCardFromDashboard={() => {}}
-              />
-            </DashboardContainer>
-          )}
-        </LoadingAndErrorWrapper>
-      </EmbedFrame>
-    );
-  }
-}
-
-export const PublicDashboard = _.compose(
-  connect(mapStateToProps, mapDispatchToProps),
-  title(({ dashboard }) => dashboard && dashboard.name),
-  DashboardControls,
-)(PublicDashboardInner);
diff --git a/frontend/src/metabase/public/containers/PublicDashboard/PublicDashboard.tsx b/frontend/src/metabase/public/containers/PublicDashboard/PublicDashboard.tsx
new file mode 100644
index 00000000000..ae0799a7c38
--- /dev/null
+++ b/frontend/src/metabase/public/containers/PublicDashboard/PublicDashboard.tsx
@@ -0,0 +1,348 @@
+import cx from "classnames";
+import { assoc } from "icepick";
+import type { ComponentType } from "react";
+import { Component } from "react";
+import type { ConnectedProps } from "react-redux";
+import { connect } from "react-redux";
+import { push } from "react-router-redux";
+import _ from "underscore";
+
+import LoadingAndErrorWrapper from "metabase/components/LoadingAndErrorWrapper";
+import ColorS from "metabase/css/core/colors.module.css";
+import CS from "metabase/css/core/index.css";
+import DashboardS from "metabase/css/dashboard.module.css";
+import {
+  initialize,
+  setParameterValueToDefault,
+  setParameterValue,
+  cancelFetchDashboardCardData,
+  fetchDashboardCardMetadata,
+  replaceCard,
+  fetchCardData,
+  markNewCardSeen,
+  setDashCardAttributes,
+  setMultipleDashCardAttributes,
+  undoRemoveCardFromDashboard,
+  onReplaceAllDashCardVisualizationSettings,
+  onUpdateDashCardVisualizationSettings,
+  showClickBehaviorSidebar,
+  fetchDashboard,
+  fetchDashboardCardData,
+} from "metabase/dashboard/actions";
+import { getDashboardActions } from "metabase/dashboard/components/DashboardActions";
+import { DashboardGridConnected } from "metabase/dashboard/components/DashboardGrid";
+import { DashboardTabs } from "metabase/dashboard/components/DashboardTabs";
+import { DashboardControls } from "metabase/dashboard/hoc/DashboardControls";
+import type {
+  DashboardControlsPassedProps,
+  DashboardControlsProps,
+} from "metabase/dashboard/hoc/types";
+import {
+  getDashboardComplete,
+  getCardData,
+  getSlowCards,
+  getParameters,
+  getParameterValues,
+  getDraftParameterValues,
+  getSelectedTabId,
+} from "metabase/dashboard/selectors";
+import type {
+  FetchDashboardResult,
+  SuccessfulFetchDashboardResult,
+} from "metabase/dashboard/types";
+import { isActionDashCard } from "metabase/dashboard/utils";
+import title from "metabase/hoc/Title";
+import { isWithinIframe } from "metabase/lib/dom";
+import ParametersS from "metabase/parameters/components/ParameterValueWidget.module.css";
+import { setErrorPage } from "metabase/redux/app";
+import { getMetadata } from "metabase/selectors/metadata";
+import {
+  setPublicDashboardEndpoints,
+  setEmbedDashboardEndpoints,
+} from "metabase/services";
+import type { Mode } from "metabase/visualizations/click-actions/Mode";
+import { PublicMode } from "metabase/visualizations/click-actions/modes/PublicMode";
+import type { Dashboard, DashboardId } from "metabase-types/api";
+import type { State } from "metabase-types/store";
+
+import EmbedFrame from "../../components/EmbedFrame";
+
+import { DashboardContainer } from "./PublicDashboard.styled";
+
+const mapStateToProps = (state: State, props: OwnProps) => {
+  return {
+    // this MUST go here, so it's passed to DashboardControls in the _.compose at the bottom
+    dashboardId: String(
+      props.params.dashboardId || props.params.uuid || props.params.token,
+    ),
+    metadata: getMetadata(state),
+    dashboard: getDashboardComplete(state),
+    dashcardData: getCardData(state),
+    slowCards: getSlowCards(state),
+    parameters: getParameters(state),
+    parameterValues: getParameterValues(state),
+    draftParameterValues: getDraftParameterValues(state),
+    selectedTabId: getSelectedTabId(state),
+  };
+};
+
+const mapDispatchToProps = {
+  initialize,
+  cancelFetchDashboardCardData,
+  fetchDashboardCardMetadata,
+  setParameterValueToDefault,
+  setParameterValue,
+  setErrorPage,
+  onChangeLocation: push,
+  fetchCardData,
+  replaceCard,
+  markNewCardSeen,
+  setDashCardAttributes,
+  setMultipleDashCardAttributes,
+  undoRemoveCardFromDashboard,
+  onReplaceAllDashCardVisualizationSettings,
+  onUpdateDashCardVisualizationSettings,
+  showClickBehaviorSidebar,
+
+  // these two must also go here, so it's passed to DashboardControls in the _.compose at the bottom
+  fetchDashboard,
+  fetchDashboardCardData,
+};
+
+const connector = connect(mapStateToProps, mapDispatchToProps);
+
+type ReduxProps = ConnectedProps<typeof connector>;
+
+type OwnProps = {
+  params: {
+    dashboardId?: DashboardId;
+    uuid?: string;
+    token?: string;
+  };
+} & DashboardControlsProps;
+
+type PublicDashboardProps = ReduxProps &
+  OwnProps &
+  DashboardControlsPassedProps;
+
+class PublicDashboardInner extends Component<PublicDashboardProps> {
+  _initialize = async () => {
+    const {
+      initialize,
+      fetchDashboard,
+      fetchDashboardCardData,
+      setErrorPage,
+      location,
+      params: { uuid, token },
+    } = this.props;
+    if (uuid) {
+      setPublicDashboardEndpoints();
+    } else if (token) {
+      setEmbedDashboardEndpoints();
+    }
+
+    initialize();
+
+    const result = await fetchDashboard({
+      dashId: String(uuid || token),
+      queryParams: location.query,
+    });
+
+    if (!isSuccessfulFetchDashboardResult(result)) {
+      setErrorPage(result.payload);
+      return;
+    }
+
+    try {
+      if (this.props.dashboard?.tabs?.length === 0) {
+        await fetchDashboardCardData({ reload: false, clearCache: true });
+      }
+    } catch (error) {
+      console.error(error);
+      setErrorPage(error);
+    }
+  };
+
+  async componentDidMount() {
+    await this._initialize();
+  }
+
+  componentWillUnmount() {
+    this.props.cancelFetchDashboardCardData();
+  }
+
+  async componentDidUpdate(prevProps: PublicDashboardProps) {
+    if (this.props.dashboardId !== prevProps.dashboardId) {
+      return this._initialize();
+    }
+
+    if (!_.isEqual(prevProps.selectedTabId, this.props.selectedTabId)) {
+      this.props.fetchDashboardCardData();
+      this.props.fetchDashboardCardMetadata();
+      return;
+    }
+
+    if (!_.isEqual(this.props.parameterValues, prevProps.parameterValues)) {
+      this.props.fetchDashboardCardData({ reload: false, clearCache: true });
+    }
+  }
+
+  getCurrentTabDashcards = () => {
+    const { dashboard, selectedTabId } = this.props;
+    if (!Array.isArray(dashboard?.dashcards)) {
+      return [];
+    }
+    if (!selectedTabId) {
+      return dashboard?.dashcards;
+    }
+    return dashboard?.dashcards.filter(
+      dashcard => dashcard.dashboard_tab_id === selectedTabId,
+    );
+  };
+
+  getHiddenParameterSlugs = () => {
+    const { parameters } = this.props;
+    const currentTabParameterIds =
+      this.getCurrentTabDashcards()?.flatMap(
+        dashcard =>
+          dashcard.parameter_mappings?.map(mapping => mapping.parameter_id) ??
+          [],
+      ) ?? [];
+    const hiddenParameters = parameters.filter(
+      parameter => !currentTabParameterIds.includes(parameter.id),
+    );
+    return hiddenParameters.map(parameter => parameter.slug).join(",");
+  };
+
+  render() {
+    const {
+      dashboard,
+      parameters,
+      parameterValues,
+      draftParameterValues,
+      isFullscreen,
+      isNightMode,
+      setParameterValueToDefault,
+      onFullscreenChange,
+      onNightModeChange,
+      onRefreshPeriodChange,
+      refreshPeriod,
+      setRefreshElapsedHook,
+      hasNightModeToggle,
+    } = this.props;
+
+    const buttons = !isWithinIframe()
+      ? getDashboardActions({
+          dashboard,
+          hasNightModeToggle,
+          isFullscreen,
+          isNightMode,
+          onFullscreenChange,
+          onNightModeChange,
+          onRefreshPeriodChange,
+          refreshPeriod,
+          setRefreshElapsedHook,
+          isPublic: true,
+        })
+      : [];
+
+    const visibleDashcards = (dashboard?.dashcards ?? []).filter(
+      dashcard => !isActionDashCard(dashcard),
+    );
+
+    return (
+      <EmbedFrame
+        name={dashboard && dashboard.name}
+        description={dashboard && dashboard.description}
+        dashboard={dashboard}
+        parameters={parameters}
+        parameterValues={parameterValues}
+        draftParameterValues={draftParameterValues}
+        hiddenParameterSlugs={this.getHiddenParameterSlugs()}
+        setParameterValue={this.props.setParameterValue}
+        setParameterValueToDefault={setParameterValueToDefault}
+        enableParameterRequiredBehavior
+        actionButtons={
+          buttons.length > 0 && <div className={CS.flex}>{buttons}</div>
+        }
+        dashboardTabs={
+          dashboard?.tabs &&
+          dashboard.tabs.length > 1 && (
+            <DashboardTabs
+              dashboardId={this.props.dashboardId}
+              location={this.props.location}
+            />
+          )
+        }
+      >
+        <LoadingAndErrorWrapper
+          className={cx({
+            [DashboardS.DashboardFullscreen]: isFullscreen,
+            [DashboardS.DashboardNight]: isNightMode,
+            [ParametersS.DashboardNight]: isNightMode,
+            [ColorS.DashboardNight]: isNightMode,
+          })}
+          loading={!dashboard}
+        >
+          {() =>
+            dashboard ? (
+              <DashboardContainer>
+                <DashboardGridConnected
+                  dashboard={assoc(dashboard, "dashcards", visibleDashcards)}
+                  isPublic
+                  mode={PublicMode as unknown as Mode}
+                  metadata={this.props.metadata}
+                  navigateToNewCardFromDashboard={() => {}}
+                  dashcardData={this.props.dashcardData}
+                  selectedTabId={this.props.selectedTabId}
+                  parameterValues={this.props.parameterValues}
+                  slowCards={this.props.slowCards}
+                  isEditing={false}
+                  isEditingParameter={false}
+                  isXray={false}
+                  isFullscreen={isFullscreen}
+                  isNightMode={isNightMode}
+                  clickBehaviorSidebarDashcard={null}
+                  width={0}
+                  fetchCardData={this.props.fetchCardData}
+                  replaceCard={this.props.replaceCard}
+                  markNewCardSeen={this.props.markNewCardSeen}
+                  setDashCardAttributes={this.props.setDashCardAttributes}
+                  setMultipleDashCardAttributes={
+                    this.props.setMultipleDashCardAttributes
+                  }
+                  undoRemoveCardFromDashboard={
+                    this.props.undoRemoveCardFromDashboard
+                  }
+                  onReplaceAllDashCardVisualizationSettings={
+                    this.props.onReplaceAllDashCardVisualizationSettings
+                  }
+                  onUpdateDashCardVisualizationSettings={
+                    this.props.onUpdateDashCardVisualizationSettings
+                  }
+                  onChangeLocation={this.props.onChangeLocation}
+                  showClickBehaviorSidebar={this.props.showClickBehaviorSidebar}
+                />
+              </DashboardContainer>
+            ) : null
+          }
+        </LoadingAndErrorWrapper>
+      </EmbedFrame>
+    );
+  }
+}
+
+function isSuccessfulFetchDashboardResult(
+  result: FetchDashboardResult,
+): result is SuccessfulFetchDashboardResult {
+  const hasError = "error" in result;
+  return !hasError;
+}
+
+export const PublicDashboard = _.compose(
+  connector,
+  title(
+    ({ dashboard }: { dashboard: Dashboard }) => dashboard && dashboard.name,
+  ),
+  DashboardControls,
+)(PublicDashboardInner) as ComponentType<OwnProps>;
-- 
GitLab