feat: Add Workspace Notification Summary API and Service

- Implemented WorkspaceNotificationSummaryAPI to handle notification summary requests.
- Created WorkspaceNotificationSummaryService for business logic related to notification summaries.
- Added enums for WorkspaceNotificationSummaryItem and WorkspaceNotificationSummaryType to define summary items and types.
- Developed a cron job to send workspace notification summaries at regular intervals.
- Enhanced error handling and logging for summary sending process.
This commit is contained in:
Nawaz Dhandala
2026-03-24 12:25:51 +00:00
parent 29c2bbbf57
commit 9806425721
22 changed files with 2990 additions and 15 deletions

View File

@@ -13,6 +13,7 @@ import SettingsProbes from "./Pages/Settings/Probes/Index";
import SettingsAIAgents from "./Pages/Settings/AIAgents/Index";
import SettingsLlmProviders from "./Pages/Settings/LlmProviders/Index";
import SendEmail from "./Pages/SendEmail/Index";
import MoreEmail from "./Pages/More/Email";
import Users from "./Pages/Users/Index";
import PageMap from "./Utils/PageMap";
import RouteMap from "./Utils/RouteMap";
@@ -155,6 +156,11 @@ const App: () => JSX.Element = () => {
path={RouteMap[PageMap.SEND_EMAIL]?.toString() || ""}
element={<SendEmail />}
/>
<PageRoute
path={RouteMap[PageMap.MORE_EMAIL]?.toString() || ""}
element={<MoreEmail />}
/>
</Routes>
</MasterPage>
);

View File

@@ -2,10 +2,7 @@ import PageMap from "../../Utils/PageMap";
import RouteMap, { RouteUtil } from "../../Utils/RouteMap";
import Route from "Common/Types/API/Route";
import IconProp from "Common/Types/Icon/IconProp";
import NavBar, {
MoreMenuItem,
NavItem,
} from "Common/UI/Components/Navbar/NavBar";
import NavBar, { NavItem } from "Common/UI/Components/Navbar/NavBar";
import React, { FunctionComponent, ReactElement } from "react";
const DashboardNavbar: FunctionComponent = (): ReactElement => {
@@ -29,23 +26,17 @@ const DashboardNavbar: FunctionComponent = (): ReactElement => {
icon: IconProp.Settings,
route: RouteUtil.populateRouteParams(RouteMap[PageMap.SETTINGS] as Route),
},
];
const moreMenuItems: MoreMenuItem[] = [
{
title: "Send Email",
description: "Send announcement emails to all registered users.",
icon: IconProp.Email,
id: "more-nav-bar-item",
title: "More",
icon: IconProp.More,
route: RouteUtil.populateRouteParams(
RouteMap[PageMap.SEND_EMAIL] as Route,
),
activeRoute: RouteUtil.populateRouteParams(
RouteMap[PageMap.SEND_EMAIL] as Route,
RouteMap[PageMap.MORE_EMAIL] as Route,
),
},
];
return <NavBar items={navItems} moreMenuItems={moreMenuItems} />;
return <NavBar items={navItems} />;
};
export default DashboardNavbar;

View File

