@checkstack/catalog-frontend 0.6.1 → 0.7.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 CHANGED
@@ -1,5 +1,49 @@
1
1
  # @checkstack/catalog-frontend
2
2
 
3
+ ## 0.7.0
4
+
5
+ ### Minor Changes
6
+
7
+ - bb1fea0: Redesign system detail page with hero banner, two-column layout, plugin metric tiles, and health check slide-over drawer.
8
+
9
+ ### New Components
10
+
11
+ - **MetricTile** (`@checkstack/ui`): Compact stat tile with icon, label, value, variant coloring
12
+ - **Sheet** (`@checkstack/ui`): Slide-over drawer built on Radix Dialog primitives
13
+
14
+ ### New Extension Slot
15
+
16
+ - **SystemOverviewMetricsSlot** (`@checkstack/catalog-common`): Plugin-contributed at-a-glance metric tiles in the system detail hero banner
17
+
18
+ ### Layout Changes
19
+
20
+ - System detail page now uses a hero banner with breadcrumb, status badges, and metric tile strip
21
+ - Two-column layout: monitoring content (left) and system context (right)
22
+ - Health checks rendered as compact card rows instead of heavy accordions
23
+ - Clicking a health check opens a slide-over drawer with summary tiles, timeline charts, and recent runs
24
+ - Right column uses lightweight borderless sections with dividers instead of heavy Card wrappers
25
+
26
+ ### Plugin Extensions
27
+
28
+ - Health check, SLO, Incident, and Maintenance plugins each contribute a metric tile to the hero banner
29
+
30
+ ### Patch Changes
31
+
32
+ - Updated dependencies [bb1fea0]
33
+ - Updated dependencies [bb1fea0]
34
+ - @checkstack/ui@1.4.0
35
+ - @checkstack/catalog-common@1.4.0
36
+ - @checkstack/auth-frontend@0.5.26
37
+ - @checkstack/gitops-frontend@0.3.1
38
+
39
+ ## 0.6.2
40
+
41
+ ### Patch Changes
42
+
43
+ - Updated dependencies [8ef367a]
44
+ - Updated dependencies [cb65e9d]
45
+ - @checkstack/gitops-frontend@0.3.0
46
+
3
47
  ## 0.6.1
4
48
 
5
49
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@checkstack/catalog-frontend",
3
- "version": "0.6.1",
3
+ "version": "0.7.0",
4
4
  "type": "module",
5
5
  "main": "src/index.tsx",
6
6
  "checkstack": {
@@ -17,7 +17,7 @@
17
17
  "@checkstack/catalog-common": "1.3.1",
18
18
  "@checkstack/common": "0.6.5",
19
19
  "@checkstack/frontend-api": "0.3.9",
20
- "@checkstack/gitops-frontend": "0.2.0",
20
+ "@checkstack/gitops-frontend": "0.2.1",
21
21
  "@checkstack/notification-common": "0.2.8",
22
22
  "@checkstack/ui": "1.3.6",
23
23
  "@dnd-kit/core": "^6.3.1",
@@ -1,5 +1,5 @@
1
1
  import React, { useEffect, useState, useCallback } from "react";
2
- import { useParams, useNavigate } from "react-router-dom";
2
+ import { useParams } from "react-router-dom";
3
3
  import {
4
4
  usePluginClient,
5
5
  ExtensionSlot,
@@ -14,32 +14,24 @@ import {
14
14
  import { NotificationApi } from "@checkstack/notification-common";
15
15
  import {
16
16
  Card,
17
- CardHeader,
18
- CardTitle,
19
17
  CardContent,
18
+ Page,
19
+ PageContent,
20
20
  PageLayout,
21
21
  SubscribeButton,
22
22
  useToast,
23
- BackLink,
23
+ LoadingSpinner,
24
+ AccessDenied,
24
25
  } from "@checkstack/ui";
25
26
  import { authApiRef } from "@checkstack/auth-frontend/api";
26
27
 
27
- import {
28
- Activity,
29
- Info,
30
- Users,
31
- FileJson,
32
- Calendar,
33
- Mail,
34
- User,
35
- } from "lucide-react";
28
+ import { Activity, Calendar, Mail, User } from "lucide-react";
36
29
  import { extractErrorMessage } from "@checkstack/common";
37
30
 
38
31
  const CATALOG_PLUGIN_ID = "catalog";
39
32
 
40
33
  export const SystemDetailPage: React.FC = () => {
41
34
  const { systemId } = useParams<{ systemId: string }>();
42
- const navigate = useNavigate();
43
35
  const catalogClient = usePluginClient(CatalogApi);
44
36
  const notificationClient = usePluginClient(NotificationApi);
45
37
  const toast = useToast();
@@ -88,9 +80,7 @@ export const SystemDetailPage: React.FC = () => {
88
80
  void refetchSubscriptions();
89
81
  },
90
82
  onError: (error) => {
91
- toast.error(
92
- extractErrorMessage(error, "Failed to subscribe"),
93
- );
83
+ toast.error(extractErrorMessage(error, "Failed to subscribe"));
94
84
  },
95
85
  });
96
86
 
@@ -101,9 +91,7 @@ export const SystemDetailPage: React.FC = () => {
101
91
  void refetchSubscriptions();
102
92
  },
103
93
  onError: (error) => {
104
- toast.error(
105
- extractErrorMessage(error, "Failed to unsubscribe"),
106
- );
94
+ toast.error(extractErrorMessage(error, "Failed to unsubscribe"));
107
95
  },
108
96
  });
