@checkstack/status-page-frontend 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +109 -0
- package/package.json +32 -0
- package/src/index.tsx +46 -0
- package/src/pages/PublicStatusPage.tsx +109 -0
- package/src/pages/StatusPageBuilderPage.tsx +809 -0
- package/src/pages/StatusPagesListPage.tsx +249 -0
- package/src/renderers.tsx +481 -0
- package/tsconfig.json +26 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# @checkstack/status-page-frontend
|
|
2
|
+
|
|
3
|
+
## 0.1.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 9ab73c5: Status pages: configurable incident/maintenance updates + recently resolved/completed items.
|
|
8
|
+
|
|
9
|
+
The Incidents and Maintenance widgets gain four config options (in the builder):
|
|
10
|
+
|
|
11
|
+
- **Show updates** (default on) — render the per-item update timeline so visitors
|
|
12
|
+
can follow progress. The maintenance widget now renders its timeline too
|
|
13
|
+
(previously it fetched updates but didn't show them). Turning this off also
|
|
14
|
+
skips the per-item detail fetch (a perf win).
|
|
15
|
+
- **Max updates per item** (default 3) — show only the latest N updates,
|
|
16
|
+
most-recent first, so a chatty incident doesn't dominate the page.
|
|
17
|
+
- **Show recently resolved / completed** (default off) — include resolved
|
|
18
|
+
incidents / completed maintenances, rendered in a separate "Recently resolved"
|
|
19
|
+
/ "Past maintenance" subsection below the active items.
|
|
20
|
+
- **Max age (days)** (default 7) — only include past items resolved/completed
|
|
21
|
+
within the window.
|
|
22
|
+
|
|
23
|
+
Scoping and isolation are unchanged: still only the systems the operator bound,
|
|
24
|
+
still fail-closed when none are bound, still field-allow-listed DTOs (no
|
|
25
|
+
`createdBy`). The active/past partition + max-age + cap is a pure, unit-tested
|
|
26
|
+
helper (`selectEvents`).
|
|
27
|
+
|
|
28
|
+
- 5c6393f: Add operator-built public Status Pages (phase 1: secure, extensible core).
|
|
29
|
+
|
|
30
|
+
Operators compose a public status page from widgets (status banner, system
|
|
31
|
+
health, group status, 90-day uptime, incidents, scheduled maintenance) plus
|
|
32
|
+
content blocks (text/Markdown, heading, links, image, divider), each bound to the
|
|
33
|
+
resources they choose, then publish it.
|
|
34
|
+
|
|
35
|
+
Security model — "only published widgets reveal data":
|
|
36
|
+
|
|
37
|
+
- A single public endpoint, `getPublishedStatusPage(slug)`, returns the layout
|
|
38
|
+
plus each widget's already-resolved, field-ALLOW-LISTED DTO. The public surface
|
|
39
|
+
has no generic data API, so it can only ever show what was placed on the page.
|
|
40
|
+
- Three gates: edit-time (you can only bind resources you can access), publish-time
|
|
41
|
+
(an audited, deliberate exposure that re-checks the editor can read every bound
|
|
42
|
+
resource via a user-scoped client), and render-time (resolvers run as a trusted
|
|
43
|
+
service but emit only DTO fields — never internal config, ids, or `createdBy`;
|
|
44
|
+
the service re-validates each DTO against its schema, so a resolver bug fails
|
|
45
|
+
closed).
|
|
46
|
+
- The overall banner rolls up only the bound systems; private resources are never
|
|
47
|
+
exposed beyond their public-safe status; per-binding label overrides avoid
|
|
48
|
+
internal-name leaks.
|
|
49
|
+
|
|
50
|
+
Coherence + extensibility:
|
|
51
|
+
|
|
52
|
+
- Status pages are team-scopable resources (RLAC): created via the standard
|
|
53
|
+
owning-team picker + create-capability flow, resolvable by name in the Teams
|
|
54
|
+
admin.
|
|
55
|
+
- Widget types come from an extension-point registry, so any plugin can contribute
|
|
56
|
+
a widget (config schema + public DTO + `resolvePublic`); the public renderers
|
|
57
|
+
are pure, prop-only components with no data access, so third-party widgets can
|
|
58
|
+
never leak.
|
|
59
|
+
- Draft vs published layouts; per-page visibility (public / authenticated-only)
|
|
60
|
+
and theming (brand color, logo).
|
|
61
|
+
|
|
62
|
+
Dependency direction: the status-page platform owns the widget-type registry and
|
|
63
|
+
the content widgets, but the DOMAIN widgets are contributed by their owning
|
|
64
|
+
plugins via the `statusWidgetTypeExtensionPoint` — system health / uptime /
|
|
65
|
+
banner / group status by `healthcheck-backend`, incidents by `incident-backend`,
|
|
66
|
+
scheduled maintenance by `maintenance-backend`. So `status-page-backend` depends
|
|
67
|
+
only on `backend-api` / `common` / `status-page-common`; the owning plugins
|
|
68
|
+
depend on the platform, never the reverse. `catalog-common` gains
|
|
69
|
+
`assertCatalogResourcesReadable` for the publish-time access check.
|
|
70
|
+
|
|
71
|
+
Phase 1 scope: the secure core, the admin builder, and the public page (served as
|
|
72
|
+
a no-access-rule route). A fully separate public bundle, custom domains + TLS,
|
|
73
|
+
drag-reorder, live-data preview, and distribution (embeds/badges/RSS/subscriptions)
|
|
74
|
+
are the next phases.
|
|
75
|
+
|
|
76
|
+
### Patch Changes
|
|
77
|
+
|
|
78
|
+
- 9ab73c5: Status pages: render the public page without the admin chrome, fix slug auto-fill, and polish the widgets.
|
|
79
|
+
|
|
80
|
+
- **Standalone routes.** A plugin route may now set `standalone: true` to render
|
|
81
|
+
WITHOUT the app chrome (no sidebar, header, ambient background, or command
|
|
82
|
+
palette). The router renders standalone routes as siblings of a new shell
|
|
83
|
+
LAYOUT route (`<Outlet/>`), so they show none of the authenticated UI while
|
|
84
|
+
still living inside the API/session providers. The public status page
|
|
85
|
+
(`/status/:slug`) uses it, so a published page no longer embeds the whole
|
|
86
|
+
Checkstack admin UI.
|
|
87
|
+
- **Slug auto-fill fix.** In the "new status page" dialog the slug now follows
|
|
88
|
+
the title as you type, until you edit the slug yourself (previously it stopped
|
|
89
|
+
after the first character).
|
|
90
|
+
- **Widget polish.** The public renderers and page were redesigned to look like a
|
|
91
|
+
real status page: a brand-accent top bar, a centered header, card sections with
|
|
92
|
+
proper spacing, an icon-led status banner, clearer status pills, nicer uptime
|
|
93
|
+
bars, an incident timeline, and severity-coloured incident badges.
|
|
94
|
+
- **Uptime "no data" fix.** A system with no run history in the window showed a
|
|
95
|
+
misleading "0.00%"; the uptime widget now shows "No uptime data for this period
|
|
96
|
+
yet" (a healthy system with no history is not 0% uptime), with accurate
|
|
97
|
+
start/end date labels under the bars.
|
|
98
|
+
|
|
99
|
+
- Updated dependencies [d2077bd]
|
|
100
|
+
- Updated dependencies [551eaa9]
|
|
101
|
+
- Updated dependencies [9ab73c5]
|
|
102
|
+
- Updated dependencies [9ab73c5]
|
|
103
|
+
- Updated dependencies [5c6393f]
|
|
104
|
+
- @checkstack/auth-frontend@0.8.0
|
|
105
|
+
- @checkstack/common@0.16.0
|
|
106
|
+
- @checkstack/catalog-common@2.4.0
|
|
107
|
+
- @checkstack/ui@1.16.1
|
|
108
|
+
- @checkstack/status-page-common@0.1.0
|
|
109
|
+
- @checkstack/frontend-api@0.10.0
|
package/package.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@checkstack/status-page-frontend",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"license": "Elastic-2.0",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "src/index.tsx",
|
|
7
|
+
"checkstack": {
|
|
8
|
+
"type": "frontend"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"typecheck": "tsgo -b",
|
|
12
|
+
"lint": "bun run lint:code",
|
|
13
|
+
"lint:code": "eslint . --max-warnings 0"
|
|
14
|
+
},
|
|
15
|
+
"dependencies": {
|
|
16
|
+
"@checkstack/status-page-common": "0.1.0",
|
|
17
|
+
"@checkstack/auth-frontend": "0.8.0",
|
|
18
|
+
"@checkstack/catalog-common": "2.4.0",
|
|
19
|
+
"@checkstack/common": "0.16.0",
|
|
20
|
+
"@checkstack/frontend-api": "0.10.0",
|
|
21
|
+
"@checkstack/ui": "1.16.1",
|
|
22
|
+
"lucide-react": "^1.17.0",
|
|
23
|
+
"react": "19.2.7",
|
|
24
|
+
"react-router-dom": "^7.16.0"
|
|
25
|
+
},
|
|
26
|
+
"devDependencies": {
|
|
27
|
+
"typescript": "^5.0.0",
|
|
28
|
+
"@types/react": "^19.0.0",
|
|
29
|
+
"@checkstack/tsconfig": "0.0.7",
|
|
30
|
+
"@checkstack/scripts": "0.6.2"
|
|
31
|
+
}
|
|
32
|
+
}
|
package/src/index.tsx
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { createFrontendPlugin } from "@checkstack/frontend-api";
|
|
2
|
+
import { MonitorCheck } from "lucide-react";
|
|
3
|
+
import {
|
|
4
|
+
pluginMetadata,
|
|
5
|
+
statusPageRoutes,
|
|
6
|
+
statusPublicRoutes,
|
|
7
|
+
statusPageAccess,
|
|
8
|
+
} from "@checkstack/status-page-common";
|
|
9
|
+
|
|
10
|
+
export default createFrontendPlugin({
|
|
11
|
+
metadata: pluginMetadata,
|
|
12
|
+
routes: [
|
|
13
|
+
{
|
|
14
|
+
route: statusPageRoutes.routes.list,
|
|
15
|
+
load: () =>
|
|
16
|
+
import("./pages/StatusPagesListPage").then((m) => ({
|
|
17
|
+
default: m.StatusPagesListPage,
|
|
18
|
+
})),
|
|
19
|
+
title: "Status pages",
|
|
20
|
+
accessRule: statusPageAccess.page.read,
|
|
21
|
+
nav: { group: "Workspace", icon: MonitorCheck, label: "Status pages" },
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
route: statusPageRoutes.routes.builder,
|
|
25
|
+
load: () =>
|
|
26
|
+
import("./pages/StatusPageBuilderPage").then((m) => ({
|
|
27
|
+
default: m.StatusPageBuilderPage,
|
|
28
|
+
})),
|
|
29
|
+
title: "Status page builder",
|
|
30
|
+
accessRule: statusPageAccess.page.manage,
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
// PUBLIC: no access rule -> renders for anonymous visitors. `standalone`
|
|
34
|
+
// renders it WITHOUT the admin chrome (no sidebar/header/command palette).
|
|
35
|
+
// Only calls the single public endpoint, which enforces published +
|
|
36
|
+
// visibility + the field allow-list server-side.
|
|
37
|
+
route: statusPublicRoutes.routes.page,
|
|
38
|
+
load: () =>
|
|
39
|
+
import("./pages/PublicStatusPage").then((m) => ({
|
|
40
|
+
default: m.PublicStatusPage,
|
|
41
|
+
})),
|
|
42
|
+
title: "Status",
|
|
43
|
+
standalone: true,
|
|
44
|
+
},
|
|
45
|
+
],
|
|
46
|
+
});
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import React, { useEffect } from "react";
|
|
2
|
+
import { useParams } from "react-router-dom";
|
|
3
|
+
import { LoadingSpinner } from "@checkstack/ui";
|
|
4
|
+
import { usePluginClient } from "@checkstack/frontend-api";
|
|
5
|
+
import { StatusPageApi } from "@checkstack/status-page-common";
|
|
6
|
+
import { BlockRenderer } from "../renderers";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The PUBLIC status page. Renders ENTIRELY from the single
|
|
10
|
+
* `getPublishedStatusPage` response — it makes no other data call, so it can
|
|
11
|
+
* only ever show what the publisher placed on the page. Registered as a
|
|
12
|
+
* `standalone` route, so it renders with NO admin chrome.
|
|
13
|
+
*/
|
|
14
|
+
export const PublicStatusPage: React.FC = () => {
|
|
15
|
+
const { slug = "" } = useParams();
|
|
16
|
+
const client = usePluginClient(StatusPageApi);
|
|
17
|
+
const { data, isLoading } = client.getPublishedStatusPage.useQuery({ slug });
|
|
18
|
+
|
|
19
|
+
// Reflect the page name in the browser tab (restored on unmount).
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
if (!data?.title) return;
|
|
22
|
+
const previous = document.title;
|
|
23
|
+
document.title = data.title;
|
|
24
|
+
return () => {
|
|
25
|
+
document.title = previous;
|
|
26
|
+
};
|
|
27
|
+
}, [data?.title]);
|
|
28
|
+
|
|
29
|
+
if (isLoading) {
|
|
30
|
+
return (
|
|
31
|
+
<div className="flex min-h-screen items-center justify-center bg-background">
|
|
32
|
+
<LoadingSpinner />
|
|
33
|
+
</div>
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (!data) {
|
|
38
|
+
return (
|
|
39
|
+
<div className="flex min-h-screen flex-col items-center justify-center gap-2 bg-background px-4 text-center">
|
|
40
|
+
<h1 className="text-xl font-semibold">Status page not found</h1>
|
|
41
|
+
<p className="text-sm text-muted-foreground">
|
|
42
|
+
This status page does not exist or is not published.
|
|
43
|
+
</p>
|
|
44
|
+
</div>
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Per-page brand color overrides the design token at the page root.
|
|
49
|
+
const style: React.CSSProperties & Record<string, string> = {};
|
|
50
|
+
if (data.theme.brandColorHsl) style["--primary"] = data.theme.brandColorHsl;
|
|
51
|
+
|
|
52
|
+
const updated = new Date(data.generatedAt).toLocaleString(undefined, {
|
|
53
|
+
dateStyle: "medium",
|
|
54
|
+
timeStyle: "short",
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
<div style={style} className="min-h-screen bg-background text-foreground">
|
|
59
|
+
{/* Brand accent + a soft wash behind the header for depth. */}
|
|
60
|
+
<div className="relative">
|
|
61
|
+
<div
|
|
62
|
+
aria-hidden
|
|
63
|
+
className="pointer-events-none absolute inset-x-0 top-0 h-64 bg-gradient-to-b from-primary/[0.07] to-transparent"
|
|
64
|
+
/>
|
|
65
|
+
<div className="relative mx-auto w-full max-w-3xl px-4 pb-16 pt-14 sm:px-6">
|
|
66
|
+
<header className="mb-10 flex flex-col items-center gap-4 text-center">
|
|
67
|
+
{data.theme.logoUrl && (
|
|
68
|
+
<img
|
|
69
|
+
src={data.theme.logoUrl}
|
|
70
|
+
alt=""
|
|
71
|
+
className="h-12 max-w-[200px] object-contain"
|
|
72
|
+
/>
|
|
73
|
+
)}
|
|
74
|
+
<h1 className="text-3xl font-bold tracking-tight sm:text-4xl">
|
|
75
|
+
{data.title}
|
|
76
|
+
</h1>
|
|
77
|
+
<p className="text-xs text-muted-foreground">Updated {updated}</p>
|
|
78
|
+
</header>
|
|
79
|
+
|
|
80
|
+
{data.blocks.length === 0 ? (
|
|
81
|
+
<p className="py-10 text-center text-sm text-muted-foreground">
|
|
82
|
+
This status page has no content yet.
|
|
83
|
+
</p>
|
|
84
|
+
) : (
|
|
85
|
+
<div className="space-y-5">
|
|
86
|
+
{data.blocks.map((block) => (
|
|
87
|
+
<BlockRenderer key={block.id} block={block} />
|
|
88
|
+
))}
|
|
89
|
+
</div>
|
|
90
|
+
)}
|
|
91
|
+
|
|
92
|
+
<footer className="mt-16 flex flex-col items-center gap-1 border-t border-border pt-6 text-center text-xs text-muted-foreground">
|
|
93
|
+
<span>
|
|
94
|
+
Powered by{" "}
|
|
95
|
+
<a
|
|
96
|
+
href="https://checkstack.dev"
|
|
97
|
+
target="_blank"
|
|
98
|
+
rel="noopener noreferrer"
|
|
99
|
+
className="font-medium text-foreground hover:text-primary hover:underline"
|
|
100
|
+
>
|
|
101
|
+
Checkstack
|
|
102
|
+
</a>
|
|
103
|
+
</span>
|
|
104
|
+
</footer>
|
|
105
|
+
</div>
|
|
106
|
+
</div>
|
|
107
|
+
</div>
|
|
108
|
+
);
|
|
109
|
+
};
|