@checkstack/dashboard-frontend 0.3.35 → 0.4.1
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 +28 -0
- package/package.json +1 -1
- package/src/Dashboard.tsx +66 -16
- package/src/components/IncidentOverviewSheet.tsx +107 -0
- package/src/components/MaintenanceOverviewSheet.tsx +128 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,33 @@
|
|
|
1
1
|
# @checkstack/dashboard-frontend
|
|
2
2
|
|
|
3
|
+
## 0.4.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Updated dependencies [80cbc51]
|
|
8
|
+
- @checkstack/catalog-frontend@0.8.0
|
|
9
|
+
|
|
10
|
+
## 0.4.0
|
|
11
|
+
|
|
12
|
+
### Minor Changes
|
|
13
|
+
|
|
14
|
+
- bb1fea0: feat: implement active incident and maintenance overview sheets on dashboard
|
|
15
|
+
|
|
16
|
+
- Replaces direct routing on status cards with slide-out overview sheets to gracefully degrade for users without manage permissions
|
|
17
|
+
- Refactors dashboard system groups into a clean table-style list layout for better density
|
|
18
|
+
- Makes global status cards more compact
|
|
19
|
+
|
|
20
|
+
### Patch Changes
|
|
21
|
+
|
|
22
|
+
- Updated dependencies [bb1fea0]
|
|
23
|
+
- Updated dependencies [bb1fea0]
|
|
24
|
+
- @checkstack/ui@1.4.0
|
|
25
|
+
- @checkstack/catalog-common@1.4.0
|
|
26
|
+
- @checkstack/catalog-frontend@0.7.0
|
|
27
|
+
- @checkstack/auth-frontend@0.5.26
|
|
28
|
+
- @checkstack/command-frontend@0.2.28
|
|
29
|
+
- @checkstack/queue-frontend@0.2.28
|
|
30
|
+
|
|
3
31
|
## 0.3.35
|
|
4
32
|
|
|
5
33
|
### Patch Changes
|
package/package.json
CHANGED
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:
|
|
125
|
+
const { data: inProgressMaintenancesData, isLoading: inProgressLoading } =
|
|
121
126
|
maintenanceClient.listMaintenances.useQuery(
|
|
122
127
|
{ status: "in_progress" },
|
|
123
128
|
{ staleTime: 30_000 },
|
|
124
129
|
);
|
|
125
|
-
|
|
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-
|
|
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-
|
|
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-
|
|
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
|
-
|
|
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
|
|
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
|
+
};
|