@checkstack/healthcheck-frontend 0.12.1 → 0.13.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 +63 -0
- package/package.json +11 -10
- package/src/components/HealthCheckRunsTable.tsx +19 -1
- package/src/components/HealthCheckSystemOverview.tsx +79 -6
- package/src/components/SystemHealthCheckAssignment.tsx +34 -825
- package/src/components/assignments/AssignmentTree.tsx +123 -0
- package/src/components/assignments/ExecutionPanel.tsx +135 -0
- package/src/components/assignments/GeneralPanel.tsx +100 -0
- package/src/components/assignments/RetentionPanel.tsx +157 -0
- package/src/components/assignments/ThresholdsPanel.tsx +254 -0
- package/src/components/editor/EditorTree.tsx +10 -75
- package/src/components/editor/IDEStatusBar.tsx +5 -58
- package/src/hooks/useHealthCheckData.ts +4 -0
- package/src/index.tsx +7 -0
- package/src/pages/AssignmentIDEPage.tsx +477 -0
- package/src/pages/HealthCheckHistoryDetailPage.tsx +53 -6
- package/src/pages/HealthCheckIDEPage.tsx +7 -15
|
@@ -1,855 +1,64 @@
|
|
|
1
|
-
import React
|
|
1
|
+
import React from "react";
|
|
2
2
|
import {
|
|
3
3
|
usePluginClient,
|
|
4
|
-
useApi,
|
|
5
4
|
type SlotContext,
|
|
5
|
+
useApi,
|
|
6
6
|
accessApiRef,
|
|
7
7
|
} from "@checkstack/frontend-api";
|
|
8
8
|
import { HealthCheckApi } from "@checkstack/healthcheck-common";
|
|
9
|
-
import {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
DialogContent,
|
|
13
|
-
DialogDescription,
|
|
14
|
-
DialogHeader,
|
|
15
|
-
DialogTitle,
|
|
16
|
-
DialogFooter,
|
|
17
|
-
Checkbox,
|
|
18
|
-
Label,
|
|
19
|
-
LoadingSpinner,
|
|
20
|
-
useToast,
|
|
21
|
-
Select,
|
|
22
|
-
SelectContent,
|
|
23
|
-
SelectItem,
|
|
24
|
-
SelectTrigger,
|
|
25
|
-
SelectValue,
|
|
26
|
-
Input,
|
|
27
|
-
Tooltip,
|
|
28
|
-
} from "@checkstack/ui";
|
|
29
|
-
import { Activity, Settings2, History, Database } from "lucide-react";
|
|
30
|
-
import { Link } from "react-router-dom";
|
|
9
|
+
import { Button } from "@checkstack/ui";
|
|
10
|
+
import { Activity } from "lucide-react";
|
|
11
|
+
import { useNavigate } from "react-router-dom";
|
|
31
12
|
import { CatalogSystemActionsSlot } from "@checkstack/catalog-common";
|
|
32
|
-
import type { StateThresholds } from "@checkstack/healthcheck-common";
|
|
33
13
|
import {
|
|
34
|
-
DEFAULT_STATE_THRESHOLDS,
|
|
35
14
|
healthcheckRoutes,
|
|
36
15
|
healthCheckAccess,
|
|
37
16
|
} from "@checkstack/healthcheck-common";
|
|
38
|
-
import { resolveRoute
|
|
39
|
-
import { DEFAULT_RETENTION_CONFIG } from "@checkstack/healthcheck-common";
|
|
40
|
-
|
|
41
|
-
type SelectedPanel = { configId: string; panel: "thresholds" | "retention" };
|
|
17
|
+
import { resolveRoute } from "@checkstack/common";
|
|
42
18
|
|
|
43
19
|
type Props = SlotContext<typeof CatalogSystemActionsSlot>;
|
|
44
20
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
stateThresholds?: StateThresholds;
|
|
50
|
-
}
|
|
51
|
-
|
|
21
|
+
/**
|
|
22
|
+
* Extension slot button for the catalog system actions bar.
|
|
23
|
+
* Navigates to the full-page Assignment IDE for the given system.
|
|
24
|
+
*/
|
|
52
25
|
export const SystemHealthCheckAssignment: React.FC<Props> = ({
|
|
53
26
|
systemId,
|
|
54
|
-
systemName: _systemName,
|
|
55
27
|
}) => {
|
|
56
28
|
const healthCheckClient = usePluginClient(HealthCheckApi);
|
|
57
29
|
const accessApi = useApi(accessApiRef);
|
|
58
30
|
const { allowed: canManage } = accessApi.useAccess(
|
|
59
|
-
healthCheckAccess.configuration.manage
|
|
31
|
+
healthCheckAccess.configuration.manage,
|
|
60
32
|
);
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
// UI state
|
|
64
|
-
const [isOpen, setIsOpen] = useState(false);
|
|
65
|
-
const [selectedPanel, setSelectedPanel] = useState<SelectedPanel>();
|
|
66
|
-
const [localThresholds, setLocalThresholds] = useState<
|
|
67
|
-
Record<string, StateThresholds>
|
|
68
|
-
>({});
|
|
69
|
-
const [retentionData, setRetentionData] = useState<
|
|
70
|
-
Record<
|
|
71
|
-
string,
|
|
72
|
-
{
|
|
73
|
-
rawRetentionDays: number;
|
|
74
|
-
hourlyRetentionDays: number;
|
|
75
|
-
dailyRetentionDays: number;
|
|
76
|
-
isCustom: boolean;
|
|
77
|
-
}
|
|
78
|
-
>
|
|
79
|
-
>({});
|
|
33
|
+
const navigate = useNavigate();
|
|
80
34
|
|
|
81
|
-
//
|
|
82
|
-
const { data:
|
|
83
|
-
healthCheckClient.
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
const {
|
|
87
|
-
data: associations = [],
|
|
88
|
-
isLoading: associationsLoading,
|
|
89
|
-
refetch: refetchAssociations,
|
|
90
|
-
} = healthCheckClient.getSystemAssociations.useQuery(
|
|
91
|
-
{ systemId },
|
|
92
|
-
{ enabled: true }
|
|
93
|
-
);
|
|
94
|
-
|
|
95
|
-
// Query: Fetch retention config for selected panel
|
|
96
|
-
const selectedConfigId =
|
|
97
|
-
selectedPanel?.panel === "retention" ? selectedPanel.configId : undefined;
|
|
98
|
-
const { data: retentionConfigData } =
|
|
99
|
-
healthCheckClient.getRetentionConfig.useQuery(
|
|
100
|
-
{ systemId, configurationId: selectedConfigId ?? "" },
|
|
101
|
-
{ enabled: !!selectedConfigId && !retentionData[selectedConfigId] }
|
|
35
|
+
// Fetch associations count for the badge
|
|
36
|
+
const { data: associations = [] } =
|
|
37
|
+
healthCheckClient.getSystemAssociations.useQuery(
|
|
38
|
+
{ systemId },
|
|
39
|
+
{ enabled: true },
|
|
102
40
|
);
|
|
103
41
|
|
|
104
|
-
|
|
105
|
-
React.useEffect(() => {
|
|
106
|
-
if (
|
|
107
|
-
retentionConfigData &&
|
|
108
|
-
selectedConfigId &&
|
|
109
|
-
!retentionData[selectedConfigId]
|
|
110
|
-
) {
|
|
111
|
-
setRetentionData((prev) => ({
|
|
112
|
-
...prev,
|
|
113
|
-
[selectedConfigId]: {
|
|
114
|
-
rawRetentionDays:
|
|
115
|
-
retentionConfigData.retentionConfig?.rawRetentionDays ??
|
|
116
|
-
DEFAULT_RETENTION_CONFIG.rawRetentionDays,
|
|
117
|
-
hourlyRetentionDays:
|
|
118
|
-
retentionConfigData.retentionConfig?.hourlyRetentionDays ??
|
|
119
|
-
DEFAULT_RETENTION_CONFIG.hourlyRetentionDays,
|
|
120
|
-
dailyRetentionDays:
|
|
121
|
-
retentionConfigData.retentionConfig?.dailyRetentionDays ??
|
|
122
|
-
DEFAULT_RETENTION_CONFIG.dailyRetentionDays,
|
|
123
|
-
isCustom: !!retentionConfigData.retentionConfig,
|
|
124
|
-
},
|
|
125
|
-
}));
|
|
126
|
-
}
|
|
127
|
-
}, [retentionConfigData, selectedConfigId, retentionData]);
|
|
42
|
+
if (!canManage) return;
|
|
128
43
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
onSuccess: () => {
|
|
132
|
-
void refetchAssociations();
|
|
133
|
-
},
|
|
134
|
-
onError: (error) => {
|
|
135
|
-
toast.error(extractErrorMessage(error, "Failed to update"));
|
|
136
|
-
},
|
|
44
|
+
const assignmentUrl = resolveRoute(healthcheckRoutes.routes.assignments, {
|
|
45
|
+
systemId,
|
|
137
46
|
});
|
|
138
47
|
|
|
139
|
-
// Mutation: Disassociate system
|
|
140
|
-
const disassociateMutation = healthCheckClient.disassociateSystem.useMutation(
|
|
141
|
-
{
|
|
142
|
-
onSuccess: () => {
|
|
143
|
-
void refetchAssociations();
|
|
144
|
-
},
|
|
145
|
-
onError: (error) => {
|
|
146
|
-
toast.error(
|
|
147
|
-
extractErrorMessage(error, "Failed to update")
|
|
148
|
-
);
|
|
149
|
-
},
|
|
150
|
-
}
|
|
151
|
-
);
|
|
152
|
-
|
|
153
|
-
// Mutation: Update retention config
|
|
154
|
-
const updateRetentionMutation =
|
|
155
|
-
healthCheckClient.updateRetentionConfig.useMutation({
|
|
156
|
-
onSuccess: () => {
|
|
157
|
-
toast.success("Retention settings saved");
|
|
158
|
-
setSelectedPanel(undefined);
|
|
159
|
-
},
|
|
160
|
-
onError: (error) => {
|
|
161
|
-
toast.error(extractErrorMessage(error, "Failed to save"));
|
|
162
|
-
},
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
const configs = configurationsData?.configurations ?? [];
|
|
166
|
-
const loading = configsLoading || associationsLoading;
|
|
167
|
-
const saving =
|
|
168
|
-
associateMutation.isPending ||
|
|
169
|
-
disassociateMutation.isPending ||
|
|
170
|
-
updateRetentionMutation.isPending;
|
|
171
|
-
|
|
172
|
-
const handleToggleAssignment = (
|
|
173
|
-
configId: string,
|
|
174
|
-
isCurrentlyAssigned: boolean
|
|
175
|
-
) => {
|
|
176
|
-
const config = configs.find((c) => c.id === configId);
|
|
177
|
-
if (!config) return;
|
|
178
|
-
|
|
179
|
-
if (isCurrentlyAssigned) {
|
|
180
|
-
disassociateMutation.mutate({ systemId, configId });
|
|
181
|
-
} else {
|
|
182
|
-
associateMutation.mutate({
|
|
183
|
-
systemId,
|
|
184
|
-
body: {
|
|
185
|
-
configurationId: configId,
|
|
186
|
-
enabled: true,
|
|
187
|
-
stateThresholds: DEFAULT_STATE_THRESHOLDS,
|
|
188
|
-
},
|
|
189
|
-
});
|
|
190
|
-
}
|
|
191
|
-
};
|
|
192
|
-
|
|
193
|
-
const handleThresholdChange = (
|
|
194
|
-
configId: string,
|
|
195
|
-
thresholds: StateThresholds
|
|
196
|
-
) => {
|
|
197
|
-
setLocalThresholds((prev) => ({
|
|
198
|
-
...prev,
|
|
199
|
-
[configId]: thresholds,
|
|
200
|
-
}));
|
|
201
|
-
};
|
|
202
|
-
|
|
203
|
-
const handleSaveThresholds = (configId: string) => {
|
|
204
|
-
const assoc = associations.find((a) => a.configurationId === configId);
|
|
205
|
-
const thresholds = localThresholds[configId] ?? assoc?.stateThresholds;
|
|
206
|
-
if (!assoc) return;
|
|
207
|
-
|
|
208
|
-
associateMutation.mutate(
|
|
209
|
-
{
|
|
210
|
-
systemId,
|
|
211
|
-
body: {
|
|
212
|
-
configurationId: configId,
|
|
213
|
-
enabled: assoc.enabled,
|
|
214
|
-
stateThresholds: thresholds,
|
|
215
|
-
},
|
|
216
|
-
},
|
|
217
|
-
{
|
|
218
|
-
onSuccess: () => {
|
|
219
|
-
toast.success("Thresholds saved");
|
|
220
|
-
setSelectedPanel(undefined);
|
|
221
|
-
setLocalThresholds((prev) => {
|
|
222
|
-
const next = { ...prev };
|
|
223
|
-
delete next[configId];
|
|
224
|
-
return next;
|
|
225
|
-
});
|
|
226
|
-
},
|
|
227
|
-
}
|
|
228
|
-
);
|
|
229
|
-
};
|
|
230
|
-
|
|
231
|
-
const handleSaveRetention = (configId: string) => {
|
|
232
|
-
const data = retentionData[configId];
|
|
233
|
-
if (!data) return;
|
|
234
|
-
|
|
235
|
-
updateRetentionMutation.mutate({
|
|
236
|
-
systemId,
|
|
237
|
-
configurationId: configId,
|
|
238
|
-
retentionConfig: {
|
|
239
|
-
rawRetentionDays: data.rawRetentionDays,
|
|
240
|
-
hourlyRetentionDays: data.hourlyRetentionDays,
|
|
241
|
-
dailyRetentionDays: data.dailyRetentionDays,
|
|
242
|
-
},
|
|
243
|
-
});
|
|
244
|
-
};
|
|
245
|
-
|
|
246
|
-
const handleResetRetention = (configId: string) => {
|
|
247
|
-
updateRetentionMutation.mutate(
|
|
248
|
-
{
|
|
249
|
-
systemId,
|
|
250
|
-
configurationId: configId,
|
|
251
|
-
// eslint-disable-next-line unicorn/no-null -- RPC contract uses nullable()
|
|
252
|
-
retentionConfig: null,
|
|
253
|
-
},
|
|
254
|
-
{
|
|
255
|
-
onSuccess: () => {
|
|
256
|
-
setRetentionData((prev) => ({
|
|
257
|
-
...prev,
|
|
258
|
-
[configId]: {
|
|
259
|
-
rawRetentionDays: DEFAULT_RETENTION_CONFIG.rawRetentionDays,
|
|
260
|
-
hourlyRetentionDays: DEFAULT_RETENTION_CONFIG.hourlyRetentionDays,
|
|
261
|
-
dailyRetentionDays: DEFAULT_RETENTION_CONFIG.dailyRetentionDays,
|
|
262
|
-
isCustom: false,
|
|
263
|
-
},
|
|
264
|
-
}));
|
|
265
|
-
toast.success("Reset to defaults");
|
|
266
|
-
},
|
|
267
|
-
}
|
|
268
|
-
);
|
|
269
|
-
};
|
|
270
|
-
|
|
271
|
-
const updateRetentionField = (
|
|
272
|
-
configId: string,
|
|
273
|
-
field: string,
|
|
274
|
-
value: number
|
|
275
|
-
) => {
|
|
276
|
-
setRetentionData((prev) => ({
|
|
277
|
-
...prev,
|
|
278
|
-
[configId]: { ...prev[configId], [field]: value, isCustom: true },
|
|
279
|
-
}));
|
|
280
|
-
};
|
|
281
|
-
|
|
282
|
-
const assignedIds = associations.map((a) => a.configurationId);
|
|
283
|
-
|
|
284
|
-
const getEffectiveThresholds = (assoc: AssociationState): StateThresholds => {
|
|
285
|
-
return (
|
|
286
|
-
localThresholds[assoc.configurationId] ??
|
|
287
|
-
assoc.stateThresholds ??
|
|
288
|
-
DEFAULT_STATE_THRESHOLDS
|
|
289
|
-
);
|
|
290
|
-
};
|
|
291
|
-
|
|
292
|
-
const renderThresholdEditor = (assoc: AssociationState) => {
|
|
293
|
-
const thresholds = getEffectiveThresholds(assoc);
|
|
294
|
-
|
|
295
|
-
return (
|
|
296
|
-
<div className="mt-4 space-y-4">
|
|
297
|
-
{/* Mode Selector */}
|
|
298
|
-
<div className="p-4 bg-muted/50 rounded-lg border">
|
|
299
|
-
<div className="flex items-center gap-2 mb-2">
|
|
300
|
-
<Label className="text-sm font-medium">Evaluation Mode</Label>
|
|
301
|
-
<Tooltip content="How health status is calculated based on check results" />
|
|
302
|
-
</div>
|
|
303
|
-
<Select
|
|
304
|
-
value={thresholds.mode}
|
|
305
|
-
onValueChange={(value: "consecutive" | "window") => {
|
|
306
|
-
if (value === "consecutive") {
|
|
307
|
-
handleThresholdChange(assoc.configurationId, {
|
|
308
|
-
mode: "consecutive",
|
|
309
|
-
healthy: { minSuccessCount: 1 },
|
|
310
|
-
degraded: { minFailureCount: 2 },
|
|
311
|
-
unhealthy: { minFailureCount: 5 },
|
|
312
|
-
});
|
|
313
|
-
} else {
|
|
314
|
-
handleThresholdChange(assoc.configurationId, {
|
|
315
|
-
mode: "window",
|
|
316
|
-
windowSize: 10,
|
|
317
|
-
degraded: { minFailureCount: 3 },
|
|
318
|
-
unhealthy: { minFailureCount: 7 },
|
|
319
|
-
});
|
|
320
|
-
}
|
|
321
|
-
}}
|
|
322
|
-
>
|
|
323
|
-
<SelectTrigger className="w-full">
|
|
324
|
-
<SelectValue />
|
|
325
|
-
</SelectTrigger>
|
|
326
|
-
<SelectContent>
|
|
327
|
-
<SelectItem value="consecutive">
|
|
328
|
-
Consecutive (streak-based)
|
|
329
|
-
</SelectItem>
|
|
330
|
-
<SelectItem value="window">
|
|
331
|
-
Window (count in last N runs)
|
|
332
|
-
</SelectItem>
|
|
333
|
-
</SelectContent>
|
|
334
|
-
</Select>
|
|
335
|
-
<p className="text-xs text-muted-foreground mt-2">
|
|
336
|
-
{thresholds.mode === "consecutive"
|
|
337
|
-
? "Status changes when a streak of consecutive results is reached."
|
|
338
|
-
: "Status is based on how many failures occur within a rolling window."}
|
|
339
|
-
</p>
|
|
340
|
-
</div>
|
|
341
|
-
|
|
342
|
-
{/* Threshold Configuration Cards */}
|
|
343
|
-
{thresholds.mode === "consecutive" ? (
|
|
344
|
-
<div className="space-y-3">
|
|
345
|
-
{/* Healthy Threshold */}
|
|
346
|
-
<div className="p-3 rounded-lg border border-success/30 bg-success/5">
|
|
347
|
-
<div className="flex items-center justify-between">
|
|
348
|
-
<div className="flex items-center gap-2">
|
|
349
|
-
<div className="h-2 w-2 rounded-full bg-success" />
|
|
350
|
-
<span className="text-sm font-medium text-success">
|
|
351
|
-
Healthy
|
|
352
|
-
</span>
|
|
353
|
-
<Tooltip content="System returns to healthy after this many consecutive successful checks" />
|
|
354
|
-
</div>
|
|
355
|
-
<div className="flex items-center gap-2">
|
|
356
|
-
<Input
|
|
357
|
-
type="number"
|
|
358
|
-
min={1}
|
|
359
|
-
value={thresholds.healthy.minSuccessCount}
|
|
360
|
-
onChange={(e) =>
|
|
361
|
-
handleThresholdChange(assoc.configurationId, {
|
|
362
|
-
...thresholds,
|
|
363
|
-
healthy: {
|
|
364
|
-
minSuccessCount: Number.parseInt(e.target.value) || 1,
|
|
365
|
-
},
|
|
366
|
-
})
|
|
367
|
-
}
|
|
368
|
-
className="h-8 w-16 text-center"
|
|
369
|
-
/>
|
|
370
|
-
<span className="text-xs text-muted-foreground w-20">
|
|
371
|
-
consecutive ✓
|
|
372
|
-
</span>
|
|
373
|
-
</div>
|
|
374
|
-
</div>
|
|
375
|
-
</div>
|
|
376
|
-
|
|
377
|
-
{/* Degraded Threshold */}
|
|
378
|
-
<div className="p-3 rounded-lg border border-warning/30 bg-warning/5">
|
|
379
|
-
<div className="flex items-center justify-between">
|
|
380
|
-
<div className="flex items-center gap-2">
|
|
381
|
-
<div className="h-2 w-2 rounded-full bg-warning" />
|
|
382
|
-
<span className="text-sm font-medium text-warning">
|
|
383
|
-
Degraded
|
|
384
|
-
</span>
|
|
385
|
-
<Tooltip content="System becomes degraded after this many consecutive failures" />
|
|
386
|
-
</div>
|
|
387
|
-
<div className="flex items-center gap-2">
|
|
388
|
-
<Input
|
|
389
|
-
type="number"
|
|
390
|
-
min={1}
|
|
391
|
-
value={thresholds.degraded.minFailureCount}
|
|
392
|
-
onChange={(e) =>
|
|
393
|
-
handleThresholdChange(assoc.configurationId, {
|
|
394
|
-
...thresholds,
|
|
395
|
-
degraded: {
|
|
396
|
-
minFailureCount: Number.parseInt(e.target.value) || 1,
|
|
397
|
-
},
|
|
398
|
-
})
|
|
399
|
-
}
|
|
400
|
-
className="h-8 w-16 text-center"
|
|
401
|
-
/>
|
|
402
|
-
<span className="text-xs text-muted-foreground w-20">
|
|
403
|
-
consecutive ✗
|
|
404
|
-
</span>
|
|
405
|
-
</div>
|
|
406
|
-
</div>
|
|
407
|
-
</div>
|
|
408
|
-
|
|
409
|
-
{/* Unhealthy Threshold */}
|
|
410
|
-
<div className="p-3 rounded-lg border border-destructive/30 bg-destructive/5">
|
|
411
|
-
<div className="flex items-center justify-between">
|
|
412
|
-
<div className="flex items-center gap-2">
|
|
413
|
-
<div className="h-2 w-2 rounded-full bg-destructive" />
|
|
414
|
-
<span className="text-sm font-medium text-destructive">
|
|
415
|
-
Unhealthy
|
|
416
|
-
</span>
|
|
417
|
-
<Tooltip content="System becomes unhealthy after this many consecutive failures" />
|
|
418
|
-
</div>
|
|
419
|
-
<div className="flex items-center gap-2">
|
|
420
|
-
<Input
|
|
421
|
-
type="number"
|
|
422
|
-
min={1}
|
|
423
|
-
value={thresholds.unhealthy.minFailureCount}
|
|
424
|
-
onChange={(e) =>
|
|
425
|
-
handleThresholdChange(assoc.configurationId, {
|
|
426
|
-
...thresholds,
|
|
427
|
-
unhealthy: {
|
|
428
|
-
minFailureCount: Number.parseInt(e.target.value) || 1,
|
|
429
|
-
},
|
|
430
|
-
})
|
|
431
|
-
}
|
|
432
|
-
className="h-8 w-16 text-center"
|
|
433
|
-
/>
|
|
434
|
-
<span className="text-xs text-muted-foreground w-20">
|
|
435
|
-
consecutive ✗
|
|
436
|
-
</span>
|
|
437
|
-
</div>
|
|
438
|
-
</div>
|
|
439
|
-
</div>
|
|
440
|
-
</div>
|
|
441
|
-
) : (
|
|
442
|
-
<div className="space-y-3">
|
|
443
|
-
{/* Window Size */}
|
|
444
|
-
<div className="p-3 rounded-lg border bg-muted/30">
|
|
445
|
-
<div className="flex items-center justify-between">
|
|
446
|
-
<div className="flex items-center gap-2">
|
|
447
|
-
<span className="text-sm font-medium">Window Size</span>
|
|
448
|
-
<Tooltip content="How many recent runs to analyze when calculating status" />
|
|
449
|
-
</div>
|
|
450
|
-
<div className="flex items-center gap-2">
|
|
451
|
-
<Input
|
|
452
|
-
type="number"
|
|
453
|
-
min={3}
|
|
454
|
-
max={100}
|
|
455
|
-
value={thresholds.windowSize}
|
|
456
|
-
onChange={(e) =>
|
|
457
|
-
handleThresholdChange(assoc.configurationId, {
|
|
458
|
-
...thresholds,
|
|
459
|
-
windowSize: Number.parseInt(e.target.value) || 10,
|
|
460
|
-
})
|
|
461
|
-
}
|
|
462
|
-
className="h-8 w-16 text-center"
|
|
463
|
-
/>
|
|
464
|
-
<span className="text-xs text-muted-foreground">runs</span>
|
|
465
|
-
</div>
|
|
466
|
-
</div>
|
|
467
|
-
</div>
|
|
468
|
-
|
|
469
|
-
{/* Degraded Threshold */}
|
|
470
|
-
<div className="p-3 rounded-lg border border-warning/30 bg-warning/5">
|
|
471
|
-
<div className="flex items-center justify-between">
|
|
472
|
-
<div className="flex items-center gap-2">
|
|
473
|
-
<div className="h-2 w-2 rounded-full bg-warning" />
|
|
474
|
-
<span className="text-sm font-medium text-warning">
|
|
475
|
-
Degraded
|
|
476
|
-
</span>
|
|
477
|
-
<Tooltip content="System becomes degraded when failures in the window reach this count" />
|
|
478
|
-
</div>
|
|
479
|
-
<div className="flex items-center gap-2">
|
|
480
|
-
<span className="text-xs text-muted-foreground">≥</span>
|
|
481
|
-
<Input
|
|
482
|
-
type="number"
|
|
483
|
-
min={1}
|
|
484
|
-
value={thresholds.degraded.minFailureCount}
|
|
485
|
-
onChange={(e) =>
|
|
486
|
-
handleThresholdChange(assoc.configurationId, {
|
|
487
|
-
...thresholds,
|
|
488
|
-
degraded: {
|
|
489
|
-
minFailureCount: Number.parseInt(e.target.value) || 1,
|
|
490
|
-
},
|
|
491
|
-
})
|
|
492
|
-
}
|
|
493
|
-
className="h-8 w-16 text-center"
|
|
494
|
-
/>
|
|
495
|
-
<span className="text-xs text-muted-foreground">
|
|
496
|
-
failures
|
|
497
|
-
</span>
|
|
498
|
-
</div>
|
|
499
|
-
</div>
|
|
500
|
-
</div>
|
|
501
|
-
|
|
502
|
-
{/* Unhealthy Threshold */}
|
|
503
|
-
<div className="p-3 rounded-lg border border-destructive/30 bg-destructive/5">
|
|
504
|
-
<div className="flex items-center justify-between">
|
|
505
|
-
<div className="flex items-center gap-2">
|
|
506
|
-
<div className="h-2 w-2 rounded-full bg-destructive" />
|
|
507
|
-
<span className="text-sm font-medium text-destructive">
|
|
508
|
-
Unhealthy
|
|
509
|
-
</span>
|
|
510
|
-
<Tooltip content="System becomes unhealthy when failures in the window reach this count" />
|
|
511
|
-
</div>
|
|
512
|
-
<div className="flex items-center gap-2">
|
|
513
|
-
<span className="text-xs text-muted-foreground">≥</span>
|
|
514
|
-
<Input
|
|
515
|
-
type="number"
|
|
516
|
-
min={1}
|
|
517
|
-
value={thresholds.unhealthy.minFailureCount}
|
|
518
|
-
onChange={(e) =>
|
|
519
|
-
handleThresholdChange(assoc.configurationId, {
|
|
520
|
-
...thresholds,
|
|
521
|
-
unhealthy: {
|
|
522
|
-
minFailureCount: Number.parseInt(e.target.value) || 1,
|
|
523
|
-
},
|
|
524
|
-
})
|
|
525
|
-
}
|
|
526
|
-
className="h-8 w-16 text-center"
|
|
527
|
-
/>
|
|
528
|
-
<span className="text-xs text-muted-foreground">
|
|
529
|
-
failures
|
|
530
|
-
</span>
|
|
531
|
-
</div>
|
|
532
|
-
</div>
|
|
533
|
-
</div>
|
|
534
|
-
</div>
|
|
535
|
-
)}
|
|
536
|
-
|
|
537
|
-
{/* Action Buttons */}
|
|
538
|
-
<div className="flex justify-end gap-2 pt-2 border-t">
|
|
539
|
-
<Button
|
|
540
|
-
variant="outline"
|
|
541
|
-
size="sm"
|
|
542
|
-
onClick={() => setSelectedPanel(undefined)}
|
|
543
|
-
>
|
|
544
|
-
Cancel
|
|
545
|
-
</Button>
|
|
546
|
-
<Button
|
|
547
|
-
size="sm"
|
|
548
|
-
onClick={() => handleSaveThresholds(assoc.configurationId)}
|
|
549
|
-
disabled={saving}
|
|
550
|
-
>
|
|
551
|
-
{saving ? "Saving..." : "Save Thresholds"}
|
|
552
|
-
</Button>
|
|
553
|
-
</div>
|
|
554
|
-
</div>
|
|
555
|
-
);
|
|
556
|
-
};
|
|
557
|
-
|
|
558
|
-
const renderRetentionEditor = (configId: string) => {
|
|
559
|
-
const data = retentionData[configId];
|
|
560
|
-
|
|
561
|
-
if (!data) {
|
|
562
|
-
return (
|
|
563
|
-
<div className="mt-4 flex justify-center py-4">
|
|
564
|
-
<LoadingSpinner />
|
|
565
|
-
</div>
|
|
566
|
-
);
|
|
567
|
-
}
|
|
568
|
-
|
|
569
|
-
// Validation
|
|
570
|
-
const isValidHierarchy =
|
|
571
|
-
data.rawRetentionDays < data.hourlyRetentionDays &&
|
|
572
|
-
data.hourlyRetentionDays < data.dailyRetentionDays;
|
|
573
|
-
|
|
574
|
-
return (
|
|
575
|
-
<div className="mt-4 space-y-3">
|
|
576
|
-
{!data.isCustom && (
|
|
577
|
-
<div className="rounded-md bg-muted p-3 text-sm text-muted-foreground">
|
|
578
|
-
Using default retention settings. Customize below to override.
|
|
579
|
-
</div>
|
|
580
|
-
)}
|
|
581
|
-
|
|
582
|
-
{!isValidHierarchy && (
|
|
583
|
-
<div className="rounded-md bg-destructive/10 border border-destructive/30 p-3 text-sm text-destructive">
|
|
584
|
-
Retention periods must increase: Raw < Hourly < Daily
|
|
585
|
-
</div>
|
|
586
|
-
)}
|
|
587
|
-
|
|
588
|
-
{/* Raw Data */}
|
|
589
|
-
<div className="p-3 rounded-lg border bg-muted/30">
|
|
590
|
-
<div className="flex items-center justify-between">
|
|
591
|
-
<div>
|
|
592
|
-
<span className="text-sm font-medium">Raw Data Retention</span>
|
|
593
|
-
<p className="text-xs text-muted-foreground">
|
|
594
|
-
Individual run data before hourly aggregation
|
|
595
|
-
</p>
|
|
596
|
-
</div>
|
|
597
|
-
<div className="flex items-center gap-2">
|
|
598
|
-
<Input
|
|
599
|
-
type="number"
|
|
600
|
-
min={1}
|
|
601
|
-
max={30}
|
|
602
|
-
value={data.rawRetentionDays}
|
|
603
|
-
onChange={(e) =>
|
|
604
|
-
updateRetentionField(
|
|
605
|
-
configId,
|
|
606
|
-
"rawRetentionDays",
|
|
607
|
-
Number(e.target.value)
|
|
608
|
-
)
|
|
609
|
-
}
|
|
610
|
-
className="h-8 w-20 text-center"
|
|
611
|
-
/>
|
|
612
|
-
<span className="text-sm text-muted-foreground w-10">days</span>
|
|
613
|
-
</div>
|
|
614
|
-
</div>
|
|
615
|
-
</div>
|
|
616
|
-
|
|
617
|
-
{/* Hourly Aggregates */}
|
|
618
|
-
<div className="p-3 rounded-lg border bg-muted/30">
|
|
619
|
-
<div className="flex items-center justify-between">
|
|
620
|
-
<div>
|
|
621
|
-
<span className="text-sm font-medium">Hourly Aggregates</span>
|
|
622
|
-
<p className="text-xs text-muted-foreground">
|
|
623
|
-
Hourly stats before daily rollup
|
|
624
|
-
</p>
|
|
625
|
-
</div>
|
|
626
|
-
<div className="flex items-center gap-2">
|
|
627
|
-
<Input
|
|
628
|
-
type="number"
|
|
629
|
-
min={7}
|
|
630
|
-
max={365}
|
|
631
|
-
value={data.hourlyRetentionDays}
|
|
632
|
-
onChange={(e) =>
|
|
633
|
-
updateRetentionField(
|
|
634
|
-
configId,
|
|
635
|
-
"hourlyRetentionDays",
|
|
636
|
-
Number(e.target.value)
|
|
637
|
-
)
|
|
638
|
-
}
|
|
639
|
-
className="h-8 w-20 text-center"
|
|
640
|
-
/>
|
|
641
|
-
<span className="text-sm text-muted-foreground w-10">days</span>
|
|
642
|
-
</div>
|
|
643
|
-
</div>
|
|
644
|
-
</div>
|
|
645
|
-
|
|
646
|
-
{/* Daily Aggregates */}
|
|
647
|
-
<div className="p-3 rounded-lg border bg-muted/30">
|
|
648
|
-
<div className="flex items-center justify-between">
|
|
649
|
-
<div>
|
|
650
|
-
<span className="text-sm font-medium">Daily Aggregates</span>
|
|
651
|
-
<p className="text-xs text-muted-foreground">
|
|
652
|
-
Long-term storage before deletion
|
|
653
|
-
</p>
|
|
654
|
-
</div>
|
|
655
|
-
<div className="flex items-center gap-2">
|
|
656
|
-
<Input
|
|
657
|
-
type="number"
|
|
658
|
-
min={30}
|
|
659
|
-
max={1095}
|
|
660
|
-
value={data.dailyRetentionDays}
|
|
661
|
-
onChange={(e) =>
|
|
662
|
-
updateRetentionField(
|
|
663
|
-
configId,
|
|
664
|
-
"dailyRetentionDays",
|
|
665
|
-
Number(e.target.value)
|
|
666
|
-
)
|
|
667
|
-
}
|
|
668
|
-
className="h-8 w-20 text-center"
|
|
669
|
-
/>
|
|
670
|
-
<span className="text-sm text-muted-foreground w-10">days</span>
|
|
671
|
-
</div>
|
|
672
|
-
</div>
|
|
673
|
-
</div>
|
|
674
|
-
|
|
675
|
-
{/* Action Buttons */}
|
|
676
|
-
<div className="flex justify-between pt-2 border-t">
|
|
677
|
-
<Button
|
|
678
|
-
variant="ghost"
|
|
679
|
-
size="sm"
|
|
680
|
-
onClick={() => handleResetRetention(configId)}
|
|
681
|
-
disabled={saving || !data.isCustom}
|
|
682
|
-
>
|
|
683
|
-
Reset to Defaults
|
|
684
|
-
</Button>
|
|
685
|
-
<div className="flex gap-2">
|
|
686
|
-
<Button
|
|
687
|
-
variant="outline"
|
|
688
|
-
size="sm"
|
|
689
|
-
onClick={() => setSelectedPanel(undefined)}
|
|
690
|
-
>
|
|
691
|
-
Cancel
|
|
692
|
-
</Button>
|
|
693
|
-
<Button
|
|
694
|
-
size="sm"
|
|
695
|
-
onClick={() => handleSaveRetention(configId)}
|
|
696
|
-
disabled={saving || !isValidHierarchy}
|
|
697
|
-
>
|
|
698
|
-
{saving ? "Saving..." : "Save Retention"}
|
|
699
|
-
</Button>
|
|
700
|
-
</div>
|
|
701
|
-
</div>
|
|
702
|
-
</div>
|
|
703
|
-
);
|
|
704
|
-
};
|
|
705
|
-
|
|
706
48
|
return (
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
</Button>
|
|
722
|
-
|
|
723
|
-
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
|
724
|
-
<DialogContent className="max-w-lg max-h-[80vh] overflow-y-auto">
|
|
725
|
-
<DialogHeader>
|
|
726
|
-
<DialogTitle>Health Check Assignments</DialogTitle>
|
|
727
|
-
<DialogDescription className="sr-only">
|
|
728
|
-
Manage health check assignments for this system
|
|
729
|
-
</DialogDescription>
|
|
730
|
-
</DialogHeader>
|
|
731
|
-
|
|
732
|
-
{loading ? (
|
|
733
|
-
<div className="flex justify-center py-8">
|
|
734
|
-
<LoadingSpinner />
|
|
735
|
-
</div>
|
|
736
|
-
) : configs.length === 0 ? (
|
|
737
|
-
<p className="text-sm text-muted-foreground text-center py-4 italic">
|
|
738
|
-
No health checks configured.
|
|
739
|
-
</p>
|
|
740
|
-
) : (
|
|
741
|
-
<div className="space-y-2">
|
|
742
|
-
{configs.map((config) => {
|
|
743
|
-
const assoc = associations.find(
|
|
744
|
-
(a) => a.configurationId === config.id
|
|
745
|
-
);
|
|
746
|
-
const isAssigned = !!assoc;
|
|
747
|
-
const isExpanded =
|
|
748
|
-
selectedPanel?.configId === config.id &&
|
|
749
|
-
selectedPanel?.panel === "thresholds";
|
|
750
|
-
const isRetentionExpanded =
|
|
751
|
-
selectedPanel?.configId === config.id &&
|
|
752
|
-
selectedPanel?.panel === "retention";
|
|
753
|
-
|
|
754
|
-
return (
|
|
755
|
-
<div
|
|
756
|
-
key={config.id}
|
|
757
|
-
className="rounded-lg border bg-card p-3"
|
|
758
|
-
>
|
|
759
|
-
<div className="flex items-center justify-between">
|
|
760
|
-
<div className="flex items-center gap-3">
|
|
761
|
-
<Checkbox
|
|
762
|
-
checked={isAssigned}
|
|
763
|
-
onCheckedChange={() =>
|
|
764
|
-
handleToggleAssignment(config.id, isAssigned)
|
|
765
|
-
}
|
|
766
|
-
disabled={saving}
|
|
767
|
-
/>
|
|
768
|
-
<div>
|
|
769
|
-
<div className="font-medium text-sm">
|
|
770
|
-
{config.name}
|
|
771
|
-
</div>
|
|
772
|
-
<div className="text-xs text-muted-foreground">
|
|
773
|
-
{config.strategyId} • every {config.intervalSeconds}
|
|
774
|
-
s
|
|
775
|
-
</div>
|
|
776
|
-
</div>
|
|
777
|
-
</div>
|
|
778
|
-
{isAssigned && (
|
|
779
|
-
<div className="flex items-center gap-1">
|
|
780
|
-
{canManage && (
|
|
781
|
-
<Button
|
|
782
|
-
variant="ghost"
|
|
783
|
-
size="sm"
|
|
784
|
-
asChild
|
|
785
|
-
className="h-7 px-2"
|
|
786
|
-
>
|
|
787
|
-
<Link
|
|
788
|
-
to={resolveRoute(
|
|
789
|
-
healthcheckRoutes.routes.historyDetail,
|
|
790
|
-
{
|
|
791
|
-
systemId,
|
|
792
|
-
configurationId: config.id,
|
|
793
|
-
}
|
|
794
|
-
)}
|
|
795
|
-
>
|
|
796
|
-
<History className="h-4 w-4" />
|
|
797
|
-
</Link>
|
|
798
|
-
</Button>
|
|
799
|
-
)}
|
|
800
|
-
<Button
|
|
801
|
-
variant="ghost"
|
|
802
|
-
size="sm"
|
|
803
|
-
onClick={() =>
|
|
804
|
-
setSelectedPanel(
|
|
805
|
-
isExpanded
|
|
806
|
-
? undefined
|
|
807
|
-
: { configId: config.id, panel: "thresholds" }
|
|
808
|
-
)
|
|
809
|
-
}
|
|
810
|
-
className="h-7 px-2"
|
|
811
|
-
>
|
|
812
|
-
<Settings2 className="h-4 w-4" />
|
|
813
|
-
<span className="ml-1 text-xs">Thresholds</span>
|
|
814
|
-
</Button>
|
|
815
|
-
<Button
|
|
816
|
-
variant="ghost"
|
|
817
|
-
size="sm"
|
|
818
|
-
onClick={() =>
|
|
819
|
-
setSelectedPanel(
|
|
820
|
-
isRetentionExpanded
|
|
821
|
-
? undefined
|
|
822
|
-
: { configId: config.id, panel: "retention" }
|
|
823
|
-
)
|
|
824
|
-
}
|
|
825
|
-
className="h-7 px-2"
|
|
826
|
-
>
|
|
827
|
-
<Database className="h-4 w-4" />
|
|
828
|
-
<span className="ml-1 text-xs">Retention</span>
|
|
829
|
-
</Button>
|
|
830
|
-
</div>
|
|
831
|
-
)}
|
|
832
|
-
</div>
|
|
833
|
-
{isAssigned &&
|
|
834
|
-
isExpanded &&
|
|
835
|
-
assoc &&
|
|
836
|
-
renderThresholdEditor(assoc)}
|
|
837
|
-
{isAssigned &&
|
|
838
|
-
isRetentionExpanded &&
|
|
839
|
-
renderRetentionEditor(config.id)}
|
|
840
|
-
</div>
|
|
841
|
-
);
|
|
842
|
-
})}
|
|
843
|
-
</div>
|
|
844
|
-
)}
|
|
845
|
-
|
|
846
|
-
<DialogFooter>
|
|
847
|
-
<Button variant="outline" onClick={() => setIsOpen(false)}>
|
|
848
|
-
Close
|
|
849
|
-
</Button>
|
|
850
|
-
</DialogFooter>
|
|
851
|
-
</DialogContent>
|
|
852
|
-
</Dialog>
|
|
853
|
-
</>
|
|
49
|
+
<Button
|
|
50
|
+
variant="outline"
|
|
51
|
+
size="sm"
|
|
52
|
+
onClick={() => navigate(assignmentUrl)}
|
|
53
|
+
className="h-8 gap-1.5 border-dashed border-input hover:border-primary/30 hover:bg-primary/5"
|
|
54
|
+
>
|
|
55
|
+
<Activity className="h-3.5 w-3.5 text-primary" />
|
|
56
|
+
<span className="text-xs font-medium">Health Checks</span>
|
|
57
|
+
{associations.length > 0 && (
|
|
58
|
+
<span className="ml-0.5 flex h-4 w-4 items-center justify-center rounded-full bg-primary/10 text-[10px] font-bold text-primary">
|
|
59
|
+
{associations.length}
|
|
60
|
+
</span>
|
|
61
|
+
)}
|
|
62
|
+
</Button>
|
|
854
63
|
);
|
|
855
64
|
};
|