feat: Add Status Page Subscriber Notification Template and related services

- Introduced `StatusPageSubscriberNotificationTemplate` model for managing custom notification templates for status page subscribers.
- Created `StatusPageSubscriberNotificationTemplateStatusPage` model to link notification templates to specific status pages.
- Implemented services for managing notification templates and their associations with status pages.
- Added permissions for creating, reading, updating, and deleting notification templates and their links.
- Developed frontend component for displaying and managing subscriber notification templates in the dashboard.
- Defined enums for notification event types and methods to standardize template usage.
This commit is contained in:
Nawaz Dhandala
2025-12-08 11:33:06 +00:00
parent 48f86579be
commit 0933f01082
10 changed files with 1391 additions and 0 deletions

View File

@@ -123,6 +123,8 @@ import StatusPageResource from "./StatusPageResource";
import StatusPageSCIM from "./StatusPageSCIM";
import StatusPageSSO from "./StatusPageSso";
import StatusPageSubscriber from "./StatusPageSubscriber";
import StatusPageSubscriberNotificationTemplate from "./StatusPageSubscriberNotificationTemplate";
import StatusPageSubscriberNotificationTemplateStatusPage from "./StatusPageSubscriberNotificationTemplateStatusPage";
// Team
import Team from "./Team";
import TeamMember from "./TeamMember";
@@ -265,6 +267,8 @@ const AllModelTypes: Array<{
StatusPageAnnouncement,
StatusPageAnnouncementTemplate,
StatusPageSubscriber,
StatusPageSubscriberNotificationTemplate,
StatusPageSubscriberNotificationTemplateStatusPage,
StatusPageFooterLink,
StatusPageHeaderLink,
StatusPagePrivateUser,

View File

@@ -0,0 +1,459 @@
import Project from "./Project";
import User from "./User";
import BaseModel from "./DatabaseBaseModel/DatabaseBaseModel";
import Route from "../../Types/API/Route";
import { PlanType } from "../../Types/Billing/SubscriptionPlan";
import ColumnAccessControl from "../../Types/Database/AccessControl/ColumnAccessControl";
import TableAccessControl from "../../Types/Database/AccessControl/TableAccessControl";
import TableBillingAccessControl from "../../Types/Database/AccessControl/TableBillingAccessControl";
import ColumnLength from "../../Types/Database/ColumnLength";
import ColumnType from "../../Types/Database/ColumnType";
import CrudApiEndpoint from "../../Types/Database/CrudApiEndpoint";
import EnableDocumentation from "../../Types/Database/EnableDocumentation";
import EnableWorkflow from "../../Types/Database/EnableWorkflow";
import TableColumn from "../../Types/Database/TableColumn";
import TableColumnType from "../../Types/Database/TableColumnType";
import TableMetadata from "../../Types/Database/TableMetadata";
import TenantColumn from "../../Types/Database/TenantColumn";
import IconProp from "../../Types/Icon/IconProp";
import ObjectID from "../../Types/ObjectID";
import Permission from "../../Types/Permission";
import StatusPageSubscriberNotificationEventType from "../../Types/StatusPage/StatusPageSubscriberNotificationEventType";
import StatusPageSubscriberNotificationMethod from "../../Types/StatusPage/StatusPageSubscriberNotificationMethod";
import { Column, Entity, Index, JoinColumn, ManyToOne } from "typeorm";
@EnableDocumentation()
@TenantColumn("projectId")
@TableBillingAccessControl({
create: PlanType.Scale,
read: PlanType.Scale,
update: PlanType.Scale,
delete: PlanType.Free,
})
@TableAccessControl({
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateStatusPageSubscriberNotificationTemplate,
],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadStatusPageSubscriberNotificationTemplate,
],
delete: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.DeleteStatusPageSubscriberNotificationTemplate,
],
update: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.EditStatusPageSubscriberNotificationTemplate,
],
})
@EnableWorkflow({
create: true,
delete: true,
update: true,
read: true,
})
@CrudApiEndpoint(new Route("/status-page-subscriber-notification-template"))
@TableMetadata({
tableName: "StatusPageSubscriberNotificationTemplate",
singularName: "Subscriber Notification Template",
pluralName: "Subscriber Notification Templates",
icon: IconProp.Email,
tableDescription:
"Manage custom notification templates for status page subscribers. These templates can be used to customize the notifications sent to subscribers via Email, SMS, Slack, Microsoft Teams, and Webhooks.",
})
@Entity({
name: "StatusPageSubscriberNotificationTemplate",
})
export default class StatusPageSubscriberNotificationTemplate extends BaseModel {
@ColumnAccessControl({
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateStatusPageSubscriberNotificationTemplate,
],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadStatusPageSubscriberNotificationTemplate,
],
update: [],
})
@TableColumn({
manyToOneRelationColumn: "projectId",
type: TableColumnType.Entity,
modelType: Project,
title: "Project",
description: "Relation to Project Resource in which this object belongs",
})
@ManyToOne(
() => {
return Project;
},
{
eager: false,
nullable: true,
onDelete: "CASCADE",
orphanedRowAction: "nullify",
},
)
@JoinColumn({ name: "projectId" })
public project?: Project = undefined;
@ColumnAccessControl({
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateStatusPageSubscriberNotificationTemplate,
],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadStatusPageSubscriberNotificationTemplate,
],
update: [],
})
@Index()
@TableColumn({
type: TableColumnType.ObjectID,
required: true,
canReadOnRelationQuery: true,
title: "Project ID",
description: "ID of your OneUptime Project in which this object belongs",
})
@Column({
type: ColumnType.ObjectID,
nullable: false,
transformer: ObjectID.getDatabaseTransformer(),
})
public projectId?: ObjectID = undefined;
@ColumnAccessControl({
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateStatusPageSubscriberNotificationTemplate,
],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadStatusPageSubscriberNotificationTemplate,
],
update: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.EditStatusPageSubscriberNotificationTemplate,
],
})
@TableColumn({
required: true,
type: TableColumnType.ShortText,
title: "Template Name",
description: "A friendly name for this notification template",
canReadOnRelationQuery: true,
})
@Column({
nullable: false,
type: ColumnType.ShortText,
length: ColumnLength.ShortText,
})
public templateName?: string = undefined;
@ColumnAccessControl({
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateStatusPageSubscriberNotificationTemplate,
],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadStatusPageSubscriberNotificationTemplate,
],
update: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.EditStatusPageSubscriberNotificationTemplate,
],
})
@TableColumn({
required: false,
type: TableColumnType.LongText,
title: "Template Description",
description: "A description for this notification template",
})
@Column({
nullable: true,
type: ColumnType.LongText,
length: ColumnLength.LongText,
})
public templateDescription?: string = undefined;
@ColumnAccessControl({
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateStatusPageSubscriberNotificationTemplate,
],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadStatusPageSubscriberNotificationTemplate,
],
update: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.EditStatusPageSubscriberNotificationTemplate,
],
})
@Index()
@TableColumn({
required: true,
type: TableColumnType.ShortText,
title: "Event Type",
description:
"The type of event this template is for (e.g., Incident Created, Announcement Created)",
canReadOnRelationQuery: true,
})
@Column({
nullable: false,
type: ColumnType.ShortText,
length: ColumnLength.ShortText,
})
public eventType?: StatusPageSubscriberNotificationEventType = undefined;
@ColumnAccessControl({
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateStatusPageSubscriberNotificationTemplate,
],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadStatusPageSubscriberNotificationTemplate,
],
update: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.EditStatusPageSubscriberNotificationTemplate,
],
})
@Index()
@TableColumn({
required: true,
type: TableColumnType.ShortText,
title: "Notification Method",
description:
"The notification method this template is for (Email, SMS, Slack, Microsoft Teams, Webhook)",
canReadOnRelationQuery: true,
})
@Column({
nullable: false,
type: ColumnType.ShortText,
length: ColumnLength.ShortText,
})
public notificationMethod?: StatusPageSubscriberNotificationMethod = undefined;
@ColumnAccessControl({
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateStatusPageSubscriberNotificationTemplate,
],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadStatusPageSubscriberNotificationTemplate,
],
update: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.EditStatusPageSubscriberNotificationTemplate,
],
})
@TableColumn({
required: false,
type: TableColumnType.ShortText,
title: "Subject (Email only)",
description: "The subject line for email notifications. Only used for Email notification method.",
})
@Column({
nullable: true,
type: ColumnType.ShortText,
length: ColumnLength.ShortText,
})
public emailSubject?: string = undefined;
@ColumnAccessControl({
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateStatusPageSubscriberNotificationTemplate,
],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadStatusPageSubscriberNotificationTemplate,
],
update: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.EditStatusPageSubscriberNotificationTemplate,
],
})
@TableColumn({
required: true,
type: TableColumnType.HTML,
title: "Template Body",
description:
"The template body content. For Email: HTML template. For SMS: Plain text. For Slack/Teams: Markdown.",
})
@Column({
nullable: false,
type: ColumnType.HTML,
})
public templateBody?: string = undefined;
@ColumnAccessControl({
create: [],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadStatusPageSubscriberNotificationTemplate,
],
update: [],
})
@TableColumn({
manyToOneRelationColumn: "createdByUserId",
type: TableColumnType.Entity,
modelType: User,
title: "Created by User",
description:
"Relation to User who created this object (if this object was created by a User)",
})
@ManyToOne(
() => {
return User;
},
{
eager: false,
nullable: true,
onDelete: "SET NULL",
orphanedRowAction: "nullify",
},
)
@JoinColumn({ name: "createdByUserId" })
public createdByUser?: User = undefined;
@ColumnAccessControl({
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateStatusPageSubscriberNotificationTemplate,
],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadStatusPageSubscriberNotificationTemplate,
],
update: [],
})
@TableColumn({
type: TableColumnType.ObjectID,
title: "Created by User ID",
description:
"User ID who created this object (if this object was created by a User)",
})
@Column({
type: ColumnType.ObjectID,
nullable: true,
transformer: ObjectID.getDatabaseTransformer(),
})
public createdByUserId?: ObjectID = undefined;
@ColumnAccessControl({
create: [],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadStatusPageSubscriberNotificationTemplate,
],
update: [],
})
@TableColumn({
manyToOneRelationColumn: "deletedByUserId",
type: TableColumnType.Entity,
title: "Deleted by User",
modelType: User,
description:
"Relation to User who deleted this object (if this object was deleted by a User)",
})
@ManyToOne(
() => {
return User;
},
{
cascade: false,
eager: false,
nullable: true,
onDelete: "SET NULL",
orphanedRowAction: "nullify",
},
)
@JoinColumn({ name: "deletedByUserId" })
public deletedByUser?: User = undefined;
@ColumnAccessControl({
create: [],
read: [],
update: [],
})
@TableColumn({
type: TableColumnType.ObjectID,
title: "Deleted by User ID",
description:
"User ID who deleted this object (if this object was deleted by a User)",
})
@Column({
type: ColumnType.ObjectID,
nullable: true,
transformer: ObjectID.getDatabaseTransformer(),
})
public deletedByUserId?: ObjectID = undefined;
}

