mirror of
https://github.com/supabase/supabase.git
synced 2026-09-22 13:37:53 +08:00
7fce0a12d9
## I have read the [CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md) file. YES ## What kind of change does this PR introduce? This is a first draft at introducing semantic colours to our Observability charts. This moves away from just random colours being assigned to prop after prop. They're only scoped to the Database reports right now, but if it flows nice, we can open it up to the other reports too. This also aims to tone down some of the harsher colours in our charts, such as the orange which sometimes can look like a warning metric/prop. | Before | After | |--------|--------| | <img width="839" height="336" alt="Screenshot 2026-06-10 at 09 14 56" src="https://github.com/user-attachments/assets/222747c5-973b-4165-aa53-df7b93412ad3" /> | <img width="950" height="341" alt="Screenshot 2026-09-14 at 18 14 47" src="https://github.com/user-attachments/assets/836f3ddd-4a97-4064-b8cf-3a3b435417ac" /> | cc @supabase/design for additional thoughts. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added semantic chart color roles with light and dark theme variants for consistent visualizations. * Standardized colors and fills across database, networking, storage, and connection charts. * Maximum-value lines now use configured chart colors when available. * Added chart palette reference and stress-test examples. * Added stacked bar charts, customizable margins, and gradient-filled line charts. * Improved multi-series bar chart focus and date-range footer alignment. * **Documentation** * Documented the chart palette, theme variants, accessibility guidance, and usage recommendations. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Co-authored-by: Gildas Garcia <1122076+djhi@users.noreply.github.com>
321 lines
9.6 KiB
TypeScript
321 lines
9.6 KiB
TypeScript
import dayjs from 'dayjs'
|
|
import maxBy from 'lodash/maxBy'
|
|
import meanBy from 'lodash/meanBy'
|
|
import sumBy from 'lodash/sumBy'
|
|
import type { ChartConfig } from 'ui'
|
|
|
|
import { calculateBarClickTimeRange } from '@/components/interfaces/Settings/Logs/LogsBarChart.utils'
|
|
import type { ChartIntervals } from '@/types'
|
|
|
|
export type EdgeFunctionChartRawDatum = {
|
|
timestamp: string | number
|
|
success_count?: string | number
|
|
redirect_count?: string | number
|
|
client_err_count?: string | number
|
|
server_err_count?: string | number
|
|
avg_execution_time?: string | number
|
|
max_execution_time?: string | number
|
|
avg_cpu_time_used?: string | number
|
|
max_cpu_time_used?: string | number
|
|
avg_memory_used?: string | number
|
|
avg_heap_memory_used?: string | number
|
|
avg_external_memory_used?: string | number
|
|
}
|
|
|
|
export type EdgeFunctionChartDatum = {
|
|
timestamp: string
|
|
success_count: number
|
|
redirect_count: number
|
|
client_err_count: number
|
|
server_err_count: number
|
|
avg_execution_time: number
|
|
max_execution_time: number
|
|
avg_cpu_time_used: number
|
|
max_cpu_time_used: number
|
|
avg_memory_used: number
|
|
avg_heap_memory_used: number
|
|
avg_external_memory_used: number
|
|
}
|
|
|
|
export type InvocationChartDatum = {
|
|
timestamp: string
|
|
ok_count: number
|
|
warning_count: number
|
|
error_count: number
|
|
}
|
|
|
|
export type InvocationUpdateAnnotation = {
|
|
timestamp: string
|
|
position: number
|
|
updatedAt: Date
|
|
}
|
|
|
|
export const EDGE_FUNCTION_CHART_INTERVALS: ChartIntervals[] = [
|
|
{
|
|
key: '15min',
|
|
label: '15 min',
|
|
startValue: 15,
|
|
startUnit: 'minute',
|
|
format: 'MMM D, h:mm:ssa',
|
|
},
|
|
{
|
|
key: '1hr',
|
|
label: '1 hour',
|
|
startValue: 1,
|
|
startUnit: 'hour',
|
|
format: 'MMM D, h:mma',
|
|
},
|
|
{
|
|
key: '3hr',
|
|
label: '3 hours',
|
|
startValue: 3,
|
|
startUnit: 'hour',
|
|
format: 'MMM D, h:mma',
|
|
},
|
|
{
|
|
key: '1day',
|
|
label: '1 day',
|
|
startValue: 1,
|
|
startUnit: 'day',
|
|
format: 'MMM D, h:mma',
|
|
},
|
|
]
|
|
|
|
export const INVOCATION_CHART_CONFIG = {
|
|
ok_count: {
|
|
label: 'Ok',
|
|
color: 'hsl(var(--brand-default))',
|
|
},
|
|
warning_count: {
|
|
label: 'Warnings',
|
|
color: 'var(--chart-status-warning)',
|
|
},
|
|
error_count: {
|
|
label: 'Errors',
|
|
color: 'hsl(var(--destructive-default))',
|
|
},
|
|
} satisfies ChartConfig
|
|
|
|
export const CPU_TIME_CHART_CONFIG = {
|
|
max_cpu_time_used: {
|
|
label: 'Max CPU Time',
|
|
color: 'hsl(var(--brand-default))',
|
|
},
|
|
} satisfies ChartConfig
|
|
|
|
export const EXECUTION_TIME_CHART_CONFIG = {
|
|
avg_execution_time: {
|
|
label: 'Average Execution Time',
|
|
color: 'var(--foreground-default)',
|
|
},
|
|
max_execution_time: {
|
|
label: 'Max Execution Time',
|
|
color: 'hsl(var(--brand-default))',
|
|
},
|
|
} satisfies ChartConfig
|
|
|
|
export const MEMORY_CHART_CONFIG = {
|
|
avg_memory_used: {
|
|
label: 'Memory Usage',
|
|
color: 'hsl(var(--brand-default))',
|
|
},
|
|
} satisfies ChartConfig
|
|
|
|
const toManipulateUnit = (unit: ChartIntervals['startUnit']) => unit as dayjs.ManipulateType
|
|
const toNumber = (value: string | number | undefined) => Number(value ?? 0)
|
|
|
|
export const getBucketedTimeRange = (
|
|
interval: ChartIntervals,
|
|
now: Date = new Date()
|
|
): [Date, Date] => {
|
|
const currentTime = dayjs(now)
|
|
const unit = toManipulateUnit(interval.startUnit)
|
|
const start = currentTime.subtract(interval.startValue, unit).startOf(unit)
|
|
const end = currentTime.startOf(unit)
|
|
|
|
return [start.toDate(), end.toDate()]
|
|
}
|
|
|
|
export const getRollingTimeRange = (
|
|
interval: ChartIntervals,
|
|
now: Date = new Date()
|
|
): [Date, Date] => {
|
|
const currentTime = dayjs(now)
|
|
const start = currentTime.subtract(interval.startValue, toManipulateUnit(interval.startUnit))
|
|
|
|
return [start.toDate(), currentTime.toDate()]
|
|
}
|
|
|
|
export const getInvocationChartNavigationUrl = ({
|
|
projectRef,
|
|
functionSlug,
|
|
isUnifiedLogsEnabled,
|
|
rangeStart,
|
|
rangeEnd,
|
|
clickedTimestamp,
|
|
}: {
|
|
projectRef: string
|
|
functionSlug: string
|
|
isUnifiedLogsEnabled: boolean
|
|
rangeStart: string
|
|
rangeEnd: string
|
|
clickedTimestamp: string
|
|
}) => {
|
|
const { start, end } = calculateBarClickTimeRange(rangeStart, rangeEnd, clickedTimestamp)
|
|
const destination = isUnifiedLogsEnabled ? 'logs' : 'invocations'
|
|
|
|
return `/project/${projectRef}/functions/${functionSlug}/${destination}?its=${encodeURIComponent(start)}&ite=${encodeURIComponent(end)}`
|
|
}
|
|
|
|
export const formatChartTimestamp = (value: Date | string | number | undefined, format: string) => {
|
|
return dayjs(value === undefined ? '' : value).format(format)
|
|
}
|
|
|
|
export const getChartTimeRangeLabels = (
|
|
data: Array<{ timestamp: string }>,
|
|
format: string
|
|
): { start: string; end: string } | undefined => {
|
|
if (data.length === 0) return undefined
|
|
|
|
return {
|
|
start: formatChartTimestamp(data[0]?.timestamp, format),
|
|
end: formatChartTimestamp(data[data.length - 1]?.timestamp, format),
|
|
}
|
|
}
|
|
|
|
export const toEdgeFunctionChartData = (
|
|
rows: EdgeFunctionChartRawDatum[] = []
|
|
): EdgeFunctionChartDatum[] =>
|
|
rows.map((row) => ({
|
|
timestamp: String(row.timestamp ?? ''),
|
|
success_count: toNumber(row.success_count),
|
|
redirect_count: toNumber(row.redirect_count),
|
|
client_err_count: toNumber(row.client_err_count),
|
|
server_err_count: toNumber(row.server_err_count),
|
|
avg_execution_time: toNumber(row.avg_execution_time),
|
|
max_execution_time: toNumber(row.max_execution_time),
|
|
avg_cpu_time_used: toNumber(row.avg_cpu_time_used),
|
|
max_cpu_time_used: toNumber(row.max_cpu_time_used),
|
|
avg_memory_used: toNumber(row.avg_memory_used),
|
|
avg_heap_memory_used: toNumber(row.avg_heap_memory_used),
|
|
avg_external_memory_used: toNumber(row.avg_external_memory_used),
|
|
}))
|
|
|
|
export const getInvocationChartData = (data: EdgeFunctionChartDatum[]): InvocationChartDatum[] =>
|
|
data.map((datum) => ({
|
|
timestamp: datum.timestamp,
|
|
ok_count: datum.success_count,
|
|
warning_count: datum.redirect_count + datum.client_err_count,
|
|
error_count: datum.server_err_count,
|
|
}))
|
|
|
|
export const getInvocationTotals = (data: InvocationChartDatum[]) => {
|
|
const totalInvocationCount = sumBy(data, (datum) => {
|
|
return datum.ok_count + datum.warning_count + datum.error_count
|
|
})
|
|
|
|
return {
|
|
totalInvocationCount,
|
|
totalWarningCount: sumBy(data, 'warning_count'),
|
|
totalErrorCount: sumBy(data, 'error_count'),
|
|
}
|
|
}
|
|
|
|
export const getExecutionMetrics = (data: EdgeFunctionChartDatum[]) => ({
|
|
averageExecutionTime: meanBy(data, 'avg_execution_time') ?? 0,
|
|
maxExecutionTime: maxBy(data, 'max_execution_time')?.max_execution_time ?? 0,
|
|
})
|
|
|
|
export const getUsageMetrics = (data: EdgeFunctionChartDatum[]) => {
|
|
const totalHeapMemory = sumBy(data, 'avg_heap_memory_used')
|
|
const totalExternalMemory = sumBy(data, 'avg_external_memory_used')
|
|
|
|
return {
|
|
averageCpuTime: meanBy(data, 'avg_cpu_time_used') ?? 0,
|
|
maxCpuTime: maxBy(data, 'max_cpu_time_used')?.max_cpu_time_used ?? 0,
|
|
averageMemoryUsage: meanBy(data, 'avg_memory_used') ?? 0,
|
|
totalHeapMemory,
|
|
totalExternalMemory,
|
|
totalMemoryByType: totalHeapMemory + totalExternalMemory,
|
|
}
|
|
}
|
|
|
|
export const getInvocationUpdateAnnotation = ({
|
|
updatedAt,
|
|
invocationChartData,
|
|
windowStart,
|
|
windowEnd,
|
|
}: {
|
|
updatedAt?: number | string
|
|
invocationChartData: InvocationChartDatum[]
|
|
windowStart: Date
|
|
windowEnd: Date
|
|
}): InvocationUpdateAnnotation | undefined => {
|
|
if (updatedAt === undefined || invocationChartData.length === 0) return undefined
|
|
|
|
const updatedAtDate = new Date(updatedAt)
|
|
const updatedAtValue = updatedAtDate.valueOf()
|
|
|
|
if (Number.isNaN(updatedAtValue)) return undefined
|
|
if (updatedAtValue < windowStart.valueOf() || updatedAtValue > windowEnd.valueOf()) {
|
|
return undefined
|
|
}
|
|
|
|
const closestTimestamp = invocationChartData.reduce((closest, datum) => {
|
|
const datumDistance = Math.abs(new Date(datum.timestamp).valueOf() - updatedAtValue)
|
|
const closestDistance = Math.abs(new Date(closest.timestamp).valueOf() - updatedAtValue)
|
|
|
|
return datumDistance < closestDistance ? datum : closest
|
|
}).timestamp
|
|
|
|
const markerIndex = invocationChartData.findIndex((datum) => datum.timestamp === closestTimestamp)
|
|
if (markerIndex < 0) return undefined
|
|
|
|
return {
|
|
timestamp: closestTimestamp,
|
|
position: ((markerIndex + 0.5) / invocationChartData.length) * 100,
|
|
updatedAt: updatedAtDate,
|
|
}
|
|
}
|
|
|
|
export const getSegmentedButtonClassName = (index: number, total: number) => {
|
|
if (index === 0) return 'rounded-tr-none rounded-br-none'
|
|
if (index === total - 1) return 'rounded-tl-none rounded-bl-none'
|
|
return 'rounded-none'
|
|
}
|
|
|
|
export const getChartEmptyStateCopy = (
|
|
subject: string,
|
|
isError: boolean,
|
|
errorMessage?: string
|
|
) => ({
|
|
title: isError ? `Unable to load ${subject}` : 'No data to show',
|
|
description: isError ? errorMessage : undefined,
|
|
})
|
|
|
|
export const formatMetric = (value?: number, unit?: string) => {
|
|
if (value === undefined || Number.isNaN(value)) return unit ? `0${unit}` : '0'
|
|
|
|
const formatted = unit === 'MB' ? value.toFixed(1) : Math.round(value).toLocaleString('en-US')
|
|
return unit ? `${formatted}${unit}` : formatted
|
|
}
|
|
|
|
export const formatRate = (count: number, total: number) =>
|
|
new Intl.NumberFormat('en-US', {
|
|
style: 'percent',
|
|
maximumFractionDigits: 1,
|
|
}).format(total === 0 ? 0 : count / total)
|
|
|
|
export const formatReferenceDelta = (value: number, reference: number, label = 'average') => {
|
|
const difference = value - reference
|
|
if (Math.abs(difference) < Number.EPSILON) return `At ${label}`
|
|
if (reference === 0) return `${difference > 0 ? 'Above' : 'Below'} ${label}`
|
|
|
|
const percentDifference = Math.round(Math.abs((difference / reference) * 100))
|
|
return `${percentDifference}% ${difference > 0 ? 'above' : 'below'} ${label}`
|
|
}
|
|
|
|
export const getMemoryTooltipDetail = (heapMemory: number, externalMemory: number) => {
|
|
return `Heap ${formatMetric(heapMemory, 'MB')} • External ${formatMetric(externalMemory, 'MB')}`
|
|
}
|