From aecf1c38a6044e75f0bd67b523e6b3d9c5ea3221 Mon Sep 17 00:00:00 2001 From: Simon Larsen Date: Fri, 17 Jan 2025 19:47:02 +0000 Subject: [PATCH] feat: rename and add routes for Incident and Alert On-Call Rules in user settings --- .../Pages/UserSettings/AlertOnCallRules.tsx | 383 ++++++++++++++++++ ...nCallRules.tsx => IncidentOnCallRules.tsx} | 0 Dashboard/src/Pages/UserSettings/SideMenu.tsx | 21 +- Dashboard/src/Routes/UserSettingsRoutes.tsx | 43 +- .../Breadcrumbs/UserSettingsBreadcrumbs.ts | 11 +- Dashboard/src/Utils/PageMap.ts | 3 +- Dashboard/src/Utils/RouteMap.ts | 13 +- 7 files changed, 455 insertions(+), 19 deletions(-) create mode 100644 Dashboard/src/Pages/UserSettings/AlertOnCallRules.tsx rename Dashboard/src/Pages/UserSettings/{OnCallRules.tsx => IncidentOnCallRules.tsx} (100%) diff --git a/Dashboard/src/Pages/UserSettings/AlertOnCallRules.tsx b/Dashboard/src/Pages/UserSettings/AlertOnCallRules.tsx new file mode 100644 index 00000000000..f80704e2d36 --- /dev/null +++ b/Dashboard/src/Pages/UserSettings/AlertOnCallRules.tsx @@ -0,0 +1,383 @@ +import NotificationMethodView from "../../Components/NotificationMethods/NotificationMethod"; +import NotifyAfterDropdownOptions from "../../Components/NotificationRule/NotifyAfterMinutesDropdownOptions"; +import DashboardNavigation from "../../Utils/Navigation"; +import PageComponentProps from "../PageComponentProps"; +import BaseModel from "Common/Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel"; +import SortOrder from "Common/Types/BaseDatabase/SortOrder"; +import { LIMIT_PER_PROJECT } from "Common/Types/Database/LimitMax"; +import { PromiseVoidFunction } from "Common/Types/FunctionTypes"; +import { JSONObject } from "Common/Types/JSON"; +import NotificationRuleType from "Common/Types/NotificationRule/NotificationRuleType"; +import { DropdownOption } from "Common/UI/Components/Dropdown/Dropdown"; +import ErrorMessage from "Common/UI/Components/ErrorMessage/ErrorMessage"; +import FormFieldSchemaType from "Common/UI/Components/Forms/Types/FormFieldSchemaType"; +import PageLoader from "Common/UI/Components/Loader/PageLoader"; +import ModelTable from "Common/UI/Components/ModelTable/ModelTable"; +import FieldType from "Common/UI/Components/Types/FieldType"; +import API from "Common/UI/Utils/API/API"; +import ModelAPI, { ListResult } from "Common/UI/Utils/ModelAPI/ModelAPI"; +import User from "Common/UI/Utils/User"; +import AlertSeverity from "Common/Models/DatabaseModels/AlertSeverity"; +import UserCall from "Common/Models/DatabaseModels/UserCall"; +import UserEmail from "Common/Models/DatabaseModels/UserEmail"; +import UserNotificationRule from "Common/Models/DatabaseModels/UserNotificationRule"; +import UserSMS from "Common/Models/DatabaseModels/UserSMS"; +import React, { + Fragment, + FunctionComponent, + ReactElement, + useEffect, + useState, +} from "react"; + +const Settings: FunctionComponent = (): ReactElement => { + const [error, setError] = useState(""); + const [isLoading, setIsLoading] = useState(true); + const [alertSeverities, setAlertSeverities] = useState< + Array + >([]); + const [userEmails, setUserEmails] = useState>([]); + const [userSMSs, setUserSMSs] = useState>([]); + const [userCalls, setUserCalls] = useState>([]); + const [ + notificationMethodsDropdownOptions, + setNotificationMethodsDropdownOptions, + ] = useState>([]); + + type GetTableFunctionProps = { + alertSeverity?: AlertSeverity; + ruleType: NotificationRuleType; + title: string; + description: string; + }; + + type GetTableFunction = (props: GetTableFunctionProps) => ReactElement; + + const getModelTable: GetTableFunction = ( + options: GetTableFunctionProps, + ): ReactElement => { + return ( + + modelType={UserNotificationRule} + query={{ + projectId: DashboardNavigation.getProjectId()!, + userId: User.getUserId()!, + ruleType: options.ruleType, + alertSeverityId: options.alertSeverity?.id || undefined, + }} + onBeforeCreate={( + model: UserNotificationRule, + miscDataProps: JSONObject, + ): Promise => { + model.projectId = DashboardNavigation.getProjectId()!; + model.userId = User.getUserId(); + model.ruleType = options.ruleType; + if (options.alertSeverity?.id) { + model.alertSeverityId = options.alertSeverity?.id; + } + + if (miscDataProps["notificationMethod"]) { + const userEmail: UserEmail | undefined = userEmails.find( + (userEmail: UserEmail) => { + return ( + userEmail.id!.toString() === + miscDataProps["notificationMethod"]?.toString() + ); + }, + ); + + if (userEmail) { + model.userEmailId = userEmail.id!; + } + + const userSMS: UserSMS | undefined = userSMSs.find( + (userSMS: UserSMS) => { + return ( + userSMS.id!.toString() === + miscDataProps["notificationMethod"]?.toString() + ); + }, + ); + + if (userSMS) { + model.userSmsId = userSMS.id!; + } + + const userCall: UserCall | undefined = userCalls.find( + (userCall: UserCall) => { + return ( + userCall.id!.toString() === + miscDataProps["notificationMethod"]?.toString() + ); + }, + ); + + if (userCall) { + model.userCallId = userCall.id!; + } + } + + return Promise.resolve(model); + }} + sortOrder={SortOrder.Ascending} + sortBy="notifyAfterMinutes" + createVerb={"Add"} + id="notification-rules" + name={`User Settings > Notification Rules > ${ + options.alertSeverity?.name || options.ruleType + }`} + isDeleteable={true} + isEditable={false} + isCreateable={true} + cardProps={{ + title: options.title, + description: options.description, + }} + noItemsMessage={ + "No notification rules found for this user. Please add one to receive notifications." + } + formFields={[ + { + overrideField: { + notificationMethod: true, + }, + showEvenIfPermissionDoesNotExist: true, + overrideFieldKey: "notificationMethod", + title: "Notification Method", + description: "How do you want to be notified?", + fieldType: FormFieldSchemaType.Dropdown, + required: true, + placeholder: "Notification Method", + dropdownOptions: notificationMethodsDropdownOptions, + }, + { + field: { + notifyAfterMinutes: true, + }, + title: "Notify me after", + fieldType: FormFieldSchemaType.Dropdown, + required: true, + placeholder: "Immediately", + dropdownOptions: NotifyAfterDropdownOptions, + }, + ]} + showRefreshButton={true} + selectMoreFields={{ + userEmail: { + email: true, + }, + userSms: { + phone: true, + }, + }} + filters={[]} + columns={[ + { + field: { + userCall: { + phone: true, + }, + }, + title: "Notification Method", + type: FieldType.Text, + getElement: (item: UserNotificationRule): ReactElement => { + return ( + + ); + }, + }, + { + field: { + notifyAfterMinutes: true, + }, + title: "Notify After", + type: FieldType.Text, + getElement: (item: UserNotificationRule): ReactElement => { + return ( +
+ {item["notifyAfterMinutes"] === 0 &&

Immediately

} + {(item["notifyAfterMinutes"] as number) > 0 && ( +

{item["notifyAfterMinutes"] as number} minutes

+ )} +
+ ); + }, + }, + ]} + /> + ); + }; + + const init: PromiseVoidFunction = async (): Promise => { + // Ping an API here. + setError(""); + setIsLoading(true); + + try { + const alertSeverities: ListResult = + await ModelAPI.getList({ + modelType: AlertSeverity, + query: { + projectId: DashboardNavigation.getProjectId()!, + }, + limit: LIMIT_PER_PROJECT, + skip: 0, + select: { + name: true, + }, + sort: {}, + }); + + const userEmails: ListResult = await ModelAPI.getList({ + modelType: UserEmail, + query: { + projectId: DashboardNavigation.getProjectId()!, + userId: User.getUserId(), + isVerified: true, + }, + limit: LIMIT_PER_PROJECT, + skip: 0, + select: { + email: true, + }, + sort: {}, + }); + + setUserEmails(userEmails.data); + + const userSMSes: ListResult = await ModelAPI.getList({ + modelType: UserSMS, + query: { + projectId: DashboardNavigation.getProjectId()!, + userId: User.getUserId(), + isVerified: true, + }, + limit: LIMIT_PER_PROJECT, + skip: 0, + select: { + phone: true, + }, + sort: {}, + }); + + setUserSMSs(userSMSes.data); + + const userCalls: ListResult = await ModelAPI.getList({ + modelType: UserCall, + query: { + projectId: DashboardNavigation.getProjectId()!, + userId: User.getUserId(), + isVerified: true, + }, + limit: LIMIT_PER_PROJECT, + skip: 0, + select: { + phone: true, + }, + sort: {}, + }); + + setUserCalls(userCalls.data); + + setAlertSeverities(alertSeverities.data); + + const dropdownOptions: Array = [ + ...userCalls.data, + ...userEmails.data, + ...userSMSes.data, + ].map((model: BaseModel) => { + const isUserCall: boolean = model instanceof UserCall; + const isUserSms: boolean = model instanceof UserSMS; + + const option: DropdownOption = { + label: model.getColumnValue("phone") + ? (model.getColumnValue("phone")?.toString() as string) + : (model.getColumnValue("email")?.toString() as string), + value: model.id!.toString(), + }; + + if (isUserCall) { + option.label = "Call: " + option.label; + } else if (isUserSms) { + option.label = "SMS: " + option.label; + } else { + option.label = "Email: " + option.label; + } + + return option; + }); + + setNotificationMethodsDropdownOptions(dropdownOptions); + } catch (err) { + setError(API.getFriendlyMessage(err)); + } + + setIsLoading(false); + }; + + useEffect(() => { + init().catch((err: Error) => { + setError(err.toString()); + }); + }, []); + + if (isLoading) { + return ; + } + + if (error) { + return ; + } + + return ( + +
+ {alertSeverities.map( + (alertSeverity: AlertSeverity, i: number) => { + return ( +
+ {getModelTable({ + alertSeverity: alertSeverity, + ruleType: NotificationRuleType.ON_CALL_INCIDENT_CREATED, + title: + "When I am on call and " + + alertSeverity.name + + " is assigned to me...", + description: + "Here are the rules when you are on call and " + + alertSeverity.name + + " is assigned to you.", + })} +
+ ); + }, + )} +
+ + {/*
+ {getModelTable({ + alertSeverity: undefined, + ruleType: NotificationRuleType.WHEN_USER_GOES_ON_CALL, + title: 'When I go on call...', + description: + 'Here are the rules to notify you when you go on call.', + })} +
+ +
+ {getModelTable({ + alertSeverity: undefined, + ruleType: NotificationRuleType.WHEN_USER_GOES_OFF_CALL, + title: 'When I go off call...', + description: + 'Here are the rules to notify you when you go off call.', + })} +
*/} +
+ ); +}; + +export default Settings; diff --git a/Dashboard/src/Pages/UserSettings/OnCallRules.tsx b/Dashboard/src/Pages/UserSettings/IncidentOnCallRules.tsx similarity index 100% rename from Dashboard/src/Pages/UserSettings/OnCallRules.tsx rename to Dashboard/src/Pages/UserSettings/IncidentOnCallRules.tsx diff --git a/Dashboard/src/Pages/UserSettings/SideMenu.tsx b/Dashboard/src/Pages/UserSettings/SideMenu.tsx index cf025964dcc..773918019da 100644 --- a/Dashboard/src/Pages/UserSettings/SideMenu.tsx +++ b/Dashboard/src/Pages/UserSettings/SideMenu.tsx @@ -44,17 +44,28 @@ const DashboardSideMenu: () => ReactElement = (): ReactElement => { }} icon={IconProp.Settings} /> + + - - + + + = lazy(() => { return import("../Pages/UserSettings/NotificationMethods"); }); -const UserSettingsNotificationRules: LazyExoticComponent< +const UserSettingsIncidentNotificationRules: LazyExoticComponent< FunctionComponent > = lazy(() => { - return import("../Pages/UserSettings/OnCallRules"); + return import("../Pages/UserSettings/IncidentOnCallRules"); }); + + +const UserSettingsAlertNotificationRules: LazyExoticComponent< + + FunctionComponent +> = lazy(() => { + return import("../Pages/UserSettings/AlertOnCallRules"); +}); + const UserSettingsNotificationLogs: LazyExoticComponent< FunctionComponent > = lazy(() => { @@ -74,7 +83,7 @@ const UserSettingsRoutes: FunctionComponent = ( = ( = ( + + + } + /> + + + - } /> + + + ); diff --git a/Dashboard/src/Utils/Breadcrumbs/UserSettingsBreadcrumbs.ts b/Dashboard/src/Utils/Breadcrumbs/UserSettingsBreadcrumbs.ts index 3a4356cb262..c2ff3eb9ee2 100644 --- a/Dashboard/src/Utils/Breadcrumbs/UserSettingsBreadcrumbs.ts +++ b/Dashboard/src/Utils/Breadcrumbs/UserSettingsBreadcrumbs.ts @@ -15,15 +15,20 @@ export function getUserSettingsBreadcrumbs( PageMap.USER_SETTINGS_NOTIFICATION_SETTINGS, ["Project", "User Settings", "Notification Settings"], ), - ...BuildBreadcrumbLinksByTitles(PageMap.USER_SETTINGS_ON_CALL_RULES, [ + ...BuildBreadcrumbLinksByTitles(PageMap.USER_SETTINGS_INCIDENT_ON_CALL_RULES, [ "Project", "User Settings", - "Notification Rules", + "Incident On-Call Rules", + ]), + ...BuildBreadcrumbLinksByTitles(PageMap.USER_SETTINGS_ALERT_ON_CALL_RULES, [ + "Project", + "User Settings", + "Alert On-Call Rules", ]), ...BuildBreadcrumbLinksByTitles(PageMap.USER_SETTINGS_ON_CALL_LOGS, [ "Project", "User Settings", - "Notification Logs", + "On-Call Logs", ]), }; return breadcrumpLinksMap[path]; diff --git a/Dashboard/src/Utils/PageMap.ts b/Dashboard/src/Utils/PageMap.ts index 4e7b04a479e..bcbc28fcb98 100644 --- a/Dashboard/src/Utils/PageMap.ts +++ b/Dashboard/src/Utils/PageMap.ts @@ -218,7 +218,8 @@ enum PageMap { USER_SETTINGS_ROOT = "USER_SETTINGS_ROOT", USER_SETTINGS = "USER_SETTINGS", USER_SETTINGS_NOTIFICATION_METHODS = "USER_SETTINGS_NOTIFICATION_METHODS", - USER_SETTINGS_ON_CALL_RULES = "USER_SETTINGS_ON_CALL_RULES", + USER_SETTINGS_INCIDENT_ON_CALL_RULES = "USER_SETTINGS_INCIDENT_ON_CALL_RULES", + USER_SETTINGS_ALERT_ON_CALL_RULES = "USER_SETTINGS_ALERT_ON_CALL_RULES", USER_SETTINGS_ON_CALL_LOGS = "USER_SETTINGS_ON_CALL_LOGS", USER_SETTINGS_ON_CALL_LOGS_TIMELINE = "USER_SETTINGS_ON_CALL_LOGS_TIMELINE", USER_SETTINGS_NOTIFICATION_SETTINGS = "USER_SETTINGS_NOTIFICATION_SETTINGS", diff --git a/Dashboard/src/Utils/RouteMap.ts b/Dashboard/src/Utils/RouteMap.ts index 7297bb0a666..8e0f2f054d9 100644 --- a/Dashboard/src/Utils/RouteMap.ts +++ b/Dashboard/src/Utils/RouteMap.ts @@ -257,7 +257,8 @@ export const UserSettingsRoutePath: Dictionary = { [PageMap.USER_SETTINGS]: "notification-methods", [PageMap.USER_SETTINGS_NOTIFICATION_SETTINGS]: "notification-settings", [PageMap.USER_SETTINGS_NOTIFICATION_METHODS]: "notification-methods", - [PageMap.USER_SETTINGS_ON_CALL_RULES]: "on-call-rules", + [PageMap.USER_SETTINGS_INCIDENT_ON_CALL_RULES]: "incident-on-call-rules", + [PageMap.USER_SETTINGS_ALERT_ON_CALL_RULES]: "alert-on-call-rules", [PageMap.USER_SETTINGS_ON_CALL_LOGS]: "on-call-logs", [PageMap.USER_SETTINGS_ON_CALL_LOGS_TIMELINE]: `on-call-logs/${RouteParams.ModelID}`, }; @@ -1241,9 +1242,15 @@ const RouteMap: Dictionary = { }`, ), - [PageMap.USER_SETTINGS_ON_CALL_RULES]: new Route( + [PageMap.USER_SETTINGS_INCIDENT_ON_CALL_RULES]: new Route( `/dashboard/${RouteParams.ProjectID}/user-settings/${ - UserSettingsRoutePath[PageMap.USER_SETTINGS_ON_CALL_RULES] + UserSettingsRoutePath[PageMap.USER_SETTINGS_INCIDENT_ON_CALL_RULES] + }`, + ), + + [PageMap.USER_SETTINGS_ALERT_ON_CALL_RULES]: new Route( + `/dashboard/${RouteParams.ProjectID}/user-settings/${ + UserSettingsRoutePath[PageMap.USER_SETTINGS_ALERT_ON_CALL_RULES] }`, ),