From bd9c63c55db350c2f7e6153b694d1df2a9105ef0 Mon Sep 17 00:00:00 2001 From: Alexander Polyankin <alexander.polyankin@metabase.com> Date: Thu, 11 Apr 2024 15:49:18 +0300 Subject: [PATCH] Migrate alerts to RTK (#41190) * Replace AlertApi * Replace AlertApi * Replace AlertApi * Fix bugs * Fix bugs * Fix typo --------- Co-authored-by: Kamil Mielnik <kamil@kamilmielnik.com> --- frontend/src/metabase-types/api/alert.ts | 48 +++++++++-- .../src/metabase-types/api/mocks/alert.ts | 14 +++- frontend/src/metabase/alert/alert.js | 29 +++++-- frontend/src/metabase/api/alert.ts | 83 +++++++++++++++++++ frontend/src/metabase/api/index.ts | 1 + frontend/src/metabase/api/tags/constants.ts | 1 + frontend/src/metabase/api/tags/utils.ts | 14 ++++ frontend/src/metabase/entities/alerts.js | 44 +++++++++- frontend/src/metabase/lib/request.js | 2 +- frontend/src/metabase/services.js | 9 -- 10 files changed, 215 insertions(+), 30 deletions(-) create mode 100644 frontend/src/metabase/api/alert.ts diff --git a/frontend/src/metabase-types/api/alert.ts b/frontend/src/metabase-types/api/alert.ts index 108d3a704e4..9e100bb2dcd 100644 --- a/frontend/src/metabase-types/api/alert.ts +++ b/frontend/src/metabase-types/api/alert.ts @@ -1,20 +1,23 @@ -import type { Card } from "./card"; +import type { CardId } from "./card"; import type { CollectionId } from "./collection"; -import type { DashboardId } from "./dashboard"; +import type { DashboardId, DashCardId } from "./dashboard"; import type { Channel } from "./notifications"; import type { Parameter } from "./parameters"; import type { UserId, UserInfo } from "./user"; +export type AlertId = number; +export type AlertCondition = "goal" | "rows"; + export interface Alert { - id: number; + id: AlertId; name: string | null; alert_above_goal: boolean | null; - alert_condition: "goal" | "rows"; + alert_condition: AlertCondition; alert_first_only: boolean; skip_if_empty: boolean; - card: Card; + card: AlertCard; parameters: Parameter[]; channels: Channel[]; @@ -33,3 +36,38 @@ export interface Alert { created_at: string; updated_at: string; } + +export interface AlertCard { + id: CardId; + include_csv: boolean; + include_xls: boolean; + format_rows?: boolean; + dashboard_card_id?: DashCardId; +} + +export interface ListAlertsRequest { + user_id?: UserId; + archived?: boolean; +} + +export interface ListCardAlertsRequest { + id: CardId; + archived?: boolean; +} + +export interface CreateAlertRequest { + card: AlertCard; + alert_condition: AlertCondition; + alert_first_only: boolean; + alert_above_goal: boolean; + channels: Channel[]; +} + +export interface UpdateAlertRequest { + id: AlertId; + card?: AlertCard; + alert_condition?: AlertCondition; + alert_first_only?: boolean; + alert_above_goal?: boolean; + channels?: Channel[]; +} diff --git a/frontend/src/metabase-types/api/mocks/alert.ts b/frontend/src/metabase-types/api/mocks/alert.ts index 7e0ad1a2f7e..0ffb13d6c5a 100644 --- a/frontend/src/metabase-types/api/mocks/alert.ts +++ b/frontend/src/metabase-types/api/mocks/alert.ts @@ -1,6 +1,5 @@ -import type { Alert } from "../alert"; +import type { Alert, AlertCard } from "../alert"; -import { createMockCard } from "./card"; import { createMockUserInfo } from "./user"; export const createMockAlert = (opts?: Partial<Alert>): Alert => ({ @@ -11,7 +10,7 @@ export const createMockAlert = (opts?: Partial<Alert>): Alert => ({ alert_first_only: false, skip_if_empty: false, - card: createMockCard(), + card: createMockAlertCard(), parameters: [], channels: [], @@ -32,3 +31,12 @@ export const createMockAlert = (opts?: Partial<Alert>): Alert => ({ ...opts, }); + +export function createMockAlertCard(opts?: Partial<AlertCard>): AlertCard { + return { + id: 1, + include_csv: false, + include_xls: false, + ...opts, + }; +} diff --git a/frontend/src/metabase/alert/alert.js b/frontend/src/metabase/alert/alert.js index d8f0e217b44..30d3a3b53c3 100644 --- a/frontend/src/metabase/alert/alert.js +++ b/frontend/src/metabase/alert/alert.js @@ -4,15 +4,17 @@ import { handleActions } from "redux-actions"; import { t } from "ttag"; import _ from "underscore"; +import { alertApi } from "metabase/api"; import CS from "metabase/css/core/index.css"; +import { entityCompatibleQuery } from "metabase/lib/entities"; import { RestfulRequest } from "metabase/lib/request"; import { addUndo } from "metabase/redux/undo"; -import { AlertApi } from "metabase/services"; import { Icon } from "metabase/ui"; export const FETCH_ALL_ALERTS = "metabase/alerts/FETCH_ALL_ALERTS"; const fetchAllAlertsRequest = new RestfulRequest({ - endpoint: AlertApi.list, + endpoint: (params, dispatch) => + entityCompatibleQuery(params, dispatch, alertApi.endpoints.listAlerts), actionPrefix: FETCH_ALL_ALERTS, storeAsDictionary: true, }); @@ -28,7 +30,8 @@ export const FETCH_ALERTS_FOR_QUESTION_CLEAR_OLD_ALERTS = export const FETCH_ALERTS_FOR_QUESTION = "metabase/alerts/FETCH_ALERTS_FOR_QUESTION"; const fetchAlertsForQuestionRequest = new RestfulRequest({ - endpoint: AlertApi.list_for_question, + endpoint: (params, dispatch) => + entityCompatibleQuery(params, dispatch, alertApi.endpoints.listCardAlerts), actionPrefix: FETCH_ALERTS_FOR_QUESTION, storeAsDictionary: true, }); @@ -38,14 +41,15 @@ export const fetchAlertsForQuestion = questionId => { payload: questionId, type: FETCH_ALERTS_FOR_QUESTION_CLEAR_OLD_ALERTS, }); - await dispatch(fetchAlertsForQuestionRequest.trigger({ questionId })); + await dispatch(fetchAlertsForQuestionRequest.trigger({ id: questionId })); dispatch({ type: FETCH_ALERTS_FOR_QUESTION }); }; }; export const CREATE_ALERT = "metabase/alerts/CREATE_ALERT"; const createAlertRequest = new RestfulRequest({ - endpoint: AlertApi.create, + endpoint: (params, dispatch) => + entityCompatibleQuery(params, dispatch, alertApi.endpoints.createAlert), actionPrefix: CREATE_ALERT, storeAsDictionary: true, }); @@ -92,7 +96,8 @@ function cleanAlert(alert) { export const UPDATE_ALERT = "metabase/alerts/UPDATE_ALERT"; const updateAlertRequest = new RestfulRequest({ - endpoint: AlertApi.update, + endpoint: (params, dispatch) => + entityCompatibleQuery(params, dispatch, alertApi.endpoints.updateAlert), actionPrefix: UPDATE_ALERT, storeAsDictionary: true, }); @@ -123,13 +128,18 @@ export const UNSUBSCRIBE_FROM_ALERT = "metabase/alerts/UNSUBSCRIBE_FROM_ALERT"; export const UNSUBSCRIBE_FROM_ALERT_CLEANUP = "metabase/alerts/UNSUBSCRIBE_FROM_ALERT_CLEANUP"; const unsubscribeFromAlertRequest = new RestfulRequest({ - endpoint: AlertApi.unsubscribe, + endpoint: (params, dispatch) => + entityCompatibleQuery( + params, + dispatch, + alertApi.endpoints.deleteAlertSubscription, + ), actionPrefix: UNSUBSCRIBE_FROM_ALERT, storeAsDictionary: true, }); export const unsubscribeFromAlert = alert => { return async (dispatch, getState) => { - await dispatch(unsubscribeFromAlertRequest.trigger(alert)); + await dispatch(unsubscribeFromAlertRequest.trigger(alert.id)); dispatch({ type: UNSUBSCRIBE_FROM_ALERT }); // This delay lets us to show "You're unsubscribed" text in place of an @@ -144,7 +154,8 @@ export const unsubscribeFromAlert = alert => { export const DELETE_ALERT = "metabase/alerts/DELETE_ALERT"; const deleteAlertRequest = new RestfulRequest({ - endpoint: AlertApi.update, + endpoint: (params, dispatch) => + entityCompatibleQuery(params, dispatch, alertApi.endpoints.updateAlert), actionPrefix: DELETE_ALERT, storeAsDictionary: true, }); diff --git a/frontend/src/metabase/api/alert.ts b/frontend/src/metabase/api/alert.ts new file mode 100644 index 00000000000..104bfc0514e --- /dev/null +++ b/frontend/src/metabase/api/alert.ts @@ -0,0 +1,83 @@ +import type { + Alert, + AlertId, + CreateAlertRequest, + ListAlertsRequest, + ListCardAlertsRequest, + UpdateAlertRequest, +} from "metabase-types/api"; + +import { Api } from "./api"; +import { + idTag, + invalidateTags, + listTag, + provideAlertListTags, + provideAlertTags, +} from "./tags"; + +export const alertApi = Api.injectEndpoints({ + endpoints: builder => ({ + listAlerts: builder.query<Alert[], ListAlertsRequest | void>({ + query: body => ({ + method: "GET", + url: "/api/alert", + body, + }), + providesTags: (alerts = []) => provideAlertListTags(alerts), + }), + listCardAlerts: builder.query<Alert[], ListCardAlertsRequest>({ + query: ({ id, ...body }) => ({ + method: "GET", + url: `/api/alert/question/${id}`, + body, + }), + providesTags: (alerts = []) => provideAlertListTags(alerts), + }), + getAlert: builder.query<Alert, AlertId>({ + query: id => ({ + method: "GET", + url: `/api/alert/${id}`, + }), + providesTags: alert => (alert ? provideAlertTags(alert) : []), + }), + createAlert: builder.mutation<Alert, CreateAlertRequest>({ + query: body => ({ + method: "POST", + url: "/api/alert", + body, + }), + invalidatesTags: (alert, error) => + invalidateTags(error, [listTag("alert")]), + }), + updateAlert: builder.mutation<Alert, UpdateAlertRequest>({ + query: ({ id, ...body }) => ({ + method: "PUT", + url: `/api/alert/${id}`, + body, + }), + invalidatesTags: (alert, error) => + invalidateTags(error, [ + listTag("alert"), + ...(alert ? [idTag("alert", alert.id)] : []), + ]), + }), + deleteAlertSubscription: builder.mutation<void, AlertId>({ + query: id => ({ + method: "DELETE", + url: `/api/alert/${id}/subscription`, + }), + invalidatesTags: (_, error, id) => + invalidateTags(error, [listTag("alert"), idTag("alert", id)]), + }), + }), +}); + +export const { + useListAlertsQuery, + useListCardAlertsQuery, + useGetAlertQuery, + useCreateAlertMutation, + useUpdateAlertMutation, + useDeleteAlertSubscriptionMutation, +} = alertApi; diff --git a/frontend/src/metabase/api/index.ts b/frontend/src/metabase/api/index.ts index 6fc6f187011..d92d7695886 100644 --- a/frontend/src/metabase/api/index.ts +++ b/frontend/src/metabase/api/index.ts @@ -1,4 +1,5 @@ export * from "./activity"; +export * from "./alert"; export * from "./api"; export * from "./api-key"; export * from "./automagic-dashboards"; diff --git a/frontend/src/metabase/api/tags/constants.ts b/frontend/src/metabase/api/tags/constants.ts index 3b7809c979c..5eaf8c7d9a2 100644 --- a/frontend/src/metabase/api/tags/constants.ts +++ b/frontend/src/metabase/api/tags/constants.ts @@ -2,6 +2,7 @@ export type TagType = typeof TAG_TYPES[number]; export const TAG_TYPES = [ "action", + "alert", "api-key", "bookmark", "card", diff --git a/frontend/src/metabase/api/tags/utils.ts b/frontend/src/metabase/api/tags/utils.ts index 65ca033cadb..5257e44ffe4 100644 --- a/frontend/src/metabase/api/tags/utils.ts +++ b/frontend/src/metabase/api/tags/utils.ts @@ -1,6 +1,7 @@ import type { TagDescription } from "@reduxjs/toolkit/query"; import type { + Alert, ApiKey, Bookmark, Card, @@ -70,6 +71,19 @@ export function provideActivityItemTags( return [idTag(TAG_TYPE_MAPPING[item.model], item.model_id)]; } +export function provideAlertListTags( + alerts: Alert[], +): TagDescription<TagType>[] { + return [listTag("alert"), ...alerts.flatMap(provideAlertTags)]; +} + +export function provideAlertTags(alert: Alert): TagDescription<TagType>[] { + return [ + idTag("alert", alert.id), + ...(alert.creator ? provideUserTags(alert.creator) : []), + ]; +} + export function provideApiKeyListTags( apiKeys: ApiKey[], ): TagDescription<TagType>[] { diff --git a/frontend/src/metabase/entities/alerts.js b/frontend/src/metabase/entities/alerts.js index 379f301458f..084b9e51aee 100644 --- a/frontend/src/metabase/entities/alerts.js +++ b/frontend/src/metabase/entities/alerts.js @@ -1,8 +1,12 @@ import { t } from "ttag"; -import { createEntity, undo } from "metabase/lib/entities"; +import { alertApi } from "metabase/api"; +import { + createEntity, + entityCompatibleQuery, + undo, +} from "metabase/lib/entities"; import { addUndo } from "metabase/redux/undo"; -import { AlertApi } from "metabase/services"; export const UNSUBSCRIBE = "metabase/entities/alerts/unsubscribe"; @@ -14,6 +18,36 @@ const Alerts = createEntity({ nameOne: "alert", path: "/api/alert", + api: { + list: (entityQuery, dispatch) => + entityCompatibleQuery( + entityQuery, + dispatch, + alertApi.endpoints.listAlerts, + ), + get: (entityQuery, options, dispatch) => + entityCompatibleQuery( + entityQuery.id, + dispatch, + alertApi.endpoints.listAlerts, + ), + create: (entityQuery, dispatch) => + entityCompatibleQuery( + entityQuery, + dispatch, + alertApi.endpoints.createAlert, + ), + update: (entityQuery, dispatch) => + entityCompatibleQuery( + entityQuery, + dispatch, + alertApi.endpoints.updateAlert, + ), + delete: () => { + throw new TypeError("Alerts.api.delete is not supported"); + }, + }, + actionTypes: { UNSUBSCRIBE, }, @@ -30,7 +64,11 @@ const Alerts = createEntity({ unsubscribe: ({ id }) => async dispatch => { - await AlertApi.unsubscribe({ id }); + await entityCompatibleQuery( + id, + dispatch, + alertApi.endpoints.deleteAlertSubscription, + ); dispatch(addUndo({ message: t`Successfully unsubscribed` })); dispatch({ type: UNSUBSCRIBE, payload: { id } }); dispatch({ type: Alerts.actionTypes.INVALIDATE_LISTS_ACTION }); diff --git a/frontend/src/metabase/lib/request.js b/frontend/src/metabase/lib/request.js index cca68f7d49a..22fd1505c44 100644 --- a/frontend/src/metabase/lib/request.js +++ b/frontend/src/metabase/lib/request.js @@ -40,7 +40,7 @@ export class RestfulRequest { trigger = params => async dispatch => { dispatch({ type: this.actions.requestStarted }); try { - const result = await this.endpoint(params); + const result = await this.endpoint(params, dispatch); dispatch({ type: this.actions.requestSuccessful, payload: { result } }); } catch (error) { dispatch({ type: this.actions.requestFailed, payload: { error } }); diff --git a/frontend/src/metabase/services.js b/frontend/src/metabase/services.js index 6a125d9e94d..8037f6c9598 100644 --- a/frontend/src/metabase/services.js +++ b/frontend/src/metabase/services.js @@ -367,15 +367,6 @@ export const PulseApi = { unsubscribe: DELETE("/api/pulse/:id/subscription"), }; -export const AlertApi = { - list: GET("/api/alert"), - list_for_question: GET("/api/alert/question/:questionId"), - get: GET("/api/alert/:id"), - create: POST("/api/alert"), - update: PUT("/api/alert/:id"), - unsubscribe: DELETE("/api/alert/:id/subscription"), -}; - export const SegmentApi = { list: GET("/api/segment"), create: POST("/api/segment"), -- GitLab