Files
supabase/apps/studio/components/interfaces/Settings/General/TransferProjectPanel/TransferProjectButton.tsx
Joshen Lim 6b741bc964 Replace ui setnotification with toast midway (#21867)
Replace ui setnotification with toast
2024-03-08 15:46:52 +08:00

369 lines
14 KiB
TypeScript

import * as Tooltip from '@radix-ui/react-tooltip'
import { PermissionAction } from '@supabase/shared-types/out/constants'
import Link from 'next/link'
import { useEffect, useState } from 'react'
import toast from 'react-hot-toast'
import {
Alert,
Button,
IconAlertCircle,
IconLoader,
IconShield,
IconTool,
IconUsers,
Listbox,
Loading,
Modal,
} from 'ui'
import { useOrganizationsQuery } from 'data/organizations/organizations-query'
import { useProjectTransferMutation } from 'data/projects/project-transfer-mutation'
import { useProjectTransferPreviewQuery } from 'data/projects/project-transfer-preview-query'
import { useCheckPermissions, useFlag, useSelectedProject } from 'hooks'
import { formatCurrency } from 'lib/helpers'
const TransferProjectButton = () => {
const project = useSelectedProject()
const projectRef = project?.ref
const projectOrgId = project?.organization_id
const { data: allOrganizations } = useOrganizationsQuery()
const disableProjectTransfer = useFlag('disableProjectTransfer')
const organizations = (allOrganizations || []).filter((it) => it.id !== projectOrgId)
const [isOpen, setIsOpen] = useState(false)
const [selectedOrg, setSelectedOrg] = useState()
const {
mutate: transferProject,
error: transferError,
isLoading: isTransferring,
} = useProjectTransferMutation({
onSuccess: () => {
toast.success(`Successfully transferred project ${project?.name}.`)
setIsOpen(false)
},
})
const {
data: transferPreviewData,
error: transferPreviewError,
isLoading: transferPreviewIsLoading,
remove,
} = useProjectTransferPreviewQuery(
{ projectRef, targetOrganizationSlug: selectedOrg },
{ enabled: !isTransferring && isOpen }
)
useEffect(() => {
if (isOpen) {
// reset state
setSelectedOrg(undefined)
} else {
// Invalidate cache
remove()
}
}, [isOpen])
const canTransferProject = useCheckPermissions(PermissionAction.UPDATE, 'organizations')
const toggle = () => {
setIsOpen(!isOpen)
}
async function handleTransferProject() {
if (project === undefined) return
if (selectedOrg === undefined) return
transferProject({ projectRef, targetOrganizationSlug: selectedOrg })
}
return (
<>
<Tooltip.Root delayDuration={0}>
<Tooltip.Trigger asChild>
<Button
onClick={toggle}
type="default"
disabled={!canTransferProject || disableProjectTransfer}
>
Transfer project
</Button>
</Tooltip.Trigger>
{(!canTransferProject || disableProjectTransfer) && (
<Tooltip.Portal>
<Tooltip.Content side="bottom">
<Tooltip.Arrow className="radix-tooltip-arrow" />
<div
className={[
'rounded bg-alternative py-1 px-2 leading-none shadow', // background
'border border-background', //border
].join(' ')}
>
<span className="text-xs text-foreground">
{!canTransferProject
? 'You need additional permissions to transfer this project'
: 'Project transfers are temporarily disabled, please try again later.'}
</span>
</div>
</Tooltip.Content>
</Tooltip.Portal>
)}
</Tooltip.Root>
<Modal
closable
onCancel={() => toggle()}
visible={isOpen}
loading={isTransferring}
size={'xlarge'}
header={`Transfer project ${project?.name}`}
customFooter={
<div className="flex items-center space-x-2 justify-end">
<Button type="default" onClick={() => setIsOpen(false)}>
Cancel
</Button>
<Button
onClick={() => handleTransferProject()}
disabled={
!transferPreviewData || !transferPreviewData.valid || isTransferring || !selectedOrg
}
>
Transfer Project
</Button>
</div>
}
>
<div className="space-y-4 py-4 text-foreground-light">
<Modal.Content>
<p className="text-sm">
To transfer projects, the owner must be a member of both the source and target
organizations. For further information see our{' '}
<Link
href="https://supabase.com/docs/guides/platform/project-transfer"
className="text-brand hover:underline"
target="_blank"
rel="noreferrer"
>
Documentation
</Link>
.
</p>
<p className="font-bold mt-6 text-sm">Transferring considerations:</p>
<ul className="mt-4 space-y-5 text-sm px-4">
<li className="flex gap-4">
<span className="shrink-0 mt-1">
<IconLoader />
</span>
<div>
<p className="font-bold">No downtime</p>
<p>There is no downtime or restrictions involved when transferring a project.</p>
</div>
</li>
<li className="flex gap-4">
<span className="shrink-0 mt-1">
<IconShield />
</span>
<div>
<p className="font-bold">Permissions</p>
<p>
Depending on your role in the target organization, your level of permissions may
change after transfer.
</p>
</div>
</li>
<li className="flex gap-4">
<span className="shrink-0 mt-1">
<IconTool w={14} className="flex-shrink-0" />
</span>
<div>
<p className="font-bold">Features</p>
<p>
Moving your project to an organization with a smaller subscription plan may
result in the loss of certain features (i.e. image transformations).
</p>
</div>
</li>
</ul>
</Modal.Content>
<Modal.Content>
{organizations && (
<div className="mt-8 mx-4 border-t pt-4 space-y-2">
{organizations.length === 0 ? (
<div className="flex items-center gap-2 bg-surface-200 p-3 text-sm">
<IconAlertCircle /> You do not have any organizations with an organization-based
subscription.
</div>
) : (
<Listbox
label="Select Target Organization"
layout="vertical"
value={selectedOrg}
onChange={(slug) => setSelectedOrg(slug)}
placeholder="Select Organization"
>
<Listbox.Option disabled key="no-results" label="Select Organization" value="">
Select Organization
</Listbox.Option>
{organizations.map((x: any) => (
<Listbox.Option
key={x.id}
label={x.name}
value={x.slug}
addOnBefore={() => <IconUsers />}
>
{x.name}
</Listbox.Option>
))}
</Listbox>
)}
<p className="text-foreground-light text-sm">
The target organization needs to use{' '}
<Link
href="https://supabase.com/docs/guides/platform/org-based-billing"
target="_blank"
rel="noreferrer"
className="underline"
>
organization-based billing
</Link>
. To migrate an organization to the new billing, head to your{' '}
<Link
href="/org/_/billing"
target="_blank"
rel="noreferrer"
className="underline"
>
organizations billing settings
</Link>
.
</p>
</div>
)}
</Modal.Content>
<Loading active={selectedOrg !== undefined && transferPreviewIsLoading}>
<Modal.Content>
<div className="px-4">
{transferPreviewData && transferPreviewData.valid && (
<div className="text-sm text-foreground-light p-4 bg-surface-200">
{transferPreviewData.source_subscription_plan !==
transferPreviewData.target_subscription_plan ? (
<div>
<p>
Your project is currently on the{' '}
{transferPreviewData.source_subscription_plan} plan, whereas the target
organization uses the {transferPreviewData.target_subscription_plan} plan.
</p>
</div>
) : (
<div>
Your project and the target organization are both on the{' '}
{transferPreviewData.source_subscription_plan} subscription plan.
</div>
)}
<div className="my-4">
{transferPreviewData.credits_on_source_organization === 0 ? (
<span>
{' '}
Your current organization won't be granted any prorated credits.
</span>
) : (
<span>
{' '}
Your current organization will be granted{' '}
<span className="text-brand">
${transferPreviewData.credits_on_source_organization}
</span>{' '}
in credits as proration.
</span>
)}
{transferPreviewData.costs_on_target_organization === 0 ? (
<span>
{' '}
The target organization won't be charged any immediate upfront payment.
</span>
) : (
<span>
{' '}
The target organization will be billed{' '}
<span className="text-brand">
{formatCurrency(transferPreviewData.costs_on_target_organization)}
</span>{' '}
immediately to prorate for the remainder of the billing period.
</span>
)}
</div>
</div>
)}
{transferPreviewData && transferPreviewData.warnings.length > 0 && (
<Alert
withIcon
variant="warning"
title="Warnings for project transfer"
className="mt-3"
>
<div className="space-y-1">
{transferPreviewData.warnings.map((warning) => (
<p key={warning.key}>{warning.message}</p>
))}
</div>
</Alert>
)}
{transferPreviewData && transferPreviewData.errors.length > 0 && (
<Alert withIcon variant="danger" title="Project cannot be transferred">
<div className="space-y-1">
{transferPreviewData.errors.map((error) => (
<p key={error.key}>{error.message}</p>
))}
</div>
{transferPreviewData.members_exceeding_free_project_limit.length > 0 && (
<div className="space-y-2">
<p className="text-sm text-foreground-light">
These members have reached their maximum limits for the number of active
Free plan projects within organizations where they are an administrator or
owner:
</p>
<ul className="pl-5 text-sm list-disc text-foreground-light">
{(transferPreviewData.members_exceeding_free_project_limit || []).map(
(member, idx: number) => (
<li key={`member-${idx}`}>
{member.name} (Limit: {member.limit} free projects)
</li>
)
)}
</ul>
<p className="text-sm text-foreground-light">
These members will need to either delete, pause, or upgrade one or more of
their projects before you can downgrade this project.
</p>
</div>
)}
</Alert>
)}
{transferPreviewError && !transferError && (
<Alert withIcon variant="danger" title="Project cannot be transferred">
<p>{transferPreviewError.message}</p>
</Alert>
)}
{transferError && (
<Alert withIcon variant="danger" title="Project cannot be transferred">
<p>{transferError.message}</p>
</Alert>
)}
</div>
</Modal.Content>
</Loading>
</div>
</Modal>
</>
)
}
export default TransferProjectButton