109
97
 
@@ -144,9 +132,38 @@ export const SystemDetailPage: React.FC = () => {
144
132
  unsubscribeMutation.mutate({ groupId: getSystemGroupId() });
145
133
  };
146
134
 
147
- // Actions for the page header
135
+ if (loading) {
136
+ return (
137
+ <Page>
138
+ <PageContent>
139
+ <div className="flex justify-center py-12">
140
+ <LoadingSpinner />
141
+ </div>
142
+ </PageContent>
143
+ </Page>
144
+ );
145
+ }
146
+
147
+ if (notFound) {
148
+ return (
149
+ <Page>
150
+ <PageContent>
151
+ <div className="max-w-3xl space-y-6">
152
+ <AccessDenied />
153
+ </div>
154
+ </PageContent>
155
+ </Page>
156
+ );
157
+ }
158
+
159
+ // Guard for TypeScript
160
+ if (!system) {
161
+ return;
162
+ }
163
+
148
164
  const headerActions = (
149
- <div className="flex items-center gap-4 flex-wrap">
165
+ <div className="flex items-center gap-2">
166
+ <ExtensionSlot slot={SystemStateBadgesSlot} context={{ system }} />
150
167
  {session && (
151
168
  <SubscribeButton
152
169
  isSubscribed={isSubscribed}
@@ -159,204 +176,146 @@ export const SystemDetailPage: React.FC = () => {
159
176
  }
160
177
  />
161
178
  )}
162
- <BackLink onClick={() => navigate("/")}>Back to Dashboard</BackLink>
163
179
  </div>
164
180
  );
165
181
 
