mirror of
https://github.com/supabase/supabase.git
synced 2026-10-11 20:35:07 +03:00
## Summary Adding an in-dashboard banner for the Fly.io May 31 suspension. Banner targets users on a Fly project (or with a Fly project in their currently-selected org) and surfaces a per-project breakdown of what's affected in a dialog. Detection is self-correcting: as soon as the user migrates off Fly, the banner disappears with no follow-up. <img width="557" height="502" alt="Screenshot 2026-05-11 at 5 08 22 PM" src="https://github.com/user-attachments/assets/7bafb712-3490-4555-9667-66e9909f1b1a" /> <img width="1675" height="536" alt="Screenshot 2026-05-11 at 3 55 06 PM" src="https://github.com/user-attachments/assets/6c1bf9d1-4dcc-4aac-a679-2ed477d2ed1c" /> ## Changes - **Detection hook** (`useFlyDeprecationProjects`): reads only from already-cached data — `useSelectedProjectQuery` for the current project, plus `useOrgProjectsInfiniteQuery` scoped to the selected org. Zero cross-org fan-out: worst case is one paginated query per session (the same one the project list page already makes). - **Banner component** (`FlyDeprecationBanner.tsx`): mounted in `AppBannerWrapper`. Dynamic title (primaries / branches / both), dialog lists affected projects with org name, numbered migration steps, links to backup/restore CLI + Dashboard backup + branching docs. List truncates to 5 entries with "…and N more." tail when more are affected. - **Telemetry**: `fly_deprecation_banner_exposed` and `fly_deprecation_banner_dismissed` events emitted via `useTrack` (auto-injects project + org groups). Properties: `primaryCount`, `branchCount`. CTA click tracking intentionally omitted — migration outcome is measured via warehouse `cloud_provider = 'FLY'` decay. - **LocalStorage**: dated dismissal key `FLY_DEPRECATION_2026_05_31`; orphan `FLY_POSTGRES_DEPRECATION_WARNING` from PR #33510 removed in the same change so users who dismissed the Feb 2025 banner still see this one. - **Support contact**: email `success@supabase.io` only (no support ticket link), per Brian's outreach copy in the Linear issues. ## Coverage trade-off Banner renders on project pages (selected-project check) and pages where the selected org's projects list is cached (org overview, project list). It does **not** render on `/dashboard` home or other pages without org context. Email outreach from GROWTH-817 / GROWTH-819 handles those users. This was a deliberate trade-off to avoid cross-org fan-out load. ## Lifecycle Banner expires `2026-06-01T00:00:00Z` (right after the May 31 deadline). Stale client bundles stop rendering it without a redeploy. Cleanup PR planned post-deadline to remove the component, hook, localStorage key, and telemetry events. ## Testing Tested on the Vercel preview with React Query cache overrides to mock a Fly project: - [x] Banner renders for a user with at least one project where `cloud_provider === 'FLY'` - [x] Banner does **not** render for a user with no Fly projects - [x] Banner does **not** render on `/sign-in` - [x] Title varies by primaries-only / branches-only / both - [x] Dialog lists affected projects with org name in parens - [x] Dialog list truncates to 5 with "…and N more." for larger sets - [x] Migration guide / Dashboard backup / branching links open in a new tab - [x] Dismiss (×) closes the banner and persists across hard reload (localStorage `fly-deprecation-2026-05-31-dismissed`) - [x] PostHog receives one `fly_deprecation_banner_exposed` per mount with `primaryCount` + `branchCount` and `$groups.organization` populated - [x] PostHog receives one `fly_deprecation_banner_dismissed` on close with the same property shape ## Linear - fixes GROWTH-817 - fixes GROWTH-819
223 lines
6.9 KiB
TypeScript
223 lines
6.9 KiB
TypeScript
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>
|
|
Supabase 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 Supabase'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}>Supabase CLI</InlineLink> (or take a{' '}
|
|
<InlineLink href={DASHBOARD_RESTORE_URL}>Dashboard backup</InlineLink>).
|
|
</li>
|
|
<li>Create a new project on Supabase'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 Supabase'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>
|
|
</>
|
|
)
|
|
}
|