From 01c6b56a7c517ee4dc1bcab486d28c9f4f032bba Mon Sep 17 00:00:00 2001 From: Ivan Vasilov Date: Thu, 18 Dec 2025 18:00:20 +0200 Subject: [PATCH] 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. --- .../interfaces/Home/ServiceStatus.tsx | 67 ++++++---- .../interfaces/HomeNew/ServiceStatus.tsx | 117 ++++++------------ .../service-status/service-status-query.ts | 12 +- 3 files changed, 92 insertions(+), 104 deletions(-) diff --git a/apps/studio/components/interfaces/Home/ServiceStatus.tsx b/apps/studio/components/interfaces/Home/ServiceStatus.tsx index a2e768ac0fe..69a57ca1a8d 100644 --- a/apps/studio/components/interfaces/Home/ServiceStatus.tsx +++ b/apps/studio/components/interfaces/Home/ServiceStatus.tsx @@ -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 = () => const AlertIcon = () => const CheckIcon = () => -const StatusIcon = ({ +export const StatusIcon = ({ isLoading, - isHealthy, isProjectNew, projectStatus, }: { isLoading: boolean - isHealthy: boolean isProjectNew: boolean projectStatus?: ProjectServiceStatus }) => { - if (isHealthy) return - if (isLoading) return - if (projectStatus === 'UNHEALTHY') return - if (projectStatus === 'COMING_UP') return + // if (projectStatus === 'ACTIVE_HEALTHY') return + if (projectStatus === 'DISABLED') return + if (projectStatus === 'COMING_UP') return + if (isLoading) return + // 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 + if (projectStatus === 'UNHEALTHY') return return } +/* + * 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 = () => {
@@ -325,7 +343,6 @@ export const ServiceStatus = () => {

diff --git a/apps/studio/components/interfaces/HomeNew/ServiceStatus.tsx b/apps/studio/components/interfaces/HomeNew/ServiceStatus.tsx index 7b8dd1706c9..810f3eb0d07 100644 --- a/apps/studio/components/interfaces/HomeNew/ServiceStatus.tsx +++ b/apps/studio/components/interfaces/HomeNew/ServiceStatus.tsx @@ -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 = () => -const AlertIcon = () => -const CheckIcon = () => - -const StatusIcon = ({ - isLoading, - isHealthy, - isProjectNew, - projectStatus, -}: { - isLoading: boolean - isHealthy: boolean - isProjectNew: boolean - projectStatus?: ProjectServiceStatus -}) => { - if (isHealthy || projectStatus === 'ACTIVE_HEALTHY') return - if (isLoading || isProjectNew || projectStatus === 'COMING_UP') return - return -} - 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 = () => {

@@ -323,7 +287,6 @@ export const ServiceStatus = () => {

diff --git a/apps/studio/data/service-status/service-status-query.ts b/apps/studio/data/service-status/service-status-query.ts index 38d481bbdd6..4b8f2d3c422 100644 --- a/apps/studio/data/service-status/service-status-query.ts +++ b/apps/studio/data/service-status/service-status-query.ts @@ -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> -export type ProjectServiceStatus = ProjectServiceStatusData[0]['status'] export type ProjectServiceStatusError = ResponseError export const useProjectServiceStatusQuery = (