@marimo-team/islands 0.23.15-dev57 → 0.23.15-dev58

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.
@@ -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: ".*access_key.*",
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: ".*secret.*access.*",
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: ".*account.*",
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: ".*azure.*key.*",
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: ".*object_storage_key.*",
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: ".*object_storage_secret.*",
160
+ optionRegex:
161
+ "(secret.?access.?key|object.?storage.?secret|aws.?secret)",
156
162
  }),
157
163
  ),
158
164
  })