@checkstack/healthcheck-frontend 0.11.8 → 0.12.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 +64 -0
- package/package.json +5 -5
- package/src/auto-charts/schema-parser.ts +0 -7
- package/src/components/HealthCheckSystemOverview.tsx +2 -66
- package/src/components/SystemHealthCheckAssignment.tsx +4 -4
- package/src/components/editor/CollectorPicker.tsx +129 -0
- package/src/components/editor/CollectorSection.tsx +94 -0
- package/src/components/editor/EditorPanel.tsx +164 -0
- package/src/components/editor/EditorTree.tsx +185 -0
- package/src/components/editor/GeneralSection.tsx +98 -0
- package/src/components/editor/IDEStatusBar.tsx +58 -0
- package/src/hooks/useCollectors.ts +1 -1
- package/src/index.tsx +14 -0
- package/src/pages/HealthCheckConfigPage.tsx +16 -63
- package/src/pages/HealthCheckIDEPage.tsx +382 -0
- package/src/pages/StrategyPickerPage.tsx +157 -0
- package/src/components/HealthCheckEditor.tsx +0 -190
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,69 @@
|
|
|
1
1
|
# @checkstack/healthcheck-frontend
|
|
2
2
|
|
|
3
|
+
## 0.12.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- d1a2796: Enforce stricter code quality standards and eliminate AI slop anti-patterns.
|
|
8
|
+
|
|
9
|
+
**New utility**
|
|
10
|
+
|
|
11
|
+
- `extractErrorMessage(error, fallback?)` in `@checkstack/common` for consistent error extraction
|
|
12
|
+
|
|
13
|
+
**ESLint rules**
|
|
14
|
+
|
|
15
|
+
- `react-hooks/rules-of-hooks` and `exhaustive-deps` for hook correctness
|
|
16
|
+
- `no-console` in frontend packages — forces `toast` over silent `console.error`
|
|
17
|
+
- `no-restricted-syntax` banning `instanceof Error` — forces `extractErrorMessage`
|
|
18
|
+
- Custom `no-eslint-disable-any` rule preventing `@typescript-eslint/no-explicit-any` circumvention
|
|
19
|
+
|
|
20
|
+
**Refactoring**
|
|
21
|
+
|
|
22
|
+
- Replace 141 `instanceof Error` boilerplate patterns across the codebase
|
|
23
|
+
- Replace swallowed `console.error` with user-visible `toast.error()` feedback
|
|
24
|
+
- Remove 15 redundant `as` type casts in IntegrationsPage and ProviderConnectionsPage
|
|
25
|
+
- Consolidate 3 identical callback handlers into `handleDialogClose`
|
|
26
|
+
- Fix conditional React hook call in `FormField.tsx`
|
|
27
|
+
- Fix unstable useMemo deps in `Dashboard.tsx`
|
|
28
|
+
- Replace `useEffect`→`setState` with derived `useMemo` in `RegisterPage.tsx`
|
|
29
|
+
- Rewrite `keystore.test.ts` with typed `DrizzleMockChain` (eliminating 7 `any` suppressions)
|
|
30
|
+
- Delete obvious comments in `encryption.ts` and Teams `provider.ts`
|
|
31
|
+
|
|
32
|
+
- Updated dependencies [d1a2796]
|
|
33
|
+
- Updated dependencies [3c34b07]
|
|
34
|
+
- @checkstack/common@0.6.5
|
|
35
|
+
- @checkstack/ui@1.2.1
|
|
36
|
+
- @checkstack/auth-frontend@0.5.18
|
|
37
|
+
- @checkstack/dashboard-frontend@0.3.26
|
|
38
|
+
- @checkstack/frontend-api@0.3.9
|
|
39
|
+
- @checkstack/catalog-common@1.3.1
|
|
40
|
+
- @checkstack/healthcheck-common@0.10.1
|
|
41
|
+
- @checkstack/signal-frontend@0.0.15
|
|
42
|
+
|
|
43
|
+
## 0.12.0
|
|
44
|
+
|
|
45
|
+
### Minor Changes
|
|
46
|
+
|
|
47
|
+
- 54a5f80: ### Health Check Editor Redesign — IDE-Style Experience
|
|
48
|
+
|
|
49
|
+
Replaces the modal-based health check editor with a full-page, IDE-style experience:
|
|
50
|
+
|
|
51
|
+
- **Strategy Picker Page**: New `/config/create` page with categorized strategy discovery, search filtering, and grouped card grid layout
|
|
52
|
+
- **IDE Editor Page**: New `/config/:configId/edit` page with a split-view layout — explorer tree on the left, editor panel on the right
|
|
53
|
+
- **Strategy Categories**: Introduces `StrategyCategory` enum with 16 categories (Networking, Database, Infrastructure, etc.) — all 13 strategy plugins now declare their category
|
|
54
|
+
- **New RPC Endpoint**: Added `getConfiguration` (singular by ID) for efficient single-resource fetching on the edit page
|
|
55
|
+
- **Explorer Tree**: Left-hand navigation with General, Check Items (collectors), and Access Control sections, with real-time validation indicators
|
|
56
|
+
- **Validation Status Bar**: Bottom bar showing aggregated validation issues with clickable navigation
|
|
57
|
+
- **Unsaved Changes Guard**: Browser `beforeunload` protection when the form is dirty
|
|
58
|
+
- **Responsive Design**: Split-view on desktop, stacked layout on mobile
|
|
59
|
+
- **Deleted**: Legacy `HealthCheckEditor.tsx` modal component
|
|
60
|
+
|
|
61
|
+
### Patch Changes
|
|
62
|
+
|
|
63
|
+
- Updated dependencies [54a5f80]
|
|
64
|
+
- @checkstack/healthcheck-common@0.10.0
|
|
65
|
+
- @checkstack/dashboard-frontend@0.3.25
|
|
66
|
+
|
|
3
67
|
## 0.11.8
|
|
4
68
|
|
|
5
69
|
### Patch Changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@checkstack/healthcheck-frontend",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "src/index.tsx",
|
|
6
6
|
"checkstack": {
|
|
@@ -13,11 +13,11 @@
|
|
|
13
13
|
},
|
|
14
14
|
"dependencies": {
|
|
15
15
|
"@checkstack/auth-frontend": "0.5.17",
|
|
16
|
-
"@checkstack/catalog-common": "1.
|
|
16
|
+
"@checkstack/catalog-common": "1.3.0",
|
|
17
17
|
"@checkstack/common": "0.6.4",
|
|
18
|
-
"@checkstack/dashboard-frontend": "0.3.
|
|
18
|
+
"@checkstack/dashboard-frontend": "0.3.25",
|
|
19
19
|
"@checkstack/frontend-api": "0.3.8",
|
|
20
|
-
"@checkstack/healthcheck-common": "0.
|
|
20
|
+
"@checkstack/healthcheck-common": "0.10.0",
|
|
21
21
|
"@checkstack/signal-frontend": "0.0.14",
|
|
22
22
|
"@checkstack/ui": "1.2.0",
|
|
23
23
|
"ajv": "^8.18.0",
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
},
|
|
33
33
|
"devDependencies": {
|
|
34
34
|
"@checkstack/scripts": "0.1.2",
|
|
35
|
-
"@checkstack/tsconfig": "0.0.
|
|
35
|
+
"@checkstack/tsconfig": "0.0.5",
|
|
36
36
|
"@types/react": "^18.2.0",
|
|
37
37
|
"typescript": "^5.0.0"
|
|
38
38
|
}
|
|
@@ -230,10 +230,6 @@ function extractComputedValue(value: unknown): unknown {
|
|
|
230
230
|
|
|
231
231
|
// _type is required for all aggregated state objects
|
|
232
232
|
if (!("_type" in obj)) {
|
|
233
|
-
console.error(
|
|
234
|
-
"[AutoChart] Missing _type discriminator in aggregated state:",
|
|
235
|
-
obj,
|
|
236
|
-
);
|
|
237
233
|
return value;
|
|
238
234
|
}
|
|
239
235
|
|
|
@@ -252,9 +248,6 @@ function extractComputedValue(value: unknown): unknown {
|
|
|
252
248
|
return obj.max;
|
|
253
249
|
}
|
|
254
250
|
default: {
|
|
255
|
-
console.error(
|
|
256
|
-
`[AutoChart] Unrecognized aggregated state type: ${String(obj._type)}`,
|
|
257
|
-
);
|
|
258
251
|
return value;
|
|
259
252
|
}
|
|
260
253
|
}
|
|
@@ -36,7 +36,7 @@ import {
|
|
|
36
36
|
CardContent,
|
|
37
37
|
CardHeader,
|
|
38
38
|
CardTitle,
|
|
39
|
-
|
|
39
|
+
|
|
40
40
|
} from "@checkstack/ui";
|
|
41
41
|
import { formatDistanceToNow } from "date-fns";
|
|
42
42
|
import { ChevronDown, ChevronRight } from "lucide-react";
|
|
@@ -71,22 +71,6 @@ interface ExpandedRowProps {
|
|
|
71
71
|
systemId: string;
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
-
// Helper to get color class for availability percentage
|
|
75
|
-
const getAvailabilityColorClass = (
|
|
76
|
-
value: number | null,
|
|
77
|
-
totalRuns: number,
|
|
78
|
-
): string => {
|
|
79
|
-
if (value === null || totalRuns === 0) {
|
|
80
|
-
return "text-muted-foreground";
|
|
81
|
-
}
|
|
82
|
-
if (value >= 99.9) {
|
|
83
|
-
return "text-green-600 dark:text-green-400";
|
|
84
|
-
}
|
|
85
|
-
if (value >= 99) {
|
|
86
|
-
return "text-yellow-600 dark:text-yellow-400";
|
|
87
|
-
}
|
|
88
|
-
return "text-red-600 dark:text-red-400";
|
|
89
|
-
};
|
|
90
74
|
|
|
91
75
|
const ExpandedDetails: React.FC<ExpandedRowProps> = ({ item, systemId }) => {
|
|
92
76
|
const healthCheckClient = usePluginClient(HealthCheckApi);
|
|
@@ -193,12 +177,11 @@ const ExpandedDetails: React.FC<ExpandedRowProps> = ({ item, systemId }) => {
|
|
|
193
177
|
}
|
|
194
178
|
const runs = displayRuns;
|
|
195
179
|
|
|
196
|
-
// Listen for realtime health check updates to refresh history table
|
|
180
|
+
// Listen for realtime health check updates to refresh history table
|
|
197
181
|
// Charts are refreshed automatically by useHealthCheckData
|
|
198
182
|
useSignal(HEALTH_CHECK_RUN_COMPLETED, ({ systemId: changedId }) => {
|
|
199
183
|
if (changedId === systemId) {
|
|
200
184
|
void refetch();
|
|
201
|
-
void refetchAvailability();
|
|
202
185
|
}
|
|
203
186
|
});
|
|
204
187
|
|
|
@@ -208,12 +191,6 @@ const ExpandedDetails: React.FC<ExpandedRowProps> = ({ item, systemId }) => {
|
|
|
208
191
|
: `Window mode (${item.stateThresholds.windowSize} runs): Degraded at ${item.stateThresholds.degraded.minFailureCount}+ failures, Unhealthy at ${item.stateThresholds.unhealthy.minFailureCount}+ failures`
|
|
209
192
|
: "Using default thresholds";
|
|
210
193
|
|
|
211
|
-
// Fetch availability stats
|
|
212
|
-
const { data: availabilityData, refetch: refetchAvailability } =
|
|
213
|
-
healthCheckClient.getAvailabilityStats.useQuery({
|
|
214
|
-
systemId,
|
|
215
|
-
configurationId: item.configurationId,
|
|
216
|
-
});
|
|
217
194
|
|
|
218
195
|
// Render charts - charts handle data transformation internally
|
|
219
196
|
const renderCharts = () => {
|
|
@@ -289,48 +266,7 @@ const ExpandedDetails: React.FC<ExpandedRowProps> = ({ item, systemId }) => {
|
|
|
289
266
|
</div>
|
|
290
267
|
</div>
|
|
291
268
|
|
|
292
|
-
{/* Availability Stats - Prominent Display */}
|
|
293
|
-
{availabilityData && (
|
|
294
|
-
<div className="grid grid-cols-2 gap-4">
|
|
295
|
-
<div className="flex flex-col gap-1 p-4 rounded-lg border bg-card">
|
|
296
|
-
<span className="text-xs font-medium text-muted-foreground uppercase tracking-wide">
|
|
297
|
-
31-Day Availability
|
|
298
|
-
</span>
|
|
299
|
-
<div className="flex items-baseline gap-2">
|
|
300
|
-
<AnimatedNumber
|
|
301
|
-
value={availabilityData.availability31Days ?? undefined}
|
|
302
|
-
suffix="%"
|
|
303
|
-
className={`text-2xl font-bold ${getAvailabilityColorClass(availabilityData.availability31Days, availabilityData.totalRuns31Days)}`}
|
|
304
|
-
/>
|
|
305
|
-
{availabilityData.totalRuns31Days > 0 && (
|
|
306
|
-
<span className="text-sm text-muted-foreground">
|
|
307
|
-
({availabilityData.totalRuns31Days.toLocaleString()} runs)
|
|
308
|
-
</span>
|
|
309
|
-
)}
|
|
310
|
-
</div>
|
|
311
|
-
</div>
|
|
312
|
-
<div className="flex flex-col gap-1 p-4 rounded-lg border bg-card">
|
|
313
|
-
<span className="text-xs font-medium text-muted-foreground uppercase tracking-wide">
|
|
314
|
-
365-Day Availability
|
|
315
|
-
</span>
|
|
316
|
-
<div className="flex items-baseline gap-2">
|
|
317
|
-
<AnimatedNumber
|
|
318
|
-
value={availabilityData.availability365Days ?? undefined}
|
|
319
|
-
suffix="%"
|
|
320
|
-
className={`text-2xl font-bold ${getAvailabilityColorClass(availabilityData.availability365Days, availabilityData.totalRuns365Days)}`}
|
|
321
|
-
/>
|
|
322
|
-
{availabilityData.totalRuns365Days > 0 && (
|
|
323
|
-
<span className="text-sm text-muted-foreground">
|
|
324
|
-
({availabilityData.totalRuns365Days.toLocaleString()} runs)
|
|
325
|
-
</span>
|
|
326
|
-
)}
|
|
327
|
-
</div>
|
|
328
|
-
</div>
|
|
329
|
-
</div>
|
|
330
|
-
)}
|
|
331
269
|
|
|
332
|
-
{/* Divider */}
|
|
333
|
-
<div className="h-px bg-border" />
|
|
334
270
|
|
|
335
271
|
{/* Date Range Filter with Loading Spinner */}
|
|
336
272
|
<div className="flex items-center gap-3 flex-wrap">
|
|
@@ -35,7 +35,7 @@ import {
|
|
|
35
35
|
healthcheckRoutes,
|
|
36
36
|
healthCheckAccess,
|
|
37
37
|
} from "@checkstack/healthcheck-common";
|
|
38
|
-
import { resolveRoute } from "@checkstack/common";
|
|
38
|
+
import { resolveRoute, extractErrorMessage} from "@checkstack/common";
|
|
39
39
|
import { DEFAULT_RETENTION_CONFIG } from "@checkstack/healthcheck-common";
|
|
40
40
|
|
|
41
41
|
type SelectedPanel = { configId: string; panel: "thresholds" | "retention" };
|
|
@@ -132,7 +132,7 @@ export const SystemHealthCheckAssignment: React.FC<Props> = ({
|
|
|
132
132
|
void refetchAssociations();
|
|
133
133
|
},
|
|
134
134
|
onError: (error) => {
|
|
135
|
-
toast.error(error
|
|
135
|
+
toast.error(extractErrorMessage(error, "Failed to update"));
|
|
136
136
|
},
|
|
137
137
|
});
|
|
138
138
|
|
|
@@ -144,7 +144,7 @@ export const SystemHealthCheckAssignment: React.FC<Props> = ({
|
|
|
144
144
|
},
|
|
145
145
|
onError: (error) => {
|
|
146
146
|
toast.error(
|
|
147
|
-
error
|
|
147
|
+
extractErrorMessage(error, "Failed to update")
|
|
148
148
|
);
|
|
149
149
|
},
|
|
150
150
|
}
|
|
@@ -158,7 +158,7 @@ export const SystemHealthCheckAssignment: React.FC<Props> = ({
|
|
|
158
158
|
setSelectedPanel(undefined);
|
|
159
159
|
},
|
|
160
160
|
onError: (error) => {
|
|
161
|
-
toast.error(error
|
|
161
|
+
toast.error(extractErrorMessage(error, "Failed to save"));
|
|
162
162
|
},
|
|
163
163
|
});
|
|
164
164
|
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import React, { useMemo } from "react";
|
|
2
|
+
import type {
|
|
3
|
+
CollectorConfigEntry,
|
|
4
|
+
CollectorDto,
|
|
5
|
+
} from "@checkstack/healthcheck-common";
|
|
6
|
+
import { Badge } from "@checkstack/ui";
|
|
7
|
+
import { Search } from "lucide-react";
|
|
8
|
+
import { isBuiltInCollector } from "../../hooks/useCollectors";
|
|
9
|
+
|
|
10
|
+
interface CollectorPickerProps {
|
|
11
|
+
availableCollectors: CollectorDto[];
|
|
12
|
+
configuredCollectors: CollectorConfigEntry[];
|
|
13
|
+
loading: boolean;
|
|
14
|
+
onAdd: (collectorId: string) => void;
|
|
15
|
+
strategyId: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const CollectorPicker: React.FC<CollectorPickerProps> = ({
|
|
19
|
+
availableCollectors,
|
|
20
|
+
configuredCollectors,
|
|
21
|
+
loading,
|
|
22
|
+
onAdd,
|
|
23
|
+
strategyId,
|
|
24
|
+
}) => {
|
|
25
|
+
const configuredIds = useMemo(
|
|
26
|
+
() => new Set(configuredCollectors.map((c) => c.collectorId)),
|
|
27
|
+
[configuredCollectors],
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
const builtInCollectors = availableCollectors.filter((c) =>
|
|
31
|
+
isBuiltInCollector(c.id, strategyId),
|
|
32
|
+
);
|
|
33
|
+
const externalCollectors = availableCollectors.filter(
|
|
34
|
+
(c) => !isBuiltInCollector(c.id, strategyId),
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
if (loading) {
|
|
38
|
+
return (
|
|
39
|
+
<div className="space-y-4">
|
|
40
|
+
<h2 className="text-lg font-semibold">Add Check Item</h2>
|
|
41
|
+
<p className="text-sm text-muted-foreground">Loading collectors...</p>
|
|
42
|
+
</div>
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const renderCollectorCard = (collector: CollectorDto) => {
|
|
47
|
+
const alreadyAdded = configuredIds.has(collector.id);
|
|
48
|
+
const canAdd = !alreadyAdded || collector.allowMultiple;
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
<button
|
|
52
|
+
key={collector.id}
|
|
53
|
+
type="button"
|
|
54
|
+
disabled={!canAdd}
|
|
55
|
+
onClick={() => onAdd(collector.id)}
|
|
56
|
+
className={`flex flex-col items-start gap-1.5 rounded-lg border p-4 text-left transition-all ${
|
|
57
|
+
canAdd
|
|
58
|
+
? "hover:border-primary/50 hover:shadow-md hover:shadow-primary/5 cursor-pointer"
|
|
59
|
+
: "opacity-50 cursor-not-allowed"
|
|
60
|
+
}`}
|
|
61
|
+
>
|
|
62
|
+
<div className="flex w-full items-center justify-between gap-2">
|
|
63
|
+
<h3 className="font-semibold text-sm">{collector.displayName}</h3>
|
|
64
|
+
<div className="flex gap-1.5 shrink-0">
|
|
65
|
+
{collector.allowMultiple && (
|
|
66
|
+
<Badge variant="outline" className="text-[10px]">
|
|
67
|
+
Multiple
|
|
68
|
+
</Badge>
|
|
69
|
+
)}
|
|
70
|
+
{alreadyAdded && (
|
|
71
|
+
<Badge variant="secondary" className="text-[10px]">
|
|
72
|
+
Added
|
|
73
|
+
</Badge>
|
|
74
|
+
)}
|
|
75
|
+
</div>
|
|
76
|
+
</div>
|
|
77
|
+
{collector.description && (
|
|
78
|
+
<p className="text-xs text-muted-foreground line-clamp-2">
|
|
79
|
+
{collector.description}
|
|
80
|
+
</p>
|
|
81
|
+
)}
|
|
82
|
+
</button>
|
|
83
|
+
);
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
return (
|
|
87
|
+
<div className="space-y-6">
|
|
88
|
+
<div>
|
|
89
|
+
<h2 className="text-lg font-semibold">Add Check Item</h2>
|
|
90
|
+
<p className="text-sm text-muted-foreground">
|
|
91
|
+
Select a collector to add to this health check.
|
|
92
|
+
</p>
|
|
93
|
+
</div>
|
|
94
|
+
|
|
95
|
+
{availableCollectors.length === 0 ? (
|
|
96
|
+
<div className="flex flex-col items-center justify-center py-12 text-muted-foreground">
|
|
97
|
+
<Search className="h-8 w-8 mb-3 opacity-40" />
|
|
98
|
+
<p className="text-sm">No collectors available for this strategy.</p>
|
|
99
|
+
</div>
|
|
100
|
+
) : (
|
|
101
|
+
<div className="space-y-6">
|
|
102
|
+
{builtInCollectors.length > 0 && (
|
|
103
|
+
<section>
|
|
104
|
+
<h3 className="text-xs font-semibold text-muted-foreground uppercase tracking-wider mb-3">
|
|
105
|
+
Built-in
|
|
106
|
+
</h3>
|
|
107
|
+
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
|
108
|
+
{builtInCollectors.map((col) => renderCollectorCard(col))}
|
|
109
|
+
</div>
|
|
110
|
+
</section>
|
|
111
|
+
)}
|
|
112
|
+
|
|
113
|
+
{externalCollectors.length > 0 && (
|
|
114
|
+
<section>
|
|
115
|
+
<h3 className="text-xs font-semibold text-muted-foreground uppercase tracking-wider mb-3">
|
|
116
|
+
External
|
|
117
|
+
</h3>
|
|
118
|
+
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
|
119
|
+
{externalCollectors.map((extCol) =>
|
|
120
|
+
renderCollectorCard(extCol),
|
|
121
|
+
)}
|
|
122
|
+
</div>
|
|
123
|
+
</section>
|
|
124
|
+
)}
|
|
125
|
+
</div>
|
|
126
|
+
)}
|
|
127
|
+
</div>
|
|
128
|
+
);
|
|
129
|
+
};
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import type {
|
|
3
|
+
CollectorConfigEntry,
|
|
4
|
+
CollectorDto,
|
|
5
|
+
} from "@checkstack/healthcheck-common";
|
|
6
|
+
import { Button, DynamicForm, Label } from "@checkstack/ui";
|
|
7
|
+
import { Trash2 } from "lucide-react";
|
|
8
|
+
import { AssertionBuilder, type Assertion } from "../AssertionBuilder";
|
|
9
|
+
|
|
10
|
+
interface CollectorSectionProps {
|
|
11
|
+
entry: CollectorConfigEntry;
|
|
12
|
+
collectorDef: CollectorDto | undefined;
|
|
13
|
+
onConfigChange: (config: Record<string, unknown>) => void;
|
|
14
|
+
onAssertionsChange: (assertions: CollectorConfigEntry["assertions"]) => void;
|
|
15
|
+
onValidChange: (isValid: boolean) => void;
|
|
16
|
+
onRemove: () => void;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const CollectorSection: React.FC<CollectorSectionProps> = ({
|
|
20
|
+
entry,
|
|
21
|
+
collectorDef,
|
|
22
|
+
onConfigChange,
|
|
23
|
+
onAssertionsChange,
|
|
24
|
+
onValidChange,
|
|
25
|
+
onRemove,
|
|
26
|
+
}) => {
|
|
27
|
+
return (
|
|
28
|
+
<div className="space-y-6">
|
|
29
|
+
{/* Header */}
|
|
30
|
+
<div className="flex items-start justify-between">
|
|
31
|
+
<div>
|
|
32
|
+
<h2 className="text-lg font-semibold">
|
|
33
|
+
{collectorDef?.displayName ?? entry.collectorId}
|
|
34
|
+
</h2>
|
|
35
|
+
{collectorDef?.description && (
|
|
36
|
+
<p className="text-sm text-muted-foreground">
|
|
37
|
+
{collectorDef.description}
|
|
38
|
+
</p>
|
|
39
|
+
)}
|
|
40
|
+
</div>
|
|
41
|
+
<Button
|
|
42
|
+
variant="ghost"
|
|
43
|
+
size="icon"
|
|
44
|
+
className="text-destructive hover:text-destructive shrink-0"
|
|
45
|
+
onClick={onRemove}
|
|
46
|
+
title="Remove collector"
|
|
47
|
+
>
|
|
48
|
+
<Trash2 className="h-4 w-4" />
|
|
49
|
+
</Button>
|
|
50
|
+
</div>
|
|
51
|
+
|
|
52
|
+
{/* Configuration */}
|
|
53
|
+
{collectorDef?.configSchema && (
|
|
54
|
+
<div className="space-y-3">
|
|
55
|
+
<div>
|
|
56
|
+
<Label className="text-sm font-semibold">Configuration</Label>
|
|
57
|
+
<p className="text-xs text-muted-foreground">
|
|
58
|
+
Configure how this check item behaves.
|
|
59
|
+
</p>
|
|
60
|
+
</div>
|
|
61
|
+
<DynamicForm
|
|
62
|
+
schema={collectorDef.configSchema}
|
|
63
|
+
value={entry.config}
|
|
64
|
+
onChange={onConfigChange}
|
|
65
|
+
onValidChange={onValidChange}
|
|
66
|
+
/>
|
|
67
|
+
</div>
|
|
68
|
+
)}
|
|
69
|
+
|
|
70
|
+
{/* Assertions */}
|
|
71
|
+
{collectorDef?.resultSchema && (
|
|
72
|
+
<div className="space-y-3 pt-2 border-t">
|
|
73
|
+
<div>
|
|
74
|
+
<Label className="text-sm font-semibold">Assertions</Label>
|
|
75
|
+
<p className="text-xs text-muted-foreground">
|
|
76
|
+
Define conditions that must be met for this check to pass.
|
|
77
|
+
</p>
|
|
78
|
+
</div>
|
|
79
|
+
<AssertionBuilder
|
|
80
|
+
resultSchema={collectorDef.resultSchema}
|
|
81
|
+
assertions={
|
|
82
|
+
(entry.assertions as unknown as Assertion[]) ?? []
|
|
83
|
+
}
|
|
84
|
+
onChange={(assertions) =>
|
|
85
|
+
onAssertionsChange(
|
|
86
|
+
assertions as unknown as CollectorConfigEntry["assertions"],
|
|
87
|
+
)
|
|
88
|
+
}
|
|
89
|
+
/>
|
|
90
|
+
</div>
|
|
91
|
+
)}
|
|
92
|
+
</div>
|
|
93
|
+
);
|
|
94
|
+
};
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import type {
|
|
3
|
+
CollectorConfigEntry,
|
|
4
|
+
CollectorDto,
|
|
5
|
+
HealthCheckStrategyDto,
|
|
6
|
+
} from "@checkstack/healthcheck-common";
|
|
7
|
+
import type { TreeNodeId } from "./EditorTree";
|
|
8
|
+
import { GeneralSection } from "./GeneralSection";
|
|
9
|
+
import { CollectorSection } from "./CollectorSection";
|
|
10
|
+
import { CollectorPicker } from "./CollectorPicker";
|
|
11
|
+
import { TeamAccessEditor } from "@checkstack/auth-frontend";
|
|
12
|
+
|
|
13
|
+
// =============================================================================
|
|
14
|
+
// TYPES
|
|
15
|
+
// =============================================================================
|
|
16
|
+
|
|
17
|
+
interface EditorPanelProps {
|
|
18
|
+
selectedNode: TreeNodeId;
|
|
19
|
+
formState: {
|
|
20
|
+
name: string;
|
|
21
|
+
intervalSeconds: number;
|
|
22
|
+
strategyConfig: Record<string, unknown>;
|
|
23
|
+
collectors: CollectorConfigEntry[];
|
|
24
|
+
};
|
|
25
|
+
strategy: HealthCheckStrategyDto | undefined;
|
|
26
|
+
availableCollectors: CollectorDto[];
|
|
27
|
+
collectorsLoading: boolean;
|
|
28
|
+
isEditMode: boolean;
|
|
29
|
+
configId: string | undefined;
|
|
30
|
+
onNameChange: (name: string) => void;
|
|
31
|
+
onIntervalChange: (interval: number) => void;
|
|
32
|
+
onStrategyConfigChange: (config: Record<string, unknown>) => void;
|
|
33
|
+
onStrategyConfigValidChange: (isValid: boolean) => void;
|
|
34
|
+
onCollectorConfigChange: (
|
|
35
|
+
entryId: string,
|
|
36
|
+
config: Record<string, unknown>,
|
|
37
|
+
) => void;
|
|
38
|
+
onCollectorAssertionsChange: (
|
|
39
|
+
entryId: string,
|
|
40
|
+
assertions: CollectorConfigEntry["assertions"],
|
|
41
|
+
) => void;
|
|
42
|
+
onCollectorValidChange: (entryId: string, isValid: boolean) => void;
|
|
43
|
+
onCollectorRemove: (entryId: string) => void;
|
|
44
|
+
onCollectorAdd: (collectorId: string) => void;
|
|
45
|
+
strategyId: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// =============================================================================
|
|
49
|
+
// PANEL COMPONENT
|
|
50
|
+
// =============================================================================
|
|
51
|
+
|
|
52
|
+
export const EditorPanel: React.FC<EditorPanelProps> = ({
|
|
53
|
+
selectedNode,
|
|
54
|
+
formState,
|
|
55
|
+
strategy,
|
|
56
|
+
availableCollectors,
|
|
57
|
+
collectorsLoading,
|
|
58
|
+
isEditMode,
|
|
59
|
+
configId,
|
|
60
|
+
onNameChange,
|
|
61
|
+
onIntervalChange,
|
|
62
|
+
onStrategyConfigChange,
|
|
63
|
+
onStrategyConfigValidChange,
|
|
64
|
+
onCollectorConfigChange,
|
|
65
|
+
onCollectorAssertionsChange,
|
|
66
|
+
onCollectorValidChange,
|
|
67
|
+
onCollectorRemove,
|
|
68
|
+
onCollectorAdd,
|
|
69
|
+
strategyId,
|
|
70
|
+
}) => {
|
|
71
|
+
// --- General Section ---
|
|
72
|
+
if (selectedNode === "general") {
|
|
73
|
+
return (
|
|
74
|
+
<div className="p-6">
|
|
75
|
+
<GeneralSection
|
|
76
|
+
name={formState.name}
|
|
77
|
+
intervalSeconds={formState.intervalSeconds}
|
|
78
|
+
strategyConfig={formState.strategyConfig}
|
|
79
|
+
strategy={strategy}
|
|
80
|
+
onNameChange={onNameChange}
|
|
81
|
+
onIntervalChange={onIntervalChange}
|
|
82
|
+
onStrategyConfigChange={onStrategyConfigChange}
|
|
83
|
+
onStrategyConfigValidChange={onStrategyConfigValidChange}
|
|
84
|
+
/>
|
|
85
|
+
</div>
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// --- Collector Picker ---
|
|
90
|
+
if (selectedNode === "collector-picker") {
|
|
91
|
+
return (
|
|
92
|
+
<div className="p-6">
|
|
93
|
+
<CollectorPicker
|
|
94
|
+
availableCollectors={availableCollectors}
|
|
95
|
+
configuredCollectors={formState.collectors}
|
|
96
|
+
loading={collectorsLoading}
|
|
97
|
+
onAdd={onCollectorAdd}
|
|
98
|
+
strategyId={strategyId}
|
|
99
|
+
/>
|
|
100
|
+
</div>
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// --- Access Control ---
|
|
105
|
+
if (selectedNode === "access") {
|
|
106
|
+
return (
|
|
107
|
+
<div className="p-6">
|
|
108
|
+
<div className="space-y-4">
|
|
109
|
+
<div>
|
|
110
|
+
<h2 className="text-lg font-semibold">Access Control</h2>
|
|
111
|
+
<p className="text-sm text-muted-foreground">
|
|
112
|
+
Manage team permissions for this health check configuration.
|
|
113
|
+
</p>
|
|
114
|
+
</div>
|
|
115
|
+
{isEditMode && configId ? (
|
|
116
|
+
<TeamAccessEditor
|
|
117
|
+
resourceType="healthcheck.configuration"
|
|
118
|
+
resourceId={configId}
|
|
119
|
+
compact
|
|
120
|
+
expanded
|
|
121
|
+
/>
|
|
122
|
+
) : (
|
|
123
|
+
<p className="text-sm text-muted-foreground italic">
|
|
124
|
+
Access control is available after saving the configuration.
|
|
125
|
+
</p>
|
|
126
|
+
)}
|
|
127
|
+
</div>
|
|
128
|
+
</div>
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// --- Collector Section ---
|
|
133
|
+
if (selectedNode.startsWith("collector:")) {
|
|
134
|
+
const entryId = selectedNode.replace("collector:", "");
|
|
135
|
+
const entry = formState.collectors.find((c) => c.id === entryId);
|
|
136
|
+
|
|
137
|
+
if (!entry) {
|
|
138
|
+
return (
|
|
139
|
+
<div className="p-6 text-muted-foreground">Collector not found.</div>
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const collectorDef = availableCollectors.find(
|
|
144
|
+
(c) => c.id === entry.collectorId,
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
return (
|
|
148
|
+
<div className="p-6">
|
|
149
|
+
<CollectorSection
|
|
150
|
+
entry={entry}
|
|
151
|
+
collectorDef={collectorDef}
|
|
152
|
+
onConfigChange={(config) => onCollectorConfigChange(entryId, config)}
|
|
153
|
+
onAssertionsChange={(assertions) =>
|
|
154
|
+
onCollectorAssertionsChange(entryId, assertions)
|
|
155
|
+
}
|
|
156
|
+
onValidChange={(isValid) => onCollectorValidChange(entryId, isValid)}
|
|
157
|
+
onRemove={() => onCollectorRemove(entryId)}
|
|
158
|
+
/>
|
|
159
|
+
</div>
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return;
|
|
164
|
+
};
|