feat: Add Push Notification Logs functionality

- Introduced PushNotificationLog model to track push notifications sent to users.
- Added permissions for reading push logs in the Permission enum.
- Updated various side menus to include links to Push Logs in Alerts, Incidents, and Settings.
- Created routes for viewing Push Logs in Alerts, Incidents, and Status Pages.
- Implemented UI components for displaying Push Logs in respective pages.
- Added filtering and column configuration for Push Logs tables.
- Integrated PushStatus enum to manage the status of push notifications.
- Implemented PushNotificationLogService for database interactions related to push logs.
This commit is contained in:
Simon Larsen
2025-08-10 13:25:53 +01:00
parent 8d9fc46506
commit 34c4ae947b
25 changed files with 1142 additions and 0 deletions

View File

@@ -282,6 +282,9 @@ import ShortLinkService, {
import SmsLogService, {
Service as SmsLogServiceType,
} from "Common/Server/Services/SmsLogService";
import PushNotificationLogService, {
Service as PushNotificationLogServiceType,
} from "Common/Server/Services/PushNotificationLogService";
import SpanService, {
SpanService as SpanServiceType,
} from "Common/Server/Services/SpanService";
@@ -379,6 +382,7 @@ import Span from "Common/Models/AnalyticsModels/Span";
import ApiKey from "Common/Models/DatabaseModels/ApiKey";
import ApiKeyPermission from "Common/Models/DatabaseModels/ApiKeyPermission";
import CallLog from "Common/Models/DatabaseModels/CallLog";
import PushNotificationLog from "Common/Models/DatabaseModels/PushNotificationLog";
import Domain from "Common/Models/DatabaseModels/Domain";
import EmailLog from "Common/Models/DatabaseModels/EmailLog";
import EmailVerificationToken from "Common/Models/DatabaseModels/EmailVerificationToken";
@@ -1531,6 +1535,14 @@ const BaseAPIFeatureSet: FeatureSet = {
new BaseAPI<SmsLog, SmsLogServiceType>(SmsLog, SmsLogService).getRouter(),
);
app.use(
`/${APP_NAME.toLocaleLowerCase()}`,
new BaseAPI<PushNotificationLog, PushNotificationLogServiceType>(
PushNotificationLog,
PushNotificationLogService,
).getRouter(),
);
app.use(
`/${APP_NAME.toLocaleLowerCase()}`,
new BaseAPI<EmailLog, EmailLogServiceType>(

View File

@@ -0,0 +1,47 @@
import PushService from "../Services/PushNotificationService";
import ClusterKeyAuthorization from "Common/Server/Middleware/ClusterKeyAuthorization";
import ObjectID from "Common/Types/ObjectID";
import Express, {
ExpressRequest,
ExpressResponse,
ExpressRouter,
} from "Common/Server/Utils/Express";
import Response from "Common/Server/Utils/Response";
import { JSONObject } from "Common/Types/JSON";
import JSONFunctions from "Common/Types/JSONFunctions";
const router: ExpressRouter = Express.getRouter();
router.post(
"/send",
ClusterKeyAuthorization.isAuthorizedServiceMiddleware,
async (req: ExpressRequest, res: ExpressResponse) => {
const body: JSONObject = JSONFunctions.deserialize(req.body);
await PushService.send(
{
deviceTokens: (body["deviceTokens"] as string[]) || [],
deviceType: (body["deviceType"] as any) || "web",
message: body["message"] as any,
},
{
projectId: (body["projectId"] as ObjectID) || undefined,
isSensitive: (body["isSensitive"] as boolean) || false,
userOnCallLogTimelineId:
(body["userOnCallLogTimelineId"] as ObjectID) || undefined,
incidentId: (body["incidentId"] as ObjectID) || undefined,
alertId: (body["alertId"] as ObjectID) || undefined,
scheduledMaintenanceId:
(body["scheduledMaintenanceId"] as ObjectID) || undefined,
statusPageId: (body["statusPageId"] as ObjectID) || undefined,
statusPageAnnouncementId:
(body["statusPageAnnouncementId"] as ObjectID) || undefined,
},
);
return Response.sendEmptySuccessResponse(req, res);
},
);
export default router;

View File

@@ -2,6 +2,7 @@ import CallAPI from "./API/Call";
// API
import MailAPI from "./API/Mail";
import SmsAPI from "./API/SMS";
import PushNotificationAPI from "./API/PushNotification";
import SMTPConfigAPI from "./API/SMTPConfig";
import "./Utils/Handlebars";
import FeatureSet from "Common/Server/Types/FeatureSet";
@@ -15,6 +16,7 @@ const NotificationFeatureSet: FeatureSet = {
app.use([`/${APP_NAME}/email`, "/email"], MailAPI);
app.use([`/${APP_NAME}/sms`, "/sms"], SmsAPI);
app.use([`/${APP_NAME}/push`, "/push"], PushNotificationAPI);
app.use([`/${APP_NAME}/call`, "/call"], CallAPI);
app.use([`/${APP_NAME}/smtp-config`, "/smtp-config"], SMTPConfigAPI);
},

View File

@@ -0,0 +1,79 @@
import PushNotificationRequest from "Common/Types/PushNotification/PushNotificationRequest";
import ObjectID from "Common/Types/ObjectID";
import PushNotificationServiceCommon from "Common/Server/Services/PushNotificationService";
import PushNotificationLog from "Common/Models/DatabaseModels/PushNotificationLog";
import PushNotificationLogService from "Common/Server/Services/PushNotificationLogService";
import UserOnCallLogTimelineService from "Common/Server/Services/UserOnCallLogTimelineService";
import UserNotificationStatus from "Common/Types/UserNotification/UserNotificationStatus";
import PushStatus from "Common/Types/PushNotification/PushStatus";
export default class PushNotificationService {
public static async send(
request: PushNotificationRequest,
options: {
projectId?: ObjectID | undefined;
isSensitive?: boolean;
userOnCallLogTimelineId?: ObjectID | undefined;
incidentId?: ObjectID | undefined;
alertId?: ObjectID | undefined;
scheduledMaintenanceId?: ObjectID | undefined;
statusPageId?: ObjectID | undefined;
statusPageAnnouncementId?: ObjectID | undefined;
} = {},
): Promise<void> {
const log: PushNotificationLog = new PushNotificationLog();
if (options.projectId) {
log.projectId = options.projectId;
}
log.title = request.message.title || "";
log.body = options.isSensitive ? "Sensitive message not logged" : (request.message.body || "");
log.deviceType = request.deviceType;
if (options.incidentId) log.incidentId = options.incidentId;
if (options.alertId) log.alertId = options.alertId;
if (options.scheduledMaintenanceId)
log.scheduledMaintenanceId = options.scheduledMaintenanceId;
if (options.statusPageId) log.statusPageId = options.statusPageId;
if (options.statusPageAnnouncementId)
log.statusPageAnnouncementId = options.statusPageAnnouncementId;
try {
await PushNotificationServiceCommon.sendPushNotification(request, {
projectId: options.projectId,
isSensitive: Boolean(options.isSensitive),
userOnCallLogTimelineId: options.userOnCallLogTimelineId,
});
log.status = PushStatus.Success;
log.statusMessage = "Push notification sent";
} catch (err: any) {
log.status = PushStatus.Error;
log.statusMessage = err?.message || err?.toString?.() || "Failed to send push notification";
if (options.userOnCallLogTimelineId) {
await UserOnCallLogTimelineService.updateOneById({
id: options.userOnCallLogTimelineId,
data: {
status: UserNotificationStatus.Error,
statusMessage: log.statusMessage || "Push send failed",
},
props: { isRoot: true },
});
}
}
if (options.projectId) {
await PushNotificationLogService.create({
data: log,
props: { isRoot: true },
});
}
if (log.status === PushStatus.Error) {
throw new Error(log.statusMessage || "Push failed");
}
}
}

View File

@@ -100,6 +100,7 @@ import ServiceCopilotCodeRepository from "./ServiceCopilotCodeRepository";
import ShortLink from "./ShortLink";
// SMS
import SmsLog from "./SmsLog";
import PushNotificationLog from "./PushNotificationLog";
// Status Page
import StatusPage from "./StatusPage";
import StatusPageAnnouncement from "./StatusPageAnnouncement";
@@ -292,6 +293,7 @@ const AllModelTypes: Array<{
StatusPageOwnerUser,
SmsLog,
PushNotificationLog,
CallLog,
EmailLog,

View File

@@ -0,0 +1,564 @@
import Project from "./Project";
import Incident from "./Incident";
import Alert from "./Alert";
import ScheduledMaintenance from "./ScheduledMaintenance";
import StatusPage from "./StatusPage";
import StatusPageAnnouncement from "./StatusPageAnnouncement";
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 EnableDocumentation from "../../Types/Database/EnableDocumentation";
import EnableWorkflow from "../../Types/Database/EnableWorkflow";
import TableColumn from "../../Types/Database/TableColumn";
import TableColumnType from "../../Types/Database/TableColumnType";
import TableMetadata from "../../Types/Database/TableMetadata";
import TenantColumn from "../../Types/Database/TenantColumn";
import IconProp from "../../Types/Icon/IconProp";
import ObjectID from "../../Types/ObjectID";
import Permission from "../../Types/Permission";
import PushStatus from "../../Types/PushNotification/PushStatus";
import { Column, Entity, Index, JoinColumn, ManyToOne } from "typeorm";
@EnableDocumentation()
@TenantColumn("projectId")
@TableAccessControl({
create: [],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadPushLog,
],
delete: [],
update: [],
})
@CrudApiEndpoint(new Route("/push-notification-log"))
@Entity({
name: "PushNotificationLog",
})
@EnableWorkflow({
create: true,
delete: false,
update: false,
})
@TableMetadata({
tableName: "PushNotificationLog",
singularName: "Push Notification Log",
pluralName: "Push Notification Logs",
icon: IconProp.Bell,
tableDescription:
"Logs of all the Push Notifications sent out to all users and subscribers for this project.",
})
export default class PushNotificationLog extends BaseModel {
@ColumnAccessControl({
create: [],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadPushLog,
],
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: [],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadPushLog,
],
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: [],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadPushLog,
],
update: [],
})
@TableColumn({
required: true,
type: TableColumnType.LongText,
title: "Title",
description: "Title of the push notification",
canReadOnRelationQuery: false,
})
@Column({
nullable: false,
type: ColumnType.LongText,
length: ColumnLength.LongText,
})
public title?: string = undefined;
@ColumnAccessControl({
create: [],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadPushLog,
],
update: [],
})
@TableColumn({
required: false,
type: TableColumnType.VeryLongText,
title: "Body",
description: "Body of the push notification",
canReadOnRelationQuery: false,
})
@Column({
nullable: true,
type: ColumnType.VeryLongText,
})
public body?: string = undefined;
@ColumnAccessControl({
create: [],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadPushLog,
],
update: [],
})
@TableColumn({
required: false,
type: TableColumnType.ShortText,
title: "Device Type",
description: "Type of device this was sent to (e.g., web)",
canReadOnRelationQuery: false,
})
@Column({
nullable: true,
type: ColumnType.ShortText,
length: ColumnLength.ShortText,
})
public deviceType?: string = undefined;
@ColumnAccessControl({
create: [],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadPushLog,
],
update: [],
})
@TableColumn({
required: false,
type: TableColumnType.LongText,
title: "Status Message",
description: "Status Message (if any)",
canReadOnRelationQuery: false,
})
@Column({
nullable: true,
type: ColumnType.LongText,
length: ColumnLength.LongText,
})
public statusMessage?: string = undefined;
@ColumnAccessControl({
create: [],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadPushLog,
],
update: [],
})
@TableColumn({
required: true,
type: TableColumnType.ShortText,
title: "Status",
description: "Status of the push notification",
canReadOnRelationQuery: false,
})
@Column({
nullable: false,
type: ColumnType.ShortText,
length: ColumnLength.ShortText,
})
public status?: PushStatus = undefined;
// Relations to resources that triggered this push notification (nullable)
@ColumnAccessControl({
create: [],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadPushLog,
],
update: [],
})
@TableColumn({
manyToOneRelationColumn: "incidentId",
type: TableColumnType.Entity,
modelType: Incident,
title: "Incident",
description: "Incident associated with this Push (if any)",
})
@ManyToOne(
() => {
return Incident;
},
{
eager: false,
nullable: true,
onDelete: "CASCADE",
orphanedRowAction: "nullify",
},
)
@JoinColumn({ name: "incidentId" })
public incident?: Incident = undefined;
@ColumnAccessControl({
create: [],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadPushLog,
],
update: [],
})
@Index()
@TableColumn({
type: TableColumnType.ObjectID,
required: false,
canReadOnRelationQuery: true,
title: "Incident ID",
description: "ID of Incident associated with this Push (if any)",
})
@Column({
type: ColumnType.ObjectID,
nullable: true,
transformer: ObjectID.getDatabaseTransformer(),
})
public incidentId?: ObjectID = undefined;
@ColumnAccessControl({
create: [],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadPushLog,
],
update: [],
})
@TableColumn({
manyToOneRelationColumn: "alertId",
type: TableColumnType.Entity,
modelType: Alert,
title: "Alert",
description: "Alert associated with this Push (if any)",
})
@ManyToOne(
() => {
return Alert;
},
{
eager: false,
nullable: true,
onDelete: "CASCADE",
orphanedRowAction: "nullify",
},
)
@JoinColumn({ name: "alertId" })
public alert?: Alert = undefined;
@ColumnAccessControl({
create: [],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadPushLog,
],
update: [],
})
@Index()
@TableColumn({
type: TableColumnType.ObjectID,
required: false,
canReadOnRelationQuery: true,
title: "Alert ID",
description: "ID of Alert associated with this Push (if any)",
})
@Column({
type: ColumnType.ObjectID,
nullable: true,
transformer: ObjectID.getDatabaseTransformer(),
})
public alertId?: ObjectID = undefined;
@ColumnAccessControl({
create: [],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadPushLog,
],
update: [],
})
@TableColumn({
manyToOneRelationColumn: "scheduledMaintenanceId",
type: TableColumnType.Entity,
modelType: ScheduledMaintenance,
title: "Scheduled Maintenance",
description:
"Scheduled Maintenance associated with this Push (if any)",
})
@ManyToOne(
() => {
return ScheduledMaintenance;
},
{
eager: false,
nullable: true,
onDelete: "CASCADE",
orphanedRowAction: "nullify",
},
)
@JoinColumn({ name: "scheduledMaintenanceId" })
public scheduledMaintenance?: ScheduledMaintenance = undefined;
@ColumnAccessControl({
create: [],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadPushLog,
],
update: [],
})
@Index()
@TableColumn({
type: TableColumnType.ObjectID,
required: false,
canReadOnRelationQuery: true,
title: "Scheduled Maintenance ID",
description:
"ID of Scheduled Maintenance associated with this Push (if any)",
})
@Column({
type: ColumnType.ObjectID,
nullable: true,
transformer: ObjectID.getDatabaseTransformer(),
})
public scheduledMaintenanceId?: ObjectID = undefined;
@ColumnAccessControl({
create: [],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadPushLog,
],
update: [],
})
@TableColumn({
manyToOneRelationColumn: "statusPageId",
type: TableColumnType.Entity,
modelType: StatusPage,
title: "Status Page",
description: "Status Page associated with this Push (if any)",
})
@ManyToOne(
() => {
return StatusPage;
},
{
eager: false,
nullable: true,
onDelete: "CASCADE",
orphanedRowAction: "nullify",
},
)
@JoinColumn({ name: "statusPageId" })
public statusPage?: StatusPage = undefined;
@ColumnAccessControl({
create: [],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadPushLog,
],
update: [],
})
@Index()
@TableColumn({
type: TableColumnType.ObjectID,
required: false,
canReadOnRelationQuery: true,
title: "Status Page ID",
description: "ID of Status Page associated with this Push (if any)",
})
@Column({
type: ColumnType.ObjectID,
nullable: true,
transformer: ObjectID.getDatabaseTransformer(),
})
public statusPageId?: ObjectID = undefined;
@ColumnAccessControl({
create: [],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadPushLog,
],
update: [],
})
@TableColumn({
manyToOneRelationColumn: "statusPageAnnouncementId",
type: TableColumnType.Entity,
modelType: StatusPageAnnouncement,
title: "Status Page Announcement",
description:
"Status Page Announcement associated with this Push (if any)",
})
@ManyToOne(
() => {
return StatusPageAnnouncement;
},
{
eager: false,
nullable: true,
onDelete: "CASCADE",
orphanedRowAction: "nullify",
},
)
@JoinColumn({ name: "statusPageAnnouncementId" })
public statusPageAnnouncement?: StatusPageAnnouncement = undefined;
@ColumnAccessControl({
create: [],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadPushLog,
],
update: [],
})
@Index()
@TableColumn({
type: TableColumnType.ObjectID,
required: false,
canReadOnRelationQuery: true,
title: "Status Page Announcement ID",
description:
"ID of Status Page Announcement associated with this Push (if any)",
})
@Column({
type: ColumnType.ObjectID,
nullable: true,
transformer: ObjectID.getDatabaseTransformer(),
})
public statusPageAnnouncementId?: ObjectID = undefined;
@ColumnAccessControl({
create: [],
read: [],
update: [],
})
@TableColumn({
manyToOneRelationColumn: "deletedByUserId",
type: TableColumnType.Entity,
title: "Deleted by User",
modelType: User,
description:
"Relation to User who deleted this object (if this object was deleted by a User)",
})
@ManyToOne(
() => {
return User;
},
{
cascade: false,
eager: false,
nullable: true,
onDelete: "SET NULL",
orphanedRowAction: "nullify",
},
)
@JoinColumn({ name: "deletedByUserId" })
public deletedByUser?: User = undefined;
@ColumnAccessControl({
create: [],
read: [],
update: [],
})
@TableColumn({
type: TableColumnType.ObjectID,
title: "Deleted by User ID",
description:
"User ID who deleted this object (if this object was deleted by a User)",
})
@Column({
type: ColumnType.ObjectID,
nullable: true,
transformer: ObjectID.getDatabaseTransformer(),
})
public deletedByUserId?: ObjectID = undefined;
}

