@checkstack/healthcheck-frontend 0.11.7 → 0.12.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 +60 -0
- package/package.json +5 -5
- package/src/components/SystemHealthBadge.tsx +3 -3
- 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/index.tsx +14 -0
- package/src/pages/HealthCheckConfigPage.tsx +12 -59
- 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,65 @@
|
|
|
1
1
|
# @checkstack/healthcheck-frontend
|
|
2
2
|
|
|
3
|
+
## 0.12.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 54a5f80: ### Health Check Editor Redesign — IDE-Style Experience
|
|
8
|
+
|
|
9
|
+
Replaces the modal-based health check editor with a full-page, IDE-style experience:
|
|
10
|
+
|
|
11
|
+
- **Strategy Picker Page**: New `/config/create` page with categorized strategy discovery, search filtering, and grouped card grid layout
|
|
12
|
+
- **IDE Editor Page**: New `/config/:configId/edit` page with a split-view layout — explorer tree on the left, editor panel on the right
|
|
13
|
+
- **Strategy Categories**: Introduces `StrategyCategory` enum with 16 categories (Networking, Database, Infrastructure, etc.) — all 13 strategy plugins now declare their category
|
|
14
|
+
- **New RPC Endpoint**: Added `getConfiguration` (singular by ID) for efficient single-resource fetching on the edit page
|
|
15
|
+
- **Explorer Tree**: Left-hand navigation with General, Check Items (collectors), and Access Control sections, with real-time validation indicators
|
|
16
|
+
- **Validation Status Bar**: Bottom bar showing aggregated validation issues with clickable navigation
|
|
17
|
+
- **Unsaved Changes Guard**: Browser `beforeunload` protection when the form is dirty
|
|
18
|
+
- **Responsive Design**: Split-view on desktop, stacked layout on mobile
|
|
19
|
+
- **Deleted**: Legacy `HealthCheckEditor.tsx` modal component
|
|
20
|
+
|
|
21
|
+
### Patch Changes
|
|
22
|
+
|
|
23
|
+
- Updated dependencies [54a5f80]
|
|
24
|
+
- @checkstack/healthcheck-common@0.10.0
|
|
25
|
+
- @checkstack/dashboard-frontend@0.3.25
|
|
26
|
+
|
|
27
|
+
## 0.11.8
|
|
28
|
+
|
|
29
|
+
### Patch Changes
|
|
30
|
+
|
|
31
|
+
- 1f191cf: Add SYSTEM_STATUS_CHANGED signal and dependency-driven notification improvements
|
|
32
|
+
|
|
33
|
+
**healthcheck-common:**
|
|
34
|
+
|
|
35
|
+
- New `SYSTEM_STATUS_CHANGED` signal that fires only on system-level health status transitions (healthy ↔ degraded ↔ unhealthy), providing a low-noise alternative to `HEALTH_CHECK_RUN_COMPLETED` for coarse-grained reactivity
|
|
36
|
+
|
|
37
|
+
**healthcheck-backend:**
|
|
38
|
+
|
|
39
|
+
- Broadcast `SYSTEM_STATUS_CHANGED` signal at both status transition code paths in the queue executor
|
|
40
|
+
|
|
41
|
+
**healthcheck-frontend:**
|
|
42
|
+
|
|
43
|
+
- Switch `SystemHealthBadge` from `HEALTH_CHECK_RUN_COMPLETED` to `SYSTEM_STATUS_CHANGED` to reduce unnecessary refetch noise
|
|
44
|
+
|
|
45
|
+
**dashboard-frontend:**
|
|
46
|
+
|
|
47
|
+
- Switch `SystemBadgeDataProvider` from `HEALTH_CHECK_RUN_COMPLETED` to `SYSTEM_STATUS_CHANGED` for more efficient badge updates
|
|
48
|
+
|
|
49
|
+
**maintenance-frontend:**
|
|
50
|
+
|
|
51
|
+
- Clarify that notification suppression toggle also applies to downstream dependency-driven notifications
|
|
52
|
+
|
|
53
|
+
**incident-frontend:**
|
|
54
|
+
|
|
55
|
+
- Clarify that notification suppression toggle also applies to downstream dependency-driven notifications
|
|
56
|
+
|
|
57
|
+
- Updated dependencies [1f191cf]
|
|
58
|
+
- Updated dependencies [3f36a64]
|
|
59
|
+
- @checkstack/healthcheck-common@0.9.0
|
|
60
|
+
- @checkstack/dashboard-frontend@0.3.24
|
|
61
|
+
- @checkstack/catalog-common@1.3.0
|
|
62
|
+
|
|
3
63
|
## 0.11.7
|
|
4
64
|
|
|
5
65
|
### 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.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "src/index.tsx",
|
|
6
6
|
"checkstack": {
|
|
@@ -12,14 +12,14 @@
|
|
|
12
12
|
"lint:code": "eslint . --max-warnings 0"
|
|
13
13
|
},
|
|
14
14
|
"dependencies": {
|
|
15
|
-
"@checkstack/auth-frontend": "0.5.
|
|
16
|
-
"@checkstack/catalog-common": "1.2.
|
|
15
|
+
"@checkstack/auth-frontend": "0.5.17",
|
|
16
|
+
"@checkstack/catalog-common": "1.2.11",
|
|
17
17
|
"@checkstack/common": "0.6.4",
|
|
18
|
-
"@checkstack/dashboard-frontend": "0.3.
|
|
18
|
+
"@checkstack/dashboard-frontend": "0.3.23",
|
|
19
19
|
"@checkstack/frontend-api": "0.3.8",
|
|
20
20
|
"@checkstack/healthcheck-common": "0.8.4",
|
|
21
21
|
"@checkstack/signal-frontend": "0.0.14",
|
|
22
|
-
"@checkstack/ui": "1.
|
|
22
|
+
"@checkstack/ui": "1.2.0",
|
|
23
23
|
"ajv": "^8.18.0",
|
|
24
24
|
"ajv-formats": "^3.0.1",
|
|
25
25
|
"date-fns": "^4.1.0",
|
|
@@ -2,7 +2,7 @@ import React from "react";
|
|
|
2
2
|
import { usePluginClient, type SlotContext } from "@checkstack/frontend-api";
|
|
3
3
|
import { useSignal } from "@checkstack/signal-frontend";
|
|
4
4
|
import { SystemStateBadgesSlot } from "@checkstack/catalog-common";
|
|
5
|
-
import {
|
|
5
|
+
import { SYSTEM_STATUS_CHANGED } from "@checkstack/healthcheck-common";
|
|
6
6
|
import { HealthCheckApi } from "../api";
|
|
7
7
|
import { HealthBadge } from "@checkstack/ui";
|
|
8
8
|
import { useSystemBadgeDataOptional } from "@checkstack/dashboard-frontend";
|
|
@@ -40,8 +40,8 @@ export const SystemHealthBadge: React.FC<Props> = ({ system }) => {
|
|
|
40
40
|
|
|
41
41
|
const localStatus = healthData?.status;
|
|
42
42
|
|
|
43
|
-
// Listen for realtime
|
|
44
|
-
useSignal(
|
|
43
|
+
// Listen for realtime system status changes (only in fallback mode)
|
|
44
|
+
useSignal(SYSTEM_STATUS_CHANGED, ({ systemId: changedId }) => {
|
|
45
45
|
if (!badgeData && changedId === system?.id) {
|
|
46
46
|
void refetch();
|
|
47
47
|
}
|
|
@@ -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
|
+
};
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import React, { useMemo } from "react";
|
|
2
|
+
import type {
|
|
3
|
+
CollectorConfigEntry,
|
|
4
|
+
CollectorDto,
|
|
5
|
+
} from "@checkstack/healthcheck-common";
|
|
6
|
+
import { Plus, Settings, Shield, ChevronRight } from "lucide-react";
|
|
7
|
+
import { isBuiltInCollector } from "../../hooks/useCollectors";
|
|
8
|
+
import type { ValidationIssue } from "./IDEStatusBar";
|
|
9
|
+
import { Badge } from "@checkstack/ui";
|
|
10
|
+
|
|
11
|
+
// =============================================================================
|
|
12
|
+
// TYPES
|
|
13
|
+
// =============================================================================
|
|
14
|
+
|
|
15
|
+
export type TreeNodeId =
|
|
16
|
+
| "general"
|
|
17
|
+
| "access"
|
|
18
|
+
| "collector-picker"
|
|
19
|
+
| `collector:${string}`;
|
|
20
|
+
|
|
21
|
+
interface EditorTreeProps {
|
|
22
|
+
collectors: CollectorConfigEntry[];
|
|
23
|
+
availableCollectors: CollectorDto[];
|
|
24
|
+
selectedNode: TreeNodeId;
|
|
25
|
+
onSelectNode: (nodeId: TreeNodeId) => void;
|
|
26
|
+
onAddCollector: (collectorId: string) => void;
|
|
27
|
+
validationIssues: ValidationIssue[];
|
|
28
|
+
strategyId: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// =============================================================================
|
|
32
|
+
// VALIDATION INDICATOR
|
|
33
|
+
// =============================================================================
|
|
34
|
+
|
|
35
|
+
function ValidationDot({ nodeId, issues }: { nodeId: string; issues: ValidationIssue[] }) {
|
|
36
|
+
const nodeIssues = issues.filter((i) => i.nodeId === nodeId);
|
|
37
|
+
if (nodeIssues.length === 0) return;
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
<span className="ml-auto flex h-2 w-2 rounded-full bg-destructive shrink-0" />
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// =============================================================================
|
|
45
|
+
// TREE NODE
|
|
46
|
+
// =============================================================================
|
|
47
|
+
|
|
48
|
+
function TreeNode({
|
|
49
|
+
nodeId,
|
|
50
|
+
label,
|
|
51
|
+
icon: Icon,
|
|
52
|
+
selected,
|
|
53
|
+
onClick,
|
|
54
|
+
issues,
|
|
55
|
+
indent = false,
|
|
56
|
+
badge,
|
|
57
|
+
}: {
|
|
58
|
+
nodeId: string;
|
|
59
|
+
label: string;
|
|
60
|
+
icon: React.ElementType;
|
|
61
|
+
selected: boolean;
|
|
62
|
+
onClick: () => void;
|
|
63
|
+
issues: ValidationIssue[];
|
|
64
|
+
indent?: boolean;
|
|
65
|
+
badge?: string;
|
|
66
|
+
}) {
|
|
67
|
+
return (
|
|
68
|
+
<button
|
|
69
|
+
type="button"
|
|
70
|
+
onClick={onClick}
|
|
71
|
+
className={`flex items-center gap-2 w-full px-3 py-2 text-sm text-left transition-colors ${
|
|
72
|
+
indent ? "pl-7" : ""
|
|
73
|
+
} ${
|
|
74
|
+
selected
|
|
75
|
+
? "bg-primary/10 text-primary border-l-2 border-primary"
|
|
76
|
+
: "hover:bg-muted/50 border-l-2 border-transparent"
|
|
77
|
+
}`}
|
|
78
|
+
>
|
|
79
|
+
<Icon className="h-4 w-4 shrink-0 opacity-60" />
|
|
80
|
+
<span className="truncate flex-1">{label}</span>
|
|
81
|
+
{badge && (
|
|
82
|
+
<Badge variant="secondary" className="text-[10px] shrink-0">
|
|
83
|
+
{badge}
|
|
84
|
+
</Badge>
|
|
85
|
+
)}
|
|
86
|
+
<ValidationDot nodeId={nodeId} issues={issues} />
|
|
87
|
+
</button>
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// =============================================================================
|
|
92
|
+
// EDITOR TREE
|
|
93
|
+
// =============================================================================
|
|
94
|
+
|
|
95
|
+
export const EditorTree: React.FC<EditorTreeProps> = ({
|
|
96
|
+
collectors,
|
|
97
|
+
availableCollectors,
|
|
98
|
+
selectedNode,
|
|
99
|
+
onSelectNode,
|
|
100
|
+
validationIssues,
|
|
101
|
+
strategyId,
|
|
102
|
+
}) => {
|
|
103
|
+
// Check if there are addable collectors remaining
|
|
104
|
+
const hasAddableCollectors = useMemo(() => {
|
|
105
|
+
const configuredIds = new Set(collectors.map((c) => c.collectorId));
|
|
106
|
+
return availableCollectors.some(
|
|
107
|
+
(c) => !configuredIds.has(c.id) || c.allowMultiple,
|
|
108
|
+
);
|
|
109
|
+
}, [collectors, availableCollectors]);
|
|
110
|
+
|
|
111
|
+
return (
|
|
112
|
+
<div className="py-2">
|
|
113
|
+
{/* General */}
|
|
114
|
+
<TreeNode
|
|
115
|
+
nodeId="general"
|
|
116
|
+
label="General"
|
|
117
|
+
icon={Settings}
|
|
118
|
+
selected={selectedNode === "general"}
|
|
119
|
+
onClick={() => onSelectNode("general")}
|
|
120
|
+
issues={validationIssues}
|
|
121
|
+
/>
|
|
122
|
+
|
|
123
|
+
{/* Collectors Section Header */}
|
|
124
|
+
<div className="px-3 pt-4 pb-1">
|
|
125
|
+
<span className="text-[11px] font-semibold text-muted-foreground uppercase tracking-wider">
|
|
126
|
+
Check Items
|
|
127
|
+
</span>
|
|
128
|
+
</div>
|
|
129
|
+
|
|
130
|
+
{/* Configured Collectors */}
|
|
131
|
+
{collectors.map((entry) => {
|
|
132
|
+
const collector = availableCollectors.find(
|
|
133
|
+
(c) => c.id === entry.collectorId,
|
|
134
|
+
);
|
|
135
|
+
const builtIn = isBuiltInCollector(entry.collectorId, strategyId);
|
|
136
|
+
|
|
137
|
+
return (
|
|
138
|
+
<TreeNode
|
|
139
|
+
key={entry.id}
|
|
140
|
+
nodeId={`collector:${entry.id}`}
|
|
141
|
+
label={collector?.displayName ?? entry.collectorId}
|
|
142
|
+
icon={ChevronRight}
|
|
143
|
+
selected={selectedNode === `collector:${entry.id}`}
|
|
144
|
+
onClick={() => onSelectNode(`collector:${entry.id}`)}
|
|
145
|
+
issues={validationIssues}
|
|
146
|
+
indent
|
|
147
|
+
badge={builtIn ? "Built-in" : undefined}
|
|
148
|
+
/>
|
|
149
|
+
);
|
|
150
|
+
})}
|
|
151
|
+
|
|
152
|
+
{/* Add Collector Button */}
|
|
153
|
+
{hasAddableCollectors && (
|
|
154
|
+
<button
|
|
155
|
+
type="button"
|
|
156
|
+
onClick={() => onSelectNode("collector-picker")}
|
|
157
|
+
className={`flex items-center gap-2 w-full px-3 py-2 pl-7 text-sm text-left transition-colors text-muted-foreground hover:text-foreground hover:bg-muted/50 border-l-2 ${
|
|
158
|
+
selectedNode === "collector-picker"
|
|
159
|
+
? "border-primary bg-primary/10 text-primary"
|
|
160
|
+
: "border-transparent"
|
|
161
|
+
}`}
|
|
162
|
+
>
|
|
163
|
+
<Plus className="h-4 w-4 shrink-0" />
|
|
164
|
+
<span className="truncate">Add check item...</span>
|
|
165
|
+
</button>
|
|
166
|
+
)}
|
|
167
|
+
|
|
168
|
+
{/* Access Control */}
|
|
169
|
+
<div className="px-3 pt-4 pb-1">
|
|
170
|
+
<span className="text-[11px] font-semibold text-muted-foreground uppercase tracking-wider">
|
|
171
|
+
Permissions
|
|
172
|
+
</span>
|
|
173
|
+
</div>
|
|
174
|
+
|
|
175
|
+
<TreeNode
|
|
176
|
+
nodeId="access"
|
|
177
|
+
label="Access Control"
|
|
178
|
+
icon={Shield}
|
|
179
|
+
selected={selectedNode === "access"}
|
|
180
|
+
onClick={() => onSelectNode("access")}
|
|
181
|
+
issues={validationIssues}
|
|
182
|
+
/>
|
|
183
|
+
</div>
|
|
184
|
+
);
|
|
185
|
+
};
|