Skip to content

Commit 4ce1431

Browse files
authored
feat: component subscriptions (#1901)
* feat: component subscriptions * fix: build and test * fix: review * chore: email manage url link * chore: empty state * fix: review * chore: overflow-y * fix: component id form * fix: test * fix: review * fix: structured data missing image * fix: build
1 parent bb49040 commit 4ce1431

File tree

61 files changed

+7662
-960
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

61 files changed

+7662
-960
lines changed

apps/dashboard/src/app/(dashboard)/status-pages/[id]/subscribers/page.tsx

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -24,35 +24,40 @@ import { columns } from "@/components/data-table/subscribers/columns";
2424
import { UpgradeDialog } from "@/components/dialogs/upgrade";
2525
import { DataTable } from "@/components/ui/data-table/data-table";
2626
import { useTRPC } from "@/lib/trpc/client";
27+
import type { RouterOutputs } from "@openstatus/api";
2728
import { useQuery } from "@tanstack/react-query";
2829
import { Lock } from "lucide-react";
2930
import { useParams } from "next/navigation";
3031
import { useState } from "react";
3132

33+
type Subscriber = RouterOutputs["pageSubscriber"]["list"][number];
34+
3235
const EXAMPLES = [
3336
{
3437
id: 1,
3538
email: "max@openstatus.dev",
3639
createdAt: new Date(),
37-
updatedAt: null,
3840
pageId: 1,
39-
token: null,
41+
channelType: "email",
4042
acceptedAt: new Date(),
41-
expiresAt: new Date(),
4243
unsubscribedAt: null,
44+
components: [],
45+
isEntirePage: true,
46+
webhookUrl: null,
4347
},
4448
{
4549
id: 2,
4650
email: "thibault@openstatus.dev",
4751
createdAt: new Date(),
48-
updatedAt: null,
4952
pageId: 1,
50-
token: null,
53+
channelType: "email",
5154
acceptedAt: new Date(),
52-
expiresAt: new Date(),
5355
unsubscribedAt: null,
56+
components: [],
57+
isEntirePage: true,
58+
webhookUrl: null,
5459
},
55-
];
60+
] satisfies Subscriber[];
5661