View File

@@ -0,0 +1,14 @@
import { IsBillingEnabled } from "../EnvironmentConfig";
import DatabaseService from "./DatabaseService";
import Model from "../../Models/DatabaseModels/PushNotificationLog";
export class Service extends DatabaseService<Model> {
public constructor() {
super(Model);
if (IsBillingEnabled) {
this.hardDeleteItemsOlderThanInDays("createdAt", 3);
}
}
}
export default new Service();

View File

@@ -142,6 +142,7 @@ enum Permission {
ReadSmsLog = "ReadSmsLog",
ReadEmailLog = "ReadEmailLog",
ReadCallLog = "ReadCallLog",
ReadPushLog = "ReadPushLog",
CreateIncidentOwnerTeam = "CreateIncidentOwnerTeam",
DeleteIncidentOwnerTeam = "DeleteIncidentOwnerTeam",
@@ -3003,6 +3004,14 @@ export class PermissionHelper {
isAccessControlPermission: false,
},
{
permission: Permission.ReadPushLog,
title: "Read Push Log",
description: "This permission can read Push Notification Logs of this project.",
isAssignableToTenant: true,
isAccessControlPermission: false,
},
{
permission: Permission.CreateMonitorProbe,
title: "Create Monitor Probe",

View File

@@ -0,0 +1,6 @@
enum PushStatus {
Success = "Success",
Error = "Error",
}
export default PushStatus;

View File

@@ -0,0 +1,59 @@
import PageComponentProps from "../../PageComponentProps";
import Navigation from "Common/UI/Utils/Navigation";
import ObjectID from "Common/Types/ObjectID";
import React, { FunctionComponent, ReactElement } from "react";
import ModelTable from "Common/UI/Components/ModelTable/ModelTable";
import PushNotificationLog from "Common/Models/DatabaseModels/PushNotificationLog";
import FieldType from "Common/UI/Components/Types/FieldType";
import Columns from "Common/UI/Components/ModelTable/Columns";
import Pill from "Common/UI/Components/Pill/Pill";
import { Green, Red } from "Common/Types/BrandColors";
import PushStatus from "Common/Types/PushNotification/PushStatus";
import ProjectUtil from "Common/UI/Utils/Project";
import Filter from "Common/UI/Components/ModelFilter/Filter";
import DropdownUtil from "Common/UI/Utils/Dropdown";
const AlertPushLogs: FunctionComponent<PageComponentProps> = (): ReactElement => {
const modelId: ObjectID = Navigation.getLastParamAsObjectID(1);
const columns: Columns<PushNotificationLog> = [
{ field: { title: true }, title: "Title", type: FieldType.Text },
{ field: { deviceType: true }, title: "Device Type", type: FieldType.Text, hideOnMobile: true },
{ field: { createdAt: true }, title: "Sent at", type: FieldType.DateTime },
{ field: { status: true }, title: "Status", type: FieldType.Text, getElement: (item: PushNotificationLog): ReactElement => {
if (item["status"]) {
return (
<Pill isMinimal={false} color={item["status"] === PushStatus.Success ? Green : Red} text={item["status"] as string} />
);
}
return <></>;
} },
];
const filters: Array<Filter<PushNotificationLog>> = [
{ field: { createdAt: true }, title: "Sent at", type: FieldType.Date },
{ field: { status: true }, title: "Status", type: FieldType.Dropdown, filterDropdownOptions: DropdownUtil.getDropdownOptionsFromEnum(PushStatus) },
];
return (
<ModelTable<PushNotificationLog>
modelType={PushNotificationLog}
id="alert-push-logs-table"
name="Push Logs"
isDeleteable={false}
isEditable={false}
isCreateable={false}
showViewIdButton={true}
userPreferencesKey="alert-push-logs-table"
query={{ projectId: ProjectUtil.getCurrentProjectId()!, alertId: modelId }}
selectMoreFields={{ statusMessage: true, body: true }}
cardProps={{ title: "Push Logs", description: "Push notifications sent for this alert." }}
noItemsMessage="No Push logs for this alert."
showRefreshButton={true}
columns={columns}
filters={filters}
/>
);
};
export default AlertPushLogs;

View File

@@ -131,6 +131,16 @@ const DashboardSideMenu: FunctionComponent<ComponentProps> = (
}}
icon={IconProp.Call}
/>
<SideMenuItem
link={{
title: "Push Logs",
to: RouteUtil.populateRouteParams(
RouteMap[PageMap.ALERT_VIEW_PUSH_LOGS] as Route,
{ modelId: props.modelId },
),
}}
icon={IconProp.Bell}
/>
</SideMenuSection>
<SideMenuSection title="Alert Notes">

View File

@@ -0,0 +1,59 @@
import PageComponentProps from "../../PageComponentProps";
import Navigation from "Common/UI/Utils/Navigation";
import ObjectID from "Common/Types/ObjectID";
import React, { FunctionComponent, ReactElement } from "react";
import ModelTable from "Common/UI/Components/ModelTable/ModelTable";
import PushNotificationLog from "Common/Models/DatabaseModels/PushNotificationLog";
import FieldType from "Common/UI/Components/Types/FieldType";
import Columns from "Common/UI/Components/ModelTable/Columns";
import Pill from "Common/UI/Components/Pill/Pill";
import { Green, Red } from "Common/Types/BrandColors";
import PushStatus from "Common/Types/PushNotification/PushStatus";
import ProjectUtil from "Common/UI/Utils/Project";
import Filter from "Common/UI/Components/ModelFilter/Filter";
import DropdownUtil from "Common/UI/Utils/Dropdown";
const IncidentPushLogs: FunctionComponent<PageComponentProps> = (): ReactElement => {
const modelId: ObjectID = Navigation.getLastParamAsObjectID(1);
const columns: Columns<PushNotificationLog> = [
{ field: { title: true }, title: "Title", type: FieldType.Text },
{ field: { deviceType: true }, title: "Device Type", type: FieldType.Text, hideOnMobile: true },
{ field: { createdAt: true }, title: "Sent at", type: FieldType.DateTime },
{ field: { status: true }, title: "Status", type: FieldType.Text, getElement: (item: PushNotificationLog): ReactElement => {
if (item["status"]) {
return (
<Pill isMinimal={false} color={item["status"] === PushStatus.Success ? Green : Red} text={item["status"] as string} />
);
}
return <></>;
} },
];
const filters: Array<Filter<PushNotificationLog>> = [
{ field: { createdAt: true }, title: "Sent at", type: FieldType.Date },
{ field: { status: true }, title: "Status", type: FieldType.Dropdown, filterDropdownOptions: DropdownUtil.getDropdownOptionsFromEnum(PushStatus) },
];
return (
<ModelTable<PushNotificationLog>
modelType={PushNotificationLog}
id="incident-push-logs-table"
name="Push Logs"
isDeleteable={false}
isEditable={false}
isCreateable={false}
showViewIdButton={true}
userPreferencesKey="incident-push-logs-table"
query={{ projectId: ProjectUtil.getCurrentProjectId()!, incidentId: modelId }}
selectMoreFields={{ statusMessage: true, body: true }}
cardProps={{ title: "Push Logs", description: "Push notifications sent for this incident." }}
noItemsMessage="No Push logs for this incident."
showRefreshButton={true}
columns={columns}
filters={filters}
/>
);
};
export default IncidentPushLogs;

View File

@@ -131,6 +131,16 @@ const DashboardSideMenu: FunctionComponent<ComponentProps> = (
}}
icon={IconProp.Call}
/>
<SideMenuItem
link={{
title: "Push Logs",
to: RouteUtil.populateRouteParams(
RouteMap[PageMap.INCIDENT_VIEW_PUSH_LOGS] as Route,
{ modelId: props.modelId },
),
}}
icon={IconProp.Bell}
/>
</SideMenuSection>
<SideMenuSection title="Incident Notes">

