From d489654cba2e8063b710cc8e66a1889ea7d1a3fc Mon Sep 17 00:00:00 2001 From: Ryan Laurie <30528226+iethree@users.noreply.github.com> Date: Thu, 26 Sep 2024 09:09:32 -0600 Subject: [PATCH] Add additional dashboard info to sidesheet (#48078) * create dashboard settings sidebar * only show settings to dashboard editors * add dashboard details * update localization * fix merged type * fix mixed-up creation and editing --- .../DashboardInfoSidebar/DashboardDetails.tsx | 129 ++++++++++++++++++ .../DashboardInfoSidebar.module.css | 13 ++ .../DashboardInfoSidebar.tsx | 6 + .../tests/common.unit.spec.ts | 70 +++++++++- .../QuestionInfoSidebar/QuestionDetails.tsx | 26 ++-- 5 files changed, 232 insertions(+), 12 deletions(-) create mode 100644 frontend/src/metabase/dashboard/components/DashboardInfoSidebar/DashboardDetails.tsx diff --git a/frontend/src/metabase/dashboard/components/DashboardInfoSidebar/DashboardDetails.tsx b/frontend/src/metabase/dashboard/components/DashboardInfoSidebar/DashboardDetails.tsx new file mode 100644 index 00000000000..951b2badb3d --- /dev/null +++ b/frontend/src/metabase/dashboard/components/DashboardInfoSidebar/DashboardDetails.tsx @@ -0,0 +1,129 @@ +import cx from "classnames"; +import { useState } from "react"; +import { c, t } from "ttag"; + +import { skipToken, useGetUserQuery } from "metabase/api"; +import { SidesheetCardSection } from "metabase/common/components/Sidesheet"; +import DateTime from "metabase/components/DateTime"; +import Link from "metabase/core/components/Link"; +import Styles from "metabase/css/core/index.css"; +import { getUserName } from "metabase/lib/user"; +import { DashboardPublicLinkPopover } from "metabase/sharing/components/PublicLinkPopover"; +import { Box, FixedSizeIcon, Flex, Text } from "metabase/ui"; +import type { Dashboard } from "metabase-types/api"; + +import SidebarStyles from "./DashboardInfoSidebar.module.css"; + +export const DashboardDetails = ({ dashboard }: { dashboard: Dashboard }) => { + const lastEditInfo = dashboard["last-edit-info"]; + const createdAt = dashboard.created_at; + + // we don't hydrate creator user info on the dashboard object + const { data: creator } = useGetUserQuery(dashboard.creator_id ?? skipToken); + + return ( + <> + <SidesheetCardSection title={t`Creator and last editor`}> + {creator && ( + <Flex gap="sm" align="top"> + <FixedSizeIcon name="ai" className={SidebarStyles.IconMargin} /> + <Text> + {c( + "Describes when a dashboard was created. {0} is a date/time and {1} is a person's name", + ).jt`${( + <DateTime unit="day" value={createdAt} key="date" /> + )} by ${getUserName(creator)}`} + </Text> + </Flex> + )} + + {lastEditInfo && ( + <Flex gap="sm" align="top"> + <FixedSizeIcon name="pencil" className={SidebarStyles.IconMargin} /> + <Text> + {c( + "Describes when a dashboard was last edited. {0} is a date/time and {1} is a person's name", + ).jt`${( + <DateTime + unit="day" + value={lastEditInfo.timestamp} + key="date" + /> + )} by ${getUserName(lastEditInfo)}`} + </Text> + </Flex> + )} + </SidesheetCardSection> + <SidesheetCardSection + title={c( + "This is a heading that appears above the name of a collection - a collection that a dashboard is saved in. Feel free to translate this heading as though it said 'Saved in collection', if you think that would make more sense in your language.", + ).t`Saved in`} + > + <Flex gap="sm" align="top"> + <FixedSizeIcon + name="folder" + className={SidebarStyles.IconMargin} + color="var(--mb-color-brand)" + /> + <div> + <Text> + <Link + to={`/collection/${dashboard.collection_id}`} + variant="brand" + > + {dashboard.collection?.name} + </Link> + </Text> + </div> + </Flex> + </SidesheetCardSection> + <SharingDisplay dashboard={dashboard} /> + </> + ); +}; + +function SharingDisplay({ dashboard }: { dashboard: Dashboard }) { + const publicUUID = dashboard.public_uuid; + const embeddingEnabled = dashboard.enable_embedding; + const [isPopoverOpen, setIsPopoverOpen] = useState(false); + + if (!publicUUID && !embeddingEnabled) { + return null; + } + + return ( + <SidesheetCardSection title={t`Visibility`}> + {publicUUID && ( + <Flex gap="sm" align="center"> + <FixedSizeIcon name="globe" color="var(--mb-color-brand)" /> + <Text>{t`Shared publicly`}</Text> + + <DashboardPublicLinkPopover + target={ + <FixedSizeIcon + name="link" + onClick={() => setIsPopoverOpen(prev => !prev)} + className={cx( + Styles.cursorPointer, + Styles.textBrandHover, + SidebarStyles.IconMargin, + )} + /> + } + isOpen={isPopoverOpen} + onClose={() => setIsPopoverOpen(false)} + dashboard={dashboard} + /> + </Flex> + )} + {embeddingEnabled && ( + <Flex gap="sm" align="center"> + <Box className={SidebarStyles.BrandCircle}> + <FixedSizeIcon name="embed" size="14px" /> + </Box> + <Text>{t`Embedded`}</Text> + </Flex> + )} + </SidesheetCardSection> + ); +} diff --git a/frontend/src/metabase/dashboard/components/DashboardInfoSidebar/DashboardInfoSidebar.module.css b/frontend/src/metabase/dashboard/components/DashboardInfoSidebar/DashboardInfoSidebar.module.css index 70613d52e25..3341dec5cbd 100644 --- a/frontend/src/metabase/dashboard/components/DashboardInfoSidebar/DashboardInfoSidebar.module.css +++ b/frontend/src/metabase/dashboard/components/DashboardInfoSidebar/DashboardInfoSidebar.module.css @@ -3,3 +3,16 @@ overflow: auto; line-height: 1.38rem; /* magic number to keep line-height from changing in edit mode */ } + +.BrandCircle { + background-color: var(--mb-color-brand); + color: var(--mb-color-text-white); + border-radius: 50%; + height: 1rem; + width: 1rem; + padding: 1px; +} + +.IconMargin { + margin-top: 0.25rem; +} diff --git a/frontend/src/metabase/dashboard/components/DashboardInfoSidebar/DashboardInfoSidebar.tsx b/frontend/src/metabase/dashboard/components/DashboardInfoSidebar/DashboardInfoSidebar.tsx index e01c49b85cb..1a890b36828 100644 --- a/frontend/src/metabase/dashboard/components/DashboardInfoSidebar/DashboardInfoSidebar.tsx +++ b/frontend/src/metabase/dashboard/components/DashboardInfoSidebar/DashboardInfoSidebar.tsx @@ -14,6 +14,7 @@ import SidesheetS from "metabase/common/components/Sidesheet/sidesheet.module.cs import { Timeline } from "metabase/common/components/Timeline"; import { getTimelineEvents } from "metabase/common/components/Timeline/utils"; import { useRevisionListQuery } from "metabase/common/hooks"; +import { EntityIdCard } from "metabase/components/EntityIdCard"; import EditableText from "metabase/core/components/EditableText"; import { revertToRevision, updateDashboard } from "metabase/dashboard/actions"; import { DASHBOARD_DESCRIPTION_MAX_LENGTH } from "metabase/dashboard/constants"; @@ -22,6 +23,7 @@ import { getUser } from "metabase/selectors/user"; import { Stack, Tabs, Text } from "metabase/ui"; import type { Dashboard, Revision, User } from "metabase-types/api"; +import { DashboardDetails } from "./DashboardDetails"; import DashboardInfoSidebarS from "./DashboardInfoSidebar.module.css"; interface DashboardInfoSidebarProps { @@ -174,6 +176,10 @@ const OverviewTab = ({ </Text> )} </SidesheetCard> + <SidesheetCard> + <DashboardDetails dashboard={dashboard} /> + </SidesheetCard> + <EntityIdCard entityId={dashboard.entity_id} /> </Stack> ); }; diff --git a/frontend/src/metabase/dashboard/components/DashboardInfoSidebar/tests/common.unit.spec.ts b/frontend/src/metabase/dashboard/components/DashboardInfoSidebar/tests/common.unit.spec.ts index 2281d3ddb75..897994c71d5 100644 --- a/frontend/src/metabase/dashboard/components/DashboardInfoSidebar/tests/common.unit.spec.ts +++ b/frontend/src/metabase/dashboard/components/DashboardInfoSidebar/tests/common.unit.spec.ts @@ -1,7 +1,10 @@ import userEvent from "@testing-library/user-event"; import { screen } from "__support__/ui"; -import { createMockDashboard } from "metabase-types/api/mocks"; +import { + createMockCollection, + createMockDashboard, +} from "metabase-types/api/mocks"; import { setup } from "./setup"; @@ -99,4 +102,69 @@ describe("DashboardInfoSidebar", () => { expect(setDashboardAttribute).toHaveBeenCalledWith("description", ""); }); + + it("should show last edited info", async () => { + await setup({ + dashboard: createMockDashboard({ + "last-edit-info": { + timestamp: "1793-09-22T00:00:00", + first_name: "Frodo", + last_name: "Baggins", + email: "dontlikejewelry@example.com", + id: 7, + }, + }), + }); + expect(screen.getByText("Creator and last editor")).toBeInTheDocument(); + expect(screen.getByText("September 22, 1793")).toBeInTheDocument(); + expect(screen.getByText("by Frodo Baggins")).toBeInTheDocument(); + }); + + it("should show creator info", async () => { + await setup({ + dashboard: createMockDashboard({ + creator_id: 1, + "last-edit-info": { + timestamp: "1793-09-22T00:00:00", + first_name: "Frodo", + last_name: "Baggins", + email: "dontlikejewelry@example.com", + id: 7, + }, + }), + }); + + expect(screen.getByText("Creator and last editor")).toBeInTheDocument(); + expect(screen.getByText("January 1, 2024")).toBeInTheDocument(); + expect(screen.getByText("by Testy Tableton")).toBeInTheDocument(); + }); + + it("should show collection", async () => { + await setup({ + dashboard: createMockDashboard({ + collection: createMockCollection({ + name: "My little collection ", + }), + }), + }); + + expect(screen.getByText("Saved in")).toBeInTheDocument(); + expect(await screen.findByText("My little collection")).toBeInTheDocument(); + }); + + it("should not show Visibility section when not shared publicly", async () => { + await setup(); + expect(screen.queryByText("Visibility")).not.toBeInTheDocument(); + }); + + it("should show Visibility section when dashboard has a public link", async () => { + await setup({ dashboard: createMockDashboard({ public_uuid: "123" }) }); + expect(screen.getByText("Visibility")).toBeInTheDocument(); + }); + + it("should show visibility section when embedding is enabled", async () => { + await setup({ dashboard: createMockDashboard({ enable_embedding: true }) }); + expect(screen.getByText("Visibility")).toBeInTheDocument(); + expect(screen.getByText("Embedded")).toBeInTheDocument(); + }); }); diff --git a/frontend/src/metabase/query_builder/components/view/sidebars/QuestionInfoSidebar/QuestionDetails.tsx b/frontend/src/metabase/query_builder/components/view/sidebars/QuestionInfoSidebar/QuestionDetails.tsx index 5cd10c0ea1d..bebb4f6b6b0 100644 --- a/frontend/src/metabase/query_builder/components/view/sidebars/QuestionInfoSidebar/QuestionDetails.tsx +++ b/frontend/src/metabase/query_builder/components/view/sidebars/QuestionInfoSidebar/QuestionDetails.tsx @@ -26,11 +26,24 @@ export const QuestionDetails = ({ question }: { question: Question }) => { return ( <> <SidesheetCardSection title={t`Creator and last editor`}> + <Flex gap="sm" align="top"> + <Icon name="ai" className={SidebarStyles.IconMargin} /> + <Text> + {c( + "Describes when a question was created. {0} is a date/time and {1} is a person's name", + ).jt`${( + <DateTime unit="day" value={createdAt} key="date" /> + )} by ${getUserName(createdBy)}`} + </Text> + </Flex> + {lastEditInfo && ( <Flex gap="sm" align="top"> - <Icon name="ai" className={SidebarStyles.IconMargin} /> + <Icon name="pencil" className={SidebarStyles.IconMargin} /> <Text> - {c("{0} is a date/time and {1} is a person's name").jt`${( + {c( + "Describes when a question was last edited. {0} is a date/time and {1} is a person's name", + ).jt`${( <DateTime unit="day" value={lastEditInfo.timestamp} @@ -40,15 +53,6 @@ export const QuestionDetails = ({ question }: { question: Question }) => { </Text> </Flex> )} - - <Flex gap="sm" align="top"> - <Icon name="pencil" className={SidebarStyles.IconMargin} /> - <Text> - {c("{0} is a date/time and {1} is a person's name").jt`${( - <DateTime unit="day" value={createdAt} key="date" /> - )} by ${getUserName(createdBy)}`} - </Text> - </Flex> </SidesheetCardSection> <SidesheetCardSection title={t`Saved in`}> <Flex gap="sm" align="top" color="var(--mb-color-brand)"> -- GitLab