useStatusPageBannerVisibility.ts97 lines · main
1import { useQueries, useQuery } from '@tanstack/react-query'
2import { LOCAL_STORAGE_KEYS, useFlag } from 'common'
3import { useCallback, useMemo } from 'react'
4
5import { getRelevantIncidentIds, shouldShowBanner } from './StatusPageBanner.utils'
6import { useOrganizationsQuery } from '@/data/organizations/organizations-query'
7import { incidentBannerQueryOptions } from '@/data/platform/incident-banner-query'
8import { projectKeys } from '@/data/projects/keys'
9import {
10 getOrganizationProjects,
11 type OrgProject,
12} from '@/data/projects/org-projects-infinite-query'
13import { useLocalStorageQuery } from '@/hooks/misc/useLocalStorage'
14
15export type StatusPageBannerData = { title: string; dismiss?: () => void }
16
17export function useStatusPageBannerVisibility(): StatusPageBannerData | null {
18 const showIncidentBannerOverride =
19 useFlag('ongoingIncident') || process.env.NEXT_PUBLIC_ONGOING_INCIDENT === 'true'
20
21 const { data: incidentBannerData } = useQuery(incidentBannerQueryOptions())
22
23 const bannerItems = incidentBannerData?.incidents ?? []
24 const incidents = bannerItems.map((i) => ({ id: i.id, cache: i.metadata }))
25 const hasActiveIncidents = incidents.length > 0
26
27 const { data: organizations } = useOrganizationsQuery({
28 enabled: !showIncidentBannerOverride && hasActiveIncidents,
29 })
30
31 const orgProjectsQueries = useQueries({
32 queries: (organizations ?? []).map((org) => ({
33 queryKey: projectKeys.bannerProjectsByOrg(org.slug),
34 queryFn: () => getOrganizationProjects({ slug: org.slug, limit: 100 }),
35 staleTime: 5 * 60 * 1000,
36 enabled: !showIncidentBannerOverride && hasActiveIncidents,
37 })),
38 })
39
40 const isProjectsFetched =
41 organizations !== undefined &&
42 (organizations.length === 0 || orgProjectsQueries.every((q) => q.isFetched))
43
44 const allProjects = orgProjectsQueries.flatMap((q) => q.data?.projects ?? [])
45 const hasProjects = allProjects.length > 0
46 const userRegions = useMemo(
47 () =>
48 new Set(
49 allProjects.flatMap((project: OrgProject) => project.databases.map((db) => db.region))
50 ),
51 [allProjects]
52 )
53 const hasUnknownRegions = orgProjectsQueries.some(
54 (q) => q.isError || (q.data !== undefined && q.data.pagination.count > q.data.projects.length)
55 )
56
57 const [dismissedIds, setDismissedIds, { isSuccess: isDismissedLoaded }] = useLocalStorageQuery<
58 Array<string>
59 >(LOCAL_STORAGE_KEYS.INCIDENT_BANNER_DISMISSED_IDS, [])
60
61 const dismiss = useCallback(() => {
62 const activeIncidentIds = new Set(incidents.map((i) => i.id))
63 const relevantIds = getRelevantIncidentIds({
64 incidents,
65 hasProjects,
66 userRegions,
67 hasUnknownRegions,
68 })
69 setDismissedIds((prev) => [
70 ...new Set([...prev.filter((id) => activeIncidentIds.has(id)), ...relevantIds]),
71 ])
72 }, [incidents, hasProjects, userRegions, hasUnknownRegions, setDismissedIds])
73
74 if (showIncidentBannerOverride) return { title: 'We are investigating a technical issue' }
75
76 if (!hasActiveIncidents || !isProjectsFetched) return null
77
78 const dismissedIdSet = new Set(dismissedIds)
79 const undismissedIncidents = incidents.filter((i) => !dismissedIdSet.has(i.id))
80
81 if (
82 !isDismissedLoaded ||
83 !shouldShowBanner({
84 incidents: undismissedIncidents,
85 hasProjects,
86 userRegions,
87 hasUnknownRegions,
88 })
89 )
90 return null
91
92 const title = hasProjects
93 ? 'We are investigating a technical issue'
94 : 'Project creation may be impacted in some regions'
95
96 return { title, dismiss }
97}