View File

@@ -0,0 +1,104 @@
import ProjectUtil from "Common/UI/Utils/Project";
import PageComponentProps from "../PageComponentProps";
import { Green, Red } from "Common/Types/BrandColors";
import IconProp from "Common/Types/Icon/IconProp";
import { ButtonStyleType } from "Common/UI/Components/Button/Button";
import ConfirmModal from "Common/UI/Components/Modal/ConfirmModal";
import Filter from "Common/UI/Components/ModelFilter/Filter";
import Columns from "Common/UI/Components/ModelTable/Columns";
import ModelTable from "Common/UI/Components/ModelTable/ModelTable";
import Pill from "Common/UI/Components/Pill/Pill";
import FieldType from "Common/UI/Components/Types/FieldType";
import DropdownUtil from "Common/UI/Utils/Dropdown";
import React, { Fragment, FunctionComponent, ReactElement, useState } from "react";
import PushNotificationLog from "Common/Models/DatabaseModels/PushNotificationLog";
import PushStatus from "Common/Types/PushNotification/PushStatus";
const PushLogs: FunctionComponent<PageComponentProps> = (): ReactElement => {
const [showModal, setShowModal] = useState<boolean>(false);
const [text, setText] = useState<string>("");
const [title, setTitle] = useState<string>("");
const filters: Array<Filter<PushNotificationLog>> = [
{ field: { _id: true }, title: "Log ID", type: FieldType.ObjectID },
{ field: { title: true }, title: "Title", type: FieldType.Text },
{ field: { createdAt: true }, title: "Sent at", type: FieldType.Date },
{ field: { status: true }, title: "Status", type: FieldType.Dropdown, filterDropdownOptions: DropdownUtil.getDropdownOptionsFromEnum(PushStatus) },
];
const columns: Columns<PushNotificationLog> = [
{ field: { title: true }, title: "Title", type: FieldType.Text },
{ field: { deviceType: true }, title: "Device Type", type: FieldType.Text, hideOnMobile: true },
{ field: { createdAt: true }, title: "Sent at", type: FieldType.DateTime },
{ field: { status: true }, title: "Status", type: FieldType.Text, getElement: (item: PushNotificationLog): ReactElement => {
if (item["status"]) {
return (
<Pill isMinimal={false} color={item["status"] === PushStatus.Success ? Green : Red} text={item["status"] as string} />
);
}
return <></>;
} },
];
return (
<Fragment>
<>
<ModelTable<PushNotificationLog>
modelType={PushNotificationLog}
id="push-logs-table"
isDeleteable={false}
isEditable={false}
isCreateable={false}
name="Push Logs"
userPreferencesKey="push-logs-table"
query={{ projectId: ProjectUtil.getCurrentProjectId()! }}
selectMoreFields={{ body: true, statusMessage: true }}
actionButtons={[
{
title: "View Body",
buttonStyleType: ButtonStyleType.NORMAL,
icon: IconProp.List,
onClick: async (item: PushNotificationLog, onCompleteAction: VoidFunction) => {
setText(item["body"] as string);
setTitle("Body");
setShowModal(true);
onCompleteAction();
},
},
{
title: "View Status Message",
buttonStyleType: ButtonStyleType.NORMAL,
icon: IconProp.Error,
onClick: async (item: PushNotificationLog, onCompleteAction: VoidFunction) => {
setText(item["statusMessage"] as string);
setTitle("Status Message");
setShowModal(true);
onCompleteAction();
},
},
]}
isViewable={false}
cardProps={{ title: "Push Logs", description: "Logs of all the Push notifications sent by this project in the last 30 days." }}
noItemsMessage={"Looks like no Push notifications were sent by this project in the last 30 days."}
showRefreshButton={true}
filters={filters}
columns={columns}
/>
{showModal && (
<ConfirmModal
title={title}
description={text}
onSubmit={() => {
setShowModal(false);
}}
submitButtonText="Close"
submitButtonType={ButtonStyleType.NORMAL}
/>
)}
</>
</Fragment>
);
};
export default PushLogs;

