@checkstack/healthcheck-frontend 0.12.1 → 0.13.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +72 -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
|
@@ -0,0 +1,477 @@
|
|
|
1
|
+
import { useState, useEffect, useMemo } from "react";
|
|
2
|
+
import { useParams, useNavigate } from "react-router-dom";
|
|
3
|
+
import { usePluginClient, wrapInSuspense } from "@checkstack/frontend-api";
|
|
4
|
+
import { HealthCheckApi } from "../api";
|
|
5
|
+
import { SatelliteApi } from "@checkstack/satellite-common";
|
|
6
|
+
import {
|
|
7
|
+
DEFAULT_STATE_THRESHOLDS,
|
|
8
|
+
DEFAULT_RETENTION_CONFIG,
|
|
9
|
+
} from "@checkstack/healthcheck-common";
|
|
10
|
+
import type { StateThresholds } from "@checkstack/healthcheck-common";
|
|
11
|
+
import { PageLayout, IDELayout, useToast, BackLink } from "@checkstack/ui";
|
|
12
|
+
import { Settings } from "lucide-react";
|
|
13
|
+
import { extractErrorMessage, resolveRoute } from "@checkstack/common";
|
|
14
|
+
import { catalogRoutes } from "@checkstack/catalog-common";
|
|
15
|
+
import {
|
|
16
|
+
AssignmentTree,
|
|
17
|
+
type AssignmentNodeId,
|
|
18
|
+
} from "../components/assignments/AssignmentTree";
|
|
19
|
+
import { GeneralPanel } from "../components/assignments/GeneralPanel";
|
|
20
|
+
import { ThresholdsPanel } from "../components/assignments/ThresholdsPanel";
|
|
21
|
+
import {
|
|
22
|
+
RetentionPanel,
|
|
23
|
+
type RetentionData,
|
|
24
|
+
} from "../components/assignments/RetentionPanel";
|
|
25
|
+
import { ExecutionPanel } from "../components/assignments/ExecutionPanel";
|
|
26
|
+
|
|
27
|
+
// =============================================================================
|
|
28
|
+
// HELPERS
|
|
29
|
+
// =============================================================================
|
|
30
|
+
|
|
31
|
+
function parseNodeId(nodeId: AssignmentNodeId): {
|
|
32
|
+
panel: "general" | "thresholds" | "retention" | "execution";
|
|
33
|
+
configId: string;
|
|
34
|
+
} {
|
|
35
|
+
const [panel, ...rest] = nodeId.split(":") as [string, ...string[]];
|
|
36
|
+
return {
|
|
37
|
+
panel: panel as "general" | "thresholds" | "retention" | "execution",
|
|
38
|
+
configId: rest.join(":"),
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// =============================================================================
|
|
43
|
+
// PAGE
|
|
44
|
+
// =============================================================================
|
|
45
|
+
|
|
46
|
+
const AssignmentIDEPageContent = () => {
|
|
47
|
+
const { systemId } = useParams<{ systemId: string }>();
|
|
48
|
+
const navigate = useNavigate();
|
|
49
|
+
const toast = useToast();
|
|
50
|
+
const healthCheckClient = usePluginClient(HealthCheckApi);
|
|
51
|
+
const satelliteClient = usePluginClient(SatelliteApi);
|
|
52
|
+
|
|
53
|
+
// --- Data Fetching ---
|
|
54
|
+
|
|
55
|
+
const { data: configurationsData, isLoading: configsLoading } =
|
|
56
|
+
healthCheckClient.getConfigurations.useQuery({});
|
|
57
|
+
|
|
58
|
+
const { data: associations = [], refetch: refetchAssociations } =
|
|
59
|
+
healthCheckClient.getSystemAssociations.useQuery(
|
|
60
|
+
{ systemId: systemId ?? "" },
|
|
61
|
+
{ enabled: !!systemId },
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
const { data: satellitesData } = satelliteClient.listSatellites.useQuery({});
|
|
65
|
+
|
|
66
|
+
// --- UI State ---
|
|
67
|
+
|
|
68
|
+
const [selectedNode, setSelectedNode] = useState<AssignmentNodeId>();
|
|
69
|
+
const [localThresholds, setLocalThresholds] = useState<
|
|
70
|
+
Record<string, StateThresholds>
|
|
71
|
+
>({});
|
|
72
|
+
const [retentionData, setRetentionData] = useState<
|
|
73
|
+
Record<string, RetentionData>
|
|
74
|
+
>({});
|
|
75
|
+
|
|
76
|
+
const configs = useMemo(
|
|
77
|
+
() => configurationsData?.configurations ?? [],
|
|
78
|
+
[configurationsData],
|
|
79
|
+
);
|
|
80
|
+
const satellites = satellitesData?.satellites ?? [];
|
|
81
|
+
const assignedIds = useMemo(
|
|
82
|
+
() => new Set(associations.map((a) => a.configurationId)),
|
|
83
|
+
[associations],
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
// Fetch retention for selected config
|
|
87
|
+
const selectedConfigId = selectedNode
|
|
88
|
+
? parseNodeId(selectedNode).configId
|
|
89
|
+
: undefined;
|
|
90
|
+
const isRetentionPanel = selectedNode?.startsWith("retention:");
|
|
91
|
+
|
|
92
|
+
const { data: retentionConfigData } =
|
|
93
|
+
healthCheckClient.getRetentionConfig.useQuery(
|
|
94
|
+
{
|
|
95
|
+
systemId: systemId ?? "",
|
|
96
|
+
configurationId: selectedConfigId ?? "",
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
enabled:
|
|
100
|
+
!!isRetentionPanel &&
|
|
101
|
+
!!selectedConfigId &&
|
|
102
|
+
!retentionData[selectedConfigId],
|
|
103
|
+
},
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
useEffect(() => {
|
|
107
|
+
if (
|
|
108
|
+
retentionConfigData &&
|
|
109
|
+
selectedConfigId &&
|
|
110
|
+
!retentionData[selectedConfigId]
|
|
111
|
+
) {
|
|
112
|
+
setRetentionData((prev) => ({
|
|
113
|
+
...prev,
|
|
114
|
+
[selectedConfigId]: {
|
|
115
|
+
rawRetentionDays:
|
|
116
|
+
retentionConfigData.retentionConfig?.rawRetentionDays ??
|
|
117
|
+
DEFAULT_RETENTION_CONFIG.rawRetentionDays,
|
|
118
|
+
hourlyRetentionDays:
|
|
119
|
+
retentionConfigData.retentionConfig?.hourlyRetentionDays ??
|
|
120
|
+
DEFAULT_RETENTION_CONFIG.hourlyRetentionDays,
|
|
121
|
+
dailyRetentionDays:
|
|
122
|
+
retentionConfigData.retentionConfig?.dailyRetentionDays ??
|
|
123
|
+
DEFAULT_RETENTION_CONFIG.dailyRetentionDays,
|
|
124
|
+
isCustom: !!retentionConfigData.retentionConfig,
|
|
125
|
+
},
|
|
126
|
+
}));
|
|
127
|
+
}
|
|
128
|
+
}, [retentionConfigData, selectedConfigId, retentionData]);
|
|
129
|
+
|
|
130
|
+
// --- Auto-select first node ---
|
|
131
|
+
|
|
132
|
+
useEffect(() => {
|
|
133
|
+
if (!selectedNode && associations.length > 0) {
|
|
134
|
+
setSelectedNode(`general:${associations[0].configurationId}`);
|
|
135
|
+
}
|
|
136
|
+
}, [selectedNode, associations]);
|
|
137
|
+
|
|
138
|
+
// --- Mutations ---
|
|
139
|
+
|
|
140
|
+
const associateMutation = healthCheckClient.associateSystem.useMutation({
|
|
141
|
+
onSuccess: () => void refetchAssociations(),
|
|
142
|
+
onError: (error) =>
|
|
143
|
+
toast.error(extractErrorMessage(error, "Failed to update")),
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
const disassociateMutation = healthCheckClient.disassociateSystem.useMutation(
|
|
147
|
+
{
|
|
148
|
+
onSuccess: () => {
|
|
149
|
+
toast.success("Health check unassigned");
|
|
150
|
+
void refetchAssociations();
|
|
151
|
+
},
|
|
152
|
+
onError: (error) =>
|
|
153
|
+
toast.error(extractErrorMessage(error, "Failed to update")),
|
|
154
|
+
},
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
const updateRetentionMutation =
|
|
158
|
+
healthCheckClient.updateRetentionConfig.useMutation({
|
|
159
|
+
onSuccess: () => toast.success("Retention settings saved"),
|
|
160
|
+
onError: (error) =>
|
|
161
|
+
toast.error(extractErrorMessage(error, "Failed to save")),
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
const saving =
|
|
165
|
+
associateMutation.isPending ||
|
|
166
|
+
disassociateMutation.isPending ||
|
|
167
|
+
updateRetentionMutation.isPending;
|
|
168
|
+
|
|
169
|
+
// --- Handlers ---
|
|
170
|
+
|
|
171
|
+
const handleToggleAssignment = (configId: string, isAssigned: boolean) => {
|
|
172
|
+
if (!systemId) return;
|
|
173
|
+
|
|
174
|
+
if (isAssigned) {
|
|
175
|
+
disassociateMutation.mutate({ systemId, configId });
|
|
176
|
+
if (selectedNode && parseNodeId(selectedNode).configId === configId) {
|
|
177
|
+
setSelectedNode(undefined);
|
|
178
|
+
}
|
|
179
|
+
} else {
|
|
180
|
+
associateMutation.mutate({
|
|
181
|
+
systemId,
|
|
182
|
+
body: {
|
|
183
|
+
configurationId: configId,
|
|
184
|
+
enabled: true,
|
|
185
|
+
stateThresholds: DEFAULT_STATE_THRESHOLDS,
|
|
186
|
+
includeLocal: true,
|
|
187
|
+
},
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
const handleToggleEnabled = (configId: string, currentEnabled: boolean) => {
|
|
193
|
+
if (!systemId) return;
|
|
194
|
+
const assoc = associations.find((a) => a.configurationId === configId);
|
|
195
|
+
if (!assoc) return;
|
|
196
|
+
|
|
197
|
+
associateMutation.mutate({
|
|
198
|
+
systemId,
|
|
199
|
+
body: {
|
|
200
|
+
configurationId: configId,
|
|
201
|
+
enabled: !currentEnabled,
|
|
202
|
+
stateThresholds: assoc.stateThresholds,
|
|
203
|
+
satelliteIds: assoc.satelliteIds,
|
|
204
|
+
includeLocal: assoc.includeLocal,
|
|
205
|
+
},
|
|
206
|
+
});
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
const handleThresholdChange = (
|
|
210
|
+
configId: string,
|
|
211
|
+
thresholds: StateThresholds,
|
|
212
|
+
) => {
|
|
213
|
+
setLocalThresholds((prev) => ({ ...prev, [configId]: thresholds }));
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
const handleSaveThresholds = (configId: string) => {
|
|
217
|
+
if (!systemId) return;
|
|
218
|
+
const assoc = associations.find((a) => a.configurationId === configId);
|
|
219
|
+
const thresholds = localThresholds[configId] ?? assoc?.stateThresholds;
|
|
220
|
+
if (!assoc) return;
|
|
221
|
+
|
|
222
|
+
associateMutation.mutate(
|
|
223
|
+
{
|
|
224
|
+
systemId,
|
|
225
|
+
body: {
|
|
226
|
+
configurationId: configId,
|
|
227
|
+
enabled: assoc.enabled,
|
|
228
|
+
stateThresholds: thresholds,
|
|
229
|
+
},
|
|
230
|
+
},
|
|
231
|
+
{
|
|
232
|
+
onSuccess: () => {
|
|
233
|
+
toast.success("Thresholds saved");
|
|
234
|
+
setLocalThresholds((prev) => {
|
|
235
|
+
const next = { ...prev };
|
|
236
|
+
delete next[configId];
|
|
237
|
+
return next;
|
|
238
|
+
});
|
|
239
|
+
},
|
|
240
|
+
},
|
|
241
|
+
);
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
const handleToggleSatellite = (configId: string, satelliteId: string) => {
|
|
245
|
+
if (!systemId) return;
|
|
246
|
+
const assoc = associations.find((a) => a.configurationId === configId);
|
|
247
|
+
if (!assoc) return;
|
|
248
|
+
|
|
249
|
+
const currentIds = assoc.satelliteIds ?? [];
|
|
250
|
+
const isAssigned = currentIds.includes(satelliteId);
|
|
251
|
+
const newIds = isAssigned
|
|
252
|
+
? currentIds.filter((id) => id !== satelliteId)
|
|
253
|
+
: [...currentIds, satelliteId];
|
|
254
|
+
|
|
255
|
+
associateMutation.mutate({
|
|
256
|
+
systemId,
|
|
257
|
+
body: {
|
|
258
|
+
configurationId: configId,
|
|
259
|
+
enabled: assoc.enabled,
|
|
260
|
+
stateThresholds: assoc.stateThresholds,
|
|
261
|
+
satelliteIds: newIds,
|
|
262
|
+
includeLocal: assoc.includeLocal,
|
|
263
|
+
},
|
|
264
|
+
});
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
const handleToggleLocal = (configId: string) => {
|
|
268
|
+
if (!systemId) return;
|
|
269
|
+
const assoc = associations.find((a) => a.configurationId === configId);
|
|
270
|
+
if (!assoc) return;
|
|
271
|
+
|
|
272
|
+
associateMutation.mutate({
|
|
273
|
+
systemId,
|
|
274
|
+
body: {
|
|
275
|
+
configurationId: configId,
|
|
276
|
+
enabled: assoc.enabled,
|
|
277
|
+
stateThresholds: assoc.stateThresholds,
|
|
278
|
+
satelliteIds: assoc.satelliteIds,
|
|
279
|
+
includeLocal: !assoc.includeLocal,
|
|
280
|
+
},
|
|
281
|
+
});
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
const handleSaveRetention = (configId: string) => {
|
|
285
|
+
if (!systemId) return;
|
|
286
|
+
const data = retentionData[configId];
|
|
287
|
+
if (!data) return;
|
|
288
|
+
|
|
289
|
+
updateRetentionMutation.mutate({
|
|
290
|
+
systemId,
|
|
291
|
+
configurationId: configId,
|
|
292
|
+
retentionConfig: {
|
|
293
|
+
rawRetentionDays: data.rawRetentionDays,
|
|
294
|
+
hourlyRetentionDays: data.hourlyRetentionDays,
|
|
295
|
+
dailyRetentionDays: data.dailyRetentionDays,
|
|
296
|
+
},
|
|
297
|
+
});
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
const handleResetRetention = (configId: string) => {
|
|
301
|
+
if (!systemId) return;
|
|
302
|
+
updateRetentionMutation.mutate(
|
|
303
|
+
{
|
|
304
|
+
systemId,
|
|
305
|
+
configurationId: configId,
|
|
306
|
+
// eslint-disable-next-line unicorn/no-null -- RPC contract uses nullable()
|
|
307
|
+
retentionConfig: null,
|
|
308
|
+
},
|
|
309
|
+
{
|
|
310
|
+
onSuccess: () => {
|
|
311
|
+
setRetentionData((prev) => ({
|
|
312
|
+
...prev,
|
|
313
|
+
[configId]: {
|
|
314
|
+
rawRetentionDays: DEFAULT_RETENTION_CONFIG.rawRetentionDays,
|
|
315
|
+
hourlyRetentionDays: DEFAULT_RETENTION_CONFIG.hourlyRetentionDays,
|
|
316
|
+
dailyRetentionDays: DEFAULT_RETENTION_CONFIG.dailyRetentionDays,
|
|
317
|
+
isCustom: false,
|
|
318
|
+
},
|
|
319
|
+
}));
|
|
320
|
+
toast.success("Reset to defaults");
|
|
321
|
+
},
|
|
322
|
+
},
|
|
323
|
+
);
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
const updateRetentionField = (
|
|
327
|
+
configId: string,
|
|
328
|
+
field: string,
|
|
329
|
+
value: number,
|
|
330
|
+
) => {
|
|
331
|
+
setRetentionData((prev) => ({
|
|
332
|
+
...prev,
|
|
333
|
+
[configId]: { ...prev[configId], [field]: value, isCustom: true },
|
|
334
|
+
}));
|
|
335
|
+
};
|
|
336
|
+
|
|
337
|
+
// --- Derived Data ---
|
|
338
|
+
|
|
339
|
+
const assignedConfigs = useMemo(
|
|
340
|
+
() =>
|
|
341
|
+
associations
|
|
342
|
+
.map((assoc) => ({
|
|
343
|
+
configurationId: assoc.configurationId,
|
|
344
|
+
configurationName: assoc.configurationName,
|
|
345
|
+
enabled: assoc.enabled,
|
|
346
|
+
satelliteCount: assoc.satelliteIds?.length ?? 0,
|
|
347
|
+
}))
|
|
348
|
+
.toSorted((a, b) =>
|
|
349
|
+
a.configurationName.localeCompare(b.configurationName),
|
|
350
|
+
),
|
|
351
|
+
[associations],
|
|
352
|
+
);
|
|
353
|
+
|
|
354
|
+
const availableConfigs = useMemo(
|
|
355
|
+
() =>
|
|
356
|
+
configs
|
|
357
|
+
.filter((c) => !assignedIds.has(c.id))
|
|
358
|
+
.map((c) => ({ id: c.id, name: c.name, strategyId: c.strategyId }))
|
|
359
|
+
.toSorted((a, b) => a.name.localeCompare(b.name)),
|
|
360
|
+
[configs, assignedIds],
|
|
361
|
+
);
|
|
362
|
+
|
|
363
|
+
// --- Render Panel ---
|
|
364
|
+
|
|
365
|
+
const renderPanel = () => {
|
|
366
|
+
if (!selectedNode) {
|
|
367
|
+
return (
|
|
368
|
+
<div className="flex items-center justify-center h-full text-sm text-muted-foreground p-12">
|
|
369
|
+
{associations.length === 0
|
|
370
|
+
? "Add a health check from the left panel to get started."
|
|
371
|
+
: "Select an item from the left panel to configure it."}
|
|
372
|
+
</div>
|
|
373
|
+
);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
const { panel, configId } = parseNodeId(selectedNode);
|
|
377
|
+
const assoc = associations.find((a) => a.configurationId === configId);
|
|
378
|
+
if (!assoc) return;
|
|
379
|
+
|
|
380
|
+
switch (panel) {
|
|
381
|
+
case "general": {
|
|
382
|
+
return (
|
|
383
|
+
<GeneralPanel
|
|
384
|
+
configurationName={assoc.configurationName}
|
|
385
|
+
strategyId={
|
|
386
|
+
configs.find((c) => c.id === configId)?.strategyId ?? ""
|
|
387
|
+
}
|
|
388
|
+
configurationId={configId}
|
|
389
|
+
enabled={assoc.enabled}
|
|
390
|
+
onToggleEnabled={() => handleToggleEnabled(configId, assoc.enabled)}
|
|
391
|
+
onUnassign={() => handleToggleAssignment(configId, true)}
|
|
392
|
+
saving={saving}
|
|
393
|
+
/>
|
|
394
|
+
);
|
|
395
|
+
}
|
|
396
|
+
case "thresholds": {
|
|
397
|
+
const thresholds =
|
|
398
|
+
localThresholds[configId] ??
|
|
399
|
+
assoc.stateThresholds ??
|
|
400
|
+
DEFAULT_STATE_THRESHOLDS;
|
|
401
|
+
return (
|
|
402
|
+
<ThresholdsPanel
|
|
403
|
+
thresholds={thresholds}
|
|
404
|
+
onChange={(t) => handleThresholdChange(configId, t)}
|
|
405
|
+
onSave={() => handleSaveThresholds(configId)}
|
|
406
|
+
saving={saving}
|
|
407
|
+
/>
|
|
408
|
+
);
|
|
409
|
+
}
|
|
410
|
+
case "retention": {
|
|
411
|
+
return (
|
|
412
|
+
<RetentionPanel
|
|
413
|
+
data={retentionData[configId]}
|
|
414
|
+
onFieldChange={(field, value) =>
|
|
415
|
+
updateRetentionField(configId, field, value)
|
|
416
|
+
}
|
|
417
|
+
onSave={() => handleSaveRetention(configId)}
|
|
418
|
+
onReset={() => handleResetRetention(configId)}
|
|
419
|
+
saving={saving}
|
|
420
|
+
/>
|
|
421
|
+
);
|
|
422
|
+
}
|
|
423
|
+
case "execution": {
|
|
424
|
+
return (
|
|
425
|
+
<ExecutionPanel
|
|
426
|
+
includeLocal={assoc.includeLocal}
|
|
427
|
+
satelliteIds={assoc.satelliteIds ?? []}
|
|
428
|
+
satellites={satellites}
|
|
429
|
+
onToggleLocal={() => handleToggleLocal(configId)}
|
|
430
|
+
onToggleSatellite={(satId) =>
|
|
431
|
+
handleToggleSatellite(configId, satId)
|
|
432
|
+
}
|
|
433
|
+
saving={saving}
|
|
434
|
+
/>
|
|
435
|
+
);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
};
|
|
439
|
+
|
|
440
|
+
if (configsLoading) {
|
|
441
|
+
return (
|
|
442
|
+
<PageLayout title="Health Check Assignments" icon={Settings} loading>
|
|
443
|
+
<div />
|
|
444
|
+
</PageLayout>
|
|
445
|
+
);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
return (
|
|
449
|
+
<PageLayout
|
|
450
|
+
title="Health Check Assignments"
|
|
451
|
+
icon={Settings}
|
|
452
|
+
maxWidth="full"
|
|
453
|
+
actions={
|
|
454
|
+
<BackLink
|
|
455
|
+
onClick={() => navigate(resolveRoute(catalogRoutes.routes.config))}
|
|
456
|
+
>
|
|
457
|
+
Back to Systems
|
|
458
|
+
</BackLink>
|
|
459
|
+
}
|
|
460
|
+
>
|
|
461
|
+
<IDELayout
|
|
462
|
+
tree={
|
|
463
|
+
<AssignmentTree
|
|
464
|
+
assigned={assignedConfigs}
|
|
465
|
+
available={availableConfigs}
|
|
466
|
+
selectedNode={selectedNode}
|
|
467
|
+
onSelectNode={setSelectedNode}
|
|
468
|
+
onToggleAssignment={handleToggleAssignment}
|
|
469
|
+
/>
|
|
470
|
+
}
|
|
471
|
+
panel={renderPanel()}
|
|
472
|
+
/>
|
|
473
|
+
</PageLayout>
|
|
474
|
+
);
|
|
475
|
+
};
|
|
476
|
+
|
|
477
|
+
export const AssignmentIDEPage = wrapInSuspense(AssignmentIDEPageContent);
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
healthCheckAccess,
|
|
11
11
|
HealthCheckApi,
|
|
12
12
|
} from "@checkstack/healthcheck-common";
|
|
13
|
+
import { SatelliteApi } from "@checkstack/satellite-common";
|
|
13
14
|
import { resolveRoute } from "@checkstack/common";
|
|
14
15
|
import {
|
|
15
16
|
PageLayout,
|
|
@@ -26,7 +27,7 @@ import {
|
|
|
26
27
|
type DateRange,
|
|
27
28
|
} from "@checkstack/ui";
|
|
28
29
|
import { useParams, useNavigate } from "react-router-dom";
|
|
29
|
-
import { History, X } from "lucide-react";
|
|
30
|
+
import { History, X, Server, Satellite } from "lucide-react";
|
|
30
31
|
import { format } from "date-fns";
|
|
31
32
|
import {
|
|
32
33
|
HealthCheckRunsTable,
|
|
@@ -44,16 +45,22 @@ const HealthCheckHistoryDetailPageContent = () => {
|
|
|
44
45
|
|
|
45
46
|
const navigate = useNavigate();
|
|
46
47
|
const healthCheckClient = usePluginClient(HealthCheckApi);
|
|
48
|
+
const satelliteClient = usePluginClient(SatelliteApi);
|
|
47
49
|
const accessApi = useApi(accessApiRef);
|
|
48
50
|
const { allowed: canManage, loading: accessLoading } = accessApi.useAccess(
|
|
49
51
|
healthCheckAccess.configuration.manage,
|
|
50
52
|
);
|
|
51
53
|
|
|
52
54
|
const [dateRange, setDateRange] = useState<DateRange>(getDefaultDateRange);
|
|
55
|
+
const [sourceFilter, setSourceFilter] = useState<string | undefined>();
|
|
53
56
|
|
|
54
57
|
// Pagination state
|
|
55
58
|
const pagination = usePagination({ defaultLimit: 20 });
|
|
56
59
|
|
|
60
|
+
// Fetch satellites for the source filter dropdown
|
|
61
|
+
const { data: satellitesData } = satelliteClient.listSatellites.useQuery({});
|
|
62
|
+
const satellites = satellitesData?.satellites ?? [];
|
|
63
|
+
|
|
57
64
|
// Fetch specific run if runId is provided
|
|
58
65
|
const { data: specificRun } = healthCheckClient.getRunById.useQuery(
|
|
59
66
|
{
|
|
@@ -81,6 +88,7 @@ const HealthCheckHistoryDetailPageContent = () => {
|
|
|
81
88
|
configurationId,
|
|
82
89
|
startDate: dateRange.startDate,
|
|
83
90
|
endDate: dateRange.endDate,
|
|
91
|
+
sourceFilter,
|
|
84
92
|
limit: pagination.limit,
|
|
85
93
|
offset: pagination.offset,
|
|
86
94
|
sortOrder: "desc",
|
|
@@ -156,11 +164,50 @@ const HealthCheckHistoryDetailPageContent = () => {
|
|
|
156
164
|
<CardTitle>Run History</CardTitle>
|
|
157
165
|
</CardHeader>
|
|
158
166
|
<CardContent>
|
|
159
|
-
<
|
|
160
|
-
value={dateRange}
|
|
161
|
-
|
|
162
|
-
className="
|
|
163
|
-
|
|
167
|
+
<div className="flex flex-wrap items-center gap-3 mb-4">
|
|
168
|
+
<DateRangeFilter value={dateRange} onChange={setDateRange} />
|
|
169
|
+
{/* Source filter */}
|
|
170
|
+
<div className="flex items-center gap-2">
|
|
171
|
+
<span className="text-sm text-muted-foreground">Source:</span>
|
|
172
|
+
<div className="flex items-center gap-1">
|
|
173
|
+
<button
|
|
174
|
+
onClick={() => setSourceFilter(undefined)}
|
|
175
|
+
className={`inline-flex items-center gap-1 text-xs px-2 py-1 rounded-full transition-colors ${
|
|
176
|
+
sourceFilter === undefined
|
|
177
|
+
? "bg-primary text-primary-foreground"
|
|
178
|
+
: "bg-muted text-muted-foreground hover:bg-muted/80"
|
|
179
|
+
}`}
|
|
180
|
+
>
|
|
181
|
+
All
|
|
182
|
+
</button>
|
|
183
|
+
<button
|
|
184
|
+
onClick={() => setSourceFilter("local")}
|
|
185
|
+
className={`inline-flex items-center gap-1 text-xs px-2 py-1 rounded-full transition-colors ${
|
|
186
|
+
sourceFilter === "local"
|
|
187
|
+
? "bg-primary text-primary-foreground"
|
|
188
|
+
: "bg-muted text-muted-foreground hover:bg-muted/80"
|
|
189
|
+
}`}
|
|
190
|
+
>
|
|
191
|
+
<Server className="h-3 w-3" />
|
|
192
|
+
Local
|
|
193
|
+
</button>
|
|
194
|
+
{satellites.map((sat) => (
|
|
195
|
+
<button
|
|
196
|
+
key={sat.id}
|
|
197
|
+
onClick={() => setSourceFilter(sat.id)}
|
|
198
|
+
className={`inline-flex items-center gap-1 text-xs px-2 py-1 rounded-full transition-colors ${
|
|
199
|
+
sourceFilter === sat.id
|
|
200
|
+
? "bg-orange-500 text-white"
|
|
201
|
+
: "bg-orange-500/10 text-orange-600 hover:bg-orange-500/20"
|
|
202
|
+
}`}
|
|
203
|
+
>
|
|
204
|
+
<Satellite className="h-3 w-3" />
|
|
205
|
+
{sat.name}
|
|
206
|
+
</button>
|
|
207
|
+
))}
|
|
208
|
+
</div>
|
|
209
|
+
</div>
|
|
210
|
+
</div>
|
|
164
211
|
<HealthCheckRunsTable
|
|
165
212
|
runs={runs}
|
|
166
213
|
loading={isLoading}
|
|
@@ -9,13 +9,12 @@ import {
|
|
|
9
9
|
healthcheckRoutes,
|
|
10
10
|
type CollectorConfigEntry,
|
|
11
11
|
} from "@checkstack/healthcheck-common";
|
|
12
|
-
import { PageLayout, Button, useToast } from "@checkstack/ui";
|
|
12
|
+
import { PageLayout, Button, useToast, IDELayout, type ValidationIssue } from "@checkstack/ui";
|
|
13
13
|
import { Save, Settings } from "lucide-react";
|
|
14
14
|
import { resolveRoute, extractErrorMessage} from "@checkstack/common";
|
|
15
15
|
import { useCollectors } from "../hooks/useCollectors";
|
|
16
16
|
import { EditorTree, type TreeNodeId } from "../components/editor/EditorTree";
|
|
17
17
|
import { EditorPanel } from "../components/editor/EditorPanel";
|
|
18
|
-
import { IDEStatusBar, type ValidationIssue } from "../components/editor/IDEStatusBar";
|
|
19
18
|
|
|
20
19
|
// =============================================================================
|
|
21
20
|
// TYPES
|
|
@@ -328,9 +327,8 @@ const HealthCheckIDEPageContent = () => {
|
|
|
328
327
|
</Button>
|
|
329
328
|
}
|
|
330
329
|
>
|
|
331
|
-
<
|
|
332
|
-
{
|
|
333
|
-
<div className="w-full lg:w-64 shrink-0 border-b lg:border-b-0 lg:border-r bg-muted/30">
|
|
330
|
+
<IDELayout
|
|
331
|
+
tree={
|
|
334
332
|
<EditorTree
|
|
335
333
|
collectors={formState.collectors}
|
|
336
334
|
availableCollectors={availableCollectors}
|
|
@@ -340,10 +338,8 @@ const HealthCheckIDEPageContent = () => {
|
|
|
340
338
|
validationIssues={validationIssues}
|
|
341
339
|
strategyId={activeStrategyId ?? ""}
|
|
342
340
|
/>
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
{/* Editor Panel — Right Panel */}
|
|
346
|
-
<div className="flex-1 min-w-0">
|
|
341
|
+
}
|
|
342
|
+
panel={
|
|
347
343
|
<EditorPanel
|
|
348
344
|
selectedNode={selectedNode}
|
|
349
345
|
formState={formState}
|
|
@@ -367,13 +363,9 @@ const HealthCheckIDEPageContent = () => {
|
|
|
367
363
|
onCollectorAdd={handleCollectorAdd}
|
|
368
364
|
strategyId={activeStrategyId ?? ""}
|
|
369
365
|
/>
|
|
370
|
-
|
|
371
|
-
</div>
|
|
372
|
-
|
|
373
|
-
{/* Status Bar */}
|
|
374
|
-
<IDEStatusBar
|
|
366
|
+
}
|
|
375
367
|
issues={validationIssues}
|
|
376
|
-
onIssueClick={(nodeId) => setSelectedNode(nodeId)}
|
|
368
|
+
onIssueClick={(nodeId) => setSelectedNode(nodeId as TreeNodeId)}
|
|
377
369
|
/>
|
|
378
370
|
</PageLayout>
|
|
379
371
|
);
|