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
252 lines
8.3 KiB
TypeScript
252 lines
8.3 KiB
TypeScript
import { zodResolver } from '@hookform/resolvers/zod'
|
|
import { PropsWithChildren, useState } from 'react'
|
|
import { SubmitHandler, useForm } from 'react-hook-form'
|
|
import { toast } from 'sonner'
|
|
import {
|
|
Badge,
|
|
Button,
|
|
Dialog,
|
|
DialogContent,
|
|
DialogDescription,
|
|
DialogFooter,
|
|
DialogHeader,
|
|
DialogSection,
|
|
DialogSectionSeparator,
|
|
DialogTitle,
|
|
DialogTrigger,
|
|
Form,
|
|
FormControl,
|
|
FormField,
|
|
TextArea,
|
|
Tooltip,
|
|
TooltipContent,
|
|
TooltipTrigger,
|
|
} from 'ui'
|
|
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
|
|
import z from 'zod'
|
|
|
|
import { useOrganizationRolesV2Query } from '@/data/organization-members/organization-roles-query'
|
|
import { useOrganizationMembersQuery } from '@/data/organizations/organization-members-query'
|
|
import {
|
|
PlanRequest,
|
|
useSendUpgradeRequestMutation,
|
|
} from '@/data/organizations/request-upgrade-mutation'
|
|
import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization'
|
|
import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
|
|
import { useTrack } from '@/lib/telemetry/track'
|
|
|
|
const FormSchema = z.object({
|
|
note: z.string().optional(),
|
|
})
|
|
|
|
const formId = 'request-upgrade-form'
|
|
|
|
interface RequestUpgradeToBillingOwnersProps {
|
|
block?: boolean
|
|
plan?: PlanRequest
|
|
addon?: 'pitr' | 'customDomain' | 'ipv4' | 'spendCap' | 'computeSize'
|
|
/** Used in the default message template, e.g: "Upgrade to ..." */
|
|
featureProposition?: string
|
|
className?: string
|
|
variant?: 'primary' | 'default'
|
|
}
|
|
|
|
export const RequestUpgradeToBillingOwners = ({
|
|
block = false,
|
|
plan = 'Pro',
|
|
addon,
|
|
featureProposition,
|
|
children,
|
|
className,
|
|
variant = 'primary',
|
|
}: PropsWithChildren<RequestUpgradeToBillingOwnersProps>) => {
|
|
const [open, setOpen] = useState(false)
|
|
const track = useTrack()
|
|
const { data: project } = useSelectedProjectQuery()
|
|
const { data: organization } = useSelectedOrganizationQuery()
|
|
const slug = organization?.slug
|
|
const currentPlan = organization?.plan?.id
|
|
const isFreePlan = currentPlan === 'free'
|
|
|
|
const { data: members = [] } = useOrganizationMembersQuery({ slug: organization?.slug })
|
|
const { data: roles } = useOrganizationRolesV2Query({ slug: organization?.slug })
|
|
const orgRoles = roles?.org_scoped_roles ?? []
|
|
|
|
const { mutate: sendUpgradeRequest, isPending: isSubmitting } = useSendUpgradeRequestMutation({
|
|
onSuccess: () => {
|
|
track('request_upgrade_submitted', {
|
|
requestedPlan: plan,
|
|
addon,
|
|
currentPlan,
|
|
})
|
|
toast.success('Successfully sent request to billing owners!')
|
|
setOpen(false)
|
|
},
|
|
})
|
|
|
|
const formattedAddonName =
|
|
addon === 'pitr'
|
|
? 'PITR'
|
|
: addon === 'customDomain'
|
|
? 'Custom domain'
|
|
: addon === 'ipv4'
|
|
? 'dedicated IPv4 address'
|
|
: ''
|
|
|
|
const target = !!project
|
|
? `for the project "${project?.name}"`
|
|
: !!organization
|
|
? `for the organization "${organization.name}"`
|
|
: ''
|
|
const action =
|
|
addon === 'spendCap'
|
|
? `disable spend cap`
|
|
: addon === 'computeSize'
|
|
? `change the compute size`
|
|
: `enable the ${formattedAddonName} add-on`
|
|
const titleText = !!addon
|
|
? addon === 'spendCap'
|
|
? `Request to disable spend cap`
|
|
: addon === 'computeSize'
|
|
? 'Request to change compute size'
|
|
: `Request to enable the ${formattedAddonName} add-on`
|
|
: `Request an upgrade for the ${plan} Plan`
|
|
const buttonText = !!children
|
|
? children
|
|
: !!addon
|
|
? addon === 'spendCap'
|
|
? 'Request to disable spend cap'
|
|
: addon === 'computeSize'
|
|
? 'Request to change compute'
|
|
: 'Request to enable addon'
|
|
: `Request upgrade to ${plan}`
|
|
|
|
const defaultValues = {
|
|
note: !!addon
|
|
? `We'd like to ${isFreePlan ? 'upgrade to Pro and ' : ''}${action} ${target} so that we can ${featureProposition}`
|
|
: `We'd like to upgrade to the ${plan} plan ${!!featureProposition ? `to ${featureProposition} ` : ''}${target}`,
|
|
}
|
|
const form = useForm<z.infer<typeof FormSchema>>({
|
|
resolver: zodResolver(FormSchema),
|
|
defaultValues,
|
|
values: defaultValues,
|
|
})
|
|
|
|
// [Joshen] This is a pretty naive way of checking billing owners by raw role names
|
|
// Ideally we derive billing owners using permissions checking - but the current permissions
|
|
// logic is only contextualized to that of the current user, not other members
|
|
const billingOwners = members.filter((member) => {
|
|
const roles = member.role_ids
|
|
.map((x) => orgRoles.find((role) => role.id === x)?.name)
|
|
.filter(Boolean)
|
|
return !member.invited_id && (roles.includes('Owner') || roles.includes('Administrator'))
|
|
})
|
|
|
|
const onSubmit: SubmitHandler<z.infer<typeof FormSchema>> = async (values) => {
|
|
if (!slug) return console.error('Slug is required')
|
|
sendUpgradeRequest({ slug, plan, note: values.note })
|
|
}
|
|
|
|
const handleOpenChange = (isOpen: boolean) => {
|
|
if (isOpen) {
|
|
track('request_upgrade_modal_opened', {
|
|
requestedPlan: plan,
|
|
addon,
|
|
currentPlan,
|
|
featureProposition,
|
|
})
|
|
}
|
|
setOpen(isOpen)
|
|
}
|
|
|
|
return (
|
|
<Dialog open={open} onOpenChange={handleOpenChange}>
|
|
<DialogTrigger asChild>
|
|
<Button block={block} variant={variant} className={className}>
|
|
{buttonText}
|
|
</Button>
|
|
</DialogTrigger>
|
|
<DialogContent>
|
|
<Form {...form}>
|
|
<form id={formId} onSubmit={form.handleSubmit(onSubmit)}>
|
|
<DialogHeader>
|
|
<DialogTitle>{titleText}</DialogTitle>
|
|
<DialogDescription>
|
|
Let your organization's billing owners know your interest in this
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
|
|
<DialogSectionSeparator />
|
|
|
|
<DialogSection className="flex flex-col gap-y-6">
|
|
<div className="flex flex-col gap-y-2">
|
|
<p className="text-sm">
|
|
Your request will be sent to the following emails, who are billing owners of your
|
|
organization:
|
|
</p>
|
|
<div className="text-sm flex gap-x-2">
|
|
<p>
|
|
{billingOwners
|
|
.slice(0, 2)
|
|
.map((x) => x.primary_email)
|
|
.join(', ')}
|
|
</p>
|
|
{billingOwners.length > 2 && (
|
|
<Tooltip>
|
|
<TooltipTrigger tabIndex={-1}>
|
|
<Badge>+1 others</Badge>
|
|
</TooltipTrigger>
|
|
<TooltipContent side="bottom">
|
|
<ul className="">
|
|
{billingOwners.slice(2).map((x) => (
|
|
<li key={x.gotrue_id}>{x.primary_email}</li>
|
|
))}
|
|
</ul>
|
|
</TooltipContent>
|
|
</Tooltip>
|
|
)}
|
|
</div>
|
|
</div>
|
|
<FormField
|
|
control={form.control}
|
|
name="note"
|
|
render={({ field }) => (
|
|
<FormItemLayout
|
|
name="note"
|
|
label="Add a note to your request (optional)"
|
|
layout="vertical"
|
|
>
|
|
<FormControl>
|
|
<TextArea
|
|
id="note"
|
|
{...field}
|
|
rows={3}
|
|
placeholder={
|
|
!!addon
|
|
? addon === 'spendCap'
|
|
? 'e.g. We need to disabled spend cap on this project to do something'
|
|
: 'e.g. We need to enable this add-on to do something with the project'
|
|
: 'e.g. We need to upgrade to the Pro plan to use this feature'
|
|
}
|
|
/>
|
|
</FormControl>
|
|
</FormItemLayout>
|
|
)}
|
|
/>
|
|
</DialogSection>
|
|
|
|
<DialogFooter>
|
|
<Button disabled={isSubmitting} onClick={() => setOpen(false)}>
|
|
Cancel
|
|
</Button>
|
|
<Button variant="primary" type="submit" form={formId} loading={isSubmitting}>
|
|
Submit request
|
|
</Button>
|
|
</DialogFooter>
|
|
</form>
|
|
</Form>
|
|
</DialogContent>
|
|
</Dialog>
|
|
)
|
|
}
|