Files
Danny White 476d4a5851 refactor(ui): drop redundant Button variant="default" props (#50161)
## 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
2026-09-11 17:05:26 +10:00

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)
}}
/>
</>
)
}