| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333 |
- import { useParams } from 'common'
- import {
- Activity,
- BarChartIcon,
- GitCommitHorizontalIcon,
- InfoIcon,
- SquareTerminal,
- } from 'lucide-react'
- import Link from 'next/link'
- import { useEffect, useState } from 'react'
- import { Badge, cn, Tooltip, TooltipContent, TooltipTrigger } from 'ui'
- import { formatPercentage, numberFormatter } from './Charts.utils'
- import { useChartHoverState } from './useChartHoverState'
- import { ButtonTooltip } from '@/components/ui/ButtonTooltip'
- import { formatDateTime, useFormatDateTime } from '@/lib/datetime'
- import { formatBytes, formatBytesMinMB } from '@/lib/helpers'
- export interface ChartHeaderProps {
- title?: string
- format?: string | ((value: unknown) => string)
- customDateFormat?: string
- minimalHeader?: boolean
- displayDateInUtc?: boolean
- highlightedLabel?: number | string | any | null
- highlightedValue?: number | string | any | null
- hideHighlightedValue?: boolean
- hideHighlightedLabel?: boolean
- hideHighlightArea?: boolean
- hideChartType?: boolean
- chartStyle?: string
- onChartStyleChange?: (style: string) => void
- showMaxValue?: boolean
- setShowMaxValue?: (value: boolean) => void
- docsUrl?: string
- syncId?: string
- data?: any[]
- xAxisKey?: string
- yAxisKey?: string
- xAxisIsDate?: boolean
- valuePrecision?: number
- shouldFormatBytes?: boolean
- isNetworkChart?: boolean
- isMemoryChart?: boolean
- attributes?: any[]
- sql?: string
- titleTooltip?: string
- showNewBadge?: boolean
- }
- export const ChartHeader = ({
- format,
- highlightedValue,
- highlightedLabel,
- hideHighlightedValue = false,
- hideHighlightedLabel = false,
- hideHighlightArea = false,
- title,
- minimalHeader = false,
- hideChartType = false,
- chartStyle = 'bar',
- onChartStyleChange,
- showMaxValue = false,
- setShowMaxValue,
- docsUrl,
- syncId,
- data,
- xAxisKey,
- yAxisKey,
- xAxisIsDate = true,
- displayDateInUtc,
- customDateFormat,
- valuePrecision = 2,
- shouldFormatBytes = false,
- isNetworkChart = false,
- attributes,
- sql,
- titleTooltip,
- showNewBadge,
- isMemoryChart,
- }: ChartHeaderProps) => {
- const { ref } = useParams()
- const { hoveredIndex, isHovered } = useChartHoverState(syncId || 'default')
- const [localHighlightedValue, setLocalHighlightedValue] = useState(highlightedValue)
- const [localHighlightedLabel, setLocalHighlightedLabel] = useState(highlightedLabel)
- // When `displayDateInUtc` is set the chart explicitly wants UTC labels.
- // Otherwise honour the user's selected timezone via the picker.
- const formatPickerDate = useFormatDateTime()
- const formatHighlightedValue = (value: any) => {
- if (typeof value !== 'number') {
- return value
- }
- if (typeof format === 'function') {
- return format(value)
- }
- if (shouldFormatBytes) {
- const bytesValue = isNetworkChart ? Math.abs(value) : value
- return isMemoryChart
- ? formatBytesMinMB(bytesValue, valuePrecision)
- : formatBytes(bytesValue, valuePrecision)
- }
- if (format === '%') {
- return formatPercentage(value, valuePrecision)
- }
- const formattedValue = numberFormatter(value, valuePrecision)
- if (typeof format === 'string' && format) {
- return `${formattedValue} ${format}`
- }
- return formattedValue
- }
- useEffect(() => {
- if (syncId && hoveredIndex !== null && isHovered && data && xAxisKey && yAxisKey) {
- const activeDataPoint = data[hoveredIndex]
- if (activeDataPoint) {
- // For stacked charts, we need to calculate the total of all attributes
- // that should be included in the total (excluding reference lines, max values, etc.)
- let newValue = activeDataPoint[yAxisKey]
- // If this is a stacked chart with multiple attributes, calculate the total
- if (attributes && attributes.length > 1) {
- const attributesToIgnore =
- attributes
- ?.filter((a) => a.omitFromTotal || a.isMaxValue || a.provider === 'reference-line')
- ?.map((a) => a.attribute) ?? []
- const totalValue = Object.entries(activeDataPoint)
- .filter(([key, value]) => {
- // Include only numeric values that are not in the ignore list
- return (
- typeof value === 'number' &&
- key !== 'timestamp' &&
- key !== 'period_start' &&
- !attributesToIgnore.includes(key) &&
- attributes.some((attr) => attr.attribute === key && attr.enabled !== false)
- )
- })
- .reduce((sum, [_, value]) => sum + (value as number), 0)
- newValue = totalValue
- }
- setLocalHighlightedValue(newValue)
- // Update highlighted label based on sync state
- let newLabel = highlightedLabel
- if (xAxisIsDate && activeDataPoint[xAxisKey]) {
- const value = activeDataPoint[xAxisKey] as number | string
- const fmt = customDateFormat || 'YYYY-MM-DD HH:mm:ss'
- newLabel = displayDateInUtc
- ? formatDateTime(value, { tz: 'UTC', format: fmt })
- : formatPickerDate(value, fmt)
- } else if (activeDataPoint[xAxisKey]) {
- newLabel = activeDataPoint[xAxisKey]
- }
- setLocalHighlightedLabel(newLabel)
- }
- } else {
- // Reset to original values when not syncing
- setLocalHighlightedValue(highlightedValue)
- setLocalHighlightedLabel(highlightedLabel)
- }
- }, [
- hoveredIndex,
- isHovered,
- syncId,
- data,
- xAxisKey,
- yAxisKey,
- xAxisIsDate,
- displayDateInUtc,
- customDateFormat,
- highlightedValue,
- highlightedLabel,
- attributes,
- formatPickerDate,
- ])
- const chartTitle = (
- <div className="flex flex-row items-center gap-x-2">
- <div className="flex flex-row items-center gap-x-2">
- <h3 className={'text-foreground-lighter ' + (minimalHeader ? 'text-xs' : 'text-sm')}>
- {title}
- </h3>
- {titleTooltip && (
- <Tooltip>
- <TooltipTrigger asChild>
- <InfoIcon className="w-4 h-4 text-foreground-lighter" />
- </TooltipTrigger>
- <TooltipContent side="top" className="max-w-xs">
- {titleTooltip}
- {docsUrl && (
- <>
- {' '}
- <Link
- href={docsUrl}
- target="_blank"
- className="underline text-foreground hover:text-foreground-light"
- >
- Read docs
- </Link>
- </>
- )}
- </TooltipContent>
- </Tooltip>
- )}
- </div>
- {!titleTooltip && docsUrl && (
- <ButtonTooltip
- type="text"
- className="px-1"
- asChild
- tooltip={{
- content: {
- side: 'top',
- text: 'Read docs',
- },
- }}
- >
- <Link href={docsUrl} target="_blank">
- <InfoIcon className="w-4 h-4 text-foreground-lighter" />
- </Link>
- </ButtonTooltip>
- )}
- </div>
- )
- const highlighted = (
- <h4
- className={`text-foreground text-xl font-normal ${minimalHeader ? 'text-base' : 'text-2xl'}`}
- >
- {localHighlightedValue !== undefined && formatHighlightedValue(localHighlightedValue)}
- </h4>
- )
- const label = <h4 className="text-foreground-lighter text-xs">{localHighlightedLabel}</h4>
- if (minimalHeader) {
- return (
- <div
- className={cn('flex flex-row items-center gap-x-4', hideHighlightArea && 'hidden')}
- style={{ minHeight: '1.8rem' }}
- >
- {title && chartTitle}
- <div className="flex flex-row items-baseline gap-x-2">
- {highlightedValue !== undefined && !hideHighlightedValue && highlighted}
- {!hideHighlightedLabel && label}
- </div>
- </div>
- )
- }
- const hasHighlightedValue = highlightedValue !== undefined && !hideHighlightedValue
- return (
- <div
- className={cn(
- 'grow flex justify-between items-start min-h-16',
- hideHighlightArea && 'hidden'
- )}
- >
- <div className="flex flex-col">
- <div className="flex items-center gap-2">
- {title && chartTitle}
- {showNewBadge && <Badge variant="success">New</Badge>}
- </div>
- <div className="h-4">
- {hasHighlightedValue && highlighted}
- {!hideHighlightedLabel && label}
- </div>
- </div>
- <div className="flex items-center gap-2">
- {sql ? (
- <ButtonTooltip
- type="default"
- className="px-1.5"
- asChild
- tooltip={{
- content: {
- side: 'top',
- text: 'Open in Log Explorer',
- },
- }}
- >
- <Link href={`/project/${ref}/logs/explorer?q=${encodeURIComponent(sql)}`}>
- <SquareTerminal className="w-4 h-4 text-foreground-lighter" />
- </Link>
- </ButtonTooltip>
- ) : null}
- {!hideChartType && onChartStyleChange && (
- <ButtonTooltip
- type="default"
- className="px-1.5"
- icon={chartStyle === 'bar' ? <Activity /> : <BarChartIcon />}
- onClick={() => onChartStyleChange(chartStyle === 'bar' ? 'line' : 'bar')}
- tooltip={{
- content: {
- side: 'top',
- text: `View as ${chartStyle === 'bar' ? 'line chart' : 'bar chart'}`,
- },
- }}
- />
- )}
- {setShowMaxValue && (
- <ButtonTooltip
- type={showMaxValue ? 'default' : 'dashed'}
- className="px-1.5"
- icon={
- <GitCommitHorizontalIcon
- className={showMaxValue ? 'text-foreground-light' : 'text-foreground-lighter'}
- />
- }
- onClick={() => setShowMaxValue(!showMaxValue)}
- tooltip={{
- content: {
- side: 'top',
- text: `${showMaxValue ? 'Hide' : 'Show'} limit`,
- },
- }}
- />
- )}
- </div>
- </div>
- )
- }
|