| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222 |
- import { LOCAL_STORAGE_KEYS } from 'common'
- import { useRouter } from 'next/router'
- import { useEffect, useRef, type ReactNode } from 'react'
- import {
- Button,
- cn,
- Dialog,
- DialogClose,
- DialogContent,
- DialogDescription,
- DialogFooter,
- DialogHeader,
- DialogSection,
- DialogSectionSeparator,
- DialogTitle,
- DialogTrigger,
- } from 'ui'
- import { HeaderBanner } from '@/components/interfaces/Organization/HeaderBanner'
- import { InlineLink, InlineLinkClassName } from '@/components/ui/InlineLink'
- import {
- useFlyDeprecationProjects,
- type FlyDeprecationProject,
- } from '@/hooks/misc/useFlyDeprecationProjects'
- import { useLocalStorageQuery } from '@/hooks/misc/useLocalStorage'
- import { useTrack } from '@/lib/telemetry/track'
- const BANNER_EXPIRES_AT = new Date('2026-06-01T00:00:00Z')
- const BACKUP_RESTORE_CLI_URL =
- 'https://supabase.com/docs/guides/platform/migrating-within-supabase/backup-restore'
- const DASHBOARD_RESTORE_URL =
- 'https://supabase.com/docs/guides/platform/migrating-within-supabase/dashboard-restore'
- const BRANCHING_DASHBOARD_URL = 'https://supabase.com/docs/guides/deployment/branching/dashboard'
- const SUPPORT_EMAIL = 'success@supabase.io'
- export const FlyDeprecationBanner = () => {
- const router = useRouter()
- const [acknowledged, setAcknowledged, { isSuccess }] = useLocalStorageQuery(
- LOCAL_STORAGE_KEYS.FLY_DEPRECATION_2026_05_31,
- false
- )
- const isExpired = Date.now() >= BANNER_EXPIRES_AT.getTime()
- const onSignIn = router.pathname.startsWith('/sign-in')
- const shouldEvaluate = !isExpired && !onSignIn && isSuccess && !acknowledged
- const { isReady, primaries, branches } = useFlyDeprecationProjects({ enabled: shouldEvaluate })
- const hasFlyResources = primaries.length > 0 || branches.length > 0
- const track = useTrack()
- const exposedRef = useRef(false)
- useEffect(() => {
- if (!shouldEvaluate || !isReady || !hasFlyResources || exposedRef.current) return
- exposedRef.current = true
- track('fly_deprecation_banner_exposed', {
- primaryCount: primaries.length,
- branchCount: branches.length,
- })
- }, [shouldEvaluate, isReady, hasFlyResources, primaries.length, branches.length, track])
- if (!shouldEvaluate || !isReady || !hasFlyResources) return null
- const onDismiss = () => {
- track('fly_deprecation_banner_dismissed', {
- primaryCount: primaries.length,
- branchCount: branches.length,
- })
- setAcknowledged(true)
- }
- const title =
- primaries.length > 0 && branches.length > 0
- ? 'Action required: Fly.io project and branch suspensions begin May 31'
- : primaries.length > 0
- ? 'Action required: Fly.io project suspensions begin May 31'
- : 'Action required: Fly.io branch suspensions begin May 31'
- return (
- <HeaderBanner
- variant="warning"
- title={title}
- description={<FlyDeprecationDialog primaries={primaries} branches={branches} />}
- onDismiss={onDismiss}
- />
- )
- }
- const FlyDeprecationDialog = ({
- primaries,
- branches,
- }: {
- primaries: FlyDeprecationProject[]
- branches: FlyDeprecationProject[]
- }) => {
- return (
- <Dialog>
- <DialogTrigger className={cn(InlineLinkClassName, 'cursor-pointer')}>
- View affected projects
- </DialogTrigger>
- <DialogContent>
- <DialogHeader>
- <DialogTitle>Fly.io deprecation: suspensions begin May 31, 2026</DialogTitle>
- <DialogDescription>
- Briven will begin suspending projects and branches still running on Fly.io
- infrastructure on May 31, 2026.
- </DialogDescription>
- </DialogHeader>
- <DialogSectionSeparator />
- <ProjectList
- label="Projects on Fly.io"
- items={primaries}
- instructions={
- <>
- <p>
- To preserve your data, migrate each project to Briven's general infrastructure:
- </p>
- <ol className="list-decimal pl-5 space-y-1">
- <li>
- Back up your database using the{' '}
- <InlineLink href={BACKUP_RESTORE_CLI_URL}>Briven CLI</InlineLink> (or take a{' '}
- <InlineLink href={DASHBOARD_RESTORE_URL}>Dashboard backup</InlineLink>).
- </li>
- <li>Create a new project on Briven's general infrastructure.</li>
- <li>Restore the backup into the new project.</li>
- </ol>
- </>
- }
- />
- <ProjectList
- label="Branches on Fly.io"
- items={branches}
- instructions={
- <>
- <p>Merge preview branches before May 31. For persistent branches:</p>
- <ol className="list-decimal pl-5 space-y-1">
- <li>
- Take a{' '}
- <InlineLink href={BACKUP_RESTORE_CLI_URL}>
- snapshot of the branch database
- </InlineLink>{' '}
- before shutting it down.
- </li>
- <li>
- <InlineLink href={BRANCHING_DASHBOARD_URL}>
- Deploy a new persistent branch
- </InlineLink>{' '}
- on Briven's general infrastructure.
- </li>
- <li>Restore your data manually from the snapshot.</li>
- </ol>
- </>
- }
- />
- <DialogSection className="text-sm">
- <p>
- Questions or need an extension? Email{' '}
- <a className={InlineLinkClassName} href={`mailto:${SUPPORT_EMAIL}`}>
- {SUPPORT_EMAIL}
- </a>
- .
- </p>
- </DialogSection>
- <DialogFooter>
- <DialogClose asChild>
- <Button type="default">Close</Button>
- </DialogClose>
- </DialogFooter>
- </DialogContent>
- </Dialog>
- )
- }
- const MAX_LISTED = 5
- const ProjectList = ({
- label,
- items,
- instructions,
- }: {
- label: string
- items: FlyDeprecationProject[]
- instructions: ReactNode
- }) => {
- if (items.length === 0) return null
- const visible = items.slice(0, MAX_LISTED)
- const remaining = items.length - visible.length
- return (
- <>
- <DialogSection className="text-sm flex flex-col gap-y-2">
- <p className="font-medium">
- {label} ({items.length})
- </p>
- {items.length === 1 ? (
- <p>
- {items[0].name} <span className="text-foreground-muted">({items[0].orgName})</span>
- </p>
- ) : (
- <ul className="list-disc pl-5 space-y-1">
- {visible.map((p) => (
- <li key={p.ref}>
- {p.name} <span className="text-foreground-muted">({p.orgName})</span>
- </li>
- ))}
- {remaining > 0 && (
- <li className="text-foreground-muted list-none -ml-5">…and {remaining} more.</li>
- )}
- </ul>
- )}
- </DialogSection>
- <DialogSection className="text-sm flex flex-col gap-y-2">{instructions}</DialogSection>
- </>
- )
- }
|