From b71676164a337394cd3b6dd62e1bf2f54a832283 Mon Sep 17 00:00:00 2001
From: Alexander Polyankin <alexander.polyankin@metabase.com>
Date: Mon, 28 Feb 2022 20:35:22 +0300
Subject: [PATCH] Add write checks (#20762)

---
 frontend/src/metabase-types/api/collection.ts |  1 +
 .../metabase-types/api/mocks/collection.ts    |  1 +
 .../components/EventCard/EventCard.tsx        | 12 +++--
 .../TimelineDetailsModal.styled.tsx           |  2 +-
 .../TimelineDetailsModal.tsx                  | 51 ++++++++++++-------
 .../TimelineEmptyState/TimelineEmptyState.tsx |  8 +--
 .../TimelineListModal/TimelineListModal.tsx   |  3 +-
 frontend/src/metabase/timelines/types.ts      |  6 +++
 8 files changed, 58 insertions(+), 26 deletions(-)

diff --git a/frontend/src/metabase-types/api/collection.ts b/frontend/src/metabase-types/api/collection.ts
index 9ca3e320c93..ea02ed31f5b 100644
--- a/frontend/src/metabase-types/api/collection.ts
+++ b/frontend/src/metabase-types/api/collection.ts
@@ -3,4 +3,5 @@ export type CollectionId = number | string;
 export interface Collection {
   id: CollectionId;
   name: string;
+  can_write: boolean;
 }
diff --git a/frontend/src/metabase-types/api/mocks/collection.ts b/frontend/src/metabase-types/api/mocks/collection.ts
index 404dee52b0b..46db10223ee 100644
--- a/frontend/src/metabase-types/api/mocks/collection.ts
+++ b/frontend/src/metabase-types/api/mocks/collection.ts
@@ -5,5 +5,6 @@ export const createMockCollection = (
 ): Collection => ({
   id: 1,
   name: "Collection",
+  can_write: false,
   ...opts,
 });
diff --git a/frontend/src/metabase/timelines/components/EventCard/EventCard.tsx b/frontend/src/metabase/timelines/components/EventCard/EventCard.tsx
index ddba507d6bd..eb036ebefd4 100644
--- a/frontend/src/metabase/timelines/components/EventCard/EventCard.tsx
+++ b/frontend/src/metabase/timelines/components/EventCard/EventCard.tsx
@@ -61,9 +61,11 @@ const EventCard = ({
         )}
         <CardCreatorInfo>{creatorMessage}</CardCreatorInfo>
       </CardBody>
-      <CardAside>
-        <EntityMenu items={menuItems} triggerIcon="ellipsis" />
-      </CardAside>
+      {menuItems.length > 0 && (
+        <CardAside>
+          <EntityMenu items={menuItems} triggerIcon="ellipsis" />
+        </CardAside>
+      )}
     </CardRoot>
   );
 };
