Showing the changes to apps (3 of 10 files). Show all
Unified · Split
| @@ -1,10 +1,11 @@ | |||
| 1 | import { useState, type ReactNode } from "react"; | 1 | import { useEffect, useState, type ReactNode } from "react"; |
| 2 | import { Link, useNavigate } from "@tanstack/react-router"; | 2 | import { Link, useNavigate } from "@tanstack/react-router"; |
| 3 | import { | 3 | import { |
| 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 | <SecretOrTokenInput | 1214 | <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 | |||
| 1408 | const INLINE_TOKEN_VALUE = "__inline_token__"; | 1412 | const INLINE_TOKEN_VALUE = "__inline_token__"; |
| 1409 | 1413 | ||
| 1410 | function SecretOrTokenInput(props: { | 1414 | function 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 | <button | 1599 | <button |
| @@ -1,13 +1,14 @@ | |||
| 1 | import { useState, type ReactNode } from "react"; | 1 | import { useEffect, useState, type ReactNode } from "react"; |
| 2 | import { Link } from "@tanstack/react-router"; | 2 | import { Link } from "@tanstack/react-router"; |
| 3 | import { | 3 | import { |
| 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"; |
| 12 | import { LoadableBlock } from "../components/loadable"; | 13 | import { LoadableBlock } from "../components/loadable"; |
| 13 | import { Button } from "../components/ui/button"; | 14 | import { Button } from "../components/ui/button"; |
| @@ -19,10 +20,26 @@ import { cn } from "../lib/utils"; · 5 unmodified lines | |||
| 19 | // Page | 20 | // Page |
| 20 | // --------------------------------------------------------------------------- | 21 | // --------------------------------------------------------------------------- |
| 21 | 22 | ||
| 23 | const storableProvidersFromConfig = (config: Loadable<InstanceConfig>) => | ||
| 24 | config.status === "ready" | ||
| 25 | ? config.data.secretProviders.filter((provider) => provider.canStore) | ||
| 26 | : []; | ||
| 27 | |||
| 28 | const 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 | |||
| 22 | export function SecretsPage() { | 33 | export 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 | Secrets | 51 | 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 | <CreateSecretForm | 66 | <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 | <SecretRow | 86 | <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 stored | 120 | 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 form | 134 | // Create form |
| 110 | // --------------------------------------------------------------------------- | 135 | // --------------------------------------------------------------------------- |
| 111 | 136 | ||
| 112 | function CreateSecretForm(props: { onClose: () => void; className?: string }) { | 137 | function 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 | <Button | 234 | <Button |
| @@ -194,11 +251,12 @@ function CreateSecretForm(props: { onClose: () => void; className?: string }) { · 16 unmodified lines | |||
| 194 | 251 | ||
| 195 | function SecretRow(props: { | 252 | function 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"> |
| @@ -1,4 +1,4 @@ | |||
| 1 | import { useState, type ReactNode } from "react"; | 1 | import { useEffect, useState, type ReactNode } from "react"; |
| 2 | import { Link, useNavigate } from "@tanstack/react-router"; | 2 | import { Link, useNavigate } from "@tanstack/react-router"; |
| 3 | import { | 3 | import { |
| 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 | <SecretPicker | 1014 | <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 | <SecretPicker | 1033 | <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 | <SecretPicker | 1045 | <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 | |||
| 1218 | const CREATE_NEW_VALUE = "__create_new__"; | 1221 | const CREATE_NEW_VALUE = "__create_new__"; |
| 1219 | 1222 | ||
| 1220 | function SecretPicker(props: { | 1223 | function 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 secret | 1316 | // 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 | <button | 1386 | <button |