mirror of
https://github.com/supabase/supabase.git
synced 2026-10-06 09:55:06 +03:00
## 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>
225 lines
7.0 KiB
TypeScript
225 lines
7.0 KiB
TypeScript
import { describe, expect, it } from 'vitest'
|
|
|
|
import { computeYAxisDomain, normalizeStackedSeriesData } from './Charts.utils'
|
|
|
|
const IOPS_DATA = [
|
|
{ timestamp: 1, disk_iops_write: 1200, disk_iops_read: 24203, disk_iops_max: 25000 },
|
|
{ timestamp: 2, disk_iops_write: 400, disk_iops_read: 3200, disk_iops_max: 25000 },
|
|
{ timestamp: 3, disk_iops_write: 100, disk_iops_read: 900, disk_iops_max: 25000 },
|
|
]
|
|
|
|
const IOPS_VISIBLE = ['disk_iops_write', 'disk_iops_read']
|
|
|
|
describe('computeYAxisDomain', () => {
|
|
describe('percentage charts with max line hidden', () => {
|
|
it('returns [0, yMaxFromVisible] to zoom in on the data', () => {
|
|
expect(
|
|
computeYAxisDomain({
|
|
isPercentage: true,
|
|
showMaxValue: false,
|
|
yMaxFromVisible: 75,
|
|
maxAttributeKey: 'cpu_usage_max',
|
|
showMaxLine: false,
|
|
data: [{ cpu_busy: 75, cpu_usage_max: 100 }],
|
|
visibleAttributeNames: ['cpu_busy'],
|
|
})
|
|
).toEqual([0, 75])
|
|
})
|
|
|
|
it('still zooms in even when a maxAttributeKey is present', () => {
|
|
expect(
|
|
computeYAxisDomain({
|
|
isPercentage: true,
|
|
showMaxValue: false,
|
|
yMaxFromVisible: 60,
|
|
maxAttributeKey: 'cpu_usage_max',
|
|
showMaxLine: true,
|
|
data: [{ cpu_busy: 60, cpu_usage_max: 100 }],
|
|
visibleAttributeNames: ['cpu_busy'],
|
|
})
|
|
).toEqual([0, 60])
|
|
})
|
|
})
|
|
|
|
describe('no max reference line', () => {
|
|
it('returns auto when maxAttributeKey is undefined', () => {
|
|
expect(
|
|
computeYAxisDomain({
|
|
isPercentage: false,
|
|
showMaxValue: false,
|
|
yMaxFromVisible: 5000,
|
|
maxAttributeKey: undefined,
|
|
showMaxLine: false,
|
|
data: IOPS_DATA,
|
|
visibleAttributeNames: IOPS_VISIBLE,
|
|
})
|
|
).toEqual(['auto', 'auto'])
|
|
})
|
|
|
|
it('returns auto when showMaxLine is false', () => {
|
|
expect(
|
|
computeYAxisDomain({
|
|
isPercentage: false,
|
|
showMaxValue: true,
|
|
yMaxFromVisible: 5000,
|
|
maxAttributeKey: 'disk_iops_max',
|
|
showMaxLine: false,
|
|
data: IOPS_DATA,
|
|
visibleAttributeNames: IOPS_VISIBLE,
|
|
})
|
|
).toEqual(['auto', 'auto'])
|
|
})
|
|
})
|
|
|
|
describe('max reference line not yet loaded (value is 0)', () => {
|
|
it('returns auto when diskConfig has not loaded and reference line value is 0', () => {
|
|
const dataWithZeroMax = IOPS_DATA.map((p) => ({ ...p, disk_iops_max: 0 }))
|
|
expect(
|
|
computeYAxisDomain({
|
|
isPercentage: false,
|
|
showMaxValue: true,
|
|
yMaxFromVisible: 5000,
|
|
maxAttributeKey: 'disk_iops_max',
|
|
showMaxLine: true,
|
|
data: dataWithZeroMax,
|
|
visibleAttributeNames: IOPS_VISIBLE,
|
|
})
|
|
).toEqual(['auto', 'auto'])
|
|
})
|
|
})
|
|
|
|
describe('explicit domain with reference line', () => {
|
|
it('uses the reference line value when bars stay below it', () => {
|
|
// All stacked bar totals (1000, 500) are well below maxRefValue (25000)
|
|
expect(
|
|
computeYAxisDomain({
|
|
isPercentage: false,
|
|
showMaxValue: true,
|
|
yMaxFromVisible: 800,
|
|
maxAttributeKey: 'disk_iops_max',
|
|
showMaxLine: true,
|
|
data: [
|
|
{ disk_iops_write: 400, disk_iops_read: 600, disk_iops_max: 25000 },
|
|
{ disk_iops_write: 200, disk_iops_read: 300, disk_iops_max: 25000 },
|
|
],
|
|
visibleAttributeNames: IOPS_VISIBLE,
|
|
})
|
|
).toEqual([0, 25000])
|
|
})
|
|
|
|
it('uses the stacked bar total when it exceeds the reference line', () => {
|
|
// Stacked total at first point: 24203 + 1200 = 25403 > 25000
|
|
expect(
|
|
computeYAxisDomain({
|
|
isPercentage: false,
|
|
showMaxValue: true,
|
|
yMaxFromVisible: 24203,
|
|
maxAttributeKey: 'disk_iops_max',
|
|
showMaxLine: true,
|
|
data: IOPS_DATA,
|
|
visibleAttributeNames: IOPS_VISIBLE,
|
|
})
|
|
).toEqual([0, 25403])
|
|
})
|
|
|
|
it('domain min is always 0', () => {
|
|
const [min] = computeYAxisDomain({
|
|
isPercentage: false,
|
|
showMaxValue: true,
|
|
yMaxFromVisible: 100,
|
|
maxAttributeKey: 'disk_iops_max',
|
|
showMaxLine: true,
|
|
data: IOPS_DATA,
|
|
visibleAttributeNames: IOPS_VISIBLE,
|
|
}) as [number, number]
|
|
expect(min).toBe(0)
|
|
})
|
|
|
|
it('works for database connections chart (single bar series, no stacking)', () => {
|
|
const data = [
|
|
{ pg_stat_database_num_backends: 45, max_db_connections: 60 },
|
|
{ pg_stat_database_num_backends: 52, max_db_connections: 60 },
|
|
]
|
|
expect(
|
|
computeYAxisDomain({
|
|
isPercentage: false,
|
|
showMaxValue: true,
|
|
yMaxFromVisible: 52,
|
|
maxAttributeKey: 'max_db_connections',
|
|
showMaxLine: true,
|
|
data,
|
|
visibleAttributeNames: ['pg_stat_database_num_backends'],
|
|
})
|
|
).toEqual([0, 60])
|
|
})
|
|
|
|
it('handles non-numeric values in data gracefully', () => {
|
|
const data = [
|
|
{ disk_iops_write: 'bad', disk_iops_read: null, disk_iops_max: 25000 },
|
|
{ disk_iops_write: 500, disk_iops_read: 1000, disk_iops_max: 25000 },
|
|
]
|
|
expect(
|
|
computeYAxisDomain({
|
|
isPercentage: false,
|
|
showMaxValue: true,
|
|
yMaxFromVisible: 1000,
|
|
maxAttributeKey: 'disk_iops_max',
|
|
showMaxLine: true,
|
|
data: data as Record<string, unknown>[],
|
|
visibleAttributeNames: IOPS_VISIBLE,
|
|
})
|
|
).toEqual([0, 25000])
|
|
})
|
|
})
|
|
})
|
|
|
|
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)
|
|
})
|
|
})
|