@@ -0,0 +1,301 @@
import PageMap from "../../Utils/PageMap";
import RouteMap, { RouteUtil } from "../../Utils/RouteMap";
import MoreSideMenu from "./SideMenu";
import Route from "Common/Types/API/Route";
import React, { FunctionComponent, ReactElement, useState } from "react";
import Page from "Common/UI/Components/Page/Page";
import Card from "Common/UI/Components/Card/Card";
import BasicForm from "Common/UI/Components/Forms/BasicForm";
import Alert, { AlertType } from "Common/UI/Components/Alerts/Alert";
import FormFieldSchemaType from "Common/UI/Components/Forms/Types/FormFieldSchemaType";
import { JSONObject } from "Common/Types/JSON";
import HTTPErrorResponse from "Common/Types/API/HTTPErrorResponse";
import HTTPResponse from "Common/Types/API/HTTPResponse";
import URL from "Common/Types/API/URL";
import API from "Common/UI/Utils/API/API";
import { APP_API_URL } from "Common/UI/Config";
import ConfirmModal from "Common/UI/Components/Modal/ConfirmModal";
import Modal from "Common/UI/Components/Modal/Modal";
import Button, { ButtonStyleType } from "Common/UI/Components/Button/Button";
const MoreEmail: FunctionComponent = (): ReactElement => {
const [isSendingTest, setIsSendingTest] = useState<boolean>(false);
const [isSendingAll, setIsSendingAll] = useState<boolean>(false);
const [error, setError] = useState<string>("");
const [success, setSuccess] = useState<string>("");
const [showConfirmModal, setShowConfirmModal] = useState<boolean>(false);
const [showTestModal, setShowTestModal] = useState<boolean>(false);
const [testEmail, setTestEmail] = useState<string>("");
const [testError, setTestError] = useState<string>("");
const [testSuccess, setTestSuccess] = useState<string>("");
const [pendingSubject, setPendingSubject] = useState<string>("");
const [pendingMessage, setPendingMessage] = useState<string>("");
const [currentFormValues, setCurrentFormValues] = useState<JSONObject>({
subject: "",
message: "",
});
const sendTestEmail: () => Promise<void> = async (): Promise<void> => {
setIsSendingTest(true);
setTestError("");
setTestSuccess("");
try {
const response: HTTPResponse<JSONObject> | HTTPErrorResponse =
await API.post({
url: URL.fromString(APP_API_URL.toString()).addRoute(
"/notification/broadcast-email/send-test",
),
data: {
subject: pendingSubject,
message: pendingMessage,
testEmail: testEmail,
},
});
if (response instanceof HTTPErrorResponse) {
throw response;
}
if (response.isFailure()) {
throw new Error("Failed to send test email.");
}
setTestSuccess("Test email sent successfully. Please check your inbox.");
} catch (err) {
setTestError(API.getFriendlyMessage(err));
} finally {
setIsSendingTest(false);
}
};
const sendToAllUsers: () => Promise<void> = async (): Promise<void> => {
setIsSendingAll(true);
setError("");
setSuccess("");
try {
const response: HTTPResponse<JSONObject> | HTTPErrorResponse =
await API.post({
url: URL.fromString(APP_API_URL.toString()).addRoute(
"/notification/broadcast-email/send-to-all-users",
),
data: {
subject: pendingSubject,
message: pendingMessage,
},
});
if (response instanceof HTTPErrorResponse) {
throw response;
}
if (response.isFailure()) {
throw new Error("Failed to send emails.");
}
const data: JSONObject = response.data as JSONObject;
setSuccess(
`Emails sent successfully. Total users: ${data["totalUsers"]}, Sent: ${data["sentCount"]}, Errors: ${data["errorCount"]}`,
);
} catch (err) {
setError(API.getFriendlyMessage(err));
} finally {
setIsSendingAll(false);
}
};
return (
<Page
title={"Send Announcement Email"}
breadcrumbLinks={[
{
title: "Admin Dashboard",
to: RouteUtil.populateRouteParams(RouteMap[PageMap.HOME] as Route),
},
{
title: "More",
to: RouteUtil.populateRouteParams(
RouteMap[PageMap.MORE_EMAIL] as Route,
),
},
{
title: "Send Email",
to: RouteUtil.populateRouteParams(
RouteMap[PageMap.MORE_EMAIL] as Route,
),
},
]}
sideMenu={<MoreSideMenu />}
>
<Card
title="Send Announcement Email"
description="Compose an announcement email to send to all registered users. You can send a test email first to preview how it looks."
>
{success ? (
<Alert
type={AlertType.SUCCESS}
title={success}
className="mb-4"
/>
) : (
<></>
)}
<BasicForm
id="send-email-form"
name="Send Announcement Email"
isLoading={isSendingAll}
error={error || ""}
submitButtonText="Send to All Users"
maxPrimaryButtonWidth={true}
submitButtonStyleType={ButtonStyleType.DANGER}
onChange={(values: JSONObject) => {
setCurrentFormValues(values as JSONObject);
}}
initialValues={{
subject: "",
message: "",
}}
fields={[
{
field: {
subject: true,
},
title: "Subject",
description: "The subject line of the announcement email.",
placeholder: "Enter email subject",
required: true,
fieldType: FormFieldSchemaType.Text,
},
{
field: {
message: true,
},
title: "Message",
description:
"The body of the announcement email. This will be displayed in a branded OneUptime email template.",
placeholder: "Enter your announcement message here...",
required: true,
fieldType: FormFieldSchemaType.LongText,
},
]}
onSubmit={async (values: JSONObject) => {
const subject: string = String(values["subject"] || "").trim();
const message: string = String(values["message"] || "").trim();
if (!subject || !message) {
setSuccess("");
setError("Please fill in all fields.");
return;
}
setPendingSubject(subject);
setPendingMessage(message);
setShowConfirmModal(true);
}}
footer={
<div className="mt-3">
<Button
title="Send Test Email"
buttonStyle={ButtonStyleType.LINK}
onClick={() => {
const subject: string = String(
currentFormValues["subject"] || "",
).trim();
const message: string = String(
currentFormValues["message"] || "",
).trim();
if (!subject || !message) {
setError(
"Please fill in subject and message before sending a test.",
);
return;
}
setError("");
setPendingSubject(subject);
setPendingMessage(message);
setTestEmail("");
setTestError("");
setTestSuccess("");
setShowTestModal(true);
}}
/>
</div>
}
/>
</Card>
{showTestModal ? (
<Modal
title="Send Test Email"
description="Enter an email address to send a test of this announcement."
onClose={() => {
setShowTestModal(false);
}}
submitButtonText="Send Test"
isLoading={isSendingTest}
onSubmit={() => {
if (!testEmail.trim()) {
setTestError("Please enter a test email address.");
return;
}
sendTestEmail().catch(() => {});
}}
error={testError}
>
{testSuccess ? (
<Alert
type={AlertType.SUCCESS}
title={testSuccess}
className="mb-4"
/>
) : (
<></>
)}
<div className="mb-4">
<label
htmlFor="test-email-input"
className="block text-sm font-medium text-gray-700 mb-1"
>
Test Email Address
</label>
<input
id="test-email-input"
type="email"
className="block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm border p-2"
placeholder="test@example.com"
value={testEmail}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setTestEmail(e.target.value);
}}
/>
</div>
</Modal>
) : (
<></>
)}
{showConfirmModal ? (
<ConfirmModal
title="Confirm Send to All Users"
description="Are you sure you want to send this announcement email to all registered users? This action cannot be undone."
submitButtonText="Yes, Send to All Users"
onSubmit={async () => {
setShowConfirmModal(false);
await sendToAllUsers();
}}
onClose={() => {
setShowConfirmModal(false);
}}
/>
) : (
<></>
)}
</Page>
);
};
export default MoreEmail;

View File

@@ -0,0 +1,28 @@
import PageMap from "../../Utils/PageMap";
import RouteMap, { RouteUtil } from "../../Utils/RouteMap";
import Route from "Common/Types/API/Route";
import IconProp from "Common/Types/Icon/IconProp";
import SideMenu from "Common/UI/Components/SideMenu/SideMenu";
import SideMenuItem from "Common/UI/Components/SideMenu/SideMenuItem";
import SideMenuSection from "Common/UI/Components/SideMenu/SideMenuSection";
import React, { ReactElement } from "react";
const MoreSideMenu: () => JSX.Element = (): ReactElement => {
return (
<SideMenu>
<SideMenuSection title="Communication">
<SideMenuItem
link={{
title: "Send Email",
to: RouteUtil.populateRouteParams(
RouteMap[PageMap.MORE_EMAIL] as Route,
),
}}
icon={IconProp.Email}
/>
</SideMenuSection>
</SideMenu>
);
};
export default MoreSideMenu;

