FlyDeprecationBanner.tsx222 lines · main
| 1 | import { LOCAL_STORAGE_KEYS } from 'common' |
| 2 | import { useRouter } from 'next/router' |
| 3 | import { useEffect, useRef, type ReactNode } from 'react' |
| 4 | import { |
| 5 | Button, |
| 6 | cn, |
| 7 | Dialog, |
| 8 | DialogClose, |
| 9 | DialogContent, |
| 10 | DialogDescription, |
| 11 | DialogFooter, |
| 12 | DialogHeader, |
| 13 | DialogSection, |
| 14 | DialogSectionSeparator, |
| 15 | DialogTitle, |
| 16 | DialogTrigger, |
| 17 | } from 'ui' |
| 18 | |
| 19 | import { HeaderBanner } from '@/components/interfaces/Organization/HeaderBanner' |
| 20 | import { InlineLink, InlineLinkClassName } from '@/components/ui/InlineLink' |
| 21 | import { |
| 22 | useFlyDeprecationProjects, |
| 23 | type FlyDeprecationProject, |
| 24 | } from '@/hooks/misc/useFlyDeprecationProjects' |
| 25 | import { useLocalStorageQuery } from '@/hooks/misc/useLocalStorage' |
| 26 | import { useTrack } from '@/lib/telemetry/track' |
| 27 | |
| 28 | const BANNER_EXPIRES_AT = new Date('2026-06-01T00:00:00Z') |
| 29 | const BACKUP_RESTORE_CLI_URL = |
| 30 | 'https://supabase.com/docs/guides/platform/migrating-within-supabase/backup-restore' |
| 31 | const DASHBOARD_RESTORE_URL = |
| 32 | 'https://supabase.com/docs/guides/platform/migrating-within-supabase/dashboard-restore' |
| 33 | const BRANCHING_DASHBOARD_URL = 'https://supabase.com/docs/guides/deployment/branching/dashboard' |
| 34 | const SUPPORT_EMAIL = 'success@supabase.io' |
| 35 | |
| 36 | export const FlyDeprecationBanner = () => { |
| 37 | const router = useRouter() |
| 38 | |
| 39 | const [acknowledged, setAcknowledged, { isSuccess }] = useLocalStorageQuery( |
| 40 | LOCAL_STORAGE_KEYS.FLY_DEPRECATION_2026_05_31, |
| 41 | false |
| 42 | ) |
| 43 | |
| 44 | const isExpired = Date.now() >= BANNER_EXPIRES_AT.getTime() |
| 45 | const onSignIn = router.pathname.startsWith('/sign-in') |
| 46 | |
| 47 | const shouldEvaluate = !isExpired && !onSignIn && isSuccess && !acknowledged |
| 48 | |
| 49 | const { isReady, primaries, branches } = useFlyDeprecationProjects({ enabled: shouldEvaluate }) |
| 50 | |
| 51 | const hasFlyResources = primaries.length > 0 || branches.length > 0 |
| 52 | |
| 53 | const track = useTrack() |
| 54 | |
| 55 | const exposedRef = useRef(false) |
| 56 | useEffect(() => { |
| 57 | if (!shouldEvaluate || !isReady || !hasFlyResources || exposedRef.current) return |
| 58 | exposedRef.current = true |
| 59 | track('fly_deprecation_banner_exposed', { |
| 60 | primaryCount: primaries.length, |
| 61 | branchCount: branches.length, |
| 62 | }) |
| 63 | }, [shouldEvaluate, isReady, hasFlyResources, primaries.length, branches.length, track]) |
| 64 | |
| 65 | if (!shouldEvaluate || !isReady || !hasFlyResources) return null |
| 66 | |
| 67 | const onDismiss = () => { |
| 68 | track('fly_deprecation_banner_dismissed', { |
| 69 | primaryCount: primaries.length, |
| 70 | branchCount: branches.length, |
| 71 | }) |
| 72 | setAcknowledged(true) |
| 73 | } |
| 74 | |
| 75 | const title = |
| 76 | primaries.length > 0 && branches.length > 0 |
| 77 | ? 'Action required: Fly.io project and branch suspensions begin May 31' |
| 78 | : primaries.length > 0 |
| 79 | ? 'Action required: Fly.io project suspensions begin May 31' |
| 80 | : 'Action required: Fly.io branch suspensions begin May 31' |
| 81 | |
| 82 | return ( |
| 83 | <HeaderBanner |
| 84 | variant="warning" |
| 85 | title={title} |
| 86 | description={<FlyDeprecationDialog primaries={primaries} branches={branches} />} |
| 87 | onDismiss={onDismiss} |
| 88 | /> |
| 89 | ) |
| 90 | } |
| 91 | |
| 92 | const FlyDeprecationDialog = ({ |
| 93 | primaries, |
| 94 | branches, |
| 95 | }: { |
| 96 | primaries: FlyDeprecationProject[] |
| 97 | branches: FlyDeprecationProject[] |
| 98 | }) => { |
| 99 | return ( |
| 100 | <Dialog> |
| 101 | <DialogTrigger className={cn(InlineLinkClassName, 'cursor-pointer')}> |
| 102 | View affected projects |
| 103 | </DialogTrigger> |
| 104 | <DialogContent> |
| 105 | <DialogHeader> |
| 106 | <DialogTitle>Fly.io deprecation: suspensions begin May 31, 2026</DialogTitle> |
| 107 | <DialogDescription> |
| 108 | Briven will begin suspending projects and branches still running on Fly.io |
| 109 | infrastructure on May 31, 2026. |
| 110 | </DialogDescription> |
| 111 | </DialogHeader> |
| 112 | |
| 113 | <DialogSectionSeparator /> |
| 114 | |
| 115 | <ProjectList |
| 116 | label="Projects on Fly.io" |
| 117 | items={primaries} |
| 118 | instructions={ |
| 119 | <> |
| 120 | <p> |
| 121 | To preserve your data, migrate each project to Briven's general infrastructure: |
| 122 | </p> |
| 123 | <ol className="list-decimal pl-5 space-y-1"> |
| 124 | <li> |
| 125 | Back up your database using the{' '} |
| 126 | <InlineLink href={BACKUP_RESTORE_CLI_URL}>Briven CLI</InlineLink> (or take a{' '} |
| 127 | <InlineLink href={DASHBOARD_RESTORE_URL}>Dashboard backup</InlineLink>). |
| 128 | </li> |
| 129 | <li>Create a new project on Briven's general infrastructure.</li> |
| 130 | <li>Restore the backup into the new project.</li> |
| 131 | </ol> |
| 132 | </> |
| 133 | } |
| 134 | /> |
| 135 | |
| 136 | <ProjectList |
| 137 | label="Branches on Fly.io" |
| 138 | items={branches} |
| 139 | instructions={ |
| 140 | <> |
| 141 | <p>Merge preview branches before May 31. For persistent branches:</p> |
| 142 | <ol className="list-decimal pl-5 space-y-1"> |
| 143 | <li> |
| 144 | Take a{' '} |
| 145 | <InlineLink href={BACKUP_RESTORE_CLI_URL}> |
| 146 | snapshot of the branch database |
| 147 | </InlineLink>{' '} |
| 148 | before shutting it down. |
| 149 | </li> |
| 150 | <li> |
| 151 | <InlineLink href={BRANCHING_DASHBOARD_URL}> |
| 152 | Deploy a new persistent branch |
| 153 | </InlineLink>{' '} |
| 154 | on Briven's general infrastructure. |
| 155 | </li> |
| 156 | <li>Restore your data manually from the snapshot.</li> |
| 157 | </ol> |
| 158 | </> |
| 159 | } |
| 160 | /> |
| 161 | |
| 162 | <DialogSection className="text-sm"> |
| 163 | <p> |
| 164 | Questions or need an extension? Email{' '} |
| 165 | <a className={InlineLinkClassName} href={`mailto:${SUPPORT_EMAIL}`}> |
| 166 | {SUPPORT_EMAIL} |
| 167 | </a> |
| 168 | . |
| 169 | </p> |
| 170 | </DialogSection> |
| 171 | |
| 172 | <DialogFooter> |
| 173 | <DialogClose asChild> |
| 174 | <Button type="default">Close</Button> |
| 175 | </DialogClose> |
| 176 | </DialogFooter> |
| 177 | </DialogContent> |
| 178 | </Dialog> |
| 179 | ) |
| 180 | } |
| 181 | |
| 182 | const MAX_LISTED = 5 |
| 183 | |
| 184 | const ProjectList = ({ |
| 185 | label, |
| 186 | items, |
| 187 | instructions, |
| 188 | }: { |
| 189 | label: string |
| 190 | items: FlyDeprecationProject[] |
| 191 | instructions: ReactNode |
| 192 | }) => { |
| 193 | if (items.length === 0) return null |
| 194 | const visible = items.slice(0, MAX_LISTED) |
| 195 | const remaining = items.length - visible.length |
| 196 | return ( |
| 197 | <> |
| 198 | <DialogSection className="text-sm flex flex-col gap-y-2"> |
| 199 | <p className="font-medium"> |
| 200 | {label} ({items.length}) |
| 201 | </p> |
| 202 | {items.length === 1 ? ( |
| 203 | <p> |
| 204 | {items[0].name} <span className="text-foreground-muted">({items[0].orgName})</span> |
| 205 | </p> |
| 206 | ) : ( |
| 207 | <ul className="list-disc pl-5 space-y-1"> |
| 208 | {visible.map((p) => ( |
| 209 | <li key={p.ref}> |
| 210 | {p.name} <span className="text-foreground-muted">({p.orgName})</span> |
| 211 | </li> |
| 212 | ))} |
| 213 | {remaining > 0 && ( |
| 214 | <li className="text-foreground-muted list-none -ml-5">…and {remaining} more.</li> |
| 215 | )} |
| 216 | </ul> |
| 217 | )} |
| 218 | </DialogSection> |
| 219 | <DialogSection className="text-sm flex flex-col gap-y-2">{instructions}</DialogSection> |
| 220 | </> |
| 221 | ) |
| 222 | } |