DestinationPanelFields.tsx 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750
  1. import { PermissionAction } from '@supabase/shared-types/out/constants'
  2. import { useParams } from 'common'
  3. import { Eye, EyeOff, Loader2 } from 'lucide-react'
  4. import { useState } from 'react'
  5. import type { UseFormReturn } from 'react-hook-form'
  6. import {
  7. Button,
  8. FormControl,
  9. FormField,
  10. Input,
  11. Select,
  12. SelectContent,
  13. SelectGroup,
  14. SelectItem,
  15. SelectSeparator,
  16. SelectTrigger,
  17. TextArea,
  18. WarningIcon,
  19. } from 'ui'
  20. import { Admonition } from 'ui-patterns'
  21. import { Input as PasswordInput } from 'ui-patterns/DataInputs/Input'
  22. import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
  23. import { CREATE_NEW_KEY, CREATE_NEW_NAMESPACE } from './DestinationForm.constants'
  24. import type { DestinationPanelSchemaType } from './DestinationForm.schema'
  25. import { InlineLink } from '@/components/ui/InlineLink'
  26. import { getKeys, useAPIKeysQuery } from '@/data/api-keys/api-keys-query'
  27. import { useAnalyticsBucketsQuery } from '@/data/storage/analytics-buckets-query'
  28. import { useIcebergNamespacesQuery } from '@/data/storage/iceberg-namespaces-query'
  29. import { useStorageCredentialsQuery } from '@/data/storage/s3-access-key-query'
  30. import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions'
  31. export const BigQueryFields = ({ form }: { form: UseFormReturn<DestinationPanelSchemaType> }) => {
  32. return (
  33. <div className="flex flex-col gap-y-6 p-5">
  34. <p className="text-sm font-medium text-foreground">BigQuery settings</p>
  35. <div className="flex flex-col gap-y-4">
  36. <FormField
  37. control={form.control}
  38. name="projectId"
  39. render={({ field }) => (
  40. <FormItemLayout
  41. layout="horizontal"
  42. label="Project ID"
  43. description="The Google Cloud project ID where data will be sent"
  44. >
  45. <FormControl>
  46. <Input {...field} placeholder="my-gcp-project" />
  47. </FormControl>
  48. </FormItemLayout>
  49. )}
  50. />
  51. <FormField
  52. control={form.control}
  53. name="datasetId"
  54. render={({ field }) => (
  55. <FormItemLayout
  56. label="Dataset ID"
  57. layout="horizontal"
  58. description="The BigQuery dataset where replicated tables will be created"
  59. >
  60. <FormControl>
  61. <Input {...field} placeholder="my_dataset" />
  62. </FormControl>
  63. </FormItemLayout>
  64. )}
  65. />
  66. <FormField
  67. control={form.control}
  68. name="serviceAccountKey"
  69. render={({ field }) => (
  70. <FormItemLayout
  71. layout="horizontal"
  72. label="Service Account Key"
  73. description="Service account credentials JSON for authenticating with BigQuery"
  74. >
  75. <FormControl>
  76. <TextArea
  77. {...field}
  78. rows={5}
  79. maxLength={5000}
  80. placeholder='{"type": "service_account", "project_id": "...", ...}'
  81. className="font-mono text-xs"
  82. />
  83. </FormControl>
  84. </FormItemLayout>
  85. )}
  86. />
  87. </div>
  88. </div>
  89. )
  90. }
  91. export const DuckLakeFields = ({ form }: { form: UseFormReturn<DestinationPanelSchemaType> }) => {
  92. const [showCatalogUrl, setShowCatalogUrl] = useState(false)
  93. const [showSecretAccessKey, setShowSecretAccessKey] = useState(false)
  94. return (
  95. <div className="flex flex-col gap-y-6 p-5">
  96. <p className="text-sm font-medium text-foreground">DuckLake settings</p>
  97. <div className="flex flex-col gap-y-1">
  98. <p className="text-sm font-medium text-foreground">Catalog</p>
  99. <p className="text-sm text-foreground-light">
  100. Configure the PostgreSQL-backed DuckLake catalog and the S3-compatible storage location
  101. for replicated data.
  102. </p>
  103. </div>
  104. <div className="flex flex-col gap-y-4">
  105. <FormField
  106. control={form.control}
  107. name="ducklakeCatalogUrl"
  108. render={({ field }) => (
  109. <FormItemLayout
  110. layout="horizontal"
  111. label="Catalog URL"
  112. description="A PostgreSQL connection string for the DuckLake catalog"
  113. >
  114. <FormControl>
  115. <PasswordInput
  116. value={field.value ?? ''}
  117. type={showCatalogUrl ? 'text' : 'password'}
  118. placeholder="postgres://user:pass@host:5432/ducklake_catalog"
  119. onChange={(event) => field.onChange(event.target.value)}
  120. actions={
  121. <div className="flex items-center justify-center">
  122. <Button
  123. type="default"
  124. className="w-7"
  125. icon={showCatalogUrl ? <Eye /> : <EyeOff />}
  126. onClick={() => setShowCatalogUrl(!showCatalogUrl)}
  127. />
  128. </div>
  129. }
  130. />
  131. </FormControl>
  132. </FormItemLayout>
  133. )}
  134. />
  135. <FormField
  136. control={form.control}
  137. name="ducklakeDataPath"
  138. render={({ field }) => (
  139. <FormItemLayout
  140. layout="horizontal"
  141. label="Data path"
  142. description="An S3 path where DuckLake data files will be written"
  143. >
  144. <FormControl>
  145. <Input {...field} placeholder="s3://bucket/path" value={field.value ?? ''} />
  146. </FormControl>
  147. </FormItemLayout>
  148. )}
  149. />
  150. <FormField
  151. control={form.control}
  152. name="ducklakePoolSize"
  153. render={({ field }) => (
  154. <FormItemLayout
  155. layout="horizontal"
  156. label="Pool size"
  157. description="Optional number of concurrent DuckDB connections to use"
  158. >
  159. <FormControl>
  160. <Input
  161. type="number"
  162. min={1}
  163. max={6}
  164. value={field.value ?? ''}
  165. placeholder="Default: 4"
  166. onChange={(event) =>
  167. field.onChange(
  168. event.target.value === '' ? undefined : Number(event.target.value)
  169. )
  170. }
  171. />
  172. </FormControl>
  173. </FormItemLayout>
  174. )}
  175. />
  176. </div>
  177. <div className="flex flex-col gap-y-1">
  178. <p className="text-sm font-medium text-foreground">Object storage</p>
  179. <p className="text-sm text-foreground-light">
  180. Optional credentials and endpoint settings for S3-compatible storage providers.
  181. </p>
  182. </div>
  183. <div className="flex flex-col gap-y-4">
  184. <FormField
  185. control={form.control}
  186. name="ducklakeS3AccessKeyId"
  187. render={({ field }) => (
  188. <FormItemLayout
  189. layout="horizontal"
  190. label="S3 Access Key ID"
  191. description="Required access key ID for the object storage provider"
  192. >
  193. <FormControl>
  194. <Input {...field} placeholder="my-access-key" value={field.value ?? ''} />
  195. </FormControl>
  196. </FormItemLayout>
  197. )}
  198. />
  199. <FormField
  200. control={form.control}
  201. name="ducklakeS3SecretAccessKey"
  202. render={({ field }) => (
  203. <FormItemLayout
  204. layout="horizontal"
  205. label="S3 Secret Access Key"
  206. description="Required secret access key for the object storage provider"
  207. className="relative"
  208. >
  209. <FormControl>
  210. <Input
  211. {...field}
  212. type={showSecretAccessKey ? 'text' : 'password'}
  213. placeholder="my-secret-key"
  214. value={field.value ?? ''}
  215. />
  216. </FormControl>
  217. <Button
  218. type="default"
  219. icon={showSecretAccessKey ? <Eye /> : <EyeOff />}
  220. className="w-7 absolute right-6 top-[4px]"
  221. onClick={() => setShowSecretAccessKey(!showSecretAccessKey)}
  222. />
  223. </FormItemLayout>
  224. )}
  225. />
  226. <FormField
  227. control={form.control}
  228. name="ducklakeS3Region"
  229. render={({ field }) => (
  230. <FormItemLayout
  231. layout="horizontal"
  232. label="S3 Region"
  233. description="Required region for the object storage provider"
  234. >
  235. <FormControl>
  236. <Input {...field} placeholder="us-east-1" value={field.value ?? ''} />
  237. </FormControl>
  238. </FormItemLayout>
  239. )}
  240. />
  241. <FormField
  242. control={form.control}
  243. name="ducklakeS3Endpoint"
  244. render={({ field }) => (
  245. <FormItemLayout
  246. layout="horizontal"
  247. label="S3 Endpoint"
  248. description="Required endpoint without the protocol scheme, for example `127.0.0.1:5000/s3`"
  249. >
  250. <FormControl>
  251. <Input {...field} placeholder="127.0.0.1:5000/s3" value={field.value ?? ''} />
  252. </FormControl>
  253. </FormItemLayout>
  254. )}
  255. />
  256. <FormField
  257. control={form.control}
  258. name="ducklakeS3UrlStyle"
  259. render={({ field }) => (
  260. <FormItemLayout
  261. layout="horizontal"
  262. label="S3 URL style"
  263. description="Choose `path` for MinIO/Briven-style endpoints or `vhost` for AWS-style virtual host addressing"
  264. >
  265. <FormControl>
  266. <Select value={field.value ?? 'path'} onValueChange={field.onChange}>
  267. <SelectTrigger>{field.value ?? 'path'}</SelectTrigger>
  268. <SelectContent>
  269. <SelectItem value="path">path</SelectItem>
  270. <SelectItem value="vhost">vhost</SelectItem>
  271. </SelectContent>
  272. </Select>
  273. </FormControl>
  274. </FormItemLayout>
  275. )}
  276. />
  277. <FormField
  278. control={form.control}
  279. name="ducklakeS3UseSsl"
  280. render={({ field }) => (
  281. <FormItemLayout
  282. layout="horizontal"
  283. label="Use SSL"
  284. description="Whether to use SSL when connecting to the S3-compatible endpoint"
  285. >
  286. <FormControl>
  287. <Select
  288. value={field.value === false ? 'false' : 'true'}
  289. onValueChange={(value) => field.onChange(value === 'true')}
  290. >
  291. <SelectTrigger>{field.value === false ? 'false' : 'true'}</SelectTrigger>
  292. <SelectContent>
  293. <SelectItem value="true">true</SelectItem>
  294. <SelectItem value="false">false</SelectItem>
  295. </SelectContent>
  296. </Select>
  297. </FormControl>
  298. </FormItemLayout>
  299. )}
  300. />
  301. </div>
  302. <div className="flex flex-col gap-y-1">
  303. <p className="text-sm font-medium text-foreground">Maintenance</p>
  304. <p className="text-sm text-foreground-light">
  305. Optional settings for DuckLake metadata tables and snapshot cleanup.
  306. </p>
  307. </div>
  308. <div className="flex flex-col gap-y-4">
  309. <FormField
  310. control={form.control}
  311. name="ducklakeMetadataSchema"
  312. render={({ field }) => (
  313. <FormItemLayout
  314. layout="horizontal"
  315. label="Metadata schema"
  316. description="Schema used for DuckLake metadata tables in PostgreSQL"
  317. >
  318. <FormControl>
  319. <Input {...field} placeholder="ducklake" value={field.value ?? ''} />
  320. </FormControl>
  321. </FormItemLayout>
  322. )}
  323. />
  324. <FormField
  325. control={form.control}
  326. name="ducklakeExpireSnapshotsOlderThan"
  327. render={({ field }) => (
  328. <FormItemLayout
  329. layout="horizontal"
  330. label="Expire snapshots older than"
  331. description="Optional snapshot retention interval, for example `7 days`"
  332. >
  333. <FormControl>
  334. <Input {...field} placeholder="7 days" value={field.value ?? ''} />
  335. </FormControl>
  336. </FormItemLayout>
  337. )}
  338. />
  339. </div>
  340. </div>
  341. )
  342. }
  343. /**
  344. * [Joshen] JFYI I'd foresee a possible UX friction point here regarding S3 access key IDs and secret access keys
  345. * - We'd allow users to select access key IDs via a dropdown here, but require a text input for secret access keys
  346. * - Chances are most users wouldn't have the corresponding secret access key for the selected key ID at the top of their heads
  347. * - So highly likely may have to default to "Create a new key" -> which from here they won't know the secret access key thereafter
  348. * - And it'll end up just creating more keys for each destination
  349. * Ideal scenario: Just select an access key ID, we then apply the secret access key in the PATCH request, so FE has no
  350. * context of the secret access key at any point
  351. */
  352. export const AnalyticsBucketFields = ({
  353. form,
  354. setIsFormInteracting,
  355. onSelectNewBucket,
  356. }: {
  357. form: UseFormReturn<DestinationPanelSchemaType>
  358. setIsFormInteracting: (value: boolean) => void
  359. onSelectNewBucket: () => void
  360. }) => {
  361. const { warehouseName, s3AccessKeyId, namespace } = form.watch()
  362. const [showCatalogToken, setShowCatalogToken] = useState(false)
  363. const [showSecretAccessKey, setShowSecretAccessKey] = useState(false)
  364. const { ref: projectRef } = useParams()
  365. const { can: canReadAPIKeys } = useAsyncCheckPermissions(PermissionAction.SECRETS_READ, '*')
  366. const { data: apiKeys } = useAPIKeysQuery(
  367. { projectRef, reveal: true },
  368. { enabled: canReadAPIKeys }
  369. )
  370. const { serviceKey } = getKeys(apiKeys)
  371. const serviceApiKey = serviceKey?.api_key ?? ''
  372. const {
  373. data: keysData,
  374. isSuccess: isSuccessKeys,
  375. isPending: isLoadingKeys,
  376. isError: isErrorKeys,
  377. } = useStorageCredentialsQuery({ projectRef })
  378. const s3Keys = keysData?.data ?? []
  379. const keyNoLongerExists =
  380. (s3AccessKeyId ?? '').length > 0 &&
  381. s3AccessKeyId !== CREATE_NEW_KEY &&
  382. !s3Keys.find((k) => k.access_key === s3AccessKeyId)
  383. const {
  384. data: analyticsBuckets = [],
  385. isPending: isLoadingBuckets,
  386. isError: isErrorBuckets,
  387. } = useAnalyticsBucketsQuery({ projectRef })
  388. const canSelectNamespace = !!warehouseName && !!serviceApiKey
  389. const {
  390. data: namespaces = [],
  391. isPending: isLoadingNamespaces,
  392. isError: isErrorNamespaces,
  393. } = useIcebergNamespacesQuery(
  394. { projectRef, warehouse: warehouseName },
  395. { enabled: !!serviceApiKey }
  396. )
  397. return (
  398. <div className="flex flex-col gap-y-6 p-5">
  399. <p className="text-sm font-medium text-foreground">Analytics Bucket settings</p>
  400. <div className="flex flex-col gap-y-4">
  401. <FormField
  402. control={form.control}
  403. name="warehouseName"
  404. render={({ field }) => (
  405. <FormItemLayout
  406. label="Bucket"
  407. layout="horizontal"
  408. description="The Analytics Bucket where data will be stored"
  409. >
  410. {isLoadingBuckets ? (
  411. <Button
  412. disabled
  413. type="default"
  414. className="w-full justify-between"
  415. size="small"
  416. iconRight={<Loader2 className="animate-spin" />}
  417. >
  418. Retrieving buckets
  419. </Button>
  420. ) : isErrorBuckets ? (
  421. <Button
  422. disabled
  423. type="default"
  424. className="w-full justify-start"
  425. size="small"
  426. icon={<WarningIcon />}
  427. >
  428. Failed to retrieve buckets
  429. </Button>
  430. ) : (
  431. <FormControl>
  432. <Select
  433. value={field.value}
  434. onValueChange={(value) => {
  435. if (value === 'new-bucket') {
  436. onSelectNewBucket()
  437. } else {
  438. setIsFormInteracting(true)
  439. field.onChange(value)
  440. // [Joshen] Ideally should select the first namespace of the selected bucket
  441. form.setValue('namespace', '')
  442. }
  443. }}
  444. >
  445. <SelectTrigger>{field.value || 'Select a bucket'}</SelectTrigger>
  446. <SelectContent>
  447. <SelectGroup>
  448. {analyticsBuckets.length === 0 ? (
  449. <SelectItem value="__no_buckets__" disabled>
  450. No buckets available
  451. </SelectItem>
  452. ) : (
  453. analyticsBuckets.map((bucket) => (
  454. <SelectItem key={bucket.name} value={bucket.name}>
  455. {bucket.name}
  456. </SelectItem>
  457. ))
  458. )}
  459. <SelectSeparator />
  460. <SelectItem value="new-bucket">Create a new bucket</SelectItem>
  461. </SelectGroup>
  462. </SelectContent>
  463. </Select>
  464. </FormControl>
  465. )}
  466. </FormItemLayout>
  467. )}
  468. />
  469. <FormField
  470. control={form.control}
  471. name="namespace"
  472. render={({ field }) => (
  473. <FormItemLayout
  474. label="Namespace"
  475. layout="horizontal"
  476. description="The namespace within the bucket where tables will be organized"
  477. >
  478. {isLoadingNamespaces && canSelectNamespace ? (
  479. <Button
  480. disabled
  481. type="default"
  482. className="w-full justify-between"
  483. size="small"
  484. iconRight={<Loader2 className="animate-spin" />}
  485. >
  486. Retrieving namespaces
  487. </Button>
  488. ) : isErrorNamespaces ? (
  489. <Button
  490. disabled
  491. type="default"
  492. className="w-full justify-start"
  493. size="small"
  494. icon={<WarningIcon />}
  495. >
  496. Failed to retrieve namespaces
  497. </Button>
  498. ) : (
  499. <FormControl>
  500. <Select
  501. value={field.value}
  502. onValueChange={(value) => {
  503. setIsFormInteracting(true)
  504. field.onChange(value)
  505. }}
  506. disabled={!canSelectNamespace}
  507. >
  508. <SelectTrigger>
  509. {!canSelectNamespace
  510. ? 'Select a warehouse first'
  511. : field.value === CREATE_NEW_NAMESPACE
  512. ? 'Create a new namespace'
  513. : field.value || 'Select a namespace'}
  514. </SelectTrigger>
  515. <SelectContent>
  516. <SelectGroup>
  517. {namespaces.length === 0 ? (
  518. <SelectItem value="__no_namespaces__" disabled>
  519. No namespaces available
  520. </SelectItem>
  521. ) : (
  522. namespaces.map((namespace) => (
  523. <SelectItem key={namespace} value={namespace}>
  524. {namespace}
  525. </SelectItem>
  526. ))
  527. )}
  528. <SelectSeparator />
  529. <SelectItem key={CREATE_NEW_NAMESPACE} value={CREATE_NEW_NAMESPACE}>
  530. Create a new namespace
  531. </SelectItem>
  532. </SelectGroup>
  533. </SelectContent>
  534. </Select>
  535. </FormControl>
  536. )}
  537. </FormItemLayout>
  538. )}
  539. />
  540. {namespace === CREATE_NEW_NAMESPACE && (
  541. <FormField
  542. control={form.control}
  543. name="newNamespaceName"
  544. render={({ field }) => (
  545. <FormItemLayout
  546. label="New Namespace Name"
  547. layout="horizontal"
  548. description="A unique name for the new namespace"
  549. >
  550. <FormControl>
  551. <Input {...field} placeholder="new_namespace" value={field.value || ''} />
  552. </FormControl>
  553. </FormItemLayout>
  554. )}
  555. />
  556. )}
  557. <FormField
  558. control={form.control}
  559. name="catalogToken"
  560. render={({ field }) => (
  561. <FormItemLayout
  562. layout="horizontal"
  563. label="Catalog Token"
  564. description={
  565. <>
  566. Automatically retrieved from your project's{' '}
  567. <InlineLink href={`/project/${projectRef}/settings/api-keys`}>
  568. service role key
  569. </InlineLink>
  570. </>
  571. }
  572. >
  573. <PasswordInput
  574. disabled
  575. value={field.value}
  576. type={showCatalogToken ? 'text' : 'password'}
  577. placeholder="Auto-populated"
  578. actions={
  579. serviceApiKey ? (
  580. <div className="flex items-center justify-center">
  581. <Button
  582. type="default"
  583. className="w-7"
  584. icon={showCatalogToken ? <Eye /> : <EyeOff />}
  585. onClick={() => setShowCatalogToken(!showCatalogToken)}
  586. />
  587. </div>
  588. ) : null
  589. }
  590. />
  591. </FormItemLayout>
  592. )}
  593. />
  594. <FormField
  595. control={form.control}
  596. name="s3AccessKeyId"
  597. render={({ field }) => (
  598. <FormItemLayout
  599. layout="horizontal"
  600. label="S3 Access Key ID"
  601. description={
  602. <div className="flex flex-col gap-y-2">
  603. <p>
  604. Access keys are managed in your Storage{' '}
  605. <InlineLink href={`/project/${projectRef}/storage/s3`}>S3 settings</InlineLink>
  606. </p>
  607. {isSuccessKeys && keyNoLongerExists && (
  608. <Admonition
  609. type="warning"
  610. title="Unable to find access key ID in project"
  611. description={
  612. <>
  613. Please select another key or create a new set, as this destination will
  614. not work otherwise. S3 access keys can be managed in your{' '}
  615. <InlineLink href={`/project/${projectRef}/storage/files/settings`}>
  616. storage settings
  617. </InlineLink>
  618. .
  619. </>
  620. }
  621. />
  622. )}
  623. {s3AccessKeyId === CREATE_NEW_KEY && (
  624. <Admonition
  625. type="default"
  626. description="A new set of S3 access keys will be created."
  627. />
  628. )}
  629. </div>
  630. }
  631. >
  632. {isLoadingKeys ? (
  633. <Button
  634. disabled
  635. type="default"
  636. className="w-full justify-between"
  637. size="small"
  638. iconRight={<Loader2 className="animate-spin" />}
  639. >
  640. Retrieving keys
  641. </Button>
  642. ) : isErrorKeys ? (
  643. <Button
  644. disabled
  645. type="default"
  646. className="w-full justify-start"
  647. size="small"
  648. icon={<WarningIcon />}
  649. >
  650. Failed to retrieve keys
  651. </Button>
  652. ) : (
  653. <FormControl>
  654. <Select value={field.value} onValueChange={field.onChange}>
  655. <SelectTrigger>
  656. {field.value === CREATE_NEW_KEY
  657. ? 'Create a new key'
  658. : (field.value ?? '').length === 0
  659. ? 'Select an access key ID'
  660. : field.value}
  661. </SelectTrigger>
  662. <SelectContent>
  663. <SelectGroup>
  664. {s3Keys.map((key) => (
  665. <SelectItem key={key.id} value={key.access_key}>
  666. {key.access_key}
  667. <p className="text-foreground-lighter">{key.description}</p>
  668. </SelectItem>
  669. ))}
  670. <SelectSeparator />
  671. <SelectItem key={CREATE_NEW_KEY} value={CREATE_NEW_KEY}>
  672. Create a new key
  673. </SelectItem>
  674. </SelectGroup>
  675. </SelectContent>
  676. </Select>
  677. </FormControl>
  678. )}
  679. </FormItemLayout>
  680. )}
  681. />
  682. {s3AccessKeyId !== CREATE_NEW_KEY && (
  683. <FormField
  684. control={form.control}
  685. name="s3SecretAccessKey"
  686. render={({ field }) => (
  687. <FormItemLayout
  688. layout="horizontal"
  689. label="S3 Secret Access Key"
  690. className="relative"
  691. description="The secret key corresponding to your selected access key ID."
  692. >
  693. <FormControl>
  694. <Input
  695. {...field}
  696. type={showSecretAccessKey ? 'text' : 'password'}
  697. value={field.value ?? ''}
  698. placeholder="Provide the secret access key"
  699. />
  700. </FormControl>
  701. <Button
  702. type="default"
  703. icon={showSecretAccessKey ? <Eye /> : <EyeOff />}
  704. className="w-7 absolute right-6 top-[4px]"
  705. onClick={() => setShowSecretAccessKey(!showSecretAccessKey)}
  706. />
  707. </FormItemLayout>
  708. )}
  709. />
  710. )}
  711. </div>
  712. </div>
  713. )
  714. }