@checkstack/dashboard-frontend 0.0.2

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 ADDED
@@ -0,0 +1,125 @@
1
+ # @checkstack/dashboard-frontend
2
+
3
+ ## 0.0.2
4
+
5
+ ### Patch Changes
6
+
7
+ - d20d274: Initial release of all @checkstack packages. Rebranded from Checkmate to Checkstack with new npm organization @checkstack and domain checkstack.dev.
8
+ - Updated dependencies [d20d274]
9
+ - @checkstack/auth-frontend@0.0.2
10
+ - @checkstack/catalog-common@0.0.2
11
+ - @checkstack/catalog-frontend@0.0.2
12
+ - @checkstack/command-common@0.0.2
13
+ - @checkstack/command-frontend@0.0.2
14
+ - @checkstack/common@0.0.2
15
+ - @checkstack/frontend-api@0.0.2
16
+ - @checkstack/healthcheck-common@0.0.2
17
+ - @checkstack/incident-common@0.0.2
18
+ - @checkstack/maintenance-common@0.0.2
19
+ - @checkstack/notification-common@0.0.2
20
+ - @checkstack/signal-frontend@0.0.2
21
+ - @checkstack/ui@0.0.2
22
+
23
+ ## 0.1.0
24
+
25
+ ### Minor Changes
26
+
27
+ - ae33df2: Move command palette from dashboard to centered navbar position
28
+
29
+ - Converted `command-frontend` into a plugin with `NavbarCenterSlot` extension
30
+ - Added compact `NavbarSearch` component with responsive search trigger
31
+ - Moved `SearchDialog` from dashboard-frontend to command-frontend
32
+ - Keyboard shortcut (⌘K / Ctrl+K) now works on every page
33
+ - Renamed navbar slots for clarity:
34
+ - `NavbarSlot` → `NavbarRightSlot`
35
+ - `NavbarMainSlot` → `NavbarLeftSlot`
36
+ - Added new `NavbarCenterSlot` for centered content
37
+
38
+ ### Patch Changes
39
+
40
+ - a65e002: Add compile-time type safety for Lucide icon names
41
+
42
+ - Add `LucideIconName` type and `lucideIconSchema` Zod schema to `@checkstack/common`
43
+ - Update backend interfaces (`AuthStrategy`, `NotificationStrategy`, `IntegrationProvider`, `CommandDefinition`) to use `LucideIconName`
44
+ - Update RPC contracts to use `lucideIconSchema` for proper type inference across RPC boundaries
45
+ - Simplify `SocialProviderButton` to use `DynamicIcon` directly (removes 30+ lines of pascalCase conversion)
46
+ - Replace static `iconMap` in `SearchDialog` with `DynamicIcon` for dynamic icon rendering
47
+ - Add fallback handling in `DynamicIcon` when icon name isn't found
48
+ - Fix legacy kebab-case icon names to PascalCase: `mail`→`Mail`, `send`→`Send`, `github`→`Github`, `key-round`→`KeyRound`, `network`→`Network`, `AlertCircle`→`CircleAlert`
49
+
50
+ - Updated dependencies [52231ef]
51
+ - Updated dependencies [b0124ef]
52
+ - Updated dependencies [54cc787]
53
+ - Updated dependencies [a65e002]
54
+ - Updated dependencies [ae33df2]
55
+ - Updated dependencies [a65e002]
56
+ - Updated dependencies [32ea706]
57
+ - @checkstack/auth-frontend@0.3.0
58
+ - @checkstack/ui@0.1.2
59
+ - @checkstack/catalog-frontend@0.1.0
60
+ - @checkstack/common@0.2.0
61
+ - @checkstack/command-frontend@0.1.0
62
+ - @checkstack/frontend-api@0.1.0
63
+ - @checkstack/catalog-common@0.1.2
64
+ - @checkstack/command-common@0.0.3
65
+ - @checkstack/healthcheck-common@0.1.1
66
+ - @checkstack/incident-common@0.1.2
67
+ - @checkstack/maintenance-common@0.1.2
68
+ - @checkstack/notification-common@0.1.1
69
+ - @checkstack/signal-frontend@0.1.1
70
+
71
+ ## 0.0.5
72
+
73
+ ### Patch Changes
74
+
75
+ - Updated dependencies [1bf71bb]
76
+ - @checkstack/auth-frontend@0.2.1
77
+ - @checkstack/catalog-frontend@0.0.5
78
+
79
+ ## 0.0.4
80
+
81
+ ### Patch Changes
82
+
83
+ - Updated dependencies [e26c08e]
84
+ - @checkstack/auth-frontend@0.2.0
85
+ - @checkstack/catalog-frontend@0.0.4
86
+
87
+ ## 0.0.3
88
+
89
+ ### Patch Changes
90
+
91
+ - Updated dependencies [0f8cc7d]
92
+ - @checkstack/frontend-api@0.0.3
93
+ - @checkstack/auth-frontend@0.1.1
94
+ - @checkstack/catalog-common@0.1.1
95
+ - @checkstack/catalog-frontend@0.0.3
96
+ - @checkstack/command-frontend@0.0.3
97
+ - @checkstack/incident-common@0.1.1
98
+ - @checkstack/maintenance-common@0.1.1
99
+ - @checkstack/ui@0.1.1
100
+
101
+ ## 0.0.2
102
+
103
+ ### Patch Changes
104
+
105
+ - Updated dependencies [eff5b4e]
106
+ - Updated dependencies [ffc28f6]
107
+ - Updated dependencies [4dd644d]
108
+ - Updated dependencies [ae19ff6]
109
+ - Updated dependencies [0babb9c]
110
+ - Updated dependencies [32f2535]
111
+ - Updated dependencies [b55fae6]
112
+ - Updated dependencies [b354ab3]
113
+ - @checkstack/maintenance-common@0.1.0
114
+ - @checkstack/ui@0.1.0
115
+ - @checkstack/common@0.1.0
116
+ - @checkstack/catalog-common@0.1.0
117
+ - @checkstack/notification-common@0.1.0
118
+ - @checkstack/incident-common@0.1.0
119
+ - @checkstack/healthcheck-common@0.1.0
120
+ - @checkstack/auth-frontend@0.1.0
121
+ - @checkstack/signal-frontend@0.1.0
122
+ - @checkstack/catalog-frontend@0.0.2
123
+ - @checkstack/command-common@0.0.2
124
+ - @checkstack/command-frontend@0.0.2
125
+ - @checkstack/frontend-api@0.0.2
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "@checkstack/dashboard-frontend",
3
+ "version": "0.0.2",
4
+ "type": "module",
5
+ "main": "src/index.tsx",
6
+ "scripts": {
7
+ "typecheck": "tsc --noEmit",
8
+ "lint": "bun run lint:code",
9
+ "lint:code": "eslint . --max-warnings 0"
10
+ },
11
+ "dependencies": {
12
+ "@checkstack/frontend-api": "workspace:*",
13
+ "@checkstack/auth-frontend": "workspace:*",
14
+ "@checkstack/common": "workspace:*",
15
+ "@checkstack/command-frontend": "workspace:*",
16
+ "@checkstack/command-common": "workspace:*",
17
+ "@checkstack/notification-common": "workspace:*",
18
+ "@checkstack/catalog-common": "workspace:*",
19
+ "@checkstack/incident-common": "workspace:*",
20
+ "@checkstack/maintenance-common": "workspace:*",
21
+ "@checkstack/healthcheck-common": "workspace:*",
22
+ "@checkstack/signal-frontend": "workspace:*",
23
+ "@checkstack/ui": "workspace:*",
24
+ "@checkstack/catalog-frontend": "workspace:*",
25
+ "react": "^18.2.0",
26
+ "react-router-dom": "^6.22.0",
27
+ "lucide-react": "^0.344.0"
28
+ },
29
+ "devDependencies": {
30
+ "typescript": "^5.0.0",
31
+ "@types/react": "^18.2.0",
32
+ "@checkstack/tsconfig": "workspace:*",
33
+ "@checkstack/scripts": "workspace:*"
34
+ }
35
+ }
@@ -0,0 +1,389 @@
1
+ import React, { useEffect, useState, useCallback } from "react";
2
+ import { useNavigate } from "react-router-dom";
3
+ import {
4
+ useApi,
5
+ rpcApiRef,
6
+ ExtensionSlot,
7
+ } from "@checkstack/frontend-api";
8
+ import { catalogApiRef } from "@checkstack/catalog-frontend";
9
+ import {
10
+ catalogRoutes,
11
+ SystemStateBadgesSlot,
12
+ System,
13
+ Group,
14
+ } from "@checkstack/catalog-common";
15
+ import { resolveRoute } from "@checkstack/common";
16
+ import {
17
+ NotificationApi,
18
+ type EnrichedSubscription,
19
+ } from "@checkstack/notification-common";
20
+ import { IncidentApi } from "@checkstack/incident-common";
21
+ import { MaintenanceApi } from "@checkstack/maintenance-common";
22
+ import { HEALTH_CHECK_RUN_COMPLETED } from "@checkstack/healthcheck-common";
23
+ import { useSignal } from "@checkstack/signal-frontend";
24
+ import {
25
+ Card,
26
+ CardHeader,
27
+ CardTitle,
28
+ CardContent,
29
+ SectionHeader,
30
+ StatusCard,
31
+ EmptyState,
32
+ LoadingSpinner,
33
+ SubscribeButton,
34
+ useToast,
35
+ AnimatedCounter,
36
+ TerminalFeed,
37
+ type TerminalEntry,
38
+ } from "@checkstack/ui";
39
+ import {
40
+ LayoutGrid,
41
+ Server,
42
+ Activity,
43
+ ChevronRight,
44
+ AlertTriangle,
45
+ Wrench,
46
+ Terminal,
47
+ } from "lucide-react";
48
+ import { authApiRef } from "@checkstack/auth-frontend/api";
49
+
50
+ const CATALOG_PLUGIN_ID = "catalog";
51
+ const MAX_TERMINAL_ENTRIES = 8;
52
+
53
+ const getGroupId = (groupId: string) => `${CATALOG_PLUGIN_ID}.group.${groupId}`;
54
+
55
+ interface GroupWithSystems extends Group {
56
+ systems: System[];
57
+ }
58
+
59
+ // Map health check status to terminal entry variant
60
+ const statusToVariant = (
61
+ status: "healthy" | "degraded" | "unhealthy"
62
+ ): TerminalEntry["variant"] => {
63
+ switch (status) {
64
+ case "healthy": {
65
+ return "success";
66
+ }
67
+ case "degraded": {
68
+ return "warning";
69
+ }
70
+ case "unhealthy": {
71
+ return "error";
72
+ }
73
+ }
74
+ };
75
+
76
+ export const Dashboard: React.FC = () => {
77
+ const catalogApi = useApi(catalogApiRef);
78
+ const rpcApi = useApi(rpcApiRef);
79
+ const notificationApi = rpcApi.forPlugin(NotificationApi);
80
+ const incidentApi = rpcApi.forPlugin(IncidentApi);
81
+ const maintenanceApi = rpcApi.forPlugin(MaintenanceApi);
82
+ const navigate = useNavigate();
83
+ const toast = useToast();
84
+ const authApi = useApi(authApiRef);
85
+ const { data: session } = authApi.useSession();
86
+
87
+ const [groupsWithSystems, setGroupsWithSystems] = useState<
88
+ GroupWithSystems[]
89
+ >([]);
90
+ const [loading, setLoading] = useState(true);
91
+
92
+ // Overview statistics state
93
+ const [systemsCount, setSystemsCount] = useState(0);
94
+ const [activeIncidentsCount, setActiveIncidentsCount] = useState(0);
95
+ const [activeMaintenancesCount, setActiveMaintenancesCount] = useState(0);
96
+
97
+ // Terminal feed entries from real healthcheck signals
98
+ const [terminalEntries, setTerminalEntries] = useState<TerminalEntry[]>([]);
99
+
100
+ // Subscription state
101
+ const [subscriptions, setSubscriptions] = useState<EnrichedSubscription[]>(
102
+ []
103
+ );
104
+ const [subscriptionLoading, setSubscriptionLoading] = useState<
105
+ Record<string, boolean>
106
+ >({});
107
+
108
+ // Listen for health check runs and add to terminal feed
109
+ useSignal(
110
+ HEALTH_CHECK_RUN_COMPLETED,
111
+ ({ systemName, configurationName, status, latencyMs }) => {
112
+ const newEntry: TerminalEntry = {
113
+ id: `${configurationName}-${Date.now()}`,
114
+ timestamp: new Date(),
115
+ content: `${systemName} (${configurationName}) → ${status}`,
116
+ variant: statusToVariant(status),
117
+ suffix: latencyMs === undefined ? undefined : `${latencyMs}ms`,
118
+ };
119
+
120
+ setTerminalEntries((prev) =>
121
+ [newEntry, ...prev].slice(0, MAX_TERMINAL_ENTRIES)
122
+ );
123
+ }
124
+ );
125
+
126
+ useEffect(() => {
127
+ if (session) {
128
+ notificationApi.getSubscriptions().then(setSubscriptions);
129
+ }
130
+ }, [session, notificationApi]);
131
+
132
+ useEffect(() => {
133
+ Promise.all([
134
+ catalogApi.getGroups(),
135
+ catalogApi.getSystems(),
136
+ incidentApi.listIncidents({ includeResolved: false }),
137
+ maintenanceApi.listMaintenances({ status: "in_progress" }),
138
+ ])
139
+ .then(([groups, systems, incidents, maintenances]) => {
140
+ // Set overview statistics
141
+ setSystemsCount(systems.length);
142
+ setActiveIncidentsCount(incidents.length);
143
+ setActiveMaintenancesCount(maintenances.length);
144
+
145
+ // Create a map of system IDs to systems
146
+ const systemMap = new Map(systems.map((s) => [s.id, s]));
147
+
148
+ // Map groups to include their systems
149
+ const groupsData: GroupWithSystems[] = groups.map((group) => {
150
+ const groupSystems = (group.systemIds || [])
151
+ .map((id) => systemMap.get(id))
152
+ .filter((s): s is System => s !== undefined);
153
+
154
+ return {
155
+ ...group,
156
+ systems: groupSystems,
157
+ };
158
+ });
159
+
160
+ setGroupsWithSystems(groupsData);
161
+ })
162
+ .catch(console.error)
163
+ .finally(() => setLoading(false));
164
+ }, [catalogApi, incidentApi, maintenanceApi]);
165
+
166
+ const handleSystemClick = (systemId: string) => {
167
+ navigate(resolveRoute(catalogRoutes.routes.systemDetail, { systemId }));
168
+ };
169
+
170
+ const isSubscribed = (groupId: string) => {
171
+ const fullId = getGroupId(groupId);
172
+ return subscriptions.some((s) => s.groupId === fullId);
173
+ };
174
+
175
+ const handleSubscribe = useCallback(
176
+ async (groupId: string) => {
177
+ const fullId = getGroupId(groupId);
178
+ setSubscriptionLoading((prev) => ({ ...prev, [groupId]: true }));
179
+ try {
180
+ await notificationApi.subscribe({ groupId: fullId });
181
+ setSubscriptions((prev) => [
182
+ ...prev,
183
+ {
184
+ groupId: fullId,
185
+ groupName: "",
186
+ groupDescription: "",
187
+ ownerPlugin: CATALOG_PLUGIN_ID,
188
+ subscribedAt: new Date(),
189
+ },
190
+ ]);
191
+ toast.success("Subscribed to group notifications");
192
+ } catch (error) {
193
+ const message =
194
+ error instanceof Error ? error.message : "Failed to subscribe";
195
+ toast.error(message);
196
+ } finally {
197
+ setSubscriptionLoading((prev) => ({ ...prev, [groupId]: false }));
198
+ }
199
+ },
200
+ [notificationApi, toast]
201
+ );
202
+
203
+ const handleUnsubscribe = useCallback(
204
+ async (groupId: string) => {
205
+ const fullId = getGroupId(groupId);
206
+ setSubscriptionLoading((prev) => ({ ...prev, [groupId]: true }));
207
+ try {
208
+ await notificationApi.unsubscribe({ groupId: fullId });
209
+ setSubscriptions((prev) => prev.filter((s) => s.groupId !== fullId));
210
+ toast.success("Unsubscribed from group notifications");
211
+ } catch (error) {
212
+ const message =
213
+ error instanceof Error ? error.message : "Failed to unsubscribe";
214
+ toast.error(message);
215
+ } finally {
216
+ setSubscriptionLoading((prev) => ({ ...prev, [groupId]: false }));
217
+ }
218
+ },
219
+ [notificationApi, toast]
220
+ );
221
+
222
+ const renderGroupsContent = () => {
223
+ if (loading) {
224
+ return <LoadingSpinner />;
225
+ }
226
+
227
+ if (groupsWithSystems.length === 0) {
228
+ return (
229
+ <EmptyState
230
+ title="No system groups found"
231
+ description="Visit the Catalog to create your first group."
232
+ icon={<Server className="w-12 h-12" />}
233
+ />
234
+ );
235
+ }
236
+
237
+ return (
238
+ <div className="space-y-4">
239
+ {groupsWithSystems.map((group) => (
240
+ <Card
241
+ key={group.id}
242
+ className="border-border shadow-sm hover:shadow-md transition-shadow"
243
+ >
244
+ <CardHeader className="border-b border-border bg-muted/30">
245
+ <div className="flex items-center gap-2">
246
+ <LayoutGrid className="h-5 w-5 text-muted-foreground" />
247
+ <CardTitle className="text-lg font-semibold text-foreground">
248
+ {group.name}
249
+ </CardTitle>
250
+ <span className="ml-auto text-sm text-muted-foreground mr-2">
251
+ {group.systems.length}{" "}
252
+ {group.systems.length === 1 ? "system" : "systems"}
253
+ </span>
254
+ {session && (
255
+ <SubscribeButton
256
+ isSubscribed={isSubscribed(group.id)}
257
+ onSubscribe={() => handleSubscribe(group.id)}
258
+ onUnsubscribe={() => handleUnsubscribe(group.id)}
259
+ loading={subscriptionLoading[group.id] || false}
260
+ />
261
+ )}
262
+ </div>
263
+ </CardHeader>
264
+ <CardContent className="p-4">
265
+ {group.systems.length === 0 ? (
266
+ <div className="py-8 text-center">
267
+ <p className="text-sm text-muted-foreground">
268
+ No systems in this group yet
269
+ </p>
270
+ </div>
271
+ ) : (
272
+ <div
273
+ className={`grid gap-3 ${
274
+ group.systems.length === 1
275
+ ? "grid-cols-1"
276
+ : "grid-cols-1 sm:grid-cols-2"
277
+ }`}
278
+ >
279
+ {group.systems.map((system) => (
280
+ <button
281
+ key={system.id}
282
+ onClick={() => handleSystemClick(system.id)}
283
+ className="flex items-center justify-between gap-3 rounded-lg border border-border bg-card px-4 py-3 transition-all cursor-pointer hover:border-border/80 hover:shadow-sm text-left"
284
+ >
285
+ <div className="flex items-center gap-3 min-w-0 flex-1">
286
+ <Activity className="h-4 w-4 text-muted-foreground flex-shrink-0" />
287
+ <p className="text-sm font-medium text-foreground truncate">
288
+ {system.name}
289
+ </p>
290
+ </div>
291
+ <ExtensionSlot
292
+ slot={SystemStateBadgesSlot}
293
+ context={{ system }}
294
+ />
295
+ <ChevronRight className="h-4 w-4 text-muted-foreground flex-shrink-0" />
296
+ </button>
297
+ ))}
298
+ </div>
299
+ )}
300
+ </CardContent>
301
+ </Card>
302
+ ))}
303
+ </div>
304
+ );
305
+ };
306
+
307
+ return (
308
+ <>
309
+ <div className="space-y-8 animate-in fade-in duration-500">
310
+ {/* Overview Section */}
311
+ <section>
312
+ <SectionHeader
313
+ title="Overview"
314
+ icon={<Activity className="w-5 h-5" />}
315
+ />
316
+ <div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
317
+ <StatusCard
318
+ title="Total Systems"
319
+ value={loading ? "..." : <AnimatedCounter value={systemsCount} />}
320
+ description="Monitored systems in your catalog"
321
+ icon={<Server className="w-4 h-4" />}
322
+ />
323
+
324
+ <StatusCard
325
+ variant={activeIncidentsCount > 0 ? "gradient" : "default"}
326
+ title="Active Incidents"
327
+ value={
328
+ loading ? (
329
+ "..."
330
+ ) : (
331
+ <AnimatedCounter value={activeIncidentsCount} />
332
+ )
333
+ }
334
+ description={
335
+ activeIncidentsCount === 0
336
+ ? "All systems operating normally"
337
+ : "Unresolved issues requiring attention"
338
+ }
339
+ icon={<AlertTriangle className="w-4 h-4" />}
340
+ />
341
+
342
+ <StatusCard
343
+ title="Active Maintenances"
344
+ value={
345
+ loading ? (
346
+ "..."
347
+ ) : (
348
+ <AnimatedCounter value={activeMaintenancesCount} />
349
+ )
350
+ }
351
+ description={
352
+ activeMaintenancesCount === 0
353
+ ? "No scheduled maintenance"
354
+ : "Ongoing or scheduled maintenance windows"
355
+ }
356
+ icon={<Wrench className="w-4 h-4" />}
357
+ />
358
+ </div>
359
+ </section>
360
+
361
+ {/* Terminal Feed and System Groups - Two Column Layout */}
362
+ <div className="grid gap-8 lg:grid-cols-3">
363
+ {/* Terminal Feed */}
364
+ <section className="lg:col-span-1">
365
+ <SectionHeader
366
+ title="Recent Activity"
367
+ icon={<Terminal className="w-5 h-5" />}
368
+ />
369
+ <TerminalFeed
370
+ entries={terminalEntries}
371
+ maxEntries={MAX_TERMINAL_ENTRIES}
372
+ maxHeight="350px"
373
+ title="checkstack status --watch"
374
+ />
375
+ </section>
376
+
377
+ {/* System Groups */}
378
+ <section className="lg:col-span-2">
379
+ <SectionHeader
380
+ title="System Groups"
381
+ icon={<LayoutGrid className="w-5 h-5" />}
382
+ />
383
+ {renderGroupsContent()}
384
+ </section>
385
+ </div>
386
+ </div>
387
+ </>
388
+ );
389
+ };
package/src/index.tsx ADDED
@@ -0,0 +1,20 @@
1
+ import { FrontendPlugin, DashboardSlot } from "@checkstack/frontend-api";
2
+ import { definePluginMetadata } from "@checkstack/common";
3
+ import { Dashboard } from "./Dashboard";
4
+
5
+ const pluginMetadata = definePluginMetadata({
6
+ pluginId: "dashboard",
7
+ });
8
+
9
+ export const dashboardPlugin: FrontendPlugin = {
10
+ metadata: pluginMetadata,
11
+ extensions: [
12
+ {
13
+ id: "dashboard-main",
14
+ slot: DashboardSlot,
15
+ component: Dashboard as React.ComponentType<unknown>,
16
+ },
17
+ ],
18
+ };
19
+
20
+ export default dashboardPlugin;
package/tsconfig.json ADDED
@@ -0,0 +1,6 @@
1
+ {
2
+ "extends": "@checkstack/tsconfig/frontend.json",
3
+ "include": [
4
+ "src"
5
+ ]
6
+ }