166
- if (notFound) {
167
- return (
168
- <PageLayout
169
- title="System Not Found"
170
- icon={Activity}
171
- actions={headerActions}
172
- >
173
- <Card className="border-destructive/30 bg-destructive/10">
174
- <CardContent className="p-12 text-center">
175
- <p className="text-destructive">
176
- The system you're looking for doesn't exist or has been removed.
177
- </p>
178
- </CardContent>
179
- </Card>
180
- </PageLayout>
181
- );
182
- }
183
-
184
- // Guard for TypeScript - PageLayout already handles loading state
185
- if (!system) {
186
- return;
187
- }
188
-
189
182
  return (
190
183
  <PageLayout
191
184
  title={system.name}
192
185
  icon={Activity}
193
- loading={loading}
194
186
  actions={headerActions}
187
+ loading={false}
195
188
  maxWidth="full"
196
189
  >
197
- {/* Top Extension Slot for urgent items like maintenance alerts */}
190
+ {/* Alert strip incidents, maintenances, dependency alerts */}
198
191
  <ExtensionSlot slot={SystemDetailsTopSlot} context={{ system }} />
199
192
 
200
- {/* System Status Card - displays plugin-provided state badges */}
201
- <Card className="border-border shadow-sm">
202
- <CardHeader className="border-b border-border bg-muted/30">
203
- <div className="flex items-center gap-2">
204
- <Activity className="h-5 w-5 text-muted-foreground" />
205
- <CardTitle className="text-lg font-semibold">
206
- System Status
207
- </CardTitle>
208
- </div>
209
- </CardHeader>
210
- <CardContent className="p-6">
211
- <div className="flex flex-wrap items-center gap-2">
212
- <ExtensionSlot slot={SystemStateBadgesSlot} context={{ system }} />
213
- </div>
214
- </CardContent>
215
- </Card>
193
+ {/* Two-Column Layout */}
194
+ <div className="grid gap-6 lg:grid-cols-[1fr_340px]">
195
+ {/* Left Column — Monitoring */}
196
+ <div className="space-y-6 min-w-0">
197
+ <ExtensionSlot slot={SystemDetailsSlot} context={{ system }} />
198
+ </div>
216
199
 
217
- {/* System Information Card */}
218
- <Card className="border-border shadow-sm">
219
- <CardHeader className="border-b border-border bg-muted/30">
220
- <div className="flex items-center gap-2">
221
- <Info className="h-5 w-5 text-muted-foreground" />
222
- <CardTitle className="text-lg font-semibold">
223
- System Information
224
- </CardTitle>
225
- </div>
226
- </CardHeader>
227
- <CardContent className="p-6 space-y-4">
228
- <div>
229
- <label className="text-sm font-medium text-muted-foreground">
230
- Description
231
- </label>
232
- <p className="mt-1 text-foreground">
233
- {system.description || "No description provided"}
234
- </p>
235
- </div>
236
- <div className="flex gap-6 text-sm">
237
- <div className="flex items-center gap-2 text-muted-foreground">
238
- <Calendar className="h-4 w-4" />
239
- <span>
240
- Created:{" "}
241
- {new Date(system.createdAt).toLocaleDateString("en-US", {
242
- year: "numeric",
243
- month: "short",
244
- day: "numeric",
245
- })}
246
- </span>
247
- </div>
248
- <div className="flex items-center gap-2 text-muted-foreground">
249
- <Calendar className="h-4 w-4" />
250
- <span>
251
- Updated:{" "}
252
- {new Date(system.updatedAt).toLocaleDateString("en-US", {
253
- year: "numeric",
254
- month: "short",
255
- day: "numeric",
256
- })}
257
- </span>
200
+ {/* Right Column — System Context */}
201
+ <Card className="h-fit">
202
+ <CardContent className="p-4 space-y-4">
203
+ {/* System Information */}
204
+ <div className="space-y-2">
205
+ <h3 className="text-xs font-medium text-muted-foreground uppercase tracking-wider">
206
+ About
207
+ </h3>
208
+ <p className="text-sm text-foreground">
209
+ {system.description || "No description provided"}
210
+ </p>
211
+ <div className="flex flex-wrap gap-x-4 gap-y-1 text-xs text-muted-foreground">
212
+ <span className="flex items-center gap-1.5">
213
+ <Calendar className="h-3 w-3" />
214
+ Created{" "}
215
+ {new Date(system.createdAt).toLocaleDateString("en-US", {
216
+ year: "numeric",
217
+ month: "short",
218
+ day: "numeric",
219
+ })}
220
+ </span>
221
+ <span className="flex items-center gap-1.5">
222
+ <Calendar className="h-3 w-3" />
223
+ Updated{" "}
224
+ {new Date(system.updatedAt).toLocaleDateString("en-US", {
225
+ year: "numeric",
226
+ month: "short",
227
+ day: "numeric",
228
+ })}
229
+ </span>
230
+ </div>
258
231
  </div>
259
- </div>
260
- </CardContent>
261
- </Card>
262
232
 
263
- {/* Contacts Card */}
264
- <Card className="border-border shadow-sm">
265
- <CardHeader className="border-b border-border bg-muted/30">
266
- <div className="flex items-center gap-2">
267
- <Mail className="h-5 w-5 text-muted-foreground" />
268
- <CardTitle className="text-lg font-semibold">Contacts</CardTitle>
269
- </div>
270
- </CardHeader>
271
- <CardContent className="p-6">
272
- {!contactsData || contactsData.length === 0 ? (
273
- <p className="text-muted-foreground text-sm">
274
- No contacts assigned to this system
275
- </p>
276
- ) : (
233
+ <div className="h-px bg-border" />
234
+
235
+ {/* Contacts */}
277
236
  <div className="space-y-2">
278
- {contactsData.map((contact) => (
279
- <div
280
- key={contact.id}
281
- className="flex items-center gap-2 text-sm"
282
- >
283
- {contact.type === "user" ? (
284
- <User className="h-4 w-4 text-muted-foreground" />
285
- ) : (
286
- <Mail className="h-4 w-4 text-muted-foreground" />
287
- )}
288
- <a
289
- href={`mailto:${contact.type === "user" ? contact.userEmail : contact.email}`}
290
- className="text-primary hover:underline"
291
- >
292
- {contact.type === "user"
293
- ? (contact.userName ?? contact.userId)
294
- : contact.email}
295
- </a>
296
- {contact.label && (
297
- <span className="text-muted-foreground">
298
- ({contact.label})
299
- </span>
300
- )}
237
+ <h3 className="text-xs font-medium text-muted-foreground uppercase tracking-wider">
238
+ Contacts
239
+ </h3>
240
+ {!contactsData || contactsData.length === 0 ? (
241
+ <p className="text-sm text-muted-foreground">
242
+ No contacts assigned
243
+ </p>
244
+ ) : (
245
+ <div className="space-y-1.5">
246
+ {contactsData.map((contact) => (
247
+ <div
248
+ key={contact.id}
249
+ className="flex items-center gap-2 text-sm"
250
+ >
251
+ {contact.type === "user" ? (
252
+ <User className="h-3.5 w-3.5 text-muted-foreground" />
253
+ ) : (
254
+ <Mail className="h-3.5 w-3.5 text-muted-foreground" />
255
+ )}
256
+ <a
257
+ href={`mailto:${contact.type === "user" ? contact.userEmail : contact.email}`}
258
+ className="text-primary hover:underline truncate"
259
+ >
260
+ {contact.type === "user"
261
+ ? (contact.userName ?? contact.userId)
262
+ : contact.email}
263
+ </a>
264
+ {contact.label && (
265
+ <span className="text-muted-foreground text-xs">
266
+ ({contact.label})
267
+ </span>
268
+ )}
269
+ </div>
270
+ ))}
301
271
  </div>
302
- ))}
272
+ )}
303
273
  </div>
304
- )}
305
- </CardContent>
306
- </Card>
307
274
 
308
- {/* Groups Card */}
309
- <Card className="border-border shadow-sm">
310
- <CardHeader className="border-b border-border bg-muted/30">
311
- <div className="flex items-center gap-2">
312
- <Users className="h-5 w-5 text-muted-foreground" />
313
- <CardTitle className="text-lg font-semibold">
314
- Member of Groups
315
- </CardTitle>
316
- </div>
317
- </CardHeader>
318
- <CardContent className="p-6">
319
- {groups.length === 0 ? (
320
- <p className="text-muted-foreground text-sm">
321
- This system is not part of any groups
322
- </p>
323
- ) : (
324
- <div className="flex flex-wrap gap-2">
325
- {groups.map((group) => (
326
- <span
327
- key={group.id}
328
- className="inline-flex items-center gap-1.5 rounded-full border border-primary/20 bg-primary/10 px-3 py-1 text-sm font-medium text-primary"
329
- >
330
- {group.name}
331
- </span>
332
- ))}
333
- </div>
334
- )}
335
- </CardContent>
336
- </Card>
275
+ <div className="h-px bg-border" />
337
276
 
338
- {/* Metadata Card */}
339
- {system.metadata &&
340
- typeof system.metadata === "object" &&
341
- Object.keys(system.metadata).length > 0 && (
342
- <Card className="border-border shadow-sm">
343
- <CardHeader className="border-b border-border bg-muted/30">
344
- <div className="flex items-center gap-2">
345
- <FileJson className="h-5 w-5 text-muted-foreground" />
346
- <CardTitle className="text-lg font-semibold">
347
- Metadata
348
- </CardTitle>
349
- </div>
350
- </CardHeader>
351
- <CardContent className="p-6">
352
- <pre className="text-sm text-foreground bg-muted/30 p-4 rounded border border-border overflow-x-auto">
353
- {JSON.stringify(system.metadata, undefined, 2)}
354
- </pre>
355
- </CardContent>
356
- </Card>
357
- )}
277
+ {/* Groups */}
278
+ <div className="space-y-2">
279
+ <h3 className="text-xs font-medium text-muted-foreground uppercase tracking-wider">
280
+ Groups
281
+ </h3>
282
+ {groups.length === 0 ? (
283
+ <p className="text-sm text-muted-foreground">
284
+ Not part of any groups
285
+ </p>
286
+ ) : (
287
+ <div className="flex flex-wrap gap-1.5">
288
+ {groups.map((group) => (
289
+ <span
290
+ key={group.id}
291
+ className="inline-flex items-center rounded-full border border-primary/20 bg-primary/10 px-2.5 py-0.5 text-xs font-medium text-primary"
292
+ >
293
+ {group.name}
294
+ </span>
295
+ ))}
296
+ </div>
297
+ )}
298
+ </div>
358
299
 
359
- <ExtensionSlot slot={SystemDetailsSlot} context={{ system }} />
300
+ {/* Metadata (conditional) */}
301
+ {system.metadata &&
302
+ typeof system.metadata === "object" &&
303
+ Object.keys(system.metadata).length > 0 && (
304
+ <>
305
+ <div className="h-px bg-border" />
306
+ <div className="space-y-2">
307
+ <h3 className="text-xs font-medium text-muted-foreground uppercase tracking-wider">
308
+ Metadata
309
+ </h3>
310
+ <pre className="text-xs text-foreground bg-muted/30 p-3 rounded-md border border-border overflow-x-auto">
311
+ {JSON.stringify(system.metadata, undefined, 2)}
312
+ </pre>
313
+ </div>
314
+ </>
315
+ )}
316
+ </CardContent>
317
+ </Card>
318
+ </div>
360
319
  </PageLayout>
361
320
  );
362
321
  };