Compare commits

...

15 Commits

Author SHA1 Message Date
Oisin Coveney
495bccb8d5 Make sure that public don't worry about nav bars 2025-04-21 16:05:12 +01:00
Oisin Coveney
d8da2043d5 Remove PublicOrEmbeddedDashboardView logic and use context 2025-04-21 16:04:31 +01:00
Oisin Coveney
9f17e6962c Fix bad import 2025-04-21 16:02:54 +01:00
Oisin Coveney
4af1c11f21 Create components for dashboard embed 2025-04-21 16:02:53 +01:00
Oisin Coveney
aa54307492 Make RefreshWidget use dashboard context 2025-04-21 16:02:01 +01:00
Oisin Coveney
d556f612ac Remove unnecessary props 2025-04-21 16:01:56 +01:00
Oisin Coveney
5ca80580d2 Forces dashboard buttons to use context 2025-04-21 16:01:54 +01:00
Oisin Coveney
84b53d006b Create Grid, ParameterList, and Tabs components 2025-04-21 15:08:17 +01:00
Oisin Coveney
7d25ad8f59 Make sure downloadsEnabled is correct 2025-04-21 15:08:09 +01:00
Oisin Coveney
84507a1f53 Fix type errors 2025-04-21 15:08:08 +01:00
Oisin Coveney
f7ac5543ec Apparently I don't get type errors 2025-04-21 15:08:08 +01:00
Oisin Coveney
0d747e3dea Fix type errors 2025-04-21 15:08:08 +01:00
Oisin Coveney
3a715e7582 More fixds 2025-04-21 15:08:07 +01:00
Oisin Coveney
e4f6d46c05 Clean up a little more 2025-04-21 15:05:07 +01:00
Oisin Coveney
ba609272be Solve more type issues and clean up code 2025-04-21 15:05:07 +01:00
57 changed files with 1286 additions and 3020 deletions

View File

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

View File

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

View File

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

View File

