mirror of
https://github.com/supabase/supabase.git
synced 2026-10-06 01:45:10 +03:00
## What kind of change does this PR introduce? Mechanical cleanup on top of the Button default-variant change (#50160). ## What is the current behavior? Many callsites still pass `variant="default"` even though that is now the component default. ## What is the new behavior? Removes redundant static `variant="default"` from legacy `Button` and `ButtonTooltip` callsites. Keeps explicit defaults where they document the API: - `button-default.tsx` and `button-sizes.tsx` demos - `DocsButton`, which pins neutral styling at the wrapper boundary ## To test Studio: - [Auth → Rate Limits](https://studio-staging-2s957kwc4-supabase.vercel.app/dashboard/project/_/auth/rate-limits): dirty the form so Cancel appears; Cancel stays neutral, Save stays green - [Project Settings → API Keys](https://studio-staging-2s957kwc4-supabase.vercel.app/dashboard/project/_/settings/api-keys): `DocsButton` in the header actions stays neutral Design system: - [Design system → Button](https://design-system-git-dnywh-dc924ac1-supabase.vercel.app/design-system/docs/components/button): `button-default` / `button-sizes` still show explicit default styling; Primary (green) is restricted to the Primary section (and `asChild`) WWW: - [www → Brand assets](https://zone-www-dot-com-git-dnywh-dc924ac1-supabase.vercel.app/brand-assets): Download logo kit / Download button kit stay neutral
355 lines
13 KiB
TypeScript
355 lines
13 KiB
TypeScript
import { zodResolver } from '@hookform/resolvers/zod'
|
|
import { PermissionAction } from '@supabase/shared-types/out/constants'
|
|
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,
|
|
AlertTitle,
|
|
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),
|
|
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 Supabase 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="reset"
|
|
onClick={() => form.reset()}
|
|
disabled={
|
|
!form.formState.isDirty || !canUpdateStorageSettings || isUpdating
|
|
}
|
|
>
|
|
Cancel
|
|
</Button>
|
|
)}
|
|
<Button
|
|
variant="primary"
|
|
type="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>
|
|
<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 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)
|
|
}}
|
|
/>
|
|
</>
|
|
)
|
|
}
|