ServiceHealthTable.tsx167 lines · main
1import { ChevronRight, HelpCircle } from 'lucide-react'
2import Link from 'next/link'
3import { Card, CardContent, Loading, Tooltip, TooltipContent, TooltipTrigger } from 'ui'
4import { LogsBarChart } from 'ui-patterns/LogsBarChart'
5
6import { ButtonTooltip } from '../../ui/ButtonTooltip'
7import type { LogsBarChartDatum } from '../ProjectHome/ProjectUsage.metrics'
8import type { ServiceKey } from './ObservabilityOverview.utils'
9
10type ServiceConfig = {
11 key: ServiceKey
12 name: string
13 description: string
14 reportUrl?: string
15 logsUrl: string
16}
17
18type ServiceData = {
19 total: number
20 errorRate: number
21 errorCount: number
22 warningCount: number
23 eventChartData: LogsBarChartDatum[]
24 isLoading: boolean
25}
26
27export type ServiceHealthTableProps = {
28 services: ServiceConfig[]
29 serviceData: Record<string, ServiceData>
30 onBarClick: (logsUrl: string) => (datum: LogsBarChartDatum) => void
31 datetimeFormat: string
32}
33
34const SERVICE_DESCRIPTIONS: Record<ServiceKey, string> = {
35 db: 'PostgreSQL database health and performance',
36 auth: 'Authentication and user management',
37 functions: 'Serverless Edge Functions execution',
38 storage: 'Object storage for files and assets',
39 realtime: 'WebSocket connections and broadcasts',
40 postgrest: 'Auto-generated REST API for your database',
41}
42
43type ServiceRowProps = {
44 service: ServiceConfig
45 data: ServiceData
46 onBarClick: (datum: LogsBarChartDatum) => void
47 datetimeFormat: string
48}
49
50const ServiceRow = ({ service, data, onBarClick, datetimeFormat }: ServiceRowProps) => {
51 const errorRate = data.total > 0 ? data.errorRate : 0
52 const warningRate = data.total > 0 ? (data.warningCount / data.total) * 100 : 0
53
54 const reportUrl = service.reportUrl || service.logsUrl
55
56 return (
57 <Link
58 href={reportUrl}
59 className="block group py-4 px-card border-b border-default last:border-b-0 hover:bg-surface-200 transition-colors cursor-pointer"
60 >
61 <div className="flex items-center justify-between mb-3">
62 <div className="flex items-center gap-2">
63 <span className="text-foreground font-medium">{service.name}</span>
64 <Tooltip>
65 <TooltipTrigger asChild>
66 <button
67 onClick={(e) => e.preventDefault()}
68 className="text-foreground-lighter hover:text-foreground-light transition-colors"
69 >
70 <HelpCircle size={14} />
71 </button>
72 </TooltipTrigger>
73 <TooltipContent side="right" className="max-w-xs">
74 <p>{SERVICE_DESCRIPTIONS[service.key as ServiceKey] || service.description}</p>
75 </TooltipContent>
76 </Tooltip>
77 </div>
78 <div className="flex items-center gap-2">
79 <ButtonTooltip
80 type="text"
81 size="tiny"
82 className="p-1.5"
83 tooltip={{ content: { text: `Go to ${service.name} report` } }}
84 >
85 <ChevronRight
86 size={14}
87 className="text-foreground-lighter group-hover:text-foreground"
88 />
89 </ButtonTooltip>
90 </div>
91 </div>
92
93 <div className="h-16" onClick={(e) => e.preventDefault()}>
94 <Loading active={data.isLoading}>
95 {data.isLoading ? (
96 <div />
97 ) : (
98 <LogsBarChart
99 data={data.eventChartData}
100 DateTimeFormat={datetimeFormat}
101 onBarClick={onBarClick}
102 EmptyState={
103 <div className="flex items-center justify-center h-full text-xs text-foreground-lighter">
104 No data
105 </div>
106 }
107 />
108 )}
109 </Loading>
110 </div>
111
112 {data.total > 0 && (
113 <div className="flex items-center justify-center mt-2 text-xs text-foreground-lighter gap-4 font-mono tabular-nums tracking-tight">
114 {errorRate > 0 && (
115 <span className="flex items-center gap-1.5">
116 <div className="w-1.5 h-1.5 rounded-full bg-destructive" />
117 {errorRate.toFixed(2)}% errors
118 </span>
119 )}
120 {warningRate > 0 && (
121 <span className="flex items-center gap-1.5">
122 <div className="w-1.5 h-1.5 rounded-full bg-warning" />
123 {warningRate.toFixed(2)}% warnings
124 </span>
125 )}
126 {errorRate === 0 && warningRate === 0 && (
127 <span className="flex items-center gap-1.5">
128 <div className="w-1.5 h-1.5 rounded-full bg-brand" />
129 0% errors
130 </span>
131 )}
132 </div>
133 )}
134 </Link>
135 )
136}
137
138export const ServiceHealthTable = ({
139 services,
140 serviceData,
141 onBarClick,
142 datetimeFormat,
143}: ServiceHealthTableProps) => {
144 return (
145 <div>
146 <h2 className="heading-section mb-4">Service Health</h2>
147 <Card>
148 <CardContent className="p-0">
149 {services.map((service) => {
150 const data = serviceData[service.key]
151 if (!data) return null
152
153 return (
154 <ServiceRow
155 key={service.key}
156 service={service}
157 data={data}
158 onBarClick={onBarClick(service.logsUrl)}
159 datetimeFormat={datetimeFormat}
160 />
161 )
162 })}
163 </CardContent>
164 </Card>
165 </div>
166 )
167}