5762
export default function Page() {
5863
const { id } = useParams<{ id: string }>();

apps/dashboard/src/components/forms/components/update.tsx

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -90,11 +90,8 @@ export function FormComponentsUpdate() {
9090
onSubmit={async (values) => {
9191
await updateComponentsMutation.mutateAsync({
9292
pageId: Number.parseInt(id),
93-
components: values.components.map(({ id, ...rest }) => rest),
94-
groups: values.groups.map(({ id, components, ...rest }) => ({
95-
...rest,
96-
components: components.map(({ id, ...c }) => c),
97-
})),
93+
components: values.components,
94+
groups: values.groups.map(({ id: _groupId, ...rest }) => rest),
9895
});
9996
}}
10097
/>

apps/server/.dockerignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# This file is generated by Dofigen v2.6.0
1+
# This file is generated by Dofigen v2.7.0
22
# See https://github.com/lenra-io/dofigen
33

44
node_modules

apps/server/Dockerfile

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# syntax=docker/dockerfile:1.19.0
2-
# This file is generated by Dofigen v2.6.0
2+
# This file is generated by Dofigen v2.7.0
33
# See https://github.com/lenra-io/dofigen
44

55
# install
@@ -37,6 +37,7 @@ RUN \
3737
--mount=type=bind,target=packages/upstash/package.json,source=packages/upstash/package.json \
3838
--mount=type=bind,target=packages/utils/package.json,source=packages/utils/package.json \
3939
--mount=type=bind,target=packages/tsconfig/package.json,source=packages/tsconfig/package.json \
40+
--mount=type=bind,target=packages/subscriptions/package.json,source=packages/subscriptions/package.json \
4041
--mount=type=bind,target=packages/assertions/package.json,source=packages/assertions/package.json \
4142
--mount=type=bind,target=packages/theme-store/package.json,source=packages/theme-store/package.json \
4243
--mount=type=cache,target=/root/.bun/install/cache,sharing=locked \
@@ -60,11 +61,11 @@ COPY \
6061
RUN bun build --compile --sourcemap src/index.ts --outfile=app
6162

6263
# runtime
63-
FROM debian@sha256:b32674fb57780ad57d7b0749242d3f585f462f4ec4a60ae0adacd945f9cb9734 AS runtime
64+
FROM debian@sha256:4333240150a6924f878e05ec2c998aec95238010e0e4d2fec6161c90128c4652 AS runtime
6465
LABEL \
65-
io.dofigen.version="2.6.0" \
66+
io.dofigen.version="2.7.0" \
6667
org.opencontainers.image.authors="OpenStatus Team" \
67-
org.opencontainers.image.base.digest="sha256:b32674fb57780ad57d7b0749242d3f585f462f4ec4a60ae0adacd945f9cb9734" \
68+
org.opencontainers.image.base.digest="sha256:4333240150a6924f878e05ec2c998aec95238010e0e4d2fec6161c90128c4652" \
6869
org.opencontainers.image.base.name="docker.io/debian:bullseye-slim" \
6970
org.opencontainers.image.description="REST API server with Hono framework for OpenStatus" \
7071
org.opencontainers.image.source="https://github.com/openstatusHQ/openstatus" \

apps/server/dofigen.lock

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@ effective: |
1515
path: oven/bun
1616
digest: sha256:f20d9cf365ab35529384f1717687c739c92e6f39157a35a95ef06f4049a10e4a
1717
label:
18-
org.opencontainers.image.base.name: docker.io/oven/bun:1.3.6
1918
org.opencontainers.image.base.digest: sha256:f20d9cf365ab35529384f1717687c739c92e6f39157a35a95ef06f4049a10e4a
19+
org.opencontainers.image.base.name: docker.io/oven/bun:1.3.6
2020
org.opencontainers.image.stage: install
2121
workdir: /app/
2222
run:
@@ -78,6 +78,8 @@ effective: |
7878
source: packages/utils/package.json
7979
- target: packages/tsconfig/package.json
8080
source: packages/tsconfig/package.json
81+
- target: packages/subscriptions/package.json
82+
source: packages/subscriptions/package.json
8183
- target: packages/assertions/package.json
8284
source: packages/assertions/package.json
8385
- target: packages/theme-store/package.json
@@ -87,8 +89,8 @@ effective: |
8789
path: oven/bun
8890
digest: sha256:f20d9cf365ab35529384f1717687c739c92e6f39157a35a95ef06f4049a10e4a
8991
label:
90-
org.opencontainers.image.stage: build
9192
org.opencontainers.image.base.digest: sha256:f20d9cf365ab35529384f1717687c739c92e6f39157a35a95ef06f4049a10e4a
93+
org.opencontainers.image.stage: build
9294
org.opencontainers.image.base.name: docker.io/oven/bun:1.3.6
9395
workdir: /app/apps/server
9496
env:
@@ -105,16 +107,16 @@ effective: |
105107
- bun build --compile --sourcemap src/index.ts --outfile=app
106108
fromImage:
107109
path: debian
108-
digest: sha256:b32674fb57780ad57d7b0749242d3f585f462f4ec4a60ae0adacd945f9cb9734
110+
digest: sha256:4333240150a6924f878e05ec2c998aec95238010e0e4d2fec6161c90128c4652
109111
label:
110-
io.dofigen.version: 2.6.0
112+
org.opencontainers.image.authors: OpenStatus Team
113+
io.dofigen.version: 2.7.0
114+
org.opencontainers.image.base.digest: sha256:4333240150a6924f878e05ec2c998aec95238010e0e4d2fec6161c90128c4652
115+
org.opencontainers.image.description: REST API server with Hono framework for OpenStatus
111116
org.opencontainers.image.vendor: OpenStatus
112-
org.opencontainers.image.base.digest: sha256:b32674fb57780ad57d7b0749242d3f585f462f4ec4a60ae0adacd945f9cb9734
113117
org.opencontainers.image.source: https://github.com/openstatusHQ/openstatus
114-
org.opencontainers.image.base.name: docker.io/debian:bullseye-slim
115-
org.opencontainers.image.description: REST API server with Hono framework for OpenStatus
116-
org.opencontainers.image.authors: OpenStatus Team
117118
org.opencontainers.image.title: OpenStatus Server
119+
org.opencontainers.image.base.name: docker.io/debian:bullseye-slim
118120
user:
119121
user: '1000'
120122
group: '1000'
@@ -141,17 +143,17 @@ effective: |
141143
retries: 3
142144
images:
143145
docker.io:
144-
library:
145-
debian:
146-
bullseye-slim:
147-
digest: sha256:b32674fb57780ad57d7b0749242d3f585f462f4ec4a60ae0adacd945f9cb9734
148146
oven:
149147
bun:
150148
1.3.6:
151149
digest: sha256:f20d9cf365ab35529384f1717687c739c92e6f39157a35a95ef06f4049a10e4a
150+
library:
151+
debian:
152+
bullseye-slim:
153+
digest: sha256:4333240150a6924f878e05ec2c998aec95238010e0e4d2fec6161c90128c4652
152154
resources:
153155
dofigen.yml:
154-
hash: 534b1d0a16fe15d3ca9a01f090b1303ac7c2e011607e25167357592a6a987129
156+
hash: d3a2fdcc47230fbc3d4bf7c1298d053cc7f7cc63b035b6c19809554394304328
155157
content: |
156158
# Files to exclude from Docker context
157159
ignore:
@@ -200,6 +202,7 @@ resources:
200202
- packages/upstash/package.json
201203
- packages/utils/package.json
202204
- packages/tsconfig/package.json
205+
- packages/subscriptions/package.json
203206
- packages/assertions/package.json
204207
- packages/theme-store/package.json
205208
run: bun install --production --frozen-lockfile --verbose

apps/server/dofigen.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ builders:
4545
- packages/upstash/package.json
4646
- packages/utils/package.json
4747
- packages/tsconfig/package.json
48+
- packages/subscriptions/package.json
4849
- packages/assertions/package.json
4950
- packages/theme-store/package.json
5051
run: bun install --production --frozen-lockfile --verbose

apps/server/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
"@logtape/otel": "2.0.1",
2525
"@logtape/sentry": "2.0.1",
2626
"@openstatus/analytics": "workspace:*",
27+
"@openstatus/subscriptions": "workspace:*",
2728
"@openstatus/notification-discord": "workspace:*",
2829
"@openstatus/notification-google-chat": "workspace:*",
2930
"@openstatus/notification-grafana-oncall": "workspace:*",

apps/server/src/libs/test/preload.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,19 @@
11
import { mock } from "bun:test";
22

3+
// Subscription dispatch spies — accessible in tests via globalThis.__subscriptionSpies
4+
const dispatchStatusReportUpdateSpy = mock((_id: number) => Promise.resolve());
5+
const dispatchMaintenanceUpdateSpy = mock((_id: number) => Promise.resolve());
6+
7+
(globalThis as Record<string, unknown>).__subscriptionSpies = {
8+
dispatchStatusReportUpdate: dispatchStatusReportUpdateSpy,
9+
dispatchMaintenanceUpdate: dispatchMaintenanceUpdateSpy,
10+
};
11+
12+
mock.module("@openstatus/subscriptions", () => ({
13+
dispatchStatusReportUpdate: dispatchStatusReportUpdateSpy,
14+
dispatchMaintenanceUpdate: dispatchMaintenanceUpdateSpy,
15+
}));
16+
317
const testRedisStore = new Map<string, string>();
418
(globalThis as Record<string, unknown>).__testRedisStore = testRedisStore;
519

apps/server/src/routes/rpc/services/maintenance/__tests__/maintenance.test.ts

Lines changed: 13 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { afterAll, beforeAll, describe, expect, spyOn, test } from "bun:test";
1+
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
22
import { db, eq } from "@openstatus/db";
33
import {
44
maintenance,
@@ -7,15 +7,14 @@ import {
77
pageComponent,
88
pageSubscriber,
99
} from "@openstatus/db/src/schema";
10-
import { EmailClient } from "@openstatus/emails";
1110

1211
import { app } from "@/index";
1312

14-
// Mock the sendMaintenanceNotification method
15-
const sendMaintenanceNotificationMock = spyOn(
16-
EmailClient.prototype,
17-
"sendMaintenanceNotification",
18-
).mockResolvedValue(undefined);
13+
const subscriptionSpies = (globalThis as Record<string, unknown>)
14+
.__subscriptionSpies as {
15+
dispatchStatusReportUpdate: ReturnType<typeof import("bun:test").mock>;
16+
dispatchMaintenanceUpdate: ReturnType<typeof import("bun:test").mock>;
17+
};
1918

2019
/**
2120
* Helper to make ConnectRPC requests using the Connect protocol (JSON).
@@ -458,7 +457,7 @@ describe("MaintenanceService.CreateMaintenance", () => {
458457
});
459458

460459
test("creates maintenance with notify=true", async () => {
461-
sendMaintenanceNotificationMock.mockClear();
460+
subscriptionSpies.dispatchMaintenanceUpdate.mockClear();
462461

463462
const fromDate = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000);
464463
const toDate = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000 + 3600000);
@@ -483,12 +482,9 @@ describe("MaintenanceService.CreateMaintenance", () => {
483482
expect(data).toHaveProperty("maintenance");
484483
expect(data.maintenance.title).toBe(`${TEST_PREFIX}-with-notify`);
485484

486-
// Verify notification was sent
487-
expect(sendMaintenanceNotificationMock).toHaveBeenCalledTimes(1);
488-
const mockCall = sendMaintenanceNotificationMock.mock.calls[0][0];
489-
expect(mockCall.maintenanceTitle).toBe(`${TEST_PREFIX}-with-notify`);
490-
expect(mockCall.message).toBe(
491-
"Notifying subscribers about this maintenance.",
485+
// Verify dispatcher was called (dispatchers are mocked in preload.ts)
486+
expect(subscriptionSpies.dispatchMaintenanceUpdate).toHaveBeenCalledTimes(
487+
1,
492488
);
493489

494490
// Clean up
@@ -506,7 +502,7 @@ describe("MaintenanceService.CreateMaintenance", () => {
506502
});
507503

508504
test("creates maintenance with notify=false (default)", async () => {
509-
sendMaintenanceNotificationMock.mockClear();
505+
subscriptionSpies.dispatchMaintenanceUpdate.mockClear();
510506

511507
const fromDate = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000);
512508
const toDate = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000 + 3600000);
@@ -531,8 +527,8 @@ describe("MaintenanceService.CreateMaintenance", () => {
531527
expect(data).toHaveProperty("maintenance");
532528
expect(data.maintenance.title).toBe(`${TEST_PREFIX}-no-notify`);
533529

534-
// Verify notification was NOT sent
535-
expect(sendMaintenanceNotificationMock).not.toHaveBeenCalled();
530+
// Verify dispatcher was NOT called
531+
expect(subscriptionSpies.dispatchMaintenanceUpdate).not.toHaveBeenCalled();
536532

537533
// Clean up
538534
await db

0 commit comments

Comments
 (0)