diff --git a/.github/workflows/terraform-provider-e2e.yml b/.github/workflows/terraform-provider-e2e.yml index bf5f818cd8..1e8efffd52 100644 --- a/.github/workflows/terraform-provider-e2e.yml +++ b/.github/workflows/terraform-provider-e2e.yml @@ -15,7 +15,7 @@ on: jobs: terraform-e2e-tests: runs-on: ubuntu-latest - timeout-minutes: 60 + timeout-minutes: 120 env: CI_PIPELINE_ID: ${{ github.run_number }} APP_TAG: latest @@ -34,16 +34,84 @@ jobs: - name: Additional Disk Cleanup run: | + echo "=== Initial disk space ===" + df -h + + echo "=== Removing unnecessary tools and libraries ===" + # Remove Android SDK (if not already removed) sudo rm -rf /usr/local/lib/android || true - sudo rm -rf /opt/ghc || true + + # Remove .NET SDK and runtime sudo rm -rf /usr/share/dotnet || true + sudo rm -rf /etc/skel/.dotnet || true + + # Remove Haskell/GHC + sudo rm -rf /opt/ghc || true + sudo rm -rf /usr/local/.ghcup || true + + # Remove CodeQL sudo rm -rf /opt/hostedtoolcache/CodeQL || true + + # Remove Boost sudo rm -rf /usr/local/share/boost || true + + # Remove Swift sudo rm -rf /usr/share/swift || true + + # Remove Julia + sudo rm -rf /usr/local/julia* || true + + # Remove Rust (cargo/rustup) + sudo rm -rf /usr/share/rust || true + sudo rm -rf /home/runner/.rustup || true + sudo rm -rf /home/runner/.cargo || true + + # Remove unnecessary hostedtoolcache items + sudo rm -rf /opt/hostedtoolcache/Python || true + sudo rm -rf /opt/hostedtoolcache/PyPy || true + sudo rm -rf /opt/hostedtoolcache/Ruby || true + sudo rm -rf /opt/hostedtoolcache/Java* || true + + # Remove additional large directories + sudo rm -rf /usr/share/miniconda || true + sudo rm -rf /usr/local/graalvm || true + sudo rm -rf /usr/local/share/chromium || true + sudo rm -rf /usr/local/share/powershell || true + sudo rm -rf /usr/share/az_* || true + + # Remove documentation + sudo rm -rf /usr/share/doc || true + sudo rm -rf /usr/share/man || true + + # Remove unnecessary locales + sudo rm -rf /usr/share/locale || true + + # Clean apt cache sudo apt-get clean || true sudo rm -rf /var/lib/apt/lists/* || true + sudo rm -rf /var/cache/apt/archives/* || true + + # Clean tmp + sudo rm -rf /tmp/* || true + + echo "=== Moving Docker data to /mnt for more space ===" + # Stop docker + sudo systemctl stop docker || true + + # Move docker data directory to /mnt (which has ~70GB) + sudo mv /var/lib/docker /mnt/docker || true + sudo mkdir -p /var/lib/docker || true + sudo mount --bind /mnt/docker /var/lib/docker || true + + # Restart docker + sudo systemctl start docker || true + + echo "=== Final disk space ===" df -h + echo "=== Docker info ===" + docker info | grep -E "Docker Root Dir|Storage Driver" || true + - name: Checkout code uses: actions/checkout@v4 diff --git a/App/FeatureSet/BaseAPI/Index.ts b/App/FeatureSet/BaseAPI/Index.ts index 25c19bf26b..48f0390b1f 100644 --- a/App/FeatureSet/BaseAPI/Index.ts +++ b/App/FeatureSet/BaseAPI/Index.ts @@ -106,6 +106,32 @@ import AlertStateTimelineService, { Service as AlertStateTimelineServiceType, } from "Common/Server/Services/AlertStateTimelineService"; +// AlertEpisode Services +import AlertEpisodeService, { + Service as AlertEpisodeServiceType, +} from "Common/Server/Services/AlertEpisodeService"; +import AlertEpisodeFeedService, { + Service as AlertEpisodeFeedServiceType, +} from "Common/Server/Services/AlertEpisodeFeedService"; +import AlertEpisodeInternalNoteService, { + Service as AlertEpisodeInternalNoteServiceType, +} from "Common/Server/Services/AlertEpisodeInternalNoteService"; +import AlertEpisodeMemberService, { + Service as AlertEpisodeMemberServiceType, +} from "Common/Server/Services/AlertEpisodeMemberService"; +import AlertEpisodeOwnerTeamService, { + Service as AlertEpisodeOwnerTeamServiceType, +} from "Common/Server/Services/AlertEpisodeOwnerTeamService"; +import AlertEpisodeOwnerUserService, { + Service as AlertEpisodeOwnerUserServiceType, +} from "Common/Server/Services/AlertEpisodeOwnerUserService"; +import AlertEpisodeStateTimelineService, { + Service as AlertEpisodeStateTimelineServiceType, +} from "Common/Server/Services/AlertEpisodeStateTimelineService"; +import AlertGroupingRuleService, { + Service as AlertGroupingRuleServiceType, +} from "Common/Server/Services/AlertGroupingRuleService"; + import IncidentCustomFieldService, { Service as IncidentCustomFieldServiceType, } from "Common/Server/Services/IncidentCustomFieldService"; @@ -422,6 +448,16 @@ import AlertSeverity from "Common/Models/DatabaseModels/AlertSeverity"; import AlertState from "Common/Models/DatabaseModels/AlertState"; import AlertStateTimeline from "Common/Models/DatabaseModels/AlertStateTimeline"; +// AlertEpisode Models +import AlertEpisode from "Common/Models/DatabaseModels/AlertEpisode"; +import AlertEpisodeFeed from "Common/Models/DatabaseModels/AlertEpisodeFeed"; +import AlertEpisodeInternalNote from "Common/Models/DatabaseModels/AlertEpisodeInternalNote"; +import AlertEpisodeMember from "Common/Models/DatabaseModels/AlertEpisodeMember"; +import AlertEpisodeOwnerTeam from "Common/Models/DatabaseModels/AlertEpisodeOwnerTeam"; +import AlertEpisodeOwnerUser from "Common/Models/DatabaseModels/AlertEpisodeOwnerUser"; +import AlertEpisodeStateTimeline from "Common/Models/DatabaseModels/AlertEpisodeStateTimeline"; +import AlertGroupingRule from "Common/Models/DatabaseModels/AlertGroupingRule"; + import IncidentCustomField from "Common/Models/DatabaseModels/IncidentCustomField"; import IncidentNoteTemplate from "Common/Models/DatabaseModels/IncidentNoteTemplate"; import IncidentPostmortemTemplate from "Common/Models/DatabaseModels/IncidentPostmortemTemplate"; @@ -905,6 +941,74 @@ const BaseAPIFeatureSet: FeatureSet = { ).getRouter(), ); + // AlertEpisode Routes + app.use( + `/${APP_NAME.toLocaleLowerCase()}`, + new BaseAPI( + AlertEpisode, + AlertEpisodeService, + ).getRouter(), + ); + + app.use( + `/${APP_NAME.toLocaleLowerCase()}`, + new BaseAPI( + AlertEpisodeFeed, + AlertEpisodeFeedService, + ).getRouter(), + ); + + app.use( + `/${APP_NAME.toLocaleLowerCase()}`, + new BaseAPI< + AlertEpisodeInternalNote, + AlertEpisodeInternalNoteServiceType + >(AlertEpisodeInternalNote, AlertEpisodeInternalNoteService).getRouter(), + ); + + app.use( + `/${APP_NAME.toLocaleLowerCase()}`, + new BaseAPI( + AlertEpisodeMember, + AlertEpisodeMemberService, + ).getRouter(), + ); + + app.use( + `/${APP_NAME.toLocaleLowerCase()}`, + new BaseAPI( + AlertEpisodeOwnerTeam, + AlertEpisodeOwnerTeamService, + ).getRouter(), + ); + + app.use( + `/${APP_NAME.toLocaleLowerCase()}`, + new BaseAPI( + AlertEpisodeOwnerUser, + AlertEpisodeOwnerUserService, + ).getRouter(), + ); + + app.use( + `/${APP_NAME.toLocaleLowerCase()}`, + new BaseAPI< + AlertEpisodeStateTimeline, + AlertEpisodeStateTimelineServiceType + >( + AlertEpisodeStateTimeline, + AlertEpisodeStateTimelineService, + ).getRouter(), + ); + + app.use( + `/${APP_NAME.toLocaleLowerCase()}`, + new BaseAPI( + AlertGroupingRule, + AlertGroupingRuleService, + ).getRouter(), + ); + app.use( `/${APP_NAME.toLocaleLowerCase()}`, new BaseAnalyticsAPI( diff --git a/App/FeatureSet/Notification/Templates/AlertEpisodeOwnerAdded.hbs b/App/FeatureSet/Notification/Templates/AlertEpisodeOwnerAdded.hbs new file mode 100644 index 0000000000..dd8a6816dd --- /dev/null +++ b/App/FeatureSet/Notification/Templates/AlertEpisodeOwnerAdded.hbs @@ -0,0 +1,30 @@ +{{> Start this}} + + +{{> Logo this}} +{{> EmailTitle title=(concat "Alert Episode: " episodeTitle) }} + +{{> InfoBlock info="You have been added as the owner of this alert 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 alert 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 alert episode changes."}} + +{{> Footer this }} + +{{> End this}} diff --git a/App/FeatureSet/Notification/Templates/AlertEpisodeOwnerNotePosted.hbs b/App/FeatureSet/Notification/Templates/AlertEpisodeOwnerNotePosted.hbs new file mode 100644 index 0000000000..413d2a86eb --- /dev/null +++ b/App/FeatureSet/Notification/Templates/AlertEpisodeOwnerNotePosted.hbs @@ -0,0 +1,37 @@ +{{> Start this}} + + +{{> Logo this}} +{{> EmailTitle title=(concat "Alert Episode: " episodeTitle) }} + +{{> InfoBlock info="A new note has been posted on this alert 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 alert 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 alert episode changes."}} + +{{> OwnerInfo this }} +{{> UnsubscribeOwnerEmail this }} + +{{> Footer this }} + +{{> End this}} diff --git a/App/FeatureSet/Notification/Templates/AlertEpisodeOwnerResourceCreated.hbs b/App/FeatureSet/Notification/Templates/AlertEpisodeOwnerResourceCreated.hbs new file mode 100644 index 0000000000..26c799b6af --- /dev/null +++ b/App/FeatureSet/Notification/Templates/AlertEpisodeOwnerResourceCreated.hbs @@ -0,0 +1,35 @@ +{{> Start this}} + + +{{> Logo this}} +{{> EmailTitle title=(concat "Alert Episode: " episodeTitle) }} + +{{> InfoBlock info=(concat "A new alert 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 alert 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 alert episode changes."}} + +{{> OwnerInfo this }} +{{> UnsubscribeOwnerEmail this }} + +{{> Footer this }} + +{{> End this}} diff --git a/App/FeatureSet/Notification/Templates/AlertEpisodeOwnerStateChanged.hbs b/App/FeatureSet/Notification/Templates/AlertEpisodeOwnerStateChanged.hbs new file mode 100644 index 0000000000..b530134dfc --- /dev/null +++ b/App/FeatureSet/Notification/Templates/AlertEpisodeOwnerStateChanged.hbs @@ -0,0 +1,37 @@ +{{> Start this}} + + +{{> Logo this}} +{{> EmailTitle title=(concat "Alert Episode: " episodeTitle) }} + +{{> InfoBlock info="Alert 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 alert 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 alert episode changes."}} + +{{> OwnerInfo this }} +{{> UnsubscribeOwnerEmail this }} + +{{> Footer this }} + +{{> End this}} diff --git a/Common/Models/DatabaseModels/Alert.ts b/Common/Models/DatabaseModels/Alert.ts index a31e67d051..b52c150f7e 100644 --- a/Common/Models/DatabaseModels/Alert.ts +++ b/Common/Models/DatabaseModels/Alert.ts @@ -1,3 +1,4 @@ +import AlertEpisode from "./AlertEpisode"; import AlertSeverity from "./AlertSeverity"; import AlertState from "./AlertState"; import Label from "./Label"; @@ -1071,4 +1072,79 @@ export default class Alert extends BaseModel { }) public postUpdatesToWorkspaceChannels?: Array = undefined; + + @ColumnAccessControl({ + create: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.ProjectMember, + Permission.CreateAlert, + ], + read: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.ProjectMember, + Permission.ReadAlert, + ], + update: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.ProjectMember, + Permission.EditAlert, + ], + }) + @TableColumn({ + manyToOneRelationColumn: "alertEpisodeId", + type: TableColumnType.Entity, + modelType: AlertEpisode, + title: "Alert Episode", + description: "The episode this alert belongs to (if grouped)", + }) + @ManyToOne( + () => { + return AlertEpisode; + }, + { + eager: false, + nullable: true, + onDelete: "SET NULL", + orphanedRowAction: "nullify", + }, + ) + @JoinColumn({ name: "alertEpisodeId" }) + public alertEpisode?: AlertEpisode = undefined; + + @ColumnAccessControl({ + create: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.ProjectMember, + Permission.CreateAlert, + ], + read: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.ProjectMember, + Permission.ReadAlert, + ], + update: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.ProjectMember, + Permission.EditAlert, + ], + }) + @Index() + @TableColumn({ + type: TableColumnType.ObjectID, + required: false, + title: "Alert Episode ID", + description: "The ID of the episode this alert belongs to (if grouped)", + }) + @Column({ + type: ColumnType.ObjectID, + nullable: true, + transformer: ObjectID.getDatabaseTransformer(), + }) + public alertEpisodeId?: ObjectID = undefined; } diff --git a/Common/Models/DatabaseModels/AlertEpisode.ts b/Common/Models/DatabaseModels/AlertEpisode.ts new file mode 100644 index 0000000000..0b235461f9 --- /dev/null +++ b/Common/Models/DatabaseModels/AlertEpisode.ts @@ -0,0 +1,1201 @@ +import AlertSeverity from "./AlertSeverity"; +import AlertState from "./AlertState"; +import Label from "./Label"; +import OnCallDutyPolicy from "./OnCallDutyPolicy"; +import Project from "./Project"; +import Team from "./Team"; +import User from "./User"; +import BaseModel from "./DatabaseBaseModel/DatabaseBaseModel"; +import Route from "../../Types/API/Route"; +import ColumnAccessControl from "../../Types/Database/AccessControl/ColumnAccessControl"; +import TableAccessControl from "../../Types/Database/AccessControl/TableAccessControl"; +import AccessControlColumn from "../../Types/Database/AccessControlColumn"; +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 MultiTenentQueryAllowed from "../../Types/Database/MultiTenentQueryAllowed"; +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 { + Column, + Entity, + Index, + JoinColumn, + JoinTable, + ManyToMany, + ManyToOne, +} from "typeorm"; + +export type AlertGroupingRuleType = typeof AlertGroupingRule; +import AlertGroupingRule from "./AlertGroupingRule"; +import NotificationRuleWorkspaceChannel from "../../Types/Workspace/NotificationRules/NotificationRuleWorkspaceChannel"; + +@EnableDocumentation() +@AccessControlColumn("labels") +@MultiTenentQueryAllowed(true) +@TenantColumn("projectId") +@TableAccessControl({ + create: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.ProjectMember, + Permission.CreateAlertEpisode, + ], + read: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.ProjectMember, + Permission.ReadAlertEpisode, + ], + delete: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.ProjectMember, + Permission.DeleteAlertEpisode, + ], + update: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.ProjectMember, + Permission.EditAlertEpisode, + ], +}) +@CrudApiEndpoint(new Route("/alert-episode")) +@Entity({ + name: "AlertEpisode", +}) +@EnableWorkflow({ + create: true, + delete: true, + update: true, + read: true, +}) +@TableMetadata({ + tableName: "AlertEpisode", + singularName: "Alert Episode", + pluralName: "Alert Episodes", + icon: IconProp.Layers, + tableDescription: + "Manage alert episodes (groups of related alerts) for your project", + enableRealtimeEventsOn: { + create: true, + update: true, + delete: true, + }, +}) +export default class AlertEpisode extends BaseModel { + @ColumnAccessControl({ + create: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.ProjectMember, + Permission.CreateAlertEpisode, + ], + read: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.ProjectMember, + Permission.ReadAlertEpisode, + ], + 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.CreateAlertEpisode, + ], + read: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.ProjectMember, + Permission.ReadAlertEpisode, + ], + 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.CreateAlertEpisode, + ], + read: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.ProjectMember, + Permission.ReadAlertEpisode, + ], + update: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.ProjectMember, + Permission.EditAlertEpisode, + ], + }) + @Index() + @TableColumn({ + required: true, + type: TableColumnType.LongText, + canReadOnRelationQuery: true, + title: "Title", + description: "Title of this alert episode", + }) + @Column({ + nullable: false, + type: ColumnType.LongText, + length: ColumnLength.LongText, + }) + public title?: string = undefined; + + @ColumnAccessControl({ + create: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.ProjectMember, + Permission.CreateAlertEpisode, + ], + read: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.ProjectMember, + Permission.ReadAlertEpisode, + ], + update: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.ProjectMember, + Permission.EditAlertEpisode, + ], + }) + @TableColumn({ + required: false, + type: TableColumnType.Markdown, + title: "Description", + description: + "Description of this alert episode. This is in markdown format.", + }) + @Column({ + nullable: true, + type: ColumnType.Markdown, + }) + public description?: string = undefined; + + @ColumnAccessControl({ + create: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.ProjectMember, + Permission.CreateAlertEpisode, + ], + read: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.ProjectMember, + Permission.ReadAlertEpisode, + ], + update: [], + }) + @Index() + @TableColumn({ + isDefaultValueColumn: false, + required: false, + type: TableColumnType.Number, + title: "Episode Number", + description: "Auto-incrementing episode number per project", + }) + @Column({ + type: ColumnType.Number, + nullable: true, + }) + public episodeNumber?: number = undefined; + + @ColumnAccessControl({ + create: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.ProjectMember, + Permission.CreateAlertEpisode, + ], + read: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.ProjectMember, + Permission.ReadAlertEpisode, + ], + update: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.ProjectMember, + Permission.EditAlertEpisode, + ], + }) + @TableColumn({ + manyToOneRelationColumn: "currentAlertStateId", + type: TableColumnType.Entity, + modelType: AlertState, + title: "Current Alert State", + description: + "Current state of this episode. Is the episode acknowledged? or resolved?", + }) + @ManyToOne( + () => { + return AlertState; + }, + { + eager: false, + nullable: true, + orphanedRowAction: "nullify", + }, + ) + @JoinColumn({ name: "currentAlertStateId" }) + public currentAlertState?: AlertState = undefined; + + @ColumnAccessControl({ + create: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.ProjectMember, + Permission.CreateAlertEpisode, + ], + read: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.ProjectMember, + Permission.ReadAlertEpisode, + ], + update: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.ProjectMember, + Permission.EditAlertEpisode, + ], + }) + @Index() + @TableColumn({ + type: TableColumnType.ObjectID, + required: true, + isDefaultValueColumn: true, + title: "Current Alert State ID", + description: "Current Alert State ID", + }) + @Column({ + type: ColumnType.ObjectID, + nullable: false, + transformer: ObjectID.getDatabaseTransformer(), + }) + public currentAlertStateId?: ObjectID = undefined; + + @ColumnAccessControl({ + create: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.ProjectMember, + Permission.CreateAlertEpisode, + ], + read: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.ProjectMember, + Permission.ReadAlertEpisode, + ], + update: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.ProjectMember, + Permission.EditAlertEpisode, + ], + }) + @TableColumn({ + manyToOneRelationColumn: "alertSeverityId", + type: TableColumnType.Entity, + modelType: AlertSeverity, + title: "Alert Severity", + description: + "High-water mark severity of this episode. Represents the highest severity among all member alerts.", + }) + @ManyToOne( + () => { + return AlertSeverity; + }, + { + eager: false, + nullable: true, + orphanedRowAction: "nullify", + }, + ) + @JoinColumn({ name: "alertSeverityId" }) + public alertSeverity?: AlertSeverity = undefined; + + @ColumnAccessControl({ + create: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.ProjectMember, + Permission.CreateAlertEpisode, + ], + read: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.ProjectMember, + Permission.ReadAlertEpisode, + ], + update: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.ProjectMember, + Permission.EditAlertEpisode, + ], + }) + @Index() + @TableColumn({ + type: TableColumnType.ObjectID, + required: false, + title: "Alert Severity ID", + description: "Alert Severity ID", + }) + @Column({ + type: ColumnType.ObjectID, + nullable: true, + transformer: ObjectID.getDatabaseTransformer(), + }) + public alertSeverityId?: ObjectID = undefined; + + @ColumnAccessControl({ + create: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.ProjectMember, + Permission.CreateAlertEpisode, + ], + read: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.ProjectMember, + Permission.ReadAlertEpisode, + ], + update: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.ProjectMember, + Permission.EditAlertEpisode, + ], + }) + @TableColumn({ + type: TableColumnType.Markdown, + required: false, + isDefaultValueColumn: false, + title: "Root Cause", + description: "User-documented root cause of this episode", + }) + @Column({ + type: ColumnType.Markdown, + nullable: true, + }) + public rootCause?: string = undefined; + + @ColumnAccessControl({ + create: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.ProjectMember, + Permission.CreateAlertEpisode, + ], + read: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.ProjectMember, + Permission.ReadAlertEpisode, + ], + update: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.ProjectMember, + Permission.EditAlertEpisode, + ], + }) + @Index() + @TableColumn({ + type: TableColumnType.Date, + title: "Last Alert Added At", + description: "When the last alert was added to this episode", + }) + @Column({ + type: ColumnType.Date, + nullable: true, + unique: false, + }) + public lastAlertAddedAt?: Date = undefined; + + @ColumnAccessControl({ + create: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.ProjectMember, + Permission.CreateAlertEpisode, + ], + read: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.ProjectMember, + Permission.ReadAlertEpisode, + ], + update: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.ProjectMember, + Permission.EditAlertEpisode, + ], + }) + @Index() + @TableColumn({ + type: TableColumnType.Date, + title: "Resolved At", + description: "When this episode was resolved", + }) + @Column({ + type: ColumnType.Date, + nullable: true, + unique: false, + }) + public resolvedAt?: Date = undefined; + + @ColumnAccessControl({ + create: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.ProjectMember, + Permission.CreateAlertEpisode, + ], + read: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.ProjectMember, + Permission.ReadAlertEpisode, + ], + update: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.ProjectMember, + Permission.EditAlertEpisode, + ], + }) + @TableColumn({ + manyToOneRelationColumn: "assignedToUserId", + type: TableColumnType.Entity, + modelType: User, + title: "Assigned To User", + description: "User who is assigned to this episode", + }) + @ManyToOne( + () => { + return User; + }, + { + eager: false, + nullable: true, + onDelete: "SET NULL", + orphanedRowAction: "nullify", + }, + ) + @JoinColumn({ name: "assignedToUserId" }) + public assignedToUser?: User = undefined; + + @ColumnAccessControl({ + create: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.ProjectMember, + Permission.CreateAlertEpisode, + ], + read: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.ProjectMember, + Permission.ReadAlertEpisode, + ], + update: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.ProjectMember, + Permission.EditAlertEpisode, + ], + }) + @Index() + @TableColumn({ + type: TableColumnType.ObjectID, + required: false, + title: "Assigned To User ID", + description: "User ID who is assigned to this episode", + }) + @Column({ + type: ColumnType.ObjectID, + nullable: true, + transformer: ObjectID.getDatabaseTransformer(), + }) + public assignedToUserId?: ObjectID = undefined; + + @ColumnAccessControl({ + create: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.ProjectMember, + Permission.CreateAlertEpisode, + ], + read: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.ProjectMember, + Permission.ReadAlertEpisode, + ], + update: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.ProjectMember, + Permission.EditAlertEpisode, + ], + }) + @TableColumn({ + manyToOneRelationColumn: "assignedToTeamId", + type: TableColumnType.Entity, + modelType: Team, + title: "Assigned To Team", + description: "Team that is assigned to this episode", + }) + @ManyToOne( + () => { + return Team; + }, + { + eager: false, + nullable: true, + onDelete: "SET NULL", + orphanedRowAction: "nullify", + }, + ) + @JoinColumn({ name: "assignedToTeamId" }) + public assignedToTeam?: Team = undefined; + + @ColumnAccessControl({ + create: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.ProjectMember, + Permission.CreateAlertEpisode, + ], + read: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.ProjectMember, + Permission.ReadAlertEpisode, + ], + update: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.ProjectMember, + Permission.EditAlertEpisode, + ], + }) + @Index() + @TableColumn({ + type: TableColumnType.ObjectID, + required: false, + title: "Assigned To Team ID", + description: "Team ID that is assigned to this episode", + }) + @Column({ + type: ColumnType.ObjectID, + nullable: true, + transformer: ObjectID.getDatabaseTransformer(), + }) + public assignedToTeamId?: ObjectID = undefined; + + @ColumnAccessControl({ + create: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.ProjectMember, + Permission.CreateAlertEpisode, + ], + read: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.ProjectMember, + Permission.ReadAlertEpisode, + ], + update: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.ProjectMember, + Permission.EditAlertEpisode, + ], + }) + @TableColumn({ + manyToOneRelationColumn: "alertGroupingRuleId", + type: TableColumnType.Entity, + modelType: AlertGroupingRule, + title: "Alert Grouping Rule", + description: + "The grouping rule that created this episode (null for manually created episodes)", + }) + @ManyToOne( + () => { + return AlertGroupingRule; + }, + { + eager: false, + nullable: true, + onDelete: "SET NULL", + orphanedRowAction: "nullify", + }, + ) + @JoinColumn({ name: "alertGroupingRuleId" }) + public alertGroupingRule?: AlertGroupingRule = undefined; + + @ColumnAccessControl({ + create: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.ProjectMember, + Permission.CreateAlertEpisode, + ], + read: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.ProjectMember, + Permission.ReadAlertEpisode, + ], + update: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.ProjectMember, + Permission.EditAlertEpisode, + ], + }) + @Index() + @TableColumn({ + type: TableColumnType.ObjectID, + required: false, + title: "Alert Grouping Rule ID", + description: "Alert Grouping Rule ID that created this episode", + }) + @Column({ + type: ColumnType.ObjectID, + nullable: true, + transformer: ObjectID.getDatabaseTransformer(), + }) + public alertGroupingRuleId?: ObjectID = undefined; + + @ColumnAccessControl({ + create: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.ProjectMember, + Permission.CreateAlertEpisode, + ], + read: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.ProjectMember, + Permission.ReadAlertEpisode, + ], + update: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.ProjectMember, + Permission.EditAlertEpisode, + ], + }) + @TableColumn({ + required: false, + type: TableColumnType.EntityArray, + modelType: OnCallDutyPolicy, + title: "On-Call Duty Policies", + description: "List of on-call duty policies to execute for this episode.", + }) + @ManyToMany( + () => { + return OnCallDutyPolicy; + }, + { eager: false }, + ) + @JoinTable({ + name: "AlertEpisodeOnCallDutyPolicy", + inverseJoinColumn: { + name: "onCallDutyPolicyId", + referencedColumnName: "_id", + }, + joinColumn: { + name: "alertEpisodeId", + referencedColumnName: "_id", + }, + }) + public onCallDutyPolicies?: Array = undefined; + + @ColumnAccessControl({ + create: [], + read: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.ProjectMember, + Permission.ReadAlertEpisode, + ], + update: [], + }) + @Index() + @TableColumn({ + type: TableColumnType.Boolean, + required: true, + isDefaultValueColumn: true, + title: "Is On-Call Policy Executed?", + description: + "Whether the on-call policy has been executed for this episode", + defaultValue: false, + }) + @Column({ + type: ColumnType.Boolean, + nullable: false, + default: false, + }) + public isOnCallPolicyExecuted?: boolean = undefined; + + @ColumnAccessControl({ + create: [], + read: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.ProjectMember, + Permission.ReadAlertEpisode, + ], + update: [], + }) + @Index() + @TableColumn({ + type: TableColumnType.Number, + required: true, + isDefaultValueColumn: true, + title: "Alert Count", + description: "Denormalized count of alerts in this episode", + defaultValue: 0, + }) + @Column({ + type: ColumnType.Number, + nullable: false, + default: 0, + }) + public alertCount?: number = undefined; + + @ColumnAccessControl({ + create: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.ProjectMember, + Permission.CreateAlertEpisode, + ], + read: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.ProjectMember, + Permission.ReadAlertEpisode, + ], + update: [], + }) + @TableColumn({ + required: false, + type: TableColumnType.LongText, + title: "Title Template", + description: + "Template used to generate the episode title. Stored for dynamic variable updates.", + }) + @Column({ + type: ColumnType.LongText, + nullable: true, + }) + public titleTemplate?: string = undefined; + + @ColumnAccessControl({ + create: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.ProjectMember, + Permission.CreateAlertEpisode, + ], + read: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.ProjectMember, + Permission.ReadAlertEpisode, + ], + update: [], + }) + @TableColumn({ + required: false, + type: TableColumnType.LongText, + title: "Description Template", + description: + "Template used to generate the episode description. Stored for dynamic variable updates.", + }) + @Column({ + type: ColumnType.LongText, + nullable: true, + }) + public descriptionTemplate?: string = undefined; + + @ColumnAccessControl({ + create: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.ProjectMember, + Permission.CreateAlertEpisode, + ], + read: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.ProjectMember, + Permission.ReadAlertEpisode, + ], + update: [], + }) + @Index() + @TableColumn({ + type: TableColumnType.Boolean, + required: true, + isDefaultValueColumn: true, + title: "Is Manually Created?", + description: + "Whether this episode was manually created vs auto-created by a rule", + defaultValue: false, + }) + @Column({ + type: ColumnType.Boolean, + nullable: false, + default: false, + }) + public isManuallyCreated?: boolean = undefined; + + @ColumnAccessControl({ + create: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.ProjectMember, + Permission.CreateAlertEpisode, + ], + read: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.ProjectMember, + Permission.ReadAlertEpisode, + ], + update: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.ProjectMember, + Permission.EditAlertEpisode, + ], + }) + @TableColumn({ + required: false, + type: TableColumnType.EntityArray, + modelType: Label, + title: "Labels", + description: + "Relation to Labels Array where this object is categorized in.", + }) + @ManyToMany( + () => { + return Label; + }, + { eager: false }, + ) + @JoinTable({ + name: "AlertEpisodeLabel", + inverseJoinColumn: { + name: "labelId", + referencedColumnName: "_id", + }, + joinColumn: { + name: "alertEpisodeId", + referencedColumnName: "_id", + }, + }) + public labels?: Array