Compare commits
76 Commits
python-sec
...
refactor/s
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e1097f125d | ||
|
|
f7ea64feae | ||
|
|
8c09b77376 | ||
|
|
7c526b2cb5 | ||
|
|
9746bd6b0c | ||
|
|
12cfb0d84e | ||
|
|
2a8cd1a265 | ||
|
|
9cd2e5b2d2 | ||
|
|
1ddf765367 | ||
|
|
1ba65e68f8 | ||
|
|
7cfc2180ec | ||
|
|
71e4cb0b5b | ||
|
|
4bf9e5135f | ||
|
|
cbb3e0e73c | ||
|
|
8437f06469 | ||
|
|
0c5ed0d976 | ||
|
|
1124dad9e6 | ||
|
|
e51023ba5d | ||
|
|
0c24f64e29 | ||
|
|
e161dd291f | ||
|
|
dcabdcf0c3 | ||
|
|
4867d59de4 | ||
|
|
e840ba5b9f | ||
|
|
9f2a1730c9 | ||
|
|
b11afefe14 | ||
|
|
f82cbb8389 | ||
|
|
b22c0357da | ||
|
|
a95cf257e3 | ||
|
|
d813a09ff0 | ||
|
|
a217e74028 | ||
|
|
9f5c1fcd24 | ||
|
|
e99ce04f08 | ||
|
|
02821b1b9e | ||
|
|
c597ec12b6 | ||
|
|
a10278ebfa | ||
|
|
bd96ce46df | ||
|
|
627c2bea9b | ||
|
|
65ea5efa58 | ||
|
|
efd8c980c7 | ||
|
|
c0fc831d88 | ||
|
|
833a9a74a3 | ||
|
|
d5360d0279 | ||
|
|
3b52f1e304 | ||
|
|
2bebe320ff | ||
|
|
785abae9f5 | ||
|
|
a04d86123a | ||
|
|
c439fb0ba0 | ||
|
|
030c73dac7 | ||
|
|
ae888ea8a7 | ||
|
|
07525347bd | ||
|
|
272bf21e19 | ||
|
|
ede997d835 | ||
|
|
d70046c1b3 | ||
|
|
99909ecd3d | ||
|
|
2af6be9af5 | ||
|
|
1d648ec6de | ||
|
|
d5317fe202 | ||
|
|
b566ecf31f | ||
|
|
ea0ed6a20b | ||
|
|
b0b9c6ec32 | ||
|
|
44c2d86a6d | ||
|
|
8336c80c5f | ||
|
|
15ef9caac6 | ||
|
|
43926d80ed | ||
|
|
f8908c5904 | ||
|
|
e394905354 | ||
|
|
5b94267824 | ||
|
|
cade33e44b | ||
|
|
c426dd2dae | ||
|
|
754bac468c | ||
|
|
c7400745ba | ||
|
|
deaf06ee41 | ||
|
|
e75edb8af5 | ||
|
|
04f7ceb6ec | ||
|
|
97e7d0fe5c | ||
|
|
de7ed1d3cb |
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 |
@@ -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>;
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -25,7 +25,6 @@ export const setEditingDashboard = (dashboard: Dashboard | null) => {
|
||||
if (dashboard === null) {
|
||||
const location = getLocation(getState());
|
||||
const locationWithoutEditHash = { ...location, hash: "" };
|
||||
|
||||
dispatch(push(locationWithoutEditHash));
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 &&
|
||||
|
||||
@@ -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 />} />
|
||||
|
||||
@@ -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,
|
||||
]);
|
||||
|
||||
@@ -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()}
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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 & {
|
||||
|
||||
@@ -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;
|
||||
},
|
||||
|
||||
@@ -67,4 +67,5 @@ export const DEFAULT_DASHBOARD_DISPLAY_OPTIONS: EmbedDisplayParams = {
|
||||
theme: "light",
|
||||
downloadsEnabled: { pdf: true, results: true },
|
||||
withFooter: true,
|
||||
getClickActionMode: undefined,
|
||||
};
|
||||
|
||||
@@ -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 ? (
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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 />;
|
||||
};
|
||||
@@ -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,
|
||||
});
|
||||
};
|
||||
149
frontend/src/metabase/dashboard/context/context.redux.ts
Normal file
149
frontend/src/metabase/dashboard/context/context.redux.ts
Normal 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>;
|
||||
385
frontend/src/metabase/dashboard/context/context.tsx
Normal file
385
frontend/src/metabase/dashboard/context/context.tsx
Normal 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);
|
||||
}
|
||||
1
frontend/src/metabase/dashboard/context/index.ts
Normal file
1
frontend/src/metabase/dashboard/context/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./context";
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -29,7 +29,7 @@ export type EmbedDisplayParams = {
|
||||
hideParameters: EmbedHideParameters;
|
||||
font: EmbedFont;
|
||||
theme: DisplayTheme;
|
||||
getClickActionMode?: ClickActionModeGetter;
|
||||
getClickActionMode: ClickActionModeGetter | undefined;
|
||||
downloadsEnabled: EmbedResourceDownloadOptions;
|
||||
withFooter: boolean;
|
||||
};
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
}),
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -203,7 +203,7 @@ export const PublicOrEmbeddedQuestion = ({
|
||||
theme={theme}
|
||||
titled={titled}
|
||||
setCard={setCard}
|
||||
downloadsEnabled={downloadsEnabled.results}
|
||||
downloadsEnabled={downloadsEnabled}
|
||||
/>
|
||||
</LocaleProvider>
|
||||
);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -21,8 +21,8 @@ export type EmbedResourceParameter = {
|
||||
};
|
||||
|
||||
export type EmbedResourceDownloadOptions = {
|
||||
pdf: boolean;
|
||||
results: boolean;
|
||||
pdf?: boolean;
|
||||
results?: boolean;
|
||||
};
|
||||
|
||||
export type EmbeddingParameterVisibility = "disabled" | "enabled" | "locked";
|
||||
|
||||
@@ -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`}>
|
||||
|
||||
Reference in New Issue
Block a user