mirror of
https://github.com/supabase/supabase.git
synced 2026-06-12 00:01:19 +08:00
## I have read the [CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md) file. YES ## What kind of change does this PR introduce? With our bug fix for homepage charts, something got borked with the health services ones on Observability Overview. Fixed the height plus styling. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Enhanced observability charts with configurable axis display options for improved data visualization flexibility * **Bug Fixes** * Corrected service health table border styling and layout when displaying odd numbers of services in multi-column grid view <!-- review_stack_entry_start --> [](https://app.coderabbit.ai/change-stack/supabase/supabase/pull/46483?utm_source=github_walkthrough&utm_medium=github&utm_campaign=change_stack) <!-- review_stack_entry_end --> <!-- end of auto-generated comment: release notes by coderabbit.ai -->
229 lines
7.0 KiB
TypeScript
229 lines
7.0 KiB
TypeScript
import { ChevronRight, HelpCircle } from 'lucide-react'
|
|
import Link from 'next/link'
|
|
import {
|
|
Button,
|
|
Card,
|
|
CardContent,
|
|
cn,
|
|
Skeleton,
|
|
Tooltip,
|
|
TooltipContent,
|
|
TooltipTrigger,
|
|
} from 'ui'
|
|
import { ChartEmptyState, ChartLoadingState } from 'ui-patterns/Chart'
|
|
import { LogsBarChart } from 'ui-patterns/LogsBarChart'
|
|
|
|
import type { LogsBarChartDatum } from '../ProjectHome/ProjectUsage.metrics'
|
|
import { getHealthStatus, type ServiceKey } from './ObservabilityOverview.utils'
|
|
|
|
type ServiceConfig = {
|
|
key: ServiceKey
|
|
name: string
|
|
description: string
|
|
reportUrl?: string
|
|
logsUrl: string
|
|
}
|
|
|
|
type ServiceData = {
|
|
total: number
|
|
errorRate: number
|
|
errorCount: number
|
|
warningCount: number
|
|
eventChartData: LogsBarChartDatum[]
|
|
isLoading: boolean
|
|
}
|
|
|
|
export type ServiceHealthTableProps = {
|
|
services: ServiceConfig[]
|
|
serviceData: Record<string, ServiceData>
|
|
onBarClick: (logsUrl: string) => (datum: LogsBarChartDatum) => void
|
|
datetimeFormat: string
|
|
}
|
|
|
|
const colorClassMap: Record<string, string> = {
|
|
muted: 'bg-gray-500',
|
|
destructive: 'bg-destructive',
|
|
warning: 'bg-warning',
|
|
brand: 'bg-brand',
|
|
}
|
|
|
|
const SERVICE_DESCRIPTIONS: Record<ServiceKey, string> = {
|
|
db: 'PostgreSQL database health and performance',
|
|
auth: 'Authentication and user management',
|
|
functions: 'Serverless Edge Functions execution',
|
|
storage: 'Object storage for files and assets',
|
|
realtime: 'WebSocket connections and broadcasts',
|
|
data_api: 'Incoming API requests routed through the edge network',
|
|
postgrest: 'Auto-generated REST API for your database',
|
|
}
|
|
|
|
const formatPercent = (value: number) =>
|
|
value >= 1 ? `${value.toFixed(1)}%` : `${value.toFixed(2)}%`
|
|
|
|
const getSubtitle = (data: ServiceData) => {
|
|
if (data.total === 0) return ''
|
|
|
|
const errorRate = data.errorRate
|
|
const warningRate = data.total > 0 ? (data.warningCount / data.total) * 100 : 0
|
|
|
|
if (errorRate > 0) return `${formatPercent(errorRate)} errors`
|
|
if (warningRate > 0) return `${formatPercent(warningRate)} warnings`
|
|
return `${data.total.toLocaleString()} requests`
|
|
}
|
|
|
|
type ServiceCellProps = {
|
|
service: ServiceConfig
|
|
data: ServiceData
|
|
onBarClick: (datum: LogsBarChartDatum) => void
|
|
datetimeFormat: string
|
|
className?: string
|
|
}
|
|
|
|
const ServiceCell = ({
|
|
service,
|
|
data,
|
|
onBarClick,
|
|
datetimeFormat,
|
|
className,
|
|
}: ServiceCellProps) => {
|
|
const reportUrl = service.reportUrl || service.logsUrl
|
|
const { color } = getHealthStatus(data.errorRate, data.total)
|
|
const description = SERVICE_DESCRIPTIONS[service.key] || service.description
|
|
|
|
return (
|
|
<div
|
|
className={cn(
|
|
'group relative px-card pt-2 pb-4 hover:bg-surface-200 transition-colors',
|
|
className
|
|
)}
|
|
>
|
|
<div className="flex items-center justify-between mb-3 gap-2">
|
|
<div className="flex items-center gap-1.5 min-w-0">
|
|
<div
|
|
className={cn(
|
|
'w-1.5 h-1.5 rounded-full shrink-0',
|
|
colorClassMap[color] || 'bg-gray-500'
|
|
)}
|
|
/>
|
|
<h3 className="text-foreground-light font-mono uppercase text-xs truncate m-0">
|
|
<Link
|
|
href={reportUrl}
|
|
className="after:absolute after:inset-0 after:content-[''] focus-visible:outline-none focus-visible:after:ring-2 focus-visible:after:ring-foreground-light focus-visible:after:ring-offset-2 focus-visible:after:rounded-sm"
|
|
>
|
|
{service.name}
|
|
</Link>
|
|
</h3>
|
|
{description && (
|
|
<Tooltip>
|
|
<TooltipTrigger asChild>
|
|
<button
|
|
type="button"
|
|
className="relative z-10 text-foreground-lighter hover:text-foreground-light transition-colors shrink-0"
|
|
aria-label={`About ${service.name}`}
|
|
>
|
|
<HelpCircle size={12} />
|
|
</button>
|
|
</TooltipTrigger>
|
|
<TooltipContent side="top" className="max-w-xs">
|
|
<p>{description}</p>
|
|
</TooltipContent>
|
|
</Tooltip>
|
|
)}
|
|
</div>
|
|
<div className="flex items-center gap-1.5">
|
|
{data.isLoading ? (
|
|
<Skeleton className="h-3 w-20 mt-0.5" />
|
|
) : (
|
|
<span
|
|
className={cn(
|
|
'text-xs truncate',
|
|
data.total === 0 ? 'text-foreground-lighter' : 'text-foreground'
|
|
)}
|
|
>
|
|
{getSubtitle(data)}
|
|
</span>
|
|
)}
|
|
<Tooltip>
|
|
<TooltipTrigger asChild>
|
|
<Button
|
|
type="text"
|
|
size="tiny"
|
|
className="relative z-10 px-1 text-foreground-lighter group-hover:text-foreground transition-colors shrink-0"
|
|
aria-label={`Go to ${service.name} report`}
|
|
asChild
|
|
>
|
|
<Link href={reportUrl}>
|
|
<ChevronRight size={14} strokeWidth={1.5} />
|
|
</Link>
|
|
</Button>
|
|
</TooltipTrigger>
|
|
<TooltipContent side="top">Go to {service.name} report</TooltipContent>
|
|
</Tooltip>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="relative z-10 h-16">
|
|
{data.isLoading ? (
|
|
<ChartLoadingState className="h-full" />
|
|
) : (
|
|
<LogsBarChart
|
|
isFullHeight
|
|
hideDateRange
|
|
hideXAxis
|
|
data={data.eventChartData}
|
|
DateTimeFormat={datetimeFormat}
|
|
onBarClick={onBarClick}
|
|
EmptyState={<ChartEmptyState className="h-full" description="No traffic" />}
|
|
/>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export const ServiceHealthTable = ({
|
|
services,
|
|
serviceData,
|
|
onBarClick,
|
|
datetimeFormat,
|
|
}: ServiceHealthTableProps) => {
|
|
return (
|
|
<div>
|
|
<h2 className="heading-section mb-4">Service Health</h2>
|
|
<Card className="overflow-auto">
|
|
<CardContent className="p-0">
|
|
<div className="grid grid-cols-1 md:grid-cols-2">
|
|
{services.map((service, index) => {
|
|
const data = serviceData[service.key]
|
|
if (!data) return null
|
|
|
|
const isFirst = index === 0
|
|
const isLeftColumn = !isFirst && (index - 1) % 2 === 0
|
|
const restCount = services.length - 1
|
|
const lastRowCount = restCount % 2 === 0 ? 2 : 1
|
|
const isInLastRow = !isFirst && index >= services.length - lastRowCount
|
|
|
|
return (
|
|
<ServiceCell
|
|
key={service.key}
|
|
service={service}
|
|
data={data}
|
|
onBarClick={onBarClick(service.logsUrl)}
|
|
datetimeFormat={datetimeFormat}
|
|
className={cn(
|
|
'border-default border-b',
|
|
isFirst && 'md:col-span-2',
|
|
isInLastRow && 'md:border-b-0',
|
|
isLeftColumn && 'md:border-r'
|
|
)}
|
|
/>
|
|
)
|
|
})}
|
|
<div className="bg-background/20" aria-hidden="true" />
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
)
|
|
}
|