ObservabilityOverview.tsx 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187
  1. import { useQueryClient } from '@tanstack/react-query'
  2. import { useParams } from 'common'
  3. import dayjs from 'dayjs'
  4. import { RefreshCw } from 'lucide-react'
  5. import { useRouter } from 'next/router'
  6. import { useCallback, useMemo, useState } from 'react'
  7. import { Badge, Button, Tooltip, TooltipContent, TooltipTrigger } from 'ui'
  8. import { DatabaseInfrastructureSection } from './DatabaseInfrastructureSection'
  9. import { useObservabilityOverviewData } from './ObservabilityOverview.utils'
  10. import { ObservabilityOverviewFooter } from './ObservabilityOverviewFooter'
  11. import { ServiceHealthTable } from './ServiceHealthTable'
  12. import { useSlowQueriesCount } from './useSlowQueriesCount'
  13. import ReportHeader from '@/components/interfaces/Reports/ReportHeader'
  14. import ReportPadding from '@/components/interfaces/Reports/ReportPadding'
  15. import { ChartIntervalDropdown } from '@/components/ui/Logs/ChartIntervalDropdown'
  16. import { CHART_INTERVALS } from '@/components/ui/Logs/logs.utils'
  17. import { useIsFeatureEnabled } from '@/hooks/misc/useIsFeatureEnabled'
  18. import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization'
  19. type ChartIntervalKey = '1hr' | '1day' | '7day'
  20. export const ObservabilityOverview = () => {
  21. const router = useRouter()
  22. const { ref: projectRef } = useParams()
  23. const { data: organization } = useSelectedOrganizationQuery()
  24. const queryClient = useQueryClient()
  25. const { projectStorageAll: storageSupported } = useIsFeatureEnabled(['project_storage:all'])
  26. const DEFAULT_INTERVAL: ChartIntervalKey = '1day'
  27. const [interval, setInterval] = useState<ChartIntervalKey>(DEFAULT_INTERVAL)
  28. const [refreshKey, setRefreshKey] = useState(0)
  29. const selectedInterval = CHART_INTERVALS.find((i) => i.key === interval) || CHART_INTERVALS[1]
  30. const { datetimeFormat } = useMemo(() => {
  31. const format = selectedInterval.format || 'MMM D, ha'
  32. return { datetimeFormat: format }
  33. }, [selectedInterval])
  34. const overviewData = useObservabilityOverviewData(projectRef!, interval, refreshKey)
  35. const { slowQueriesCount, isLoading: slowQueriesLoading } = useSlowQueriesCount(
  36. projectRef,
  37. refreshKey
  38. )
  39. const handleRefresh = useCallback(() => {
  40. setRefreshKey((prev) => prev + 1)
  41. queryClient.invalidateQueries({ queryKey: ['project-metrics'] })
  42. queryClient.invalidateQueries({ queryKey: ['postgrest-overview-metrics'] })
  43. queryClient.invalidateQueries({ queryKey: ['infra-monitoring'] })
  44. queryClient.invalidateQueries({ queryKey: ['max-connections'] })
  45. }, [queryClient])
  46. const serviceBase = useMemo(
  47. () => [
  48. {
  49. key: 'db' as const,
  50. name: 'Database',
  51. reportUrl: `/project/${projectRef}/observability/database`,
  52. logsUrl: `/project/${projectRef}/logs/postgres-logs`,
  53. enabled: true,
  54. hasReport: true,
  55. },
  56. {
  57. key: 'auth' as const,
  58. name: 'Auth',
  59. reportUrl: `/project/${projectRef}/observability/auth`,
  60. logsUrl: `/project/${projectRef}/logs/auth-logs`,
  61. enabled: true,
  62. hasReport: true,
  63. },
  64. {
  65. key: 'functions' as const,
  66. name: 'Edge Functions',
  67. reportUrl: `/project/${projectRef}/observability/edge-functions`,
  68. logsUrl: `/project/${projectRef}/logs/edge-functions-logs`,
  69. enabled: true,
  70. hasReport: true,
  71. },
  72. {
  73. key: 'realtime' as const,
  74. name: 'Realtime',
  75. reportUrl: `/project/${projectRef}/observability/realtime`,
  76. logsUrl: `/project/${projectRef}/logs/realtime-logs`,
  77. enabled: true,
  78. hasReport: true,
  79. },
  80. {
  81. key: 'storage' as const,
  82. name: 'Storage',
  83. reportUrl: `/project/${projectRef}/observability/storage`,
  84. logsUrl: `/project/${projectRef}/logs/storage-logs`,
  85. enabled: storageSupported,
  86. hasReport: true,
  87. },
  88. {
  89. key: 'postgrest' as const,
  90. name: 'Data API',
  91. reportUrl: `/project/${projectRef}/observability/postgrest`,
  92. logsUrl: `/project/${projectRef}/logs/postgrest-logs`,
  93. enabled: true,
  94. hasReport: true,
  95. },
  96. ],
  97. [projectRef, storageSupported]
  98. )
  99. const enabledServices = serviceBase.filter((s) => s.enabled)
  100. const dbServiceData = overviewData.services.db
  101. // Navigate to the log view scoped to the clicked bar's bucket window
  102. const handleBarClick = useCallback(
  103. (logsUrl: string) => (datum: any) => {
  104. if (!datum?.timestamp) return
  105. // datum.timestamp is already the UTC-truncated bucket boundary from timestamp_trunc(),
  106. // so use it directly to avoid local-timezone startOf() misalignment (e.g. UTC+5:30).
  107. const unit = interval === '1hr' ? 'minute' : 'hour'
  108. const start = datum.timestamp
  109. const end = dayjs.utc(datum.timestamp).add(1, unit).toISOString()
  110. const queryParams = new URLSearchParams({ its: start, ite: end })
  111. router.push(`${logsUrl}?${queryParams.toString()}`)
  112. },
  113. [router, interval]
  114. )
  115. return (
  116. <ReportPadding>
  117. <div className="flex flex-row justify-between items-center">
  118. <div className="flex items-center gap-3">
  119. <ReportHeader title="Overview" />
  120. <Tooltip>
  121. <TooltipTrigger asChild>
  122. <Badge variant="warning">Beta</Badge>
  123. </TooltipTrigger>
  124. <TooltipContent>
  125. <p>This page is subject to change</p>
  126. </TooltipContent>
  127. </Tooltip>
  128. </div>
  129. <div className="flex items-center gap-2">
  130. <Button type="outline" icon={<RefreshCw size={14} />} onClick={handleRefresh}>
  131. Refresh
  132. </Button>
  133. <ChartIntervalDropdown
  134. value={interval}
  135. onChange={(interval) => setInterval(interval as ChartIntervalKey)}
  136. organizationSlug={organization?.slug}
  137. dropdownAlign="end"
  138. tooltipSide="left"
  139. />
  140. </div>
  141. </div>
  142. <div className="space-y-12 mt-8">
  143. <DatabaseInfrastructureSection
  144. interval={interval}
  145. refreshKey={refreshKey}
  146. dbErrorRate={dbServiceData.errorRate}
  147. isLoading={dbServiceData.isLoading}
  148. slowQueriesCount={slowQueriesCount}
  149. slowQueriesLoading={slowQueriesLoading}
  150. />
  151. <ServiceHealthTable
  152. services={enabledServices.map((service) => ({
  153. key: service.key,
  154. name: service.name,
  155. description: '',
  156. reportUrl: service.hasReport ? service.reportUrl : undefined,
  157. logsUrl: service.logsUrl,
  158. }))}
  159. serviceData={overviewData.services}
  160. onBarClick={handleBarClick}
  161. datetimeFormat={datetimeFormat}
  162. />
  163. </div>
  164. <ObservabilityOverviewFooter />
  165. </ReportPadding>
  166. )
  167. }