Compare commits

...

10 Commits

Author SHA1 Message Date
Oisin Coveney
c5fbe5221d Merge branch 'dashboard-context' of https://github.com/metabase/metabase into dashboard-context 2025-04-21 16:34:51 +01:00
Oisin Coveney
a4a295ca0c Resolve merge conflict 2025-04-21 16:25:31 +01:00
Oisin Coveney
2f133d9394 Discard changes to frontend/src/metabase/plugins/index.ts 2025-04-21 16:16:34 +01:00
Oisin Coveney
7d25ad8f59 Make sure downloadsEnabled is correct 2025-04-21 15:08:09 +01:00
Oisin Coveney
84507a1f53 Fix type errors 2025-04-21 15:08:08 +01:00
Oisin Coveney
f7ac5543ec Apparently I don't get type errors 2025-04-21 15:08:08 +01:00
Oisin Coveney
0d747e3dea Fix type errors 2025-04-21 15:08:08 +01:00
Oisin Coveney
3a715e7582 More fixds 2025-04-21 15:08:07 +01:00
Oisin Coveney
e4f6d46c05 Clean up a little more 2025-04-21 15:05:07 +01:00
Oisin Coveney
ba609272be Solve more type issues and clean up code 2025-04-21 15:05:07 +01:00
17 changed files with 737 additions and 772 deletions

View File

