diff --git a/App/FeatureSet/BaseAPI/Index.ts b/App/FeatureSet/BaseAPI/Index.ts index 48f0390b1f..3daa65d757 100644 --- a/App/FeatureSet/BaseAPI/Index.ts +++ b/App/FeatureSet/BaseAPI/Index.ts @@ -128,6 +128,30 @@ import AlertEpisodeOwnerUserService, { import AlertEpisodeStateTimelineService, { Service as AlertEpisodeStateTimelineServiceType, } from "Common/Server/Services/AlertEpisodeStateTimelineService"; + +// IncidentEpisode Services +import IncidentEpisodeService, { + Service as IncidentEpisodeServiceType, +} from "Common/Server/Services/IncidentEpisodeService"; +import IncidentEpisodeFeedService, { + Service as IncidentEpisodeFeedServiceType, +} from "Common/Server/Services/IncidentEpisodeFeedService"; +import IncidentEpisodeInternalNoteService, { + Service as IncidentEpisodeInternalNoteServiceType, +} from "Common/Server/Services/IncidentEpisodeInternalNoteService"; +import IncidentEpisodeMemberService, { + Service as IncidentEpisodeMemberServiceType, +} from "Common/Server/Services/IncidentEpisodeMemberService"; +import IncidentEpisodeOwnerTeamService, { + Service as IncidentEpisodeOwnerTeamServiceType, +} from "Common/Server/Services/IncidentEpisodeOwnerTeamService"; +import IncidentEpisodeOwnerUserService, { + Service as IncidentEpisodeOwnerUserServiceType, +} from "Common/Server/Services/IncidentEpisodeOwnerUserService"; +import IncidentEpisodeStateTimelineService, { + Service as IncidentEpisodeStateTimelineServiceType, +} from "Common/Server/Services/IncidentEpisodeStateTimelineService"; + import AlertGroupingRuleService, { Service as AlertGroupingRuleServiceType, } from "Common/Server/Services/AlertGroupingRuleService"; @@ -458,6 +482,15 @@ import AlertEpisodeOwnerUser from "Common/Models/DatabaseModels/AlertEpisodeOwne import AlertEpisodeStateTimeline from "Common/Models/DatabaseModels/AlertEpisodeStateTimeline"; import AlertGroupingRule from "Common/Models/DatabaseModels/AlertGroupingRule"; +// IncidentEpisode Models +import IncidentEpisode from "Common/Models/DatabaseModels/IncidentEpisode"; +import IncidentEpisodeFeed from "Common/Models/DatabaseModels/IncidentEpisodeFeed"; +import IncidentEpisodeInternalNote from "Common/Models/DatabaseModels/IncidentEpisodeInternalNote"; +import IncidentEpisodeMember from "Common/Models/DatabaseModels/IncidentEpisodeMember"; +import IncidentEpisodeOwnerTeam from "Common/Models/DatabaseModels/IncidentEpisodeOwnerTeam"; +import IncidentEpisodeOwnerUser from "Common/Models/DatabaseModels/IncidentEpisodeOwnerUser"; +import IncidentEpisodeStateTimeline from "Common/Models/DatabaseModels/IncidentEpisodeStateTimeline"; + import IncidentCustomField from "Common/Models/DatabaseModels/IncidentCustomField"; import IncidentNoteTemplate from "Common/Models/DatabaseModels/IncidentNoteTemplate"; import IncidentPostmortemTemplate from "Common/Models/DatabaseModels/IncidentPostmortemTemplate"; @@ -1001,6 +1034,66 @@ const BaseAPIFeatureSet: FeatureSet = { ).getRouter(), ); + // IncidentEpisode Routes + app.use( + `/${APP_NAME.toLocaleLowerCase()}`, + new BaseAPI( + IncidentEpisode, + IncidentEpisodeService, + ).getRouter(), + ); + + app.use( + `/${APP_NAME.toLocaleLowerCase()}`, + new BaseAPI( + IncidentEpisodeFeed, + IncidentEpisodeFeedService, + ).getRouter(), + ); + + app.use( + `/${APP_NAME.toLocaleLowerCase()}`, + new BaseAPI< + IncidentEpisodeInternalNote, + IncidentEpisodeInternalNoteServiceType + >(IncidentEpisodeInternalNote, IncidentEpisodeInternalNoteService).getRouter(), + ); + + app.use( + `/${APP_NAME.toLocaleLowerCase()}`, + new BaseAPI( + IncidentEpisodeMember, + IncidentEpisodeMemberService, + ).getRouter(), + ); + + app.use( + `/${APP_NAME.toLocaleLowerCase()}`, + new BaseAPI( + IncidentEpisodeOwnerTeam, + IncidentEpisodeOwnerTeamService, + ).getRouter(), + ); + + app.use( + `/${APP_NAME.toLocaleLowerCase()}`, + new BaseAPI( + IncidentEpisodeOwnerUser, + IncidentEpisodeOwnerUserService, + ).getRouter(), + ); + + app.use( + `/${APP_NAME.toLocaleLowerCase()}`, + new BaseAPI< + IncidentEpisodeStateTimeline, + IncidentEpisodeStateTimelineServiceType + >( + IncidentEpisodeStateTimeline, + IncidentEpisodeStateTimelineService, + ).getRouter(), + ); + app.use( `/${APP_NAME.toLocaleLowerCase()}`, new BaseAPI( diff --git a/App/FeatureSet/Notification/Templates/AcknowledgeIncidentEpisode.hbs b/App/FeatureSet/Notification/Templates/AcknowledgeIncidentEpisode.hbs new file mode 100644 index 0000000000..cf24cc90c2 --- /dev/null +++ b/App/FeatureSet/Notification/Templates/AcknowledgeIncidentEpisode.hbs @@ -0,0 +1,66 @@ +{{> Start this}} + + +{{> Logo this}} +{{> EmailTitle title=(concat "Incident Episode " episodeNumber ": " incidentEpisodeTitle) }} + +{{> InfoBlock info=(concat "A new incident episode has been created in the project - " projectName)}} + +{{> InfoBlock info="Here are the details: "}} + +{{> DetailBoxStart this }} +{{> DetailBoxField title="Incident Episode Title:" text=incidentEpisodeTitle }} +{{> DetailBoxField title="Current State: " text=currentState }} +{{> DetailBoxField title="Resources Affected: " text=resourcesAffected }} +{{> DetailBoxField title="Severity: " text=incidentEpisodeSeverity }} +{{> DetailBoxField title="Root Cause: " text=rootCause }} +{{> DetailBoxField title="Description: " text=incidentEpisodeDescription }} +{{> DetailBoxEnd this }} + +{{#if incidentsList}} +{{> TitleBlock title=(concat "Incidents in this Episode (" incidentsCount ")") }} + + + + + + + + + + + + +
+
+
+ {{{incidentsList}}} + +
+
+
+
+ +{{/if}} + +{{> InfoBlock info="ACTION REQUIRED: Please acknowledge this incident episode by clicking on the button below - "}} + +{{> ButtonBlock buttonUrl=acknowledgeIncidentEpisodeLink buttonText="Acknowledge Incident Episode"}} + +{{> InfoBlock info="You can also copy and paste this link:"}} +{{> InfoBlock info=acknowledgeIncidentEpisodeLink}} + +{{> InfoBlock info="You will be notified when the status of this incident episode changes."}} + +{{> TitleBlock title="Why am I receiving this email?"}} +{{> InfoBlock info="You are receiving this email because you are a member of the team that is responsible for this incident episode or you are currently on-call."}} + +{{> Footer this }} + +{{> End this}} diff --git a/App/FeatureSet/Notification/Templates/IncidentEpisodeOwnerAdded.hbs b/App/FeatureSet/Notification/Templates/IncidentEpisodeOwnerAdded.hbs new file mode 100644 index 0000000000..f907220540 --- /dev/null +++ b/App/FeatureSet/Notification/Templates/IncidentEpisodeOwnerAdded.hbs @@ -0,0 +1,30 @@ +{{> Start this}} + + +{{> Logo this}} +{{> EmailTitle title=(concat "Incident Episode " episodeNumber ": " episodeTitle) }} + +{{> InfoBlock info="You have been added as the owner of this incident episode."}} + +{{> InfoBlock info="Here are the details: "}} + +{{> DetailBoxStart this }} +{{> DetailBoxField title="Episode Title:" text=episodeTitle }} +{{> DetailBoxField title="Current State: " text=currentState }} +{{> DetailBoxField title="Severity: " text=episodeSeverity }} +{{> DetailBoxField title="Description: " text=episodeDescription }} +{{> DetailBoxEnd this }} + + +{{> InfoBlock info="You can view this incident episode by clicking on the button below - "}} + +{{> ButtonBlock buttonUrl=episodeViewLink buttonText="View on Dashboard"}} + +{{> InfoBlock info="You can also copy and paste this link:"}} +{{> InfoBlock info=episodeViewLink}} + +{{> InfoBlock info="You will be notified when the status of this incident episode changes."}} + +{{> Footer this }} + +{{> End this}} diff --git a/App/FeatureSet/Notification/Templates/IncidentEpisodeOwnerNotePosted.hbs b/App/FeatureSet/Notification/Templates/IncidentEpisodeOwnerNotePosted.hbs new file mode 100644 index 0000000000..8360798f90 --- /dev/null +++ b/App/FeatureSet/Notification/Templates/IncidentEpisodeOwnerNotePosted.hbs @@ -0,0 +1,37 @@ +{{> Start this}} + + +{{> Logo this}} +{{> EmailTitle title=(concat "Incident Episode " episodeNumber ": " episodeTitle) }} + +{{> InfoBlock info="A new note has been posted on this incident episode."}} + +{{> InfoBlock info="Here are the details: "}} + +{{> DetailBoxStart this }} +{{> DetailBoxField title="Episode Title:" text=episodeTitle }} +{{> DetailBoxField title="Current State: " text=currentState }} +{{> DetailBoxField title="Severity: " text=episodeSeverity }} +{{#if isPrivateNote}} +{{> DetailBoxField title="Private Note: " text=note }} +{{else}} +{{> DetailBoxField title="Public Note: " text=note }} +{{/if}} +{{> DetailBoxEnd this }} + + +{{> InfoBlock info="You can view this incident episode by clicking on the button below - "}} + +{{> ButtonBlock buttonUrl=episodeViewLink buttonText="View on Dashboard"}} + +{{> InfoBlock info="You can also copy and paste this link:"}} +{{> InfoBlock info=episodeViewLink}} + +{{> InfoBlock info="You will be notified when the status of this incident episode changes."}} + +{{> OwnerInfo this }} +{{> UnsubscribeOwnerEmail this }} + +{{> Footer this }} + +{{> End this}} diff --git a/App/FeatureSet/Notification/Templates/IncidentEpisodeOwnerResourceCreated.hbs b/App/FeatureSet/Notification/Templates/IncidentEpisodeOwnerResourceCreated.hbs new file mode 100644 index 0000000000..4e740cdf74 --- /dev/null +++ b/App/FeatureSet/Notification/Templates/IncidentEpisodeOwnerResourceCreated.hbs @@ -0,0 +1,35 @@ +{{> Start this}} + + +{{> Logo this}} +{{> EmailTitle title=(concat "Incident Episode " episodeNumber ": " episodeTitle) }} + +{{> InfoBlock info=(concat "A new incident episode has been created in the project - " projectName)}} + +{{> InfoBlock info="Here are the details: "}} + +{{> DetailBoxStart this }} +{{> DetailBoxField title="Episode Title:" text=episodeTitle }} +{{> DetailBoxField title="Current State: " text=currentState }} +{{> DetailBoxField title="Episode Created By: " text=declaredBy }} +{{> DetailBoxField title="Episode Created At: " text=declaredAt }} +{{> DetailBoxField title="Severity: " text=episodeSeverity }} +{{> DetailBoxField title="Description: " text=episodeDescription }} +{{> DetailBoxEnd this }} + + +{{> InfoBlock info="You can view this incident episode by clicking on the button below - "}} + +{{> ButtonBlock buttonUrl=episodeViewLink buttonText="View on Dashboard"}} + +{{> InfoBlock info="You can also copy and paste this link:"}} +{{> InfoBlock info=episodeViewLink}} + +{{> InfoBlock info="You will be notified when the status of this incident episode changes."}} + +{{> OwnerInfo this }} +{{> UnsubscribeOwnerEmail this }} + +{{> Footer this }} + +{{> End this}} diff --git a/App/FeatureSet/Notification/Templates/IncidentEpisodeOwnerStateChanged.hbs b/App/FeatureSet/Notification/Templates/IncidentEpisodeOwnerStateChanged.hbs new file mode 100644 index 0000000000..9fd023858e --- /dev/null +++ b/App/FeatureSet/Notification/Templates/IncidentEpisodeOwnerStateChanged.hbs @@ -0,0 +1,37 @@ +{{> Start this}} + + +{{> Logo this}} +{{> EmailTitle title=(concat "Incident Episode " episodeNumber ": " episodeTitle) }} + +{{> InfoBlock info="Incident episode state has changed"}} + +{{> InfoBlock info="Here are the details: "}} + +{{> DetailBoxStart this }} +{{> StateTransition this}} +{{#ifNotCond previousStateDurationText ""}} +{{> DetailBoxField title="Duration in Previous State:" text=previousStateDurationText }} +{{/ifNotCond}} +{{> DetailBoxField title="Episode Title:" text=episodeTitle }} +{{> DetailBoxField title="State changed at:" text=stateChangedAt }} +{{> DetailBoxField title="Severity:" text=episodeSeverity }} +{{> DetailBoxField title="Description:" text=episodeDescription }} +{{> DetailBoxEnd this }} + + +{{> InfoBlock info="You can view this incident episode by clicking on the button below - "}} + +{{> ButtonBlock buttonUrl=episodeViewLink buttonText="View on Dashboard"}} + +{{> InfoBlock info="You can also copy and paste this link:"}} +{{> InfoBlock info=episodeViewLink}} + +{{> InfoBlock info="You will be notified when the status of this incident episode changes."}} + +{{> OwnerInfo this }} +{{> UnsubscribeOwnerEmail this }} + +{{> Footer this }} + +{{> End this}} diff --git a/Common/Server/API/SlackAPI.ts b/Common/Server/API/SlackAPI.ts index 58210fa4fc..d52f7f1b75 100644 --- a/Common/Server/API/SlackAPI.ts +++ b/Common/Server/API/SlackAPI.ts @@ -32,6 +32,7 @@ import SlackAuthAction, { import SlackIncidentActions from "../Utils/Workspace/Slack/Actions/Incident"; import SlackAlertActions from "../Utils/Workspace/Slack/Actions/Alert"; import SlackAlertEpisodeActions from "../Utils/Workspace/Slack/Actions/AlertEpisode"; +import SlackIncidentEpisodeActions from "../Utils/Workspace/Slack/Actions/IncidentEpisode"; import SlackScheduledMaintenanceActions from "../Utils/Workspace/Slack/Actions/ScheduledMaintenance"; import LIMIT_MAX from "../../Types/Database/LimitMax"; import SlackMonitorActions from "../Utils/Workspace/Slack/Actions/Monitor"; @@ -647,6 +648,19 @@ export default class SlackAPI { }); } + if ( + SlackIncidentEpisodeActions.isIncidentEpisodeAction({ + actionType: action.actionType, + }) + ) { + return SlackIncidentEpisodeActions.handleIncidentEpisodeAction({ + slackRequest: authResult, + action: action, + req: req, + res: res, + }); + } + if ( SlackMonitorActions.isMonitorAction({ actionType: action.actionType, @@ -837,6 +851,13 @@ export default class SlackAPI { logger.error(err); } + try { + await SlackIncidentEpisodeActions.handleEmojiReaction(reactionData); + } catch (err) { + logger.error("Error handling incident episode emoji reaction:"); + logger.error(err); + } + try { await SlackScheduledMaintenanceActions.handleEmojiReaction( reactionData, diff --git a/Common/Server/Services/OnCallDutyPolicyService.ts b/Common/Server/Services/OnCallDutyPolicyService.ts index 9271304a9d..9978daa743 100644 --- a/Common/Server/Services/OnCallDutyPolicyService.ts +++ b/Common/Server/Services/OnCallDutyPolicyService.ts @@ -264,6 +264,7 @@ ${onCallPolicy.description || "No description provided."} triggeredByIncidentId?: ObjectID | undefined; triggeredByAlertId?: ObjectID | undefined; triggeredByAlertEpisodeId?: ObjectID | undefined; + triggeredByIncidentEpisodeId?: ObjectID | undefined; userNotificationEventType: UserNotificationEventType; }, ): Promise { @@ -299,6 +300,16 @@ ${onCallPolicy.description || "No description provided."} ); } + if ( + UserNotificationEventType.IncidentEpisodeCreated === + options.userNotificationEventType && + !options.triggeredByIncidentEpisodeId + ) { + throw new BadDataException( + "triggeredByIncidentEpisodeId is required when userNotificationEventType is IncidentEpisodeCreated", + ); + } + const policy: OnCallDutyPolicy | null = await this.findOneById({ id: policyId, select: { @@ -338,6 +349,10 @@ ${onCallPolicy.description || "No description provided."} log.triggeredByAlertEpisodeId = options.triggeredByAlertEpisodeId; } + if (options.triggeredByIncidentEpisodeId) { + log.triggeredByIncidentEpisodeId = options.triggeredByIncidentEpisodeId; + } + await OnCallDutyPolicyExecutionLogService.create({ data: log, props: { diff --git a/Common/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.ts b/Common/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.ts index 67227aa934..fcf42e9eb0 100644 --- a/Common/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.ts +++ b/Common/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.ts @@ -86,6 +86,7 @@ import { } from "./Actions/ActionTypes"; import MicrosoftTeamsAlertActions from "./Actions/Alert"; import MicrosoftTeamsAlertEpisodeActions from "./Actions/AlertEpisode"; +import MicrosoftTeamsIncidentEpisodeActions from "./Actions/IncidentEpisode"; import MicrosoftTeamsMonitorActions from "./Actions/Monitor"; import MicrosoftTeamsScheduledMaintenanceActions from "./Actions/ScheduledMaintenance"; import MicrosoftTeamsOnCallDutyActions from "./Actions/OnCallDutyPolicy"; @@ -2535,6 +2536,21 @@ All monitoring checks are passing normally.`; return; } + // Handle incident episode actions + if ( + MicrosoftTeamsIncidentEpisodeActions.isIncidentEpisodeAction({ actionType }) + ) { + await MicrosoftTeamsIncidentEpisodeActions.handleBotIncidentEpisodeAction({ + actionType, + actionValue, + value, + projectId, + oneUptimeUserId, + turnContext: data.turnContext, + }); + return; + } + // Handle monitor actions if (MicrosoftTeamsMonitorActions.isMonitorAction({ actionType })) { await MicrosoftTeamsMonitorActions.handleBotMonitorAction({