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