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