ChartHeader.tsx 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333
  1. import { useParams } from 'common'
  2. import {
  3. Activity,
  4. BarChartIcon,
  5. GitCommitHorizontalIcon,
  6. InfoIcon,
  7. SquareTerminal,
  8. } from 'lucide-react'
  9. import Link from 'next/link'
  10. import { useEffect, useState } from 'react'
  11. import { Badge, cn, Tooltip, TooltipContent, TooltipTrigger } from 'ui'
  12. import { formatPercentage, numberFormatter } from './Charts.utils'
  13. import { useChartHoverState } from './useChartHoverState'
  14. import { ButtonTooltip } from '@/components/ui/ButtonTooltip'
  15. import { formatDateTime, useFormatDateTime } from '@/lib/datetime'
  16. import { formatBytes, formatBytesMinMB } from '@/lib/helpers'
  17. export interface ChartHeaderProps {
  18. title?: string
  19. format?: string | ((value: unknown) => string)
  20. customDateFormat?: string
  21. minimalHeader?: boolean
  22. displayDateInUtc?: boolean
  23. highlightedLabel?: number | string | any | null
  24. highlightedValue?: number | string | any | null
  25. hideHighlightedValue?: boolean
  26. hideHighlightedLabel?: boolean
  27. hideHighlightArea?: boolean
  28. hideChartType?: boolean
  29. chartStyle?: string
  30. onChartStyleChange?: (style: string) => void
  31. showMaxValue?: boolean
  32. setShowMaxValue?: (value: boolean) => void
  33. docsUrl?: string
  34. syncId?: string
  35. data?: any[]
  36. xAxisKey?: string
  37. yAxisKey?: string
  38. xAxisIsDate?: boolean
  39. valuePrecision?: number
  40. shouldFormatBytes?: boolean
  41. isNetworkChart?: boolean
  42. isMemoryChart?: boolean
  43. attributes?: any[]
  44. sql?: string
  45. titleTooltip?: string
  46. showNewBadge?: boolean
  47. }
  48. export const ChartHeader = ({
  49. format,
  50. highlightedValue,
  51. highlightedLabel,
  52. hideHighlightedValue = false,
  53. hideHighlightedLabel = false,
  54. hideHighlightArea = false,
  55. title,
  56. minimalHeader = false,
  57. hideChartType = false,
  58. chartStyle = 'bar',
  59. onChartStyleChange,
  60. showMaxValue = false,
  61. setShowMaxValue,
  62. docsUrl,
  63. syncId,
  64. data,
  65. xAxisKey,
  66. yAxisKey,
  67. xAxisIsDate = true,
  68. displayDateInUtc,
  69. customDateFormat,
  70. valuePrecision = 2,
  71. shouldFormatBytes = false,
  72. isNetworkChart = false,
  73. attributes,
  74. sql,
  75. titleTooltip,
  76. showNewBadge,
  77. isMemoryChart,
  78. }: ChartHeaderProps) => {
  79. const { ref } = useParams()
  80. const { hoveredIndex, isHovered } = useChartHoverState(syncId || 'default')
  81. const [localHighlightedValue, setLocalHighlightedValue] = useState(highlightedValue)
  82. const [localHighlightedLabel, setLocalHighlightedLabel] = useState(highlightedLabel)
  83. // When `displayDateInUtc` is set the chart explicitly wants UTC labels.
  84. // Otherwise honour the user's selected timezone via the picker.
  85. const formatPickerDate = useFormatDateTime()
  86. const formatHighlightedValue = (value: any) => {
  87. if (typeof value !== 'number') {
  88. return value
  89. }
  90. if (typeof format === 'function') {
  91. return format(value)
  92. }
  93. if (shouldFormatBytes) {
  94. const bytesValue = isNetworkChart ? Math.abs(value) : value
  95. return isMemoryChart
  96. ? formatBytesMinMB(bytesValue, valuePrecision)
  97. : formatBytes(bytesValue, valuePrecision)
  98. }
  99. if (format === '%') {
  100. return formatPercentage(value, valuePrecision)
  101. }
  102. const formattedValue = numberFormatter(value, valuePrecision)
  103. if (typeof format === 'string' && format) {
  104. return `${formattedValue} ${format}`
  105. }
  106. return formattedValue
  107. }
  108. useEffect(() => {
  109. if (syncId && hoveredIndex !== null && isHovered && data && xAxisKey && yAxisKey) {
  110. const activeDataPoint = data[hoveredIndex]
  111. if (activeDataPoint) {
  112. // For stacked charts, we need to calculate the total of all attributes
  113. // that should be included in the total (excluding reference lines, max values, etc.)
  114. let newValue = activeDataPoint[yAxisKey]
  115. // If this is a stacked chart with multiple attributes, calculate the total
  116. if (attributes && attributes.length > 1) {
  117. const attributesToIgnore =
  118. attributes
  119. ?.filter((a) => a.omitFromTotal || a.isMaxValue || a.provider === 'reference-line')
  120. ?.map((a) => a.attribute) ?? []
  121. const totalValue = Object.entries(activeDataPoint)
  122. .filter(([key, value]) => {
  123. // Include only numeric values that are not in the ignore list
  124. return (
  125. typeof value === 'number' &&
  126. key !== 'timestamp' &&
  127. key !== 'period_start' &&
  128. !attributesToIgnore.includes(key) &&
  129. attributes.some((attr) => attr.attribute === key && attr.enabled !== false)
  130. )
  131. })
  132. .reduce((sum, [_, value]) => sum + (value as number), 0)
  133. newValue = totalValue
  134. }
  135. setLocalHighlightedValue(newValue)
  136. // Update highlighted label based on sync state
  137. let newLabel = highlightedLabel
  138. if (xAxisIsDate && activeDataPoint[xAxisKey]) {
  139. const value = activeDataPoint[xAxisKey] as number | string
  140. const fmt = customDateFormat || 'YYYY-MM-DD HH:mm:ss'
  141. newLabel = displayDateInUtc
  142. ? formatDateTime(value, { tz: 'UTC', format: fmt })
  143. : formatPickerDate(value, fmt)
  144. } else if (activeDataPoint[xAxisKey]) {
  145. newLabel = activeDataPoint[xAxisKey]
  146. }
  147. setLocalHighlightedLabel(newLabel)
  148. }
  149. } else {
  150. // Reset to original values when not syncing
  151. setLocalHighlightedValue(highlightedValue)
  152. setLocalHighlightedLabel(highlightedLabel)
  153. }
  154. }, [
  155. hoveredIndex,
  156. isHovered,
  157. syncId,
  158. data,
  159. xAxisKey,
  160. yAxisKey,
  161. xAxisIsDate,
  162. displayDateInUtc,
  163. customDateFormat,
  164. highlightedValue,
  165. highlightedLabel,
  166. attributes,
  167. formatPickerDate,
  168. ])
  169. const chartTitle = (
  170. <div className="flex flex-row items-center gap-x-2">
  171. <div className="flex flex-row items-center gap-x-2">
  172. <h3 className={'text-foreground-lighter ' + (minimalHeader ? 'text-xs' : 'text-sm')}>
  173. {title}
  174. </h3>
  175. {titleTooltip && (
  176. <Tooltip>
  177. <TooltipTrigger asChild>
  178. <InfoIcon className="w-4 h-4 text-foreground-lighter" />
  179. </TooltipTrigger>
  180. <TooltipContent side="top" className="max-w-xs">
  181. {titleTooltip}
  182. {docsUrl && (
  183. <>
  184. {' '}
  185. <Link
  186. href={docsUrl}
  187. target="_blank"
  188. className="underline text-foreground hover:text-foreground-light"
  189. >
  190. Read docs
  191. </Link>
  192. </>
  193. )}
  194. </TooltipContent>
  195. </Tooltip>
  196. )}
  197. </div>
  198. {!titleTooltip && docsUrl && (
  199. <ButtonTooltip
  200. type="text"
  201. className="px-1"
  202. asChild
  203. tooltip={{
  204. content: {
  205. side: 'top',
  206. text: 'Read docs',
  207. },
  208. }}
  209. >
  210. <Link href={docsUrl} target="_blank">
  211. <InfoIcon className="w-4 h-4 text-foreground-lighter" />
  212. </Link>
  213. </ButtonTooltip>
  214. )}
  215. </div>
  216. )
  217. const highlighted = (
  218. <h4
  219. className={`text-foreground text-xl font-normal ${minimalHeader ? 'text-base' : 'text-2xl'}`}
  220. >
  221. {localHighlightedValue !== undefined && formatHighlightedValue(localHighlightedValue)}
  222. </h4>
  223. )
  224. const label = <h4 className="text-foreground-lighter text-xs">{localHighlightedLabel}</h4>
  225. if (minimalHeader) {
  226. return (
  227. <div
  228. className={cn('flex flex-row items-center gap-x-4', hideHighlightArea && 'hidden')}
  229. style={{ minHeight: '1.8rem' }}
  230. >
  231. {title && chartTitle}
  232. <div className="flex flex-row items-baseline gap-x-2">
  233. {highlightedValue !== undefined && !hideHighlightedValue && highlighted}
  234. {!hideHighlightedLabel && label}
  235. </div>
  236. </div>
  237. )
  238. }
  239. const hasHighlightedValue = highlightedValue !== undefined && !hideHighlightedValue
  240. return (
  241. <div
  242. className={cn(
  243. 'grow flex justify-between items-start min-h-16',
  244. hideHighlightArea && 'hidden'
  245. )}
  246. >
  247. <div className="flex flex-col">
  248. <div className="flex items-center gap-2">
  249. {title && chartTitle}
  250. {showNewBadge && <Badge variant="success">New</Badge>}
  251. </div>
  252. <div className="h-4">
  253. {hasHighlightedValue && highlighted}
  254. {!hideHighlightedLabel && label}
  255. </div>
  256. </div>
  257. <div className="flex items-center gap-2">
  258. {sql ? (
  259. <ButtonTooltip
  260. type="default"
  261. className="px-1.5"
  262. asChild
  263. tooltip={{
  264. content: {
  265. side: 'top',
  266. text: 'Open in Log Explorer',
  267. },
  268. }}
  269. >
  270. <Link href={`/project/${ref}/logs/explorer?q=${encodeURIComponent(sql)}`}>
  271. <SquareTerminal className="w-4 h-4 text-foreground-lighter" />
  272. </Link>
  273. </ButtonTooltip>
  274. ) : null}
  275. {!hideChartType && onChartStyleChange && (
  276. <ButtonTooltip
  277. type="default"
  278. className="px-1.5"
  279. icon={chartStyle === 'bar' ? <Activity /> : <BarChartIcon />}
  280. onClick={() => onChartStyleChange(chartStyle === 'bar' ? 'line' : 'bar')}
  281. tooltip={{
  282. content: {
  283. side: 'top',
  284. text: `View as ${chartStyle === 'bar' ? 'line chart' : 'bar chart'}`,
  285. },
  286. }}
  287. />
  288. )}
  289. {setShowMaxValue && (
  290. <ButtonTooltip
  291. type={showMaxValue ? 'default' : 'dashed'}
  292. className="px-1.5"
  293. icon={
  294. <GitCommitHorizontalIcon
  295. className={showMaxValue ? 'text-foreground-light' : 'text-foreground-lighter'}
  296. />
  297. }
  298. onClick={() => setShowMaxValue(!showMaxValue)}
  299. tooltip={{
  300. content: {
  301. side: 'top',
  302. text: `${showMaxValue ? 'Hide' : 'Show'} limit`,
  303. },
  304. }}
  305. />
  306. )}
  307. </div>
  308. </div>
  309. )
  310. }