@@ -1,6 +1,3 @@
import type { Query } from "history";
import type { ComponentType, FC } from "react";
import type { ConnectedProps } from "react-redux";
import _ from "underscore";
import type { DashboardEventHandlersProps } from "embedding-sdk/types/dashboard";
@@ -10,90 +7,26 @@ import * as dashboardActions from "metabase/dashboard/actions";
import type { NavigateToNewCardFromDashboardOpts } from "metabase/dashboard/components/DashCard/types";
import { Dashboard } from "metabase/dashboard/components/Dashboard/Dashboard";
import {
getClickBehaviorSidebarDashcard,
getDashboardBeforeEditing,
getDashboardComplete,
getDocumentTitle,
getFavicon,
getIsAddParameterPopoverOpen,
getIsAdditionalInfoVisible,
getIsDashCardsLoadingComplete,
getIsDashCardsRunning,
getIsDirty,
getIsEditing,
getIsEditingParameter,
getIsHeaderVisible,
getIsNavigatingBackToDashboard,
getIsSharing,
getLoadingStartTime,
getParameterValues,
getSelectedTabId,
getSidebar,
getSlowCards,
} from "metabase/dashboard/selectors";
DashboardContextProvider,
useDashboardContext,
} from "metabase/dashboard/context";
import type {
DashboardFullscreenControls,
DashboardLoaderWrapperProps,
DashboardRefreshPeriodControls,
} from "metabase/dashboard/types";
import { connect } from "metabase/lib/redux";
import type { PublicOrEmbeddedDashboardEventHandlersProps } from "metabase/public/containers/PublicOrEmbeddedDashboard/types";
import { useDashboardLoadHandlers } from "metabase/public/containers/PublicOrEmbeddedDashboard/use-dashboard-load-handlers";
import { closeNavbar, setErrorPage } from "metabase/redux/app";
import { getIsNavbarOpen } from "metabase/selectors/app";
import {
canManageSubscriptions,
getUserIsAdmin,
} from "metabase/selectors/user";
import type { DashboardId } from "metabase-types/api";
import type { State } from "metabase-types/store";
const mapStateToProps = (state: State) => {
return {
canManageSubscriptions: canManageSubscriptions(state),
isAdmin: getUserIsAdmin(state),
isNavbarOpen: getIsNavbarOpen(state),
isEditing: getIsEditing(state),
isSharing: getIsSharing(state),
dashboardBeforeEditing: getDashboardBeforeEditing(state),
isEditingParameter: getIsEditingParameter(state),
isDirty: getIsDirty(state),
dashboard: getDashboardComplete(state),
slowCards: getSlowCards(state),
parameterValues: getParameterValues(state),
loadingStartTime: getLoadingStartTime(state),
clickBehaviorSidebarDashcard: getClickBehaviorSidebarDashcard(state),
isAddParameterPopoverOpen: getIsAddParameterPopoverOpen(state),
sidebar: getSidebar(state),
pageFavicon: getFavicon(state),
documentTitle: getDocumentTitle(state),
isRunning: getIsDashCardsRunning(state),
isLoadingComplete: getIsDashCardsLoadingComplete(state),
isHeaderVisible: getIsHeaderVisible(state),
isAdditionalInfoVisible: getIsAdditionalInfoVisible(state),
selectedTabId: getSelectedTabId(state),
isNavigatingBackToDashboard: getIsNavigatingBackToDashboard(state),
};
};
const mapDispatchToProps = {
...dashboardActions,
closeNavbar,
setErrorPage,
};
const connector = connect(mapStateToProps, mapDispatchToProps);
type ReduxProps = ConnectedProps<typeof connector>;
type ConnectedDashboardProps = {
dashboardId: DashboardId;
isLoading: boolean;
parameterQueryParams: Query;
parameterQueryParams: Record<string, string>;
downloadsEnabled?: boolean;
onNavigateToNewCardFromDashboard: (
onNavigateToNewCardFromDashboard?: (
opts: NavigateToNewCardFromDashboardOpts,
) => void;
plugins?: MetabasePluginsConfig;
} & Pick<CommonStylingProps, "className"> &
DashboardFullscreenControls &
@@ -102,28 +35,54 @@ type ConnectedDashboardProps = {
DashboardEventHandlersProps;
const ConnectedDashboardInner = ({
dashboard,
onLoad,
onLoadWithoutCards,
onNavigateToNewCardFromDashboard,
...restProps
}: ConnectedDashboardProps & ReduxProps) => {
}: PublicOrEmbeddedDashboardEventHandlersProps) => {
const { dashboard } = useDashboardContext();
useDashboardLoadHandlers({ dashboard, onLoad, onLoadWithoutCards });
return <Dashboard />;
};
export const ConnectedDashboard = ({
dashboardId,
parameterQueryParams,
downloadsEnabled,
onNavigateToNewCardFromDashboard,
isFullscreen,
onFullscreenChange,
refreshPeriod,
onRefreshPeriodChange,
setRefreshElapsedHook,
onLoad,
onLoadWithoutCards,
}: ConnectedDashboardProps) => {
return (
<Dashboard
dashboard={dashboard}
{...restProps}
<DashboardContextProvider
dashboardId={dashboardId}
parameterQueryParams={parameterQueryParams}
downloadsEnabled={downloadsEnabled ?? false}
isNightMode={false}
onNightModeChange={_.noop}
hasNightModeToggle={false}
isFullscreen={isFullscreen}
onFullscreenChange={onFullscreenChange}
refreshPeriod={refreshPeriod}
onRefreshPeriodChange={onRefreshPeriodChange}
setRefreshElapsedHook={setRefreshElapsedHook}
navigateToNewCardFromDashboard={onNavigateToNewCardFromDashboard}
autoScrollToDashcardId={undefined}
reportAutoScrolledToDashcard={_.noop}
/>
>
<ConnectedDashboardInner
onLoad={onLoad}
onLoadWithoutCards={onLoadWithoutCards}
/>
</DashboardContextProvider>
);
};
export const ConnectedDashboard = connector<
ComponentType<ConnectedDashboardProps & ReduxProps>
>(ConnectedDashboardInner) as FC<ConnectedDashboardProps>;

View File

@@ -136,7 +136,6 @@ export const EditableDashboard = ({
>
<ConnectedDashboard
dashboardId={dashboardId}
isLoading={isLoading}
parameterQueryParams={initialParameters}
refreshPeriod={refreshPeriod}
onRefreshPeriodChange={onRefreshPeriodChange}

View File

@@ -1,4 +1,5 @@
import type { Query } from "history";
import type { CSSProperties } from "react";
import { pick } from "underscore";
import type { SdkDashboardId } from "embedding-sdk/types/dashboard";

View File

@@ -1,26 +1,13 @@
import cx from "classnames";
import type { Query } from "history";
import type { ReactNode } from "react";
import { useCallback, useEffect, useMemo, useState } from "react";
import { usePrevious, useUnmount } from "react-use";
import _ from "underscore";
import { useMemo } from "react";
import { useListDatabasesQuery } from "metabase/api";
import { deletePermanently } from "metabase/archive/actions";
import { ArchivedEntityBanner } from "metabase/archive/components/ArchivedEntityBanner";
import {
type NewDashCardOpts,
type SetDashboardAttributesOpts,
moveDashboardToCollection,
setArchivedDashboard,
} from "metabase/dashboard/actions";
import type { NavigateToNewCardFromDashboardOpts } from "metabase/dashboard/components/DashCard/types";
import { DashboardHeader } from "metabase/dashboard/components/DashboardHeader";
import { useDashboardContext } from "metabase/dashboard/context";
import type {
CancelledFetchDashboardResult,
DashboardDisplayOptionControls,
FetchDashboardResult,
SuccessfulFetchDashboardResult,
} from "metabase/dashboard/types";
import Bookmarks from "metabase/entities/bookmarks";
import Dashboards from "metabase/entities/dashboards";
@@ -28,29 +15,10 @@ import { useDispatch } from "metabase/lib/redux";
import { getHasDataAccess, getHasNativeWrite } from "metabase/selectors/data";
import { FullWidthContainer } from "metabase/styled-components/layout/FullWidthContainer";
import { Box, Flex } from "metabase/ui";
import type {
CardId,
DashCardId,
DashCardVisualizationSettings,
DashboardCard,
DashboardId,
DashboardTabId,
Dashboard as IDashboard,
ParameterId,
ParameterValueOrArray,
TemporalUnit,
ValuesQueryType,
ValuesSourceConfig,
ValuesSourceType,
} from "metabase-types/api";
import type { DashboardCard } from "metabase-types/api";
import { isObject } from "metabase-types/guards";
import type {
DashboardSidebarName,
SelectedTabId,
State,
} from "metabase-types/store";
import { DASHBOARD_PDF_EXPORT_ROOT_ID, SIDEBAR_NAME } from "../../constants";
import { DASHBOARD_PDF_EXPORT_ROOT_ID } from "../../constants";
import { DashboardGridConnected } from "../DashboardGrid";
import { DashboardParameterPanel } from "../DashboardParameterPanel";
import { DashboardSidebars } from "../DashboardSidebars";
@@ -62,156 +30,64 @@ import {
DashboardEmptyStateWithoutAddPrompt,
} from "./DashboardEmptyState/DashboardEmptyState";
export type DashboardProps = {
children?: ReactNode;
canManageSubscriptions: boolean;
isAdmin: boolean;
isNavbarOpen: boolean;
isEditing: boolean;
isSharing: boolean;
dashboardBeforeEditing: IDashboard | null;
isEditingParameter: boolean;
isDirty: boolean;
dashboard: IDashboard | null;
slowCards: Record<DashCardId, boolean>;
parameterValues: Record<ParameterId, ParameterValueOrArray>;
loadingStartTime: number | null;
clickBehaviorSidebarDashcard: DashboardCard | null;
isAddParameterPopoverOpen: boolean;
sidebar: State["dashboard"]["sidebar"];
isHeaderVisible: boolean;
isAdditionalInfoVisible: boolean;
selectedTabId: SelectedTabId;
isNavigatingBackToDashboard: boolean;
addCardOnLoad?: DashCardId;
editingOnLoad?: string | string[] | boolean;
autoScrollToDashcardId: DashCardId | undefined;
dashboardId: DashboardId;
parameterQueryParams: Query;
initialize: (opts?: { clearCache?: boolean }) => void;
cancelFetchDashboardCardData: () => void;
addCardToDashboard: (opts: {
dashId: DashboardId;
cardId: CardId;
tabId: DashboardTabId | null;
}) => void;
addHeadingDashCardToDashboard: (opts: NewDashCardOpts) => void;
addMarkdownDashCardToDashboard: (opts: NewDashCardOpts) => void;
addLinkDashCardToDashboard: (opts: NewDashCardOpts) => void;
setEditingDashboard: (dashboard: IDashboard | null) => void;
setDashboardAttributes: (opts: SetDashboardAttributesOpts) => void;
setSharing: (isSharing: boolean) => void;
toggleSidebar: (sidebarName: DashboardSidebarName) => void;
closeSidebar: () => void;
closeNavbar: () => void;
setErrorPage: (error: unknown) => void;
setParameterName: (id: ParameterId, name: string) => void;
setParameterType: (id: ParameterId, type: string, sectionId: string) => void;
navigateToNewCardFromDashboard: (
opts: NavigateToNewCardFromDashboardOpts,
) => void;
setParameterDefaultValue: (id: ParameterId, value: unknown) => void;
setParameterRequired: (id: ParameterId, value: boolean) => void;
setParameterTemporalUnits: (
id: ParameterId,
temporalUnits: TemporalUnit[],
) => void;
setParameterIsMultiSelect: (id: ParameterId, isMultiSelect: boolean) => void;
setParameterQueryType: (id: ParameterId, queryType: ValuesQueryType) => void;
setParameterSourceType: (
id: ParameterId,
sourceType: ValuesSourceType,
) => void;
setParameterSourceConfig: (
id: ParameterId,
config: ValuesSourceConfig,
) => void;
setParameterFilteringParameters: (
parameterId: ParameterId,
filteringParameters: ParameterId[],
) => void;
showAddParameterPopover: () => void;
removeParameter: (id: ParameterId) => void;
onReplaceAllDashCardVisualizationSettings: (
id: DashCardId,
settings: DashCardVisualizationSettings | null | undefined,
) => void;
onUpdateDashCardVisualizationSettings: (
id: DashCardId,
settings: DashCardVisualizationSettings | null | undefined,
) => void;
onUpdateDashCardColumnSettings: (
id: DashCardId,
columnKey: string,
settings?: Record<string, unknown> | null,
) => void;
updateDashboardAndCards: () => void;
setSidebar: (opts: { name: DashboardSidebarName }) => void;
hideAddParameterPopover: () => void;
fetchDashboard: (opts: {
dashId: DashboardId;
queryParams: Query;
options?: {
clearCache?: boolean;
preserveParameters?: boolean;
};
}) => Promise<FetchDashboardResult>;
fetchDashboardCardData: (opts?: {
isRefreshing?: boolean;
reload?: boolean;
clearCache?: boolean;
}) => void;
reportAutoScrolledToDashcard: () => void;
} & DashboardDisplayOptionControls;
function Dashboard(props: DashboardProps) {
function Dashboard() {
const {
addCardOnLoad,
addCardToDashboard,
autoScrollToDashcardId,
cancelFetchDashboardCardData,
dashboard,
dashboardId,
editingOnLoad,
fetchDashboard,
fetchDashboardCardData,
initialize,
isEditing,
isFullscreen,
isNavigatingBackToDashboard,
isNightMode = false,
isSharing,
onRefreshPeriodChange,
parameterValues,
reportAutoScrolledToDashcard,
selectedTabId,
setEditingDashboard,
setErrorPage,
setSharing,
toggleSidebar,
parameterQueryParams,
downloadsEnabled = true,
noLoaderWrapper = false,
} = props;
parameterQueryParams = {},
downloadsEnabled,
handleAddQuestion,
shouldRenderAsNightMode,
setArchivedDashboard,
moveDashboardToCollection,
deletePermanently,
dashboardBeforeEditing,
onFullscreenChange,
isAdditionalInfoVisible,
hasNightModeToggle,
onNightModeChange,
refreshPeriod,
setRefreshElapsedHook,
isEditingParameter,
slowCards,
navigateToNewCardFromDashboard,
showAddParameterPopover,
removeParameter,
addCardToDashboard,
clickBehaviorSidebarDashcard,
onReplaceAllDashCardVisualizationSettings,
onUpdateDashCardVisualizationSettings,
onUpdateDashCardColumnSettings,
setParameterName,
setParameterType,
setParameterDefaultValue,
setParameterIsMultiSelect,
setParameterQueryType,
setParameterSourceType,
setParameterSourceConfig,
setParameterFilteringParameters,
setParameterRequired,
setParameterTemporalUnits,
sidebar,
closeSidebar,
} = useDashboardContext();
const canWrite = Boolean(dashboard?.can_write);
const canRestore = Boolean(dashboard?.can_restore);
const canDelete = Boolean(dashboard?.can_delete);
const dispatch = useDispatch();
const [isInitialized, setIsInitialized] = useState(false);
const [error, setError] = useState<unknown>(null);
const previousDashboard = usePrevious(dashboard);
const previousDashboardId = usePrevious(dashboardId);
const previousTabId = usePrevious(selectedTabId);
const previousParameterValues = usePrevious(parameterValues);
const invalidateBookmarks = async () =>
await dispatch(Bookmarks.actions.invalidateLists());
const currentTabDashcards = useMemo(() => {
if (!dashboard || !Array.isArray(dashboard.dashcards)) {
@@ -225,9 +101,6 @@ function Dashboard(props: DashboardProps) {
);
}, [dashboard, selectedTabId]);
const canWrite = Boolean(dashboard?.can_write);
const canRestore = Boolean(dashboard?.can_restore);
const canDelete = Boolean(dashboard?.can_delete);
const tabHasCards = currentTabDashcards.length > 0;
const dashboardHasCards = dashboard && dashboard.dashcards.length > 0;
@@ -243,125 +116,6 @@ function Dashboard(props: DashboardProps) {
);
const canCreateQuestions = hasDataAccess || hasNativeWrite;
const shouldRenderAsNightMode = isNightMode && isFullscreen;
const handleSetEditing = useCallback(
(dashboard: IDashboard | null) => {
if (!isEditing) {
onRefreshPeriodChange(null);
setEditingDashboard(dashboard);
}
},
[isEditing, onRefreshPeriodChange, setEditingDashboard],
);
const handleAddQuestion = useCallback(() => {
handleSetEditing(dashboard);
toggleSidebar(SIDEBAR_NAME.addQuestion);
}, [handleSetEditing, dashboard, toggleSidebar]);
const handleLoadDashboard = useCallback(
async (dashboardId: DashboardId) => {
initialize({ clearCache: !isNavigatingBackToDashboard });
const result = await fetchDashboard({
dashId: dashboardId,
queryParams: parameterQueryParams,
options: {
clearCache: !isNavigatingBackToDashboard,
preserveParameters: isNavigatingBackToDashboard,
},
});
if (!isSuccessfulFetchDashboardResult(result)) {
if (!isCancelledFetchDashboardResult(result)) {
setErrorPage(result.payload);
}
return;
}
try {
const dashboard = result.payload.dashboard;
if (editingOnLoad) {
onRefreshPeriodChange(null);
setEditingDashboard(dashboard);
}
if (addCardOnLoad != null) {
const searchParams = new URLSearchParams(window.location.search);
const tabParam = searchParams.get("tab");
const tabId = tabParam ? parseInt(tabParam, 10) : null;
addCardToDashboard({
dashId: dashboardId,
cardId: addCardOnLoad,
tabId,
});
}
} catch (error) {
if (error instanceof Response && error.status === 404) {
setErrorPage({ ...error, context: "dashboard" });
} else {
console.error(error);
setError(error);
}
}
},
[
addCardOnLoad,
addCardToDashboard,
editingOnLoad,
fetchDashboard,
initialize,
isNavigatingBackToDashboard,
onRefreshPeriodChange,
parameterQueryParams,
setEditingDashboard,
setErrorPage,
],
);
useEffect(() => {
const hasDashboardChanged = dashboardId !== previousDashboardId;
if (hasDashboardChanged) {
handleLoadDashboard(dashboardId).then(() => setIsInitialized(true));
return;
}
if (!dashboard) {
return;
}
const hasDashboardLoaded = !previousDashboard;
const hasTabChanged = selectedTabId !== previousTabId;
const hasParameterValueChanged = !_.isEqual(
parameterValues,
previousParameterValues,
);
if (hasDashboardLoaded) {
fetchDashboardCardData({ reload: false, clearCache: true });
} else if (hasTabChanged || hasParameterValueChanged) {
fetchDashboardCardData();
}
}, [
dashboard,
dashboardId,
fetchDashboardCardData,
handleLoadDashboard,
isInitialized,
parameterValues,
previousDashboard,
previousDashboardId,
previousParameterValues,
previousTabId,
selectedTabId,
]);
useUnmount(() => {
cancelFetchDashboardCardData();
});
const renderEmptyStates = () => {
if (!dashboardHasCards) {
return canWrite ? (
@@ -404,8 +158,7 @@ function Dashboard(props: DashboardProps) {
isFullscreen={isFullscreen}
isNightMode={shouldRenderAsNightMode}
loading={!dashboard}
error={error}
noWrapper={noLoaderWrapper}
noWrapper={true}
>
{() => {
if (!dashboard) {
@@ -425,14 +178,14 @@ function Dashboard(props: DashboardProps) {
canRestore={canRestore}
canDelete={canDelete}
onUnarchive={async () => {
await dispatch(setArchivedDashboard(false));
await dispatch(Bookmarks.actions.invalidateLists());
await setArchivedDashboard(false);
await invalidateBookmarks();
}}
onMove={({ id }) => dispatch(moveDashboardToCollection({ id }))}
onMove={({ id }) => moveDashboardToCollection({ id })}
onDeletePermanently={() => {
const { id } = dashboard;
const deleteAction = Dashboards.actions.delete({ id });
dispatch(deletePermanently(deleteAction));
deletePermanently(deleteAction);
}}
/>
)}
@@ -457,13 +210,13 @@ function Dashboard(props: DashboardProps) {
isNightMode={shouldRenderAsNightMode}
isFullscreen={isFullscreen}
onRefreshPeriodChange={onRefreshPeriodChange}
dashboardBeforeEditing={props.dashboardBeforeEditing}
onFullscreenChange={props.onFullscreenChange}
isAdditionalInfoVisible={props.isAdditionalInfoVisible}
hasNightModeToggle={props.hasNightModeToggle}
onNightModeChange={props.onNightModeChange}
refreshPeriod={props.refreshPeriod}
setRefreshElapsedHook={props.setRefreshElapsedHook}
dashboardBeforeEditing={dashboardBeforeEditing}
onFullscreenChange={onFullscreenChange}
isAdditionalInfoVisible={isAdditionalInfoVisible}
hasNightModeToggle={hasNightModeToggle}
onNightModeChange={onNightModeChange}
refreshPeriod={refreshPeriod}
setRefreshElapsedHook={setRefreshElapsedHook}
/>
</Box>
@@ -495,19 +248,18 @@ function Dashboard(props: DashboardProps) {
>
<DashboardGridConnected
clickBehaviorSidebarDashcard={
props.clickBehaviorSidebarDashcard
clickBehaviorSidebarDashcard
}
isNightMode={shouldRenderAsNightMode}
isFullscreen={props.isFullscreen}
isEditingParameter={props.isEditingParameter}
isEditing={props.isEditing}
isFullscreen={isFullscreen}
isEditingParameter={isEditingParameter}
isEditing={isEditing}
dashboard={dashboard}
slowCards={props.slowCards}
slowCards={slowCards}
navigateToNewCardFromDashboard={
props.navigateToNewCardFromDashboard
navigateToNewCardFromDashboard
}
selectedTabId={selectedTabId}
onEditingChange={handleSetEditing}
downloadsEnabled={downloadsEnabled}
autoScrollToDashcardId={autoScrollToDashcardId}
reportAutoScrolledToDashcard={
@@ -520,36 +272,32 @@ function Dashboard(props: DashboardProps) {
<DashboardSidebars
dashboard={dashboard}
showAddParameterPopover={props.showAddParameterPopover}
removeParameter={props.removeParameter}
addCardToDashboard={props.addCardToDashboard}
clickBehaviorSidebarDashcard={
props.clickBehaviorSidebarDashcard
}
showAddParameterPopover={showAddParameterPopover}
removeParameter={removeParameter}
addCardToDashboard={addCardToDashboard}
clickBehaviorSidebarDashcard={clickBehaviorSidebarDashcard}
onReplaceAllDashCardVisualizationSettings={
props.onReplaceAllDashCardVisualizationSettings
onReplaceAllDashCardVisualizationSettings
}
onUpdateDashCardVisualizationSettings={
props.onUpdateDashCardVisualizationSettings
onUpdateDashCardVisualizationSettings
}
onUpdateDashCardColumnSettings={
props.onUpdateDashCardColumnSettings
}
setParameterName={props.setParameterName}
setParameterType={props.setParameterType}
setParameterDefaultValue={props.setParameterDefaultValue}
setParameterIsMultiSelect={props.setParameterIsMultiSelect}
setParameterQueryType={props.setParameterQueryType}
setParameterSourceType={props.setParameterSourceType}
setParameterSourceConfig={props.setParameterSourceConfig}
onUpdateDashCardColumnSettings={onUpdateDashCardColumnSettings}
setParameterName={setParameterName}
setParameterType={setParameterType}
setParameterDefaultValue={setParameterDefaultValue}
setParameterIsMultiSelect={setParameterIsMultiSelect}
setParameterQueryType={setParameterQueryType}
setParameterSourceType={setParameterSourceType}
setParameterSourceConfig={setParameterSourceConfig}
setParameterFilteringParameters={
props.setParameterFilteringParameters
setParameterFilteringParameters
}
setParameterRequired={props.setParameterRequired}
setParameterTemporalUnits={props.setParameterTemporalUnits}
isFullscreen={props.isFullscreen}
sidebar={props.sidebar}
closeSidebar={props.closeSidebar}
setParameterRequired={setParameterRequired}
setParameterTemporalUnits={setParameterTemporalUnits}
isFullscreen={isFullscreen}
sidebar={sidebar}
closeSidebar={closeSidebar}
selectedTabId={selectedTabId}
onCancel={() => setSharing(false)}
/>
@@ -561,13 +309,6 @@ function Dashboard(props: DashboardProps) {
);
}
function isSuccessfulFetchDashboardResult(
result: FetchDashboardResult,
): result is SuccessfulFetchDashboardResult {
const hasError = "error" in result;
return !hasError;
}
export function isCancelledFetchDashboardResult(
result: FetchDashboardResult,
): result is CancelledFetchDashboardResult {

View File

@@ -31,6 +31,7 @@ import {
} from "metabase/lib/dashboard_grid";
import { connect } from "metabase/lib/redux";
import EmbedFrameS from "metabase/public/components/EmbedFrame/EmbedFrame.module.css";
import type { EmbedResourceDownloadOptions } from "metabase/public/lib/types";
import { addUndo } from "metabase/redux/undo";
import { Box, Flex } from "metabase/ui";
import LegendS from "metabase/visualizations/components/Legend.module.css";
@@ -151,7 +152,7 @@ type OwnProps = {
opts: NavigateToNewCardFromDashboardOpts,
) => void;
onEditingChange?: (dashboard: Dashboard | null) => void;
downloadsEnabled: boolean;
downloadsEnabled: EmbedResourceDownloadOptions;
autoScrollToDashcardId: DashCardId | undefined;
reportAutoScrolledToDashcard: () => void;
};
@@ -526,7 +527,7 @@ class DashboardGridInner extends Component<
isMobile: boolean;
gridItemWidth: number;
totalNumGridCols: number;
downloadsEnabled: boolean;
downloadsEnabled: EmbedResourceDownloadOptions;
shouldAutoScrollTo: boolean;
reportAutoScrolledToDashcard: () => void;
},

View File

@@ -1,4 +1,3 @@
import type { Query } from "history";
import { useState } from "react";
import { useMount } from "react-use";
import { t } from "ttag";
@@ -37,7 +36,7 @@ import { CancelEditButton, SaveEditButton } from "./buttons";
export type DashboardHeaderProps = {
dashboard: Dashboard;
dashboardBeforeEditing?: Dashboard | null;
parameterQueryParams: Query;
parameterQueryParams: Record<string, string>;
isAdditionalInfoVisible: boolean;
} & DashboardFullscreenControls &
DashboardRefreshPeriodControls &

View File

@@ -2,28 +2,25 @@ import { type Route, type WithRouterProps, withRouter } from "react-router";
import { t } from "ttag";
import { updateDashboardAndCards } from "metabase/dashboard/actions/save";
import { getIsDirty, getIsEditing } from "metabase/dashboard/selectors";
import { useConfirmRouteLeaveModal } from "metabase/hooks/use-confirm-route-leave-modal";
import { useDispatch } from "metabase/lib/redux";
import { useDispatch, useSelector } from "metabase/lib/redux";
import { dismissAllUndo } from "metabase/redux/undo";
import { Box, Button, Flex, Modal, Text } from "metabase/ui";
import { isNavigatingToCreateADashboardQuestion } from "./utils";
interface DashboardLeaveConfirmationModalProps extends WithRouterProps {
isEditing: boolean;
isDirty: boolean;
route: Route;
}
export const DashboardLeaveConfirmationModal = withRouter(
({
isEditing,
isDirty,
router,
route,
}: DashboardLeaveConfirmationModalProps) => {
({ router, route }: DashboardLeaveConfirmationModalProps) => {
const dispatch = useDispatch();
const isEditing = useSelector(getIsEditing);
const isDirty = useSelector(getIsDirty);
const { opened, close, confirm, nextLocation } = useConfirmRouteLeaveModal({
isEnabled: isEditing && isDirty,
route,

View File

@@ -1,57 +1,28 @@
import cx from "classnames";
import type { ReactNode } from "react";
import { useCallback, useEffect } from "react";
import type { ConnectedProps } from "react-redux";
import type { PropsWithChildren } from "react";
import { useCallback, useEffect, useState } from "react";
import type { Route, WithRouterProps } from "react-router";
import { push } from "react-router-redux";
import { useUnmount } from "react-use";
import { t } from "ttag";
import _ from "underscore";
import ErrorBoundary from "metabase/ErrorBoundary";
import CS from "metabase/css/core/index.css";
import {
addCardToDashboard,
addHeadingDashCardToDashboard,
addLinkDashCardToDashboard,
addMarkdownDashCardToDashboard,
cancelFetchDashboardCardData,
closeDashboard,
closeSidebar,
fetchDashboard,
fetchDashboardCardData,
hideAddParameterPopover,
initialize,
navigateToNewCardFromDashboard,
onReplaceAllDashCardVisualizationSettings,
onUpdateDashCardColumnSettings,
onUpdateDashCardVisualizationSettings,
removeParameter,
reset,
setDashboardAttributes,
setEditingDashboard,
setParameterDefaultValue,
setParameterFilteringParameters,
setParameterIsMultiSelect,
setParameterName,
setParameterQueryType,
setParameterRequired,
setParameterSourceConfig,
setParameterSourceType,
setParameterTemporalUnits,
setParameterType,
setSharing,
setSidebar,
showAddParameterPopover,
toggleSidebar,
updateDashboardAndCards,
} from "metabase/dashboard/actions";
import { Dashboard } from "metabase/dashboard/components/Dashboard/Dashboard";
import { DashboardLeaveConfirmationModal } from "metabase/dashboard/components/DashboardLeaveConfirmationModal";
import {
DashboardContextProvider,
useDashboardContext,
} from "metabase/dashboard/context";
import {
useDashboardUrlParams,
useDashboardUrlQuery,
useRefreshDashboard,
} from "metabase/dashboard/hooks";
import type { SuccessfulFetchDashboardResult } from "metabase/dashboard/types";
import title from "metabase/hoc/Title";
import titleWithLoadingTime from "metabase/hoc/TitleWithLoadingTime";
import { useFavicon } from "metabase/hooks/use-favicon";
@@ -59,203 +30,27 @@ import { useLoadingTimer } from "metabase/hooks/use-loading-timer";
import { useUniqueId } from "metabase/hooks/use-unique-id";
import { useWebNotification } from "metabase/hooks/use-web-notification";
import { parseHashOptions } from "metabase/lib/browser";
import { connect, useDispatch } from "metabase/lib/redux";
import { useDispatch, useSelector } from "metabase/lib/redux";
import * as Urls from "metabase/lib/urls";
import { closeNavbar, setErrorPage } from "metabase/redux/app";
import { setErrorPage } from "metabase/redux/app";
import { addUndo, dismissUndo } from "metabase/redux/undo";
import { getIsNavbarOpen } from "metabase/selectors/app";
import {
canManageSubscriptions,
getUserIsAdmin,
} from "metabase/selectors/user";
import type { DashboardId } from "metabase-types/api";
import type { State } from "metabase-types/store";
import { DASHBOARD_SLOW_TIMEOUT } from "../../constants";
import {
getClickBehaviorSidebarDashcard,
getDashboardBeforeEditing,
getDashboardComplete,
getDocumentTitle,
getFavicon,
getIsAddParameterPopoverOpen,
getIsAdditionalInfoVisible,
getIsDashCardsLoadingComplete,
getIsDashCardsRunning,
getIsDirty,
getIsEditing,
getIsEditingParameter,
getIsHeaderVisible,
getIsNavigatingBackToDashboard,
getIsSharing,
getLoadingStartTime,
getParameterValues,
getSelectedTabId,
getSidebar,
getSlowCards,
} from "../../selectors";
import { getFavicon } from "../../selectors";
type OwnProps = {
interface DashboardAppProps extends PropsWithChildren {
dashboardId?: DashboardId;
route: Route;
params: { slug: string };
children?: ReactNode;
};
}
const mapStateToProps = (state: State) => {
return {
canManageSubscriptions: canManageSubscriptions(state),
isAdmin: getUserIsAdmin(state),
isNavbarOpen: getIsNavbarOpen(state),
isEditing: getIsEditing(state),
isSharing: getIsSharing(state),
dashboardBeforeEditing: getDashboardBeforeEditing(state),
isEditingParameter: getIsEditingParameter(state),
isDirty: getIsDirty(state),
dashboard: getDashboardComplete(state),
slowCards: getSlowCards(state),
parameterValues: getParameterValues(state),
loadingStartTime: getLoadingStartTime(state),
clickBehaviorSidebarDashcard: getClickBehaviorSidebarDashcard(state),
isAddParameterPopoverOpen: getIsAddParameterPopoverOpen(state),
sidebar: getSidebar(state),
pageFavicon: getFavicon(state),
documentTitle: getDocumentTitle(state),
isRunning: getIsDashCardsRunning(state),
isLoadingComplete: getIsDashCardsLoadingComplete(state),
isHeaderVisible: getIsHeaderVisible(state),
isAdditionalInfoVisible: getIsAdditionalInfoVisible(state),
selectedTabId: getSelectedTabId(state),
isNavigatingBackToDashboard: getIsNavigatingBackToDashboard(state),
};
};
const mapDispatchToProps = {
initialize,
cancelFetchDashboardCardData,
addCardToDashboard,
addHeadingDashCardToDashboard,
addMarkdownDashCardToDashboard,
addLinkDashCardToDashboard,
setEditingDashboard,
setDashboardAttributes,
setSharing,
toggleSidebar,
closeSidebar,
closeNavbar,
setErrorPage,
setParameterName,
setParameterType,
navigateToNewCardFromDashboard,
setParameterDefaultValue,
setParameterRequired,
setParameterTemporalUnits,
setParameterIsMultiSelect,
setParameterQueryType,
setParameterSourceType,
setParameterSourceConfig,
setParameterFilteringParameters,
showAddParameterPopover,
removeParameter,
onReplaceAllDashCardVisualizationSettings,
onUpdateDashCardVisualizationSettings,
onUpdateDashCardColumnSettings,
updateDashboardAndCards,
setSidebar,
hideAddParameterPopover,
fetchDashboard,
fetchDashboardCardData,
onChangeLocation: push,
};
const connector = connect(mapStateToProps, mapDispatchToProps);
type ReduxProps = ConnectedProps<typeof connector>;
export type DashboardAppProps = OwnProps & ReduxProps & WithRouterProps;
const DashboardApp = (props: DashboardAppProps) => {
useFavicon({ favicon: props.pageFavicon });
const {
dashboard,
isRunning,
isLoadingComplete,
isEditing,
isDirty,
route,
router,
documentTitle: _documentTitle,
isRunning: _isRunning,
isLoadingComplete: _isLoadingComplete,
location,
canManageSubscriptions,
isAdmin,
isNavbarOpen,
isSharing,
dashboardBeforeEditing,
isEditingParameter,
slowCards,
parameterValues,
loadingStartTime,
clickBehaviorSidebarDashcard,
isAddParameterPopoverOpen,
sidebar,
isHeaderVisible,
isAdditionalInfoVisible,
selectedTabId,
isNavigatingBackToDashboard,
initialize,
cancelFetchDashboardCardData,
addCardToDashboard,
addHeadingDashCardToDashboard,
addMarkdownDashCardToDashboard,
addLinkDashCardToDashboard,
setEditingDashboard,
setDashboardAttributes,
setSharing,
toggleSidebar,
closeSidebar,
closeNavbar,
setErrorPage,
setParameterName,
setParameterType,
navigateToNewCardFromDashboard,
setParameterDefaultValue,
setParameterRequired,
setParameterTemporalUnits,
setParameterIsMultiSelect,
setParameterQueryType,
setParameterSourceType,
setParameterSourceConfig,
setParameterFilteringParameters,
showAddParameterPopover,
removeParameter,
onReplaceAllDashCardVisualizationSettings,
onUpdateDashCardVisualizationSettings,
onUpdateDashCardColumnSettings,
updateDashboardAndCards,
setSidebar,
hideAddParameterPopover,
fetchDashboard,
fetchDashboardCardData,
} = props;
const parameterQueryParams = location.query;
const dashboardId = getDashboardId(props);
const options = parseHashOptions(window.location.hash);
const editingOnLoad = options.edit;
const addCardOnLoad = options.add != null ? Number(options.add) : undefined;
const useSlowCardNotification = () => {
const { dashboard, isRunning, isLoadingComplete } = useDashboardContext();
const dispatch = useDispatch();
const { requestPermission, showNotification } = useWebNotification();
useUnmount(() => {
dispatch(reset());
dispatch(closeDashboard());
});
const slowToastId = useUniqueId();
useEffect(() => {
@@ -306,9 +101,33 @@ const DashboardApp = (props: DashboardAppProps) => {
timer: DASHBOARD_SLOW_TIMEOUT,
onTimeout,
});
};
export const DashboardApp = ({
location,
params,
router,
route,
dashboardId: _dashboardId,
children,
}: DashboardAppProps & WithRouterProps<{ slug: string }>) => {
const dispatch = useDispatch();
const [error, setError] = useState<string>();
const favicon = useSelector(getFavicon);
useFavicon({ favicon });
const parameterQueryParams = location.query;
const dashboardId =
_dashboardId || (Urls.extractEntityId(params.slug) as DashboardId);
const options = parseHashOptions(window.location.hash);
const editingOnLoad = options.edit;
const addCardOnLoad = options.add != null ? Number(options.add) : undefined;
const { refreshDashboard } = useRefreshDashboard({
dashboardId: dashboardId,
dashboardId,
parameterQueryParams,
});
@@ -323,114 +142,98 @@ const DashboardApp = (props: DashboardAppProps) => {
onRefreshPeriodChange,
autoScrollToDashcardId,
reportAutoScrolledToDashcard,
downloadsEnabled,
background,
bordered,
titled,
theme,
setTheme,
} = useDashboardUrlParams({ location, onRefresh: refreshDashboard });
useDashboardUrlQuery(router, location);
const onLoadDashboard = (result: SuccessfulFetchDashboardResult) => {
const dashboard = result.payload.dashboard;
try {
if (editingOnLoad) {
onRefreshPeriodChange(null);
setEditingDashboard(dashboard);
}
if (addCardOnLoad != null) {
const searchParams = new URLSearchParams(window.location.search);
const tabParam = searchParams.get("tab");
const tabId = tabParam ? parseInt(tabParam, 10) : null;
dispatch(
addCardToDashboard({
dashId: dashboardId,
cardId: addCardOnLoad,
tabId,
}),
);
}
} catch (error) {
if (error instanceof Response && error.status === 404) {
setErrorPage({ ...error, context: "dashboard" });
} else {
console.error(error);
setError(error as string);
}
}
};
return (
<div className={cx(CS.shrinkBelowContentSize, CS.fullHeight)}>
<DashboardLeaveConfirmationModal
route={route}
isDirty={isDirty}
isEditing={isEditing}
/>
<Dashboard
<ErrorBoundary message={error}>
<DashboardContextProvider
dashboardId={dashboardId}
editingOnLoad={editingOnLoad}
addCardOnLoad={addCardOnLoad}
parameterQueryParams={parameterQueryParams}
isFullscreen={isFullscreen}
onFullscreenChange={onFullscreenChange}
hasNightModeToggle={hasNightModeToggle}
onNightModeChange={onNightModeChange}
isNightMode={isNightMode}
refreshPeriod={refreshPeriod}
setRefreshElapsedHook={setRefreshElapsedHook}
onRefreshPeriodChange={onRefreshPeriodChange}
background={background}
bordered={bordered}
titled={titled}
theme={theme}
setTheme={setTheme}
downloadsEnabled={downloadsEnabled}
autoScrollToDashcardId={autoScrollToDashcardId}
reportAutoScrolledToDashcard={reportAutoScrolledToDashcard}
isFullscreen={isFullscreen}
refreshPeriod={refreshPeriod}
isNightMode={isNightMode}
hasNightModeToggle={hasNightModeToggle}
setRefreshElapsedHook={setRefreshElapsedHook}
onNightModeChange={onNightModeChange}
onFullscreenChange={onFullscreenChange}
onRefreshPeriodChange={onRefreshPeriodChange}
parameterQueryParams={parameterQueryParams}
canManageSubscriptions={canManageSubscriptions}
isAdmin={isAdmin}
isNavbarOpen={isNavbarOpen}
isEditing={isEditing}
isSharing={isSharing}
dashboardBeforeEditing={dashboardBeforeEditing}
isEditingParameter={isEditingParameter}
isDirty={isDirty}
dashboard={dashboard}
slowCards={slowCards}
parameterValues={parameterValues}
loadingStartTime={loadingStartTime}
clickBehaviorSidebarDashcard={clickBehaviorSidebarDashcard}
isAddParameterPopoverOpen={isAddParameterPopoverOpen}
sidebar={sidebar}
isHeaderVisible={isHeaderVisible}
isAdditionalInfoVisible={isAdditionalInfoVisible}
selectedTabId={selectedTabId}
isNavigatingBackToDashboard={isNavigatingBackToDashboard}
initialize={initialize}
cancelFetchDashboardCardData={cancelFetchDashboardCardData}
addCardToDashboard={addCardToDashboard}
addHeadingDashCardToDashboard={addHeadingDashCardToDashboard}
addMarkdownDashCardToDashboard={addMarkdownDashCardToDashboard}
addLinkDashCardToDashboard={addLinkDashCardToDashboard}
setEditingDashboard={setEditingDashboard}
setDashboardAttributes={setDashboardAttributes}
setSharing={setSharing}
toggleSidebar={toggleSidebar}
closeSidebar={closeSidebar}
closeNavbar={closeNavbar}
setErrorPage={setErrorPage}
setParameterName={setParameterName}
setParameterType={setParameterType}
navigateToNewCardFromDashboard={navigateToNewCardFromDashboard}
setParameterDefaultValue={setParameterDefaultValue}
setParameterRequired={setParameterRequired}
setParameterTemporalUnits={setParameterTemporalUnits}
setParameterIsMultiSelect={setParameterIsMultiSelect}
setParameterQueryType={setParameterQueryType}
setParameterSourceType={setParameterSourceType}
setParameterSourceConfig={setParameterSourceConfig}
setParameterFilteringParameters={setParameterFilteringParameters}
showAddParameterPopover={showAddParameterPopover}
removeParameter={removeParameter}
onReplaceAllDashCardVisualizationSettings={
onReplaceAllDashCardVisualizationSettings
}
onUpdateDashCardVisualizationSettings={
onUpdateDashCardVisualizationSettings
}
onUpdateDashCardColumnSettings={onUpdateDashCardColumnSettings}
updateDashboardAndCards={updateDashboardAndCards}
setSidebar={setSidebar}
hideAddParameterPopover={hideAddParameterPopover}
fetchDashboard={fetchDashboard}
fetchDashboardCardData={fetchDashboardCardData}
/>
{/* For rendering modal urls */}
{props.children}
</div>
onLoad={onLoadDashboard}
onError={(result) => dispatch(setErrorPage(result.payload))}
hideParameters={null}
font={null}
>
<DashboardTitle />
<div className={cx(CS.shrinkBelowContentSize, CS.fullHeight)}>
<DashboardLeaveConfirmationModal route={route} />
<Dashboard />
{/* For rendering modal urls */}
{children}
</div>
</DashboardContextProvider>
</ErrorBoundary>
);
};
function getDashboardId({ dashboardId, params }: DashboardAppProps) {
if (dashboardId) {
return dashboardId;
}
const DashboardTitle = () => {
const { dashboard, documentTitle, pageFavicon } = useDashboardContext();
return Urls.extractEntityId(params.slug) as DashboardId;
}
useFavicon({ favicon: pageFavicon });
useSlowCardNotification();
export const DashboardAppConnected = _.compose(
connector,
title(
({
dashboard,
documentTitle,
}: Pick<ReduxProps, "dashboard" | "documentTitle">) => ({
const Component = _.compose(
title(() => ({
title: documentTitle || dashboard?.name,
titleIndex: 1,
}),
),
titleWithLoadingTime("loadingStartTime"),
)(DashboardApp);
})),
titleWithLoadingTime("loadingStartTime"),
)(() => null);
return <Component />;
};

View File

@@ -23,7 +23,7 @@ import {
screen,
waitForLoaderToBeRemoved,
} from "__support__/ui";
import { DashboardAppConnected } from "metabase/dashboard/containers/DashboardApp/DashboardApp";
import { DashboardApp } from "metabase/dashboard/containers/DashboardApp/DashboardApp";
import { BEFORE_UNLOAD_UNSAVED_MESSAGE } from "metabase/hooks/use-before-unload";
import { checkNotNull } from "metabase/lib/types";
import type { Dashboard } from "metabase-types/api";
@@ -91,7 +91,7 @@ async function setup({ dashboard }: Options = {}) {
return (
<main>
<link rel="icon" />
<DashboardAppConnected {...props} />
<DashboardApp {...props} />
</main>
);
};

View File

@@ -0,0 +1,319 @@
import {
type PropsWithChildren,
createContext,
useCallback,
useContext,
useEffect,
useState,
} from "react";
import { usePrevious, useUnmount } from "react-use";
import { isEqual, isObject, noop } from "underscore";
import { useDispatch } from "metabase/lib/redux";
import type {
DisplayTheme,
EmbedResourceDownloadOptions,
} from "metabase/public/lib/types";
import type { DashboardId } from "metabase-types/api";
import { navigateToNewCardFromDashboard } from "../actions";
import type { NavigateToNewCardFromDashboardOpts } from "../components/DashCard/types";
import { SIDEBAR_NAME } from "../constants";
import type { UseAutoScrollToDashcardResult } from "../hooks/use-auto-scroll-to-dashcard";
import type {
DashboardFullscreenControls,
DashboardNightModeControls,
DashboardRefreshPeriodControls,
EmbedBackground,
EmbedFont,
EmbedHideParameters,
EmbedTitle,
FailedFetchDashboardResult,
FetchDashboardResult,
SuccessfulFetchDashboardResult,
} from "../types";
import { type ReduxProps, connector } from "./redux";
type DashboardLoadingState = {
isLoading: boolean;
};
type OwnProps = {
dashboardId: DashboardId;
parameterQueryParams?: Record<string, string>;
onLoad?: (result: SuccessfulFetchDashboardResult) => void;
onError?: (result: FailedFetchDashboardResult) => void;
navigateToNewCardFromDashboard?: (
opts: NavigateToNewCardFromDashboardOpts,
) => void;
};
type OwnResult = {
shouldRenderAsNightMode: boolean;
handleAddQuestion: () => void;
};
type DashboardOptions = {
background: EmbedBackground;
bordered: boolean;
titled: EmbedTitle;
hideParameters: EmbedHideParameters;
font: EmbedFont;
theme: DisplayTheme;
setTheme: (theme: DisplayTheme) => void;
downloadsEnabled: EmbedResourceDownloadOptions;
};
type DashboardControls = DashboardFullscreenControls &
DashboardNightModeControls &
DashboardRefreshPeriodControls &
UseAutoScrollToDashcardResult &
DashboardOptions;
type ContextProps = OwnProps & ReduxProps & Partial<DashboardControls>;
type ContextReturned = OwnResult &
OwnProps &
ReduxProps &
Required<DashboardControls> &
DashboardLoadingState;
const DashboardContext = createContext<ContextReturned | undefined>(undefined);
const DashboardContextProviderInner = ({
dashboardId,
parameterQueryParams = {},
onLoad,
onError,
children,
// url params
isFullscreen = false,
onFullscreenChange = noop,
hasNightModeToggle = false,
onNightModeChange = noop,
isNightMode = false,
refreshPeriod = null,
setRefreshElapsedHook = noop,
onRefreshPeriodChange = noop,
background = true,
bordered = true,
titled = true,
font = null,
theme = "light",
setTheme = noop,
hideParameters = null,
downloadsEnabled = { pdf: true, dashcard: true },
autoScrollToDashcardId = undefined,
reportAutoScrolledToDashcard = noop,
// redux selectors
dashboard,
selectedTabId,
isEditing,
isNavigatingBackToDashboard,
parameterValues,
// redux actions
addCardToDashboard,
cancelFetchDashboardCardData,
fetchDashboard,
fetchDashboardCardData,
initialize,
setEditingDashboard,
toggleSidebar,
reset,
closeDashboard,
navigateToNewCardFromDashboard: _navigateToNewCardFromDashboard,
...reduxProps
}: PropsWithChildren<ContextProps>) => {
const dispatch = useDispatch();
const [isInitialized, setIsInitialized] = useState(false);
const previousDashboard = usePrevious(dashboard);
const previousDashboardId = usePrevious(dashboardId);
const previousTabId = usePrevious(selectedTabId);
const previousParameterValues = usePrevious(parameterValues);
const shouldRenderAsNightMode = Boolean(isNightMode && isFullscreen);
const handleAddQuestion = useCallback(() => {
if (!isEditing) {
onRefreshPeriodChange?.(null);
setEditingDashboard(dashboard);
}
toggleSidebar(SIDEBAR_NAME.addQuestion);
}, [
dashboard,
isEditing,
onRefreshPeriodChange,
setEditingDashboard,
toggleSidebar,
]);
const handleLoadDashboard = useCallback(
async (dashboardId: DashboardId) => {
initialize({ clearCache: !isNavigatingBackToDashboard });
const result = await fetchDashboard({
dashId: dashboardId,
queryParams: parameterQueryParams,
options: {
clearCache: !isNavigatingBackToDashboard,
preserveParameters: isNavigatingBackToDashboard,
},
});
if (!isSuccessfulFetchDashboardResult(result)) {
if (isFailedFetchDashboardResult(result)) {
onError?.(result);
}
return;
}
onLoad?.(result);
},
[
fetchDashboard,
initialize,
isNavigatingBackToDashboard,
onError,
onLoad,
parameterQueryParams,
],
);
useEffect(() => {
const hasDashboardChanged = dashboardId !== previousDashboardId;
if (hasDashboardChanged) {
handleLoadDashboard(dashboardId).then(() => setIsInitialized(true));
return;
}
if (!dashboard) {
return;
}
const hasDashboardLoaded = !previousDashboard;
const hasTabChanged = selectedTabId !== previousTabId;
const hasParameterValueChanged = !isEqual(
parameterValues,
previousParameterValues,
);
if (hasDashboardLoaded) {
fetchDashboardCardData({ reload: false, clearCache: true });
} else if (hasTabChanged || hasParameterValueChanged) {
fetchDashboardCardData();
}
}, [
dashboard,
dashboardId,
fetchDashboardCardData,
handleLoadDashboard,
isInitialized,
parameterValues,
previousDashboard,
previousDashboardId,
previousParameterValues,
previousTabId,
selectedTabId,
]);
useUnmount(() => {
cancelFetchDashboardCardData();
});
useUnmount(() => {
reset();
closeDashboard();
});
return (
<DashboardContext.Provider
value={{
dashboardId,
parameterQueryParams,
onLoad,
onError,
isLoading: !dashboard,
handleAddQuestion,
isFullscreen,
onFullscreenChange,
hasNightModeToggle,
onNightModeChange,
isNightMode,
shouldRenderAsNightMode,
refreshPeriod,
setRefreshElapsedHook,
onRefreshPeriodChange,
background,
bordered,
titled,
font,
theme,
setTheme,
hideParameters,
downloadsEnabled,
autoScrollToDashcardId,
reportAutoScrolledToDashcard,
// redux selectors
dashboard,
selectedTabId,
isEditing,
isNavigatingBackToDashboard,
parameterValues,
// redux actions
addCardToDashboard,
cancelFetchDashboardCardData,
fetchDashboard,
fetchDashboardCardData,
initialize,
setEditingDashboard,
toggleSidebar,
reset,
closeDashboard,
navigateToNewCardFromDashboard:
_navigateToNewCardFromDashboard ??
(() => dispatch(navigateToNewCardFromDashboard())),
...reduxProps,
}}
>
{children}
</DashboardContext.Provider>
);
};
export const DashboardContextProvider = connector(
DashboardContextProviderInner,
);
export function useDashboardContext() {
const context = useContext(DashboardContext);
if (context === undefined) {
throw new Error(
"useDashboardContext must be used within a DashboardContextProvider",
);
}
return context;
}
function isSuccessfulFetchDashboardResult(
result: FetchDashboardResult,
): result is SuccessfulFetchDashboardResult {
const hasError = "error" in result;
return !hasError;
}
export function isFailedFetchDashboardResult(
result: FetchDashboardResult,
): result is FailedFetchDashboardResult {
return isObject(result.payload) && "error" in result;
}

View File

@@ -0,0 +1 @@
export * from "./context";

View File

@@ -0,0 +1,145 @@
import type { ConnectedProps } from "react-redux";
import { push } from "react-router-redux";
import { deletePermanently } from "metabase/archive/actions";
import { connect } from "metabase/lib/redux";
import { closeNavbar, setErrorPage } from "metabase/redux/app";
import { dismissUndo } from "metabase/redux/undo";
import { getIsNavbarOpen } from "metabase/selectors/app";
import {
canManageSubscriptions,
getUserIsAdmin,
} from "metabase/selectors/user";
import type { State } from "metabase-types/store";
import {
addCardToDashboard,
addHeadingDashCardToDashboard,
addLinkDashCardToDashboard,
addMarkdownDashCardToDashboard,
cancelFetchDashboardCardData,
closeDashboard,
closeSidebar,
fetchDashboard,
fetchDashboardCardData,
hideAddParameterPopover,
initialize,
moveDashboardToCollection,
onReplaceAllDashCardVisualizationSettings,
onUpdateDashCardColumnSettings,
onUpdateDashCardVisualizationSettings,
removeParameter,
reset,
setArchivedDashboard,
setDashboardAttributes,
setEditingDashboard,
setParameterDefaultValue,
setParameterFilteringParameters,
setParameterIsMultiSelect,
setParameterName,
setParameterQueryType,
setParameterRequired,
setParameterSourceConfig,
setParameterSourceType,
setParameterTemporalUnits,
setParameterType,
setSharing,
setSidebar,
showAddParameterPopover,
toggleSidebar,
updateDashboardAndCards,
} from "../actions";
import {
getClickBehaviorSidebarDashcard,
getDashboardBeforeEditing,
getDashboardComplete,
getDocumentTitle,
getFavicon,
getIsAddParameterPopoverOpen,
getIsAdditionalInfoVisible,
getIsDashCardsLoadingComplete,
getIsDashCardsRunning,
getIsDirty,
getIsEditing,
getIsEditingParameter,
getIsHeaderVisible,
getIsNavigatingBackToDashboard,
getIsSharing,
getLoadingStartTime,
getParameterValues,
getSelectedTabId,
getSidebar,
getSlowCards,
} from "../selectors";
const mapStateToProps = (state: State) => ({
canManageSubscriptions: canManageSubscriptions(state),
isAdmin: getUserIsAdmin(state),
isNavbarOpen: getIsNavbarOpen(state),
isEditing: getIsEditing(state),
isSharing: getIsSharing(state),
dashboardBeforeEditing: getDashboardBeforeEditing(state),
isEditingParameter: getIsEditingParameter(state),
isDirty: getIsDirty(state),
dashboard: getDashboardComplete(state),
slowCards: getSlowCards(state),
parameterValues: getParameterValues(state),
loadingStartTime: getLoadingStartTime(state),
clickBehaviorSidebarDashcard: getClickBehaviorSidebarDashcard(state),
isAddParameterPopoverOpen: getIsAddParameterPopoverOpen(state),
sidebar: getSidebar(state),
pageFavicon: getFavicon(state),
documentTitle: getDocumentTitle(state),
isRunning: getIsDashCardsRunning(state),
isLoadingComplete: getIsDashCardsLoadingComplete(state),
isHeaderVisible: getIsHeaderVisible(state),
isAdditionalInfoVisible: getIsAdditionalInfoVisible(state),
selectedTabId: getSelectedTabId(state),
isNavigatingBackToDashboard: getIsNavigatingBackToDashboard(state),
});
const mapDispatchToProps = {
initialize,
cancelFetchDashboardCardData,
addCardToDashboard,
addHeadingDashCardToDashboard,
addMarkdownDashCardToDashboard,
addLinkDashCardToDashboard,
setEditingDashboard,
setDashboardAttributes,
setSharing,
toggleSidebar,
closeSidebar,
closeNavbar,
setErrorPage,
setParameterName,
setParameterType,
setParameterDefaultValue,
setParameterRequired,
setParameterTemporalUnits,
setParameterIsMultiSelect,
setParameterQueryType,
setParameterSourceType,
setParameterSourceConfig,
setParameterFilteringParameters,
showAddParameterPopover,
removeParameter,
onReplaceAllDashCardVisualizationSettings,
onUpdateDashCardVisualizationSettings,
onUpdateDashCardColumnSettings,
updateDashboardAndCards,
setSidebar,
hideAddParameterPopover,
fetchDashboard,
fetchDashboardCardData,
onChangeLocation: push,
reset,
closeDashboard,
dismissUndo,
setArchivedDashboard,
moveDashboardToCollection,
deletePermanently,
};
export const connector = connect(mapStateToProps, mapDispatchToProps);
export type ReduxProps = ConnectedProps<typeof connector>;

View File

@@ -6,7 +6,7 @@ import { parseHashOptions, stringifyHashOptions } from "metabase/lib/browser";
import { useDispatch } from "metabase/lib/redux";
import type { DashCardId } from "metabase-types/api";
interface UseAutoScrollToDashcardResult {
export interface UseAutoScrollToDashcardResult {
autoScrollToDashcardId: DashCardId | undefined;
reportAutoScrolledToDashcard: () => void;
}

View File

@@ -183,7 +183,7 @@ export const getDashboardById = (state: State, dashboardId: DashboardId) => {
export const getDashboardComplete = createSelector(
[getDashboard, getDashcards],
(dashboard, dashcards) => {
(dashboard, dashcards): Dashboard | null => {
if (!dashboard) {
return null;
}

View File

@@ -5,9 +5,9 @@ import type {
} from "metabase/public/lib/types";
import type { ClickActionModeGetter } from "metabase/visualizations/types";
type EmbedBackground = boolean;
export type EmbedBackground = boolean;
type EmbedTitle = boolean;
export type EmbedTitle = boolean;
export type EmbedHideParameters = string | null;
export type EmbedHideParametersControls = {

View File

@@ -36,7 +36,7 @@ export type EmbeddingParametersValues = Record<string, string>;
*/
export type EmbeddingDisplayOptions = {
font: null | string;
theme: DisplayTheme;
theme?: DisplayTheme;
background: boolean;
bordered: boolean;
titled: boolean;

View File

@@ -29,7 +29,7 @@ import { DashboardCopyModalConnected } from "metabase/dashboard/components/Dashb
import { DashboardMoveModalConnected } from "metabase/dashboard/components/DashboardMoveModal";
import { ArchiveDashboardModalConnected } from "metabase/dashboard/containers/ArchiveDashboardModal";
import { AutomaticDashboardAppConnected } from "metabase/dashboard/containers/AutomaticDashboardApp";
import { DashboardAppConnected } from "metabase/dashboard/containers/DashboardApp/DashboardApp";
import { DashboardApp } from "metabase/dashboard/containers/DashboardApp/DashboardApp";
import { ModalRoute } from "metabase/hoc/ModalRoute";
import { Route } from "metabase/hoc/Title";
import { HomePage } from "metabase/home/components/HomePage";
@@ -199,7 +199,7 @@ export const getRoutes = (store) => {
<Route
path="dashboard/:slug"
title={t`Dashboard`}
component={DashboardAppConnected}
component={DashboardApp}
>
<ModalRoute
path="move"