Compare commits

...

76 Commits

Author SHA1 Message Date
Oisin Coveney
e1097f125d Merge branch 'emb-327-ms1-add-dashboard-context' into emb-357-wrap-editabledashboard-with-dashboardcontext 2025-06-10 09:37:07 +03:00
Oisin Coveney
f7ea64feae Merge branch 'master' into emb-327-ms1-add-dashboard-context 2025-06-10 09:37:02 +03:00
Oisin Coveney
8c09b77376 Merge branch 'emb-327-ms1-add-dashboard-context' into emb-357-wrap-editabledashboard-with-dashboardcontext 2025-06-09 10:38:07 +03:00
Oisin Coveney
7c526b2cb5 Merge branch 'master' into emb-327-ms1-add-dashboard-context 2025-06-09 10:36:35 +03:00
Oisin Coveney
9746bd6b0c Merge branch 'emb-327-ms1-add-dashboard-context' into emb-357-wrap-editabledashboard-with-dashboardcontext 2025-06-06 18:06:57 +03:00
Oisin Coveney
12cfb0d84e Merge branch 'master' into emb-327-ms1-add-dashboard-context 2025-06-06 18:06:52 +03:00
Oisin Coveney
2a8cd1a265 Remove one layer of HTML to fix annoying weirdness with height 2025-06-05 13:56:20 +03:00
Oisin Coveney
9cd2e5b2d2 Merge branch 'emb-327-ms1-add-dashboard-context' into emb-357-wrap-editabledashboard-with-dashboardcontext 2025-06-05 12:11:34 +03:00
Oisin Coveney
1ddf765367 Merge branch 'master' into emb-327-ms1-add-dashboard-context 2025-06-05 12:10:02 +03:00
Oisin Coveney
1ba65e68f8 Make sure that footers don't exist 2025-06-05 11:56:01 +03:00
Oisin Coveney
7cfc2180ec Make sure that error handling is sane here, sort of 2025-06-04 17:39:54 +03:00
Oisin Coveney
71e4cb0b5b Remove duplicate line and use unknown instead of Error type 2025-06-04 15:42:22 +03:00
Oisin Coveney
4bf9e5135f Merge branch 'emb-327-ms1-add-dashboard-context' into emb-357-wrap-editabledashboard-with-dashboardcontext 2025-06-04 15:36:48 +03:00
Oisin Coveney
cbb3e0e73c Merge branch 'master' into emb-327-ms1-add-dashboard-context 2025-06-04 15:36:43 +03:00
Oisin Coveney
8437f06469 Merge branch 'emb-327-ms1-add-dashboard-context' into emb-357-wrap-editabledashboard-with-dashboardcontext 2025-06-04 14:31:26 +03:00
Oisin Coveney
0c5ed0d976 Merge branch 'master' into emb-327-ms1-add-dashboard-context 2025-06-04 14:31:22 +03:00
Oisin Coveney
1124dad9e6 Merge branch 'emb-327-ms1-add-dashboard-context' into emb-357-wrap-editabledashboard-with-dashboardcontext 2025-06-04 11:45:45 +03:00
Oisin Coveney
e51023ba5d Merge branch 'master' into emb-327-ms1-add-dashboard-context 2025-06-04 11:33:03 +03:00
Oisin Coveney
0c24f64e29 Merge branch 'emb-327-ms1-add-dashboard-context' into emb-357-wrap-editabledashboard-with-dashboardcontext 2025-05-14 13:04:41 +02:00
Oisin Coveney
e161dd291f Fix a bad merge with handleSetEditing, i think 2025-05-14 13:04:19 +02:00
Oisin Coveney
dcabdcf0c3 Fix a bad merge with handleSetEditing, i think 2025-05-14 13:03:58 +02:00
Oisin Coveney
4867d59de4 Merge branch 'emb-327-ms1-add-dashboard-context' into emb-357-wrap-editabledashboard-with-dashboardcontext 2025-05-14 12:54:30 +02:00
Oisin Coveney
e840ba5b9f Merge branch 'master' into emb-327-ms1-add-dashboard-context 2025-05-14 12:54:14 +02:00
Metabase Automation
9f2a1730c9 Update Loki Snapshots 2025-05-13 15:33:55 +00:00
Oisin Coveney
b11afefe14 That did not fix the loki issue. this should fix the loki issue 2025-05-13 16:34:12 +02:00
Oisin Coveney
f82cbb8389 Hopefully this fixes the loki issues 2025-05-13 16:12:18 +02:00
Oisin Coveney
b22c0357da Make sure that the editable dashboard tests work on the E2E component tests 2025-05-13 14:57:14 +02:00
Oisin Coveney
a95cf257e3 Fix E2E SDK test 2025-05-13 14:05:10 +02:00
Oisin Coveney
d813a09ff0 Suggestions from review 2025-05-13 12:27:28 +02:00
Oisin Coveney
a217e74028 Fix type 2025-05-13 12:15:49 +02:00
Oisin Coveney
9f5c1fcd24 Fix some click behavior tests 2025-05-13 11:54:26 +02:00
Oisin Coveney
e99ce04f08 Fix some tests 2025-05-12 18:53:29 +02:00
Oisin Coveney
02821b1b9e Merge branch 'emb-327-ms1-add-dashboard-context' into emb-357-wrap-editabledashboard-with-dashboardcontext 2025-05-12 17:45:09 +02:00
Oisin Coveney
c597ec12b6 Somehow this managed to make it past the merge 2025-05-12 17:43:23 +02:00
Oisin Coveney
a10278ebfa Fix type errors 2025-05-12 17:26:57 +02:00
Oisin Coveney
bd96ce46df Attempt to solve more tests 2025-05-12 17:14:36 +02:00
Oisin Coveney
627c2bea9b Merge branch 'emb-327-ms1-add-dashboard-context' into emb-357-wrap-editabledashboard-with-dashboardcontext 2025-05-12 16:43:56 +02:00
Oisin Coveney
65ea5efa58 Merge branch 'master' into emb-327-ms1-add-dashboard-context 2025-05-12 16:43:42 +02:00
Oisin Coveney
efd8c980c7 Make sure that entity IDs work 2025-05-12 11:35:39 +02:00
Oisin Coveney
c0fc831d88 Merge branch 'master' into emb-327-ms1-add-dashboard-context 2025-05-12 10:25:54 +02:00
Oisin Coveney
833a9a74a3 Fix global load handlers test 2025-05-09 17:39:27 +02:00
Oisin Coveney
d5360d0279 Merge branch 'emb-327-ms1-add-dashboard-context' into emb-357-wrap-editabledashboard-with-dashboardcontext 2025-05-09 17:17:40 +02:00
Oisin Coveney
3b52f1e304 Merge branch 'master' into emb-327-ms1-add-dashboard-context 2025-05-09 17:17:16 +02:00
Oisin Coveney
2bebe320ff Merge branch 'master' into emb-327-ms1-add-dashboard-context 2025-05-09 14:25:04 +02:00
Oisin Coveney
785abae9f5 Merge branch 'emb-330-wrap-publicorembeddeddashboard-with-context' into emb-357-wrap-editabledashboard-with-dashboardcontext 2025-05-09 13:43:04 +02:00
Oisin Coveney
a04d86123a Merge branch 'emb-327-ms1-add-dashboard-context' into emb-330-wrap-publicorembeddeddashboard-with-context 2025-05-09 13:42:56 +02:00
Oisin Coveney
c439fb0ba0 Wrap PublicOrEmbeddedDashboard with DashboardContext (#57337) 2025-05-09 13:41:40 +02:00
Oisin Coveney
030c73dac7 Make sure that the right error message is passed along 2025-05-09 13:22:44 +02:00
Oisin Coveney
ae888ea8a7 Merge branch 'emb-330-wrap-publicorembeddeddashboard-with-context' into emb-357-wrap-editabledashboard-with-dashboardcontext 2025-05-09 12:29:51 +02:00
Oisin Coveney
07525347bd fix another e2e test 2025-05-09 12:12:58 +02:00
Oisin Coveney
272bf21e19 fix another e2e test 2025-05-09 12:12:13 +02:00
Oisin Coveney
ede997d835 Merge branch 'emb-330-wrap-publicorembeddeddashboard-with-context' into emb-357-wrap-editabledashboard-with-dashboardcontext 2025-05-08 17:45:04 +02:00
Oisin Coveney
d70046c1b3 Try it this way 2025-05-08 17:39:40 +02:00
Oisin Coveney
99909ecd3d Merge branch 'emb-330-wrap-publicorembeddeddashboard-with-context' into emb-357-wrap-editabledashboard-with-dashboardcontext 2025-05-08 16:28:10 +02:00
Oisin Coveney
2af6be9af5 hopefully fix some more tests 2025-05-08 16:26:32 +02:00
Oisin Coveney
1d648ec6de Merge branch 'emb-330-wrap-publicorembeddeddashboard-with-context' into emb-357-wrap-editabledashboard-with-dashboardcontext 2025-05-08 15:57:16 +02:00
Oisin Coveney
d5317fe202 Merge branch 'emb-327-ms1-add-dashboard-context' into emb-330-wrap-publicorembeddeddashboard-with-context 2025-05-08 15:57:08 +02:00
Oisin Coveney
b566ecf31f Merge branch 'master' into emb-327-ms1-add-dashboard-context 2025-05-08 15:56:12 +02:00
Oisin Coveney
ea0ed6a20b suggestions from review 2025-05-08 12:21:03 +02:00
Oisin Coveney
b0b9c6ec32 Remove unnecessary changes 2025-05-08 12:12:57 +02:00
Oisin Coveney
44c2d86a6d Fix more type errors 2025-05-08 12:12:57 +02:00
Oisin Coveney
8336c80c5f Fix another type error 2025-05-08 12:12:57 +02:00
Oisin Coveney
15ef9caac6 Ensure that we have all the actions for now 2025-05-08 12:12:57 +02:00
Oisin Coveney
43926d80ed Mess fixing pt 2 2025-05-08 12:12:57 +02:00
Oisin Coveney
f8908c5904 Fix type errors 2025-05-08 12:10:04 +02:00
Oisin Coveney
e394905354 Make a bunch of changes to satisfy the tests 2025-05-08 12:10:04 +02:00
Oisin Coveney
5b94267824 Use the new loading flow for onLoad and onLoadWithoutCards 2025-05-08 12:10:03 +02:00
Oisin Coveney
cade33e44b Hopefully fix the merge mess that happened here 2025-05-08 12:10:03 +02:00
Oisin Coveney
c426dd2dae Merge branch 'master' into emb-327-ms1-add-dashboard-context 2025-05-08 11:35:40 +02:00
Oisin Coveney
754bac468c Wrap AutomaticDashboardApp with context (#57334) 2025-05-08 11:21:22 +02:00
Oisin Coveney
c7400745ba Merge branch 'master' into emb-327-ms1-add-dashboard-context 2025-05-07 11:41:12 +02:00
Oisin Coveney
deaf06ee41 Add dashboard context (#57042) 2025-05-06 19:19:00 +02:00
Oisin Coveney
e75edb8af5 Merge branch 'master' into emb-327-ms1-add-dashboard-context 2025-05-06 13:37:42 +02:00
Oisin Coveney
04f7ceb6ec kick ci 2025-05-05 14:37:44 +02:00
Oisin Coveney
97e7d0fe5c Merge branch 'master' into emb-327-ms1-add-dashboard-context 2025-05-05 14:35:31 +02:00
Oisin Coveney
de7ed1d3cb kick ci 2025-05-05 11:08:16 +02:00
44 changed files with 1494 additions and 1593 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

After

Width:  |  Height:  |  Size: 64 KiB

View File

@@ -1,129 +0,0 @@
import type { ComponentType, FC } from "react";
import type { ConnectedProps } from "react-redux";
import _ from "underscore";
import type { DashboardEventHandlersProps } from "embedding-sdk/types/dashboard";
import type { MetabasePluginsConfig } from "embedding-sdk/types/plugins";
import type { CommonStylingProps } from "embedding-sdk/types/props";
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";
import type {
DashboardFullscreenControls,
DashboardLoaderWrapperProps,
DashboardRefreshPeriodControls,
} from "metabase/dashboard/types";
import type { ParameterValues } from "metabase/embedding-sdk/types/dashboard";
import { connect } from "metabase/lib/redux";
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: ParameterValues;
downloadsEnabled?: boolean;
onNavigateToNewCardFromDashboard: (
opts: NavigateToNewCardFromDashboardOpts,
) => void;
plugins?: MetabasePluginsConfig;
} & Pick<CommonStylingProps, "className"> &
DashboardFullscreenControls &
DashboardRefreshPeriodControls &
DashboardLoaderWrapperProps &
DashboardEventHandlersProps;
const ConnectedDashboardInner = ({
dashboard,
onLoad,
onLoadWithoutCards,
onNavigateToNewCardFromDashboard,
...restProps
}: ConnectedDashboardProps & ReduxProps) => {
useDashboardLoadHandlers({ dashboard, onLoad, onLoadWithoutCards });
return (
<Dashboard
dashboard={dashboard}
{...restProps}
isNightMode={false}
onNightModeChange={_.noop}
hasNightModeToggle={false}
navigateToNewCardFromDashboard={onNavigateToNewCardFromDashboard}
autoScrollToDashcardId={undefined}
reportAutoScrolledToDashcard={_.noop}
/>
);
};
export const ConnectedDashboard = connector<
ComponentType<ConnectedDashboardProps & ReduxProps>
>(ConnectedDashboardInner) as FC<ConnectedDashboardProps>;

View File

@@ -1,8 +1,10 @@
import { useEffect } from "react";
import { t } from "ttag";
import { InteractiveAdHocQuestion } from "embedding-sdk/components/private/InteractiveAdHocQuestion";
import {
DashboardNotFoundError,
SdkError,
SdkLoader,
} from "embedding-sdk/components/private/PublicComponentWrapper";
import { StyledPublicComponentWrapper } from "embedding-sdk/components/public/InteractiveDashboard/EditableDashboard.styled";
@@ -13,18 +15,28 @@ import {
import { useSdkDispatch, useSdkSelector } from "embedding-sdk/store";
import type { DashboardEventHandlersProps } from "embedding-sdk/types/dashboard";
import type { MetabasePluginsConfig } from "embedding-sdk/types/plugins";
import { Dashboard } from "metabase/dashboard/components/Dashboard/Dashboard";
import {
DASHBOARD_EDITING_ACTIONS,
SDK_DASHBOARD_VIEW_ACTIONS,
} from "metabase/dashboard/components/DashboardHeader/DashboardHeaderButtonRow/constants";
import { getIsEditing } from "metabase/dashboard/selectors";
import {
DashboardContextProvider,
useDashboardContext,
} from "metabase/dashboard/context";
import type { MetabasePluginsConfig as InternalMetabasePluginsConfig } from "metabase/embedding-sdk/types/plugins";
import { useDashboardLoadHandlers } from "metabase/public/containers/PublicOrEmbeddedDashboard/use-dashboard-load-handlers";
import { setErrorPage } from "metabase/redux/app";
import { getErrorPage } from "metabase/selectors/app";
import { getEmbeddingMode } from "metabase/visualizations/click-actions/lib/modes";
import { EmbeddingSdkMode } from "metabase/visualizations/click-actions/modes/EmbeddingSdkMode";
import type { DrillThroughQuestionProps } from "../InteractiveQuestion/InteractiveQuestion";
import { ConnectedDashboard } from "./ConnectedDashboard";
import { InteractiveDashboardProvider } from "./context";
import {
type InteractiveDashboardContextType,
InteractiveDashboardProvider,
} from "./context";
import { useCommonDashboardParams } from "./use-common-dashboard-params";
/**
@@ -50,6 +62,28 @@ export type EditableDashboardProps = {
} & Omit<SdkDashboardDisplayProps, "withTitle" | "hiddenParameters"> &
DashboardEventHandlersProps;
const EditableDashboardInner = ({
drillThroughQuestionProps,
onEditQuestion,
}: Pick<InteractiveDashboardContextType, "onEditQuestion"> &
Pick<EditableDashboardProps, "drillThroughQuestionProps">) => {
const { isEditing } = useDashboardContext();
const dashboardActions = isEditing
? DASHBOARD_EDITING_ACTIONS
: SDK_DASHBOARD_VIEW_ACTIONS;
return (
<InteractiveDashboardProvider
plugins={drillThroughQuestionProps?.plugins}
onEditQuestion={onEditQuestion}
dashboardActions={dashboardActions}
>
<Dashboard />
</InteractiveDashboardProvider>
);
};
/**
* A dashboard component with the features available in the `InteractiveDashboard` component, as well as the ability to add and update questions, layout, and content within your dashboard.
*
@@ -73,7 +107,13 @@ export const EditableDashboard = ({
plugins: plugins,
},
}: EditableDashboardProps) => {
const { handleLoad, handleLoadWithoutCards } = useDashboardLoadHandlers({
onLoad,
onLoadWithoutCards,
});
const {
displayOptions,
ref,
isFullscreen,
onFullscreenChange,
@@ -99,11 +139,6 @@ export const EditableDashboard = ({
dashboardId,
});
const isEditing = useSdkSelector(getIsEditing);
const dashboardActions = isEditing
? DASHBOARD_EDITING_ACTIONS
: SDK_DASHBOARD_VIEW_ACTIONS;
const errorPage = useSdkSelector(getErrorPage);
const dispatch = useSdkDispatch();
useEffect(() => {
@@ -113,44 +148,78 @@ export const EditableDashboard = ({
}, [dispatch, dashboardId]);
if (isLoading) {
return <SdkLoader />;
return (
<StyledPublicComponentWrapper className={className} style={style}>
<SdkLoader />
</StyledPublicComponentWrapper>
);
}
if (!dashboardId || errorPage?.status === 404) {
return <DashboardNotFoundError id={initialDashboardId} />;
return (
<StyledPublicComponentWrapper className={className} style={style}>
<DashboardNotFoundError id={initialDashboardId} />
</StyledPublicComponentWrapper>
);
}
if (errorPage) {
return (
<StyledPublicComponentWrapper
className={className}
style={style}
ref={ref}
>
<SdkError
message={errorPage.data?.message ?? t`Something's gone wrong`}
/>
</StyledPublicComponentWrapper>
);
}
return (
<StyledPublicComponentWrapper className={className} style={style} ref={ref}>
{adhocQuestionUrl ? (
<InteractiveAdHocQuestion
questionPath={adhocQuestionUrl}
onNavigateBack={onNavigateBackToDashboard}
{...drillThroughQuestionProps}
/>
) : (
<InteractiveDashboardProvider
plugins={plugins}
onEditQuestion={onEditQuestion}
dashboardActions={dashboardActions}
>
<ConnectedDashboard
dashboardId={dashboardId}
isLoading={isLoading}
parameterQueryParams={initialParameters}
refreshPeriod={refreshPeriod}
onRefreshPeriodChange={onRefreshPeriodChange}
setRefreshElapsedHook={setRefreshElapsedHook}
isFullscreen={isFullscreen}
onFullscreenChange={onFullscreenChange}
noLoaderWrapper
onNavigateToNewCardFromDashboard={onNavigateToNewCardFromDashboard}
downloadsEnabled={withDownloads}
onLoad={onLoad}
onLoadWithoutCards={onLoadWithoutCards}
<DashboardContextProvider
dashboardId={dashboardId}
parameterQueryParams={initialParameters}
refreshPeriod={refreshPeriod}
onRefreshPeriodChange={onRefreshPeriodChange}
setRefreshElapsedHook={setRefreshElapsedHook}
isFullscreen={isFullscreen}
onFullscreenChange={onFullscreenChange}
navigateToNewCardFromDashboard={onNavigateToNewCardFromDashboard}
downloadsEnabled={displayOptions.downloadsEnabled}
background={displayOptions.background}
bordered={displayOptions.bordered}
hideParameters={displayOptions.hideParameters}
titled={displayOptions.titled}
cardTitled={displayOptions.cardTitled}
theme={displayOptions.theme}
onLoad={handleLoad}
onLoadWithoutCards={handleLoadWithoutCards}
onError={(error) => dispatch(setErrorPage(error))}
getClickActionMode={({ question }) =>
getEmbeddingMode({
question,
queryMode: EmbeddingSdkMode,
plugins:
drillThroughQuestionProps.plugins as InternalMetabasePluginsConfig,
})
}
>
{adhocQuestionUrl ? (
<InteractiveAdHocQuestion
questionPath={adhocQuestionUrl}
onNavigateBack={onNavigateBackToDashboard}
{...drillThroughQuestionProps}
/>
</InteractiveDashboardProvider>
)}
) : (
<EditableDashboardInner
drillThroughQuestionProps={drillThroughQuestionProps}
onEditQuestion={onEditQuestion}
/>
)}
</DashboardContextProvider>
</StyledPublicComponentWrapper>
);
};

View File

@@ -4,11 +4,13 @@ import {
useCallback,
useEffect,
} from "react";
import { t } from "ttag";
import _ from "underscore";
import { InteractiveAdHocQuestion } from "embedding-sdk/components/private/InteractiveAdHocQuestion";
import {
DashboardNotFoundError,
SdkError,
SdkLoader,
} from "embedding-sdk/components/private/PublicComponentWrapper";
import { renderOnlyInSdkProvider } from "embedding-sdk/components/private/SdkContext";
@@ -25,6 +27,7 @@ import { DASHBOARD_DISPLAY_ACTIONS } from "metabase/dashboard/components/Dashboa
import { useEmbedTheme } from "metabase/dashboard/hooks";
import type { MetabasePluginsConfig as InternalMetabasePluginsConfig } from "metabase/embedding-sdk/types/plugins";
import { PublicOrEmbeddedDashboard } from "metabase/public/containers/PublicOrEmbeddedDashboard/PublicOrEmbeddedDashboard";
import { useDashboardLoadHandlers } from "metabase/public/containers/PublicOrEmbeddedDashboard/use-dashboard-load-handlers";
import { setErrorPage } from "metabase/redux/app";
import { getErrorPage } from "metabase/selectors/app";
import { getEmbeddingMode } from "metabase/visualizations/click-actions/lib/modes";
@@ -85,6 +88,11 @@ const InteractiveDashboardInner = ({
},
renderDrillThroughQuestion: AdHocQuestionView,
}: InteractiveDashboardProps) => {
const { handleLoad, handleLoadWithoutCards } = useDashboardLoadHandlers({
onLoad,
onLoadWithoutCards,
});
const {
displayOptions,
ref,
@@ -147,6 +155,19 @@ const InteractiveDashboardInner = ({
);
}
if (errorPage) {
return (
<StyledPublicComponentWrapper
className={className}
style={style}
ref={ref}
>
<SdkError
message={errorPage.data?.message ?? t`Something's gone wrong`}
/>
</StyledPublicComponentWrapper>
);
}
return (
<StyledPublicComponentWrapper className={className} style={style} ref={ref}>
{adhocQuestionUrl ? (
@@ -180,8 +201,9 @@ const InteractiveDashboardInner = ({
setRefreshElapsedHook={setRefreshElapsedHook}
bordered={displayOptions.bordered}
navigateToNewCardFromDashboard={onNavigateToNewCardFromDashboard}
onLoad={onLoad}
onLoadWithoutCards={onLoadWithoutCards}
onLoad={handleLoad}
onLoadWithoutCards={handleLoadWithoutCards}
onError={(error) => dispatch(setErrorPage(error))}
downloadsEnabled={{ pdf: withDownloads, results: withDownloads }}
isNightMode={false}
onNightModeChange={_.noop}

View File

@@ -13,7 +13,7 @@ import type { MetabasePluginsConfig } from "embedding-sdk/types/plugins";
import type { DashboardActionKey } from "metabase/dashboard/components/DashboardHeader/DashboardHeaderButtonRow/types";
import type Question from "metabase-lib/v1/Question";
type InteractiveDashboardContextType = Partial<{
export type InteractiveDashboardContextType = Partial<{
plugins: MetabasePluginsConfig;
dashboardActions: DashboardActionKey[];
onEditQuestion?: (question: Question) => void;

View File

@@ -16,11 +16,14 @@ import type { DashboardEventHandlersProps } from "embedding-sdk/types/dashboard"
import CS from "metabase/css/core/index.css";
import { useEmbedTheme } from "metabase/dashboard/hooks";
import type { EmbedDisplayParams } from "metabase/dashboard/types";
import { useValidatedEntityId } from "metabase/lib/entity-id/hooks/use-validated-entity-id";
import { PublicOrEmbeddedDashboard } from "metabase/public/containers/PublicOrEmbeddedDashboard/PublicOrEmbeddedDashboard";
import { useDashboardLoadHandlers } from "metabase/public/containers/PublicOrEmbeddedDashboard/use-dashboard-load-handlers";
import { setErrorPage } from "metabase/redux/app";
import { getErrorPage } from "metabase/selectors/app";
import { Box } from "metabase/ui";
import { getEmbeddingMode } from "metabase/visualizations/click-actions/lib/modes";
import { StaticQuestionSdkMode } from "../StaticQuestion/mode";
/**
* @interface
@@ -31,7 +34,7 @@ export type StaticDashboardProps = SdkDashboardDisplayProps &
DashboardEventHandlersProps;
export const StaticDashboardInner = ({
dashboardId,
dashboardId: initialDashboardId,
initialParameters = {},
withTitle = true,
withCardTitle = true,
@@ -42,6 +45,13 @@ export const StaticDashboardInner = ({
style,
className,
}: StaticDashboardProps) => {
const errorPage = useSdkSelector(getErrorPage);
const { handleLoad, handleLoadWithoutCards } = useDashboardLoadHandlers({
onLoad,
onLoadWithoutCards,
});
const {
displayOptions,
ref,
@@ -50,8 +60,10 @@ export const StaticDashboardInner = ({
refreshPeriod,
onRefreshPeriodChange,
setRefreshElapsedHook,
} = useSdkDashboardParams({
dashboardId,
isLoading,
} = useSdkDashboardParams({
dashboardId: initialDashboardId,
initialParameters,
withTitle,
withDownloads,
@@ -60,6 +72,21 @@ export const StaticDashboardInner = ({
const { theme } = useEmbedTheme();
const dispatch = useSdkDispatch();
useEffect(() => {
if (dashboardId) {
dispatch(setErrorPage(null));
}
}, [dashboardId, dispatch]);
if (isLoading) {
return <SdkLoader />;
}
if (!dashboardId || errorPage?.status === 404) {
return <DashboardNotFoundError id={initialDashboardId} />;
}
return (
<Box
w="100%"
@@ -81,13 +108,18 @@ export const StaticDashboardInner = ({
onRefreshPeriodChange={onRefreshPeriodChange}
setRefreshElapsedHook={setRefreshElapsedHook}
bordered={displayOptions.bordered}
onLoad={onLoad}
onLoadWithoutCards={onLoadWithoutCards}
onLoad={handleLoad}
onLoadWithoutCards={handleLoadWithoutCards}
onError={(error) => dispatch(setErrorPage(error))}
downloadsEnabled={{ pdf: withDownloads, results: withDownloads }}
isNightMode={false}
onNightModeChange={_.noop}
hasNightModeToggle={false}
withFooter={false}
getClickActionMode={({ question }) =>
getEmbeddingMode({ question, queryMode: StaticQuestionSdkMode })
}
navigateToNewCardFromDashboard={null}
/>
</Box>
);
@@ -99,31 +131,7 @@ export const StaticDashboardInner = ({
* @function
* @category StaticDashboard
*/
const StaticDashboard = withPublicComponentWrapper<StaticDashboardProps>(
({ dashboardId: initialDashboardId, ...rest }) => {
const { isLoading, id: resolvedDashboardId } = useValidatedEntityId({
type: "dashboard",
id: initialDashboardId,
});
const errorPage = useSdkSelector(getErrorPage);
const dispatch = useSdkDispatch();
useEffect(() => {
if (resolvedDashboardId) {
dispatch(setErrorPage(null));
}
}, [dispatch, resolvedDashboardId]);
if (isLoading) {
return <SdkLoader />;
}
if (!resolvedDashboardId || errorPage?.status === 404) {
return <DashboardNotFoundError id={initialDashboardId} />;
}
return <StaticDashboardInner dashboardId={resolvedDashboardId} {...rest} />;
},
);
const StaticDashboard =
withPublicComponentWrapper<StaticDashboardProps>(StaticDashboardInner);
export { EmbedDisplayParams, StaticDashboard };

View File

@@ -25,7 +25,6 @@ export const setEditingDashboard = (dashboard: Dashboard | null) => {
if (dashboard === null) {
const location = getLocation(getState());
const locationWithoutEditHash = { ...location, hash: "" };
dispatch(push(locationWithoutEditHash));
}

View File

@@ -20,6 +20,7 @@ import { color } from "metabase/lib/colors";
import { useSelector, useStore } from "metabase/lib/redux";
import { PLUGIN_COLLECTIONS } from "metabase/plugins";
import EmbedFrameS from "metabase/public/components/EmbedFrame/EmbedFrame.module.css";
import type { EmbedResourceDownloadOptions } from "metabase/public/lib/types";
import { Box } from "metabase/ui";
import { getVisualizationRaw } from "metabase/visualizations";
import { extendCardWithDashcardSettings } from "metabase/visualizations/lib/settings/typed-utils";
@@ -81,9 +82,9 @@ export interface DashCardProps {
onRemove: (dashcard: StoreDashcard) => void;
onReplaceCard: (dashcard: StoreDashcard) => void;
markNewCardSeen: (dashcardId: DashCardId) => void;
navigateToNewCardFromDashboard?: (
opts: NavigateToNewCardFromDashboardOpts,
) => void;
navigateToNewCardFromDashboard:
| ((opts: NavigateToNewCardFromDashboardOpts) => void)
| null;
onReplaceAllDashCardVisualizationSettings: (
dashcardId: DashCardId,
settings: VisualizationSettings,
@@ -95,7 +96,7 @@ export interface DashCardProps {
showClickBehaviorSidebar: (dashcardId: DashCardId | null) => void;
onChangeLocation: (location: LocationDescriptor) => void;
downloadsEnabled: boolean;
downloadsEnabled: EmbedResourceDownloadOptions;
/** Auto-scroll to this card on mount */
autoScroll: boolean;

View File

@@ -18,6 +18,7 @@ import { getParameterValuesBySlugMap } from "metabase/dashboard/selectors";
import { useUserKeyValue } from "metabase/hooks/use-user-key-value";
import { useStore } from "metabase/lib/redux";
import { exportFormatPng, exportFormats } from "metabase/lib/urls";
import type { EmbedResourceDownloadOptions } from "metabase/public/lib/types";
import { QuestionDownloadWidget } from "metabase/query_builder/components/QuestionDownloadWidget";
import { useDownloadData } from "metabase/query_builder/components/QuestionDownloadWidget/use-download-data";
import {
@@ -48,7 +49,7 @@ interface DashCardMenuProps {
uuid?: string;
token?: string;
visualizationSettings?: VisualizationSettings;
downloadsEnabled: boolean;
downloadsEnabled: EmbedResourceDownloadOptions;
onEditVisualization?: () => void;
openUnderlyingQuestionItems?: React.ReactNode;
}
@@ -218,7 +219,7 @@ interface ShouldRenderDashcardMenuProps {
/** If public sharing or static/public embed */
isPublicOrEmbedded?: boolean;
isEditing: boolean;
downloadsEnabled: boolean;
downloadsEnabled: EmbedResourceDownloadOptions;
}
DashCardMenu.shouldRender = ({
@@ -236,7 +237,7 @@ DashCardMenu.shouldRender = ({
);
if (isPublicOrEmbedded) {
return downloadsEnabled && !!result?.data && !result?.error;
return downloadsEnabled.results && !!result?.data && !result?.error;
}
return (
!isInternalQuery &&

View File

@@ -119,7 +119,11 @@ const setup = ({ card = TEST_CARD, result = TEST_RESULT }: SetupOpts = {}) => {
<Route
path="dashboard/:slug"
component={() => (
<DashCardMenu question={question} result={result} downloadsEnabled />
<DashCardMenu
question={question}
result={result}
downloadsEnabled={{ results: true }}
/>
)}
/>
<Route path="question/:slug" component={() => <div />} />

View File

@@ -15,6 +15,7 @@ import {
import { useSelector } from "metabase/lib/redux";
import { isJWT } from "metabase/lib/utils";
import { isUuid } from "metabase/lib/uuid";
import type { EmbedResourceDownloadOptions } from "metabase/public/lib/types";
import { getMetadata } from "metabase/selectors/metadata";
import { Flex, type IconName, type IconProps, Menu, Title } from "metabase/ui";
import { getVisualizationRaw, isCartesianChart } from "metabase/visualizations";
@@ -104,7 +105,7 @@ interface DashCardVisualizationProps {
onChangeLocation: (location: LocationDescriptor) => void;
onTogglePreviewing: () => void;
downloadsEnabled: boolean;
downloadsEnabled: EmbedResourceDownloadOptions;
onEditVisualization?: () => void;
}
@@ -318,7 +319,8 @@ export function DashCardVisualization({
[dashcard],
);
const uuid = useMemo(
() => (isUuid(dashcard.dashboard_id) ? dashcard.dashboard_id : undefined),
() =>
isUuid(dashcard.dashboard_id) ? String(dashcard.dashboard_id) : undefined,
[dashcard],
);
@@ -382,14 +384,6 @@ export function DashCardVisualization({
return null;
}
const token = isJWT(dashcard.dashboard_id)
? String(dashcard.dashboard_id)
: undefined;
const uuid = isUuid(dashcard.dashboard_id)
? dashcard.dashboard_id
: undefined;
// Only show the download button if the dashboard is public or embedded.
if (isPublicOrEmbedded && downloadsEnabled) {
return (
@@ -429,10 +423,11 @@ export function DashCardVisualization({
isXray,
isPublicOrEmbedded,
isEditing,
dashcard.id,
dashcard.dashboard_id,
dashboard.id,
downloadsEnabled,
dashcard.id,
dashboard.id,
token,
uuid,
onEditVisualization,
titleMenuItems,
]);

View File

@@ -100,7 +100,7 @@ function setup({
onUpdateVisualizationSettings={jest.fn()}
showClickBehaviorSidebar={jest.fn()}
onChangeLocation={jest.fn()}
downloadsEnabled
downloadsEnabled={{ results: true }}
autoScroll={false}
reportAutoScrolledToDashcard={jest.fn()}
onEditVisualization={jest.fn()}

View File

@@ -1,54 +1,24 @@
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 { useCallback, useMemo } from "react";
import { t } from "ttag";
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 ColorS from "metabase/css/core/colors.module.css";
import DashboardS from "metabase/css/dashboard.module.css";
import { DashboardHeader } from "metabase/dashboard/components/DashboardHeader";
import type {
CancelledFetchDashboardResult,
DashboardDisplayOptionControls,
FetchDashboardResult,
SuccessfulFetchDashboardResult,
} from "metabase/dashboard/types";
import { useDashboardContext } from "metabase/dashboard/context";
import Bookmarks from "metabase/entities/bookmarks";
import Dashboards from "metabase/entities/dashboards";
import { useDispatch } from "metabase/lib/redux";
import ParametersS from "metabase/parameters/components/ParameterValueWidget.module.css";
import { getHasDataAccess, getHasNativeWrite } from "metabase/selectors/data";
import { FullWidthContainer } from "metabase/styled-components/layout/FullWidthContainer";
import { Box, Flex } from "metabase/ui";
import { Box, Flex, Loader, Stack, Text } 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 { 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 { DashboardGridConnected } from "../DashboardGrid";
@@ -56,161 +26,69 @@ import { DashboardParameterPanel } from "../DashboardParameterPanel";
import { DashboardSidebars } from "../DashboardSidebars";
import S from "./Dashboard.module.css";
import { DashboardLoadingAndErrorWrapper } from "./DashboardComponents";
import {
DashboardEmptyState,
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;
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,
parameterQueryParams = {},
downloadsEnabled,
shouldRenderAsNightMode,
setArchivedDashboard,
moveDashboardToCollection,
deletePermanently,
dashboardBeforeEditing,
onFullscreenChange,
isAdditionalInfoVisible,
hasNightModeToggle,
onNightModeChange,
refreshPeriod,
setRefreshElapsedHook,
isEditingParameter,
slowCards,
navigateToNewCardFromDashboard,
removeParameter,
addCardToDashboard,
clickBehaviorSidebarDashcard,
onReplaceAllDashCardVisualizationSettings,
onUpdateDashCardVisualizationSettings,
onUpdateDashCardColumnSettings,
setParameterName,
setParameterType,
setParameterDefaultValue,
setParameterIsMultiSelect,
setParameterQueryType,
setParameterSourceType,
setParameterSourceConfig,
setParameterFilteringParameters,
setParameterRequired,
setParameterTemporalUnits,
sidebar,
closeSidebar,
toggleSidebar,
parameterQueryParams,
downloadsEnabled = true,
noLoaderWrapper = false,
} = props;
setEditingDashboard,
} = 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)) {
@@ -224,9 +102,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;
@@ -242,8 +117,6 @@ function Dashboard(props: DashboardProps) {
);
const canCreateQuestions = hasDataAccess || hasNativeWrite;
const shouldRenderAsNightMode = isNightMode && isFullscreen;
const handleSetEditing = useCallback(
(dashboard: IDashboard | null) => {
if (!isEditing) {
@@ -259,108 +132,6 @@ function Dashboard(props: DashboardProps) {
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 ? (
@@ -397,179 +168,157 @@ function Dashboard(props: DashboardProps) {
}
};
if (!dashboard) {
return (
<Stack justify="center" align="center" gap="sm" mt="xl">
<Loader size="lg" />
<Text c="text-light" size="xl">{t`Loading…`}</Text>
</Stack>
);
}
const isEmpty = !dashboardHasCards || (dashboardHasCards && !tabHasCards);
const isFullHeight = isEditing || isSharing;
return (
<DashboardLoadingAndErrorWrapper
isFullHeight={isEditing || isSharing}
isFullscreen={isFullscreen}
isNightMode={shouldRenderAsNightMode}
loading={!dashboard}
error={error}
noWrapper={noLoaderWrapper}
<Flex
className={cx(DashboardS.Dashboard, S.DashboardLoadingAndErrorWrapper, {
[DashboardS.DashboardFullscreen]: isFullscreen,
[DashboardS.DashboardNight]: shouldRenderAsNightMode,
[ParametersS.DashboardNight]: shouldRenderAsNightMode,
[ColorS.DashboardNight]: shouldRenderAsNightMode,
[S.isFullHeight]: isFullHeight,
})}
direction="column"
mih="100%"
w="100%"
flex="1 0 auto"
>
{() => {
if (!dashboard) {
return null;
}
{dashboard.archived && (
<ArchivedEntityBanner
name={dashboard.name}
entityType="dashboard"
canMove={canWrite}
canRestore={canRestore}
canDelete={canDelete}
onUnarchive={async () => {
await setArchivedDashboard(false);
await invalidateBookmarks();
}}
onMove={({ id }) => moveDashboardToCollection({ id })}
onDeletePermanently={() => {
const { id } = dashboard;
const deleteAction = Dashboards.actions.delete({ id });
deletePermanently(deleteAction);
}}
/>
)}
const isEmpty =
!dashboardHasCards || (dashboardHasCards && !tabHasCards);
<Box
component="header"
className={cx(S.DashboardHeaderContainer, {
[S.isFullscreen]: isFullscreen,
[S.isNightMode]: shouldRenderAsNightMode,
})}
data-element-id="dashboard-header-container"
data-testid="dashboard-header-container"
>
{/**
* Do not conditionally render `<DashboardHeader />` as it calls
* `useDashboardTabs` under the hood. This hook sets `selectedTabId`
* in Redux state which kicks off a fetch for the dashboard cards.
*/}
<DashboardHeader
parameterQueryParams={parameterQueryParams}
dashboard={dashboard}
isNightMode={shouldRenderAsNightMode}
isFullscreen={isFullscreen}
onRefreshPeriodChange={onRefreshPeriodChange}
dashboardBeforeEditing={dashboardBeforeEditing}
onFullscreenChange={onFullscreenChange}
isAdditionalInfoVisible={isAdditionalInfoVisible}
hasNightModeToggle={hasNightModeToggle}
onNightModeChange={onNightModeChange}
refreshPeriod={refreshPeriod}
setRefreshElapsedHook={setRefreshElapsedHook}
/>
</Box>
return (
<Flex direction="column" mih="100%" w="100%" flex="1 0 auto">
{dashboard.archived && (
<ArchivedEntityBanner
name={dashboard.name}
entityType="dashboard"
canMove={canWrite}
canRestore={canRestore}
canDelete={canDelete}
onUnarchive={async () => {
await dispatch(setArchivedDashboard(false));
await dispatch(Bookmarks.actions.invalidateLists());
}}
onMove={({ id }) => dispatch(moveDashboardToCollection({ id }))}
onDeletePermanently={() => {
const { id } = dashboard;
const deleteAction = Dashboards.actions.delete({ id });
dispatch(deletePermanently(deleteAction));
}}
/>
)}
<Box
component="header"
className={cx(S.DashboardHeaderContainer, {
[S.isFullscreen]: isFullscreen,
[S.isNightMode]: shouldRenderAsNightMode,
})}
data-element-id="dashboard-header-container"
data-testid="dashboard-header-container"
<Flex
pos="relative"
miw={0}
mih={0}
className={cx(S.DashboardBody, {
[S.isEditingOrSharing]: isEditing || isSharing,
})}
>
<Box
className={cx(S.ParametersAndCardsContainer, {
[S.shouldMakeDashboardHeaderStickyAfterScrolling]:
!isFullscreen && (isEditing || isSharing),
[S.notEmpty]: !isEmpty,
})}
id={DASHBOARD_PDF_EXPORT_ROOT_ID}
data-element-id="dashboard-parameters-and-cards"
data-testid="dashboard-parameters-and-cards"
>
<DashboardParameterPanel isFullscreen={isFullscreen} />
{isEmpty ? (
renderEmptyStates()
) : (
<FullWidthContainer
className={S.CardsContainer}
data-element-id="dashboard-cards-container"
>
{/**
* Do not conditionally render `<DashboardHeader />` as it calls
* `useDashboardTabs` under the hood. This hook sets `selectedTabId`
* in Redux state which kicks off a fetch for the dashboard cards.
*/}
<DashboardHeader
parameterQueryParams={parameterQueryParams}
dashboard={dashboard}
<DashboardGridConnected
clickBehaviorSidebarDashcard={clickBehaviorSidebarDashcard}
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}
/>
</Box>
<Flex
pos="relative"
miw={0}
mih={0}
className={cx(S.DashboardBody, {
[S.isEditingOrSharing]: isEditing || isSharing,
})}
>
<Box
className={cx(S.ParametersAndCardsContainer, {
[S.shouldMakeDashboardHeaderStickyAfterScrolling]:
!isFullscreen && (isEditing || isSharing),
[S.notEmpty]: !isEmpty,
})}
id={DASHBOARD_PDF_EXPORT_ROOT_ID}
data-element-id="dashboard-parameters-and-cards"
data-testid="dashboard-parameters-and-cards"
>
<DashboardParameterPanel isFullscreen={isFullscreen} />
{isEmpty ? (
renderEmptyStates()
) : (
<FullWidthContainer
className={S.CardsContainer}
data-element-id="dashboard-cards-container"
>
<DashboardGridConnected
clickBehaviorSidebarDashcard={
props.clickBehaviorSidebarDashcard
}
isNightMode={shouldRenderAsNightMode}
isFullscreen={props.isFullscreen}
isEditingParameter={props.isEditingParameter}
isEditing={props.isEditing}
dashboard={dashboard}
slowCards={props.slowCards}
navigateToNewCardFromDashboard={
props.navigateToNewCardFromDashboard
}
selectedTabId={selectedTabId}
downloadsEnabled={downloadsEnabled}
autoScrollToDashcardId={autoScrollToDashcardId}
reportAutoScrolledToDashcard={
reportAutoScrolledToDashcard
}
handleSetEditing={handleSetEditing}
/>
</FullWidthContainer>
)}
</Box>
<DashboardSidebars
isEditingParameter={isEditingParameter}
isEditing={isEditing}
dashboard={dashboard}
removeParameter={props.removeParameter}
addCardToDashboard={props.addCardToDashboard}
clickBehaviorSidebarDashcard={
props.clickBehaviorSidebarDashcard
}
onReplaceAllDashCardVisualizationSettings={
props.onReplaceAllDashCardVisualizationSettings
}
onUpdateDashCardVisualizationSettings={
props.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}
setParameterFilteringParameters={
props.setParameterFilteringParameters
}
setParameterRequired={props.setParameterRequired}
setParameterTemporalUnits={props.setParameterTemporalUnits}
isFullscreen={props.isFullscreen}
sidebar={props.sidebar}
closeSidebar={props.closeSidebar}
slowCards={slowCards}
navigateToNewCardFromDashboard={navigateToNewCardFromDashboard}
selectedTabId={selectedTabId}
onCancel={() => setSharing(false)}
downloadsEnabled={downloadsEnabled}
autoScrollToDashcardId={autoScrollToDashcardId}
reportAutoScrolledToDashcard={reportAutoScrolledToDashcard}
handleSetEditing={handleSetEditing}
/>
</Flex>
</Flex>
);
}}
</DashboardLoadingAndErrorWrapper>
</FullWidthContainer>
)}
</Box>
<DashboardSidebars
dashboard={dashboard}
removeParameter={removeParameter}
addCardToDashboard={addCardToDashboard}
clickBehaviorSidebarDashcard={clickBehaviorSidebarDashcard}
onReplaceAllDashCardVisualizationSettings={
onReplaceAllDashCardVisualizationSettings
}
onUpdateDashCardVisualizationSettings={
onUpdateDashCardVisualizationSettings
}
onUpdateDashCardColumnSettings={onUpdateDashCardColumnSettings}
setParameterName={setParameterName}
setParameterType={setParameterType}
setParameterDefaultValue={setParameterDefaultValue}
setParameterIsMultiSelect={setParameterIsMultiSelect}
setParameterQueryType={setParameterQueryType}
setParameterSourceType={setParameterSourceType}
setParameterSourceConfig={setParameterSourceConfig}
setParameterFilteringParameters={setParameterFilteringParameters}
setParameterRequired={setParameterRequired}
setParameterTemporalUnits={setParameterTemporalUnits}
isFullscreen={isFullscreen}
sidebar={sidebar}
closeSidebar={closeSidebar}
selectedTabId={selectedTabId}
onCancel={() => setSharing(false)}
/>
</Flex>
</Flex>
);
}
function isSuccessfulFetchDashboardResult(
result: FetchDashboardResult,
): result is SuccessfulFetchDashboardResult {
const hasError = "error" in result;
return !hasError;
}
export function isCancelledFetchDashboardResult(
result: FetchDashboardResult,
): result is CancelledFetchDashboardResult {
return isObject(result.payload) && Boolean(result.payload.isCancelled);
}
export { Dashboard };

View File

@@ -1,49 +1,9 @@
import cx from "classnames";
import {
LoadingAndErrorWrapper,
type LoadingAndErrorWrapperProps,
} from "metabase/components/LoadingAndErrorWrapper";
import ColorS from "metabase/css/core/colors.module.css";
import DashboardS from "metabase/css/dashboard.module.css";
import ParametersS from "metabase/parameters/components/ParameterValueWidget.module.css";
import { Box, type BoxProps } from "metabase/ui";
import S from "./Dashboard.module.css";
interface DashboardLoadingAndErrorWrapperProps
extends LoadingAndErrorWrapperProps {
isFullscreen: boolean;
isNightMode: boolean;
isFullHeight: boolean;
}
export const DashboardLoadingAndErrorWrapper = ({
isFullscreen,
isNightMode,
isFullHeight,
className,
...props
}: DashboardLoadingAndErrorWrapperProps) => {
return (
<LoadingAndErrorWrapper
className={cx(
className,
DashboardS.Dashboard,
S.DashboardLoadingAndErrorWrapper,
{
[DashboardS.DashboardFullscreen]: isFullscreen,
[DashboardS.DashboardNight]: isNightMode,
[ParametersS.DashboardNight]: isNightMode,
[ColorS.DashboardNight]: isNightMode,
[S.isFullHeight]: isFullHeight,
},
)}
{...props}
/>
);
};
export const FIXED_WIDTH = "1048px";
export const FixedWidthContainer = (
props: BoxProps & {

View File

@@ -28,6 +28,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";
@@ -156,10 +157,10 @@ export type DashboardGridProps = {
// public dashboard passes it explicitly
width?: number;
// public or embedded dashboard passes it as noop
navigateToNewCardFromDashboard?: (
opts: NavigateToNewCardFromDashboardOpts,
) => void;
downloadsEnabled: boolean;
navigateToNewCardFromDashboard:
| ((opts: NavigateToNewCardFromDashboardOpts) => void)
| null;
downloadsEnabled: EmbedResourceDownloadOptions;
autoScrollToDashcardId?: DashCardId;
reportAutoScrolledToDashcard?: () => void;
handleSetEditing?: (dashboard: Dashboard | null) => void;
@@ -508,7 +509,7 @@ class DashboardGridInner extends Component<
isMobile: boolean;
gridItemWidth: number;
totalNumGridCols: number;
downloadsEnabled: boolean;
downloadsEnabled: EmbedResourceDownloadOptions;
shouldAutoScrollTo: boolean;
reportAutoScrolledToDashcard?: () => void;
},

View File

@@ -67,4 +67,5 @@ export const DEFAULT_DASHBOARD_DISPLAY_OPTIONS: EmbedDisplayParams = {
theme: "light",
downloadsEnabled: { pdf: true, results: true },
withFooter: true,
getClickActionMode: undefined,
};

View File

@@ -1,8 +1,6 @@
import { usePrevious } from "@mantine/hooks";
import cx from "classnames";
import { dissoc } from "icepick";
import { useEffect, useState } from "react";
import type { ConnectedProps } from "react-redux";
import type { WithRouterProps } from "react-router";
import { t } from "ttag";
import _ from "underscore";
@@ -15,24 +13,22 @@ import Button from "metabase/core/components/Button";
import Link from "metabase/core/components/Link";
import CS from "metabase/css/core/index.css";
import DashboardS from "metabase/css/dashboard.module.css";
import { navigateToNewCardFromDashboard } from "metabase/dashboard/actions";
import { DashboardGridConnected } from "metabase/dashboard/components/DashboardGrid";
import { DashboardTabs } from "metabase/dashboard/components/DashboardTabs";
import { DASHBOARD_PARAMETERS_PDF_EXPORT_NODE_ID } from "metabase/dashboard/constants";
import {
DashboardData,
type DashboardDataReturnedProps,
} from "metabase/dashboard/hoc/DashboardData";
import { getIsHeaderVisible, getTabs } from "metabase/dashboard/selectors";
import title from "metabase/hoc/Title";
import { connect, useDispatch } from "metabase/lib/redux";
DashboardContextProvider,
useDashboardContext,
} from "metabase/dashboard/context";
import { SetTitle } from "metabase/hoc/Title";
import { useDispatch } from "metabase/lib/redux";
import * as Urls from "metabase/lib/urls";
import { ParametersList } from "metabase/parameters/components/ParametersList";
import { addUndo } from "metabase/redux/undo";
import { getMetadata } from "metabase/selectors/metadata";
import { Box } from "metabase/ui";
import { getValuePopulatedParameters } from "metabase-lib/v1/parameters/utils/parameter-values";
import type { Dashboard, DashboardId } from "metabase-types/api";
import type { State } from "metabase-types/store";
import { FixedWidthContainer } from "../../components/Dashboard/DashboardComponents";
import { useDashboardUrlQuery } from "../../hooks/use-dashboard-url-query";
@@ -43,43 +39,19 @@ import { SuggestionsSidebar } from "./SuggestionsSidebar";
type AutomaticDashboardAppRouterProps = WithRouterProps<{ splat: string }>;
const getDashboardId = (
_state: State,
{ params: { splat }, location: { hash } }: AutomaticDashboardAppRouterProps,
) => `/auto/dashboard/${splat}${hash.replace(/^#?/, "?")}`;
const mapStateToProps = (
state: State,
props: AutomaticDashboardAppRouterProps,
) => ({
metadata: getMetadata(state),
dashboardId: getDashboardId(state, props),
isHeaderVisible: getIsHeaderVisible(state),
tabs: getTabs(state),
});
const connector = connect(mapStateToProps);
type ReduxProps = ConnectedProps<typeof connector>;
type AutomaticDashboardAppInnerProps = ReduxProps &
AutomaticDashboardAppRouterProps &
DashboardDataReturnedProps;
const AutomaticDashboardAppInner = ({
dashboard,
parameters,
parameterValues,
setParameterValue,
isHeaderVisible,
tabs,
selectedTabId,
slowCards,
navigateToNewCardFromDashboard,
location,
router,
}: AutomaticDashboardAppInnerProps) => {
useDashboardUrlQuery(router, location);
const AutomaticDashboardAppInner = () => {
const {
dashboard,
parameters,
parameterValues,
setParameterValue,
isHeaderVisible,
tabs,
selectedTabId,
slowCards,
navigateToNewCardFromDashboard,
downloadsEnabled,
} = useDashboardContext();
const dispatch = useDispatch();
@@ -91,17 +63,11 @@ const AutomaticDashboardAppInner = ({
null,
);
const prevPathName = usePrevious(location.pathname);
useEffect(() => {
if (prevPathName !== location.pathname) {
// scroll to the top when the pathname changes
window.scrollTo(0, 0);
// clear savedDashboardId if changing to a different dashboard
if (dashboard?.id) {
setSavedDashboardId(null);
}
}, [prevPathName, location.pathname]);
}, [dashboard?.id]);
const save = async () => {
if (dashboard) {
@@ -146,7 +112,8 @@ const AutomaticDashboardAppInner = ({
"AutomaticDashboard--withSidebar": hasSidebar,
})}
>
<div className="" style={{ marginRight: hasSidebar ? 346 : undefined }}>
{dashboard && <SetTitle title={dashboard.name} />}
<div style={{ marginRight: hasSidebar ? 346 : undefined }}>
{isHeaderVisible && (
<div
className={cx(CS.bgWhite, CS.borderBottom)}
@@ -219,11 +186,11 @@ const AutomaticDashboardAppInner = ({
selectedTabId={selectedTabId}
slowCards={slowCards}
clickBehaviorSidebarDashcard={null}
downloadsEnabled={false}
downloadsEnabled={downloadsEnabled}
autoScrollToDashcardId={undefined}
reportAutoScrolledToDashcard={_.noop}
navigateToNewCardFromDashboard={
navigateToNewCardFromDashboard ?? undefined
navigateToNewCardFromDashboard ?? null
}
/>
)
@@ -238,6 +205,7 @@ const AutomaticDashboardAppInner = ({
</div>
)}
</div>
{hasSidebar && (
<Box
className={cx(
@@ -255,13 +223,29 @@ const AutomaticDashboardAppInner = ({
);
};
export const AutomaticDashboardAppConnected = _.compose(
connector,
DashboardData,
title(
({ dashboard }: { dashboard: Dashboard }) => dashboard && dashboard.name,
),
)(AutomaticDashboardAppInner);
export const AutomaticDashboardApp = ({
router,
location,
params,
}: AutomaticDashboardAppRouterProps) => {
useDashboardUrlQuery(router, location);
const dispatch = useDispatch();
const dashboardId = `/auto/dashboard/${params.splat}${location.hash.replace(/^#?/, "?")}`;
return (
<DashboardContextProvider
dashboardId={dashboardId}
navigateToNewCardFromDashboard={(opts) =>
dispatch(navigateToNewCardFromDashboard(opts))
}
downloadsEnabled={{ pdf: false, results: false }}
>
<AutomaticDashboardAppInner />
</DashboardContextProvider>
);
};
const TransientTitle = ({ dashboard }: { dashboard: Dashboard }) =>
dashboard.transient_name ? (

View File

@@ -1,312 +1,64 @@
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 { 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,
toggleSidebar,
updateDashboardAndCards,
} from "metabase/dashboard/actions";
import { Dashboard } from "metabase/dashboard/components/Dashboard/Dashboard";
import { DashboardLeaveConfirmationModal } from "metabase/dashboard/components/DashboardLeaveConfirmationModal";
import { DashboardContextProvider } from "metabase/dashboard/context";
import {
useDashboardUrlParams,
useDashboardUrlQuery,
useRefreshDashboard,
} from "metabase/dashboard/hooks";
import title from "metabase/hoc/Title";
import titleWithLoadingTime from "metabase/hoc/TitleWithLoadingTime";
import { useFavicon } from "metabase/hooks/use-favicon";
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 { 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 { setErrorPage } from "metabase/redux/app";
import type { DashboardId, Dashboard as IDashboard } from "metabase-types/api";
import { DASHBOARD_SLOW_TIMEOUT } from "../../constants";
import { useRegisterDashboardMetabotContext } from "../../hooks/use-register-dashboard-metabot-context";
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 = {
import { DashboardTitle } from "./DashboardTitle";
import { useSlowCardNotification } from "./use-slow-card-notification";
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),
};
};
export const DashboardApp = ({
location,
params,
router,
route,
dashboardId: _dashboardId,
children,
}: DashboardAppProps & WithRouterProps<{ slug: string }>) => {
const dispatch = useDispatch();
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,
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,
removeParameter,
onReplaceAllDashCardVisualizationSettings,
onUpdateDashCardVisualizationSettings,
onUpdateDashCardColumnSettings,
updateDashboardAndCards,
setSidebar,
hideAddParameterPopover,
fetchDashboard,
fetchDashboardCardData,
} = props;
const [error, setError] = useState<string>();
const parameterQueryParams = location.query;
const dashboardId = getDashboardId(props);
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 dispatch = useDispatch();
const { requestPermission, showNotification } = useWebNotification();
useUnmount(() => {
dispatch(reset());
dispatch(closeDashboard());
});
const slowToastId = useUniqueId();
useEffect(() => {
if (isLoadingComplete) {
if (
"Notification" in window &&
Notification.permission === "granted" &&
document.hidden
) {
showNotification(
t`All Set! ${dashboard?.name} is ready.`,
t`All questions loaded`,
);
}
}
return () => {
dispatch(dismissUndo({ undoId: slowToastId }));
};
}, [
dashboard?.name,
dispatch,
isLoadingComplete,
showNotification,
slowToastId,
]);
const onConfirmToast = useCallback(async () => {
await requestPermission();
dispatch(dismissUndo({ undoId: slowToastId }));
}, [dispatch, requestPermission, slowToastId]);
const onTimeout = useCallback(() => {
if ("Notification" in window && Notification.permission === "default") {
dispatch(
addUndo({
id: slowToastId,
timeout: false,
message: t`Would you like to be notified when this dashboard is done loading?`,
action: onConfirmToast,
actionLabel: t`Turn on`,
}),
);
}
}, [dispatch, onConfirmToast, slowToastId]);
useLoadingTimer(isRunning, {
timer: DASHBOARD_SLOW_TIMEOUT,
onTimeout,
});
const { refreshDashboard } = useRefreshDashboard({
dashboardId: dashboardId,
dashboardId,
parameterQueryParams,
});
@@ -321,110 +73,87 @@ const DashboardApp = (props: DashboardAppProps) => {
onRefreshPeriodChange,
autoScrollToDashcardId,
reportAutoScrolledToDashcard,
theme,
setTheme,
} = useDashboardUrlParams({ location, onRefresh: refreshDashboard });
useDashboardUrlQuery(router, location);
useRegisterDashboardMetabotContext();
useDashboardUrlQuery(router, location);
const onLoadDashboard = (dashboard: IDashboard) => {
try {
if (editingOnLoad) {
onRefreshPeriodChange(null);
dispatch(setEditingDashboard(dashboard));
dispatch(push({ ...location, hash: "" }));
}
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} />
<Dashboard
<ErrorBoundary message={error}>
<DashboardContextProvider
dashboardId={dashboardId}
editingOnLoad={editingOnLoad}
addCardOnLoad={addCardOnLoad}
parameterQueryParams={parameterQueryParams}
theme={theme}
setTheme={setTheme}
isFullscreen={isFullscreen}
onFullscreenChange={onFullscreenChange}
hasNightModeToggle={hasNightModeToggle}
onNightModeChange={onNightModeChange}
isNightMode={isNightMode}
refreshPeriod={refreshPeriod}
setRefreshElapsedHook={setRefreshElapsedHook}
onRefreshPeriodChange={onRefreshPeriodChange}
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}
removeParameter={removeParameter}
onReplaceAllDashCardVisualizationSettings={
onReplaceAllDashCardVisualizationSettings
onLoad={onLoadDashboard}
onError={(error) => dispatch(setErrorPage(error))}
navigateToNewCardFromDashboard={(opts) =>
dispatch(navigateToNewCardFromDashboard(opts))
}
onUpdateDashCardVisualizationSettings={
onUpdateDashCardVisualizationSettings
}
onUpdateDashCardColumnSettings={onUpdateDashCardColumnSettings}
updateDashboardAndCards={updateDashboardAndCards}
setSidebar={setSidebar}
hideAddParameterPopover={hideAddParameterPopover}
fetchDashboard={fetchDashboard}
fetchDashboardCardData={fetchDashboardCardData}
/>
{/* For rendering modal urls */}
{props.children}
</div>
>
<DashboardTitle />
<DashboardFavicon />
<DashboardNotifications />
<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;
}
return Urls.extractEntityId(params.slug) as DashboardId;
function DashboardFavicon() {
const pageFavicon = useSelector(getFavicon);
useFavicon({ favicon: pageFavicon });
return null;
}
export const DashboardAppConnected = _.compose(
connector,
title(
({
dashboard,
documentTitle,
}: Pick<ReduxProps, "dashboard" | "documentTitle">) => ({
title: documentTitle || dashboard?.name,
titleIndex: 1,
}),
),
titleWithLoadingTime("loadingStartTime"),
)(DashboardApp);
function DashboardNotifications() {
useSlowCardNotification();
return null;
}

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,22 @@
import _ from "underscore";
import { useDashboardContext } from "metabase/dashboard/context";
import { getDocumentTitle } from "metabase/dashboard/selectors";
import title from "metabase/hoc/Title";
import titleWithLoadingTime from "metabase/hoc/TitleWithLoadingTime";
import { useSelector } from "metabase/lib/redux";
export const DashboardTitle = () => {
const { dashboard } = useDashboardContext();
const documentTitle = useSelector(getDocumentTitle);
const Component = _.compose(
title(() => ({
title: documentTitle || dashboard?.name,
titleIndex: 1,
})),
titleWithLoadingTime("loadingStartTime"),
)(() => null);
return <Component />;
};

View File

@@ -0,0 +1,69 @@
import { useCallback, useEffect } from "react";
import { t } from "ttag";
import { DASHBOARD_SLOW_TIMEOUT } from "metabase/dashboard/constants";
import { useDashboardContext } from "metabase/dashboard/context";
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 { useDispatch } from "metabase/lib/redux";
import { addUndo, dismissUndo } from "metabase/redux/undo";
export const useSlowCardNotification = () => {
const { dashboard, isRunning, isLoadingComplete } = useDashboardContext();
const dispatch = useDispatch();
const { requestPermission, showNotification } = useWebNotification();
const slowToastId = useUniqueId();
useEffect(() => {
if (isLoadingComplete) {
if (
"Notification" in window &&
Notification.permission === "granted" &&
document.hidden
) {
showNotification(
t`All Set! ${dashboard?.name} is ready.`,
t`All questions loaded`,
);
}
}
return () => {
dispatch(dismissUndo({ undoId: slowToastId }));
};
}, [
dashboard?.name,
dispatch,
isLoadingComplete,
showNotification,
slowToastId,
]);
const onConfirmToast = useCallback(async () => {
await requestPermission();
dispatch(dismissUndo({ undoId: slowToastId }));
}, [dispatch, requestPermission, slowToastId]);
const onTimeout = useCallback(() => {
if ("Notification" in window && Notification.permission === "default") {
dispatch(
addUndo({
id: slowToastId,
timeout: false,
message: t`Would you like to be notified when this dashboard is done loading?`,
action: onConfirmToast,
actionLabel: t`Turn on`,
}),
);
}
}, [dispatch, onConfirmToast, slowToastId]);
useLoadingTimer(isRunning, {
timer: DASHBOARD_SLOW_TIMEOUT,
onTimeout,
});
};

View File

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

View File

@@ -0,0 +1,385 @@
import {
type PropsWithChildren,
createContext,
useCallback,
useContext,
useEffect,
useState,
} from "react";
import { usePrevious, useUnmount } from "react-use";
import { isEqual, isObject, noop } from "underscore";
import type { ParameterValues } from "metabase/embedding-sdk/types/dashboard";
import { fetchEntityId } from "metabase/lib/entity-id/fetch-entity-id";
import { useDispatch } from "metabase/lib/redux";
import type { Dashboard, DashboardId } from "metabase-types/api";
import type { NavigateToNewCardFromDashboardOpts } from "../components/DashCard/types";
import type { UseAutoScrollToDashcardResult } from "../hooks/use-auto-scroll-to-dashcard";
import type {
CancelledFetchDashboardResult,
DashboardFullscreenControls,
DashboardRefreshPeriodControls,
EmbedDisplayParams,
EmbedThemeControls,
FailedFetchDashboardResult,
FetchDashboardResult,
SuccessfulFetchDashboardResult,
} from "../types";
import { type ReduxProps, connector } from "./context.redux";
export type DashboardContextErrorState = {
error: unknown | null;
};
export type DashboardContextOwnProps = {
dashboardId: DashboardId;
parameterQueryParams?: ParameterValues;
onLoad?: (dashboard: Dashboard) => void;
onError?: (error: unknown) => void;
onLoadWithoutCards?: (dashboard: Dashboard) => void;
navigateToNewCardFromDashboard:
| ((opts: NavigateToNewCardFromDashboardOpts) => void)
| null;
};
export type DashboardContextOwnResult = {
shouldRenderAsNightMode: boolean;
initialDashboardId: DashboardContextOwnProps["dashboardId"];
dashboardId: DashboardId | null;
};
export type DashboardControls = DashboardFullscreenControls &
DashboardRefreshPeriodControls &
UseAutoScrollToDashcardResult &
EmbedDisplayParams &
EmbedThemeControls;
export type DashboardContextProps = DashboardContextOwnProps &
Partial<DashboardControls>;
type ContextProps = DashboardContextProps & ReduxProps;
type ContextReturned = DashboardContextOwnResult &
Omit<DashboardContextOwnProps, "dashboardId"> &
ReduxProps &
Required<DashboardControls> &
DashboardContextErrorState;
export const DashboardContext = createContext<ContextReturned | undefined>(
undefined,
);
const DashboardContextProviderInner = ({
dashboardId: initialDashboardId,
parameterQueryParams = {},
onLoad,
onLoadWithoutCards,
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, results: true },
autoScrollToDashcardId = undefined,
reportAutoScrolledToDashcard = noop,
cardTitled = true,
getClickActionMode = undefined,
withFooter = true,
// redux selectors
dashboard,
selectedTabId,
isEditing,
isNavigatingBackToDashboard,
parameterValues,
isLoading,
isLoadingWithoutCards,
// redux actions
addCardToDashboard,
cancelFetchDashboardCardData,
fetchDashboard,
fetchDashboardCardData,
initialize,
setEditingDashboard,
toggleSidebar,
reset,
closeDashboard,
navigateToNewCardFromDashboard,
...reduxProps
}: PropsWithChildren<ContextProps>) => {
const dispatch = useDispatch();
const [error, setError] = useState<unknown | null>(null);
const previousIsLoading = usePrevious(isLoading);
const previousIsLoadingWithoutCards = usePrevious(isLoadingWithoutCards);
const [dashboardId, setDashboardId] = useState<DashboardId | null>(null);
const previousDashboard = usePrevious(dashboard);
const previousDashboardId = usePrevious(dashboardId);
const previousTabId = usePrevious(selectedTabId);
const previousParameterValues = usePrevious(parameterValues);
const shouldRenderAsNightMode = Boolean(isNightMode && isFullscreen);
const handleError = useCallback(
(error: unknown) => {
onError?.(error);
setError(error);
},
[onError],
);
const fetchId = useCallback(
async (idToFetch: DashboardId) => {
try {
const { id, isError } = await dispatch(
fetchEntityId({ type: "dashboard", id: idToFetch }),
);
if (isError || id === null) {
handleError({
status: 404,
message: "Not found",
name: "Not found",
});
}
setDashboardId(id);
} catch (e) {
handleError(e);
}
return;
},
[dispatch, handleError],
);
const fetchData = useCallback(
async (dashboardId: DashboardId) => {
const hasDashboardChanged = dashboardId !== previousDashboardId;
if (hasDashboardChanged) {
setError(null);
initialize({ clearCache: !isNavigatingBackToDashboard });
fetchDashboard({
dashId: dashboardId,
queryParams: parameterQueryParams,
options: {
clearCache: !isNavigatingBackToDashboard,
preserveParameters: isNavigatingBackToDashboard,
},
})
.then((result) => {
if (isFailedFetchDashboardResult(result)) {
handleError(result.payload);
}
})
.catch((err) => handleError(err));
}
},
[
fetchDashboard,
handleError,
initialize,
isNavigatingBackToDashboard,
parameterQueryParams,
previousDashboardId,
],
);
const fetchCards = useCallback(() => {
const hasDashboardLoaded = !previousDashboard;
const hasTabChanged = selectedTabId !== previousTabId;
const hasParameterValueChanged = !isEqual(
parameterValues,
previousParameterValues,
);
try {
if (hasDashboardLoaded) {
fetchDashboardCardData({ reload: false, clearCache: true });
} else if (hasTabChanged || hasParameterValueChanged) {
fetchDashboardCardData();
}
} catch (e) {
handleError?.(e);
}
}, [
fetchDashboardCardData,
handleError,
parameterValues,
previousDashboard,
previousParameterValues,
previousTabId,
selectedTabId,
]);
useEffect(() => {
if (initialDashboardId !== dashboardId) {
fetchId(initialDashboardId);
}
}, [dashboardId, dispatch, fetchId, handleError, initialDashboardId]);
useEffect(() => {
if (
initialDashboardId &&
dashboardId &&
dashboardId !== previousDashboardId
) {
fetchData(dashboardId);
}
}, [dashboardId, fetchData, initialDashboardId, previousDashboardId]);
useEffect(() => {
if (dashboard) {
fetchCards();
}
}, [dashboard, fetchCards]);
useEffect(() => {
if (
!isLoadingWithoutCards &&
previousIsLoadingWithoutCards &&
!error &&
dashboard
) {
onLoadWithoutCards?.(dashboard);
// For whatever reason, isLoading waits for all cards to be loaded but doesn't account for the
// fact that there might be no dashcards. So onLoad never triggers when there are no cards,
// so this solves that issue
if (dashboard?.dashcards.length === 0) {
onLoad?.(dashboard);
}
}
}, [
previousIsLoadingWithoutCards,
dashboard,
onLoadWithoutCards,
error,
isLoadingWithoutCards,
onLoad,
]);
useEffect(() => {
if (!isLoading && previousIsLoading && !error && dashboard) {
onLoad?.(dashboard);
}
}, [isLoading, previousIsLoading, dashboard, onLoad, error]);
useUnmount(() => {
cancelFetchDashboardCardData();
reset();
closeDashboard();
});
return (
<DashboardContext.Provider
value={{
initialDashboardId,
dashboardId,
parameterQueryParams,
onLoad,
onError,
navigateToNewCardFromDashboard,
isLoading,
isLoadingWithoutCards,
error,
isFullscreen,
onFullscreenChange,
hasNightModeToggle,
onNightModeChange,
isNightMode,
shouldRenderAsNightMode,
refreshPeriod,
setRefreshElapsedHook,
onRefreshPeriodChange,
background,
bordered,
titled,
font,
theme,
setTheme,
hideParameters,
downloadsEnabled,
autoScrollToDashcardId,
reportAutoScrolledToDashcard,
cardTitled,
getClickActionMode,
withFooter,
// redux selectors
dashboard,
selectedTabId,
isEditing,
isNavigatingBackToDashboard,
parameterValues,
// redux actions
addCardToDashboard,
cancelFetchDashboardCardData,
fetchDashboard,
fetchDashboardCardData,
initialize,
setEditingDashboard,
toggleSidebar,
reset,
closeDashboard,
...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;
}
export 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) && !result.payload.isCancelled && "error" in result
);
}
export function isCancelledFetchDashboardResult(
result: FetchDashboardResult,
): result is CancelledFetchDashboardResult {
return isObject(result.payload) && Boolean(result.payload.isCancelled);
}

View File

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

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

@@ -1,3 +1,5 @@
import type { EmbedResourceDownloadOptions } from "metabase/public/lib/types";
export type DashboardFullscreenControls = {
isFullscreen: boolean;
onFullscreenChange: (
@@ -23,7 +25,7 @@ export type DashboardNightModeControls = {
};
export type DashboardDownloadControls = {
downloadsEnabled?: boolean;
downloadsEnabled?: EmbedResourceDownloadOptions;
};
export type DashboardFooterControls = {

View File

@@ -29,7 +29,7 @@ export type EmbedDisplayParams = {
hideParameters: EmbedHideParameters;
font: EmbedFont;
theme: DisplayTheme;
getClickActionMode?: ClickActionModeGetter;
getClickActionMode: ClickActionModeGetter | undefined;
downloadsEnabled: EmbedResourceDownloadOptions;
withFooter: boolean;
};

View File

@@ -167,6 +167,10 @@ export function hasDatabaseActionsEnabled(database: Database) {
return database.settings?.["database-enable-actions"] ?? false;
}
export function isTransientId(id: unknown) {
return typeof id === "string" && /\/auto\/dashboard/.test(id);
}
export function getDashboardType(id: unknown) {
if (id == null || typeof id === "object") {
// HACK: support inline dashboards
@@ -175,7 +179,7 @@ export function getDashboardType(id: unknown) {
return "public";
} else if (isJWT(id)) {
return "embed";
} else if (typeof id === "string" && /\/auto\/dashboard/.test(id)) {
} else if (isTransientId(id)) {
return "transient";
} else {
return "normal";

View File

@@ -2,9 +2,13 @@ import _ from "underscore";
import { entityIdApi } from "metabase/api";
import type { TranslateEntityIdResponse } from "metabase/api/entity-id";
import { isTransientId } from "metabase/dashboard/utils";
import { isBaseEntityID } from "metabase-types/api/entity-id";
import type { Dispatch } from "metabase-types/store";
import { isJWT } from "../utils";
import { isUuid } from "../uuid";
import type {
SUPPORTED_ENTITIES,
ValidatedEntityIdProps,
@@ -22,7 +26,10 @@ export const fetchEntityId =
async (
dispatch: Dispatch,
): Promise<ValidatedEntityIdReturned<TEntity, TReturnedId>> => {
if (_.isNumber(id)) {
if (
id &&
(_.isNumber(id) || isUuid(id) || isJWT(id) || isTransientId(id))
) {
return { id: id as TReturnedId, isError: false };
}

View File

@@ -21,7 +21,7 @@ function s4() {
.substring(1);
}
export function isUuid(uuid: unknown): uuid is string {
export function isUuid(uuid: unknown) {
return (
typeof uuid === "string" &&
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/.test(uuid)

View File

@@ -40,6 +40,7 @@ import {
NotFoundPlaceholder,
PluginPlaceholder,
} from "metabase/plugins/components/PluginPlaceholder";
import type { EmbedResourceDownloadOptions } from "metabase/public/lib/types";
import type { SearchFilterComponent } from "metabase/search/types";
import { _FileUploadErrorModal } from "metabase/status/components/FileUploadStatusLarge/FileUploadErrorModal";
import type { IconName, IconProps, StackProps } from "metabase/ui";
@@ -608,7 +609,9 @@ export const PLUGIN_RESOURCE_DOWNLOADS = {
/**
* Returns if 'download results' on cards and pdf exports are enabled in public and embedded contexts.
*/
areDownloadsEnabled: (_args: { downloads?: string | boolean | null }) => ({
areDownloadsEnabled: (_args: {
downloads?: string | boolean | null;
}): EmbedResourceDownloadOptions => ({
pdf: true,
results: true,
}),

View File

@@ -1,267 +1,44 @@
import { useEffect, useRef } from "react";
import type { ConnectedProps } from "react-redux";
import { usePrevious, useUnmount } from "react-use";
import _ from "underscore";
import {
cancelFetchDashboardCardData,
fetchDashboard,
fetchDashboardCardData,
initialize,
setParameterValue,
setParameterValueToDefault,
} from "metabase/dashboard/actions";
import type { NavigateToNewCardFromDashboardOpts } from "metabase/dashboard/components/DashCard/types";
import {
getDashboardComplete,
getDraftParameterValues,
getIsNavigatingBackToDashboard,
getParameterValues,
getParameters,
getSelectedTabId,
getSlowCards,
} from "metabase/dashboard/selectors";
import type {
DashboardDisplayOptionControls,
EmbedDisplayParams,
FetchDashboardResult,
SuccessfulFetchDashboardResult,
} from "metabase/dashboard/types";
import type { ParameterValues } from "metabase/embedding-sdk/types/dashboard";
import { connect } from "metabase/lib/redux";
import { type DispatchFn, useDispatch } from "metabase/lib/redux";
import { LocaleProvider } from "metabase/public/LocaleProvider";
import type { PublicOrEmbeddedDashboardEventHandlersProps } from "metabase/public/containers/PublicOrEmbeddedDashboard/types";
import { useDashboardLoadHandlers } from "metabase/public/containers/PublicOrEmbeddedDashboard/use-dashboard-load-handlers";
type DashboardContextProps,
DashboardContextProvider,
} from "metabase/dashboard/context";
import type { EmbeddingAdditionalHashOptions } from "metabase/public/lib/types";
import { setErrorPage } from "metabase/redux/app";
import type { DashboardId } from "metabase-types/api";
import type { State } from "metabase-types/store";
import { PublicOrEmbeddedDashboardView } from "./PublicOrEmbeddedDashboardView";
const mapStateToProps = (state: State) => {
return {
dashboard: getDashboardComplete(state),
slowCards: getSlowCards(state),
parameters: getParameters(state),
parameterValues: getParameterValues(state),
draftParameterValues: getDraftParameterValues(state),
selectedTabId: getSelectedTabId(state),
isNavigatingBackToDashboard: getIsNavigatingBackToDashboard(state),
};
};
const mapDispatchToProps = {
cancelFetchDashboardCardData,
setParameterValueToDefault,
setParameterValue,
fetchDashboardCardData,
};
const connector = connect(mapStateToProps, mapDispatchToProps);
type ReduxProps = ConnectedProps<typeof connector>;
type OwnProps = {
dashboardId: DashboardId;
parameterQueryParams: ParameterValues;
navigateToNewCardFromDashboard?: (
opts: NavigateToNewCardFromDashboardOpts,
) => void;
} & PublicOrEmbeddedDashboardEventHandlersProps;
type PublicOrEmbeddedDashboardProps = OwnProps &
ReduxProps &
Omit<DashboardDisplayOptionControls, "downloadsEnabled"> &
Omit<EmbedDisplayParams, "font"> &
export type PublicOrEmbeddedDashboardProps = Pick<
DashboardContextProps,
| "dashboardId"
| "hasNightModeToggle"
| "isFullscreen"
| "isNightMode"
| "onFullscreenChange"
| "onNightModeChange"
| "onRefreshPeriodChange"
| "refreshPeriod"
| "setRefreshElapsedHook"
| "background"
| "bordered"
| "titled"
| "theme"
| "hideParameters"
| "downloadsEnabled"
| "parameterQueryParams"
| "onLoad"
| "onLoadWithoutCards"
| "cardTitled"
| "withFooter"
| "onError"
| "getClickActionMode"
| "navigateToNewCardFromDashboard"
> &
Pick<EmbeddingAdditionalHashOptions, "locale">;
const initializeData = async ({
dashboardId,
shouldReload,
parameterQueryParams,
dispatch,
}: {
dashboardId: string;
shouldReload: boolean;
parameterQueryParams: OwnProps["parameterQueryParams"];
dispatch: DispatchFn;
}) => {
dispatch(initialize({ clearCache: shouldReload }));
const result = await dispatch(
fetchDashboard({
dashId: String(dashboardId),
queryParams: parameterQueryParams,
options: {
clearCache: shouldReload,
},
}),
);
if (!isSuccessfulFetchDashboardResult(result)) {
dispatch(setErrorPage(result.payload));
return;
}
try {
if ((result.payload.dashboard?.tabs?.length || 0) === 0) {
await dispatch(
fetchDashboardCardData({ reload: false, clearCache: true }),
);
}
} catch (error) {
console.error(error);
dispatch(setErrorPage(error));
}
};
const PublicOrEmbeddedDashboardInner = ({
dashboard,
parameters,
parameterValues,
draftParameterValues,
isFullscreen,
isNightMode = false,
onFullscreenChange,
onNightModeChange,
onRefreshPeriodChange,
refreshPeriod,
setRefreshElapsedHook,
hasNightModeToggle,
background,
bordered,
titled,
theme,
getClickActionMode,
downloadsEnabled,
hideParameters,
withFooter,
navigateToNewCardFromDashboard,
selectedTabId,
slowCards,
dashboardId,
cardTitled,
isNavigatingBackToDashboard,
parameterQueryParams,
onLoad,
onLoadWithoutCards,
cancelFetchDashboardCardData,
setParameterValueToDefault,
setParameterValue,
fetchDashboardCardData,
export const PublicOrEmbeddedDashboard = ({
locale,
}: PublicOrEmbeddedDashboardProps) => {
const dispatch = useDispatch();
const didMountRef = useRef(false);
const previousDashboardId = usePrevious(dashboardId);
const previousSelectedTabId = usePrevious(selectedTabId);
const previousParameterValues = usePrevious(parameterValues);
const isDashboardLoaded = dashboard != null;
const wasDashboardLoaded = usePrevious(isDashboardLoaded);
const shouldFetchCardData = dashboard?.tabs?.length === 0;
useDashboardLoadHandlers({ dashboard, onLoad, onLoadWithoutCards });
useEffect(() => {
if (!didMountRef.current) {
initializeData({
dashboardId: String(dashboardId),
shouldReload: !isNavigatingBackToDashboard,
parameterQueryParams,
dispatch,
});
didMountRef.current = true;
return;
}
if (dashboardId !== previousDashboardId) {
initializeData({
dashboardId: String(dashboardId),
shouldReload: true,
parameterQueryParams,
dispatch,
});
return;
}
if (selectedTabId && selectedTabId !== previousSelectedTabId) {
fetchDashboardCardData();
return;
}
// `initializeData` changes `parameterValues`. We should not re-run queries
// in this case.
if (
wasDashboardLoaded &&
!_.isEqual(parameterValues, previousParameterValues)
) {
fetchDashboardCardData({ reload: false, clearCache: true });
}
}, [
dashboardId,
dispatch,
fetchDashboardCardData,
isNavigatingBackToDashboard,
parameterQueryParams,
parameterValues,
previousDashboardId,
previousParameterValues,
previousSelectedTabId,
wasDashboardLoaded,
selectedTabId,
shouldFetchCardData,
]);
useUnmount(() => {
cancelFetchDashboardCardData();
});
return (
<LocaleProvider locale={locale} shouldWaitForLocale>
<PublicOrEmbeddedDashboardView
dashboard={dashboard}
hasNightModeToggle={hasNightModeToggle}
isFullscreen={isFullscreen}
isNightMode={isNightMode}
onFullscreenChange={onFullscreenChange}
onNightModeChange={onNightModeChange}
onRefreshPeriodChange={onRefreshPeriodChange}
refreshPeriod={refreshPeriod}
setRefreshElapsedHook={setRefreshElapsedHook}
selectedTabId={selectedTabId}
parameters={parameters}
parameterValues={parameterValues}
draftParameterValues={draftParameterValues}
setParameterValue={setParameterValue}
setParameterValueToDefault={setParameterValueToDefault}
dashboardId={dashboardId}
background={background}
bordered={bordered}
titled={titled}
theme={theme}
getClickActionMode={getClickActionMode}
hideParameters={hideParameters}
navigateToNewCardFromDashboard={navigateToNewCardFromDashboard}
slowCards={slowCards}
cardTitled={cardTitled}
downloadsEnabled={downloadsEnabled}
withFooter={withFooter}
/>
</LocaleProvider>
);
};
function isSuccessfulFetchDashboardResult(
result: FetchDashboardResult,
): result is SuccessfulFetchDashboardResult {
const hasError = "error" in result;
return !hasError;
}
export const PublicOrEmbeddedDashboard = connector(
PublicOrEmbeddedDashboardInner,
...contextProps
}: PublicOrEmbeddedDashboardProps) => (
<DashboardContextProvider {...contextProps}>
<PublicOrEmbeddedDashboardView />
</DashboardContextProvider>
);

View File

@@ -5,15 +5,19 @@ import {
useRefreshDashboard,
} from "metabase/dashboard/hooks";
import { useDashboardUrlQuery } from "metabase/dashboard/hooks/use-dashboard-url-query";
import { getDashboardComplete } from "metabase/dashboard/selectors";
import { SetTitle } from "metabase/hoc/Title";
import { useSelector } from "metabase/lib/redux";
import { useDispatch, useSelector } from "metabase/lib/redux";
import { LocaleProvider } from "metabase/public/LocaleProvider";
import { setErrorPage } from "metabase/redux/app";
import { getCanWhitelabel } from "metabase/selectors/whitelabel";
import { Mode } from "metabase/visualizations/click-actions/Mode";
import { PublicMode } from "metabase/visualizations/click-actions/modes/PublicMode";
import { PublicOrEmbeddedDashboard } from "../PublicOrEmbeddedDashboard";
import { usePublicDashboardEndpoints } from "../WithPublicDashboardEndpoints";
export const PublicOrEmbeddedDashboardPage = (props: WithRouterProps) => {
const dispatch = useDispatch();
const { location, router } = props;
const parameterQueryParams = props.location.query;
@@ -46,11 +50,11 @@ export const PublicOrEmbeddedDashboardPage = (props: WithRouterProps) => {
const canWhitelabel = useSelector(getCanWhitelabel);
const dashboard = useSelector(getDashboardComplete);
return (
<>
<SetTitle title={dashboard?.name} />
<LocaleProvider
locale={canWhitelabel ? locale : undefined}
shouldWaitForLocale
>
<PublicOrEmbeddedDashboard
dashboardId={dashboardId}
isFullscreen={isFullscreen}
@@ -69,9 +73,13 @@ export const PublicOrEmbeddedDashboardPage = (props: WithRouterProps) => {
titled={titled}
parameterQueryParams={parameterQueryParams}
cardTitled={true}
locale={canWhitelabel ? locale : undefined}
withFooter={true}
getClickActionMode={({ question }) => new Mode(question, PublicMode)}
navigateToNewCardFromDashboard={null}
onError={(error) => {
dispatch(setErrorPage(error));
}}
/>
</>
</LocaleProvider>
);
};

View File

@@ -1,14 +1,18 @@
import type { StoryContext, StoryFn } from "@storybook/react";
import { userEvent, within } from "@storybook/test";
import type { ComponentProps } from "react";
import { getStore } from "__support__/entities-store";
import { createMockMetadata } from "__support__/metadata";
import { createWaitForResizeToStopDecorator } from "__support__/storybook";
import { getNextId } from "__support__/utils";
import { NumberColumn, StringColumn } from "__support__/visualizations";
import { Api } from "metabase/api";
import { MetabaseReduxProvider } from "metabase/lib/redux/custom-context";
import { getUnsavedDashboardUiParameters } from "metabase/parameters/utils/dashboards";
import {
MockDashboardContext,
type MockDashboardContextProps,
} from "metabase/public/containers/PublicOrEmbeddedDashboard/mock-context";
import { publicReducers } from "metabase/reducers-public";
import { registerVisualization } from "metabase/visualizations";
import { BarChart } from "metabase/visualizations/visualizations/BarChart";
@@ -34,10 +38,7 @@ import {
createMockState,
} from "metabase-types/store/mocks";
import {
PublicOrEmbeddedDashboardView,
type PublicOrEmbeddedDashboardViewProps,
} from "./PublicOrEmbeddedDashboardView";
import { PublicOrEmbeddedDashboardView } from "./PublicOrEmbeddedDashboardView";
// @ts-expect-error: incompatible prop types with registerVisualization
registerVisualization(Table);
@@ -112,7 +113,7 @@ function ReduxDecorator(Story: StoryFn, context: StoryContext) {
},
});
const store = getStore(publicReducers, initialState);
const store = getStore(publicReducers, initialState, [Api.middleware]);
return (
<MetabaseReduxProvider store={store}>
<Story />
@@ -222,7 +223,7 @@ function createDashboard({ hasScroll }: CreateDashboardOpts = {}) {
});
}
const Template: StoryFn<PublicOrEmbeddedDashboardViewProps> = (args) => {
const Template: StoryFn<MockDashboardContextProps> = (args) => {
// @ts-expect-error -- custom prop to support non JSON-serializable value as args
const parameterType: ParameterType = args.parameterType;
const dashboard = args.dashboard;
@@ -375,16 +376,18 @@ const Template: StoryFn<PublicOrEmbeddedDashboardViewProps> = (args) => {
{},
),
};
return (
<PublicOrEmbeddedDashboardView
<MockDashboardContext
{...args}
dashboardId={dashboard.id}
parameters={PARAMETER_MAPPING[parameterType]}
/>
>
<PublicOrEmbeddedDashboardView />
</MockDashboardContext>
);
};
type ArgType = Partial<ComponentProps<typeof PublicOrEmbeddedDashboardView>>;
type ParameterType =
| "text"
| "number"
@@ -399,12 +402,20 @@ type ParameterType =
| "date_relative"
| "temporal_unit";
type DefaultArgs = MockDashboardContextProps & {
parameterType?: ParameterType;
};
const createDefaultArgs = (
args: ArgType & { parameterType?: ParameterType } = {},
): ArgType & { parameterType: ParameterType } => {
args: Omit<
DefaultArgs,
"dashboardId" | "navigateToNewCardFromDashboard"
> = {},
): DefaultArgs => {
const dashboard = createDashboard();
return {
dashboard,
dashboardId: dashboard.id,
navigateToNewCardFromDashboard: null,
titled: true,
bordered: true,
background: true,

View File

@@ -1,7 +1,7 @@
// @ts-expect-error There is no type definition
import createAsyncCallback from "@loki/create-async-callback";
import type { StoryFn } from "@storybook/react";
import { type ComponentProps, useEffect, useMemo } from "react";
import { useEffect, useMemo } from "react";
import { getStore } from "__support__/entities-store";
import { createWaitForResizeToStopDecorator } from "__support__/storybook";
@@ -9,6 +9,10 @@ import { getNextId } from "__support__/utils";
import { NumberColumn, StringColumn } from "__support__/visualizations";
import { Api } from "metabase/api";
import { MetabaseReduxProvider } from "metabase/lib/redux";
import {
MockDashboardContext,
type MockDashboardContextProps,
} from "metabase/public/containers/PublicOrEmbeddedDashboard/mock-context";
import { publicReducers } from "metabase/reducers-public";
import { Box, Card, Popover, Text, Tooltip } from "metabase/ui";
import { registerVisualization } from "metabase/visualizations";
@@ -32,10 +36,7 @@ import {
createMockState,
} from "metabase-types/store/mocks";
import {
PublicOrEmbeddedDashboardView,
type PublicOrEmbeddedDashboardViewProps,
} from "./PublicOrEmbeddedDashboardView";
import { PublicOrEmbeddedDashboardView } from "./PublicOrEmbeddedDashboardView";
// @ts-expect-error: incompatible prop types with registerVisualization
registerVisualization(Table);
@@ -151,13 +152,12 @@ function createDashboard({ hasScroll, dashcards }: CreateDashboardOpts = {}) {
});
}
const Template: StoryFn<PublicOrEmbeddedDashboardViewProps> = (args) => {
return <PublicOrEmbeddedDashboardView {...args} />;
};
const defaultArgs: Partial<
ComponentProps<typeof PublicOrEmbeddedDashboardView>
> = {
const Template: StoryFn<MockDashboardContextProps> = (args) => (
<MockDashboardContext {...args}>
<PublicOrEmbeddedDashboardView />
</MockDashboardContext>
);
const defaultArgs: Partial<MockDashboardContextProps> = {
dashboard: createDashboard(),
downloadsEnabled: { pdf: true, results: true },
titled: true,

View File

@@ -1,116 +1,65 @@
import cx from "classnames";
import { assoc } from "icepick";
import { useCallback } from "react";
import type { HandleThunkActionCreator } from "react-redux";
import _ from "underscore";
import { LoadingAndErrorWrapper } from "metabase/components/LoadingAndErrorWrapper";
import ColorS from "metabase/css/core/colors.module.css";
import CS from "metabase/css/core/index.css";
import DashboardS from "metabase/css/dashboard.module.css";
import type {
setParameterValue as setParameterValueDashboardAction,
setParameterValueToDefault as setParameterValueToDefaultDashboardAction,
} from "metabase/dashboard/actions";
import type { NavigateToNewCardFromDashboardOpts } from "metabase/dashboard/components/DashCard/types";
import { DashboardEmptyStateWithoutAddPrompt } from "metabase/dashboard/components/Dashboard/DashboardEmptyState/DashboardEmptyState";
import { DashboardGridConnected } from "metabase/dashboard/components/DashboardGrid";
import { DashboardHeaderButtonRow } from "metabase/dashboard/components/DashboardHeader/DashboardHeaderButtonRow/DashboardHeaderButtonRow";
import { DASHBOARD_DISPLAY_ACTIONS } from "metabase/dashboard/components/DashboardHeader/DashboardHeaderButtonRow/constants";
import { DashboardTabs } from "metabase/dashboard/components/DashboardTabs";
import type {
DashboardFooterControls,
DashboardFullscreenControls,
DashboardNightModeControls,
DashboardRefreshPeriodControls,
EmbedHideParameters,
} from "metabase/dashboard/types";
import { useDashboardContext } from "metabase/dashboard/context";
import { isActionDashCard } from "metabase/dashboard/utils";
import { SetTitle } from "metabase/hoc/Title";
import { isWithinIframe } from "metabase/lib/dom";
import ParametersS from "metabase/parameters/components/ParameterValueWidget.module.css";
import type {
DisplayTheme,
EmbedResourceDownloadOptions,
} from "metabase/public/lib/types";
import type { DisplayTheme } from "metabase/public/lib/types";
import { FullWidthContainer } from "metabase/styled-components/layout/FullWidthContainer";
import { getEmbeddingMode } from "metabase/visualizations/click-actions/lib/modes";
import { EmbeddingSdkMode } from "metabase/visualizations/click-actions/modes/EmbeddingSdkMode";
import { PublicMode } from "metabase/visualizations/click-actions/modes/PublicMode";
import type { ClickActionModeGetter } from "metabase/visualizations/types";
import type { UiParameter } from "metabase-lib/v1/parameters/types";
import type {
Dashboard,
DashboardCard,
DashboardId,
ParameterValueOrArray,
} from "metabase-types/api";
import type { Dashboard, DashboardCard } from "metabase-types/api";
import type { SelectedTabId } from "metabase-types/store";
import { EmbedFrame } from "../../components/EmbedFrame";
interface InnerPublicOrEmbeddedDashboardViewProps {
dashboard: Dashboard | null;
selectedTabId: SelectedTabId;
parameters: UiParameter[];
parameterValues: Record<string, ParameterValueOrArray>;
draftParameterValues: Record<string, ParameterValueOrArray | null>;
setParameterValue: HandleThunkActionCreator<
typeof setParameterValueDashboardAction
>;
setParameterValueToDefault: HandleThunkActionCreator<
typeof setParameterValueToDefaultDashboardAction
>;
dashboardId: DashboardId;
background: boolean;
bordered: boolean;
titled: boolean;
theme: DisplayTheme;
getClickActionMode?: ClickActionModeGetter;
hideParameters: EmbedHideParameters;
navigateToNewCardFromDashboard?: (
opts: NavigateToNewCardFromDashboardOpts,
) => void;
slowCards: Record<number, boolean>;
cardTitled: boolean;
downloadsEnabled: EmbedResourceDownloadOptions;
}
export function PublicOrEmbeddedDashboardView() {
const {
setParameterValue,
setParameterValueToDefault,
dashboard,
hasNightModeToggle,
isFullscreen,
isNightMode,
onFullscreenChange,
onNightModeChange,
onRefreshPeriodChange,
refreshPeriod,
setRefreshElapsedHook,
selectedTabId,
parameters,
parameterValues,
draftParameterValues,
dashboardId,
background,
bordered,
titled,
theme,
getClickActionMode: externalGetClickActionMode,
hideParameters,
withFooter,
navigateToNewCardFromDashboard,
slowCards,
cardTitled,
downloadsEnabled,
} = useDashboardContext();
export type PublicOrEmbeddedDashboardViewProps =
InnerPublicOrEmbeddedDashboardViewProps &
DashboardRefreshPeriodControls &
DashboardNightModeControls &
DashboardFullscreenControls &
DashboardFooterControls;
export function PublicOrEmbeddedDashboardView({
dashboard,
hasNightModeToggle,
isFullscreen,
isNightMode,
onFullscreenChange,
onNightModeChange,
onRefreshPeriodChange,
refreshPeriod,
setRefreshElapsedHook,
selectedTabId,
parameters,
parameterValues,
draftParameterValues,
setParameterValue,
setParameterValueToDefault,
dashboardId,
background,
bordered,
titled,
theme,
getClickActionMode: externalGetClickActionMode,
hideParameters,
withFooter,
navigateToNewCardFromDashboard,
slowCards,
cardTitled,
downloadsEnabled,
}: PublicOrEmbeddedDashboardViewProps) {
const buttons = !isWithinIframe() ? (
<DashboardHeaderButtonRow
canResetFilters={false}
@@ -180,6 +129,7 @@ export function PublicOrEmbeddedDashboardView({
enableParameterRequiredBehavior
actionButtons={buttons ? <div className={CS.flex}>{buttons}</div> : null}
dashboardTabs={
dashboardId &&
dashboardHasTabs && <DashboardTabs dashboardId={dashboardId} />
}
background={background}
@@ -190,6 +140,7 @@ export function PublicOrEmbeddedDashboardView({
pdfDownloadsEnabled={downloadsEnabled.pdf}
withFooter={withFooter}
>
{dashboard && <SetTitle title={dashboard.name} />}
<LoadingAndErrorWrapper
className={cx({
[DashboardS.DashboardFullscreen]: isFullscreen,
@@ -237,8 +188,10 @@ export function PublicOrEmbeddedDashboardView({
isNightMode={isNightMode}
withCardTitle={cardTitled}
clickBehaviorSidebarDashcard={null}
navigateToNewCardFromDashboard={navigateToNewCardFromDashboard}
downloadsEnabled={downloadsEnabled.results}
navigateToNewCardFromDashboard={
navigateToNewCardFromDashboard ?? null
}
downloadsEnabled={downloadsEnabled}
autoScrollToDashcardId={undefined}
reportAutoScrolledToDashcard={_.noop}
/>

View File

@@ -0,0 +1,134 @@
import type { ComponentType, PropsWithChildren } from "react";
import { noop } from "underscore";
import {
DashboardContext,
type DashboardContextErrorState,
type DashboardContextOwnProps,
type DashboardContextOwnResult,
type DashboardContextProps,
type DashboardControls,
} from "metabase/dashboard/context";
import {
type ReduxProps,
mapDispatchToProps,
mapStateToProps,
} from "metabase/dashboard/context/context.redux";
import { connect } from "metabase/lib/redux";
export type MockDashboardContextProps = DashboardContextProps &
Partial<ReduxProps> &
Partial<DashboardContextErrorState>;
// Create a component that accepts all redux props and passes them into DashboardContext
const DashboardContextWithReduxProps = (
props: PropsWithChildren<
ReduxProps &
DashboardContextOwnProps &
DashboardContextOwnResult &
DashboardControls
>,
) => (
<DashboardContext.Provider value={{ error: null, ...props }}>
{props.children}
</DashboardContext.Provider>
);
const ConnectedDashboardContextWithReduxProps = connect(
mapStateToProps,
mapDispatchToProps,
(stateProps, dispatchProps, ownProps) =>
// this is a bit of a hack to get the types to agree (since overridden functions of mapDispatch might
// have the correct type) and we have bigger fish to fry. so we can come back to this
({
...stateProps,
...dispatchProps,
...ownProps,
}) as unknown as ReduxProps &
DashboardContextOwnProps &
DashboardContextOwnResult &
DashboardControls,
)(DashboardContextWithReduxProps) as ComponentType<
PropsWithChildren<
MockDashboardContextProps & DashboardContextOwnResult & Partial<ReduxProps>
>
>;
/*
* NOTE: DO NOT USE THIS IN REAL COMPONENTS. This is specifically for the storybook stories for the
* PublicOrEmbeddedDashboardView. We were relying on hard-coded redux values passed directly into
* the component, but this is different now with the dashboard context.
*
* TODO: Adapt PublicOrEmbeddedDashboardView stories to new dashboard context code by adding data
* directly to the redux store instead of overriding data. Then we can maybe even use the
* actual context for our stories.
* */
export const MockDashboardContext = ({
children,
dashboardId,
parameterQueryParams,
onLoad,
onError,
navigateToNewCardFromDashboard = null,
// 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, results: true },
autoScrollToDashcardId = undefined,
reportAutoScrolledToDashcard = noop,
cardTitled = true,
getClickActionMode = undefined,
withFooter = true,
...reduxProps
}: PropsWithChildren<MockDashboardContextProps>) => {
const shouldRenderAsNightMode = Boolean(isNightMode && isFullscreen);
return (
<ConnectedDashboardContextWithReduxProps
initialDashboardId={dashboardId}
dashboardId={dashboardId}
parameterQueryParams={parameterQueryParams}
onLoad={onLoad}
onError={onError}
navigateToNewCardFromDashboard={navigateToNewCardFromDashboard}
isFullscreen={isFullscreen}
onFullscreenChange={onFullscreenChange}
hasNightModeToggle={hasNightModeToggle}
onNightModeChange={onNightModeChange}
isNightMode={isNightMode}
shouldRenderAsNightMode={shouldRenderAsNightMode}
refreshPeriod={refreshPeriod}
setRefreshElapsedHook={setRefreshElapsedHook}
onRefreshPeriodChange={onRefreshPeriodChange}
background={background}
bordered={bordered}
titled={titled}
font={font}
theme={theme}
setTheme={setTheme}
hideParameters={hideParameters}
downloadsEnabled={downloadsEnabled}
autoScrollToDashcardId={autoScrollToDashcardId}
reportAutoScrolledToDashcard={reportAutoScrolledToDashcard}
cardTitled={cardTitled}
getClickActionMode={getClickActionMode}
withFooter={withFooter}
{...reduxProps}
>
{children}
</ConnectedDashboardContextWithReduxProps>
);
};

View File

@@ -1,62 +1,43 @@
import { useEffect } from "react";
import { usePrevious } from "react-use";
import { useCallback, useEffect, useRef } from "react";
/* eslint-disable-next-line no-restricted-imports -- deprecated sdk import */
import { getEventHandlers } from "embedding-sdk/store/selectors";
import {
getIsLoading,
getIsLoadingWithoutCards,
} from "metabase/dashboard/selectors";
import { useSelector } from "metabase/lib/redux";
import type { PublicOrEmbeddedDashboardEventHandlersProps } from "metabase/public/containers/PublicOrEmbeddedDashboard/types";
import { getErrorPage } from "metabase/selectors/app";
import type { Dashboard } from "metabase-types/api";
export const useDashboardLoadHandlers = ({
dashboard,
onLoad,
onLoadWithoutCards,
}: {
dashboard: Dashboard | null;
} & PublicOrEmbeddedDashboardEventHandlersProps) => {
const isLoading = useSelector(getIsLoading);
const isLoadingWithoutCards = useSelector(getIsLoadingWithoutCards);
const isErrorPage = useSelector(getErrorPage);
const previousIsLoading = usePrevious(isLoading);
const previousIsLoadingWithoutCards = usePrevious(isLoadingWithoutCards);
}: PublicOrEmbeddedDashboardEventHandlersProps) => {
const sdkEventHandlers = useSelector(getEventHandlers);
// Hack: since we're storing functions in the redux store there are issues
// with timing and serialization. We'll need to do something about this in the future
const sdkEventHandlersRef = useRef(sdkEventHandlers);
useEffect(() => {
if (
!isLoadingWithoutCards &&
previousIsLoadingWithoutCards &&
!isErrorPage
) {
sdkEventHandlers?.onDashboardLoadWithoutCards?.(dashboard);
sdkEventHandlersRef.current = sdkEventHandlers;
}, [sdkEventHandlers]);
// Use the ref in your callbacks
const handleLoadWithoutCards = useCallback(
(dashboard: Dashboard) => {
onLoadWithoutCards?.(dashboard);
}
}, [
isLoadingWithoutCards,
isErrorPage,
previousIsLoadingWithoutCards,
dashboard,
sdkEventHandlers,
onLoadWithoutCards,
]);
sdkEventHandlersRef.current?.onDashboardLoadWithoutCards?.(dashboard);
},
[onLoadWithoutCards],
);
useEffect(() => {
if (!isLoading && previousIsLoading && !isErrorPage) {
sdkEventHandlers?.onDashboardLoad?.(dashboard);
const handleLoad = useCallback(
(dashboard: Dashboard) => {
onLoad?.(dashboard);
}
}, [
isLoading,
isErrorPage,
previousIsLoading,
sdkEventHandlers,
dashboard,
onLoad,
]);
sdkEventHandlersRef.current?.onDashboardLoad?.(dashboard);
},
[onLoad],
);
return {
handleLoad,
handleLoadWithoutCards,
};
};

View File

@@ -203,7 +203,7 @@ export const PublicOrEmbeddedQuestion = ({
theme={theme}
titled={titled}
setCard={setCard}
downloadsEnabled={downloadsEnabled.results}
downloadsEnabled={downloadsEnabled}
/>
</LocaleProvider>
);

View File

@@ -97,6 +97,7 @@ const defaultArgs: Partial<
bordered: true,
getParameters: () => [],
setCard: () => {},
downloadsEnabled: { pdf: true, results: true },
result: createMockDataset({
data: createMockDatasetData({
cols: [
@@ -130,7 +131,7 @@ export const LightThemeDownload = {
args: {
...LightThemeDefault.args,
downloadsEnabled: true,
downloadsEnabled: { pdf: true, results: true },
},
play: async ({ canvasElement }: { canvasElement: HTMLCanvasElement }) => {
@@ -163,7 +164,7 @@ export const DarkThemeDownload = {
args: {
...DarkThemeDefault.args,
downloadsEnabled: true,
downloadsEnabled: { pdf: true, results: true },
},
play: LightThemeDownload.play,

View File

@@ -5,7 +5,10 @@ import type { Dispatch, SetStateAction } from "react";
import { LoadingAndErrorWrapper } from "metabase/components/LoadingAndErrorWrapper";
import CS from "metabase/css/core/index.css";
import { EmbedFrame } from "metabase/public/components/EmbedFrame";
import type { DisplayTheme } from "metabase/public/lib/types";
import type {
DisplayTheme,
EmbedResourceDownloadOptions,
} from "metabase/public/lib/types";
import { PublicOrEmbeddedQuestionDownloadPopover } from "metabase/query_builder/components/QuestionDownloadPopover/QuestionDownloadPopover";
import { PublicMode } from "metabase/visualizations/click-actions/modes/PublicMode";
import Visualization from "metabase/visualizations/components/Visualization";
@@ -38,7 +41,7 @@ export interface PublicOrEmbeddedQuestionViewProps {
theme: DisplayTheme | undefined;
titled: boolean;
setCard: Dispatch<SetStateAction<Card<DatasetQuery> | null>>;
downloadsEnabled: boolean;
downloadsEnabled: EmbedResourceDownloadOptions;
}
export function PublicOrEmbeddedQuestionView({
@@ -61,7 +64,7 @@ export function PublicOrEmbeddedQuestionView({
const question = new Question(card, metadata);
const questionResultDownloadButton =
result && downloadsEnabled ? (
result && downloadsEnabled.results ? (
<PublicOrEmbeddedQuestionDownloadPopover
className={cx(
CS.m1,

View File

@@ -21,8 +21,8 @@ export type EmbedResourceParameter = {
};
export type EmbedResourceDownloadOptions = {
pdf: boolean;
results: boolean;
pdf?: boolean;
results?: boolean;
};
export type EmbeddingParameterVisibility = "disabled" | "enabled" | "locked";

View File

@@ -28,8 +28,8 @@ import { UserCollectionList } from "metabase/containers/UserCollectionList";
import { DashboardCopyModalConnected } from "metabase/dashboard/components/DashboardCopyModal";
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 { AutomaticDashboardApp } from "metabase/dashboard/containers/AutomaticDashboardApp";
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"
@@ -282,10 +282,7 @@ export const getRoutes = (store) => {
{/* INDIVIDUAL DASHBOARDS */}
<Route
path="/auto/dashboard/*"
component={AutomaticDashboardAppConnected}
/>
<Route path="/auto/dashboard/*" component={AutomaticDashboardApp} />
{/* REFERENCE */}
<Route path="/reference" title={t`Data Reference`}>