View File

@@ -343,6 +343,15 @@ const DashboardSideMenu: () => JSX.Element = (): ReactElement => {
},
icon: IconProp.Email,
},
{
link: {
title: "Push Logs",
to: RouteUtil.populateRouteParams(
RouteMap[PageMap.SETTINGS_PUSH_LOGS] as Route,
),
},
icon: IconProp.Bell,
},
],
},
{

View File

@@ -61,6 +61,16 @@ const AnnouncementSideMenu: FunctionComponent<ComponentProps> = (
}}
icon={IconProp.Call}
/>
<SideMenuItem
link={{
title: "Push Logs",
to: RouteUtil.populateRouteParams(
RouteMap[PageMap.ANNOUNCEMENT_VIEW_PUSH_LOGS] as Route,
{ modelId: props.modelId },
),
}}
icon={IconProp.Bell}
/>
</SideMenuSection>
<SideMenuSection title="Advanced">

View File

@@ -0,0 +1,59 @@
import PageComponentProps from "../../../PageComponentProps";
import Navigation from "Common/UI/Utils/Navigation";
import ObjectID from "Common/Types/ObjectID";
import React, { FunctionComponent, ReactElement } from "react";
import ModelTable from "Common/UI/Components/ModelTable/ModelTable";
import PushNotificationLog from "Common/Models/DatabaseModels/PushNotificationLog";
import FieldType from "Common/UI/Components/Types/FieldType";
import Columns from "Common/UI/Components/ModelTable/Columns";
import Pill from "Common/UI/Components/Pill/Pill";
import { Green, Red } from "Common/Types/BrandColors";
import PushStatus from "Common/Types/PushNotification/PushStatus";
import ProjectUtil from "Common/UI/Utils/Project";
import Filter from "Common/UI/Components/ModelFilter/Filter";
import DropdownUtil from "Common/UI/Utils/Dropdown";
const AnnouncementPushLogs: FunctionComponent<PageComponentProps> = (): ReactElement => {
const modelId: ObjectID = Navigation.getLastParamAsObjectID(1);
const columns: Columns<PushNotificationLog> = [
{ field: { title: true }, title: "Title", type: FieldType.Text },
{ field: { deviceType: true }, title: "Device Type", type: FieldType.Text, hideOnMobile: true },
{ field: { createdAt: true }, title: "Sent at", type: FieldType.DateTime },
{ field: { status: true }, title: "Status", type: FieldType.Text, getElement: (item: PushNotificationLog): ReactElement => {
if (item["status"]) {
return (
<Pill isMinimal={false} color={item["status"] === PushStatus.Success ? Green : Red} text={item["status"] as string} />
);
}
return <></>;
} },
];
const filters: Array<Filter<PushNotificationLog>> = [
{ field: { createdAt: true }, title: "Sent at", type: FieldType.Date },
{ field: { status: true }, title: "Status", type: FieldType.Dropdown, filterDropdownOptions: DropdownUtil.getDropdownOptionsFromEnum(PushStatus) },
];
return (
<ModelTable<PushNotificationLog>
modelType={PushNotificationLog}
id="announcement-push-logs-table"
name="Push Logs"
isDeleteable={false}
isEditable={false}
isCreateable={false}
showViewIdButton={true}
userPreferencesKey="announcement-push-logs-table"
query={{ projectId: ProjectUtil.getCurrentProjectId()!, statusPageAnnouncementId: modelId }}
selectMoreFields={{ statusMessage: true, body: true }}
cardProps={{ title: "Push Logs", description: "Push notifications sent for this announcement." }}
noItemsMessage="No Push logs for this announcement."
showRefreshButton={true}
columns={columns}
filters={filters}
/>
);
};
export default AnnouncementPushLogs;

