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