useStatusPageBannerVisibility.ts 3.4 KB

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