@checkstack/ui 1.1.3 → 1.1.5
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
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# @checkstack/ui
|
|
2
2
|
|
|
3
|
+
## 1.1.5
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 95aa716: Fix LDAP CA certificate input: The custom CA certificate field was rendered as a single-line password input, which stripped newlines from PEM certificates and caused TLS connection failures ("Failed to connect"). The field now renders as a multi-line secret textarea that properly preserves PEM format while still encrypting the value in storage.
|
|
8
|
+
|
|
9
|
+
## 1.1.4
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- c0c0ed2: Fix LDAP group-to-role mapping not assigning roles on login. The LDAP search now explicitly requests the `memberOf` operational attribute, which is not returned by default. Also fixes array flattening that discarded multi-valued group memberships, and adds case-insensitive DN comparison for group matching. The test LDAP environment now uses `groupOfUniqueNames` to enable the memberOf overlay. Additionally, the DynamicForm validation no longer blocks saving when optional array fields (like group mappings) are empty.
|
|
14
|
+
|
|
3
15
|
## 1.1.3
|
|
4
16
|
|
|
5
17
|
### Patch Changes
|
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@checkstack/ui",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.5",
|
|
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.4",
|
|
8
|
+
"@checkstack/frontend-api": "0.3.8",
|
|
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",
|
|
@@ -30,9 +30,9 @@
|
|
|
30
30
|
"typescript": "^5.0.0",
|
|
31
31
|
"@types/react": "^18.2.0",
|
|
32
32
|
"@testing-library/react": "^16.0.0",
|
|
33
|
-
"@checkstack/test-utils-frontend": "0.0.
|
|
34
|
-
"@checkstack/tsconfig": "0.0.
|
|
35
|
-
"@checkstack/scripts": "0.1.
|
|
33
|
+
"@checkstack/test-utils-frontend": "0.0.4",
|
|
34
|
+
"@checkstack/tsconfig": "0.0.4",
|
|
35
|
+
"@checkstack/scripts": "0.1.2"
|
|
36
36
|
},
|
|
37
37
|
"scripts": {
|
|
38
38
|
"typecheck": "tsc --noEmit",
|
|
@@ -140,6 +140,20 @@ export const FormField: React.FC<FormFieldProps> = ({
|
|
|
140
140
|
propSchema as JsonSchemaProperty & { "x-secret"?: boolean }
|
|
141
141
|
)["x-secret"];
|
|
142
142
|
|
|
143
|
+
// Secret textarea fields (e.g., PEM certificates)
|
|
144
|
+
if (isTextarea && isSecret) {
|
|
145
|
+
return (
|
|
146
|
+
<SecretTextareaField
|
|
147
|
+
id={id}
|
|
148
|
+
label={label}
|
|
149
|
+
description={cleanDesc}
|
|
150
|
+
value={value as string}
|
|
151
|
+
isRequired={isRequired}
|
|
152
|
+
onChange={onChange}
|
|
153
|
+
/>
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
|
|
143
157
|
// Textarea fields
|
|
144
158
|
if (isTextarea) {
|
|
145
159
|
return (
|
|
@@ -577,6 +591,56 @@ export const FormField: React.FC<FormFieldProps> = ({
|
|
|
577
591
|
return <></>;
|
|
578
592
|
};
|
|
579
593
|
|
|
594
|
+
/**
|
|
595
|
+
* Shared visibility toggle button for secret fields.
|
|
596
|
+
*/
|
|
597
|
+
const VisibilityToggle: React.FC<{
|
|
598
|
+
visible: boolean;
|
|
599
|
+
onToggle: () => void;
|
|
600
|
+
}> = ({ visible, onToggle }) => (
|
|
601
|
+
<button
|
|
602
|
+
type="button"
|
|
603
|
+
onClick={onToggle}
|
|
604
|
+
className="absolute right-2 top-3 text-muted-foreground hover:text-foreground"
|
|
605
|
+
>
|
|
606
|
+
{visible ? (
|
|
607
|
+
<svg
|
|
608
|
+
className="h-5 w-5"
|
|
609
|
+
fill="none"
|
|
610
|
+
stroke="currentColor"
|
|
611
|
+
viewBox="0 0 24 24"
|
|
612
|
+
>
|
|
613
|
+
<path
|
|
614
|
+
strokeLinecap="round"
|
|
615
|
+
strokeLinejoin="round"
|
|
616
|
+
strokeWidth={2}
|
|
617
|
+
d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.88 9.88l-3.29-3.29m7.532 7.532l3.29 3.29M3 3l3.59 3.59m0 0A9.953 9.953 0 0112 5c4.478 0 8.268 2.943 9.543 7a10.025 10.025 0 01-4.132 5.411m0 0L21 21"
|
|
618
|
+
/>
|
|
619
|
+
</svg>
|
|
620
|
+
) : (
|
|
621
|
+
<svg
|
|
622
|
+
className="h-5 w-5"
|
|
623
|
+
fill="none"
|
|
624
|
+
stroke="currentColor"
|
|
625
|
+
viewBox="0 0 24 24"
|
|
626
|
+
>
|
|
627
|
+
<path
|
|
628
|
+
strokeLinecap="round"
|
|
629
|
+
strokeLinejoin="round"
|
|
630
|
+
strokeWidth={2}
|
|
631
|
+
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
|
|
632
|
+
/>
|
|
633
|
+
<path
|
|
634
|
+
strokeLinecap="round"
|
|
635
|
+
strokeLinejoin="round"
|
|
636
|
+
strokeWidth={2}
|
|
637
|
+
d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"
|
|
638
|
+
/>
|
|
639
|
+
</svg>
|
|
640
|
+
)}
|
|
641
|
+
</button>
|
|
642
|
+
);
|
|
643
|
+
|
|
580
644
|
/**
|
|
581
645
|
* Secret field component with password visibility toggle.
|
|
582
646
|
* Extracted to keep hooks at component top level.
|
|
@@ -612,47 +676,91 @@ const SecretField: React.FC<{
|
|
|
612
676
|
placeholder={hasExistingValue ? "••••••••" : "Enter secret value"}
|
|
613
677
|
className="pr-10"
|
|
614
678
|
/>
|
|
615
|
-
<
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
679
|
+
<VisibilityToggle
|
|
680
|
+
visible={showPassword}
|
|
681
|
+
onToggle={() => setShowPassword(!showPassword)}
|
|
682
|
+
/>
|
|
683
|
+
</div>
|
|
684
|
+
{hasExistingValue && currentValue === "" && (
|
|
685
|
+
<p className="text-xs text-muted-foreground">
|
|
686
|
+
Leave empty to keep existing value
|
|
687
|
+
</p>
|
|
688
|
+
)}
|
|
689
|
+
</div>
|
|
690
|
+
);
|
|
691
|
+
};
|
|
692
|
+
|
|
693
|
+
/**
|
|
694
|
+
* Secret textarea field for multi-line secrets (e.g., PEM certificates).
|
|
695
|
+
* Renders a textarea with a visibility toggle and secret-like behavior.
|
|
696
|
+
*/
|
|
697
|
+
const SecretTextareaField: React.FC<{
|
|
698
|
+
id: string;
|
|
699
|
+
label: string;
|
|
700
|
+
description?: string;
|
|
701
|
+
value: string;
|
|
702
|
+
isRequired?: boolean;
|
|
703
|
+
onChange: (val: unknown) => void;
|
|
704
|
+
}> = ({ id, label, description, value, isRequired, onChange }) => {
|
|
705
|
+
const [showContent, setShowContent] = React.useState(false);
|
|
706
|
+
const currentValue = value || "";
|
|
707
|
+
const hasExistingValue = currentValue.length > 0;
|
|
708
|
+
|
|
709
|
+
return (
|
|
710
|
+
<div className="space-y-2">
|
|
711
|
+
<div>
|
|
712
|
+
<Label htmlFor={id}>
|
|
713
|
+
{label} {isRequired && "*"}
|
|
714
|
+
</Label>
|
|
715
|
+
{description && (
|
|
716
|
+
<p className="text-sm text-muted-foreground mt-0.5">{description}</p>
|
|
717
|
+
)}
|
|
718
|
+
</div>
|
|
719
|
+
<div className="relative">
|
|
720
|
+
{showContent ? (
|
|
721
|
+
<Textarea
|
|
722
|
+
id={id}
|
|
723
|
+
value={currentValue}
|
|
724
|
+
onChange={(e) => onChange(e.target.value)}
|
|
725
|
+
placeholder={
|
|
726
|
+
hasExistingValue ? "Leave empty to keep existing value" : "Paste content here"
|
|
727
|
+
}
|
|
728
|
+
rows={5}
|
|
729
|
+
className="pr-10 font-mono text-xs"
|
|
730
|
+
/>
|
|
731
|
+
) : (
|
|
732
|
+
<Textarea
|
|
733
|
+
id={id}
|
|
734
|
+
value={currentValue ? "••••••••••••••••••••" : ""}
|
|
735
|
+
onChange={(e) => {
|
|
736
|
+
// If user types/pastes into masked field, switch to visible mode
|
|
737
|
+
const newVal = e.target.value.replaceAll("•", "");
|
|
738
|
+
if (newVal) {
|
|
739
|
+
setShowContent(true);
|
|
740
|
+
onChange(newVal);
|
|
741
|
+
}
|
|
742
|
+
}}
|
|
743
|
+
onPaste={(e) => {
|
|
744
|
+
// On paste, switch to visible mode and use pasted content
|
|
745
|
+
e.preventDefault();
|
|
746
|
+
const pastedText = e.clipboardData.getData("text");
|
|
747
|
+
if (pastedText) {
|
|
748
|
+
setShowContent(true);
|
|
749
|
+
onChange(pastedText);
|
|
750
|
+
}
|
|
751
|
+
}}
|
|
752
|
+
placeholder={
|
|
753
|
+
hasExistingValue ? "Leave empty to keep existing value" : "Paste content here"
|
|
754
|
+
}
|
|
755
|
+
rows={3}
|
|
756
|
+
className="pr-10"
|
|
757
|
+
readOnly={!!currentValue}
|
|
758
|
+
/>
|
|
759
|
+
)}
|
|
760
|
+
<VisibilityToggle
|
|
761
|
+
visible={showContent}
|
|
762
|
+
onToggle={() => setShowContent(!showContent)}
|
|
763
|
+
/>
|
|
656
764
|
</div>
|
|
657
765
|
{hasExistingValue && currentValue === "" && (
|
|
658
766
|
<p className="text-xs text-muted-foreground">
|
|
@@ -155,13 +155,23 @@ describe("isValueEmpty", () => {
|
|
|
155
155
|
});
|
|
156
156
|
|
|
157
157
|
describe("arrays", () => {
|
|
158
|
-
it("treats empty array as
|
|
159
|
-
expect(isValueEmpty([], arraySchema)).toBe(
|
|
158
|
+
it("treats empty array as valid when no minItems specified", () => {
|
|
159
|
+
expect(isValueEmpty([], arraySchema)).toBe(false);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it("treats empty array as empty when minItems > 0", () => {
|
|
163
|
+
const requiredArraySchema: JsonSchemaProperty = { type: "array", minItems: 1 } as JsonSchemaProperty;
|
|
164
|
+
expect(isValueEmpty([], requiredArraySchema)).toBe(true);
|
|
160
165
|
});
|
|
161
166
|
|
|
162
167
|
it("treats non-empty array as not empty", () => {
|
|
163
168
|
expect(isValueEmpty([1, 2, 3], arraySchema)).toBe(false);
|
|
164
169
|
});
|
|
170
|
+
|
|
171
|
+
it("treats non-empty array as not empty even with minItems", () => {
|
|
172
|
+
const requiredArraySchema: JsonSchemaProperty = { type: "array", minItems: 1 } as JsonSchemaProperty;
|
|
173
|
+
expect(isValueEmpty([1], requiredArraySchema)).toBe(false);
|
|
174
|
+
});
|
|
165
175
|
});
|
|
166
176
|
|
|
167
177
|
describe("objects", () => {
|
|
@@ -48,8 +48,13 @@ export function isValueEmpty(
|
|
|
48
48
|
): boolean {
|
|
49
49
|
if (val === undefined || val === null) return true;
|
|
50
50
|
if (typeof val === "string" && val.trim() === "") return true;
|
|
51
|
-
// For arrays,
|
|
52
|
-
if (Array.isArray(val) && val.length === 0)
|
|
51
|
+
// For arrays, only consider empty if schema requires minimum items
|
|
52
|
+
if (Array.isArray(val) && val.length === 0) {
|
|
53
|
+
const minItems = (propSchema as JsonSchemaProperty & { minItems?: number }).minItems;
|
|
54
|
+
if (minItems !== undefined && minItems > 0) return true;
|
|
55
|
+
// Empty arrays are valid by default (e.g., optional mappings lists)
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
53
58
|
// For objects (nested schemas), recursively check required fields
|
|
54
59
|
if (propSchema.type === "object" && propSchema.properties) {
|
|
55
60
|
const objVal = val as Record<string, unknown>;
|