'use client' import { useState } from 'react' import { cn, Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from 'ui' import { CHART_COLORS, DateTimeFormats } from './Charts.constants' import { formatPercentage, numberFormatter } from './Charts.utils' import { useFormatDateTime, useTimezone } from '@/lib/datetime' import { formatBytes, formatBytesMinMB } from '@/lib/helpers' export interface ReportAttributes { id?: string titleTooltip?: string label: string attributes?: (MultiAttribute | false)[] defaultChartStyle?: 'bar' | 'line' | 'stackedAreaLine' hide?: boolean entitlement?: string requiredPlan?: string hideChartType?: boolean format?: string className?: string showTooltip?: boolean showLegend?: boolean showTotal?: boolean showMaxValue?: boolean valuePrecision?: number docsUrl?: string syncId?: string showGrid?: boolean YAxisProps?: { width?: number tickFormatter?: (value: any) => string domain?: [number | string, number | string] allowDataOverflow?: boolean } normalizeVisibleStackToPercent?: boolean hideHighlightedValue?: boolean } export type Provider = 'infra-monitoring' | 'daily-stats' | 'mock' | 'reference-line' | 'logs' export type MultiAttribute = { attribute: string provider?: Provider label?: string color?: { light: string dark: string } fill?: { light?: string dark?: string } statusCode?: string grantType?: string providerType?: string stackId?: string format?: string description?: string docsLink?: string isMaxValue?: boolean type?: 'line' | 'area-bar' omitFromTotal?: boolean tooltip?: string customValue?: number [key: string]: any /** * Manipulate the value of the attribute before it is displayed on the chart. * @param value - The value of the attribute. * @returns The manipulated value. */ manipulateValue?: (value: number) => number /** * Create a virtual attribute by combining values from other attributes. * Expression should use attribute names and basic math operators (+, -, *, /). * Example: 'disk_fs_used - pg_database_size - disk_fs_used_wal' */ combine?: string id?: string value?: number isReferenceLine?: boolean strokeDasharray?: string className?: string hide?: boolean enabled?: boolean } interface CustomIconProps { color: string } const CustomIcon = ({ color }: CustomIconProps) => ( ) const MaxConnectionsIcon = ({ color }: { color?: string }) => ( ) interface TooltipProps { active?: boolean payload?: any[] label?: string | number attributes?: MultiAttribute[] data?: Record[] xAxisKey?: string isPercentage?: boolean format?: string | ((value: unknown) => string) valuePrecision?: number showMaxValue?: boolean showTotal?: boolean isActiveHoveredChart?: boolean } const isMaxAttribute = (attributes?: MultiAttribute[]) => attributes?.find((a) => a.isMaxValue) /** * Calculate the total aggregate of the chart values * by summing the values of the attributes * that are not in the `ignoreAttributes` array */ export const calculateTotalChartAggregate = ( payload: { dataKey: string; value: number }[], ignoreAttributes?: string[] ) => payload ?.filter((p) => !ignoreAttributes?.includes(p.dataKey)) .reduce((acc, curr) => acc + curr.value, 0) export const CustomTooltip = ({ active, payload, label: _label, attributes, data, xAxisKey = 'period_start', isPercentage, format, valuePrecision, showTotal, isActiveHoveredChart, }: TooltipProps) => { const formatDateTime = useFormatDateTime() const { timezone } = useTimezone() if (active && payload && payload.length) { /** * Depending on the data source, the timestamp key could be 'timestamp' or 'period_start' */ const firstItem = payload[0].payload const timestampKey = firstItem?.hasOwnProperty('timestamp') ? 'timestamp' : 'period_start' const timestamp = payload[0].payload[timestampKey] const rawDataPoint = data?.find( (point) => point[xAxisKey] === timestamp || point[timestampKey] === timestamp ) const maxValueAttribute = isMaxAttribute(attributes) const maxValue = maxValueAttribute && rawDataPoint ? Number(rawDataPoint[maxValueAttribute.attribute]) : undefined const hasFiniteMaxValue = typeof maxValue === 'number' && Number.isFinite(maxValue) const isRamChart = !payload?.some((p: any) => p.dataKey.toLowerCase() === 'ram_usage') && payload?.some((p: any) => p.dataKey.toLowerCase().includes('ram_')) const isSwapChart = payload?.some((p: any) => p.dataKey.toLowerCase().includes('swap_')) const isMemoryChart = isRamChart || isSwapChart const isDBSizeChart = payload?.some((p: any) => p.dataKey.toLowerCase().includes('disk_fs_')) || payload?.some((p: any) => p.dataKey.toLowerCase().includes('pg_database_size')) const isNetworkChart = payload?.some((p: any) => p.dataKey.toLowerCase().includes('network_')) const isBytesFormat = format === 'bytes' || format === 'bytes-per-second' const shouldFormatBytes = isBytesFormat || isMemoryChart || isDBSizeChart || isNetworkChart const byteUnitSuffix = format === 'bytes-per-second' ? '/s' : '' const attributesToIgnore = attributes?.filter((a) => a.omitFromTotal)?.map((a) => a.attribute) ?? [] const referenceLines = attributes ?.filter((attribute: MultiAttribute) => attribute?.provider === 'reference-line') ?.map((a: MultiAttribute) => a.attribute) ?? [] const attributesToIgnoreFromTotal = [ ...attributesToIgnore, ...referenceLines, ...(maxValueAttribute?.attribute ? [maxValueAttribute.attribute] : []), ] const localTimeZone = timezone const rawPayload = payload.map((entry: any) => ({ ...entry, value: rawDataPoint && typeof rawDataPoint[entry.dataKey] === 'number' ? Number(rawDataPoint[entry.dataKey]) : entry.value, })) const total = showTotal && calculateTotalChartAggregate(rawPayload, attributesToIgnoreFromTotal) const getIcon = (color: string, isMax: boolean) => isMax ? : const formatNumeric = (value: number) => { if (!shouldFormatBytes && valuePrecision === 0 && value > 0 && value < 1) return '<1' if (shouldFormatBytes) { const val = isNetworkChart ? Math.abs(value) : value if (isMemoryChart) return formatBytesMinMB(val, valuePrecision) return formatBytes(val, valuePrecision) } const formatted = numberFormatter(value, valuePrecision) if ( !isBytesFormat && format !== '%' && format !== 'ms' && typeof format === 'string' && format ) { return `${formatted}${format}` } return formatted } const LabelItem = ({ entry }: { entry: any }) => { const attribute = attributes?.find((a: MultiAttribute) => a?.attribute === entry.name) const rawValue = rawDataPoint && typeof rawDataPoint[entry.dataKey] === 'number' ? Number(rawDataPoint[entry.dataKey]) : entry.value const percentage = hasFiniteMaxValue && maxValue > 0 ? ((rawValue / maxValue) * 100).toFixed(valuePrecision) : null const isMax = entry.dataKey === maxValueAttribute?.attribute return ( {getIcon(entry.color, isMax)} {attribute?.label || entry.name} {formatNumeric(rawValue) + (!isPercentage && format !== 'ms' ? byteUnitSuffix : '')} {isPercentage ? '%' : ''} {format === 'ms' ? 'ms' : ''} {/* Show percentage if max value is set */} {percentage !== null && !isMax && !isPercentage && ( ({percentage}%) )} ) } return ( {localTimeZone} {formatDateTime(timestamp, DateTimeFormats.FULL_SECONDS)} {[...payload].reverse().map((entry: any, index: number) => ( ))} {active && showTotal && ( Total {isPercentage ? formatPercentage(total as number, valuePrecision) : formatNumeric(total as number) + (!isPercentage && format !== 'ms' ? byteUnitSuffix : '')} {format === 'ms' ? 'ms' : ''} {maxValueAttribute && hasFiniteMaxValue && !isPercentage && !isNaN((total as number) / maxValue) && isFinite((total as number) / maxValue) && ( ({(((total as number) / maxValue) * 100).toFixed(1)}%) )} )} ) } return null } interface CustomLabelProps { payload?: any[] attributes?: MultiAttribute[] showMaxValue?: boolean onLabelHover?: (label: string | null) => void onToggleAttribute?: (attribute: string, options?: { exclusive?: boolean }) => void hiddenAttributes?: Set } export const CustomLabel = ({ payload, attributes, showMaxValue, onLabelHover, onToggleAttribute, hiddenAttributes, }: CustomLabelProps) => { const items = payload ?? [] const maxValueAttribute = isMaxAttribute(attributes) const [, setHoveredLabel] = useState(null) const handleMouseEnter = (label: string) => { setHoveredLabel(label) onLabelHover?.(label) } const handleMouseLeave = () => { setHoveredLabel(null) onLabelHover?.(null) } const getIcon = (name: string, color: string) => { switch (name === maxValueAttribute?.attribute) { case true: return default: return } } const LabelItem = ({ entry }: { entry: any }) => { const attribute = attributes?.find((a) => a.attribute === entry.name) const isMax = entry.name === maxValueAttribute?.attribute const isHidden = hiddenAttributes?.has(entry.name) const color = isHidden ? 'gray' : entry.color const Label = () => ( {getIcon(entry.name, color)} {attribute?.label || entry.name} ) if (!showMaxValue && isMax) return null return ( handleMouseEnter(entry.name)} onMouseOutCapture={handleMouseLeave} onClick={(e) => onToggleAttribute?.(entry.name, { exclusive: e.metaKey || e.ctrlKey })} > {!!attribute?.tooltip ? ( {attribute.tooltip} ) : ( )} ) } return ( {items?.map((entry, index) => ( ))} ) }
{localTimeZone}
{formatDateTime(timestamp, DateTimeFormats.FULL_SECONDS)}