DowngradeModal.tsx 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161
  1. import { MinusCircle, PauseCircle } from 'lucide-react'
  2. import { useMemo } from 'react'
  3. import { plans as subscriptionsPlans } from 'shared-data/plans'
  4. import { Modal } from 'ui'
  5. import { Admonition } from 'ui-patterns'
  6. import { getComputeSize, OrgProject } from '@/data/projects/org-projects-infinite-query'
  7. import type { OrgSubscription, ProjectAddon } from '@/data/subscriptions/types'
  8. export interface DowngradeModalProps {
  9. visible: boolean
  10. subscription?: OrgSubscription
  11. onClose: () => void
  12. onConfirm: () => void
  13. projects: OrgProject[]
  14. }
  15. const ProjectDowngradeListItem = ({ projectAddon }: { projectAddon: ProjectAddon }) => {
  16. const needsRestart = projectAddon.addons.find((addon) => addon.type === 'compute_instance')
  17. /**
  18. * We do not include Log Drains and Advanced MFA Phone for the following reasons:
  19. * 1. These addons are not removed automatically. Instead, users have to remove the respective configuration themselves
  20. * 2. It's not obvious to users that Log Drains and MFA Phone are addons
  21. */
  22. const relevantAddonsToList = projectAddon.addons.filter(
  23. (addon) => !['log_drain', 'auth_mfa_phone'].includes(addon.type)
  24. )
  25. const addonNames = relevantAddonsToList.map((addon) => {
  26. if (addon.type === 'compute_instance') return `${addon.variant.name} Compute Instance`
  27. return addon.variant.name
  28. })
  29. return (
  30. <li className="list-disc ml-6">
  31. {projectAddon.name}: {addonNames.join(', ')} will be removed.
  32. {needsRestart ? (
  33. <>
  34. {' '}
  35. Project will also <span className="font-bold">need to be restarted</span> due to change in
  36. compute instance
  37. </>
  38. ) : (
  39. ''
  40. )}
  41. </li>
  42. )
  43. }
  44. const DowngradeModal = ({
  45. visible,
  46. subscription,
  47. onClose,
  48. onConfirm,
  49. projects,
  50. }: DowngradeModalProps) => {
  51. const selectedPlan = useMemo(() => subscriptionsPlans.find((tier) => tier.id === 'tier_free'), [])
  52. // Filter out the micro addon as we're dealing with that separately
  53. const previousProjectAddons =
  54. subscription?.project_addons.flatMap((projectAddons) => {
  55. const addons = projectAddons.addons.filter((it) => it.variant.identifier !== 'ci_micro')
  56. if (!addons.length) {
  57. return []
  58. } else {
  59. return {
  60. ...projectAddons,
  61. // Overwrite addons, filtered out the micro addon
  62. addons,
  63. }
  64. }
  65. }) || []
  66. const hasInstancesOnMicro = projects.some((project) => {
  67. const computeSize = getComputeSize(project)
  68. return computeSize === 'micro'
  69. })
  70. return (
  71. <Modal
  72. size="large"
  73. alignFooter="right"
  74. variant="warning"
  75. visible={visible}
  76. onCancel={onClose}
  77. onConfirm={onConfirm}
  78. header={`Confirm to downgrade to ${selectedPlan?.name} plan`}
  79. >
  80. <Modal.Content>
  81. <div className="space-y-2">
  82. <Admonition
  83. type="warning"
  84. title="Downgrading to the Free Plan will lead to reductions in your organization's quota"
  85. description="If you're already past the limits of the Free Plan, your projects could become
  86. unresponsive or enter read only mode."
  87. />
  88. {((previousProjectAddons.length ?? 0) > 0 || hasInstancesOnMicro) && (
  89. <Admonition type="warning" title="Projects affected by the downgrade">
  90. <ul className="space-y-1 max-h-[100px] overflow-y-auto">
  91. {previousProjectAddons.map((project) => (
  92. <ProjectDowngradeListItem key={project.ref} projectAddon={project} />
  93. ))}
  94. {projects
  95. .filter((it) => {
  96. const computeSize = getComputeSize(it)
  97. return computeSize === 'micro'
  98. })
  99. .map((project) => (
  100. <li className="list-disc ml-6" key={project.ref}>
  101. {project.name}: Compute will be downgraded. Project will also{' '}
  102. <span className="font-bold">need to be restarted</span>.
  103. </li>
  104. ))}
  105. </ul>
  106. </Admonition>
  107. )}
  108. </div>
  109. <ul className="mt-4 space-y-5 text-sm">
  110. <li className="flex items-center gap-3">
  111. <PauseCircle size={18} />
  112. <span>Projects will be paused after a week of inactivity</span>
  113. </li>
  114. <li className="flex items-center gap-3 mb-2">
  115. <MinusCircle size={18} />
  116. <span>Add ons from all projects under this organization will be removed.</span>
  117. </li>
  118. <li className="flex gap-3">
  119. <div>
  120. <strong>Before you downgrade to the {selectedPlan?.name} plan, consider:</strong>
  121. <ul className="space-y-2 mt-2">
  122. <li className="list-disc ml-6 text-foreground-light">
  123. Your projects no longer require their respective add-ons.
  124. </li>
  125. <li className="list-disc ml-6 text-foreground-light">
  126. Your resource consumption are well within the {selectedPlan?.name} plan's quota.
  127. </li>
  128. <li className="list-disc ml-6 text-foreground-light">
  129. Alternatively, you may also transfer projects across organizations.
  130. </li>
  131. </ul>
  132. </div>
  133. </li>
  134. </ul>
  135. {subscription?.billing_via_partner === true && subscription.billing_partner === 'fly' && (
  136. <p className="mt-4 text-sm">
  137. Your organization will be downgraded at the end of your current billing cycle.
  138. </p>
  139. )}
  140. </Modal.Content>
  141. </Modal>
  142. )
  143. }
  144. export default DowngradeModal