@@ -75,7 +77,9 @@ const getMenuItems = (
   onArchive?: (event: TimelineEvent) => void,
   onUnarchive?: (event: TimelineEvent) => void,
 ) => {
-  if (!event.archived) {
+  if (!collection.can_write) {
+    return [];
+  } else if (!event.archived) {
     return [
       {
         title: t`Edit event`,
diff --git a/frontend/src/metabase/timelines/components/TimelineDetailsModal/TimelineDetailsModal.styled.tsx b/frontend/src/metabase/timelines/components/TimelineDetailsModal/TimelineDetailsModal.styled.tsx
index f21cc45ad52..835c09f45ce 100644
--- a/frontend/src/metabase/timelines/components/TimelineDetailsModal/TimelineDetailsModal.styled.tsx
+++ b/frontend/src/metabase/timelines/components/TimelineDetailsModal/TimelineDetailsModal.styled.tsx
@@ -15,7 +15,6 @@ export const ModalToolbar = styled.div`
 
 export const ModalToolbarInput = styled(TextInput)`
   flex: 1 1 auto;
-  margin-right: 1rem;
 
   ${TextInput.Input} {
     height: 2.5rem;
@@ -27,6 +26,7 @@ export const ModalToolbarLink = styled(Link)`
   flex: 0 0 auto;
   align-items: center;
   height: 2.5rem;
+  margin-left: 1rem;
 `;
 
 export const ModalBody = styled.div`
diff --git a/frontend/src/metabase/timelines/components/TimelineDetailsModal/TimelineDetailsModal.tsx b/frontend/src/metabase/timelines/components/TimelineDetailsModal/TimelineDetailsModal.tsx
index faee2fe09ef..17d8cbb8ce1 100644
--- a/frontend/src/metabase/timelines/components/TimelineDetailsModal/TimelineDetailsModal.tsx
+++ b/frontend/src/metabase/timelines/components/TimelineDetailsModal/TimelineDetailsModal.tsx
@@ -19,6 +19,7 @@ import {
   ModalToolbarInput,
   ModalToolbarLink,
 } from "./TimelineDetailsModal.styled";
+import { MenuItem } from "../../types";
 
 export interface TimelineDetailsModalProps {
   timeline: Timeline;
@@ -50,16 +51,19 @@ const TimelineDetailsModal = ({
   }, [timeline, searchText, isArchive]);
 
   const menuItems = useMemo(() => {
-    return getMenuItems(timeline, collection);
-  }, [timeline, collection]);
+    return getMenuItems(timeline, collection, isArchive);
+  }, [timeline, collection, isArchive]);
 
   const isNotEmpty = events.length > 0;
   const isSearching = searchText.length > 0;
+  const canWrite = collection.can_write;
 
   return (
     <ModalRoot>
       <ModalHeader title={title} onClose={onClose}>
-        {!isArchive && <EntityMenu items={menuItems} triggerIcon="ellipsis" />}
+        {menuItems.length > 0 && (
+          <EntityMenu items={menuItems} triggerIcon="ellipsis" />
+        )}
       </ModalHeader>
       {(isNotEmpty || isSearching) && (
         <ModalToolbar>
@@ -69,7 +73,7 @@ const TimelineDetailsModal = ({
             icon={<Icon name="search" />}
             onChange={setInputText}
           />
-          {!isArchive && (
+          {canWrite && !isArchive && (
             <ModalToolbarLink
               className="Button"
               to={Urls.newEventInCollection(timeline, collection)}
@@ -119,21 +123,34 @@ const isEventMatch = (event: TimelineEvent, searchText: string) => {
   );
 };
 
-const getMenuItems = (timeline: Timeline, collection: Collection) => {
-  return [
-    {
-      title: t`New timeline`,
-      link: Urls.newTimelineInCollection(collection),
-    },
-    {
-      title: t`Edit timeline details`,
-      link: Urls.editTimelineInCollection(timeline, collection),
-    },
-    {
+const getMenuItems = (
+  timeline: Timeline,
+  collection: Collection,
+  isArchive: boolean,
+) => {
+  const items: MenuItem[] = [];
+
+  if (collection.can_write && !isArchive) {
+    items.push(
+      {
+        title: t`New timeline`,
+        link: Urls.newTimelineInCollection(collection),
+      },
+      {
+        title: t`Edit timeline details`,
+        link: Urls.editTimelineInCollection(timeline, collection),
+      },
+    );
+  }
+
+  if (!isArchive) {
+    items.push({
       title: t`View archived events`,
       link: Urls.timelineArchiveInCollection(timeline, collection),
-    },
-  ];
+    });
+  }
+
+  return items;
 };
 
 export default TimelineDetailsModal;
diff --git a/frontend/src/metabase/timelines/components/TimelineEmptyState/TimelineEmptyState.tsx b/frontend/src/metabase/timelines/components/TimelineEmptyState/TimelineEmptyState.tsx
index 2fda3fbfd50..1ba6972c103 100644
--- a/frontend/src/metabase/timelines/components/TimelineEmptyState/TimelineEmptyState.tsx
+++ b/frontend/src/metabase/timelines/components/TimelineEmptyState/TimelineEmptyState.tsx
@@ -66,9 +66,11 @@ const TimelineEmptyState = ({
         <EmptyStateMessage>
           {t`Add events to Metabase to open important milestones, launches, or anything else, right alongside your data.`}
         </EmptyStateMessage>
-        <Link className="Button Button--primary" to={link}>
-          {t`Add an event`}
-        </Link>
+        {collection.can_write && (
+          <Link className="Button Button--primary" to={link}>
+            {t`Add an event`}
+          </Link>
+        )}
       </EmptyStateBody>
     </EmptyStateRoot>
   );
diff --git a/frontend/src/metabase/timelines/components/TimelineListModal/TimelineListModal.tsx b/frontend/src/metabase/timelines/components/TimelineListModal/TimelineListModal.tsx
index 5aa3f486ce3..1628ec045a9 100644
--- a/frontend/src/metabase/timelines/components/TimelineListModal/TimelineListModal.tsx
+++ b/frontend/src/metabase/timelines/components/TimelineListModal/TimelineListModal.tsx
@@ -19,13 +19,14 @@ const TimelineListModal = ({
   collection,
   onClose,
 }: TimelineListModalProps): JSX.Element => {
+  const canWrite = collection.can_write;
   const hasTimelines = timelines.length > 0;
   const title = hasTimelines ? t`Events` : t`${collection.name} events`;
 
   return (
     <div>
       <ModalHeader title={title} onClose={onClose}>
-        {hasTimelines && <TimelineMenu collection={collection} />}
+        {canWrite && hasTimelines && <TimelineMenu collection={collection} />}
       </ModalHeader>
       <ModalBody>
         {hasTimelines ? (
diff --git a/frontend/src/metabase/timelines/types.ts b/frontend/src/metabase/timelines/types.ts
index 903d8931310..3ca07dee24b 100644
--- a/frontend/src/metabase/timelines/types.ts
+++ b/frontend/src/metabase/timelines/types.ts
@@ -1,3 +1,9 @@
+export interface MenuItem {
+  title: string;
+  link?: string;
+  action?: () => void;
+}
+
 export interface ModalParams {
   slug: string;
   timelineId?: string;
-- 
GitLab