ServiceStatus.tsx333 lines · main
1import { useParams } from 'common'
2import dayjs from 'dayjs'
3import { ChevronRight, Loader2 } from 'lucide-react'
4import Link from 'next/link'
5import { cn, HoverCard, HoverCardContent, HoverCardTrigger, InfoIcon } from 'ui'
6
7import {
8 extractDbSchema,
9 ProjectServiceStatus,
10 StatusIcon,
11 StatusMessage,
12} from '../Home/ServiceStatus'
13import { InlineLink } from '@/components/ui/InlineLink'
14import { SingleStat } from '@/components/ui/SingleStat'
15import { useBranchesQuery } from '@/data/branches/branches-query'
16import { useEdgeFunctionServiceStatusQuery } from '@/data/service-status/edge-functions-status-query'
17import { useProjectServiceStatusQuery } from '@/data/service-status/service-status-query'
18import { useIsFeatureEnabled } from '@/hooks/misc/useIsFeatureEnabled'
19import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
20import { DOCS_URL } from '@/lib/constants'
21
22const SERVICE_STATUS_THRESHOLD = 5 // minutes
23
24/**
25 * [Joshen] JFYI before we go live with this, we need to revisit the migrations section
26 * as I don't think it should live in the ServiceStatus component since its not indicative
27 * of a project's "service". ServiceStatus's intention is to be an ongoing health/status check.
28 *
29 * For context, migrations are meant to be indicative for only when creating branches or projects
30 * with an initial SQL, so "healthy" migrations just means that migrations have all been successfully
31 * ran. So it might be a matter of decoupling "ready" state vs "health checks"
32 * [Edit] Now that migrations are only showing up if the project is a branch, i think its okay for now
33 *
34 * [Joshen] Another issue that requires investigation before we go live with the changes:
35 * We've removed the isProjectNew check in this component which we had that logic cause new
36 * projects would show unhealthy as the services are still starting up - but it causes a
37 * perceived negative impression as new projects were showing unhealthy, hence the 5 minute
38 * threshold check (we’d show “Coming up” instead of “unhealthy” if the project is within 5
39 * minutes of when it was created). Might be related to decoupling "ready" state vs "health checks"
40 */
41
42export const ServiceStatus = () => {
43 const { ref } = useParams()
44 const { data: project } = useSelectedProjectQuery()
45
46 const {
47 projectAuthAll: authEnabled,
48 projectEdgeFunctionAll: edgeFunctionsEnabled,
49 realtimeAll: realtimeEnabled,
50 projectStorageAll: storageEnabled,
51 } = useIsFeatureEnabled([
52 'project_auth:all',
53 'project_edge_function:all',
54 'realtime:all',
55 'project_storage:all',
56 ])
57
58 const isBranch = project?.parentRef !== project?.ref
59
60 // Get branches data when on a branch
61 const { data: branches, isPending: isBranchesLoading } = useBranchesQuery(
62 { projectRef: isBranch ? project?.parentRef : undefined },
63 {
64 enabled: isBranch,
65 }
66 )
67
68 const currentBranch = isBranch
69 ? branches?.find((branch) => branch.project_ref === ref)
70 : undefined
71
72 // [Joshen] Need pooler service check eventually
73 const { data: status, isPending: isLoading } = useProjectServiceStatusQuery(
74 { projectRef: ref },
75 {
76 refetchInterval: (query) => {
77 const data = query.state.data
78 const isServiceUnhealthy = data?.some((service) => {
79 // if the postgrest service has an empty schema, postgrest has been disabled
80 if (service.name === 'rest' && extractDbSchema(service) === '') {
81 return false
82 }
83 if (service.status === 'ACTIVE_HEALTHY') {
84 return false
85 }
86 return true
87 })
88
89 return isServiceUnhealthy ? 5000 : false
90 },
91 }
92 )
93 const { data: edgeFunctionsStatus } = useEdgeFunctionServiceStatusQuery(
94 { projectRef: ref },
95 { refetchInterval: (query) => (!query.state.data?.healthy ? 5000 : false) }
96 )
97
98 const authStatus = status?.find((service) => service.name === 'auth')
99 const restStatus = status?.find((service) => service.name === 'rest')
100 const realtimeStatus = status?.find((service) => service.name === 'realtime')
101 const storageStatus = status?.find((service) => service.name === 'storage')
102 const dbStatus = status?.find((service) => service.name === 'db')
103
104 const isMigrationLoading =
105 project?.status === 'COMING_UP' ||
106 (isBranch &&
107 (isBranchesLoading ||
108 currentBranch?.status === 'CREATING_PROJECT' ||
109 currentBranch?.status === 'RUNNING_MIGRATIONS'))
110
111 // [Joshen] Need individual troubleshooting docs for each service eventually for users to self serve
112 const services: {
113 name: string
114 error?: string
115 docsUrl?: string
116 isLoading: boolean
117 status: ProjectServiceStatus
118 logsUrl: string
119 }[] = [
120 {
121 name: 'Database',
122 error: undefined,
123 docsUrl: undefined,
124 isLoading: isLoading,
125 status: dbStatus?.status ?? 'UNHEALTHY',
126 logsUrl: '/logs/postgres-logs',
127 },
128 {
129 name: 'PostgREST',
130 error: restStatus?.error,
131 docsUrl: undefined,
132 isLoading,
133 // If PostgREST has an empty schema, it means it's been disabled
134 status: extractDbSchema(restStatus) === '' ? 'DISABLED' : (restStatus?.status ?? 'UNHEALTHY'),
135 logsUrl: '/logs/postgrest-logs',
136 },
137 ...(authEnabled
138 ? [
139 {
140 name: 'Auth',
141 error: authStatus?.error,
142 docsUrl: undefined,
143 isLoading,
144 status: authStatus?.status ?? 'UNHEALTHY',
145 logsUrl: '/logs/auth-logs',
146 },
147 ]
148 : []),
149 ...(realtimeEnabled
150 ? [
151 {
152 name: 'Realtime',
153 error: realtimeStatus?.error,
154 docsUrl: undefined,
155 isLoading,
156 status: realtimeStatus?.status ?? 'UNHEALTHY',
157 logsUrl: '/logs/realtime-logs',
158 },
159 ]
160 : []),
161 ...(storageEnabled
162 ? [
163 {
164 name: 'Storage',
165 error: storageStatus?.error,
166 docsUrl: undefined,
167 isLoading,
168 status: storageStatus?.status ?? 'UNHEALTHY',
169 logsUrl: '/logs/storage-logs',
170 },
171 ]
172 : []),
173 ...(edgeFunctionsEnabled
174 ? [
175 {
176 name: 'Edge Functions',
177 error: undefined,
178 docsUrl: `${DOCS_URL}/guides/functions/troubleshooting`,
179 isLoading,
180 status: edgeFunctionsStatus?.healthy
181 ? ('ACTIVE_HEALTHY' as const)
182 : isLoading
183 ? ('COMING_UP' as const)
184 : ('UNHEALTHY' as const),
185 logsUrl: '/logs/edge-functions-logs',
186 },
187 ]
188 : []),
189 ...(isBranch
190 ? [
191 {
192 name: 'Migrations',
193 error: undefined,
194 docsUrl: undefined,
195 isLoading: isBranchesLoading,
196 status: isBranch
197 ? currentBranch?.status === 'FUNCTIONS_DEPLOYED'
198 ? ('ACTIVE_HEALTHY' as const)
199 : currentBranch?.status === 'FUNCTIONS_FAILED' ||
200 currentBranch?.status === 'MIGRATIONS_FAILED'
201 ? ('UNHEALTHY' as const)
202 : ('COMING_UP' as const)
203 : isMigrationLoading
204 ? 'COMING_UP'
205 : 'ACTIVE_HEALTHY',
206 logsUrl: isBranch ? '/branches' : '/logs/database-logs',
207 },
208 ]
209 : []),
210 ]
211
212 const isLoadingChecks = services.some((service) => service.isLoading)
213 // We consider a service operational if it's healthy or intentionally disabled
214 const allServicesOperational = services.every(
215 (service) => service.status === 'ACTIVE_HEALTHY' || service.status === 'DISABLED'
216 )
217
218 // Check if project or branch is in a startup state
219 const isProjectNew =
220 dayjs.utc().diff(dayjs.utc(project?.inserted_at), 'minute') < SERVICE_STATUS_THRESHOLD ||
221 project?.status === 'COMING_UP' ||
222 (isBranch &&
223 (currentBranch?.status === 'CREATING_PROJECT' ||
224 currentBranch?.status === 'RUNNING_MIGRATIONS' ||
225 isMigrationLoading))
226
227 const isProjectComingUp = ['COMING_UP', 'UNKNOWN'].includes(project?.status ?? '')
228
229 const anyUnhealthy = services.some((service) => service.status === 'UNHEALTHY')
230 const anyComingUp =
231 isProjectComingUp || services.some((service) => service.status === 'COMING_UP')
232
233 const getOverallStatusLabel = (): string => {
234 if (isLoadingChecks) return 'Checking...'
235 if (anyComingUp) return 'Coming up...'
236 if (anyUnhealthy) return 'Unhealthy'
237 return 'Healthy'
238 }
239
240 const overallStatusLabel = getOverallStatusLabel()
241
242 return (
243 <HoverCard openDelay={200} closeDelay={100}>
244 <HoverCardTrigger>
245 <SingleStat
246 icon={
247 // Spinner only while the overall project is in COMING_UP; otherwise show 6-dot grid
248 isProjectComingUp ? (
249 <Loader2 className="animate-spin" size={18} />
250 ) : (
251 <div className="grid grid-cols-3 gap-1">
252 {services.map((service, index) => (
253 <div
254 key={`${service.name}-${index}`}
255 className={cn(
256 'w-1.5 h-1.5 rounded-full',
257 service.isLoading ||
258 service.status === 'COMING_UP' ||
259 (isProjectNew && service.status !== 'ACTIVE_HEALTHY')
260 ? 'bg-foreground-lighter animate-pulse'
261 : service.status === 'ACTIVE_HEALTHY'
262 ? 'bg-brand'
263 : 'bg-selection'
264 )}
265 />
266 ))}
267 </div>
268 )
269 }
270 label={<span>Status</span>}
271 value={<span>{overallStatusLabel}</span>}
272 />
273 </HoverCardTrigger>
274 <HoverCardContent className="p-0 w-60" side="bottom" align="start">
275 {services.map((service) => (
276 <Link
277 href={`/project/${ref}${service.logsUrl}`}
278 key={service.name}
279 className="transition px-3 py-2 text-xs flex items-center justify-between border-b last:border-none group relative hover:bg-surface-300"
280 >
281 <div className="flex gap-x-2">
282 <StatusIcon
283 isLoading={service.isLoading}
284 isProjectNew={isProjectNew}
285 projectStatus={service.status}
286 />
287 <div className="flex-1">
288 <p>{service.name}</p>
289 <p className="text-foreground-light flex items-center gap-1">
290 <StatusMessage
291 isLoading={service.isLoading}
292 isProjectNew={isProjectNew}
293 status={
294 isProjectComingUp && service.status === 'UNHEALTHY'
295 ? 'COMING_UP'
296 : service.status
297 }
298 />
299 </p>
300 </div>
301 </div>
302 <div className="flex items-center gap-x-1 transition opacity-0 group-hover:opacity-100">
303 <span className="text-xs text-foreground">View logs</span>
304 <ChevronRight size={14} className="text-foreground" />
305 </div>
306 </Link>
307 ))}
308 {!allServicesOperational && (
309 <div className="flex gap-2 text-xs text-foreground-light px-3 py-2">
310 <div className="mt-0.5">
311 <InfoIcon />
312 </div>
313 <div className="flex flex-col gap-y-1">
314 <p>
315 {isProjectNew ? 'New' : 'Recently restored'} projects can take up to{' '}
316 {SERVICE_STATUS_THRESHOLD} minutes to become fully operational.
317 </p>
318 <p>
319 If services stay unhealthy, refer to our{' '}
320 <InlineLink
321 href={`${DOCS_URL}/guides/troubleshooting/project-status-reports-unhealthy-services`}
322 >
323 docs
324 </InlineLink>{' '}
325 for more information.
326 </p>
327 </div>
328 </div>
329 )}
330 </HoverCardContent>
331 </HoverCard>
332 )
333}