FlyDeprecationBanner.tsx 6.9 KB

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