History

Refactor secret storage to use providers

Rhys Sullivan committed 8 months ago

Commit 22e370c1 parent 6e204f010 files · +1066 −263Browse files

Showing the changes to apps (3 of 10 files). Show all

Unified · Split

3 files
  1. apps/web/src/views/add-source.tsx +58 −5
  2. apps/web/src/views/secrets.tsx +68 −7
  3. apps/web/src/views/source-editor.tsx +50 −7
apps/web/src/views/add-source.tsx+58−5
@@ -1,10 +1,11 @@
1import { useState, type ReactNode } from "react";1import { useEffect, useState, type ReactNode } from "react";
2import { Link, useNavigate } from "@tanstack/react-router";2import { Link, useNavigate } from "@tanstack/react-router";
3import {3import {
4 type CompleteSourceOAuthResult,4 type CompleteSourceOAuthResult,
5 type ConnectSourcePayload,5 type ConnectSourcePayload,
6 type ConnectSourceResult,6 type ConnectSourceResult,
7 type DiscoverSourcePayload,7 type DiscoverSourcePayload,
8 type InstanceConfig,
8 type Loadable,9 type Loadable,
9 type SecretListItem,10 type SecretListItem,
10 type Source,11 type Source,
@@ -12,6 +13,7 @@ import { · 1 unmodified line
12 useConnectSource,13 useConnectSource,
13 useCreateSecret,14 useCreateSecret,
14 useDiscoverSource,15 useDiscoverSource,
16 useInstanceConfig,
15 useRefreshSecrets,17 useRefreshSecrets,
16 useSecrets,18 useSecrets,
17 useStartSourceOAuth,19 useStartSourceOAuth,
@@ -590,6 +592,7 @@ export function AddSourcePage() { · 572 unmodified lines
590 const navigate = useNavigate();592 const navigate = useNavigate();
591 const discoverSource = useDiscoverSource();593 const discoverSource = useDiscoverSource();
592 const connectSource = useConnectSource();594 const connectSource = useConnectSource();
595 const instanceConfig = useInstanceConfig();
593 const secrets = useSecrets();596 const secrets = useSecrets();
594 const refreshSecrets = useRefreshSecrets();597 const refreshSecrets = useRefreshSecrets();
595 const startSourceOAuth = useStartSourceOAuth();598 const startSourceOAuth = useStartSourceOAuth();
@@ -1209,6 +1212,7 @@ export function AddSourcePage() { · 613 unmodified lines
1209 {connectForm.authKind === "bearer" && (1212 {connectForm.authKind === "bearer" && (
1210 <Field label="Token" className="sm:col-span-2">1213 <Field label="Token" className="sm:col-span-2">
1211 <SecretOrTokenInput1214 <SecretOrTokenInput
1215 instanceConfig={instanceConfig}
1212 secrets={secrets}1216 secrets={secrets}
1213 providerId={connectForm.bearerProviderId}1217 providerId={connectForm.bearerProviderId}
1214 handle={connectForm.bearerHandle}1218 handle={connectForm.bearerHandle}
@@ -1408,6 +1412,7 @@ const CREATE_NEW_VALUE = "__create_new__"; · 193 unmodified lines
1408const INLINE_TOKEN_VALUE = "__inline_token__";1412const INLINE_TOKEN_VALUE = "__inline_token__";
1409 1413
1410function SecretOrTokenInput(props: {1414function SecretOrTokenInput(props: {
1415 instanceConfig: Loadable<InstanceConfig>;
1411 secrets: Loadable<ReadonlyArray<SecretListItem>>;1416 secrets: Loadable<ReadonlyArray<SecretListItem>>;
1412 providerId: string;1417 providerId: string;
1413 handle: string;1418 handle: string;
@@ -1415,13 +1420,35 @@ function SecretOrTokenInput(props: { · 1 unmodified line
1415 onSelectSecret: (providerId: string, handle: string) => void;1420 onSelectSecret: (providerId: string, handle: string) => void;
1416 onChangeToken: (token: string) => void;1421 onChangeToken: (token: string) => void;
1417}) {1422}) {
1418 const { secrets, providerId, handle, inlineToken, onSelectSecret, onChangeToken } = props;1423 const {
1424 instanceConfig,
1425 secrets,
1426 providerId,
1427 handle,
1428 inlineToken,
1429 onSelectSecret,
1430 onChangeToken,
1431 } = props;
1419 const createSecret = useCreateSecret();1432 const createSecret = useCreateSecret();
1420 const [showCreate, setShowCreate] = useState(false);1433 const [showCreate, setShowCreate] = useState(false);
1421 const [useInline, setUseInline] = useState(false);1434 const [useInline, setUseInline] = useState(false);
1422 const [newName, setNewName] = useState("");1435 const [newName, setNewName] = useState("");
1423 const [newValue, setNewValue] = useState("");1436 const [newValue, setNewValue] = useState("");
1437 const [newProviderId, setNewProviderId] = useState("");
1424 const [createError, setCreateError] = useState<string | null>(null);1438 const [createError, setCreateError] = useState<string | null>(null);
1439 const storableProviders =
1440 instanceConfig.status === "ready"
1441 ? instanceConfig.data.secretProviders.filter((provider) => provider.canStore)
1442 : [];
1443
1444 useEffect(() => {
1445 if (newProviderId.length > 0) {
1446 return;
1447 }
1448 if (instanceConfig.status === "ready") {
1449 setNewProviderId(instanceConfig.data.defaultSecretStoreProvider);
1450 }
1451 }, [instanceConfig, newProviderId]);
1425 1452
1426 const selectedValue = showCreate ? CREATE_NEW_VALUE : useInline ? INLINE_TOKEN_VALUE : (handle || "");1453 const selectedValue = showCreate ? CREATE_NEW_VALUE : useInline ? INLINE_TOKEN_VALUE : (handle || "");
1427 1454
@@ -1431,6 +1458,11 @@ function SecretOrTokenInput(props: { · 3 unmodified lines
1431 setUseInline(false);1458 setUseInline(false);
1432 setNewName("");1459 setNewName("");
1433 setNewValue("");1460 setNewValue("");
1461 setNewProviderId(
1462 instanceConfig.status === "ready"
1463 ? instanceConfig.data.defaultSecretStoreProvider
1464 : "",
1465 );
1434 setCreateError(null);1466 setCreateError(null);
1435 return;1467 return;
1436 }1468 }
@@ -1448,7 +1480,10 @@ function SecretOrTokenInput(props: { · 11 unmodified lines
1448 }1480 }
1449 setUseInline(false);1481 setUseInline(false);
1450 setShowCreate(false);1482 setShowCreate(false);
1451 onSelectSecret("postgres", value);1483 const matchedSecret = secrets.status === "ready"
1484 ? secrets.data.find((secret) => secret.id === value)
1485 : null;
1486 onSelectSecret(matchedSecret?.providerId ?? "local", value);
1452 };1487 };
1453 1488
1454 const handleCreate = async () => {1489 const handleCreate = async () => {
@@ -1464,7 +1499,11 @@ function SecretOrTokenInput(props: { · 9 unmodified lines
1464 }1499 }
1465 1500
1466 try {1501 try {
1467 const result = await createSecret.mutateAsync({ name: trimmedName, value: newValue });1502 const result = await createSecret.mutateAsync({
1503 name: trimmedName,
1504 value: newValue,
1505 ...(newProviderId ? { providerId: newProviderId } : {}),
1506 });
1468 onSelectSecret(result.providerId, result.id);1507 onSelectSecret(result.providerId, result.id);
1469 setShowCreate(false);1508 setShowCreate(false);
1470 } catch (err) {1509 } catch (err) {
@@ -1495,7 +1534,7 @@ function SecretOrTokenInput(props: { · 24 unmodified lines
1495 <option value="">Select a secret...</option>1534 <option value="">Select a secret...</option>
1496 {items.map((secret) => (1535 {items.map((secret) => (
1497 <option key={secret.id} value={secret.id}>1536 <option key={secret.id} value={secret.id}>
1498 {secret.name || secret.id}1537 {secret.name || secret.id} ({secret.providerId})
1499 </option>1538 </option>
1500 ))}1539 ))}
1501 <option value={INLINE_TOKEN_VALUE}>Paste token directly</option>1540 <option value={INLINE_TOKEN_VALUE}>Paste token directly</option>
@@ -1541,6 +1580,20 @@ function SecretOrTokenInput(props: { · 39 unmodified lines
1541 className="h-8 w-full rounded-lg border border-input bg-background px-3 font-mono text-[11px] text-foreground outline-none transition-colors placeholder:text-muted-foreground/35 focus:border-ring focus:ring-1 focus:ring-ring/25"1580 className="h-8 w-full rounded-lg border border-input bg-background px-3 font-mono text-[11px] text-foreground outline-none transition-colors placeholder:text-muted-foreground/35 focus:border-ring focus:ring-1 focus:ring-ring/25"
1542 />1581 />
1543 </label>1582 </label>
1583 <label className="block space-y-1">
1584 <span className="text-[11px] font-medium text-muted-foreground">Store in</span>
1585 <select
1586 value={newProviderId}
1587 onChange={(e) => setNewProviderId(e.target.value)}
1588 className="h-8 w-full rounded-lg border border-input bg-background px-3 text-[12px] text-foreground outline-none transition-colors focus:border-ring focus:ring-1 focus:ring-ring/25"
1589 >
1590 {storableProviders.map((provider) => (
1591 <option key={provider.id} value={provider.id}>
1592 {provider.name}
1593 </option>
1594 ))}
1595 </select>
1596 </label>
1544 </div>1597 </div>
1545 <div className="flex items-center justify-end gap-2">1598 <div className="flex items-center justify-end gap-2">
1546 <button1599 <button
apps/web/src/views/secrets.tsx+68−7
@@ -1,13 +1,14 @@
1import { useState, type ReactNode } from "react";1import { useEffect, useState, type ReactNode } from "react";
2import { Link } from "@tanstack/react-router";2import { Link } from "@tanstack/react-router";
3import {3import {
4 type InstanceConfig,
4 type SecretListItem,5 type SecretListItem,
5 type Loadable,6 type Loadable,
6 useSecrets,7 useSecrets,
7 useCreateSecret,8 useCreateSecret,
8 useUpdateSecret,9 useUpdateSecret,
9 useDeleteSecret,10 useDeleteSecret,
10 useRefreshSecrets,11 useInstanceConfig,
11} from "@executor/react";12} from "@executor/react";
12import { LoadableBlock } from "../components/loadable";13import { LoadableBlock } from "../components/loadable";
13import { Button } from "../components/ui/button";14import { Button } from "../components/ui/button";
@@ -19,10 +20,26 @@ import { cn } from "../lib/utils"; · 5 unmodified lines
19// Page20// Page
20// ---------------------------------------------------------------------------21// ---------------------------------------------------------------------------
21 22
23const storableProvidersFromConfig = (config: Loadable<InstanceConfig>) =>
24 config.status === "ready"
25 ? config.data.secretProviders.filter((provider) => provider.canStore)
26 : [];
27
28const providerLabelForId = (config: Loadable<InstanceConfig>, providerId: string): string =>
29 config.status === "ready"
30 ? config.data.secretProviders.find((provider) => provider.id === providerId)?.name ?? providerId
31 : providerId;
32
22export function SecretsPage() {33export function SecretsPage() {
34 const instanceConfig = useInstanceConfig();
23 const secrets = useSecrets();35 const secrets = useSecrets();
24 const [showCreate, setShowCreate] = useState(false);36 const [showCreate, setShowCreate] = useState(false);
25 const [editingId, setEditingId] = useState<string | null>(null);37 const [editingId, setEditingId] = useState<string | null>(null);
38 const storableProviders = storableProvidersFromConfig(instanceConfig);
39 const defaultProviderLabel =
40 instanceConfig.status === "ready"
41 ? providerLabelForId(instanceConfig, instanceConfig.data.defaultSecretStoreProvider)
42 : null;
26 43
27 return (44 return (
28 <div className="flex-1 overflow-y-auto">45 <div className="flex-1 overflow-y-auto">
@@ -34,7 +51,8 @@ export function SecretsPage() { · 5 unmodified lines
34 Secrets51 Secrets
35 </h1>52 </h1>
36 <p className="mt-1.5 text-[14px] text-muted-foreground">53 <p className="mt-1.5 text-[14px] text-muted-foreground">
37 Tokens and credentials stored in this instance.54 Tokens and credentials stored by executor.
55 {defaultProviderLabel ? ` New secrets default to ${defaultProviderLabel}.` : ""}
38 </p>56 </p>
39 </div>57 </div>
40 <Button size="sm" onClick={() => { setShowCreate(true); setEditingId(null); }}>58 <Button size="sm" onClick={() => { setShowCreate(true); setEditingId(null); }}>
@@ -48,6 +66,12 @@ export function SecretsPage() { · 7 unmodified lines
48 <CreateSecretForm66 <CreateSecretForm
49 onClose={() => setShowCreate(false)}67 onClose={() => setShowCreate(false)}
50 className="mb-6"68 className="mb-6"
69 providerOptions={storableProviders}
70 defaultProviderId={
71 instanceConfig.status === "ready"
72 ? instanceConfig.data.defaultSecretStoreProvider
73 : null
74 }
51 />75 />
52 )}76 )}
53 77
@@ -62,6 +86,7 @@ export function SecretsPage() { · 8 unmodified lines
62 <SecretRow86 <SecretRow
63 key={secret.id}87 key={secret.id}
64 secret={secret}88 secret={secret}
89 providerLabel={providerLabelForId(instanceConfig, secret.providerId)}
65 isEditing={editingId === secret.id}90 isEditing={editingId === secret.id}
66 onEdit={() => setEditingId(editingId === secret.id ? null : secret.id)}91 onEdit={() => setEditingId(editingId === secret.id ? null : secret.id)}
67 onCancelEdit={() => setEditingId(null)}92 onCancelEdit={() => setEditingId(null)}
@@ -95,7 +120,7 @@ function EmptyState(props: { onAdd: () => void }) { · 27 unmodified lines
95 No secrets stored120 No secrets stored
96 </p>121 </p>
97 <p className="text-[13px] text-muted-foreground/60 mb-5">122 <p className="text-[13px] text-muted-foreground/60 mb-5">
98 Secrets let you securely provide tokens for source authentication.123 Secrets let you securely provide tokens for source authentication without putting them in workspace config.
99 </p>124 </p>
100 <Button size="sm" onClick={props.onAdd}>125 <Button size="sm" onClick={props.onAdd}>
101 <IconPlus className="size-3.5" />126 <IconPlus className="size-3.5" />
@@ -109,12 +134,27 @@ function EmptyState(props: { onAdd: () => void }) { · 7 unmodified lines
109// Create form134// Create form
110// ---------------------------------------------------------------------------135// ---------------------------------------------------------------------------
111 136
112function CreateSecretForm(props: { onClose: () => void; className?: string }) {137function CreateSecretForm(props: {
138 onClose: () => void;
139 className?: string;
140 providerOptions: ReadonlyArray<{ id: string; name: string }>;
141 defaultProviderId: string | null;
142}) {
113 const createSecret = useCreateSecret();143 const createSecret = useCreateSecret();
114 const [name, setName] = useState("");144 const [name, setName] = useState("");
115 const [value, setValue] = useState("");145 const [value, setValue] = useState("");
146 const [providerId, setProviderId] = useState(props.defaultProviderId ?? "");
116 const [error, setError] = useState<string | null>(null);147 const [error, setError] = useState<string | null>(null);
117 148
149 useEffect(() => {
150 if (providerId.length > 0) {
151 return;
152 }
153 if (props.defaultProviderId) {
154 setProviderId(props.defaultProviderId);
155 }
156 }, [props.defaultProviderId, providerId]);
157
118 const handleSubmit = async () => {158 const handleSubmit = async () => {
119 setError(null);159 setError(null);
120 const trimmedName = name.trim();160 const trimmedName = name.trim();
@@ -128,7 +168,11 @@ function CreateSecretForm(props: { onClose: () => void; className?: string }) { · 7 unmodified lines
128 }168 }
129 169
130 try {170 try {
131 await createSecret.mutateAsync({ name: trimmedName, value });171 await createSecret.mutateAsync({
172 name: trimmedName,
173 value,
174 ...(providerId ? { providerId } : {}),
175 });
132 props.onClose();176 props.onClose();
133 } catch (err) {177 } catch (err) {
134 setError(err instanceof Error ? err.message : "Failed creating secret.");178 setError(err instanceof Error ? err.message : "Failed creating secret.");
@@ -172,6 +216,19 @@ function CreateSecretForm(props: { onClose: () => void; className?: string }) { · 37 unmodified lines
172 className="h-9 w-full rounded-lg border border-input bg-background px-3 font-mono text-[12px] text-foreground outline-none transition-colors placeholder:text-muted-foreground/35 focus:border-ring focus:ring-1 focus:ring-ring/25"216 className="h-9 w-full rounded-lg border border-input bg-background px-3 font-mono text-[12px] text-foreground outline-none transition-colors placeholder:text-muted-foreground/35 focus:border-ring focus:ring-1 focus:ring-ring/25"
173 />217 />
174 </FieldLabel>218 </FieldLabel>
219 <FieldLabel label="Store in">
220 <select
221 value={providerId}
222 onChange={(e) => setProviderId(e.target.value)}
223 className="h-9 w-full rounded-lg border border-input bg-background px-3 text-[13px] text-foreground outline-none transition-colors focus:border-ring focus:ring-1 focus:ring-ring/25"
224 >
225 {props.providerOptions.map((provider) => (
226 <option key={provider.id} value={provider.id}>
227 {provider.name}
228 </option>
229 ))}
230 </select>
231 </FieldLabel>
175 </div>232 </div>
176 <div className="flex justify-end">233 <div className="flex justify-end">
177 <Button234 <Button
@@ -194,11 +251,12 @@ function CreateSecretForm(props: { onClose: () => void; className?: string }) { · 16 unmodified lines
194 251
195function SecretRow(props: {252function SecretRow(props: {
196 secret: SecretListItem;253 secret: SecretListItem;
254 providerLabel: string;
197 isEditing: boolean;255 isEditing: boolean;
198 onEdit: () => void;256 onEdit: () => void;
199 onCancelEdit: () => void;257 onCancelEdit: () => void;
200}) {258}) {
201 const { secret, isEditing } = props;259 const { secret, providerLabel, isEditing } = props;
202 const deleteSecret = useDeleteSecret();260 const deleteSecret = useDeleteSecret();
203 const [isDeleting, setIsDeleting] = useState(false);261 const [isDeleting, setIsDeleting] = useState(false);
204 262
@@ -239,6 +297,9 @@ function SecretRow(props: { · 34 unmodified lines
239 <Badge variant="outline" className="text-[9px] shrink-0">297 <Badge variant="outline" className="text-[9px] shrink-0">
240 {secret.purpose.replace(/_/g, " ")}298 {secret.purpose.replace(/_/g, " ")}
241 </Badge>299 </Badge>
300 <Badge variant="outline" className="text-[9px] shrink-0">
301 {providerLabel}
302 </Badge>
242 </div>303 </div>
243 <div className="flex items-center gap-2 mt-0.5">304 <div className="flex items-center gap-2 mt-0.5">
244 <span className="text-[11px] font-mono text-muted-foreground/50 truncate">305 <span className="text-[11px] font-mono text-muted-foreground/50 truncate">
apps/web/src/views/source-editor.tsx+50−7
@@ -1,4 +1,4 @@
1import { useState, type ReactNode } from "react";1import { useEffect, useState, type ReactNode } from "react";
2import { Link, useNavigate } from "@tanstack/react-router";2import { Link, useNavigate } from "@tanstack/react-router";
3import {3import {
4 type CompleteSourceOAuthResult,4 type CompleteSourceOAuthResult,
@@ -1012,6 +1012,7 @@ function SourceEditor(props: { mode: "create" | "edit"; source?: Source }) { · 1007 unmodified lines
1012 {formState.authKind === "bearer" && (1012 {formState.authKind === "bearer" && (
1013 <Field label="Token" className="sm:col-span-2">1013 <Field label="Token" className="sm:col-span-2">
1014 <SecretPicker1014 <SecretPicker
1015 instanceConfig={instanceConfig}
1015 secrets={secrets}1016 secrets={secrets}
1016 providerId={formState.bearerProviderId}1017 providerId={formState.bearerProviderId}
1017 handle={formState.bearerHandle}1018 handle={formState.bearerHandle}
@@ -1030,6 +1031,7 @@ function SourceEditor(props: { mode: "create" | "edit"; source?: Source }) { · 12 unmodified lines
1030 <>1031 <>
1031 <Field label="Access token" className="sm:col-span-2">1032 <Field label="Access token" className="sm:col-span-2">
1032 <SecretPicker1033 <SecretPicker
1034 instanceConfig={instanceConfig}
1033 secrets={secrets}1035 secrets={secrets}
1034 providerId={formState.oauthAccessProviderId}1036 providerId={formState.oauthAccessProviderId}
1035 handle={formState.oauthAccessHandle}1037 handle={formState.oauthAccessHandle}
@@ -1041,6 +1043,7 @@ function SourceEditor(props: { mode: "create" | "edit"; source?: Source }) { · 5 unmodified lines
1041 </Field>1043 </Field>
1042 <Field label="Refresh token (optional)" className="sm:col-span-2">1044 <Field label="Refresh token (optional)" className="sm:col-span-2">
1043 <SecretPicker1045 <SecretPicker
1046 instanceConfig={instanceConfig}
1044 secrets={secrets}1047 secrets={secrets}
1045 providerId={formState.oauthRefreshProviderId}1048 providerId={formState.oauthRefreshProviderId}
1046 handle={formState.oauthRefreshHandle}1049 handle={formState.oauthRefreshHandle}
@@ -1218,18 +1221,33 @@ function StatusBanner(props: { state: StatusBannerState; className?: string }) { · 171 unmodified lines
1218const CREATE_NEW_VALUE = "__create_new__";1221const CREATE_NEW_VALUE = "__create_new__";
1219 1222
1220function SecretPicker(props: {1223function SecretPicker(props: {
1224 instanceConfig: Loadable<InstanceConfig>;
1221 secrets: Loadable<ReadonlyArray<SecretListItem>>;1225 secrets: Loadable<ReadonlyArray<SecretListItem>>;
1222 providerId: string;1226 providerId: string;
1223 handle: string;1227 handle: string;
1224 onSelect: (providerId: string, handle: string) => void;1228 onSelect: (providerId: string, handle: string) => void;
1225 allowEmpty?: boolean;1229 allowEmpty?: boolean;
1226}) {1230}) {
1227 const { secrets, providerId, handle, onSelect, allowEmpty } = props;1231 const { instanceConfig, secrets, providerId, handle, onSelect, allowEmpty } = props;
1228 const createSecret = useCreateSecret();1232 const createSecret = useCreateSecret();
1229 const [showCreate, setShowCreate] = useState(false);1233 const [showCreate, setShowCreate] = useState(false);
1230 const [newName, setNewName] = useState("");1234 const [newName, setNewName] = useState("");
1231 const [newValue, setNewValue] = useState("");1235 const [newValue, setNewValue] = useState("");
1236 const [newProviderId, setNewProviderId] = useState("");
1232 const [createError, setCreateError] = useState<string | null>(null);1237 const [createError, setCreateError] = useState<string | null>(null);
1238 const storableProviders =
1239 instanceConfig.status === "ready"
1240 ? instanceConfig.data.secretProviders.filter((provider) => provider.canStore)
1241 : [];
1242
1243 useEffect(() => {
1244 if (newProviderId.length > 0) {
1245 return;
1246 }
1247 if (instanceConfig.status === "ready") {
1248 setNewProviderId(instanceConfig.data.defaultSecretStoreProvider);
1249 }
1250 }, [instanceConfig, newProviderId]);
1233 1251
1234 // Build the selected value key: if handle is set, use it (it's the secret ID)1252 // Build the selected value key: if handle is set, use it (it's the secret ID)
1235 const selectedValue = handle || "";1253 const selectedValue = handle || "";
@@ -1239,6 +1257,11 @@ function SecretPicker(props: { · 3 unmodified lines
1239 setShowCreate(true);1257 setShowCreate(true);
1240 setNewName("");1258 setNewName("");
1241 setNewValue("");1259 setNewValue("");
1260 setNewProviderId(
1261 instanceConfig.status === "ready"
1262 ? instanceConfig.data.defaultSecretStoreProvider
1263 : "",
1264 );
1242 setCreateError(null);1265 setCreateError(null);
1243 return;1266 return;
1244 }1267 }
@@ -1246,8 +1269,10 @@ function SecretPicker(props: { · 1 unmodified line
1246 onSelect("", "");1269 onSelect("", "");
1247 return;1270 return;
1248 }1271 }
1249 // Selecting an existing secret: providerId is always "postgres"1272 const matchedSecret = secrets.status === "ready"
1250 onSelect("postgres", value);1273 ? secrets.data.find((secret) => secret.id === value)
1274 : null;
1275 onSelect(matchedSecret?.providerId ?? "local", value);
1251 };1276 };
1252 1277
1253 const handleCreate = async () => {1278 const handleCreate = async () => {
@@ -1263,7 +1288,11 @@ function SecretPicker(props: { · 9 unmodified lines
1263 }1288 }
1264 1289
1265 try {1290 try {
1266 const result = await createSecret.mutateAsync({ name: trimmedName, value: newValue });1291 const result = await createSecret.mutateAsync({
1292 name: trimmedName,
1293 value: newValue,
1294 ...(newProviderId ? { providerId: newProviderId } : {}),
1295 });
1267 onSelect(result.providerId, result.id);1296 onSelect(result.providerId, result.id);
1268 setShowCreate(false);1297 setShowCreate(false);
1269 } catch (err) {1298 } catch (err) {
@@ -1286,7 +1315,7 @@ function SecretPicker(props: { · 16 unmodified lines
1286 1315
1287 // Check if the current handle matches a known secret1316 // Check if the current handle matches a known secret
1288 const matchedSecret = items.find((s) => s.id === handle);1317 const matchedSecret = items.find((s) => s.id === handle);
1289 const isExternalRef = handle && !matchedSecret && providerId !== "postgres";1318 const isExternalRef = handle && !matchedSecret && providerId !== "local";
1290 1319
1291 return (1320 return (
1292 <div className="space-y-2">1321 <div className="space-y-2">
@@ -1299,7 +1328,7 @@ function SecretPicker(props: { · 6 unmodified lines
1299 {!allowEmpty && !selectedValue && <option value="">Select a secret…</option>}1328 {!allowEmpty && !selectedValue && <option value="">Select a secret…</option>}
1300 {items.map((secret) => (1329 {items.map((secret) => (
1301 <option key={secret.id} value={secret.id}>1330 <option key={secret.id} value={secret.id}>
1302 {secret.name || secret.id}1331 {secret.name || secret.id} ({secret.providerId})
1303 </option>1332 </option>
1304 ))}1333 ))}
1305 {isExternalRef && (1334 {isExternalRef && (
@@ -1338,6 +1367,20 @@ function SecretPicker(props: { · 32 unmodified lines
1338 className="h-8 w-full rounded-lg border border-input bg-background px-3 font-mono text-[11px] text-foreground outline-none transition-colors placeholder:text-muted-foreground/35 focus:border-ring focus:ring-1 focus:ring-ring/25"1367 className="h-8 w-full rounded-lg border border-input bg-background px-3 font-mono text-[11px] text-foreground outline-none transition-colors placeholder:text-muted-foreground/35 focus:border-ring focus:ring-1 focus:ring-ring/25"
1339 />1368 />
1340 </label>1369 </label>
1370 <label className="block space-y-1">
1371 <span className="text-[11px] font-medium text-muted-foreground">Store in</span>
1372 <select
1373 value={newProviderId}
1374 onChange={(e) => setNewProviderId(e.target.value)}
1375 className="h-8 w-full rounded-lg border border-input bg-background px-3 text-[12px] text-foreground outline-none transition-colors focus:border-ring focus:ring-1 focus:ring-ring/25"
1376 >
1377 {storableProviders.map((provider) => (
1378 <option key={provider.id} value={provider.id}>
1379 {provider.name}
1380 </option>
1381 ))}
1382 </select>
1383 </label>
1341 </div>1384 </div>
1342 <div className="flex items-center justify-end gap-2">1385 <div className="flex items-center justify-end gap-2">
1343 <button1386 <button