Merge pull request #501 from OneUptime/on-call-duty

On call duty
This commit is contained in:
Nawaz Dhandala
2023-07-09 20:44:57 +01:00
committed by GitHub
149 changed files with 57919 additions and 2189 deletions

28
.vscode/launch.json vendored
View File

@@ -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",

View File

@@ -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')}

View File

@@ -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

View File

@@ -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>

View File

@@ -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>;
}

View File

@@ -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)
);
}
}

View File

@@ -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(
'|'
);

View File

@@ -33,6 +33,7 @@ enum EmailTemplateType {
StatusPageOwnerAnnouncementPosted = 'StatusPageOwnerAnnouncementPosted.hbs',
SimpleMessage = 'SimpleMessage.hbs',
VerificationCode = 'VerificationCode.hbs',
AcknowledgeIncident = 'AcknowledgeIncident.hbs',
}
export default EmailTemplateType;

View File

@@ -100,6 +100,8 @@ enum IconProp {
BellRinging = 'BellRinging',
AdjustmentVertical = 'AdjustmentVertical',
AdjustmentHorizontal = 'AdjustmentHorizontal',
Minus = 'Minus',
MinusSmall = 'MinusSmall',
}
export default IconProp;

View File

@@ -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;
}

View File

@@ -6,4 +6,5 @@ export interface CriteriaIncident {
incidentSeverityId?: ObjectID | undefined;
autoResolveIncident?: boolean | undefined;
id: string;
onCallPolicyIds?: Array<ObjectID> | undefined;
}

View File

@@ -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,

View File

@@ -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;
}

View File

@@ -0,0 +1,10 @@
enum OnCallDutyExecutionLogTimelineStatus {
Skipped = 'Skipped',
Started = 'Started',
Running = 'Running',
SuccessfullyAcknowledged = 'Successfully Acknowledged',
NotificationSent = 'Notification Sent',
Error = 'Error',
}
export default OnCallDutyExecutionLogTimelineStatus;

View File

@@ -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',
}

View File

@@ -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',

View File

@@ -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
View File

@@ -0,0 +1,6 @@
import Phone from '../Phone';
export default interface SMS {
to: Phone;
message: string;
}

View File

@@ -0,0 +1,5 @@
enum UserNotificationEventType {
IncidentCreated = 'Incident Created',
}
export default UserNotificationEventType;

View File

@@ -0,0 +1,9 @@
enum UserNotificationExecutionStatus {
Scheduled = 'Scheduled',
Started = 'Strated',
Running = 'Running',
Completed = 'Completed',
Error = 'Error',
}
export default UserNotificationExecutionStatus;

View File

@@ -0,0 +1,9 @@
enum UserNotificationStatus {
Sent = 'Sent',
Acknowledged = 'Acknowledged',
Error = 'Error',
Sending = 'Sending',
Skipped = 'Skipped',
}
export default UserNotificationStatus;

View 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()}`
)
)
);
}
);
}
}

View File

@@ -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'
);

View 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();
}
}

View File

@@ -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>(

View File

@@ -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;
}

View File

@@ -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,

View File

@@ -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();

View File

@@ -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();

View File

@@ -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();

View 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();

View File

@@ -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>(

View File

@@ -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> {

View File

@@ -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();

View 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();

View 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();

View File

@@ -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>> {

View File

@@ -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();

View File

@@ -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,

View File

@@ -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 {

View File

@@ -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,

View File

@@ -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",

View File

@@ -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",

View File

@@ -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 &&

View File

@@ -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>

View File

@@ -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

View File

@@ -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}

View File

@@ -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

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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>

View File

@@ -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;
})}

View File

@@ -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>
</>
);
};

View File

@@ -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>
);
};

View File

@@ -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
);
}

View File

@@ -29,6 +29,8 @@ enum FieldType {
DictionaryOfStrings = 'DictionaryOfStrings',
JSON = 'JSON',
USDCents = 'USDCents',
Element = 'Element',
Minutes = 'Minutes',
}
export default FieldType;

View File

@@ -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[

View File

@@ -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 (

View File

@@ -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,

View File

@@ -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

View File

@@ -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(

View File

@@ -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);

View File

@@ -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

View 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;

View File

@@ -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,

View File

@@ -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>

View File

@@ -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
}

View File

@@ -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>

View File

@@ -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}
/>

View File

@@ -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>

View File

@@ -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!}

View File

@@ -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}
/>
);
}

View File

@@ -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"

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View 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>,&nbsp;</span>
)}
</span>
);
})}
</div>
);
};
export default OnCallDutyPoliciesView;

View 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;

View File

@@ -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>

View File

@@ -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}`

View File

@@ -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}`

View 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;

View File

@@ -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}`

View File

@@ -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,

View File

@@ -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;

View 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;

View File

@@ -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}

View File

@@ -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>

View File

@@ -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;

View File

@@ -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;

View File

@@ -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',

View 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;

View File

@@ -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,

View 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;

View 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;

View File

@@ -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