mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 17:35:10 +03:00
feat: Add disabled state for PostgREST (#39625)
* Fix the type for the project service status query. * Fix the statuses math in ServiceStatus for services health. * Minor fixes. * Fix minor type issues.
This commit is contained in:
1 parent
d247cc0c9d
commit
01c6b56a7c
3 files changed
+92
-104
No files matched your search
@@ -8,7 +8,8 @@ import { InlineLink } from 'components/ui/InlineLink'
|
||||
import { useBranchesQuery } from 'data/branches/branches-query'
|
||||
import { useEdgeFunctionServiceStatusQuery } from 'data/service-status/edge-functions-status-query'
|
||||
import {
|
||||
ProjectServiceStatus,
|
||||
ProjectServiceStatus as APIProjectServiceStatus,
|
||||
ServiceHealthResponse,
|
||||
useProjectServiceStatusQuery,
|
||||
} from 'data/service-status/service-status-query'
|
||||
import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled'
|
||||
@@ -24,22 +25,23 @@ import {
|
||||
|
||||
const SERVICE_STATUS_THRESHOLD = 5 // minutes
|
||||
|
||||
const StatusMessage = ({
|
||||
export type ProjectServiceStatus = APIProjectServiceStatus | 'DISABLED'
|
||||
|
||||
export const StatusMessage = ({
|
||||
status,
|
||||
isLoading,
|
||||
isHealthy,
|
||||
isProjectNew,
|
||||
}: {
|
||||
isLoading: boolean
|
||||
isHealthy: boolean
|
||||
isProjectNew: boolean
|
||||
status?: ProjectServiceStatus
|
||||
}) => {
|
||||
if (isHealthy) return 'Healthy'
|
||||
if (isLoading) return 'Checking status'
|
||||
if (status === 'DISABLED') return 'Disabled'
|
||||
if (status === 'UNHEALTHY') return 'Unhealthy'
|
||||
if (status === 'COMING_UP') return 'Coming up...'
|
||||
if (status === 'ACTIVE_HEALTHY') return 'Healthy'
|
||||
// isProjectNew has to be after all other statuses
|
||||
if (isProjectNew) return 'Coming up...'
|
||||
if (status) return status
|
||||
return 'Unable to connect'
|
||||
@@ -53,26 +55,36 @@ const LoaderIcon = () => <Loader2 {...iconProps} className="animate-spin" />
|
||||
const AlertIcon = () => <AlertTriangle {...iconProps} />
|
||||
const CheckIcon = () => <CheckCircle2 {...iconProps} className="text-brand" />
|
||||
|
||||
const StatusIcon = ({
|
||||
export const StatusIcon = ({
|
||||
isLoading,
|
||||
isHealthy,
|
||||
isProjectNew,
|
||||
projectStatus,
|
||||
}: {
|
||||
isLoading: boolean
|
||||
isHealthy: boolean
|
||||
isProjectNew: boolean
|
||||
projectStatus?: ProjectServiceStatus
|
||||
}) => {
|
||||
if (isHealthy) return <CheckIcon />
|
||||
if (isLoading) return <LoaderIcon />
|
||||
if (projectStatus === 'UNHEALTHY') return <AlertIcon />
|
||||
if (projectStatus === 'COMING_UP') return <LoaderIcon />
|
||||
//
|
||||
if (projectStatus === 'ACTIVE_HEALTHY') return <CheckIcon />
|
||||
if (projectStatus === 'DISABLED') return <AlertIcon />
|
||||
if (projectStatus === 'COMING_UP') return <LoaderIcon />
|
||||
if (isLoading) return <LoaderIcon />
|
||||
// isProjectNew has to be above UNHEALTHY because in the first few minutes, some services might be starting up and show as UNHEALTHY
|
||||
if (isProjectNew) return <LoaderIcon />
|
||||
if (projectStatus === 'UNHEALTHY') return <AlertIcon />
|
||||
return <AlertIcon />
|
||||
}
|
||||
|
||||
/*
|
||||
* Extract the db_schema from the response.info object
|
||||
*/
|
||||
export const extractDbSchema = (response: ServiceHealthResponse | undefined) => {
|
||||
if (response?.info && 'db_schema' in response.info) {
|
||||
return response.info.db_schema
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
export const ServiceStatus = () => {
|
||||
const { ref } = useParams()
|
||||
const { data: project } = useSelectedProjectQuery()
|
||||
@@ -126,7 +138,18 @@ export const ServiceStatus = () => {
|
||||
{
|
||||
refetchInterval: (query) => {
|
||||
const data = query.state.data
|
||||
return data?.some((service) => !service.healthy) ? 5000 : false
|
||||
const isServiceUnhealthy = data?.some((service) => {
|
||||
// if the postgrest service has an empty schema, the user chose to turn off postgrest during project creation
|
||||
if (service.name === 'rest' && extractDbSchema(service) === '') {
|
||||
return false
|
||||
}
|
||||
if (service.status === 'ACTIVE_HEALTHY') {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
return isServiceUnhealthy ? 5000 : false
|
||||
},
|
||||
}
|
||||
)
|
||||
@@ -155,7 +178,6 @@ export const ServiceStatus = () => {
|
||||
error?: string
|
||||
docsUrl?: string
|
||||
isLoading: boolean
|
||||
isHealthy: boolean
|
||||
status: ProjectServiceStatus
|
||||
logsUrl: string
|
||||
}[] = [
|
||||
@@ -164,7 +186,6 @@ export const ServiceStatus = () => {
|
||||
error: undefined,
|
||||
docsUrl: undefined,
|
||||
isLoading: isLoading,
|
||||
isHealthy: !!dbStatus?.healthy,
|
||||
status: dbStatus?.status ?? 'UNHEALTHY',
|
||||
logsUrl: '/logs/postgres-logs',
|
||||
},
|
||||
@@ -173,8 +194,8 @@ export const ServiceStatus = () => {
|
||||
error: restStatus?.error,
|
||||
docsUrl: undefined,
|
||||
isLoading,
|
||||
isHealthy: !!restStatus?.healthy,
|
||||
status: restStatus?.status ?? 'UNHEALTHY',
|
||||
// If PostgREST has an empty schema, it means it's been disabled
|
||||
status: extractDbSchema(restStatus) === '' ? 'DISABLED' : restStatus?.status ?? 'UNHEALTHY',
|
||||
logsUrl: '/logs/postgrest-logs',
|
||||
},
|
||||
...(authEnabled
|
||||
@@ -184,7 +205,6 @@ export const ServiceStatus = () => {
|
||||
error: authStatus?.error,
|
||||
docsUrl: undefined,
|
||||
isLoading,
|
||||
isHealthy: !!authStatus?.healthy,
|
||||
status: authStatus?.status ?? 'UNHEALTHY',
|
||||
logsUrl: '/logs/auth-logs',
|
||||
},
|
||||
@@ -197,7 +217,6 @@ export const ServiceStatus = () => {
|
||||
error: realtimeStatus?.error,
|
||||
docsUrl: undefined,
|
||||
isLoading,
|
||||
isHealthy: !!realtimeStatus?.healthy,
|
||||
status: realtimeStatus?.status ?? 'UNHEALTHY',
|
||||
logsUrl: '/logs/realtime-logs',
|
||||
},
|
||||
@@ -210,7 +229,6 @@ export const ServiceStatus = () => {
|
||||
error: storageStatus?.error,
|
||||
docsUrl: undefined,
|
||||
isLoading,
|
||||
isHealthy: !!storageStatus?.healthy,
|
||||
status: storageStatus?.status ?? 'UNHEALTHY',
|
||||
logsUrl: '/logs/storage-logs',
|
||||
},
|
||||
@@ -223,7 +241,6 @@ export const ServiceStatus = () => {
|
||||
error: undefined,
|
||||
docsUrl: `${DOCS_URL}/guides/functions/troubleshooting`,
|
||||
isLoading,
|
||||
isHealthy: !!edgeFunctionsStatus?.healthy,
|
||||
status: edgeFunctionsStatus?.healthy
|
||||
? 'ACTIVE_HEALTHY'
|
||||
: isLoading
|
||||
@@ -240,7 +257,6 @@ export const ServiceStatus = () => {
|
||||
error: undefined,
|
||||
docsUrl: undefined,
|
||||
isLoading: isBranchesLoading,
|
||||
isHealthy: currentBranch?.status === 'FUNCTIONS_DEPLOYED',
|
||||
status: (currentBranch?.status === 'FUNCTIONS_DEPLOYED'
|
||||
? 'ACTIVE_HEALTHY'
|
||||
: currentBranch?.status === 'FUNCTIONS_FAILED' ||
|
||||
@@ -258,7 +274,10 @@ export const ServiceStatus = () => {
|
||||
currentBranch?.status === 'CREATING_PROJECT' ||
|
||||
currentBranch?.status === 'RUNNING_MIGRATIONS'
|
||||
const isLoadingChecks = services.some((service) => service.isLoading)
|
||||
const allServicesOperational = services.every((service) => service.isHealthy)
|
||||
// We consider a service operational if it's healthy or intentionally disabled
|
||||
const allServicesOperational = services.every(
|
||||
(service) => service.status === 'ACTIVE_HEALTHY' || service.status === 'DISABLED'
|
||||
)
|
||||
|
||||
// If the project is less than 5 minutes old, and status is not operational, then it's likely the service is still starting up
|
||||
const isProjectNew =
|
||||
@@ -316,7 +335,6 @@ export const ServiceStatus = () => {
|
||||
<div className="flex gap-x-2">
|
||||
<StatusIcon
|
||||
isLoading={service.isLoading}
|
||||
isHealthy={!!service.isHealthy}
|
||||
isProjectNew={isProjectNew}
|
||||
projectStatus={service.status}
|
||||
/>
|
||||
@@ -325,7 +343,6 @@ export const ServiceStatus = () => {
|
||||
<p className="text-foreground-light flex items-center gap-1">
|
||||
<StatusMessage
|
||||
isLoading={service.isLoading}
|
||||
isHealthy={!!service.isHealthy}
|
||||
isProjectNew={isProjectNew}
|
||||
status={service.status}
|
||||
/>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import dayjs from 'dayjs'
|
||||
import { AlertTriangle, CheckCircle2, ChevronRight, Loader2 } from 'lucide-react'
|
||||
import { ChevronRight, Loader2 } from 'lucide-react'
|
||||
import Link from 'next/link'
|
||||
|
||||
import { useParams } from 'common'
|
||||
@@ -7,14 +7,17 @@ import { InlineLink } from 'components/ui/InlineLink'
|
||||
import { SingleStat } from 'components/ui/SingleStat'
|
||||
import { useBranchesQuery } from 'data/branches/branches-query'
|
||||
import { useEdgeFunctionServiceStatusQuery } from 'data/service-status/edge-functions-status-query'
|
||||
import {
|
||||
ProjectServiceStatus,
|
||||
useProjectServiceStatusQuery,
|
||||
} from 'data/service-status/service-status-query'
|
||||
import { useProjectServiceStatusQuery } from 'data/service-status/service-status-query'
|
||||
import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled'
|
||||
import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
|
||||
import { DOCS_URL } from 'lib/constants'
|
||||
import { InfoIcon, PopoverContent_Shadcn_, PopoverTrigger_Shadcn_, Popover_Shadcn_, cn } from 'ui'
|
||||
import {
|
||||
ProjectServiceStatus,
|
||||
StatusIcon,
|
||||
StatusMessage,
|
||||
extractDbSchema,
|
||||
} from '../Home/ServiceStatus'
|
||||
|
||||
const SERVICE_STATUS_THRESHOLD = 5 // minutes
|
||||
|
||||
@@ -36,49 +39,6 @@ const SERVICE_STATUS_THRESHOLD = 5 // minutes
|
||||
* minutes of when it was created). Might be related to decoupling "ready" state vs "health checks"
|
||||
*/
|
||||
|
||||
const StatusMessage = ({
|
||||
status,
|
||||
isLoading,
|
||||
isHealthy,
|
||||
isProjectNew,
|
||||
}: {
|
||||
isLoading: boolean
|
||||
isHealthy: boolean
|
||||
isProjectNew: boolean
|
||||
status?: ProjectServiceStatus
|
||||
}) => {
|
||||
if (isHealthy || status === 'ACTIVE_HEALTHY') return 'Healthy'
|
||||
if (isLoading) return 'Checking status'
|
||||
if (status === 'UNHEALTHY') return 'Unhealthy'
|
||||
if (isProjectNew || status === 'COMING_UP') return 'Coming up...'
|
||||
if (status) return status
|
||||
return 'Unable to connect'
|
||||
}
|
||||
|
||||
const iconProps = {
|
||||
size: 18,
|
||||
strokeWidth: 1.5,
|
||||
}
|
||||
const LoaderIcon = () => <Loader2 {...iconProps} className="animate-spin" />
|
||||
const AlertIcon = () => <AlertTriangle {...iconProps} />
|
||||
const CheckIcon = () => <CheckCircle2 {...iconProps} className="text-brand" />
|
||||
|
||||
const StatusIcon = ({
|
||||
isLoading,
|
||||
isHealthy,
|
||||
isProjectNew,
|
||||
projectStatus,
|
||||
}: {
|
||||
isLoading: boolean
|
||||
isHealthy: boolean
|
||||
isProjectNew: boolean
|
||||
projectStatus?: ProjectServiceStatus
|
||||
}) => {
|
||||
if (isHealthy || projectStatus === 'ACTIVE_HEALTHY') return <CheckIcon />
|
||||
if (isLoading || isProjectNew || projectStatus === 'COMING_UP') return <LoaderIcon />
|
||||
return <AlertIcon />
|
||||
}
|
||||
|
||||
export const ServiceStatus = () => {
|
||||
const { ref } = useParams()
|
||||
const { data: project } = useSelectedProjectQuery()
|
||||
@@ -113,8 +73,21 @@ export const ServiceStatus = () => {
|
||||
const { data: status, isPending: isLoading } = useProjectServiceStatusQuery(
|
||||
{ projectRef: ref },
|
||||
{
|
||||
refetchInterval: (query) =>
|
||||
query.state.data?.some((service) => !service.healthy) ? 5000 : false,
|
||||
refetchInterval: (query) => {
|
||||
const data = query.state.data
|
||||
const isServiceUnhealthy = data?.some((service) => {
|
||||
// if the postgrest service has an empty schema, postgrest has been disabled
|
||||
if (service.name === 'rest' && extractDbSchema(service) === '') {
|
||||
return false
|
||||
}
|
||||
if (service.status === 'ACTIVE_HEALTHY') {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
return isServiceUnhealthy ? 5000 : false
|
||||
},
|
||||
}
|
||||
)
|
||||
const { data: edgeFunctionsStatus } = useEdgeFunctionServiceStatusQuery(
|
||||
@@ -141,7 +114,6 @@ export const ServiceStatus = () => {
|
||||
error?: string
|
||||
docsUrl?: string
|
||||
isLoading: boolean
|
||||
isHealthy: boolean
|
||||
status: ProjectServiceStatus
|
||||
logsUrl: string
|
||||
}[] = [
|
||||
@@ -150,7 +122,6 @@ export const ServiceStatus = () => {
|
||||
error: undefined,
|
||||
docsUrl: undefined,
|
||||
isLoading: isLoading,
|
||||
isHealthy: !!dbStatus?.healthy,
|
||||
status: dbStatus?.status ?? 'UNHEALTHY',
|
||||
logsUrl: '/logs/postgres-logs',
|
||||
},
|
||||
@@ -159,8 +130,8 @@ export const ServiceStatus = () => {
|
||||
error: restStatus?.error,
|
||||
docsUrl: undefined,
|
||||
isLoading,
|
||||
isHealthy: !!restStatus?.healthy,
|
||||
status: restStatus?.status ?? 'UNHEALTHY',
|
||||
// If PostgREST has an empty schema, it means it's been disabled
|
||||
status: extractDbSchema(restStatus) === '' ? 'DISABLED' : restStatus?.status ?? 'UNHEALTHY',
|
||||
logsUrl: '/logs/postgrest-logs',
|
||||
},
|
||||
...(authEnabled
|
||||
@@ -170,7 +141,6 @@ export const ServiceStatus = () => {
|
||||
error: authStatus?.error,
|
||||
docsUrl: undefined,
|
||||
isLoading,
|
||||
isHealthy: !!authStatus?.healthy,
|
||||
status: authStatus?.status ?? 'UNHEALTHY',
|
||||
logsUrl: '/logs/auth-logs',
|
||||
},
|
||||
@@ -183,7 +153,6 @@ export const ServiceStatus = () => {
|
||||
error: realtimeStatus?.error,
|
||||
docsUrl: undefined,
|
||||
isLoading,
|
||||
isHealthy: !!realtimeStatus?.healthy,
|
||||
status: realtimeStatus?.status ?? 'UNHEALTHY',
|
||||
logsUrl: '/logs/realtime-logs',
|
||||
},
|
||||
@@ -196,7 +165,6 @@ export const ServiceStatus = () => {
|
||||
error: storageStatus?.error,
|
||||
docsUrl: undefined,
|
||||
isLoading,
|
||||
isHealthy: !!storageStatus?.healthy,
|
||||
status: storageStatus?.status ?? 'UNHEALTHY',
|
||||
logsUrl: '/logs/storage-logs',
|
||||
},
|
||||
@@ -209,12 +177,11 @@ export const ServiceStatus = () => {
|
||||
error: undefined,
|
||||
docsUrl: `${DOCS_URL}/guides/functions/troubleshooting`,
|
||||
isLoading,
|
||||
isHealthy: !!edgeFunctionsStatus?.healthy,
|
||||
status: edgeFunctionsStatus?.healthy
|
||||
? 'ACTIVE_HEALTHY'
|
||||
? ('ACTIVE_HEALTHY' as const)
|
||||
: isLoading
|
||||
? 'COMING_UP'
|
||||
: ('UNHEALTHY' as ProjectServiceStatus),
|
||||
? ('COMING_UP' as const)
|
||||
: ('UNHEALTHY' as const),
|
||||
logsUrl: '/logs/edge-functions-logs',
|
||||
},
|
||||
]
|
||||
@@ -226,19 +193,16 @@ export const ServiceStatus = () => {
|
||||
error: undefined,
|
||||
docsUrl: undefined,
|
||||
isLoading: isBranchesLoading,
|
||||
isHealthy: isBranch
|
||||
status: isBranch
|
||||
? currentBranch?.status === 'FUNCTIONS_DEPLOYED'
|
||||
: !isMigrationLoading,
|
||||
status: (isBranch
|
||||
? currentBranch?.status === 'FUNCTIONS_DEPLOYED'
|
||||
? 'ACTIVE_HEALTHY'
|
||||
? ('ACTIVE_HEALTHY' as const)
|
||||
: currentBranch?.status === 'FUNCTIONS_FAILED' ||
|
||||
currentBranch?.status === 'MIGRATIONS_FAILED'
|
||||
? 'UNHEALTHY'
|
||||
: 'COMING_UP'
|
||||
? ('UNHEALTHY' as const)
|
||||
: ('COMING_UP' as const)
|
||||
: isMigrationLoading
|
||||
? 'COMING_UP'
|
||||
: 'ACTIVE_HEALTHY') as ProjectServiceStatus,
|
||||
: 'ACTIVE_HEALTHY',
|
||||
logsUrl: isBranch ? '/branches' : '/logs/database-logs',
|
||||
},
|
||||
]
|
||||
@@ -246,7 +210,10 @@ export const ServiceStatus = () => {
|
||||
]
|
||||
|
||||
const isLoadingChecks = services.some((service) => service.isLoading)
|
||||
const allServicesOperational = services.every((service) => service.isHealthy)
|
||||
// We consider a service operational if it's healthy or intentionally disabled
|
||||
const allServicesOperational = services.every(
|
||||
(service) => service.status === 'ACTIVE_HEALTHY' || service.status === 'DISABLED'
|
||||
)
|
||||
|
||||
// Check if project or branch is in a startup state
|
||||
const isProjectNew =
|
||||
@@ -257,9 +224,7 @@ export const ServiceStatus = () => {
|
||||
currentBranch?.status === 'RUNNING_MIGRATIONS' ||
|
||||
isMigrationLoading))
|
||||
|
||||
const anyUnhealthy = services.some(
|
||||
(service) => !service.isHealthy && service.status !== 'COMING_UP'
|
||||
)
|
||||
const anyUnhealthy = services.some((service) => service.status === 'UNHEALTHY')
|
||||
const anyComingUp = services.some((service) => service.status === 'COMING_UP')
|
||||
// Spinner only while the overall project is in COMING_UP; otherwise show 6-dot grid
|
||||
const showSpinnerIcon = project?.status === 'COMING_UP'
|
||||
@@ -289,9 +254,9 @@ export const ServiceStatus = () => {
|
||||
'w-1.5 h-1.5 rounded-full',
|
||||
service.isLoading ||
|
||||
service.status === 'COMING_UP' ||
|
||||
(isProjectNew && !service.isHealthy)
|
||||
(isProjectNew && service.status !== 'ACTIVE_HEALTHY')
|
||||
? 'bg-foreground-lighter animate-pulse'
|
||||
: service.isHealthy
|
||||
: service.status === 'ACTIVE_HEALTHY'
|
||||
? 'bg-brand'
|
||||
: 'bg-selection'
|
||||
)}
|
||||
@@ -314,7 +279,6 @@ export const ServiceStatus = () => {
|
||||
<div className="flex gap-x-2">
|
||||
<StatusIcon
|
||||
isLoading={service.isLoading}
|
||||
isHealthy={!!service.isHealthy}
|
||||
isProjectNew={isProjectNew}
|
||||
projectStatus={service.status}
|
||||
/>
|
||||
@@ -323,7 +287,6 @@ export const ServiceStatus = () => {
|
||||
<p className="text-foreground-light flex items-center gap-1">
|
||||
<StatusMessage
|
||||
isLoading={service.isLoading}
|
||||
isHealthy={!!service.isHealthy}
|
||||
isProjectNew={isProjectNew}
|
||||
status={service.status}
|
||||
/>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
|
||||
import { components } from 'api-types'
|
||||
import { get, handleError } from 'data/fetchers'
|
||||
import type { ResponseError, UseCustomQueryOptions } from 'types'
|
||||
import { serviceStatusKeys } from './keys'
|
||||
@@ -8,6 +9,13 @@ export type ProjectServiceStatusVariables = {
|
||||
projectRef?: string
|
||||
}
|
||||
|
||||
// Omit the 'healthy' field as it's equivalent to status = 'ACTIVE_HEALTHY'
|
||||
export type ServiceHealthResponse = Omit<
|
||||
components['schemas']['V1ServiceHealthResponse'],
|
||||
'healthy'
|
||||
>
|
||||
export type ProjectServiceStatus = ServiceHealthResponse['status']
|
||||
|
||||
export async function getProjectServiceStatus(
|
||||
{ projectRef }: ProjectServiceStatusVariables,
|
||||
signal?: AbortSignal
|
||||
@@ -25,11 +33,11 @@ export async function getProjectServiceStatus(
|
||||
})
|
||||
|
||||
if (error) handleError(error)
|
||||
return data
|
||||
|
||||
return data as ServiceHealthResponse[]
|
||||
}
|
||||
|
||||
export type ProjectServiceStatusData = Awaited<ReturnType<typeof getProjectServiceStatus>>
|
||||
export type ProjectServiceStatus = ProjectServiceStatusData[0]['status']
|
||||
export type ProjectServiceStatusError = ResponseError
|
||||
|
||||
export const useProjectServiceStatusQuery = <TData = ProjectServiceStatusData>(
|
||||
|
||||
Reference in new issue
Block a user