mirror of
https://github.com/supabase/supabase.git
synced 2026-10-10 20:05:06 +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
63 lines
1.9 KiB
TypeScript
63 lines
1.9 KiB
TypeScript
import { useOrgProjectsInfiniteQuery } from '@/data/projects/org-projects-infinite-query'
|
|
import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization'
|
|
import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
|
|
import { PROVIDERS } from '@/lib/constants/infrastructure'
|
|
|
|
export interface FlyDeprecationProject {
|
|
ref: string
|
|
name: string
|
|
orgSlug: string
|
|
orgName: string
|
|
}
|
|
|
|
export function useFlyDeprecationProjects({ enabled }: { enabled: boolean }) {
|
|
const { data: selectedProject } = useSelectedProjectQuery()
|
|
const { data: selectedOrg } = useSelectedOrganizationQuery()
|
|
|
|
const orgSlug = selectedOrg?.slug
|
|
const orgName = selectedOrg?.name ?? ''
|
|
|
|
const { data: orgProjectsData, isFetched } = useOrgProjectsInfiniteQuery(
|
|
{ slug: orgSlug },
|
|
{ enabled: enabled && Boolean(orgSlug), staleTime: 30 * 60 * 1000 }
|
|
)
|
|
|
|
if (!enabled || !orgSlug) {
|
|
return { isReady: false, primaries: [], branches: [] }
|
|
}
|
|
|
|
const byRef = new Map<string, FlyDeprecationProject & { isBranch: boolean }>()
|
|
|
|
if (selectedProject?.cloud_provider === PROVIDERS.FLY.id) {
|
|
byRef.set(selectedProject.ref, {
|
|
ref: selectedProject.ref,
|
|
name: selectedProject.name,
|
|
orgSlug,
|
|
orgName,
|
|
isBranch: Boolean(selectedProject.parent_project_ref),
|
|
})
|
|
}
|
|
|
|
const orgProjects = orgProjectsData?.pages.flatMap((page) => page.projects) ?? []
|
|
for (const p of orgProjects) {
|
|
if (p.cloud_provider !== PROVIDERS.FLY.id) continue
|
|
if (byRef.has(p.ref)) continue
|
|
byRef.set(p.ref, {
|
|
ref: p.ref,
|
|
name: p.name,
|
|
orgSlug,
|
|
orgName,
|
|
isBranch: Boolean(p.is_branch),
|
|
})
|
|
}
|
|
|
|
const all = Array.from(byRef.values())
|
|
const primaries: FlyDeprecationProject[] = []
|
|
const branches: FlyDeprecationProject[] = []
|
|
for (const { isBranch, ...rest } of all) {
|
|
;(isBranch ? branches : primaries).push(rest)
|
|
}
|
|
|
|
return { isReady: isFetched, primaries, branches }
|
|
}
|