@checkstack/dashboard-frontend 0.3.35 → 0.4.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,26 @@
1
1
  # @checkstack/dashboard-frontend
2
2
 
3
+ ## 0.4.0
4
+
5
+ ### Minor Changes
6
+
7
+ - bb1fea0: feat: implement active incident and maintenance overview sheets on dashboard
8
+
9
+ - Replaces direct routing on status cards with slide-out overview sheets to gracefully degrade for users without manage permissions
10
+ - Refactors dashboard system groups into a clean table-style list layout for better density
11
+ - Makes global status cards more compact
12
+
13
+ ### Patch Changes
14
+
15
+ - Updated dependencies [bb1fea0]
16
+ - Updated dependencies [bb1fea0]
17
+ - @checkstack/ui@1.4.0
18
+ - @checkstack/catalog-common@1.4.0
19
+ - @checkstack/catalog-frontend@0.7.0
20
+ - @checkstack/auth-frontend@0.5.26
21
+ - @checkstack/command-frontend@0.2.28
22
+ - @checkstack/queue-frontend@0.2.28
23
+
3
24
  ## 0.3.35
4
25
 
5
26
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@checkstack/dashboard-frontend",
3
- "version": "0.3.35",
3
+ "version": "0.4.0",
4
4
  "type": "module",
5
5
  "main": "src/index.tsx",
