mirror of
https://github.com/OneUptime/oneuptime.git
synced 2026-04-06 00:32:12 +02:00
feat: Add sendFirstReportAt column and update defaults for OnCallDutyPolicyScheduleLayer
- Introduced a new column `sendFirstReportAt` to the `WorkspaceNotificationSummary` table. - Updated default values for `rotation` and `restrictionTimes` columns in the `OnCallDutyPolicyScheduleLayer` table. - Adjusted the logic in SendSummary.ts to calculate the next send time based on the recurring interval before updating the nextSendAt field.
This commit is contained in:
@@ -90,6 +90,8 @@ const WorkspaceSummaryTable: FunctionComponent<ComponentProps> = (
|
||||
const allSummaryItems: Array<WorkspaceNotificationSummaryItem> =
|
||||
Object.values(WorkspaceNotificationSummaryItem);
|
||||
|
||||
const typeLabel: string = props.summaryType;
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<ModelTable<WorkspaceNotificationSummary>
|
||||
@@ -102,7 +104,7 @@ const WorkspaceSummaryTable: FunctionComponent<ComponentProps> = (
|
||||
userPreferencesKey={`workspace-summary-table-${props.summaryType}-${props.workspaceType}`}
|
||||
actionButtons={[
|
||||
{
|
||||
title: "Test Summary",
|
||||
title: "Send Test Now",
|
||||
buttonStyleType: ButtonStyleType.OUTLINE,
|
||||
icon: IconProp.Play,
|
||||
onClick: async (
|
||||
@@ -121,27 +123,29 @@ const WorkspaceSummaryTable: FunctionComponent<ComponentProps> = (
|
||||
},
|
||||
},
|
||||
]}
|
||||
singularName={`${props.summaryType} Summary`}
|
||||
pluralName={`${props.summaryType} Summaries`}
|
||||
singularName={`${typeLabel} Summary`}
|
||||
pluralName={`${typeLabel} Summaries`}
|
||||
id={`workspace-summary-table-${props.summaryType}`}
|
||||
name={`Settings > ${props.summaryType} Workspace Summaries`}
|
||||
name={`${typeLabel} Workspace Summaries`}
|
||||
isDeleteable={true}
|
||||
isEditable={true}
|
||||
createEditModalWidth={ModalWidth.Large}
|
||||
isCreateable={true}
|
||||
cardProps={{
|
||||
title: `${props.summaryType} - ${getWorkspaceTypeDisplayName(props.workspaceType)} Summary`,
|
||||
description: `Configure recurring ${props.summaryType.toLowerCase()} summary reports to be sent to ${getWorkspaceTypeDisplayName(props.workspaceType)} channels.`,
|
||||
title: `${typeLabel} Summary - ${getWorkspaceTypeDisplayName(props.workspaceType)}`,
|
||||
description: `Set up recurring ${typeLabel.toLowerCase()} summary reports posted to ${getWorkspaceTypeDisplayName(props.workspaceType)}. Each summary includes stats like total count, MTTA/MTTR, severity breakdown, and a list of ${typeLabel.toLowerCase()}s with links.`,
|
||||
}}
|
||||
showAs={ShowAs.List}
|
||||
noItemsMessage={"No summary rules found."}
|
||||
noItemsMessage={`No ${typeLabel.toLowerCase()} summary rules configured yet. Create one to start receiving periodic reports.`}
|
||||
onBeforeCreate={(values: WorkspaceNotificationSummary) => {
|
||||
values.summaryType = props.summaryType;
|
||||
values.projectId = ProjectUtil.getCurrentProjectId()!;
|
||||
values.workspaceType = props.workspaceType;
|
||||
|
||||
// Set initial nextSendAt based on recurring interval
|
||||
if (values.recurringInterval) {
|
||||
// Set nextSendAt: use sendFirstReportAt if provided, otherwise compute from interval
|
||||
if (values.sendFirstReportAt) {
|
||||
values.nextSendAt = values.sendFirstReportAt;
|
||||
} else if (values.recurringInterval) {
|
||||
const recurring: Recurring = Recurring.fromJSON(
|
||||
values.recurringInterval,
|
||||
);
|
||||
@@ -166,12 +170,12 @@ const WorkspaceSummaryTable: FunctionComponent<ComponentProps> = (
|
||||
});
|
||||
}
|
||||
|
||||
// Ensure summaryItems is an array
|
||||
if (!values.summaryItems) {
|
||||
// Default to all summary items if none selected
|
||||
if (!values.summaryItems || (Array.isArray(values.summaryItems) && values.summaryItems.length === 0)) {
|
||||
values.summaryItems = allSummaryItems;
|
||||
}
|
||||
|
||||
if (!values.isEnabled) {
|
||||
if (values.isEnabled === undefined || values.isEnabled === null) {
|
||||
values.isEnabled = true;
|
||||
}
|
||||
|
||||
@@ -193,15 +197,13 @@ const WorkspaceSummaryTable: FunctionComponent<ComponentProps> = (
|
||||
});
|
||||
}
|
||||
|
||||
// Recalculate nextSendAt if interval changed
|
||||
if (values.recurringInterval) {
|
||||
const recurring: Recurring = Recurring.fromJSON(
|
||||
values.recurringInterval,
|
||||
);
|
||||
values.nextSendAt = Recurring.getNextDateInterval(
|
||||
OneUptimeDate.getCurrentDate(),
|
||||
recurring,
|
||||
);
|
||||
// If sendFirstReportAt was changed and is in the future, use it as nextSendAt.
|
||||
// Otherwise leave nextSendAt alone — the worker manages it after the first send.
|
||||
if (values.sendFirstReportAt) {
|
||||
const firstReportDate: Date = new Date(values.sendFirstReportAt as unknown as string);
|
||||
if (firstReportDate.getTime() > OneUptimeDate.getCurrentDate().getTime()) {
|
||||
values.nextSendAt = firstReportDate;
|
||||
}
|
||||
}
|
||||
|
||||
return Promise.resolve(values);
|
||||
@@ -215,7 +217,7 @@ const WorkspaceSummaryTable: FunctionComponent<ComponentProps> = (
|
||||
fieldType: FormFieldSchemaType.Text,
|
||||
required: true,
|
||||
stepId: "basic",
|
||||
placeholder: "Weekly Incident Summary",
|
||||
placeholder: `Weekly ${typeLabel} Summary`,
|
||||
validation: {
|
||||
minLength: 2,
|
||||
},
|
||||
@@ -228,8 +230,7 @@ const WorkspaceSummaryTable: FunctionComponent<ComponentProps> = (
|
||||
title: "Description",
|
||||
fieldType: FormFieldSchemaType.LongText,
|
||||
required: false,
|
||||
placeholder:
|
||||
"Weekly summary of incidents sent to the #ops channel.",
|
||||
placeholder: `e.g., Weekly ${typeLabel.toLowerCase()} summary for the engineering team.`,
|
||||
},
|
||||
{
|
||||
field: {
|
||||
@@ -237,11 +238,10 @@ const WorkspaceSummaryTable: FunctionComponent<ComponentProps> = (
|
||||
},
|
||||
stepId: "basic",
|
||||
title: "Channel Names",
|
||||
description:
|
||||
"Comma-separated list of channel names to post the summary to (e.g., #incidents, #ops-summary).",
|
||||
description: `Enter one or more ${getWorkspaceTypeDisplayName(props.workspaceType)} channel names (comma-separated) where the summary will be posted.`,
|
||||
fieldType: FormFieldSchemaType.Text,
|
||||
required: true,
|
||||
placeholder: "#incidents-summary",
|
||||
placeholder: "#incidents-summary, #engineering",
|
||||
},
|
||||
{
|
||||
field: {
|
||||
@@ -249,7 +249,8 @@ const WorkspaceSummaryTable: FunctionComponent<ComponentProps> = (
|
||||
},
|
||||
stepId: "basic",
|
||||
title: "Enabled",
|
||||
description: "Enable or disable this recurring summary.",
|
||||
description:
|
||||
"When enabled, the summary will be sent automatically on the configured schedule.",
|
||||
fieldType: FormFieldSchemaType.Toggle,
|
||||
required: false,
|
||||
},
|
||||
@@ -257,20 +258,21 @@ const WorkspaceSummaryTable: FunctionComponent<ComponentProps> = (
|
||||
field: {
|
||||
recurringInterval: true,
|
||||
},
|
||||
title: "Recurring Interval",
|
||||
description: "How often should this summary be sent?",
|
||||
title: "How Often",
|
||||
description:
|
||||
"Choose how frequently this summary should be posted (e.g., every 1 day, every 1 week).",
|
||||
fieldType: FormFieldSchemaType.CustomComponent,
|
||||
required: true,
|
||||
stepId: "schedule",
|
||||
getCustomElement: (
|
||||
value: FormValues<WorkspaceNotificationSummary>,
|
||||
props: CustomElementProps,
|
||||
elementProps: CustomElementProps,
|
||||
): ReactElement => {
|
||||
return (
|
||||
<RecurringFieldElement
|
||||
error={props.error}
|
||||
error={elementProps.error}
|
||||
onChange={(recurring: Recurring) => {
|
||||
props.onChange(recurring);
|
||||
elementProps.onChange(recurring);
|
||||
}}
|
||||
initialValue={
|
||||
value.recurringInterval
|
||||
@@ -281,13 +283,24 @@ const WorkspaceSummaryTable: FunctionComponent<ComponentProps> = (
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
field: {
|
||||
sendFirstReportAt: true,
|
||||
},
|
||||
title: "Send First Report At",
|
||||
description:
|
||||
"When should the first summary report be sent? Subsequent reports will follow the recurring interval from this date. If left empty, the first report will be sent after the recurring interval from now.",
|
||||
fieldType: FormFieldSchemaType.DateTime,
|
||||
required: false,
|
||||
stepId: "schedule",
|
||||
},
|
||||
{
|
||||
field: {
|
||||
numberOfDaysOfData: true,
|
||||
},
|
||||
title: "Number of Days of Data",
|
||||
title: "Lookback Period (Days)",
|
||||
description:
|
||||
"How many days of historical data should be included in each summary?",
|
||||
"How many days of data to include in each summary. For example, 7 means the summary will cover the last 7 days.",
|
||||
fieldType: FormFieldSchemaType.Number,
|
||||
required: true,
|
||||
stepId: "schedule",
|
||||
@@ -297,9 +310,9 @@ const WorkspaceSummaryTable: FunctionComponent<ComponentProps> = (
|
||||
field: {
|
||||
summaryItems: true,
|
||||
},
|
||||
title: "Items to Include",
|
||||
title: "What to Include",
|
||||
description:
|
||||
"Select which items to include in the summary report.",
|
||||
"Choose which sections appear in the summary. The report will be formatted with headers, statistics, and a detailed list.",
|
||||
fieldType: FormFieldSchemaType.MultiSelectDropdown,
|
||||
required: true,
|
||||
stepId: "content",
|
||||
@@ -315,7 +328,7 @@ const WorkspaceSummaryTable: FunctionComponent<ComponentProps> = (
|
||||
]}
|
||||
formSteps={[
|
||||
{
|
||||
title: "Basic",
|
||||
title: "Basic Info",
|
||||
id: "basic",
|
||||
},
|
||||
{
|
||||
@@ -349,15 +362,7 @@ const WorkspaceSummaryTable: FunctionComponent<ComponentProps> = (
|
||||
field: {
|
||||
name: true,
|
||||
},
|
||||
title: "Summary Name",
|
||||
type: FieldType.Text,
|
||||
},
|
||||
{
|
||||
field: {
|
||||
description: true,
|
||||
},
|
||||
noValueMessage: "-",
|
||||
title: "Description",
|
||||
title: "Name",
|
||||
type: FieldType.Text,
|
||||
},
|
||||
{
|
||||
@@ -371,7 +376,7 @@ const WorkspaceSummaryTable: FunctionComponent<ComponentProps> = (
|
||||
field: {
|
||||
recurringInterval: true,
|
||||
},
|
||||
title: "Recurring Interval",
|
||||
title: "Frequency",
|
||||
type: FieldType.Element,
|
||||
getElement: (
|
||||
value: WorkspaceNotificationSummary,
|
||||
@@ -383,12 +388,25 @@ const WorkspaceSummaryTable: FunctionComponent<ComponentProps> = (
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
field: {
|
||||
sendFirstReportAt: true,
|
||||
},
|
||||
noValueMessage: "-",
|
||||
title: "First Report",
|
||||
type: FieldType.DateTime,
|
||||
},
|
||||
{
|
||||
field: {
|
||||
numberOfDaysOfData: true,
|
||||
},
|
||||
title: "Days of Data",
|
||||
type: FieldType.Number,
|
||||
title: "Lookback",
|
||||
type: FieldType.Element,
|
||||
getElement: (
|
||||
value: WorkspaceNotificationSummary,
|
||||
): ReactElement => {
|
||||
return <span>{value.numberOfDaysOfData} days</span>;
|
||||
},
|
||||
},
|
||||
{
|
||||
field: {
|
||||
@@ -398,15 +416,23 @@ const WorkspaceSummaryTable: FunctionComponent<ComponentProps> = (
|
||||
title: "Last Sent",
|
||||
type: FieldType.DateTime,
|
||||
},
|
||||
{
|
||||
field: {
|
||||
nextSendAt: true,
|
||||
},
|
||||
noValueMessage: "-",
|
||||
title: "Next Send",
|
||||
type: FieldType.DateTime,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
{showTestModal && testSummary ? (
|
||||
<ConfirmModal
|
||||
title={`Test Summary`}
|
||||
title={`Send Test Summary Now`}
|
||||
error={testError}
|
||||
description={`Test the summary "${testSummary.name}" by sending it to ${getWorkspaceTypeDisplayName(props.workspaceType)} now.`}
|
||||
submitButtonText={"Send Test Summary"}
|
||||
description={`This will send the "${testSummary.name}" summary to ${getWorkspaceTypeDisplayName(props.workspaceType)} right now. The summary will include data from the last ${testSummary.numberOfDaysOfData || 7} days. This will not affect the regular schedule.`}
|
||||
submitButtonText={"Send Now"}
|
||||
onClose={() => {
|
||||
setShowTestModal(false);
|
||||
setTestSummary(undefined);
|
||||
@@ -427,10 +453,10 @@ const WorkspaceSummaryTable: FunctionComponent<ComponentProps> = (
|
||||
{showTestSuccessModal ? (
|
||||
<ConfirmModal
|
||||
title={
|
||||
testError ? `Test Failed` : `Test Summary Sent Successfully`
|
||||
testError ? `Test Failed` : `Summary Sent`
|
||||
}
|
||||
error={testError}
|
||||
description={`Test summary sent successfully. You should now see the summary in ${getWorkspaceTypeDisplayName(props.workspaceType)}.`}
|
||||
description={`The test summary was sent successfully. Check your ${getWorkspaceTypeDisplayName(props.workspaceType)} channel to see how it looks.`}
|
||||
submitButtonType={ButtonStyleType.NORMAL}
|
||||
submitButtonText={"Close"}
|
||||
onSubmit={async () => {
|
||||
|
||||
@@ -357,6 +357,41 @@ class WorkspaceNotificationSummary extends BaseModel {
|
||||
})
|
||||
public numberOfDaysOfData?: number = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectMember,
|
||||
Permission.CreateWorkspaceNotificationSummary,
|
||||
],
|
||||
read: [
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectMember,
|
||||
Permission.ReadWorkspaceNotificationSummary,
|
||||
Permission.ReadAllProjectResources,
|
||||
],
|
||||
update: [
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectMember,
|
||||
Permission.EditWorkspaceNotificationSummary,
|
||||
],
|
||||
})
|
||||
@TableColumn({
|
||||
title: "Send First Report At",
|
||||
description:
|
||||
"When should the first summary report be sent? Subsequent reports will follow the recurring interval from this date.",
|
||||
required: false,
|
||||
unique: false,
|
||||
type: TableColumnType.Date,
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.Date,
|
||||
nullable: true,
|
||||
})
|
||||
public sendFirstReportAt?: Date = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [
|
||||
Permission.ProjectAdmin,
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||
|
||||
export class MigrationName1774357353502 implements MigrationInterface {
|
||||
name = 'MigrationName1774357353502'
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "WorkspaceNotificationSummary" ADD "sendFirstReportAt" TIMESTAMP WITH TIME ZONE`);
|
||||
await queryRunner.query(`ALTER TABLE "OnCallDutyPolicyScheduleLayer" ALTER COLUMN "rotation" SET DEFAULT '{"_type":"Recurring","value":{"intervalType":"Day","intervalCount":{"_type":"PositiveNumber","value":1}}}'`);
|
||||
await queryRunner.query(`ALTER TABLE "OnCallDutyPolicyScheduleLayer" ALTER COLUMN "restrictionTimes" SET DEFAULT '{"_type":"RestrictionTimes","value":{"restictionType":"None","dayRestrictionTimes":null,"weeklyRestrictionTimes":[]}}'`);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "OnCallDutyPolicyScheduleLayer" ALTER COLUMN "restrictionTimes" SET DEFAULT '{"_type": "RestrictionTimes", "value": {"restictionType": "None", "dayRestrictionTimes": null, "weeklyRestrictionTimes": []}}'`);
|
||||
await queryRunner.query(`ALTER TABLE "OnCallDutyPolicyScheduleLayer" ALTER COLUMN "rotation" SET DEFAULT '{"_type": "Recurring", "value": {"intervalType": "Day", "intervalCount": {"_type": "PositiveNumber", "value": 1}}}'`);
|
||||
await queryRunner.query(`ALTER TABLE "WorkspaceNotificationSummary" DROP COLUMN "sendFirstReportAt"`);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -269,6 +269,7 @@ import { MigrationName1773676206197 } from "./1773676206197-MigrationName";
|
||||
import { MigrationName1774000000000 } from "./1774000000000-MigrationName";
|
||||
import { MigrationName1774000000001 } from "./1774000000001-MigrationName";
|
||||
import { MigrationName1774355321449 } from "./1774355321449-MigrationName";
|
||||
import { MigrationName1774357353502 } from "./1774357353502-MigrationName";
|
||||
|
||||
export default [
|
||||
InitialMigration,
|
||||
@@ -541,5 +542,6 @@ export default [
|
||||
MigrationName1773676206197,
|
||||
MigrationName1774000000000,
|
||||
MigrationName1774000000001,
|
||||
MigrationName1774355321449
|
||||
MigrationName1774355321449,
|
||||
MigrationName1774357353502
|
||||
];
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -32,12 +32,13 @@ RunCron(
|
||||
|
||||
for (const summary of summariesToSend) {
|
||||
try {
|
||||
// Update nextSendAt first to prevent double-sends
|
||||
// Calculate next send time based on recurring interval
|
||||
const nextSendAt: Date = Recurring.getNextDate(
|
||||
summary.nextSendAt!,
|
||||
summary.recurringInterval!,
|
||||
);
|
||||
|
||||
// Update nextSendAt first to prevent double-sends
|
||||
await WorkspaceNotificationSummaryService.updateOneById({
|
||||
id: summary.id!,
|
||||
data: {
|
||||
|
||||
Reference in New Issue
Block a user