@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
package/package.json
CHANGED
|
@@ -43,10 +43,11 @@ export const WriteSecretModal: React.FC<{
|
|
|
43
43
|
providerNames: string[];
|
|
44
44
|
onClose: () => void;
|
|
45
45
|
onSuccess: (secretName: string) => void;
|
|
46
|
-
|
|
46
|
+
initialValue?: string;
|
|
47
|
+
}> = ({ providerNames, onClose, onSuccess, initialValue }) => {
|
|
47
48
|
const { writeSecret } = useRequestClient();
|
|
48
49
|
const [key, setKey] = React.useState("");
|
|
49
|
-
const [value, setValue] = React.useState("");
|
|
50
|
+
const [value, setValue] = React.useState(initialValue || "");
|
|
50
51
|
const [location, setLocation] = React.useState<string | undefined>(
|
|
51
52
|
providerNames[0],
|
|
52
53
|
);
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
import { describe, expect, test } from "vitest";
|
|
3
|
+
import { partitionSecretKeys } from "../secret-combobox";
|
|
4
|
+
|
|
5
|
+
describe("partitionSecretKeys", () => {
|
|
6
|
+
const keys = [
|
|
7
|
+
"HOSTNAME",
|
|
8
|
+
"PGHOST",
|
|
9
|
+
"DB_HOST",
|
|
10
|
+
"KUBERNETES_SERVICE_HOST",
|
|
11
|
+
"GPG_KEY",
|
|
12
|
+
"HOME",
|
|
13
|
+
"POSTGRES_PASSWORD",
|
|
14
|
+
"DB_PASSWORD",
|
|
15
|
+
"USERNAME",
|
|
16
|
+
"PGUSER",
|
|
17
|
+
"USER_AGENT",
|
|
18
|
+
"AWS_ACCESS_KEY_ID",
|
|
19
|
+
"AWS_SECRET_ACCESS_KEY",
|
|
20
|
+
"AWS_SESSION_TOKEN",
|
|
21
|
+
"OBJECT_STORAGE_KEY",
|
|
22
|
+
"OBJECT_STORAGE_SECRET",
|
|
23
|
+
"DATABASE_URL",
|
|
24
|
+
"SNOWFLAKE_PRIVATE_KEY_PASSPHRASE",
|
|
25
|
+
"PATH",
|
|
26
|
+
"GITHUB_PAT",
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
test("returns all keys as other when regex is empty", () => {
|
|
30
|
+
expect(partitionSecretKeys(keys, "")).toEqual({
|
|
31
|
+
recommended: [],
|
|
32
|
+
other: keys,
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test("handles invalid regex", () => {
|
|
37
|
+
expect(partitionSecretKeys(keys, "[invalid")).toEqual({
|
|
38
|
+
recommended: [],
|
|
39
|
+
other: keys,
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test("recommends host-like keys and excludes kubernetes", () => {
|
|
44
|
+
const hostRegex =
|
|
45
|
+
"^(?!.*(kubernetes|gpg)).*(host(name)?|pghost|db.?host|database.?host|mysql.?host|postgres.?host|server.?host)";
|
|
46
|
+
const { recommended, other } = partitionSecretKeys(keys, hostRegex);
|
|
47
|
+
|
|
48
|
+
expect(recommended).toEqual(["HOSTNAME", "PGHOST", "DB_HOST"]);
|
|
49
|
+
expect(other).toContain("KUBERNETES_SERVICE_HOST");
|
|
50
|
+
expect(other).toContain("GPG_KEY");
|
|
51
|
+
expect(other).toContain("HOME");
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test("recommends password-like keys", () => {
|
|
55
|
+
const { recommended } = partitionSecretKeys(
|
|
56
|
+
keys,
|
|
57
|
+
"(password|passwd|pgpassword|db.?pass(word)?)",
|
|
58
|
+
);
|
|
59
|
+
expect(recommended).toEqual(["POSTGRES_PASSWORD", "DB_PASSWORD"]);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test("recommends username keys without matching USER_AGENT", () => {
|
|
63
|
+
const { recommended } = partitionSecretKeys(
|
|
64
|
+
keys,
|
|
65
|
+
"(username|pguser|db.?user|^USER$|_USER$)",
|
|
66
|
+
);
|
|
67
|
+
expect(recommended).toEqual(["USERNAME", "PGUSER"]);
|
|
68
|
+
expect(recommended).not.toContain("USER_AGENT");
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test("recommends aws access key id without matching the secret key", () => {
|
|
72
|
+
const { recommended } = partitionSecretKeys(
|
|
73
|
+
keys,
|
|
74
|
+
"(access.?key.?id|aws.?access.?key)",
|
|
75
|
+
);
|
|
76
|
+
expect(recommended).toEqual(["AWS_ACCESS_KEY_ID"]);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test("recommends aws secret access key", () => {
|
|
80
|
+
const { recommended } = partitionSecretKeys(
|
|
81
|
+
keys,
|
|
82
|
+
"(secret.?access.?key|aws.?secret)",
|
|
83
|
+
);
|
|
84
|
+
expect(recommended).toEqual(["AWS_SECRET_ACCESS_KEY"]);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test("recommends aws session token", () => {
|
|
88
|
+
const { recommended } = partitionSecretKeys(
|
|
89
|
+
keys,
|
|
90
|
+
"(session.?token|aws.?session)",
|
|
91
|
+
);
|
|
92
|
+
expect(recommended).toEqual(["AWS_SESSION_TOKEN"]);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test("recommends object storage credentials", () => {
|
|
96
|
+
expect(
|
|
97
|
+
partitionSecretKeys(
|
|
98
|
+
keys,
|
|
99
|
+
"(access.?key.?id|object.?storage.?key|aws.?access.?key)",
|
|
100
|
+
).recommended,
|
|
101
|
+
).toEqual(["AWS_ACCESS_KEY_ID", "OBJECT_STORAGE_KEY"]);
|
|
102
|
+
expect(
|
|
103
|
+
partitionSecretKeys(
|
|
104
|
+
keys,
|
|
105
|
+
"(secret.?access.?key|object.?storage.?secret|aws.?secret)",
|
|
106
|
+
).recommended,
|
|
107
|
+
).toEqual(["AWS_SECRET_ACCESS_KEY", "OBJECT_STORAGE_SECRET"]);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test("recommends uri/url-like keys", () => {
|
|
111
|
+
const { recommended } = partitionSecretKeys(
|
|
112
|
+
keys,
|
|
113
|
+
"(uri|url|connection.?string|database.?url|jdbc)",
|
|
114
|
+
);
|
|
115
|
+
expect(recommended).toEqual(["DATABASE_URL"]);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test("recommends token keys and bounds PAT so PATH is excluded", () => {
|
|
119
|
+
const { recommended, other } = partitionSecretKeys(
|
|
120
|
+
keys,
|
|
121
|
+
"(token|api.?key|access.?token|auth.?token|(^|_)pat(_|$))",
|
|
122
|
+
);
|
|
123
|
+
expect(recommended).toEqual(["AWS_SESSION_TOKEN", "GITHUB_PAT"]);
|
|
124
|
+
expect(other).toContain("PATH");
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test("recommends passphrase keys", () => {
|
|
128
|
+
const { recommended } = partitionSecretKeys(
|
|
129
|
+
keys,
|
|
130
|
+
"(passphrase|private.?key.?passphrase|key.?passphrase)",
|
|
131
|
+
);
|
|
132
|
+
expect(recommended).toEqual(["SNOWFLAKE_PRIVATE_KEY_PASSPHRASE"]);
|
|
133
|
+
});
|
|
134
|
+
});
|
|
@@ -10,8 +10,7 @@ function passwordField() {
|
|
|
10
10
|
FieldOptions.of({
|
|
11
11
|
label: "Password",
|
|
12
12
|
inputType: "password",
|
|
13
|
-
|
|
14
|
-
optionRegex: ".*password.*",
|
|
13
|
+
optionRegex: "(password|passwd|pgpassword|db.?pass(word)?)",
|
|
15
14
|
}),
|
|
16
15
|
);
|
|
17
16
|
}
|
|
@@ -24,8 +23,7 @@ function tokenField(label?: string, required?: boolean) {
|
|
|
24
23
|
FieldOptions.of({
|
|
25
24
|
label: label || "Token",
|
|
26
25
|
inputType: "password",
|
|
27
|
-
|
|
28
|
-
optionRegex: ".*token.*",
|
|
26
|
+
optionRegex: "(token|api.?key|access.?token|auth.?token|(^|_)pat(_|$))",
|
|
29
27
|
}),
|
|
30
28
|
);
|
|
31
29
|
return field;
|
|
@@ -38,8 +36,7 @@ function warehouseNameField() {
|
|
|
38
36
|
.describe(
|
|
39
37
|
FieldOptions.of({
|
|
40
38
|
label: "Warehouse Name",
|
|
41
|
-
|
|
42
|
-
optionRegex: ".*warehouse.*",
|
|
39
|
+
optionRegex: "(warehouse|snowflake.?warehouse)",
|
|
43
40
|
}),
|
|
44
41
|
);
|
|
45
42
|
}
|
|
@@ -49,7 +46,10 @@ function uriField(label?: string, required?: boolean) {
|
|
|
49
46
|
field = required ? field.nonempty() : field.optional();
|
|
50
47
|
|
|
51
48
|
return field.describe(
|
|
52
|
-
FieldOptions.of({
|
|
49
|
+
FieldOptions.of({
|
|
50
|
+
label: label || "URI",
|
|
51
|
+
optionRegex: "(uri|url|connection.?string|database.?url|jdbc)",
|
|
52
|
+
}),
|
|
53
53
|
);
|
|
54
54
|
}
|
|
55
55
|
|
|
@@ -60,8 +60,9 @@ function hostField(label?: string) {
|
|
|
60
60
|
.describe(
|
|
61
61
|
FieldOptions.of({
|
|
62
62
|
label: label || "Host",
|
|
63
|
-
|
|
64
|
-
optionRegex:
|
|
63
|
+
// Exclude kubernetes/system host vars that match a naive "host" search.
|
|
64
|
+
optionRegex:
|
|
65
|
+
"^(?!.*(kubernetes|gpg)).*(host(name)?|pghost|db.?host|database.?host|mysql.?host|postgres.?host|server.?host)",
|
|
65
66
|
}),
|
|
66
67
|
);
|
|
67
68
|
}
|
|
@@ -70,8 +71,7 @@ function databaseField() {
|
|
|
70
71
|
return z.string().describe(
|
|
71
72
|
FieldOptions.of({
|
|
72
73
|
label: "Database",
|
|
73
|
-
|
|
74
|
-
optionRegex: ".*database.*",
|
|
74
|
+
optionRegex: "(database|db.?name|pgdatabase|^DB$|^DATABASE$)",
|
|
75
75
|
}),
|
|
76
76
|
);
|
|
77
77
|
}
|
|
@@ -80,8 +80,7 @@ function schemaField() {
|
|
|
80
80
|
return z.string().describe(
|
|
81
81
|
FieldOptions.of({
|
|
82
82
|
label: "Schema",
|
|
83
|
-
|
|
84
|
-
optionRegex: ".*schema.*",
|
|
83
|
+
optionRegex: "(^SCHEMA$|db.?schema|postgres.?schema|pg.?schema|_SCHEMA$)",
|
|
85
84
|
}),
|
|
86
85
|
);
|
|
87
86
|
}
|
|
@@ -93,8 +92,7 @@ function usernameField() {
|
|
|
93
92
|
.describe(
|
|
94
93
|
FieldOptions.of({
|
|
95
94
|
label: "Username",
|
|
96
|
-
|
|
97
|
-
optionRegex: ".*username.*",
|
|
95
|
+
optionRegex: "(username|pguser|db.?user|^USER$|_USER$)",
|
|
98
96
|
}),
|
|
99
97
|
);
|
|
100
98
|
}
|
|
@@ -196,7 +194,7 @@ export const SnowflakeConnectionSchema = z
|
|
|
196
194
|
.describe(
|
|
197
195
|
FieldOptions.of({
|
|
198
196
|
label: "Account",
|
|
199
|
-
optionRegex: "
|
|
197
|
+
optionRegex: "(snowflake.?account|^SNOWFLAKE_ACCOUNT$|account.?id)",
|
|
200
198
|
}),
|
|
201
199
|
),
|
|
202
200
|
warehouse: z
|
|
@@ -205,7 +203,7 @@ export const SnowflakeConnectionSchema = z
|
|
|
205
203
|
.describe(
|
|
206
204
|
FieldOptions.of({
|
|
207
205
|
label: "Warehouse",
|
|
208
|
-
optionRegex: "
|
|
206
|
+
optionRegex: "(snowflake.?warehouse|warehouse)",
|
|
209
207
|
}),
|
|
210
208
|
),
|
|
211
209
|
database: databaseField(),
|
|
@@ -215,7 +213,7 @@ export const SnowflakeConnectionSchema = z
|
|
|
215
213
|
.describe(
|
|
216
214
|
FieldOptions.of({
|
|
217
215
|
label: "Schema",
|
|
218
|
-
optionRegex: "
|
|
216
|
+
optionRegex: "(snowflake.?schema|^SCHEMA$|_SCHEMA$)",
|
|
219
217
|
}),
|
|
220
218
|
),
|
|
221
219
|
role: z
|
|
@@ -256,7 +254,8 @@ export const SnowflakeConnectionSchema = z
|
|
|
256
254
|
FieldOptions.of({
|
|
257
255
|
label: "Private Key Passphrase",
|
|
258
256
|
inputType: "password",
|
|
259
|
-
optionRegex:
|
|
257
|
+
optionRegex:
|
|
258
|
+
"(passphrase|private.?key.?passphrase|key.?passphrase)",
|
|
260
259
|
}),
|
|
261
260
|
),
|
|
262
261
|
}),
|
|
@@ -283,7 +282,8 @@ export const BigQueryConnectionSchema = z
|
|
|
283
282
|
.describe(
|
|
284
283
|
FieldOptions.of({
|
|
285
284
|
label: "Project ID",
|
|
286
|
-
optionRegex:
|
|
285
|
+
optionRegex:
|
|
286
|
+
"(bigquery.?project|gcp.?project|google.?cloud.?project|project.?id)",
|
|
287
287
|
}),
|
|
288
288
|
),
|
|
289
289
|
dataset: z
|
|
@@ -292,7 +292,7 @@ export const BigQueryConnectionSchema = z
|
|
|
292
292
|
.describe(
|
|
293
293
|
FieldOptions.of({
|
|
294
294
|
label: "Dataset",
|
|
295
|
-
optionRegex: "
|
|
295
|
+
optionRegex: "(bigquery.?dataset|dataset)",
|
|
296
296
|
}),
|
|
297
297
|
),
|
|
298
298
|
credentials_json: z
|
|
@@ -377,7 +377,7 @@ export const IcebergConnectionSchema = z.object({
|
|
|
377
377
|
FieldOptions.of({
|
|
378
378
|
label: "URI",
|
|
379
379
|
placeholder: "https://",
|
|
380
|
-
optionRegex: "
|
|
380
|
+
optionRegex: "(uri|url|connection.?string|database.?url|jdbc)",
|
|
381
381
|
}),
|
|
382
382
|
),
|
|
383
383
|
token: tokenField(),
|
|
@@ -392,7 +392,7 @@ export const IcebergConnectionSchema = z.object({
|
|
|
392
392
|
FieldOptions.of({
|
|
393
393
|
label: "URI",
|
|
394
394
|
placeholder: "jdbc:iceberg://host:port/database",
|
|
395
|
-
optionRegex: "
|
|
395
|
+
optionRegex: "(uri|url|connection.?string|database.?url|jdbc)",
|
|
396
396
|
}),
|
|
397
397
|
),
|
|
398
398
|
}),
|
|
@@ -419,7 +419,13 @@ export const IcebergConnectionSchema = z.object({
|
|
|
419
419
|
"dynamodb.access-key-id": z
|
|
420
420
|
.string()
|
|
421
421
|
.optional()
|
|
422
|
-
.describe(
|
|
422
|
+
.describe(
|
|
423
|
+
FieldOptions.of({
|
|
424
|
+
label: "Access Key ID",
|
|
425
|
+
inputType: "password",
|
|
426
|
+
optionRegex: "(access.?key.?id|aws.?access.?key)",
|
|
427
|
+
}),
|
|
428
|
+
),
|
|
423
429
|
"dynamodb.secret-access-key": z
|
|
424
430
|
.string()
|
|
425
431
|
.optional()
|
|
@@ -427,6 +433,7 @@ export const IcebergConnectionSchema = z.object({
|
|
|
427
433
|
FieldOptions.of({
|
|
428
434
|
label: "Secret Access Key",
|
|
429
435
|
inputType: "password",
|
|
436
|
+
optionRegex: "(secret.?access.?key|aws.?secret)",
|
|
430
437
|
}),
|
|
431
438
|
),
|
|
432
439
|
"dynamodb.session-token": z
|
|
@@ -436,6 +443,7 @@ export const IcebergConnectionSchema = z.object({
|
|
|
436
443
|
FieldOptions.of({
|
|
437
444
|
label: "Session Token",
|
|
438
445
|
inputType: "password",
|
|
446
|
+
optionRegex: "(session.?token|aws.?session)",
|
|
439
447
|
}),
|
|
440
448
|
),
|
|
441
449
|
}),
|
|
@@ -484,7 +492,7 @@ export const RedshiftConnectionSchema = z
|
|
|
484
492
|
FieldOptions.of({
|
|
485
493
|
label: "AWS Access Key ID",
|
|
486
494
|
inputType: "password",
|
|
487
|
-
optionRegex: "
|
|
495
|
+
optionRegex: "(access.?key.?id|aws.?access.?key)",
|
|
488
496
|
}),
|
|
489
497
|
),
|
|
490
498
|
aws_secret_access_key: z
|
|
@@ -494,7 +502,7 @@ export const RedshiftConnectionSchema = z
|
|
|
494
502
|
FieldOptions.of({
|
|
495
503
|
label: "AWS Secret Access Key",
|
|
496
504
|
inputType: "password",
|
|
497
|
-
optionRegex: "
|
|
505
|
+
optionRegex: "(secret.?access.?key|aws.?secret)",
|
|
498
506
|
}),
|
|
499
507
|
),
|
|
500
508
|
aws_session_token: z
|
|
@@ -504,7 +512,7 @@ export const RedshiftConnectionSchema = z
|
|
|
504
512
|
FieldOptions.of({
|
|
505
513
|
label: "AWS Session Token",
|
|
506
514
|
inputType: "password",
|
|
507
|
-
optionRegex: "
|
|
515
|
+
optionRegex: "(session.?token|aws.?session)",
|
|
508
516
|
}),
|
|
509
517
|
),
|
|
510
518
|
}),
|
|
@@ -1,20 +1,10 @@
|
|
|
1
1
|
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
import { createContext, type ReactNode, use } from "react";
|
|
3
|
+
import { createContext, type ReactNode, use, useMemo } from "react";
|
|
5
4
|
import { z } from "zod";
|
|
6
5
|
import type { FormRenderer } from "@/components/forms/form";
|
|
7
6
|
import { FieldOptions } from "@/components/forms/options";
|
|
8
7
|
import { useImperativeModal } from "@/components/modal/ImperativeModal";
|
|
9
|
-
import { Button } from "@/components/ui/button";
|
|
10
|
-
import {
|
|
11
|
-
DropdownMenu,
|
|
12
|
-
DropdownMenuContent,
|
|
13
|
-
DropdownMenuItem,
|
|
14
|
-
DropdownMenuLabel,
|
|
15
|
-
DropdownMenuSeparator,
|
|
16
|
-
DropdownMenuTrigger,
|
|
17
|
-
} from "@/components/ui/dropdown-menu";
|
|
18
8
|
import {
|
|
19
9
|
FormControl,
|
|
20
10
|
FormDescription,
|
|
@@ -23,18 +13,15 @@ import {
|
|
|
23
13
|
FormLabel,
|
|
24
14
|
FormMessage,
|
|
25
15
|
} from "@/components/ui/form";
|
|
26
|
-
import { Input } from "@/components/ui/input";
|
|
27
|
-
import { NumberField } from "@/components/ui/number-field";
|
|
28
16
|
import { SECRETS_REGISTRY } from "@/core/secrets/request-registry";
|
|
29
17
|
import { useAsyncData } from "@/hooks/useAsyncData";
|
|
30
|
-
import { partition } from "@/utils/arrays";
|
|
31
|
-
import { cn } from "@/utils/cn";
|
|
32
18
|
import { Functions } from "@/utils/functions";
|
|
33
19
|
import {
|
|
34
20
|
sortProviders,
|
|
35
21
|
WriteSecretModal,
|
|
36
22
|
} from "../chrome/panels/write-secret-modal";
|
|
37
|
-
import {
|
|
23
|
+
import { partitionSecretKeys, SecretCombobox } from "./secret-combobox";
|
|
24
|
+
import { prefixSecret } from "./secrets";
|
|
38
25
|
|
|
39
26
|
interface SecretsContextType {
|
|
40
27
|
providerNames: string[];
|
|
@@ -77,25 +64,23 @@ export const SecretsProvider = ({ children }: SecretsProviderProps) => {
|
|
|
77
64
|
};
|
|
78
65
|
}, []);
|
|
79
66
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
>
|
|
90
|
-
{children}
|
|
91
|
-
</SecretsContext>
|
|
67
|
+
const value = useMemo(
|
|
68
|
+
() => ({
|
|
69
|
+
secretKeys: data?.secretKeys || [],
|
|
70
|
+
providerNames: data?.providerNames || [],
|
|
71
|
+
loading: isPending,
|
|
72
|
+
error,
|
|
73
|
+
refreshSecrets: reload,
|
|
74
|
+
}),
|
|
75
|
+
[data?.secretKeys, data?.providerNames, isPending, error, reload],
|
|
92
76
|
);
|
|
77
|
+
|
|
78
|
+
return <SecretsContext value={value}>{children}</SecretsContext>;
|
|
93
79
|
};
|
|
94
80
|
|
|
95
|
-
export const ENV_RENDERER: FormRenderer<z.ZodString
|
|
96
|
-
isMatch: (schema: z.ZodType): schema is z.ZodString
|
|
97
|
-
|
|
98
|
-
if (schema instanceof z.ZodString || schema instanceof z.ZodNumber) {
|
|
81
|
+
export const ENV_RENDERER: FormRenderer<z.ZodString> = {
|
|
82
|
+
isMatch: (schema: z.ZodType): schema is z.ZodString => {
|
|
83
|
+
if (schema instanceof z.ZodString) {
|
|
99
84
|
const { optionRegex } = FieldOptions.parse(schema.description || "");
|
|
100
85
|
return Boolean(optionRegex);
|
|
101
86
|
}
|
|
@@ -109,12 +94,13 @@ export const ENV_RENDERER: FormRenderer<z.ZodString | z.ZodNumber> = {
|
|
|
109
94
|
const {
|
|
110
95
|
label,
|
|
111
96
|
description,
|
|
97
|
+
placeholder,
|
|
112
98
|
optionRegex = "",
|
|
99
|
+
inputType,
|
|
113
100
|
} = FieldOptions.parse(schema.description || "");
|
|
114
101
|
|
|
115
|
-
const
|
|
116
|
-
|
|
117
|
-
);
|
|
102
|
+
const secretsOnly = inputType === "password";
|
|
103
|
+
const { recommended, other } = partitionSecretKeys(secretKeys, optionRegex);
|
|
118
104
|
|
|
119
105
|
return (
|
|
120
106
|
<FormField
|
|
@@ -125,82 +111,28 @@ export const ENV_RENDERER: FormRenderer<z.ZodString | z.ZodNumber> = {
|
|
|
125
111
|
<FormLabel>{label}</FormLabel>
|
|
126
112
|
<FormDescription>{description}</FormDescription>
|
|
127
113
|
<FormControl>
|
|
128
|
-
<
|
|
129
|
-
{
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
<DropdownMenu>
|
|
145
|
-
<DropdownMenuTrigger asChild={true}>
|
|
146
|
-
<Button
|
|
147
|
-
variant="outline"
|
|
148
|
-
size="icon"
|
|
149
|
-
className={cn(
|
|
150
|
-
isSecret(field.value as string) && "bg-accent",
|
|
151
|
-
)}
|
|
152
|
-
>
|
|
153
|
-
<KeyIcon className="h-3 w-3" />
|
|
154
|
-
</Button>
|
|
155
|
-
</DropdownMenuTrigger>
|
|
156
|
-
<DropdownMenuContent
|
|
157
|
-
align="end"
|
|
158
|
-
className="max-h-60 overflow-y-auto"
|
|
159
|
-
>
|
|
160
|
-
<DropdownMenuItem
|
|
161
|
-
onSelect={() => {
|
|
162
|
-
openModal(
|
|
163
|
-
<WriteSecretModal
|
|
164
|
-
providerNames={providerNames}
|
|
165
|
-
onSuccess={(secretKey) => {
|
|
166
|
-
refreshSecrets();
|
|
167
|
-
field.onChange(prefixSecret(secretKey));
|
|
168
|
-
closeModal();
|
|
169
|
-
}}
|
|
170
|
-
onClose={closeModal}
|
|
171
|
-
/>,
|
|
172
|
-
);
|
|
114
|
+
<SecretCombobox
|
|
115
|
+
value={field.value ? String(field.value) : ""}
|
|
116
|
+
onChange={field.onChange}
|
|
117
|
+
placeholder={placeholder}
|
|
118
|
+
secretsOnly={secretsOnly}
|
|
119
|
+
recommendedKeys={recommended}
|
|
120
|
+
otherKeys={other}
|
|
121
|
+
onCreateSecret={(suggestedValue) => {
|
|
122
|
+
openModal(
|
|
123
|
+
<WriteSecretModal
|
|
124
|
+
providerNames={providerNames}
|
|
125
|
+
initialValue={suggestedValue}
|
|
126
|
+
onSuccess={(secretKey) => {
|
|
127
|
+
refreshSecrets();
|
|
128
|
+
field.onChange(prefixSecret(secretKey));
|
|
129
|
+
closeModal();
|
|
173
130
|
}}
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
<>
|
|
180
|
-
<DropdownMenuSeparator />
|
|
181
|
-
<DropdownMenuLabel>Recommended</DropdownMenuLabel>
|
|
182
|
-
</>
|
|
183
|
-
)}
|
|
184
|
-
{recommendedKeys.map((key) => (
|
|
185
|
-
<DropdownMenuItem
|
|
186
|
-
key={key}
|
|
187
|
-
onSelect={() => field.onChange(prefixSecret(key))}
|
|
188
|
-
>
|
|
189
|
-
{displaySecret(key)}
|
|
190
|
-
</DropdownMenuItem>
|
|
191
|
-
))}
|
|
192
|
-
{otherKeys.length > 0 && <DropdownMenuSeparator />}
|
|
193
|
-
{otherKeys.map((key) => (
|
|
194
|
-
<DropdownMenuItem
|
|
195
|
-
key={key}
|
|
196
|
-
onSelect={() => field.onChange(prefixSecret(key))}
|
|
197
|
-
>
|
|
198
|
-
{displaySecret(key)}
|
|
199
|
-
</DropdownMenuItem>
|
|
200
|
-
))}
|
|
201
|
-
</DropdownMenuContent>
|
|
202
|
-
</DropdownMenu>
|
|
203
|
-
</div>
|
|
131
|
+
onClose={closeModal}
|
|
132
|
+
/>,
|
|
133
|
+
);
|
|
134
|
+
}}
|
|
135
|
+
/>
|
|
204
136
|
</FormControl>
|
|
205
137
|
<FormMessage />
|
|
206
138
|
</FormItem>
|