ServiceStatus.tsx 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377
  1. import { useParams } from 'common'
  2. import dayjs from 'dayjs'
  3. import { AlertTriangle, CheckCircle2, ChevronRight, Loader2 } from 'lucide-react'
  4. import Link from 'next/link'
  5. import { useEffect, useState } from 'react'
  6. import { Button, InfoIcon, Popover, PopoverContent, PopoverTrigger } from 'ui'
  7. import { InlineLink } from '@/components/ui/InlineLink'
  8. import { useBranchesQuery } from '@/data/branches/branches-query'
  9. import { useEdgeFunctionServiceStatusQuery } from '@/data/service-status/edge-functions-status-query'
  10. import {
  11. ProjectServiceStatus as APIProjectServiceStatus,
  12. ServiceHealthResponse,
  13. useProjectServiceStatusQuery,
  14. } from '@/data/service-status/service-status-query'
  15. import { useIsFeatureEnabled } from '@/hooks/misc/useIsFeatureEnabled'
  16. import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
  17. import { DOCS_URL } from '@/lib/constants'
  18. const SERVICE_STATUS_THRESHOLD = 5 // minutes
  19. export type ProjectServiceStatus = APIProjectServiceStatus | 'DISABLED'
  20. export const StatusMessage = ({
  21. status,
  22. isLoading,
  23. isProjectNew,
  24. }: {
  25. isLoading: boolean
  26. isProjectNew: boolean
  27. status?: ProjectServiceStatus
  28. }) => {
  29. if (isLoading) return 'Checking status'
  30. if (status === 'DISABLED') return 'Disabled'
  31. if (status === 'UNHEALTHY') return 'Unhealthy'
  32. if (status === 'COMING_UP') return 'Coming up...'
  33. if (status === 'ACTIVE_HEALTHY') return 'Healthy'
  34. // isProjectNew has to be after all other statuses
  35. if (isProjectNew) return 'Coming up...'
  36. if (status) return status
  37. return 'Unable to connect'
  38. }
  39. const iconProps = {
  40. size: 18,
  41. strokeWidth: 1.5,
  42. }
  43. const LoaderIcon = () => <Loader2 {...iconProps} className="animate-spin" />
  44. const AlertIcon = () => <AlertTriangle {...iconProps} />
  45. const CheckIcon = () => <CheckCircle2 {...iconProps} className="text-brand" />
  46. export const StatusIcon = ({
  47. isLoading,
  48. isProjectNew,
  49. projectStatus,
  50. }: {
  51. isLoading: boolean
  52. isProjectNew: boolean
  53. projectStatus?: ProjectServiceStatus
  54. }) => {
  55. //
  56. if (projectStatus === 'ACTIVE_HEALTHY') return <CheckIcon />
  57. if (projectStatus === 'DISABLED') return <AlertIcon />
  58. if (projectStatus === 'COMING_UP') return <LoaderIcon />
  59. if (isLoading) return <LoaderIcon />
  60. // isProjectNew has to be above UNHEALTHY because in the first few minutes, some services might be starting up and show as UNHEALTHY
  61. if (isProjectNew) return <LoaderIcon />
  62. if (projectStatus === 'UNHEALTHY') return <AlertIcon />
  63. return <AlertIcon />
  64. }
  65. /*
  66. * Extract the db_schema from the response.info object
  67. */
  68. export const extractDbSchema = (response: ServiceHealthResponse | undefined) => {
  69. if (response?.info && 'db_schema' in response.info) {
  70. return response.info.db_schema
  71. }
  72. return undefined
  73. }
  74. export const ServiceStatus = () => {
  75. const { ref } = useParams()
  76. const { data: project } = useSelectedProjectQuery()
  77. const [open, setOpen] = useState(false)
  78. const {
  79. projectAuthAll: authEnabled,
  80. projectEdgeFunctionAll: edgeFunctionsEnabled,
  81. realtimeAll: realtimeEnabled,
  82. projectStorageAll: storageEnabled,
  83. } = useIsFeatureEnabled([
  84. 'project_auth:all',
  85. 'project_edge_function:all',
  86. 'realtime:all',
  87. 'project_storage:all',
  88. ])
  89. const isBranch = project?.parentRef !== project?.ref
  90. // Get branches data when on a branch
  91. const { data: branches, isPending: isBranchesLoading } = useBranchesQuery(
  92. { projectRef: isBranch ? project?.parentRef : undefined },
  93. {
  94. enabled: isBranch,
  95. refetchInterval: (query) => {
  96. const data = query.state.data
  97. if (!data) return false
  98. const currentBranch = data.find((branch) => branch.project_ref === ref)
  99. return ['FUNCTIONS_DEPLOYED', 'MIGRATIONS_FAILED', 'FUNCTIONS_FAILED'].includes(
  100. currentBranch?.status || ''
  101. )
  102. ? false
  103. : 5000
  104. },
  105. }
  106. )
  107. const currentBranch = isBranch
  108. ? branches?.find((branch) => branch.project_ref === ref)
  109. : undefined
  110. // [Joshen] Need pooler service check eventually
  111. const {
  112. data: status,
  113. isPending: isLoading,
  114. refetch: refetchServiceStatus,
  115. } = useProjectServiceStatusQuery(
  116. {
  117. projectRef: ref,
  118. },
  119. {
  120. refetchInterval: (query) => {
  121. const data = query.state.data
  122. const isServiceUnhealthy = data?.some((service) => {
  123. // if the postgrest service has an empty schema, the user chose to turn off postgrest during project creation
  124. if (service.name === 'rest' && extractDbSchema(service) === '') {
  125. return false
  126. }
  127. if (service.status === 'ACTIVE_HEALTHY') {
  128. return false
  129. }
  130. return true
  131. })
  132. return isServiceUnhealthy ? 5000 : false
  133. },
  134. }
  135. )
  136. const { data: edgeFunctionsStatus, refetch: refetchEdgeFunctionServiceStatus } =
  137. useEdgeFunctionServiceStatusQuery(
  138. {
  139. projectRef: ref,
  140. },
  141. {
  142. refetchInterval: (query) => {
  143. const data = query.state.data
  144. return !data?.healthy ? 5000 : false
  145. },
  146. }
  147. )
  148. const authStatus = status?.find((service) => service.name === 'auth')
  149. const restStatus = status?.find((service) => service.name === 'rest')
  150. const realtimeStatus = status?.find((service) => service.name === 'realtime')
  151. const storageStatus = status?.find((service) => service.name === 'storage')
  152. const dbStatus = status?.find((service) => service.name === 'db')
  153. // [Joshen] Need individual troubleshooting docs for each service eventually for users to self serve
  154. const services: {
  155. name: string
  156. error?: string
  157. docsUrl?: string
  158. isLoading: boolean
  159. status: ProjectServiceStatus
  160. logsUrl: string
  161. }[] = [
  162. {
  163. name: 'Database',
  164. error: undefined,
  165. docsUrl: undefined,
  166. isLoading: isLoading,
  167. status: dbStatus?.status ?? 'UNHEALTHY',
  168. logsUrl: '/logs/postgres-logs',
  169. },
  170. {
  171. name: 'PostgREST',
  172. error: restStatus?.error,
  173. docsUrl: undefined,
  174. isLoading,
  175. // If PostgREST has an empty schema, it means it's been disabled
  176. status: extractDbSchema(restStatus) === '' ? 'DISABLED' : (restStatus?.status ?? 'UNHEALTHY'),
  177. logsUrl: '/logs/postgrest-logs',
  178. },
  179. ...(authEnabled
  180. ? [
  181. {
  182. name: 'Auth',
  183. error: authStatus?.error,
  184. docsUrl: undefined,
  185. isLoading,
  186. status: authStatus?.status ?? 'UNHEALTHY',
  187. logsUrl: '/logs/auth-logs',
  188. },
  189. ]
  190. : []),
  191. ...(realtimeEnabled
  192. ? [
  193. {
  194. name: 'Realtime',
  195. error: realtimeStatus?.error,
  196. docsUrl: undefined,
  197. isLoading,
  198. status: realtimeStatus?.status ?? 'UNHEALTHY',
  199. logsUrl: '/logs/realtime-logs',
  200. },
  201. ]
  202. : []),
  203. ...(storageEnabled
  204. ? [
  205. {
  206. name: 'Storage',
  207. error: storageStatus?.error,
  208. docsUrl: undefined,
  209. isLoading,
  210. status: storageStatus?.status ?? 'UNHEALTHY',
  211. logsUrl: '/logs/storage-logs',
  212. },
  213. ]
  214. : []),
  215. ...(edgeFunctionsEnabled
  216. ? [
  217. {
  218. name: 'Edge Functions',
  219. error: undefined,
  220. docsUrl: `${DOCS_URL}/guides/functions/troubleshooting`,
  221. isLoading,
  222. status: edgeFunctionsStatus?.healthy
  223. ? 'ACTIVE_HEALTHY'
  224. : isLoading
  225. ? 'COMING_UP'
  226. : ('UNHEALTHY' as ProjectServiceStatus),
  227. logsUrl: '/logs/edge-functions-logs',
  228. },
  229. ]
  230. : []),
  231. ...(isBranch
  232. ? [
  233. {
  234. name: 'Migrations',
  235. error: undefined,
  236. docsUrl: undefined,
  237. isLoading: isBranchesLoading,
  238. status: (currentBranch?.status === 'FUNCTIONS_DEPLOYED'
  239. ? 'ACTIVE_HEALTHY'
  240. : currentBranch?.status === 'FUNCTIONS_FAILED' ||
  241. currentBranch?.status === 'MIGRATIONS_FAILED'
  242. ? 'UNHEALTHY'
  243. : 'COMING_UP') as ProjectServiceStatus,
  244. logsUrl: '/branches',
  245. },
  246. ]
  247. : []),
  248. ]
  249. const isMigrationLoading =
  250. isBranchesLoading ||
  251. currentBranch?.status === 'CREATING_PROJECT' ||
  252. currentBranch?.status === 'RUNNING_MIGRATIONS'
  253. const isLoadingChecks = services.some((service) => service.isLoading)
  254. // We consider a service operational if it's healthy or intentionally disabled
  255. const allServicesOperational = services.every(
  256. (service) => service.status === 'ACTIVE_HEALTHY' || service.status === 'DISABLED'
  257. )
  258. // If the project is less than 5 minutes old, and status is not operational, then it's likely the service is still starting up
  259. const isProjectNew =
  260. dayjs.utc().diff(dayjs.utc(project?.inserted_at), 'minute') < SERVICE_STATUS_THRESHOLD ||
  261. project?.status === 'COMING_UP'
  262. useEffect(() => {
  263. let timer: any
  264. if (isProjectNew) {
  265. const secondsSinceProjectCreated = dayjs
  266. .utc()
  267. .diff(dayjs.utc(project?.inserted_at), 'seconds')
  268. const remainingTimeTillNextCheck = SERVICE_STATUS_THRESHOLD * 60 - secondsSinceProjectCreated
  269. timer = setTimeout(() => {
  270. refetchServiceStatus()
  271. refetchEdgeFunctionServiceStatus()
  272. }, remainingTimeTillNextCheck * 1000)
  273. }
  274. return () => {
  275. clearTimeout(timer)
  276. }
  277. // eslint-disable-next-line react-hooks/exhaustive-deps
  278. }, [isProjectNew])
  279. return (
  280. <Popover modal={false} open={open} onOpenChange={setOpen}>
  281. <PopoverTrigger asChild>
  282. <Button
  283. type="default"
  284. icon={
  285. isLoadingChecks || (!allServicesOperational && isProjectNew && isMigrationLoading) ? (
  286. <LoaderIcon />
  287. ) : (
  288. <div
  289. className={`w-2 h-2 rounded-full ${
  290. allServicesOperational ? 'bg-brand' : 'bg-warning'
  291. }`}
  292. />
  293. )
  294. }
  295. >
  296. {isBranch ? 'Branch' : 'Project'} Status
  297. </Button>
  298. </PopoverTrigger>
  299. <PopoverContent className="p-0 w-56" side="bottom" align="center">
  300. {services.map((service) => (
  301. <Link
  302. href={`/project/${ref}${service.logsUrl}`}
  303. key={service.name}
  304. className="transition px-3 py-2 text-xs flex items-center justify-between border-b last:border-none group relative hover:bg-surface-300"
  305. >
  306. <div className="flex gap-x-2">
  307. <StatusIcon
  308. isLoading={service.isLoading}
  309. isProjectNew={isProjectNew}
  310. projectStatus={service.status}
  311. />
  312. <div className="flex-1">
  313. <p>{service.name}</p>
  314. <p className="text-foreground-light flex items-center gap-1">
  315. <StatusMessage
  316. isLoading={service.isLoading}
  317. isProjectNew={isProjectNew}
  318. status={service.status}
  319. />
  320. </p>
  321. </div>
  322. </div>
  323. <div className="flex items-center gap-x-1 transition opacity-0 group-hover:opacity-100">
  324. <span className="text-xs text-foreground">View logs</span>
  325. <ChevronRight size={14} className="text-foreground" />
  326. </div>
  327. </Link>
  328. ))}
  329. {!allServicesOperational && (
  330. <div className="flex gap-2 text-xs text-foreground-light px-3 py-2">
  331. <div className="mt-0.5">
  332. <InfoIcon />
  333. </div>
  334. <div className="flex flex-col gap-y-1">
  335. <p>
  336. {isProjectNew ? 'New' : 'Recently restored'} projects can take up to{' '}
  337. {SERVICE_STATUS_THRESHOLD} minutes to become fully operational.
  338. </p>
  339. <p>
  340. If services stay unhealthy, refer to our{' '}
  341. <InlineLink
  342. href={`${DOCS_URL}/guides/troubleshooting/project-status-reports-unhealthy-services`}
  343. >
  344. docs
  345. </InlineLink>{' '}
  346. for more information.
  347. </p>
  348. </div>
  349. </div>
  350. )}
  351. </PopoverContent>
  352. </Popover>
  353. )
  354. }