View File

@@ -0,0 +1,397 @@
import Project from "./Project";
import StatusPage from "./StatusPage";
import StatusPageSubscriberNotificationTemplate from "./StatusPageSubscriberNotificationTemplate";
import User from "./User";
import BaseModel from "./DatabaseBaseModel/DatabaseBaseModel";
import Route from "../../Types/API/Route";
import { PlanType } from "../../Types/Billing/SubscriptionPlan";
import ColumnAccessControl from "../../Types/Database/AccessControl/ColumnAccessControl";
import TableAccessControl from "../../Types/Database/AccessControl/TableAccessControl";
import TableBillingAccessControl from "../../Types/Database/AccessControl/TableBillingAccessControl";
import ColumnType from "../../Types/Database/ColumnType";
import CrudApiEndpoint from "../../Types/Database/CrudApiEndpoint";
import EnableDocumentation from "../../Types/Database/EnableDocumentation";
import EnableWorkflow from "../../Types/Database/EnableWorkflow";
import TableColumn from "../../Types/Database/TableColumn";
import TableColumnType from "../../Types/Database/TableColumnType";
import TableMetadata from "../../Types/Database/TableMetadata";
import TenantColumn from "../../Types/Database/TenantColumn";
import IconProp from "../../Types/Icon/IconProp";
import ObjectID from "../../Types/ObjectID";
import Permission from "../../Types/Permission";
import CanAccessIfCanReadOn from "../../Types/Database/CanAccessIfCanReadOn";
import { Column, Entity, Index, JoinColumn, ManyToOne } from "typeorm";
@EnableDocumentation()
@TenantColumn("projectId")
@CanAccessIfCanReadOn("statusPage")
@TableBillingAccessControl({
create: PlanType.Scale,
read: PlanType.Scale,
update: PlanType.Scale,
delete: PlanType.Scale,
})
@TableAccessControl({
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateStatusPageSubscriberNotificationTemplateStatusPage,
],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadStatusPageSubscriberNotificationTemplateStatusPage,
],
delete: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.DeleteStatusPageSubscriberNotificationTemplateStatusPage,
],
update: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.EditStatusPageSubscriberNotificationTemplateStatusPage,
],
})
@EnableWorkflow({
create: true,
delete: true,
update: true,
read: true,
})
@CrudApiEndpoint(new Route("/status-page-subscriber-notification-template-status-page"))
@TableMetadata({
tableName: "StatusPageSubscriberNotificationTemplateStatusPage",
singularName: "Status Page Notification Template Link",
pluralName: "Status Page Notification Template Links",
icon: IconProp.Link,
tableDescription:
"Links subscriber notification templates to specific status pages. This allows you to use different notification templates for different status pages.",
})
@Entity({
name: "StatusPageSubscriberNotificationTemplateStatusPage",
})
export default class StatusPageSubscriberNotificationTemplateStatusPage extends BaseModel {
@ColumnAccessControl({
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateStatusPageSubscriberNotificationTemplateStatusPage,
],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadStatusPageSubscriberNotificationTemplateStatusPage,
],
update: [],
})
@TableColumn({
manyToOneRelationColumn: "projectId",
type: TableColumnType.Entity,
modelType: Project,
title: "Project",
description: "Relation to Project Resource in which this object belongs",
})
@ManyToOne(
() => {
return Project;
},
{
eager: false,
nullable: true,
onDelete: "CASCADE",
orphanedRowAction: "nullify",
},
)
@JoinColumn({ name: "projectId" })
public project?: Project = undefined;
@ColumnAccessControl({
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateStatusPageSubscriberNotificationTemplateStatusPage,
],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadStatusPageSubscriberNotificationTemplateStatusPage,
],
update: [],
})
@Index()
@TableColumn({
type: TableColumnType.ObjectID,
required: true,
canReadOnRelationQuery: true,
title: "Project ID",
description: "ID of your OneUptime Project in which this object belongs",
})
@Column({
type: ColumnType.ObjectID,
nullable: false,
transformer: ObjectID.getDatabaseTransformer(),
})
public projectId?: ObjectID = undefined;
@ColumnAccessControl({
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateStatusPageSubscriberNotificationTemplateStatusPage,
],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadStatusPageSubscriberNotificationTemplateStatusPage,
],
update: [],
})
@TableColumn({
manyToOneRelationColumn: "statusPageId",
type: TableColumnType.Entity,
modelType: StatusPage,
title: "Status Page",
description: "Status Page this template is linked to",
})
@ManyToOne(
() => {
return StatusPage;
},
{
eager: false,
nullable: false,
onDelete: "CASCADE",
orphanedRowAction: "nullify",
},
)
@JoinColumn({ name: "statusPageId" })
public statusPage?: StatusPage = undefined;
@ColumnAccessControl({
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateStatusPageSubscriberNotificationTemplateStatusPage,
],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadStatusPageSubscriberNotificationTemplateStatusPage,
],
update: [],
})
@Index()
@TableColumn({
type: TableColumnType.ObjectID,
required: true,
canReadOnRelationQuery: true,
title: "Status Page ID",
description: "ID of the Status Page this template is linked to",
})
@Column({
type: ColumnType.ObjectID,
nullable: false,
transformer: ObjectID.getDatabaseTransformer(),
})
public statusPageId?: ObjectID = undefined;
@ColumnAccessControl({
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateStatusPageSubscriberNotificationTemplateStatusPage,
],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadStatusPageSubscriberNotificationTemplateStatusPage,
],
update: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.EditStatusPageSubscriberNotificationTemplateStatusPage,
],
})
@TableColumn({
manyToOneRelationColumn: "statusPageSubscriberNotificationTemplateId",
type: TableColumnType.Entity,
modelType: StatusPageSubscriberNotificationTemplate,
title: "Notification Template",
description: "The notification template to use for this status page",
})
@ManyToOne(
() => {
return StatusPageSubscriberNotificationTemplate;
},
{
eager: false,
nullable: false,
onDelete: "CASCADE",
orphanedRowAction: "nullify",
},
)
@JoinColumn({ name: "statusPageSubscriberNotificationTemplateId" })
public statusPageSubscriberNotificationTemplate?: StatusPageSubscriberNotificationTemplate = undefined;
@ColumnAccessControl({
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateStatusPageSubscriberNotificationTemplateStatusPage,
],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadStatusPageSubscriberNotificationTemplateStatusPage,
],
update: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.EditStatusPageSubscriberNotificationTemplateStatusPage,
],
})
@Index()
@TableColumn({
type: TableColumnType.ObjectID,
required: true,
canReadOnRelationQuery: true,
title: "Notification Template ID",
description: "ID of the notification template linked to this status page",
})
@Column({
type: ColumnType.ObjectID,
nullable: false,
transformer: ObjectID.getDatabaseTransformer(),
})
public statusPageSubscriberNotificationTemplateId?: ObjectID = undefined;
@ColumnAccessControl({
create: [],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadStatusPageSubscriberNotificationTemplateStatusPage,
],
update: [],
})
@TableColumn({
manyToOneRelationColumn: "createdByUserId",
type: TableColumnType.Entity,
modelType: User,
title: "Created by User",
description:
"Relation to User who created this object (if this object was created by a User)",
})
@ManyToOne(
() => {
return User;
},
{
eager: false,
nullable: true,
onDelete: "SET NULL",
orphanedRowAction: "nullify",
},
)
@JoinColumn({ name: "createdByUserId" })
public createdByUser?: User = undefined;
@ColumnAccessControl({
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateStatusPageSubscriberNotificationTemplateStatusPage,
],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadStatusPageSubscriberNotificationTemplateStatusPage,
],
update: [],
})
@TableColumn({
type: TableColumnType.ObjectID,
title: "Created by User ID",
description:
"User ID who created this object (if this object was created by a User)",
})
@Column({
type: ColumnType.ObjectID,
nullable: true,
transformer: ObjectID.getDatabaseTransformer(),
})
public createdByUserId?: ObjectID = undefined;
@ColumnAccessControl({
create: [],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadStatusPageSubscriberNotificationTemplateStatusPage,
],
update: [],
})
@TableColumn({
manyToOneRelationColumn: "deletedByUserId",
type: TableColumnType.Entity,
title: "Deleted by User",
modelType: User,
description:
"Relation to User who deleted this object (if this object was deleted by a User)",
})
@ManyToOne(
() => {
return User;
},
{
cascade: false,
eager: false,
nullable: true,
onDelete: "SET NULL",
orphanedRowAction: "nullify",
},
)
@JoinColumn({ name: "deletedByUserId" })
public deletedByUser?: User = undefined;
@ColumnAccessControl({
create: [],
read: [],
update: [],
})
@TableColumn({
type: TableColumnType.ObjectID,
title: "Deleted by User ID",
description:
"User ID who deleted this object (if this object was deleted by a User)",
})
@Column({
type: ColumnType.ObjectID,
nullable: true,
transformer: ObjectID.getDatabaseTransformer(),
})
public deletedByUserId?: ObjectID = undefined;
}

