add recurring field element adn controls.

This commit is contained in:
Simon Larsen 2024-09-10 15:25:50 +01:00
parent 72ffaace2d
commit 20e56fb1d7
No known key found for this signature in database
GPG Key ID: 96C5DCA24769DBCA
15 changed files with 446 additions and 77 deletions

View File

@ -8,7 +8,6 @@ import OnCallDutyPolicyExecutionLogService from "Common/Server/Services/OnCallDu
import logger from "Common/Server/Utils/Logger";
import OnCallDutyPolicyEscalationRule from "Common/Models/DatabaseModels/OnCallDutyPolicyEscalationRule";
import OnCallDutyPolicyExecutionLog from "Common/Models/DatabaseModels/OnCallDutyPolicyExecutionLog";
import ObjectID from "Common/Types/ObjectID";
import IncidentService from "Common/Server/Services/IncidentService";
RunCron(
@ -58,7 +57,6 @@ RunCron(
},
);
type ExecuteOnCallPolicyFunction = (
executionLog: OnCallDutyPolicyExecutionLog,
) => Promise<void>;
@ -67,31 +65,30 @@ const executeOnCallPolicy: ExecuteOnCallPolicyFunction = async (
executionLog: OnCallDutyPolicyExecutionLog,
): Promise<void> => {
try {
// get trigger by incident
if(executionLog.triggeredByIncidentId){
// check if this incident is ack.
const isAcknowledged: boolean = await IncidentService.isIncidentAcknowledged({
incidentId: executionLog.triggeredByIncidentId
});
if (executionLog.triggeredByIncidentId) {
// check if this incident is ack.
const isAcknowledged: boolean =
await IncidentService.isIncidentAcknowledged({
incidentId: executionLog.triggeredByIncidentId,
});
if(isAcknowledged){
// then mark this policy as executed.
if (isAcknowledged) {
// then mark this policy as executed.
await OnCallDutyPolicyExecutionLogService.updateOneById({
id: executionLog.id!,
data: {
status: OnCallDutyPolicyStatus.Completed
status: OnCallDutyPolicyStatus.Completed,
},
props: {
isRoot: true
}
})
isRoot: true,
},
});
return;
return;
}
}
// check if this execution needs to be executed.
const currentDate: Date = OneUptimeDate.getCurrentDate();

View File

@ -882,7 +882,6 @@ export default class ScheduledMaintenance extends BaseModel {
})
public isOwnerNotifiedOfResourceCreation?: boolean = undefined;
@ColumnAccessControl({
create: [
Permission.ProjectOwner,
@ -916,7 +915,6 @@ export default class ScheduledMaintenance extends BaseModel {
})
public subscriberNotificationsBeforeTheEvent?: Array<Recurring> = undefined;
@ColumnAccessControl({
create: [
Permission.ProjectOwner,
@ -950,5 +948,4 @@ export default class ScheduledMaintenance extends BaseModel {
nullable: true,
})
public nextSubscriberNotificationBeforeTheEventAt?: Date = undefined;
}

View File

@ -950,4 +950,37 @@ export default class ScheduledMaintenanceTemplate extends BaseModel {
nullable: true,
})
public customFields?: JSONObject = undefined;
@ColumnAccessControl({
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateScheduledMaintenanceTemplate,
],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadScheduledMaintenanceTemplate,
],
update: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.EditScheduledMaintenanceTemplate,
],
})
@TableColumn({
type: TableColumnType.JSON,
required: false,
isDefaultValueColumn: false,
title: "Subscriber notifications before the event",
description: "Should subscribers be notified before the event?",
})
@Column({
type: ColumnType.JSON,
nullable: true,
})
public subscriberNotificationsBeforeTheEvent?: Array<Recurring> = undefined;
}

View File

