ServiceHealthTable.tsx 5.2 KB

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