TransferProjectButton.tsx 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302
  1. import { PermissionAction } from '@supabase/shared-types/out/constants'
  2. import { useQueryClient } from '@tanstack/react-query'
  3. import { useFlag } from 'common'
  4. import { Loader, Shield, Wrench } from 'lucide-react'
  5. import { useEffect, useState } from 'react'
  6. import { toast } from 'sonner'
  7. import {
  8. Button,
  9. InfoIcon,
  10. Loading,
  11. Modal,
  12. Select,
  13. SelectContent,
  14. SelectItem,
  15. SelectTrigger,
  16. SelectValue,
  17. WarningIcon,
  18. } from 'ui'
  19. import { Admonition } from 'ui-patterns'
  20. import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
  21. import { ButtonTooltip } from '@/components/ui/ButtonTooltip'
  22. import { DocsButton } from '@/components/ui/DocsButton'
  23. import { useOrganizationsQuery } from '@/data/organizations/organizations-query'
  24. import { projectKeys } from '@/data/projects/keys'
  25. import { useProjectTransferMutation } from '@/data/projects/project-transfer-mutation'
  26. import { useProjectTransferPreviewQuery } from '@/data/projects/project-transfer-preview-query'
  27. import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions'
  28. import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
  29. import { DOCS_URL } from '@/lib/constants'
  30. export const TransferProjectButton = () => {
  31. const { data: project } = useSelectedProjectQuery()
  32. const projectRef = project?.ref
  33. const projectOrgId = project?.organization_id
  34. const [isOpen, setIsOpen] = useState(false)
  35. const { data: allOrganizations } = useOrganizationsQuery({ enabled: isOpen })
  36. const disableProjectTransfer = useFlag('disableProjectTransfer')
  37. const organizations = (allOrganizations || []).filter((it) => it.id !== projectOrgId)
  38. const [selectedOrg, setSelectedOrg] = useState<string>()
  39. const {
  40. mutate: transferProject,
  41. error: transferError,
  42. isPending: isTransferring,
  43. } = useProjectTransferMutation({
  44. onSuccess: () => {
  45. toast.success(`Successfully transferred project ${project?.name}.`)
  46. setIsOpen(false)
  47. },
  48. })
  49. const {
  50. data: transferPreviewData,
  51. error: transferPreviewError,
  52. isPending: transferPreviewIsLoading,
  53. } = useProjectTransferPreviewQuery(
  54. { projectRef, targetOrganizationSlug: selectedOrg },
  55. { enabled: !isTransferring && isOpen }
  56. )
  57. const queryClient = useQueryClient()
  58. useEffect(() => {
  59. if (isOpen) {
  60. // reset state
  61. setSelectedOrg(undefined)
  62. } else {
  63. // Invalidate cache
  64. queryClient.removeQueries({
  65. queryKey: projectKeys.projectTransferPreview(projectRef, selectedOrg),
  66. })
  67. }
  68. // eslint-disable-next-line react-hooks/exhaustive-deps
  69. }, [isOpen])
  70. const { can: canTransferProject } = useAsyncCheckPermissions(
  71. PermissionAction.UPDATE,
  72. 'organizations'
  73. )
  74. const toggle = () => {
  75. setIsOpen(!isOpen)
  76. }
  77. async function handleTransferProject() {
  78. if (project === undefined) return
  79. if (selectedOrg === undefined) return
  80. transferProject({ projectRef, targetOrganizationSlug: selectedOrg })
  81. }
  82. return (
  83. <>
  84. <ButtonTooltip
  85. type="default"
  86. onClick={toggle}
  87. disabled={!canTransferProject || disableProjectTransfer}
  88. tooltip={{
  89. content: {
  90. side: 'bottom',
  91. text: !canTransferProject
  92. ? 'You need additional permissions to transfer this project'
  93. : disableProjectTransfer
  94. ? 'Project transfers are temporarily disabled, please try again later.'
  95. : undefined,
  96. },
  97. }}
  98. >
  99. Transfer project
  100. </ButtonTooltip>
  101. <Modal
  102. onCancel={() => toggle()}
  103. visible={isOpen}
  104. loading={isTransferring}
  105. size={'xlarge'}
  106. header={`Transfer project ${project?.name}`}
  107. customFooter={
  108. <div className="flex items-center space-x-2 justify-end">
  109. <Button type="default" onClick={() => setIsOpen(false)}>
  110. Cancel
  111. </Button>
  112. <Button
  113. onClick={() => handleTransferProject()}
  114. disabled={
  115. !transferPreviewData || !transferPreviewData.valid || isTransferring || !selectedOrg
  116. }
  117. >
  118. Transfer Project
  119. </Button>
  120. </div>
  121. }
  122. >
  123. <Modal.Content className="text-foreground-light">
  124. <p className="text-sm">
  125. To transfer projects, the owner must be a member of both the source and target
  126. organizations. Consider the following before transferring your project:
  127. </p>
  128. <ul className="mt-4 space-y-5 text-sm">
  129. <li className="flex gap-4">
  130. <span className="shrink-0 mt-1">
  131. <Loader />
  132. </span>
  133. <div>
  134. <p className="font-bold">Possible downtime</p>
  135. <p>
  136. There might be a short downtime when transferring projects from a paid to a free
  137. organization.
  138. </p>
  139. </div>
  140. </li>
  141. <li className="flex gap-4">
  142. <span className="shrink-0 mt-1">
  143. <Shield />
  144. </span>
  145. <div>
  146. <p className="font-bold">Permissions</p>
  147. <p>
  148. Depending on your role in the target organization, your level of permissions may
  149. change after transfer.
  150. </p>
  151. </div>
  152. </li>
  153. <li className="flex gap-4">
  154. <span className="shrink-0 mt-1">
  155. <Wrench size={24} className="shrink-0" />
  156. </span>
  157. <div>
  158. <p className="font-bold">Features</p>
  159. <p>
  160. Moving your project to an organization with a smaller subscription plan may result
  161. in the loss of certain features (i.e. image transformations).
  162. </p>
  163. </div>
  164. </li>
  165. </ul>
  166. <DocsButton
  167. abbrev={false}
  168. className="mt-6"
  169. href={`${DOCS_URL}/guides/platform/project-transfer`}
  170. />
  171. </Modal.Content>
  172. <Modal.Separator />
  173. <Modal.Content>
  174. {organizations && (
  175. <div className="space-y-2">
  176. {organizations.length === 0 ? (
  177. <div className="flex items-center gap-3 bg-surface-200 p-3 text-sm rounded-md border">
  178. <InfoIcon /> You do not have any organizations you can transfer your project to.
  179. </div>
  180. ) : (
  181. <FormItemLayout
  182. id="organization"
  183. isReactForm={false}
  184. layout="vertical"
  185. label="Select Target Organization"
  186. className="gap-[2px]"
  187. size="tiny"
  188. >
  189. <Select onValueChange={(slug) => setSelectedOrg(slug)} value={selectedOrg}>
  190. <SelectTrigger id="organization">
  191. <SelectValue placeholder="Select Organization" />
  192. </SelectTrigger>
  193. <SelectContent>
  194. {organizations.map((x) => (
  195. <SelectItem key={x.id} value={x.slug}>
  196. {x.name}
  197. </SelectItem>
  198. ))}
  199. </SelectContent>
  200. </Select>
  201. </FormItemLayout>
  202. )}
  203. </div>
  204. )}
  205. </Modal.Content>
  206. {selectedOrg !== undefined && (
  207. <Loading active={selectedOrg !== undefined && transferPreviewIsLoading}>
  208. <Modal.Content>
  209. <div className="space-y-2">
  210. {transferPreviewData && transferPreviewData.errors.length > 0 && (
  211. <Admonition type="danger" title="Project cannot be transferred">
  212. <div className="space-y-1">
  213. {transferPreviewData.errors.map((error) => (
  214. <p key={error.key}>{error.message}</p>
  215. ))}
  216. </div>
  217. {transferPreviewData.members_exceeding_free_project_limit.length > 0 && (
  218. <div className="space-y-2">
  219. <p className="text-sm text-foreground-light">
  220. These members have reached their maximum limits for the number of active
  221. Free plan projects within organizations where they are an administrator or
  222. owner:
  223. </p>
  224. <ul className="pl-5 text-sm list-disc text-foreground-light">
  225. {(transferPreviewData.members_exceeding_free_project_limit || []).map(
  226. (member, idx: number) => (
  227. <li key={`member-${idx}`}>
  228. {member.name} (Limit: {member.limit} free projects)
  229. </li>
  230. )
  231. )}
  232. </ul>
  233. <p className="text-sm text-foreground-light">
  234. These members will need to either delete, pause, or upgrade one or more of
  235. their projects before you can transfer this project.
  236. </p>
  237. </div>
  238. )}
  239. </Admonition>
  240. )}
  241. {transferPreviewData &&
  242. (transferPreviewData.warnings.length > 0 ||
  243. transferPreviewData.info.length > 0) && (
  244. <Admonition type="caution" showIcon={false} className="mt-3">
  245. <div className="flex flex-col gap-y-2">
  246. {transferPreviewData.warnings.map((warning) => (
  247. <div key={warning.key} className="flex items-center gap-2">
  248. <WarningIcon className="shrink-0" />
  249. <p className="mb-0.5">{warning.message}</p>
  250. </div>
  251. ))}
  252. {transferPreviewData.info.map((info) => (
  253. <div key={info.key} className="flex items-start gap-2">
  254. <InfoIcon className="shrink-0 mt-0.5" />
  255. <p className="mb-0.5">{info.message}</p>
  256. </div>
  257. ))}
  258. </div>
  259. </Admonition>
  260. )}
  261. {transferPreviewError && !transferError && (
  262. <Admonition
  263. type="danger"
  264. title="Project cannot be transferred"
  265. description={transferPreviewError.message}
  266. />
  267. )}
  268. {transferError && (
  269. <Admonition
  270. type="danger"
  271. title="Project cannot be transferred"
  272. description={transferError.message}
  273. />
  274. )}
  275. </div>
  276. </Modal.Content>
  277. </Loading>
  278. )}
  279. </Modal>
  280. </>
  281. )
  282. }