View File

@@ -25,6 +25,8 @@ enum PageMap {
SETTINGS_DATA_RETENTION = "SETTINGS_DATA_RETENTION",
SEND_EMAIL = "SEND_EMAIL",
MORE_EMAIL = "MORE_EMAIL",
}
export default PageMap;

View File

@@ -41,6 +41,8 @@ const RouteMap: Dictionary<Route> = {
),
[PageMap.SEND_EMAIL]: new Route(`/admin/send-email`),
[PageMap.MORE_EMAIL]: new Route(`/admin/more/email`),
};
export class RouteUtil {

View File

@@ -28,6 +28,7 @@ import MonitorAPI from "Common/Server/API/MonitorAPI";
import ShortLinkAPI from "Common/Server/API/ShortLinkAPI";
import StatusPageAPI from "Common/Server/API/StatusPageAPI";
import WorkspaceNotificationRuleAPI from "Common/Server/API/WorkspaceNotificationRuleAPI";
import WorkspaceNotificationSummaryAPI from "Common/Server/API/WorkspaceNotificationSummaryAPI";
import StatusPageDomainAPI from "Common/Server/API/StatusPageDomainAPI";
import StatusPageSubscriberAPI from "Common/Server/API/StatusPageSubscriberAPI";
import UserCallAPI from "Common/Server/API/UserCallAPI";
@@ -2057,6 +2058,10 @@ const BaseAPIFeatureSet: FeatureSet = {
`/${APP_NAME.toLocaleLowerCase()}`,
new WorkspaceNotificationRuleAPI().getRouter(),
);
app.use(
`/${APP_NAME.toLocaleLowerCase()}`,
new WorkspaceNotificationSummaryAPI().getRouter(),
);
app.use(`/${APP_NAME.toLocaleLowerCase()}`, new FileAPI().getRouter());
app.use(
`/${APP_NAME.toLocaleLowerCase()}`,

View File

@@ -0,0 +1,450 @@
import ProjectUtil from "Common/UI/Utils/Project";
import FormFieldSchemaType from "Common/UI/Components/Forms/Types/FormFieldSchemaType";
import ModelTable from "Common/UI/Components/ModelTable/ModelTable";
import FieldType from "Common/UI/Components/Types/FieldType";
import React, {
Fragment,
FunctionComponent,
ReactElement,
} from "react";
import WorkspaceType, {
getWorkspaceTypeDisplayName,
} from "Common/Types/Workspace/WorkspaceType";
import WorkspaceNotificationSummary from "Common/Models/DatabaseModels/WorkspaceNotificationSummary";
import WorkspaceNotificationSummaryType from "Common/Types/Workspace/NotificationSummary/WorkspaceNotificationSummaryType";
import WorkspaceNotificationSummaryItem from "Common/Types/Workspace/NotificationSummary/WorkspaceNotificationSummaryItem";
import API from "Common/Utils/API";
import Exception from "Common/Types/Exception/Exception";
import { ErrorFunction } from "Common/Types/FunctionTypes";
import ObjectID from "Common/Types/ObjectID";
import { ButtonStyleType } from "Common/UI/Components/Button/Button";
import IconProp from "Common/Types/Icon/IconProp";
import ConfirmModal from "Common/UI/Components/Modal/ConfirmModal";
import HTTPResponse from "Common/Types/API/HTTPResponse";
import EmptyResponseData from "Common/Types/API/EmptyResponse";
import HTTPErrorResponse from "Common/Types/API/HTTPErrorResponse";
import URL from "Common/Types/API/URL";
import { APP_API_URL } from "Common/UI/Config";
import { ShowAs } from "Common/UI/Components/ModelTable/BaseModelTable";
import { ModalWidth } from "Common/UI/Components/Modal/Modal";
import RecurringFieldElement from "Common/UI/Components/Events/RecurringFieldElement";
import RecurringViewElement from "Common/UI/Components/Events/RecurringViewElement";
import Recurring from "Common/Types/Events/Recurring";
import FormValues from "Common/UI/Components/Forms/Types/FormValues";
import { CustomElementProps } from "Common/UI/Components/Forms/Types/Field";
import OneUptimeDate from "Common/Types/Date";
export interface ComponentProps {
workspaceType: WorkspaceType;
summaryType: WorkspaceNotificationSummaryType;
}
const WorkspaceSummaryTable: FunctionComponent<ComponentProps> = (
props: ComponentProps,
): ReactElement => {
const [showTestModal, setShowTestModal] = React.useState<boolean>(false);
const [isTestLoading, setIsTestLoading] = React.useState<boolean>(false);
const [testError, setTestError] = React.useState<string | undefined>(
undefined,
);
const [testSummary, setTestSummary] = React.useState<
WorkspaceNotificationSummary | undefined
>(undefined);
const [showTestSuccessModal, setShowTestSuccessModal] =
React.useState<boolean>(false);
type TestSummaryFunction = (summaryId: ObjectID) => Promise<void>;
const testSummaryFn: TestSummaryFunction = async (
summaryId: ObjectID,
): Promise<void> => {
try {
setIsTestLoading(true);
setTestError(undefined);
const response: HTTPResponse<EmptyResponseData> | HTTPErrorResponse =
await API.get({
url: URL.fromString(APP_API_URL.toString()).addRoute(
`/workspace-notification-summary/test/${summaryId.toString()}`,
),
data: {},
});
if (response.isSuccess()) {
setIsTestLoading(false);
setShowTestModal(false);
setShowTestSuccessModal(true);
}
if (response instanceof HTTPErrorResponse) {
throw response;
}
setIsTestLoading(false);
} catch (err) {
setTestError(API.getFriendlyErrorMessage(err as Exception));
setIsTestLoading(false);
}
};
const allSummaryItems: Array<WorkspaceNotificationSummaryItem> =
Object.values(WorkspaceNotificationSummaryItem);
return (
<Fragment>
<ModelTable<WorkspaceNotificationSummary>
modelType={WorkspaceNotificationSummary}
query={{
projectId: ProjectUtil.getCurrentProjectId()!,
summaryType: props.summaryType,
workspaceType: props.workspaceType,
}}
userPreferencesKey={`workspace-summary-table-${props.summaryType}-${props.workspaceType}`}
actionButtons={[
{
title: "Test Summary",
buttonStyleType: ButtonStyleType.OUTLINE,
icon: IconProp.Play,
onClick: async (
item: WorkspaceNotificationSummary,
onCompleteAction: VoidFunction,
onError: ErrorFunction,
) => {
try {
setTestSummary(item);
setShowTestModal(true);
onCompleteAction();
} catch (err) {
onCompleteAction();
onError(err as Error);
}
},
},
]}
singularName={`${props.summaryType} Summary`}
pluralName={`${props.summaryType} Summaries`}
id={`workspace-summary-table-${props.summaryType}`}
name={`Settings > ${props.summaryType} 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.`,
}}
showAs={ShowAs.List}
noItemsMessage={"No summary rules found."}
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) {
const recurring: Recurring = Recurring.fromJSON(
values.recurringInterval,
);
values.nextSendAt = Recurring.getNextDateInterval(
OneUptimeDate.getCurrentDate(),
recurring,
);
}
// Parse channel names from comma-separated string
if (
values.channelNames &&
typeof values.channelNames === "string"
) {
values.channelNames = (values.channelNames as unknown as string)
.split(",")
.map((name: string) => {
return name.trim();
})
.filter((name: string) => {
return name.length > 0;
});
}
// Ensure summaryItems is an array
if (!values.summaryItems) {
values.summaryItems = allSummaryItems;
}
if (!values.isEnabled) {
values.isEnabled = true;
}
return Promise.resolve(values);
}}
onBeforeEdit={(values: WorkspaceNotificationSummary) => {
// Parse channel names from comma-separated string
if (
values.channelNames &&
typeof values.channelNames === "string"
) {
values.channelNames = (values.channelNames as unknown as string)
.split(",")
.map((name: string) => {
return name.trim();
})
.filter((name: string) => {
return name.length > 0;
});
}
// Recalculate nextSendAt if interval changed
if (values.recurringInterval) {
const recurring: Recurring = Recurring.fromJSON(
values.recurringInterval,
);
values.nextSendAt = Recurring.getNextDateInterval(
OneUptimeDate.getCurrentDate(),
recurring,
);
}
return Promise.resolve(values);
}}
formFields={[
{
field: {
name: true,
},
title: "Summary Name",
fieldType: FormFieldSchemaType.Text,
required: true,
stepId: "basic",
placeholder: "Weekly Incident Summary",
validation: {
minLength: 2,
},
},
{
field: {
description: true,
},
stepId: "basic",
title: "Description",
fieldType: FormFieldSchemaType.LongText,
required: false,
placeholder:
"Weekly summary of incidents sent to the #ops channel.",
},
{
field: {
channelNames: true,
},
stepId: "basic",
title: "Channel Names",
description:
"Comma-separated list of channel names to post the summary to (e.g., #incidents, #ops-summary).",
fieldType: FormFieldSchemaType.Text,
required: true,
placeholder: "#incidents-summary",
},
{
field: {
isEnabled: true,
},
stepId: "basic",
title: "Enabled",
description: "Enable or disable this recurring summary.",
fieldType: FormFieldSchemaType.Toggle,
required: false,
},
{
field: {
recurringInterval: true,
},
title: "Recurring Interval",
description: "How often should this summary be sent?",
fieldType: FormFieldSchemaType.CustomComponent,
required: true,
stepId: "schedule",
getCustomElement: (
value: FormValues<WorkspaceNotificationSummary>,
props: CustomElementProps,
): ReactElement => {
return (
<RecurringFieldElement
error={props.error}
onChange={(recurring: Recurring) => {
props.onChange(recurring);
}}
initialValue={
value.recurringInterval
? Recurring.fromJSON(value.recurringInterval)
: undefined
}
/>
);
},
},
{
field: {
numberOfDaysOfData: true,
},
title: "Number of Days of Data",
description:
"How many days of historical data should be included in each summary?",
fieldType: FormFieldSchemaType.Number,
required: true,
stepId: "schedule",
placeholder: "7",
},
{
field: {
summaryItems: true,
},
title: "Items to Include",
description:
"Select which items to include in the summary report.",
fieldType: FormFieldSchemaType.MultiSelectDropdown,
required: true,
stepId: "content",
dropdownOptions: allSummaryItems.map(
(item: WorkspaceNotificationSummaryItem) => {
return {
label: item,
value: item,
};
},
),
},
]}
formSteps={[
{
title: "Basic",
id: "basic",
},
{
title: "Schedule",
id: "schedule",
},
{
title: "Content",
id: "content",
},
]}
showRefreshButton={true}
filters={[
{
field: {
name: true,
},
type: FieldType.Text,
title: "Summary Name",
},
{
field: {
isEnabled: true,
},
type: FieldType.Boolean,
title: "Enabled",
},
]}
columns={[
{
field: {
name: true,
},
title: "Summary Name",
type: FieldType.Text,
},
{
field: {
description: true,
},
noValueMessage: "-",
title: "Description",
type: FieldType.Text,
},
{
field: {
isEnabled: true,
},
title: "Enabled",
type: FieldType.Boolean,
},
{
field: {
recurringInterval: true,
},
title: "Recurring Interval",
type: FieldType.Element,
getElement: (
value: WorkspaceNotificationSummary,
): ReactElement => {
return (
<RecurringViewElement
value={value.recurringInterval as Recurring}
/>
);
},
},
{
field: {
numberOfDaysOfData: true,
},
title: "Days of Data",
type: FieldType.Number,
},
{
field: {
lastSentAt: true,
},
noValueMessage: "Never",
title: "Last Sent",
type: FieldType.DateTime,
},
]}
/>
{showTestModal && testSummary ? (
<ConfirmModal
title={`Test Summary`}
error={testError}
description={`Test the summary "${testSummary.name}" by sending it to ${getWorkspaceTypeDisplayName(props.workspaceType)} now.`}
submitButtonText={"Send Test Summary"}
onClose={() => {
setShowTestModal(false);
setTestSummary(undefined);
setTestError(undefined);
}}
isLoading={isTestLoading}
onSubmit={async () => {
if (!testSummary.id) {
return;
}
await testSummaryFn(testSummary.id!);
}}
/>
) : (
<></>
)}
{showTestSuccessModal ? (
<ConfirmModal
title={
testError ? `Test Failed` : `Test Summary Sent Successfully`
}
error={testError}
description={`Test summary sent successfully. You should now see the summary in ${getWorkspaceTypeDisplayName(props.workspaceType)}.`}
submitButtonType={ButtonStyleType.NORMAL}
submitButtonText={"Close"}
onSubmit={async () => {
setShowTestSuccessModal(false);
setTestSummary(undefined);
setShowTestModal(false);
setTestError("");
}}
/>
) : (
<></>
)}
</Fragment>
);
};
export default WorkspaceSummaryTable;

