Files
supabase/apps/studio/components/interfaces/Functions/EdgeFunctionOverview/EdgeFunctionOverview.utils.ts
Danny White 484726a45c fix(studio): warning colours broken in light mode after colour migration (#47660)
## What kind of change does this PR introduce?

UI bug fix

## What is the current behavior?

After the colour system migration (#47288), `--warning-default` was
removed in light mode in favour of the semantic `--warning` token.
Several studio call sites still referenced
`hsl(var(--warning-default))`, which resolves to an invalid colour in
light mode.

This caused warning segments in stacked bar charts (e.g. Realtime on
project overview v2) to render black instead of amber, with missing
tooltip swatches. The colour appeared to "fix itself" on hover because
the dimmed state used `--warning-500`, which is still defined.

## What is the new behaviour?

Studio consumers that referenced the removed token now point at tokens
that still resolve in light mode. Chart warnings use new app-level
`--chart-warning` / `--chart-warning-muted` variables (stepped scale,
theme-aware) rather than the removed `--warning-default`.

We only update **Studio app consumers** that were still calling the old
token:

- `LogsBarChart` → `--chart-warning` tokens
- `apps/studio/styles/globals.css` → defines those chart tokens + fixes
`--sidebar-primary-foreground`
- A handful of chart/tooltip call sites in Studio
(`EdgeFunctionOverview`, `UnifiedLogs`, etc.)
- Table editor dirty cell text → `--warning-600` (still on the stepped
scale)

## To test

Use a hosted project that already has warnings on project home (e.g.
Realtime with a non-zero warnings count). Switch Studio to **light
mode**.

1. Open **Project home** (`newHomepageUsageDeltas` flag enabled).
2. Find a service card with warnings in **Project usage**.
3. Confirm warning bar segments are amber/orange (not black), tooltip
swatches show amber, and hover does not flip them black.
4. Quick dark mode sanity check. Should look unchanged.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Style**
* Standardized warning-series and highlight colors across charts, logs,
countdown timers, and interface indicators using the shared theme tokens
(`--chart-warning` / `--chart-warning-muted`).
* Refreshed warning-related theme wiring for both light and dark modes,
including sidebar foreground color.
* **Bug Fixes**
* Updated “dirty” table cell text color to align with the revised
warning palette.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-07-07 14:34:29 +10:00

299 lines
8.9 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 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-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 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?: string
invocationChartData: InvocationChartDatum[]
windowStart: Date
windowEnd: Date
}): InvocationUpdateAnnotation | undefined => {
if (!updatedAt || 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')}`
}