ChartHandler.tsx 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208
  1. import dayjs from 'dayjs'
  2. import { Activity, BarChartIcon, Loader2 } from 'lucide-react'
  3. import { useRouter } from 'next/router'
  4. import { PropsWithChildren, useMemo, useState } from 'react'
  5. import { Button, Tooltip, TooltipContent, TooltipTrigger, WarningIcon } from 'ui'
  6. import type { ChartData } from './Charts.types'
  7. import AreaChart from '@/components/ui/Charts/AreaChart'
  8. import BarChart from '@/components/ui/Charts/BarChart'
  9. import { AnalyticsInterval } from '@/data/analytics/constants'
  10. import { mapMultiResponseToAnalyticsData } from '@/data/analytics/infra-monitoring-queries'
  11. import {
  12. InfraMonitoringAttribute,
  13. useInfraMonitoringAttributesQuery,
  14. } from '@/data/analytics/infra-monitoring-query'
  15. import {
  16. ProjectDailyStatsAttribute,
  17. useProjectDailyStatsQuery,
  18. } from '@/data/analytics/project-daily-stats-query'
  19. import { useDatabaseSelectorStateSnapshot } from '@/state/database-selector'
  20. interface ChartHandlerProps {
  21. id?: string
  22. label: string
  23. attribute: string
  24. provider: 'infra-monitoring' | 'daily-stats'
  25. startDate: string
  26. endDate: string
  27. interval: string
  28. customDateFormat?: string
  29. defaultChartStyle?: 'bar' | 'line'
  30. hideChartType?: boolean
  31. data?: ChartData
  32. isLoading?: boolean
  33. format?: string
  34. highlightedValue?: string | number
  35. syncId?: string
  36. }
  37. /**
  38. * Controls chart display state. Optionally fetches static chart data if data is not provided.
  39. *
  40. * If the `data` prop is provided, it will disable automatic chart data fetching and pass the data directly to the chart render.
  41. * - loading state can also be provided through the `isLoading` prop, to display loading placeholders. Ignored if `data` key not provided.
  42. * - if `isLoading=true` and `data` is `undefined`, loading error message will be shown.
  43. *
  44. * Provided data must be in the expected chart format.
  45. */
  46. const ChartHandler = ({
  47. label,
  48. attribute,
  49. provider,
  50. startDate,
  51. endDate,
  52. interval,
  53. customDateFormat,
  54. children = null,
  55. defaultChartStyle = 'bar',
  56. hideChartType = false,
  57. data,
  58. isLoading,
  59. format,
  60. highlightedValue,
  61. syncId,
  62. ...otherProps
  63. }: PropsWithChildren<ChartHandlerProps>) => {
  64. const router = useRouter()
  65. const { ref } = router.query
  66. const state = useDatabaseSelectorStateSnapshot()
  67. const [chartStyle, setChartStyle] = useState<string>(defaultChartStyle)
  68. const databaseIdentifier = state.selectedDatabaseId
  69. const { data: dailyStatsData, isPending: isFetchingDailyStats } = useProjectDailyStatsQuery(
  70. {
  71. projectRef: ref as string,
  72. attribute: attribute as ProjectDailyStatsAttribute,
  73. startDate: dayjs(startDate).format('YYYY-MM-DD'),
  74. endDate: dayjs(endDate).format('YYYY-MM-DD'),
  75. },
  76. { enabled: provider === 'daily-stats' && data === undefined }
  77. )
  78. const { data: infraMonitoringData, isPending: isFetchingInfraMonitoring } =
  79. useInfraMonitoringAttributesQuery(
  80. {
  81. projectRef: ref as string,
  82. attributes: [attribute as InfraMonitoringAttribute],
  83. startDate,
  84. endDate,
  85. interval: interval as AnalyticsInterval,
  86. databaseIdentifier,
  87. },
  88. { enabled: provider === 'infra-monitoring' && data === undefined }
  89. )
  90. const transformedInfraData = useMemo(() => {
  91. if (!infraMonitoringData) return undefined
  92. const mapped = mapMultiResponseToAnalyticsData(infraMonitoringData, [
  93. attribute as InfraMonitoringAttribute,
  94. ])
  95. return mapped[attribute]
  96. }, [infraMonitoringData, attribute])
  97. const chartData =
  98. data ||
  99. (provider === 'infra-monitoring'
  100. ? transformedInfraData
  101. : provider === 'daily-stats'
  102. ? dailyStatsData
  103. : undefined)
  104. const loading =
  105. isLoading ||
  106. (provider === 'infra-monitoring'
  107. ? isFetchingInfraMonitoring
  108. : provider === 'daily-stats'
  109. ? isFetchingDailyStats
  110. : isLoading)
  111. const shouldHighlightMaxValue =
  112. provider === 'daily-stats' &&
  113. !attribute.includes('ingress') &&
  114. !attribute.includes('egress') &&
  115. chartData !== undefined &&
  116. 'maximum' in chartData
  117. const shouldHighlightTotalGroupedValue = chartData !== undefined && 'totalGrouped' in chartData
  118. const _highlightedValue =
  119. highlightedValue !== undefined
  120. ? highlightedValue
  121. : shouldHighlightMaxValue
  122. ? chartData?.maximum
  123. : provider === 'daily-stats'
  124. ? chartData?.total
  125. : shouldHighlightTotalGroupedValue
  126. ? chartData?.totalGrouped?.[attribute]
  127. : (chartData?.data[chartData?.data.length - 1] as any)?.[attribute as any]
  128. if (loading) {
  129. return (
  130. <div className="flex h-52 w-full flex-col items-center justify-center gap-y-2">
  131. <Loader2 size={18} className="animate-spin text-border-strong" />
  132. <p className="text-xs text-foreground-lighter">Loading data for {label}</p>
  133. </div>
  134. )
  135. }
  136. if (chartData === undefined) {
  137. return (
  138. <div className="flex h-52 w-full flex-col items-center justify-center gap-y-2">
  139. <WarningIcon />
  140. <p className="text-xs text-foreground-lighter">Unable to load data for {label}</p>
  141. </div>
  142. )
  143. }
  144. return (
  145. <div className="h-full w-full">
  146. <div className="absolute right-6 z-10 flex justify-between">
  147. {!hideChartType && (
  148. <Tooltip>
  149. <TooltipTrigger asChild>
  150. <Button
  151. type="default"
  152. className="px-1.5"
  153. icon={chartStyle === 'bar' ? <Activity /> : <BarChartIcon />}
  154. onClick={() => setChartStyle(chartStyle === 'bar' ? 'line' : 'bar')}
  155. />
  156. </TooltipTrigger>
  157. <TooltipContent side="left" align="center">
  158. View as {chartStyle === 'bar' ? 'line chart' : 'bar chart'}
  159. </TooltipContent>
  160. </Tooltip>
  161. )}
  162. {children}
  163. </div>
  164. {chartStyle === 'bar' ? (
  165. <BarChart
  166. data={(chartData?.data ?? []) as any}
  167. format={format || chartData?.format}
  168. xAxisKey={'period_start'}
  169. yAxisKey={attribute}
  170. highlightedValue={_highlightedValue}
  171. title={label}
  172. customDateFormat={customDateFormat}
  173. syncId={syncId}
  174. {...otherProps}
  175. />
  176. ) : (
  177. <AreaChart
  178. data={(chartData?.data ?? []) as any}
  179. format={format || chartData?.format}
  180. xAxisKey="period_start"
  181. yAxisKey={attribute}
  182. highlightedValue={_highlightedValue}
  183. title={label}
  184. customDateFormat={customDateFormat}
  185. syncId={syncId}
  186. {...otherProps}
  187. />
  188. )}
  189. </div>
  190. )
  191. }
  192. export default ChartHandler