@ -1,18 +1,29 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class MigrationName1725975175669 implements MigrationInterface {
public name = 'MigrationName1725975175669'
public name = "MigrationName1725975175669";
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "ScheduledMaintenance" ADD "subscriberNotificationsBeforeTheEvent" jsonb`);
await queryRunner.query(`ALTER TABLE "ScheduledMaintenance" ADD "nextSubscriberNotificationBeforeTheEventAt" TIMESTAMP WITH TIME ZONE`);
await queryRunner.query(`CREATE INDEX "IDX_37b2094ce25cc62b4766a7d3b1" ON "ScheduledMaintenance" ("nextSubscriberNotificationBeforeTheEventAt") `);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`DROP INDEX "public"."IDX_37b2094ce25cc62b4766a7d3b1"`);
await queryRunner.query(`ALTER TABLE "ScheduledMaintenance" DROP COLUMN "nextSubscriberNotificationBeforeTheEventAt"`);
await queryRunner.query(`ALTER TABLE "ScheduledMaintenance" DROP COLUMN "subscriberNotificationsBeforeTheEvent"`);
}
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "ScheduledMaintenance" ADD "subscriberNotificationsBeforeTheEvent" jsonb`,
);
await queryRunner.query(
`ALTER TABLE "ScheduledMaintenance" ADD "nextSubscriberNotificationBeforeTheEventAt" TIMESTAMP WITH TIME ZONE`,
);
await queryRunner.query(
`CREATE INDEX "IDX_37b2094ce25cc62b4766a7d3b1" ON "ScheduledMaintenance" ("nextSubscriberNotificationBeforeTheEventAt") `,
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`DROP INDEX "public"."IDX_37b2094ce25cc62b4766a7d3b1"`,
);
await queryRunner.query(
`ALTER TABLE "ScheduledMaintenance" DROP COLUMN "nextSubscriberNotificationBeforeTheEventAt"`,
);
await queryRunner.query(
`ALTER TABLE "ScheduledMaintenance" DROP COLUMN "subscriberNotificationsBeforeTheEvent"`,
);
}
}

View File

@ -0,0 +1,17 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class MigrationName1725976810107 implements MigrationInterface {
public name = "MigrationName1725976810107";
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "ScheduledMaintenanceTemplate" ADD "subscriberNotificationsBeforeTheEvent" jsonb`,
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "ScheduledMaintenanceTemplate" DROP COLUMN "subscriberNotificationsBeforeTheEvent"`,
);
}
}

View File

