@checkstack/healthcheck-frontend 0.19.5 → 0.21.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 +106 -0
- package/package.json +14 -14
- package/src/components/EmptyRunsTableRow.tsx +27 -0
- package/src/components/HealthCheckDrawer.tsx +28 -2
- package/src/components/HealthCheckRunsTable.tsx +11 -4
- package/src/components/HealthCheckSystemOverview.tsx +77 -9
- package/src/components/StatusFilterPills.tsx +79 -0
- package/src/components/assignments/AssignmentTree.tsx +21 -1
- package/src/components/assignments/NotificationsPanel.tsx +385 -0
- package/src/components/assignments/PlatformDefaultsDialog.tsx +90 -0
- package/src/components/editor/CollectorSection.tsx +2 -0
- package/src/pages/AssignmentIDEPage.tsx +163 -4
- package/src/pages/HealthCheckHistoryDetailPage.tsx +20 -2
- package/src/pages/HealthCheckHistoryPage.tsx +1 -1
|
@@ -6,10 +6,14 @@ import { SatelliteApi } from "@checkstack/satellite-common";
|
|
|
6
6
|
import {
|
|
7
7
|
DEFAULT_STATE_THRESHOLDS,
|
|
8
8
|
DEFAULT_RETENTION_CONFIG,
|
|
9
|
+
DEFAULT_NOTIFICATION_POLICY,
|
|
10
|
+
} from "@checkstack/healthcheck-common";
|
|
11
|
+
import type {
|
|
12
|
+
StateThresholds,
|
|
13
|
+
NotificationPolicy,
|
|
9
14
|
} from "@checkstack/healthcheck-common";
|
|
10
|
-
import type { StateThresholds } from "@checkstack/healthcheck-common";
|
|
11
15
|
import { PageLayout, IDELayout, useToast, BackLink, Button } from "@checkstack/ui";
|
|
12
|
-
import { Settings, Plus } from "lucide-react";
|
|
16
|
+
import { Settings, Plus, Bell } from "lucide-react";
|
|
13
17
|
import { extractErrorMessage, resolveRoute } from "@checkstack/common";
|
|
14
18
|
import { catalogRoutes } from "@checkstack/catalog-common";
|
|
15
19
|
import { healthcheckRoutes } from "@checkstack/healthcheck-common";
|
|
@@ -25,6 +29,8 @@ import {
|
|
|
25
29
|
type RetentionData,
|
|
26
30
|
} from "../components/assignments/RetentionPanel";
|
|
27
31
|
import { ExecutionPanel } from "../components/assignments/ExecutionPanel";
|
|
32
|
+
import { NotificationsPanel } from "../components/assignments/NotificationsPanel";
|
|
33
|
+
import { PlatformDefaultsDialog } from "../components/assignments/PlatformDefaultsDialog";
|
|
28
34
|
import { AssignmentIDEPanelSlot } from "../slots";
|
|
29
35
|
|
|
30
36
|
// =============================================================================
|
|
@@ -32,12 +38,22 @@ import { AssignmentIDEPanelSlot } from "../slots";
|
|
|
32
38
|
// =============================================================================
|
|
33
39
|
|
|
34
40
|
function parseNodeId(nodeId: AssignmentNodeId): {
|
|
35
|
-
panel:
|
|
41
|
+
panel:
|
|
42
|
+
| "general"
|
|
43
|
+
| "thresholds"
|
|
44
|
+
| "retention"
|
|
45
|
+
| "execution"
|
|
46
|
+
| "notifications";
|
|
36
47
|
configId: string;
|
|
37
48
|
} {
|
|
38
49
|
const [panel, ...rest] = nodeId.split(":") as [string, ...string[]];
|
|
39
50
|
return {
|
|
40
|
-
panel: panel as
|
|
51
|
+
panel: panel as
|
|
52
|
+
| "general"
|
|
53
|
+
| "thresholds"
|
|
54
|
+
| "retention"
|
|
55
|
+
| "execution"
|
|
56
|
+
| "notifications",
|
|
41
57
|
configId: rest.join(":"),
|
|
42
58
|
};
|
|
43
59
|
}
|
|
@@ -80,6 +96,16 @@ const AssignmentIDEPageContent = () => {
|
|
|
80
96
|
const [retentionData, setRetentionData] = useState<
|
|
81
97
|
Record<string, RetentionData>
|
|
82
98
|
>({});
|
|
99
|
+
const [localNotificationPolicy, setLocalNotificationPolicy] = useState<
|
|
100
|
+
Record<string, NotificationPolicy>
|
|
101
|
+
>({});
|
|
102
|
+
const [platformDefaultsOpen, setPlatformDefaultsOpen] = useState(false);
|
|
103
|
+
|
|
104
|
+
// Platform notification defaults — used as the fallback for any
|
|
105
|
+
// assignment that hasn't overridden them. Refetched whenever the
|
|
106
|
+
// platform-defaults dialog closes so changes propagate immediately.
|
|
107
|
+
const { data: platformDefaults } =
|
|
108
|
+
healthCheckClient.getPlatformNotificationDefaults.useQuery();
|
|
83
109
|
|
|
84
110
|
const configs = useMemo(
|
|
85
111
|
() => configurationsData?.configurations ?? [],
|
|
@@ -214,6 +240,7 @@ const AssignmentIDEPageContent = () => {
|
|
|
214
240
|
stateThresholds: assoc.stateThresholds,
|
|
215
241
|
satelliteIds: assoc.satelliteIds,
|
|
216
242
|
includeLocal: assoc.includeLocal,
|
|
243
|
+
notificationPolicy: assoc.notificationPolicy,
|
|
217
244
|
},
|
|
218
245
|
});
|
|
219
246
|
};
|
|
@@ -238,6 +265,9 @@ const AssignmentIDEPageContent = () => {
|
|
|
238
265
|
configurationId: configId,
|
|
239
266
|
enabled: assoc.enabled,
|
|
240
267
|
stateThresholds: thresholds,
|
|
268
|
+
satelliteIds: assoc.satelliteIds,
|
|
269
|
+
includeLocal: assoc.includeLocal,
|
|
270
|
+
notificationPolicy: assoc.notificationPolicy,
|
|
241
271
|
},
|
|
242
272
|
},
|
|
243
273
|
{
|
|
@@ -253,6 +283,96 @@ const AssignmentIDEPageContent = () => {
|
|
|
253
283
|
);
|
|
254
284
|
};
|
|
255
285
|
|
|
286
|
+
const handleNotificationPolicyChange = (
|
|
287
|
+
configId: string,
|
|
288
|
+
policy: NotificationPolicy,
|
|
289
|
+
) => {
|
|
290
|
+
setLocalNotificationPolicy((prev) => ({ ...prev, [configId]: policy }));
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
const handleSaveNotificationPolicy = (configId: string) => {
|
|
294
|
+
if (!systemId) return;
|
|
295
|
+
const assoc = associations.find((a) => a.configurationId === configId);
|
|
296
|
+
if (!assoc) return;
|
|
297
|
+
const policy =
|
|
298
|
+
localNotificationPolicy[configId] ??
|
|
299
|
+
assoc.notificationPolicy ??
|
|
300
|
+
platformDefaults ??
|
|
301
|
+
DEFAULT_NOTIFICATION_POLICY;
|
|
302
|
+
|
|
303
|
+
associateMutation.mutate(
|
|
304
|
+
{
|
|
305
|
+
systemId,
|
|
306
|
+
body: {
|
|
307
|
+
configurationId: configId,
|
|
308
|
+
enabled: assoc.enabled,
|
|
309
|
+
stateThresholds: assoc.stateThresholds,
|
|
310
|
+
satelliteIds: assoc.satelliteIds,
|
|
311
|
+
includeLocal: assoc.includeLocal,
|
|
312
|
+
notificationPolicy: policy,
|
|
313
|
+
},
|
|
314
|
+
},
|
|
315
|
+
{
|
|
316
|
+
onSuccess: () => {
|
|
317
|
+
toast.success("Notification policy saved");
|
|
318
|
+
setLocalNotificationPolicy((prev) => {
|
|
319
|
+
const next = { ...prev };
|
|
320
|
+
delete next[configId];
|
|
321
|
+
return next;
|
|
322
|
+
});
|
|
323
|
+
},
|
|
324
|
+
},
|
|
325
|
+
);
|
|
326
|
+
};
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Revert this assignment to platform defaults. Sends an undefined
|
|
330
|
+
* `notificationPolicy` which is persisted as null and re-resolves to
|
|
331
|
+
* the platform defaults on the next read.
|
|
332
|
+
*/
|
|
333
|
+
const handleUseDefaultsForAssignment = (configId: string) => {
|
|
334
|
+
if (!systemId) return;
|
|
335
|
+
const assoc = associations.find((a) => a.configurationId === configId);
|
|
336
|
+
if (!assoc) return;
|
|
337
|
+
|
|
338
|
+
associateMutation.mutate(
|
|
339
|
+
{
|
|
340
|
+
systemId,
|
|
341
|
+
body: {
|
|
342
|
+
configurationId: configId,
|
|
343
|
+
enabled: assoc.enabled,
|
|
344
|
+
stateThresholds: assoc.stateThresholds,
|
|
345
|
+
satelliteIds: assoc.satelliteIds,
|
|
346
|
+
includeLocal: assoc.includeLocal,
|
|
347
|
+
notificationPolicy: undefined,
|
|
348
|
+
},
|
|
349
|
+
},
|
|
350
|
+
{
|
|
351
|
+
onSuccess: () => {
|
|
352
|
+
toast.success("Reverted to platform defaults");
|
|
353
|
+
setLocalNotificationPolicy((prev) => {
|
|
354
|
+
const next = { ...prev };
|
|
355
|
+
delete next[configId];
|
|
356
|
+
return next;
|
|
357
|
+
});
|
|
358
|
+
},
|
|
359
|
+
},
|
|
360
|
+
);
|
|
361
|
+
};
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* Start customising — clone the current platform defaults into the
|
|
365
|
+
* draft state so the operator has a baseline to edit, then persist.
|
|
366
|
+
* The persistence step is what flips the row out of "inherit" mode.
|
|
367
|
+
*/
|
|
368
|
+
const handleOverrideForAssignment = (configId: string) => {
|
|
369
|
+
const baseline = platformDefaults ?? DEFAULT_NOTIFICATION_POLICY;
|
|
370
|
+
setLocalNotificationPolicy((prev) => ({ ...prev, [configId]: baseline }));
|
|
371
|
+
// Defer the actual save: operators may want to tweak the cloned
|
|
372
|
+
// baseline before persisting. The Save button at the bottom of
|
|
373
|
+
// the panel handles it.
|
|
374
|
+
};
|
|
375
|
+
|
|
256
376
|
const handleToggleSatellite = (configId: string, satelliteId: string) => {
|
|
257
377
|
if (!systemId) return;
|
|
258
378
|
const assoc = associations.find((a) => a.configurationId === configId);
|
|
@@ -272,6 +392,7 @@ const AssignmentIDEPageContent = () => {
|
|
|
272
392
|
stateThresholds: assoc.stateThresholds,
|
|
273
393
|
satelliteIds: newIds,
|
|
274
394
|
includeLocal: assoc.includeLocal,
|
|
395
|
+
notificationPolicy: assoc.notificationPolicy,
|
|
275
396
|
},
|
|
276
397
|
});
|
|
277
398
|
};
|
|
@@ -289,6 +410,7 @@ const AssignmentIDEPageContent = () => {
|
|
|
289
410
|
stateThresholds: assoc.stateThresholds,
|
|
290
411
|
satelliteIds: assoc.satelliteIds,
|
|
291
412
|
includeLocal: !assoc.includeLocal,
|
|
413
|
+
notificationPolicy: assoc.notificationPolicy,
|
|
292
414
|
},
|
|
293
415
|
});
|
|
294
416
|
};
|
|
@@ -450,6 +572,31 @@ const AssignmentIDEPageContent = () => {
|
|
|
450
572
|
/>
|
|
451
573
|
);
|
|
452
574
|
}
|
|
575
|
+
case "notifications": {
|
|
576
|
+
// Is the operator actively editing a draft? Drafts are stored
|
|
577
|
+
// when override starts, so the presence of a draft AND the
|
|
578
|
+
// assignment being persisted-as-override mean the same thing.
|
|
579
|
+
const draft = localNotificationPolicy[configId];
|
|
580
|
+
const isOverridden =
|
|
581
|
+
draft !== undefined || assoc.notificationPolicy !== undefined;
|
|
582
|
+
const policy =
|
|
583
|
+
draft ??
|
|
584
|
+
assoc.notificationPolicy ??
|
|
585
|
+
platformDefaults ??
|
|
586
|
+
DEFAULT_NOTIFICATION_POLICY;
|
|
587
|
+
return (
|
|
588
|
+
<NotificationsPanel
|
|
589
|
+
policy={policy}
|
|
590
|
+
onChange={(p) => handleNotificationPolicyChange(configId, p)}
|
|
591
|
+
onSave={() => handleSaveNotificationPolicy(configId)}
|
|
592
|
+
saving={saving}
|
|
593
|
+
isLocked={isLocked}
|
|
594
|
+
isOverridden={isOverridden}
|
|
595
|
+
onOverride={() => handleOverrideForAssignment(configId)}
|
|
596
|
+
onUseDefaults={() => handleUseDefaultsForAssignment(configId)}
|
|
597
|
+
/>
|
|
598
|
+
);
|
|
599
|
+
}
|
|
453
600
|
default: {
|
|
454
601
|
return (
|
|
455
602
|
<ExtensionSlot
|
|
@@ -482,6 +629,14 @@ const AssignmentIDEPageContent = () => {
|
|
|
482
629
|
maxWidth="full"
|
|
483
630
|
actions={
|
|
484
631
|
<div className="flex items-center gap-2">
|
|
632
|
+
<Button
|
|
633
|
+
size="sm"
|
|
634
|
+
variant="outline"
|
|
635
|
+
onClick={() => setPlatformDefaultsOpen(true)}
|
|
636
|
+
>
|
|
637
|
+
<Bell className="mr-2 h-4 w-4" />
|
|
638
|
+
Notification defaults
|
|
639
|
+
</Button>
|
|
485
640
|
{!isLocked && systemId && (
|
|
486
641
|
<Button
|
|
487
642
|
size="sm"
|
|
@@ -522,6 +677,10 @@ const AssignmentIDEPageContent = () => {
|
|
|
522
677
|
}
|
|
523
678
|
panel={renderPanel()}
|
|
524
679
|
/>
|
|
680
|
+
<PlatformDefaultsDialog
|
|
681
|
+
open={platformDefaultsOpen}
|
|
682
|
+
onOpenChange={setPlatformDefaultsOpen}
|
|
683
|
+
/>
|
|
525
684
|
</PageLayout>
|
|
526
685
|
);
|
|
527
686
|
};
|
|
@@ -35,6 +35,11 @@ import {
|
|
|
35
35
|
} from "../components/HealthCheckRunsTable";
|
|
36
36
|
import { ExpandedResultView } from "../components/ExpandedResultView";
|
|
37
37
|
import { SingleRunChartGrid } from "../auto-charts";
|
|
38
|
+
import {
|
|
39
|
+
StatusFilterPills,
|
|
40
|
+
STATUS_FILTER_TO_STATUSES,
|
|
41
|
+
type StatusFilter,
|
|
42
|
+
} from "../components/StatusFilterPills";
|
|
38
43
|
|
|
39
44
|
const HealthCheckHistoryDetailPageContent = () => {
|
|
40
45
|
const { systemId, configurationId, runId } = useParams<{
|
|
@@ -53,9 +58,10 @@ const HealthCheckHistoryDetailPageContent = () => {
|
|
|
53
58
|
|
|
54
59
|
const [dateRange, setDateRange] = useState<DateRange>(getDefaultDateRange);
|
|
55
60
|
const [sourceFilter, setSourceFilter] = useState<string | undefined>();
|
|
61
|
+
const [statusFilter, setStatusFilter] = useState<StatusFilter>("all");
|
|
56
62
|
|
|
57
63
|
// Pagination state
|
|
58
|
-
const pagination = usePagination({ defaultLimit:
|
|
64
|
+
const pagination = usePagination({ defaultLimit: 25 });
|
|
59
65
|
|
|
60
66
|
// Fetch satellites for the source filter dropdown
|
|
61
67
|
const { data: satellitesData } = satelliteClient.listSatellites.useQuery({});
|
|
@@ -89,6 +95,7 @@ const HealthCheckHistoryDetailPageContent = () => {
|
|
|
89
95
|
startDate: dateRange.startDate,
|
|
90
96
|
endDate: dateRange.endDate,
|
|
91
97
|
sourceFilter,
|
|
98
|
+
statusFilter: STATUS_FILTER_TO_STATUSES[statusFilter],
|
|
92
99
|
limit: pagination.limit,
|
|
93
100
|
offset: pagination.offset,
|
|
94
101
|
sortOrder: "desc",
|
|
@@ -166,6 +173,13 @@ const HealthCheckHistoryDetailPageContent = () => {
|
|
|
166
173
|
<CardContent>
|
|
167
174
|
<div className="flex flex-wrap items-center gap-3 mb-4">
|
|
168
175
|
<DateRangeFilter value={dateRange} onChange={setDateRange} />
|
|
176
|
+
<StatusFilterPills
|
|
177
|
+
value={statusFilter}
|
|
178
|
+
onChange={(next) => {
|
|
179
|
+
setStatusFilter(next);
|
|
180
|
+
pagination.setPage(1);
|
|
181
|
+
}}
|
|
182
|
+
/>
|
|
169
183
|
{/* Source filter */}
|
|
170
184
|
<div className="flex items-center gap-2">
|
|
171
185
|
<span className="text-sm text-muted-foreground">Source:</span>
|
|
@@ -211,7 +225,11 @@ const HealthCheckHistoryDetailPageContent = () => {
|
|
|
211
225
|
<HealthCheckRunsTable
|
|
212
226
|
runs={runs}
|
|
213
227
|
loading={isLoading}
|
|
214
|
-
emptyMessage=
|
|
228
|
+
emptyMessage={
|
|
229
|
+
statusFilter !== "all" || sourceFilter !== undefined
|
|
230
|
+
? "No runs match the current filters."
|
|
231
|
+
: "No health check runs found for this configuration."
|
|
232
|
+
}
|
|
215
233
|
pagination={pagination}
|
|
216
234
|
/>
|
|
217
235
|
</CardContent>
|
|
@@ -31,7 +31,7 @@ const HealthCheckHistoryPageContent = () => {
|
|
|
31
31
|
);
|
|
32
32
|
|
|
33
33
|
// Pagination state
|
|
34
|
-
const pagination = usePagination({ defaultLimit:
|
|
34
|
+
const pagination = usePagination({ defaultLimit: 25 });
|
|
35
35
|
|
|
36
36
|
// Fetch data with useQuery - newest first for table display
|
|
37
37
|
const { data, isLoading } = healthCheckClient.getDetailedHistory.useQuery({
|