From 53cf083fe534c87aaa0a6adae26c1cb1a03c88f2 Mon Sep 17 00:00:00 2001
From: Nemanja Glumac <31325167+nemanjaglumac@users.noreply.github.com>
Date: Thu, 25 Apr 2024 12:54:45 +0200
Subject: [PATCH] Re-wire the `Pulses` entity to use RTK Query under the hood
 (#41775)

* Throw on `Pulse.api.delete`

* Add boilerplate `subscriptionApi`

* Add `unsubscribe` endpoint

* Add types

* Delete unused `Pulses.objectActions`

* Re-wire `Pulses` entity to use RTK Query for `objectActions`

* Re-wire `Pulses.api` to use RTK Query under the hood

* Add cache invalidation

* Fix `setArchived` objectAction

* Update DELETE response type

* Fix `params`
---
 frontend/src/metabase-types/api/index.ts      |  1 +
 .../src/metabase-types/api/subscription.ts    | 55 ++++++++++++
 frontend/src/metabase/api/index.ts            |  1 +
 frontend/src/metabase/api/subscription.ts     | 86 +++++++++++++++++++
 frontend/src/metabase/api/tags/constants.ts   |  3 +-
 frontend/src/metabase/api/tags/utils.ts       | 16 ++++
 frontend/src/metabase/entities/pulses.js      | 72 +++++++++-------
 7 files changed, 202 insertions(+), 32 deletions(-)
 create mode 100644 frontend/src/metabase-types/api/subscription.ts
 create mode 100644 frontend/src/metabase/api/subscription.ts

diff --git a/frontend/src/metabase-types/api/index.ts b/frontend/src/metabase-types/api/index.ts
index b3e231edee4..d28fa6a5d78 100644
--- a/frontend/src/metabase-types/api/index.ts
+++ b/frontend/src/metabase-types/api/index.ts
@@ -33,6 +33,7 @@ export * from "./setup";
 export * from "./slack";
 export * from "./snippets";
 export * from "./store";
+export * from "./subscription";
 export * from "./table";
 export * from "./task";
 export * from "./timeline";
diff --git a/frontend/src/metabase-types/api/subscription.ts b/frontend/src/metabase-types/api/subscription.ts
new file mode 100644
index 00000000000..07d44c6ec1e
--- /dev/null
+++ b/frontend/src/metabase-types/api/subscription.ts
@@ -0,0 +1,55 @@
+import type { Card } from "./card";
+import type { RegularCollectionId } from "./collection";
+import type { DashboardId } from "./dashboard";
+import type { Channel } from "./notifications";
+import type { Parameter } from "./parameters";
+import type { User } from "./user";
+
+export interface ListSubscriptionsRequest {
+  archived?: boolean;
+  dashboard_id?: DashboardId;
+  creator_or_recipient?: boolean;
+}
+
+export interface DashboardSubscription {
+  archived: boolean;
+  cards: Card[];
+  channels: Channel[];
+  collection_id: RegularCollectionId | null;
+  collection_position: number | null;
+  created_at: string;
+  creator: User;
+  creator_id: number;
+  dashboard_id: DashboardId;
+  entity_id: string;
+  id: number;
+  name: string;
+  parameters: Parameter[];
+  skip_if_empty: boolean;
+  updated_at: string;
+}
+
+export interface CreateSubscriptionRequest {
+  name: string;
+  cards: Card[];
+  channels: Channel[];
+  skip_if_empty?: boolean;
+  collection_id?: RegularCollectionId | null;
+  collection_position?: number | null;
+  dashboard_id?: DashboardId;
+  parameters?: Parameter[];
+}
+
+export interface UpdateSubscriptionRequest {
+  id: number;
+  name?: string;
+  cards?: Card[];
+  channels?: Channel[];
+  skip_if_empty?: boolean;
+  collection_id?: RegularCollectionId | null;
+  collection_position?: number | null;
+  dashboard_id?: DashboardId;
+  parameters?: Parameter[];
+  archived?: boolean;
+  can_write?: boolean;
+}
diff --git a/frontend/src/metabase/api/index.ts b/frontend/src/metabase/api/index.ts
index ea7e0c5b1c2..09bed25e514 100644
--- a/frontend/src/metabase/api/index.ts
+++ b/frontend/src/metabase/api/index.ts
@@ -19,6 +19,7 @@ export * from "./search";
 export * from "./segment";
 export * from "./session";
 export * from "./snippet";
+export * from "./subscription";
 export * from "./table";
 export * from "./task";
 export * from "./timeline";
diff --git a/frontend/src/metabase/api/subscription.ts b/frontend/src/metabase/api/subscription.ts
new file mode 100644
index 00000000000..35f4b48ec73
--- /dev/null
+++ b/frontend/src/metabase/api/subscription.ts
@@ -0,0 +1,86 @@
+import type {
+  ListSubscriptionsRequest,
+  DashboardSubscription,
+  CreateSubscriptionRequest,
+  UpdateSubscriptionRequest,
+} from "metabase-types/api";
+
+import { Api } from "./api";
+import {
+  idTag,
+  invalidateTags,
+  listTag,
+  provideSubscriptionListTags,
+  provideSubscriptionTags,
+} from "./tags";
+
+export const subscriptionApi = Api.injectEndpoints({
+  endpoints: builder => ({
+    listSubscriptions: builder.query<
+      DashboardSubscription[],
+      ListSubscriptionsRequest
+    >({
+      query: params => ({
+        method: "GET",
+        url: "/api/pulse",
+        params,
+      }),
+      providesTags: (subscriptions = []) =>
+        provideSubscriptionListTags(subscriptions),
+    }),
+    getSubscription: builder.query<DashboardSubscription, number>({
+      query: id => ({
+        method: "GET",
+        url: `/api/pulse/${id}`,
+      }),
+      providesTags: subscription =>
+        subscription ? provideSubscriptionTags(subscription) : [],
+    }),
+    createSubscription: builder.mutation<
+      DashboardSubscription,
+      CreateSubscriptionRequest
+    >({
+      query: body => ({
+        method: "POST",
+        url: "/api/pulse",
+        body,
+      }),
+      invalidatesTags: (_, error) =>
+        invalidateTags(error, [listTag("subscription")]),
+    }),
+    updateSubscription: builder.mutation<
+      DashboardSubscription,
+      UpdateSubscriptionRequest
+    >({
+      query: ({ id, ...body }) => ({
+        method: "PUT",
+        url: `/api/pulse/${id}`,
+        body,
+      }),
+      invalidatesTags: (_, error, { id }) =>
+        invalidateTags(error, [
+          listTag("subscription"),
+          idTag("subscription", id),
+        ]),
+    }),
+    unsubscribe: builder.mutation<void, number>({
+      query: id => ({
+        method: "DELETE",
+        url: `/api/pulse/${id}/subscription`,
+      }),
+      invalidatesTags: (_, error, id) =>
+        invalidateTags(error, [
+          listTag("subscription"),
+          idTag("subscription", id),
+        ]),
+    }),
+  }),
+});
+
+export const {
+  useListSubscriptionsQuery,
+  useGetSubscriptionQuery,
+  useCreateSubscriptionMutation,
+  useUpdateSubscriptionMutation,
+  useUnsubscribeMutation,
+} = subscriptionApi;
diff --git a/frontend/src/metabase/api/tags/constants.ts b/frontend/src/metabase/api/tags/constants.ts
index db0db90805f..e558fa6e881 100644
--- a/frontend/src/metabase/api/tags/constants.ts
+++ b/frontend/src/metabase/api/tags/constants.ts
@@ -18,8 +18,9 @@ export const TAG_TYPES = [
   "persisted-model",
   "revision",
   "schema",
-  "snippet",
   "segment",
+  "snippet",
+  "subscription",
   "table",
   "task",
   "timeline",
diff --git a/frontend/src/metabase/api/tags/utils.ts b/frontend/src/metabase/api/tags/utils.ts
index 7679676ea01..18b06a9e09e 100644
--- a/frontend/src/metabase/api/tags/utils.ts
+++ b/frontend/src/metabase/api/tags/utils.ts
@@ -10,6 +10,7 @@ import type {
   CollectionItem,
   CollectionItemModel,
   Dashboard,
+  DashboardSubscription,
   Database,
   DatabaseCandidate,
   Field,
@@ -375,6 +376,21 @@ export function provideSnippetTags(
   return [idTag("snippet", snippet.id)];
 }
 
+export function provideSubscriptionListTags(
+  subscriptions: DashboardSubscription[],
+): TagDescription<TagType>[] {
+  return [
+    listTag("subscription"),
+    ...subscriptions.flatMap(provideSubscriptionTags),
+  ];
+}
+
+export function provideSubscriptionTags(
+  subscription: DashboardSubscription,
+): TagDescription<TagType>[] {
+  return [idTag("subscription", subscription.id)];
+}
+
 export function provideTableListTags(
   tables: Table[],
 ): TagDescription<TagType>[] {
diff --git a/frontend/src/metabase/entities/pulses.js b/frontend/src/metabase/entities/pulses.js
index 978ff30c140..7befcf18c67 100644
--- a/frontend/src/metabase/entities/pulses.js
+++ b/frontend/src/metabase/entities/pulses.js
@@ -1,12 +1,15 @@
 import { t } from "ttag";
 
-import { canonicalCollectionId } from "metabase/collections/utils";
+import { subscriptionApi } from "metabase/api";
 import { getCollectionType } from "metabase/entities/collections";
 import { color } from "metabase/lib/colors";
-import { createEntity, undo } from "metabase/lib/entities";
+import {
+  createEntity,
+  undo,
+  entityCompatibleQuery,
+} from "metabase/lib/entities";
 import * as Urls from "metabase/lib/urls";
 import { addUndo } from "metabase/redux/undo";
-import { PulseApi } from "metabase/services";
 
 export const UNSUBSCRIBE = "metabase/entities/pulses/unsubscribe";
 
@@ -22,6 +25,36 @@ const Pulses = createEntity({
     UNSUBSCRIBE,
   },
 
+  api: {
+    list: (entityQuery, dispatch) =>
+      entityCompatibleQuery(
+        entityQuery,
+        dispatch,
+        subscriptionApi.endpoints.listSubscriptions,
+      ),
+    get: (entityQuery, options, dispatch) =>
+      entityCompatibleQuery(
+        entityQuery.id,
+        dispatch,
+        subscriptionApi.endpoints.getSubscription,
+      ),
+    create: (entityQuery, dispatch) =>
+      entityCompatibleQuery(
+        entityQuery,
+        dispatch,
+        subscriptionApi.endpoints.createSubscription,
+      ),
+    update: (entityQuery, dispatch) =>
+      entityCompatibleQuery(
+        entityQuery,
+        dispatch,
+        subscriptionApi.endpoints.updateSubscription,
+      ),
+    delete: () => {
+      throw new TypeError("Pulses.api.delete is not supported");
+    },
+  },
+
   objectActions: {
     setArchived: ({ id }, archived, opts) => {
       return Pulses.actions.update(
@@ -31,37 +64,14 @@ const Pulses = createEntity({
       );
     },
 
-    setChannels: ({ id }, channels, opts) => {
-      return Pulses.actions.update(
-        { id },
-        { channels },
-        undo(opts, t`subscription`, t`updated`),
-      );
-    },
-
-    setCollection: ({ id }, collection, opts) => {
-      return Pulses.actions.update(
-        { id },
-        { collection_id: canonicalCollectionId(collection && collection.id) },
-        undo(opts, t`subscription`, t`moved`),
-      );
-    },
-
-    setPinned: ({ id }, pinned, opts) => {
-      return Pulses.actions.update(
-        { id },
-        {
-          collection_position:
-            typeof pinned === "number" ? pinned : pinned ? 1 : null,
-        },
-        opts,
-      );
-    },
-
     unsubscribe:
       ({ id }) =>
       async dispatch => {
-        await PulseApi.unsubscribe({ id });
+        await entityCompatibleQuery(
+          id,
+          dispatch,
+          subscriptionApi.endpoints.unsubscribe,
+        );
         dispatch(addUndo({ message: t`Successfully unsubscribed` }));
         dispatch({ type: UNSUBSCRIBE, payload: { id } });
         dispatch({ type: Pulses.actionTypes.INVALIDATE_LISTS_ACTION });
-- 
GitLab