View File

@@ -61,6 +61,16 @@ const AnnouncementSideMenu: FunctionComponent<ComponentProps> = (
}}
icon={IconProp.Call}
/>
<SideMenuItem
link={{
title: "Push Logs",
to: RouteUtil.populateRouteParams(
RouteMap[PageMap.ANNOUNCEMENT_VIEW_PUSH_LOGS] as Route,
{ modelId: props.modelId },
),
}}
icon={IconProp.Bell}
/>
</SideMenuSection>
<SideMenuSection title="Advanced">

View File

@@ -42,6 +42,11 @@ const AlertViewNotificationLogsCall: LazyExoticComponent<
> = lazy(() => {
return import("../Pages/Alerts/View/NotificationLogsCall");
});
const AlertViewNotificationLogsPush: LazyExoticComponent<
FunctionComponent<ComponentProps>
> = lazy(() => {
return import("../Pages/Alerts/View/NotificationLogsPush");
});
const AlertsWorkspaceConnectionSlack: LazyExoticComponent<
FunctionComponent<ComponentProps>
@@ -339,6 +344,17 @@ const AlertsRoutes: FunctionComponent<ComponentProps> = (
</Suspense>
}
/>
<PageRoute
path={RouteUtil.getLastPathForKey(PageMap.ALERT_VIEW_PUSH_LOGS)}
element={
<Suspense fallback={Loader}>
<AlertViewNotificationLogsPush
{...props}
pageRoute={RouteMap[PageMap.ALERT_VIEW_PUSH_LOGS] as Route}
/>
</Suspense>
}
/>
</PageRoute>
</Routes>
);

