@checkstack/healthcheck-frontend 0.0.2 → 0.1.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 +73 -0
- package/package.json +1 -1
- package/src/auto-charts/AutoChartGrid.tsx +377 -78
- package/src/auto-charts/index.ts +1 -1
- package/src/auto-charts/schema-parser.ts +122 -51
- package/src/components/AssertionBuilder.tsx +425 -0
- package/src/components/CollectorList.tsx +303 -0
- package/src/components/HealthCheckDiagram.tsx +39 -62
- package/src/components/HealthCheckEditor.tsx +37 -8
- package/src/components/HealthCheckLatencyChart.tsx +119 -59
- package/src/components/HealthCheckRunsTable.tsx +142 -6
- package/src/components/HealthCheckStatusTimeline.tsx +35 -52
- package/src/components/HealthCheckSystemOverview.tsx +208 -185
- package/src/hooks/useCollectors.ts +63 -0
- package/src/hooks/useHealthCheckData.ts +52 -33
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
import React, { useState, useEffect, useCallback } from "react";
|
|
2
|
+
import {
|
|
3
|
+
CollectorDto,
|
|
4
|
+
CollectorConfigEntry,
|
|
5
|
+
} from "@checkstack/healthcheck-common";
|
|
6
|
+
import {
|
|
7
|
+
Card,
|
|
8
|
+
CardContent,
|
|
9
|
+
CardHeader,
|
|
10
|
+
CardTitle,
|
|
11
|
+
Label,
|
|
12
|
+
Select,
|
|
13
|
+
SelectContent,
|
|
14
|
+
SelectItem,
|
|
15
|
+
SelectTrigger,
|
|
16
|
+
SelectValue,
|
|
17
|
+
Accordion,
|
|
18
|
+
AccordionItem,
|
|
19
|
+
AccordionTrigger,
|
|
20
|
+
AccordionContent,
|
|
21
|
+
DynamicForm,
|
|
22
|
+
Badge,
|
|
23
|
+
} from "@checkstack/ui";
|
|
24
|
+
import { Plus, Trash2 } from "lucide-react";
|
|
25
|
+
import { isBuiltInCollector } from "../hooks/useCollectors";
|
|
26
|
+
import { AssertionBuilder, type Assertion } from "./AssertionBuilder";
|
|
27
|
+
|
|
28
|
+
interface CollectorListProps {
|
|
29
|
+
strategyId: string;
|
|
30
|
+
availableCollectors: CollectorDto[];
|
|
31
|
+
configuredCollectors: CollectorConfigEntry[];
|
|
32
|
+
onChange: (collectors: CollectorConfigEntry[]) => void;
|
|
33
|
+
loading?: boolean;
|
|
34
|
+
/** Called when collector form validity changes */
|
|
35
|
+
onValidChange?: (isValid: boolean) => void;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Component for managing collector configurations within a health check.
|
|
40
|
+
* Shows currently configured collectors and allows adding new ones.
|
|
41
|
+
*/
|
|
42
|
+
export const CollectorList: React.FC<CollectorListProps> = ({
|
|
43
|
+
strategyId,
|
|
44
|
+
availableCollectors,
|
|
45
|
+
configuredCollectors,
|
|
46
|
+
onChange,
|
|
47
|
+
loading,
|
|
48
|
+
onValidChange,
|
|
49
|
+
}) => {
|
|
50
|
+
// Track validity state per collector index
|
|
51
|
+
const [validityMap, setValidityMap] = useState<Record<number, boolean>>({});
|
|
52
|
+
|
|
53
|
+
// Compute overall validity and report changes
|
|
54
|
+
useEffect(() => {
|
|
55
|
+
if (!onValidChange) return;
|
|
56
|
+
|
|
57
|
+
// All collectors must be valid (or have no config schema)
|
|
58
|
+
const isValid = configuredCollectors.every((_, index) => {
|
|
59
|
+
// If no validity recorded for this collector, assume valid (no schema)
|
|
60
|
+
return validityMap[index] !== false;
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
onValidChange(isValid);
|
|
64
|
+
}, [validityMap, configuredCollectors, onValidChange]);
|
|
65
|
+
|
|
66
|
+
const handleCollectorValidChange = useCallback(
|
|
67
|
+
(index: number, isValid: boolean) => {
|
|
68
|
+
setValidityMap((prev) => ({ ...prev, [index]: isValid }));
|
|
69
|
+
},
|
|
70
|
+
[]
|
|
71
|
+
);
|
|
72
|
+
// Separate built-in and external collectors
|
|
73
|
+
const builtInCollectors = availableCollectors.filter((c) =>
|
|
74
|
+
isBuiltInCollector(c.id, strategyId)
|
|
75
|
+
);
|
|
76
|
+
const externalCollectors = availableCollectors.filter(
|
|
77
|
+
(c) => !isBuiltInCollector(c.id, strategyId)
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
// Get collectors that can still be added
|
|
81
|
+
const getAddableCollectors = () => {
|
|
82
|
+
const configuredIds = new Set(
|
|
83
|
+
configuredCollectors.map((c) => c.collectorId)
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
return availableCollectors.filter((c) => {
|
|
87
|
+
// Already configured?
|
|
88
|
+
if (configuredIds.has(c.id)) {
|
|
89
|
+
// Can add multiple?
|
|
90
|
+
return c.allowMultiple;
|
|
91
|
+
}
|
|
92
|
+
return true;
|
|
93
|
+
});
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const addableCollectors = getAddableCollectors();
|
|
97
|
+
|
|
98
|
+
const handleAdd = (collectorId: string) => {
|
|
99
|
+
const collector = availableCollectors.find((c) => c.id === collectorId);
|
|
100
|
+
if (!collector) return;
|
|
101
|
+
|
|
102
|
+
const newEntry: CollectorConfigEntry = {
|
|
103
|
+
collectorId,
|
|
104
|
+
config: {},
|
|
105
|
+
assertions: [],
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
onChange([...configuredCollectors, newEntry]);
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const handleRemove = (index: number) => {
|
|
112
|
+
const updated = [...configuredCollectors];
|
|
113
|
+
updated.splice(index, 1);
|
|
114
|
+
onChange(updated);
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
const handleConfigChange = (
|
|
118
|
+
index: number,
|
|
119
|
+
config: Record<string, unknown>
|
|
120
|
+
) => {
|
|
121
|
+
const updated = [...configuredCollectors];
|
|
122
|
+
updated[index] = { ...updated[index], config };
|
|
123
|
+
onChange(updated);
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
const handleAssertionsChange = (index: number, assertions: Assertion[]) => {
|
|
127
|
+
const updated = [...configuredCollectors];
|
|
128
|
+
updated[index] = { ...updated[index], assertions };
|
|
129
|
+
onChange(updated);
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
const getCollectorDetails = (collectorId: string) => {
|
|
133
|
+
return availableCollectors.find((c) => c.id === collectorId);
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
if (loading) {
|
|
137
|
+
return (
|
|
138
|
+
<Card>
|
|
139
|
+
<CardHeader>
|
|
140
|
+
<CardTitle className="text-base">Check Items</CardTitle>
|
|
141
|
+
</CardHeader>
|
|
142
|
+
<CardContent>
|
|
143
|
+
<div className="text-muted-foreground text-sm">
|
|
144
|
+
Loading collectors...
|
|
145
|
+
</div>
|
|
146
|
+
</CardContent>
|
|
147
|
+
</Card>
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return (
|
|
152
|
+
<Card>
|
|
153
|
+
<CardHeader className="flex flex-row items-center justify-between">
|
|
154
|
+
<CardTitle className="text-base">Check Items</CardTitle>
|
|
155
|
+
{addableCollectors.length > 0 && (
|
|
156
|
+
<Select value="" onValueChange={handleAdd}>
|
|
157
|
+
<SelectTrigger className="w-[200px]">
|
|
158
|
+
<Plus className="h-4 w-4 mr-2" />
|
|
159
|
+
<SelectValue placeholder="Add collector..." />
|
|
160
|
+
</SelectTrigger>
|
|
161
|
+
<SelectContent>
|
|
162
|
+
{/* Built-in collectors first */}
|
|
163
|
+
{builtInCollectors.length > 0 && (
|
|
164
|
+
<>
|
|
165
|
+
<div className="px-2 py-1.5 text-xs font-semibold text-muted-foreground">
|
|
166
|
+
Built-in
|
|
167
|
+
</div>
|
|
168
|
+
{builtInCollectors
|
|
169
|
+
.filter((c) => addableCollectors.some((a) => a.id === c.id))
|
|
170
|
+
.map((collector) => (
|
|
171
|
+
<SelectItem key={collector.id} value={collector.id}>
|
|
172
|
+
<div className="flex items-center gap-2">
|
|
173
|
+
<span>{collector.displayName}</span>
|
|
174
|
+
{collector.allowMultiple && (
|
|
175
|
+
<Badge variant="outline" className="text-xs">
|
|
176
|
+
Multiple
|
|
177
|
+
</Badge>
|
|
178
|
+
)}
|
|
179
|
+
</div>
|
|
180
|
+
</SelectItem>
|
|
181
|
+
))}
|
|
182
|
+
</>
|
|
183
|
+
)}
|
|
184
|
+
{/* External collectors */}
|
|
185
|
+
{externalCollectors.length > 0 && (
|
|
186
|
+
<>
|
|
187
|
+
<div className="px-2 py-1.5 text-xs font-semibold text-muted-foreground">
|
|
188
|
+
External
|
|
189
|
+
</div>
|
|
190
|
+
{externalCollectors
|
|
191
|
+
.filter((c) => addableCollectors.some((a) => a.id === c.id))
|
|
192
|
+
.map((collector) => (
|
|
193
|
+
<SelectItem key={collector.id} value={collector.id}>
|
|
194
|
+
<div className="flex items-center gap-2">
|
|
195
|
+
<span>{collector.displayName}</span>
|
|
196
|
+
{collector.allowMultiple && (
|
|
197
|
+
<Badge variant="outline" className="text-xs">
|
|
198
|
+
Multiple
|
|
199
|
+
</Badge>
|
|
200
|
+
)}
|
|
201
|
+
</div>
|
|
202
|
+
</SelectItem>
|
|
203
|
+
))}
|
|
204
|
+
</>
|
|
205
|
+
)}
|
|
206
|
+
</SelectContent>
|
|
207
|
+
</Select>
|
|
208
|
+
)}
|
|
209
|
+
</CardHeader>
|
|
210
|
+
<CardContent>
|
|
211
|
+
{configuredCollectors.length === 0 ? (
|
|
212
|
+
<div className="text-muted-foreground text-sm text-center py-4">
|
|
213
|
+
No check items configured. Add a collector to define what to check.
|
|
214
|
+
</div>
|
|
215
|
+
) : (
|
|
216
|
+
<Accordion type="multiple" className="w-full">
|
|
217
|
+
{configuredCollectors.map((entry, index) => {
|
|
218
|
+
const collector = getCollectorDetails(entry.collectorId);
|
|
219
|
+
const isBuiltIn = isBuiltInCollector(
|
|
220
|
+
entry.collectorId,
|
|
221
|
+
strategyId
|
|
222
|
+
);
|
|
223
|
+
|
|
224
|
+
return (
|
|
225
|
+
<AccordionItem key={index} value={`item-${index}`}>
|
|
226
|
+
<AccordionTrigger className="hover:no-underline">
|
|
227
|
+
<div className="flex items-center gap-2 flex-1">
|
|
228
|
+
<span className="font-medium">
|
|
229
|
+
{collector?.displayName || entry.collectorId}
|
|
230
|
+
</span>
|
|
231
|
+
{isBuiltIn && (
|
|
232
|
+
<Badge variant="secondary" className="text-xs">
|
|
233
|
+
Built-in
|
|
234
|
+
</Badge>
|
|
235
|
+
)}
|
|
236
|
+
</div>
|
|
237
|
+
<div
|
|
238
|
+
role="button"
|
|
239
|
+
tabIndex={0}
|
|
240
|
+
className="inline-flex items-center justify-center rounded-md h-8 w-8 text-destructive hover:text-destructive hover:bg-accent cursor-pointer"
|
|
241
|
+
onClick={(e) => {
|
|
242
|
+
e.stopPropagation();
|
|
243
|
+
handleRemove(index);
|
|
244
|
+
}}
|
|
245
|
+
onKeyDown={(e) => {
|
|
246
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
247
|
+
e.stopPropagation();
|
|
248
|
+
handleRemove(index);
|
|
249
|
+
}
|
|
250
|
+
}}
|
|
251
|
+
>
|
|
252
|
+
<Trash2 className="h-4 w-4" />
|
|
253
|
+
</div>
|
|
254
|
+
</AccordionTrigger>
|
|
255
|
+
<AccordionContent>
|
|
256
|
+
<div className="space-y-6 pt-4">
|
|
257
|
+
{/* Configuration Section */}
|
|
258
|
+
{collector?.configSchema && (
|
|
259
|
+
<div className="space-y-4">
|
|
260
|
+
<Label className="text-sm font-medium">
|
|
261
|
+
Configuration
|
|
262
|
+
</Label>
|
|
263
|
+
<DynamicForm
|
|
264
|
+
schema={collector.configSchema}
|
|
265
|
+
value={entry.config}
|
|
266
|
+
onChange={(config) =>
|
|
267
|
+
handleConfigChange(index, config)
|
|
268
|
+
}
|
|
269
|
+
onValidChange={(isValid) =>
|
|
270
|
+
handleCollectorValidChange(index, isValid)
|
|
271
|
+
}
|
|
272
|
+
/>
|
|
273
|
+
</div>
|
|
274
|
+
)}
|
|
275
|
+
|
|
276
|
+
{/* Assertion Builder Section */}
|
|
277
|
+
{collector?.resultSchema && (
|
|
278
|
+
<div className="space-y-4">
|
|
279
|
+
<Label className="text-sm font-medium">
|
|
280
|
+
Assertions
|
|
281
|
+
</Label>
|
|
282
|
+
<AssertionBuilder
|
|
283
|
+
resultSchema={collector.resultSchema}
|
|
284
|
+
assertions={
|
|
285
|
+
(entry.assertions as unknown as Assertion[]) ?? []
|
|
286
|
+
}
|
|
287
|
+
onChange={(assertions) =>
|
|
288
|
+
handleAssertionsChange(index, assertions)
|
|
289
|
+
}
|
|
290
|
+
/>
|
|
291
|
+
</div>
|
|
292
|
+
)}
|
|
293
|
+
</div>
|
|
294
|
+
</AccordionContent>
|
|
295
|
+
</AccordionItem>
|
|
296
|
+
);
|
|
297
|
+
})}
|
|
298
|
+
</Accordion>
|
|
299
|
+
)}
|
|
300
|
+
</CardContent>
|
|
301
|
+
</Card>
|
|
302
|
+
);
|
|
303
|
+
};
|
|
@@ -1,74 +1,30 @@
|
|
|
1
1
|
import { ExtensionSlot } from "@checkstack/frontend-api";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
|
|
2
|
+
import { InfoBanner } from "@checkstack/ui";
|
|
3
|
+
import {
|
|
4
|
+
HealthCheckDiagramSlot,
|
|
5
|
+
type HealthCheckDiagramSlotContext,
|
|
6
|
+
} from "../slots";
|
|
5
7
|
import { AggregatedDataBanner } from "./AggregatedDataBanner";
|
|
6
8
|
|
|
7
9
|
interface HealthCheckDiagramProps {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
};
|
|
15
|
-
limit?: number;
|
|
16
|
-
offset?: number;
|
|
10
|
+
/** The context from useHealthCheckData - handles both raw and aggregated modes */
|
|
11
|
+
context: HealthCheckDiagramSlotContext;
|
|
12
|
+
/** Whether the data is aggregated (for showing the info banner) */
|
|
13
|
+
isAggregated?: boolean;
|
|
14
|
+
/** Raw retention days (for the info banner) */
|
|
15
|
+
rawRetentionDays?: number;
|
|
17
16
|
}
|
|
18
17
|
|
|
19
18
|
/**
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
* Automatically determines whether to use raw or aggregated data based on
|
|
24
|
-
* the date range and the configured rawRetentionDays.
|
|
19
|
+
* Component that renders the diagram extension slot with the provided context.
|
|
20
|
+
* Expects parent component to fetch data via useHealthCheckData and pass context.
|
|
25
21
|
*/
|
|
26
22
|
export function HealthCheckDiagram({
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
dateRange,
|
|
31
|
-
limit,
|
|
32
|
-
offset,
|
|
23
|
+
context,
|
|
24
|
+
isAggregated = false,
|
|
25
|
+
rawRetentionDays = 7,
|
|
33
26
|
}: HealthCheckDiagramProps) {
|
|
34
|
-
|
|
35
|
-
context,
|
|
36
|
-
loading,
|
|
37
|
-
hasPermission,
|
|
38
|
-
permissionLoading,
|
|
39
|
-
isAggregated,
|
|
40
|
-
retentionConfig,
|
|
41
|
-
} = useHealthCheckData({
|
|
42
|
-
systemId,
|
|
43
|
-
configurationId,
|
|
44
|
-
strategyId,
|
|
45
|
-
dateRange,
|
|
46
|
-
limit,
|
|
47
|
-
offset,
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
if (permissionLoading) {
|
|
51
|
-
return <LoadingSpinner />;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
if (!hasPermission) {
|
|
55
|
-
return (
|
|
56
|
-
<InfoBanner variant="info">
|
|
57
|
-
Additional strategy-specific visualizations are available with the
|
|
58
|
-
"Read Health Check Details" permission.
|
|
59
|
-
</InfoBanner>
|
|
60
|
-
);
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
if (loading) {
|
|
64
|
-
return <LoadingSpinner />;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
if (!context) {
|
|
68
|
-
return;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
// Determine bucket size from context for aggregated data
|
|
27
|
+
// Determine bucket size from context for aggregated data info banner
|
|
72
28
|
const bucketSize =
|
|
73
29
|
context.type === "aggregated" && context.buckets.length > 0
|
|
74
30
|
? context.buckets[0].bucketSize
|
|
@@ -79,10 +35,31 @@ export function HealthCheckDiagram({
|
|
|
79
35
|
{isAggregated && (
|
|
80
36
|
<AggregatedDataBanner
|
|
81
37
|
bucketSize={bucketSize}
|
|
82
|
-
rawRetentionDays={
|
|
38
|
+
rawRetentionDays={rawRetentionDays}
|
|
83
39
|
/>
|
|
84
40
|
)}
|
|
85
41
|
<ExtensionSlot slot={HealthCheckDiagramSlot} context={context} />
|
|
86
42
|
</>
|
|
87
43
|
);
|
|
88
44
|
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Wrapper that shows permission message when user lacks access.
|
|
48
|
+
*/
|
|
49
|
+
export function HealthCheckDiagramPermissionGate({
|
|
50
|
+
hasPermission,
|
|
51
|
+
children,
|
|
52
|
+
}: {
|
|
53
|
+
hasPermission: boolean;
|
|
54
|
+
children: React.ReactNode;
|
|
55
|
+
}) {
|
|
56
|
+
if (!hasPermission) {
|
|
57
|
+
return (
|
|
58
|
+
<InfoBanner variant="info">
|
|
59
|
+
Additional strategy-specific visualizations are available with the
|
|
60
|
+
"Read Health Check Details" permission.
|
|
61
|
+
</InfoBanner>
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
return <>{children}</>;
|
|
65
|
+
}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import React, { useState } from "react";
|
|
1
|
+
import React, { useState, useEffect } from "react";
|
|
2
2
|
import {
|
|
3
3
|
HealthCheckConfiguration,
|
|
4
4
|
HealthCheckStrategyDto,
|
|
5
5
|
CreateHealthCheckConfiguration,
|
|
6
|
+
CollectorConfigEntry,
|
|
6
7
|
} from "@checkstack/healthcheck-common";
|
|
7
8
|
import {
|
|
8
9
|
Button,
|
|
@@ -16,6 +17,8 @@ import {
|
|
|
16
17
|
DialogTitle,
|
|
17
18
|
DialogFooter,
|
|
18
19
|
} from "@checkstack/ui";
|
|
20
|
+
import { useCollectors } from "../hooks/useCollectors";
|
|
21
|
+
import { CollectorList } from "./CollectorList";
|
|
19
22
|
|
|
20
23
|
interface HealthCheckEditorProps {
|
|
21
24
|
strategies: HealthCheckStrategyDto[];
|
|
@@ -40,20 +43,36 @@ export const HealthCheckEditor: React.FC<HealthCheckEditorProps> = ({
|
|
|
40
43
|
const [config, setConfig] = useState<Record<string, unknown>>(
|
|
41
44
|
(initialData?.config as Record<string, unknown>) || {}
|
|
42
45
|
);
|
|
46
|
+
const [collectors, setCollectors] = useState<CollectorConfigEntry[]>(
|
|
47
|
+
initialData?.collectors || []
|
|
48
|
+
);
|
|
43
49
|
|
|
44
50
|
const toast = useToast();
|
|
45
51
|
const [loading, setLoading] = useState(false);
|
|
52
|
+
const [collectorsValid, setCollectorsValid] = useState(true);
|
|
53
|
+
|
|
54
|
+
// Fetch available collectors for the selected strategy
|
|
55
|
+
const { collectors: availableCollectors, loading: collectorsLoading } =
|
|
56
|
+
useCollectors(strategyId);
|
|
46
57
|
|
|
47
58
|
// Reset form when dialog opens with new data
|
|
48
|
-
|
|
59
|
+
useEffect(() => {
|
|
49
60
|
if (open) {
|
|
50
61
|
setName(initialData?.name || "");
|
|
51
62
|
setStrategyId(initialData?.strategyId || "");
|
|
52
63
|
setInterval(initialData?.intervalSeconds?.toString() || "60");
|
|
53
64
|
setConfig((initialData?.config as Record<string, unknown>) || {});
|
|
65
|
+
setCollectors(initialData?.collectors || []);
|
|
54
66
|
}
|
|
55
67
|
}, [open, initialData]);
|
|
56
68
|
|
|
69
|
+
// Clear collectors when strategy changes (new strategy = different collectors)
|
|
70
|
+
const handleStrategyChange = (id: string) => {
|
|
71
|
+
setStrategyId(id);
|
|
72
|
+
setConfig({});
|
|
73
|
+
setCollectors([]);
|
|
74
|
+
};
|
|
75
|
+
|
|
57
76
|
const handleSave = async (e: React.FormEvent) => {
|
|
58
77
|
e.preventDefault();
|
|
59
78
|
setLoading(true);
|
|
@@ -63,6 +82,7 @@ export const HealthCheckEditor: React.FC<HealthCheckEditorProps> = ({
|
|
|
63
82
|
strategyId,
|
|
64
83
|
intervalSeconds: Number.parseInt(interval, 10),
|
|
65
84
|
config,
|
|
85
|
+
collectors: collectors.length > 0 ? collectors : undefined,
|
|
66
86
|
});
|
|
67
87
|
} catch (error) {
|
|
68
88
|
const message =
|
|
@@ -84,7 +104,7 @@ export const HealthCheckEditor: React.FC<HealthCheckEditorProps> = ({
|
|
|
84
104
|
</DialogTitle>
|
|
85
105
|
</DialogHeader>
|
|
86
106
|
|
|
87
|
-
<div className="space-y-
|
|
107
|
+
<div className="space-y-6 py-4 max-h-[70vh] overflow-y-auto">
|
|
88
108
|
<div className="space-y-2">
|
|
89
109
|
<Label htmlFor="name">Name</Label>
|
|
90
110
|
<Input
|
|
@@ -111,21 +131,30 @@ export const HealthCheckEditor: React.FC<HealthCheckEditorProps> = ({
|
|
|
111
131
|
label="Strategy"
|
|
112
132
|
plugins={strategies}
|
|
113
133
|
selectedPluginId={strategyId}
|
|
114
|
-
onPluginChange={
|
|
115
|
-
setStrategyId(id);
|
|
116
|
-
setConfig({});
|
|
117
|
-
}}
|
|
134
|
+
onPluginChange={handleStrategyChange}
|
|
118
135
|
config={config}
|
|
119
136
|
onConfigChange={setConfig}
|
|
120
137
|
disabled={!!initialData}
|
|
121
138
|
/>
|
|
139
|
+
|
|
140
|
+
{/* Collector Configuration Section */}
|
|
141
|
+
{strategyId && (
|
|
142
|
+
<CollectorList
|
|
143
|
+
strategyId={strategyId}
|
|
144
|
+
availableCollectors={availableCollectors}
|
|
145
|
+
configuredCollectors={collectors}
|
|
146
|
+
onChange={setCollectors}
|
|
147
|
+
loading={collectorsLoading}
|
|
148
|
+
onValidChange={setCollectorsValid}
|
|
149
|
+
/>
|
|
150
|
+
)}
|
|
122
151
|
</div>
|
|
123
152
|
|
|
124
153
|
<DialogFooter>
|
|
125
154
|
<Button type="button" variant="outline" onClick={onCancel}>
|
|
126
155
|
Cancel
|
|
127
156
|
</Button>
|
|
128
|
-
<Button type="submit" disabled={loading}>
|
|
157
|
+
<Button type="submit" disabled={loading || !collectorsValid}>
|
|
129
158
|
{loading ? "Saving..." : "Save"}
|
|
130
159
|
</Button>
|
|
131
160
|
</DialogFooter>
|