Files
supabase/apps/studio/components/interfaces/ProjectHome/ServiceStatus.tsx
T
Danny White ec53175b8a refactor(ui): rename text-brand to text-primary (#50564)
## What kind of change does this PR introduce?

Refactor. Follow-up to #49871.

## What is the current behavior?

Branded (green) text still uses the `text-brand` classname while the
colour comes from `--primary`.

## What is the new behavior?

**Rename-only:** `text-brand` → `text-primary` across callsites and
docs. Leftover `bg-brand` / `border-brand` alias to `brand-default`. No
intentional colour changes in this PR.

This better matches how we treat our green in other components and
props, like `variant="primary"` for green buttons.

## To test

On light mode: smoke-test that branded text still looks like #49871
(readable green, not the bright fill).

-
[Homepage](https://zone-www-dot-com-git-dnywh-depr-316-text-brand-de2380-supabase.vercel.app/):
“Scale to millions” uses `text-primary`
- [Docs
homepage](https://docs-git-dnywh-depr-316-text-brand-to-primary-supabase.vercel.app/docs):
branded links like “More on self-hosting” are still readable
- [Typography
docs](https://design-system-git-dnywh-depr-316-text-brand-to-primary-supabase.vercel.app/design-system/docs/typography):
documents `text-primary`
- [Colour
usage](https://design-system-git-dnywh-depr-316-text-brand-to-primary-supabase.vercel.app/design-system/docs/color-usage):
`text-primary` is visibly darker than `bg-brand-default` _on light mode_
- [Studio auth
providers](https://studio-staging-git-dnywh-depr-316-text-brand-to-60fa6c-supabase.vercel.app/dashboard/project/_/auth/providers):
enabled provider badge text readable; status dot stays bright green

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Style**
* Updated branded text, link hover states, icons, badges, indicators,
and highlighted content across the Design System, Docs, Studio, Learn,
UI Library, and marketing experiences to use the primary theme color.
* Updated syntax highlighting and table-of-contents styling for
consistent primary-color presentation.
  * Refined brand color fallback behavior for bright fills and borders.

* **Documentation**
* Updated color-usage and typography guidance to recommend the primary
text utility.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-09-22 16:19:03 +10:00

422 lines
15 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useParams } from 'common'
import dayjs from 'dayjs'
import { AlertTriangle, CheckCircle2, ChevronRight, Loader2, MinusCircle } from 'lucide-react'
import Link from 'next/link'
import { cn, HoverCard, HoverCardContent, HoverCardTrigger, InfoIcon } from 'ui'
import { useUnifiedLogsPreview } from '../App/FeaturePreview/FeaturePreviewContext'
import { resolveRealtimeServiceStatus, type ProjectServiceStatus } from './ServiceStatus.utils'
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 {
useProjectServiceStatusQuery,
type ServiceHealthResponse,
} from '@/data/service-status/service-status-query'
import { useHighAvailability } from '@/hooks/misc/useHighAvailability'
import { useIsFeatureEnabled } from '@/hooks/misc/useIsFeatureEnabled'
import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
import { DOCS_URL } from '@/lib/constants'
const SERVICE_STATUS_THRESHOLD = 5 // minutes
const SERVICE_ROW_CLASS =
'px-3 py-2 text-xs flex items-center justify-between border-b last:border-none'
const iconProps = {
size: 18,
strokeWidth: 1.5,
}
const LoaderIcon = () => <Loader2 {...iconProps} className="animate-spin" />
const AlertIcon = () => <AlertTriangle {...iconProps} />
const CheckIcon = () => <CheckCircle2 {...iconProps} className="text-primary" />
const DisabledIcon = () => <MinusCircle {...iconProps} className="text-foreground-lighter" />
export const StatusIcon = ({
isLoading,
isProjectNew,
projectStatus,
}: {
isLoading: boolean
isProjectNew: boolean
projectStatus?: ProjectServiceStatus
}) => {
//
if (projectStatus === 'ACTIVE_HEALTHY') return <CheckIcon />
if (projectStatus === 'DISABLED') return <DisabledIcon />
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 />
}
const StatusMessage = ({
status,
isLoading,
isProjectNew,
}: {
isLoading: boolean
isProjectNew: boolean
status?: ProjectServiceStatus
}) => {
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'
}
/*
* Extract the db_schema from the response.info object
*/
const extractDbSchema = (response: ServiceHealthResponse | undefined) => {
if (response?.info && 'db_schema' in response.info) {
return response.info.db_schema
}
return undefined
}
/**
* [Joshen] JFYI before we go live with this, we need to revisit the migrations section
* as I don't think it should live in the ServiceStatus component since its not indicative
* of a project's "service". ServiceStatus's intention is to be an ongoing health/status check.
*
* For context, migrations are meant to be indicative for only when creating branches or projects
* with an initial SQL, so "healthy" migrations just means that migrations have all been successfully
* ran. So it might be a matter of decoupling "ready" state vs "health checks"
* [Edit] Now that migrations are only showing up if the project is a branch, i think its okay for now
*
* [Joshen] Another issue that requires investigation before we go live with the changes:
* We've removed the isProjectNew check in this component which we had that logic cause new
* projects would show unhealthy as the services are still starting up - but it causes a
* perceived negative impression as new projects were showing unhealthy, hence the 5 minute
* threshold check (we’d show “Coming up” instead of “unhealthy” if the project is within 5
* minutes of when it was created). Might be related to decoupling "ready" state vs "health checks"
*/
export const ServiceStatus = () => {
const { ref } = useParams()
const { data: project } = useSelectedProjectQuery()
const { isHighAvailability } = useHighAvailability()
const { isEnabled: isUnifiedLogsEnabled } = useUnifiedLogsPreview()
const {
projectAuthAll: authEnabled,
projectEdgeFunctionAll: edgeFunctionsEnabled,
realtimeAll: realtimeEnabled,
projectStorageAll: storageEnabled,
} = useIsFeatureEnabled([
'project_auth:all',
'project_edge_function:all',
'realtime:all',
'project_storage:all',
])
const isBranch = project?.parentRef !== project?.ref
// Get branches data when on a branch
const { data: branches, isPending: isBranchesLoading } = useBranchesQuery(
{ projectRef: isBranch ? project?.parentRef : undefined },
{
enabled: isBranch,
}
)
const currentBranch = isBranch
? branches?.find((branch) => branch.project_ref === ref)
: undefined
// [Joshen] Need pooler service check eventually
const { data: status, isPending: isLoading } = useProjectServiceStatusQuery(
{ projectRef: ref },
{
refetchInterval: (query) => {
const data = query.state.data
const isServiceUnhealthy = data?.some((service) => {
if (isHighAvailability && service.name === 'realtime') {
return false
}
// 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(
{ projectRef: ref },
{ refetchInterval: (query) => (!query.state.data?.healthy ? 5000 : false) }
)
const authStatus = status?.find((service) => service.name === 'auth')
const restStatus = status?.find((service) => service.name === 'rest')
const realtimeStatus = status?.find((service) => service.name === 'realtime')
const storageStatus = status?.find((service) => service.name === 'storage')
const dbStatus = status?.find((service) => service.name === 'db')
const isMigrationLoading =
project?.status === 'COMING_UP' ||
(isBranch &&
(isBranchesLoading ||
currentBranch?.status === 'CREATING_PROJECT' ||
currentBranch?.status === 'RUNNING_MIGRATIONS'))
// [Joshen] Need individual troubleshooting docs for each service eventually for users to self serve
const services: {
name: string
error?: string
docsUrl?: string
isLoading: boolean
status: ProjectServiceStatus
logsUrl: string
}[] = [
{
name: 'Database',
error: undefined,
docsUrl: undefined,
isLoading: isLoading,
status: dbStatus?.status ?? 'UNHEALTHY',
logsUrl: isUnifiedLogsEnabled ? '/logs?filter=log_type:eq:postgres' : '/logs/postgres-logs',
},
{
name: 'PostgREST',
error: restStatus?.error,
docsUrl: undefined,
isLoading,
// If PostgREST has an empty schema, it means it's been disabled
status: extractDbSchema(restStatus) === '' ? 'DISABLED' : (restStatus?.status ?? 'UNHEALTHY'),
logsUrl: isUnifiedLogsEnabled ? '/logs?filter=log_type:eq:postgrest' : '/logs/postgrest-logs',
},
...(authEnabled
? [
{
name: 'Auth',
error: authStatus?.error,
docsUrl: undefined,
isLoading,
status: authStatus?.status ?? 'UNHEALTHY',
logsUrl: isUnifiedLogsEnabled ? '/logs?filter=log_type:eq:auth' : '/logs/auth-logs',
},
]
: []),
...(realtimeEnabled
? [
{
name: 'Realtime',
error: realtimeStatus?.error,
docsUrl: undefined,
isLoading,
status: resolveRealtimeServiceStatus(isHighAvailability, realtimeStatus?.status),
logsUrl: isUnifiedLogsEnabled
? '/logs?filter=log_type:eq:realtime'
: '/logs/realtime-logs',
},
]
: []),
...(storageEnabled
? [
{
name: 'Storage',
error: storageStatus?.error,
docsUrl: undefined,
isLoading,
status: storageStatus?.status ?? 'UNHEALTHY',
logsUrl: isUnifiedLogsEnabled
? '/logs?filter=log_type:eq:storage'
: '/logs/storage-logs',
},
]
: []),
...(edgeFunctionsEnabled
? [
{
name: 'Edge Functions',
error: undefined,
docsUrl: `${DOCS_URL}/guides/functions/troubleshooting`,
isLoading,
status: edgeFunctionsStatus?.healthy
? ('ACTIVE_HEALTHY' as const)
: isLoading
? ('COMING_UP' as const)
: ('UNHEALTHY' as const),
logsUrl: isUnifiedLogsEnabled
? '/logs?filter=log_type:eq:edge+function'
: '/logs/edge-functions-logs',
},
]
: []),
...(isBranch
? [
{
name: 'Migrations',
error: undefined,
docsUrl: undefined,
isLoading: isBranchesLoading,
status: isBranch
? currentBranch?.status === 'FUNCTIONS_DEPLOYED'
? ('ACTIVE_HEALTHY' as const)
: currentBranch?.status === 'FUNCTIONS_FAILED' ||
currentBranch?.status === 'MIGRATIONS_FAILED'
? ('UNHEALTHY' as const)
: ('COMING_UP' as const)
: isMigrationLoading
? 'COMING_UP'
: 'ACTIVE_HEALTHY',
logsUrl: isBranch ? '/branches' : '/logs/database-logs',
},
]
: []),
]
const isLoadingChecks = services.some((service) => service.isLoading)
// 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 =
dayjs.utc().diff(dayjs.utc(project?.inserted_at), 'minute') < SERVICE_STATUS_THRESHOLD ||
project?.status === 'COMING_UP' ||
(isBranch &&
(currentBranch?.status === 'CREATING_PROJECT' ||
currentBranch?.status === 'RUNNING_MIGRATIONS' ||
isMigrationLoading))
const isProjectComingUp = ['COMING_UP', 'UNKNOWN'].includes(project?.status ?? '')
const anyUnhealthy = services.some((service) => service.status === 'UNHEALTHY')
const anyComingUp =
isProjectComingUp || services.some((service) => service.status === 'COMING_UP')
const getOverallStatusLabel = (): string => {
if (isLoadingChecks) return 'Checking...'
if (anyComingUp) return 'Coming up...'
if (anyUnhealthy) return 'Unhealthy'
return 'Healthy'
}
const overallStatusLabel = getOverallStatusLabel()
return (
<HoverCard openDelay={200} closeDelay={100}>
<HoverCardTrigger tabIndex={0}>
<SingleStat
icon={
// Spinner only while the overall project is in COMING_UP; otherwise show 6-dot grid
isProjectComingUp ? (
<Loader2 className="animate-spin" size={18} />
) : (
<div className="grid grid-cols-3 gap-1">
{services.map((service, index) => (
<div
key={`${service.name}-${index}`}
className={cn(
'w-1.5 h-1.5 rounded-full',
service.isLoading ||
service.status === 'COMING_UP' ||
(isProjectNew && service.status !== 'ACTIVE_HEALTHY')
? 'bg-foreground-lighter animate-pulse'
: service.status === 'ACTIVE_HEALTHY'
? 'bg-brand-default'
: 'bg-selection'
)}
/>
))}
</div>
)
}
label={<span>Status</span>}
value={<span>{overallStatusLabel}</span>}
/>
</HoverCardTrigger>
<HoverCardContent className="p-0 w-60" side="bottom" align="start">
{services.map((service) => {
const serviceInfo = (
<div className="flex gap-x-2">
<StatusIcon
isLoading={service.isLoading}
isProjectNew={isProjectNew}
projectStatus={service.status}
/>
<div className="flex-1">
<p>{service.name}</p>
<p className="text-foreground-light flex items-center gap-1">
<StatusMessage
isLoading={service.isLoading}
isProjectNew={isProjectNew}
status={
isProjectComingUp && service.status === 'UNHEALTHY'
? 'COMING_UP'
: service.status
}
/>
</p>
</div>
</div>
)
// Disabled services have no logs to link out to
if (service.status === 'DISABLED') {
return (
<div key={service.name} className={SERVICE_ROW_CLASS}>
{serviceInfo}
</div>
)
}
return (
<Link
href={`/project/${ref}${service.logsUrl}`}
key={service.name}
className={cn(SERVICE_ROW_CLASS, 'transition group relative hover:bg-surface-300')}
>
{serviceInfo}
<div className="flex items-center gap-x-1 transition opacity-0 group-hover:opacity-100 group-focus-visible:opacity-100">
<span className="text-xs text-foreground">View logs</span>
<ChevronRight size={14} className="text-foreground" />
</div>
</Link>
)
})}
{!allServicesOperational && (
<div className="flex gap-2 text-xs text-foreground-light px-3 py-2">
<div className="mt-0.5">
<InfoIcon />
</div>
<div className="flex flex-col gap-y-1">
<p>
{isProjectNew ? 'New' : 'Recently restored'} projects can take up to{' '}
{SERVICE_STATUS_THRESHOLD} minutes to become fully operational.
</p>
<p>
If services stay unhealthy, refer to our{' '}
<InlineLink
href={`${DOCS_URL}/guides/troubleshooting/project-status-reports-unhealthy-services`}
>
docs
</InlineLink>{' '}
for more information.
</p>
</div>
</div>
)}
</HoverCardContent>
</HoverCard>
)
}