mirror of
https://github.com/OneUptime/oneuptime.git
synced 2026-04-06 00:32:12 +02:00
28
.vscode/launch.json
vendored
28
.vscode/launch.json
vendored
@@ -41,6 +41,34 @@
|
||||
"restart": true,
|
||||
"autoAttachChildProcesses": true
|
||||
},
|
||||
{
|
||||
"address": "127.0.0.1",
|
||||
"localRoot": "${workspaceFolder}/ApiReference",
|
||||
"name": "API Reference: Debug with Docker",
|
||||
"port": 9178,
|
||||
"remoteRoot": "/usr/src/app",
|
||||
"request": "attach",
|
||||
"skipFiles": [
|
||||
"<node_internals>/**"
|
||||
],
|
||||
"type": "node",
|
||||
"restart": true,
|
||||
"autoAttachChildProcesses": true
|
||||
},
|
||||
{
|
||||
"address": "127.0.0.1",
|
||||
"localRoot": "${workspaceFolder}/LinkShortner",
|
||||
"name": "Link Shortner: Debug with Docker",
|
||||
"port": 9826,
|
||||
"remoteRoot": "/usr/src/app",
|
||||
"request": "attach",
|
||||
"skipFiles": [
|
||||
"<node_internals>/**"
|
||||
],
|
||||
"type": "node",
|
||||
"restart": true,
|
||||
"autoAttachChildProcesses": true
|
||||
},
|
||||
{
|
||||
"address": "127.0.0.1",
|
||||
"localRoot": "${workspaceFolder}/TestServer",
|
||||
|
||||
@@ -65,7 +65,7 @@ const ForgotPassword: FunctionComponent = () => {
|
||||
formType={FormType.Create}
|
||||
maxPrimaryButtonWidth={true}
|
||||
footer={
|
||||
<div className="actions pointer text-center mt-4 underline-on-hover fw-semibold">
|
||||
<div className="actions pointer text-center mt-4 hover:underline fw-semibold">
|
||||
<p>
|
||||
<Link
|
||||
to={new Route('/accounts/login')}
|
||||
|
||||
@@ -79,7 +79,7 @@ const LoginPage: FunctionComponent = () => {
|
||||
}}
|
||||
maxPrimaryButtonWidth={true}
|
||||
footer={
|
||||
<div className="actions pointer text-center mt-4 underline-on-hover fw-semibold">
|
||||
<div className="actions pointer text-center mt-4 hover:underline fw-semibold">
|
||||
<p>
|
||||
{!showSsoTip && (
|
||||
<div
|
||||
|
||||
@@ -109,7 +109,7 @@ const VerifyEmail: FunctionComponent = () => {
|
||||
'/accounts/login'
|
||||
)
|
||||
}
|
||||
className="underline-on-hover text-primary fw-semibold"
|
||||
className="hover:underline text-primary fw-semibold"
|
||||
>
|
||||
Login.
|
||||
</Link>
|
||||
|
||||
@@ -1,11 +1,27 @@
|
||||
import URL from '../API/URL';
|
||||
import Phone from '../Phone';
|
||||
|
||||
export interface Say {
|
||||
sayMessage: string;
|
||||
}
|
||||
|
||||
export enum CallAction {
|
||||
Hangup = 'Hangup',
|
||||
export interface OnCallInputRequest {
|
||||
[x: string]: Say; // input.
|
||||
default: Say; // what if there is no input or invalid input.
|
||||
}
|
||||
|
||||
export default interface CallRequest {
|
||||
data: Array<Say | CallAction>;
|
||||
export interface GatherInput {
|
||||
introMessage: string;
|
||||
numDigits: number;
|
||||
timeoutInSeconds: number;
|
||||
noInputMessage: string;
|
||||
onInputCallRequest: OnCallInputRequest;
|
||||
responseUrl: URL;
|
||||
}
|
||||
|
||||
export enum CallAction {}
|
||||
|
||||
export default interface CallRequest {
|
||||
to: Phone;
|
||||
data: Array<Say | CallAction | GatherInput>;
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import InBetween from './Database/InBetween';
|
||||
import BadDataException from './Exception/BadDataException';
|
||||
import { JSONObject, ObjectType } from './JSON';
|
||||
import PositiveNumber from './PositiveNumber';
|
||||
@@ -379,6 +380,18 @@ export default class OneUptimeDate {
|
||||
);
|
||||
}
|
||||
|
||||
public static getDifferenceInMinutes(date: Date, date2: Date): number {
|
||||
date = this.fromString(date);
|
||||
date2 = this.fromString(date2);
|
||||
const minutes: number = moment(date).diff(moment(date2), 'minutes');
|
||||
|
||||
if (minutes < 0) {
|
||||
return minutes * -1;
|
||||
}
|
||||
|
||||
return minutes;
|
||||
}
|
||||
|
||||
public static getDateAsFormattedArrayInMultipleTimezones(
|
||||
date: string | Date,
|
||||
onlyShowDate?: boolean
|
||||
@@ -464,6 +477,10 @@ export default class OneUptimeDate {
|
||||
);
|
||||
}
|
||||
|
||||
public static getDayInSeconds(): number {
|
||||
return 24 * 60 * 60;
|
||||
}
|
||||
|
||||
public static getCurrentTimezoneString(): string {
|
||||
return moment.tz(moment.tz.guess()).zoneAbbr();
|
||||
}
|
||||
@@ -510,4 +527,13 @@ export default class OneUptimeDate {
|
||||
const formatstring: string = 'YYYY-MM-DD';
|
||||
return moment(date).local().format(formatstring);
|
||||
}
|
||||
|
||||
public static asFilterDateForDatabaseQuery(date: string | Date): InBetween {
|
||||
date = this.fromString(date);
|
||||
const formattedDate: Date = moment(date).toDate();
|
||||
return new InBetween(
|
||||
OneUptimeDate.getStartOfDay(formattedDate),
|
||||
OneUptimeDate.getEndOfDay(formattedDate)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ export default class Domain extends DatabaseProperty {
|
||||
'|'
|
||||
);
|
||||
const secondTLDs: Array<string> =
|
||||
'ac|academy|accountant|accountants|actor|adult|ag|agency|ai|airforce|am|amsterdam|apartments|app|archi|army|art|asia|associates|at|attorney|au|auction|auto|autos|baby|band|bar|barcelona|bargains|basketball|bayern|be|beauty|beer|berlin|best|bet|bid|bike|bingo|bio|biz|biz.pl|black|blog|blue|boats|boston|boutique|broker|build|builders|business|buzz|bz|ca|cab|cafe|camera|camp|capital|car|cards|care|careers|cars|casa|cash|casino|catering|cc|center|ceo|ch|charity|chat|cheap|church|city|cl|claims|cleaning|clinic|clothing|cloud|club|cn|co|co.in|co.jp|co.kr|co.nz|co.uk|co.za|coach|codes|coffee|college|com|com.ag|com.au|com.br|com.bz|com.cn|com.co|com.es|com.ky|com.mx|com.pe|com.ph|com.pl|com.ru|com.tw|community|company|computer|condos|construction|consulting|contact|contractors|cooking|cool|country|coupons|courses|credit|creditcard|cricket|cruises|cymru|cz|dance|date|dating|de|deals|degree|delivery|democrat|dental|dentist|design|dev|diamonds|digital|direct|directory|discount|dk|doctor|dog|domains|download|earth|education|email|energy|engineer|engineering|enterprises|equipment|es|estate|eu|events|exchange|expert|exposed|express|fail|faith|family|fan|fans|farm|fashion|film|finance|financial|firm.in|fish|fishing|fit|fitness|flights|florist|fm|football|forsale|foundation|fr|fun|fund|furniture|futbol|fyi|gallery|games|garden|gay|gen.in|gg|gifts|gives|giving|glass|global|gmbh|gold|golf|graphics|gratis|green|gripe|group|gs|guide|guru|hair|haus|health|healthcare|hockey|holdings|holiday|homes|horse|hospital|host|house|idv.tw|immo|immobilien|in|inc|ind.in|industries|info|info.pl|ink|institute|insure|international|investments|io|irish|ist|istanbul|it|jetzt|jewelry|jobs|jp|kaufen|kids|kim|kitchen|kiwi|kr|ky|la|land|lat|law|lawyer|lease|legal|lgbt|life|lighting|limited|limo|live|llc|llp|loan|loans|london|love|ltd|ltda|luxury|maison|makeup|management|market|marketing|mba|me|me.uk|media|melbourne|memorial|men|menu|miami|mobi|moda|moe|money|monster|mortgage|motorcycles|movie|ms|music|mx|nagoya|name|navy|ne.kr|net|net.ag|net.au|net.br|net.bz|net.cn|net.co|net.in|net.ky|net.nz|net.pe|net.ph|net.pl|net.ru|network|news|ninja|nl|no|nom.co|nom.es|nom.pe|nrw|nyc|okinawa|one|onl|online|org|org.ag|org.au|org.cn|org.es|org.in|org.ky|org.nz|org.pe|org.ph|org.pl|org.ru|org.uk|organic|page|paris|partners|parts|party|pe|pet|ph|photography|photos|pictures|pink|pizza|pl|place|plumbing|plus|poker|porn|press|pro|productions|promo|properties|protection|pub|pw|quebec|quest|racing|re.kr|realestate|recipes|red|rehab|reise|reisen|rent|rentals|repair|report|republican|rest|restaurant|review|reviews|rich|rip|rocks|rodeo|rugby|run|ryukyu|sale|salon|sarl|school|schule|science|se|security|services|sex|sg|sh|shiksha|shoes|shop|shopping|show|singles|site|ski|skin|soccer|social|software|solar|solutions|space|storage|store|stream|studio|study|style|supplies|supply|support|surf|surgery|sydney|systems|tax|taxi|team|tech|technology|tel|tennis|theater|theatre|tickets|tienda|tips|tires|today|tokyo|tools|tours|town|toys|trade|trading|training|travel|tube|tv|tw|uk|university|uno|us|vacations|vc|vegas|ventures|vet|viajes|video|villas|vin|vip|vision|vodka|vote|voto|voyage|wales|watch|web|webcam|website|wedding|wiki|win|wine|work|works|world|ws|wtf|xxx|xyz|yachts|yoga|yokohama|zone|移动|dev|com|edu|gov|net|mil|org|nom|sch|caa|res|off|gob|int|tur|ip6|uri|urn|asn|act|nsw|qld|tas|vic|pro|biz|adm|adv|agr|arq|art|ato|bio|bmd|cim|cng|cnt|ecn|eco|emp|eng|esp|etc|eti|far|fnd|fot|fst|g12|ggf|imb|ind|inf|jor|jus|leg|lel|mat|med|mus|not|ntr|odo|ppg|psc|psi|qsl|rec|slg|srv|teo|tmp|trd|vet|zlg|web|ltd|sld|pol|fin|k12|lib|pri|aip|fie|eun|sci|prd|cci|pvt|mod|idv|rel|sex|gen|nic|abr|bas|cal|cam|emr|fvg|laz|lig|lom|mar|mol|pmn|pug|sar|sic|taa|tos|umb|vao|vda|ven|mie|北海道|和歌山|神奈川|鹿児島|ass|rep|tra|per|ngo|soc|grp|plc|its|air|and|bus|can|ddr|jfk|mad|nrw|nyc|ski|spy|tcm|ulm|usa|war|fhs|vgs|dep|eid|fet|fla|flå|gol|hof|hol|sel|vik|cri|iwi|ing|abo|fam|gok|gon|gop|gos|aid|atm|gsm|sos|elk|waw|est|aca|bar|cpa|jur|law|sec|plo|www|bir|cbg|jar|khv|msk|nov|nsk|ptz|rnd|spb|stv|tom|tsk|udm|vrn|cmw|kms|nkz|snz|pub|fhv|red|ens|nat|rns|rnu|bbs|tel|bel|kep|nhs|dni|fed|isa|nsn|gub|e12|tec|орг|обр|упр|alt|nis|jpn|mex|ath|iki|nid|gda|inc'.split(
|
||||
'ac|academy|accountant|accountants|actor|adult|ag|agency|ai|airforce|am|amsterdam|apartments|app|archi|army|art|asia|associates|at|attorney|au|auction|auto|autos|baby|band|bar|barcelona|bargains|basketball|bayern|be|beauty|beer|berlin|best|bet|bid|bike|bingo|bio|biz|biz.pl|black|blog|blue|boats|boston|boutique|broker|build|builders|business|buzz|bz|ca|cab|cafe|camera|camp|capital|car|cards|care|careers|cars|casa|cash|casino|catering|cc|center|ceo|ch|charity|chat|cheap|church|city|cl|claims|cleaning|clinic|clothing|cloud|club|cn|co|co.in|co.jp|co.kr|co.nz|co.uk|co.za|coach|codes|coffee|college|com|com.ag|com.au|com.br|com.bz|com.cn|com.co|com.es|com.ky|com.mx|com.pe|com.ph|com.pl|com.ru|com.tw|community|company|computer|condos|construction|consulting|contact|contractors|cooking|cool|country|coupons|courses|credit|creditcard|cricket|cruises|cymru|cz|dance|date|dating|de|deals|degree|delivery|democrat|dental|dentist|design|dev|diamonds|digital|direct|directory|discount|dk|doctor|dog|domains|download|earth|education|email|energy|engineer|engineering|enterprises|equipment|es|estate|eu|events|exchange|expert|exposed|express|fail|faith|family|fan|fans|farm|fashion|film|finance|financial|firm.in|fish|fishing|fit|fitness|flights|florist|fm|football|forsale|foundation|fr|fun|fund|furniture|futbol|fyi|gallery|games|garden|gay|gen.in|gg|gifts|gives|giving|glass|global|gmbh|gold|golf|graphics|gratis|green|gripe|group|gs|guide|guru|hair|haus|health|healthcare|hockey|holdings|holiday|homes|horse|hospital|host|house|idv.tw|immo|immobilien|in|inc|ind.in|industries|info|info.pl|ink|institute|insure|international|investments|io|irish|ist|istanbul|it|jetzt|jewelry|jobs|jp|kaufen|kids|kim|kitchen|kiwi|kr|ky|la|land|lat|law|lawyer|lease|legal|lgbt|life|lighting|limited|limo|live|llc|llp|loan|loans|london|love|ltd|ltda|luxury|maison|makeup|management|market|marketing|mba|me|me.uk|media|melbourne|memorial|men|menu|miami|mobi|moda|moe|money|monster|mortgage|motorcycles|movie|ms|music|mx|nagoya|name|navy|ne.kr|net|net.ag|net.au|net.br|net.bz|net.cn|net.co|net.in|net.ky|net.nz|net.pe|net.ph|net.pl|net.ru|network|news|ninja|nl|no|nom.co|nom.es|nom.pe|nrw|nyc|okinawa|one|onl|online|org|org.ag|org.au|org.cn|org.es|org.in|org.ky|org.nz|org.pe|org.ph|org.pl|org.ru|org.uk|organic|page|paris|partners|parts|party|pe|pet|ph|photography|photos|pictures|pink|pizza|pl|place|plumbing|plus|poker|porn|press|pro|productions|promo|properties|protection|pub|pw|quebec|quest|racing|re.kr|realestate|recipes|red|rehab|reise|reisen|rent|rentals|repair|report|republican|rest|restaurant|review|reviews|rich|rip|rocks|rodeo|rugby|run|ryukyu|sale|salon|sarl|school|schule|science|se|security|services|sex|sg|sh|shiksha|shoes|shop|shopping|show|singles|site|ski|skin|soccer|social|software|solar|solutions|space|storage|store|stream|studio|study|style|supplies|supply|support|surf|surgery|sydney|systems|tax|taxi|team|tech|technology|tel|tennis|theater|theatre|tickets|tienda|tips|tires|today|tokyo|tools|tours|town|toys|trade|trading|training|travel|tube|tv|tw|uk|university|uno|us|vacations|vc|vegas|ventures|vet|viajes|video|villas|vin|vip|vision|vodka|vote|voto|voyage|wales|watch|web|webcam|website|wedding|wiki|win|wine|work|works|world|ws|wtf|xxx|xyz|yachts|yoga|yokohama|zone|移动|dev|com|edu|gov|net|mil|org|nom|sch|caa|res|off|gob|int|tur|ip6|uri|urn|asn|act|nsw|qld|tas|vic|pro|biz|adm|adv|agr|arq|art|ato|bio|bmd|cim|cng|cnt|ecn|eco|emp|eng|esp|etc|eti|far|fnd|fot|fst|g12|ggf|imb|ind|inf|jor|jus|leg|lel|mat|med|mus|not|ntr|odo|ppg|psc|psi|qsl|rec|slg|srv|teo|tmp|trd|vet|zlg|web|ltd|sld|pol|fin|k12|lib|pri|aip|fie|eun|sci|prd|cci|pvt|mod|idv|rel|sex|gen|nic|abr|bas|cal|cam|emr|fvg|laz|lig|lom|mar|mol|pmn|pug|sar|sic|taa|tos|umb|vao|vda|ven|mie|北海道|和歌山|神奈川|鹿児島|ass|rep|tra|per|ngo|soc|grp|plc|its|air|and|bus|can|ddr|jfk|mad|nrw|nyc|ski|spy|tcm|ulm|usa|war|fhs|vgs|dep|eid|fet|fla|flå|gol|hof|hol|sel|vik|cri|iwi|ing|abo|fam|gok|gon|gop|gos|aid|atm|gsm|sos|elk|waw|est|aca|bar|cpa|jur|law|sec|plo|www|bir|cbg|jar|khv|msk|nov|nsk|ptz|rnd|spb|stv|tom|tsk|udm|vrn|cmw|kms|nkz|snz|pub|fhv|red|ens|nat|rns|rnu|bbs|tel|bel|kep|nhs|dni|fed|isa|nsn|gub|e12|tec|орг|обр|упр|alt|nis|jpn|mex|ath|iki|nid|gda|inc|za'.split(
|
||||
'|'
|
||||
);
|
||||
|
||||
|
||||
@@ -33,6 +33,7 @@ enum EmailTemplateType {
|
||||
StatusPageOwnerAnnouncementPosted = 'StatusPageOwnerAnnouncementPosted.hbs',
|
||||
SimpleMessage = 'SimpleMessage.hbs',
|
||||
VerificationCode = 'VerificationCode.hbs',
|
||||
AcknowledgeIncident = 'AcknowledgeIncident.hbs',
|
||||
}
|
||||
|
||||
export default EmailTemplateType;
|
||||
|
||||
@@ -100,6 +100,8 @@ enum IconProp {
|
||||
BellRinging = 'BellRinging',
|
||||
AdjustmentVertical = 'AdjustmentVertical',
|
||||
AdjustmentHorizontal = 'AdjustmentHorizontal',
|
||||
Minus = 'Minus',
|
||||
MinusSmall = 'MinusSmall',
|
||||
}
|
||||
|
||||
export default IconProp;
|
||||
|
||||
@@ -159,9 +159,13 @@ export default class JSONFunctions {
|
||||
}
|
||||
|
||||
public static fromJSONObject<T extends BaseModel>(
|
||||
json: JSONObject,
|
||||
json: JSONObject | T,
|
||||
type: { new (): T }
|
||||
): T {
|
||||
if (json instanceof BaseModel) {
|
||||
return json;
|
||||
}
|
||||
|
||||
return this.fromJSON<T>(json, type) as T;
|
||||
}
|
||||
|
||||
|
||||
@@ -6,4 +6,5 @@ export interface CriteriaIncident {
|
||||
incidentSeverityId?: ObjectID | undefined;
|
||||
autoResolveIncident?: boolean | undefined;
|
||||
id: string;
|
||||
onCallPolicyIds?: Array<ObjectID> | undefined;
|
||||
}
|
||||
|
||||
@@ -119,6 +119,7 @@ export default class MonitorCriteriaInstance extends DatabaseProperty {
|
||||
incidentSeverityId: arg.incidentSeverityId,
|
||||
autoResolveIncident: true,
|
||||
id: ObjectID.generate().toString(),
|
||||
onCallPolicyIds: [],
|
||||
},
|
||||
],
|
||||
changeMonitorStatus: true,
|
||||
@@ -155,6 +156,7 @@ export default class MonitorCriteriaInstance extends DatabaseProperty {
|
||||
incidentSeverityId: arg.incidentSeverityId,
|
||||
autoResolveIncident: true,
|
||||
id: ObjectID.generate().toString(),
|
||||
onCallPolicyIds: [],
|
||||
},
|
||||
],
|
||||
changeMonitorStatus: true,
|
||||
|
||||
@@ -19,6 +19,10 @@ export default class ObjectID extends DatabaseProperty {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public equals(other: ObjectID): boolean {
|
||||
return this.id.toString() === other.id.toString();
|
||||
}
|
||||
|
||||
public override toString(): string {
|
||||
return this.id;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
enum OnCallDutyExecutionLogTimelineStatus {
|
||||
Skipped = 'Skipped',
|
||||
Started = 'Started',
|
||||
Running = 'Running',
|
||||
SuccessfullyAcknowledged = 'Successfully Acknowledged',
|
||||
NotificationSent = 'Notification Sent',
|
||||
Error = 'Error',
|
||||
}
|
||||
|
||||
export default OnCallDutyExecutionLogTimelineStatus;
|
||||
@@ -1,7 +1,8 @@
|
||||
enum OnCallDutyPolicyStatus {
|
||||
SuccessfullyAcknowledged = 'Successfully Acknowledged',
|
||||
ExecutionInProgress = 'Execution in Progress',
|
||||
FailedToAcknowledge = 'Failed to Acknowledge',
|
||||
Scheduled = 'Scheduled',
|
||||
Started = 'Started',
|
||||
Running = 'Running',
|
||||
Completed = 'Completed',
|
||||
Error = 'Error',
|
||||
}
|
||||
|
||||
|
||||
@@ -500,6 +500,13 @@ export class PermissionHelper {
|
||||
isAssignableToTenant: true,
|
||||
isAccessControlPermission: false,
|
||||
},
|
||||
{
|
||||
permission: Permission.ProjectUser,
|
||||
title: 'Project User',
|
||||
description: 'User of this project.',
|
||||
isAssignableToTenant: true,
|
||||
isAccessControlPermission: false,
|
||||
},
|
||||
{
|
||||
permission: Permission.CurrentUser,
|
||||
title: 'Logged in User',
|
||||
|
||||
@@ -19,7 +19,7 @@ export default class Phone extends DatabaseProperty {
|
||||
* }
|
||||
*/
|
||||
const re: RegExp =
|
||||
/^[+]?[(]?[0-9]{3}[)]?[-\s.]?[0-9]{3}[-\s.]?[0-9]{4,6}$/; // regex for international phone numbers format based on (ITU-T E.123)
|
||||
/^[+]?[(]?[0-9]{3}[)]?[-\s.]?[0-9]{3}[-\s.]?[0-9]{4,7}$/; // regex for international phone numbers format based on (ITU-T E.123)
|
||||
const isValid: boolean = re.test(v);
|
||||
if (!isValid) {
|
||||
throw new BadDataException(`Phone is not in valid format: ${v}`);
|
||||
|
||||
6
Common/Types/SMS/SMS.ts
Normal file
6
Common/Types/SMS/SMS.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import Phone from '../Phone';
|
||||
|
||||
export default interface SMS {
|
||||
to: Phone;
|
||||
message: string;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
enum UserNotificationEventType {
|
||||
IncidentCreated = 'Incident Created',
|
||||
}
|
||||
|
||||
export default UserNotificationEventType;
|
||||
@@ -0,0 +1,9 @@
|
||||
enum UserNotificationExecutionStatus {
|
||||
Scheduled = 'Scheduled',
|
||||
Started = 'Strated',
|
||||
Running = 'Running',
|
||||
Completed = 'Completed',
|
||||
Error = 'Error',
|
||||
}
|
||||
|
||||
export default UserNotificationExecutionStatus;
|
||||
9
Common/Types/UserNotification/UserNotificationStatus.ts
Normal file
9
Common/Types/UserNotification/UserNotificationStatus.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
enum UserNotificationStatus {
|
||||
Sent = 'Sent',
|
||||
Acknowledged = 'Acknowledged',
|
||||
Error = 'Error',
|
||||
Sending = 'Sending',
|
||||
Skipped = 'Skipped',
|
||||
}
|
||||
|
||||
export default UserNotificationStatus;
|
||||
161
CommonServer/API/UserNotificationLogTimelineAPI.ts
Normal file
161
CommonServer/API/UserNotificationLogTimelineAPI.ts
Normal file
@@ -0,0 +1,161 @@
|
||||
import UserNotificationLogTimeline from 'Model/Models/UserNotificationLogTimeline';
|
||||
import UserNotificationLogTimelineService, {
|
||||
Service as UserNotificationLogTimelineServiceType,
|
||||
} from '../Services/UserNotificationLogTimelineService';
|
||||
import BaseAPI from './BaseAPI';
|
||||
import {
|
||||
ExpressRequest,
|
||||
ExpressResponse,
|
||||
OneUptimeRequest,
|
||||
} from '../Utils/Express';
|
||||
import BadDataException from 'Common/Types/Exception/BadDataException';
|
||||
import Response from '../Utils/Response';
|
||||
import ObjectID from 'Common/Types/ObjectID';
|
||||
import { JSONObject } from 'Common/Types/JSON';
|
||||
import NotificationMiddleware from '../Middleware/NotificationMiddleware';
|
||||
import OneUptimeDate from 'Common/Types/Date';
|
||||
import URL from 'Common/Types/API/URL';
|
||||
import { DashboardRoute, Domain, HttpProtocol } from '../Config';
|
||||
import UserNotificationStatus from 'Common/Types/UserNotification/UserNotificationStatus';
|
||||
|
||||
export default class UserNotificationLogTimelineAPI extends BaseAPI<
|
||||
UserNotificationLogTimeline,
|
||||
UserNotificationLogTimelineServiceType
|
||||
> {
|
||||
public constructor() {
|
||||
super(UserNotificationLogTimeline, UserNotificationLogTimelineService);
|
||||
|
||||
this.router.post(
|
||||
`${new this.entityType()
|
||||
.getCrudApiPath()
|
||||
?.toString()}/call/gather-input/:itemId`,
|
||||
NotificationMiddleware.isValidCallNotificationRequest,
|
||||
async (req: ExpressRequest, res: ExpressResponse) => {
|
||||
req = req as OneUptimeRequest;
|
||||
|
||||
if (!req.params['itemId']) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException('Invalid item ID')
|
||||
);
|
||||
}
|
||||
|
||||
const token: JSONObject = (req as any).callTokenData;
|
||||
|
||||
const itemId: ObjectID = new ObjectID(req.params['itemId']);
|
||||
|
||||
const timelineItem: UserNotificationLogTimeline | null =
|
||||
await this.service.findOneById({
|
||||
id: itemId,
|
||||
select: {
|
||||
_id: true,
|
||||
projectId: true,
|
||||
triggeredByIncidentId: true,
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!timelineItem) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException('Invalid item Id')
|
||||
);
|
||||
}
|
||||
|
||||
// check digits.
|
||||
|
||||
if (req.body['Digits'] === '1') {
|
||||
// then ack incident
|
||||
await this.service.updateOneById({
|
||||
id: itemId,
|
||||
data: {
|
||||
acknowledgedAt: OneUptimeDate.getCurrentDate(),
|
||||
isAcknowledged: true,
|
||||
status: UserNotificationStatus.Acknowledged,
|
||||
statusMessage: 'Notification Acknowledged',
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return NotificationMiddleware.sendResponse(
|
||||
req,
|
||||
res,
|
||||
token as any
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
this.router.get(
|
||||
`${new this.entityType()
|
||||
.getCrudApiPath()
|
||||
?.toString()}/acknowledge/:itemId`,
|
||||
async (req: ExpressRequest, res: ExpressResponse) => {
|
||||
req = req as OneUptimeRequest;
|
||||
|
||||
if (!req.params['itemId']) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException('Item ID is required')
|
||||
);
|
||||
}
|
||||
|
||||
const itemId: ObjectID = new ObjectID(req.params['itemId']);
|
||||
|
||||
const timelineItem: UserNotificationLogTimeline | null =
|
||||
await this.service.findOneById({
|
||||
id: itemId,
|
||||
select: {
|
||||
_id: true,
|
||||
projectId: true,
|
||||
triggeredByIncidentId: true,
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!timelineItem) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException('Invalid item Id')
|
||||
);
|
||||
}
|
||||
|
||||
await this.service.updateOneById({
|
||||
id: itemId,
|
||||
data: {
|
||||
acknowledgedAt: OneUptimeDate.getCurrentDate(),
|
||||
isAcknowledged: true,
|
||||
status: UserNotificationStatus.Acknowledged,
|
||||
statusMessage: 'Notification Acknowledged',
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
// redirect to dashboard to incidents page.
|
||||
return Response.redirect(
|
||||
req,
|
||||
res,
|
||||
new URL(
|
||||
HttpProtocol,
|
||||
Domain,
|
||||
DashboardRoute.addRoute(
|
||||
`/${timelineItem.projectId?.toString()}/incidents/${timelineItem.triggeredByIncidentId!.toString()}`
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -66,6 +66,10 @@ export const WorkerHostname: Hostname = Hostname.fromString(
|
||||
process.env['WORKER_HOSTNAME'] || 'worker'
|
||||
);
|
||||
|
||||
export const LinkShortnerHostname: Route = new Route(
|
||||
process.env['LINK_SHORTNER_HOSTNAME'] || 'link-shortner'
|
||||
);
|
||||
|
||||
export const WorkflowHostname: Hostname = Hostname.fromString(
|
||||
process.env['WORKFLOW_HOSTNAME'] || 'workflow'
|
||||
);
|
||||
@@ -124,6 +128,10 @@ export const StatusPageRoute: Route = new Route(
|
||||
process.env['STATUS_PAGE_ROUTE'] || '/status-page'
|
||||
);
|
||||
|
||||
export const LinkShortnerRoute: Route = new Route(
|
||||
process.env['LINK_SHORTNER_ROUTE'] || '/l'
|
||||
);
|
||||
|
||||
export const DashboardRoute: Route = new Route(
|
||||
process.env['DASHBOARD_ROUTE'] || '/dashboard'
|
||||
);
|
||||
|
||||
71
CommonServer/Middleware/NotificationMiddleware.ts
Normal file
71
CommonServer/Middleware/NotificationMiddleware.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
import {
|
||||
ExpressRequest,
|
||||
ExpressResponse,
|
||||
NextFunction,
|
||||
OneUptimeRequest,
|
||||
} from '../Utils/Express';
|
||||
|
||||
import Response from '../Utils/Response';
|
||||
import BadDataException from 'Common/Types/Exception/BadDataException';
|
||||
import JSONFunctions from 'Common/Types/JSONFunctions';
|
||||
import JSONWebToken from '../Utils/JsonWebToken';
|
||||
import { OnCallInputRequest } from 'Common/Types/Call/CallRequest';
|
||||
import VoiceResponse from 'twilio/lib/twiml/VoiceResponse';
|
||||
|
||||
export default class NotificationMiddleware {
|
||||
public static async sendResponse(
|
||||
req: ExpressRequest,
|
||||
res: ExpressResponse,
|
||||
onCallInputRequest: OnCallInputRequest
|
||||
): Promise<void> {
|
||||
const response: VoiceResponse = new VoiceResponse();
|
||||
|
||||
if (onCallInputRequest[req.body['Digits']]) {
|
||||
response.say(onCallInputRequest[req.body['Digits']]!.sayMessage);
|
||||
} else {
|
||||
response.say(onCallInputRequest['default']!.sayMessage);
|
||||
}
|
||||
|
||||
return Response.sendXmlResponse(req, res, response.toString());
|
||||
}
|
||||
|
||||
public static async isValidCallNotificationRequest(
|
||||
req: ExpressRequest,
|
||||
res: ExpressResponse,
|
||||
next: NextFunction
|
||||
): Promise<void> {
|
||||
req = req as OneUptimeRequest;
|
||||
|
||||
if (!req.body['Digits']) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException('Invalid input')
|
||||
);
|
||||
}
|
||||
|
||||
if (!req.query['token']) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException('Invalid token')
|
||||
);
|
||||
}
|
||||
|
||||
const token: string = req.query['token'] as string;
|
||||
|
||||
try {
|
||||
(req as any).callTokenData = JSONFunctions.deserialize(
|
||||
JSONWebToken.decodeJsonPayload(token)
|
||||
);
|
||||
} catch (e) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException('Invalid token')
|
||||
);
|
||||
}
|
||||
|
||||
return next();
|
||||
}
|
||||
}
|
||||
@@ -13,20 +13,21 @@ import CallRequest from 'Common/Types/Call/CallRequest';
|
||||
|
||||
export default class CallService {
|
||||
public static async makeCall(
|
||||
to: Phone,
|
||||
callRequest: CallRequest,
|
||||
options: {
|
||||
projectId?: ObjectID | undefined; // project id for sms log
|
||||
from?: Phone; // from phone number
|
||||
isSensitive?: boolean; // if true, message will not be logged
|
||||
userNotificationLogTimelineId?: ObjectID;
|
||||
}
|
||||
): Promise<HTTPResponse<EmptyResponseData>> {
|
||||
const body: JSONObject = {
|
||||
to: to.toString(),
|
||||
callRequest: callRequest,
|
||||
from: options.from?.toString(),
|
||||
projectId: options.projectId?.toString(),
|
||||
isSensitive: options.isSensitive,
|
||||
userNotificationLogTimelineId:
|
||||
options.userNotificationLogTimelineId?.toString(),
|
||||
};
|
||||
|
||||
return await API.post<EmptyResponseData>(
|
||||
|
||||
@@ -23,12 +23,67 @@ import TeamMemberService from './TeamMemberService';
|
||||
import { LIMIT_PER_PROJECT } from 'Common/Types/Database/LimitMax';
|
||||
import UserService from './UserService';
|
||||
import { JSONObject } from 'Common/Types/JSON';
|
||||
import OnCallDutyPolicyService from './OnCallDutyPolicyService';
|
||||
import UserNotificationEventType from 'Common/Types/UserNotification/UserNotificationEventType';
|
||||
|
||||
export class Service extends DatabaseService<Model> {
|
||||
public constructor(postgresDatabase?: PostgresDatabase) {
|
||||
super(Model, postgresDatabase);
|
||||
}
|
||||
|
||||
public async acknowledgeIncident(
|
||||
incidentId: ObjectID,
|
||||
acknowledgedByUserId: ObjectID
|
||||
): Promise<void> {
|
||||
const incident: Model | null = await this.findOneById({
|
||||
id: incidentId,
|
||||
select: {
|
||||
projectId: true,
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!incident || !incident.projectId) {
|
||||
throw new BadDataException('Incident not found.');
|
||||
}
|
||||
|
||||
const incidentState: IncidentState | null =
|
||||
await IncidentStateService.findOneBy({
|
||||
query: {
|
||||
projectId: incident.projectId,
|
||||
isAcknowledgedState: true,
|
||||
},
|
||||
select: {
|
||||
_id: true,
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!incidentState || !incidentState.id) {
|
||||
throw new BadDataException(
|
||||
'Acknowledged state not found for this project. Please add acknowledged state from settings.'
|
||||
);
|
||||
}
|
||||
|
||||
const incidentStateTimeline: IncidentStateTimeline =
|
||||
new IncidentStateTimeline();
|
||||
incidentStateTimeline.projectId = incident.projectId;
|
||||
incidentStateTimeline.incidentId = incidentId;
|
||||
incidentStateTimeline.incidentStateId = incidentState.id;
|
||||
incidentStateTimeline.createdByUserId = acknowledgedByUserId;
|
||||
|
||||
await IncidentStateTimelineService.create({
|
||||
data: incidentStateTimeline,
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
protected override async onBeforeCreate(
|
||||
createBy: CreateBy<Model>
|
||||
): Promise<OnCreate<Model>> {
|
||||
@@ -167,6 +222,22 @@ export class Service extends DatabaseService<Model> {
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
createdItem.onCallDutyPolicies?.length &&
|
||||
createdItem.onCallDutyPolicies?.length > 0
|
||||
) {
|
||||
for (const policy of createdItem.onCallDutyPolicies) {
|
||||
await OnCallDutyPolicyService.executePolicy(
|
||||
new ObjectID(policy._id as string),
|
||||
{
|
||||
triggeredByIncidentId: createdItem.id!,
|
||||
userNotificationEventType:
|
||||
UserNotificationEventType.IncidentCreated,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return createdItem;
|
||||
}
|
||||
|
||||
|
||||
@@ -9,11 +9,15 @@ import Email from 'Common/Types/Email/EmailMessage';
|
||||
import EmailServer from 'Common/Types/Email/EmailServer';
|
||||
import Protocol from 'Common/Types/API/Protocol';
|
||||
import ClusterKeyAuthorization from '../Middleware/ClusterKeyAuthorization';
|
||||
import ObjectID from 'Common/Types/ObjectID';
|
||||
|
||||
export default class MailService {
|
||||
public static async sendMail(
|
||||
mail: Email,
|
||||
mailServer?: EmailServer
|
||||
mailServer?: EmailServer,
|
||||
options?: {
|
||||
userNotificationLogTimelineId?: ObjectID;
|
||||
}
|
||||
): Promise<HTTPResponse<EmptyResponseData>> {
|
||||
const body: JSONObject = {
|
||||
...mail,
|
||||
@@ -30,6 +34,11 @@ export default class MailService {
|
||||
body['SMTP_PASSWORD'] = mailServer.password;
|
||||
}
|
||||
|
||||
if (options?.userNotificationLogTimelineId) {
|
||||
body['userNotificationLogTimelineId'] =
|
||||
options.userNotificationLogTimelineId.toString();
|
||||
}
|
||||
|
||||
return await API.post<EmptyResponseData>(
|
||||
new URL(
|
||||
Protocol.HTTP,
|
||||
|
||||
@@ -1,10 +1,637 @@
|
||||
import PostgresDatabase from '../Infrastructure/PostgresDatabase';
|
||||
import Model from 'Model/Models/OnCallDutyPolicyEscalationRule';
|
||||
import DatabaseService from './DatabaseService';
|
||||
import DatabaseService, {
|
||||
OnCreate,
|
||||
OnDelete,
|
||||
OnUpdate,
|
||||
} from './DatabaseService';
|
||||
import CreateBy from '../Types/Database/CreateBy';
|
||||
import BadDataException from 'Common/Types/Exception/BadDataException';
|
||||
import QueryHelper from '../Types/Database/QueryHelper';
|
||||
import DeleteBy from '../Types/Database/DeleteBy';
|
||||
import ObjectID from 'Common/Types/ObjectID';
|
||||
import LIMIT_MAX, { LIMIT_PER_PROJECT } from 'Common/Types/Database/LimitMax';
|
||||
import SortOrder from 'Common/Types/Database/SortOrder';
|
||||
import UpdateBy from '../Types/Database/UpdateBy';
|
||||
import Query from '../Types/Database/Query';
|
||||
import PositiveNumber from 'Common/Types/PositiveNumber';
|
||||
import DatabaseCommonInteractionProps from 'Common/Types/Database/DatabaseCommonInteractionProps';
|
||||
import OnCallDutyPolicyEscalationRuleUser from 'Model/Models/OnCallDutyPolicyEscalationRuleUser';
|
||||
import OnCallDutyPolicyEscalationRuleUserService from './OnCallDutyPolicyEscalationRuleUserService';
|
||||
import OnCallDutyPolicyEscalationRuleTeam from 'Model/Models/OnCallDutyPolicyEscalationRuleTeam';
|
||||
import OnCallDutyPolicyEscalationRuleTeamService from './OnCallDutyPolicyEscalationRuleTeamService';
|
||||
import TeamMemberService from './TeamMemberService';
|
||||
import UserNotificationEventType from 'Common/Types/UserNotification/UserNotificationEventType';
|
||||
import UserNotificationRuleService from './UserNotificationRuleService';
|
||||
import OnCallDutyPolicyExecutionLogTimeline from 'Model/Models/OnCallDutyPolicyExecutionLogTimeline';
|
||||
import OnCallDutyPolicyExecutionLogTimelineService from './OnCallDutyPolicyExecutionLogTimelineService';
|
||||
import OnCallDutyExecutionLogTimelineStatus from 'Common/Types/OnCallDutyPolicy/OnCalDutyExecutionLogTimelineStatus';
|
||||
import User from 'Model/Models/User';
|
||||
import OneUptimeDate from 'Common/Types/Date';
|
||||
import OnCallDutyPolicyExecutionLogService from './OnCallDutyPolicyExecutionLogService';
|
||||
import { IsBillingEnabled } from '../Config';
|
||||
import { PlanSelect } from 'Common/Types/Billing/SubscriptionPlan';
|
||||
|
||||
export class Service extends DatabaseService<Model> {
|
||||
public async startRuleExecution(
|
||||
ruleId: ObjectID,
|
||||
options: {
|
||||
projectId: ObjectID;
|
||||
triggeredByIncidentId?: ObjectID | undefined;
|
||||
userNotificationEventType: UserNotificationEventType;
|
||||
onCallPolicyExecutionLogId: ObjectID;
|
||||
onCallPolicyId: ObjectID;
|
||||
}
|
||||
): Promise<void> {
|
||||
// add log timeline.
|
||||
|
||||
const rule: Model | null = await this.findOneById({
|
||||
id: ruleId,
|
||||
select: {
|
||||
_id: true,
|
||||
order: true,
|
||||
escalateAfterInMinutes: true,
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!rule) {
|
||||
throw new BadDataException(
|
||||
`On Call Duty Policy Escalation Rule with id ${ruleId.toString()} not found`
|
||||
);
|
||||
}
|
||||
|
||||
await OnCallDutyPolicyExecutionLogService.updateOneById({
|
||||
id: options.onCallPolicyExecutionLogId,
|
||||
data: {
|
||||
lastEscalationRuleExecutedAt: OneUptimeDate.getCurrentDate(),
|
||||
lastExecutedEscalationRuleId: ruleId,
|
||||
lastExecutedEscalationRuleOrder: rule.order!,
|
||||
executeNextEscalationRuleInMinutes:
|
||||
rule.escalateAfterInMinutes || 0,
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
const getNewLog: Function =
|
||||
(): OnCallDutyPolicyExecutionLogTimeline => {
|
||||
const log: OnCallDutyPolicyExecutionLogTimeline =
|
||||
new OnCallDutyPolicyExecutionLogTimeline();
|
||||
|
||||
log.projectId = options.projectId;
|
||||
log.onCallDutyPolicyExecutionLogId =
|
||||
options.onCallPolicyExecutionLogId;
|
||||
log.onCallDutyPolicyId = options.onCallPolicyId;
|
||||
log.onCallDutyPolicyEscalationRuleId = ruleId;
|
||||
log.userNotificationEventType =
|
||||
options.userNotificationEventType;
|
||||
|
||||
if (options.triggeredByIncidentId) {
|
||||
log.triggeredByIncidentId = options.triggeredByIncidentId;
|
||||
}
|
||||
|
||||
return log;
|
||||
};
|
||||
|
||||
if (
|
||||
UserNotificationEventType.IncidentCreated ===
|
||||
options.userNotificationEventType &&
|
||||
!options.triggeredByIncidentId
|
||||
) {
|
||||
throw new BadDataException(
|
||||
'triggeredByIncidentId is required when userNotificationEventType is IncidentCreated'
|
||||
);
|
||||
}
|
||||
|
||||
const usersInRule: Array<OnCallDutyPolicyEscalationRuleUser> =
|
||||
await OnCallDutyPolicyEscalationRuleUserService.findBy({
|
||||
query: {
|
||||
onCallDutyPolicyEscalationRuleId: ruleId,
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
skip: 0,
|
||||
limit: LIMIT_PER_PROJECT,
|
||||
select: {
|
||||
userId: true,
|
||||
},
|
||||
});
|
||||
|
||||
const teamsInRule: Array<OnCallDutyPolicyEscalationRuleTeam> =
|
||||
await OnCallDutyPolicyEscalationRuleTeamService.findBy({
|
||||
query: {
|
||||
onCallDutyPolicyEscalationRuleId: ruleId,
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
skip: 0,
|
||||
limit: LIMIT_PER_PROJECT,
|
||||
select: {
|
||||
teamId: true,
|
||||
},
|
||||
});
|
||||
|
||||
// get unique users and notify all the users.
|
||||
|
||||
const startUserNotifcationRuleExecution: Function = async (
|
||||
userId: ObjectID,
|
||||
teamId: ObjectID | undefined
|
||||
): Promise<void> => {
|
||||
// no users in this rule. Skipping.
|
||||
let log: OnCallDutyPolicyExecutionLogTimeline = getNewLog();
|
||||
log.statusMessage = 'Sending notification to user.';
|
||||
log.status = OnCallDutyExecutionLogTimelineStatus.Running;
|
||||
log.alertSentToUserId = userId;
|
||||
if (teamId) {
|
||||
log.userBelongsToTeamId = teamId;
|
||||
}
|
||||
|
||||
log = await OnCallDutyPolicyExecutionLogTimelineService.create({
|
||||
data: log,
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
await UserNotificationRuleService.startUserNotificationRulesExecution(
|
||||
userId,
|
||||
{
|
||||
userNotificationEventType:
|
||||
options.userNotificationEventType!,
|
||||
triggeredByIncidentId:
|
||||
options.triggeredByIncidentId || undefined,
|
||||
onCallPolicyExecutionLogId:
|
||||
options.onCallPolicyExecutionLogId,
|
||||
onCallPolicyId: options.onCallPolicyId,
|
||||
onCallPolicyEscalationRuleId: ruleId,
|
||||
userBelongsToTeamId: teamId,
|
||||
onCallDutyPolicyExecutionLogTimelineId: log.id!,
|
||||
projectId: options.projectId,
|
||||
}
|
||||
);
|
||||
|
||||
// notification sent to user.
|
||||
await OnCallDutyPolicyExecutionLogTimelineService.updateOneById({
|
||||
id: log.id!,
|
||||
data: {
|
||||
status: OnCallDutyExecutionLogTimelineStatus.NotificationSent,
|
||||
statusMessage: 'Notification sent to user.',
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const uniqueUserIds: Array<ObjectID> = [];
|
||||
|
||||
for (const teamInRule of teamsInRule) {
|
||||
const usersInTeam: Array<User> =
|
||||
await TeamMemberService.getUsersInTeam(teamInRule.teamId!);
|
||||
|
||||
for (const user of usersInTeam) {
|
||||
if (
|
||||
!uniqueUserIds.find((userId: ObjectID) => {
|
||||
return user.id?.toString() === userId.toString();
|
||||
})
|
||||
) {
|
||||
uniqueUserIds.push(user.id!);
|
||||
await startUserNotifcationRuleExecution(
|
||||
user.id!,
|
||||
teamInRule.teamId!
|
||||
);
|
||||
} else {
|
||||
// no users in this rule. Skipping.
|
||||
const log: OnCallDutyPolicyExecutionLogTimeline =
|
||||
getNewLog();
|
||||
log.statusMessage =
|
||||
'Skipped because notification sent to this user already.';
|
||||
log.status = OnCallDutyExecutionLogTimelineStatus.Skipped;
|
||||
log.alertSentToUserId = user.id!;
|
||||
log.userBelongsToTeamId = teamInRule.teamId!;
|
||||
|
||||
await OnCallDutyPolicyExecutionLogTimelineService.create({
|
||||
data: log,
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const userRule of usersInRule) {
|
||||
if (
|
||||
!uniqueUserIds.find((userId: ObjectID) => {
|
||||
return userRule.userId?.toString() === userId.toString();
|
||||
})
|
||||
) {
|
||||
uniqueUserIds.push(userRule.userId!);
|
||||
await startUserNotifcationRuleExecution(
|
||||
userRule.userId!,
|
||||
undefined
|
||||
);
|
||||
} else {
|
||||
// no users in this rule. Skipping.
|
||||
const log: OnCallDutyPolicyExecutionLogTimeline = getNewLog();
|
||||
log.statusMessage =
|
||||
'Skipped because notification sent to this user already.';
|
||||
log.status = OnCallDutyExecutionLogTimelineStatus.Skipped;
|
||||
log.alertSentToUserId = userRule.userId!;
|
||||
|
||||
await OnCallDutyPolicyExecutionLogTimelineService.create({
|
||||
data: log,
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (uniqueUserIds.length === 0) {
|
||||
// no users in this rule. Skipping.
|
||||
const log: OnCallDutyPolicyExecutionLogTimeline = getNewLog();
|
||||
log.statusMessage = 'Skipped because no users in this rule.';
|
||||
log.status = OnCallDutyExecutionLogTimelineStatus.Skipped;
|
||||
|
||||
await OnCallDutyPolicyExecutionLogTimelineService.create({
|
||||
data: log,
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public constructor(postgresDatabase?: PostgresDatabase) {
|
||||
super(Model, postgresDatabase);
|
||||
}
|
||||
|
||||
protected override async onCreateSuccess(
|
||||
onCreate: OnCreate<Model>,
|
||||
createdItem: Model
|
||||
): Promise<Model> {
|
||||
if (!createdItem.projectId) {
|
||||
throw new BadDataException('projectId is required');
|
||||
}
|
||||
|
||||
if (!createdItem.id) {
|
||||
throw new BadDataException('id is required');
|
||||
}
|
||||
|
||||
// add people in escalation rule.
|
||||
|
||||
if (
|
||||
onCreate.createBy.miscDataProps &&
|
||||
(onCreate.createBy.miscDataProps['teams'] ||
|
||||
onCreate.createBy.miscDataProps['users'])
|
||||
) {
|
||||
await this.addUsersAndTeams(
|
||||
createdItem.projectId,
|
||||
createdItem.id,
|
||||
createdItem.onCallDutyPolicyId!,
|
||||
(onCreate.createBy.miscDataProps['users'] as Array<ObjectID>) ||
|
||||
[],
|
||||
(onCreate.createBy.miscDataProps['teams'] as Array<ObjectID>) ||
|
||||
[],
|
||||
onCreate.createBy.props
|
||||
);
|
||||
}
|
||||
|
||||
return createdItem;
|
||||
}
|
||||
|
||||
public async addUsersAndTeams(
|
||||
projectId: ObjectID,
|
||||
escalationRuleId: ObjectID,
|
||||
onCallDutyPolicyId: ObjectID,
|
||||
usersIds: Array<ObjectID>,
|
||||
teamIds: Array<ObjectID>,
|
||||
props: DatabaseCommonInteractionProps
|
||||
): Promise<void> {
|
||||
for (const userId of usersIds) {
|
||||
await this.addUser(
|
||||
projectId,
|
||||
escalationRuleId,
|
||||
onCallDutyPolicyId,
|
||||
userId,
|
||||
props
|
||||
);
|
||||
}
|
||||
|
||||
for (const teamId of teamIds) {
|
||||
await this.addTeam(
|
||||
projectId,
|
||||
escalationRuleId,
|
||||
onCallDutyPolicyId,
|
||||
teamId,
|
||||
props
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public async addTeam(
|
||||
projectId: ObjectID,
|
||||
escalationRuleId: ObjectID,
|
||||
onCallDutyPolicyId: ObjectID,
|
||||
teamId: ObjectID,
|
||||
props: DatabaseCommonInteractionProps
|
||||
): Promise<void> {
|
||||
const teamInRule: OnCallDutyPolicyEscalationRuleTeam =
|
||||
new OnCallDutyPolicyEscalationRuleTeam();
|
||||
teamInRule.projectId = projectId;
|
||||
teamInRule.onCallDutyPolicyId = onCallDutyPolicyId;
|
||||
teamInRule.onCallDutyPolicyEscalationRuleId = escalationRuleId;
|
||||
teamInRule.teamId = teamId;
|
||||
|
||||
await OnCallDutyPolicyEscalationRuleTeamService.create({
|
||||
data: teamInRule,
|
||||
props,
|
||||
});
|
||||
}
|
||||
|
||||
public async addUser(
|
||||
projectId: ObjectID,
|
||||
escalationRuleId: ObjectID,
|
||||
onCallDutyPolicyId: ObjectID,
|
||||
userId: ObjectID,
|
||||
props: DatabaseCommonInteractionProps
|
||||
): Promise<void> {
|
||||
const userInRule: OnCallDutyPolicyEscalationRuleUser =
|
||||
new OnCallDutyPolicyEscalationRuleUser();
|
||||
userInRule.projectId = projectId;
|
||||
userInRule.onCallDutyPolicyId = onCallDutyPolicyId;
|
||||
userInRule.onCallDutyPolicyEscalationRuleId = escalationRuleId;
|
||||
userInRule.userId = userId;
|
||||
|
||||
await OnCallDutyPolicyEscalationRuleUserService.create({
|
||||
data: userInRule,
|
||||
props,
|
||||
});
|
||||
}
|
||||
|
||||
protected override async onBeforeCreate(
|
||||
createBy: CreateBy<Model>
|
||||
): Promise<OnCreate<Model>> {
|
||||
if (
|
||||
IsBillingEnabled &&
|
||||
createBy.props.currentPlan === PlanSelect.Free
|
||||
) {
|
||||
// then check no of policies and if it is more than one, return error
|
||||
const count: PositiveNumber = await this.countBy({
|
||||
query: {
|
||||
projectId: createBy.data.projectId!,
|
||||
onCallDutyPolicyId:
|
||||
createBy.data.onCallDutyPolicyId! ||
|
||||
createBy.data.onCallDutyPolicy?._id!,
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (count.toNumber() >= 1) {
|
||||
throw new BadDataException(
|
||||
'You can only create one escalation rule in free plan.'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (!createBy.data.onCallDutyPolicyId) {
|
||||
throw new BadDataException(
|
||||
'Status Page Resource onCallDutyPolicyId is required'
|
||||
);
|
||||
}
|
||||
|
||||
if (!createBy.data.order) {
|
||||
const query: Query<Model> = {
|
||||
onCallDutyPolicyId: createBy.data.onCallDutyPolicyId,
|
||||
};
|
||||
|
||||
const count: PositiveNumber = await this.countBy({
|
||||
query: query,
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
createBy.data.order = count.toNumber() + 1;
|
||||
}
|
||||
|
||||
await this.rearrangeOrder(
|
||||
createBy.data.order,
|
||||
createBy.data.onCallDutyPolicyId,
|
||||
true
|
||||
);
|
||||
|
||||
return {
|
||||
createBy: createBy,
|
||||
carryForward: null,
|
||||
};
|
||||
}
|
||||
|
||||
protected override async onBeforeDelete(
|
||||
deleteBy: DeleteBy<Model>
|
||||
): Promise<OnDelete<Model>> {
|
||||
if (!deleteBy.query._id && !deleteBy.props.isRoot) {
|
||||
throw new BadDataException(
|
||||
'_id should be present when deleting status page resource. Please try the delete with objectId'
|
||||
);
|
||||
}
|
||||
|
||||
let resource: Model | null = null;
|
||||
|
||||
if (!deleteBy.props.isRoot) {
|
||||
resource = await this.findOneBy({
|
||||
query: deleteBy.query,
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
select: {
|
||||
order: true,
|
||||
onCallDutyPolicyId: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
deleteBy,
|
||||
carryForward: resource,
|
||||
};
|
||||
}
|
||||
|
||||
protected override async onDeleteSuccess(
|
||||
onDelete: OnDelete<Model>,
|
||||
_itemIdsBeforeDelete: ObjectID[]
|
||||
): Promise<OnDelete<Model>> {
|
||||
const deleteBy: DeleteBy<Model> = onDelete.deleteBy;
|
||||
const resource: Model | null = onDelete.carryForward;
|
||||
|
||||
if (!deleteBy.props.isRoot && resource) {
|
||||
if (resource && resource.order && resource.onCallDutyPolicyId) {
|
||||
await this.rearrangeOrder(
|
||||
resource.order,
|
||||
resource.onCallDutyPolicyId,
|
||||
|
||||
false
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
deleteBy: deleteBy,
|
||||
carryForward: null,
|
||||
};
|
||||
}
|
||||
|
||||
protected override async onBeforeUpdate(
|
||||
updateBy: UpdateBy<Model>
|
||||
): Promise<OnUpdate<Model>> {
|
||||
if (
|
||||
updateBy.data.order &&
|
||||
!updateBy.props.isRoot &&
|
||||
updateBy.query._id
|
||||
) {
|
||||
const resource: Model | null = await this.findOneBy({
|
||||
query: {
|
||||
_id: updateBy.query._id!,
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
select: {
|
||||
order: true,
|
||||
onCallDutyPolicyId: true,
|
||||
|
||||
_id: true,
|
||||
},
|
||||
});
|
||||
|
||||
const currentOrder: number = resource?.order!;
|
||||
const newOrder: number = updateBy.data.order as number;
|
||||
|
||||
const resources: Array<Model> = await this.findBy({
|
||||
query: {
|
||||
onCallDutyPolicyId: resource?.onCallDutyPolicyId!,
|
||||
},
|
||||
|
||||
limit: LIMIT_MAX,
|
||||
skip: 0,
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
select: {
|
||||
order: true,
|
||||
onCallDutyPolicyId: true,
|
||||
|
||||
_id: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (currentOrder > newOrder) {
|
||||
// moving up.
|
||||
|
||||
for (const resource of resources) {
|
||||
if (
|
||||
resource.order! >= newOrder &&
|
||||
resource.order! < currentOrder
|
||||
) {
|
||||
// increment order.
|
||||
await this.updateOneBy({
|
||||
query: {
|
||||
_id: resource._id!,
|
||||
},
|
||||
data: {
|
||||
order: resource.order! + 1,
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (newOrder > currentOrder) {
|
||||
// moving down.
|
||||
|
||||
for (const resource of resources) {
|
||||
if (
|
||||
resource.order! < newOrder &&
|
||||
resource.order! >= currentOrder
|
||||
) {
|
||||
// increment order.
|
||||
await this.updateOneBy({
|
||||
query: {
|
||||
_id: resource._id!,
|
||||
},
|
||||
data: {
|
||||
order: resource.order! - 1,
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { updateBy, carryForward: null };
|
||||
}
|
||||
|
||||
private async rearrangeOrder(
|
||||
currentOrder: number,
|
||||
onCallDutyPolicyId: ObjectID,
|
||||
increaseOrder: boolean = true
|
||||
): Promise<void> {
|
||||
// get status page resource with this order.
|
||||
const resources: Array<Model> = await this.findBy({
|
||||
query: {
|
||||
order: QueryHelper.greaterThanEqualTo(currentOrder),
|
||||
onCallDutyPolicyId: onCallDutyPolicyId,
|
||||
},
|
||||
limit: LIMIT_MAX,
|
||||
skip: 0,
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
select: {
|
||||
_id: true,
|
||||
order: true,
|
||||
},
|
||||
sort: {
|
||||
order: SortOrder.Ascending,
|
||||
},
|
||||
});
|
||||
|
||||
let newOrder: number = currentOrder;
|
||||
|
||||
for (const resource of resources) {
|
||||
if (increaseOrder) {
|
||||
newOrder = resource.order! + 1;
|
||||
} else {
|
||||
newOrder = resource.order! - 1;
|
||||
}
|
||||
|
||||
await this.updateOneBy({
|
||||
query: {
|
||||
_id: resource._id!,
|
||||
},
|
||||
data: {
|
||||
order: newOrder,
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
export default new Service();
|
||||
|
||||
@@ -1,10 +1,98 @@
|
||||
import PostgresDatabase from '../Infrastructure/PostgresDatabase';
|
||||
import Model from 'Model/Models/OnCallDutyPolicyExecutionLog';
|
||||
import DatabaseService from './DatabaseService';
|
||||
import DatabaseService, { OnCreate } from './DatabaseService';
|
||||
import CreateBy from '../Types/Database/CreateBy';
|
||||
import OnCallDutyPolicyStatus from 'Common/Types/OnCallDutyPolicy/OnCallDutyPolicyStatus';
|
||||
import OnCallDutyPolicyEscalationRule from 'Model/Models/OnCallDutyPolicyEscalationRule';
|
||||
import OnCallDutyPolicyEscalationRuleService from './OnCallDutyPolicyEscalationRuleService';
|
||||
import UserNotificationEventType from 'Common/Types/UserNotification/UserNotificationEventType';
|
||||
|
||||
export class Service extends DatabaseService<Model> {
|
||||
public constructor(postgresDatabase?: PostgresDatabase) {
|
||||
super(Model, postgresDatabase);
|
||||
}
|
||||
|
||||
protected override async onBeforeCreate(
|
||||
createBy: CreateBy<Model>
|
||||
): Promise<OnCreate<Model>> {
|
||||
if (!createBy.data.status) {
|
||||
createBy.data.status = OnCallDutyPolicyStatus.Scheduled;
|
||||
}
|
||||
|
||||
createBy.data.onCallPolicyExecutionRepeatCount = 1;
|
||||
|
||||
return { createBy, carryForward: null };
|
||||
}
|
||||
|
||||
protected override async onCreateSuccess(
|
||||
_onCreate: OnCreate<Model>,
|
||||
createdItem: Model
|
||||
): Promise<Model> {
|
||||
// get execution rules in this policy adn execute the first rule.
|
||||
const executionRule: OnCallDutyPolicyEscalationRule | null =
|
||||
await OnCallDutyPolicyEscalationRuleService.findOneBy({
|
||||
query: {
|
||||
projectId: createdItem.projectId!,
|
||||
onCallDutyPolicyId: createdItem.onCallDutyPolicyId!,
|
||||
order: 1,
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
select: {
|
||||
_id: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (executionRule) {
|
||||
await this.updateOneById({
|
||||
id: createdItem.id!,
|
||||
data: {
|
||||
status: OnCallDutyPolicyStatus.Started,
|
||||
statusMessage: 'Execution started...',
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
await OnCallDutyPolicyEscalationRuleService.startRuleExecution(
|
||||
executionRule.id!,
|
||||
{
|
||||
projectId: createdItem.projectId!,
|
||||
triggeredByIncidentId: createdItem.triggeredByIncidentId,
|
||||
userNotificationEventType:
|
||||
UserNotificationEventType.IncidentCreated,
|
||||
onCallPolicyExecutionLogId: createdItem.id!,
|
||||
onCallPolicyId: createdItem.onCallDutyPolicyId!,
|
||||
}
|
||||
);
|
||||
|
||||
await this.updateOneById({
|
||||
id: createdItem.id!,
|
||||
data: {
|
||||
status: OnCallDutyPolicyStatus.Running,
|
||||
statusMessage: 'First escalation rule executed....',
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
await this.updateOneById({
|
||||
id: createdItem.id!,
|
||||
data: {
|
||||
status: OnCallDutyPolicyStatus.Error,
|
||||
statusMessage:
|
||||
'No Escalation Rules in Policy. Please add escalation rules to this policy.',
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return createdItem;
|
||||
}
|
||||
}
|
||||
export default new Service();
|
||||
|
||||
@@ -1,10 +1,74 @@
|
||||
import PostgresDatabase from '../Infrastructure/PostgresDatabase';
|
||||
import Model from 'Model/Models/OnCallDutyPolicy';
|
||||
import DatabaseService from './DatabaseService';
|
||||
import ObjectID from 'Common/Types/ObjectID';
|
||||
import OnCallDutyPolicyExecutionLog from 'Model/Models/OnCallDutyPolicyExecutionLog';
|
||||
import OnCallDutyPolicy from 'Model/Models/OnCallDutyPolicy';
|
||||
import BadDataException from 'Common/Types/Exception/BadDataException';
|
||||
import OnCallDutyPolicyExecutionLogService from './OnCallDutyPolicyExecutionLogService';
|
||||
import UserNotificationEventType from 'Common/Types/UserNotification/UserNotificationEventType';
|
||||
import OnCallDutyPolicyStatus from 'Common/Types/OnCallDutyPolicy/OnCallDutyPolicyStatus';
|
||||
|
||||
export class Service extends DatabaseService<Model> {
|
||||
export class Service extends DatabaseService<OnCallDutyPolicy> {
|
||||
public constructor(postgresDatabase?: PostgresDatabase) {
|
||||
super(Model, postgresDatabase);
|
||||
super(OnCallDutyPolicy, postgresDatabase);
|
||||
}
|
||||
|
||||
public async executePolicy(
|
||||
policyId: ObjectID,
|
||||
options: {
|
||||
triggeredByIncidentId?: ObjectID | undefined;
|
||||
userNotificationEventType: UserNotificationEventType;
|
||||
}
|
||||
): Promise<void> {
|
||||
// execute this policy
|
||||
|
||||
if (
|
||||
UserNotificationEventType.IncidentCreated ===
|
||||
options.userNotificationEventType &&
|
||||
!options.triggeredByIncidentId
|
||||
) {
|
||||
throw new BadDataException(
|
||||
'triggeredByIncidentId is required when userNotificationEventType is IncidentCreated'
|
||||
);
|
||||
}
|
||||
|
||||
const policy: OnCallDutyPolicy | null = await this.findOneById({
|
||||
id: policyId,
|
||||
select: {
|
||||
_id: true,
|
||||
projectId: true,
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!policy) {
|
||||
throw new BadDataException(
|
||||
`On Call Duty Policy with id ${policyId.toString()} not found`
|
||||
);
|
||||
}
|
||||
|
||||
// add policy log.
|
||||
const log: OnCallDutyPolicyExecutionLog =
|
||||
new OnCallDutyPolicyExecutionLog();
|
||||
|
||||
log.projectId = policy.projectId!;
|
||||
log.onCallDutyPolicyId = policyId;
|
||||
log.userNotificationEventType = options.userNotificationEventType;
|
||||
log.statusMessage = 'Scheduled.';
|
||||
log.status = OnCallDutyPolicyStatus.Scheduled;
|
||||
|
||||
if (options.triggeredByIncidentId) {
|
||||
log.triggeredByIncidentId = options.triggeredByIncidentId;
|
||||
}
|
||||
|
||||
await OnCallDutyPolicyExecutionLogService.create({
|
||||
data: log,
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
export default new Service();
|
||||
|
||||
53
CommonServer/Services/ShortLinkService.ts
Normal file
53
CommonServer/Services/ShortLinkService.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import PostgresDatabase from '../Infrastructure/PostgresDatabase';
|
||||
import Model from 'Model/Models/ShortLink';
|
||||
import DatabaseService, { OnCreate } from './DatabaseService';
|
||||
import CreateBy from '../Types/Database/CreateBy';
|
||||
import Text from 'Common/Types/Text';
|
||||
import URL from 'Common/Types/API/URL';
|
||||
import { Domain, HttpProtocol, LinkShortnerRoute } from '../Config';
|
||||
|
||||
export class Service extends DatabaseService<Model> {
|
||||
public constructor(postgresDatabase?: PostgresDatabase) {
|
||||
super(Model, postgresDatabase);
|
||||
this.hardDeleteItemsOlderThanInDays('createdAt', 3); //expire links in 3 days.
|
||||
}
|
||||
|
||||
protected override async onBeforeCreate(
|
||||
createBy: CreateBy<Model>
|
||||
): Promise<OnCreate<Model>> {
|
||||
createBy.data.shortId = Text.generateRandomText(8);
|
||||
|
||||
return { createBy: createBy, carryForward: [] };
|
||||
}
|
||||
|
||||
public async saveShortLinkFor(url: URL): Promise<Model> {
|
||||
const model: Model = new Model();
|
||||
model.link = url;
|
||||
return await this.create({ data: model, props: { isRoot: true } });
|
||||
}
|
||||
|
||||
public getShortenedUrl(model: Model): URL {
|
||||
return new URL(
|
||||
HttpProtocol,
|
||||
Domain,
|
||||
LinkShortnerRoute.addRoute('/' + model.shortId?.toString())
|
||||
);
|
||||
}
|
||||
|
||||
public async getShortLinkFor(shortLinkId: string): Promise<Model | null> {
|
||||
return await this.findOneBy({
|
||||
query: {
|
||||
shortId: shortLinkId,
|
||||
},
|
||||
select: {
|
||||
_id: true,
|
||||
link: true,
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default new Service();
|
||||
@@ -9,23 +9,26 @@ import Protocol from 'Common/Types/API/Protocol';
|
||||
import ClusterKeyAuthorization from '../Middleware/ClusterKeyAuthorization';
|
||||
import Phone from 'Common/Types/Phone';
|
||||
import ObjectID from 'Common/Types/ObjectID';
|
||||
import SMS from 'Common/Types/SMS/SMS';
|
||||
|
||||
export default class SmsService {
|
||||
public static async sendSms(
|
||||
to: Phone,
|
||||
message: string,
|
||||
sms: SMS,
|
||||
options: {
|
||||
projectId?: ObjectID | undefined; // project id for sms log
|
||||
from?: Phone; // from phone number
|
||||
isSensitive?: boolean; // if true, message will not be logged
|
||||
userNotificationLogTimelineId?: ObjectID;
|
||||
}
|
||||
): Promise<HTTPResponse<EmptyResponseData>> {
|
||||
const body: JSONObject = {
|
||||
to: to.toString(),
|
||||
message,
|
||||
to: sms.to.toString(),
|
||||
message: sms.message,
|
||||
from: options.from?.toString(),
|
||||
projectId: options.projectId?.toString(),
|
||||
isSensitive: options.isSensitive,
|
||||
userNotificationLogTimelineId:
|
||||
options.userNotificationLogTimelineId?.toString(),
|
||||
};
|
||||
|
||||
return await API.post<EmptyResponseData>(
|
||||
|
||||
@@ -336,6 +336,32 @@ export class TeamMemberService extends DatabaseService<TeamMember> {
|
||||
});
|
||||
}
|
||||
|
||||
public async getUsersInTeam(teamId: ObjectID): Promise<Array<User>> {
|
||||
const members: Array<TeamMember> = await this.findBy({
|
||||
query: {
|
||||
teamId: teamId,
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
select: {
|
||||
_id: true,
|
||||
user: {
|
||||
_id: true,
|
||||
email: true,
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
|
||||
skip: 0,
|
||||
limit: LIMIT_MAX,
|
||||
});
|
||||
|
||||
return members.map((member: TeamMember) => {
|
||||
return member.user!;
|
||||
});
|
||||
}
|
||||
|
||||
public async updateSubscriptionSeatsByUnqiqueTeamMembersInProject(
|
||||
projectId: ObjectID
|
||||
): Promise<void> {
|
||||
|
||||
@@ -9,7 +9,7 @@ import CallService from './CallService';
|
||||
import logger from '../Utils/Logger';
|
||||
import ObjectID from 'Common/Types/ObjectID';
|
||||
import Text from 'Common/Types/Text';
|
||||
import CallRequest, { CallAction } from 'Common/Types/Call/CallRequest';
|
||||
import CallRequest from 'Common/Types/Call/CallRequest';
|
||||
import DeleteBy from '../Types/Database/DeleteBy';
|
||||
import LIMIT_MAX from 'Common/Types/Database/LimitMax';
|
||||
import UserNotificationRuleService from './UserNotificationRuleService';
|
||||
@@ -145,6 +145,7 @@ export class Service extends DatabaseService<Model> {
|
||||
|
||||
public sendVerificationCode(item: Model): void {
|
||||
const callRequest: CallRequest = {
|
||||
to: item.phone!,
|
||||
data: [
|
||||
{
|
||||
sayMessage:
|
||||
@@ -159,12 +160,11 @@ export class Service extends DatabaseService<Model> {
|
||||
{
|
||||
sayMessage: 'Thank you for using OneUptime. Goodbye.',
|
||||
},
|
||||
CallAction.Hangup,
|
||||
],
|
||||
};
|
||||
|
||||
// send verifiction sms.
|
||||
CallService.makeCall(item.phone!, callRequest, {
|
||||
CallService.makeCall(callRequest, {
|
||||
projectId: item.projectId,
|
||||
isSensitive: true,
|
||||
}).catch((err: Error) => {
|
||||
@@ -172,4 +172,5 @@ export class Service extends DatabaseService<Model> {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default new Service();
|
||||
|
||||
234
CommonServer/Services/UserNotificationLogService.ts
Normal file
234
CommonServer/Services/UserNotificationLogService.ts
Normal file
@@ -0,0 +1,234 @@
|
||||
import PostgresDatabase from '../Infrastructure/PostgresDatabase';
|
||||
import Model from 'Model/Models/UserNotificationLog';
|
||||
import DatabaseService, { OnCreate, OnUpdate } from './DatabaseService';
|
||||
import UserNotificationRule from 'Model/Models/UserNotificationRule';
|
||||
import UserNotificationRuleService from './UserNotificationRuleService';
|
||||
import { LIMIT_PER_PROJECT } from 'Common/Types/Database/LimitMax';
|
||||
import NotificationRuleType from 'Common/Types/NotificationRule/NotificationRuleType';
|
||||
import UserNotificationEventType from 'Common/Types/UserNotification/UserNotificationEventType';
|
||||
import BadDataException from 'Common/Types/Exception/BadDataException';
|
||||
import CreateBy from '../Types/Database/CreateBy';
|
||||
import UserNotificationExecutionStatus from 'Common/Types/UserNotification/UserNotificationExecutionStatus';
|
||||
import IncidentService from './IncidentService';
|
||||
import Incident from 'Model/Models/Incident';
|
||||
import PositiveNumber from 'Common/Types/PositiveNumber';
|
||||
import ObjectID from 'Common/Types/ObjectID';
|
||||
import OnCallDutyPolicyExecutionLogTimelineService from './OnCallDutyPolicyExecutionLogTimelineService';
|
||||
import OnCallDutyExecutionLogTimelineStatus from 'Common/Types/OnCallDutyPolicy/OnCalDutyExecutionLogTimelineStatus';
|
||||
|
||||
export class Service extends DatabaseService<Model> {
|
||||
public constructor(postgresDatabase?: PostgresDatabase) {
|
||||
super(Model, postgresDatabase);
|
||||
}
|
||||
|
||||
protected override async onBeforeCreate(
|
||||
createBy: CreateBy<Model>
|
||||
): Promise<OnCreate<Model>> {
|
||||
createBy.data.status = UserNotificationExecutionStatus.Scheduled;
|
||||
|
||||
return {
|
||||
createBy,
|
||||
carryForward: null,
|
||||
};
|
||||
}
|
||||
|
||||
protected override async onUpdateSuccess(
|
||||
onUpdate: OnUpdate<Model>,
|
||||
_updatedItemIds: ObjectID[]
|
||||
): Promise<OnUpdate<Model>> {
|
||||
if (onUpdate.updateBy.data.status) {
|
||||
//update the correspomnding oncallTimeline.
|
||||
const items: Array<Model> = await this.findBy({
|
||||
query: onUpdate.updateBy.query,
|
||||
select: {
|
||||
onCallDutyPolicyExecutionLogTimelineId: true,
|
||||
},
|
||||
skip: 0,
|
||||
limit: LIMIT_PER_PROJECT,
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
let status: OnCallDutyExecutionLogTimelineStatus | undefined =
|
||||
undefined;
|
||||
|
||||
switch (onUpdate.updateBy.data.status) {
|
||||
case UserNotificationExecutionStatus.Completed:
|
||||
status =
|
||||
OnCallDutyExecutionLogTimelineStatus.NotificationSent;
|
||||
break;
|
||||
case UserNotificationExecutionStatus.Error:
|
||||
status = OnCallDutyExecutionLogTimelineStatus.Error;
|
||||
break;
|
||||
case UserNotificationExecutionStatus.Running:
|
||||
status = OnCallDutyExecutionLogTimelineStatus.Running;
|
||||
break;
|
||||
case UserNotificationExecutionStatus.Scheduled:
|
||||
status = OnCallDutyExecutionLogTimelineStatus.Started;
|
||||
break;
|
||||
case UserNotificationExecutionStatus.Started:
|
||||
status = OnCallDutyExecutionLogTimelineStatus.Started;
|
||||
break;
|
||||
default:
|
||||
throw new BadDataException('Invalid status');
|
||||
}
|
||||
|
||||
for (const item of items) {
|
||||
await OnCallDutyPolicyExecutionLogTimelineService.updateOneById(
|
||||
{
|
||||
id: item.onCallDutyPolicyExecutionLogTimelineId!,
|
||||
data: {
|
||||
status: status!,
|
||||
statusMessage:
|
||||
onUpdate.updateBy.data.statusMessage!,
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return onUpdate;
|
||||
}
|
||||
|
||||
protected override async onCreateSuccess(
|
||||
_onCreate: OnCreate<Model>,
|
||||
createdItem: Model
|
||||
): Promise<Model> {
|
||||
// update this item to be processed.
|
||||
await this.updateOneById({
|
||||
id: createdItem.id!,
|
||||
data: {
|
||||
status: UserNotificationExecutionStatus.Started,
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
const notificationRuleType: NotificationRuleType =
|
||||
this.getNotificationRuleType(
|
||||
createdItem.userNotificationEventType!
|
||||
);
|
||||
|
||||
const incident: Incident | null = await IncidentService.findOneById({
|
||||
id: createdItem.triggeredByIncidentId!,
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
select: {
|
||||
incidentSeverityId: true,
|
||||
},
|
||||
});
|
||||
|
||||
// Check if there are any rules .
|
||||
const ruleCount: PositiveNumber =
|
||||
await UserNotificationRuleService.countBy({
|
||||
query: {
|
||||
userId: createdItem.userId!,
|
||||
projectId: createdItem.projectId!,
|
||||
ruleType: notificationRuleType,
|
||||
incidentSeverityId: incident?.incidentSeverityId!,
|
||||
},
|
||||
skip: 0,
|
||||
limit: LIMIT_PER_PROJECT,
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (ruleCount.toNumber() === 0) {
|
||||
// update this item to be processed.
|
||||
await this.updateOneById({
|
||||
id: createdItem.id!,
|
||||
data: {
|
||||
status: UserNotificationExecutionStatus.Error, // now the worker will pick this up and complete this or mark this as failed.
|
||||
statusMessage:
|
||||
'No notification rules found. Please add rules in User Settings > On Call Rules.',
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
return createdItem;
|
||||
}
|
||||
|
||||
// find immediate notification rule and alert the user.
|
||||
const immediateNotificationRule: Array<UserNotificationRule> =
|
||||
await UserNotificationRuleService.findBy({
|
||||
query: {
|
||||
userId: createdItem.userId!,
|
||||
projectId: createdItem.projectId!,
|
||||
notifyAfterMinutes: 0,
|
||||
ruleType: notificationRuleType,
|
||||
incidentSeverityId: incident?.incidentSeverityId!,
|
||||
},
|
||||
select: {
|
||||
_id: true,
|
||||
},
|
||||
skip: 0,
|
||||
limit: LIMIT_PER_PROJECT,
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
for (const immediateNotificationRuleItem of immediateNotificationRule) {
|
||||
await UserNotificationRuleService.executeNotificationRuleItem(
|
||||
immediateNotificationRuleItem.id!,
|
||||
{
|
||||
userNotificationLogId: createdItem.id!,
|
||||
projectId: createdItem.projectId!,
|
||||
triggeredByIncidentId: createdItem.triggeredByIncidentId,
|
||||
userNotificationEventType:
|
||||
createdItem.userNotificationEventType!,
|
||||
onCallPolicyExecutionLogId:
|
||||
createdItem.onCallDutyPolicyExecutionLogId,
|
||||
onCallPolicyId: createdItem.onCallDutyPolicyId,
|
||||
onCallPolicyEscalationRuleId:
|
||||
createdItem.onCallDutyPolicyEscalationRuleId,
|
||||
userBelongsToTeamId: createdItem.userBelongsToTeamId,
|
||||
onCallDutyPolicyExecutionLogTimelineId:
|
||||
createdItem.onCallDutyPolicyExecutionLogTimelineId,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// update this item to be processed.
|
||||
await this.updateOneById({
|
||||
id: createdItem.id!,
|
||||
data: {
|
||||
status: UserNotificationExecutionStatus.Running, // now the worker will pick this up and complete this or mark this as failed.
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
return createdItem;
|
||||
}
|
||||
|
||||
public getNotificationRuleType(
|
||||
userNotificationEventType: UserNotificationEventType
|
||||
): NotificationRuleType {
|
||||
let notificationRuleType: NotificationRuleType =
|
||||
NotificationRuleType.ON_CALL_INCIDENT_CREATED;
|
||||
|
||||
if (
|
||||
userNotificationEventType ===
|
||||
UserNotificationEventType.IncidentCreated
|
||||
) {
|
||||
notificationRuleType =
|
||||
NotificationRuleType.ON_CALL_INCIDENT_CREATED;
|
||||
} else {
|
||||
// Invlaid user notification event type.
|
||||
throw new BadDataException('Invalid user notification event type.');
|
||||
}
|
||||
return notificationRuleType;
|
||||
}
|
||||
}
|
||||
export default new Service();
|
||||
141
CommonServer/Services/UserNotificationLogTimelineService.ts
Normal file
141
CommonServer/Services/UserNotificationLogTimelineService.ts
Normal file
@@ -0,0 +1,141 @@
|
||||
import PostgresDatabase from '../Infrastructure/PostgresDatabase';
|
||||
import Model from 'Model/Models/UserNotificationLogTimeline';
|
||||
import DatabaseService, { OnUpdate } from './DatabaseService';
|
||||
import ObjectID from 'Common/Types/ObjectID';
|
||||
import { LIMIT_PER_PROJECT } from 'Common/Types/Database/LimitMax';
|
||||
import UserNotificationLogService from './UserNotificationLogService';
|
||||
import OnCallDutyPolicyExecutionLogService from './OnCallDutyPolicyExecutionLogService';
|
||||
import OnCallDutyPolicyExecutionLogTimelineService from './OnCallDutyPolicyExecutionLogTimelineService';
|
||||
import IncidentService from './IncidentService';
|
||||
import UserNotificationExecutionStatus from 'Common/Types/UserNotification/UserNotificationExecutionStatus';
|
||||
import OnCallDutyPolicyStatus from 'Common/Types/OnCallDutyPolicy/OnCallDutyPolicyStatus';
|
||||
import User from 'Model/Models/User';
|
||||
import UserService from './UserService';
|
||||
import BadDataException from 'Common/Types/Exception/BadDataException';
|
||||
import OnCallDutyExecutionLogTimelineStatus from 'Common/Types/OnCallDutyPolicy/OnCalDutyExecutionLogTimelineStatus';
|
||||
|
||||
export class Service extends DatabaseService<Model> {
|
||||
public constructor(postgresDatabase?: PostgresDatabase) {
|
||||
super(Model, postgresDatabase);
|
||||
}
|
||||
|
||||
protected override async onUpdateSuccess(
|
||||
onUpdate: OnUpdate<Model>,
|
||||
_updatedItemIds: ObjectID[]
|
||||
): Promise<OnUpdate<Model>> {
|
||||
if (
|
||||
onUpdate.updateBy.data.acknowledgedAt &&
|
||||
onUpdate.updateBy.data.isAcknowledged
|
||||
) {
|
||||
const items: Array<Model> = await this.findBy({
|
||||
query: onUpdate.updateBy.query,
|
||||
select: {
|
||||
_id: true,
|
||||
projectId: true,
|
||||
userId: true,
|
||||
userNotificationLogId: true,
|
||||
onCallDutyPolicyExecutionLogId: true,
|
||||
triggeredByIncidentId: true,
|
||||
onCallDutyPolicyExecutionLogTimelineId: true,
|
||||
},
|
||||
skip: 0,
|
||||
limit: LIMIT_PER_PROJECT,
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
for (const item of items) {
|
||||
// this incident is acknowledged.
|
||||
|
||||
// now we need to ack the parent log.
|
||||
|
||||
const user: User | null = await UserService.findOneById({
|
||||
id: item.userId!,
|
||||
select: {
|
||||
_id: true,
|
||||
name: true,
|
||||
email: true,
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
throw new BadDataException('User not found.');
|
||||
}
|
||||
|
||||
await UserNotificationLogService.updateOneById({
|
||||
id: item.userNotificationLogId!,
|
||||
data: {
|
||||
acknowledgedAt: onUpdate.updateBy.data.acknowledgedAt,
|
||||
acknowledgedByUserId: item.userId!,
|
||||
status: UserNotificationExecutionStatus.Completed,
|
||||
statusMessage:
|
||||
'Incident acknowledged by ' +
|
||||
user.name +
|
||||
' (' +
|
||||
user.email +
|
||||
')',
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
// and then oncall log.
|
||||
|
||||
await OnCallDutyPolicyExecutionLogService.updateOneById({
|
||||
id: item.onCallDutyPolicyExecutionLogId!,
|
||||
data: {
|
||||
acknowledgedAt: onUpdate.updateBy.data.acknowledgedAt,
|
||||
acknowledgedByUserId: item.userId!,
|
||||
status: OnCallDutyPolicyStatus.Completed,
|
||||
statusMessage:
|
||||
'Incident acknowledged by ' +
|
||||
user.name +
|
||||
' (' +
|
||||
user.email +
|
||||
')',
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
// and then oncall log timeline.
|
||||
await OnCallDutyPolicyExecutionLogTimelineService.updateOneById(
|
||||
{
|
||||
id: item.onCallDutyPolicyExecutionLogTimelineId!,
|
||||
data: {
|
||||
acknowledgedAt:
|
||||
onUpdate.updateBy.data.acknowledgedAt,
|
||||
isAcknowledged: true,
|
||||
status: OnCallDutyExecutionLogTimelineStatus.SuccessfullyAcknowledged,
|
||||
statusMessage:
|
||||
'Incident acknowledged by ' +
|
||||
user.name +
|
||||
' (' +
|
||||
user.email +
|
||||
')',
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
// incident.
|
||||
await IncidentService.acknowledgeIncident(
|
||||
item.triggeredByIncidentId!,
|
||||
item.userId!
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return onUpdate;
|
||||
}
|
||||
}
|
||||
|
||||
export default new Service();
|
||||
@@ -11,12 +11,576 @@ import IncidentSeverity from 'Model/Models/IncidentSeverity';
|
||||
import UserEmailService from './UserEmailService';
|
||||
import UserEmail from 'Model/Models/UserEmail';
|
||||
import NotificationRuleType from 'Common/Types/NotificationRule/NotificationRuleType';
|
||||
import UserNotificationEventType from 'Common/Types/UserNotification/UserNotificationEventType';
|
||||
import UserNotificationLog from 'Model/Models/UserNotificationLog';
|
||||
import UserNotificationLogService from './UserNotificationLogService';
|
||||
import UserNotificationLogTimeline from 'Model/Models/UserNotificationLogTimeline';
|
||||
import UserNotificationStatus from 'Common/Types/UserNotification/UserNotificationStatus';
|
||||
import CallRequest from 'Common/Types/Call/CallRequest';
|
||||
import EmailMessage from 'Common/Types/Email/EmailMessage';
|
||||
import SMS from 'Common/Types/SMS/SMS';
|
||||
import Incident from 'Model/Models/Incident';
|
||||
import URL from 'Common/Types/API/URL';
|
||||
import { DashboardApiRoute, Domain, HttpProtocol } from '../Config';
|
||||
import ShortLinkService from './ShortLinkService';
|
||||
import ShortLink from 'Model/Models/ShortLink';
|
||||
import Phone from 'Common/Types/Phone';
|
||||
import Dictionary from 'Common/Types/Dictionary';
|
||||
import Markdown from '../Types/Markdown';
|
||||
import IncidentService from './IncidentService';
|
||||
import EmailTemplateType from 'Common/Types/Email/EmailTemplateType';
|
||||
import UserNotificationLogTimelineService from './UserNotificationLogTimelineService';
|
||||
import MailService from './MailService';
|
||||
import SmsService from './SmsService';
|
||||
import CallService from './CallService';
|
||||
import OneUptimeDate from 'Common/Types/Date';
|
||||
import UserNotificationExecutionStatus from 'Common/Types/UserNotification/UserNotificationExecutionStatus';
|
||||
|
||||
export class Service extends DatabaseService<Model> {
|
||||
public constructor(postgresDatabase?: PostgresDatabase) {
|
||||
super(Model, postgresDatabase);
|
||||
}
|
||||
|
||||
public async executeNotificationRuleItem(
|
||||
userNotificationRuleId: ObjectID,
|
||||
options: {
|
||||
projectId: ObjectID;
|
||||
triggeredByIncidentId?: ObjectID | undefined;
|
||||
userNotificationEventType: UserNotificationEventType;
|
||||
onCallPolicyExecutionLogId?: ObjectID | undefined;
|
||||
onCallPolicyId: ObjectID | undefined;
|
||||
onCallPolicyEscalationRuleId?: ObjectID | undefined;
|
||||
userNotificationLogId: ObjectID;
|
||||
userBelongsToTeamId?: ObjectID | undefined;
|
||||
onCallDutyPolicyExecutionLogTimelineId?: ObjectID | undefined;
|
||||
}
|
||||
): Promise<void> {
|
||||
// get user notifcation log and see if this rule has already been executed. If so then skip.
|
||||
|
||||
const userNotificationLog: UserNotificationLog | null =
|
||||
await UserNotificationLogService.findOneById({
|
||||
id: options.userNotificationLogId,
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
select: {
|
||||
_id: true,
|
||||
executedNotificationRules: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!userNotificationLog) {
|
||||
throw new BadDataException('User notification log not found.');
|
||||
}
|
||||
|
||||
if (
|
||||
Object.keys(
|
||||
userNotificationLog.executedNotificationRules || {}
|
||||
).includes(userNotificationRuleId.toString())
|
||||
) {
|
||||
// already executed.
|
||||
return;
|
||||
}
|
||||
|
||||
if (!userNotificationLog.executedNotificationRules) {
|
||||
userNotificationLog.executedNotificationRules = {};
|
||||
}
|
||||
|
||||
userNotificationLog.executedNotificationRules[
|
||||
userNotificationRuleId.toString()
|
||||
] = OneUptimeDate.getCurrentDate();
|
||||
|
||||
await UserNotificationLogService.updateOneById({
|
||||
id: userNotificationLog.id!,
|
||||
data: {
|
||||
executedNotificationRules: {
|
||||
...userNotificationLog.executedNotificationRules,
|
||||
},
|
||||
} as any,
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
// find notification rule item.
|
||||
const notificationRuleItem: Model | null = await this.findOneById({
|
||||
id: userNotificationRuleId!,
|
||||
select: {
|
||||
_id: true,
|
||||
userId: true,
|
||||
userCall: {
|
||||
phone: true,
|
||||
isVerified: true,
|
||||
},
|
||||
userSms: {
|
||||
phone: true,
|
||||
isVerified: true,
|
||||
},
|
||||
userEmail: {
|
||||
email: true,
|
||||
isVerified: true,
|
||||
},
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!notificationRuleItem) {
|
||||
throw new BadDataException('Notification rule item not found.');
|
||||
}
|
||||
|
||||
const logTimelineItem: UserNotificationLogTimeline =
|
||||
new UserNotificationLogTimeline();
|
||||
logTimelineItem.projectId = options.projectId;
|
||||
logTimelineItem.userNotificationLogId = options.userNotificationLogId;
|
||||
logTimelineItem.userNotificationRuleId = userNotificationRuleId;
|
||||
logTimelineItem.userNotificationLogId = options.userNotificationLogId;
|
||||
logTimelineItem.userId = notificationRuleItem.userId!;
|
||||
logTimelineItem.userNotificationEventType =
|
||||
options.userNotificationEventType;
|
||||
|
||||
if (options.userBelongsToTeamId) {
|
||||
logTimelineItem.userBelongsToTeamId = options.userBelongsToTeamId;
|
||||
}
|
||||
|
||||
if (options.onCallPolicyId) {
|
||||
logTimelineItem.onCallDutyPolicyId = options.onCallPolicyId;
|
||||
}
|
||||
|
||||
if (options.onCallPolicyEscalationRuleId) {
|
||||
logTimelineItem.onCallDutyPolicyEscalationRuleId =
|
||||
options.onCallPolicyEscalationRuleId;
|
||||
}
|
||||
|
||||
if (options.onCallPolicyExecutionLogId) {
|
||||
logTimelineItem.onCallDutyPolicyExecutionLogId =
|
||||
options.onCallPolicyExecutionLogId;
|
||||
}
|
||||
|
||||
if (options.triggeredByIncidentId) {
|
||||
logTimelineItem.triggeredByIncidentId =
|
||||
options.triggeredByIncidentId;
|
||||
}
|
||||
|
||||
if (options.onCallDutyPolicyExecutionLogTimelineId) {
|
||||
logTimelineItem.onCallDutyPolicyExecutionLogTimelineId =
|
||||
options.onCallDutyPolicyExecutionLogTimelineId;
|
||||
}
|
||||
|
||||
// add status and status message and save.
|
||||
|
||||
let incident: Incident | null = null;
|
||||
|
||||
if (
|
||||
options.userNotificationEventType ===
|
||||
UserNotificationEventType.IncidentCreated &&
|
||||
options.triggeredByIncidentId
|
||||
) {
|
||||
incident = await IncidentService.findOneById({
|
||||
id: options.triggeredByIncidentId!,
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
select: {
|
||||
_id: true,
|
||||
title: true,
|
||||
description: true,
|
||||
projectId: true,
|
||||
project: {
|
||||
name: true,
|
||||
},
|
||||
currentIncidentState: {
|
||||
name: true,
|
||||
},
|
||||
incidentSeverity: {
|
||||
name: true,
|
||||
},
|
||||
rootCause: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (!incident) {
|
||||
throw new BadDataException('Incident not found.');
|
||||
}
|
||||
|
||||
if (
|
||||
notificationRuleItem.userEmail?.email &&
|
||||
notificationRuleItem.userEmail?.isVerified
|
||||
) {
|
||||
// send email.
|
||||
if (
|
||||
options.userNotificationEventType ===
|
||||
UserNotificationEventType.IncidentCreated &&
|
||||
incident
|
||||
) {
|
||||
// create an error log.
|
||||
logTimelineItem.status = UserNotificationStatus.Sending;
|
||||
logTimelineItem.statusMessage = `Sending email to ${notificationRuleItem.userEmail?.email.toString()}`;
|
||||
logTimelineItem.userEmailId =
|
||||
notificationRuleItem.userEmail.id!;
|
||||
|
||||
const updatedLog: UserNotificationLogTimeline =
|
||||
await UserNotificationLogTimelineService.create({
|
||||
data: logTimelineItem,
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
const emailMessage: EmailMessage =
|
||||
await this.generateEmailTemplateForIncidentCreated(
|
||||
notificationRuleItem.userEmail?.email,
|
||||
incident,
|
||||
updatedLog.id!
|
||||
);
|
||||
|
||||
// send email.
|
||||
|
||||
MailService.sendMail(emailMessage, undefined, {
|
||||
userNotificationLogTimelineId: updatedLog.id!,
|
||||
}).catch(async (err: Error) => {
|
||||
await UserNotificationLogTimelineService.updateOneById({
|
||||
id: updatedLog.id!,
|
||||
data: {
|
||||
status: UserNotificationStatus.Error,
|
||||
statusMessage:
|
||||
err.message || 'Error sending email.',
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// if you have an email but is not verified, then create a log.
|
||||
if (
|
||||
notificationRuleItem.userEmail?.email &&
|
||||
!notificationRuleItem.userEmail?.isVerified
|
||||
) {
|
||||
// create an error log.
|
||||
logTimelineItem.status = UserNotificationStatus.Error;
|
||||
logTimelineItem.statusMessage = `Email notification not sent because email ${notificationRuleItem.userEmail?.email.toString()} is not verified.`;
|
||||
|
||||
await UserNotificationLogTimelineService.create({
|
||||
data: logTimelineItem,
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// send sms.
|
||||
if (
|
||||
notificationRuleItem.userSms?.phone &&
|
||||
notificationRuleItem.userSms?.isVerified
|
||||
) {
|
||||
// send sms.
|
||||
if (
|
||||
options.userNotificationEventType ===
|
||||
UserNotificationEventType.IncidentCreated &&
|
||||
incident
|
||||
) {
|
||||
// create an error log.
|
||||
logTimelineItem.status = UserNotificationStatus.Sending;
|
||||
logTimelineItem.statusMessage = `Sending SMS to ${notificationRuleItem.userSms?.phone.toString()}.`;
|
||||
logTimelineItem.userSmsId = notificationRuleItem.userSms.id!;
|
||||
|
||||
const updatedLog: UserNotificationLogTimeline =
|
||||
await UserNotificationLogTimelineService.create({
|
||||
data: logTimelineItem,
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
const smsMessage: SMS =
|
||||
await this.generateSmsTemplateForIncidentCreated(
|
||||
notificationRuleItem.userSms?.phone!,
|
||||
incident,
|
||||
updatedLog.id!
|
||||
);
|
||||
|
||||
// send email.
|
||||
|
||||
SmsService.sendSms(smsMessage, {
|
||||
projectId: incident.projectId,
|
||||
userNotificationLogTimelineId: updatedLog.id!,
|
||||
}).catch(async (err: Error) => {
|
||||
await UserNotificationLogTimelineService.updateOneById({
|
||||
id: updatedLog.id!,
|
||||
data: {
|
||||
status: UserNotificationStatus.Error,
|
||||
statusMessage: err.message || 'Error sending SMS.',
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
notificationRuleItem.userSms?.phone &&
|
||||
!notificationRuleItem.userSms?.isVerified
|
||||
) {
|
||||
// create a log.
|
||||
logTimelineItem.status = UserNotificationStatus.Error;
|
||||
logTimelineItem.statusMessage = `SMS not sent because phone ${notificationRuleItem.userSms?.phone.toString()} is not verified.`;
|
||||
|
||||
await UserNotificationLogTimelineService.create({
|
||||
data: logTimelineItem,
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// send call.
|
||||
if (
|
||||
notificationRuleItem.userCall?.phone &&
|
||||
notificationRuleItem.userCall?.isVerified
|
||||
) {
|
||||
// send call.
|
||||
logTimelineItem.status = UserNotificationStatus.Sending;
|
||||
logTimelineItem.statusMessage = `Making a call to ${notificationRuleItem.userCall?.phone.toString()}.`;
|
||||
logTimelineItem.userCallId = notificationRuleItem.userCall.id!;
|
||||
|
||||
const updatedLog: UserNotificationLogTimeline =
|
||||
await UserNotificationLogTimelineService.create({
|
||||
data: logTimelineItem,
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
const callRequest: CallRequest =
|
||||
await this.generateCallTemplateForIncidentCreated(
|
||||
notificationRuleItem.userCall?.phone!,
|
||||
incident,
|
||||
updatedLog.id!
|
||||
);
|
||||
|
||||
// send email.
|
||||
|
||||
CallService.makeCall(callRequest, {
|
||||
projectId: incident.projectId,
|
||||
userNotificationLogTimelineId: updatedLog.id!,
|
||||
}).catch(async (err: Error) => {
|
||||
await UserNotificationLogTimelineService.updateOneById({
|
||||
id: updatedLog.id!,
|
||||
data: {
|
||||
status: UserNotificationStatus.Error,
|
||||
statusMessage: err.message || 'Error making call.',
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
notificationRuleItem.userCall?.phone &&
|
||||
!notificationRuleItem.userCall?.isVerified
|
||||
) {
|
||||
// create a log.
|
||||
logTimelineItem.status = UserNotificationStatus.Error;
|
||||
logTimelineItem.statusMessage = `Call not sent because phone ${notificationRuleItem.userCall?.phone.toString()} is not verified.`;
|
||||
|
||||
await UserNotificationLogTimelineService.create({
|
||||
data: logTimelineItem,
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public async generateCallTemplateForIncidentCreated(
|
||||
to: Phone,
|
||||
incident: Incident,
|
||||
userNotificationLogTimelineId: ObjectID
|
||||
): Promise<CallRequest> {
|
||||
const callRequest: CallRequest = {
|
||||
to: to,
|
||||
data: [
|
||||
{
|
||||
sayMessage: 'This is a call from OneUptime',
|
||||
},
|
||||
{
|
||||
sayMessage: 'A new incident has been created',
|
||||
},
|
||||
{
|
||||
sayMessage: incident.title!,
|
||||
},
|
||||
{
|
||||
introMessage: 'To acknowledge this incident press 1',
|
||||
numDigits: 1,
|
||||
timeoutInSeconds: 10,
|
||||
noInputMessage: 'You have not entered any input. Good bye',
|
||||
onInputCallRequest: {
|
||||
'1': {
|
||||
sayMessage:
|
||||
'You have acknowledged this incident. Good bye',
|
||||
},
|
||||
default: {
|
||||
sayMessage: 'Invalid input. Good bye',
|
||||
},
|
||||
},
|
||||
responseUrl: new URL(
|
||||
HttpProtocol,
|
||||
Domain,
|
||||
DashboardApiRoute.addRoute(
|
||||
new UserNotificationLogTimeline().crudApiPath!
|
||||
).addRoute(
|
||||
'/call/gather-input/' +
|
||||
userNotificationLogTimelineId.toString()
|
||||
)
|
||||
),
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
return callRequest;
|
||||
}
|
||||
|
||||
public async generateSmsTemplateForIncidentCreated(
|
||||
to: Phone,
|
||||
incident: Incident,
|
||||
userNotificationLogTimelineId: ObjectID
|
||||
): Promise<SMS> {
|
||||
const shortUrl: ShortLink = await ShortLinkService.saveShortLinkFor(
|
||||
new URL(
|
||||
HttpProtocol,
|
||||
Domain,
|
||||
DashboardApiRoute.addRoute(
|
||||
new UserNotificationLogTimeline().crudApiPath!
|
||||
).addRoute(
|
||||
'/acknowledge/' + userNotificationLogTimelineId.toString()
|
||||
)
|
||||
)
|
||||
);
|
||||
const url: URL = ShortLinkService.getShortenedUrl(shortUrl);
|
||||
|
||||
const sms: SMS = {
|
||||
to,
|
||||
message: `This is a message from OneUptime. A new incident has been created. ${
|
||||
incident.title
|
||||
}. To acknowledge this incident, please click on the following link ${url.toString()}`,
|
||||
};
|
||||
|
||||
return sms;
|
||||
}
|
||||
|
||||
public async generateEmailTemplateForIncidentCreated(
|
||||
to: Email,
|
||||
incident: Incident,
|
||||
userNotificationLogTimelineId: ObjectID
|
||||
): Promise<EmailMessage> {
|
||||
const vars: Dictionary<string> = {
|
||||
incidentTitle: incident.title!,
|
||||
projectName: incident.project!.name!,
|
||||
currentState: incident.currentIncidentState!.name!,
|
||||
incidentDescription: Markdown.convertToHTML(
|
||||
incident.description! || ''
|
||||
),
|
||||
incidentSeverity: incident.incidentSeverity!.name!,
|
||||
rootCause:
|
||||
incident.rootCause ||
|
||||
'No root cause identified for this incident',
|
||||
incidentViewLink: IncidentService.getIncidentLinkInDashboard(
|
||||
incident.projectId!,
|
||||
incident.id!
|
||||
).toString(),
|
||||
acknowledgeIncidentLink: new URL(
|
||||
HttpProtocol,
|
||||
Domain,
|
||||
DashboardApiRoute.addRoute(
|
||||
new UserNotificationLogTimeline().crudApiPath!
|
||||
).addRoute(
|
||||
'/acknowledge/' + userNotificationLogTimelineId.toString()
|
||||
)
|
||||
).toString(),
|
||||
};
|
||||
|
||||
const emailMessage: EmailMessage = {
|
||||
toEmail: to!,
|
||||
templateType: EmailTemplateType.AcknowledgeIncident,
|
||||
vars: vars,
|
||||
subject: 'ACTION REQUIRED: Incident created - ' + incident.title!,
|
||||
};
|
||||
|
||||
return emailMessage;
|
||||
}
|
||||
|
||||
public async startUserNotificationRulesExecution(
|
||||
userId: ObjectID,
|
||||
options: {
|
||||
projectId: ObjectID;
|
||||
triggeredByIncidentId?: ObjectID | undefined;
|
||||
userNotificationEventType: UserNotificationEventType;
|
||||
onCallPolicyExecutionLogId?: ObjectID | undefined;
|
||||
onCallPolicyId: ObjectID | undefined;
|
||||
onCallPolicyEscalationRuleId?: ObjectID | undefined;
|
||||
userBelongsToTeamId?: ObjectID | undefined;
|
||||
onCallDutyPolicyExecutionLogTimelineId?: ObjectID | undefined;
|
||||
}
|
||||
): Promise<void> {
|
||||
// add user notification log.
|
||||
const userNotificationLog: UserNotificationLog =
|
||||
new UserNotificationLog();
|
||||
|
||||
userNotificationLog.userId = userId;
|
||||
userNotificationLog.projectId = options.projectId;
|
||||
|
||||
if (options.triggeredByIncidentId) {
|
||||
userNotificationLog.triggeredByIncidentId =
|
||||
options.triggeredByIncidentId;
|
||||
}
|
||||
|
||||
userNotificationLog.userNotificationEventType =
|
||||
options.userNotificationEventType;
|
||||
|
||||
if (options.onCallPolicyExecutionLogId) {
|
||||
userNotificationLog.onCallDutyPolicyExecutionLogId =
|
||||
options.onCallPolicyExecutionLogId;
|
||||
}
|
||||
|
||||
if (options.onCallPolicyId) {
|
||||
userNotificationLog.onCallDutyPolicyId = options.onCallPolicyId;
|
||||
}
|
||||
|
||||
if (options.onCallDutyPolicyExecutionLogTimelineId) {
|
||||
userNotificationLog.onCallDutyPolicyExecutionLogTimelineId =
|
||||
options.onCallDutyPolicyExecutionLogTimelineId;
|
||||
}
|
||||
|
||||
if (options.onCallPolicyEscalationRuleId) {
|
||||
userNotificationLog.onCallDutyPolicyEscalationRuleId =
|
||||
options.onCallPolicyEscalationRuleId;
|
||||
}
|
||||
|
||||
if (options.userBelongsToTeamId) {
|
||||
userNotificationLog.userBelongsToTeamId =
|
||||
options.userBelongsToTeamId;
|
||||
}
|
||||
|
||||
userNotificationLog.status = UserNotificationExecutionStatus.Scheduled;
|
||||
userNotificationLog.statusMessage = 'Scheduled';
|
||||
|
||||
await UserNotificationLogService.create({
|
||||
data: userNotificationLog,
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
protected override async onBeforeCreate(
|
||||
createBy: CreateBy<Model>
|
||||
): Promise<OnCreate<Model>> {
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import PostgresDatabase from '../Infrastructure/PostgresDatabase';
|
||||
import Model from 'Model/Models/UserResourceOwnerNotification';
|
||||
import DatabaseService from './DatabaseService';
|
||||
|
||||
export class Service extends DatabaseService<Model> {
|
||||
public constructor(postgresDatabase?: PostgresDatabase) {
|
||||
super(Model, postgresDatabase);
|
||||
}
|
||||
}
|
||||
export default new Service();
|
||||
@@ -146,8 +146,10 @@ export class Service extends DatabaseService<Model> {
|
||||
public sendVerificationCode(item: Model): void {
|
||||
// send verifiction sms.
|
||||
SmsService.sendSms(
|
||||
item.phone!,
|
||||
'Your verification code is ' + item.verificationCode,
|
||||
{
|
||||
to: item.phone!,
|
||||
message: 'Your verification code is ' + item.verificationCode,
|
||||
},
|
||||
{
|
||||
projectId: item.projectId,
|
||||
isSensitive: true,
|
||||
|
||||
@@ -44,17 +44,30 @@ class JSONWebToken {
|
||||
};
|
||||
}
|
||||
|
||||
return jwt.sign(jsonObj, EncryptionSecret.toString(), {
|
||||
return JSONWebToken.signJsonPayload(jsonObj, expiresInSeconds);
|
||||
}
|
||||
|
||||
public static signJsonPayload(
|
||||
payload: JSONObject,
|
||||
expiresInSeconds: number
|
||||
): string {
|
||||
return jwt.sign(payload, EncryptionSecret.toString(), {
|
||||
expiresIn: expiresInSeconds,
|
||||
});
|
||||
}
|
||||
|
||||
public static decodeJsonPayload(token: string): JSONObject {
|
||||
const decodedToken: string = JSON.stringify(
|
||||
jwt.verify(token, EncryptionSecret.toString()) as string
|
||||
);
|
||||
const decoded: JSONObject = JSONFunctions.parse(decodedToken);
|
||||
|
||||
return decoded;
|
||||
}
|
||||
|
||||
public static decode(token: string): JSONWebTokenData {
|
||||
try {
|
||||
const decodedToken: string = JSON.stringify(
|
||||
jwt.verify(token, EncryptionSecret.toString()) as string
|
||||
);
|
||||
const decoded: JSONObject = JSONFunctions.parse(decodedToken);
|
||||
const decoded: JSONObject = JSONWebToken.decodeJsonPayload(token);
|
||||
|
||||
if (decoded['statusPageId']) {
|
||||
return {
|
||||
|
||||
@@ -338,6 +338,27 @@ export default class Response {
|
||||
this.logResponse(req, res, { html: html as string });
|
||||
}
|
||||
|
||||
public static sendXmlResponse(
|
||||
req: ExpressRequest,
|
||||
res: ExpressResponse,
|
||||
xml: string
|
||||
): void {
|
||||
const oneUptimeRequest: OneUptimeRequest = req as OneUptimeRequest;
|
||||
const oneUptimeResponse: OneUptimeResponse = res as OneUptimeResponse;
|
||||
|
||||
oneUptimeResponse.set(
|
||||
'ExpressRequest-Id',
|
||||
oneUptimeRequest.id.toString()
|
||||
);
|
||||
|
||||
oneUptimeResponse.set('Pod-Id', process.env['POD_NAME']);
|
||||
|
||||
oneUptimeResponse.logBody = { xml: xml as string };
|
||||
oneUptimeResponse.writeHead(200, { 'Content-Type': 'text/xml' });
|
||||
oneUptimeResponse.end(xml);
|
||||
this.logResponse(req, res, { xml: xml as string });
|
||||
}
|
||||
|
||||
public static sendJavaScriptResponse(
|
||||
req: ExpressRequest,
|
||||
res: ExpressResponse,
|
||||
|
||||
54
CommonServer/package-lock.json
generated
54
CommonServer/package-lock.json
generated
@@ -40,6 +40,7 @@
|
||||
"redis": "^4.2.0",
|
||||
"socket.io": "^4.4.1",
|
||||
"stripe": "^10.17.0",
|
||||
"twilio": "^4.13.0",
|
||||
"typeorm": "^0.3.10",
|
||||
"typeorm-extension": "^2.2.13",
|
||||
"vm2": "^3.9.14",
|
||||
@@ -4119,7 +4120,6 @@
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
|
||||
"integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"debug": "4"
|
||||
},
|
||||
@@ -4959,6 +4959,11 @@
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/dayjs": {
|
||||
"version": "1.11.9",
|
||||
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.9.tgz",
|
||||
"integrity": "sha512-QvzAURSbQ0pKdIye2txOzNaHmxtUBXerpY0FJsFXUMKbIZeFm5ht1LS/jFsrncjnmtv8HsG0W2g6c0zUjZWmpA=="
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"version": "4.3.4",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
|
||||
@@ -6012,7 +6017,6 @@
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
|
||||
"integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"agent-base": "6",
|
||||
"debug": "4"
|
||||
@@ -8416,8 +8420,7 @@
|
||||
"node_modules/querystringify": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz",
|
||||
"integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==",
|
||||
"dev": true
|
||||
"integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ=="
|
||||
},
|
||||
"node_modules/queue-microtask": {
|
||||
"version": "1.2.3",
|
||||
@@ -8574,8 +8577,7 @@
|
||||
"node_modules/requires-port": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
|
||||
"integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==",
|
||||
"dev": true
|
||||
"integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ=="
|
||||
},
|
||||
"node_modules/resolve": {
|
||||
"version": "1.22.2",
|
||||
@@ -8766,6 +8768,11 @@
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/scmp": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/scmp/-/scmp-2.1.0.tgz",
|
||||
"integrity": "sha512-o/mRQGk9Rcer/jEEw/yw4mwo3EU/NvYvp577/Btqrym9Qy5/MdWGBqipbALgd2lrdWTJ5/gqDusxfnQBxOxT2Q=="
|
||||
},
|
||||
"node_modules/secure-json-parse": {
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-2.7.0.tgz",
|
||||
@@ -9501,6 +9508,32 @@
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz",
|
||||
"integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg=="
|
||||
},
|
||||
"node_modules/twilio": {
|
||||
"version": "4.13.0",
|
||||
"resolved": "https://registry.npmjs.org/twilio/-/twilio-4.13.0.tgz",
|
||||
"integrity": "sha512-fecPGy2lXnULwle4iXcCH3rP5z4fgkirzp+rRIXsFi45+y3qjkY5DBZSzmYr5T4vUOzZ2djmODZJ2jpRfgIBSw==",
|
||||
"dependencies": {
|
||||
"axios": "^0.26.1",
|
||||
"dayjs": "^1.8.29",
|
||||
"https-proxy-agent": "^5.0.0",
|
||||
"jsonwebtoken": "^9.0.0",
|
||||
"qs": "^6.9.4",
|
||||
"scmp": "^2.1.0",
|
||||
"url-parse": "^1.5.9",
|
||||
"xmlbuilder": "^13.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0"
|
||||
}
|
||||
},
|
||||
"node_modules/twilio/node_modules/axios": {
|
||||
"version": "0.26.1",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-0.26.1.tgz",
|
||||
"integrity": "sha512-fPwcX4EvnSHuInCMItEhAGnaSEXRBjtzh9fOtsE6E1G6p7vl7edEeZe11QHf18+6+9gR5PbKV/sGKNaD8YaMeA==",
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.14.8"
|
||||
}
|
||||
},
|
||||
"node_modules/type-check": {
|
||||
"version": "0.3.2",
|
||||
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz",
|
||||
@@ -9868,7 +9901,6 @@
|
||||
"version": "1.5.10",
|
||||
"resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz",
|
||||
"integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"querystringify": "^2.1.1",
|
||||
"requires-port": "^1.0.0"
|
||||
@@ -10141,6 +10173,14 @@
|
||||
"integrity": "sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/xmlbuilder": {
|
||||
"version": "13.0.2",
|
||||
"resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-13.0.2.tgz",
|
||||
"integrity": "sha512-Eux0i2QdDYKbdbA6AM6xE4m6ZTZr4G4xF9kahI2ukSEMCzwce2eX9WlTI5J3s+NU7hpasFsr8hWIONae7LluAQ==",
|
||||
"engines": {
|
||||
"node": ">=6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/xmlchars": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz",
|
||||
|
||||
@@ -42,6 +42,7 @@
|
||||
"redis": "^4.2.0",
|
||||
"socket.io": "^4.4.1",
|
||||
"stripe": "^10.17.0",
|
||||
"twilio": "^4.13.0",
|
||||
"typeorm": "^0.3.10",
|
||||
"typeorm-extension": "^2.2.13",
|
||||
"vm2": "^3.9.14",
|
||||
|
||||
@@ -77,6 +77,14 @@ const Detail: Function = (props: ComponentProps): ReactElement => {
|
||||
return <div className="text-gray-900">{usdCents / 100} USD</div>;
|
||||
};
|
||||
|
||||
const getMinutesField: Function = (minutes: number): ReactElement => {
|
||||
return (
|
||||
<div className="text-gray-900">
|
||||
{minutes} {minutes > 1 ? 'minutes' : 'minute'}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const getField: Function = (field: Field, index: number): ReactElement => {
|
||||
const fieldKey: string = field.key;
|
||||
|
||||
@@ -120,6 +128,10 @@ const Detail: Function = (props: ComponentProps): ReactElement => {
|
||||
data = getUSDCentsField(data);
|
||||
}
|
||||
|
||||
if (data && field.fieldType === FieldType.Minutes) {
|
||||
data = getMinutesField(data);
|
||||
}
|
||||
|
||||
if (data && field.fieldType === FieldType.DictionaryOfStrings) {
|
||||
data = getDictionaryOfStringsViewer(props.item[field.key]);
|
||||
}
|
||||
@@ -274,7 +286,7 @@ const Detail: Function = (props: ComponentProps): ReactElement => {
|
||||
<div
|
||||
className={`grid grid-cols-1 gap-x-4 gap-y-8 sm:grid-cols-${
|
||||
props.showDetailsInNumberOfColumns || 1
|
||||
}`}
|
||||
} w-full`}
|
||||
>
|
||||
{props.fields &&
|
||||
props.fields.length > 0 &&
|
||||
|
||||
@@ -34,7 +34,7 @@ const FieldLabelElement: FunctionComponent<ComponentProps> = (
|
||||
<span>
|
||||
<Link
|
||||
to={props.sideLink?.url}
|
||||
className="underline-on-hover"
|
||||
className="hover:underline"
|
||||
>
|
||||
{props.sideLink?.text}
|
||||
</Link>
|
||||
|
||||
@@ -154,6 +154,18 @@ const Icon: FunctionComponent<ComponentProps> = ({
|
||||
d="M3.375 19.5h17.25m-17.25 0a1.125 1.125 0 01-1.125-1.125M3.375 19.5h7.5c.621 0 1.125-.504 1.125-1.125m-9.75 0V5.625m0 12.75v-1.5c0-.621.504-1.125 1.125-1.125m18.375 2.625V5.625m0 12.75c0 .621-.504 1.125-1.125 1.125m1.125-1.125v-1.5c0-.621-.504-1.125-1.125-1.125m0 3.75h-7.5A1.125 1.125 0 0112 18.375m9.75-12.75c0-.621-.504-1.125-1.125-1.125H3.375c-.621 0-1.125.504-1.125 1.125m19.5 0v1.5c0 .621-.504 1.125-1.125 1.125M2.25 5.625v1.5c0 .621.504 1.125 1.125 1.125m0 0h17.25m-17.25 0h7.5c.621 0 1.125.504 1.125 1.125M3.375 8.25c-.621 0-1.125.504-1.125 1.125v1.5c0 .621.504 1.125 1.125 1.125m17.25-3.75h-7.5c-.621 0-1.125.504-1.125 1.125m8.625-1.125c.621 0 1.125.504 1.125 1.125v1.5c0 .621-.504 1.125-1.125 1.125m-17.25 0h7.5m-7.5 0c-.621 0-1.125.504-1.125 1.125v1.5c0 .621.504 1.125 1.125 1.125M12 10.875v-1.5m0 1.5c0 .621-.504 1.125-1.125 1.125M12 10.875c0 .621.504 1.125 1.125 1.125m-2.25 0c.621 0 1.125.504 1.125 1.125M13.125 12h7.5m-7.5 0c-.621 0-1.125.504-1.125 1.125M20.625 12c.621 0 1.125.504 1.125 1.125v1.5c0 .621-.504 1.125-1.125 1.125m-17.25 0h7.5M12 14.625v-1.5m0 1.5c0 .621-.504 1.125-1.125 1.125M12 14.625c0 .621.504 1.125 1.125 1.125m-2.25 0c.621 0 1.125.504 1.125 1.125m0 1.5v-1.5m0 0c0-.621.504-1.125 1.125-1.125m0 0h7.5"
|
||||
/>
|
||||
);
|
||||
} else if (icon === IconProp.MinusSmall) {
|
||||
return getSvgWrapper(
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M18 12H6" />
|
||||
);
|
||||
} else if (icon === IconProp.Minus) {
|
||||
return getSvgWrapper(
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M19.5 12h-15"
|
||||
/>
|
||||
);
|
||||
} else if (icon === IconProp.Database) {
|
||||
return getSvgWrapper(
|
||||
<path
|
||||
|
||||
@@ -39,7 +39,7 @@ const Link: FunctionComponent<ComponentProps> = (
|
||||
|
||||
return (
|
||||
<a
|
||||
className={`cursor-pointer ${props.className || ''}`}
|
||||
className={`cursor-pointer ${props.className || ''}`}
|
||||
onMouseOver={props.onMouseOver}
|
||||
onMouseOut={props.onMouseOut}
|
||||
onMouseLeave={props.onMouseLeave}
|
||||
|
||||
@@ -6,6 +6,8 @@ import ErrorMessage from '../ErrorMessage/ErrorMessage';
|
||||
import ComponentLoader from '../ComponentLoader/ComponentLoader';
|
||||
import ListBody from './ListBody';
|
||||
import Field from '../Detail/Field';
|
||||
import { DragDropContext, DropResult } from 'react-beautiful-dnd';
|
||||
import { ListDetailProps } from './ListRow';
|
||||
|
||||
export interface ComponentProps {
|
||||
data: Array<JSONObject>;
|
||||
@@ -16,6 +18,10 @@ export interface ComponentProps {
|
||||
currentPageNumber: number;
|
||||
totalItemsCount: number;
|
||||
itemsOnPage: number;
|
||||
enableDragAndDrop?: boolean | undefined;
|
||||
dragDropIndexField?: string | undefined;
|
||||
dragDropIdField?: string | undefined;
|
||||
onDragDrop?: ((id: string, newIndex: number) => void) | undefined;
|
||||
error: string;
|
||||
isLoading: boolean;
|
||||
singularLabel: string;
|
||||
@@ -23,6 +29,7 @@ export interface ComponentProps {
|
||||
actionButtons?: undefined | Array<ActionButtonSchema>;
|
||||
onRefreshClick?: undefined | (() => void);
|
||||
noItemsMessage?: undefined | string;
|
||||
listDetailOptions?: undefined | ListDetailProps;
|
||||
}
|
||||
|
||||
const List: FunctionComponent<ComponentProps> = (
|
||||
@@ -61,13 +68,29 @@ const List: FunctionComponent<ComponentProps> = (
|
||||
data={props.data}
|
||||
fields={props.fields}
|
||||
actionButtons={props.actionButtons}
|
||||
enableDragAndDrop={props.enableDragAndDrop}
|
||||
dragAndDropScope={`${props.id}-dnd`}
|
||||
dragDropIdField={props.dragDropIdField}
|
||||
dragDropIndexField={props.dragDropIndexField}
|
||||
listDetailOptions={props.listDetailOptions}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
{getListbody()}
|
||||
<DragDropContext
|
||||
onDragEnd={(result: DropResult) => {
|
||||
result.destination?.index &&
|
||||
props.onDragDrop &&
|
||||
props.onDragDrop(
|
||||
result.draggableId,
|
||||
result.destination.index
|
||||
);
|
||||
}}
|
||||
>
|
||||
{getListbody()}
|
||||
</DragDropContext>
|
||||
{!props.disablePagination && (
|
||||
<div className=" -ml-6 mt-5 -mr-6 -mb-6">
|
||||
<Pagination
|
||||
|
||||
@@ -1,34 +1,64 @@
|
||||
import { JSONObject } from 'Common/Types/JSON';
|
||||
import React, { FunctionComponent, ReactElement } from 'react';
|
||||
import ListRow from './ListRow';
|
||||
import ListRow, { ListDetailProps } from './ListRow';
|
||||
import ActionButtonSchema from '../ActionButton/ActionButtonSchema';
|
||||
import Field from '../Detail/Field';
|
||||
import { Droppable, DroppableProvided } from 'react-beautiful-dnd';
|
||||
|
||||
export interface ComponentProps {
|
||||
data: Array<JSONObject>;
|
||||
id: string;
|
||||
fields: Array<Field>;
|
||||
actionButtons?: undefined | Array<ActionButtonSchema> | undefined;
|
||||
enableDragAndDrop?: undefined | boolean;
|
||||
dragAndDropScope?: string | undefined;
|
||||
dragDropIdField?: string | undefined;
|
||||
dragDropIndexField?: string | undefined;
|
||||
listDetailOptions?: undefined | ListDetailProps;
|
||||
}
|
||||
|
||||
const ListBody: FunctionComponent<ComponentProps> = (
|
||||
props: ComponentProps
|
||||
): ReactElement => {
|
||||
return (
|
||||
<div id={props.id} className="space-y-6">
|
||||
{props.data &&
|
||||
props.data.map((item: JSONObject, i: number) => {
|
||||
return (
|
||||
<ListRow
|
||||
key={i}
|
||||
item={item}
|
||||
fields={props.fields}
|
||||
actionButtons={props.actionButtons}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
const getBody: Function = (provided?: DroppableProvided): ReactElement => {
|
||||
return (
|
||||
<div
|
||||
ref={provided?.innerRef}
|
||||
{...provided?.droppableProps}
|
||||
id={props.id}
|
||||
className="space-y-6"
|
||||
>
|
||||
{props.data &&
|
||||
props.data.map((item: JSONObject, i: number) => {
|
||||
return (
|
||||
<ListRow
|
||||
key={i}
|
||||
item={item}
|
||||
fields={props.fields}
|
||||
actionButtons={props.actionButtons}
|
||||
dragAndDropScope={props.dragAndDropScope}
|
||||
enableDragAndDrop={props.enableDragAndDrop}
|
||||
dragDropIdField={props.dragDropIdField}
|
||||
dragDropIndexField={props.dragDropIndexField}
|
||||
listDetailOptions={props.listDetailOptions}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
if (props.enableDragAndDrop) {
|
||||
return (
|
||||
<Droppable droppableId={props.dragAndDropScope || ''}>
|
||||
{(provided: DroppableProvided) => {
|
||||
return getBody(provided);
|
||||
}}
|
||||
</Droppable>
|
||||
);
|
||||
}
|
||||
|
||||
return getBody();
|
||||
};
|
||||
|
||||
export default ListBody;
|
||||
|
||||
@@ -5,10 +5,23 @@ import Detail from '../Detail/Detail';
|
||||
import Field from '../Detail/Field';
|
||||
import ConfirmModal from '../Modal/ConfirmModal';
|
||||
import ActionButtonSchema from '../ActionButton/ActionButtonSchema';
|
||||
import { Draggable, DraggableProvided } from 'react-beautiful-dnd';
|
||||
import Icon, { ThickProp } from '../Icon/Icon';
|
||||
import IconProp from 'Common/Types/Icon/IconProp';
|
||||
|
||||
export interface ListDetailProps {
|
||||
showDetailsInNumberOfColumns?: number | undefined;
|
||||
}
|
||||
|
||||
export interface ComponentProps {
|
||||
item: JSONObject;
|
||||
fields: Array<Field>;
|
||||
actionButtons?: Array<ActionButtonSchema> | undefined;
|
||||
enableDragAndDrop?: boolean | undefined;
|
||||
dragAndDropScope?: string | undefined;
|
||||
dragDropIdField?: string | undefined;
|
||||
dragDropIndexField?: string | undefined;
|
||||
listDetailOptions?: ListDetailProps | undefined;
|
||||
}
|
||||
|
||||
const ListRow: FunctionComponent<ComponentProps> = (
|
||||
@@ -22,70 +35,140 @@ const ListRow: FunctionComponent<ComponentProps> = (
|
||||
|
||||
const [error, setError] = useState<string>('');
|
||||
|
||||
return (
|
||||
<div className="bg-white px-4 py-6 shadow sm:rounded-lg sm:px-6">
|
||||
<div>
|
||||
<Detail item={props.item} fields={props.fields} />
|
||||
</div>
|
||||
|
||||
<div className="flex mt-5 -ml-3">
|
||||
{props.actionButtons?.map(
|
||||
(button: ActionButtonSchema, i: number) => {
|
||||
if (button.isVisible && !button.isVisible(props.item)) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div key={i}>
|
||||
<Button
|
||||
buttonSize={ButtonSize.Small}
|
||||
title={button.title}
|
||||
icon={button.icon}
|
||||
buttonStyle={button.buttonStyleType}
|
||||
isLoading={isButtonLoading[i]}
|
||||
onClick={() => {
|
||||
if (button.onClick) {
|
||||
isButtonLoading[i] = true;
|
||||
setIsButtonLoading(isButtonLoading);
|
||||
button.onClick(
|
||||
props.item,
|
||||
() => {
|
||||
// on aciton complete
|
||||
isButtonLoading[i] = false;
|
||||
setIsButtonLoading(
|
||||
isButtonLoading
|
||||
);
|
||||
},
|
||||
(err: Error) => {
|
||||
isButtonLoading[i] = false;
|
||||
setIsButtonLoading(
|
||||
isButtonLoading
|
||||
);
|
||||
setError(
|
||||
(err as Error).message
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
}}
|
||||
const getRow: Function = (provided?: DraggableProvided): ReactElement => {
|
||||
return (
|
||||
<div
|
||||
{...provided?.draggableProps}
|
||||
ref={provided?.innerRef}
|
||||
className="bg-white px-4 py-6 shadow sm:rounded-lg sm:px-6"
|
||||
>
|
||||
<div>
|
||||
{props.enableDragAndDrop && (
|
||||
<div className="flex">
|
||||
<div
|
||||
className="ml-0 -ml-2 w-10"
|
||||
{...provided?.dragHandleProps}
|
||||
>
|
||||
<Icon
|
||||
icon={IconProp.Drag}
|
||||
thick={ThickProp.Thick}
|
||||
className=" h-6 w-6 text-gray-500 hover:text-gray-700 m-auto cursor-ns-resize"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
<Detail
|
||||
item={props.item}
|
||||
fields={props.fields}
|
||||
showDetailsInNumberOfColumns={
|
||||
props.listDetailOptions
|
||||
?.showDetailsInNumberOfColumns || 1
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{!props.enableDragAndDrop && (
|
||||
<Detail
|
||||
item={props.item}
|
||||
fields={props.fields}
|
||||
showDetailsInNumberOfColumns={
|
||||
props.listDetailOptions
|
||||
?.showDetailsInNumberOfColumns || 1
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={
|
||||
props.enableDragAndDrop
|
||||
? `flex mt-5 ml-5`
|
||||
: `flex mt-5 -ml-3`
|
||||
}
|
||||
>
|
||||
{props.actionButtons?.map(
|
||||
(button: ActionButtonSchema, i: number) => {
|
||||
if (
|
||||
button.isVisible &&
|
||||
!button.isVisible(props.item)
|
||||
) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div key={i}>
|
||||
<Button
|
||||
buttonSize={ButtonSize.Small}
|
||||
title={button.title}
|
||||
icon={button.icon}
|
||||
buttonStyle={button.buttonStyleType}
|
||||
isLoading={isButtonLoading[i]}
|
||||
onClick={() => {
|
||||
if (button.onClick) {
|
||||
isButtonLoading[i] = true;
|
||||
setIsButtonLoading(
|
||||
isButtonLoading
|
||||
);
|
||||
button.onClick(
|
||||
props.item,
|
||||
() => {
|
||||
// on aciton complete
|
||||
isButtonLoading[i] =
|
||||
false;
|
||||
setIsButtonLoading(
|
||||
isButtonLoading
|
||||
);
|
||||
},
|
||||
(err: Error) => {
|
||||
isButtonLoading[i] =
|
||||
false;
|
||||
setIsButtonLoading(
|
||||
isButtonLoading
|
||||
);
|
||||
setError(
|
||||
(err as Error)
|
||||
.message
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
{error && (
|
||||
<ConfirmModal
|
||||
title={`Error`}
|
||||
description={error}
|
||||
submitButtonText={'Close'}
|
||||
onSubmit={() => {
|
||||
return setError('');
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{error && (
|
||||
<ConfirmModal
|
||||
title={`Error`}
|
||||
description={error}
|
||||
submitButtonText={'Close'}
|
||||
onSubmit={() => {
|
||||
return setError('');
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
);
|
||||
};
|
||||
|
||||
if (props.enableDragAndDrop) {
|
||||
return (
|
||||
<Draggable
|
||||
draggableId={
|
||||
(props.item[props.dragDropIdField || ''] as string) || ''
|
||||
}
|
||||
index={
|
||||
(props.item[props.dragDropIndexField || 0] as number) || 0
|
||||
}
|
||||
>
|
||||
{(provided: DraggableProvided) => {
|
||||
return getRow(provided);
|
||||
}}
|
||||
</Draggable>
|
||||
);
|
||||
}
|
||||
|
||||
return getRow();
|
||||
};
|
||||
|
||||
export default ListRow;
|
||||
|
||||
@@ -8,6 +8,7 @@ import Query from '../../Utils/ModelAPI/Query';
|
||||
|
||||
import IconProp from 'Common/Types/Icon/IconProp';
|
||||
import FieldType from '../Types/FieldType';
|
||||
import { DropdownOption } from '../Dropdown/Dropdown';
|
||||
|
||||
export interface ActionButton {
|
||||
buttonText: string;
|
||||
@@ -33,6 +34,7 @@ export default interface Columns<TEntity> {
|
||||
value: string;
|
||||
}
|
||||
| undefined;
|
||||
filterDropdownOptions?: Array<DropdownOption> | undefined;
|
||||
actionButtons?: Array<ActionButton>;
|
||||
alignItem?: AlignItem | undefined;
|
||||
noValueMessage?: string | undefined;
|
||||
|
||||
@@ -61,6 +61,7 @@ import ErrorMessage from '../ErrorMessage/ErrorMessage';
|
||||
import { DropdownOption } from '../Dropdown/Dropdown';
|
||||
import { FormStep } from '../Forms/Types/FormStep';
|
||||
import URL from 'Common/Types/API/URL';
|
||||
import { ListDetailProps } from '../List/ListRow';
|
||||
|
||||
export enum ShowTableAs {
|
||||
Table,
|
||||
@@ -79,6 +80,7 @@ export interface ComponentProps<TBaseModel extends BaseModel> {
|
||||
| ((data: Array<TBaseModel>, totalCount: number) => void);
|
||||
cardProps?: CardComponentProps | undefined;
|
||||
columns: Columns<TBaseModel>;
|
||||
listDetailOptions?: undefined | ListDetailProps;
|
||||
selectMoreFields?: Select<TBaseModel>;
|
||||
initialItemsOnPage?: number;
|
||||
isDeleteable: boolean;
|
||||
@@ -300,6 +302,10 @@ const ModelTable: Function = <TBaseModel extends BaseModel>(
|
||||
return i.key === columnKey;
|
||||
});
|
||||
|
||||
if (column.filterDropdownOptions) {
|
||||
filterDropdownOptions = column.filterDropdownOptions;
|
||||
}
|
||||
|
||||
if (
|
||||
tableColumns &&
|
||||
existingTableColumn &&
|
||||
@@ -400,12 +406,7 @@ const ModelTable: Function = <TBaseModel extends BaseModel>(
|
||||
continue;
|
||||
}
|
||||
|
||||
if (
|
||||
!(
|
||||
column.type === FieldType.Entity ||
|
||||
column.type === FieldType.EntityArray
|
||||
)
|
||||
) {
|
||||
if (!column.filterEntityType) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -413,13 +414,6 @@ const ModelTable: Function = <TBaseModel extends BaseModel>(
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!column.filterEntityType) {
|
||||
Logger.warn(
|
||||
`Cannot filter on ${key} because column.filterEntityType is not set.`
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!column.filterDropdownField) {
|
||||
Logger.warn(
|
||||
`Cannot filter on ${key} because column.dropdownField is not set.`
|
||||
@@ -1129,6 +1123,27 @@ const ModelTable: Function = <TBaseModel extends BaseModel>(
|
||||
pluralLabel={props.pluralName || model.pluralName || 'Items'}
|
||||
error={error}
|
||||
currentPageNumber={currentPageNumber}
|
||||
listDetailOptions={props.listDetailOptions}
|
||||
enableDragAndDrop={props.enableDragAndDrop}
|
||||
onDragDrop={async (id: string, newOrder: number) => {
|
||||
if (!props.dragDropIndexField) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
|
||||
await ModelAPI.updateById(
|
||||
props.modelType,
|
||||
new ObjectID(id),
|
||||
{
|
||||
[props.dragDropIndexField]: newOrder,
|
||||
}
|
||||
);
|
||||
|
||||
fetchItems();
|
||||
}}
|
||||
dragDropIdField={'_id'}
|
||||
dragDropIndexField={props.dragDropIndexField}
|
||||
isLoading={isLoading}
|
||||
totalItemsCount={totalItemsCount}
|
||||
data={JSONFunctions.toJSONObjectArray(data, props.modelType)}
|
||||
@@ -1270,7 +1285,7 @@ const ModelTable: Function = <TBaseModel extends BaseModel>(
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="mb-10">{getCardComponent()}</div>
|
||||
<div className="mb-5 mt-5">{getCardComponent()}</div>
|
||||
|
||||
{showModel ? (
|
||||
<ModelFormModal<TBaseModel>
|
||||
|
||||
@@ -14,7 +14,7 @@ const SideMenu: FunctionComponent<ComponentProps> = (props: ComponentProps) => {
|
||||
|
||||
return (
|
||||
<aside className="py-6 px-2 sm:px-6 lg:col-span-2 md:col-span-3 lg:py-0 lg:px-0 mb-10">
|
||||
<nav className="space-y-1">
|
||||
<nav className="space-y-3">
|
||||
{children.map((child: ReactElement) => {
|
||||
return child;
|
||||
})}
|
||||
|
||||
@@ -14,6 +14,8 @@ export interface ComponentProps {
|
||||
badgeType?: BadgeType | undefined;
|
||||
icon?: undefined | IconProp;
|
||||
className?: undefined | string;
|
||||
subItemLink?: undefined | Link;
|
||||
subItemIcon?: undefined | IconProp;
|
||||
}
|
||||
|
||||
const SideMenuItem: FunctionComponent<ComponentProps> = (
|
||||
@@ -27,6 +29,12 @@ const SideMenuItem: FunctionComponent<ComponentProps> = (
|
||||
linkClassName = `bg-gray-100 text-indigo-600 hover:bg-white group rounded-md px-3 py-2 flex items-center text-sm font-medium`;
|
||||
}
|
||||
|
||||
let subItemLinkClassName: string = `text-gray-500 hover:text-gray-900 hover:bg-gray-100 group rounded-md px-3 py-2 flex items-center text-sm font-medium`;
|
||||
|
||||
if (props.subItemLink && Navigation.isOnThisPage(props.subItemLink.to)) {
|
||||
subItemLinkClassName = `bg-gray-100 text-indigo-600 hover:bg-white group rounded-md px-3 py-2 flex items-center text-sm font-medium`;
|
||||
}
|
||||
|
||||
// if(props.badge && props.badge > 0){
|
||||
// if(props.badgeType === BadgeType.DANGER){
|
||||
// linkClassName = `text-red-400 hover:text-red-600 hover:bg-gray-100 group rounded-md px-3 py-2 flex items-center text-sm font-medium`;
|
||||
@@ -61,6 +69,14 @@ const SideMenuItem: FunctionComponent<ComponentProps> = (
|
||||
iconClassName = 'text-indigo-500 flex-shrink-0 -ml-1 mr-3 h-6 w-6';
|
||||
}
|
||||
|
||||
let subItemIconClassName: string =
|
||||
'text-gray-400 group-hover:text-gray-500 flex-shrink-0 -ml-1 mr-3 h-6 w-6';
|
||||
|
||||
if (props.subItemLink && Navigation.isOnThisPage(props.subItemLink.to)) {
|
||||
subItemIconClassName =
|
||||
'text-indigo-500 flex-shrink-0 -ml-1 mr-3 h-6 w-6';
|
||||
}
|
||||
|
||||
// if(props.badge && props.badge > 0){
|
||||
// if(props.badgeType === BadgeType.DANGER){
|
||||
// iconClassName = `text-red-400 group-hover:text-red-500 flex-shrink-0 -ml-1 mr-3 h-6 w-6`;
|
||||
@@ -89,60 +105,91 @@ const SideMenuItem: FunctionComponent<ComponentProps> = (
|
||||
// }
|
||||
|
||||
return (
|
||||
<UILink
|
||||
className={`${
|
||||
props.className ? props.className : ''
|
||||
} ${linkClassName} flex justify-between`}
|
||||
to={props.link.to}
|
||||
>
|
||||
<div className="flex">
|
||||
{props.icon ? (
|
||||
<>
|
||||
<Icon className={iconClassName} icon={props.icon} />
|
||||
</>
|
||||
<>
|
||||
<UILink
|
||||
className={`${
|
||||
props.className ? props.className : ''
|
||||
} ${linkClassName} flex justify-between`}
|
||||
to={props.link.to}
|
||||
>
|
||||
<div className="flex">
|
||||
{props.icon ? (
|
||||
<>
|
||||
<Icon className={iconClassName} icon={props.icon} />
|
||||
</>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
|
||||
<span className="truncate mt-1">{props.link.title}</span>
|
||||
</div>
|
||||
|
||||
{props.badge || props.showAlert || props.showWarning ? (
|
||||
<div className={badgeClasName}>
|
||||
{props.badge ? (
|
||||
<Badge
|
||||
badgeCount={props.badge}
|
||||
badgeType={props.badgeType}
|
||||
/>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
|
||||
{props.showAlert ? (
|
||||
<>
|
||||
<Icon
|
||||
className="float-end text-red-900"
|
||||
icon={IconProp.Error}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
{props.showWarning ? (
|
||||
<>
|
||||
<Icon
|
||||
className="float-end text-yellow-900"
|
||||
icon={IconProp.Alert}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</UILink>
|
||||
{props.subItemLink ? (
|
||||
<UILink
|
||||
className={`${
|
||||
props.className ? props.className : ''
|
||||
} ${subItemLinkClassName} flex justify-between`}
|
||||
to={props.subItemLink.to}
|
||||
>
|
||||
<div className="ml-8 flex">
|
||||
{props.icon ? (
|
||||
<>
|
||||
<Icon
|
||||
className={subItemIconClassName}
|
||||
icon={
|
||||
props.subItemIcon || IconProp.MinusSmall
|
||||
}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
|
||||
<span className="truncate mt-1">{props.link.title}</span>
|
||||
</div>
|
||||
|
||||
{props.badge || props.showAlert || props.showWarning ? (
|
||||
<div className={badgeClasName}>
|
||||
{props.badge ? (
|
||||
<Badge
|
||||
badgeCount={props.badge}
|
||||
badgeType={props.badgeType}
|
||||
/>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
|
||||
{props.showAlert ? (
|
||||
<>
|
||||
<Icon
|
||||
className="float-end text-red-900"
|
||||
icon={IconProp.Error}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
{props.showWarning ? (
|
||||
<>
|
||||
<Icon
|
||||
className="float-end text-yellow-900"
|
||||
icon={IconProp.Alert}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</div>
|
||||
<span className="truncate mt-1">
|
||||
{props.subItemLink.title}
|
||||
</span>
|
||||
</div>
|
||||
</UILink>
|
||||
) : (
|
||||
<></>
|
||||
<> </>
|
||||
)}
|
||||
</UILink>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -9,10 +9,10 @@ const SideMenuItem: FunctionComponent<ComponentProps> = (
|
||||
props: ComponentProps
|
||||
) => {
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<h6 className="text-sm text-gray-500">{props.title}</h6>
|
||||
<div>{props.children}</div>
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -147,6 +147,58 @@ const Filter: FunctionComponent<ComponentProps> = (
|
||||
/>
|
||||
)}
|
||||
|
||||
{column.type !== FieldType.Entity &&
|
||||
column.type !==
|
||||
FieldType.EntityArray &&
|
||||
column.filterDropdownOptions && (
|
||||
<Dropdown
|
||||
options={
|
||||
column.filterDropdownOptions
|
||||
}
|
||||
onChange={(
|
||||
value:
|
||||
| DropdownValue
|
||||
| Array<DropdownValue>
|
||||
| null
|
||||
) => {
|
||||
if (!column.key) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
!value ||
|
||||
(Array.isArray(
|
||||
value
|
||||
) &&
|
||||
value.length ===
|
||||
0)
|
||||
) {
|
||||
delete filterData[
|
||||
column.key
|
||||
];
|
||||
} else {
|
||||
filterData[
|
||||
column.key
|
||||
] = value;
|
||||
}
|
||||
|
||||
setFilterData(
|
||||
filterData
|
||||
);
|
||||
|
||||
if (
|
||||
props.onFilterChanged
|
||||
) {
|
||||
props.onFilterChanged(
|
||||
filterData
|
||||
);
|
||||
}
|
||||
}}
|
||||
isMultiSelect={false}
|
||||
placeholder={`Filter by ${column.title}`}
|
||||
/>
|
||||
)}
|
||||
|
||||
{column.type === FieldType.Boolean && (
|
||||
<Dropdown
|
||||
options={[
|
||||
@@ -216,7 +268,7 @@ const Filter: FunctionComponent<ComponentProps> = (
|
||||
) {
|
||||
filterData[
|
||||
column.key
|
||||
] = OneUptimeDate.asDateForDatabaseQuery(
|
||||
] = OneUptimeDate.asFilterDateForDatabaseQuery(
|
||||
changedValue as string
|
||||
);
|
||||
}
|
||||
|
||||
@@ -29,6 +29,8 @@ enum FieldType {
|
||||
DictionaryOfStrings = 'DictionaryOfStrings',
|
||||
JSON = 'JSON',
|
||||
USDCents = 'USDCents',
|
||||
Element = 'Element',
|
||||
Minutes = 'Minutes',
|
||||
}
|
||||
|
||||
export default FieldType;
|
||||
|
||||
@@ -114,6 +114,8 @@ import UserProfilePassword from './Pages/Global/UserProfile/Password';
|
||||
|
||||
// On Call Duty
|
||||
import OnCallDutyPoliciesPage from './Pages/OnCallDuty/OnCallDutyPolicies';
|
||||
import OnCallDutyExecutionLogs from './Pages/OnCallDuty/OnCallDutyExecutionLogs';
|
||||
import OnCallDutyPolicyExecutionLogTimeline from './Pages/OnCallDuty/OnCallDutyExecutionLogView';
|
||||
import OnCallDutyPolicyView from './Pages/OnCallDuty/OnCallDutyPolicy/Index';
|
||||
import OnCallDutyPolicyViewDelete from './Pages/OnCallDuty/OnCallDutyPolicy/Delete';
|
||||
import OnCallDutyPolicyViewLogs from './Pages/OnCallDuty/OnCallDutyPolicy/ExecutionLogs';
|
||||
@@ -152,6 +154,8 @@ import PageComponentProps from './Pages/PageComponentProps';
|
||||
|
||||
import UserSettingsNotificationMethods from './Pages/UserSettings/NotificationMethods';
|
||||
import UserSettingsNotificationRules from './Pages/UserSettings/OnCallRules';
|
||||
import UserSettingsNotificationLogs from './Pages/UserSettings/NotificationLogs';
|
||||
import UserSettingsNotificationLogsTimeline from './Pages/UserSettings/NotificationLogsTimeline';
|
||||
|
||||
const App: FunctionComponent = () => {
|
||||
Navigation.setNavigateHook(useNavigate());
|
||||
@@ -1821,9 +1825,43 @@ const App: FunctionComponent = () => {
|
||||
path={RouteMap[PageMap.ON_CALL_DUTY]?.toString() || ''}
|
||||
element={
|
||||
<OnCallDutyPoliciesPage
|
||||
{...commonPageProps}
|
||||
pageRoute={RouteMap[PageMap.ON_CALL_DUTY] as Route}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
<PageRoute
|
||||
path={
|
||||
RouteMap[
|
||||
PageMap.ON_CALL_DUTY_EXECUTION_LOGS
|
||||
]?.toString() || ''
|
||||
}
|
||||
element={
|
||||
<OnCallDutyExecutionLogs
|
||||
{...commonPageProps}
|
||||
pageRoute={
|
||||
RouteMap[PageMap.SETTINGS_TEAM_VIEW] as Route
|
||||
RouteMap[
|
||||
PageMap.ON_CALL_DUTY_EXECUTION_LOGS
|
||||
] as Route
|
||||
}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
<PageRoute
|
||||
path={
|
||||
RouteMap[
|
||||
PageMap.ON_CALL_DUTY_EXECUTION_LOGS_TIMELINE
|
||||
]?.toString() || ''
|
||||
}
|
||||
element={
|
||||
<OnCallDutyPolicyExecutionLogTimeline
|
||||
{...commonPageProps}
|
||||
pageRoute={
|
||||
RouteMap[
|
||||
PageMap.ON_CALL_DUTY_EXECUTION_LOGS_TIMELINE
|
||||
] as Route
|
||||
}
|
||||
/>
|
||||
}
|
||||
@@ -2030,6 +2068,43 @@ const App: FunctionComponent = () => {
|
||||
}
|
||||
/>
|
||||
|
||||
<PageRoute
|
||||
path={
|
||||
RouteMap[
|
||||
PageMap.USER_SETTINGS_NOTIFICATION_LOGS
|
||||
]?.toString() || ''
|
||||
}
|
||||
element={
|
||||
<UserSettingsNotificationLogs
|
||||
{...commonPageProps}
|
||||
pageRoute={
|
||||
RouteMap[
|
||||
PageMap.USER_SETTINGS_NOTIFICATION_LOGS
|
||||
] as Route
|
||||
}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
<PageRoute
|
||||
path={
|
||||
RouteMap[
|
||||
PageMap.USER_SETTINGS_NOTIFICATION_LOGS_TIMELINE
|
||||
]?.toString() || ''
|
||||
}
|
||||
element={
|
||||
<UserSettingsNotificationLogsTimeline
|
||||
{...commonPageProps}
|
||||
pageRoute={
|
||||
RouteMap[
|
||||
PageMap
|
||||
.USER_SETTINGS_NOTIFICATION_LOGS_TIMELINE
|
||||
] as Route
|
||||
}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
<PageRoute
|
||||
path={
|
||||
RouteMap[
|
||||
|
||||
@@ -16,6 +16,7 @@ export interface ComponentProps {
|
||||
onChange?: undefined | ((value: MonitorCriteria) => void);
|
||||
monitorStatusDropdownOptions: Array<DropdownOption>;
|
||||
incidentSeverityDropdownOptions: Array<DropdownOption>;
|
||||
onCallPolicyDropdownOptions: Array<DropdownOption>;
|
||||
monitorType: MonitorType;
|
||||
}
|
||||
|
||||
@@ -50,6 +51,9 @@ const MonitorCriteriaElement: FunctionComponent<ComponentProps> = (
|
||||
incidentSeverityDropdownOptions={
|
||||
props.incidentSeverityDropdownOptions
|
||||
}
|
||||
onCallPolicyDropdownOptions={
|
||||
props.onCallPolicyDropdownOptions
|
||||
}
|
||||
initialValue={i}
|
||||
onDelete={() => {
|
||||
if (
|
||||
|
||||
@@ -11,6 +11,7 @@ export interface ComponentProps {
|
||||
initialValue?: undefined | CriteriaIncident;
|
||||
onChange?: undefined | ((value: CriteriaIncident) => void);
|
||||
incidentSeverityDropdownOptions: Array<DropdownOption>;
|
||||
onCallPolicyDropdownOptions: Array<DropdownOption>;
|
||||
// onDelete?: undefined | (() => void);
|
||||
}
|
||||
|
||||
@@ -79,6 +80,19 @@ const MonitorCriteriaIncidentForm: FunctionComponent<ComponentProps> = (
|
||||
required: true,
|
||||
placeholder: 'Incident Severity',
|
||||
},
|
||||
{
|
||||
field: {
|
||||
onCallPolicyIds: true,
|
||||
},
|
||||
title: 'On Call Policy',
|
||||
stepId: 'incident-details',
|
||||
description:
|
||||
'Execute these on call policies when this incident is created.',
|
||||
fieldType: FormFieldSchemaType.MultiSelectDropdown,
|
||||
dropdownOptions: props.onCallPolicyDropdownOptions,
|
||||
required: false,
|
||||
placeholder: 'Select On Call Policies',
|
||||
},
|
||||
{
|
||||
field: {
|
||||
autoResolveIncident: true,
|
||||
|
||||
@@ -8,6 +8,7 @@ export interface ComponentProps {
|
||||
initialValue: Array<CriteriaIncident> | undefined;
|
||||
onChange?: undefined | ((value: Array<CriteriaIncident>) => void);
|
||||
incidentSeverityDropdownOptions: Array<DropdownOption>;
|
||||
onCallPolicyDropdownOptions: Array<DropdownOption>;
|
||||
}
|
||||
|
||||
const MonitorCriteriaIncidentsForm: FunctionComponent<ComponentProps> = (
|
||||
@@ -39,6 +40,9 @@ const MonitorCriteriaIncidentsForm: FunctionComponent<ComponentProps> = (
|
||||
incidentSeverityDropdownOptions={
|
||||
props.incidentSeverityDropdownOptions
|
||||
}
|
||||
onCallPolicyDropdownOptions={
|
||||
props.onCallPolicyDropdownOptions
|
||||
}
|
||||
initialValue={i}
|
||||
// onDelete={() => {
|
||||
// // remove the criteria filter
|
||||
|
||||
@@ -35,6 +35,7 @@ import MonitorType from 'Common/Types/Monitor/MonitorType';
|
||||
export interface ComponentProps {
|
||||
monitorStatusDropdownOptions: Array<DropdownOption>;
|
||||
incidentSeverityDropdownOptions: Array<DropdownOption>;
|
||||
onCallPolicyDropdownOptions: Array<DropdownOption>;
|
||||
monitorType: MonitorType;
|
||||
initialValue?: undefined | MonitorCriteriaInstance;
|
||||
onChange?: undefined | ((value: MonitorCriteriaInstance) => void);
|
||||
@@ -364,6 +365,9 @@ const MonitorCriteriaInstanceElement: FunctionComponent<ComponentProps> = (
|
||||
incidentSeverityDropdownOptions={
|
||||
props.incidentSeverityDropdownOptions
|
||||
}
|
||||
onCallPolicyDropdownOptions={
|
||||
props.onCallPolicyDropdownOptions
|
||||
}
|
||||
onChange={(value: Array<CriteriaIncident>) => {
|
||||
monitorCriteriaInstance.setIncidents(value);
|
||||
setMonitorCriteriaInstance(
|
||||
|
||||
@@ -30,6 +30,7 @@ import Hostname from 'Common/Types/API/Hostname';
|
||||
export interface ComponentProps {
|
||||
monitorStatusDropdownOptions: Array<DropdownOption>;
|
||||
incidentSeverityDropdownOptions: Array<DropdownOption>;
|
||||
onCallPolicyDropdownOptions: Array<DropdownOption>;
|
||||
initialValue?: undefined | MonitorStep;
|
||||
onChange?: undefined | ((value: MonitorStep) => void);
|
||||
// onDelete?: undefined | (() => void);
|
||||
@@ -310,6 +311,9 @@ const MonitorStepElement: FunctionComponent<ComponentProps> = (
|
||||
incidentSeverityDropdownOptions={
|
||||
props.incidentSeverityDropdownOptions
|
||||
}
|
||||
onCallPolicyDropdownOptions={
|
||||
props.onCallPolicyDropdownOptions
|
||||
}
|
||||
initialValue={monitorStep?.data?.monitorCriteria}
|
||||
onChange={(value: MonitorCriteria) => {
|
||||
monitorStep.setMonitorCriteria(value);
|
||||
|
||||
@@ -18,6 +18,7 @@ import HorizontalRule from 'CommonUI/src/Components/HorizontalRule/HorizontalRul
|
||||
import FieldLabelElement from 'CommonUI/src/Components/Forms/Fields/FieldLabel';
|
||||
import ObjectID from 'Common/Types/ObjectID';
|
||||
import SortOrder from 'Common/Types/Database/SortOrder';
|
||||
import OnCallDutyPolicy from 'Model/Models/OnCallDutyPolicy';
|
||||
|
||||
export interface ComponentProps extends CustomElementProps {
|
||||
error?: string | undefined;
|
||||
@@ -38,6 +39,9 @@ const MonitorStepsElement: FunctionComponent<ComponentProps> = (
|
||||
setIncidentSeverityDropdownOptions,
|
||||
] = React.useState<Array<DropdownOption>>([]);
|
||||
|
||||
const [onCallPolicyDropdownOptions, setOnCallPolicyDropdownOptions] =
|
||||
React.useState<Array<DropdownOption>>([]);
|
||||
|
||||
const [isLoading, setIsLoading] = React.useState<boolean>(false);
|
||||
const [error, setError] = React.useState<string>();
|
||||
|
||||
@@ -90,6 +94,18 @@ const MonitorStepsElement: FunctionComponent<ComponentProps> = (
|
||||
}
|
||||
);
|
||||
|
||||
const onCallPolicyList: ListResult<OnCallDutyPolicy> =
|
||||
await ModelAPI.getList(
|
||||
OnCallDutyPolicy,
|
||||
{},
|
||||
LIMIT_PER_PROJECT,
|
||||
0,
|
||||
{
|
||||
name: true,
|
||||
},
|
||||
{}
|
||||
);
|
||||
|
||||
if (incidentSeverityList.data) {
|
||||
setIncidentSeverityDropdownOptions(
|
||||
incidentSeverityList.data.map((i: IncidentSeverity) => {
|
||||
@@ -101,6 +117,17 @@ const MonitorStepsElement: FunctionComponent<ComponentProps> = (
|
||||
);
|
||||
}
|
||||
|
||||
if (onCallPolicyList.data) {
|
||||
setOnCallPolicyDropdownOptions(
|
||||
onCallPolicyList.data.map((i: OnCallDutyPolicy) => {
|
||||
return {
|
||||
value: i._id!,
|
||||
label: i.name!,
|
||||
};
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
// if there is no initial value then....
|
||||
|
||||
if (!monitorSteps) {
|
||||
@@ -179,6 +206,9 @@ const MonitorStepsElement: FunctionComponent<ComponentProps> = (
|
||||
incidentSeverityDropdownOptions={
|
||||
incidentSeverityDropdownOptions
|
||||
}
|
||||
onCallPolicyDropdownOptions={
|
||||
onCallPolicyDropdownOptions
|
||||
}
|
||||
initialValue={i}
|
||||
// onDelete={() => {
|
||||
// // remove the criteria filter
|
||||
|
||||
37
Dashboard/src/Components/Incident/Incident.tsx
Normal file
37
Dashboard/src/Components/Incident/Incident.tsx
Normal file
@@ -0,0 +1,37 @@
|
||||
import React, { FunctionComponent, ReactElement } from 'react';
|
||||
import Link from 'CommonUI/src/Components/Link/Link';
|
||||
import Route from 'Common/Types/API/Route';
|
||||
import RouteMap, { RouteUtil } from '../../Utils/RouteMap';
|
||||
import PageMap from '../../Utils/PageMap';
|
||||
import ObjectID from 'Common/Types/ObjectID';
|
||||
import Incident from 'Model/Models/Incident';
|
||||
|
||||
export interface ComponentProps {
|
||||
incident: Incident;
|
||||
onNavigateComplete?: (() => void) | undefined;
|
||||
}
|
||||
|
||||
const IncidentElement: FunctionComponent<ComponentProps> = (
|
||||
props: ComponentProps
|
||||
): ReactElement => {
|
||||
if (props.incident._id) {
|
||||
return (
|
||||
<Link
|
||||
onNavigateComplete={props.onNavigateComplete}
|
||||
className="hover:underline"
|
||||
to={RouteUtil.populateRouteParams(
|
||||
RouteMap[PageMap.INCIDENT_VIEW] as Route,
|
||||
{
|
||||
modelId: new ObjectID(props.incident._id as string),
|
||||
}
|
||||
)}
|
||||
>
|
||||
<span>{props.incident.title}</span>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
return <span>{props.incident.title}</span>;
|
||||
};
|
||||
|
||||
export default IncidentElement;
|
||||
@@ -22,6 +22,7 @@ import EventName from '../../Utils/EventName';
|
||||
import DashboardNavigation from '../../Utils/Navigation';
|
||||
import Team from 'Model/Models/Team';
|
||||
import ProjectUser from '../../Utils/ProjectUser';
|
||||
import OnCallDutyPolicy from 'Model/Models/OnCallDutyPolicy';
|
||||
|
||||
export interface ComponentProps {
|
||||
query?: Query<Incident> | undefined;
|
||||
@@ -66,6 +67,10 @@ const IncidentsTable: FunctionComponent<ComponentProps> = (
|
||||
title: 'Resources Affected',
|
||||
id: 'resources-affected',
|
||||
},
|
||||
{
|
||||
title: 'On Call',
|
||||
id: 'on-call',
|
||||
},
|
||||
{
|
||||
title: 'Owners',
|
||||
id: 'owners',
|
||||
@@ -130,6 +135,23 @@ const IncidentsTable: FunctionComponent<ComponentProps> = (
|
||||
required: true,
|
||||
placeholder: 'Monitors affected',
|
||||
},
|
||||
{
|
||||
field: {
|
||||
onCallDutyPolicies: true,
|
||||
},
|
||||
title: 'On Call Policy',
|
||||
stepId: 'on-call',
|
||||
description:
|
||||
'Select on call duty policy to execute when this incident is created.',
|
||||
fieldType: FormFieldSchemaType.MultiSelectDropdown,
|
||||
dropdownModal: {
|
||||
type: OnCallDutyPolicy,
|
||||
labelField: 'name',
|
||||
valueField: '_id',
|
||||
},
|
||||
required: false,
|
||||
placeholder: 'Select on call policies',
|
||||
},
|
||||
{
|
||||
field: {
|
||||
changeMonitorStatusTo: true,
|
||||
|
||||
@@ -2,6 +2,9 @@ import React, { FunctionComponent, ReactElement } from 'react';
|
||||
import Monitor from 'Model/Models/Monitor';
|
||||
import Link from 'CommonUI/src/Components/Link/Link';
|
||||
import Route from 'Common/Types/API/Route';
|
||||
import RouteMap, { RouteUtil } from '../../Utils/RouteMap';
|
||||
import PageMap from '../../Utils/PageMap';
|
||||
import ObjectID from 'Common/Types/ObjectID';
|
||||
|
||||
export interface ComponentProps {
|
||||
monitor: Monitor;
|
||||
@@ -11,25 +14,17 @@ export interface ComponentProps {
|
||||
const MonitorElement: FunctionComponent<ComponentProps> = (
|
||||
props: ComponentProps
|
||||
): ReactElement => {
|
||||
if (
|
||||
props.monitor._id &&
|
||||
(props.monitor.projectId ||
|
||||
(props.monitor.project && props.monitor.project._id))
|
||||
) {
|
||||
const projectId: string | undefined = props.monitor.projectId
|
||||
? props.monitor.projectId.toString()
|
||||
: props.monitor.project
|
||||
? props.monitor.project._id
|
||||
: '';
|
||||
if (props.monitor._id) {
|
||||
return (
|
||||
<Link
|
||||
onNavigateComplete={props.onNavigateComplete}
|
||||
className="underline-on-hover"
|
||||
to={
|
||||
new Route(
|
||||
`/dashboard/${projectId}/monitors/${props.monitor._id}`
|
||||
)
|
||||
}
|
||||
className="hover:underline"
|
||||
to={RouteUtil.populateRouteParams(
|
||||
RouteMap[PageMap.MONITOR_VIEW] as Route,
|
||||
{
|
||||
modelId: new ObjectID(props.monitor._id as string),
|
||||
}
|
||||
)}
|
||||
>
|
||||
<span>{props.monitor.name}</span>
|
||||
</Link>
|
||||
|
||||
@@ -5,11 +5,13 @@ import MonitorCriteriaInstance from 'Common/Types/Monitor/MonitorCriteriaInstanc
|
||||
import Text from 'Common/Types/Text';
|
||||
import MonitorStatus from 'Model/Models/MonitorStatus';
|
||||
import IncidentSeverity from 'Model/Models/IncidentSeverity';
|
||||
import OnCallDutyPolicy from 'Model/Models/OnCallDutyPolicy';
|
||||
|
||||
export interface ComponentProps {
|
||||
monitorCriteria: MonitorCriteria;
|
||||
monitorStatusOptions: Array<MonitorStatus>;
|
||||
incidentSeverityOptions: Array<IncidentSeverity>;
|
||||
onCallPolicyOptions: Array<OnCallDutyPolicy>;
|
||||
}
|
||||
|
||||
const MonitorCriteriaElement: FunctionComponent<ComponentProps> = (
|
||||
@@ -40,6 +42,9 @@ const MonitorCriteriaElement: FunctionComponent<ComponentProps> = (
|
||||
monitorStatusOptions={
|
||||
props.monitorStatusOptions
|
||||
}
|
||||
onCallPolicyOptions={
|
||||
props.onCallPolicyOptions
|
||||
}
|
||||
incidentSeverityOptions={
|
||||
props.incidentSeverityOptions
|
||||
}
|
||||
|
||||
@@ -7,10 +7,14 @@ import { JSONObject } from 'Common/Types/JSON';
|
||||
import Pill from 'CommonUI/src/Components/Pill/Pill';
|
||||
import { Black } from 'Common/Types/BrandColors';
|
||||
import Color from 'Common/Types/Color';
|
||||
import OnCallDutyPolicy from 'Model/Models/OnCallDutyPolicy';
|
||||
import OnCallDutyPoliciesView from '../../OnCallPolicy/OnCallPolicies';
|
||||
import ObjectID from 'Common/Types/ObjectID';
|
||||
|
||||
export interface ComponentProps {
|
||||
incident: CriteriaIncident;
|
||||
incidentSeverityOptions: Array<IncidentSeverity>;
|
||||
onCallPolicyOptions: Array<OnCallDutyPolicy>;
|
||||
}
|
||||
|
||||
const MonitorCriteriaIncidentForm: FunctionComponent<ComponentProps> = (
|
||||
@@ -84,6 +88,34 @@ const MonitorCriteriaIncidentForm: FunctionComponent<ComponentProps> = (
|
||||
fieldType: FieldType.Boolean,
|
||||
placeholder: 'No',
|
||||
},
|
||||
{
|
||||
key: 'onCallPolicyIds',
|
||||
title: 'On Call Policies',
|
||||
description:
|
||||
'These are the on call policies that will be executed when this incident is created.',
|
||||
fieldType: FieldType.Element,
|
||||
getElement: (item: JSONObject): ReactElement => {
|
||||
return (
|
||||
<OnCallDutyPoliciesView
|
||||
onCallPolicies={props.onCallPolicyOptions.filter(
|
||||
(policy: OnCallDutyPolicy) => {
|
||||
return (
|
||||
(item[
|
||||
'onCallPolicyIds'
|
||||
] as Array<ObjectID>) || []
|
||||
)
|
||||
.map((id: ObjectID) => {
|
||||
return id.toString();
|
||||
})
|
||||
.includes(
|
||||
policy.id?.toString() || ''
|
||||
);
|
||||
}
|
||||
)}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -2,10 +2,12 @@ import React, { FunctionComponent, ReactElement } from 'react';
|
||||
import MonitorCriteriaIncident from './MonitorCriteriaIncident';
|
||||
import { CriteriaIncident } from 'Common/Types/Monitor/CriteriaIncident';
|
||||
import IncidentSeverity from 'Model/Models/IncidentSeverity';
|
||||
import OnCallDutyPolicy from 'Model/Models/OnCallDutyPolicy';
|
||||
|
||||
export interface ComponentProps {
|
||||
incidents: Array<CriteriaIncident>;
|
||||
incidentSeverityOptions: Array<IncidentSeverity>;
|
||||
onCallPolicyOptions: Array<OnCallDutyPolicy>;
|
||||
}
|
||||
|
||||
const MonitorCriteriaIncidentsForm: FunctionComponent<ComponentProps> = (
|
||||
@@ -17,6 +19,7 @@ const MonitorCriteriaIncidentsForm: FunctionComponent<ComponentProps> = (
|
||||
return (
|
||||
<MonitorCriteriaIncident
|
||||
key={index}
|
||||
onCallPolicyOptions={props.onCallPolicyOptions}
|
||||
incidentSeverityOptions={props.incidentSeverityOptions}
|
||||
incident={i}
|
||||
/>
|
||||
|
||||
@@ -14,12 +14,14 @@ import Color from 'Common/Types/Color';
|
||||
import { Black } from 'Common/Types/BrandColors';
|
||||
import Statusbubble from 'CommonUI/src/Components/StatusBubble/StatusBubble';
|
||||
import { FilterCondition } from 'Common/Types/Monitor/CriteriaFilter';
|
||||
import OnCallDutyPolicy from 'Model/Models/OnCallDutyPolicy';
|
||||
|
||||
export interface ComponentProps {
|
||||
monitorStatusOptions: Array<MonitorStatus>;
|
||||
incidentSeverityOptions: Array<IncidentSeverity>;
|
||||
isLastCriteria: boolean;
|
||||
monitorCriteriaInstance: MonitorCriteriaInstance;
|
||||
onCallPolicyOptions: Array<OnCallDutyPolicy>;
|
||||
}
|
||||
|
||||
const MonitorCriteriaInstanceElement: FunctionComponent<ComponentProps> = (
|
||||
@@ -125,6 +127,7 @@ const MonitorCriteriaInstanceElement: FunctionComponent<ComponentProps> = (
|
||||
incidents={
|
||||
props.monitorCriteriaInstance?.data?.incidents || []
|
||||
}
|
||||
onCallPolicyOptions={props.onCallPolicyOptions}
|
||||
incidentSeverityOptions={props.incidentSeverityOptions}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -14,12 +14,14 @@ import FieldType from 'CommonUI/src/Components/Types/FieldType';
|
||||
import Field from 'CommonUI/src/Components/Detail/Field';
|
||||
import MonitorStatus from 'Model/Models/MonitorStatus';
|
||||
import IncidentSeverity from 'Model/Models/IncidentSeverity';
|
||||
import OnCallDutyPolicy from 'Model/Models/OnCallDutyPolicy';
|
||||
|
||||
export interface ComponentProps {
|
||||
monitorStatusOptions: Array<MonitorStatus>;
|
||||
incidentSeverityOptions: Array<IncidentSeverity>;
|
||||
monitorStep: MonitorStep;
|
||||
monitorType: MonitorType;
|
||||
onCallPolicyOptions: Array<OnCallDutyPolicy>;
|
||||
}
|
||||
|
||||
const MonitorStepElement: FunctionComponent<ComponentProps> = (
|
||||
@@ -130,6 +132,7 @@ const MonitorStepElement: FunctionComponent<ComponentProps> = (
|
||||
/>
|
||||
|
||||
<MonitorCriteriaElement
|
||||
onCallPolicyOptions={props.onCallPolicyOptions}
|
||||
monitorStatusOptions={props.monitorStatusOptions}
|
||||
incidentSeverityOptions={props.incidentSeverityOptions}
|
||||
monitorCriteria={props.monitorStep?.data?.monitorCriteria!}
|
||||
|
||||
@@ -21,6 +21,7 @@ import IconProp from 'Common/Types/Icon/IconProp';
|
||||
import Statusbubble from 'CommonUI/src/Components/StatusBubble/StatusBubble';
|
||||
import Color from 'Common/Types/Color';
|
||||
import { Black } from 'Common/Types/BrandColors';
|
||||
import OnCallDutyPolicy from 'Model/Models/OnCallDutyPolicy';
|
||||
|
||||
export interface ComponentProps extends CustomElementProps {
|
||||
monitorSteps: MonitorSteps;
|
||||
@@ -37,6 +38,10 @@ const MonitorStepsElement: FunctionComponent<ComponentProps> = (
|
||||
const [incidentSeverityOptions, setIncidentSeverityOptions] =
|
||||
React.useState<Array<IncidentSeverity>>([]);
|
||||
|
||||
const [onCallPolicyOptions, setOnCallPolicyOptions] = React.useState<
|
||||
Array<OnCallDutyPolicy>
|
||||
>([]);
|
||||
|
||||
const [isLoading, setIsLoading] = React.useState<boolean>(false);
|
||||
const [error, setError] = React.useState<string>('');
|
||||
|
||||
@@ -84,11 +89,29 @@ const MonitorStepsElement: FunctionComponent<ComponentProps> = (
|
||||
{}
|
||||
);
|
||||
|
||||
const onCallPolicyList: ListResult<OnCallDutyPolicy> =
|
||||
await ModelAPI.getList(
|
||||
OnCallDutyPolicy,
|
||||
{},
|
||||
LIMIT_PER_PROJECT,
|
||||
0,
|
||||
{
|
||||
name: true,
|
||||
},
|
||||
{}
|
||||
);
|
||||
|
||||
if (incidentSeverityList.data) {
|
||||
setIncidentSeverityOptions(
|
||||
incidentSeverityList.data as Array<IncidentSeverity>
|
||||
);
|
||||
}
|
||||
|
||||
if (onCallPolicyList.data) {
|
||||
setOnCallPolicyOptions(
|
||||
onCallPolicyList.data as Array<OnCallDutyPolicy>
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
setError(API.getFriendlyMessage(err));
|
||||
}
|
||||
@@ -122,6 +145,7 @@ const MonitorStepsElement: FunctionComponent<ComponentProps> = (
|
||||
monitorStatusOptions={monitorStatusOptions}
|
||||
incidentSeverityOptions={incidentSeverityOptions}
|
||||
monitorStep={i}
|
||||
onCallPolicyOptions={onCallPolicyOptions}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -129,7 +129,7 @@ const DashboardNavbar: FunctionComponent<ComponentProps> = (
|
||||
),
|
||||
}}
|
||||
>
|
||||
{/* <NavBarMenuItem
|
||||
<NavBarMenuItem
|
||||
title="On-Call Duty"
|
||||
description="Manage your on-call schedules, escalations and more."
|
||||
route={RouteUtil.populateRouteParams(
|
||||
@@ -139,7 +139,7 @@ const DashboardNavbar: FunctionComponent<ComponentProps> = (
|
||||
onClick={() => {
|
||||
forceHideMoreMenu();
|
||||
}}
|
||||
/> */}
|
||||
/>
|
||||
|
||||
<NavBarMenuItem
|
||||
title="Workflows"
|
||||
@@ -163,7 +163,7 @@ const DashboardNavbar: FunctionComponent<ComponentProps> = (
|
||||
forceHideMoreMenu();
|
||||
}}
|
||||
/>
|
||||
{/* <NavBarMenuItem
|
||||
<NavBarMenuItem
|
||||
title="User Settings"
|
||||
description="Review or manage user settings related to this project here."
|
||||
route={RouteUtil.populateRouteParams(
|
||||
@@ -173,7 +173,7 @@ const DashboardNavbar: FunctionComponent<ComponentProps> = (
|
||||
onClick={() => {
|
||||
forceHideMoreMenu();
|
||||
}}
|
||||
/> */}
|
||||
/>
|
||||
|
||||
{/* <NavBarMenuItem
|
||||
title="Logs Management"
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
import React, { FunctionComponent, ReactElement } from 'react';
|
||||
import BaseModel from 'Common/Models/BaseModel';
|
||||
import { JSONObject } from 'Common/Types/JSON';
|
||||
import JSONFunctions from 'Common/Types/JSONFunctions';
|
||||
|
||||
export interface ComponentProps {
|
||||
item: BaseModel;
|
||||
modelType: { new (): BaseModel };
|
||||
}
|
||||
|
||||
const NotificationMethodView: FunctionComponent<ComponentProps> = (
|
||||
props: ComponentProps
|
||||
): ReactElement => {
|
||||
const item: BaseModel = JSONFunctions.fromJSONObject(
|
||||
props.item,
|
||||
props.modelType
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
{item.getColumnValue('userEmail') &&
|
||||
(item.getColumnValue('userEmail') as JSONObject)['email'] && (
|
||||
<p>
|
||||
Email:{' '}
|
||||
{(item.getColumnValue('userEmail') as JSONObject)[
|
||||
'email'
|
||||
]?.toString()}
|
||||
</p>
|
||||
)}
|
||||
{item.getColumnValue('userCall') &&
|
||||
(item.getColumnValue('userCall') as JSONObject)['phone'] && (
|
||||
<p>
|
||||
Call:{' '}
|
||||
{(item.getColumnValue('userCall') as JSONObject)[
|
||||
'phone'
|
||||
]?.toString()}
|
||||
</p>
|
||||
)}
|
||||
{item.getColumnValue('userSms') &&
|
||||
(item.getColumnValue('userSms') as JSONObject)['phone'] && (
|
||||
<p>
|
||||
SMS:{' '}
|
||||
{(item.getColumnValue('userSms') as JSONObject)[
|
||||
'phone'
|
||||
]?.toString()}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default NotificationMethodView;
|
||||
@@ -0,0 +1,39 @@
|
||||
import React, { FunctionComponent, ReactElement } from 'react';
|
||||
import Link from 'CommonUI/src/Components/Link/Link';
|
||||
import Route from 'Common/Types/API/Route';
|
||||
import RouteMap, { RouteUtil } from '../../../Utils/RouteMap';
|
||||
import PageMap from '../../../Utils/PageMap';
|
||||
import ObjectID from 'Common/Types/ObjectID';
|
||||
import OnCallDutyPolicyEscalationRule from 'Model/Models/OnCallDutyPolicyEscalationRule';
|
||||
|
||||
export interface ComponentProps {
|
||||
escalationRule: OnCallDutyPolicyEscalationRule;
|
||||
onNavigateComplete?: (() => void) | undefined;
|
||||
}
|
||||
|
||||
const EscalationRuleView: FunctionComponent<ComponentProps> = (
|
||||
props: ComponentProps
|
||||
): ReactElement => {
|
||||
if (props.escalationRule.onCallDutyPolicyId) {
|
||||
return (
|
||||
<Link
|
||||
onNavigateComplete={props.onNavigateComplete}
|
||||
className="hover:underline"
|
||||
to={RouteUtil.populateRouteParams(
|
||||
RouteMap[PageMap.ON_CALL_DUTY_POLICY_VIEW] as Route,
|
||||
{
|
||||
modelId: new ObjectID(
|
||||
props.escalationRule.onCallDutyPolicyId.toString() as string
|
||||
),
|
||||
}
|
||||
)}
|
||||
>
|
||||
<span>{props.escalationRule.name}</span>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
return <span>{props.escalationRule.name}</span>;
|
||||
};
|
||||
|
||||
export default EscalationRuleView;
|
||||
@@ -0,0 +1,76 @@
|
||||
import React, { FunctionComponent, ReactElement, useState } from 'react';
|
||||
import ObjectID from 'Common/Types/ObjectID';
|
||||
import Team from 'Model/Models/Team';
|
||||
import ComponentLoader from 'CommonUI/src/Components/ComponentLoader/ComponentLoader';
|
||||
import ModelAPI, { ListResult } from 'CommonUI/src/Utils/ModelAPI/ModelAPI';
|
||||
import OnCallDutyPolicyEscalationRuleTeam from 'Model/Models/OnCallDutyPolicyEscalationRuleTeam';
|
||||
import { LIMIT_PER_PROJECT } from 'Common/Types/Database/LimitMax';
|
||||
import useAsyncEffect from 'use-async-effect';
|
||||
import API from 'CommonUI/src/Utils/API/API';
|
||||
import ErrorMessage from 'CommonUI/src/Components/ErrorMessage/ErrorMessage';
|
||||
import TeamsElement from '../../Team/TeamsElement';
|
||||
|
||||
export interface ComponentProps {
|
||||
escalationRuleId: ObjectID;
|
||||
}
|
||||
|
||||
const TeamView: FunctionComponent<ComponentProps> = (
|
||||
props: ComponentProps
|
||||
): ReactElement => {
|
||||
const [teams, setTeams] = useState<Array<Team>>([]);
|
||||
|
||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||
|
||||
const [error, setError] = useState<string>('');
|
||||
|
||||
useAsyncEffect(async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
|
||||
const onCallTeams: ListResult<OnCallDutyPolicyEscalationRuleTeam> =
|
||||
await ModelAPI.getList(
|
||||
OnCallDutyPolicyEscalationRuleTeam,
|
||||
{
|
||||
onCallDutyPolicyEscalationRuleId:
|
||||
props.escalationRuleId,
|
||||
},
|
||||
LIMIT_PER_PROJECT,
|
||||
0,
|
||||
{
|
||||
team: {
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
{}
|
||||
);
|
||||
|
||||
const teams: Array<Team> = onCallTeams.data.map(
|
||||
(onCallUser: OnCallDutyPolicyEscalationRuleTeam) => {
|
||||
return onCallUser.team!;
|
||||
}
|
||||
);
|
||||
|
||||
setTeams(teams);
|
||||
} catch (err) {
|
||||
setError(API.getFriendlyMessage(err));
|
||||
}
|
||||
|
||||
setIsLoading(false);
|
||||
}, []);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex justify-center w-full">
|
||||
<ComponentLoader />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return <ErrorMessage error={error} />;
|
||||
}
|
||||
|
||||
return <TeamsElement teams={teams} />;
|
||||
};
|
||||
|
||||
export default TeamView;
|
||||
@@ -0,0 +1,78 @@
|
||||
import React, { FunctionComponent, ReactElement, useState } from 'react';
|
||||
import ObjectID from 'Common/Types/ObjectID';
|
||||
import User from 'Model/Models/User';
|
||||
import ComponentLoader from 'CommonUI/src/Components/ComponentLoader/ComponentLoader';
|
||||
import ModelAPI, { ListResult } from 'CommonUI/src/Utils/ModelAPI/ModelAPI';
|
||||
import OnCallDutyPolicyEscalationRuleUser from 'Model/Models/OnCallDutyPolicyEscalationRuleUser';
|
||||
import { LIMIT_PER_PROJECT } from 'Common/Types/Database/LimitMax';
|
||||
import useAsyncEffect from 'use-async-effect';
|
||||
import API from 'CommonUI/src/Utils/API/API';
|
||||
import ErrorMessage from 'CommonUI/src/Components/ErrorMessage/ErrorMessage';
|
||||
import UsersElement from '../../User/Users';
|
||||
|
||||
export interface ComponentProps {
|
||||
escalationRuleId: ObjectID;
|
||||
}
|
||||
|
||||
const UserView: FunctionComponent<ComponentProps> = (
|
||||
props: ComponentProps
|
||||
): ReactElement => {
|
||||
const [users, setUsers] = useState<Array<User>>([]);
|
||||
|
||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||
|
||||
const [error, setError] = useState<string>('');
|
||||
|
||||
useAsyncEffect(async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
|
||||
const onCallUsers: ListResult<OnCallDutyPolicyEscalationRuleUser> =
|
||||
await ModelAPI.getList(
|
||||
OnCallDutyPolicyEscalationRuleUser,
|
||||
{
|
||||
onCallDutyPolicyEscalationRuleId:
|
||||
props.escalationRuleId,
|
||||
},
|
||||
LIMIT_PER_PROJECT,
|
||||
0,
|
||||
{
|
||||
user: {
|
||||
name: true,
|
||||
email: true,
|
||||
profilePictureId: true,
|
||||
},
|
||||
},
|
||||
{}
|
||||
);
|
||||
|
||||
const users: Array<User> = onCallUsers.data.map(
|
||||
(onCallUser: OnCallDutyPolicyEscalationRuleUser) => {
|
||||
return onCallUser.user!;
|
||||
}
|
||||
);
|
||||
|
||||
setUsers(users);
|
||||
} catch (err) {
|
||||
setError(API.getFriendlyMessage(err));
|
||||
}
|
||||
|
||||
setIsLoading(false);
|
||||
}, []);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex justify-center w-full">
|
||||
<ComponentLoader />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return <ErrorMessage error={error} />;
|
||||
}
|
||||
|
||||
return <UsersElement users={users} />;
|
||||
};
|
||||
|
||||
export default UserView;
|
||||
@@ -0,0 +1,248 @@
|
||||
import React, { FunctionComponent, ReactElement, useState } from 'react';
|
||||
import ModelTable from 'CommonUI/src/Components/ModelTable/ModelTable';
|
||||
import OnCallDutyPolicyExecutionLog from 'Model/Models/OnCallDutyPolicyExecutionLog';
|
||||
import DashboardNavigation from '../../../Utils/Navigation';
|
||||
import IconProp from 'Common/Types/Icon/IconProp';
|
||||
import FieldType from 'CommonUI/src/Components/Types/FieldType';
|
||||
import { JSONObject } from 'Common/Types/JSON';
|
||||
import Pill from 'CommonUI/src/Components/Pill/Pill';
|
||||
import { Green, Red, Yellow } from 'Common/Types/BrandColors';
|
||||
import { ButtonStyleType } from 'CommonUI/src/Components/Button/Button';
|
||||
import ConfirmModal from 'CommonUI/src/Components/Modal/ConfirmModal';
|
||||
import IncidentView from '../../../Components/Incident/Incident';
|
||||
import Incident from 'Model/Models/Incident';
|
||||
import OnCallDutyPolicyStatus from 'Common/Types/OnCallDutyPolicy/OnCallDutyPolicyStatus';
|
||||
import UserElement from '../../../Components/User/User';
|
||||
import JSONFunctions from 'Common/Types/JSONFunctions';
|
||||
import User from 'Model/Models/User';
|
||||
import DropdownUtil from 'CommonUI/src/Utils/Dropdown';
|
||||
import ObjectID from 'Common/Types/ObjectID';
|
||||
import Query from 'CommonUI/src/Utils/ModelAPI/Query';
|
||||
import Columns from 'CommonUI/src/Components/ModelTable/Columns';
|
||||
import OnCallPolicyView from '../OnCallPolicy';
|
||||
import OnCallDutyPolicy from 'Model/Models/OnCallDutyPolicy';
|
||||
import Navigation from 'CommonUI/src/Utils/Navigation';
|
||||
|
||||
export interface ComponentProps {
|
||||
onCallDutyPolicyId?: ObjectID | undefined; // if this is undefined. then it'll show logs for all policies.
|
||||
}
|
||||
|
||||
const ExecutionLogsTable: FunctionComponent<ComponentProps> = (
|
||||
props: ComponentProps
|
||||
): ReactElement => {
|
||||
const [showViewStatusMessageModal, setShowViewStatusMessageModal] =
|
||||
useState<boolean>(false);
|
||||
const [statusMessage, setStatusMessage] = useState<string>('');
|
||||
|
||||
const query: Query<OnCallDutyPolicyExecutionLog> = {
|
||||
projectId: DashboardNavigation.getProjectId()?.toString(),
|
||||
};
|
||||
|
||||
if (props.onCallDutyPolicyId) {
|
||||
query.onCallDutyPolicyId = props.onCallDutyPolicyId.toString();
|
||||
}
|
||||
|
||||
let columns: Columns<OnCallDutyPolicyExecutionLog> = [];
|
||||
|
||||
if (props.onCallDutyPolicyId) {
|
||||
// add a column for the policy name
|
||||
columns = columns.concat([
|
||||
{
|
||||
field: {
|
||||
onCallDutyPolicy: {
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
title: 'Policy Name',
|
||||
type: FieldType.Element,
|
||||
isFilterable: true,
|
||||
getElement: (item: JSONObject): ReactElement => {
|
||||
if (item['onCallDutyPolicy']) {
|
||||
return (
|
||||
<OnCallPolicyView
|
||||
onCallPolicy={
|
||||
item['onCallDutyPolicy'] as OnCallDutyPolicy
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return <p>No on call policy.</p>;
|
||||
},
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
columns = columns.concat([
|
||||
{
|
||||
field: {
|
||||
triggeredByIncident: {
|
||||
title: true,
|
||||
},
|
||||
},
|
||||
title: 'Triggered By Incident',
|
||||
type: FieldType.Element,
|
||||
isFilterable: false,
|
||||
getElement: (item: JSONObject): ReactElement => {
|
||||
if (item['triggeredByIncident']) {
|
||||
return (
|
||||
<IncidentView
|
||||
incident={item['triggeredByIncident'] as Incident}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return <p>No incident.</p>;
|
||||
},
|
||||
},
|
||||
{
|
||||
field: {
|
||||
createdAt: true,
|
||||
},
|
||||
title: 'Triggered at',
|
||||
type: FieldType.DateTime,
|
||||
isFilterable: true,
|
||||
},
|
||||
{
|
||||
field: {
|
||||
status: true,
|
||||
},
|
||||
title: 'Status',
|
||||
type: FieldType.Element,
|
||||
isFilterable: true,
|
||||
filterDropdownOptions: DropdownUtil.getDropdownOptionsFromEnum(
|
||||
OnCallDutyPolicyStatus
|
||||
),
|
||||
getElement: (item: JSONObject): ReactElement => {
|
||||
if (item['status'] === OnCallDutyPolicyStatus.Completed) {
|
||||
return (
|
||||
<Pill
|
||||
color={Green}
|
||||
text={OnCallDutyPolicyStatus.Completed}
|
||||
/>
|
||||
);
|
||||
} else if (item['status'] === OnCallDutyPolicyStatus.Started) {
|
||||
return (
|
||||
<Pill
|
||||
color={Yellow}
|
||||
text={OnCallDutyPolicyStatus.Started}
|
||||
/>
|
||||
);
|
||||
} else if (
|
||||
item['status'] === OnCallDutyPolicyStatus.Scheduled
|
||||
) {
|
||||
return (
|
||||
<Pill
|
||||
color={Yellow}
|
||||
text={OnCallDutyPolicyStatus.Scheduled}
|
||||
/>
|
||||
);
|
||||
} else if (item['status'] === OnCallDutyPolicyStatus.Running) {
|
||||
return (
|
||||
<Pill
|
||||
color={Yellow}
|
||||
text={OnCallDutyPolicyStatus.Running}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return <Pill color={Red} text={OnCallDutyPolicyStatus.Error} />;
|
||||
},
|
||||
},
|
||||
{
|
||||
field: {
|
||||
acknowledgedByUser: {
|
||||
name: true,
|
||||
email: true,
|
||||
},
|
||||
},
|
||||
title: 'Acknowledged By',
|
||||
type: FieldType.Element,
|
||||
isFilterable: false,
|
||||
getElement: (item: JSONObject): ReactElement => {
|
||||
if (item['acknowledgedByUser']) {
|
||||
return (
|
||||
<UserElement
|
||||
user={
|
||||
JSONFunctions.fromJSON(
|
||||
item['acknowledgedByUser'] as JSONObject,
|
||||
User
|
||||
) as User
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return <p>-</p>;
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<ModelTable<OnCallDutyPolicyExecutionLog>
|
||||
modelType={OnCallDutyPolicyExecutionLog}
|
||||
query={query}
|
||||
id="execution-logs-table"
|
||||
name="On Call Policy > Logs"
|
||||
isDeleteable={false}
|
||||
isEditable={false}
|
||||
isCreateable={false}
|
||||
isViewable={true}
|
||||
cardProps={{
|
||||
icon: IconProp.Logs,
|
||||
title: 'On Call Policy Logs',
|
||||
description:
|
||||
'Here are all the notification logs. This will help you to debug any notification issues that your team may face.',
|
||||
}}
|
||||
selectMoreFields={{
|
||||
statusMessage: true,
|
||||
onCallDutyPolicyId: true,
|
||||
}}
|
||||
noItemsMessage={'This policy has not executed so far.'}
|
||||
viewPageRoute={Navigation.getCurrentRoute()}
|
||||
showRefreshButton={true}
|
||||
showFilterButton={true}
|
||||
showViewIdButton={true}
|
||||
actionButtons={[
|
||||
{
|
||||
title: 'View Status Message',
|
||||
buttonStyleType: ButtonStyleType.NORMAL,
|
||||
onClick: async (
|
||||
item: JSONObject,
|
||||
onCompleteAction: Function,
|
||||
onError: (err: Error) => void
|
||||
) => {
|
||||
try {
|
||||
setStatusMessage(
|
||||
item['statusMessage'] as string
|
||||
);
|
||||
setShowViewStatusMessageModal(true);
|
||||
|
||||
onCompleteAction();
|
||||
} catch (err) {
|
||||
onCompleteAction();
|
||||
onError(err as Error);
|
||||
}
|
||||
},
|
||||
},
|
||||
]}
|
||||
viewButtonText={'View Timeline'}
|
||||
columns={columns}
|
||||
/>
|
||||
|
||||
{showViewStatusMessageModal ? (
|
||||
<ConfirmModal
|
||||
title={'Status Message'}
|
||||
description={statusMessage}
|
||||
submitButtonText={'Close'}
|
||||
onSubmit={async () => {
|
||||
setShowViewStatusMessageModal(false);
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ExecutionLogsTable;
|
||||
@@ -0,0 +1,276 @@
|
||||
import React, { FunctionComponent, ReactElement, useState } from 'react';
|
||||
import ModelTable from 'CommonUI/src/Components/ModelTable/ModelTable';
|
||||
import DashboardNavigation from '../../../Utils/Navigation';
|
||||
import IconProp from 'Common/Types/Icon/IconProp';
|
||||
import FieldType from 'CommonUI/src/Components/Types/FieldType';
|
||||
import { JSONObject } from 'Common/Types/JSON';
|
||||
import Pill from 'CommonUI/src/Components/Pill/Pill';
|
||||
import { Green, Red, Yellow } from 'Common/Types/BrandColors';
|
||||
import { ButtonStyleType } from 'CommonUI/src/Components/Button/Button';
|
||||
import ConfirmModal from 'CommonUI/src/Components/Modal/ConfirmModal';
|
||||
import OnCallDutyPolicyExecutionLogTimeline from 'Model/Models/OnCallDutyPolicyExecutionLogTimeline';
|
||||
import OnCallDutyExecutionLogTimelineStatus from 'Common/Types/OnCallDutyPolicy/OnCalDutyExecutionLogTimelineStatus';
|
||||
import UserElement from '../../User/User';
|
||||
import User from 'Model/Models/User';
|
||||
import JSONFunctions from 'Common/Types/JSONFunctions';
|
||||
import EscalationRule from '../EscalationRule/EscalationRule';
|
||||
import OnCallDutyPolicyEscalationRule from 'Model/Models/OnCallDutyPolicyEscalationRule';
|
||||
import ObjectID from 'Common/Types/ObjectID';
|
||||
import DropdownUtil from 'CommonUI/src/Utils/Dropdown';
|
||||
|
||||
export interface ComponentProps {
|
||||
onCallPolicyExecutionLogId: ObjectID;
|
||||
}
|
||||
|
||||
const ExecutionLogTimelineTable: FunctionComponent<ComponentProps> = (
|
||||
props: ComponentProps
|
||||
): ReactElement => {
|
||||
const [showViewStatusMessageModal, setShowViewStatusMessageModal] =
|
||||
useState<boolean>(false);
|
||||
const [statusMessage, setStatusMessage] = useState<string>('');
|
||||
|
||||
const getModelTable: Function = (): ReactElement => {
|
||||
return (
|
||||
<ModelTable<OnCallDutyPolicyExecutionLogTimeline>
|
||||
modelType={OnCallDutyPolicyExecutionLogTimeline}
|
||||
query={{
|
||||
projectId: DashboardNavigation.getProjectId()?.toString(),
|
||||
onCallDutyPolicyExecutionLogId:
|
||||
props.onCallPolicyExecutionLogId.toString(),
|
||||
}}
|
||||
id="notification-logs-timeline-table"
|
||||
name="On Call > Execution Logs > Timeline"
|
||||
isDeleteable={false}
|
||||
isEditable={false}
|
||||
isCreateable={false}
|
||||
cardProps={{
|
||||
icon: IconProp.Logs,
|
||||
title: 'Policy Execution Timeline',
|
||||
description:
|
||||
'You can view the timeline of the execution of the policy here. You can also view the status of the notification sent out to the users.',
|
||||
}}
|
||||
selectMoreFields={{
|
||||
statusMessage: true,
|
||||
}}
|
||||
noItemsMessage={'No notifications sent out so far.'}
|
||||
showRefreshButton={true}
|
||||
showFilterButton={true}
|
||||
showViewIdButton={true}
|
||||
actionButtons={[
|
||||
{
|
||||
title: 'View Status Message',
|
||||
buttonStyleType: ButtonStyleType.NORMAL,
|
||||
onClick: async (
|
||||
item: JSONObject,
|
||||
onCompleteAction: Function,
|
||||
onError: (err: Error) => void
|
||||
) => {
|
||||
try {
|
||||
setStatusMessage(
|
||||
item['statusMessage'] as string
|
||||
);
|
||||
setShowViewStatusMessageModal(true);
|
||||
|
||||
onCompleteAction();
|
||||
} catch (err) {
|
||||
onCompleteAction();
|
||||
onError(err as Error);
|
||||
}
|
||||
},
|
||||
},
|
||||
]}
|
||||
columns={[
|
||||
{
|
||||
field: {
|
||||
onCallDutyPolicyEscalationRule: {
|
||||
name: true,
|
||||
onCallDutyPolicyId: true,
|
||||
},
|
||||
},
|
||||
title: 'Escalation Rule',
|
||||
type: FieldType.Element,
|
||||
getElement: (item: JSONObject): ReactElement => {
|
||||
if (
|
||||
item &&
|
||||
item['onCallDutyPolicyEscalationRule']
|
||||
) {
|
||||
return (
|
||||
<EscalationRule
|
||||
escalationRule={
|
||||
item[
|
||||
'onCallDutyPolicyEscalationRule'
|
||||
] as OnCallDutyPolicyEscalationRule
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return <p>No escalation rule found.</p>;
|
||||
},
|
||||
},
|
||||
{
|
||||
field: {
|
||||
createdAt: true,
|
||||
},
|
||||
title: 'Started At',
|
||||
type: FieldType.DateTime,
|
||||
isFilterable: true,
|
||||
},
|
||||
{
|
||||
field: {
|
||||
alertSentToUser: {
|
||||
name: true,
|
||||
email: true,
|
||||
},
|
||||
},
|
||||
title: 'Notification Sent To',
|
||||
type: FieldType.Element,
|
||||
getElement: (item: JSONObject): ReactElement => {
|
||||
if (item['alertSentToUser']) {
|
||||
return (
|
||||
<UserElement
|
||||
user={
|
||||
JSONFunctions.fromJSON(
|
||||
item[
|
||||
'alertSentToUser'
|
||||
] as JSONObject,
|
||||
User
|
||||
) as User
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return <p>Invalid User</p>;
|
||||
},
|
||||
},
|
||||
{
|
||||
field: {
|
||||
acknowledgedAt: true,
|
||||
},
|
||||
title: 'Acknowledged At',
|
||||
type: FieldType.DateTime,
|
||||
isFilterable: true,
|
||||
noValueMessage: '-',
|
||||
},
|
||||
{
|
||||
field: {
|
||||
status: true,
|
||||
},
|
||||
title: 'Status',
|
||||
type: FieldType.Element,
|
||||
isFilterable: true,
|
||||
filterDropdownOptions:
|
||||
DropdownUtil.getDropdownOptionsFromEnum(
|
||||
OnCallDutyExecutionLogTimelineStatus
|
||||
),
|
||||
getElement: (item: JSONObject): ReactElement => {
|
||||
if (
|
||||
item['status'] ===
|
||||
OnCallDutyExecutionLogTimelineStatus.NotificationSent
|
||||
) {
|
||||
return (
|
||||
<Pill
|
||||
color={Green}
|
||||
text={
|
||||
OnCallDutyExecutionLogTimelineStatus.NotificationSent
|
||||
}
|
||||
/>
|
||||
);
|
||||
} else if (
|
||||
item['status'] ===
|
||||
OnCallDutyExecutionLogTimelineStatus.SuccessfullyAcknowledged
|
||||
) {
|
||||
return (
|
||||
<Pill
|
||||
color={Green}
|
||||
text={
|
||||
OnCallDutyExecutionLogTimelineStatus.SuccessfullyAcknowledged
|
||||
}
|
||||
/>
|
||||
);
|
||||
} else if (
|
||||
item['status'] ===
|
||||
OnCallDutyExecutionLogTimelineStatus.Error
|
||||
) {
|
||||
return (
|
||||
<Pill
|
||||
color={Yellow}
|
||||
text={
|
||||
OnCallDutyExecutionLogTimelineStatus.Error
|
||||
}
|
||||
/>
|
||||
);
|
||||
} else if (
|
||||
item['status'] ===
|
||||
OnCallDutyExecutionLogTimelineStatus.Skipped
|
||||
) {
|
||||
return (
|
||||
<Pill
|
||||
color={Yellow}
|
||||
text={
|
||||
OnCallDutyExecutionLogTimelineStatus.Skipped
|
||||
}
|
||||
/>
|
||||
);
|
||||
} else if (
|
||||
item['status'] ===
|
||||
OnCallDutyExecutionLogTimelineStatus.Running
|
||||
) {
|
||||
return (
|
||||
<Pill
|
||||
color={Yellow}
|
||||
text={
|
||||
OnCallDutyExecutionLogTimelineStatus.Running
|
||||
}
|
||||
/>
|
||||
);
|
||||
} else if (
|
||||
item['status'] ===
|
||||
OnCallDutyExecutionLogTimelineStatus.Started
|
||||
) {
|
||||
return (
|
||||
<Pill
|
||||
color={Yellow}
|
||||
text={
|
||||
OnCallDutyExecutionLogTimelineStatus.Started
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Pill
|
||||
color={Red}
|
||||
text={
|
||||
OnCallDutyExecutionLogTimelineStatus.Error
|
||||
}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{getModelTable()}
|
||||
|
||||
{showViewStatusMessageModal ? (
|
||||
<ConfirmModal
|
||||
title={'Status Message'}
|
||||
description={statusMessage}
|
||||
submitButtonText={'Close'}
|
||||
onSubmit={async () => {
|
||||
setShowViewStatusMessageModal(false);
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ExecutionLogTimelineTable;
|
||||
37
Dashboard/src/Components/OnCallPolicy/OnCallPolicies.tsx
Normal file
37
Dashboard/src/Components/OnCallPolicy/OnCallPolicies.tsx
Normal file
@@ -0,0 +1,37 @@
|
||||
import Monitor from 'Model/Models/Monitor';
|
||||
import React, { FunctionComponent, ReactElement } from 'react';
|
||||
import OnCallPolicyElement from './OnCallPolicy';
|
||||
import OnCallDutyPolicy from 'Model/Models/OnCallDutyPolicy';
|
||||
|
||||
export interface ComponentProps {
|
||||
onCallPolicies: Array<OnCallDutyPolicy>;
|
||||
onNavigateComplete?: (() => void) | undefined;
|
||||
}
|
||||
|
||||
const OnCallDutyPoliciesView: FunctionComponent<ComponentProps> = (
|
||||
props: ComponentProps
|
||||
): ReactElement => {
|
||||
if (!props.onCallPolicies || props.onCallPolicies.length === 0) {
|
||||
return <p>No on call policies.</p>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{props.onCallPolicies.map((onCallPolicy: Monitor, i: number) => {
|
||||
return (
|
||||
<span key={i}>
|
||||
<OnCallPolicyElement
|
||||
onCallPolicy={onCallPolicy}
|
||||
onNavigateComplete={props.onNavigateComplete}
|
||||
/>
|
||||
{i !== props.onCallPolicies.length - 1 && (
|
||||
<span>, </span>
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default OnCallDutyPoliciesView;
|
||||
37
Dashboard/src/Components/OnCallPolicy/OnCallPolicy.tsx
Normal file
37
Dashboard/src/Components/OnCallPolicy/OnCallPolicy.tsx
Normal file
@@ -0,0 +1,37 @@
|
||||
import React, { FunctionComponent, ReactElement } from 'react';
|
||||
import Link from 'CommonUI/src/Components/Link/Link';
|
||||
import Route from 'Common/Types/API/Route';
|
||||
import OnCallDutyPolicy from 'Model/Models/OnCallDutyPolicy';
|
||||
import RouteMap, { RouteUtil } from '../../Utils/RouteMap';
|
||||
import PageMap from '../../Utils/PageMap';
|
||||
import ObjectID from 'Common/Types/ObjectID';
|
||||
|
||||
export interface ComponentProps {
|
||||
onCallPolicy: OnCallDutyPolicy;
|
||||
onNavigateComplete?: (() => void) | undefined;
|
||||
}
|
||||
|
||||
const OnCallPolicyView: FunctionComponent<ComponentProps> = (
|
||||
props: ComponentProps
|
||||
): ReactElement => {
|
||||
if (props.onCallPolicy._id) {
|
||||
return (
|
||||
<Link
|
||||
onNavigateComplete={props.onNavigateComplete}
|
||||
className="hover:underline"
|
||||
to={RouteUtil.populateRouteParams(
|
||||
RouteMap[PageMap.ON_CALL_DUTY_POLICY_VIEW] as Route,
|
||||
{
|
||||
modelId: new ObjectID(props.onCallPolicy._id as string),
|
||||
}
|
||||
)}
|
||||
>
|
||||
<span>{props.onCallPolicy.name}</span>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
return <span>{props.onCallPolicy.name}</span>;
|
||||
};
|
||||
|
||||
export default OnCallPolicyView;
|
||||
@@ -18,7 +18,7 @@ const ProjectElement: FunctionComponent<ComponentProps> = (
|
||||
return (
|
||||
<Link
|
||||
onNavigateComplete={props.onNavigateComplete}
|
||||
className="underline-on-hover"
|
||||
className="hover:underline"
|
||||
to={new Route(`/dashboard/${_id}`)}
|
||||
>
|
||||
<span>{props.project.name}</span>
|
||||
|
||||
@@ -24,7 +24,7 @@ const StatusPageElement: FunctionComponent<ComponentProps> = (
|
||||
return (
|
||||
<Link
|
||||
onNavigateComplete={props.onNavigateComplete}
|
||||
className="underline-on-hover"
|
||||
className="hover:underline"
|
||||
to={
|
||||
new Route(
|
||||
`/dashboard/${projectId}/status-pages/${props.statusPage._id}`
|
||||
|
||||
@@ -23,7 +23,7 @@ const TeamElement: FunctionComponent<ComponentProps> = (
|
||||
return (
|
||||
<Link
|
||||
onNavigateComplete={props.onNavigateComplete}
|
||||
className="underline-on-hover"
|
||||
className="hover:underline"
|
||||
to={
|
||||
new Route(
|
||||
`/dashboard/${projectId}/settings/teams/${props.team._id}`
|
||||
|
||||
40
Dashboard/src/Components/User/Users.tsx
Normal file
40
Dashboard/src/Components/User/Users.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
import User from 'Model/Models/User';
|
||||
import UserElement from './User';
|
||||
import React, { FunctionComponent, ReactElement } from 'react';
|
||||
|
||||
export interface ComponentProps {
|
||||
users?: Array<User>;
|
||||
prefix?: string | undefined;
|
||||
suffix?: string | undefined;
|
||||
suffixClassName?: string | undefined;
|
||||
usernameClassName?: string | undefined;
|
||||
prefixClassName?: string | undefined;
|
||||
}
|
||||
|
||||
const UsersElement: FunctionComponent<ComponentProps> = (
|
||||
props: ComponentProps
|
||||
): ReactElement => {
|
||||
if (!props.users || props.users.length === 0) {
|
||||
return <p>No users.</p>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-2 mt-2 mb-2">
|
||||
{props.users?.map((user: User) => {
|
||||
return (
|
||||
<UserElement
|
||||
key={user.id?.toString()}
|
||||
user={user}
|
||||
prefix={props.prefix}
|
||||
suffix={props.suffix}
|
||||
suffixClassName={props.suffixClassName}
|
||||
usernameClassName={props.usernameClassName}
|
||||
prefixClassName={props.prefixClassName}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default UsersElement;
|
||||
@@ -24,7 +24,7 @@ const WorkflowElement: FunctionComponent<ComponentProps> = (
|
||||
return (
|
||||
<Link
|
||||
onNavigateComplete={props.onNavigateComplete}
|
||||
className="underline-on-hover"
|
||||
className="hover:underline"
|
||||
to={
|
||||
new Route(
|
||||
`/dashboard/${projectId}/workflows/workflow/${props.workflow._id}`
|
||||
|
||||
@@ -30,6 +30,8 @@ import LabelsElement from '../../../Components/Label/Labels';
|
||||
import JSONFunctions from 'Common/Types/JSONFunctions';
|
||||
import GlobalEvent from 'CommonUI/src/Utils/GlobalEvents';
|
||||
import EventName from '../../../Utils/EventName';
|
||||
import OnCallDutyPoliciesView from '../../../Components/OnCallPolicy/OnCallPolicies';
|
||||
import OnCallDutyPolicy from 'Model/Models/OnCallDutyPolicy';
|
||||
|
||||
const IncidentView: FunctionComponent<PageComponentProps> = (
|
||||
_props: PageComponentProps
|
||||
@@ -266,7 +268,7 @@ const IncidentView: FunctionComponent<PageComponentProps> = (
|
||||
},
|
||||
},
|
||||
title: 'Monitors Affected',
|
||||
fieldType: FieldType.Text,
|
||||
fieldType: FieldType.Element,
|
||||
getElement: (item: JSONObject): ReactElement => {
|
||||
return (
|
||||
<MonitorsElement
|
||||
@@ -282,6 +284,30 @@ const IncidentView: FunctionComponent<PageComponentProps> = (
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
field: {
|
||||
onCallDutyPolicies: {
|
||||
name: true,
|
||||
_id: true,
|
||||
},
|
||||
},
|
||||
title: 'On Call Duty Policies',
|
||||
fieldType: FieldType.Element,
|
||||
getElement: (item: JSONObject): ReactElement => {
|
||||
return (
|
||||
<OnCallDutyPoliciesView
|
||||
onCallPolicies={
|
||||
JSONFunctions.fromJSON(
|
||||
(item[
|
||||
'onCallDutyPolicies'
|
||||
] as JSONArray) || [],
|
||||
OnCallDutyPolicy
|
||||
) as Array<OnCallDutyPolicy>
|
||||
}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
field: {
|
||||
createdAt: true,
|
||||
@@ -314,7 +340,7 @@ const IncidentView: FunctionComponent<PageComponentProps> = (
|
||||
},
|
||||
{
|
||||
title: 'Acknowledge Incident',
|
||||
fieldType: FieldType.Text,
|
||||
fieldType: FieldType.Element,
|
||||
getElement: (
|
||||
_item: JSONObject,
|
||||
onBeforeFetchData: JSONObject,
|
||||
@@ -338,7 +364,7 @@ const IncidentView: FunctionComponent<PageComponentProps> = (
|
||||
},
|
||||
{
|
||||
title: 'Resolve Incident',
|
||||
fieldType: FieldType.Text,
|
||||
fieldType: FieldType.Element,
|
||||
getElement: (
|
||||
_item: JSONObject,
|
||||
onBeforeFetchData: JSONObject,
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
import Route from 'Common/Types/API/Route';
|
||||
import React, { FunctionComponent, ReactElement } from 'react';
|
||||
import PageMap from '../../Utils/PageMap';
|
||||
import RouteMap, { RouteUtil } from '../../Utils/RouteMap';
|
||||
import PageComponentProps from '../PageComponentProps';
|
||||
import Navigation from 'CommonUI/src/Utils/Navigation';
|
||||
import ObjectID from 'Common/Types/ObjectID';
|
||||
import ExecutionLogTimelineTable from '../../Components/OnCallPolicy/ExecutionLogs/ExecutionLogsTimelineTable';
|
||||
import DashboardSideMenu from './SideMenu';
|
||||
import Page from 'CommonUI/src/Components/Page/Page';
|
||||
|
||||
const Settings: FunctionComponent<PageComponentProps> = (
|
||||
_props: PageComponentProps
|
||||
): ReactElement => {
|
||||
const modelId: ObjectID = Navigation.getLastParamAsObjectID();
|
||||
|
||||
return (
|
||||
<Page
|
||||
title={'On-Call Duty'}
|
||||
breadcrumbLinks={[
|
||||
{
|
||||
title: 'Project',
|
||||
to: RouteUtil.populateRouteParams(
|
||||
RouteMap[PageMap.HOME] as Route
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'On Call Duty',
|
||||
to: RouteUtil.populateRouteParams(
|
||||
RouteMap[PageMap.ON_CALL_DUTY] as Route
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'Execution Logs',
|
||||
to: RouteUtil.populateRouteParams(
|
||||
RouteMap[PageMap.ON_CALL_DUTY_EXECUTION_LOGS] as Route
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'Timeline',
|
||||
to: RouteUtil.populateRouteParams(
|
||||
RouteMap[PageMap.ON_CALL_DUTY_EXECUTION_LOGS] as Route
|
||||
),
|
||||
},
|
||||
]}
|
||||
sideMenu={<DashboardSideMenu />}
|
||||
>
|
||||
<ExecutionLogTimelineTable onCallPolicyExecutionLogId={modelId} />
|
||||
</Page>
|
||||
);
|
||||
};
|
||||
|
||||
export default Settings;
|
||||
43
Dashboard/src/Pages/OnCallDuty/OnCallDutyExecutionLogs.tsx
Normal file
43
Dashboard/src/Pages/OnCallDuty/OnCallDutyExecutionLogs.tsx
Normal file
@@ -0,0 +1,43 @@
|
||||
import Route from 'Common/Types/API/Route';
|
||||
import Page from 'CommonUI/src/Components/Page/Page';
|
||||
import React, { FunctionComponent, ReactElement } from 'react';
|
||||
import PageMap from '../../Utils/PageMap';
|
||||
import RouteMap, { RouteUtil } from '../../Utils/RouteMap';
|
||||
import PageComponentProps from '../PageComponentProps';
|
||||
import DashboardSideMenu from './SideMenu';
|
||||
import ExecutionLogsTable from '../../Components/OnCallPolicy/ExecutionLogs/ExecutionLogsTable';
|
||||
|
||||
const Settings: FunctionComponent<PageComponentProps> = (
|
||||
_props: PageComponentProps
|
||||
): ReactElement => {
|
||||
return (
|
||||
<Page
|
||||
title={'On-Call Duty'}
|
||||
breadcrumbLinks={[
|
||||
{
|
||||
title: 'Project',
|
||||
to: RouteUtil.populateRouteParams(
|
||||
RouteMap[PageMap.HOME] as Route
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'On Call Duty',
|
||||
to: RouteUtil.populateRouteParams(
|
||||
RouteMap[PageMap.ON_CALL_DUTY] as Route
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'Execution Logs',
|
||||
to: RouteUtil.populateRouteParams(
|
||||
RouteMap[PageMap.ON_CALL_DUTY_EXECUTION_LOGS] as Route
|
||||
),
|
||||
},
|
||||
]}
|
||||
sideMenu={<DashboardSideMenu />}
|
||||
>
|
||||
<ExecutionLogsTable />
|
||||
</Page>
|
||||
);
|
||||
};
|
||||
|
||||
export default Settings;
|
||||
@@ -15,6 +15,7 @@ import LabelsElement from '../../Components/Label/Labels';
|
||||
import JSONFunctions from 'Common/Types/JSONFunctions';
|
||||
import DashboardNavigation from '../../Utils/Navigation';
|
||||
import Navigation from 'CommonUI/src/Utils/Navigation';
|
||||
import DashboardSideMenu from './SideMenu';
|
||||
|
||||
const OnCallDutyPage: FunctionComponent<PageComponentProps> = (
|
||||
_props: PageComponentProps
|
||||
@@ -42,6 +43,7 @@ const OnCallDutyPage: FunctionComponent<PageComponentProps> = (
|
||||
),
|
||||
},
|
||||
]}
|
||||
sideMenu={<DashboardSideMenu />}
|
||||
>
|
||||
<ModelTable<OnCallDutyPolicy>
|
||||
modelType={OnCallDutyPolicy}
|
||||
|
||||
@@ -6,12 +6,27 @@ import RouteMap, { RouteUtil } from '../../../Utils/RouteMap';
|
||||
import PageComponentProps from '../../PageComponentProps';
|
||||
import SideMenu from './SideMenu';
|
||||
import Navigation from 'CommonUI/src/Utils/Navigation';
|
||||
import ModelDelete from 'CommonUI/src/Components/ModelDelete/ModelDelete';
|
||||
import OnCallDutyEscalationRule from 'Model/Models/OnCallDutyPolicyEscalationRule';
|
||||
import ObjectID from 'Common/Types/ObjectID';
|
||||
import OnCallDutyPolicy from 'Model/Models/OnCallDutyPolicy';
|
||||
import ModelTable, {
|
||||
ShowTableAs,
|
||||
} from 'CommonUI/src/Components/ModelTable/ModelTable';
|
||||
import DashboardNavigation from '../../../Utils/Navigation';
|
||||
import BadDataException from 'Common/Types/Exception/BadDataException';
|
||||
import IconProp from 'Common/Types/Icon/IconProp';
|
||||
import FormFieldSchemaType from 'CommonUI/src/Components/Forms/Types/FormFieldSchemaType';
|
||||
import FieldType from 'CommonUI/src/Components/Types/FieldType';
|
||||
import Team from 'Model/Models/Team';
|
||||
import ProjectUser from '../../../Utils/ProjectUser';
|
||||
import CardModelDetail from 'CommonUI/src/Components/ModelDetail/CardModelDetail';
|
||||
import SortOrder from 'Common/Types/Database/SortOrder';
|
||||
import { JSONObject } from 'Common/Types/JSON';
|
||||
import TeamView from '../../../Components/OnCallPolicy/EscalationRule/TeamView';
|
||||
import UserView from '../../../Components/OnCallPolicy/EscalationRule/UserView';
|
||||
|
||||
const OnCallPolicyDelete: FunctionComponent<PageComponentProps> = (
|
||||
_props: PageComponentProps
|
||||
props: PageComponentProps
|
||||
): ReactElement => {
|
||||
const modelId: ObjectID = Navigation.getLastParamAsObjectID(1);
|
||||
|
||||
@@ -55,13 +70,275 @@ const OnCallPolicyDelete: FunctionComponent<PageComponentProps> = (
|
||||
]}
|
||||
sideMenu={<SideMenu modelId={modelId} />}
|
||||
>
|
||||
<ModelDelete
|
||||
modelType={OnCallDutyPolicy}
|
||||
modelId={modelId}
|
||||
onDeleteSuccess={() => {
|
||||
Navigation.navigate(
|
||||
RouteMap[PageMap.ON_CALL_DUTY] as Route
|
||||
);
|
||||
<ModelTable<OnCallDutyEscalationRule>
|
||||
modelType={OnCallDutyEscalationRule}
|
||||
id="table-scheduled-maintenance-internal-note"
|
||||
name="Scheduled Maintenance Events > Public Notes"
|
||||
isDeleteable={true}
|
||||
isCreateable={true}
|
||||
isEditable={false}
|
||||
sortBy="order"
|
||||
sortOrder={SortOrder.Ascending}
|
||||
showViewIdButton={true}
|
||||
isViewable={false}
|
||||
enableDragAndDrop={true}
|
||||
dragDropIndexField="order"
|
||||
listDetailOptions={{
|
||||
showDetailsInNumberOfColumns: 2,
|
||||
}}
|
||||
query={{
|
||||
onCallDutyPolicyId: modelId,
|
||||
projectId: DashboardNavigation.getProjectId()?.toString(),
|
||||
}}
|
||||
onBeforeCreate={(
|
||||
item: OnCallDutyEscalationRule
|
||||
): Promise<OnCallDutyEscalationRule> => {
|
||||
if (!props.currentProject || !props.currentProject._id) {
|
||||
throw new BadDataException('Project ID cannot be null');
|
||||
}
|
||||
item.onCallDutyPolicyId = modelId;
|
||||
item.projectId = new ObjectID(props.currentProject._id);
|
||||
return Promise.resolve(item);
|
||||
}}
|
||||
cardProps={{
|
||||
icon: IconProp.BarsArrowDown,
|
||||
title: 'Escalation Rules',
|
||||
description:
|
||||
'Escalation rules are used to determine who to contact and when to contact them when an incident is triggered.',
|
||||
}}
|
||||
noItemsMessage={
|
||||
'There are no escalation rules for this on call policy.'
|
||||
}
|
||||
formSteps={[
|
||||
{
|
||||
title: 'Overview',
|
||||
id: 'overview',
|
||||
},
|
||||
{
|
||||
title: 'Notification',
|
||||
id: 'notification',
|
||||
},
|
||||
{
|
||||
title: 'Escalation',
|
||||
id: 'escalation',
|
||||
},
|
||||
]}
|
||||
formFields={[
|
||||
{
|
||||
field: {
|
||||
name: true,
|
||||
},
|
||||
stepId: 'overview',
|
||||
title: 'Name',
|
||||
fieldType: FormFieldSchemaType.Text,
|
||||
required: true,
|
||||
description:
|
||||
'The name of the escalation rule. This is used to identify the rule.',
|
||||
},
|
||||
{
|
||||
field: {
|
||||
description: true,
|
||||
},
|
||||
title: 'Description',
|
||||
stepId: 'overview',
|
||||
fieldType: FormFieldSchemaType.LongText,
|
||||
required: true,
|
||||
description:
|
||||
'The description of the escalation rule. This is used to describe the rule.',
|
||||
},
|
||||
{
|
||||
field: {
|
||||
teams: true,
|
||||
},
|
||||
forceShow: true,
|
||||
title: 'Teams',
|
||||
stepId: 'notification',
|
||||
description:
|
||||
'Select teams who will be notified when incident is triggered.',
|
||||
fieldType: FormFieldSchemaType.MultiSelectDropdown,
|
||||
dropdownModal: {
|
||||
type: Team,
|
||||
labelField: 'name',
|
||||
valueField: '_id',
|
||||
},
|
||||
required: false,
|
||||
placeholder: 'Select Teams',
|
||||
overideFieldKey: 'teams',
|
||||
},
|
||||
{
|
||||
field: {
|
||||
users: true,
|
||||
},
|
||||
forceShow: true,
|
||||
title: 'Users',
|
||||
stepId: 'notification',
|
||||
description:
|
||||
'Select users who will be notified when incident is triggered.',
|
||||
fieldType: FormFieldSchemaType.MultiSelectDropdown,
|
||||
fetchDropdownOptions: async () => {
|
||||
return await ProjectUser.fetchProjectUsersAsDropdownOptions(
|
||||
DashboardNavigation.getProjectId()!
|
||||
);
|
||||
},
|
||||
required: false,
|
||||
placeholder: 'Select Users',
|
||||
overideFieldKey: 'users',
|
||||
},
|
||||
{
|
||||
field: {
|
||||
escalateAfterInMinutes: true,
|
||||
},
|
||||
stepId: 'escalation',
|
||||
title: 'Escalate after (in minutes)',
|
||||
fieldType: FormFieldSchemaType.Number,
|
||||
placeholder: 30,
|
||||
required: true,
|
||||
description:
|
||||
'The amount of time to wait before escalating to the next escalation rule.',
|
||||
},
|
||||
]}
|
||||
showRefreshButton={true}
|
||||
showTableAs={ShowTableAs.List}
|
||||
columns={[
|
||||
{
|
||||
field: {
|
||||
order: true,
|
||||
},
|
||||
isFilterable: false,
|
||||
title: 'Escalation Rule Order',
|
||||
description: 'The order of the escalation rule.',
|
||||
type: FieldType.Number,
|
||||
},
|
||||
{
|
||||
field: {
|
||||
name: true,
|
||||
},
|
||||
isFilterable: true,
|
||||
title: 'Name',
|
||||
description: 'The name of the escalation rule.',
|
||||
type: FieldType.Text,
|
||||
},
|
||||
{
|
||||
field: {
|
||||
description: true,
|
||||
},
|
||||
isFilterable: true,
|
||||
title: 'Description',
|
||||
description: 'The description of the escalation rule.',
|
||||
type: FieldType.Text,
|
||||
},
|
||||
|
||||
{
|
||||
field: {
|
||||
name: true,
|
||||
},
|
||||
title: 'Teams',
|
||||
description:
|
||||
'Teams who will be notified when incident is triggered.',
|
||||
type: FieldType.Element,
|
||||
getElement: (item: JSONObject): ReactElement => {
|
||||
return (
|
||||
<TeamView
|
||||
escalationRuleId={
|
||||
new ObjectID(item['_id'] as string)
|
||||
}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
field: {
|
||||
escalateAfterInMinutes: true,
|
||||
},
|
||||
isFilterable: true,
|
||||
title: 'Escalate after (in minutes)',
|
||||
description:
|
||||
'The amount of minutes to wait before escalating to the next escalation rule.',
|
||||
type: FieldType.Minutes,
|
||||
},
|
||||
{
|
||||
field: {
|
||||
name: true,
|
||||
},
|
||||
title: 'Users',
|
||||
description:
|
||||
'Users who will be notified when incident is triggered.',
|
||||
type: FieldType.Element,
|
||||
getElement: (item: JSONObject): ReactElement => {
|
||||
return (
|
||||
<UserView
|
||||
escalationRuleId={
|
||||
new ObjectID(item['_id'] as string)
|
||||
}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
<CardModelDetail
|
||||
name="On Call Policy > On Call Policy Details"
|
||||
cardProps={{
|
||||
title: 'Repeat Policy',
|
||||
description:
|
||||
'Repeat policies are used to determine how often an on call policy should be repeated.',
|
||||
icon: IconProp.Call,
|
||||
}}
|
||||
isEditable={true}
|
||||
formFields={[
|
||||
{
|
||||
field: {
|
||||
repeatPolicyIfNoOneAcknowledges: true,
|
||||
},
|
||||
title: 'Repeat Policy If No One Acknowledges',
|
||||
fieldType: FormFieldSchemaType.Toggle,
|
||||
required: false,
|
||||
description:
|
||||
'If enabled, the on call policy will repeat if no one acknowledges the incident.',
|
||||
validation: {
|
||||
minLength: 2,
|
||||
},
|
||||
},
|
||||
{
|
||||
field: {
|
||||
repeatPolicyIfNoOneAcknowledgesNoOfTimes: true,
|
||||
},
|
||||
title: 'Number of Times to Repeat',
|
||||
fieldType: FormFieldSchemaType.Number,
|
||||
required: false,
|
||||
description:
|
||||
'The number of times to repeat the on call policy if no one acknowledges the incident.',
|
||||
placeholder: 3,
|
||||
},
|
||||
]}
|
||||
modelDetailProps={{
|
||||
showDetailsInNumberOfColumns: 2,
|
||||
modelType: OnCallDutyPolicy,
|
||||
id: 'model-detail-monitors',
|
||||
fields: [
|
||||
{
|
||||
field: {
|
||||
repeatPolicyIfNoOneAcknowledges: true,
|
||||
},
|
||||
title: 'Repeat Policy If No One Acknowledges',
|
||||
fieldType: FieldType.Boolean,
|
||||
description:
|
||||
'If enabled, the on call policy will repeat if no one acknowledges the incident.',
|
||||
placeholder: 'No',
|
||||
},
|
||||
{
|
||||
field: {
|
||||
repeatPolicyIfNoOneAcknowledgesNoOfTimes: true,
|
||||
},
|
||||
title: 'Number of Times to Repeat',
|
||||
fieldType: FieldType.Number,
|
||||
placeholder: '0',
|
||||
description:
|
||||
'The number of times to repeat the on call policy if no one acknowledges the incident.',
|
||||
},
|
||||
],
|
||||
modelId: modelId,
|
||||
}}
|
||||
/>
|
||||
</ModelPage>
|
||||
|
||||
@@ -1,25 +1,30 @@
|
||||
import Route from 'Common/Types/API/Route';
|
||||
import ModelPage from 'CommonUI/src/Components/Page/ModelPage';
|
||||
import React, { FunctionComponent, ReactElement } from 'react';
|
||||
import PageMap from '../../../Utils/PageMap';
|
||||
import RouteMap, { RouteUtil } from '../../../Utils/RouteMap';
|
||||
import PageComponentProps from '../../PageComponentProps';
|
||||
import SideMenu from './SideMenu';
|
||||
import Navigation from 'CommonUI/src/Utils/Navigation';
|
||||
import ModelDelete from 'CommonUI/src/Components/ModelDelete/ModelDelete';
|
||||
import ObjectID from 'Common/Types/ObjectID';
|
||||
import ModelPage from 'CommonUI/src/Components/Page/ModelPage';
|
||||
import OnCallDutyPolicy from 'Model/Models/OnCallDutyPolicy';
|
||||
import RouteParams from '../../../Utils/RouteParams';
|
||||
import SideMenu from './SideMenu';
|
||||
import ExecutionLogTimelineTable from '../../../Components/OnCallPolicy/ExecutionLogs/ExecutionLogsTimelineTable';
|
||||
|
||||
const OnCallPolicyDelete: FunctionComponent<PageComponentProps> = (
|
||||
const Settings: FunctionComponent<PageComponentProps> = (
|
||||
_props: PageComponentProps
|
||||
): ReactElement => {
|
||||
const modelId: ObjectID = Navigation.getLastParamAsObjectID(1);
|
||||
const onCallDutyPolicyId: string | null = Navigation.getParamByName(
|
||||
RouteParams.ModelID,
|
||||
RouteMap[PageMap.ON_CALL_DUTY_POLICY_VIEW_EXECUTION_LOG_VIEW]!
|
||||
);
|
||||
const modelId: ObjectID = Navigation.getLastParamAsObjectID();
|
||||
|
||||
return (
|
||||
<ModelPage
|
||||
title="On Call Policy"
|
||||
modelType={OnCallDutyPolicy}
|
||||
modelId={modelId}
|
||||
modelId={onCallDutyPolicyId}
|
||||
modelNameField="name"
|
||||
breadcrumbLinks={[
|
||||
{
|
||||
@@ -44,28 +49,36 @@ const OnCallPolicyDelete: FunctionComponent<PageComponentProps> = (
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'Delete On Call Policy',
|
||||
title: 'Logs',
|
||||
to: RouteUtil.populateRouteParams(
|
||||
RouteMap[
|
||||
PageMap.ON_CALL_DUTY_POLICY_VIEW_DELETE
|
||||
PageMap.ON_CALL_DUTY_POLICY_VIEW_EXECUTION_LOGS
|
||||
] as Route,
|
||||
{ modelId }
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'Timeline',
|
||||
to: RouteUtil.populateRouteParams(
|
||||
RouteMap[
|
||||
PageMap.ON_CALL_DUTY_POLICY_VIEW_EXECUTION_LOG_VIEW
|
||||
] as Route,
|
||||
{
|
||||
modelId: new ObjectID(onCallDutyPolicyId as string),
|
||||
subModelId: modelId,
|
||||
}
|
||||
),
|
||||
},
|
||||
]}
|
||||
sideMenu={<SideMenu modelId={modelId} />}
|
||||
sideMenu={
|
||||
<SideMenu
|
||||
modelId={new ObjectID(onCallDutyPolicyId as string)}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<ModelDelete
|
||||
modelType={OnCallDutyPolicy}
|
||||
modelId={modelId}
|
||||
onDeleteSuccess={() => {
|
||||
Navigation.navigate(
|
||||
RouteMap[PageMap.ON_CALL_DUTY] as Route
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<ExecutionLogTimelineTable onCallPolicyExecutionLogId={modelId} />
|
||||
</ModelPage>
|
||||
);
|
||||
};
|
||||
|
||||
export default OnCallPolicyDelete;
|
||||
export default Settings;
|
||||
|
||||
@@ -1,19 +1,25 @@
|
||||
import Route from 'Common/Types/API/Route';
|
||||
import ModelPage from 'CommonUI/src/Components/Page/ModelPage';
|
||||
import React, { FunctionComponent, ReactElement } from 'react';
|
||||
import PageMap from '../../../Utils/PageMap';
|
||||
import RouteMap, { RouteUtil } from '../../../Utils/RouteMap';
|
||||
import PageComponentProps from '../../PageComponentProps';
|
||||
import SideMenu from './SideMenu';
|
||||
import Navigation from 'CommonUI/src/Utils/Navigation';
|
||||
import ModelDelete from 'CommonUI/src/Components/ModelDelete/ModelDelete';
|
||||
import ObjectID from 'Common/Types/ObjectID';
|
||||
import OnCallDutyPolicy from 'Model/Models/OnCallDutyPolicy';
|
||||
import ModelPage from 'CommonUI/src/Components/Page/ModelPage';
|
||||
import ObjectID from 'Common/Types/ObjectID';
|
||||
import SideMenu from './SideMenu';
|
||||
import RouteParams from '../../../Utils/RouteParams';
|
||||
import ExecutionLogsTable from '../../../Components/OnCallPolicy/ExecutionLogs/ExecutionLogsTable';
|
||||
|
||||
const OnCallPolicyDelete: FunctionComponent<PageComponentProps> = (
|
||||
const Settings: FunctionComponent<PageComponentProps> = (
|
||||
_props: PageComponentProps
|
||||
): ReactElement => {
|
||||
const modelId: ObjectID = Navigation.getLastParamAsObjectID(1);
|
||||
const modelId: ObjectID = new ObjectID(
|
||||
Navigation.getParamByName(
|
||||
RouteParams.ModelID,
|
||||
RouteMap[PageMap.ON_CALL_DUTY_POLICY_VIEW_EXECUTION_LOGS]! as Route
|
||||
) as string
|
||||
);
|
||||
|
||||
return (
|
||||
<ModelPage
|
||||
@@ -44,10 +50,10 @@ const OnCallPolicyDelete: FunctionComponent<PageComponentProps> = (
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'Delete On Call Policy',
|
||||
title: 'Logs',
|
||||
to: RouteUtil.populateRouteParams(
|
||||
RouteMap[
|
||||
PageMap.ON_CALL_DUTY_POLICY_VIEW_DELETE
|
||||
PageMap.ON_CALL_DUTY_POLICY_VIEW_EXECUTION_LOGS
|
||||
] as Route,
|
||||
{ modelId }
|
||||
),
|
||||
@@ -55,17 +61,9 @@ const OnCallPolicyDelete: FunctionComponent<PageComponentProps> = (
|
||||
]}
|
||||
sideMenu={<SideMenu modelId={modelId} />}
|
||||
>
|
||||
<ModelDelete
|
||||
modelType={OnCallDutyPolicy}
|
||||
modelId={modelId}
|
||||
onDeleteSuccess={() => {
|
||||
Navigation.navigate(
|
||||
RouteMap[PageMap.ON_CALL_DUTY] as Route
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<ExecutionLogsTable onCallDutyPolicyId={modelId} />
|
||||
</ModelPage>
|
||||
);
|
||||
};
|
||||
|
||||
export default OnCallPolicyDelete;
|
||||
export default Settings;
|
||||
|
||||
@@ -7,6 +7,8 @@ import SideMenuSection from 'CommonUI/src/Components/SideMenu/SideMenuSection';
|
||||
import RouteMap, { RouteUtil } from '../../../Utils/RouteMap';
|
||||
import PageMap from '../../../Utils/PageMap';
|
||||
import ObjectID from 'Common/Types/ObjectID';
|
||||
import Link from 'Common/Types/Link';
|
||||
import Navigation from 'CommonUI/src/Utils/Navigation';
|
||||
|
||||
export interface ComponentProps {
|
||||
modelId: ObjectID;
|
||||
@@ -15,6 +17,19 @@ export interface ComponentProps {
|
||||
const DashboardSideMenu: FunctionComponent<ComponentProps> = (
|
||||
props: ComponentProps
|
||||
): ReactElement => {
|
||||
let subItemMenuLink: Link | undefined = undefined;
|
||||
|
||||
if (
|
||||
Navigation.isOnThisPage(
|
||||
RouteMap[PageMap.ON_CALL_DUTY_POLICY_VIEW_EXECUTION_LOG_VIEW]!
|
||||
)
|
||||
) {
|
||||
subItemMenuLink = {
|
||||
title: 'Timeline',
|
||||
to: Navigation.getCurrentRoute(),
|
||||
};
|
||||
}
|
||||
|
||||
return (
|
||||
<SideMenu>
|
||||
<SideMenuSection title="Basic">
|
||||
@@ -43,7 +58,7 @@ const DashboardSideMenu: FunctionComponent<ComponentProps> = (
|
||||
/>
|
||||
</SideMenuSection>
|
||||
|
||||
<SideMenuSection title="Advanced">
|
||||
<SideMenuSection title="Logs">
|
||||
<SideMenuItem
|
||||
link={{
|
||||
title: 'Execution Logs',
|
||||
@@ -55,7 +70,12 @@ const DashboardSideMenu: FunctionComponent<ComponentProps> = (
|
||||
),
|
||||
}}
|
||||
icon={IconProp.Logs}
|
||||
subItemLink={subItemMenuLink}
|
||||
subItemIcon={IconProp.Clock}
|
||||
/>
|
||||
</SideMenuSection>
|
||||
|
||||
<SideMenuSection title="Advanced">
|
||||
<SideMenuItem
|
||||
link={{
|
||||
title: 'Custom Fields',
|
||||
|
||||
58
Dashboard/src/Pages/OnCallDuty/SideMenu.tsx
Normal file
58
Dashboard/src/Pages/OnCallDuty/SideMenu.tsx
Normal file
@@ -0,0 +1,58 @@
|
||||
import React, { FunctionComponent, ReactElement } from 'react';
|
||||
import Route from 'Common/Types/API/Route';
|
||||
import IconProp from 'Common/Types/Icon/IconProp';
|
||||
import SideMenu from 'CommonUI/src/Components/SideMenu/SideMenu';
|
||||
import SideMenuItem from 'CommonUI/src/Components/SideMenu/SideMenuItem';
|
||||
import SideMenuSection from 'CommonUI/src/Components/SideMenu/SideMenuSection';
|
||||
import RouteMap, { RouteUtil } from '../../Utils/RouteMap';
|
||||
import PageMap from '../../Utils/PageMap';
|
||||
import Link from 'Common/Types/Link';
|
||||
import Navigation from 'CommonUI/src/Utils/Navigation';
|
||||
|
||||
const DashboardSideMenu: FunctionComponent = (): ReactElement => {
|
||||
let subItemMenuLink: Link | undefined = undefined;
|
||||
|
||||
if (
|
||||
Navigation.isOnThisPage(
|
||||
RouteMap[PageMap.ON_CALL_DUTY_EXECUTION_LOGS_TIMELINE]!
|
||||
)
|
||||
) {
|
||||
subItemMenuLink = {
|
||||
title: 'Timeline',
|
||||
to: Navigation.getCurrentRoute(),
|
||||
};
|
||||
}
|
||||
|
||||
return (
|
||||
<SideMenu>
|
||||
<SideMenuSection title="Overview">
|
||||
<SideMenuItem
|
||||
link={{
|
||||
title: 'On Call Policies',
|
||||
to: RouteUtil.populateRouteParams(
|
||||
RouteMap[PageMap.ON_CALL_DUTY_POLICIES] as Route
|
||||
),
|
||||
}}
|
||||
icon={IconProp.Call}
|
||||
/>
|
||||
</SideMenuSection>
|
||||
<SideMenuSection title="More">
|
||||
<SideMenuItem
|
||||
link={{
|
||||
title: 'Execution Logs',
|
||||
to: RouteUtil.populateRouteParams(
|
||||
RouteMap[
|
||||
PageMap.ON_CALL_DUTY_EXECUTION_LOGS
|
||||
] as Route
|
||||
),
|
||||
}}
|
||||
icon={IconProp.Logs}
|
||||
subItemIcon={IconProp.Clock}
|
||||
subItemLink={subItemMenuLink}
|
||||
/>
|
||||
</SideMenuSection>
|
||||
</SideMenu>
|
||||
);
|
||||
};
|
||||
|
||||
export default DashboardSideMenu;
|
||||
@@ -283,7 +283,7 @@ const ScheduledMaintenanceView: FunctionComponent<PageComponentProps> = (
|
||||
},
|
||||
},
|
||||
title: 'Monitors Affected',
|
||||
fieldType: FieldType.Text,
|
||||
fieldType: FieldType.Element,
|
||||
getElement: (item: JSONObject): ReactElement => {
|
||||
return (
|
||||
<MonitorsElement
|
||||
@@ -307,7 +307,7 @@ const ScheduledMaintenanceView: FunctionComponent<PageComponentProps> = (
|
||||
},
|
||||
},
|
||||
title: 'Shown on Status Pages',
|
||||
fieldType: FieldType.Text,
|
||||
fieldType: FieldType.Element,
|
||||
getElement: (item: JSONObject): ReactElement => {
|
||||
return (
|
||||
<StatusPagesElement
|
||||
@@ -369,7 +369,7 @@ const ScheduledMaintenanceView: FunctionComponent<PageComponentProps> = (
|
||||
},
|
||||
{
|
||||
title: 'Change State to Ongoing',
|
||||
fieldType: FieldType.Text,
|
||||
fieldType: FieldType.Element,
|
||||
getElement: (
|
||||
_item: JSONObject,
|
||||
onBeforeFetchData: JSONObject,
|
||||
@@ -393,7 +393,7 @@ const ScheduledMaintenanceView: FunctionComponent<PageComponentProps> = (
|
||||
},
|
||||
{
|
||||
title: 'Change State to Completed',
|
||||
fieldType: FieldType.Text,
|
||||
fieldType: FieldType.Element,
|
||||
getElement: (
|
||||
_item: JSONObject,
|
||||
onBeforeFetchData: JSONObject,
|
||||
|
||||
275
Dashboard/src/Pages/UserSettings/NotificationLogs.tsx
Normal file
275
Dashboard/src/Pages/UserSettings/NotificationLogs.tsx
Normal file
@@ -0,0 +1,275 @@
|
||||
import Route from 'Common/Types/API/Route';
|
||||
import Page from 'CommonUI/src/Components/Page/Page';
|
||||
import React, { FunctionComponent, ReactElement, useState } from 'react';
|
||||
import PageMap from '../../Utils/PageMap';
|
||||
import RouteMap, { RouteUtil } from '../../Utils/RouteMap';
|
||||
import PageComponentProps from '../PageComponentProps';
|
||||
import DashboardSideMenu from './SideMenu';
|
||||
import ModelTable from 'CommonUI/src/Components/ModelTable/ModelTable';
|
||||
import UserNotificationLog from 'Model/Models/UserNotificationLog';
|
||||
import DashboardNavigation from '../../Utils/Navigation';
|
||||
import User from 'CommonUI/src/Utils/User';
|
||||
import IconProp from 'Common/Types/Icon/IconProp';
|
||||
import Navigation from 'CommonUI/src/Utils/Navigation';
|
||||
import FieldType from 'CommonUI/src/Components/Types/FieldType';
|
||||
import { JSONObject } from 'Common/Types/JSON';
|
||||
import OnCallDutyPolicy from 'Model/Models/OnCallDutyPolicy';
|
||||
import OnCallDutyPolicyView from '../../Components/OnCallPolicy/OnCallPolicy';
|
||||
import OnCallDutyPolicyEscalationRule from 'Model/Models/OnCallDutyPolicyEscalationRule';
|
||||
import EscalationRuleView from '../../Components/OnCallPolicy/EscalationRule/EscalationRule';
|
||||
import Pill from 'CommonUI/src/Components/Pill/Pill';
|
||||
import UserNotificationExecutionStatus from 'Common/Types/UserNotification/UserNotificationExecutionStatus';
|
||||
import { Green, Red, Yellow } from 'Common/Types/BrandColors';
|
||||
import { ButtonStyleType } from 'CommonUI/src/Components/Button/Button';
|
||||
import ConfirmModal from 'CommonUI/src/Components/Modal/ConfirmModal';
|
||||
import DropdownUtil from 'CommonUI/src/Utils/Dropdown';
|
||||
|
||||
const Settings: FunctionComponent<PageComponentProps> = (
|
||||
_props: PageComponentProps
|
||||
): ReactElement => {
|
||||
const [showViewStatusMessageModal, setShowViewStatusMessageModal] =
|
||||
useState<boolean>(false);
|
||||
const [statusMessage, setStatusMessage] = useState<string>('');
|
||||
|
||||
return (
|
||||
<Page
|
||||
title={'User Settings'}
|
||||
breadcrumbLinks={[
|
||||
{
|
||||
title: 'Project',
|
||||
to: RouteUtil.populateRouteParams(
|
||||
RouteMap[PageMap.HOME] as Route
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'User Settings',
|
||||
to: RouteUtil.populateRouteParams(
|
||||
RouteMap[PageMap.USER_SETTINGS] as Route
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'Notification Logs',
|
||||
to: RouteUtil.populateRouteParams(
|
||||
RouteMap[
|
||||
PageMap.USER_SETTINGS_NOTIFICATION_LOGS
|
||||
] as Route
|
||||
),
|
||||
},
|
||||
]}
|
||||
sideMenu={<DashboardSideMenu />}
|
||||
>
|
||||
<ModelTable<UserNotificationLog>
|
||||
modelType={UserNotificationLog}
|
||||
query={{
|
||||
projectId: DashboardNavigation.getProjectId()?.toString(),
|
||||
userId: User.getUserId()?.toString(),
|
||||
}}
|
||||
id="notification-logs-table"
|
||||
name="User Settings > Notification Logs"
|
||||
isDeleteable={false}
|
||||
isEditable={false}
|
||||
isCreateable={false}
|
||||
cardProps={{
|
||||
icon: IconProp.Logs,
|
||||
title: 'Notification Logs',
|
||||
description:
|
||||
'Here are all the notification logs. This will help you to debug any notification issues that you may face.',
|
||||
}}
|
||||
selectMoreFields={{
|
||||
statusMessage: true,
|
||||
}}
|
||||
noItemsMessage={'No notifications sent out so far.'}
|
||||
viewPageRoute={Navigation.getCurrentRoute()}
|
||||
showRefreshButton={true}
|
||||
showFilterButton={true}
|
||||
showViewIdButton={true}
|
||||
isViewable={true}
|
||||
actionButtons={[
|
||||
{
|
||||
title: 'View Status Message',
|
||||
buttonStyleType: ButtonStyleType.NORMAL,
|
||||
onClick: async (
|
||||
item: JSONObject,
|
||||
onCompleteAction: Function,
|
||||
onError: (err: Error) => void
|
||||
) => {
|
||||
try {
|
||||
setStatusMessage(
|
||||
item['statusMessage'] as string
|
||||
);
|
||||
setShowViewStatusMessageModal(true);
|
||||
|
||||
onCompleteAction();
|
||||
} catch (err) {
|
||||
onCompleteAction();
|
||||
onError(err as Error);
|
||||
}
|
||||
},
|
||||
},
|
||||
]}
|
||||
viewButtonText={'View Timeline'}
|
||||
columns={[
|
||||
{
|
||||
field: {
|
||||
onCallDutyPolicy: {
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
title: 'On Call Policy',
|
||||
type: FieldType.Element,
|
||||
isFilterable: true,
|
||||
filterEntityType: OnCallDutyPolicy,
|
||||
filterQuery: {
|
||||
projectId:
|
||||
DashboardNavigation.getProjectId()?.toString(),
|
||||
},
|
||||
filterDropdownField: {
|
||||
label: 'name',
|
||||
value: '_id',
|
||||
},
|
||||
getElement: (item: JSONObject): ReactElement => {
|
||||
if (item['onCallDutyPolicy']) {
|
||||
return (
|
||||
<OnCallDutyPolicyView
|
||||
onCallPolicy={
|
||||
item[
|
||||
'onCallDutyPolicy'
|
||||
] as OnCallDutyPolicy
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return <p>No on-call policy.</p>;
|
||||
},
|
||||
},
|
||||
{
|
||||
field: {
|
||||
onCallDutyPolicyEscalationRule: {
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
title: 'Escalation Rule',
|
||||
type: FieldType.Element,
|
||||
isFilterable: true,
|
||||
filterEntityType: OnCallDutyPolicyEscalationRule,
|
||||
filterQuery: {
|
||||
projectId:
|
||||
DashboardNavigation.getProjectId()?.toString(),
|
||||
},
|
||||
filterDropdownField: {
|
||||
label: 'name',
|
||||
value: '_id',
|
||||
},
|
||||
getElement: (item: JSONObject): ReactElement => {
|
||||
if (item['onCallDutyPolicyEscalationRule']) {
|
||||
return (
|
||||
<EscalationRuleView
|
||||
escalationRule={
|
||||
item[
|
||||
'onCallDutyPolicyEscalationRule'
|
||||
] as OnCallDutyPolicyEscalationRule
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return <p>No escalation rule.</p>;
|
||||
},
|
||||
},
|
||||
{
|
||||
field: {
|
||||
createdAt: true,
|
||||
},
|
||||
title: 'Created At',
|
||||
type: FieldType.DateTime,
|
||||
isFilterable: true,
|
||||
},
|
||||
{
|
||||
field: {
|
||||
status: true,
|
||||
},
|
||||
title: 'Status',
|
||||
type: FieldType.Element,
|
||||
isFilterable: true,
|
||||
filterDropdownOptions:
|
||||
DropdownUtil.getDropdownOptionsFromEnum(
|
||||
UserNotificationExecutionStatus
|
||||
),
|
||||
getElement: (item: JSONObject): ReactElement => {
|
||||
if (
|
||||
item['status'] ===
|
||||
UserNotificationExecutionStatus.Completed
|
||||
) {
|
||||
return (
|
||||
<Pill
|
||||
color={Green}
|
||||
text={
|
||||
UserNotificationExecutionStatus.Completed
|
||||
}
|
||||
/>
|
||||
);
|
||||
} else if (
|
||||
item['status'] ===
|
||||
UserNotificationExecutionStatus.Started
|
||||
) {
|
||||
return (
|
||||
<Pill
|
||||
color={Yellow}
|
||||
text={
|
||||
UserNotificationExecutionStatus.Started
|
||||
}
|
||||
/>
|
||||
);
|
||||
} else if (
|
||||
item['status'] ===
|
||||
UserNotificationExecutionStatus.Scheduled
|
||||
) {
|
||||
return (
|
||||
<Pill
|
||||
color={Yellow}
|
||||
text={
|
||||
UserNotificationExecutionStatus.Scheduled
|
||||
}
|
||||
/>
|
||||
);
|
||||
} else if (
|
||||
item['status'] ===
|
||||
UserNotificationExecutionStatus.Running
|
||||
) {
|
||||
return (
|
||||
<Pill
|
||||
color={Yellow}
|
||||
text={
|
||||
UserNotificationExecutionStatus.Running
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Pill
|
||||
color={Red}
|
||||
text={UserNotificationExecutionStatus.Error}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
{showViewStatusMessageModal ? (
|
||||
<ConfirmModal
|
||||
title={'Status Message'}
|
||||
description={statusMessage}
|
||||
submitButtonText={'Close'}
|
||||
onSubmit={async () => {
|
||||
setShowViewStatusMessageModal(false);
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</Page>
|
||||
);
|
||||
};
|
||||
|
||||
export default Settings;
|
||||
240
Dashboard/src/Pages/UserSettings/NotificationLogsTimeline.tsx
Normal file
240
Dashboard/src/Pages/UserSettings/NotificationLogsTimeline.tsx
Normal file
@@ -0,0 +1,240 @@
|
||||
import Route from 'Common/Types/API/Route';
|
||||
import Page from 'CommonUI/src/Components/Page/Page';
|
||||
import React, { FunctionComponent, ReactElement, useState } from 'react';
|
||||
import PageMap from '../../Utils/PageMap';
|
||||
import RouteMap, { RouteUtil } from '../../Utils/RouteMap';
|
||||
import PageComponentProps from '../PageComponentProps';
|
||||
import DashboardSideMenu from './SideMenu';
|
||||
import ModelTable from 'CommonUI/src/Components/ModelTable/ModelTable';
|
||||
import DashboardNavigation from '../../Utils/Navigation';
|
||||
import User from 'CommonUI/src/Utils/User';
|
||||
import IconProp from 'Common/Types/Icon/IconProp';
|
||||
import Navigation from 'CommonUI/src/Utils/Navigation';
|
||||
import FieldType from 'CommonUI/src/Components/Types/FieldType';
|
||||
import { JSONObject } from 'Common/Types/JSON';
|
||||
import Pill from 'CommonUI/src/Components/Pill/Pill';
|
||||
import UserNotificationStatus from 'Common/Types/UserNotification/UserNotificationStatus';
|
||||
import { Green, Red, Yellow } from 'Common/Types/BrandColors';
|
||||
import { ButtonStyleType } from 'CommonUI/src/Components/Button/Button';
|
||||
import ConfirmModal from 'CommonUI/src/Components/Modal/ConfirmModal';
|
||||
import ObjectID from 'Common/Types/ObjectID';
|
||||
import UserNotificationLogTimeline from 'Model/Models/UserNotificationLogTimeline';
|
||||
import BaseModel from 'Common/Models/BaseModel';
|
||||
import NotificationMethodView from '../../Components/NotificationMethods/NotificationMethod';
|
||||
import DropdownUtil from 'CommonUI/src/Utils/Dropdown';
|
||||
|
||||
const Settings: FunctionComponent<PageComponentProps> = (
|
||||
_props: PageComponentProps
|
||||
): ReactElement => {
|
||||
const modelId: ObjectID = Navigation.getLastParamAsObjectID();
|
||||
|
||||
const [showViewStatusMessageModal, setShowViewStatusMessageModal] =
|
||||
useState<boolean>(false);
|
||||
const [statusMessage, setStatusMessage] = useState<string>('');
|
||||
|
||||
const getModelTable: Function = (): ReactElement => {
|
||||
return (
|
||||
<ModelTable<UserNotificationLogTimeline>
|
||||
modelType={UserNotificationLogTimeline}
|
||||
query={{
|
||||
projectId: DashboardNavigation.getProjectId()?.toString(),
|
||||
userNotificationLogId: modelId.toString(),
|
||||
userId: User.getUserId()?.toString(),
|
||||
}}
|
||||
id="notification-logs-timeline-table"
|
||||
name="User Settings > Notification Logs > Timeline"
|
||||
isDeleteable={false}
|
||||
isEditable={false}
|
||||
isCreateable={false}
|
||||
cardProps={{
|
||||
icon: IconProp.Logs,
|
||||
title: 'Notification Timeline',
|
||||
description:
|
||||
'Here are all the timeline events. This will help you to debug any notification issues that you may face.',
|
||||
}}
|
||||
selectMoreFields={{
|
||||
statusMessage: true,
|
||||
userEmail: {
|
||||
email: true,
|
||||
},
|
||||
userSms: {
|
||||
phone: true,
|
||||
},
|
||||
}}
|
||||
noItemsMessage={'No notifications sent out so far.'}
|
||||
showRefreshButton={true}
|
||||
showFilterButton={true}
|
||||
showViewIdButton={true}
|
||||
actionButtons={[
|
||||
{
|
||||
title: 'View Status Message',
|
||||
buttonStyleType: ButtonStyleType.NORMAL,
|
||||
onClick: async (
|
||||
item: JSONObject,
|
||||
onCompleteAction: Function,
|
||||
onError: (err: Error) => void
|
||||
) => {
|
||||
try {
|
||||
setStatusMessage(
|
||||
item['statusMessage'] as string
|
||||
);
|
||||
setShowViewStatusMessageModal(true);
|
||||
|
||||
onCompleteAction();
|
||||
} catch (err) {
|
||||
onCompleteAction();
|
||||
onError(err as Error);
|
||||
}
|
||||
},
|
||||
},
|
||||
]}
|
||||
columns={[
|
||||
{
|
||||
field: {
|
||||
userCall: {
|
||||
phone: true,
|
||||
},
|
||||
},
|
||||
title: 'Notification Method',
|
||||
type: FieldType.Element,
|
||||
getElement: (item: BaseModel): ReactElement => {
|
||||
return (
|
||||
<NotificationMethodView
|
||||
item={item}
|
||||
modelType={UserNotificationLogTimeline}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
field: {
|
||||
createdAt: true,
|
||||
},
|
||||
title: 'Notification Sent At',
|
||||
type: FieldType.DateTime,
|
||||
isFilterable: true,
|
||||
},
|
||||
{
|
||||
field: {
|
||||
status: true,
|
||||
},
|
||||
title: 'Status',
|
||||
type: FieldType.Element,
|
||||
isFilterable: true,
|
||||
filterDropdownOptions:
|
||||
DropdownUtil.getDropdownOptionsFromEnum(
|
||||
UserNotificationStatus
|
||||
),
|
||||
getElement: (item: JSONObject): ReactElement => {
|
||||
if (
|
||||
item['status'] === UserNotificationStatus.Sent
|
||||
) {
|
||||
return (
|
||||
<Pill
|
||||
color={Green}
|
||||
text={UserNotificationStatus.Sent}
|
||||
/>
|
||||
);
|
||||
} else if (
|
||||
item['status'] ===
|
||||
UserNotificationStatus.Acknowledged
|
||||
) {
|
||||
return (
|
||||
<Pill
|
||||
color={Green}
|
||||
text={
|
||||
UserNotificationStatus.Acknowledged
|
||||
}
|
||||
/>
|
||||
);
|
||||
} else if (
|
||||
item['status'] === UserNotificationStatus.Error
|
||||
) {
|
||||
return (
|
||||
<Pill
|
||||
color={Yellow}
|
||||
text={UserNotificationStatus.Error}
|
||||
/>
|
||||
);
|
||||
} else if (
|
||||
item['status'] ===
|
||||
UserNotificationStatus.Skipped
|
||||
) {
|
||||
return (
|
||||
<Pill
|
||||
color={Yellow}
|
||||
text={UserNotificationStatus.Skipped}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Pill
|
||||
color={Red}
|
||||
text={UserNotificationStatus.Error}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Page
|
||||
title={'User Settings'}
|
||||
breadcrumbLinks={[
|
||||
{
|
||||
title: 'Project',
|
||||
to: RouteUtil.populateRouteParams(
|
||||
RouteMap[PageMap.HOME] as Route
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'User Settings',
|
||||
to: RouteUtil.populateRouteParams(
|
||||
RouteMap[PageMap.USER_SETTINGS] as Route
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'Notification Logs',
|
||||
to: RouteUtil.populateRouteParams(
|
||||
RouteMap[
|
||||
PageMap.USER_SETTINGS_NOTIFICATION_LOGS
|
||||
] as Route
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'Timeline',
|
||||
to: RouteUtil.populateRouteParams(
|
||||
RouteMap[
|
||||
PageMap.USER_SETTINGS_NOTIFICATION_LOGS_TIMELINE
|
||||
] as Route,
|
||||
{
|
||||
modelId,
|
||||
}
|
||||
),
|
||||
},
|
||||
]}
|
||||
sideMenu={<DashboardSideMenu />}
|
||||
>
|
||||
{getModelTable()}
|
||||
|
||||
{showViewStatusMessageModal ? (
|
||||
<ConfirmModal
|
||||
title={'Status Message'}
|
||||
description={statusMessage}
|
||||
submitButtonText={'Close'}
|
||||
onSubmit={async () => {
|
||||
setShowViewStatusMessageModal(false);
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</Page>
|
||||
);
|
||||
};
|
||||
|
||||
export default Settings;
|
||||
@@ -32,6 +32,7 @@ import FieldType from 'CommonUI/src/Components/Types/FieldType';
|
||||
import { JSONObject } from 'Common/Types/JSON';
|
||||
import NotificationRuleType from 'Common/Types/NotificationRule/NotificationRuleType';
|
||||
import SortOrder from 'Common/Types/Database/SortOrder';
|
||||
import NotificationMethodView from '../../Components/NotificationMethods/NotificationMethod';
|
||||
|
||||
const Settings: FunctionComponent<PageComponentProps> = (
|
||||
_props: PageComponentProps
|
||||
@@ -186,49 +187,12 @@ const Settings: FunctionComponent<PageComponentProps> = (
|
||||
},
|
||||
title: 'Notification Method',
|
||||
type: FieldType.Text,
|
||||
getElement: (item: JSONObject): ReactElement => {
|
||||
getElement: (item: BaseModel): ReactElement => {
|
||||
return (
|
||||
<div>
|
||||
{item['userEmail'] &&
|
||||
(item['userEmail'] as JSONObject)[
|
||||
'email'
|
||||
] && (
|
||||
<p>
|
||||
Email:{' '}
|
||||
{(
|
||||
item[
|
||||
'userEmail'
|
||||
] as JSONObject
|
||||
)['email']?.toString()}
|
||||
</p>
|
||||
)}
|
||||
{item['userCall'] &&
|
||||
(item['userCall'] as JSONObject)[
|
||||
'phone'
|
||||
] && (
|
||||
<p>
|
||||
Call:{' '}
|
||||
{(
|
||||
item[
|
||||
'userCall'
|
||||
] as JSONObject
|
||||
)['phone']?.toString()}
|
||||
</p>
|
||||
)}
|
||||
{item['userSms'] &&
|
||||
(item['userSms'] as JSONObject)[
|
||||
'phone'
|
||||
] && (
|
||||
<p>
|
||||
SMS:{' '}
|
||||
{(
|
||||
item[
|
||||
'userSms'
|
||||
] as JSONObject
|
||||
)['phone']?.toString()}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<NotificationMethodView
|
||||
item={item}
|
||||
modelType={UserNotificationRule}
|
||||
/>
|
||||
);
|
||||
},
|
||||
isFilterable: false,
|
||||
@@ -435,7 +399,7 @@ const Settings: FunctionComponent<PageComponentProps> = (
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
{/* <div>
|
||||
{getModelTable({
|
||||
incidentSeverity: undefined,
|
||||
ruleType: NotificationRuleType.WHEN_USER_GOES_ON_CALL,
|
||||
@@ -453,7 +417,7 @@ const Settings: FunctionComponent<PageComponentProps> = (
|
||||
description:
|
||||
'Here are the rules to notify you when you go off call.',
|
||||
})}
|
||||
</div>
|
||||
</div> */}
|
||||
</Page>
|
||||
);
|
||||
};
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user