| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302 |
- import { PermissionAction } from '@supabase/shared-types/out/constants'
- import { useQueryClient } from '@tanstack/react-query'
- import { useFlag } from 'common'
- import { Loader, Shield, Wrench } from 'lucide-react'
- import { useEffect, useState } from 'react'
- import { toast } from 'sonner'
- import {
- Button,
- InfoIcon,
- Loading,
- Modal,
- Select,
- SelectContent,
- SelectItem,
- SelectTrigger,
- SelectValue,
- WarningIcon,
- } from 'ui'
- import { Admonition } from 'ui-patterns'
- import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
- import { ButtonTooltip } from '@/components/ui/ButtonTooltip'
- import { DocsButton } from '@/components/ui/DocsButton'
- import { useOrganizationsQuery } from '@/data/organizations/organizations-query'
- import { projectKeys } from '@/data/projects/keys'
- import { useProjectTransferMutation } from '@/data/projects/project-transfer-mutation'
- import { useProjectTransferPreviewQuery } from '@/data/projects/project-transfer-preview-query'
- import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions'
- import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
- import { DOCS_URL } from '@/lib/constants'
- export const TransferProjectButton = () => {
- const { data: project } = useSelectedProjectQuery()
- const projectRef = project?.ref
- const projectOrgId = project?.organization_id
- const [isOpen, setIsOpen] = useState(false)
- const { data: allOrganizations } = useOrganizationsQuery({ enabled: isOpen })
- const disableProjectTransfer = useFlag('disableProjectTransfer')
- const organizations = (allOrganizations || []).filter((it) => it.id !== projectOrgId)
- const [selectedOrg, setSelectedOrg] = useState<string>()
- const {
- mutate: transferProject,
- error: transferError,
- isPending: isTransferring,
- } = useProjectTransferMutation({
- onSuccess: () => {
- toast.success(`Successfully transferred project ${project?.name}.`)
- setIsOpen(false)
- },
- })
- const {
- data: transferPreviewData,
- error: transferPreviewError,
- isPending: transferPreviewIsLoading,
- } = useProjectTransferPreviewQuery(
- { projectRef, targetOrganizationSlug: selectedOrg },
- { enabled: !isTransferring && isOpen }
- )
- const queryClient = useQueryClient()
- useEffect(() => {
- if (isOpen) {
- // reset state
- setSelectedOrg(undefined)
- } else {
- // Invalidate cache
- queryClient.removeQueries({
- queryKey: projectKeys.projectTransferPreview(projectRef, selectedOrg),
- })
- }
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [isOpen])
- const { can: canTransferProject } = useAsyncCheckPermissions(
- PermissionAction.UPDATE,
- 'organizations'
- )
- const toggle = () => {
- setIsOpen(!isOpen)
- }
- async function handleTransferProject() {
- if (project === undefined) return
- if (selectedOrg === undefined) return
- transferProject({ projectRef, targetOrganizationSlug: selectedOrg })
- }
- return (
- <>
- <ButtonTooltip
- type="default"
- onClick={toggle}
- disabled={!canTransferProject || disableProjectTransfer}
- tooltip={{
- content: {
- side: 'bottom',
- text: !canTransferProject
- ? 'You need additional permissions to transfer this project'
- : disableProjectTransfer
- ? 'Project transfers are temporarily disabled, please try again later.'
- : undefined,
- },
- }}
- >
- Transfer project
- </ButtonTooltip>
- <Modal
- 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>
- }
- >
- <Modal.Content className="text-foreground-light">
- <p className="text-sm">
- To transfer projects, the owner must be a member of both the source and target
- organizations. Consider the following before transferring your project:
- </p>
- <ul className="mt-4 space-y-5 text-sm">
- <li className="flex gap-4">
- <span className="shrink-0 mt-1">
- <Loader />
- </span>
- <div>
- <p className="font-bold">Possible downtime</p>
- <p>
- There might be a short downtime when transferring projects from a paid to a free
- organization.
- </p>
- </div>
- </li>
- <li className="flex gap-4">
- <span className="shrink-0 mt-1">
- <Shield />
- </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">
- <Wrench size={24} className="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>
- <DocsButton
- abbrev={false}
- className="mt-6"
- href={`${DOCS_URL}/guides/platform/project-transfer`}
- />
- </Modal.Content>
- <Modal.Separator />
- <Modal.Content>
- {organizations && (
- <div className="space-y-2">
- {organizations.length === 0 ? (
- <div className="flex items-center gap-3 bg-surface-200 p-3 text-sm rounded-md border">
- <InfoIcon /> You do not have any organizations you can transfer your project to.
- </div>
- ) : (
- <FormItemLayout
- id="organization"
- isReactForm={false}
- layout="vertical"
- label="Select Target Organization"
- className="gap-[2px]"
- size="tiny"
- >
- <Select onValueChange={(slug) => setSelectedOrg(slug)} value={selectedOrg}>
- <SelectTrigger id="organization">
- <SelectValue placeholder="Select Organization" />
- </SelectTrigger>
- <SelectContent>
- {organizations.map((x) => (
- <SelectItem key={x.id} value={x.slug}>
- {x.name}
- </SelectItem>
- ))}
- </SelectContent>
- </Select>
- </FormItemLayout>
- )}
- </div>
- )}
- </Modal.Content>
- {selectedOrg !== undefined && (
- <Loading active={selectedOrg !== undefined && transferPreviewIsLoading}>
- <Modal.Content>
- <div className="space-y-2">
- {transferPreviewData && transferPreviewData.errors.length > 0 && (
- <Admonition type="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 transfer this project.
- </p>
- </div>
- )}
- </Admonition>
- )}
- {transferPreviewData &&
- (transferPreviewData.warnings.length > 0 ||
- transferPreviewData.info.length > 0) && (
- <Admonition type="caution" showIcon={false} className="mt-3">
- <div className="flex flex-col gap-y-2">
- {transferPreviewData.warnings.map((warning) => (
- <div key={warning.key} className="flex items-center gap-2">
- <WarningIcon className="shrink-0" />
- <p className="mb-0.5">{warning.message}</p>
- </div>
- ))}
- {transferPreviewData.info.map((info) => (
- <div key={info.key} className="flex items-start gap-2">
- <InfoIcon className="shrink-0 mt-0.5" />
- <p className="mb-0.5">{info.message}</p>
- </div>
- ))}
- </div>
- </Admonition>
- )}
- {transferPreviewError && !transferError && (
- <Admonition
- type="danger"
- title="Project cannot be transferred"
- description={transferPreviewError.message}
- />
- )}
- {transferError && (
- <Admonition
- type="danger"
- title="Project cannot be transferred"
- description={transferError.message}
- />
- )}
- </div>
- </Modal.Content>
- </Loading>
- )}
- </Modal>
- </>
- )
- }
|