mirror of
https://github.com/supabase/supabase.git
synced 2026-10-08 19:05:06 +03:00
## What kind of change does this PR introduce? Bug fix / UX polish. ## What is the current behavior? Cancel on the Vercel create interstitial redirects to Vercel’s `next` URL. That finishes the install flow and shows “Completing installation…” / “Installation complete”, even though the user cancelled. The Deploy Button job still proceeds and fails. The button was also labelled “Return to Vercel”, which read like an alternate success path. | Before | | --- | | <img width="719" height="118" alt="Create Vercel Project Supabase" src="https://github.com/user-attachments/assets/c7ad261a-f132-4226-b460-21ec267595c0" />| | <img width="800" height="599" alt="95801" src="https://github.com/user-attachments/assets/6d235c4b-be90-4d5e-89bc-a68933f9dc46" /> | ## What is the new behavior? - _Return to Vercel_ button now labelled **Cancel** - **Cancel** closes the popup via `window.close()` (same honest abort as manually closing the window) - If the browser blocks programmatic close, Cancel is replaced by muted fallback copy: “Close window to cancel” (`role="status"` / `aria-live="polite"`) - Cancel is left-aligned with `justify-between` when there’s no additional-costs block; with costs, both actions stay on the right - Removes the free-limit hint line about returning to Vercel (Cancel makes that self-evident) - Success path is unchanged: after create, we still redirect via `next` - `/new` Cancel still navigates into Studio (`cancelAction: 'studio'`). This is unchanged behaviour | After (Fallback) | | --- | | <img width="713" height="103" alt="img" src="https://github.com/user-attachments/assets/95aafc0d-1c3d-4093-9681-88ad0a40f9fe" /> | ## Additional context Follow-up to #48311. Vercel’s `next` URL has no documented cancel/abort status, so closing the popup is the correct escape hatch. ### To test Full Deploy Button popup cancel can’t be verified on prod until this merges. Locally: 1. Open Studio on this branch. 2. In `ProjectCreationFooter.tsx`, temporarily force the fallback: ```ts const [showCloseWindowHint, setShowCloseWindowHint] = useState(true) ``` 3. Load create-project UI: - Free org / no costs: Confirm Cancel is left, Create is right (`justify-between`). With the forced hint, Cancel is replaced by “Close window to cancel”. - Paid org with additional costs: Confirm costs on the left; hint/Create on the right. 4. Revert `useState` to `false`. 5. Optional: open a Vercel create interstitial URL in a normal tab (not a popup), click Cancel. `window.close()` fails and the hint should replace Cancel after ~100ms. 6. On `/new/[slug]`, Confirm Cancel still returns to the Studio dashboard (not close-window). <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit ## Summary by CodeRabbit * **New Features** * Updated Project Creation Cancel to close the popup window directly when applicable, with a “close window to cancel” hint when closing isn’t available. * **Bug Fixes** * Removed Vercel-specific “return and restart” messaging and related return-url handling. * Standardized Cancel navigation for non-popup flows to return to the last relevant location (or the organizations page). * **Refactor** * Improved Project Creation footer layout/visibility for additional cost details. * **Chores** * Simplified the free-project limit warning configuration by removing an unused option. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Joshen Lim <joshenlimek@gmail.com>
223 lines
8.6 KiB
TypeScript
223 lines
8.6 KiB
TypeScript
import { useFlag } from 'common'
|
|
import { useRouter } from 'next/router'
|
|
import { useEffect, useState } from 'react'
|
|
import { UseFormReturn } from 'react-hook-form'
|
|
import {
|
|
Badge,
|
|
Button,
|
|
cn,
|
|
PopoverSeparator,
|
|
Table,
|
|
TableBody,
|
|
TableCell,
|
|
TableHead,
|
|
TableHeader,
|
|
TableRow,
|
|
} from 'ui'
|
|
import { InfoTooltip } from 'ui-patterns/info-tooltip'
|
|
|
|
import { CreateProjectForm } from './ProjectCreation.schema'
|
|
import { instanceLabel, monthlyInstancePrice } from './ProjectCreation.utils'
|
|
import { InlineLink } from '@/components/ui/InlineLink'
|
|
import { OrgProject } from '@/data/projects/org-projects-infinite-query'
|
|
import { useLastVisitedOrganization } from '@/hooks/misc/useLastVisitedOrganization'
|
|
import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization'
|
|
import { DOCS_URL } from '@/lib/constants'
|
|
|
|
/** `close` = close the popup (Vercel interstitial); `studio` = navigate into Studio. */
|
|
export type ProjectCreationCancelAction = 'studio' | 'close' | 'hidden'
|
|
|
|
interface ProjectCreationFooterProps {
|
|
form: UseFormReturn<CreateProjectForm>
|
|
canCreateProject: boolean
|
|
instanceSize?: string
|
|
organizationProjects: OrgProject[]
|
|
isCreatingNewProject: boolean
|
|
isSuccessNewProject: boolean
|
|
cancelAction?: ProjectCreationCancelAction
|
|
}
|
|
|
|
export const ProjectCreationFooter = ({
|
|
form,
|
|
canCreateProject,
|
|
instanceSize,
|
|
organizationProjects,
|
|
isCreatingNewProject,
|
|
isSuccessNewProject,
|
|
cancelAction = 'studio',
|
|
}: ProjectCreationFooterProps) => {
|
|
const router = useRouter()
|
|
const { data: currentOrg } = useSelectedOrganizationQuery()
|
|
const isFreePlan = currentOrg?.plan?.id === 'free'
|
|
const { lastVisitedOrganization } = useLastVisitedOrganization()
|
|
const [showCloseWindowHint, setShowCloseWindowHint] = useState(false)
|
|
|
|
const projectCreationDisabled = useFlag('disableProjectCreationAndUpdate')
|
|
|
|
const availableComputeCredits = organizationProjects.length === 0 ? 10 : 0
|
|
const additionalMonthlySpend = isFreePlan
|
|
? 0
|
|
: monthlyInstancePrice(instanceSize) - availableComputeCredits
|
|
|
|
const showAdditionalCosts =
|
|
!isFreePlan && !projectCreationDisabled && canCreateProject && additionalMonthlySpend > 0
|
|
|
|
// [kevin] This will eventually all be provided by a new API endpoint to preview and validate project creation, this is just for kaizen now
|
|
const monthlyComputeCosts =
|
|
// current project costs
|
|
organizationProjects.reduce((prev, acc) => {
|
|
const primaryDatabase = acc.databases.find((db) => db.identifier === acc.ref)
|
|
const cost = !!primaryDatabase ? monthlyInstancePrice(primaryDatabase.infra_compute_size) : 0
|
|
return prev + cost
|
|
}, 0) +
|
|
// selected compute size
|
|
monthlyInstancePrice(instanceSize) -
|
|
// compute credits
|
|
10
|
|
|
|
const onCancel = () => {
|
|
if (cancelAction === 'close') {
|
|
window.close()
|
|
return
|
|
}
|
|
|
|
if (!!lastVisitedOrganization) router.push(`/org/${lastVisitedOrganization}`)
|
|
else router.push('/organizations')
|
|
}
|
|
|
|
useEffect(() => {
|
|
// Browsers only allow closing windows that were opened by script (i.e. have a live `opener`).
|
|
// Detect this upfront so we don't need to attempt-and-check after the user clicks cancel.
|
|
if (cancelAction === 'close' && !window.opener) setShowCloseWindowHint(true)
|
|
}, [cancelAction])
|
|
|
|
return (
|
|
<div key="panel-footer" className="grid grid-cols-12 w-full gap-4 items-center">
|
|
{showAdditionalCosts && (
|
|
<div className="col-span-4">
|
|
<div className="flex justify-between text-sm">
|
|
<span>Additional costs</span>
|
|
<div className="text-brand flex gap-1 items-center font-mono font-medium">
|
|
<span>${additionalMonthlySpend}/m</span>
|
|
<InfoTooltip side="top" className="max-w-[450px] p-0">
|
|
<div className="p-4 text-sm text-foreground-light space-y-1">
|
|
<p>
|
|
Each project includes a dedicated Postgres instance running on its own server.
|
|
You are charged for the{' '}
|
|
<InlineLink href={`${DOCS_URL}/guides/platform/billing-on-supabase`}>
|
|
Compute resource
|
|
</InlineLink>{' '}
|
|
of that server, independent of your database usage.
|
|
</p>
|
|
{monthlyComputeCosts > 0 && (
|
|
<p>Compute costs are applied on top of your subscription plan costs.</p>
|
|
)}
|
|
</div>
|
|
|
|
<Table className="mt-2">
|
|
<TableHeader className="[&_th]:h-7">
|
|
<TableRow className="py-2">
|
|
<TableHead className="w-[170px]">Project</TableHead>
|
|
<TableHead>Compute Size</TableHead>
|
|
<TableHead className="text-right">Monthly Costs</TableHead>
|
|
</TableRow>
|
|
</TableHeader>
|
|
<TableBody className="[&_td]:py-2">
|
|
{organizationProjects.map((project) => {
|
|
const primaryDb = project.databases.find(
|
|
(db) => db.identifier === project.ref
|
|
)
|
|
return (
|
|
<TableRow key={project.ref} className="text-foreground-light">
|
|
<TableCell className="w-[170px] truncate">{project.name}</TableCell>
|
|
<TableCell className="text-center">
|
|
{instanceLabel(primaryDb?.infra_compute_size)}
|
|
</TableCell>
|
|
<TableCell className="text-right">
|
|
${monthlyInstancePrice(primaryDb?.infra_compute_size)}
|
|
</TableCell>
|
|
</TableRow>
|
|
)
|
|
})}
|
|
|
|
<TableRow>
|
|
<TableCell className="w-[170px] flex gap-2">
|
|
<span className="truncate">
|
|
{form.getValues('projectName') || 'New project'}
|
|
</span>
|
|
<Badge variant="success">New</Badge>
|
|
</TableCell>
|
|
<TableCell className="text-center">{instanceLabel(instanceSize)}</TableCell>
|
|
<TableCell className="text-right">
|
|
${monthlyInstancePrice(instanceSize)}
|
|
</TableCell>
|
|
</TableRow>
|
|
</TableBody>
|
|
</Table>
|
|
<PopoverSeparator />
|
|
<Table>
|
|
<TableHeader className="[&_th]:h-7">
|
|
<TableRow>
|
|
<TableHead colSpan={2}>Compute Credits</TableHead>
|
|
<TableHead colSpan={1} className="text-right">
|
|
-$10
|
|
</TableHead>
|
|
</TableRow>
|
|
</TableHeader>
|
|
<TableBody className="[&_td]:py-2">
|
|
<TableRow className="text-foreground">
|
|
<TableCell colSpan={2}>
|
|
Total Monthly Compute Costs
|
|
{/**
|
|
* API currently doesnt output replica information on the projects list endpoint. Until then, we cannot correctly calculate the costs including RRs.
|
|
* Will be adjusted in the future [kevin]
|
|
*/}
|
|
{organizationProjects.length > 0 && (
|
|
<p className="text-xs text-foreground-lighter">Excluding Read replicas</p>
|
|
)}
|
|
</TableCell>
|
|
<TableCell colSpan={1} className="text-right">
|
|
${monthlyComputeCosts}
|
|
</TableCell>
|
|
</TableRow>
|
|
</TableBody>
|
|
</Table>
|
|
</InfoTooltip>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
<div
|
|
className={cn(
|
|
showAdditionalCosts ? 'col-span-8' : 'col-span-12',
|
|
'flex items-center gap-x-2 ml-auto'
|
|
)}
|
|
>
|
|
{cancelAction === 'hidden' ? null : showCloseWindowHint ? (
|
|
<p role="status" aria-live="polite" className="text-xs text-foreground-muted mr-3">
|
|
Close window to cancel
|
|
</p>
|
|
) : (
|
|
<Button
|
|
type="button"
|
|
variant="default"
|
|
disabled={isCreatingNewProject || isSuccessNewProject}
|
|
onClick={onCancel}
|
|
>
|
|
Cancel
|
|
</Button>
|
|
)}
|
|
|
|
<Button
|
|
type="submit"
|
|
loading={isCreatingNewProject || isSuccessNewProject}
|
|
disabled={!canCreateProject}
|
|
>
|
|
Create new project
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|