@checkstack/ui 1.2.0 → 1.3.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 +66 -0
- package/package.json +4 -4
- package/src/components/DynamicForm/DynamicForm.tsx +2 -1
- package/src/components/DynamicForm/DynamicOptionsField.tsx +2 -1
- package/src/components/DynamicForm/FormField.tsx +18 -13
- package/src/components/IDELayout/index.tsx +200 -0
- package/src/index.ts +1 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,71 @@
|
|
|
1
1
|
# @checkstack/ui
|
|
2
2
|
|
|
3
|
+
## 1.3.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 26d8bae: Distributed satellite health checks and Assignment IDE page
|
|
8
|
+
|
|
9
|
+
**Satellite System**
|
|
10
|
+
|
|
11
|
+
- New `satellite-backend`, `satellite-common`, `satellite-frontend`, and `satellite` agent packages for distributed health check execution
|
|
12
|
+
- WebSocket-based satellite connectivity with authentication, heartbeats, and live configuration push
|
|
13
|
+
- Satellite management UI with create dialog, status badges, and list page
|
|
14
|
+
|
|
15
|
+
**Live Configuration Updates**
|
|
16
|
+
|
|
17
|
+
- Added `assignmentChanged` hook to `healthcheck-backend` for cross-plugin communication
|
|
18
|
+
- `satellite-backend` subscribes to assignment changes and pushes config updates to connected satellites in real-time
|
|
19
|
+
|
|
20
|
+
**Assignment IDE Page**
|
|
21
|
+
|
|
22
|
+
- Replaced the 1028-line modal-based `SystemHealthCheckAssignment` component with a full-page IDE layout
|
|
23
|
+
- New modular components: `AssignmentTree`, `GeneralPanel`, `ThresholdsPanel`, `RetentionPanel`, `ExecutionPanel`
|
|
24
|
+
- Added unassign capability and sorted assignment lists for stable ordering
|
|
25
|
+
|
|
26
|
+
**Shared IDE Primitives**
|
|
27
|
+
|
|
28
|
+
- Extracted `IDETreeNode`, `IDETreeSection`, `IDEStatusBar`, `IDELayout` to `@checkstack/ui` for cross-plugin reuse
|
|
29
|
+
- Migrated existing health check IDE editor to use shared primitives
|
|
30
|
+
|
|
31
|
+
**Infrastructure**
|
|
32
|
+
|
|
33
|
+
- Added `Dockerfile.satellite` for containerized satellite deployment
|
|
34
|
+
- WebSocket route registry in `@checkstack/backend` and `@checkstack/backend-api`
|
|
35
|
+
|
|
36
|
+
## 1.2.1
|
|
37
|
+
|
|
38
|
+
### Patch Changes
|
|
39
|
+
|
|
40
|
+
- d1a2796: Enforce stricter code quality standards and eliminate AI slop anti-patterns.
|
|
41
|
+
|
|
42
|
+
**New utility**
|
|
43
|
+
|
|
44
|
+
- `extractErrorMessage(error, fallback?)` in `@checkstack/common` for consistent error extraction
|
|
45
|
+
|
|
46
|
+
**ESLint rules**
|
|
47
|
+
|
|
48
|
+
- `react-hooks/rules-of-hooks` and `exhaustive-deps` for hook correctness
|
|
49
|
+
- `no-console` in frontend packages — forces `toast` over silent `console.error`
|
|
50
|
+
- `no-restricted-syntax` banning `instanceof Error` — forces `extractErrorMessage`
|
|
51
|
+
- Custom `no-eslint-disable-any` rule preventing `@typescript-eslint/no-explicit-any` circumvention
|
|
52
|
+
|
|
53
|
+
**Refactoring**
|
|
54
|
+
|
|
55
|
+
- Replace 141 `instanceof Error` boilerplate patterns across the codebase
|
|
56
|
+
- Replace swallowed `console.error` with user-visible `toast.error()` feedback
|
|
57
|
+
- Remove 15 redundant `as` type casts in IntegrationsPage and ProviderConnectionsPage
|
|
58
|
+
- Consolidate 3 identical callback handlers into `handleDialogClose`
|
|
59
|
+
- Fix conditional React hook call in `FormField.tsx`
|
|
60
|
+
- Fix unstable useMemo deps in `Dashboard.tsx`
|
|
61
|
+
- Replace `useEffect`→`setState` with derived `useMemo` in `RegisterPage.tsx`
|
|
62
|
+
- Rewrite `keystore.test.ts` with typed `DrizzleMockChain` (eliminating 7 `any` suppressions)
|
|
63
|
+
- Delete obvious comments in `encryption.ts` and Teams `provider.ts`
|
|
64
|
+
|
|
65
|
+
- Updated dependencies [d1a2796]
|
|
66
|
+
- @checkstack/common@0.6.5
|
|
67
|
+
- @checkstack/frontend-api@0.3.9
|
|
68
|
+
|
|
3
69
|
## 1.2.0
|
|
4
70
|
|
|
5
71
|
### Minor Changes
|
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@checkstack/ui",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "src/index.ts",
|
|
6
6
|
"dependencies": {
|
|
7
|
-
"@checkstack/common": "0.6.
|
|
8
|
-
"@checkstack/frontend-api": "0.3.
|
|
7
|
+
"@checkstack/common": "0.6.5",
|
|
8
|
+
"@checkstack/frontend-api": "0.3.9",
|
|
9
9
|
"@monaco-editor/react": "^4.7.0",
|
|
10
10
|
"@radix-ui/react-accordion": "^1.2.12",
|
|
11
11
|
"@radix-ui/react-dialog": "^1.1.15",
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"@types/react": "^18.2.0",
|
|
32
32
|
"@testing-library/react": "^16.0.0",
|
|
33
33
|
"@checkstack/test-utils-frontend": "0.0.4",
|
|
34
|
-
"@checkstack/tsconfig": "0.0.
|
|
34
|
+
"@checkstack/tsconfig": "0.0.5",
|
|
35
35
|
"@checkstack/scripts": "0.1.2"
|
|
36
36
|
},
|
|
37
37
|
"scripts": {
|
|
@@ -33,7 +33,8 @@ export const DynamicForm: React.FC<DynamicFormProps> = ({
|
|
|
33
33
|
if (JSON.stringify(merged) !== JSON.stringify(value)) {
|
|
34
34
|
onChange(merged);
|
|
35
35
|
}
|
|
36
|
-
|
|
36
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps -- Intentional: runs only on schema change. Including onChange would re-fire on parent re-renders; including value would cause an infinite loop since this effect calls onChange(merged)
|
|
37
|
+
}, [schema]);
|
|
37
38
|
|
|
38
39
|
// Compute validity and report changes
|
|
39
40
|
React.useEffect(() => {
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
|
|
14
14
|
import type { DynamicOptionsFieldProps, ResolverOption } from "./types";
|
|
15
15
|
import { getCleanDescription, NONE_SENTINEL } from "./utils";
|
|
16
|
+
import { extractErrorMessage } from "@checkstack/common";
|
|
16
17
|
|
|
17
18
|
/**
|
|
18
19
|
* Field component for dynamically resolved options.
|
|
@@ -72,7 +73,7 @@ export const DynamicOptionsField: React.FC<DynamicOptionsFieldProps> = ({
|
|
|
72
73
|
.catch((error_) => {
|
|
73
74
|
if (!cancelled) {
|
|
74
75
|
setError(
|
|
75
|
-
error_
|
|
76
|
+
extractErrorMessage(error_, "Failed to load options"),
|
|
76
77
|
);
|
|
77
78
|
setLoading(false);
|
|
78
79
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { useEffect, useState } from "react";
|
|
2
2
|
import { Plus, Trash2 } from "lucide-react";
|
|
3
3
|
|
|
4
4
|
import {
|
|
@@ -37,6 +37,14 @@ export const FormField: React.FC<FormFieldProps> = ({
|
|
|
37
37
|
}) => {
|
|
38
38
|
const description = propSchema.description || "";
|
|
39
39
|
|
|
40
|
+
// Const field handling - must be before any early returns (rules-of-hooks)
|
|
41
|
+
const isConstField = propSchema.const !== undefined;
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
if (isConstField && value !== propSchema.const) {
|
|
44
|
+
onChange(propSchema.const);
|
|
45
|
+
}
|
|
46
|
+
}, [isConstField, value, propSchema.const, onChange]);
|
|
47
|
+
|
|
40
48
|
// Dynamic options via resolver
|
|
41
49
|
const resolverName = propSchema["x-options-resolver"];
|
|
42
50
|
if (resolverName && optionsResolvers) {
|
|
@@ -57,14 +65,7 @@ export const FormField: React.FC<FormFieldProps> = ({
|
|
|
57
65
|
);
|
|
58
66
|
}
|
|
59
67
|
|
|
60
|
-
|
|
61
|
-
if (propSchema.const !== undefined) {
|
|
62
|
-
// Silently ensure the value is set, no UI needed
|
|
63
|
-
React.useEffect(() => {
|
|
64
|
-
if (value !== propSchema.const) {
|
|
65
|
-
onChange(propSchema.const);
|
|
66
|
-
}
|
|
67
|
-
}, [value, propSchema.const, onChange]);
|
|
68
|
+
if (isConstField) {
|
|
68
69
|
return <></>;
|
|
69
70
|
}
|
|
70
71
|
|
|
@@ -653,7 +654,7 @@ const SecretField: React.FC<{
|
|
|
653
654
|
isRequired?: boolean;
|
|
654
655
|
onChange: (val: unknown) => void;
|
|
655
656
|
}> = ({ id, label, description, value, isRequired, onChange }) => {
|
|
656
|
-
const [showPassword, setShowPassword] =
|
|
657
|
+
const [showPassword, setShowPassword] = useState(false);
|
|
657
658
|
const currentValue = value || "";
|
|
658
659
|
const hasExistingValue = currentValue.length > 0;
|
|
659
660
|
|
|
@@ -702,7 +703,7 @@ const SecretTextareaField: React.FC<{
|
|
|
702
703
|
isRequired?: boolean;
|
|
703
704
|
onChange: (val: unknown) => void;
|
|
704
705
|
}> = ({ id, label, description, value, isRequired, onChange }) => {
|
|
705
|
-
const [showContent, setShowContent] =
|
|
706
|
+
const [showContent, setShowContent] = useState(false);
|
|
706
707
|
const currentValue = value || "";
|
|
707
708
|
const hasExistingValue = currentValue.length > 0;
|
|
708
709
|
|
|
@@ -723,7 +724,9 @@ const SecretTextareaField: React.FC<{
|
|
|
723
724
|
value={currentValue}
|
|
724
725
|
onChange={(e) => onChange(e.target.value)}
|
|
725
726
|
placeholder={
|
|
726
|
-
hasExistingValue
|
|
727
|
+
hasExistingValue
|
|
728
|
+
? "Leave empty to keep existing value"
|
|
729
|
+
: "Paste content here"
|
|
727
730
|
}
|
|
728
731
|
rows={5}
|
|
729
732
|
className="pr-10 font-mono text-xs"
|
|
@@ -750,7 +753,9 @@ const SecretTextareaField: React.FC<{
|
|
|
750
753
|
}
|
|
751
754
|
}}
|
|
752
755
|
placeholder={
|
|
753
|
-
hasExistingValue
|
|
756
|
+
hasExistingValue
|
|
757
|
+
? "Leave empty to keep existing value"
|
|
758
|
+
: "Paste content here"
|
|
754
759
|
}
|
|
755
760
|
rows={3}
|
|
756
761
|
className="pr-10"
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { AlertCircle, CheckCircle2 } from "lucide-react";
|
|
3
|
+
import { Badge } from "../Badge";
|
|
4
|
+
|
|
5
|
+
// =============================================================================
|
|
6
|
+
// SHARED TYPES
|
|
7
|
+
// =============================================================================
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Generic validation issue for IDE-style editors.
|
|
11
|
+
* nodeId identifies which tree node the issue belongs to.
|
|
12
|
+
*/
|
|
13
|
+
export interface ValidationIssue {
|
|
14
|
+
nodeId: string;
|
|
15
|
+
message: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// =============================================================================
|
|
19
|
+
// VALIDATION DOT
|
|
20
|
+
// =============================================================================
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Small red dot indicator shown on tree nodes with validation issues.
|
|
24
|
+
*/
|
|
25
|
+
export function IDEValidationDot({
|
|
26
|
+
nodeId,
|
|
27
|
+
issues,
|
|
28
|
+
}: {
|
|
29
|
+
nodeId: string;
|
|
30
|
+
issues: ValidationIssue[];
|
|
31
|
+
}) {
|
|
32
|
+
const nodeIssues = issues.filter((i) => i.nodeId === nodeId);
|
|
33
|
+
if (nodeIssues.length === 0) return;
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<span className="ml-auto flex h-2 w-2 rounded-full bg-destructive shrink-0" />
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// =============================================================================
|
|
41
|
+
// TREE NODE
|
|
42
|
+
// =============================================================================
|
|
43
|
+
|
|
44
|
+
interface IDETreeNodeProps {
|
|
45
|
+
nodeId: string;
|
|
46
|
+
label: string;
|
|
47
|
+
icon: React.ElementType;
|
|
48
|
+
selected: boolean;
|
|
49
|
+
onClick: () => void;
|
|
50
|
+
issues?: ValidationIssue[];
|
|
51
|
+
indent?: boolean;
|
|
52
|
+
badge?: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Generic tree node for IDE-style left panel navigation.
|
|
57
|
+
* Highlights when selected, shows validation dots and optional badges.
|
|
58
|
+
*/
|
|
59
|
+
export const IDETreeNode: React.FC<IDETreeNodeProps> = ({
|
|
60
|
+
nodeId,
|
|
61
|
+
label,
|
|
62
|
+
icon: Icon,
|
|
63
|
+
selected,
|
|
64
|
+
onClick,
|
|
65
|
+
issues = [],
|
|
66
|
+
indent = false,
|
|
67
|
+
badge,
|
|
68
|
+
}) => {
|
|
69
|
+
return (
|
|
70
|
+
<button
|
|
71
|
+
type="button"
|
|
72
|
+
onClick={onClick}
|
|
73
|
+
className={`flex items-center gap-2 w-full px-3 py-2 text-sm text-left transition-colors ${
|
|
74
|
+
indent ? "pl-7" : ""
|
|
75
|
+
} ${
|
|
76
|
+
selected
|
|
77
|
+
? "bg-primary/10 text-primary border-l-2 border-primary"
|
|
78
|
+
: "hover:bg-muted/50 border-l-2 border-transparent"
|
|
79
|
+
}`}
|
|
80
|
+
>
|
|
81
|
+
<Icon className="h-4 w-4 shrink-0 opacity-60" />
|
|
82
|
+
<span className="truncate flex-1">{label}</span>
|
|
83
|
+
{badge && (
|
|
84
|
+
<Badge variant="secondary" className="text-[10px] shrink-0">
|
|
85
|
+
{badge}
|
|
86
|
+
</Badge>
|
|
87
|
+
)}
|
|
88
|
+
<IDEValidationDot nodeId={nodeId} issues={issues} />
|
|
89
|
+
</button>
|
|
90
|
+
);
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
// =============================================================================
|
|
94
|
+
// TREE SECTION HEADER
|
|
95
|
+
// =============================================================================
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Section header for grouping tree nodes (e.g., "Check Items", "Permissions").
|
|
99
|
+
*/
|
|
100
|
+
export const IDETreeSection: React.FC<{ label: string }> = ({ label }) => (
|
|
101
|
+
<div className="px-3 pt-4 pb-1">
|
|
102
|
+
<span className="text-[11px] font-semibold text-muted-foreground uppercase tracking-wider">
|
|
103
|
+
{label}
|
|
104
|
+
</span>
|
|
105
|
+
</div>
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
// =============================================================================
|
|
109
|
+
// STATUS BAR
|
|
110
|
+
// =============================================================================
|
|
111
|
+
|
|
112
|
+
interface IDEStatusBarProps {
|
|
113
|
+
issues: ValidationIssue[];
|
|
114
|
+
onIssueClick: (nodeId: string) => void;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Bottom status bar showing validation issue count with clickable navigation.
|
|
119
|
+
*/
|
|
120
|
+
export const IDEStatusBar: React.FC<IDEStatusBarProps> = ({
|
|
121
|
+
issues,
|
|
122
|
+
onIssueClick,
|
|
123
|
+
}) => {
|
|
124
|
+
if (issues.length === 0) {
|
|
125
|
+
return (
|
|
126
|
+
<div className="flex items-center gap-2 px-4 py-2 mt-2 rounded-md border bg-card text-xs text-muted-foreground">
|
|
127
|
+
<CheckCircle2 className="h-3.5 w-3.5 text-green-500" />
|
|
128
|
+
<span>No issues found</span>
|
|
129
|
+
</div>
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return (
|
|
134
|
+
<div className="flex items-center gap-3 px-4 py-2 mt-2 rounded-md border bg-card text-xs">
|
|
135
|
+
<div className="flex items-center gap-1.5 text-destructive shrink-0">
|
|
136
|
+
<AlertCircle className="h-3.5 w-3.5" />
|
|
137
|
+
<span className="font-medium">
|
|
138
|
+
{issues.length} {issues.length === 1 ? "issue" : "issues"}
|
|
139
|
+
</span>
|
|
140
|
+
</div>
|
|
141
|
+
<div className="flex items-center gap-2 overflow-x-auto">
|
|
142
|
+
{issues.map((issue, i) => (
|
|
143
|
+
<button
|
|
144
|
+
key={`${issue.nodeId}-${i}`}
|
|
145
|
+
type="button"
|
|
146
|
+
onClick={() => onIssueClick(issue.nodeId)}
|
|
147
|
+
className="text-muted-foreground hover:text-foreground transition-colors whitespace-nowrap underline-offset-2 hover:underline"
|
|
148
|
+
>
|
|
149
|
+
{issue.message}
|
|
150
|
+
</button>
|
|
151
|
+
))}
|
|
152
|
+
</div>
|
|
153
|
+
</div>
|
|
154
|
+
);
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
// =============================================================================
|
|
158
|
+
// IDE LAYOUT
|
|
159
|
+
// =============================================================================
|
|
160
|
+
|
|
161
|
+
interface IDELayoutProps {
|
|
162
|
+
/** Left panel tree content */
|
|
163
|
+
tree: React.ReactNode;
|
|
164
|
+
/** Right panel editor content */
|
|
165
|
+
panel: React.ReactNode;
|
|
166
|
+
/** Optional status bar issues (renders IDEStatusBar automatically) */
|
|
167
|
+
issues?: ValidationIssue[];
|
|
168
|
+
/** Callback when a status bar issue is clicked */
|
|
169
|
+
onIssueClick?: (nodeId: string) => void;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Two-column IDE layout shell: left tree panel + right editor panel + status bar.
|
|
174
|
+
* Used by health check configuration editor and assignment editor.
|
|
175
|
+
*/
|
|
176
|
+
export const IDELayout: React.FC<IDELayoutProps> = ({
|
|
177
|
+
tree,
|
|
178
|
+
panel,
|
|
179
|
+
issues,
|
|
180
|
+
onIssueClick,
|
|
181
|
+
}) => {
|
|
182
|
+
return (
|
|
183
|
+
<>
|
|
184
|
+
<div className="flex flex-col lg:flex-row gap-0 min-h-[60vh] border rounded-lg bg-card overflow-hidden">
|
|
185
|
+
{/* Explorer Tree — Left Panel */}
|
|
186
|
+
<div className="w-full lg:w-64 shrink-0 border-b lg:border-b-0 lg:border-r bg-muted/30">
|
|
187
|
+
{tree}
|
|
188
|
+
</div>
|
|
189
|
+
|
|
190
|
+
{/* Editor Panel — Right Panel */}
|
|
191
|
+
<div className="flex-1 min-w-0">{panel}</div>
|
|
192
|
+
</div>
|
|
193
|
+
|
|
194
|
+
{/* Status Bar */}
|
|
195
|
+
{issues && onIssueClick && (
|
|
196
|
+
<IDEStatusBar issues={issues} onIssueClick={onIssueClick} />
|
|
197
|
+
)}
|
|
198
|
+
</>
|
|
199
|
+
);
|
|
200
|
+
};
|
package/src/index.ts
CHANGED