Compare commits
15 Commits
python-run
...
dashboard-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
495bccb8d5 | ||
|
|
d8da2043d5 | ||
|
|
9f17e6962c | ||
|
|
4af1c11f21 | ||
|
|
aa54307492 | ||
|
|
d556f612ac | ||
|
|
5ca80580d2 | ||
|
|
84b53d006b | ||
|
|
7d25ad8f59 | ||
|
|
84507a1f53 | ||
|
|
f7ac5543ec | ||
|
|
0d747e3dea | ||
|
|
3a715e7582 | ||
|
|
e4f6d46c05 | ||
|
|
ba609272be |
@@ -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>;
|
||||
|
||||
@@ -136,7 +136,6 @@ export const EditableDashboard = ({
|
||||
>
|
||||
<ConnectedDashboard
|
||||
dashboardId={dashboardId}
|
||||
isLoading={isLoading}
|
||||
parameterQueryParams={initialParameters}
|
||||
refreshPeriod={refreshPeriod}
|
||||
onRefreshPeriodChange={onRefreshPeriodChange}
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
import { useDashboardContext } from "metabase/dashboard/context";
|
||||
|
||||
export const Description = () => {
|
||||
const { dashboard } = useDashboardContext();
|
||||
|
||||
return <>{dashboard?.description}</>;
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -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
|
||||
@@ -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>
|
||||
);
|
||||
});
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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;
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,7 @@
|
||||
import { useDashboardContext } from "metabase/dashboard/context";
|
||||
|
||||
export const Title = () => {
|
||||
const { dashboard } = useDashboardContext();
|
||||
|
||||
return <>{dashboard?.name}</>;
|
||||
};
|
||||
@@ -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";
|
||||
@@ -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>;
|
||||
|
||||
@@ -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 />;
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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: {},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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`}
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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();
|
||||
};
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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 />;
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
319
frontend/src/metabase/dashboard/context/context.tsx
Normal file
319
frontend/src/metabase/dashboard/context/context.tsx
Normal 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;
|
||||
}
|
||||
1
frontend/src/metabase/dashboard/context/index.ts
Normal file
1
frontend/src/metabase/dashboard/context/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./context";
|
||||
153
frontend/src/metabase/dashboard/context/redux.ts
Normal file
153
frontend/src/metabase/dashboard/context/redux.ts
Normal 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>;
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user