mirror of
https://github.com/supabase/supabase.git
synced 2026-10-07 10:25:06 +03:00
## What kind of change does this PR introduce? Bug fix / design-system alignment for the legacy `Button` from `ui`. ## What is the current behavior? Omitting `variant` on the legacy `Button` falls back to brand-green `primary`. That makes accidental greens easy, and it is hard to spot the real main action on busy pages. ## What is the new behavior? - Legacy `Button` now defaults to neutral `default` - Intentional primary CTAs (create, save, submit, marketing CTAs, and matching `ButtonTooltip` usages) now set `variant="primary"` so their appearance is unchanged - Neutral actions that previously relied on the old fallback (cancel, close, back, dashboard nav, and similar) become grey/white - Design-system docs updated; regression tests cover the new default `Button_Shadcn_` is unchanged. It already uses its own CVA default. This is PR 1 of 2 in a stack. PR 2 drops now-redundant `variant="default"` props. ## To test Studio (http://localhost:8082): - `/sign-in`: Sign in stays green - Open a project → Database → Tables: New table stays green - Auth → Users → Invite: Invite user stays green; Cancel / dismiss controls stay neutral - Project Settings → General: edit a field so Cancel and Save appear. Cancel is neutral, Save is green Design system (http://localhost:3003): - Components → Button: default demo is neutral; primary demo is green; featured preview is the default variant Marketing (optional): - www header: Start your project stays green; logged-in Dashboard is neutral <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **Style** - Buttons now default to a neutral style, while primary actions across Studio, documentation, marketing pages, forms, dialogs, and error states use prominent primary styling. - Updated button examples and previews clarify the distinction between default and primary variants. - Event registration now includes a directional arrow icon. - **Tests** - Added coverage confirming default button styling and explicit primary styling behave as expected. - Updated related test fixtures to use primary styling where appropriate. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
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 variant="default" disabled={isCreating} onClick={() => onOpenChange(false)}>
|
||
Cancel
|
||
</Button>
|
||
<Button
|
||
variant="primary"
|
||
form={formId}
|
||
type="submit"
|
||
loading={isCreating}
|
||
disabled={wrappersExtensionNeedsUpgrading || isCreating}
|
||
>
|
||
Create bucket
|
||
</Button>
|
||
</Footer>
|
||
</>
|
||
)
|
||
}
|