@checkstack/dashboard-frontend 0.7.8 → 0.8.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 +68 -0
- package/package.json +12 -16
- package/src/Dashboard.tsx +182 -388
- package/src/components/DashboardAllClear.tsx +40 -0
- package/src/components/FleetHealthHeader.tsx +100 -0
- package/src/components/ProblemSystemCard.tsx +164 -0
- package/src/index.tsx +6 -2
- package/src/logic/systemSignals.test.ts +110 -0
- package/src/logic/systemSignals.ts +106 -0
- package/tsconfig.json +0 -12
- package/src/components/AnomalyOverviewSheet.tsx +0 -98
- package/src/components/IncidentOverviewSheet.tsx +0 -107
- package/src/components/MaintenanceOverviewSheet.tsx +0 -128
package/src/Dashboard.tsx
CHANGED
|
@@ -1,38 +1,23 @@
|
|
|
1
|
-
import React, {
|
|
2
|
-
import { useNavigate } from "react-router-dom";
|
|
3
|
-
import {
|
|
4
|
-
useApi,
|
|
5
|
-
usePluginClient,
|
|
6
|
-
ExtensionSlot,
|
|
7
|
-
} from "@checkstack/frontend-api";
|
|
1
|
+
import React, { useCallback, useMemo, useState } from "react";
|
|
2
|
+
import { Link, useNavigate } from "react-router-dom";
|
|
3
|
+
import { usePluginClient, ExtensionSlot } from "@checkstack/frontend-api";
|
|
8
4
|
import {
|
|
9
5
|
CatalogApi,
|
|
10
6
|
catalogRoutes,
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
catalogGroupTarget,
|
|
7
|
+
SystemSignalsSlot,
|
|
8
|
+
type SystemSignalsMap,
|
|
9
|
+
type SystemSignalTone,
|
|
15
10
|
} from "@checkstack/catalog-common";
|
|
16
11
|
import { resolveRoute } from "@checkstack/common";
|
|
17
12
|
import { TipBanner } from "@checkstack/tips-frontend";
|
|
18
13
|
import { pluginMetadata as dashboardTipMetadata } from "./pluginMetadata";
|
|
19
|
-
import { NotificationSubscriptionsManager } from "@checkstack/notification-frontend";
|
|
20
|
-
import { IncidentApi } from "@checkstack/incident-common";
|
|
21
|
-
import { MaintenanceApi } from "@checkstack/maintenance-common";
|
|
22
|
-
import { AnomalyApi } from "@checkstack/anomaly-common";
|
|
23
14
|
import { HEALTH_CHECK_RUN_COMPLETED } from "@checkstack/healthcheck-common";
|
|
24
15
|
import { useSignal } from "@checkstack/signal-frontend";
|
|
25
16
|
import {
|
|
26
|
-
Card,
|
|
27
|
-
CardHeader,
|
|
28
|
-
CardTitle,
|
|
29
|
-
CardContent,
|
|
30
17
|
SectionHeader,
|
|
31
|
-
StatusCard,
|
|
32
18
|
EmptyState,
|
|
33
19
|
Button,
|
|
34
20
|
LoadingSpinner,
|
|
35
|
-
AnimatedCounter,
|
|
36
21
|
TerminalFeed,
|
|
37
22
|
type TerminalEntry,
|
|
38
23
|
usePerformance,
|
|
@@ -42,26 +27,23 @@ import {
|
|
|
42
27
|
LayoutGrid,
|
|
43
28
|
Server,
|
|
44
29
|
Activity,
|
|
45
|
-
ChevronRight,
|
|
46
|
-
AlertTriangle,
|
|
47
|
-
Wrench,
|
|
48
30
|
Terminal,
|
|
49
|
-
ActivitySquare,
|
|
50
31
|
Lightbulb,
|
|
32
|
+
ArrowRight,
|
|
51
33
|
} from "lucide-react";
|
|
52
|
-
import { authApiRef } from "@checkstack/auth-frontend/api";
|
|
53
34
|
import { QueueLagAlert } from "@checkstack/queue-frontend";
|
|
54
|
-
import {
|
|
55
|
-
import {
|
|
56
|
-
import {
|
|
57
|
-
import {
|
|
35
|
+
import { FleetHealthHeader } from "./components/FleetHealthHeader";
|
|
36
|
+
import { ProblemSystemCard } from "./components/ProblemSystemCard";
|
|
37
|
+
import { DashboardAllClear } from "./components/DashboardAllClear";
|
|
38
|
+
import {
|
|
39
|
+
buildProblemSystems,
|
|
40
|
+
countByTone,
|
|
41
|
+
mergeSignalsBySystem,
|
|
42
|
+
type SignalsBySource,
|
|
43
|
+
} from "./logic/systemSignals";
|
|
58
44
|
|
|
59
45
|
const MAX_TERMINAL_ENTRIES = 8;
|
|
60
46
|
|
|
61
|
-
interface GroupWithSystems extends Group {
|
|
62
|
-
systems: System[];
|
|
63
|
-
}
|
|
64
|
-
|
|
65
47
|
const statusToVariant = (
|
|
66
48
|
status: string,
|
|
67
49
|
): "default" | "success" | "warning" | "error" => {
|
|
@@ -84,103 +66,59 @@ const statusToVariant = (
|
|
|
84
66
|
export const Dashboard: React.FC = () => {
|
|
85
67
|
const { isLowPower } = usePerformance();
|
|
86
68
|
const catalogClient = usePluginClient(CatalogApi);
|
|
87
|
-
const incidentClient = usePluginClient(IncidentApi);
|
|
88
|
-
const maintenanceClient = usePluginClient(MaintenanceApi);
|
|
89
|
-
const anomalyClient = usePluginClient(AnomalyApi);
|
|
90
|
-
|
|
91
69
|
const navigate = useNavigate();
|
|
92
|
-
const authApi = useApi(authApiRef);
|
|
93
|
-
const { data: session } = authApi.useSession();
|
|
94
70
|
|
|
95
|
-
// Terminal feed entries from real healthcheck signals
|
|
96
71
|
const [terminalEntries, setTerminalEntries] = useState<TerminalEntry[]>([]);
|
|
72
|
+
const [activeTone, setActiveTone] = useState<SystemSignalTone | null>(null);
|
|
97
73
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
// -------------------------------------------------------------------------
|
|
103
|
-
// DATA QUERIES
|
|
104
|
-
// -------------------------------------------------------------------------
|
|
74
|
+
// ----------------------------------------------------------------------- //
|
|
75
|
+
// DATA
|
|
76
|
+
// ----------------------------------------------------------------------- //
|
|
105
77
|
|
|
106
|
-
// Fetch entities from catalog (groups and systems in one call)
|
|
107
78
|
const { data: entitiesData, isLoading: entitiesLoading } =
|
|
108
79
|
catalogClient.getEntities.useQuery({}, { staleTime: 30_000 });
|
|
109
|
-
const systems = entitiesData?.systems ?? [];
|
|
110
|
-
|
|
111
|
-
// Fetch active incidents
|
|
112
|
-
const { data: incidentsData, isLoading: incidentsLoading } =
|
|
113
|
-
incidentClient.listIncidents.useQuery(
|
|
114
|
-
{ includeResolved: false },
|
|
115
|
-
{ staleTime: 30_000 },
|
|
116
|
-
);
|
|
117
|
-
const incidents = incidentsData?.incidents ?? [];
|
|
118
|
-
|
|
119
|
-
// Fetch active maintenances
|
|
120
|
-
const { data: inProgressMaintenancesData, isLoading: inProgressLoading } =
|
|
121
|
-
maintenanceClient.listMaintenances.useQuery(
|
|
122
|
-
{ status: "in_progress" },
|
|
123
|
-
{ staleTime: 30_000 },
|
|
124
|
-
);
|
|
125
|
-
|
|
126
|
-
// Fetch scheduled maintenances
|
|
127
|
-
const { data: scheduledMaintenancesData, isLoading: scheduledLoading } =
|
|
128
|
-
maintenanceClient.listMaintenances.useQuery(
|
|
129
|
-
{ status: "scheduled" },
|
|
130
|
-
{ staleTime: 30_000 },
|
|
131
|
-
);
|
|
132
|
-
|
|
133
|
-
const maintenances = useMemo(() => {
|
|
134
|
-
return [
|
|
135
|
-
...(inProgressMaintenancesData?.maintenances ?? []),
|
|
136
|
-
...(scheduledMaintenancesData?.maintenances ?? []),
|
|
137
|
-
].toSorted(
|
|
138
|
-
(a, b) => new Date(a.startAt).getTime() - new Date(b.startAt).getTime(),
|
|
139
|
-
);
|
|
140
|
-
}, [inProgressMaintenancesData, scheduledMaintenancesData]);
|
|
141
80
|
|
|
142
|
-
const
|
|
143
|
-
|
|
144
|
-
//
|
|
145
|
-
const
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
81
|
+
const systems = useMemo(() => entitiesData?.systems ?? [], [entitiesData]);
|
|
82
|
+
// Stable across renders (derives from the query's stable data ref) so the
|
|
83
|
+
// signal fillers don't re-report in a loop.
|
|
84
|
+
const systemIds = useMemo(() => systems.map((s) => s.id), [systems]);
|
|
85
|
+
const systemMap = useMemo(
|
|
86
|
+
() => new Map(systems.map((s) => [s.id, s])),
|
|
87
|
+
[systems],
|
|
88
|
+
);
|
|
150
89
|
|
|
151
|
-
//
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
incidentsLoading ||
|
|
155
|
-
maintenancesLoading ||
|
|
156
|
-
anomaliesLoading;
|
|
90
|
+
// ----------------------------------------------------------------------- //
|
|
91
|
+
// SIGNAL AGGREGATION (extensible: any plugin fills SystemSignalsSlot)
|
|
92
|
+
// ----------------------------------------------------------------------- //
|
|
157
93
|
|
|
158
|
-
|
|
159
|
-
// COMPUTED DATA
|
|
160
|
-
// -------------------------------------------------------------------------
|
|
94
|
+
const [signalsBySource, setSignalsBySource] = useState<SignalsBySource>({});
|
|
161
95
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
96
|
+
const handleSignals = useCallback(
|
|
97
|
+
(sourceId: string, signals: SystemSignalsMap) => {
|
|
98
|
+
setSignalsBySource((prev) =>
|
|
99
|
+
prev[sourceId] === signals ? prev : { ...prev, [sourceId]: signals },
|
|
100
|
+
);
|
|
101
|
+
},
|
|
102
|
+
[],
|
|
103
|
+
);
|
|
167
104
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
105
|
+
const problems = useMemo(
|
|
106
|
+
() => buildProblemSystems(mergeSignalsBySystem(signalsBySource)),
|
|
107
|
+
[signalsBySource],
|
|
108
|
+
);
|
|
109
|
+
const counts = useMemo(() => countByTone(problems), [problems]);
|
|
110
|
+
|
|
111
|
+
const visibleProblems = useMemo(
|
|
112
|
+
() =>
|
|
113
|
+
activeTone
|
|
114
|
+
? problems.filter((p) => p.worstTone === activeTone)
|
|
115
|
+
: problems,
|
|
116
|
+
[problems, activeTone],
|
|
117
|
+
);
|
|
180
118
|
|
|
181
|
-
//
|
|
182
|
-
//
|
|
183
|
-
//
|
|
119
|
+
// ----------------------------------------------------------------------- //
|
|
120
|
+
// SIGNALS
|
|
121
|
+
// ----------------------------------------------------------------------- //
|
|
184
122
|
|
|
185
123
|
useSignal(
|
|
186
124
|
HEALTH_CHECK_RUN_COMPLETED,
|
|
@@ -192,43 +130,40 @@ export const Dashboard: React.FC = () => {
|
|
|
192
130
|
variant: statusToVariant(status),
|
|
193
131
|
suffix: latencyMs === undefined ? undefined : `${latencyMs}ms`,
|
|
194
132
|
};
|
|
195
|
-
|
|
196
133
|
setTerminalEntries((prev) =>
|
|
197
134
|
[newEntry, ...prev].slice(0, MAX_TERMINAL_ENTRIES),
|
|
198
135
|
);
|
|
199
136
|
},
|
|
200
137
|
);
|
|
201
138
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
const handleSystemClick = (systemId: string) => {
|
|
207
|
-
navigate(resolveRoute(catalogRoutes.routes.systemDetail, { systemId }));
|
|
208
|
-
};
|
|
139
|
+
const systemsCount = systems.length;
|
|
140
|
+
const healthyCount = Math.max(systemsCount - problems.length, 0);
|
|
141
|
+
const catalogHref = resolveRoute(catalogRoutes.routes.home);
|
|
209
142
|
|
|
210
|
-
//
|
|
143
|
+
// ----------------------------------------------------------------------- //
|
|
211
144
|
// RENDER
|
|
212
|
-
//
|
|
145
|
+
// ----------------------------------------------------------------------- //
|
|
213
146
|
|
|
214
|
-
const
|
|
215
|
-
if (
|
|
147
|
+
const renderOverview = () => {
|
|
148
|
+
if (entitiesLoading) {
|
|
216
149
|
return <LoadingSpinner />;
|
|
217
150
|
}
|
|
218
151
|
|
|
219
|
-
if (
|
|
152
|
+
if (systemsCount === 0) {
|
|
220
153
|
return (
|
|
221
154
|
<EmptyState
|
|
222
155
|
icon={<Server className="w-12 h-12" />}
|
|
223
156
|
title="Nothing to show on the dashboard yet"
|
|
224
|
-
description="
|
|
157
|
+
description="Add the systems you want to monitor and attach health checks. Anything that needs attention will surface here automatically."
|
|
225
158
|
steps={[
|
|
226
159
|
"Open the Catalog and add the systems you want to monitor.",
|
|
227
160
|
"Group related systems together (e.g. one group per team).",
|
|
228
|
-
"Attach health checks
|
|
161
|
+
"Attach health checks so the dashboard surfaces issues the moment they appear.",
|
|
229
162
|
]}
|
|
230
163
|
actions={
|
|
231
|
-
<Button
|
|
164
|
+
<Button
|
|
165
|
+
onClick={() => navigate(resolveRoute(catalogRoutes.routes.config))}
|
|
166
|
+
>
|
|
232
167
|
<LayoutGrid className="w-4 h-4 mr-2" />
|
|
233
168
|
Open Catalog
|
|
234
169
|
</Button>
|
|
@@ -237,271 +172,130 @@ export const Dashboard: React.FC = () => {
|
|
|
237
172
|
);
|
|
238
173
|
}
|
|
239
174
|
|
|
240
|
-
// Collect all system IDs for bulk data fetching
|
|
241
|
-
const allSystemIds = groupsWithSystems.flatMap((g) =>
|
|
242
|
-
g.systems.map((s) => s.id),
|
|
243
|
-
);
|
|
244
|
-
|
|
245
175
|
return (
|
|
246
|
-
<
|
|
247
|
-
<
|
|
248
|
-
{
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
{group.systems.length === 0 ? (
|
|
279
|
-
<div className="py-8 text-center">
|
|
280
|
-
<p className="text-sm text-muted-foreground">
|
|
281
|
-
No systems in this group yet
|
|
282
|
-
</p>
|
|
283
|
-
</div>
|
|
284
|
-
) : (
|
|
285
|
-
<div className="flex flex-col divide-y divide-border">
|
|
286
|
-
{group.systems.map((system) => (
|
|
287
|
-
<button
|
|
288
|
-
key={system.id}
|
|
289
|
-
onClick={() => handleSystemClick(system.id)}
|
|
290
|
-
className="flex items-center gap-4 px-4 py-3 bg-card hover:bg-muted/50 transition-colors text-left w-full group"
|
|
291
|
-
>
|
|
292
|
-
<div className="flex items-center gap-3 min-w-32 flex-shrink-0">
|
|
293
|
-
<Activity className="h-4 w-4 text-muted-foreground flex-shrink-0" />
|
|
294
|
-
<p className="text-sm font-medium text-foreground truncate">
|
|
295
|
-
{system.name}
|
|
296
|
-
</p>
|
|
297
|
-
</div>
|
|
298
|
-
<div className="flex items-center gap-2 flex-wrap flex-1 justify-end min-w-0">
|
|
299
|
-
<ExtensionSlot
|
|
300
|
-
slot={SystemStateBadgesSlot}
|
|
301
|
-
context={{ system }}
|
|
302
|
-
/>
|
|
303
|
-
</div>
|
|
304
|
-
<ChevronRight
|
|
305
|
-
className={cn(
|
|
306
|
-
"h-4 w-4 text-muted-foreground opacity-50 group-hover:opacity-100 flex-shrink-0 ml-2",
|
|
307
|
-
!isLowPower && "transition-opacity",
|
|
308
|
-
)}
|
|
309
|
-
/>
|
|
310
|
-
</button>
|
|
311
|
-
))}
|
|
312
|
-
</div>
|
|
313
|
-
)}
|
|
314
|
-
</CardContent>
|
|
315
|
-
</Card>
|
|
316
|
-
))}
|
|
317
|
-
</div>
|
|
318
|
-
</SystemBadgeDataProvider>
|
|
176
|
+
<div className="space-y-4">
|
|
177
|
+
<FleetHealthHeader
|
|
178
|
+
systemsCount={systemsCount}
|
|
179
|
+
problemsCount={problems.length}
|
|
180
|
+
counts={counts}
|
|
181
|
+
healthyCount={healthyCount}
|
|
182
|
+
activeTone={activeTone}
|
|
183
|
+
onToneSelect={setActiveTone}
|
|
184
|
+
isLowPower={isLowPower}
|
|
185
|
+
/>
|
|
186
|
+
{problems.length === 0 ? (
|
|
187
|
+
<DashboardAllClear
|
|
188
|
+
systemsCount={systemsCount}
|
|
189
|
+
isLowPower={isLowPower}
|
|
190
|
+
/>
|
|
191
|
+
) : (
|
|
192
|
+
<div className="grid gap-4 md:grid-cols-2">
|
|
193
|
+
{visibleProblems.map((problem) => {
|
|
194
|
+
const system = systemMap.get(problem.systemId);
|
|
195
|
+
if (!system) return null;
|
|
196
|
+
return (
|
|
197
|
+
<ProblemSystemCard
|
|
198
|
+
key={problem.systemId}
|
|
199
|
+
system={system}
|
|
200
|
+
problem={problem}
|
|
201
|
+
isLowPower={isLowPower}
|
|
202
|
+
/>
|
|
203
|
+
);
|
|
204
|
+
})}
|
|
205
|
+
</div>
|
|
206
|
+
)}
|
|
207
|
+
</div>
|
|
319
208
|
);
|
|
320
209
|
};
|
|
321
210
|
|
|
322
211
|
return (
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
<Lightbulb
|
|
353
|
-
className="size-3.5 shrink-0 text-amber-500"
|
|
354
|
-
aria-hidden="true"
|
|
355
|
-
/>
|
|
356
|
-
See a small lightbulb next to a button or control? Click it
|
|
357
|
-
for a short explanation of what that feature does.
|
|
358
|
-
</span>
|
|
359
|
-
}
|
|
360
|
-
/>
|
|
361
|
-
|
|
362
|
-
{/* Overview Section */}
|
|
363
|
-
<section>
|
|
364
|
-
<SectionHeader
|
|
365
|
-
title="Overview"
|
|
366
|
-
icon={<Activity className="w-5 h-5" />}
|
|
367
|
-
/>
|
|
368
|
-
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-4">
|
|
369
|
-
<StatusCard
|
|
370
|
-
title="Total Systems"
|
|
371
|
-
value={loading ? "..." : <AnimatedCounter value={systemsCount} />}
|
|
372
|
-
description="Monitored systems in your catalog"
|
|
373
|
-
icon={<Server className="w-4 h-4" />}
|
|
374
|
-
/>
|
|
375
|
-
|
|
376
|
-
<StatusCard
|
|
377
|
-
variant={activeIncidentsCount > 0 ? "gradient" : "default"}
|
|
378
|
-
title="Active Incidents"
|
|
379
|
-
value={
|
|
380
|
-
loading ? (
|
|
381
|
-
"..."
|
|
382
|
-
) : (
|
|
383
|
-
<AnimatedCounter value={activeIncidentsCount} />
|
|
384
|
-
)
|
|
385
|
-
}
|
|
386
|
-
description={
|
|
387
|
-
activeIncidentsCount === 0
|
|
388
|
-
? "All systems operating normally"
|
|
389
|
-
: "Unresolved issues requiring attention"
|
|
390
|
-
}
|
|
391
|
-
icon={<AlertTriangle className="w-4 h-4" />}
|
|
392
|
-
onClick={
|
|
393
|
-
activeIncidentsCount > 0
|
|
394
|
-
? () => setIncidentSheetOpen(true)
|
|
395
|
-
: undefined
|
|
396
|
-
}
|
|
397
|
-
className={cn(
|
|
398
|
-
activeIncidentsCount > 0 && "cursor-pointer hover:opacity-90",
|
|
399
|
-
activeIncidentsCount > 0 && !isLowPower && "hover:scale-[1.02]",
|
|
400
|
-
)}
|
|
212
|
+
<div
|
|
213
|
+
className={cn(
|
|
214
|
+
"space-y-8",
|
|
215
|
+
!isLowPower && "animate-in fade-in duration-500",
|
|
216
|
+
)}
|
|
217
|
+
>
|
|
218
|
+
<QueueLagAlert />
|
|
219
|
+
|
|
220
|
+
<TipBanner
|
|
221
|
+
plugin={dashboardTipMetadata}
|
|
222
|
+
id="welcome"
|
|
223
|
+
title="Welcome to Checkstack"
|
|
224
|
+
description={
|
|
225
|
+
<>
|
|
226
|
+
This is your dashboard. It surfaces only the systems that need
|
|
227
|
+
attention right now - everything healthy stays out of your way. To
|
|
228
|
+
bring it to life, add a system in the <strong>Catalog</strong>, then
|
|
229
|
+
attach a health check to it.
|
|
230
|
+
</>
|
|
231
|
+
}
|
|
232
|
+
action={{
|
|
233
|
+
label: "Open Catalog",
|
|
234
|
+
onClick: () => navigate(resolveRoute(catalogRoutes.routes.config)),
|
|
235
|
+
}}
|
|
236
|
+
actionHint={
|
|
237
|
+
<span className="inline-flex items-center gap-1.5">
|
|
238
|
+
<Lightbulb
|
|
239
|
+
className="size-3.5 shrink-0 text-amber-500"
|
|
240
|
+
aria-hidden="true"
|
|
401
241
|
/>
|
|
242
|
+
See a small lightbulb next to a button or control? Click it for a
|
|
243
|
+
short explanation of what that feature does.
|
|
244
|
+
</span>
|
|
245
|
+
}
|
|
246
|
+
/>
|
|
402
247
|
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
description={
|
|
414
|
-
activeMaintenancesCount === 0
|
|
415
|
-
? "No scheduled maintenance"
|
|
416
|
-
: "Ongoing or upcoming maintenance windows"
|
|
417
|
-
}
|
|
418
|
-
icon={<Wrench className="w-4 h-4" />}
|
|
419
|
-
onClick={
|
|
420
|
-
activeMaintenancesCount > 0
|
|
421
|
-
? () => setMaintenanceSheetOpen(true)
|
|
422
|
-
: undefined
|
|
423
|
-
}
|
|
424
|
-
className={cn(
|
|
425
|
-
activeMaintenancesCount > 0 && "cursor-pointer hover:opacity-90",
|
|
426
|
-
activeMaintenancesCount > 0 &&
|
|
427
|
-
!isLowPower &&
|
|
428
|
-
"hover:scale-[1.02]",
|
|
429
|
-
)}
|
|
430
|
-
/>
|
|
248
|
+
{/*
|
|
249
|
+
Headless, render-once aggregation boundary. Every plugin fills
|
|
250
|
+
SystemSignalsSlot with its bulk per-system state; the dashboard merges
|
|
251
|
+
the reports to drive the overview. The dashboard is agnostic to which
|
|
252
|
+
plugins contribute - third-party plugins participate the same way.
|
|
253
|
+
*/}
|
|
254
|
+
<ExtensionSlot
|
|
255
|
+
slot={SystemSignalsSlot}
|
|
256
|
+
context={{ systemIds, onSignals: handleSignals }}
|
|
257
|
+
/>
|
|
431
258
|
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
activeAnomaliesCount > 0
|
|
446
|
-
? () => setAnomalySheetOpen(true)
|
|
447
|
-
: undefined
|
|
448
|
-
}
|
|
259
|
+
<section>
|
|
260
|
+
<div className="mb-6 flex items-center justify-between gap-4">
|
|
261
|
+
<div className="flex items-center gap-2">
|
|
262
|
+
<span className="text-primary">
|
|
263
|
+
<Activity className="h-5 w-5" />
|
|
264
|
+
</span>
|
|
265
|
+
<h2 className="text-xl font-semibold tracking-tight text-foreground">
|
|
266
|
+
System health
|
|
267
|
+
</h2>
|
|
268
|
+
</div>
|
|
269
|
+
{systemsCount > 0 && (
|
|
270
|
+
<Link
|
|
271
|
+
to={catalogHref}
|
|
449
272
|
className={cn(
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
activeAnomaliesCount > 0 &&
|
|
453
|
-
!isLowPower &&
|
|
454
|
-
"hover:scale-[1.02]",
|
|
273
|
+
"inline-flex shrink-0 items-center gap-1 text-sm font-medium text-primary hover:underline",
|
|
274
|
+
!isLowPower && "transition-colors",
|
|
455
275
|
)}
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
<div className="grid gap-8 lg:grid-cols-3">
|
|
462
|
-
{/* Terminal Feed */}
|
|
463
|
-
<section className="lg:col-span-1">
|
|
464
|
-
<SectionHeader
|
|
465
|
-
title="Recent Activity"
|
|
466
|
-
icon={<Terminal className="w-5 h-5" />}
|
|
467
|
-
/>
|
|
468
|
-
<TerminalFeed
|
|
469
|
-
entries={terminalEntries}
|
|
470
|
-
maxEntries={MAX_TERMINAL_ENTRIES}
|
|
471
|
-
maxHeight="350px"
|
|
472
|
-
title="checkstack status --watch"
|
|
473
|
-
/>
|
|
474
|
-
</section>
|
|
475
|
-
|
|
476
|
-
{/* System Groups */}
|
|
477
|
-
<section className="lg:col-span-2">
|
|
478
|
-
<SectionHeader
|
|
479
|
-
title="System Groups"
|
|
480
|
-
icon={<LayoutGrid className="w-5 h-5" />}
|
|
481
|
-
/>
|
|
482
|
-
{renderGroupsContent()}
|
|
483
|
-
</section>
|
|
276
|
+
>
|
|
277
|
+
View catalog
|
|
278
|
+
<ArrowRight className="h-4 w-4" />
|
|
279
|
+
</Link>
|
|
280
|
+
)}
|
|
484
281
|
</div>
|
|
485
|
-
|
|
282
|
+
{renderOverview()}
|
|
283
|
+
</section>
|
|
486
284
|
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
anomalies={anomalies}
|
|
503
|
-
systems={systems}
|
|
504
|
-
/>
|
|
505
|
-
</>
|
|
285
|
+
{systemsCount > 0 && (
|
|
286
|
+
<section>
|
|
287
|
+
<SectionHeader
|
|
288
|
+
title="Recent activity"
|
|
289
|
+
icon={<Terminal className="w-5 h-5" />}
|
|
290
|
+
/>
|
|
291
|
+
<TerminalFeed
|
|
292
|
+
entries={terminalEntries}
|
|
293
|
+
maxEntries={MAX_TERMINAL_ENTRIES}
|
|
294
|
+
maxHeight="320px"
|
|
295
|
+
title="checkstack status --watch"
|
|
296
|
+
/>
|
|
297
|
+
</section>
|
|
298
|
+
)}
|
|
299
|
+
</div>
|
|
506
300
|
);
|
|
507
301
|
};
|