mirror of
https://github.com/supabase/supabase.git
synced 2026-06-06 05:17:15 +08:00
feat: improve readability of CPU chart O11Y-1290 (#43822)
## Problem The CPU usage chart in the Observability dashboard could show values exceeding 100%, differing from the internal Grafana panel that support uses for debugging. ## Solution Add a stackedPercent prop to the chart system that locks the Y-axis to [0, 100]. When enabled, the chart never visually overflows 100% while tooltip values remain completely untouched. We fill the rest of the unused with an "Idle" prop. # Before <img width="1240" height="684" alt="CleanShot 2026-03-18 at 16 04 26@2x" src="https://github.com/user-attachments/assets/c1b1a5ac-86e0-4d6b-9bc4-c7837f30c28c" /> ## After <img width="1240" height="684" alt="CleanShot 2026-03-18 at 16 03 13@2x" src="https://github.com/user-attachments/assets/419eea55-176d-46c4-8780-b8c372428047" /> <img width="934" height="700" alt="CleanShot 2026-03-18 at 16 03 41@2x" src="https://github.com/user-attachments/assets/9b5ef61d-21f4-40e4-ab0c-1a11caadb1f8" /> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { computeYAxisDomain } from './Charts.utils'
|
||||
import { computeYAxisDomain, normalizeStackedSeriesData } from './Charts.utils'
|
||||
|
||||
const IOPS_DATA = [
|
||||
{ timestamp: 1, disk_iops_write: 1200, disk_iops_read: 24203, disk_iops_max: 25000 },
|
||||
@@ -172,3 +172,53 @@ describe('computeYAxisDomain', () => {
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('normalizeStackedSeriesData', () => {
|
||||
it('normalizes each stacked point to 100%', () => {
|
||||
const normalized = normalizeStackedSeriesData({
|
||||
data: [{ system: 25, user: 25, idle: 100 }],
|
||||
attributeNames: ['system', 'user', 'idle'],
|
||||
})
|
||||
|
||||
expect(normalized[0].system).toBeCloseTo(16.6666666667)
|
||||
expect(normalized[0].user).toBeCloseTo(16.6666666667)
|
||||
expect(normalized[0].idle).toBeCloseTo(66.6666666666)
|
||||
expect(
|
||||
Number(normalized[0].system) + Number(normalized[0].user) + Number(normalized[0].idle)
|
||||
).toBeCloseTo(100)
|
||||
})
|
||||
|
||||
it('leaves empty stacks unchanged', () => {
|
||||
const empty = [{ system: 0, user: 0, idle: 0 }]
|
||||
|
||||
expect(
|
||||
normalizeStackedSeriesData({
|
||||
data: empty,
|
||||
attributeNames: ['system', 'user', 'idle'],
|
||||
})
|
||||
).toEqual(empty)
|
||||
})
|
||||
|
||||
it('leaves data unchanged when attributeNames is empty', () => {
|
||||
const data = [{ system: 50, user: 30, timestamp: 1000 }]
|
||||
|
||||
expect(
|
||||
normalizeStackedSeriesData({
|
||||
data,
|
||||
attributeNames: [],
|
||||
})
|
||||
).toEqual(data)
|
||||
})
|
||||
|
||||
it('preserves non-stacked keys (timestamps, metadata) unchanged', () => {
|
||||
const data = [{ system: 60, user: 80, period_start: '2024-01-01', timestamp: 1000 }]
|
||||
const normalized = normalizeStackedSeriesData({
|
||||
data,
|
||||
attributeNames: ['system', 'user'],
|
||||
})
|
||||
|
||||
expect(normalized[0].period_start).toBe('2024-01-01')
|
||||
expect(normalized[0].timestamp).toBe(1000)
|
||||
expect(Number(normalized[0].system) + Number(normalized[0].user)).toBeCloseTo(100)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -174,6 +174,45 @@ export function computeYAxisDomain({
|
||||
return [0, Math.max(maxRefValue, maxStackedTotal)]
|
||||
}
|
||||
|
||||
export function normalizeStackedSeriesData<T extends Record<string, unknown>>({
|
||||
data,
|
||||
attributeNames,
|
||||
totalTarget = 100,
|
||||
}: {
|
||||
data: T[]
|
||||
attributeNames: string[]
|
||||
totalTarget?: number
|
||||
}): T[] {
|
||||
return data.map((point) => {
|
||||
const values = attributeNames.map((name) => ({
|
||||
name,
|
||||
value: typeof point[name] === 'number' ? point[name] : 0,
|
||||
}))
|
||||
const total = values.reduce((sum, entry) => sum + entry.value, 0)
|
||||
|
||||
if (total <= 0) return point
|
||||
|
||||
const largestEntry = values.reduce((largest, entry) =>
|
||||
entry.value > largest.value ? entry : largest
|
||||
)
|
||||
|
||||
let normalizedTotal = 0
|
||||
const nextPoint: Record<string, unknown> = { ...point }
|
||||
|
||||
values.forEach(({ name, value }) => {
|
||||
if (name === largestEntry.name) return
|
||||
|
||||
const normalizedValue = (value / total) * totalTarget
|
||||
nextPoint[name] = normalizedValue
|
||||
normalizedTotal += normalizedValue
|
||||
})
|
||||
|
||||
nextPoint[largestEntry.name] = Math.max(0, totalTarget - normalizedTotal)
|
||||
|
||||
return nextPoint as T
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to create common wrapping components, perform data transformations
|
||||
* returns a Container component and the minHeight set
|
||||
|
||||
@@ -29,7 +29,13 @@ import {
|
||||
updateStackedChartColors,
|
||||
} from './Charts.constants'
|
||||
import { CommonChartProps, Datum } from './Charts.types'
|
||||
import { computeYAxisDomain, formatPercentage, numberFormatter, useChartSize } from './Charts.utils'
|
||||
import {
|
||||
computeYAxisDomain,
|
||||
formatPercentage,
|
||||
normalizeStackedSeriesData,
|
||||
numberFormatter,
|
||||
useChartSize,
|
||||
} from './Charts.utils'
|
||||
import {
|
||||
calculateTotalChartAggregate,
|
||||
CustomLabel,
|
||||
@@ -71,6 +77,7 @@ export interface ComposedChartProps<D = Datum> extends CommonChartProps<D> {
|
||||
sql?: string
|
||||
highlightActions?: ChartHighlightAction[]
|
||||
showNewBadge?: boolean
|
||||
normalizeVisibleStackToPercent?: boolean
|
||||
}
|
||||
|
||||
interface CustomizedDotProps {
|
||||
@@ -130,12 +137,12 @@ export function ComposedChart({
|
||||
highlightActions,
|
||||
titleTooltip,
|
||||
showNewBadge,
|
||||
normalizeVisibleStackToPercent = false,
|
||||
}: ComposedChartProps) {
|
||||
const { resolvedTheme } = useTheme()
|
||||
const { hoveredIndex, syncTooltip, setHover, clearHover } = useChartHoverState(
|
||||
syncId || 'default'
|
||||
)
|
||||
const [_activePayload, setActivePayload] = useState<any>(null)
|
||||
const [_showMaxValue, setShowMaxValue] = useState(showMaxValue)
|
||||
const [focusDataIndex, setFocusDataIndex] = useState<number | null>(null)
|
||||
const [isActiveHoveredChart, setIsActiveHoveredChart] = useState(false)
|
||||
@@ -173,6 +180,15 @@ export function ComposedChart({
|
||||
tick: false,
|
||||
width: 0,
|
||||
}
|
||||
const yAxisPadding = useMemo(() => {
|
||||
const needsTopPadding = normalizeVisibleStackToPercent && chartStyle !== 'bar'
|
||||
if (!needsTopPadding) return _YAxisProps.padding
|
||||
|
||||
return {
|
||||
..._YAxisProps.padding,
|
||||
top: Math.max(8, _YAxisProps.padding?.top ?? 0),
|
||||
}
|
||||
}, [_YAxisProps.padding, chartStyle, normalizeVisibleStackToPercent])
|
||||
|
||||
function getHeaderLabel() {
|
||||
if (!xAxisIsDate) {
|
||||
@@ -242,8 +258,24 @@ export function ComposedChart({
|
||||
: undefined
|
||||
|
||||
if (focusDataIndex !== null) {
|
||||
const focusedDataPoint = data[focusDataIndex]
|
||||
? Object.entries(data[focusDataIndex])
|
||||
.map(([key, value]) => ({
|
||||
dataKey: key,
|
||||
value: value as number,
|
||||
}))
|
||||
.filter(
|
||||
(entry) =>
|
||||
entry.dataKey !== 'timestamp' &&
|
||||
entry.dataKey !== 'period_start' &&
|
||||
attributes.some(
|
||||
(attr) => attr.attribute === entry.dataKey && attr.enabled !== false
|
||||
)
|
||||
)
|
||||
: undefined
|
||||
|
||||
return showTotal
|
||||
? calculateTotalChartAggregate(_activePayload, attributesToIgnoreFromTotal)
|
||||
? calculateTotalChartAggregate(focusedDataPoint ?? [], attributesToIgnoreFromTotal)
|
||||
: data[focusDataIndex]?.[yAxisKey]
|
||||
}
|
||||
|
||||
@@ -311,7 +343,20 @@ export function ComposedChart({
|
||||
return !attribute?.isMaxValue
|
||||
})
|
||||
|
||||
const visibleAttributes = stackedAttributes.filter((att) => !hiddenAttributes.has(att.name))
|
||||
const visibleAttributes = useMemo(
|
||||
() => stackedAttributes.filter((att) => !hiddenAttributes.has(att.name)),
|
||||
[stackedAttributes, hiddenAttributes]
|
||||
)
|
||||
const displayData = useMemo(
|
||||
() =>
|
||||
normalizeVisibleStackToPercent
|
||||
? normalizeStackedSeriesData({
|
||||
data,
|
||||
attributeNames: visibleAttributes.map((attribute) => attribute.name),
|
||||
})
|
||||
: data,
|
||||
[data, normalizeVisibleStackToPercent, visibleAttributes]
|
||||
)
|
||||
|
||||
const isPercentage = format === '%'
|
||||
const isRamChart =
|
||||
@@ -327,16 +372,10 @@ export function ComposedChart({
|
||||
const isBytesFormat = format === 'bytes' || format === 'bytes-per-second'
|
||||
const shouldFormatBytes =
|
||||
isBytesFormat || isRamChart || isDiskSpaceChart || isDBSizeChart || isNetworkChart
|
||||
//*
|
||||
// Set the y-axis domain
|
||||
// to the highest value in the chart data for percentage charts
|
||||
// to vertically zoom in on the data
|
||||
// */
|
||||
const yMaxFromVisible = Math.max(
|
||||
0,
|
||||
...visibleAttributes.map((att) => (typeof att.value === 'number' ? att.value : 0))
|
||||
)
|
||||
const yDomain = [0, yMaxFromVisible]
|
||||
|
||||
const yAxisDomain = useMemo(
|
||||
() =>
|
||||
@@ -409,30 +448,31 @@ export function ComposedChart({
|
||||
/>
|
||||
<Container className="relative z-10">
|
||||
<RechartComposedChart
|
||||
data={data}
|
||||
data={displayData}
|
||||
syncId={syncId}
|
||||
style={{ cursor: 'crosshair' }}
|
||||
onMouseMove={({ activeLabel, activeTooltipIndex, activePayload }) => {
|
||||
if (!activeTooltipIndex) return
|
||||
onMouseMove={({ activeLabel, activeTooltipIndex }) => {
|
||||
if (activeTooltipIndex === undefined || activeTooltipIndex === null) return
|
||||
|
||||
setIsActiveHoveredChart(true)
|
||||
if (activeTooltipIndex !== focusDataIndex) {
|
||||
setFocusDataIndex(activeTooltipIndex)
|
||||
setActivePayload(activePayload ?? [])
|
||||
}
|
||||
|
||||
setHover(activeTooltipIndex)
|
||||
|
||||
const activeTimestamp = data[activeTooltipIndex]?.timestamp
|
||||
const activeTimestamp =
|
||||
data[activeTooltipIndex]?.[xAxisKey] ?? data[activeTooltipIndex]?.timestamp
|
||||
chartHighlight?.handleMouseMove({
|
||||
activeLabel: activeTimestamp?.toString(),
|
||||
coordinates: activeLabel,
|
||||
})
|
||||
}}
|
||||
onMouseDown={({ activeLabel, activeTooltipIndex }) => {
|
||||
if (!activeTooltipIndex) return
|
||||
if (activeTooltipIndex === undefined || activeTooltipIndex === null) return
|
||||
|
||||
const activeTimestamp = data[activeTooltipIndex]?.timestamp
|
||||
const activeTimestamp =
|
||||
data[activeTooltipIndex]?.[xAxisKey] ?? data[activeTooltipIndex]?.timestamp
|
||||
chartHighlight?.handleMouseDown({
|
||||
activeLabel: activeTimestamp?.toString(),
|
||||
coordinates: activeLabel,
|
||||
@@ -442,7 +482,6 @@ export function ComposedChart({
|
||||
onMouseLeave={() => {
|
||||
setIsActiveHoveredChart(false)
|
||||
setFocusDataIndex(null)
|
||||
setActivePayload(null)
|
||||
|
||||
clearHover()
|
||||
}}
|
||||
@@ -457,7 +496,8 @@ export function ComposedChart({
|
||||
hide={hideYAxis}
|
||||
axisLine={{ stroke: CHART_COLORS.AXIS }}
|
||||
tickLine={{ stroke: CHART_COLORS.AXIS }}
|
||||
domain={yAxisDomain}
|
||||
domain={_YAxisProps.domain ?? yAxisDomain}
|
||||
padding={yAxisPadding}
|
||||
key={yAxisKey}
|
||||
/>
|
||||
<XAxis
|
||||
@@ -571,10 +611,12 @@ export function ComposedChart({
|
||||
showTooltip && !showHighlightActions ? (
|
||||
<CustomTooltip
|
||||
{...props}
|
||||
data={data}
|
||||
format={format}
|
||||
isPercentage={isPercentage}
|
||||
label={resolvedHighlightedLabel}
|
||||
attributes={attributes}
|
||||
xAxisKey={xAxisKey}
|
||||
valuePrecision={valuePrecision}
|
||||
showTotal={showTotal}
|
||||
isActiveHoveredChart={
|
||||
|
||||
@@ -4,6 +4,7 @@ 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'
|
||||
|
||||
@@ -30,7 +31,10 @@ export interface ReportAttributes {
|
||||
YAxisProps?: {
|
||||
width?: number
|
||||
tickFormatter?: (value: any) => string
|
||||
domain?: [number | string, number | string]
|
||||
allowDataOverflow?: boolean
|
||||
}
|
||||
normalizeVisibleStackToPercent?: boolean
|
||||
hideHighlightedValue?: boolean
|
||||
}
|
||||
|
||||
@@ -111,6 +115,8 @@ interface TooltipProps {
|
||||
payload?: any[]
|
||||
label?: string | number
|
||||
attributes?: MultiAttribute[]
|
||||
data?: Record<string, unknown>[]
|
||||
xAxisKey?: string
|
||||
isPercentage?: boolean
|
||||
format?: string | ((value: unknown) => string)
|
||||
valuePrecision?: number
|
||||
@@ -139,6 +145,8 @@ export const CustomTooltip = ({
|
||||
payload,
|
||||
label,
|
||||
attributes,
|
||||
data,
|
||||
xAxisKey = 'period_start',
|
||||
isPercentage,
|
||||
format,
|
||||
valuePrecision,
|
||||
@@ -152,10 +160,15 @@ export const CustomTooltip = ({
|
||||
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 maxValueData =
|
||||
maxValueAttribute && payload?.find((p: any) => p.dataKey === maxValueAttribute.attribute)
|
||||
const maxValue = maxValueData?.value
|
||||
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_'))
|
||||
@@ -182,7 +195,15 @@ export const CustomTooltip = ({
|
||||
|
||||
const localTimeZone = dayjs.tz.guess()
|
||||
|
||||
const total = showTotal && calculateTotalChartAggregate(payload, attributesToIgnoreFromTotal)
|
||||
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 ? <MaxConnectionsIcon /> : <CustomIcon color={color} />
|
||||
@@ -196,7 +217,14 @@ export const CustomTooltip = ({
|
||||
|
||||
const LabelItem = ({ entry }: { entry: any }) => {
|
||||
const attribute = attributes?.find((a: MultiAttribute) => a?.attribute === entry.name)
|
||||
const percentage = ((entry.value / maxValue) * 100).toFixed(valuePrecision)
|
||||
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 (
|
||||
@@ -206,12 +234,12 @@ export const CustomTooltip = ({
|
||||
{attribute?.label || entry.name}
|
||||
</span>
|
||||
<span className="ml-3.5 flex items-end gap-1">
|
||||
{formatNumeric(entry.value) + (!isPercentage && format !== 'ms' ? byteUnitSuffix : '')}
|
||||
{formatNumeric(rawValue) + (!isPercentage && format !== 'ms' ? byteUnitSuffix : '')}
|
||||
{isPercentage ? '%' : ''}
|
||||
{format === 'ms' ? 'ms' : ''}
|
||||
|
||||
{/* Show percentage if max value is set */}
|
||||
{!!maxValueData && !isMax && !isPercentage && (
|
||||
{percentage !== null && !isMax && !isPercentage && (
|
||||
<span className="text-[11px] text-foreground-light mb-0.5">({percentage}%)</span>
|
||||
)}
|
||||
</span>
|
||||
@@ -229,7 +257,7 @@ export const CustomTooltip = ({
|
||||
<p className="text-foreground-light text-xs">{localTimeZone}</p>
|
||||
<p className="font-medium">{dayjs(timestamp).format(DateTimeFormats.FULL_SECONDS)}</p>
|
||||
<div className="grid gap-0">
|
||||
{payload.reverse().map((entry: any, index: number) => (
|
||||
{[...payload].reverse().map((entry: any, index: number) => (
|
||||
<LabelItem key={`${entry.name}-${index}`} entry={entry} />
|
||||
))}
|
||||
{active && showTotal && (
|
||||
@@ -244,11 +272,12 @@ export const CustomTooltip = ({
|
||||
{format === 'ms' ? 'ms' : ''}
|
||||
</span>
|
||||
{maxValueAttribute &&
|
||||
hasFiniteMaxValue &&
|
||||
!isPercentage &&
|
||||
!isNaN((total as number) / maxValueData?.value) &&
|
||||
isFinite((total as number) / maxValueData?.value) && (
|
||||
!isNaN((total as number) / maxValue) &&
|
||||
isFinite((total as number) / maxValue) && (
|
||||
<span className="text-[11px] text-foreground-light mb-0.5">
|
||||
({(((total as number) / maxValueData?.value) * 100).toFixed(1)}%)
|
||||
({(((total as number) / maxValue) * 100).toFixed(1)}%)
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,24 +1,22 @@
|
||||
import { List, Loader2 } from 'lucide-react'
|
||||
import { useRouter } from 'next/router'
|
||||
import React, { PropsWithChildren, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { Card, cn, WarningIcon } from 'ui'
|
||||
|
||||
import Panel from 'components/ui/Panel'
|
||||
import type { ChartHighlightAction } from './ChartHighlightActions'
|
||||
import { ComposedChart } from './ComposedChart'
|
||||
|
||||
import { AnalyticsInterval, DataPoint } from 'data/analytics/constants'
|
||||
import { useInfraMonitoringQueries } from 'data/analytics/infra-monitoring-queries'
|
||||
import { InfraMonitoringAttribute } from 'data/analytics/infra-monitoring-query'
|
||||
import { useProjectDailyStatsQueries } from 'data/analytics/project-daily-stats-queries'
|
||||
import { ProjectDailyStatsAttribute } from 'data/analytics/project-daily-stats-query'
|
||||
import { useDatabaseSelectorStateSnapshot } from 'state/database-selector'
|
||||
import { useChartHighlight } from './useChartHighlight'
|
||||
|
||||
import dayjs from 'dayjs'
|
||||
import { List, Loader2 } from 'lucide-react'
|
||||
import { useRouter } from 'next/router'
|
||||
import type { UpdateDateRange } from 'pages/project/[ref]/observability/database'
|
||||
import React, { PropsWithChildren, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { useDatabaseSelectorStateSnapshot } from 'state/database-selector'
|
||||
import { Card, cn, WarningIcon } from 'ui'
|
||||
|
||||
import type { ChartHighlightAction } from './ChartHighlightActions'
|
||||
import type { ChartData } from './Charts.types'
|
||||
import { ComposedChart } from './ComposedChart'
|
||||
import { MultiAttribute } from './ComposedChart.utils'
|
||||
import { useChartHighlight } from './useChartHighlight'
|
||||
|
||||
export interface ComposedChartHandlerProps {
|
||||
id?: string
|
||||
@@ -30,7 +28,7 @@ export interface ComposedChartHandlerProps {
|
||||
customDateFormat?: string
|
||||
defaultChartStyle?: 'bar' | 'line' | 'stackedAreaLine'
|
||||
hideChartType?: boolean
|
||||
data?: ChartData
|
||||
data?: ChartData | DataPoint[]
|
||||
isLoading?: boolean
|
||||
format?: string
|
||||
highlightedValue?: string | number
|
||||
@@ -39,6 +37,7 @@ export interface ComposedChartHandlerProps {
|
||||
showLegend?: boolean
|
||||
showTotal?: boolean
|
||||
showMaxValue?: boolean
|
||||
normalizeVisibleStackToPercent?: boolean
|
||||
updateDateRange?: UpdateDateRange
|
||||
valuePrecision?: number
|
||||
isVisible?: boolean
|
||||
@@ -134,12 +133,12 @@ const ComposedChartHandler = ({
|
||||
endDate,
|
||||
interval as AnalyticsInterval,
|
||||
databaseIdentifier,
|
||||
data,
|
||||
Array.isArray(data) ? undefined : data,
|
||||
isVisible
|
||||
)
|
||||
|
||||
const combinedData = useMemo(() => {
|
||||
if (data) return data
|
||||
if (data) return Array.isArray(data) ? data : data.data
|
||||
|
||||
const isLoading = attributeQueries.some((query: any) => query.isLoading)
|
||||
if (isLoading) return undefined
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { compactNumberFormatter, numberFormatter } from 'components/ui/Charts/Charts.utils'
|
||||
import { compactNumberFormatter } from 'components/ui/Charts/Charts.utils'
|
||||
import { ReportAttributes } from 'components/ui/Charts/ComposedChart.utils'
|
||||
import { DOCS_URL } from 'lib/constants'
|
||||
import { formatBytes } from 'lib/helpers'
|
||||
@@ -76,13 +76,12 @@ export const getReportAttributesV2: (
|
||||
showLegend: true,
|
||||
showMaxValue: false,
|
||||
showGrid: true,
|
||||
normalizeVisibleStackToPercent: true,
|
||||
YAxisProps: {
|
||||
width: 45,
|
||||
tickFormatter: (value: any) => {
|
||||
// avoid displaying 100.00%
|
||||
if (value === 100) return '100%'
|
||||
return `${numberFormatter(value, 2)}%`
|
||||
},
|
||||
width: 55,
|
||||
domain: [0, 100] as [number, number],
|
||||
allowDataOverflow: true,
|
||||
tickFormatter: (v: number) => `${Math.round(v)}%`,
|
||||
},
|
||||
hideChartType: false,
|
||||
defaultChartStyle: 'bar',
|
||||
@@ -92,6 +91,8 @@ export const getReportAttributesV2: (
|
||||
provider: 'infra-monitoring',
|
||||
label: 'System',
|
||||
format: '%',
|
||||
color: { light: '#EDC35E', dark: '#EDD35E' },
|
||||
fill: { light: '#F6D99F', dark: '#5C5230' },
|
||||
tooltip:
|
||||
'CPU time spent on kernel operations (e.g., process scheduling, memory management). High values may indicate system overhead',
|
||||
},
|
||||
@@ -100,6 +101,8 @@ export const getReportAttributesV2: (
|
||||
provider: 'infra-monitoring',
|
||||
label: 'User',
|
||||
format: '%',
|
||||
color: { light: '#0063E8', dark: '#65BCD9' },
|
||||
fill: { light: '#80B1F4', dark: '#2A3D45' },
|
||||
tooltip:
|
||||
'CPU time used by database queries and user-space processes. High values may suggest CPU-intensive queries',
|
||||
},
|
||||
@@ -108,6 +111,8 @@ export const getReportAttributesV2: (
|
||||
provider: 'infra-monitoring',
|
||||
label: 'IOwait',
|
||||
format: '%',
|
||||
color: { light: '#DB3A34', dark: '#FF6B6B' },
|
||||
fill: { light: '#F2A7A3', dark: '#5C2A2A' },
|
||||
tooltip:
|
||||
'CPU time waiting for disk or network I/O. High values may indicate disk bottlenecks',
|
||||
},
|
||||
@@ -116,6 +121,8 @@ export const getReportAttributesV2: (
|
||||
provider: 'infra-monitoring',
|
||||
label: 'IRQs',
|
||||
format: '%',
|
||||
color: { light: '#DA760B', dark: '#DA760B' },
|
||||
fill: { light: '#FFB885', dark: '#5C3D0A' },
|
||||
tooltip: 'CPU time handling hardware interrupt requests (IRQ)',
|
||||
},
|
||||
{
|
||||
@@ -123,9 +130,21 @@ export const getReportAttributesV2: (
|
||||
provider: 'infra-monitoring',
|
||||
label: 'Other',
|
||||
format: '%',
|
||||
color: { light: '#B616A6', dark: '#DB8DF9' },
|
||||
fill: { light: '#DB8BD3', dark: '#4A3D5C' },
|
||||
tooltip:
|
||||
'CPU time spent on other tasks (e.g., background processes, software interrupts)',
|
||||
},
|
||||
{
|
||||
attribute: 'cpu_usage_busy_idle',
|
||||
provider: 'infra-monitoring',
|
||||
label: 'Idle',
|
||||
format: '%',
|
||||
omitFromTotal: true,
|
||||
color: { light: '#6EA85F', dark: '#A3FFC2' },
|
||||
fill: { light: '#A6D8AE', dark: '#2A5C3F' },
|
||||
tooltip: 'CPU time spent idle and available for new work',
|
||||
},
|
||||
{
|
||||
attribute: 'cpu_usage_max',
|
||||
provider: 'reference-line',
|
||||
|
||||
Reference in New Issue
Block a user