@ -59,6 +59,7 @@ import { MigrationName1725898621366 } from "./1725898621366-MigrationName";
import { MigrationName1725900315712 } from "./1725900315712-MigrationName";
import { MigrationName1725901024444 } from "./1725901024444-MigrationName";
import { MigrationName1725975175669 } from "./1725975175669-MigrationName";
import { MigrationName1725976810107 } from "./1725976810107-MigrationName";
export default [
InitialMigration,
@ -121,5 +122,6 @@ export default [
MigrationName1725898621366,
MigrationName1725900315712,
MigrationName1725901024444,
MigrationName1725975175669
MigrationName1725975175669,
MigrationName1725976810107,
];

View File

@ -41,8 +41,8 @@ export class Service extends DatabaseService<Model> {
}
public async isIncidentAcknowledged(data: {
incidentId: ObjectID
}){
incidentId: ObjectID;
}): Promise<boolean> {
const incident: Model | null = await this.findOneBy({
query: {
_id: data.incidentId,
@ -50,38 +50,39 @@ export class Service extends DatabaseService<Model> {
select: {
projectId: true,
currentIncidentState: {
order: true
}
order: true,
},
},
props: {
isRoot: true
}
isRoot: true,
},
});
if(!incident){
if (!incident) {
throw new BadDataException("Incident not found");
}
if(!incident.projectId){
if (!incident.projectId) {
throw new BadDataException("Incient Project ID not found");
}
const ackIncidentState: IncidentState = await IncidentStateService.getAcknowledgedIncidentState({
projectId: incident.projectId,
props: {
isRoot: true,
}
});
const ackIncidentState: IncidentState =
await IncidentStateService.getAcknowledgedIncidentState({
projectId: incident.projectId,
props: {
isRoot: true,
},
});
const currentIncidentStateOrder: number = incident.currentIncidentState?.order!;
const ackIncidentStateOrder: number = ackIncidentState.order!;
const currentIncidentStateOrder: number =
incident.currentIncidentState!.order!;
const ackIncidentStateOrder: number = ackIncidentState.order!;
if(currentIncidentStateOrder>=ackIncidentStateOrder){
return true;
if (currentIncidentStateOrder >= ackIncidentStateOrder) {
return true;
}
return false;
return false;
}
public async acknowledgeIncident(

View File

@ -153,9 +153,9 @@ export class Service extends DatabaseService<IncidentState> {
}
public async getAllIncidentStates(data: {
projectId: ObjectID,
props: DatabaseCommonInteractionProps,
}){
projectId: ObjectID;
props: DatabaseCommonInteractionProps;
}): Promise<Array<IncidentState>> {
const incidentStates: Array<IncidentState> = await this.findBy({
query: {
projectId: data.projectId,
@ -168,24 +168,25 @@ export class Service extends DatabaseService<IncidentState> {
select: {
_id: true,
isResolvedState: true,
isAcknowledgedState: true,
isAcknowledgedState: true,
isCreatedState: true,
order: true
order: true,
},
props: data.props,
});
return incidentStates;
return incidentStates;
}
public async getUnresolvedIncidentStates(
projectId: ObjectID,
props: DatabaseCommonInteractionProps,
): Promise<IncidentState[]> {
const incidentStates: Array<IncidentState> = await this.getAllIncidentStates({
projectId: projectId,
props: props
})
const incidentStates: Array<IncidentState> =
await this.getAllIncidentStates({
projectId: projectId,
props: props,
});
const unresolvedIncidentStates: Array<IncidentState> = [];
@ -201,21 +202,25 @@ export class Service extends DatabaseService<IncidentState> {
}
public async getAcknowledgedIncidentState(data: {
projectId: ObjectID,
props: DatabaseCommonInteractionProps,
}
): Promise<IncidentState> {
const incidentStates: Array<IncidentState> = await this.getAllIncidentStates({
projectId: data.projectId,
props: data.props
})
projectId: ObjectID;
props: DatabaseCommonInteractionProps;
}): Promise<IncidentState> {
const incidentStates: Array<IncidentState> =
await this.getAllIncidentStates({
projectId: data.projectId,
props: data.props,
});
const ackIncidentState: IncidentState | undefined = incidentStates.find((incidentState: IncidentState)=>{
return incidentState?.isAcknowledgedState;
});
const ackIncidentState: IncidentState | undefined = incidentStates.find(
(incidentState: IncidentState) => {
return incidentState?.isAcknowledgedState;
},
);
if(!ackIncidentState){
throw new BadDataException("Acknowledged Incident State not found for this project");
if (!ackIncidentState) {
throw new BadDataException(
"Acknowledged Incident State not found for this project",
);
}
return ackIncidentState;

View File

@ -1,7 +1,7 @@
import DatabaseProperty from "../Database/DatabaseProperty";
import OneUptimeDate from "../Date";
import BadDataException from "../Exception/BadDataException";
import { JSONObject, ObjectType } from "../JSON";
import { JSONArray, JSONObject, ObjectType } from "../JSON";
import JSONFunctions from "../JSONFunctions";
import PositiveNumber from "../PositiveNumber";
import EventInterval from "./EventInterval";
@ -114,6 +114,18 @@ export default class Recurring extends DatabaseProperty {
});
}
public static fromJSONArray(
json: JSONArray | Array<Recurring>,
): Array<Recurring> {
const arrayToReturn: Array<Recurring> = [];
for (const item of json) {
arrayToReturn.push(this.fromJSON(item));
}
return arrayToReturn;
}
public static override fromJSON(json: JSONObject | Recurring): Recurring {
if (json instanceof Recurring) {
return json;

View File

@ -0,0 +1,106 @@
import EventInterval from "Common/Types/Events/EventInterval";
import Recurring from "Common/Types/Events/Recurring";
import PositiveNumber from "Common/Types/PositiveNumber";
import React, { FunctionComponent, ReactElement, useState } from "react";
import RecurringFieldElement from "./RecurringFieldElement";
import Button, { ButtonSize, ButtonStyleType } from "../Button/Button";
import IconProp from "../../../Types/Icon/IconProp";
export interface ComponentProps {
error?: string | undefined;
onChange?: ((value: Array<Recurring>) => void) | undefined;
initialValue?: Array<Recurring> | undefined;
}
const RecurringArrayFieldElement: FunctionComponent<ComponentProps> = (
props: ComponentProps,
): ReactElement => {
const [recurrings, setRecurrings] = useState<Array<Recurring> | undefined>(
props.initialValue && props.initialValue.length > 0
? props.initialValue.map((item: Recurring) => {
return Recurring.fromJSON(item);
})
: undefined,
);
type UpdateRecurringFunction = (recurring: Recurring, index: number) => void;
const updateRecurrings: UpdateRecurringFunction = (
recurring: Recurring,
index: number,
): void => {
const existingRecurrings: Array<Recurring> = [...(recurrings || [])];
existingRecurrings[index] = recurring;
setRecurrings(existingRecurrings);
if (props.onChange) {
props.onChange(existingRecurrings);
}
};
return (
<div>
{recurrings &&
recurrings.map((recurring: Recurring, index: number) => {
return (
<div key={index} className="flex">
<div className="">
<RecurringFieldElement
initialValue={recurring}
onChange={(recurring: Recurring) => {
updateRecurrings(recurring, index);
}}
/>
</div>
<div>
<Button
dataTestId={`delete-${index}`}
title="Delete"
buttonStyle={ButtonStyleType.ICON}
icon={IconProp.Trash}
onClick={() => {
const newData: Array<Recurring> = [...(recurrings || [])];
newData.splice(index, 1);
setRecurrings(newData);
props.onChange && props.onChange(newData);
}}
/>
</div>
</div>
);
})}
<div className="flex space-x-3 mt-3">
<Button
dataTestId={`add-recurring`}
title="Add"
buttonStyle={ButtonStyleType.NORMAL}
buttonSize={ButtonSize.Small}
icon={IconProp.Add}
onClick={() => {
const newData: Array<Recurring> = [...(recurrings || [])];
const recurring: Recurring = new Recurring();
recurring.intervalCount = new PositiveNumber(1);
recurring.intervalType = EventInterval.Day;
newData.push(recurring);
setRecurrings(newData);
props.onChange && props.onChange(newData);
}}
/>
</div>
{props.error && (
<p data-testid="error-message" className="mt-1 text-sm text-red-400">
{props.error}
</p>
)}
</div>
);
};
export default RecurringArrayFieldElement;

View File

@ -0,0 +1,30 @@
import Recurring from "Common/Types/Events/Recurring";
import React, { FunctionComponent, ReactElement } from "react";
import RecurringViewElement from "./RecurringViewElement";
export interface ComponentProps {
value?: Array<Recurring> | undefined;
}
const RecurringArrayViewElement: FunctionComponent<ComponentProps> = (
props: ComponentProps,
): ReactElement => {
if (!props.value) {
return <p>-</p>;
}
const items: Array<Recurring> = Recurring.fromJSONArray(props.value);
return (
<div className="space-y-2">
{items &&
items.length > 0 &&
items.map((item: Recurring, index: number) => {
return <RecurringViewElement key={index} value={item} />;
})}
{(!items || items.length === 0) && <p>-</p>}
</div>
);
};
export default RecurringArrayViewElement;

View File

@ -33,6 +33,9 @@ import DropdownUtil from "Common/UI/Utils/Dropdown";
import IconProp from "Common/Types/Icon/IconProp";
import { ButtonStyleType } from "Common/UI/Components/Button/Button";
import FormValues from "Common/UI/Components/Forms/Types/FormValues";
import { CustomElementProps } from "Common/UI/Components/Forms/Types/Field";
import RecurringArrayFieldElement from "Common/UI/Components/Events/RecurringArrayFieldElement";
import Recurring from "Common/Types/Events/Recurring";
export interface ComponentProps {
query?: Query<ScheduledMaintenance> | undefined;
@ -84,6 +87,7 @@ const ScheduledMaintenancesTable: FunctionComponent<ComponentProps> = (
shouldStatusPageSubscribersBeNotifiedWhenEventChangedToEnded: true,
shouldStatusPageSubscribersBeNotifiedWhenEventChangedToOngoing:
true,
subscriberNotificationsBeforeTheEvent: true,
},
});
@ -432,6 +436,30 @@ const ScheduledMaintenancesTable: FunctionComponent<ComponentProps> = (
defaultValue: true,
required: false,
},
{
field: {
subscriberNotificationsBeforeTheEvent: true,
},
stepId: "subscribers",
title: "Send reminders to subscribers before the event",
description:
"Please add a list of notification options to notify subscribers before the event",
fieldType: FormFieldSchemaType.CustomComponent,
getCustomElement: (
value: FormValues<ScheduledMaintenance>,
props: CustomElementProps,
) => {
return (
<RecurringArrayFieldElement
{...props}
initialValue={
value.subscriberNotificationsBeforeTheEvent as Array<Recurring>
}
/>
);
},
required: true,
},
{
field: {
labels: true,

View File

@ -24,6 +24,11 @@ import ScheduledMaintenance from "Common/Models/DatabaseModels/ScheduledMaintena
import ScheduledMaintenanceStateTimeline from "Common/Models/DatabaseModels/ScheduledMaintenanceStateTimeline";
import StatusPage from "Common/Models/DatabaseModels/StatusPage";
import React, { Fragment, FunctionComponent, ReactElement } from "react";
import FormValues from "Common/UI/Components/Forms/Types/FormValues";
import { CustomElementProps } from "Common/UI/Components/Forms/Types/Field";
import RecurringArrayFieldElement from "Common/UI/Components/Events/RecurringArrayFieldElement";
import Recurring from "Common/Types/Events/Recurring";
import RecurringArrayViewElement from "Common/UI/Components/Events/RecurringArrayViewElement";
const ScheduledMaintenanceView: FunctionComponent<
PageComponentProps
@ -53,6 +58,10 @@ const ScheduledMaintenanceView: FunctionComponent<
title: "Status Pages",
id: "status-pages",
},
{
title: "Subscribers",
id: "subscribers",
},
{
title: "Labels",
id: "labels",
@ -127,6 +136,72 @@ const ScheduledMaintenanceView: FunctionComponent<
required: false,
placeholder: "Select Status Pages",
},
{
field: {
shouldStatusPageSubscribersBeNotifiedOnEventCreated: true,
},
title: "Event Created: Notify Status Page Subscribers",
stepId: "subscribers",
description:
"Should status page subscribers be notified when this event is created?",
fieldType: FormFieldSchemaType.Checkbox,
defaultValue: true,
required: false,
},
{
field: {
shouldStatusPageSubscribersBeNotifiedWhenEventChangedToOngoing:
true,
},
title: "Event Ongoing: Notify Status Page Subscribers",
stepId: "subscribers",
description:
"Should status page subscribers be notified when this event state changes to ongoing?",
fieldType: FormFieldSchemaType.Checkbox,
defaultValue: true,
required: false,
},
{
field: {
shouldStatusPageSubscribersBeNotifiedWhenEventChangedToEnded:
true,
},
title: "Event Ended: Notify Status Page Subscribers",
stepId: "subscribers",
description:
"Should status page subscribers be notified when this event state changes to ended?",
fieldType: FormFieldSchemaType.Checkbox,
defaultValue: true,
required: false,
},
{
field: {
subscriberNotificationsBeforeTheEvent: true,
},
stepId: "subscribers",
title: "Send reminders to subscribers before the event",
description:
"Please add a list of notification options to notify subscribers before the event",
fieldType: FormFieldSchemaType.CustomComponent,
getCustomElement: (
value: FormValues<ScheduledMaintenance>,
props: CustomElementProps,
) => {
return (
<RecurringArrayFieldElement
{...props}
initialValue={
value.subscriberNotificationsBeforeTheEvent as Array<Recurring>
}
/>
);
},
required: true,
},
{
field: {
labels: true,
@ -276,6 +351,20 @@ const ScheduledMaintenanceView: FunctionComponent<
title: "Created At",
fieldType: FieldType.DateTime,
},
{
field: {
shouldStatusPageSubscribersBeNotifiedOnEventCreated: true,
},
title: "Send reminders to subscribers before the event",
fieldType: FieldType.Boolean,
getElement: (item: ScheduledMaintenance): ReactElement => {
return (
<RecurringArrayViewElement
value={item.subscriberNotificationsBeforeTheEvent}
/>
);
},
},
{
field: {
shouldStatusPageSubscribersBeNotifiedOnEventCreated: true,

View File

@ -28,6 +28,7 @@ import {
getFormSteps,
getTemplateFormFields,
} from "./ScheduledMaintenanceTemplates";
import RecurringArrayViewElement from "Common/UI/Components/Events/RecurringArrayViewElement";
const TeamView: FunctionComponent<PageComponentProps> = (): ReactElement => {
const modelId: ObjectID = Navigation.getLastParamAsObjectID();
@ -176,7 +177,22 @@ const TeamView: FunctionComponent<PageComponentProps> = (): ReactElement => {
return Boolean(item.isRecurringEvent);
},
},
{
field: {
shouldStatusPageSubscribersBeNotifiedOnEventCreated: true,
},
title: "Send reminders to subscribers before the event",
fieldType: FieldType.Boolean,
getElement: (
item: ScheduledMaintenanceTemplate,
): ReactElement => {
return (
<RecurringArrayViewElement
value={item.subscriberNotificationsBeforeTheEvent}
/>
);
},
},
{
field: {
shouldStatusPageSubscribersBeNotifiedOnEventCreated: true,

View File

@ -19,6 +19,7 @@ import { CustomElementProps } from "Common/UI/Components/Forms/Types/Field";
import RecurringFieldElement from "Common/UI/Components/Events/RecurringFieldElement";
import Recurring from "Common/Types/Events/Recurring";
import OneUptimeDate from "Common/Types/Date";
import RecurringArrayFieldElement from "Common/UI/Components/Events/RecurringArrayFieldElement";
type GetTemplateFormFieldsFunction = (data: {
isViewPage: boolean;
@ -230,6 +231,30 @@ export const getTemplateFormFields: GetTemplateFormFieldsFunction = (data: {
defaultValue: true,
required: false,
},
{
field: {
subscriberNotificationsBeforeTheEvent: true,
},
stepId: "subscribers",
title: "Send reminders to subscribers before the event",
description:
"Please add a list of notification options to notify subscribers before the event",
fieldType: FormFieldSchemaType.CustomComponent,
getCustomElement: (
value: FormValues<ScheduledMaintenanceTemplate>,
props: CustomElementProps,
) => {
return (
<RecurringArrayFieldElement
{...props}
initialValue={
value.subscriberNotificationsBeforeTheEvent as Array<Recurring>
}
/>
);
},
required: true,
},
{
field: {
isRecurringEvent: true,