'use client' import dayjs from 'dayjs' import { formatBytes } from 'lib/helpers' 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' 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 } 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[] 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, attributes, isPercentage, format, valuePrecision, showTotal, isActiveHoveredChart, }: TooltipProps) => { 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 maxValueAttribute = isMaxAttribute(attributes) const maxValueData = maxValueAttribute && payload?.find((p: any) => p.dataKey === maxValueAttribute.attribute) const maxValue = maxValueData?.value const isRamChart = !payload?.some((p: any) => p.dataKey.toLowerCase() === 'ram_usage') && payload?.some((p: any) => p.dataKey.toLowerCase().includes('ram_')) 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 || isRamChart || 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 = dayjs.tz.guess() const total = showTotal && calculateTotalChartAggregate(payload, attributesToIgnoreFromTotal) const getIcon = (color: string, isMax: boolean) => isMax ? : const formatNumeric = (value: number) => { if (!shouldFormatBytes && valuePrecision === 0 && value > 0 && value < 1) return '<1' return shouldFormatBytes ? formatBytes(isNetworkChart ? Math.abs(value) : value, valuePrecision) : numberFormatter(value, valuePrecision) } const LabelItem = ({ entry }: { entry: any }) => { const attribute = attributes?.find((a: MultiAttribute) => a?.attribute === entry.name) const percentage = ((entry.value / maxValue) * 100).toFixed(valuePrecision) const isMax = entry.dataKey === maxValueAttribute?.attribute return (
{getIcon(entry.color, isMax)} {attribute?.label || entry.name} {formatNumeric(entry.value) + (!isPercentage && format !== 'ms' ? byteUnitSuffix : '')} {isPercentage ? '%' : ''} {format === 'ms' ? 'ms' : ''} {/* Show percentage if max value is set */} {!!maxValueData && !isMax && !isPercentage && ( ({percentage}%) )}
) } return (

{localTimeZone}

{dayjs(timestamp).format(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 && !isPercentage && !isNaN((total as number) / maxValueData?.value) && isFinite((total as number) / maxValueData?.value) && ( ({(((total as number) / maxValueData?.value) * 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 [hoveredLabel, 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 ( ) } return (
{items?.map((entry, index) => )}
) }