View File

@@ -1,5 +1,7 @@
import WorkspaceType from "Common/Types/Workspace/WorkspaceType";
import WorkspaceNotificationRuleTable from "../../Components/Workspace/WorkspaceNotificationRulesTable";
import WorkspaceSummaryTable from "../../Components/Workspace/WorkspaceSummaryTable";
import WorkspaceNotificationSummaryType from "Common/Types/Workspace/NotificationSummary/WorkspaceNotificationSummaryType";
import PageComponentProps from "../PageComponentProps";
import React, { FunctionComponent, ReactElement } from "react";
import NotificationRuleEventType from "Common/Types/Workspace/NotificationRules/EventType";
@@ -70,6 +72,21 @@ const AlertsTeamsPage: FunctionComponent<
/>
),
},
{
name: "Summary",
children: (
<>
<WorkspaceSummaryTable
workspaceType={WorkspaceType.MicrosoftTeams}
summaryType={WorkspaceNotificationSummaryType.Alert}
/>
<WorkspaceSummaryTable
workspaceType={WorkspaceType.MicrosoftTeams}
summaryType={WorkspaceNotificationSummaryType.AlertEpisode}
/>
</>
),
},
];
return (

View File

@@ -1,5 +1,7 @@
import WorkspaceType from "Common/Types/Workspace/WorkspaceType";
import WorkspaceNotificationRuleTable from "../../Components/Workspace/WorkspaceNotificationRulesTable";
import WorkspaceSummaryTable from "../../Components/Workspace/WorkspaceSummaryTable";
import WorkspaceNotificationSummaryType from "Common/Types/Workspace/NotificationSummary/WorkspaceNotificationSummaryType";
import PageComponentProps from "../PageComponentProps";
import React, { FunctionComponent, ReactElement } from "react";
import NotificationRuleEventType from "Common/Types/Workspace/NotificationRules/EventType";
@@ -100,6 +102,21 @@ When you react with a pin emoji, OneUptime will automatically save the message c
</>
),
},
{
name: "Summary",
children: (
<>
<WorkspaceSummaryTable
workspaceType={WorkspaceType.Slack}
summaryType={WorkspaceNotificationSummaryType.Alert}
/>
<WorkspaceSummaryTable
workspaceType={WorkspaceType.Slack}
summaryType={WorkspaceNotificationSummaryType.AlertEpisode}
/>
</>
),
},
];
return (

View File

@@ -1,5 +1,7 @@
import WorkspaceType from "Common/Types/Workspace/WorkspaceType";
import WorkspaceNotificationRuleTable from "../../Components/Workspace/WorkspaceNotificationRulesTable";
import WorkspaceSummaryTable from "../../Components/Workspace/WorkspaceSummaryTable";
import WorkspaceNotificationSummaryType from "Common/Types/Workspace/NotificationSummary/WorkspaceNotificationSummaryType";
import PageComponentProps from "../PageComponentProps";
import React, { FunctionComponent, ReactElement } from "react";
import NotificationRuleEventType from "Common/Types/Workspace/NotificationRules/EventType";
@@ -70,6 +72,21 @@ const IncidentsTeamsPage: FunctionComponent<
/>
),
},
{
name: "Summary",
children: (
<>
<WorkspaceSummaryTable
workspaceType={WorkspaceType.MicrosoftTeams}
summaryType={WorkspaceNotificationSummaryType.Incident}
/>
<WorkspaceSummaryTable
workspaceType={WorkspaceType.MicrosoftTeams}
summaryType={WorkspaceNotificationSummaryType.IncidentEpisode}
/>
</>
),
},
];
return (

View File

@@ -1,5 +1,7 @@
import WorkspaceType from "Common/Types/Workspace/WorkspaceType";
import WorkspaceNotificationRuleTable from "../../Components/Workspace/WorkspaceNotificationRulesTable";
import WorkspaceSummaryTable from "../../Components/Workspace/WorkspaceSummaryTable";
import WorkspaceNotificationSummaryType from "Common/Types/Workspace/NotificationSummary/WorkspaceNotificationSummaryType";
import PageComponentProps from "../PageComponentProps";
import React, { FunctionComponent, ReactElement } from "react";
import NotificationRuleEventType from "Common/Types/Workspace/NotificationRules/EventType";
@@ -101,6 +103,21 @@ When you react with a pin emoji, OneUptime will automatically save the message c
</>
),
},
{
name: "Summary",
children: (
<>
<WorkspaceSummaryTable
workspaceType={WorkspaceType.Slack}
summaryType={WorkspaceNotificationSummaryType.Incident}
/>
<WorkspaceSummaryTable
workspaceType={WorkspaceType.Slack}
summaryType={WorkspaceNotificationSummaryType.IncidentEpisode}
/>
</>
),
},
];
return (

View File

@@ -232,6 +232,7 @@ import WorkspaceUserAuthToken from "./WorkspaceUserAuthToken";
import WorkspaceProjectAuthToken from "./WorkspaceProjectAuthToken";
import WorkspaceSetting from "./WorkspaceSetting";
import WorkspaceNotificationRule from "./WorkspaceNotificationRule";
import WorkspaceNotificationSummary from "./WorkspaceNotificationSummary";
import OnCallDutyPolicyUserOverride from "./OnCallDutyPolicyUserOverride";
import MonitorFeed from "./MonitorFeed";
@@ -490,6 +491,7 @@ const AllModelTypes: Array<{
WorkspaceSetting,
WorkspaceNotificationRule,
WorkspaceNotificationSummary,
MonitorFeed,

View File

@@ -0,0 +1,784 @@
import Project from "./Project";
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 ColumnLength from "../../Types/Database/ColumnLength";
import ColumnType from "../../Types/Database/ColumnType";
import CrudApiEndpoint from "../../Types/Database/CrudApiEndpoint";
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 { Column, Entity, Index, JoinColumn, ManyToOne } from "typeorm";
import WorkspaceType from "../../Types/Workspace/WorkspaceType";
import WorkspaceNotificationSummaryType from "../../Types/Workspace/NotificationSummary/WorkspaceNotificationSummaryType";
import WorkspaceNotificationSummaryItem from "../../Types/Workspace/NotificationSummary/WorkspaceNotificationSummaryItem";
import Permission from "../../Types/Permission";
import TableBillingAccessControl from "../../Types/Database/AccessControl/TableBillingAccessControl";
import { PlanType } from "../../Types/Billing/SubscriptionPlan";
import EnableDocumentation from "../../Types/Database/EnableDocumentation";
import Recurring from "../../Types/Events/Recurring";
import NotificationRuleCondition from "../../Types/Workspace/NotificationRules/NotificationRuleCondition";
import FilterCondition from "../../Types/Filter/FilterCondition";
@EnableDocumentation()
@TenantColumn("projectId")
@TableAccessControl({
create: [
Permission.ProjectAdmin,
Permission.ProjectOwner,
Permission.ProjectMember,
Permission.CreateWorkspaceNotificationSummary,
],
read: [
Permission.ProjectAdmin,
Permission.ProjectOwner,
Permission.ProjectMember,
Permission.ReadWorkspaceNotificationSummary,
Permission.ReadAllProjectResources,
],
delete: [
Permission.ProjectAdmin,
Permission.ProjectOwner,
Permission.ProjectMember,
Permission.DeleteWorkspaceNotificationSummary,
],
update: [
Permission.ProjectAdmin,
Permission.ProjectOwner,
Permission.ProjectMember,
Permission.EditWorkspaceNotificationSummary,
],
})
@TableBillingAccessControl({
create: PlanType.Growth,
read: PlanType.Growth,
update: PlanType.Growth,
delete: PlanType.Growth,
})
@CrudApiEndpoint(new Route("/workspace-notification-summary"))
@Entity({
name: "WorkspaceNotificationSummary",
})
@TableMetadata({
tableName: "WorkspaceNotificationSummary",
singularName: "Workspace Notification Summary",
pluralName: "Workspace Notification Summaries",
icon: IconProp.ChartBar,
tableDescription:
"Recurring summary reports for incidents and alerts sent to Slack or Microsoft Teams",
})
class WorkspaceNotificationSummary extends BaseModel {
@ColumnAccessControl({
create: [
Permission.ProjectAdmin,
Permission.ProjectOwner,
Permission.ProjectMember,
Permission.CreateWorkspaceNotificationSummary,
],
read: [
Permission.ProjectAdmin,
Permission.ProjectOwner,
Permission.ProjectMember,
Permission.ReadWorkspaceNotificationSummary,
Permission.ReadAllProjectResources,
],
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.ProjectAdmin,
Permission.ProjectOwner,
Permission.ProjectMember,
Permission.CreateWorkspaceNotificationSummary,
],
read: [
Permission.ProjectAdmin,
Permission.ProjectOwner,
Permission.ProjectMember,
Permission.ReadWorkspaceNotificationSummary,
Permission.ReadAllProjectResources,
],
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.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: "Summary Name",
description: "Name of the Summary Rule",
required: true,
unique: false,
type: TableColumnType.LongText,
canReadOnRelationQuery: true,
})
@Column({
type: ColumnType.LongText,
length: ColumnLength.LongText,
unique: false,
nullable: false,
})
public name?: string = 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: "Summary Description",
description: "Description of the Summary Rule",
required: false,
unique: false,
type: TableColumnType.LongText,
canReadOnRelationQuery: true,
})
@Column({
type: ColumnType.LongText,
length: ColumnLength.LongText,
unique: false,
nullable: true,
})
public description?: string = 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: "Workspace Type",
description: "Type of Workspace - Slack, Microsoft Teams, etc.",
required: true,
unique: false,
type: TableColumnType.LongText,
canReadOnRelationQuery: true,
})
@Column({
type: ColumnType.LongText,
length: ColumnLength.LongText,
unique: false,
nullable: false,
})
public workspaceType?: WorkspaceType = 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: "Summary Type",
description:
"Type of summary - Incident, Alert, Incident Episode, or Alert Episode",
required: true,
unique: false,
type: TableColumnType.ShortText,
canReadOnRelationQuery: true,
})
@Column({
type: ColumnType.ShortText,
unique: false,
nullable: false,
})
public summaryType?: WorkspaceNotificationSummaryType = 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({
type: TableColumnType.JSON,
title: "Recurring Interval",
description: "How often should the summary be sent?",
required: true,
})
@Column({
type: ColumnType.JSON,
nullable: false,
transformer: Recurring.getDatabaseTransformer(),
})
public recurringInterval?: Recurring = 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({
type: TableColumnType.Number,
title: "Number of Days of Data",
description: "How many days of data to include in the summary",
required: true,
})
@Column({
type: ColumnType.Number,
nullable: false,
default: 7,
})
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({
type: TableColumnType.JSON,
title: "Channel Names",
description: "List of channel names to post the summary to",
required: true,
})
@Column({
type: ColumnType.JSON,
nullable: false,
})
public channelNames?: Array<string> = 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: "Team Name",
description: "Microsoft Teams team name (only for Microsoft Teams)",
required: false,
unique: false,
type: TableColumnType.LongText,
})
@Column({
type: ColumnType.LongText,
length: ColumnLength.LongText,
unique: false,
nullable: true,
})
public teamName?: string = 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({
type: TableColumnType.JSON,
title: "Summary Items",
description: "Checklist of items to include in the summary",
required: true,
})
@Column({
type: ColumnType.JSON,
nullable: false,
})
public summaryItems?: Array<WorkspaceNotificationSummaryItem> = 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({
type: TableColumnType.JSON,
title: "Filters",
description: "Filter conditions for which items to include in the summary",
required: false,
})
@Column({
type: ColumnType.JSON,
nullable: true,
})
public filters?: Array<NotificationRuleCondition> = 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: "Filter Condition",
description: "How to combine filters - Any or All",
required: false,
unique: false,
type: TableColumnType.ShortText,
})
@Column({
type: ColumnType.ShortText,
unique: false,
nullable: true,
})
public filterCondition?: FilterCondition = 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({
type: TableColumnType.Date,
title: "Next Send At",
description: "When the next summary should be sent",
})
@Column({
type: ColumnType.Date,
nullable: true,
})
public nextSendAt?: Date = 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({
type: TableColumnType.Date,
title: "Last Sent At",
description: "When the last summary was sent",
})
@Column({
type: ColumnType.Date,
nullable: true,
})
public lastSentAt?: Date = 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({
type: TableColumnType.Boolean,
title: "Enabled",
description: "Is this summary rule enabled?",
required: true,
})
@Column({
type: ColumnType.Boolean,
nullable: false,
default: true,
})
public isEnabled?: boolean = 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({
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.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({
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: [
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({
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: [
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({
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;
}
export default WorkspaceNotificationSummary;

View File

@@ -0,0 +1,55 @@
import UserMiddleware from "../Middleware/UserAuthorization";
import WorkspaceNotificationSummaryService, {
Service as WorkspaceNotificationSummaryServiceType,
} from "../Services/WorkspaceNotificationSummaryService";
import {
ExpressRequest,
ExpressResponse,
NextFunction,
} from "../Utils/Express";
import Response from "../Utils/Response";
import BaseAPI from "./BaseAPI";
import CommonAPI from "./CommonAPI";
import DatabaseCommonInteractionProps from "../../Types/BaseDatabase/DatabaseCommonInteractionProps";
import WorkspaceNotificationSummary from "../../Models/DatabaseModels/WorkspaceNotificationSummary";
import ObjectID from "../../Types/ObjectID";
export default class WorkspaceNotificationSummaryAPI extends BaseAPI<
WorkspaceNotificationSummary,
WorkspaceNotificationSummaryServiceType
> {
public constructor() {
super(
WorkspaceNotificationSummary,
WorkspaceNotificationSummaryService,
);
this.router.get(
`${new this.entityType().getCrudApiPath()?.toString()}/test/:workspaceNotificationSummaryId`,
UserMiddleware.getUserMiddleware,
async (
req: ExpressRequest,
res: ExpressResponse,
next: NextFunction,
) => {
try {
const databaseProps: DatabaseCommonInteractionProps =
await CommonAPI.getDatabaseCommonInteractionProps(req);
await this.service.testSummary({
summaryId: new ObjectID(
req.params["workspaceNotificationSummaryId"] as string,
),
props: databaseProps,
projectId: databaseProps.tenantId!,
testByUserId: databaseProps.userId!,
});
return Response.sendEmptySuccessResponse(req, res);
} catch (e) {
next(e);
}
},
);
}
}

View File

@@ -198,6 +198,7 @@ import WorkspaceUserAuthTokenService from "./WorkspaceUserAuthTokenService";
import WorkspaceSettingService from "./WorkspaceSettingService";
import WorkspaceNotificationRuleService from "./WorkspaceNotificationRuleService";
import WorkspaceNotificationLogService from "./WorkspaceNotificationLogService";
import WorkspaceNotificationSummaryService from "./WorkspaceNotificationSummaryService";
import OnCallDutyPolicyUserOverrideService from "./OnCallDutyPolicyUserOverrideService";
import MonitorLogService from "./MonitorLogService";
@@ -416,6 +417,7 @@ const services: Array<BaseService> = [
WorkspaceSettingService,
WorkspaceNotificationRuleService,
WorkspaceNotificationLogService,
WorkspaceNotificationSummaryService,
ProjectSCIMLogService,
StatusPageSCIMLogService,

File diff suppressed because it is too large Load Diff

View File

@@ -771,6 +771,11 @@ enum Permission {
EditWorkspaceNotificationRule = "EditWorkspaceNotificationRule",
ReadWorkspaceNotificationRule = "ReadWorkspaceNotificationRule",
CreateWorkspaceNotificationSummary = "CreateWorkspaceNotificationSummary",
DeleteWorkspaceNotificationSummary = "DeleteWorkspaceNotificationSummary",
EditWorkspaceNotificationSummary = "EditWorkspaceNotificationSummary",
ReadWorkspaceNotificationSummary = "ReadWorkspaceNotificationSummary",
// Alert Episode Permissions
CreateAlertEpisode = "CreateAlertEpisode",
DeleteAlertEpisode = "DeleteAlertEpisode",
@@ -1389,6 +1394,43 @@ export class PermissionHelper {
group: PermissionGroup.Settings,
},
{
permission: Permission.CreateWorkspaceNotificationSummary,
title: "Create Workspace Notification Summary",
description:
"This permission can create workspace notification summaries for this project.",
isAssignableToTenant: true,
isAccessControlPermission: false,
group: PermissionGroup.Settings,
},
{
permission: Permission.DeleteWorkspaceNotificationSummary,
title: "Delete Workspace Notification Summary",
description:
"This permission can delete workspace notification summaries of this project.",
isAssignableToTenant: true,
isAccessControlPermission: false,
group: PermissionGroup.Settings,
},
{
permission: Permission.EditWorkspaceNotificationSummary,
title: "Edit Workspace Notification Summary",
description:
"This permission can edit workspace notification summaries of this project.",
isAssignableToTenant: true,
isAccessControlPermission: false,
group: PermissionGroup.Settings,
},
{
permission: Permission.ReadWorkspaceNotificationSummary,
title: "Read Workspace Notification Summary",
description:
"This permission can read workspace notification summaries of this project.",
isAssignableToTenant: true,
isAccessControlPermission: false,
group: PermissionGroup.Settings,
},
{
permission: Permission.CreateIncidentStateTimeline,
title: "Create Incident State Timeline",

View File

@@ -0,0 +1,13 @@
enum WorkspaceNotificationSummaryItem {
TotalCount = "Total Count",
ListWithLinks = "List with Links",
WhoAcknowledged = "Who Acknowledged",
WhoResolved = "Who Resolved",
TimeToAcknowledge = "Time to Acknowledge",
TimeToResolve = "Time to Resolve",
ResourcesAffected = "Resources Affected",
SeverityBreakdown = "Severity Breakdown",
StateBreakdown = "State Breakdown",
}
export default WorkspaceNotificationSummaryItem;

View File

@@ -0,0 +1,8 @@
enum WorkspaceNotificationSummaryType {
Incident = "Incident",
Alert = "Alert",
IncidentEpisode = "Incident Episode",
AlertEpisode = "Alert Episode",
}
export default WorkspaceNotificationSummaryType;

View File

@@ -0,0 +1,66 @@
import RunCron from "../../Utils/Cron";
import OneUptimeDate from "Common/Types/Date";
import Recurring from "Common/Types/Events/Recurring";
import { EVERY_MINUTE } from "Common/Utils/CronTime";
import WorkspaceNotificationSummaryService from "Common/Server/Services/WorkspaceNotificationSummaryService";
import QueryHelper from "Common/Server/Types/Database/QueryHelper";
import logger from "Common/Server/Utils/Logger";
import WorkspaceNotificationSummary from "Common/Models/DatabaseModels/WorkspaceNotificationSummary";
RunCron(
"WorkspaceNotificationSummary:SendSummary",
{
schedule: EVERY_MINUTE,
runOnStartup: false,
},
async () => {
const summariesToSend: Array<WorkspaceNotificationSummary> =
await WorkspaceNotificationSummaryService.findAllBy({
query: {
isEnabled: true,
nextSendAt: QueryHelper.lessThan(OneUptimeDate.getCurrentDate()),
},
props: {
isRoot: true,
},
select: {
_id: true,
nextSendAt: true,
recurringInterval: true,
},
});
for (const summary of summariesToSend) {
try {
// Update nextSendAt first to prevent double-sends
const nextSendAt: Date = Recurring.getNextDate(
summary.nextSendAt!,
summary.recurringInterval!,
);
await WorkspaceNotificationSummaryService.updateOneById({
id: summary.id!,
data: {
nextSendAt: nextSendAt,
},
props: {
isRoot: true,
},
});
await WorkspaceNotificationSummaryService.sendSummary({
summaryId: summary.id!,
});
logger.debug(
`WorkspaceNotificationSummary:SendSummary: Sent summary ${summary.id?.toString()}`,
);
} catch (err) {
logger.error(
`WorkspaceNotificationSummary:SendSummary: Error sending summary ${summary.id?.toString()}`,
);
logger.error(err);
}
}
},
);

View File

@@ -110,6 +110,9 @@ import "./Jobs/StatusPageOwners/SendOwnerAddedNotification";
// Status Page Reports
import "./Jobs/StatusPage/SendReportsToSubscribers";
// Workspace Notification Summaries
import "./Jobs/WorkspaceNotificationSummary/SendSummary";
// User Notifications Log
import "./Jobs/UserOnCallLog/ExecutePendingExecutions";
import "./Jobs/UserOnCallLog/TimeoutStuckExecutions";