mirror of
https://github.com/supabase/supabase.git
synced 2026-09-08 19:08:44 +08:00
## Context Related to Explorer / Notebooks - this adds chart functionality for the Query cells <img width="250" alt="image" src="https://github.com/user-attachments/assets/4ea37c14-87dc-4c43-ba7f-cb9436085c81" /> Query results can be rendered as either bar or line chart - using the chart packages from `ui-patterns` [NOTE]: For design team reviewers - am patching the chart packages to be agnostic to the `timestamp` property within the provided data set. Would love to use this component from a consistency POV instead of the old `BarChart` component we have. Have intentionally omitted log scale functionality from this PR - will have that separately 🙏 <img width="999" height="483" alt="image" src="https://github.com/user-attachments/assets/14356ee4-c658-4fd1-90e0-17c38dac4822" /> <img width="988" height="478" alt="image" src="https://github.com/user-attachments/assets/cd0ca088-9a03-4aa3-9884-17bc36d3cabf" /> <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Added chart views for notebook query results, including bar and line charts. - Added display settings for selecting X/Y columns, chart type, scale, cumulative values, and label visibility. - Added configurable X-axis support for charts. - Display preferences are saved with each notebook cell. - **Improvements** - New database cells default to table view. - Chart results better handle varied data types. - Empty results and incomplete chart settings now display clear placeholders. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
194 lines
5.5 KiB
TypeScript
194 lines
5.5 KiB
TypeScript
import { describe, expect, it } from 'vitest'
|
|
|
|
import {
|
|
checkHasNonPositiveValues,
|
|
formatLogTick,
|
|
getCumulativeResults,
|
|
} from '@/components/ui/QueryBlock/QueryBlock.utils'
|
|
|
|
describe('checkHasNonPositiveValues', () => {
|
|
it('returns false for an empty array', () => {
|
|
expect(checkHasNonPositiveValues([], 'value')).toBe(false)
|
|
})
|
|
|
|
it('returns false when all values are positive', () => {
|
|
const data = [{ value: 1 }, { value: 2 }, { value: 100 }]
|
|
expect(checkHasNonPositiveValues(data, 'value')).toBe(false)
|
|
})
|
|
|
|
it('returns true when a value is zero', () => {
|
|
const data = [{ value: 1 }, { value: 0 }, { value: 3 }]
|
|
expect(checkHasNonPositiveValues(data, 'value')).toBe(true)
|
|
})
|
|
|
|
it('returns true when a value is negative', () => {
|
|
const data = [{ value: 5 }, { value: -1 }, { value: 3 }]
|
|
expect(checkHasNonPositiveValues(data, 'value')).toBe(true)
|
|
})
|
|
|
|
it('returns true when all values are non-positive', () => {
|
|
const data = [{ value: -5 }, { value: 0 }, { value: -1 }]
|
|
expect(checkHasNonPositiveValues(data, 'value')).toBe(true)
|
|
})
|
|
|
|
it('checks only the specified key', () => {
|
|
const data = [
|
|
{ x: -1, y: 5 },
|
|
{ x: 2, y: 10 },
|
|
]
|
|
expect(checkHasNonPositiveValues(data, 'y')).toBe(false)
|
|
expect(checkHasNonPositiveValues(data, 'x')).toBe(true)
|
|
})
|
|
|
|
it('returns false when key is absent (undefined cast to NaN is not <= 0)', () => {
|
|
const data = [{ value: 1 }, { value: 2 }]
|
|
expect(checkHasNonPositiveValues(data, 'missing')).toBe(false)
|
|
})
|
|
})
|
|
|
|
describe('formatLogTick', () => {
|
|
it('formats values below 1,000 as plain locale strings', () => {
|
|
expect(formatLogTick(0)).toBe('0')
|
|
expect(formatLogTick(1)).toBe('1')
|
|
expect(formatLogTick(999)).toBe('999')
|
|
expect(formatLogTick(500)).toBe('500')
|
|
})
|
|
|
|
it('formats values >= 1,000 with a "k" suffix', () => {
|
|
expect(formatLogTick(1_000)).toBe('1k')
|
|
expect(formatLogTick(1_500)).toBe('1.5k')
|
|
expect(formatLogTick(10_000)).toBe('10k')
|
|
expect(formatLogTick(999_999)).toBe('1,000k')
|
|
})
|
|
|
|
it('formats values >= 1,000,000 with an "M" suffix', () => {
|
|
expect(formatLogTick(1_000_000)).toBe('1M')
|
|
expect(formatLogTick(1_500_000)).toBe('1.5M')
|
|
expect(formatLogTick(10_000_000)).toBe('10M')
|
|
expect(formatLogTick(1_234_567)).toBe('1.2M')
|
|
})
|
|
|
|
it('respects maximumFractionDigits of 1', () => {
|
|
// 1,050 → 1.05k, but max 1 decimal → "1.1k" (rounded)
|
|
expect(formatLogTick(1_050)).toBe('1.1k')
|
|
// 1,049 → 1.049k → "1k" (rounded down)
|
|
expect(formatLogTick(1_049)).toBe('1k')
|
|
})
|
|
})
|
|
|
|
describe('getCumulativeResults', () => {
|
|
it('returns empty array when results are empty', () => {
|
|
expect(getCumulativeResults({ rows: [] }, { yKey: 'y' })).toEqual([])
|
|
})
|
|
|
|
it('returns empty array when results are undefined', () => {
|
|
expect(getCumulativeResults(undefined as any, { yKey: 'y' })).toEqual([])
|
|
})
|
|
|
|
it('accumulates yKey values across rows', () => {
|
|
const results = {
|
|
rows: [
|
|
{ x: 'a', y: 10 },
|
|
{ x: 'b', y: 20 },
|
|
{ x: 'c', y: 5 },
|
|
],
|
|
}
|
|
const config = {
|
|
type: 'bar' as const,
|
|
xKey: 'x',
|
|
yKey: 'y',
|
|
cumulative: true,
|
|
showLabels: false,
|
|
showGrid: false,
|
|
}
|
|
const output = getCumulativeResults(results, config)
|
|
expect(output).toEqual([
|
|
{ x: 'a', y: 10 },
|
|
{ x: 'b', y: 30 },
|
|
{ x: 'c', y: 35 },
|
|
])
|
|
})
|
|
|
|
it('preserves other keys on each row', () => {
|
|
const results = {
|
|
rows: [
|
|
{ x: 'a', y: 1, label: 'foo' },
|
|
{ x: 'b', y: 2, label: 'bar' },
|
|
],
|
|
}
|
|
const config = {
|
|
type: 'bar' as const,
|
|
xKey: 'x',
|
|
yKey: 'y',
|
|
cumulative: true,
|
|
showLabels: false,
|
|
showGrid: false,
|
|
}
|
|
const output = getCumulativeResults(results, config)
|
|
expect(output[0]).toMatchObject({ x: 'a', y: 1, label: 'foo' })
|
|
expect(output[1]).toMatchObject({ x: 'b', y: 3, label: 'bar' })
|
|
})
|
|
|
|
it('handles a single row', () => {
|
|
const results = { rows: [{ x: 'a', y: 42 }] }
|
|
const config = {
|
|
type: 'bar' as const,
|
|
xKey: 'x',
|
|
yKey: 'y',
|
|
cumulative: true,
|
|
showLabels: false,
|
|
showGrid: false,
|
|
}
|
|
const output = getCumulativeResults(results, config)
|
|
expect(output).toEqual([{ x: 'a', y: 42 }])
|
|
})
|
|
|
|
// Postgres returns `bigint`, `numeric`, `money` and `count(*)` columns as
|
|
// strings, so the running total must sum numerically rather than concatenate.
|
|
it('sums numeric string yKey values instead of concatenating them', () => {
|
|
const results = {
|
|
rows: [
|
|
{ x: 'a', y: '10' },
|
|
{ x: 'b', y: '20' },
|
|
{ x: 'c', y: '30' },
|
|
],
|
|
}
|
|
const config = {
|
|
type: 'bar' as const,
|
|
xKey: 'x',
|
|
yKey: 'y',
|
|
cumulative: true,
|
|
showLabels: false,
|
|
showGrid: false,
|
|
}
|
|
const output = getCumulativeResults(results, config)
|
|
expect(output).toEqual([
|
|
{ x: 'a', y: 10 },
|
|
{ x: 'b', y: 30 },
|
|
{ x: 'c', y: 60 },
|
|
])
|
|
})
|
|
|
|
it('treats null/undefined and non-numeric yKey values as 0', () => {
|
|
const results = {
|
|
rows: [
|
|
{ x: 'a', y: null },
|
|
{ x: 'b', y: '5' },
|
|
{ x: 'c', y: undefined },
|
|
{ x: 'd', y: 'not-a-number' },
|
|
{ x: 'e', y: 4 },
|
|
],
|
|
}
|
|
const config = {
|
|
type: 'bar' as const,
|
|
xKey: 'x',
|
|
yKey: 'y',
|
|
cumulative: true,
|
|
showLabels: false,
|
|
showGrid: false,
|
|
}
|
|
const output = getCumulativeResults(results, config)
|
|
expect(output.map((row: { y: number }) => row.y)).toEqual([0, 5, 5, 5, 9])
|
|
})
|
|
})
|