View File

@@ -112,6 +112,8 @@ import StatusPageResourceService from "./StatusPageResourceService";
import StatusPageService from "./StatusPageService";
import StatusPageSsoService from "./StatusPageSsoService";
import StatusPageSubscriberService from "./StatusPageSubscriberService";
import StatusPageSubscriberNotificationTemplateService from "./StatusPageSubscriberNotificationTemplateService";
import StatusPageSubscriberNotificationTemplateStatusPageService from "./StatusPageSubscriberNotificationTemplateStatusPageService";
import TeamMemberService from "./TeamMemberService";
import TeamPermissionService from "./TeamPermissionService";
import TeamComplianceSettingService from "./TeamComplianceSettingService";
@@ -273,6 +275,8 @@ const services: Array<BaseService> = [
StatusPageService,
StatusPageSsoService,
StatusPageSubscriberService,
StatusPageSubscriberNotificationTemplateService,
StatusPageSubscriberNotificationTemplateStatusPageService,
StatusPageHistoryChartBarColorRuleService,
TeamMemberService,

View File

@@ -0,0 +1,196 @@
import DatabaseService from "./DatabaseService";
import Model from "../../Models/DatabaseModels/StatusPageSubscriberNotificationTemplate";
import StatusPageSubscriberNotificationTemplateStatusPage from "../../Models/DatabaseModels/StatusPageSubscriberNotificationTemplateStatusPage";
import ObjectID from "../../Types/ObjectID";
import StatusPageSubscriberNotificationEventType from "../../Types/StatusPage/StatusPageSubscriberNotificationEventType";
import StatusPageSubscriberNotificationMethod from "../../Types/StatusPage/StatusPageSubscriberNotificationMethod";
import StatusPageSubscriberNotificationTemplateStatusPageService from "./StatusPageSubscriberNotificationTemplateStatusPageService";
import BadDataException from "../../Types/Exception/BadDataException";
export class Service extends DatabaseService<Model> {
public constructor() {
super(Model);
}
/**
* Get template for a specific status page, event type, and notification method.
* Returns null if no custom template is found (caller should use default template).
*/
public async getTemplateForStatusPage(data: {
statusPageId: ObjectID;
eventType: StatusPageSubscriberNotificationEventType;
notificationMethod: StatusPageSubscriberNotificationMethod;
}): Promise<Model | null> {
const { statusPageId, eventType, notificationMethod } = data;
// First find the template link for this status page
const templateLinks: Array<StatusPageSubscriberNotificationTemplateStatusPage> =
await StatusPageSubscriberNotificationTemplateStatusPageService.findBy({
query: {
statusPageId: statusPageId,
},
select: {
statusPageSubscriberNotificationTemplateId: true,
},
skip: 0,
limit: 100,
props: {
isRoot: true,
},
});
if (templateLinks.length === 0) {
return null;
}
// Get the template IDs
const templateIds: Array<ObjectID> = templateLinks
.map((link: StatusPageSubscriberNotificationTemplateStatusPage) => {
return link.statusPageSubscriberNotificationTemplateId;
})
.filter((id: ObjectID | undefined): id is ObjectID => {
return id !== undefined;
});
if (templateIds.length === 0) {
return null;
}
// Find the specific template matching the event type and notification method
const templates = await this.findBy({
query: {
eventType: eventType,
notificationMethod: notificationMethod,
},
select: {
_id: true,
templateName: true,
templateBody: true,
emailSubject: true,
eventType: true,
notificationMethod: true,
},
skip: 0,
limit: 100,
props: {
isRoot: true,
},
});
// Find a template that matches one of the linked template IDs
for (const template of templates) {
if (templateIds.some((id: ObjectID) => { return id.toString() === template._id?.toString(); })) {
return template;
}
}
return null;
}
/**
* Get available variables for a specific event type.
* These variables can be used in templates with {{variableName}} syntax.
*/
public static getAvailableVariablesForEventType(
eventType: StatusPageSubscriberNotificationEventType,
): Array<{ name: string; description: string }> {
const commonVariables = [
{ name: "statusPageName", description: "Name of the status page" },
{ name: "statusPageUrl", description: "URL of the status page" },
{ name: "unsubscribeUrl", description: "URL to unsubscribe from notifications" },
{ name: "resourcesAffected", description: "List of affected resources" },
];
switch (eventType) {
case StatusPageSubscriberNotificationEventType.SubscriberIncidentCreated:
return [
...commonVariables,
{ name: "incidentTitle", description: "Title of the incident" },
{ name: "incidentDescription", description: "Description of the incident" },
{ name: "incidentSeverity", description: "Severity of the incident" },
{ name: "detailsUrl", description: "URL to view incident details" },
];
case StatusPageSubscriberNotificationEventType.SubscriberIncidentStateChanged:
return [
...commonVariables,
{ name: "incidentTitle", description: "Title of the incident" },
{ name: "incidentDescription", description: "Description of the incident" },
{ name: "incidentSeverity", description: "Severity of the incident" },
{ name: "incidentState", description: "Current state of the incident" },
{ name: "detailsUrl", description: "URL to view incident details" },
];
case StatusPageSubscriberNotificationEventType.SubscriberIncidentNoteCreated:
return [
...commonVariables,
{ name: "incidentTitle", description: "Title of the incident" },
{ name: "incidentSeverity", description: "Severity of the incident" },
{ name: "incidentState", description: "Current state of the incident" },
{ name: "postedAt", description: "When the note was posted" },
{ name: "note", description: "Content of the note" },
{ name: "detailsUrl", description: "URL to view incident details" },
];
case StatusPageSubscriberNotificationEventType.SubscriberAnnouncementCreated:
return [
...commonVariables,
{ name: "announcementTitle", description: "Title of the announcement" },
{ name: "announcementDescription", description: "Description of the announcement" },
{ name: "detailsUrl", description: "URL to view announcement details" },
];
case StatusPageSubscriberNotificationEventType.SubscriberScheduledMaintenanceCreated:
return [
...commonVariables,
{ name: "scheduledMaintenanceTitle", description: "Title of the scheduled maintenance" },
{ name: "scheduledMaintenanceDescription", description: "Description of the scheduled maintenance" },
{ name: "scheduledStartTime", description: "When the maintenance is scheduled to start" },
{ name: "scheduledEndTime", description: "When the maintenance is scheduled to end" },
{ name: "detailsUrl", description: "URL to view scheduled maintenance details" },
];
case StatusPageSubscriberNotificationEventType.SubscriberScheduledMaintenanceStateChanged:
return [
...commonVariables,
{ name: "scheduledMaintenanceTitle", description: "Title of the scheduled maintenance" },
{ name: "scheduledMaintenanceDescription", description: "Description of the scheduled maintenance" },
{ name: "scheduledMaintenanceState", description: "Current state of the scheduled maintenance" },
{ name: "detailsUrl", description: "URL to view scheduled maintenance details" },
];
case StatusPageSubscriberNotificationEventType.SubscriberScheduledMaintenanceNoteCreated:
return [
...commonVariables,
{ name: "scheduledMaintenanceTitle", description: "Title of the scheduled maintenance" },
{ name: "scheduledMaintenanceState", description: "Current state of the scheduled maintenance" },
{ name: "postedAt", description: "When the note was posted" },
{ name: "note", description: "Content of the note" },
{ name: "detailsUrl", description: "URL to view scheduled maintenance details" },
];
default:
throw new BadDataException(`Unknown event type: ${eventType}`);
}
}
/**
* Compile a template with the given variables.
* Replaces {{variableName}} with the actual values.
*/
public static compileTemplate(
template: string,
variables: Record<string, string>,
): string {
let compiledTemplate = template;
for (const [key, value] of Object.entries(variables)) {
const regex = new RegExp(`{{\\s*${key}\\s*}}`, "g");
compiledTemplate = compiledTemplate.replace(regex, value || "");
}
return compiledTemplate;
}
}
export default new Service();

View File

@@ -0,0 +1,10 @@
import DatabaseService from "./DatabaseService";
import Model from "../../Models/DatabaseModels/StatusPageSubscriberNotificationTemplateStatusPage";
export class Service extends DatabaseService<Model> {
public constructor() {
super(Model);
}
}
export default new Service();

View File

@@ -476,6 +476,18 @@ enum Permission {
ReadStatusPageAnnouncementTemplate = "ReadStatusPageAnnouncementTemplate",
DeleteStatusPageAnnouncementTemplate = "DeleteStatusPageAnnouncementTemplate",
// Status Page Subscriber Notification Template Permissions (Owner + Admin Permission by default)
CreateStatusPageSubscriberNotificationTemplate = "CreateStatusPageSubscriberNotificationTemplate",
EditStatusPageSubscriberNotificationTemplate = "EditStatusPageSubscriberNotificationTemplate",
ReadStatusPageSubscriberNotificationTemplate = "ReadStatusPageSubscriberNotificationTemplate",
DeleteStatusPageSubscriberNotificationTemplate = "DeleteStatusPageSubscriberNotificationTemplate",
// Status Page Subscriber Notification Template Status Page Permissions (Owner + Admin Permission by default)
CreateStatusPageSubscriberNotificationTemplateStatusPage = "CreateStatusPageSubscriberNotificationTemplateStatusPage",
EditStatusPageSubscriberNotificationTemplateStatusPage = "EditStatusPageSubscriberNotificationTemplateStatusPage",
ReadStatusPageSubscriberNotificationTemplateStatusPage = "ReadStatusPageSubscriberNotificationTemplateStatusPage",
DeleteStatusPageSubscriberNotificationTemplateStatusPage = "DeleteStatusPageSubscriberNotificationTemplateStatusPage",
// Resource Permissions (Team Permission)
CreateIncidentInternalNote = "CreateIncidentInternalNote",
EditIncidentInternalNote = "EditIncidentInternalNote",
@@ -1501,6 +1513,72 @@ export class PermissionHelper {
isAccessControlPermission: false,
},
{
permission: Permission.CreateStatusPageSubscriberNotificationTemplate,
title: "Create Status Page Subscriber Notification Template",
description:
"This permission can create Status Page Subscriber Notification Templates in this project.",
isAssignableToTenant: true,
isAccessControlPermission: false,
},
{
permission: Permission.DeleteStatusPageSubscriberNotificationTemplate,
title: "Delete Status Page Subscriber Notification Template",
description:
"This permission can delete Status Page Subscriber Notification Templates of this project.",
isAssignableToTenant: true,
isAccessControlPermission: false,
},
{
permission: Permission.EditStatusPageSubscriberNotificationTemplate,
title: "Edit Status Page Subscriber Notification Template",
description:
"This permission can edit Status Page Subscriber Notification Templates of this project.",
isAssignableToTenant: true,
isAccessControlPermission: false,
},
{
permission: Permission.ReadStatusPageSubscriberNotificationTemplate,
title: "Read Status Page Subscriber Notification Template",
description:
"This permission can read Status Page Subscriber Notification Templates of this project.",
isAssignableToTenant: true,
isAccessControlPermission: false,
},
{
permission: Permission.CreateStatusPageSubscriberNotificationTemplateStatusPage,
title: "Create Status Page Subscriber Notification Template Link",
description:
"This permission can create Status Page Subscriber Notification Template Links in this project.",
isAssignableToTenant: true,
isAccessControlPermission: false,
},
{
permission: Permission.DeleteStatusPageSubscriberNotificationTemplateStatusPage,
title: "Delete Status Page Subscriber Notification Template Link",
description:
"This permission can delete Status Page Subscriber Notification Template Links of this project.",
isAssignableToTenant: true,
isAccessControlPermission: false,
},
{
permission: Permission.EditStatusPageSubscriberNotificationTemplateStatusPage,
title: "Edit Status Page Subscriber Notification Template Link",
description:
"This permission can edit Status Page Subscriber Notification Template Links of this project.",
isAssignableToTenant: true,
isAccessControlPermission: false,
},
{
permission: Permission.ReadStatusPageSubscriberNotificationTemplateStatusPage,
title: "Read Status Page Subscriber Notification Template Link",
description:
"This permission can read Status Page Subscriber Notification Template Links of this project.",
isAssignableToTenant: true,
isAccessControlPermission: false,
},
{
permission: Permission.CreateProjectDomain,
title: "Create Domain",

View File

@@ -0,0 +1,19 @@
// Different types of notification events for Status Page Subscribers
// Each event type has different variables available for templates
enum StatusPageSubscriberNotificationEventType {
// Incident related events
SubscriberIncidentCreated = "Subscriber Incident Created",
SubscriberIncidentStateChanged = "Subscriber Incident State Changed",
SubscriberIncidentNoteCreated = "Subscriber Incident Note Created",
// Announcement related events
SubscriberAnnouncementCreated = "Subscriber Announcement Created",
// Scheduled Maintenance related events
SubscriberScheduledMaintenanceCreated = "Subscriber Scheduled Maintenance Created",
SubscriberScheduledMaintenanceStateChanged = "Subscriber Scheduled Maintenance State Changed",
SubscriberScheduledMaintenanceNoteCreated = "Subscriber Scheduled Maintenance Note Created",
}
export default StatusPageSubscriberNotificationEventType;

View File

@@ -0,0 +1,12 @@
// Notification methods for Status Page Subscribers
// Different methods require different template formats
enum StatusPageSubscriberNotificationMethod {
Email = "Email",
SMS = "SMS",
Slack = "Slack",
MicrosoftTeams = "Microsoft Teams",
Webhook = "Webhook",
}
export default StatusPageSubscriberNotificationMethod;

View File

@@ -0,0 +1,212 @@
import ProjectUtil from "Common/UI/Utils/Project";
import ModelTable from "Common/UI/Components/ModelTable/ModelTable";
import FieldType from "Common/UI/Components/Types/FieldType";
import StatusPageSubscriberNotificationTemplate from "Common/Models/DatabaseModels/StatusPageSubscriberNotificationTemplate";
import React, { FunctionComponent, ReactElement } from "react";
import Query from "Common/Types/BaseDatabase/Query";
import FormFieldSchemaType from "Common/UI/Components/Forms/Types/FormFieldSchemaType";
import { ModalWidth } from "Common/UI/Components/Modal/Modal";
import PageMap from "../../Utils/PageMap";
import RouteMap, { RouteUtil } from "../../Utils/RouteMap";
import Route from "Common/Types/API/Route";
import DropdownUtil from "Common/UI/Utils/Dropdown";
import StatusPageSubscriberNotificationEventType from "Common/Types/StatusPage/StatusPageSubscriberNotificationEventType";
import StatusPageSubscriberNotificationMethod from "Common/Types/StatusPage/StatusPageSubscriberNotificationMethod";
import Pill from "Common/UI/Components/Pill/Pill";
import { Green500, Yellow500, Blue500, Purple500, Cyan500 } from "Common/Types/BrandColors";
import Color from "Common/Types/Color";
export interface ComponentProps {
query?: Query<StatusPageSubscriberNotificationTemplate> | undefined;
title?: string;
description?: string;
disableCreate?: boolean | undefined;
}
const SubscriberNotificationTemplateTable: FunctionComponent<ComponentProps> = (
props: ComponentProps,
): ReactElement => {
const getMethodColor = (method: StatusPageSubscriberNotificationMethod | undefined): Color => {
switch (method) {
case StatusPageSubscriberNotificationMethod.Email:
return Green500;
case StatusPageSubscriberNotificationMethod.SMS:
return Yellow500;
case StatusPageSubscriberNotificationMethod.Slack:
return Purple500;
case StatusPageSubscriberNotificationMethod.MicrosoftTeams:
return Blue500;
case StatusPageSubscriberNotificationMethod.Webhook:
return Cyan500;
default:
return Green500;
}
};
return (
<ModelTable<StatusPageSubscriberNotificationTemplate>
modelType={StatusPageSubscriberNotificationTemplate}
userPreferencesKey="status-page-subscriber-notification-templates-table"
id="table-status-page-subscriber-notification-templates"
isDeleteable={true}
isCreateable={!props.disableCreate}
showViewIdButton={true}
isEditable={true}
name="Status Page > Subscriber Notification Templates"
isViewable={true}
query={{
...(props.query || {}),
projectId: ProjectUtil.getCurrentProjectId()!,
}}
cardProps={{
title: props.title || "Subscriber Notification Templates",
description:
props.description ||
"Create and manage custom notification templates for status page subscribers. These templates can be used to customize emails, SMS, Slack, and Microsoft Teams notifications.",
}}
noItemsMessage={"No subscriber notification templates found."}
createEditModalWidth={ModalWidth.Large}
showRefreshButton={true}
viewPageRoute={RouteUtil.populateRouteParams(
RouteMap[PageMap.STATUS_PAGE_SUBSCRIBER_TEMPLATES] as Route,
)}
formFields={[
{
field: {
templateName: true,
},
title: "Template Name",
description: "A friendly name for this notification template.",
fieldType: FormFieldSchemaType.Text,
required: true,
placeholder: "My Email Template",
},
{
field: {
templateDescription: true,
},
title: "Template Description",
description: "A description for this notification template.",
fieldType: FormFieldSchemaType.LongText,
required: false,
placeholder: "Description of what this template is for...",
},
{
field: {
eventType: true,
},
title: "Event Type",
description: "The type of event this template is for.",
fieldType: FormFieldSchemaType.Dropdown,
required: true,
placeholder: "Select Event Type",
dropdownOptions: DropdownUtil.getDropdownOptionsFromEnum(
StatusPageSubscriberNotificationEventType,
),
},
{
field: {
notificationMethod: true,
},
title: "Notification Method",
description:
"The notification method this template is for. Email uses HTML, SMS uses plain text, Slack/Teams use Markdown.",
fieldType: FormFieldSchemaType.Dropdown,
required: true,
placeholder: "Select Notification Method",
dropdownOptions: DropdownUtil.getDropdownOptionsFromEnum(
StatusPageSubscriberNotificationMethod,
),
},
{
field: {
emailSubject: true,
},
title: "Email Subject (Email only)",
description:
"The subject line for email notifications. Only used when notification method is Email. You can use template variables like {{incidentTitle}}.",
fieldType: FormFieldSchemaType.Text,
required: false,
placeholder: "[{{statusPageName}}] {{incidentTitle}}",
showIf: (values: any) => {
return values.notificationMethod === StatusPageSubscriberNotificationMethod.Email;
},
},
{
field: {
templateBody: true,
},
title: "Template Body",
description:
"The template content. Use {{variableName}} for variables. For Email: use HTML. For SMS: use plain text. For Slack/Teams: use Markdown.",
fieldType: FormFieldSchemaType.HTML,
required: true,
},
]}
filters={[
{
field: {
templateName: true,
},
title: "Template Name",
type: FieldType.Text,
},
{
field: {
eventType: true,
},
title: "Event Type",
type: FieldType.Text,
},
{
field: {
notificationMethod: true,
},
title: "Notification Method",
type: FieldType.Text,
},
]}
columns={[
{
field: {
templateName: true,
},
title: "Template Name",
type: FieldType.Text,
},
{
field: {
eventType: true,
},
title: "Event Type",
type: FieldType.Text,
},
{
field: {
notificationMethod: true,
},
title: "Notification Method",
type: FieldType.Element,
getElement: (item: StatusPageSubscriberNotificationTemplate) => {
return (
<Pill
text={item.notificationMethod || "Unknown"}
color={getMethodColor(item.notificationMethod)}
/>
);
},
},
{
field: {
templateDescription: true,
},
title: "Description",
type: FieldType.Text,
noValueMessage: "-",
},
]}
/>
);
};
export default SubscriberNotificationTemplateTable;