ProjectUsageSection.tsx 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290
  1. // @ts-nocheck
  2. import { useParams } from 'common'
  3. import dayjs from 'dayjs'
  4. import Link from 'next/link'
  5. import { useRouter } from 'next/router'
  6. import { useEffect, useMemo, useState } from 'react'
  7. import { Card, CardContent, CardHeader, CardTitle, Loading } from 'ui'
  8. import { Row } from 'ui-patterns'
  9. import { LogsBarChart } from 'ui-patterns/LogsBarChart'
  10. import NoDataPlaceholder from '@/components/ui/Charts/NoDataPlaceholder'
  11. import { ChartIntervalDropdown } from '@/components/ui/Logs/ChartIntervalDropdown'
  12. import { CHART_INTERVALS } from '@/components/ui/Logs/logs.utils'
  13. import { UsageApiCounts, useProjectLogStatsQuery } from '@/data/analytics/project-log-stats-query'
  14. import { useSendEventMutation } from '@/data/telemetry/send-event-mutation'
  15. import { useFillTimeseriesSorted } from '@/hooks/analytics/useFillTimeseriesSorted'
  16. import { useCheckEntitlements } from '@/hooks/misc/useCheckEntitlements'
  17. import { useIsFeatureEnabled } from '@/hooks/misc/useIsFeatureEnabled'
  18. import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization'
  19. type LogsBarChartDatum = {
  20. timestamp: string
  21. error_count: number
  22. ok_count: number
  23. warning_count: number
  24. }
  25. type ChartIntervalKey = '1hr' | '1day' | '7day'
  26. type ServiceKey = 'db' | 'auth' | 'storage' | 'realtime'
  27. type ServiceEntry = {
  28. key: ServiceKey
  29. title: string
  30. href?: string
  31. route: string
  32. enabled: boolean
  33. }
  34. type ServiceComputed = ServiceEntry & {
  35. data: LogsBarChartDatum[]
  36. total: number
  37. isLoading: boolean
  38. error: unknown | null
  39. }
  40. export const ProjectUsageSection = () => {
  41. const router = useRouter()
  42. const { ref: projectRef } = useParams()
  43. const { data: organization } = useSelectedOrganizationQuery()
  44. const { mutate: sendEvent } = useSendEventMutation()
  45. const { projectAuthAll: authEnabled, projectStorageAll: storageEnabled } = useIsFeatureEnabled([
  46. 'project_auth:all',
  47. 'project_storage:all',
  48. ])
  49. const { getEntitlementMax } = useCheckEntitlements('log.retention_days')
  50. const retentionDays = getEntitlementMax()
  51. const DEFAULT_INTERVAL: ChartIntervalKey =
  52. retentionDays !== undefined && retentionDays < 7 ? '1hr' : '1day'
  53. const [interval, setInterval] = useState<ChartIntervalKey>(DEFAULT_INTERVAL)
  54. useEffect(() => {
  55. setInterval(retentionDays !== undefined && retentionDays < 7 ? '1hr' : '1day')
  56. }, [retentionDays])
  57. const selectedInterval = CHART_INTERVALS.find((i) => i.key === interval) || CHART_INTERVALS[1]
  58. const { datetimeFormat } = useMemo(() => {
  59. const format = selectedInterval.format || 'MMM D, ha'
  60. return { datetimeFormat: format }
  61. }, [selectedInterval])
  62. // Use V1 data fetching
  63. const { data: logStatsData, isPending: isLoading } = useProjectLogStatsQuery({
  64. projectRef,
  65. interval,
  66. })
  67. // Calculate date range for gap filling
  68. const startDateLocal = dayjs().subtract(
  69. selectedInterval.startValue,
  70. selectedInterval.startUnit as dayjs.ManipulateType
  71. )
  72. const endDateLocal = dayjs()
  73. // Fill gaps in timeseries data
  74. const { data: filledCharts } = useFillTimeseriesSorted({
  75. data: logStatsData?.result ?? [],
  76. timestampKey: 'timestamp',
  77. valueKey: [
  78. 'total_auth_requests',
  79. 'total_rest_requests',
  80. 'total_storage_requests',
  81. 'total_realtime_requests',
  82. ],
  83. defaultValue: 0,
  84. startDate: startDateLocal.toISOString(),
  85. endDate: endDateLocal.toISOString(),
  86. minPointsToFill: 5,
  87. })
  88. const serviceBase: ServiceEntry[] = useMemo(
  89. () => [
  90. {
  91. key: 'db',
  92. title: 'Database requests',
  93. href: `/project/${projectRef}/editor`,
  94. route: '/logs/postgres-logs',
  95. enabled: true,
  96. },
  97. {
  98. key: 'auth',
  99. title: 'Auth requests',
  100. href: `/project/${projectRef}/auth/users`,
  101. route: '/logs/auth-logs',
  102. enabled: authEnabled,
  103. },
  104. {
  105. key: 'storage',
  106. title: 'Storage requests',
  107. href: `/project/${projectRef}/storage/buckets`,
  108. route: '/logs/storage-logs',
  109. enabled: storageEnabled,
  110. },
  111. {
  112. key: 'realtime',
  113. title: 'Realtime requests',
  114. route: '/logs/realtime-logs',
  115. enabled: true,
  116. },
  117. ],
  118. [projectRef, authEnabled, storageEnabled]
  119. )
  120. const services: ServiceComputed[] = useMemo(
  121. () =>
  122. serviceBase.map((s) => {
  123. // Map service keys to V1 data field names
  124. const dataKeyMap: Record<ServiceKey, keyof UsageApiCounts> = {
  125. db: 'total_rest_requests',
  126. auth: 'total_auth_requests',
  127. storage: 'total_storage_requests',
  128. realtime: 'total_realtime_requests',
  129. }
  130. const dataKey = dataKeyMap[s.key]
  131. // Transform V1 data to LogsBarChart format
  132. // Since V1 doesn't have error/warning breakdown, we show everything as "ok"
  133. const transformedData: LogsBarChartDatum[] = (filledCharts || []).map((item) => ({
  134. timestamp: item.timestamp,
  135. error_count: 0,
  136. warning_count: 0,
  137. ok_count: Number(item[dataKey]) || 0,
  138. }))
  139. // Calculate total from filled data
  140. const total = transformedData.reduce((sum, item) => sum + item.ok_count, 0)
  141. return {
  142. ...s,
  143. data: transformedData,
  144. total,
  145. isLoading,
  146. error: null,
  147. }
  148. }),
  149. [serviceBase, filledCharts, isLoading]
  150. )
  151. const handleBarClick =
  152. (logRoute: string, serviceKey: ServiceKey) => (datum: LogsBarChartDatum) => {
  153. if (!datum?.timestamp) return
  154. const datumTimestamp = dayjs(datum.timestamp).toISOString()
  155. const start = dayjs(datumTimestamp).subtract(1, 'minute').toISOString()
  156. const end = dayjs(datumTimestamp).add(1, 'minute').toISOString()
  157. const queryParams = new URLSearchParams({
  158. iso_timestamp_start: start,
  159. iso_timestamp_end: end,
  160. })
  161. router.push(`/project/${projectRef}${logRoute}?${queryParams.toString()}`)
  162. if (projectRef && organization?.slug) {
  163. sendEvent({
  164. action: 'home_project_usage_chart_clicked',
  165. properties: {
  166. service_type: serviceKey,
  167. bar_timestamp: datum.timestamp,
  168. },
  169. groups: {
  170. project: projectRef,
  171. organization: organization.slug,
  172. },
  173. })
  174. }
  175. }
  176. const enabledServices = services.filter((s) => s.enabled)
  177. const totalRequests = enabledServices.reduce((sum, s) => sum + (s.total || 0), 0)
  178. return (
  179. <div className="space-y-6">
  180. <div className="flex flex-row justify-between items-center gap-x-2">
  181. <div className="flex items-start gap-2 heading-section text-foreground-light">
  182. <span className="text-foreground">{totalRequests.toLocaleString()}</span>
  183. <span>Total Requests</span>
  184. </div>
  185. <ChartIntervalDropdown
  186. value={interval}
  187. onChange={(interval) => setInterval(interval as ChartIntervalKey)}
  188. organizationSlug={organization?.slug}
  189. dropdownAlign="end"
  190. tooltipSide="left"
  191. />
  192. </div>
  193. <Row maxColumns={4} minWidth={280}>
  194. {enabledServices.map((s) => (
  195. <Card key={s.key} className="mb-0 md:mb-0 h-full flex flex-col h-64">
  196. <CardHeader className="flex flex-row items-end justify-between gap-2 space-y-0 pb-0 border-b-0">
  197. <div className="flex items-center gap-2">
  198. <div className="flex flex-col">
  199. <CardTitle className="text-xs font-mono uppercase text-foreground-light">
  200. {s.href ? (
  201. <Link
  202. href={s.href}
  203. onClick={() => {
  204. if (projectRef && organization?.slug) {
  205. sendEvent({
  206. action: 'home_project_usage_service_clicked',
  207. properties: {
  208. service_type: s.key,
  209. total_requests: s.total || 0,
  210. },
  211. groups: {
  212. project: projectRef,
  213. organization: organization.slug,
  214. },
  215. })
  216. }
  217. }}
  218. >
  219. {s.title}
  220. </Link>
  221. ) : (
  222. s.title
  223. )}
  224. </CardTitle>
  225. <span className="text-foreground text-xl">{(s.total || 0).toLocaleString()}</span>
  226. </div>
  227. </div>
  228. </CardHeader>
  229. <CardContent className="p-card flex-1 h-full overflow-hidden">
  230. <Loading isFullHeight active={isLoading}>
  231. <LogsBarChart
  232. isFullHeight
  233. data={s.data}
  234. DateTimeFormat={datetimeFormat}
  235. onBarClick={handleBarClick(s.route, s.key)}
  236. hideZeroValues={true}
  237. chartConfig={{
  238. error_count: {
  239. label: 'Errors',
  240. },
  241. warning_count: {
  242. label: 'Warnings',
  243. },
  244. ok_count: {
  245. label: 'Requests',
  246. },
  247. }}
  248. EmptyState={
  249. <NoDataPlaceholder
  250. size="small"
  251. message="No data for selected period"
  252. isFullHeight
  253. />
  254. }
  255. />
  256. </Loading>
  257. </CardContent>
  258. </Card>
  259. ))}
  260. </Row>
  261. </div>
  262. )
  263. }