| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355 |
- import { zodResolver } from '@hookform/resolvers/zod'
- import { PermissionAction } from '@supabase/shared-types/out/constants'
- import { AlertTitle } from '@ui/components/shadcn/ui/alert'
- import { useParams } from 'common'
- import Link from 'next/link'
- import { useEffect, useState } from 'react'
- import { SubmitHandler, useForm } from 'react-hook-form'
- import { toast } from 'sonner'
- import {
- Alert,
- AlertDescription,
- Button,
- Card,
- CardContent,
- CardFooter,
- Form,
- FormControl,
- FormField,
- Switch,
- Table,
- TableBody,
- TableCell,
- TableHead,
- TableHeader,
- TableRow,
- WarningIcon,
- } from 'ui'
- import { Input } from 'ui-patterns/DataInputs/Input'
- import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
- import { PageContainer } from 'ui-patterns/PageContainer'
- import {
- PageSection,
- PageSectionAside,
- PageSectionContent,
- PageSectionDescription,
- PageSectionMeta,
- PageSectionSummary,
- PageSectionTitle,
- } from 'ui-patterns/PageSection'
- import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader'
- import * as z from 'zod'
- import { CreateCredentialModal } from './CreateCredentialModal'
- import { RevokeCredentialModal } from './RevokeCredentialModal'
- import { StorageCredItem } from './StorageCredItem'
- import { getConnectionURL } from './StorageSettings.utils'
- import AlertError from '@/components/ui/AlertError'
- import { DocsButton } from '@/components/ui/DocsButton'
- import NoPermission from '@/components/ui/NoPermission'
- import { useProjectSettingsV2Query } from '@/data/config/project-settings-v2-query'
- import { useProjectStorageConfigQuery } from '@/data/config/project-storage-config-query'
- import { useProjectStorageConfigUpdateUpdateMutation } from '@/data/config/project-storage-config-update-mutation'
- import { useStorageCredentialsQuery } from '@/data/storage/s3-access-key-query'
- import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions'
- import { useIsProjectActive, useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
- import { DOCS_URL } from '@/lib/constants'
- export const S3Connection = () => {
- const { ref: projectRef } = useParams()
- const isProjectActive = useIsProjectActive()
- const { data: project, isPending: projectIsLoading } = useSelectedProjectQuery()
- const [openCreateCred, setOpenCreateCred] = useState(false)
- const [openDeleteDialog, setOpenDeleteDialog] = useState(false)
- const [deleteCred, setDeleteCred] = useState<{ id: string; description: string }>()
- const { can: canReadS3Credentials, isLoading: isLoadingPermissions } = useAsyncCheckPermissions(
- PermissionAction.STORAGE_ADMIN_READ,
- '*'
- )
- const { can: canUpdateStorageSettings } = useAsyncCheckPermissions(
- PermissionAction.STORAGE_ADMIN_WRITE,
- '*'
- )
- const { data: settings } = useProjectSettingsV2Query({ projectRef })
- const {
- data: config,
- error: configError,
- isSuccess: isSuccessStorageConfig,
- isError: isErrorStorageConfig,
- } = useProjectStorageConfigQuery({ projectRef })
- const { data: storageCreds, isPending: isLoadingStorageCreds } = useStorageCredentialsQuery(
- { projectRef },
- { enabled: canReadS3Credentials }
- )
- const { mutate: updateStorageConfig, isPending: isUpdating } =
- useProjectStorageConfigUpdateUpdateMutation({
- onSuccess: (_, vars) => {
- if (vars.features?.s3Protocol) {
- form.reset({ s3ConnectionEnabled: vars.features.s3Protocol.enabled })
- }
- toast.success('Successfully updated storage settings')
- },
- })
- const FormSchema = z.object({ s3ConnectionEnabled: z.boolean() })
- const form = useForm<z.infer<typeof FormSchema>>({
- resolver: zodResolver(FormSchema as any),
- defaultValues: { s3ConnectionEnabled: false },
- })
- const protocol = settings?.app_config?.protocol ?? 'https'
- const endpoint = settings?.app_config?.storage_endpoint || settings?.app_config?.endpoint
- const hasStorageCreds = storageCreds?.data && storageCreds.data.length > 0
- const s3connectionUrl = getConnectionURL(projectRef ?? '', protocol, endpoint)
- const onSubmit: SubmitHandler<z.infer<typeof FormSchema>> = async (data) => {
- if (!projectRef) return console.error('Project ref is required')
- if (!config) return console.error('Storage config is required')
- updateStorageConfig({
- projectRef,
- features: {
- ...config?.features,
- s3Protocol: { enabled: data.s3ConnectionEnabled },
- },
- })
- }
- useEffect(() => {
- form.reset({ s3ConnectionEnabled: config?.features.s3Protocol.enabled })
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [isSuccessStorageConfig])
- return (
- <>
- <PageContainer>
- <PageSection>
- <PageSectionMeta>
- <PageSectionSummary>
- <PageSectionTitle>Connection</PageSectionTitle>
- <PageSectionDescription>
- Connect to your bucket using any S3-compatible service via the S3 protocol
- </PageSectionDescription>
- </PageSectionSummary>
- <PageSectionAside>
- <DocsButton href={`${DOCS_URL}/guides/storage/s3/authentication`} />
- </PageSectionAside>
- </PageSectionMeta>
- <PageSectionContent>
- {isErrorStorageConfig && (
- <AlertError
- className="mb-4"
- subject="Failed to retrieve storage configuration"
- error={configError}
- />
- )}
- <Form {...form}>
- <form id="s3-connection-form" onSubmit={form.handleSubmit(onSubmit)}>
- {projectIsLoading ? (
- <GenericSkeletonLoader />
- ) : isProjectActive ? (
- <Card>
- <CardContent>
- <FormField
- name="s3ConnectionEnabled"
- control={form.control}
- render={({ field }) => (
- <FormItemLayout
- layout="flex-row-reverse"
- className="[&>*>label]:text-foreground"
- label="S3 protocol connection"
- description="Allow clients to connect to Briven Storage via the S3 protocol"
- >
- <FormControl>
- <Switch
- size="large"
- checked={field.value}
- onCheckedChange={field.onChange}
- disabled={!isSuccessStorageConfig || field.disabled}
- />
- </FormControl>
- </FormItemLayout>
- )}
- />
- </CardContent>
- <CardContent>
- <FormItemLayout
- layout="flex-row-reverse"
- className="[&>div]:md:w-1/2 [&>div>div]:w-full [&>div]:min-w-100"
- label="Endpoint"
- isReactForm={false}
- >
- <Input readOnly copy value={s3connectionUrl} />
- </FormItemLayout>
- </CardContent>
- <CardContent>
- <FormItemLayout
- layout="flex-row-reverse"
- className="[&>div]:md:w-1/2 [&>div>div]:w-full [&>div]:min-w-100"
- label="Region"
- isReactForm={false}
- >
- <Input
- readOnly
- copy
- value={project?.region}
- data-1p-ignore
- data-lpignore="true"
- data-form-type="other"
- data-bwignore
- />
- </FormItemLayout>
- </CardContent>
- {!isLoadingPermissions && !canUpdateStorageSettings && (
- <CardContent>
- <p className="text-sm text-foreground-light">
- You need additional permissions to update storage settings
- </p>
- </CardContent>
- )}
- <CardFooter className="justify-end space-x-2">
- {form.formState.isDirty && (
- <Button
- type="default"
- htmlType="reset"
- onClick={() => form.reset()}
- disabled={
- !form.formState.isDirty || !canUpdateStorageSettings || isUpdating
- }
- >
- Cancel
- </Button>
- )}
- <Button
- type="primary"
- htmlType="submit"
- loading={isUpdating}
- disabled={
- !form.formState.isDirty || !canUpdateStorageSettings || isUpdating
- }
- >
- Save
- </Button>
- </CardFooter>
- </Card>
- ) : (
- <Alert variant="warning">
- <WarningIcon />
- <AlertTitle>Project is paused</AlertTitle>
- <AlertDescription>
- To connect to your S3 bucket, you need to restore your project.
- </AlertDescription>
- <div className="mt-3 flex items-center space-x-2">
- <Button asChild type="default">
- <Link href={`/project/${projectRef}`}>Restore project</Link>
- </Button>
- </div>
- </Alert>
- )}
- </form>
- </Form>
- </PageSectionContent>
- </PageSection>
- <PageSection>
- <PageSectionMeta>
- <PageSectionSummary>
- <PageSectionTitle>Access keys</PageSectionTitle>
- <PageSectionDescription>
- Manage your access keys for this project
- </PageSectionDescription>
- </PageSectionSummary>
- <PageSectionAside>
- <CreateCredentialModal visible={openCreateCred} onOpenChange={setOpenCreateCred} />
- </PageSectionAside>
- </PageSectionMeta>
- <PageSectionContent>
- {projectIsLoading || isLoadingPermissions ? (
- <GenericSkeletonLoader />
- ) : !canReadS3Credentials ? (
- <NoPermission resourceText="view this project's S3 access keys" />
- ) : !isProjectActive ? (
- <Alert variant="warning">
- <WarningIcon />
- <AlertTitle>Can't fetch S3 access keys</AlertTitle>
- <AlertDescription>
- To fetch your S3 access keys, you need to restore your project.
- </AlertDescription>
- <AlertDescription>
- <Button asChild type="default" className="mt-3">
- <Link href={`/project/${projectRef}`}>Restore project</Link>
- </Button>
- </AlertDescription>
- </Alert>
- ) : (
- <>
- {isLoadingStorageCreds ? (
- <GenericSkeletonLoader />
- ) : (
- <Card>
- <Table>
- <TableHeader>
- <TableRow>
- <TableHead key="description">Name</TableHead>
- <TableHead key="access-key-id">Key ID</TableHead>
- <TableHead key="created-at">Created at</TableHead>
- <TableHead key="actions" />
- </TableRow>
- </TableHeader>
- <TableBody>
- {hasStorageCreds ? (
- storageCreds.data?.map((cred) => (
- <StorageCredItem
- key={cred.id}
- created_at={cred.created_at}
- access_key={cred.access_key}
- description={cred.description}
- id={cred.id}
- onDeleteClick={() => {
- setDeleteCred(cred)
- setOpenDeleteDialog(true)
- }}
- />
- ))
- ) : (
- <TableRow>
- <TableCell colSpan={4} className="rounded-b-md! overflow-hidden">
- <p className="text-sm text-foreground">No access keys created</p>
- <p className="text-sm text-foreground-light">
- There are no access keys associated with your project yet
- </p>
- </TableCell>
- </TableRow>
- )}
- </TableBody>
- </Table>
- </Card>
- )}
- </>
- )}
- </PageSectionContent>
- </PageSection>
- </PageContainer>
- <RevokeCredentialModal
- visible={openDeleteDialog}
- selectedCredential={deleteCred}
- onClose={() => {
- setOpenDeleteDialog(false)
- setDeleteCred(undefined)
- }}
- />
- </>
- )
- }
|