6
6
  "checkstack": {
package/src/Dashboard.tsx CHANGED
@@ -50,6 +50,8 @@ import {
50
50
  import { authApiRef } from "@checkstack/auth-frontend/api";
51
51
  import { QueueLagAlert } from "@checkstack/queue-frontend";
52
52
  import { SystemBadgeDataProvider } from "./components/SystemBadgeDataProvider";
53
+ import { IncidentOverviewSheet } from "./components/IncidentOverviewSheet";
54
+ import { MaintenanceOverviewSheet } from "./components/MaintenanceOverviewSheet";
53
55
 
54
56
  const CATALOG_PLUGIN_ID = "catalog";
55
57
  const MAX_TERMINAL_ENTRIES = 8;
@@ -99,6 +101,9 @@ export const Dashboard: React.FC = () => {
99
101
  Record<string, boolean>
100
102
  >({});
101
103
 
104
+ const [isIncidentSheetOpen, setIncidentSheetOpen] = useState(false);
105
+ const [isMaintenanceSheetOpen, setMaintenanceSheetOpen] = useState(false);
106
+
102
107
  // -------------------------------------------------------------------------
103
108
  // DATA QUERIES
104
109
  // -------------------------------------------------------------------------
@@ -117,12 +122,29 @@ export const Dashboard: React.FC = () => {
117
122
  const incidents = incidentsData?.incidents ?? [];
118
123
 
119
124
  // Fetch active maintenances
120
- const { data: maintenancesData, isLoading: maintenancesLoading } =
125
+ const { data: inProgressMaintenancesData, isLoading: inProgressLoading } =
121
126
  maintenanceClient.listMaintenances.useQuery(
122
127
  { status: "in_progress" },
123
128
  { staleTime: 30_000 },
124
129
  );
125
- const maintenances = maintenancesData?.maintenances ?? [];
130
+
131
+ // Fetch scheduled maintenances
132
+ const { data: scheduledMaintenancesData, isLoading: scheduledLoading } =
133
+ maintenanceClient.listMaintenances.useQuery(
134
+ { status: "scheduled" },
135
+ { staleTime: 30_000 },
136
+ );
137
+
138
+ const maintenances = useMemo(() => {
139
+ return [
140
+ ...(inProgressMaintenancesData?.maintenances ?? []),
141
+ ...(scheduledMaintenancesData?.maintenances ?? []),
142
+ ].toSorted(
143
+ (a, b) => new Date(a.startAt).getTime() - new Date(b.startAt).getTime(),
144
+ );
145
+ }, [inProgressMaintenancesData, scheduledMaintenancesData]);
146
+
147
+ const maintenancesLoading = inProgressLoading || scheduledLoading;
126
148
 
127
149
  // Fetch subscriptions (only when logged in)
128
150
  const { data: subscriptions = [], refetch: refetchSubscriptions } =
@@ -294,7 +316,7 @@ export const Dashboard: React.FC = () => {
294
316
  )}
295
317
  </div>
296
318
  </CardHeader>
297
- <CardContent className="p-4">
319
+ <CardContent className="p-0">
298
320
  {group.systems.length === 0 ? (
299
321
  <div className="py-8 text-center">
300
322
  <p className="text-sm text-muted-foreground">
@@ -302,32 +324,26 @@ export const Dashboard: React.FC = () => {
302
324
  </p>
303
325
  </div>
304
326
  ) : (
305
- <div
306
- className={`grid gap-3 ${
307
- group.systems.length === 1
308
- ? "grid-cols-1"
309
- : "grid-cols-1 sm:grid-cols-2"
310
- }`}
311
- >
327
+ <div className="flex flex-col divide-y divide-border">
312
328
  {group.systems.map((system) => (
313
329
  <button
314
330
  key={system.id}
315
331
  onClick={() => handleSystemClick(system.id)}
316
- className="flex items-center 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"
332
+ className="flex items-center gap-4 px-4 py-3 bg-card hover:bg-muted/50 transition-colors text-left w-full group"
317
333
  >
318
- <div className="flex items-center gap-3 min-w-24 flex-shrink-0">
334
+ <div className="flex items-center gap-3 min-w-32 flex-shrink-0">
319
335
  <Activity className="h-4 w-4 text-muted-foreground flex-shrink-0" />
320
336
  <p className="text-sm font-medium text-foreground truncate">
321
337
  {system.name}
322
338
  </p>
323
339
  </div>
324
- <div className="flex items-center gap-2 flex-wrap flex-1 justify-end">
340
+ <div className="flex items-center gap-2 flex-wrap flex-1 justify-end min-w-0">
325
341
  <ExtensionSlot
326
342
  slot={SystemStateBadgesSlot}
327
343
  context={{ system }}
328
344
  />
329
345
  </div>
330
- <ChevronRight className="h-4 w-4 text-muted-foreground flex-shrink-0" />
346
+ <ChevronRight className="h-4 w-4 text-muted-foreground opacity-50 group-hover:opacity-100 transition-opacity flex-shrink-0 ml-2" />
331
347
  </button>
332
348
  ))}
333
349
  </div>
@@ -381,10 +397,21 @@ export const Dashboard: React.FC = () => {
381
397
  : "Unresolved issues requiring attention"
382
398
  }
383
399
  icon={<AlertTriangle className="w-4 h-4" />}
400
+ onClick={
401
+ activeIncidentsCount > 0
402
+ ? () => setIncidentSheetOpen(true)
403
+ : undefined
404
+ }
405
+ className={
406
+ activeIncidentsCount > 0
407
+ ? "cursor-pointer hover:opacity-90 hover:scale-[1.02]"
408
+ : ""
409
+ }
384
410
  />
385
411
 
386
412
  <StatusCard
387
- title="Active Maintenances"
413
+ variant={activeMaintenancesCount > 0 ? "gradient" : "default"}
414
+ title="Active & Scheduled Maintenances"
388
415
  value={
389
416
  loading ? (
390
417
  "..."
@@ -395,9 +422,19 @@ export const Dashboard: React.FC = () => {
395
422
  description={
396
423
  activeMaintenancesCount === 0
397
424
  ? "No scheduled maintenance"
398
- : "Ongoing or scheduled maintenance windows"
425
+ : "Ongoing or upcoming maintenance windows"
399
426
  }
400
427
  icon={<Wrench className="w-4 h-4" />}
428
+ onClick={
429
+ activeMaintenancesCount > 0
430
+ ? () => setMaintenanceSheetOpen(true)
431
+ : undefined
432
+ }
433
+ className={
434
+ activeMaintenancesCount > 0
435
+ ? "cursor-pointer hover:opacity-90 hover:scale-[1.02]"
436
+ : ""
437
+ }
401
438
  />
402
439
  </div>
403
440
  </section>
@@ -428,6 +465,19 @@ export const Dashboard: React.FC = () => {
428
465
  </section>
429
466
  </div>
430
467
  </div>
468
+
469
+ <IncidentOverviewSheet
470
+ open={isIncidentSheetOpen}
471
+ onOpenChange={setIncidentSheetOpen}
472
+ incidents={incidents}
473
+ systems={systems}
474
+ />
475
+ <MaintenanceOverviewSheet
476
+ open={isMaintenanceSheetOpen}
477
+ onOpenChange={setMaintenanceSheetOpen}
478
+ maintenances={maintenances}
479
+ systems={systems}
480
+ />
431
481
  </>
432
482
  );
433
483
  };
@@ -0,0 +1,107 @@
1
+ import React from "react";
2
+ import {
3
+ Sheet,
4
+ SheetContent,
5
+ SheetHeader,
6
+ SheetTitle,
7
+ SheetBody,
8
+ Button,
9
+ Badge,
10
+ } from "@checkstack/ui";
11
+ import { Link } from "react-router-dom";
12
+ import { usePluginRoute, useApi, accessApiRef } from "@checkstack/frontend-api";
13
+ import {
14
+ incidentRoutes,
15
+ incidentAccess,
16
+ type IncidentWithSystems,
17
+ } from "@checkstack/incident-common";
18
+ import { resolveRoute } from "@checkstack/common";
19
+ import type { System } from "@checkstack/catalog-common";
20
+
21
+ interface Props {
22
+ open: boolean;
23
+ onOpenChange: (open: boolean) => void;
24
+ incidents: IncidentWithSystems[];
25
+ systems: System[];
26
+ }
27
+
28
+ export const IncidentOverviewSheet: React.FC<Props> = ({
29
+ open,
30
+ onOpenChange,
31
+ incidents,
32
+ systems,
33
+ }) => {
34
+ const getRoute = usePluginRoute();
35
+ const accessApi = useApi(accessApiRef);
36
+ const { allowed: canManage } = accessApi.useAccess(incidentAccess.incident.manage);
37
+
38
+ // Map of systemId -> systemName
39
+ const systemMap = new Map(systems.map((s) => [s.id, s.name]));
40
+
41
+ return (
42
+ <Sheet open={open} onOpenChange={onOpenChange}>
43
+ <SheetContent>
44
+ <SheetHeader className="flex flex-row items-start justify-between gap-4 pt-6">
45
+ <div className="flex flex-col gap-1 text-left">
46
+ <SheetTitle>Active Incidents</SheetTitle>
47
+ <p className="text-sm text-muted-foreground">
48
+ Overview of unresolved issues
49
+ </p>
50
+ </div>
51
+ {canManage && (
52
+ <Button variant="outline" size="sm" asChild>
53
+ <Link to={getRoute(incidentRoutes.routes.config)}>Manage</Link>
54
+ </Button>
55
+ )}
56
+ </SheetHeader>
57
+
58
+ <SheetBody className="flex flex-col gap-3 pb-8">
59
+ {incidents.length === 0 ? (
60
+ <p className="text-sm text-muted-foreground text-center py-8">
61
+ No active incidents
62
+ </p>
63
+ ) : (
64
+ incidents.map((incident) => {
65
+ const affectedSystemNames = incident.systemIds
66
+ .map((id) => systemMap.get(id) || id)
67
+ .join(", ");
68
+
69
+ const variant =
70
+ incident.severity === "critical"
71
+ ? "destructive"
72
+ : incident.severity === "major"
73
+ ? "warning"
74
+ : "info";
75
+
76
+ return (
77
+ <Link
78
+ key={incident.id}
79
+ to={resolveRoute(incidentRoutes.routes.detail, { incidentId: incident.id })}
80
+ onClick={() => onOpenChange(false)}
81
+ className="flex flex-col gap-2 rounded-lg border border-border bg-card p-4 hover:border-primary/50 hover:shadow-sm transition-all text-left"
82
+ >
83
+ <div className="flex items-start justify-between gap-4">
84
+ <h4 className="font-medium text-foreground">
85
+ {incident.title}
86
+ </h4>
87
+ <Badge variant={variant} className="capitalize flex-shrink-0">
88
+ {incident.severity}
89
+ </Badge>
90
+ </div>
91
+ <div className="flex flex-col gap-1 mt-2">
92
+ <span className="text-[10px] font-semibold text-muted-foreground uppercase tracking-wider">
93
+ Affected Systems
94
+ </span>
95
+ <span className="text-sm text-foreground">
96
+ {affectedSystemNames || "None"}
97
+ </span>
98
+ </div>
99
+ </Link>
100
+ );
101
+ })
102
+ )}
103
+ </SheetBody>
104
+ </SheetContent>
105
+ </Sheet>
106
+ );
107
+ };
@@ -0,0 +1,128 @@
1
+ import React from "react";
2
+ import {
3
+ Sheet,
4
+ SheetContent,
5
+ SheetHeader,
6
+ SheetTitle,
7
+ SheetBody,
8
+ Button,
9
+ Badge,
10
+ } from "@checkstack/ui";
11
+ import { Link } from "react-router-dom";
12
+ import { usePluginRoute, useApi, accessApiRef } from "@checkstack/frontend-api";
13
+ import {
14
+ maintenanceRoutes,
15
+ maintenanceAccess,
16
+ type MaintenanceWithSystems,
17
+ } from "@checkstack/maintenance-common";
18
+ import { resolveRoute } from "@checkstack/common";
19
+ import type { System } from "@checkstack/catalog-common";
20
+
21
+ interface Props {
22
+ open: boolean;
23
+ onOpenChange: (open: boolean) => void;
24
+ maintenances: MaintenanceWithSystems[];
25
+ systems: System[];
26
+ }
27
+
28
+ export const MaintenanceOverviewSheet: React.FC<Props> = ({
29
+ open,
30
+ onOpenChange,
31
+ maintenances,
32
+ systems,
33
+ }) => {
34
+ const getRoute = usePluginRoute();
35
+ const accessApi = useApi(accessApiRef);
36
+ const { allowed: canManage } = accessApi.useAccess(maintenanceAccess.maintenance.manage);
37
+
38
+ const systemMap = new Map(systems.map((s) => [s.id, s.name]));
39
+
40
+ return (
41
+ <Sheet open={open} onOpenChange={onOpenChange}>
42
+ <SheetContent>
43
+ <SheetHeader className="flex flex-row items-start justify-between gap-4 pt-6">
44
+ <div className="flex flex-col gap-1 text-left">
45
+ <SheetTitle>Active & Scheduled Maintenances</SheetTitle>
46
+ <p className="text-sm text-muted-foreground">
47
+ Overview of scheduled and ongoing windows
48
+ </p>
49
+ </div>
50
+ {canManage && (
51
+ <Button variant="outline" size="sm" asChild>
52
+ <Link to={getRoute(maintenanceRoutes.routes.config)}>Manage</Link>
53
+ </Button>
54
+ )}
55
+ </SheetHeader>
56
+
57
+ <SheetBody className="flex flex-col gap-3 pb-8">
58
+ {maintenances.length === 0 ? (
59
+ <p className="text-sm text-muted-foreground text-center py-8">
60
+ No active maintenances
61
+ </p>
62
+ ) : (
63
+ maintenances.map((maintenance) => {
64
+ const affectedSystemNames = maintenance.systemIds
65
+ .map((id) => systemMap.get(id) || id)
66
+ .join(", ");
67
+
68
+ const badgeVariant =
69
+ maintenance.status === "in_progress"
70
+ ? "warning"
71
+ : maintenance.status === "scheduled"
72
+ ? "info"
73
+ : maintenance.status === "completed"
74
+ ? "success"
75
+ : "secondary";
76
+
77
+ return (
78
+ <Link
79
+ key={maintenance.id}
80
+ to={resolveRoute(maintenanceRoutes.routes.detail, { maintenanceId: maintenance.id })}
81
+ onClick={() => onOpenChange(false)}
82
+ className="flex flex-col gap-2 rounded-lg border border-border bg-card p-4 hover:border-primary/50 hover:shadow-sm transition-all text-left"
83
+ >
84
+ <div className="flex items-start justify-between gap-4">
85
+ <h4 className="font-medium text-foreground">
86
+ {maintenance.title}
87
+ </h4>
88
+ <Badge variant={badgeVariant} className="capitalize flex-shrink-0">
89
+ {maintenance.status.replace("_", " ")}
90
+ </Badge>
91
+ </div>
92
+ <div className="flex flex-col gap-1 mt-2">
93
+ <span className="text-[10px] font-semibold text-muted-foreground uppercase tracking-wider">
94
+ Affected Systems
95
+ </span>
96
+ <span className="text-sm text-foreground">
97
+ {affectedSystemNames || "None"}
98
+ </span>
99
+ </div>
100
+ <div className="flex flex-col gap-1 mt-2">
101
+ <span className="text-[10px] font-semibold text-muted-foreground uppercase tracking-wider">
102
+ Schedule
103
+ </span>
104
+ <span className="text-sm text-foreground">
105
+ {new Date(maintenance.startAt).toLocaleString(undefined, {
106
+ month: "short",
107
+ day: "numeric",
108
+ hour: "numeric",
109
+ minute: "2-digit",
110
+ })}
111
+ {" - "}
112
+ {new Date(maintenance.endAt).toLocaleString(undefined, {
113
+ month: "short",
114
+ day: "numeric",
115
+ hour: "numeric",
116
+ minute: "2-digit",
117
+ })}
118
+ </span>
119
+ </div>
120
+ </Link>
121
+ );
122
+ })
123
+ )}
124
+ </SheetBody>
125
+ </SheetContent>
126
+ </Sheet>
127
+ );
128
+ };