View File

@@ -55,6 +55,11 @@ const IncidentViewNotificationLogsCall: LazyExoticComponent<
> = lazy(() => {
return import("../Pages/Incidents/View/NotificationLogsCall");
});
const IncidentViewNotificationLogsPush: LazyExoticComponent<
FunctionComponent<ComponentProps>
> = lazy(() => {
return import("../Pages/Incidents/View/NotificationLogsPush");
});
const IncidentViewDelete: LazyExoticComponent<
FunctionComponent<ComponentProps>
> = lazy(() => {
@@ -418,6 +423,17 @@ const IncidentsRoutes: FunctionComponent<ComponentProps> = (
</Suspense>
}
/>
<PageRoute
path={RouteUtil.getLastPathForKey(PageMap.INCIDENT_VIEW_PUSH_LOGS)}
element={
<Suspense fallback={Loader}>
<IncidentViewNotificationLogsPush
{...props}
pageRoute={RouteMap[PageMap.INCIDENT_VIEW_PUSH_LOGS] as Route}
/>
</Suspense>
}
/>
</PageRoute>
</Routes>
);

View File

@@ -214,6 +214,10 @@ const SettingsEmailLog: LazyExoticComponent<FunctionComponent<ComponentProps>> =
lazy(() => {
return import("../Pages/Settings/EmailLog");
});
const SettingsPushLog: LazyExoticComponent<FunctionComponent<ComponentProps>> =
lazy(() => {
return import("../Pages/Settings/PushLog");
});
const SettingsNotifications: LazyExoticComponent<
FunctionComponent<ComponentProps>
> = lazy(() => {
@@ -572,6 +576,18 @@ const SettingsRoutes: FunctionComponent<ComponentProps> = (
}
/>
<PageRoute
path={RouteUtil.getLastPathForKey(PageMap.SETTINGS_PUSH_LOGS)}
element={
<Suspense fallback={Loader}>
<SettingsPushLog
{...props}
pageRoute={RouteMap[PageMap.SETTINGS_PUSH_LOGS] as Route}
/>
</Suspense>
}
/>
<PageRoute
path={RouteUtil.getLastPathForKey(
PageMap.SETTINGS_NOTIFICATION_SETTINGS,

View File

@@ -187,6 +187,9 @@ const AnnouncementViewSmsLogs: LazyExoticComponent<FunctionComponent<ComponentPr
const AnnouncementViewCallLogs: LazyExoticComponent<FunctionComponent<ComponentProps>> = lazy(() => {
return import("../Pages/StatusPages/Announcements/View/NotificationLogsCall");
});
const AnnouncementViewPushLogs: LazyExoticComponent<FunctionComponent<ComponentProps>> = lazy(() => {
return import("../Pages/StatusPages/Announcements/View/NotificationLogsPush");
});
const AnnouncementViewDelete: LazyExoticComponent<FunctionComponent<ComponentProps>> = lazy(() => {
return import("../Pages/StatusPages/Announcements/View/Delete");
});
@@ -294,6 +297,17 @@ const StatusPagesRoutes: FunctionComponent<ComponentProps> = (
</Suspense>
}
/>
<PageRoute
path={RouteUtil.getLastPathForKey(PageMap.ANNOUNCEMENT_VIEW_PUSH_LOGS)}
element={
<Suspense fallback={Loader}>
<AnnouncementViewPushLogs
{...props}
pageRoute={RouteMap[PageMap.ANNOUNCEMENT_VIEW_PUSH_LOGS] as Route}
/>
</Suspense>
}
/>
<PageRoute
path={RouteUtil.getLastPathForKey(PageMap.ANNOUNCEMENT_VIEW_DELETE)}
element={

View File

@@ -152,6 +152,11 @@ export function getSettingsBreadcrumbs(path: string): Array<Link> | undefined {
"Settings",
"Call Logs",
]),
...BuildBreadcrumbLinksByTitles(PageMap.SETTINGS_PUSH_LOGS, [
"Project",
"Settings",
"Push Logs",
]),
...BuildBreadcrumbLinksByTitles(PageMap.SETTINGS_EMAIL_LOGS, [
"Project",
"Settings",

View File

@@ -391,6 +391,12 @@ enum PageMap {
SETTINGS_SMS_LOGS = "SETTINGS_SMS_LOGS",
SETTINGS_EMAIL_LOGS = "SETTINGS_EMAIL_LOGS",
SETTINGS_CALL_LOGS = "SETTINGS_CALL_LOGS",
SETTINGS_PUSH_LOGS = "SETTINGS_PUSH_LOGS",
// Push Logs in resource views
INCIDENT_VIEW_PUSH_LOGS = "INCIDENT_VIEW_PUSH_LOGS",
ALERT_VIEW_PUSH_LOGS = "ALERT_VIEW_PUSH_LOGS",
ANNOUNCEMENT_VIEW_PUSH_LOGS = "ANNOUNCEMENT_VIEW_PUSH_LOGS",
}
export default PageMap;

View File

@@ -119,6 +119,7 @@ export const StatusPagesRoutePath: Dictionary<string> = {
[PageMap.ANNOUNCEMENT_VIEW_EMAIL_LOGS]: `announcements/${RouteParams.ModelID}/notification-logs/email`,
[PageMap.ANNOUNCEMENT_VIEW_SMS_LOGS]: `announcements/${RouteParams.ModelID}/notification-logs/sms`,
[PageMap.ANNOUNCEMENT_VIEW_CALL_LOGS]: `announcements/${RouteParams.ModelID}/notification-logs/call`,
[PageMap.ANNOUNCEMENT_VIEW_PUSH_LOGS]: `announcements/${RouteParams.ModelID}/notification-logs/push`,
[PageMap.ANNOUNCEMENT_VIEW_DELETE]: `announcements/${RouteParams.ModelID}/delete`,
[PageMap.STATUS_PAGE_VIEW]: `${RouteParams.ModelID}`,
[PageMap.STATUS_PAGE_VIEW_BRANDING]: `${RouteParams.ModelID}/branding`,
@@ -165,6 +166,7 @@ export const IncidentsRoutePath: Dictionary<string> = {
[PageMap.INCIDENT_VIEW_EMAIL_LOGS]: `${RouteParams.ModelID}/notification-logs/email`,
[PageMap.INCIDENT_VIEW_SMS_LOGS]: `${RouteParams.ModelID}/notification-logs/sms`,
[PageMap.INCIDENT_VIEW_CALL_LOGS]: `${RouteParams.ModelID}/notification-logs/call`,
[PageMap.INCIDENT_VIEW_PUSH_LOGS]: `${RouteParams.ModelID}/notification-logs/push`,
[PageMap.INCIDENT_VIEW_DELETE]: `${RouteParams.ModelID}/delete`,
[PageMap.INCIDENT_VIEW_SETTINGS]: `${RouteParams.ModelID}/settings`,
[PageMap.INCIDENT_VIEW_CUSTOM_FIELDS]: `${RouteParams.ModelID}/custom-fields`,
@@ -184,6 +186,7 @@ export const AlertsRoutePath: Dictionary<string> = {
[PageMap.ALERT_VIEW_EMAIL_LOGS]: `${RouteParams.ModelID}/notification-logs/email`,
[PageMap.ALERT_VIEW_SMS_LOGS]: `${RouteParams.ModelID}/notification-logs/sms`,
[PageMap.ALERT_VIEW_CALL_LOGS]: `${RouteParams.ModelID}/notification-logs/call`,
[PageMap.ALERT_VIEW_PUSH_LOGS]: `${RouteParams.ModelID}/notification-logs/push`,
[PageMap.ALERT_VIEW_DELETE]: `${RouteParams.ModelID}/delete`,
[PageMap.ALERT_VIEW_DESCRIPTION]: `${RouteParams.ModelID}/description`,
[PageMap.ALERT_VIEW_ROOT_CAUSE]: `${RouteParams.ModelID}/root-cause`,
@@ -216,6 +219,7 @@ export const SettingsRoutePath: Dictionary<string> = {
[PageMap.SETTINGS_SMS_LOGS]: "sms-logs",
[PageMap.SETTINGS_EMAIL_LOGS]: "email-logs",
[PageMap.SETTINGS_CALL_LOGS]: "call-logs",
[PageMap.SETTINGS_PUSH_LOGS]: "push-logs",
[PageMap.SETTINGS_APIKEYS]: `api-keys`,
[PageMap.SETTINGS_APIKEY_VIEW]: `api-keys/${RouteParams.ModelID}`,
[PageMap.SETTINGS_TELEMETRY_INGESTION_KEYS]: `telemetry-ingestion-keys`,