@checkstack/healthcheck-frontend 0.21.0 → 0.23.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 +309 -0
- package/package.json +21 -18
- package/src/auto-charts/extension.tsx +15 -2
- package/src/components/CatalogBrowseHealthFiller.tsx +60 -0
- package/src/components/CollectorList.tsx +10 -0
- package/src/components/HealthCheckDrawer.tsx +35 -4
- package/src/components/HealthCheckRunsTable.tsx +38 -4
- package/src/components/HealthCheckSystemOverview.tsx +19 -11
- package/src/components/HealthSignalsFiller.tsx +76 -0
- package/src/components/SystemHealthBadge.tsx +7 -2
- package/src/components/assignments/ExecutionPanel.tsx +91 -1
- package/src/components/assignments/NotificationsPanel.tsx +8 -237
- package/src/components/assignments/environment-selector.logic.test.ts +68 -0
- package/src/components/assignments/environment-selector.logic.ts +69 -0
- package/src/components/editor/CollectorScriptTestRenderer.tsx +115 -0
- package/src/components/editor/CollectorSection.tsx +80 -13
- package/src/components/editor/EditorPanel.tsx +26 -0
- package/src/components/editor/collector-preview-context.logic.test.ts +101 -0
- package/src/components/editor/collector-preview-context.logic.ts +88 -0
- package/src/index.tsx +65 -23
- package/src/pages/AssignmentIDEPage.tsx +88 -1
- package/src/pages/HealthCheckIDEPage.tsx +74 -0
- package/tsconfig.json +9 -0
- package/src/components/HealthCheckMenuItems.tsx +0 -30
|
@@ -8,9 +8,10 @@ import {
|
|
|
8
8
|
TableCell,
|
|
9
9
|
HealthBadge,
|
|
10
10
|
Pagination,
|
|
11
|
+
Spinner,
|
|
11
12
|
} from "@checkstack/ui";
|
|
12
13
|
import { formatDistanceToNow, format } from "date-fns";
|
|
13
|
-
import { ExternalLink,
|
|
14
|
+
import { ExternalLink, Satellite, Server, Layers } from "lucide-react";
|
|
14
15
|
import { useNavigate } from "react-router-dom";
|
|
15
16
|
import { healthcheckRoutes } from "@checkstack/healthcheck-common";
|
|
16
17
|
import { resolveRoute } from "@checkstack/common";
|
|
@@ -23,16 +24,32 @@ export interface HealthCheckRunDetailed {
|
|
|
23
24
|
status: "healthy" | "unhealthy" | "degraded";
|
|
24
25
|
result: Record<string, unknown>;
|
|
25
26
|
timestamp: Date;
|
|
27
|
+
/**
|
|
28
|
+
* Environment this run executed for (per-environment fan-out). undefined =
|
|
29
|
+
* env-less run (opt-out / no membership).
|
|
30
|
+
*/
|
|
31
|
+
environmentId?: string;
|
|
26
32
|
/** Source ID for result attribution (undefined = local core, UUID = satellite) */
|
|
27
33
|
sourceId?: string;
|
|
28
34
|
/** Human-readable source label (e.g. "Local" or "EU West (eu-west-1)") */
|
|
29
35
|
sourceLabel?: string;
|
|
30
36
|
}
|
|
31
37
|
|
|
38
|
+
export interface EnvironmentLabel {
|
|
39
|
+
id: string;
|
|
40
|
+
name: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
32
43
|
export interface HealthCheckRunsTableProps {
|
|
33
44
|
runs: HealthCheckRunDetailed[];
|
|
34
45
|
loading: boolean;
|
|
35
46
|
emptyMessage?: string;
|
|
47
|
+
/**
|
|
48
|
+
* Optional id -> name map for the Environment column. When a run's
|
|
49
|
+
* `environmentId` is present, its display name is looked up here (falling
|
|
50
|
+
* back to the id). Env-less runs render a muted dash.
|
|
51
|
+
*/
|
|
52
|
+
environmentLabels?: EnvironmentLabel[];
|
|
36
53
|
/** Show System ID and Configuration ID columns with link to detail page */
|
|
37
54
|
showFilterColumns?: boolean;
|
|
38
55
|
/** Number of columns for the expanded result row */
|
|
@@ -52,11 +69,15 @@ export const HealthCheckRunsTable: React.FC<HealthCheckRunsTableProps> = ({
|
|
|
52
69
|
runs,
|
|
53
70
|
loading,
|
|
54
71
|
emptyMessage = "No health check runs found.",
|
|
72
|
+
environmentLabels,
|
|
55
73
|
showFilterColumns = false,
|
|
56
74
|
pagination,
|
|
57
75
|
}) => {
|
|
58
76
|
const navigate = useNavigate();
|
|
59
77
|
const prevRunsRef = useRef(runs);
|
|
78
|
+
const envNameById = new Map(
|
|
79
|
+
(environmentLabels ?? []).map((e) => [e.id, e.name]),
|
|
80
|
+
);
|
|
60
81
|
|
|
61
82
|
// Keep previous runs during loading to prevent layout shift
|
|
62
83
|
const displayRuns =
|
|
@@ -75,9 +96,9 @@ export const HealthCheckRunsTable: React.FC<HealthCheckRunsTableProps> = ({
|
|
|
75
96
|
);
|
|
76
97
|
};
|
|
77
98
|
|
|
78
|
-
//
|
|
99
|
+
// 4 base columns (Status, Timestamp, Environment, Source) + 3 extras when
|
|
79
100
|
// showFilterColumns is on (System ID, Configuration ID, link icon).
|
|
80
|
-
const columnCount = showFilterColumns ?
|
|
101
|
+
const columnCount = showFilterColumns ? 7 : 4;
|
|
81
102
|
const showEmptyRow = !loading && displayRuns.length === 0;
|
|
82
103
|
|
|
83
104
|
return (
|
|
@@ -89,7 +110,9 @@ export const HealthCheckRunsTable: React.FC<HealthCheckRunsTableProps> = ({
|
|
|
89
110
|
<TableHead className="w-24">
|
|
90
111
|
<span className="flex items-center gap-2">
|
|
91
112
|
Status
|
|
92
|
-
{loading &&
|
|
113
|
+
{loading && (
|
|
114
|
+
<Spinner size="sm" className="h-3 w-3" />
|
|
115
|
+
)}
|
|
93
116
|
</span>
|
|
94
117
|
</TableHead>
|
|
95
118
|
{showFilterColumns && (
|
|
@@ -99,6 +122,7 @@ export const HealthCheckRunsTable: React.FC<HealthCheckRunsTableProps> = ({
|
|
|
99
122
|
</>
|
|
100
123
|
)}
|
|
101
124
|
<TableHead>Timestamp</TableHead>
|
|
125
|
+
<TableHead>Environment</TableHead>
|
|
102
126
|
<TableHead>Source</TableHead>
|
|
103
127
|
{showFilterColumns && <TableHead className="w-16"></TableHead>}
|
|
104
128
|
</TableRow>
|
|
@@ -135,6 +159,16 @@ export const HealthCheckRunsTable: React.FC<HealthCheckRunsTableProps> = ({
|
|
|
135
159
|
})}
|
|
136
160
|
</span>
|
|
137
161
|
</TableCell>
|
|
162
|
+
<TableCell>
|
|
163
|
+
{run.environmentId ? (
|
|
164
|
+
<span className="inline-flex items-center gap-1 text-xs px-1.5 py-0.5 rounded-full bg-primary/10 text-primary">
|
|
165
|
+
<Layers className="h-3 w-3" />
|
|
166
|
+
{envNameById.get(run.environmentId) ?? run.environmentId}
|
|
167
|
+
</span>
|
|
168
|
+
) : (
|
|
169
|
+
<span className="text-xs text-muted-foreground">None</span>
|
|
170
|
+
)}
|
|
171
|
+
</TableCell>
|
|
138
172
|
<TableCell>
|
|
139
173
|
{run.sourceId ? (
|
|
140
174
|
<span className="inline-flex items-center gap-1 text-xs px-1.5 py-0.5 rounded-full bg-orange-500/10 text-orange-600">
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, { useState } from "react";
|
|
1
|
+
import React, { useState, lazy, Suspense } from "react";
|
|
2
2
|
import { useSearchParams } from "react-router-dom";
|
|
3
3
|
import {
|
|
4
4
|
usePluginClient,
|
|
@@ -17,7 +17,13 @@ import {
|
|
|
17
17
|
} from "@checkstack/ui";
|
|
18
18
|
import { Heart } from "lucide-react";
|
|
19
19
|
import { HealthCheckSparkline } from "./HealthCheckSparkline";
|
|
20
|
-
|
|
20
|
+
// Lazy-loaded: the drawer pulls in the recharts-based latency/timeline charts
|
|
21
|
+
// (~300 KB). This component is an eagerly-registered slot extension, so a static
|
|
22
|
+
// import would ship recharts in the initial bundle. The drawer only renders when
|
|
23
|
+
// a check is selected, so deferring it keeps charts out of the initial load.
|
|
24
|
+
const HealthCheckDrawer = lazy(() =>
|
|
25
|
+
import("./HealthCheckDrawer").then((m) => ({ default: m.HealthCheckDrawer })),
|
|
26
|
+
);
|
|
21
27
|
|
|
22
28
|
import type {
|
|
23
29
|
StateThresholds,
|
|
@@ -206,16 +212,18 @@ export function HealthCheckSystemOverview(props: SlotProps) {
|
|
|
206
212
|
</CardContent>
|
|
207
213
|
</Card>
|
|
208
214
|
|
|
209
|
-
{/* Slide-over Drawer */}
|
|
215
|
+
{/* Slide-over Drawer (lazy: loads the chart bundle on first open) */}
|
|
210
216
|
{selectedCheck && (
|
|
211
|
-
<
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
217
|
+
<Suspense fallback={null}>
|
|
218
|
+
<HealthCheckDrawer
|
|
219
|
+
item={selectedCheck}
|
|
220
|
+
systemId={systemId}
|
|
221
|
+
open={!!selectedCheck}
|
|
222
|
+
onOpenChange={(open) => {
|
|
223
|
+
if (!open) setSelectedCheck(undefined);
|
|
224
|
+
}}
|
|
225
|
+
/>
|
|
226
|
+
</Suspense>
|
|
219
227
|
)}
|
|
220
228
|
</>
|
|
221
229
|
);
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import React, { useEffect, useMemo } from "react";
|
|
2
|
+
import { usePluginClient, type SlotContext } from "@checkstack/frontend-api";
|
|
3
|
+
import { resolveRoute } from "@checkstack/common";
|
|
4
|
+
import {
|
|
5
|
+
SystemSignalsSlot,
|
|
6
|
+
type SystemSignal,
|
|
7
|
+
type SystemSignalsMap,
|
|
8
|
+
} from "@checkstack/catalog-common";
|
|
9
|
+
import { healthcheckRoutes } from "@checkstack/healthcheck-common";
|
|
10
|
+
import { HealthCheckApi } from "../api";
|
|
11
|
+
|
|
12
|
+
type Props = SlotContext<typeof SystemSignalsSlot>;
|
|
13
|
+
|
|
14
|
+
const SOURCE_ID = "healthcheck";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Reports per-system health as dashboard signals. Bulk-fetches health for all
|
|
18
|
+
* overview systems in one request and contributes a signal for every system
|
|
19
|
+
* that is degraded or unhealthy, deep-linking to the failing check's history
|
|
20
|
+
* (or the system's check assignments when no specific check is failing).
|
|
21
|
+
* Renders nothing — it is a headless filler for {@link SystemSignalsSlot}.
|
|
22
|
+
*/
|
|
23
|
+
export const HealthSignalsFiller: React.FC<Props> = ({
|
|
24
|
+
systemIds,
|
|
25
|
+
onSignals,
|
|
26
|
+
}) => {
|
|
27
|
+
const healthCheckClient = usePluginClient(HealthCheckApi);
|
|
28
|
+
|
|
29
|
+
const { data } = healthCheckClient.getBulkSystemHealthStatus.useQuery(
|
|
30
|
+
{ systemIds },
|
|
31
|
+
{ enabled: systemIds.length > 0, staleTime: 30_000 },
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
const signals = useMemo<SystemSignalsMap>(() => {
|
|
35
|
+
const result: SystemSignalsMap = {};
|
|
36
|
+
if (!data) return result;
|
|
37
|
+
|
|
38
|
+
for (const systemId of systemIds) {
|
|
39
|
+
const status = data.statuses[systemId];
|
|
40
|
+
if (!status || status.status === "healthy") continue;
|
|
41
|
+
|
|
42
|
+
const failing = status.checkStatuses.filter(
|
|
43
|
+
(c) => c.status !== "healthy",
|
|
44
|
+
);
|
|
45
|
+
const failingCheck = failing[0];
|
|
46
|
+
const href = failingCheck
|
|
47
|
+
? resolveRoute(healthcheckRoutes.routes.historyDetail, {
|
|
48
|
+
systemId,
|
|
49
|
+
configurationId: failingCheck.configurationId,
|
|
50
|
+
})
|
|
51
|
+
: resolveRoute(healthcheckRoutes.routes.assignments, { systemId });
|
|
52
|
+
|
|
53
|
+
const detail =
|
|
54
|
+
status.checkStatuses.length > 0
|
|
55
|
+
? `${failing.length} of ${status.checkStatuses.length} checks failing`
|
|
56
|
+
: undefined;
|
|
57
|
+
|
|
58
|
+
const signal: SystemSignal = {
|
|
59
|
+
source: SOURCE_ID,
|
|
60
|
+
tone: status.status === "unhealthy" ? "error" : "warn",
|
|
61
|
+
label: status.status === "unhealthy" ? "Unhealthy" : "Degraded",
|
|
62
|
+
detail,
|
|
63
|
+
href,
|
|
64
|
+
iconName: "Activity",
|
|
65
|
+
};
|
|
66
|
+
result[systemId] = [signal];
|
|
67
|
+
}
|
|
68
|
+
return result;
|
|
69
|
+
}, [data, systemIds]);
|
|
70
|
+
|
|
71
|
+
useEffect(() => {
|
|
72
|
+
onSignals(SOURCE_ID, signals);
|
|
73
|
+
}, [signals, onSignals]);
|
|
74
|
+
|
|
75
|
+
return null;
|
|
76
|
+
};
|
|
@@ -2,7 +2,8 @@ import React from "react";
|
|
|
2
2
|
import { usePluginClient, type SlotContext } from "@checkstack/frontend-api";
|
|
3
3
|
import { SystemStateBadgesSlot } from "@checkstack/catalog-common";
|
|
4
4
|
import { HealthCheckApi } from "../api";
|
|
5
|
-
import {
|
|
5
|
+
import { StatusBadge } from "@checkstack/ui";
|
|
6
|
+
import { Activity } from "lucide-react";
|
|
6
7
|
import { useSystemBadgeDataOptional } from "@checkstack/dashboard-frontend";
|
|
7
8
|
|
|
8
9
|
type Props = SlotContext<typeof SystemStateBadgesSlot>;
|
|
@@ -37,5 +38,9 @@ export const SystemHealthBadge: React.FC<Props> = ({ system }) => {
|
|
|
37
38
|
const status = providerStatus ?? localStatus;
|
|
38
39
|
|
|
39
40
|
if (!status || status === "healthy") return <></>;
|
|
40
|
-
return
|
|
41
|
+
return status === "unhealthy" ? (
|
|
42
|
+
<StatusBadge tone="error" icon={Activity} label="Unhealthy" />
|
|
43
|
+
) : (
|
|
44
|
+
<StatusBadge tone="warn" icon={Activity} label="Degraded" />
|
|
45
|
+
);
|
|
41
46
|
};
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
import { Checkbox, Label, Tooltip } from "@checkstack/ui";
|
|
3
|
-
import { Satellite } from "lucide-react";
|
|
3
|
+
import { Satellite, Layers } from "lucide-react";
|
|
4
|
+
import {
|
|
5
|
+
modeFromEnvironmentIds,
|
|
6
|
+
type EnvironmentSelectorMode,
|
|
7
|
+
} from "./environment-selector.logic";
|
|
4
8
|
|
|
5
9
|
interface SatelliteDto {
|
|
6
10
|
id: string;
|
|
@@ -9,12 +13,26 @@ interface SatelliteDto {
|
|
|
9
13
|
status: "online" | "offline";
|
|
10
14
|
}
|
|
11
15
|
|
|
16
|
+
interface EnvironmentDto {
|
|
17
|
+
id: string;
|
|
18
|
+
name: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
12
21
|
interface ExecutionPanelProps {
|
|
13
22
|
includeLocal: boolean;
|
|
14
23
|
satelliteIds: string[];
|
|
15
24
|
satellites: SatelliteDto[];
|
|
16
25
|
onToggleLocal: () => void;
|
|
17
26
|
onToggleSatellite: (satelliteId: string) => void;
|
|
27
|
+
/**
|
|
28
|
+
* Per-assignment environment selector value. null = all current
|
|
29
|
+
* environments; [] = opt out (env-less); non-empty = those env ids.
|
|
30
|
+
*/
|
|
31
|
+
environmentIds: string[] | null;
|
|
32
|
+
/** Environments the system currently belongs to. */
|
|
33
|
+
environments: EnvironmentDto[];
|
|
34
|
+
onSetEnvironmentMode: (mode: EnvironmentSelectorMode) => void;
|
|
35
|
+
onToggleEnvironment: (environmentId: string) => void;
|
|
18
36
|
saving: boolean;
|
|
19
37
|
isLocked?: boolean;
|
|
20
38
|
}
|
|
@@ -29,11 +47,24 @@ export const ExecutionPanel: React.FC<ExecutionPanelProps> = ({
|
|
|
29
47
|
satellites,
|
|
30
48
|
onToggleLocal,
|
|
31
49
|
onToggleSatellite,
|
|
50
|
+
environmentIds,
|
|
51
|
+
environments,
|
|
52
|
+
onSetEnvironmentMode,
|
|
53
|
+
onToggleEnvironment,
|
|
32
54
|
saving,
|
|
33
55
|
isLocked,
|
|
34
56
|
}) => {
|
|
35
57
|
const hasSatellites = satelliteIds.length > 0;
|
|
36
58
|
const willRunAnywhere = includeLocal || hasSatellites;
|
|
59
|
+
const envMode = modeFromEnvironmentIds(environmentIds);
|
|
60
|
+
const selectedEnvIds = new Set<string>(
|
|
61
|
+
environmentIds === null ? [] : environmentIds,
|
|
62
|
+
);
|
|
63
|
+
const envModes: { value: EnvironmentSelectorMode; label: string; hint: string }[] = [
|
|
64
|
+
{ value: "all", label: "All environments", hint: "Run once per environment the system belongs to" },
|
|
65
|
+
{ value: "specific", label: "Specific", hint: "Run only for the selected environments" },
|
|
66
|
+
{ value: "none", label: "None", hint: "Run once with no environment" },
|
|
67
|
+
];
|
|
37
68
|
|
|
38
69
|
return (
|
|
39
70
|
<div className="p-6 space-y-4">
|
|
@@ -118,6 +149,65 @@ export const ExecutionPanel: React.FC<ExecutionPanelProps> = ({
|
|
|
118
149
|
)}
|
|
119
150
|
</div>
|
|
120
151
|
|
|
152
|
+
{/* Environment Selector */}
|
|
153
|
+
<div className="space-y-2">
|
|
154
|
+
<div className="flex items-center gap-2">
|
|
155
|
+
<Layers className="h-3.5 w-3.5 text-muted-foreground" />
|
|
156
|
+
<Label className="text-sm font-medium">Environments</Label>
|
|
157
|
+
<Tooltip content="Fan this check out into one run per environment. The custom fields of each environment are exposed to scripts and templating." />
|
|
158
|
+
</div>
|
|
159
|
+
<p className="text-xs text-muted-foreground">
|
|
160
|
+
Choose how this check fans out across the system's environments.
|
|
161
|
+
</p>
|
|
162
|
+
|
|
163
|
+
<div className="space-y-1.5">
|
|
164
|
+
{envModes.map((m) => (
|
|
165
|
+
<label
|
|
166
|
+
key={m.value}
|
|
167
|
+
className="flex items-start gap-3 p-2.5 rounded-md border hover:bg-muted/30 transition-colors cursor-pointer"
|
|
168
|
+
>
|
|
169
|
+
<input
|
|
170
|
+
type="radio"
|
|
171
|
+
name="environment-mode"
|
|
172
|
+
className="mt-1"
|
|
173
|
+
checked={envMode === m.value}
|
|
174
|
+
disabled={saving || isLocked}
|
|
175
|
+
onChange={() => onSetEnvironmentMode(m.value)}
|
|
176
|
+
/>
|
|
177
|
+
<div className="flex-1 min-w-0">
|
|
178
|
+
<span className="text-sm font-medium">{m.label}</span>
|
|
179
|
+
<p className="text-xs text-muted-foreground mt-0.5">{m.hint}</p>
|
|
180
|
+
</div>
|
|
181
|
+
</label>
|
|
182
|
+
))}
|
|
183
|
+
</div>
|
|
184
|
+
|
|
185
|
+
{envMode === "specific" && (
|
|
186
|
+
<div className="space-y-1.5 pl-6">
|
|
187
|
+
{environments.length === 0 ? (
|
|
188
|
+
<p className="text-sm text-muted-foreground italic py-2">
|
|
189
|
+
This system has no environments. Attach environments to the
|
|
190
|
+
system in the catalog first.
|
|
191
|
+
</p>
|
|
192
|
+
) : (
|
|
193
|
+
environments.map((env) => (
|
|
194
|
+
<div
|
|
195
|
+
key={env.id}
|
|
196
|
+
className="flex items-center gap-3 p-2 rounded-md border hover:bg-muted/30 transition-colors"
|
|
197
|
+
>
|
|
198
|
+
<Checkbox
|
|
199
|
+
checked={selectedEnvIds.has(env.id)}
|
|
200
|
+
onCheckedChange={() => onToggleEnvironment(env.id)}
|
|
201
|
+
disabled={saving || isLocked}
|
|
202
|
+
/>
|
|
203
|
+
<span className="text-sm truncate">{env.name}</span>
|
|
204
|
+
</div>
|
|
205
|
+
))
|
|
206
|
+
)}
|
|
207
|
+
</div>
|
|
208
|
+
)}
|
|
209
|
+
</div>
|
|
210
|
+
|
|
121
211
|
{/* Execution Summary */}
|
|
122
212
|
<div className="p-3 bg-muted/30 rounded-lg border text-xs text-muted-foreground">
|
|
123
213
|
<span className="font-medium">Execution: </span>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
import type { NotificationPolicy } from "@checkstack/healthcheck-common";
|
|
3
|
-
import { Button,
|
|
3
|
+
import { Button, Label, Toggle, Tooltip } from "@checkstack/ui";
|
|
4
4
|
|
|
5
5
|
interface NotificationsPanelProps {
|
|
6
6
|
policy: NotificationPolicy;
|
|
@@ -25,6 +25,12 @@ interface NotificationsPanelProps {
|
|
|
25
25
|
* Panel for configuring per-association notification behaviour. All
|
|
26
26
|
* settings are scoped to a single (system, configuration) assignment
|
|
27
27
|
* — different checks on the same system are independent.
|
|
28
|
+
*
|
|
29
|
+
* Auto-incident opening/closing is no longer configured here: it ships
|
|
30
|
+
* as ordinary user automations. Flapping thresholds likewise moved onto
|
|
31
|
+
* the automation engine's windowed-count gate (the
|
|
32
|
+
* `healthcheck.system_health_changed` trigger's `window` block). What
|
|
33
|
+
* remains is the de-escalation notification preference.
|
|
28
34
|
*/
|
|
29
35
|
export const NotificationsPanel: React.FC<NotificationsPanelProps> = ({
|
|
30
36
|
policy,
|
|
@@ -56,7 +62,7 @@ export const NotificationsPanel: React.FC<NotificationsPanelProps> = ({
|
|
|
56
62
|
<h3 className="text-sm font-semibold">Notifications</h3>
|
|
57
63
|
<p className="text-xs text-muted-foreground mt-1">
|
|
58
64
|
Control which health state transitions notify subscribers for this
|
|
59
|
-
check
|
|
65
|
+
check.
|
|
60
66
|
</p>
|
|
61
67
|
</div>
|
|
62
68
|
|
|
@@ -135,241 +141,6 @@ export const NotificationsPanel: React.FC<NotificationsPanelProps> = ({
|
|
|
135
141
|
</div>
|
|
136
142
|
</div>
|
|
137
143
|
|
|
138
|
-
{/* Auto-open incident */}
|
|
139
|
-
<div className="p-4 bg-muted/50 rounded-lg border space-y-4">
|
|
140
|
-
<div className="flex items-start justify-between gap-4">
|
|
141
|
-
<div className="flex-1 min-w-0">
|
|
142
|
-
<div className="flex items-center gap-2">
|
|
143
|
-
<Label className="text-sm font-medium">
|
|
144
|
-
Auto-open incident when this check is critical
|
|
145
|
-
</Label>
|
|
146
|
-
<Tooltip content="When either trigger below fires, an incident is auto-opened on the system. Different checks on the same system are independent — disabling here only affects this check." />
|
|
147
|
-
</div>
|
|
148
|
-
<p className="text-xs text-muted-foreground mt-1">
|
|
149
|
-
One incident per outage instead of one ping per state change.
|
|
150
|
-
Especially useful for Jira / Slack / email — the incident's
|
|
151
|
-
suppression silences downstream channels for the lifetime of
|
|
152
|
-
the incident.
|
|
153
|
-
</p>
|
|
154
|
-
</div>
|
|
155
|
-
<Toggle
|
|
156
|
-
checked={policy.autoOpenIncidentOnUnhealthy}
|
|
157
|
-
onCheckedChange={(checked: boolean) =>
|
|
158
|
-
onChange({ ...policy, autoOpenIncidentOnUnhealthy: checked })
|
|
159
|
-
}
|
|
160
|
-
disabled={disabled}
|
|
161
|
-
aria-label="Auto-open incident when this check is critical"
|
|
162
|
-
/>
|
|
163
|
-
</div>
|
|
164
|
-
|
|
165
|
-
{policy.autoOpenIncidentOnUnhealthy && (
|
|
166
|
-
<div className="pl-4 border-l-2 border-border space-y-4">
|
|
167
|
-
{/* Suppress further notifications */}
|
|
168
|
-
<div className="flex items-start justify-between gap-4">
|
|
169
|
-
<div className="flex-1 min-w-0">
|
|
170
|
-
<Label className="text-sm">
|
|
171
|
-
Suppress further notifications while open
|
|
172
|
-
</Label>
|
|
173
|
-
<p className="text-xs text-muted-foreground mt-1">
|
|
174
|
-
Email, Jira, Slack all silenced for this system until the
|
|
175
|
-
incident is resolved.
|
|
176
|
-
</p>
|
|
177
|
-
</div>
|
|
178
|
-
<Toggle
|
|
179
|
-
checked={policy.useNotificationSuppression}
|
|
180
|
-
onCheckedChange={(checked: boolean) =>
|
|
181
|
-
onChange({
|
|
182
|
-
...policy,
|
|
183
|
-
useNotificationSuppression: checked,
|
|
184
|
-
})
|
|
185
|
-
}
|
|
186
|
-
disabled={disabled}
|
|
187
|
-
aria-label="Suppress further notifications while open"
|
|
188
|
-
/>
|
|
189
|
-
</div>
|
|
190
|
-
|
|
191
|
-
{/* Skip during maintenance */}
|
|
192
|
-
<div className="flex items-start justify-between gap-4">
|
|
193
|
-
<div className="flex-1 min-w-0">
|
|
194
|
-
<Label className="text-sm">
|
|
195
|
-
Skip during active maintenance
|
|
196
|
-
</Label>
|
|
197
|
-
<p className="text-xs text-muted-foreground mt-1">
|
|
198
|
-
No auto-incident is opened while the system has an active
|
|
199
|
-
maintenance window with suppression.
|
|
200
|
-
</p>
|
|
201
|
-
</div>
|
|
202
|
-
<Toggle
|
|
203
|
-
checked={policy.skipDuringMaintenance}
|
|
204
|
-
onCheckedChange={(checked: boolean) =>
|
|
205
|
-
onChange({ ...policy, skipDuringMaintenance: checked })
|
|
206
|
-
}
|
|
207
|
-
disabled={disabled}
|
|
208
|
-
aria-label="Skip auto-incident during active maintenance"
|
|
209
|
-
/>
|
|
210
|
-
</div>
|
|
211
|
-
|
|
212
|
-
{/* Sustained-duration trigger */}
|
|
213
|
-
<div className="space-y-2 pt-2 border-t border-border">
|
|
214
|
-
<div className="flex items-center justify-between gap-4">
|
|
215
|
-
<div className="flex items-center gap-2">
|
|
216
|
-
<Label className="text-sm">
|
|
217
|
-
Open when unhealthy continuously
|
|
218
|
-
</Label>
|
|
219
|
-
<Tooltip content="Catches real outages: the check has stayed unhealthy for at least this long without recovering." />
|
|
220
|
-
</div>
|
|
221
|
-
<Toggle
|
|
222
|
-
checked={policy.sustainedUnhealthyTrigger.enabled}
|
|
223
|
-
onCheckedChange={(checked: boolean) =>
|
|
224
|
-
onChange({
|
|
225
|
-
...policy,
|
|
226
|
-
sustainedUnhealthyTrigger: {
|
|
227
|
-
...policy.sustainedUnhealthyTrigger,
|
|
228
|
-
enabled: checked,
|
|
229
|
-
},
|
|
230
|
-
})
|
|
231
|
-
}
|
|
232
|
-
disabled={disabled}
|
|
233
|
-
aria-label="Enable sustained-unhealthy trigger"
|
|
234
|
-
/>
|
|
235
|
-
</div>
|
|
236
|
-
{policy.sustainedUnhealthyTrigger.enabled && (
|
|
237
|
-
<div className="flex items-center gap-2 text-xs text-muted-foreground">
|
|
238
|
-
<span>Open after</span>
|
|
239
|
-
<Input
|
|
240
|
-
type="number"
|
|
241
|
-
min={1}
|
|
242
|
-
className="h-8 w-16 text-center"
|
|
243
|
-
value={policy.sustainedUnhealthyTrigger.durationMinutes}
|
|
244
|
-
onChange={(e) =>
|
|
245
|
-
onChange({
|
|
246
|
-
...policy,
|
|
247
|
-
sustainedUnhealthyTrigger: {
|
|
248
|
-
...policy.sustainedUnhealthyTrigger,
|
|
249
|
-
durationMinutes:
|
|
250
|
-
Number.parseInt(e.target.value, 10) || 1,
|
|
251
|
-
},
|
|
252
|
-
})
|
|
253
|
-
}
|
|
254
|
-
disabled={disabled}
|
|
255
|
-
/>
|
|
256
|
-
<span>minutes of continuous unhealthy state</span>
|
|
257
|
-
</div>
|
|
258
|
-
)}
|
|
259
|
-
</div>
|
|
260
|
-
|
|
261
|
-
{/* Flapping trigger */}
|
|
262
|
-
<div className="space-y-2 pt-2 border-t border-border">
|
|
263
|
-
<div className="flex items-center justify-between gap-4">
|
|
264
|
-
<div className="flex items-center gap-2">
|
|
265
|
-
<Label className="text-sm">Open on flapping</Label>
|
|
266
|
-
<Tooltip content="Catches checks that flip in and out of unhealthy too quickly for the sustained trigger to fire." />
|
|
267
|
-
</div>
|
|
268
|
-
<Toggle
|
|
269
|
-
checked={policy.flappingTrigger.enabled}
|
|
270
|
-
onCheckedChange={(checked: boolean) =>
|
|
271
|
-
onChange({
|
|
272
|
-
...policy,
|
|
273
|
-
flappingTrigger: {
|
|
274
|
-
...policy.flappingTrigger,
|
|
275
|
-
enabled: checked,
|
|
276
|
-
},
|
|
277
|
-
})
|
|
278
|
-
}
|
|
279
|
-
disabled={disabled}
|
|
280
|
-
aria-label="Enable flapping trigger"
|
|
281
|
-
/>
|
|
282
|
-
</div>
|
|
283
|
-
{policy.flappingTrigger.enabled && (
|
|
284
|
-
<div className="flex flex-wrap items-center gap-2 text-xs text-muted-foreground">
|
|
285
|
-
<span>Open after</span>
|
|
286
|
-
<Input
|
|
287
|
-
type="number"
|
|
288
|
-
min={1}
|
|
289
|
-
className="h-8 w-16 text-center"
|
|
290
|
-
value={policy.flappingTrigger.transitions}
|
|
291
|
-
onChange={(e) =>
|
|
292
|
-
onChange({
|
|
293
|
-
...policy,
|
|
294
|
-
flappingTrigger: {
|
|
295
|
-
...policy.flappingTrigger,
|
|
296
|
-
transitions:
|
|
297
|
-
Number.parseInt(e.target.value, 10) || 1,
|
|
298
|
-
},
|
|
299
|
-
})
|
|
300
|
-
}
|
|
301
|
-
disabled={disabled}
|
|
302
|
-
/>
|
|
303
|
-
<span>transitions to unhealthy within</span>
|
|
304
|
-
<Input
|
|
305
|
-
type="number"
|
|
306
|
-
min={1}
|
|
307
|
-
className="h-8 w-16 text-center"
|
|
308
|
-
value={policy.flappingTrigger.windowMinutes}
|
|
309
|
-
onChange={(e) =>
|
|
310
|
-
onChange({
|
|
311
|
-
...policy,
|
|
312
|
-
flappingTrigger: {
|
|
313
|
-
...policy.flappingTrigger,
|
|
314
|
-
windowMinutes:
|
|
315
|
-
Number.parseInt(e.target.value, 10) || 1,
|
|
316
|
-
},
|
|
317
|
-
})
|
|
318
|
-
}
|
|
319
|
-
disabled={disabled}
|
|
320
|
-
/>
|
|
321
|
-
<span>minutes</span>
|
|
322
|
-
</div>
|
|
323
|
-
)}
|
|
324
|
-
</div>
|
|
325
|
-
|
|
326
|
-
{/* Auto-close cooldown */}
|
|
327
|
-
<div className="space-y-2 pt-2 border-t border-border">
|
|
328
|
-
<div className="flex items-center gap-2">
|
|
329
|
-
<Label className="text-sm">Auto-close cooldown</Label>
|
|
330
|
-
<Tooltip content="Resolve the auto-incident once the system has stayed healthy for this long. Snapshotted per-incident at open time — later policy edits don't change in-flight incidents." />
|
|
331
|
-
</div>
|
|
332
|
-
<div className="flex flex-wrap items-center gap-3 text-xs text-muted-foreground">
|
|
333
|
-
<label className="inline-flex items-center gap-2 cursor-pointer">
|
|
334
|
-
<input
|
|
335
|
-
type="checkbox"
|
|
336
|
-
checked={policy.autoCloseAfterMinutes === null}
|
|
337
|
-
onChange={(e) =>
|
|
338
|
-
onChange({
|
|
339
|
-
...policy,
|
|
340
|
-
autoCloseAfterMinutes: e.target.checked ? null : 30,
|
|
341
|
-
})
|
|
342
|
-
}
|
|
343
|
-
disabled={disabled}
|
|
344
|
-
/>
|
|
345
|
-
<span>Never auto-close (manual resolve only)</span>
|
|
346
|
-
</label>
|
|
347
|
-
{policy.autoCloseAfterMinutes !== null && (
|
|
348
|
-
<div className="flex items-center gap-2">
|
|
349
|
-
<span>After</span>
|
|
350
|
-
<Input
|
|
351
|
-
type="number"
|
|
352
|
-
min={1}
|
|
353
|
-
className="h-8 w-16 text-center"
|
|
354
|
-
value={policy.autoCloseAfterMinutes}
|
|
355
|
-
onChange={(e) =>
|
|
356
|
-
onChange({
|
|
357
|
-
...policy,
|
|
358
|
-
autoCloseAfterMinutes:
|
|
359
|
-
Number.parseInt(e.target.value, 10) || 1,
|
|
360
|
-
})
|
|
361
|
-
}
|
|
362
|
-
disabled={disabled}
|
|
363
|
-
/>
|
|
364
|
-
<span>minutes of sustained healthy</span>
|
|
365
|
-
</div>
|
|
366
|
-
)}
|
|
367
|
-
</div>
|
|
368
|
-
</div>
|
|
369
|
-
</div>
|
|
370
|
-
)}
|
|
371
|
-
</div>
|
|
372
|
-
|
|
373
144
|
{/* Save button hides when the assignment is inheriting — there
|
|
374
145
|
is nothing to save. The Override button drives the transition
|
|
375
146
|
into edit mode. */}
|