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