@@ -1,26 +1,14 @@
import cx from "classnames";
import type { Query } from "history";
import type { ReactNode } from "react";
import { useCallback, useEffect, useMemo, useState } from "react";
import { usePrevious, useUnmount } from "react-use";
import _ from "underscore";
import { useMemo } from "react";
import { useListDatabasesQuery } from "metabase/api";
import { deletePermanently } from "metabase/archive/actions";
import { ArchivedEntityBanner } from "metabase/archive/components/ArchivedEntityBanner";
import {
type NewDashCardOpts,
type SetDashboardAttributesOpts,
moveDashboardToCollection,
setArchivedDashboard,
} from "metabase/dashboard/actions";
import type { NavigateToNewCardFromDashboardOpts } from "metabase/dashboard/components/DashCard/types";
import { DashboardHeader } from "metabase/dashboard/components/DashboardHeader";
import { useDashboardContext } from "metabase/dashboard/context";
import type {
CancelledFetchDashboardResult,
DashboardDisplayOptionControls,
FetchDashboardResult,
SuccessfulFetchDashboardResult,
} from "metabase/dashboard/types";
import Bookmarks from "metabase/entities/bookmarks";
import Dashboards from "metabase/entities/dashboards";
@@ -28,29 +16,10 @@ import { useDispatch } from "metabase/lib/redux";
import { getHasDataAccess, getHasNativeWrite } from "metabase/selectors/data";
import { FullWidthContainer } from "metabase/styled-components/layout/FullWidthContainer";
import { Box, Flex } from "metabase/ui";
import type {
CardId,
DashCardId,
DashCardVisualizationSettings,
DashboardCard,
DashboardId,
DashboardTabId,
Dashboard as IDashboard,
ParameterId,
ParameterValueOrArray,
TemporalUnit,
ValuesQueryType,
ValuesSourceConfig,
ValuesSourceType,
} from "metabase-types/api";
import type { DashboardCard } from "metabase-types/api";
import { isObject } from "metabase-types/guards";
import type {
DashboardSidebarName,
SelectedTabId,
State,
} from "metabase-types/store";
import { DASHBOARD_PDF_EXPORT_ROOT_ID, SIDEBAR_NAME } from "../../constants";
import { DASHBOARD_PDF_EXPORT_ROOT_ID } from "../../constants";
import { DashboardGridConnected } from "../DashboardGrid";
import { DashboardParameterPanel } from "../DashboardParameterPanel";
import { DashboardSidebars } from "../DashboardSidebars";
@@ -61,157 +30,76 @@ import {
DashboardEmptyState,
DashboardEmptyStateWithoutAddPrompt,
} from "./DashboardEmptyState/DashboardEmptyState";
import {
Description,
ExportAsPdfButton,
FullscreenToggle,
Grid,
NightModeToggleButton,
ParameterList,
RefreshWidget,
Tabs,
Title,
} from "./components";
export type DashboardProps = {
children?: ReactNode;
canManageSubscriptions: boolean;
isAdmin: boolean;
isNavbarOpen: boolean;
isEditing: boolean;
isSharing: boolean;
dashboardBeforeEditing: IDashboard | null;
isEditingParameter: boolean;
isDirty: boolean;
dashboard: IDashboard | null;
slowCards: Record<DashCardId, boolean>;
parameterValues: Record<ParameterId, ParameterValueOrArray>;
loadingStartTime: number | null;
clickBehaviorSidebarDashcard: DashboardCard | null;
isAddParameterPopoverOpen: boolean;
sidebar: State["dashboard"]["sidebar"];
isHeaderVisible: boolean;
isAdditionalInfoVisible: boolean;
selectedTabId: SelectedTabId;
isNavigatingBackToDashboard: boolean;
addCardOnLoad?: DashCardId;
editingOnLoad?: string | string[] | boolean;
autoScrollToDashcardId: DashCardId | undefined;
dashboardId: DashboardId;
parameterQueryParams: Query;
initialize: (opts?: { clearCache?: boolean }) => void;
cancelFetchDashboardCardData: () => void;
addCardToDashboard: (opts: {
dashId: DashboardId;
cardId: CardId;
tabId: DashboardTabId | null;
}) => void;
addHeadingDashCardToDashboard: (opts: NewDashCardOpts) => void;
addMarkdownDashCardToDashboard: (opts: NewDashCardOpts) => void;
addLinkDashCardToDashboard: (opts: NewDashCardOpts) => void;
setEditingDashboard: (dashboard: IDashboard | null) => void;
setDashboardAttributes: (opts: SetDashboardAttributesOpts) => void;
setSharing: (isSharing: boolean) => void;
toggleSidebar: (sidebarName: DashboardSidebarName) => void;
closeSidebar: () => void;
closeNavbar: () => void;
setErrorPage: (error: unknown) => void;
setParameterName: (id: ParameterId, name: string) => void;
setParameterType: (id: ParameterId, type: string, sectionId: string) => void;
navigateToNewCardFromDashboard: (
opts: NavigateToNewCardFromDashboardOpts,
) => void;
setParameterDefaultValue: (id: ParameterId, value: unknown) => void;
setParameterRequired: (id: ParameterId, value: boolean) => void;
setParameterTemporalUnits: (
id: ParameterId,
temporalUnits: TemporalUnit[],
) => void;
setParameterIsMultiSelect: (id: ParameterId, isMultiSelect: boolean) => void;
setParameterQueryType: (id: ParameterId, queryType: ValuesQueryType) => void;
setParameterSourceType: (
id: ParameterId,
sourceType: ValuesSourceType,
) => void;
setParameterSourceConfig: (
id: ParameterId,
config: ValuesSourceConfig,
) => void;
setParameterFilteringParameters: (
parameterId: ParameterId,
filteringParameters: ParameterId[],
) => void;
showAddParameterPopover: () => void;
removeParameter: (id: ParameterId) => void;
onReplaceAllDashCardVisualizationSettings: (
id: DashCardId,
settings: DashCardVisualizationSettings | null | undefined,
) => void;
onUpdateDashCardVisualizationSettings: (
id: DashCardId,
settings: DashCardVisualizationSettings | null | undefined,
) => void;
onUpdateDashCardColumnSettings: (
id: DashCardId,
columnKey: string,
settings?: Record<string, unknown> | null,
) => void;
updateDashboardAndCards: () => void;
setSidebar: (opts: { name: DashboardSidebarName }) => void;
hideAddParameterPopover: () => void;
fetchDashboard: (opts: {
dashId: DashboardId;
queryParams: Query;
options?: {
clearCache?: boolean;
preserveParameters?: boolean;
};
}) => Promise<FetchDashboardResult>;
fetchDashboardCardData: (opts?: {
isRefreshing?: boolean;
reload?: boolean;
clearCache?: boolean;
}) => void;
reportAutoScrolledToDashcard: () => void;
} & DashboardDisplayOptionControls;
function Dashboard(props: DashboardProps) {
function Dashboard() {
const {
addCardOnLoad,
addCardToDashboard,
autoScrollToDashcardId,
cancelFetchDashboardCardData,
dashboard,
dashboardId,
editingOnLoad,
fetchDashboard,
fetchDashboardCardData,
initialize,
isEditing,
isFullscreen,
isNavigatingBackToDashboard,
isNightMode = false,
isSharing,
onRefreshPeriodChange,
parameterValues,
reportAutoScrolledToDashcard,
selectedTabId,
setEditingDashboard,
setErrorPage,
setSharing,
toggleSidebar,
parameterQueryParams,
parameterQueryParams = {},
downloadsEnabled = true,
noLoaderWrapper = false,
} = props;
handleAddQuestion,
shouldRenderAsNightMode,
setArchivedDashboard,
moveDashboardToCollection,
deletePermanently,
dashboardBeforeEditing,
onFullscreenChange,
isAdditionalInfoVisible,
hasNightModeToggle,
onNightModeChange,
refreshPeriod,
setRefreshElapsedHook,
isEditingParameter,
slowCards,
navigateToNewCardFromDashboard,
showAddParameterPopover,
removeParameter,
addCardToDashboard,
clickBehaviorSidebarDashcard,
onReplaceAllDashCardVisualizationSettings,
onUpdateDashCardVisualizationSettings,
onUpdateDashCardColumnSettings,
setParameterName,
setParameterType,
setParameterDefaultValue,
setParameterIsMultiSelect,
setParameterQueryType,
setParameterSourceType,
setParameterSourceConfig,
setParameterFilteringParameters,
setParameterRequired,
setParameterTemporalUnits,
sidebar,
closeSidebar,
} = useDashboardContext();
const canWrite = Boolean(dashboard?.can_write);
const canRestore = Boolean(dashboard?.can_restore);
const canDelete = Boolean(dashboard?.can_delete);
const dispatch = useDispatch();
const [isInitialized, setIsInitialized] = useState(false);
const [error, setError] = useState<unknown>(null);
const previousDashboard = usePrevious(dashboard);
const previousDashboardId = usePrevious(dashboardId);
const previousTabId = usePrevious(selectedTabId);
const previousParameterValues = usePrevious(parameterValues);
const invalidateBookmarks = async () =>
await dispatch(Bookmarks.actions.invalidateLists());
const currentTabDashcards = useMemo(() => {
if (!dashboard || !Array.isArray(dashboard.dashcards)) {
@@ -225,12 +113,10 @@ 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;
<<<<<<< HEAD
const { data: databasesResponse } = useListDatabasesQuery();
const databases = useMemo(
() => databasesResponse?.data ?? [],
@@ -362,6 +248,8 @@ function Dashboard(props: DashboardProps) {
cancelFetchDashboardCardData();
});
=======
>>>>>>> fe48e208cd (Solve more type issues and clean up code)
const renderEmptyStates = () => {
if (!dashboardHasCards) {
return canWrite ? (
@@ -404,8 +292,7 @@ function Dashboard(props: DashboardProps) {
isFullscreen={isFullscreen}
isNightMode={shouldRenderAsNightMode}
loading={!dashboard}
error={error}
noWrapper={noLoaderWrapper}
noWrapper={true}
>
{() => {
if (!dashboard) {
@@ -425,14 +312,14 @@ function Dashboard(props: DashboardProps) {
canRestore={canRestore}
canDelete={canDelete}
onUnarchive={async () => {
await dispatch(setArchivedDashboard(false));
await dispatch(Bookmarks.actions.invalidateLists());
await setArchivedDashboard(false);
await invalidateBookmarks();
}}
onMove={({ id }) => dispatch(moveDashboardToCollection({ id }))}
onMove={({ id }) => moveDashboardToCollection({ id })}
onDeletePermanently={() => {
const { id } = dashboard;
const deleteAction = Dashboards.actions.delete({ id });
dispatch(deletePermanently(deleteAction));
deletePermanently(deleteAction);
}}
/>
)}
@@ -457,13 +344,13 @@ function Dashboard(props: DashboardProps) {
isNightMode={shouldRenderAsNightMode}
isFullscreen={isFullscreen}
onRefreshPeriodChange={onRefreshPeriodChange}
dashboardBeforeEditing={props.dashboardBeforeEditing}
onFullscreenChange={props.onFullscreenChange}
isAdditionalInfoVisible={props.isAdditionalInfoVisible}
hasNightModeToggle={props.hasNightModeToggle}
onNightModeChange={props.onNightModeChange}
refreshPeriod={props.refreshPeriod}
setRefreshElapsedHook={props.setRefreshElapsedHook}
dashboardBeforeEditing={dashboardBeforeEditing}
onFullscreenChange={onFullscreenChange}
isAdditionalInfoVisible={isAdditionalInfoVisible}
hasNightModeToggle={hasNightModeToggle}
onNightModeChange={onNightModeChange}
refreshPeriod={refreshPeriod}
setRefreshElapsedHook={setRefreshElapsedHook}
/>
</Box>
@@ -495,19 +382,18 @@ function Dashboard(props: DashboardProps) {
>
<DashboardGridConnected
clickBehaviorSidebarDashcard={
props.clickBehaviorSidebarDashcard
clickBehaviorSidebarDashcard
}
isNightMode={shouldRenderAsNightMode}
isFullscreen={props.isFullscreen}
isEditingParameter={props.isEditingParameter}
isEditing={props.isEditing}
isFullscreen={isFullscreen}
isEditingParameter={isEditingParameter}
isEditing={isEditing}
dashboard={dashboard}
slowCards={props.slowCards}
slowCards={slowCards}
navigateToNewCardFromDashboard={
props.navigateToNewCardFromDashboard
navigateToNewCardFromDashboard
}
selectedTabId={selectedTabId}
onEditingChange={handleSetEditing}
downloadsEnabled={downloadsEnabled}
autoScrollToDashcardId={autoScrollToDashcardId}
reportAutoScrolledToDashcard={
@@ -520,36 +406,32 @@ function Dashboard(props: DashboardProps) {
<DashboardSidebars
dashboard={dashboard}
showAddParameterPopover={props.showAddParameterPopover}
removeParameter={props.removeParameter}
addCardToDashboard={props.addCardToDashboard}
clickBehaviorSidebarDashcard={
props.clickBehaviorSidebarDashcard
}
showAddParameterPopover={showAddParameterPopover}
removeParameter={removeParameter}
addCardToDashboard={addCardToDashboard}
clickBehaviorSidebarDashcard={clickBehaviorSidebarDashcard}
onReplaceAllDashCardVisualizationSettings={
props.onReplaceAllDashCardVisualizationSettings
onReplaceAllDashCardVisualizationSettings
}
onUpdateDashCardVisualizationSettings={
props.onUpdateDashCardVisualizationSettings
onUpdateDashCardVisualizationSettings
}
onUpdateDashCardColumnSettings={
props.onUpdateDashCardColumnSettings
}
setParameterName={props.setParameterName}
setParameterType={props.setParameterType}
setParameterDefaultValue={props.setParameterDefaultValue}
setParameterIsMultiSelect={props.setParameterIsMultiSelect}
setParameterQueryType={props.setParameterQueryType}
setParameterSourceType={props.setParameterSourceType}
setParameterSourceConfig={props.setParameterSourceConfig}
onUpdateDashCardColumnSettings={onUpdateDashCardColumnSettings}
setParameterName={setParameterName}
setParameterType={setParameterType}
setParameterDefaultValue={setParameterDefaultValue}
setParameterIsMultiSelect={setParameterIsMultiSelect}
setParameterQueryType={setParameterQueryType}
setParameterSourceType={setParameterSourceType}
setParameterSourceConfig={setParameterSourceConfig}
setParameterFilteringParameters={
props.setParameterFilteringParameters
setParameterFilteringParameters
}
setParameterRequired={props.setParameterRequired}
setParameterTemporalUnits={props.setParameterTemporalUnits}
isFullscreen={props.isFullscreen}
sidebar={props.sidebar}
closeSidebar={props.closeSidebar}
setParameterRequired={setParameterRequired}
setParameterTemporalUnits={setParameterTemporalUnits}
isFullscreen={isFullscreen}
sidebar={sidebar}
closeSidebar={closeSidebar}
selectedTabId={selectedTabId}
onCancel={() => setSharing(false)}
/>
@@ -561,17 +443,14 @@ function Dashboard(props: DashboardProps) {
);
}
function isSuccessfulFetchDashboardResult(
result: FetchDashboardResult,
): result is SuccessfulFetchDashboardResult {
const hasError = "error" in result;
return !hasError;
}
export function isCancelledFetchDashboardResult(
result: FetchDashboardResult,
): result is CancelledFetchDashboardResult {
return isObject(result.payload) && Boolean(result.payload.isCancelled);
}
Dashboard.Tabs = Tabs;
Dashboard.Title = Title;
Dashboard.Description = Description;
Dashboard.ParameterList = ParameterList;
Dashboard.Grid = Grid;
Dashboard.FullscreenToggle = FullscreenToggle;
Dashboard.NightModeToggle = NightModeToggleButton;
Dashboard.ExportAsPdfButton = ExportAsPdfButton;
Dashboard.RefreshWidget = RefreshWidget;
export { Dashboard };

View File

@@ -0,0 +1,7 @@
import { useDashboardContext } from "metabase/dashboard/context";
export const Description = () => {
const { dashboard } = useDashboardContext();
return <>{dashboard?.description}</>;
};

View File

@@ -0,0 +1,59 @@
import { match } from "ts-pattern";
import { t } from "ttag";
import {
type DashboardAccessedVia,
trackExportDashboardToPDF,
} from "metabase/dashboard/analytics";
import { DASHBOARD_PDF_EXPORT_ROOT_ID } from "metabase/dashboard/constants";
import { isJWT } from "metabase/lib/utils";
import { isUuid } from "metabase/lib/uuid";
import { Button, Icon } from "metabase/ui";
import {
getExportTabAsPdfButtonText,
saveDashboardPdf,
} from "metabase/visualizations/lib/save-dashboard-pdf";
import { useDashboardContext } from "metabase/dashboard/context";
export const ExportAsPdfButton = ({
color,
}: {
color?: string;
}) => {
const { dashboard } = useDashboardContext()
const saveAsPDF = () => {
const dashboardAccessedVia = match(dashboard?.id)
.returnType<DashboardAccessedVia>()
.when(isJWT, () => "static-embed")
.when(isUuid, () => "public-link")
.otherwise(() => "sdk-embed");
trackExportDashboardToPDF({
dashboardAccessedVia,
});
const cardNodeSelector = `#${DASHBOARD_PDF_EXPORT_ROOT_ID}`;
return saveDashboardPdf(
cardNodeSelector,
dashboard?.name ?? t`Exported dashboard`,
);
};
if (!dashboard) {
return null;
}
return (
<Button
variant="subtle"
px="0.5rem"
leftSection={<Icon name="document" />}
color={color || "text-dark"}
onClick={saveAsPDF}
{...buttonProps}
>
{getExportTabAsPdfButtonText(dashboard?.tabs)}
</Button>
);
};

View File

@@ -1,12 +1,11 @@
import { t } from "ttag";
import { ToolbarButton } from "metabase/components/ToolbarButton";
import type { DashboardFullscreenControls } from "metabase/dashboard/types";
import { useDashboardContext } from "metabase/dashboard/context";
export const FullscreenToggle = () => {
const { isFullscreen, onFullscreenChange } = useDashboardContext();
export const FullscreenToggle = ({
isFullscreen,
onFullscreenChange,
}: DashboardFullscreenControls) => {
const label = isFullscreen ? t`Exit fullscreen` : t`Enter fullscreen`;
return (
<ToolbarButton

View File

@@ -0,0 +1,53 @@
import { useDashboardContext } from "metabase/dashboard/context";
import type { FlexProps } from "metabase/ui";
import { DashboardGridConnected } from "../../DashboardGrid";
export const Grid = ({
className,
h,
w,
flex,
}: Pick<FlexProps, "className" | "h" | "w" | "flex">) => {
const {
dashboard,
isEditing,
isFullscreen,
shouldRenderAsNightMode,
handleSetEditing,
selectedTabId,
clickBehaviorSidebarDashcard,
isEditingParameter,
slowCards,
navigateToNewCardFromDashboard,
autoScrollToDashcardId,
reportAutoScrolledToDashcard,
downloadsEnabled,
} = useDashboardContext();
if (!dashboard) {
return null;
}
return (
<DashboardGridConnected
className={className}
h={h}
w={w}
flex={flex}
clickBehaviorSidebarDashcard={clickBehaviorSidebarDashcard}
isNightMode={shouldRenderAsNightMode}
isFullscreen={isFullscreen}
isEditingParameter={isEditingParameter}
isEditing={isEditing}
dashboard={dashboard}
slowCards={slowCards}
navigateToNewCardFromDashboard={navigateToNewCardFromDashboard}
selectedTabId={selectedTabId}
onEditingChange={handleSetEditing}
downloadsEnabled={downloadsEnabled}
autoScrollToDashcardId={autoScrollToDashcardId}
reportAutoScrolledToDashcard={reportAutoScrolledToDashcard}
/>
);
};

View File

@@ -1,12 +1,11 @@
import { t } from "ttag";
import { ToolbarButton } from "metabase/components/ToolbarButton";
import type { DashboardNightModeControls } from "metabase/dashboard/types";
import { useDashboardContext } from "metabase/dashboard/context";
export const NightModeToggleButton = () => {
const { isNightMode, onNightModeChange } = useDashboardContext();
export const NightModeToggleButton = ({
isNightMode,
onNightModeChange,
}: Pick<DashboardNightModeControls, "isNightMode" | "onNightModeChange">) => {
const label = isNightMode ? t`Daytime mode` : t`Nighttime mode`;
return (
<ToolbarButton

View File

@@ -0,0 +1,22 @@
import { forwardRef } from "react";
import { useDashboardContext } from "metabase/dashboard/context";
import { Box } from "metabase/ui";
import { DashboardParameterList } from "../../DashboardParameterList";
export const ParameterList = forwardRef(function _ParameterList(
{ className }: { className?: string },
ref,
) {
const { isFullscreen } = useDashboardContext();
return (
<Box ref={ref}>
<DashboardParameterList
className={className}
isFullscreen={isFullscreen}
/>
</Box>
);
});

View File

@@ -0,0 +1,73 @@
import { useState } from "react";
import { t } from "ttag";
import { useDashboardContext } from "metabase/dashboard/context";
import { Box, type BoxProps, Popover } from "metabase/ui";
import { RefreshOption } from "./RefreshOption";
import { RefreshWidgetTarget } from "./RefreshWidgetTarget";
const toSeconds = (minutes: number) => minutes * 60;
const OPTIONS = [
{ name: t`Off`, period: null },
{ name: t`1 minute`, period: toSeconds(1) },
{ name: t`5 minutes`, period: toSeconds(5) },
{ name: t`10 minutes`, period: toSeconds(10) },
{ name: t`15 minutes`, period: toSeconds(15) },
{ name: t`30 minutes`, period: toSeconds(30) },
{ name: t`60 minutes`, period: toSeconds(60) },
];
const RefreshPanel = ({
onClick,
...boxProps
}: { onClick?: () => void } & BoxProps) => {
const { onRefreshPeriodChange, refreshPeriod: period } =
useDashboardContext();
return (
<Box {...boxProps}>
<Box
fw="bold"
fz="sm"
tt="uppercase"
mb="md"
ml="sm"
c="text-medium"
>{t`Auto Refresh`}</Box>
<ul>
{OPTIONS.map((option) => (
<RefreshOption
key={option.period}
name={option.name}
period={option.period}
selected={option.period === period}
onClick={() => {
onClick?.();
onRefreshPeriodChange(option.period);
}}
/>
))}
</ul>
</Box>
);
};
export const RefreshWidget = () => {
const [isOpen, setIsOpen] = useState(false);
const { refreshPeriod: period, elapsed } = useDashboardContext();
return (
<Popover position="bottom-end" opened={isOpen} onChange={setIsOpen}>
<Popover.Target>
<Box onClick={() => setIsOpen(!isOpen)}>
<RefreshWidgetTarget elapsed={elapsed} period={period} />
</Box>
</Popover.Target>
<Popover.Dropdown>
<RefreshPanel p="md" miw="12.5rem" />
</Popover.Dropdown>
</Popover>
);
};

View File

@@ -3,7 +3,7 @@ import { t } from "ttag";
import { CountdownIcon } from "metabase/components/icons/CountdownIcon";
import { isNotNull } from "metabase/lib/types";
import { ToolbarButton } from "../../../components/ToolbarButton/ToolbarButton";
import { ToolbarButton } from "../../../../../components/ToolbarButton/ToolbarButton";
type RefreshWidgetTargetProps = {
period: number | null;

View File

@@ -0,0 +1,18 @@
import { useDashboardContext } from "metabase/dashboard/context";
import type { FlexProps } from "metabase/ui";
import { DashboardTabs } from "../../DashboardTabs";
export const Tabs = (flexProps: FlexProps) => {
const { dashboard, isEditing } = useDashboardContext();
if (!dashboard) {
return null;
}
return (
<DashboardTabs
dashboardId={dashboard.id}
isEditing={isEditing}
{...flexProps}
/>
);
};

View File

@@ -0,0 +1,7 @@
import { useDashboardContext } from "metabase/dashboard/context";
export const Title = () => {
const { dashboard } = useDashboardContext();
return <>{dashboard?.name}</>;
};

View File

@@ -0,0 +1,9 @@
export * from "./Grid";
export * from "./ParameterList";
export * from "./Tabs";
export * from "./Title";
export * from "./Description";
export * from "./FullscreenToggle";
export * from "./NightModeToggleButton";
export * from "./ExportAsPdfButton";
export * from "./RefreshWidget";

View File

@@ -31,8 +31,9 @@ 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 { Box, Flex, type FlexProps } from "metabase/ui";
import LegendS from "metabase/visualizations/components/Legend.module.css";
import type { ClickActionModeGetter } from "metabase/visualizations/types";
import {
@@ -151,12 +152,13 @@ type OwnProps = {
opts: NavigateToNewCardFromDashboardOpts,
) => void;
onEditingChange?: (dashboard: Dashboard | null) => void;
downloadsEnabled: boolean;
downloadsEnabled: EmbedResourceDownloadOptions;
autoScrollToDashcardId: DashCardId | undefined;
reportAutoScrolledToDashcard: () => void;
};
type DashboardGridProps = OwnProps &
type DashboardGridProps = FlexProps &
OwnProps &
DashboardGridReduxProps &
ExplicitSizeProps & {
forwardedRef?: ForwardedRef<HTMLDivElement>;
@@ -526,7 +528,7 @@ class DashboardGridInner extends Component<
isMobile: boolean;
gridItemWidth: number;
totalNumGridCols: number;
downloadsEnabled: boolean;
downloadsEnabled: EmbedResourceDownloadOptions;
shouldAutoScrollTo: boolean;
reportAutoScrolledToDashcard: () => void;
},
@@ -659,19 +661,24 @@ class DashboardGridInner extends Component<
}
render() {
const { dashboard, width, forwardedRef } = this.props;
const { dashboard, width, forwardedRef, className, h, w, flex } =
this.props;
return (
<Flex
align="center"
justify="center"
className={cx(S.DashboardGridContainer, {
[S.isFixedWidth]: dashboard?.width === "fixed",
className,
})}
ref={forwardedRef}
data-testid="dashboard-grid"
style={{
"--dashboard-fixed-width": FIXED_WIDTH,
}}
h={h}
w={w}
flex={flex}
>
{width > 0 ? this.renderGrid() : <div />}
{this.renderAddSeriesModal()}
@@ -712,4 +719,4 @@ const DashboardGrid = forwardRef<HTMLDivElement, DashboardGridProps>(
export const DashboardGridConnected = _.compose(
ExplicitSize(),
connector,
)(DashboardGrid) as ComponentType<OwnProps>;
)(DashboardGrid) as ComponentType<OwnProps & FlexProps>;

View File

@@ -1,9 +1,8 @@
import type { Query } from "history";
import { useState } from "react";
import { useMount } from "react-use";
import { t } from "ttag";
import { useGetCollectionQuery } from "metabase/api";
import { skipToken, useGetCollectionQuery } from "metabase/api";
import { LeaveConfirmationModalContent } from "metabase/components/LeaveConfirmationModal";
import Modal from "metabase/components/Modal";
import {
@@ -11,8 +10,8 @@ import {
fetchDashboard,
setSidebar,
} from "metabase/dashboard/actions";
import { useDashboardContext } from "metabase/dashboard/context";
import {
getDashboardComplete,
getIsAdditionalInfoVisible,
getIsDirty,
getIsEditing,
@@ -37,25 +36,27 @@ import { CancelEditButton, SaveEditButton } from "./buttons";
export type DashboardHeaderProps = {
dashboard: Dashboard;
dashboardBeforeEditing?: Dashboard | null;
parameterQueryParams: Query;
parameterQueryParams: Record<string, string>;
isAdditionalInfoVisible: boolean;
} & DashboardFullscreenControls &
DashboardRefreshPeriodControls &
DashboardNightModeControls;
export const DashboardHeaderInner = ({
dashboard,
dashboardBeforeEditing,
hasNightModeToggle,
isFullscreen,
isNightMode,
parameterQueryParams,
onFullscreenChange,
onNightModeChange,
onRefreshPeriodChange,
refreshPeriod,
setRefreshElapsedHook,
}: DashboardHeaderProps) => {
export const DashboardHeaderInner = () => {
const {
dashboard,
dashboardBeforeEditing,
hasNightModeToggle,
isFullscreen,
isNightMode,
parameterQueryParams,
onFullscreenChange,
onNightModeChange,
onRefreshPeriodChange,
refreshPeriod,
setRefreshElapsedHook,
} = useDashboardContext();
const [showCancelWarning, setShowCancelWarning] = useState(false);
const dispatch = useDispatch();
@@ -75,7 +76,7 @@ export const DashboardHeaderInner = ({
);
const { data: collection, isLoading: isLoadingCollection } =
useGetCollectionQuery({ id: dashboard.collection_id || "root" });
useGetCollectionQuery(dashboard ? { id: dashboard.collection_id || "root" } : skipToken);
const onRequestCancel = () => {
if (isDirty && isEditing) {
@@ -86,14 +87,14 @@ export const DashboardHeaderInner = ({
};
const onCancel = () => {
dispatch(
if (dashboard) {
fetchDashboard({
dashId: dashboard.id,
queryParams: parameterQueryParams,
queryParams: parameterQueryParams ?? {},
options: { preserveParameters: true },
}),
);
dispatch(cancelEditingDashboard());
})
cancelEditingDashboard();
}
};
const getEditWarning = (dashboard: Dashboard) => {
@@ -136,13 +137,13 @@ export const DashboardHeaderInner = ({
);
}
const hasLastEditInfo = dashboard["last-edit-info"] != null;
const hasLastEditInfo = dashboard?.["last-edit-info"] != null;
const editingButtons = getEditingButtons();
return (
<>
<DashboardHeaderView
{dashboard && <DashboardHeaderView
dashboard={dashboard}
collection={collection}
isBadgeVisible={!isEditing && !isFullscreen && isAdditionalInfoVisible}
@@ -158,8 +159,8 @@ export const DashboardHeaderInner = ({
isEmbeddingSdk
? undefined
: () => {
dispatch(setSidebar({ name: SIDEBAR_NAME.info }));
}
dispatch(setSidebar({ name: SIDEBAR_NAME.info }));
}
}
refreshPeriod={refreshPeriod}
onRefreshPeriodChange={onRefreshPeriodChange}
@@ -170,7 +171,7 @@ export const DashboardHeaderInner = ({
onNightModeChange={onNightModeChange}
isNightMode={isNightMode}
/>
}
<Modal isOpen={showCancelWarning}>
<LeaveConfirmationModalContent
onAction={onCancel}
@@ -181,12 +182,12 @@ export const DashboardHeaderInner = ({
);
};
export const DashboardHeader = (props: DashboardHeaderProps) => {
const dashboard = useSelector(getDashboardComplete);
export const DashboardHeader = () => {
const { dashboard } = useDashboardContext();
if (!dashboard) {
return null;
}
return <DashboardHeaderInner {...props} />;
return <DashboardHeaderInner />;
};

View File

@@ -1,9 +1,9 @@
import { DashboardSharingMenu } from "metabase/embedding/components/SharingMenu/DashboardSharingMenu";
import { Center, Divider } from "metabase/ui";
import { RefreshWidget } from "../../Dashboard/components/RefreshWidget";
import { DashboardBookmark } from "../../DashboardBookmark";
import { ExtraEditButtonsMenu } from "../../ExtraEditButtonsMenu";
import { RefreshWidget } from "../../RefreshWidget";
import {
AddActionElementButton,
AddFilterParameterButton,
@@ -80,9 +80,7 @@ export const dashboardActionButtons: Record<
// VIEW ACTIONS
[DASHBOARD_ACTION.EDIT_DASHBOARD]: {
component: ({ onRefreshPeriodChange }) => (
<EditDashboardButton onRefreshPeriodChange={onRefreshPeriodChange} />
),
component: EditDashboardButton,
enabled: ({ isFullscreen, isEditing, canEdit }) =>
!isFullscreen && !isEditing && canEdit,
},
@@ -105,12 +103,7 @@ export const dashboardActionButtons: Record<
enabled: ({ dashboard, isEditing }) => !isEditing && !dashboard?.archived,
},
[DASHBOARD_ACTION.NIGHT_MODE_TOGGLE]: {
component: ({ isNightMode, onNightModeChange }) => (
<NightModeToggleButton
isNightMode={isNightMode}
onNightModeChange={onNightModeChange}
/>
),
component: NightModeToggleButton,
enabled: ({
isEditing,
isFullscreen,
@@ -127,12 +120,7 @@ export const dashboardActionButtons: Record<
),
},
[DASHBOARD_ACTION.FULLSCREEN_TOGGLE]: {
component: ({ isFullscreen, onFullscreenChange }) => (
<FullscreenToggle
isFullscreen={isFullscreen}
onFullscreenChange={onFullscreenChange}
/>
),
component: FullscreenToggle,
enabled: ({ isFullscreen, isPublic, isEmbeddingSdk = false }) =>
isPublic || isFullscreen || isEmbeddingSdk,
},
@@ -173,18 +161,13 @@ export const dashboardActionButtons: Record<
// ACTIONS WHEN DASHBOARD IS ANALYTICS DASHBOARD
[DASHBOARD_ACTION.COPY_ANALYTICS_DASHBOARD]: {
component: () => <CopyAnalyticsDashboardButton />,
component: CopyAnalyticsDashboardButton,
enabled: ({ isAnalyticsDashboard = false }) => {
return isAnalyticsDashboard;
},
},
[DASHBOARD_ACTION.FULLSCREEN_ANALYTICS_DASHBOARD]: {
component: ({ isFullscreen, onFullscreenChange }) => (
<FullscreenAnalyticsDashboard
isFullscreen={isFullscreen}
onFullscreenChange={onFullscreenChange}
/>
),
component: FullscreenAnalyticsDashboard,
enabled: ({ isAnalyticsDashboard = false }) => isAnalyticsDashboard,
},

View File

@@ -17,7 +17,6 @@ import {
} from "metabase/dashboard/actions";
import { useSetDashboardAttributeHandler } from "metabase/dashboard/components/Dashboard/use-set-dashboard-attribute";
import { DashboardHeaderButtonRow } from "metabase/dashboard/components/DashboardHeader/DashboardHeaderButtonRow/DashboardHeaderButtonRow";
import { DashboardTabs } from "metabase/dashboard/components/DashboardTabs";
import {
getCanResetFilters,
getIsEditing,
@@ -43,6 +42,7 @@ import { Box, Flex } from "metabase/ui";
import type { Collection, Dashboard } from "metabase-types/api";
import { FixedWidthContainer } from "../Dashboard/DashboardComponents";
import { Tabs } from "../Dashboard/components/Tabs";
import { SIDEBAR_WIDTH } from "../Sidebar";
import S from "./DashboardHeaderView.module.css";
@@ -246,7 +246,7 @@ export function DashboardHeaderView({
data-testid="fixed-width-dashboard-tabs"
isFixedWidth={dashboard?.width === "fixed"}
>
<DashboardTabs dashboardId={dashboard.id} isEditing={isEditing} />
<Tabs></Tabs>
</FixedWidthContainer>
</FullWidthContainer>
</div>

View File

@@ -1,25 +1,20 @@
import { t } from "ttag";
import { ToolbarButton } from "metabase/components/ToolbarButton";
import { addActionToDashboard } from "metabase/dashboard/actions";
import { getDashboard, getSelectedTabId } from "metabase/dashboard/selectors";
import { useDispatch, useSelector } from "metabase/lib/redux";
import { useDashboardContext } from "metabase/dashboard/context";
export const AddActionElementButton = () => {
const dispatch = useDispatch();
const dashboard = useSelector(getDashboard);
const selectedTabId = useSelector(getSelectedTabId);
const { dashboard, selectedTabId, addActionToDashboard } =
useDashboardContext();
const onAddAction = () => {
if (dashboard) {
dispatch(
addActionToDashboard({
dashId: dashboard.id,
tabId: selectedTabId,
displayType: "button",
action: {},
}),
);
addActionToDashboard({
dashId: dashboard.id,
tabId: selectedTabId,
displayType: "button",
action: {},
});
}
};

View File

@@ -2,14 +2,8 @@ import { useLayoutEffect, useRef, useState } from "react";
import { t } from "ttag";
import { ToolbarButton } from "metabase/components/ToolbarButton";
import {
addParameter,
hideAddParameterPopover,
showAddParameterPopover,
} from "metabase/dashboard/actions";
import { getIsAddParameterPopoverOpen } from "metabase/dashboard/selectors";
import { useDispatch, useSelector } from "metabase/lib/redux";
import { useRegisterShortcut } from "metabase/palette/hooks/useRegisterShortcut";
import { useDashboardContext } from "metabase/dashboard/context";
import {
type ParameterSection,
getDashboardParameterSections,
@@ -20,15 +14,20 @@ import { Icon, Menu, Text } from "metabase/ui";
export const AddFilterParameterButton = () => {
const sections = getDashboardParameterSections();
const dispatch = useDispatch();
const isOpened = useSelector(getIsAddParameterPopoverOpen);
const {
isAddParameterPopoverOpen,
hideAddParameterPopover,
showAddParameterPopover,
addParameter,
} = useDashboardContext();
const [rightSectionWidth, setRightSectionWidth] = useState(0);
const rightSectionWidthRef = useRef(0);
const handleItemClick = (section: ParameterSection) => {
const defaultOption = getDefaultOptionForParameterSectionMap()[section.id];
if (defaultOption) {
dispatch(addParameter(defaultOption));
addParameter(defaultOption);
}
};
@@ -46,24 +45,24 @@ export const AddFilterParameterButton = () => {
{
id: "dashboard-add-filter",
perform: () =>
isOpened
? dispatch(hideAddParameterPopover())
: dispatch(showAddParameterPopover()),
isAddParameterPopoverOpen
? (hideAddParameterPopover())
: (showAddParameterPopover()),
},
],
[isOpened],
[isAddParameterPopoverOpen],
);
useLayoutEffect(() => {
if (isOpened) {
if (isAddParameterPopoverOpen) {
setRightSectionWidth(rightSectionWidthRef.current);
}
}, [isOpened]);
}, [isAddParameterPopoverOpen]);
return (
<Menu
opened={isOpened}
onClose={() => dispatch(hideAddParameterPopover())}
opened={isAddParameterPopoverOpen}
onClose={() => hideAddParameterPopover()}
position="bottom-end"
trapFocus
>
@@ -71,9 +70,9 @@ export const AddFilterParameterButton = () => {
<ToolbarButton
icon="filter"
onClick={() =>
isOpened
? dispatch(hideAddParameterPopover())
: dispatch(showAddParameterPopover())
isAddParameterPopoverOpen
? hideAddParameterPopover()
: showAddParameterPopover()
}
aria-label={t`Add a filter or parameter`}
tooltipLabel={t`Add a filter or parameter`}

View File

@@ -1,38 +1,32 @@
import { t } from "ttag";
import { ToolbarButton } from "metabase/components/ToolbarButton";
import {
addHeadingDashCardToDashboard,
addMarkdownDashCardToDashboard,
} from "metabase/dashboard/actions";
import { getDashboard, getSelectedTabId } from "metabase/dashboard/selectors";
import { useDispatch, useSelector } from "metabase/lib/redux";
import { useDashboardContext } from "metabase/dashboard/context";
import { Group, Icon, Menu } from "metabase/ui";
export const AddHeadingOrTextButton = () => {
const dispatch = useDispatch();
const dashboard = useSelector(getDashboard);
const selectedTabId = useSelector(getSelectedTabId);
const {
dashboard,
selectedTabId,
addMarkdownDashCardToDashboard,
addHeadingDashCardToDashboard,
} = useDashboardContext();
const onAddMarkdownBox = () => {
if (dashboard) {
dispatch(
addMarkdownDashCardToDashboard({
dashId: dashboard.id,
tabId: selectedTabId,
}),
);
addMarkdownDashCardToDashboard({
dashId: dashboard.id,
tabId: selectedTabId,
});
}
};
const onAddHeading = () => {
if (dashboard) {
dispatch(
addHeadingDashCardToDashboard({
dashId: dashboard.id,
tabId: selectedTabId,
}),
);
addHeadingDashCardToDashboard({
dashId: dashboard.id,
tabId: selectedTabId,
});
}
};

View File

@@ -1,38 +1,32 @@
import { t } from "ttag";
import { ToolbarButton } from "metabase/components/ToolbarButton";
import {
addIFrameDashCardToDashboard,
addLinkDashCardToDashboard,
} from "metabase/dashboard/actions";
import { getDashboard, getSelectedTabId } from "metabase/dashboard/selectors";
import { useDispatch, useSelector } from "metabase/lib/redux";
import { useDashboardContext } from "metabase/dashboard/context";
import { Group, Icon, Menu } from "metabase/ui";
export const AddLinkOrEmbedButton = () => {
const dispatch = useDispatch();
const dashboard = useSelector(getDashboard);
const selectedTabId = useSelector(getSelectedTabId);
const {
dashboard,
selectedTabId,
addLinkDashCardToDashboard,
addIFrameDashCardToDashboard,
} = useDashboardContext();
const onAddLinkCard = () => {
if (dashboard) {
dispatch(
addLinkDashCardToDashboard({
dashId: dashboard.id,
tabId: selectedTabId,
}),
);
addLinkDashCardToDashboard({
dashId: dashboard.id,
tabId: selectedTabId,
});
}
};
const onAddIFrameCard = () => {
if (dashboard) {
dispatch(
addIFrameDashCardToDashboard({
dashId: dashboard.id,
tabId: selectedTabId,
}),
);
addIFrameDashCardToDashboard({
dashId: dashboard.id,
tabId: selectedTabId,
});
}
};

View File

@@ -1,17 +1,13 @@
import { t } from "ttag";
import { ToolbarButton } from "metabase/components/ToolbarButton";
import { toggleSidebar } from "metabase/dashboard/actions";
import { SIDEBAR_NAME } from "metabase/dashboard/constants";
import { getSidebar } from "metabase/dashboard/selectors";
import { useDispatch, useSelector } from "metabase/lib/redux";
import { useRegisterShortcut } from "metabase/palette/hooks/useRegisterShortcut";
import { addDashboardQuestion } from "../../QuestionPicker/actions";
import { useDashboardContext } from "metabase/dashboard/context";
export const AddQuestionButton = () => {
const dispatch = useDispatch();
const sidebar = useSelector(getSidebar);
const { sidebar, toggleSidebar, addDashboardQuestion } = useDashboardContext();
const sidebarOpen = sidebar.name === SIDEBAR_NAME.addQuestion;
@@ -23,11 +19,11 @@ export const AddQuestionButton = () => {
[
{
id: "dashboard-add-notebook-question",
perform: () => dispatch(addDashboardQuestion("notebook")),
perform: () => (addDashboardQuestion("notebook")),
},
{
id: "dashboard-add-native-question",
perform: () => dispatch(addDashboardQuestion("native")),
perform: () => (addDashboardQuestion("native")),
},
],
[sidebarOpen],
@@ -38,7 +34,7 @@ export const AddQuestionButton = () => {
tooltipLabel={addQuestionButtonHint}
icon="add"
isActive={sidebar.name === SIDEBAR_NAME.addQuestion}
onClick={() => dispatch(toggleSidebar(SIDEBAR_NAME.addQuestion))}
onClick={() => toggleSidebar(SIDEBAR_NAME.addQuestion)}
aria-label={addQuestionButtonHint}
/>
);

View File

@@ -1,29 +1,25 @@
import { t } from "ttag";
import { ToolbarButton } from "metabase/components/ToolbarButton";
import { addSectionToDashboard } from "metabase/dashboard/actions";
import { SectionLayoutPreview } from "metabase/dashboard/components/DashboardHeader/SectionLayoutPreview";
import { useDashboardContext } from "metabase/dashboard/context";
import { type SectionLayout, layoutOptions } from "metabase/dashboard/sections";
import { getDashboard, getSelectedTabId } from "metabase/dashboard/selectors";
import { darken } from "metabase/lib/colors";
import { useDispatch, useSelector } from "metabase/lib/redux";
import { Flex, Menu } from "metabase/ui";
import AddSectionButtonS from "./AddSectionButton.module.css";
export const AddSectionButton = () => {
const dispatch = useDispatch();
const dashboard = useSelector(getDashboard);
const selectedTabId = useSelector(getSelectedTabId);
const { dashboard, selectedTabId, addSectionToDashboard } =
useDashboardContext();
const onAddSection = (sectionLayout: SectionLayout) => {
if (dashboard) {
dispatch(
addSectionToDashboard({
dashId: dashboard.id,
tabId: selectedTabId,
sectionLayout,
}),
);
addSectionToDashboard({
dashId: dashboard.id,
tabId: selectedTabId,
sectionLayout,
});
}
};
return (

View File

@@ -5,12 +5,27 @@ import Button from "metabase/core/components/Button";
import ButtonsS from "metabase/css/components/buttons.module.css";
import CS from "metabase/css/core/index.css";
import { useRegisterShortcut } from "metabase/palette/hooks/useRegisterShortcut";
import { useDashboardContext } from "metabase/dashboard/context";
export const CancelEditButton = (props: { onClick?: () => void }) => {
const { fetchDashboard, cancelEditingDashboard, dashboard, parameterQueryParams } = useDashboardContext()
const onClickCancel = props.onClick ?? (() => {
if (dashboard) {
fetchDashboard({
dashId: dashboard.id,
queryParams: parameterQueryParams ?? {},
options: { preserveParameters: true },
})
cancelEditingDashboard();
}
})
export const CancelEditButton = (props: { onClick: () => void }) => {
useRegisterShortcut([
{
id: "dashboard-cancel-edit",
perform: props.onClick,
perform: onClickCancel
},
]);
@@ -18,7 +33,7 @@ export const CancelEditButton = (props: { onClick: () => void }) => {
<Button
key="cancel"
className={cx(ButtonsS.Button, ButtonsS.ButtonSmall, CS.mr1)}
onClick={props.onClick}
onClick={onClickCancel}
>
{t`Cancel`}
</Button>

View File

@@ -1,22 +1,19 @@
import { t } from "ttag";
import { ToolbarButton } from "metabase/components/ToolbarButton";
import { closeSidebar, setSidebar } from "metabase/dashboard/actions";
import { SIDEBAR_NAME } from "metabase/dashboard/constants";
import { getIsShowDashboardInfoSidebar } from "metabase/dashboard/selectors";
import { useDispatch, useSelector } from "metabase/lib/redux";
import { useRegisterShortcut } from "metabase/palette/hooks/useRegisterShortcut";
import { useDashboardContext } from "metabase/dashboard/context";
export const DashboardInfoButton = () => {
const dispatch = useDispatch();
const isShowingDashboardInfoSidebar = useSelector(
getIsShowDashboardInfoSidebar,
);
const { sidebar, closeSidebar, setSidebar } = useDashboardContext();
const isShowingDashboardInfoSidebar = sidebar?.name === SIDEBAR_NAME.info;
const handleClick = () => {
isShowingDashboardInfoSidebar
? dispatch(closeSidebar())
: dispatch(setSidebar({ name: SIDEBAR_NAME.info }));
? (closeSidebar())
: (setSidebar({ name: SIDEBAR_NAME.info }));
};
useRegisterShortcut(

View File

@@ -1,21 +1,17 @@
import { t } from "ttag";
import { ToolbarButton } from "metabase/components/ToolbarButton";
import { setEditingDashboard } from "metabase/dashboard/actions";
import { getDashboardComplete } from "metabase/dashboard/selectors";
import type { DashboardRefreshPeriodControls } from "metabase/dashboard/types";
import { useDispatch, useSelector } from "metabase/lib/redux";
import { useRegisterShortcut } from "metabase/palette/hooks/useRegisterShortcut";
import { useDashboardContext } from "metabase/dashboard/context";
export const EditDashboardButton = () => {
const { dashboard, onRefreshPeriodChange, setEditingDashboard } =
useDashboardContext();
export const EditDashboardButton = ({
onRefreshPeriodChange,
}: Pick<DashboardRefreshPeriodControls, "onRefreshPeriodChange">) => {
const dispatch = useDispatch();
const dashboard = useSelector(getDashboardComplete);
const onBeginEditing = () => {
if (dashboard) {
onRefreshPeriodChange(null);
dispatch(setEditingDashboard(dashboard));
setEditingDashboard(dashboard);
}
};

View File

@@ -1,68 +0,0 @@
import cx from "classnames";
import { match } from "ts-pattern";
import { t } from "ttag";
import {
type DashboardAccessedVia,
trackExportDashboardToPDF,
} from "metabase/dashboard/analytics";
import { DASHBOARD_PDF_EXPORT_ROOT_ID } from "metabase/dashboard/constants";
import { useDispatch } from "metabase/lib/redux";
import { isJWT } from "metabase/lib/utils";
import { isUuid } from "metabase/lib/uuid";
import { ActionIcon, Icon, Tooltip } from "metabase/ui";
import { saveDashboardPdf } from "metabase/visualizations/lib/save-dashboard-pdf";
import type { Dashboard } from "metabase-types/api";
import CS from "./ExportAsPdfButton.module.css";
export const ExportAsPdfButton = ({
dashboard,
hasTitle,
hasVisibleParameters,
}: {
dashboard: Dashboard;
hasTitle?: boolean;
hasVisibleParameters?: boolean;
}) => {
const dispatch = useDispatch();
const saveAsPDF = () => {
const dashboardAccessedVia = match(dashboard?.id)
.returnType<DashboardAccessedVia>()
.when(isJWT, () => "static-embed")
.when(isUuid, () => "public-link")
.otherwise(() => "sdk-embed");
trackExportDashboardToPDF({
dashboardAccessedVia,
});
const cardNodeSelector = `#${DASHBOARD_PDF_EXPORT_ROOT_ID}`;
return saveDashboardPdf(
cardNodeSelector,
dashboard.name ?? t`Exported dashboard`,
);
};
const hasDashboardTabs = dashboard?.tabs && dashboard.tabs.length > 1;
return (
<Tooltip label={t`Download as PDF`}>
<ActionIcon
onClick={() => dispatch(saveAsPDF)}
className={cx({
[CS.CompactExportAsPdfButton]:
!hasTitle && (hasVisibleParameters || hasDashboardTabs),
[CS.ParametersVisibleWithNoTabs]:
hasVisibleParameters && !hasDashboardTabs,
})}
aria-label={t`Download as PDF`}
>
<Icon name="download" />
</ActionIcon>
</Tooltip>
);
};

View File

@@ -2,17 +2,17 @@ import { t } from "ttag";
import { ToolbarButton } from "metabase/components/ToolbarButton";
import CS from "metabase/css/core/index.css";
import type { DashboardFullscreenControls } from "metabase/dashboard/types";
import { useDashboardContext } from "metabase/dashboard/context";
export const FullscreenAnalyticsDashboard = () => {
const { isFullscreen, onFullscreenChange } = useDashboardContext();
export const FullscreenAnalyticsDashboard = ({
isFullscreen,
onFullscreenChange,
}: DashboardFullscreenControls) => (
<ToolbarButton
key="expand"
aria-label={t`Enter Fullscreen`}
icon="expand"
className={CS.cursorPointer}
onClick={(e) => onFullscreenChange(!isFullscreen, !e.altKey)}
/>
);
return (
<ToolbarButton
key="expand"
aria-label={t`Enter Fullscreen`}
icon="expand"
className={CS.cursorPointer}
onClick={(e) => onFullscreenChange(!isFullscreen, !e.altKey)}
/>
);
};

View File

@@ -3,21 +3,17 @@ import { msgid, ngettext, t } from "ttag";
import ActionButton from "metabase/components/ActionButton";
import ButtonsS from "metabase/css/components/buttons.module.css";
import {
setEditingDashboard,
updateDashboardAndCards,
} from "metabase/dashboard/actions";
import { getMissingRequiredParameters } from "metabase/dashboard/selectors";
import { useDispatch, useSelector } from "metabase/lib/redux";
import { useRegisterShortcut } from "metabase/palette/hooks/useRegisterShortcut";
import { dismissAllUndo } from "metabase/redux/undo";
import { useDashboardContext } from "metabase/dashboard/context";
import { Tooltip } from "metabase/ui";
import type { UiParameter } from "metabase-lib/v1/parameters/types";
export const SaveEditButton = (props: { onDoneEditing: () => void }) => {
const dispatch = useDispatch();
const missingRequiredParameters = useSelector(getMissingRequiredParameters);
const {
updateDashboardAndCards,
setEditingDashboard,
missingRequiredParameters = [], // This would need to be added to context
} = useDashboardContext();
const disabledSaveTooltip = getDisabledSaveButtonTooltip(
missingRequiredParameters,
@@ -26,15 +22,13 @@ export const SaveEditButton = (props: { onDoneEditing: () => void }) => {
const handleDoneEditing = () => {
props.onDoneEditing();
dispatch(setEditingDashboard(null));
setEditingDashboard(null);
};
const onSave = async () => {
// optimistically dismissing all the undos before the saving has finished
// clicking on them wouldn't do anything at this moment anyway
dispatch(dismissAllUndo());
await dispatch(updateDashboardAndCards());
// We would need to move this to the context or pass it as a prop
// For now, we can assume dismissAllUndo is handled in updateDashboardAndCards
await updateDashboardAndCards();
handleDoneEditing();
};

View File

@@ -10,6 +10,6 @@ export * from "./DashboardInfoButton";
export * from "./EditDashboardButton";
export * from "./FullscreenAnalyticsDashboard";
export * from "./SaveEditButton";
export * from "./FullscreenToggle";
export * from "./NightModeToggleButton";
export * from "./ExportAsPdfButton";
export * from "../../Dashboard/components/FullscreenToggle";
export * from "../../Dashboard/components/NightModeToggleButton";
export * from "../../Dashboard/components/ExportAsPdfButton";

View File

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

View File

@@ -18,10 +18,12 @@ import { ParametersList } from "../../../parameters/components/ParametersList";
interface DashboardParameterListProps {
isFullscreen: boolean;
className?: string;
}
export function DashboardParameterList({
isFullscreen,
className,
}: DashboardParameterListProps) {
const dashboard = useSelector(getDashboardComplete);
const parameters = useSelector(getValuePopulatedParameters);
@@ -34,6 +36,7 @@ export function DashboardParameterList({
return (
<ParametersList
className={className}
parameters={parameters}
editingParameter={editingParameter}
hideParameters={hiddenParameterSlugs}

View File

@@ -5,23 +5,22 @@ import { Sortable } from "metabase/core/components/Sortable";
import type { TabButtonMenuItem } from "metabase/core/components/TabButton";
import { TabButton } from "metabase/core/components/TabButton";
import { TabRow } from "metabase/core/components/TabRow";
import { Flex } from "metabase/ui";
import { Flex, type FlexProps } from "metabase/ui";
import type { DashboardId } from "metabase-types/api";
import type { SelectedTabId } from "metabase-types/store";
import S from "./DashboardTabs.module.css";
import { useDashboardTabs } from "./use-dashboard-tabs";
export type DashboardTabsProps = {
export interface DashboardTabsProps extends FlexProps {
dashboardId: DashboardId;
isEditing?: boolean;
className?: string;
};
}
export function DashboardTabs({
dashboardId,
isEditing = false,
className,
...flexProps
}: DashboardTabsProps) {
const {
tabs,
@@ -55,7 +54,7 @@ export function DashboardTabs({
}
return (
<Flex align="start" gap="lg" w="100%" className={className}>
<Flex align="start" gap="lg" w="100%" {...flexProps}>
<TabRow<SelectedTabId>
value={selectedTabId}
onChange={selectTab}

View File

@@ -1,77 +0,0 @@
import { useEffect, useState } from "react";
import { t } from "ttag";
import { Box, Popover } from "metabase/ui";
import { RefreshOption } from "./RefreshOption";
import { RefreshWidgetTarget } from "./RefreshWidgetTarget";
const toSeconds = (minutes: number) => minutes * 60;
const OPTIONS = [
{ name: t`Off`, period: null },
{ name: t`1 minute`, period: toSeconds(1) },
{ name: t`5 minutes`, period: toSeconds(5) },
{ name: t`10 minutes`, period: toSeconds(10) },
{ name: t`15 minutes`, period: toSeconds(15) },
{ name: t`30 minutes`, period: toSeconds(30) },
{ name: t`60 minutes`, period: toSeconds(60) },
];
export const RefreshWidget = ({
setRefreshElapsedHook,
period,
onChangePeriod,
}: {
setRefreshElapsedHook?: (hook: (elapsed: number | null) => void) => void;
period: number | null;
onChangePeriod: (period: number | null) => void;
}) => {
const [elapsed, setElapsed] = useState<number | null>(null);
const [isOpen, setIsOpen] = useState(false);
useEffect(() => {
if (setRefreshElapsedHook) {
setRefreshElapsedHook((elapsed: number | null) => {
setElapsed(elapsed);
});
}
}, [setRefreshElapsedHook]);
return (
<Popover position="bottom-end" opened={isOpen} onChange={setIsOpen}>
<Popover.Target>
<Box onClick={() => setIsOpen(!isOpen)}>
<RefreshWidgetTarget elapsed={elapsed} period={period} />
</Box>
</Popover.Target>
<Popover.Dropdown>
<Box p="md" miw="12.5rem">
<Box
fw="bold"
fz="sm"
tt="uppercase"
mb="md"
ml="sm"
c="text-medium"
>{t`Auto Refresh`}</Box>
<ul>
{OPTIONS.map((option) => (
<RefreshOption
key={option.period}
name={option.name}
period={option.period}
selected={option.period === period}
onClick={() => {
setIsOpen(false);
onChangePeriod(option.period);
}}
/>
))}
</ul>
</Box>
</Popover.Dropdown>
</Popover>
);
};

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,153 @@
import type { ConnectedProps } from "react-redux";
import { push } from "react-router-redux";
import { deletePermanently } from "metabase/archive/actions";
import { connect } from "metabase/lib/redux";
import { closeNavbar, setErrorPage } from "metabase/redux/app";
import { dismissUndo } from "metabase/redux/undo";
import { getIsNavbarOpen } from "metabase/selectors/app";
import {
canManageSubscriptions,
getUserIsAdmin,
} from "metabase/selectors/user";
import type { State } from "metabase-types/store";
import {
addCardToDashboard,
addHeadingDashCardToDashboard,
addLinkDashCardToDashboard,
addMarkdownDashCardToDashboard,
addParameter,
cancelEditingDashboard,
cancelFetchDashboardCardData,
closeDashboard,
closeSidebar,
fetchDashboard,
fetchDashboardCardData,
hideAddParameterPopover,
initialize,
moveDashboardToCollection,
onReplaceAllDashCardVisualizationSettings,
onUpdateDashCardColumnSettings,
onUpdateDashCardVisualizationSettings,
removeParameter,
reset,
setArchivedDashboard,
setDashboardAttributes,
setEditingDashboard,
setParameterDefaultValue,
setParameterFilteringParameters,
setParameterIsMultiSelect,
setParameterName,
setParameterQueryType,
setParameterRequired,
setParameterSourceConfig,
setParameterSourceType,
setParameterTemporalUnits,
setParameterType,
setSharing,
setSidebar,
showAddParameterPopover,
toggleSidebar,
updateDashboardAndCards,
} from "../actions";
import {
getClickBehaviorSidebarDashcard,
getDashboardBeforeEditing,
getDashboardComplete,
getDocumentTitle,
getFavicon,
getIsAddParameterPopoverOpen,
getIsAdditionalInfoVisible,
getIsDashCardsLoadingComplete,
getIsDashCardsRunning,
getIsDirty,
getIsEditing,
getIsEditingParameter,
getIsHeaderVisible,
getIsNavigatingBackToDashboard,
getIsSharing,
getLoadingStartTime,
getMissingRequiredParameters,
getParameterValues,
getSelectedTabId,
getSidebar,
getSlowCards,
} from "../selectors";
import { addDashboardQuestion } from "../components/QuestionPicker/actions";
const mapStateToProps = (state: State) => ({
canManageSubscriptions: canManageSubscriptions(state),
isAdmin: getUserIsAdmin(state),
isNavbarOpen: false, // 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),
missingRequiredParameters: getMissingRequiredParameters(state)
});
const mapDispatchToProps = {
initialize,
cancelFetchDashboardCardData,
addCardToDashboard,
addHeadingDashCardToDashboard,
addMarkdownDashCardToDashboard,
addLinkDashCardToDashboard,
setEditingDashboard,
setDashboardAttributes,
setSharing,
toggleSidebar,
closeSidebar,
closeNavbar,
setErrorPage,
setParameterName,
setParameterType,
setParameterDefaultValue,
setParameterRequired,
setParameterTemporalUnits,
setParameterIsMultiSelect,
setParameterQueryType,
setParameterSourceType,
setParameterSourceConfig,
setParameterFilteringParameters,
showAddParameterPopover,
removeParameter,
onReplaceAllDashCardVisualizationSettings,
onUpdateDashCardVisualizationSettings,
onUpdateDashCardColumnSettings,
updateDashboardAndCards,
setSidebar,
addParameter,
addDashboardQuestion,
hideAddParameterPopover,
cancelEditingDashboard,
fetchDashboard,
fetchDashboardCardData,
onChangeLocation: push,
reset,
closeDashboard,
dismissUndo,
setArchivedDashboard,
moveDashboardToCollection,
deletePermanently,
};
export const connector = connect(mapStateToProps, mapDispatchToProps);
export type ReduxProps = ConnectedProps<typeof connector>;

View File

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

View File

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

View File

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

View File

@@ -37,6 +37,7 @@ import type { LinkProps } from "metabase/core/components/Link";
import type { EmbeddingEntityType } from "metabase/embedding-sdk/store";
import { getIconBase } from "metabase/lib/icon";
import PluginPlaceholder from "metabase/plugins/components/PluginPlaceholder";
import type { EmbedResourceDownloadOptions } from "metabase/public/lib/types";
import type { SearchFilterComponent } from "metabase/search/types";
import type { IconName, IconProps, StackProps } from "metabase/ui";
import type Question from "metabase-lib/v1/Question";
@@ -157,15 +158,15 @@ export const PLUGIN_DATA_PERMISSIONS: {
) => boolean;
upgradeViewPermissionsIfNeeded:
| ((
permissions: GroupsPermissions,
groupId: number,
entityId: EntityId,
value: any,
database: Database,
permission: DataPermission,
) => GroupPermissions)
| null;
| ((
permissions: GroupsPermissions,
groupId: number,
entityId: EntityId,
value: any,
database: Database,
permission: DataPermission,
) => GroupPermissions)
| null;
} = {
permissionsPayloadExtraSelectors: [],
hasChanges: [],
@@ -279,9 +280,9 @@ interface PluginDashboardSubscriptionParametersSectionOverride {
}>;
}
export const PLUGIN_DASHBOARD_SUBSCRIPTION_PARAMETERS_SECTION_OVERRIDE: PluginDashboardSubscriptionParametersSectionOverride =
{
Component: undefined,
};
{
Component: undefined,
};
export const PLUGIN_LLM_AUTODESCRIPTION: PluginLLMAutoDescription = {
isEnabled: () => false,
@@ -567,13 +568,13 @@ export const PLUGIN_CONTENT_VERIFICATION = {
export type InsightsLinkProps = (
| {
question: Pick<Question, "id" | "collection">;
dashboard?: never;
}
question: Pick<Question, "id" | "collection">;
dashboard?: never;
}
| {
question?: never;
dashboard: Pick<Dashboard, "id" | "collection">;
}
question?: never;
dashboard: Pick<Dashboard, "id" | "collection">;
}
) &
Omit<LinkProps, "to">;
@@ -622,10 +623,11 @@ export const PLUGIN_DB_ROUTING = {
null as React.ReactElement | null,
useRedirectDestinationDatabase: (
_database: Pick<DatabaseType, "id" | "router_database_id"> | undefined,
): void => {},
): void => { },
getPrimaryDBEngineFieldState: (
_database: Pick<Database, "router_user_attribute">,
): "default" | "hidden" | "disabled" => "default",
}): EmbedResourceDownloadOptions => ({ pdf: true, dashcard: true }),
};
export const PLUGIN_API = {

View File

@@ -8,7 +8,7 @@ import CS from "metabase/css/core/index.css";
import TransitionS from "metabase/css/core/transitions.module.css";
import DashboardS from "metabase/dashboard/components/Dashboard/Dashboard.module.css";
import { FixedWidthContainer } from "metabase/dashboard/components/Dashboard/DashboardComponents";
import { ExportAsPdfButton } from "metabase/dashboard/components/DashboardHeader/buttons/ExportAsPdfButton";
import { ExportAsPdfButton } from "metabase/dashboard/components/Dashboard/components/ExportAsPdfButton";
import {
DASHBOARD_PARAMETERS_PDF_EXPORT_NODE_ID,
DASHBOARD_PDF_EXPORT_ROOT_ID,

View File

@@ -1,267 +0,0 @@
import type { Query } from "history";
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 { 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";
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: Query;
navigateToNewCardFromDashboard?: (
opts: NavigateToNewCardFromDashboardOpts,
) => void;
} & PublicOrEmbeddedDashboardEventHandlersProps;
type PublicOrEmbeddedDashboardProps = OwnProps &
ReduxProps &
Omit<DashboardDisplayOptionControls, "downloadsEnabled"> &
Omit<EmbedDisplayParams, "font"> &
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,
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,
);

View File

@@ -1,5 +1,8 @@
import type { WithRouterProps } from "react-router";
import CS from "metabase/css/core/index.css";
import { Dashboard } from "metabase/dashboard/components/Dashboard/Dashboard";
import { DashboardContextProvider } from "metabase/dashboard/context";
import {
useDashboardUrlParams,
useRefreshDashboard,
@@ -8,17 +11,14 @@ import { useDashboardUrlQuery } from "metabase/dashboard/hooks/use-dashboard-url
import { getDashboardComplete } from "metabase/dashboard/selectors";
import { SetTitle } from "metabase/hoc/Title";
import { useSelector } from "metabase/lib/redux";
import { getCanWhitelabel } from "metabase/selectors/whitelabel";
import { Box, Divider, Group, Icon, Stack, Title, Tooltip } from "metabase/ui";
import { PublicOrEmbeddedDashboard } from "../PublicOrEmbeddedDashboard";
import { usePublicDashboardEndpoints } from "../WithPublicDashboardEndpoints";
export const PublicOrEmbeddedDashboardPage = (props: WithRouterProps) => {
const { location, router } = props;
const parameterQueryParams = props.location.query;
const { dashboardId } = usePublicDashboardEndpoints(props);
const { refreshDashboard } = useRefreshDashboard({
dashboardId,
parameterQueryParams,
@@ -27,8 +27,8 @@ export const PublicOrEmbeddedDashboardPage = (props: WithRouterProps) => {
useDashboardUrlQuery(router, location);
const {
background,
bordered,
// background,
// bordered,
hasNightModeToggle,
downloadsEnabled,
hideParameters,
@@ -39,39 +39,71 @@ export const PublicOrEmbeddedDashboardPage = (props: WithRouterProps) => {
onFullscreenChange,
setRefreshElapsedHook,
onRefreshPeriodChange,
theme,
titled,
locale,
// theme,
// titled,
} = useDashboardUrlParams({ location, onRefresh: refreshDashboard });
const canWhitelabel = useSelector(getCanWhitelabel);
const dashboard = useSelector(getDashboardComplete);
return (
<>
<SetTitle title={dashboard?.name} />
<PublicOrEmbeddedDashboard
<DashboardContextProvider
dashboardId={dashboardId}
isFullscreen={isFullscreen}
refreshPeriod={refreshPeriod}
hideParameters={hideParameters}
isNightMode={isNightMode}
hasNightModeToggle={hasNightModeToggle}
setRefreshElapsedHook={setRefreshElapsedHook}
onNightModeChange={onNightModeChange}
onFullscreenChange={onFullscreenChange}
onRefreshPeriodChange={onRefreshPeriodChange}
background={background}
bordered={bordered}
// background={background}
// bordered={bordered}
downloadsEnabled={downloadsEnabled}
theme={theme}
titled={titled}
parameterQueryParams={parameterQueryParams}
cardTitled={true}
locale={canWhitelabel ? locale : undefined}
withFooter={true}
/>
// theme={theme}
// titled={titled}
>
<Box h="100%" pos="relative">
<Box bg="bg-white">
<Group justify="space-between">
<Title order={2} px="lg">
<Dashboard.Title />
<Tooltip label={<Dashboard.Description />}>
<Icon ml="md" name="info" />
</Tooltip>
</Title>
<Dashboard.ExportAsPdfButton px="lg" />
</Group>
<Box>
<Dashboard.Tabs px="md" />
<Divider />
</Box>
</Box>
{!hideParameters && (
<Box
px="md"
pos="sticky"
top={0}
className={CS.Overlay}
bg="bg-white"
>
<Dashboard.ParameterList />
</Box>
)}
<Box flex="1">
<Dashboard.Grid h="100%" w="100%" />
</Box>
<Stack bg="bg-white">
<Divider />
<Group justify="flex-end" px="sm">
<Dashboard.RefreshWidget />
<Dashboard.FullscreenToggle />
<Dashboard.NightModeToggle />
</Group>
</Stack>
</Box>
</DashboardContextProvider>
</>
);
};

View File

@@ -1,958 +0,0 @@
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 { MetabaseReduxProvider } from "metabase/lib/redux/custom-context";
import { getDashboardUiParameters } from "metabase/parameters/utils/dashboards";
import { publicReducers } from "metabase/reducers-public";
import { registerVisualization } from "metabase/visualizations";
import { BarChart } from "metabase/visualizations/visualizations/BarChart";
import Table from "metabase/visualizations/visualizations/Table/Table";
import TABLE_RAW_SERIES from "metabase/visualizations/visualizations/Table/stories-data/orders-with-people.json";
import type { UiParameter } from "metabase-lib/v1/parameters/types";
import {
createMockCard,
createMockColumn,
createMockDashboard,
createMockDashboardCard,
createMockDataset,
createMockDatasetData,
createMockParameter,
} from "metabase-types/api/mocks";
import {
PRODUCTS,
createSampleDatabase,
} from "metabase-types/api/mocks/presets";
import {
createMockDashboardState,
createMockSettingsState,
createMockState,
} from "metabase-types/store/mocks";
import {
PublicOrEmbeddedDashboardView,
type PublicOrEmbeddedDashboardViewProps,
} from "./PublicOrEmbeddedDashboardView";
// @ts-expect-error: incompatible prop types with registerVisualization
registerVisualization(Table);
// @ts-expect-error: incompatible prop types with registerVisualization
registerVisualization(BarChart);
/**
* This is an arbitrary number, it should be big enough to pass CI tests.
* This works because we set delays for ExplicitSize to 0 in storybook.
*/
const TIME_UNTIL_ALL_ELEMENTS_STOP_RESIZING = 2500;
export default {
title: "App/Embed/PublicOrEmbeddedDashboardView/filters",
component: PublicOrEmbeddedDashboardView,
decorators: [
ReduxDecorator,
createWaitForResizeToStopDecorator(TIME_UNTIL_ALL_ELEMENTS_STOP_RESIZING),
MockIsEmbeddingDecorator,
],
parameters: {
layout: "fullscreen",
},
argTypes: {
parameterType: {
options: ["text", "dropdown_multiple"],
control: { type: "select" },
},
},
};
function ReduxDecorator(Story: StoryFn, context: StoryContext) {
const parameterType = context.args.parameterType as ParameterType;
const initialState = createMockState({
settings: createMockSettingsState({
"hide-embed-branding?": false,
}),
dashboard: createMockDashboardState({
dashcardData: {
[DASHCARD_BAR_ID]: {
[CARD_BAR_ID]: createMockDataset({
data: createMockDatasetData({
cols: [
createMockColumn(StringColumn({ name: "Dimension" })),
createMockColumn(NumberColumn({ name: "Count" })),
],
rows: [
["foo", 1],
["bar", 2],
],
}),
}),
},
[DASHCARD_TABLE_ID]: {
// Couldn't really figure out the type here.
[CARD_TABLE_ID]: createMockDataset(TABLE_RAW_SERIES[0] as any),
},
},
}),
parameters: {
parameterValuesCache: {
[`{"paramId":"${CATEGORY_FILTER.id}","dashId":${DASHBOARD_ID}}`]: {
values: [["Doohickey"], ["Gadget"], ["Gizmo"], ["Widget"]],
has_more_values: parameterType === "search" ? true : false,
},
[`{"paramId":"${CATEGORY_FILTER.id}","dashId":${DASHBOARD_ID},"query":"g"}`]:
{
values: [["Gadget"], ["Gizmo"], ["Widget"]],
has_more_values: parameterType === "search" ? true : false,
},
},
},
});
const store = getStore(publicReducers, initialState);
return (
<MetabaseReduxProvider store={store}>
<Story />
</MetabaseReduxProvider>
);
}
declare global {
interface Window {
overrideIsWithinIframe?: boolean;
}
}
function MockIsEmbeddingDecorator(Story: StoryFn) {
window.overrideIsWithinIframe = true;
return <Story />;
}
const DASHBOARD_ID = getNextId();
const DASHCARD_BAR_ID = getNextId();
const DASHCARD_TABLE_ID = getNextId();
const CARD_BAR_ID = getNextId();
const CARD_TABLE_ID = getNextId();
const TAB_ID = getNextId();
const CATEGORY_FILTER = createMockParameter({
id: "category-hex",
name: "Category",
slug: "category",
});
const CATEGORY_SINGLE_FILTER = createMockParameter({
id: "category-hex",
name: "Category",
slug: "category",
isMultiSelect: false,
});
const NUMBER_FILTER_ID = "number-hex";
const DATE_FILTER_ID = "date-hex";
const UNIT_OF_TIME_FILTER_ID = "unit-of-time-hex";
interface CreateDashboardOpts {
hasScroll?: boolean;
}
function createDashboard({ hasScroll }: CreateDashboardOpts = {}) {
return createMockDashboard({
id: DASHBOARD_ID,
name: "My dashboard",
width: "full",
dashcards: [
createMockDashboardCard({
id: DASHCARD_BAR_ID,
dashboard_tab_id: TAB_ID,
card: createMockCard({ id: CARD_BAR_ID, name: "Bar", display: "bar" }),
size_x: 12,
size_y: 8,
parameter_mappings: [
{
card_id: CARD_BAR_ID,
parameter_id: CATEGORY_FILTER.id,
target: [
"dimension",
["field", PRODUCTS.CATEGORY, { "base-type": "type/Text" }],
],
},
{
card_id: CARD_BAR_ID,
parameter_id: DATE_FILTER_ID,
target: [
"dimension",
["field", PRODUCTS.CREATED_AT, { "base-type": "type/DateTime" }],
],
},
{
card_id: CARD_BAR_ID,
parameter_id: UNIT_OF_TIME_FILTER_ID,
target: [
"dimension",
[
"field",
PRODUCTS.CREATED_AT,
{ "base-type": "type/DateTime", "temporal-unit": "month" },
],
],
},
{
card_id: CARD_BAR_ID,
parameter_id: NUMBER_FILTER_ID,
target: [
"dimension",
["field", PRODUCTS.RATING, { "base-type": "type/Float" }],
],
},
],
}),
createMockDashboardCard({
id: DASHCARD_TABLE_ID,
dashboard_tab_id: TAB_ID,
card: createMockCard({
id: CARD_TABLE_ID,
name: "Table",
display: "table",
}),
...(!hasScroll ? { col: 12 } : { row: 8 }),
size_x: 12,
size_y: 8,
}),
],
});
}
const Template: StoryFn<PublicOrEmbeddedDashboardViewProps> = (args) => {
// @ts-expect-error -- custom prop to support non JSON-serializable value as args
const parameterType: ParameterType = args.parameterType;
const dashboard = args.dashboard;
if (!dashboard) {
return <>Please pass `dashboard`</>;
}
const PARAMETER_MAPPING: Record<ParameterType, UiParameter[]> = {
text: getDashboardUiParameters(
dashboard.dashcards,
[CATEGORY_FILTER],
createMockMetadata({}),
{},
),
number: getDashboardUiParameters(
dashboard.dashcards,
[
createMockParameter({
id: NUMBER_FILTER_ID,
name: "Number Equals",
sectionId: "number",
slug: "number_equals",
type: "number/=",
}),
],
createMockMetadata({}),
{},
),
dropdown_multiple: getDashboardUiParameters(
dashboard.dashcards,
[CATEGORY_FILTER],
createMockMetadata({
databases: [createSampleDatabase()],
}),
{},
),
dropdown_single: getDashboardUiParameters(
dashboard.dashcards,
[CATEGORY_SINGLE_FILTER],
createMockMetadata({
databases: [createSampleDatabase()],
}),
{},
),
search: getDashboardUiParameters(
dashboard.dashcards,
[CATEGORY_FILTER],
createMockMetadata({
databases: [createSampleDatabase()],
}),
{},
),
date_all_options: getDashboardUiParameters(
dashboard.dashcards,
[
createMockParameter({
id: DATE_FILTER_ID,
name: "Date all options",
sectionId: "date",
slug: "date_all_options",
type: "date/all-options",
}),
],
createMockMetadata({}),
{},
),
date_month_year: getDashboardUiParameters(
dashboard.dashcards,
[
createMockParameter({
id: DATE_FILTER_ID,
name: "Date Month and Year",
sectionId: "date",
slug: "date_month_and_year",
type: "date/month-year",
}),
],
createMockMetadata({}),
{},
),
date_quarter_year: getDashboardUiParameters(
dashboard.dashcards,
[
createMockParameter({
id: DATE_FILTER_ID,
name: "Date Quarter and Year",
sectionId: "date",
slug: "date_quarter_and_year",
type: "date/quarter-year",
}),
],
createMockMetadata({}),
{},
),
date_single: getDashboardUiParameters(
dashboard.dashcards,
[
createMockParameter({
id: DATE_FILTER_ID,
name: "Date single",
sectionId: "date",
slug: "date_single",
type: "date/single",
}),
],
createMockMetadata({}),
{},
),
date_range: getDashboardUiParameters(
dashboard.dashcards,
[
createMockParameter({
id: DATE_FILTER_ID,
name: "Date range",
sectionId: "date",
slug: "date_range",
type: "date/range",
}),
],
createMockMetadata({}),
{},
),
date_relative: getDashboardUiParameters(
dashboard.dashcards,
[
createMockParameter({
id: DATE_FILTER_ID,
name: "Date relative",
sectionId: "date",
slug: "date_relative",
type: "date/relative",
}),
],
createMockMetadata({}),
{},
),
temporal_unit: getDashboardUiParameters(
dashboard.dashcards,
[
createMockParameter({
id: UNIT_OF_TIME_FILTER_ID,
name: "Time grouping",
sectionId: "temporal-unit",
slug: "unit_of_time",
type: "temporal-unit",
}),
],
createMockMetadata({}),
{},
),
};
return (
<PublicOrEmbeddedDashboardView
{...args}
parameters={PARAMETER_MAPPING[parameterType]}
/>
);
};
type ArgType = Partial<ComponentProps<typeof PublicOrEmbeddedDashboardView>>;
type ParameterType =
| "text"
| "number"
| "dropdown_multiple"
| "dropdown_single"
| "search"
| "date_all_options"
| "date_month_year"
| "date_quarter_year"
| "date_single"
| "date_range"
| "date_relative"
| "temporal_unit";
const createDefaultArgs = (
args: ArgType & { parameterType?: ParameterType } = {},
): ArgType & { parameterType: ParameterType } => {
const dashboard = createDashboard();
return {
dashboard,
titled: true,
bordered: true,
background: true,
slowCards: {},
selectedTabId: TAB_ID,
parameterType: "text",
...args,
downloadsEnabled: { pdf: true, results: false },
};
};
function getLastPopover() {
const lastPopover = Array.from(
document.documentElement.querySelectorAll(
'[data-element-id="mantine-popover"]',
),
).at(-1) as HTMLElement;
return within(lastPopover);
}
function getLastPopoverElement() {
const lastPopover = Array.from(
document.documentElement.querySelectorAll(
'[data-element-id="mantine-popover"]',
),
).at(-1) as HTMLElement;
return lastPopover;
}
export const LightThemeText = {
render: Template,
args: createDefaultArgs(),
play: async ({ canvasElement }: { canvasElement: HTMLCanvasElement }) => {
const canvas = within(canvasElement);
const filter = await canvas.findByRole("button", { name: "Category" });
await userEvent.click(filter);
},
};
export const LightThemeTextWithValue = {
render: Template,
args: createDefaultArgs(),
play: async ({ canvasElement }: { canvasElement: HTMLCanvasElement }) => {
const canvas = within(canvasElement);
const filter = await canvas.findByRole("button", { name: "Category" });
await userEvent.click(filter);
const popover = getLastPopover();
await userEvent.type(
popover.getByPlaceholderText("Enter some text"),
"filter value",
);
await userEvent.click(getLastPopoverElement());
},
};
export const LightThemeParameterSearch = {
render: Template,
args: createDefaultArgs({
parameterType: "search",
}),
play: async ({ canvasElement }: { canvasElement: HTMLCanvasElement }) => {
const canvas = within(canvasElement);
const filter = await canvas.findByRole("button", { name: "Category" });
await userEvent.click(filter);
},
};
export const LightThemeParameterSearchWithValue = {
render: Template,
args: createDefaultArgs({
parameterType: "search",
}),
play: async ({ canvasElement }: { canvasElement: HTMLCanvasElement }) => {
const canvas = within(canvasElement);
const filter = await canvas.findByRole("button", { name: "Category" });
await userEvent.click(filter);
const documentElement = within(document.documentElement);
const searchInput = documentElement.getByPlaceholderText("Search the list");
await userEvent.click(documentElement.getByText("Widget"));
await userEvent.type(searchInput, "g");
const dropdown = getLastPopover();
(dropdown.getByText("Gadget").parentNode as HTMLElement).setAttribute(
"data-hovered",
"true",
);
},
};
export const DarkThemeText = {
render: Template,
args: createDefaultArgs({ theme: "night" }),
play: LightThemeText.play,
};
export const DarkThemeTextWithValue = {
render: Template,
args: createDefaultArgs({ theme: "night" }),
play: LightThemeTextWithValue.play,
};
export const DarkThemeParameterSearch = {
render: Template,
args: createDefaultArgs({
theme: "night",
parameterType: "search",
}),
play: LightThemeParameterSearch.play,
};
export const DarkThemeParameterSearchWithValue = {
render: Template,
args: createDefaultArgs({
theme: "night",
parameterType: "search",
}),
play: LightThemeParameterSearchWithValue.play,
};
export const LightThemeParameterList = {
render: Template,
args: createDefaultArgs({
parameterType: "dropdown_multiple",
}),
play: async ({ canvasElement }: { canvasElement: HTMLCanvasElement }) => {
const canvas = within(canvasElement);
const filter = await canvas.findByRole("button", { name: "Category" });
await userEvent.click(filter);
},
};
export const LightThemeParameterListWithValue = {
render: Template,
args: createDefaultArgs({
parameterType: "dropdown_multiple",
}),
play: async ({ canvasElement }: { canvasElement: HTMLCanvasElement }) => {
const canvas = within(canvasElement);
const filter = await canvas.findByRole("button", { name: "Category" });
await userEvent.click(filter);
const popover = getLastPopover();
await userEvent.type(popover.getByPlaceholderText("Search the list"), "g");
await userEvent.click(popover.getByText("Widget"));
const gizmo = popover.getByRole("checkbox", {
name: "Gizmo",
}) as HTMLInputElement;
gizmo.disabled = true;
},
};
export const DarkThemeParameterList = {
render: Template,
args: createDefaultArgs({
theme: "night",
parameterType: "dropdown_multiple",
}),
play: LightThemeParameterList.play,
};
export const DarkThemeParameterListWithValue = {
render: Template,
args: createDefaultArgs({
theme: "night",
parameterType: "dropdown_multiple",
}),
play: LightThemeParameterListWithValue.play,
};
export const LightThemeParameterListSingleWithValue = {
render: Template,
args: createDefaultArgs({
parameterType: "dropdown_single",
}),
play: async ({ canvasElement }: { canvasElement: HTMLCanvasElement }) => {
const canvas = within(canvasElement);
const filter = await canvas.findByRole("button", { name: "Category" });
await userEvent.click(filter);
const documentElement = within(document.documentElement);
await userEvent.type(
documentElement.getByPlaceholderText("Search the list"),
"g",
);
await userEvent.click(documentElement.getByText("Widget"));
const popover = getLastPopover();
(popover.getByText("Gadget").parentNode as HTMLElement).classList.add(
"pseudo-hover",
);
},
};
export const DarkThemeParameterListSingleWithValue = {
render: Template,
args: createDefaultArgs({
theme: "night",
parameterType: "dropdown_single",
}),
play: LightThemeParameterListSingleWithValue.play,
};
export const LightThemeDateFilterAllOptions = {
render: Template,
args: createDefaultArgs({
parameterType: "date_all_options",
}),
play: async ({ canvasElement }: { canvasElement: HTMLCanvasElement }) => {
const canvas = within(canvasElement);
const filter = await canvas.findByRole("button", {
name: "Date all options",
});
await userEvent.click(filter);
const popover = getLastPopover();
const today = popover.getByRole("button", { name: "Today" });
today.classList.add("pseudo-hover");
},
};
export const DarkThemeDateFilterAllOptions = {
render: Template,
args: createDefaultArgs({
theme: "night",
parameterType: "date_all_options",
}),
play: LightThemeDateFilterAllOptions.play,
};
export const LightThemeDateFilterMonthYear = {
render: Template,
args: createDefaultArgs({
parameterType: "date_month_year",
parameterValues: {
[DATE_FILTER_ID]: "2024-01",
},
}),
play: async ({ canvasElement }: { canvasElement: HTMLCanvasElement }) => {
const canvas = within(canvasElement);
const filter = await canvas.findByRole("button", {
name: "Date Month and Year",
});
await userEvent.click(filter);
const popover = getLastPopover();
const month = popover.getByText("May");
month.classList.add("pseudo-hover");
},
};
export const DarkThemeDateFilterMonthYear = {
render: Template,
args: createDefaultArgs({
theme: "night",
parameterType: "date_month_year",
parameterValues: {
[DATE_FILTER_ID]: "2024-01",
},
}),
play: LightThemeDateFilterMonthYear.play,
};
export const LightThemeDateFilterQuarterYear = {
render: Template,
args: createDefaultArgs({
parameterType: "date_quarter_year",
parameterValues: {
[DATE_FILTER_ID]: "Q1-2024",
},
}),
play: async ({ canvasElement }: { canvasElement: HTMLCanvasElement }) => {
const canvas = within(canvasElement);
const filter = await canvas.findByRole("button", {
name: "Date Quarter and Year",
});
await userEvent.click(filter);
const popover = getLastPopover();
const month = popover.getByText("Q2");
month.classList.add("pseudo-hover");
},
};
export const LightThemeDateFilterQuarterYearDropdown = {
render: Template,
args: createDefaultArgs({
parameterType: "date_quarter_year",
parameterValues: {
[DATE_FILTER_ID]: "Q1-2024",
},
}),
play: async ({ canvasElement }: { canvasElement: HTMLCanvasElement }) => {
const canvas = within(canvasElement);
const filter = await canvas.findByRole("button", {
name: "Date Quarter and Year",
});
await userEvent.click(filter);
const popover = getLastPopover();
await userEvent.click(popover.getByText("2024"));
popover.getByRole("button", { name: "2023" }).classList.add("pseudo-hover");
},
};
export const DarkThemeDateFilterQuarterYear = {
render: Template,
args: createDefaultArgs({
theme: "night",
parameterType: "date_quarter_year",
parameterValues: {
[DATE_FILTER_ID]: "Q1-2024",
},
}),
play: LightThemeDateFilterQuarterYear.play,
};
export const DarkThemeDateFilterQuarterYearDropdown = {
render: Template,
args: createDefaultArgs({
theme: "night",
parameterType: "date_quarter_year",
parameterValues: {
[DATE_FILTER_ID]: "Q1-2024",
},
}),
play: LightThemeDateFilterQuarterYearDropdown.play,
};
export const LightThemeDateFilterSingle = {
render: Template,
args: createDefaultArgs({
parameterType: "date_single",
parameterValues: {
[DATE_FILTER_ID]: "2024-06-01",
},
}),
play: async ({ canvasElement }: { canvasElement: HTMLCanvasElement }) => {
const canvas = within(canvasElement);
const filter = await canvas.findByRole("button", {
name: "Date single",
});
await userEvent.click(filter);
const popover = getLastPopover();
popover.getByText("15").classList.add("pseudo-hover");
},
};
export const DarkThemeDateFilterSingle = {
render: Template,
args: createDefaultArgs({
theme: "night",
parameterType: "date_single",
parameterValues: {
[DATE_FILTER_ID]: "2024-06-01",
},
}),
play: LightThemeDateFilterSingle.play,
};
export const LightThemeDateFilterRange = {
render: Template,
args: createDefaultArgs({
parameterType: "date_range",
parameterValues: {
[DATE_FILTER_ID]: "2024-06-01~2024-06-10",
},
}),
play: async ({ canvasElement }: { canvasElement: HTMLCanvasElement }) => {
const canvas = within(canvasElement);
const filter = await canvas.findByRole("button", {
name: "Date range",
});
await userEvent.click(filter);
const popover = getLastPopover();
await userEvent.click(popover.getByRole("button", { name: "Add time" }));
popover.getAllByText("15")[0].classList.add("pseudo-hover");
},
};
export const DarkThemeDateFilterRange = {
render: Template,
args: createDefaultArgs({
theme: "night",
parameterType: "date_range",
parameterValues: {
[DATE_FILTER_ID]: "2024-06-01~2024-06-10",
},
}),
play: LightThemeDateFilterRange.play,
};
export const LightThemeDateFilterRelative = {
render: Template,
args: createDefaultArgs({
parameterType: "date_relative",
parameterValues: {
[DATE_FILTER_ID]: "thisday",
},
}),
play: async ({ canvasElement }: { canvasElement: HTMLCanvasElement }) => {
const canvas = within(canvasElement);
const filter = await canvas.findByRole("button", {
name: "Date relative",
});
await userEvent.click(filter);
const popover = getLastPopover();
popover
.getByRole("button", { name: "Yesterday" })
.classList.add("pseudo-hover");
},
};
export const DarkThemeDateFilterRelative = {
render: Template,
args: createDefaultArgs({
theme: "night",
parameterType: "date_relative",
parameterValues: {
[DATE_FILTER_ID]: "thisday",
},
}),
play: LightThemeDateFilterRelative.play,
};
export const LightThemeUnitOfTime = {
render: Template,
args: createDefaultArgs({
parameterType: "temporal_unit",
parameterValues: {
[UNIT_OF_TIME_FILTER_ID]: "minute",
},
}),
play: async ({ canvasElement }: { canvasElement: HTMLCanvasElement }) => {
const canvas = within(canvasElement);
const filter = await canvas.findByRole("button", {
name: "Time grouping",
});
await userEvent.click(filter);
const popover = getLastPopover();
(popover.getByText("Hour").parentNode as HTMLElement).classList.add(
"pseudo-hover",
);
},
};
export const DarkThemeUnitOfTime = {
render: Template,
args: createDefaultArgs({
theme: "night",
parameterType: "temporal_unit",
parameterValues: {
[UNIT_OF_TIME_FILTER_ID]: "minute",
},
}),
play: LightThemeUnitOfTime.play,
};
export const LightThemeNumber = {
render: Template,
args: createDefaultArgs({
parameterType: "number",
}),
play: async ({ canvasElement }: { canvasElement: HTMLCanvasElement }) => {
const canvas = within(canvasElement);
const filter = await canvas.findByRole("button", {
name: "Number Equals",
});
await userEvent.click(filter);
const popover = getLastPopover();
const searchInput = popover.getByPlaceholderText("Enter a number");
await userEvent.type(searchInput, "11");
await userEvent.click(getLastPopoverElement());
await userEvent.type(searchInput, "99");
},
};
export const DarkThemeNumber = {
render: Template,
args: createDefaultArgs({
theme: "night",
parameterType: "number",
}),
play: LightThemeNumber.play,
};

View File

@@ -1,416 +0,0 @@
// @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 { getStore } from "__support__/entities-store";
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";
import { publicReducers } from "metabase/reducers-public";
import { Box, Card, Popover, Text, Tooltip } from "metabase/ui";
import { registerVisualization } from "metabase/visualizations";
import { BarChart } from "metabase/visualizations/visualizations/BarChart";
import ObjectDetail from "metabase/visualizations/visualizations/ObjectDetail";
import Table from "metabase/visualizations/visualizations/Table/Table";
import TABLE_RAW_SERIES from "metabase/visualizations/visualizations/Table/stories-data/orders-with-people.json";
import type { DashboardCard } from "metabase-types/api";
import {
createMockCard,
createMockColumn,
createMockDashboard,
createMockDashboardCard,
createMockDataset,
createMockDatasetData,
createMockParameter,
} from "metabase-types/api/mocks";
import {
createMockDashboardState,
createMockSettingsState,
createMockState,
} from "metabase-types/store/mocks";
import {
PublicOrEmbeddedDashboardView,
type PublicOrEmbeddedDashboardViewProps,
} from "./PublicOrEmbeddedDashboardView";
// @ts-expect-error: incompatible prop types with registerVisualization
registerVisualization(Table);
// @ts-expect-error: incompatible prop types with registerVisualization
registerVisualization(BarChart);
export default {
title: "App/Embed/PublicOrEmbeddedDashboardView",
component: PublicOrEmbeddedDashboardView,
decorators: [
ReduxDecorator,
createWaitForResizeToStopDecorator(),
MockIsEmbeddingDecorator,
],
parameters: {
layout: "fullscreen",
},
};
function ReduxDecorator(Story: StoryFn) {
return (
<MetabaseReduxProvider store={store}>
<Story />
</MetabaseReduxProvider>
);
}
declare global {
interface Window {
overrideIsWithinIframe?: boolean;
}
}
function MockIsEmbeddingDecorator(Story: StoryFn) {
window.overrideIsWithinIframe = true;
return <Story />;
}
const DASHBOARD_ID = getNextId();
const DASHCARD_BAR_ID = getNextId();
const DASHCARD_TABLE_ID = getNextId();
const CARD_BAR_ID = getNextId();
const CARD_TABLE_ID = getNextId();
const TAB_ID = getNextId();
const PARAMETER_ID = "param-hex";
const initialState = createMockState({
currentUser: null,
settings: createMockSettingsState({
"hide-embed-branding?": false,
}),
dashboard: createMockDashboardState({
dashcardData: {
[DASHCARD_BAR_ID]: {
[CARD_BAR_ID]: createMockDataset({
data: createMockDatasetData({
cols: [
createMockColumn(StringColumn({ name: "Dimension" })),
createMockColumn(NumberColumn({ name: "Count" })),
],
rows: [
["foo", 1],
["bar", 2],
],
}),
}),
},
[DASHCARD_TABLE_ID]: {
// Couldn't really figure out the type here.
[CARD_TABLE_ID]: createMockDataset(TABLE_RAW_SERIES[0] as any),
},
},
}),
});
const store = getStore(publicReducers, initialState, [Api.middleware]);
interface CreateDashboardOpts {
hasScroll?: boolean;
dashcards?: DashboardCard[];
}
function createDashboard({ hasScroll, dashcards }: CreateDashboardOpts = {}) {
return createMockDashboard({
id: DASHBOARD_ID,
name: "My dashboard",
width: "full",
dashcards: dashcards ?? [
createMockDashboardCard({
id: DASHCARD_BAR_ID,
dashboard_tab_id: TAB_ID,
card: createMockCard({ id: CARD_BAR_ID, name: "Bar", display: "bar" }),
size_x: 12,
size_y: 8,
parameter_mappings: [
{
card_id: CARD_BAR_ID,
parameter_id: PARAMETER_ID,
target: ["variable", ["template-tag", "abc"]],
},
],
}),
createMockDashboardCard({
id: DASHCARD_TABLE_ID,
dashboard_tab_id: TAB_ID,
card: createMockCard({
id: CARD_TABLE_ID,
name: "Table",
display: "table",
}),
...(!hasScroll ? { col: 12 } : { row: 8 }),
size_x: 12,
size_y: 8,
}),
],
});
}
const Template: StoryFn<PublicOrEmbeddedDashboardViewProps> = (args) => {
return <PublicOrEmbeddedDashboardView {...args} />;
};
const defaultArgs: Partial<
ComponentProps<typeof PublicOrEmbeddedDashboardView>
> = {
dashboard: createDashboard(),
downloadsEnabled: { pdf: true, results: true },
titled: true,
bordered: true,
background: true,
slowCards: {},
selectedTabId: TAB_ID,
parameters: [
createMockParameter({
id: PARAMETER_ID,
}),
],
withFooter: true,
};
export const LightThemeDefault = {
render: Template,
args: defaultArgs,
};
export const LightThemeScroll = {
render: Template,
args: {
...defaultArgs,
dashboard: createDashboard({ hasScroll: true }),
},
decorators: [ScrollDecorator],
};
export const LightThemeNoBackgroundDefault = {
render: Template,
args: {
...defaultArgs,
background: false,
},
};
export const LightThemeNoBackgroundScroll = {
render: Template,
args: {
...defaultArgs,
background: false,
dashboard: createDashboard({ hasScroll: true }),
},
decorators: [ScrollDecorator],
};
export const DarkThemeDefault = {
render: Template,
args: {
...defaultArgs,
theme: "night",
},
decorators: [DarkBackgroundDecorator],
};
export const DarkThemeScroll = {
render: Template,
args: {
...defaultArgs,
theme: "night",
dashboard: createDashboard({ hasScroll: true }),
},
decorators: [DarkBackgroundDecorator, ScrollDecorator],
};
export const DarkThemeNoBackgroundDefault = {
render: Template,
args: {
...defaultArgs,
theme: "night",
background: false,
},
decorators: [DarkBackgroundDecorator],
};
export const DarkThemeNoBackgroundScroll = {
render: Template,
args: {
...defaultArgs,
theme: "night",
background: false,
dashboard: createDashboard({ hasScroll: true }),
},
decorators: [DarkBackgroundDecorator, ScrollDecorator],
};
export const TransparentThemeDefault = {
render: Template,
args: {
...defaultArgs,
theme: "transparent",
},
decorators: [LightBackgroundDecorator],
};
export const TransparentThemeScroll = {
render: Template,
args: {
...defaultArgs,
theme: "transparent",
dashboard: createDashboard({ hasScroll: true }),
},
decorators: [LightBackgroundDecorator, ScrollDecorator],
};
export const TransparentThemeNoBackgroundDefault = {
render: Template,
args: {
...defaultArgs,
theme: "transparent",
background: false,
},
decorators: [LightBackgroundDecorator],
};
export const TransparentThemeNoBackgroundScroll = {
render: Template,
args: {
...defaultArgs,
theme: "transparent",
background: false,
dashboard: createDashboard({ hasScroll: true }),
},
decorators: [LightBackgroundDecorator, ScrollDecorator],
};
// Other components compatibility test
export function ComponentCompatibility() {
return (
// Loki doesn't take into account the tooltips and dropdowns dimensions.
// This padding is to make sure we cover the area of all of them.
<Box pb="50px">
<Tooltip
label={
<Text size="sm" c="var(--mb-color-text-primary)">
Label
</Text>
}
opened
>
<Card withBorder display="inline-block">
Mantine Tooltip
</Card>
</Tooltip>
<Popover withArrow shadow="md" opened>
<Popover.Target>
<Card withBorder display="inline-block">
Mantine Popover
</Card>
</Popover.Target>
<Popover.Dropdown>
<Text size="sm" c="var(--mb-color-text-primary)">
Dropdown
</Text>
</Popover.Dropdown>
</Popover>
</Box>
);
}
// Card visualizations
// @ts-expect-error: incompatible prop types with registerVisualization
registerVisualization(ObjectDetail);
export const CardVisualizationsLightTheme = {
render: Template,
args: {
...defaultArgs,
dashboard: createDashboard({
dashcards: [
createMockDashboardCard({
id: DASHCARD_TABLE_ID,
dashboard_tab_id: TAB_ID,
card: createMockCard({
id: CARD_TABLE_ID,
name: "Table detail",
display: "object",
}),
size_x: 12,
size_y: 8,
}),
],
}),
},
};
export const CardVisualizationsDarkTheme = {
render: Template,
args: {
...CardVisualizationsLightTheme.args,
theme: "night",
},
};
function ScrollDecorator(Story: StoryFn) {
const asyncCallback = useMemo(() => createAsyncCallback(), []);
useEffect(() => {
const scrollContainer = document.querySelector("[data-testid=embed-frame]");
const intervalId = setInterval(() => {
const contentHeight = scrollContainer?.scrollHeight ?? 0;
if (contentHeight > 1000) {
scrollContainer?.scrollBy(0, 9999);
clearInterval(intervalId);
asyncCallback();
}
}, 100);
return () => {
clearInterval(intervalId);
};
}, [asyncCallback]);
return <Story />;
}
function DarkBackgroundDecorator(Story: StoryFn) {
return (
<Box bg="#434e56" mih="100vh">
<Story />
</Box>
);
}
function LightBackgroundDecorator(Story: StoryFn) {
return (
<Box bg="#ddd" mih="100vh">
<Story />
</Box>
);
}

View File

@@ -1,308 +0,0 @@
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 { isActionDashCard } from "metabase/dashboard/utils";
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 { 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 { 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 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}
onResetFilters={_.noop}
dashboardActionKeys={DASHBOARD_DISPLAY_ACTIONS}
refreshPeriod={refreshPeriod}
onRefreshPeriodChange={onRefreshPeriodChange}
onFullscreenChange={onFullscreenChange}
setRefreshElapsedHook={setRefreshElapsedHook}
isFullscreen={isFullscreen}
hasNightModeToggle={hasNightModeToggle}
onNightModeChange={onNightModeChange}
isNightMode={isNightMode}
isPublic={true}
/>
) : null;
const visibleDashcards = (dashboard?.dashcards ?? []).filter(
(dashcard) => !isActionDashCard(dashcard),
);
const dashboardHasCards = dashboard && visibleDashcards.length > 0;
const dashboardHasTabs = dashboard?.tabs && dashboard.tabs.length > 1;
const hasVisibleParameters = parameters.filter((p) => !p.hidden).length > 0;
const tabHasCards =
visibleDashcards.filter(
(dc: DashboardCard) => dc.dashboard_tab_id === selectedTabId,
).length > 0;
const hiddenParameterSlugs = getTabHiddenParameterSlugs({
parameters,
dashboard,
selectedTabId,
});
const normalizedTheme = normalizeTheme({
theme,
background,
});
const getClickActionMode: ClickActionModeGetter = useCallback(
({ question }) =>
externalGetClickActionMode?.({ question }) ??
getEmbeddingMode({
question,
queryMode: navigateToNewCardFromDashboard
? EmbeddingSdkMode
: PublicMode,
}),
[externalGetClickActionMode, navigateToNewCardFromDashboard],
);
const isCompactHeader = !titled && !hasVisibleParameters && !dashboardHasTabs;
return (
<EmbedFrame
name={dashboard && dashboard.name}
description={dashboard && dashboard.description}
dashboard={dashboard}
parameters={parameters}
parameterValues={parameterValues}
draftParameterValues={draftParameterValues}
hiddenParameterSlugs={hiddenParameterSlugs}
setParameterValue={setParameterValue}
setParameterValueToDefault={setParameterValueToDefault}
enableParameterRequiredBehavior
actionButtons={buttons ? <div className={CS.flex}>{buttons}</div> : null}
dashboardTabs={
dashboardHasTabs && <DashboardTabs dashboardId={dashboardId} />
}
background={background}
bordered={bordered}
titled={titled}
theme={normalizedTheme}
hide_parameters={hideParameters}
pdfDownloadsEnabled={downloadsEnabled.pdf}
withFooter={withFooter}
>
<LoadingAndErrorWrapper
className={cx({
[DashboardS.DashboardFullscreen]: isFullscreen,
[DashboardS.DashboardNight]: isNightMode,
[ParametersS.DashboardNight]: isNightMode,
[ColorS.DashboardNight]: isNightMode,
})}
loading={!dashboard}
>
{() => {
if (!dashboard) {
return null;
}
if (!dashboardHasCards) {
return (
<DashboardEmptyStateWithoutAddPrompt
isNightMode={isNightMode}
isDashboardEmpty={true}
/>
);
}
if (dashboardHasCards && !tabHasCards) {
return (
<DashboardEmptyStateWithoutAddPrompt
isNightMode={isNightMode}
isDashboardEmpty={false}
/>
);
}
return (
<FullWidthContainer mt={isCompactHeader ? "xs" : "sm"}>
<DashboardGridConnected
dashboard={assoc(dashboard, "dashcards", visibleDashcards)}
isPublicOrEmbedded
getClickActionMode={getClickActionMode}
selectedTabId={selectedTabId}
slowCards={slowCards}
isEditing={false}
isEditingParameter={false}
isXray={false}
isFullscreen={isFullscreen}
isNightMode={isNightMode}
withCardTitle={cardTitled}
clickBehaviorSidebarDashcard={null}
navigateToNewCardFromDashboard={navigateToNewCardFromDashboard}
downloadsEnabled={downloadsEnabled.results}
autoScrollToDashcardId={undefined}
reportAutoScrolledToDashcard={_.noop}
/>
</FullWidthContainer>
);
}}
</LoadingAndErrorWrapper>
</EmbedFrame>
);
}
function getTabHiddenParameterSlugs({
parameters,
dashboard,
selectedTabId,
}: {
parameters: UiParameter[];
dashboard: Dashboard | null;
selectedTabId: SelectedTabId;
}) {
const currentTabParameterIds =
getCurrentTabDashcards({ dashboard, selectedTabId })?.flatMap(
(dashcard) =>
dashcard.parameter_mappings?.map((mapping) => mapping.parameter_id) ??
[],
) ?? [];
const hiddenParameters = parameters.filter(
(parameter) => !currentTabParameterIds.includes(parameter.id),
);
return hiddenParameters.map((parameter) => parameter.slug).join(",");
}
function getCurrentTabDashcards({
dashboard,
selectedTabId,
}: {
dashboard: Dashboard | null;
selectedTabId: SelectedTabId;
}) {
if (!Array.isArray(dashboard?.dashcards)) {
return [];
}
if (!selectedTabId) {
return dashboard?.dashcards;
}
return dashboard?.dashcards.filter(
(dashcard) => dashcard.dashboard_tab_id === selectedTabId,
);
}
/**
* When both `background: false` and `theme: "transparent"` options are supplied,
* the new behavior takes precedence (metabase#43838)
*/
function normalizeTheme({
theme,
background,
}: {
theme: DisplayTheme;
background: boolean;
}) {
if (!background && theme === "transparent") {
return "light";
}
return theme;
}

View File

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

View File

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