mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 17:35: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
275 lines
9.3 KiB
TypeScript
275 lines
9.3 KiB
TypeScript
import { zodResolver } from '@hookform/resolvers/zod'
|
||
import { useParams } from 'common'
|
||
import { SubmitHandler, useForm } from 'react-hook-form'
|
||
import { toast } from 'sonner'
|
||
import {
|
||
Button,
|
||
cn,
|
||
DialogFooter,
|
||
DialogSection,
|
||
Form,
|
||
FormControl,
|
||
FormField,
|
||
Input,
|
||
SheetFooter,
|
||
SheetSection,
|
||
} from 'ui'
|
||
import { Admonition } from 'ui-patterns/Admonition'
|
||
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
|
||
import z from 'zod'
|
||
|
||
import { useIcebergWrapperExtension } from './AnalyticsBucketDetails/useIcebergWrapper'
|
||
import {
|
||
reservedPrefixes,
|
||
reservedSuffixes,
|
||
validBucketNameRegex,
|
||
} from './CreateAnalyticsBucketForm.utils'
|
||
import { getServiceVersionsPath } from '@/components/interfaces/Settings/General/ServiceVersions/ServiceVersions.utils'
|
||
import { InlineLink } from '@/components/ui/InlineLink'
|
||
import { useDatabaseExtensionEnableMutation } from '@/data/database-extensions/database-extension-enable-mutation'
|
||
import { useAnalyticsBucketCreateMutation } from '@/data/storage/analytics-bucket-create-mutation'
|
||
import { useAnalyticsBucketsQuery } from '@/data/storage/analytics-buckets-query'
|
||
import { useIcebergWrapperCreateMutation } from '@/data/storage/iceberg-wrapper-create-mutation'
|
||
import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
|
||
import { DOCS_URL } from '@/lib/constants'
|
||
import { useTrack } from '@/lib/telemetry/track'
|
||
|
||
const FormSchema = z
|
||
.object({
|
||
name: z
|
||
.string()
|
||
.trim()
|
||
.min(3, 'Bucket name should be at least 3 characters')
|
||
.max(63, 'Bucket name should be up to 63 characters')
|
||
.refine(
|
||
(value) => !value.endsWith(' '),
|
||
'The name of the bucket cannot end with a whitespace'
|
||
)
|
||
.refine(
|
||
(value) => value !== 'public',
|
||
'"public" is a reserved name. Please choose another name'
|
||
),
|
||
})
|
||
.superRefine((data, ctx) => {
|
||
if (reservedPrefixes.test(data.name)) {
|
||
const [match] = data.name.match(reservedPrefixes) ?? []
|
||
return ctx.addIssue({
|
||
path: ['name'],
|
||
code: z.ZodIssueCode.custom,
|
||
message: `Bucket name cannot start with "${match}"`,
|
||
})
|
||
}
|
||
|
||
if (reservedSuffixes.test(data.name)) {
|
||
const [match] = data.name.match(reservedSuffixes) ?? []
|
||
return ctx.addIssue({
|
||
path: ['name'],
|
||
code: z.ZodIssueCode.custom,
|
||
message: `Bucket name cannot end with "${match}"`,
|
||
})
|
||
}
|
||
|
||
if (/[A-Z]/.test(data.name)) {
|
||
return ctx.addIssue({
|
||
path: ['name'],
|
||
code: z.ZodIssueCode.custom,
|
||
message: 'Bucket name can only be lowercase characters',
|
||
})
|
||
}
|
||
|
||
if (!validBucketNameRegex.test(data.name)) {
|
||
if (!/^[a-z0-9]/.test(data.name)) {
|
||
return ctx.addIssue({
|
||
path: ['name'],
|
||
code: z.ZodIssueCode.custom,
|
||
message: 'Bucket name must start with a lowercase letter or number.',
|
||
})
|
||
}
|
||
|
||
if (!/[a-z0-9]$/.test(data.name)) {
|
||
return ctx.addIssue({
|
||
path: ['name'],
|
||
code: z.ZodIssueCode.custom,
|
||
message: 'Bucket name must end with a lowercase letter or number.',
|
||
})
|
||
}
|
||
|
||
const [match] = data.name.match(/[^a-z0-9-]/) ?? []
|
||
return ctx.addIssue({
|
||
path: ['name'],
|
||
code: z.ZodIssueCode.custom,
|
||
message: !!match
|
||
? `Bucket name cannot contain the "${match}" character`
|
||
: 'Bucket name contains an invalid special character',
|
||
})
|
||
}
|
||
})
|
||
|
||
const formId = 'create-analytics-storage-bucket-form'
|
||
|
||
export type CreateAnalyticsBucketForm = z.infer<typeof FormSchema>
|
||
|
||
interface CreateAnalyticsBucketFormProps {
|
||
type?: 'dialog' | 'sheet'
|
||
onOpenChange: (value: boolean) => void
|
||
}
|
||
|
||
export const CreateAnalyticsBucketForm = ({
|
||
type = 'dialog',
|
||
onOpenChange,
|
||
}: CreateAnalyticsBucketFormProps) => {
|
||
const { ref } = useParams()
|
||
const { data: project } = useSelectedProjectQuery()
|
||
const { extension: wrappersExtension, state: wrappersExtensionState } =
|
||
useIcebergWrapperExtension()
|
||
|
||
const { data: buckets = [] } = useAnalyticsBucketsQuery({ projectRef: ref })
|
||
const wrappersExtensionNeedsUpgrading = wrappersExtensionState === 'needs-upgrade'
|
||
|
||
const track = useTrack()
|
||
|
||
const { mutateAsync: createAnalyticsBucket, isPending: isCreatingAnalyticsBucket } =
|
||
useAnalyticsBucketCreateMutation({
|
||
// [Joshen] Silencing the error here as it's being handled in onSubmit
|
||
onError: () => {},
|
||
})
|
||
|
||
const { mutateAsync: createIcebergWrapper, isPending: isCreatingIcebergWrapper } =
|
||
useIcebergWrapperCreateMutation()
|
||
|
||
const { mutateAsync: enableExtension, isPending: isEnablingExtension } =
|
||
useDatabaseExtensionEnableMutation()
|
||
|
||
const isCreating = isEnablingExtension || isCreatingIcebergWrapper || isCreatingAnalyticsBucket
|
||
|
||
const form = useForm<CreateAnalyticsBucketForm>({
|
||
resolver: zodResolver(FormSchema),
|
||
defaultValues: { name: '' },
|
||
})
|
||
|
||
const onSubmit: SubmitHandler<CreateAnalyticsBucketForm> = async (values) => {
|
||
if (!ref) return console.error('Project ref is required')
|
||
if (!project) return console.error('Project details is required')
|
||
if (!wrappersExtension) return console.error('Unable to find wrappers extension')
|
||
|
||
const hasExistingBucket = buckets.some((x) => x.name === values.name)
|
||
if (hasExistingBucket) return toast.error('Bucket name already exists')
|
||
|
||
try {
|
||
await createAnalyticsBucket({
|
||
projectRef: ref,
|
||
bucketName: values.name,
|
||
})
|
||
|
||
if (wrappersExtensionState === 'not-installed') {
|
||
await enableExtension({
|
||
projectRef: project?.ref,
|
||
connectionString: project?.connectionString,
|
||
name: wrappersExtension.name,
|
||
schema: wrappersExtension.schema ?? 'extensions',
|
||
version: wrappersExtension.default_version,
|
||
})
|
||
}
|
||
|
||
await createIcebergWrapper({ bucketName: values.name })
|
||
|
||
track('storage_bucket_created', { bucketType: 'analytics' })
|
||
|
||
form.reset()
|
||
toast.success(`Created bucket “${values.name}”`)
|
||
onOpenChange(false)
|
||
} catch (error: any) {
|
||
toast.error(`Failed to create bucket: ${error.message}`)
|
||
}
|
||
}
|
||
|
||
const Section = type === 'dialog' ? DialogSection : SheetSection
|
||
const Footer = type === 'dialog' ? DialogFooter : SheetFooter
|
||
|
||
return (
|
||
<>
|
||
<Section className="flex flex-col p-0! grow">
|
||
<Form {...form}>
|
||
<form id={formId} onSubmit={form.handleSubmit(onSubmit)}>
|
||
<FormField
|
||
key="name"
|
||
name="name"
|
||
control={form.control}
|
||
render={({ field }) => (
|
||
<FormItemLayout
|
||
name="name"
|
||
className="p-5"
|
||
label="Bucket name"
|
||
labelOptional="Cannot be changed after creation"
|
||
description="Must be between 3 – 63 characters. Only lowercase letters, numbers, and hyphens are allowed."
|
||
>
|
||
<FormControl>
|
||
<Input
|
||
id="name"
|
||
data-1p-ignore
|
||
data-lpignore="true"
|
||
data-form-type="other"
|
||
data-bwignore
|
||
{...field}
|
||
placeholder="Enter bucket name"
|
||
/>
|
||
</FormControl>
|
||
</FormItemLayout>
|
||
)}
|
||
/>
|
||
|
||
{wrappersExtensionNeedsUpgrading ? (
|
||
<Admonition
|
||
type="warning"
|
||
className={cn('border-x-0 rounded-none', type === 'dialog' && 'border-b-0')}
|
||
title="Wrappers extension must be updated for Iceberg Wrapper support"
|
||
>
|
||
<p className="prose max-w-full text-sm leading-normal!">
|
||
Update the <code className="text-code-inline">wrappers</code> extension by
|
||
upgrading your project from your{' '}
|
||
<InlineLink href={getServiceVersionsPath(ref)}>project settings</InlineLink>{' '}
|
||
before creating an Analytics bucket.{' '}
|
||
<InlineLink href={`${DOCS_URL}/guides/database/extensions/wrappers/iceberg`}>
|
||
Learn more
|
||
</InlineLink>
|
||
.
|
||
</p>
|
||
</Admonition>
|
||
) : (
|
||
<Admonition
|
||
type="default"
|
||
className={cn('border-x-0 rounded-none', type === 'dialog' && 'border-b-0')}
|
||
>
|
||
<p className="leading-normal!">
|
||
Supabase will install the{' '}
|
||
{wrappersExtensionState !== 'installed' ? 'Wrappers extension and ' : ''}
|
||
Iceberg Wrapper integration on your behalf.{' '}
|
||
<InlineLink href={`${DOCS_URL}/guides/database/extensions/wrappers/iceberg`}>
|
||
Learn more
|
||
</InlineLink>
|
||
.
|
||
</p>
|
||
</Admonition>
|
||
)}
|
||
</form>
|
||
</Form>
|
||
</Section>
|
||
|
||
<Footer>
|
||
<Button disabled={isCreating} onClick={() => onOpenChange(false)}>
|
||
Cancel
|
||
</Button>
|
||
<Button
|
||
variant="primary"
|
||
form={formId}
|
||
type="submit"
|
||
loading={isCreating}
|
||
disabled={wrappersExtensionNeedsUpgrading || isCreating}
|
||
>
|
||
Create bucket
|
||
</Button>
|
||
</Footer>
|
||
</>
|
||
)
|
||
}
|