@marimo-team/islands 0.23.15-dev57 → 0.23.15-dev59
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/dist/{code-visibility-DDYb93Ql.js → code-visibility-gaLtG9fg.js} +1 -1
- package/dist/main.js +2 -2
- package/dist/{reveal-component-C5pW80ik.js → reveal-component-EGXP3UKC.js} +1 -1
- package/dist/style.css +1 -1
- package/package.json +1 -1
- package/src/components/editor/chrome/panels/write-secret-modal.tsx +3 -2
- package/src/components/editor/connections/__tests__/secret-combobox.test.ts +134 -0
- package/src/components/editor/connections/database/schemas.ts +35 -27
- package/src/components/editor/connections/form-renderers.tsx +42 -110
- package/src/components/editor/connections/secret-combobox.tsx +253 -0
- package/src/components/editor/connections/storage/schemas.ts +12 -6
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
|
|
3
|
+
import { ChevronDownIcon, KeyIcon, PlusCircleIcon, XIcon } from "lucide-react";
|
|
4
|
+
import React, { useState } from "react";
|
|
5
|
+
import {
|
|
6
|
+
Command,
|
|
7
|
+
CommandGroup,
|
|
8
|
+
CommandInput,
|
|
9
|
+
CommandItem,
|
|
10
|
+
CommandList,
|
|
11
|
+
CommandSeparator,
|
|
12
|
+
} from "@/components/ui/command";
|
|
13
|
+
import {
|
|
14
|
+
Popover,
|
|
15
|
+
PopoverContent,
|
|
16
|
+
PopoverTrigger,
|
|
17
|
+
} from "@/components/ui/popover";
|
|
18
|
+
import { cn } from "@/utils/cn";
|
|
19
|
+
import { displaySecret, isSecret, prefixSecret } from "./secrets";
|
|
20
|
+
|
|
21
|
+
export function partitionSecretKeys(
|
|
22
|
+
secretKeys: string[],
|
|
23
|
+
optionRegex: string,
|
|
24
|
+
): { recommended: string[]; other: string[] } {
|
|
25
|
+
if (!optionRegex) {
|
|
26
|
+
return { recommended: [], other: [...secretKeys] };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
let pattern: RegExp;
|
|
30
|
+
try {
|
|
31
|
+
pattern = new RegExp(optionRegex, "i");
|
|
32
|
+
} catch {
|
|
33
|
+
return { recommended: [], other: [...secretKeys] };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const recommended: string[] = [];
|
|
37
|
+
const other: string[] = [];
|
|
38
|
+
for (const key of secretKeys) {
|
|
39
|
+
if (pattern.test(key)) {
|
|
40
|
+
recommended.push(key);
|
|
41
|
+
} else {
|
|
42
|
+
other.push(key);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return { recommended, other };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
interface SecretComboboxProps {
|
|
49
|
+
value: string;
|
|
50
|
+
onChange: (value: string) => void;
|
|
51
|
+
placeholder?: string;
|
|
52
|
+
/** When true, only secrets (or creating one) can be chosen — no free-text literals. */
|
|
53
|
+
secretsOnly?: boolean;
|
|
54
|
+
recommendedKeys: string[];
|
|
55
|
+
otherKeys: string[];
|
|
56
|
+
/** Opens the create-secret flow; `suggestedValue` is the current search text when present. */
|
|
57
|
+
onCreateSecret: (suggestedValue?: string) => void;
|
|
58
|
+
className?: string;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Searchable combobox for connection fields that may reference secrets.
|
|
63
|
+
*
|
|
64
|
+
* - Default: pick a secret, create one, or commit a free-text literal.
|
|
65
|
+
* - `secretsOnly`: pick or create a secret only (for passwords/tokens/keys).
|
|
66
|
+
*/
|
|
67
|
+
export const SecretCombobox: React.FC<SecretComboboxProps> = ({
|
|
68
|
+
value,
|
|
69
|
+
onChange,
|
|
70
|
+
placeholder,
|
|
71
|
+
secretsOnly = false,
|
|
72
|
+
recommendedKeys,
|
|
73
|
+
otherKeys,
|
|
74
|
+
onCreateSecret,
|
|
75
|
+
className,
|
|
76
|
+
}) => {
|
|
77
|
+
const [open, setOpen] = useState(false);
|
|
78
|
+
const [search, setSearch] = useState("");
|
|
79
|
+
|
|
80
|
+
const trimmedSearch = search.trim();
|
|
81
|
+
// Non-secret fields may commit a literal even when it collides with an
|
|
82
|
+
// existing secret key, so we intentionally don't filter out matches here.
|
|
83
|
+
const showCustomValue =
|
|
84
|
+
!secretsOnly && trimmedSearch.length > 0 && trimmedSearch !== value;
|
|
85
|
+
|
|
86
|
+
const displayValue = (() => {
|
|
87
|
+
if (!value) {
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
if (isSecret(value)) {
|
|
91
|
+
return displaySecret(value);
|
|
92
|
+
}
|
|
93
|
+
return value;
|
|
94
|
+
})();
|
|
95
|
+
|
|
96
|
+
const selectSecret = (key: string) => {
|
|
97
|
+
onChange(prefixSecret(key));
|
|
98
|
+
setOpen(false);
|
|
99
|
+
setSearch("");
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const selectCustom = (custom: string) => {
|
|
103
|
+
onChange(custom);
|
|
104
|
+
setOpen(false);
|
|
105
|
+
setSearch("");
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
const clearValue = () => {
|
|
109
|
+
onChange("");
|
|
110
|
+
setSearch("");
|
|
111
|
+
setOpen(false);
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
return (
|
|
115
|
+
<div
|
|
116
|
+
className={cn("flex w-full min-w-48 mb-1 items-center gap-1", className)}
|
|
117
|
+
>
|
|
118
|
+
<Popover
|
|
119
|
+
modal={true} // own scroll lock so trackpad/wheel works inside the portaled list
|
|
120
|
+
open={open}
|
|
121
|
+
onOpenChange={(next) => {
|
|
122
|
+
setOpen(next);
|
|
123
|
+
if (!next) {
|
|
124
|
+
setSearch("");
|
|
125
|
+
}
|
|
126
|
+
}}
|
|
127
|
+
>
|
|
128
|
+
<PopoverTrigger asChild={true}>
|
|
129
|
+
<button
|
|
130
|
+
type="button"
|
|
131
|
+
className={cn(
|
|
132
|
+
"flex h-6 min-w-0 flex-1 shadow-xs-solid items-center justify-between",
|
|
133
|
+
"rounded-sm border border-input bg-background px-1.5 text-sm font-code ring-offset-background placeholder:text-muted-foreground",
|
|
134
|
+
"hover:shadow-sm-solid focus:outline-hidden focus:ring-1 focus:ring-ring focus:border-primary focus:shadow-md-solid",
|
|
135
|
+
isSecret(value) && "bg-accent",
|
|
136
|
+
)}
|
|
137
|
+
aria-expanded={open}
|
|
138
|
+
>
|
|
139
|
+
<span className="truncate flex-1 min-w-0 text-left flex items-center gap-1.5">
|
|
140
|
+
{displayValue ? (
|
|
141
|
+
<>
|
|
142
|
+
{isSecret(value) && (
|
|
143
|
+
<KeyIcon className="h-3 w-3 shrink-0 opacity-70" />
|
|
144
|
+
)}
|
|
145
|
+
<span className="truncate">{displayValue}</span>
|
|
146
|
+
</>
|
|
147
|
+
) : (
|
|
148
|
+
<span className="text-muted-foreground">{placeholder}</span>
|
|
149
|
+
)}
|
|
150
|
+
</span>
|
|
151
|
+
<ChevronDownIcon className="ml-1 h-4 w-4 opacity-50 shrink-0" />
|
|
152
|
+
</button>
|
|
153
|
+
</PopoverTrigger>
|
|
154
|
+
<PopoverContent
|
|
155
|
+
className="w-(--radix-popover-trigger-width) p-0"
|
|
156
|
+
align="start"
|
|
157
|
+
>
|
|
158
|
+
<Command>
|
|
159
|
+
<CommandInput
|
|
160
|
+
placeholder={
|
|
161
|
+
secretsOnly
|
|
162
|
+
? "Search secrets..."
|
|
163
|
+
: "Type a value or search secrets..."
|
|
164
|
+
}
|
|
165
|
+
rootClassName="px-1 h-8"
|
|
166
|
+
autoFocus={true}
|
|
167
|
+
value={search}
|
|
168
|
+
onValueChange={setSearch}
|
|
169
|
+
/>
|
|
170
|
+
<CommandList className="max-h-60 overscroll-contain">
|
|
171
|
+
{showCustomValue && (
|
|
172
|
+
<CommandGroup>
|
|
173
|
+
<CommandItem
|
|
174
|
+
value={`use custom value ${trimmedSearch}`}
|
|
175
|
+
onSelect={() => selectCustom(trimmedSearch)}
|
|
176
|
+
>
|
|
177
|
+
Use "{trimmedSearch}"
|
|
178
|
+
</CommandItem>
|
|
179
|
+
</CommandGroup>
|
|
180
|
+
)}
|
|
181
|
+
{showCustomValue && <CommandSeparator />}
|
|
182
|
+
<CommandGroup className="mt-0">
|
|
183
|
+
<CommandItem
|
|
184
|
+
// Include search so this stays visible while filtering
|
|
185
|
+
value={`create new secret ${trimmedSearch}`}
|
|
186
|
+
onSelect={() => {
|
|
187
|
+
const suggestedValue = trimmedSearch || undefined;
|
|
188
|
+
setOpen(false);
|
|
189
|
+
setSearch("");
|
|
190
|
+
onCreateSecret(suggestedValue);
|
|
191
|
+
}}
|
|
192
|
+
>
|
|
193
|
+
<PlusCircleIcon className="mr-2 h-3.5 w-3.5" />
|
|
194
|
+
Create a new secret
|
|
195
|
+
</CommandItem>
|
|
196
|
+
</CommandGroup>
|
|
197
|
+
{recommendedKeys.length > 0 && (
|
|
198
|
+
<>
|
|
199
|
+
<CommandSeparator className="mt-0" />
|
|
200
|
+
<CommandGroup heading="Recommended">
|
|
201
|
+
{recommendedKeys.map((key) => (
|
|
202
|
+
<CommandItem
|
|
203
|
+
key={key}
|
|
204
|
+
value={key}
|
|
205
|
+
onSelect={() => selectSecret(key)}
|
|
206
|
+
>
|
|
207
|
+
<KeyIcon className="mr-2 h-3 w-3 opacity-70" />
|
|
208
|
+
{key}
|
|
209
|
+
</CommandItem>
|
|
210
|
+
))}
|
|
211
|
+
</CommandGroup>
|
|
212
|
+
</>
|
|
213
|
+
)}
|
|
214
|
+
{otherKeys.length > 0 && (
|
|
215
|
+
<>
|
|
216
|
+
<CommandSeparator className="mt-0" />
|
|
217
|
+
<CommandGroup
|
|
218
|
+
heading={recommendedKeys.length > 0 ? "Other" : undefined}
|
|
219
|
+
>
|
|
220
|
+
{otherKeys.map((key) => (
|
|
221
|
+
<CommandItem
|
|
222
|
+
key={key}
|
|
223
|
+
value={key}
|
|
224
|
+
onSelect={() => selectSecret(key)}
|
|
225
|
+
>
|
|
226
|
+
<KeyIcon className="mr-2 h-3 w-3 opacity-70" />
|
|
227
|
+
{key}
|
|
228
|
+
</CommandItem>
|
|
229
|
+
))}
|
|
230
|
+
</CommandGroup>
|
|
231
|
+
</>
|
|
232
|
+
)}
|
|
233
|
+
</CommandList>
|
|
234
|
+
</Command>
|
|
235
|
+
</PopoverContent>
|
|
236
|
+
</Popover>
|
|
237
|
+
{value && (
|
|
238
|
+
<button
|
|
239
|
+
type="button"
|
|
240
|
+
aria-label="Clear"
|
|
241
|
+
className={cn(
|
|
242
|
+
"flex h-6 w-6 shrink-0 items-center justify-center rounded-sm",
|
|
243
|
+
"text-muted-foreground hover:bg-muted hover:text-foreground",
|
|
244
|
+
"focus:outline-hidden focus:ring-1 focus:ring-ring",
|
|
245
|
+
)}
|
|
246
|
+
onClick={clearValue}
|
|
247
|
+
>
|
|
248
|
+
<XIcon className="h-3.5 w-3.5" />
|
|
249
|
+
</button>
|
|
250
|
+
)}
|
|
251
|
+
</div>
|
|
252
|
+
);
|
|
253
|
+
};
|
|
@@ -12,6 +12,7 @@ export const S3StorageSchema = z
|
|
|
12
12
|
FieldOptions.of({
|
|
13
13
|
label: "Bucket",
|
|
14
14
|
placeholder: "my-bucket",
|
|
15
|
+
optionRegex: "(bucket|s3.?bucket)",
|
|
15
16
|
}),
|
|
16
17
|
),
|
|
17
18
|
region: z
|
|
@@ -21,6 +22,7 @@ export const S3StorageSchema = z
|
|
|
21
22
|
FieldOptions.of({
|
|
22
23
|
label: "Region",
|
|
23
24
|
placeholder: "us-east-1",
|
|
25
|
+
optionRegex: "(region|aws.?region)",
|
|
24
26
|
}),
|
|
25
27
|
),
|
|
26
28
|
access_key_id: z
|
|
@@ -30,7 +32,7 @@ export const S3StorageSchema = z
|
|
|
30
32
|
FieldOptions.of({
|
|
31
33
|
label: "Access Key ID",
|
|
32
34
|
inputType: "password",
|
|
33
|
-
optionRegex: "
|
|
35
|
+
optionRegex: "(access.?key.?id|aws.?access.?key)",
|
|
34
36
|
}),
|
|
35
37
|
),
|
|
36
38
|
secret_access_key: z
|
|
@@ -40,7 +42,7 @@ export const S3StorageSchema = z
|
|
|
40
42
|
FieldOptions.of({
|
|
41
43
|
label: "Secret Access Key",
|
|
42
44
|
inputType: "password",
|
|
43
|
-
optionRegex: "
|
|
45
|
+
optionRegex: "(secret.?access.?key|aws.?secret)",
|
|
44
46
|
}),
|
|
45
47
|
),
|
|
46
48
|
endpoint_url: z
|
|
@@ -50,6 +52,7 @@ export const S3StorageSchema = z
|
|
|
50
52
|
FieldOptions.of({
|
|
51
53
|
label: "Endpoint URL",
|
|
52
54
|
placeholder: "https://s3.amazonaws.com",
|
|
55
|
+
optionRegex: "(endpoint|s3.?url|s3.?endpoint)",
|
|
53
56
|
}),
|
|
54
57
|
),
|
|
55
58
|
})
|
|
@@ -65,6 +68,7 @@ export const GCSStorageSchema = z
|
|
|
65
68
|
FieldOptions.of({
|
|
66
69
|
label: "Bucket",
|
|
67
70
|
placeholder: "my-bucket",
|
|
71
|
+
optionRegex: "(bucket|gcs.?bucket|google.?bucket)",
|
|
68
72
|
}),
|
|
69
73
|
),
|
|
70
74
|
service_account_key: z
|
|
@@ -98,7 +102,7 @@ export const AzureStorageSchema = z
|
|
|
98
102
|
FieldOptions.of({
|
|
99
103
|
label: "Account Name",
|
|
100
104
|
placeholder: "storageaccount",
|
|
101
|
-
optionRegex: "
|
|
105
|
+
optionRegex: "(azure.?account|account.?name|storage.?account)",
|
|
102
106
|
}),
|
|
103
107
|
),
|
|
104
108
|
account_key: z
|
|
@@ -108,7 +112,7 @@ export const AzureStorageSchema = z
|
|
|
108
112
|
FieldOptions.of({
|
|
109
113
|
label: "Account Key",
|
|
110
114
|
inputType: "password",
|
|
111
|
-
optionRegex: "
|
|
115
|
+
optionRegex: "(azure.?key|account.?key|storage.?key)",
|
|
112
116
|
}),
|
|
113
117
|
),
|
|
114
118
|
})
|
|
@@ -142,7 +146,8 @@ export const CoreWeaveStorageSchema = z
|
|
|
142
146
|
FieldOptions.of({
|
|
143
147
|
label: "Access Key ID",
|
|
144
148
|
inputType: "password",
|
|
145
|
-
optionRegex:
|
|
149
|
+
optionRegex:
|
|
150
|
+
"(access.?key.?id|object.?storage.?key|aws.?access.?key)",
|
|
146
151
|
}),
|
|
147
152
|
),
|
|
148
153
|
secret_access_key: z
|
|
@@ -152,7 +157,8 @@ export const CoreWeaveStorageSchema = z
|
|
|
152
157
|
FieldOptions.of({
|
|
153
158
|
label: "Secret Access Key",
|
|
154
159
|
inputType: "password",
|
|
155
|
-
optionRegex:
|
|
160
|
+
optionRegex:
|
|
161
|
+
"(secret.?access.?key|object.?storage.?secret|aws.?secret)",
|
|
156
162
|
}),
|
|
157
163
|
),
|
|
158
164
|
})
|