Files
supabase/apps/studio/components/interfaces/Reports/ReportBlock/ChartBlock.tsx
T
Saxon FletcherandClaude Opus 4.8 fb02182e86 Color system (#47288)
## I have read the
[CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md)
file.

YES/NO

## What kind of change does this PR introduce?

Bug fix, feature, docs update, ...

## What is the current behavior?

Please link any relevant issues here.

## What is the new behavior?

Feel free to include screenshots if it includes visual changes.

## Additional context

Add any other context or screenshots.


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

* **New Features**
* Refreshed theming across the UI to use modern color expressions and
shared theme variables (including OKLCH-based gradients), improving
consistency for charts, code blocks, overlays, icons, and decorative
backgrounds.
* **Bug Fixes**
* Improved light/dark color and gradient consistency across axis/grid
styling, reference lines, buttons/badges, sidebar accents, loaders, and
other visual components.
* **Documentation**
* Updated styling/theming guidance to align with the revised semantic
token system and the updated theme variable usage patterns.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 15:00:43 +10:00

374 lines
13 KiB
TypeScript

import dayjs from 'dayjs'
import { Activity, BarChartIcon, Loader2 } from 'lucide-react'
import { useRouter } from 'next/router'
import { ReactNode, useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { Bar, BarChart, CartesianGrid, Line, LineChart, XAxis, YAxis } from 'recharts'
import { ChartContainer, ChartTooltip, WarningIcon } from 'ui'
import { METRIC_THRESHOLDS } from './ReportBlock.constants'
import { ReportBlockContainer } from './ReportBlockContainer'
import { ChartConfig } from '@/components/interfaces/SQLEditor/UtilityPanel/ChartConfig'
import { ButtonTooltip } from '@/components/ui/ButtonTooltip'
import { timestampFormatter } from '@/components/ui/Charts/Charts.utils'
import NoDataPlaceholder from '@/components/ui/Charts/NoDataPlaceholder'
import { PortalChartTooltip } from '@/components/ui/Charts/PortalChartTooltip'
import {
checkHasNonPositiveValues,
computeYAxisWidth,
formatLogTick,
formatYAxisTick,
} from '@/components/ui/QueryBlock/QueryBlock.utils'
import { AnalyticsInterval } from '@/data/analytics/constants'
import { mapResponseToAnalyticsData } from '@/data/analytics/infra-monitoring-queries'
import {
InfraMonitoringAttribute,
useInfraMonitoringAttributesQuery,
} from '@/data/analytics/infra-monitoring-query'
import {
ProjectDailyStatsAttribute,
useProjectDailyStatsQuery,
} from '@/data/analytics/project-daily-stats-query'
import { METRICS } from '@/lib/constants/metrics'
import { useFormatDateTime } from '@/lib/datetime'
import { useDatabaseSelectorStateSnapshot } from '@/state/database-selector'
import type { Dashboards } from '@/types'
interface ChartBlockProps {
label: string
attribute: string
provider: 'infra-monitoring' | 'daily-stats'
startDate: string
endDate: string
interval?: AnalyticsInterval
defaultChartStyle?: 'bar' | 'line'
defaultLogScale?: boolean
isLoading?: boolean
actions?: ReactNode
maxHeight?: number
onUpdateChartConfig?: ({
chart,
chartConfig,
}: {
chart?: Partial<Dashboards.Chart>
chartConfig?: Partial<ChartConfig>
}) => void
}
export const ChartBlock = ({
label,
attribute,
provider,
startDate,
endDate,
interval = '1d',
defaultChartStyle = 'bar',
defaultLogScale = false,
isLoading = false,
actions,
maxHeight,
onUpdateChartConfig,
}: ChartBlockProps) => {
const router = useRouter()
const { ref } = router.query
const state = useDatabaseSelectorStateSnapshot()
const chartRef = useRef<HTMLDivElement>(null)
const [chartStyle, setChartStyle] = useState<string>(defaultChartStyle)
const logScale = useMemo(() => defaultLogScale, [defaultLogScale])
const [latestValue, setLatestValue] = useState<string | undefined>()
const formatChartDate = useFormatDateTime()
const formatTooltipDate = useCallback(
(value: string | number, format: string) =>
/^\d{4}-\d{2}-\d{2}$/.test(String(value))
? timestampFormatter(String(value), format, true)
: formatChartDate(value, format),
[formatChartDate]
)
const databaseIdentifier = state.selectedDatabaseId
const {
data: dailyStatsData,
isFetching: isFetchingDailyStats,
isPending: isLoadingDailyStats,
} = useProjectDailyStatsQuery(
{
projectRef: ref as string,
attribute: attribute as ProjectDailyStatsAttribute,
startDate: dayjs(startDate).format('YYYY-MM-DD'),
endDate: dayjs(endDate).format('YYYY-MM-DD'),
},
{ enabled: provider === 'daily-stats' }
)
const {
data: infraMonitoringRawData,
isFetching: isFetchingInfraMonitoring,
isPending: isLoadingInfraMonitoring,
} = useInfraMonitoringAttributesQuery(
{
projectRef: ref as string,
attributes: [attribute as InfraMonitoringAttribute],
startDate,
endDate,
interval: interval as AnalyticsInterval,
databaseIdentifier,
},
{ enabled: provider === 'infra-monitoring' }
)
const infraMonitoringData = useMemo(() => {
if (!infraMonitoringRawData) return undefined
const mapped = mapResponseToAnalyticsData(infraMonitoringRawData, [
attribute as InfraMonitoringAttribute,
])
return mapped[attribute]
}, [infraMonitoringRawData, attribute])
const chartData =
provider === 'infra-monitoring'
? infraMonitoringData
: provider === 'daily-stats'
? dailyStatsData
: undefined
const isFetching =
provider === 'infra-monitoring'
? isFetchingInfraMonitoring
: provider === 'daily-stats'
? isFetchingDailyStats
: false
const loading =
isLoading ||
attribute.startsWith('new_snippet_') ||
(provider === 'infra-monitoring'
? isLoadingInfraMonitoring
: provider === 'daily-stats'
? isLoadingDailyStats
: isLoading)
const metric = METRICS.find((x) => x.key === attribute)
const metricLabel = metric?.label ?? attribute
const getCellColor = (attribute: string, value: number) => {
const threshold = METRIC_THRESHOLDS[attribute as keyof typeof METRIC_THRESHOLDS]
if (!threshold) return 'var(--chart-1)'
if (threshold.check === 'gt') {
return value >= threshold.danger
? 'var(--chart-destructive)'
: value >= threshold.warning
? 'var(--chart-warning)'
: 'var(--chart-1)'
} else {
return value <= threshold.danger
? 'var(--chart-destructive)'
: value <= threshold.warning
? 'var(--chart-warning)'
: 'var(--chart-1)'
}
}
const isPercentage = chartData?.format === '%'
const data = (chartData?.data ?? []).map((x: any) => {
const value = isPercentage ? x[attribute] : x[attribute]
const color = getCellColor(attribute, x[attribute])
return {
...x,
period_start: dayjs(x.period_start).utc().format('YYYY-MM-DD'),
[attribute]: value,
[metricLabel]: value,
fill: color,
stroke: color,
}
})
const hasNonPositiveValues = useMemo(() => {
if (!logScale || !data.length) return false
return checkHasNonPositiveValues(data, metricLabel)
}, [logScale, data, metricLabel])
const effectiveLogScale = logScale && !hasNonPositiveValues
const yAxisWidth = computeYAxisWidth(data, metricLabel, {
isLogScale: effectiveLogScale,
isPercentage,
})
const getInitialHighlightedValue = useCallback(() => {
if (!chartData?.data?.length) return undefined
const lastDataPoint = chartData.data[chartData.data.length - 1]
const value = lastDataPoint[attribute]
return isPercentage
? `${typeof value === 'number' ? value.toFixed(1) : value}%`
: typeof value === 'number'
? value.toLocaleString()
: value
}, [chartData?.data, chartData?.format, attribute])
useEffect(() => {
if (defaultChartStyle) setChartStyle(defaultChartStyle)
}, [defaultChartStyle])
useEffect(() => {
setLatestValue(getInitialHighlightedValue())
}, [chartData, getInitialHighlightedValue])
return (
<ReportBlockContainer
draggable
showDragHandle
loading={isFetching}
icon={metric?.category?.icon('text-foreground-muted')}
label={label}
actions={
<>
<ButtonTooltip
variant="text"
size="tiny"
disabled={loading}
className="w-7 h-7"
icon={chartStyle === 'bar' ? <Activity /> : <BarChartIcon />}
onClick={() => {
const style = chartStyle === 'bar' ? 'line' : 'bar'
if (onUpdateChartConfig) onUpdateChartConfig({ chart: { chart_type: style } })
setChartStyle(style)
}}
tooltip={{
content: {
side: 'bottom',
className: 'max-w-56 text-center',
text: `View as ${chartStyle === 'bar' ? 'line chart' : 'bar chart'}`,
},
}}
/>
<ButtonTooltip
variant={logScale ? 'default' : 'text'}
size="tiny"
disabled={loading}
className="h-7 px-1.5 font-mono text-[10px]"
icon={<span className="font-mono text-[10px] leading-none">Log</span>}
onClick={() => {
const next = !logScale
if (onUpdateChartConfig) onUpdateChartConfig({ chartConfig: { logScale: next } })
}}
tooltip={{
content: {
side: 'bottom',
className: 'max-w-56 text-center',
text: `Switch to ${logScale ? 'linear' : 'logarithmic'} scale`,
},
}}
/>
{actions}
</>
}
>
{loading ? (
<div className="flex grow w-full flex-col items-center justify-center gap-y-2 px-4">
<Loader2 size={18} className="animate-spin text-border-strong" />
<p className="text-xs text-foreground-lighter text-center">Loading data for {label}</p>
</div>
) : chartData === undefined ? (
<div className="flex grow w-full flex-col items-center justify-center gap-y-2 px-4">
<WarningIcon />
<p className="text-xs text-foreground-lighter text-center">
Unable to load data for {label}
</p>
</div>
) : data.length === 0 ? (
<div className="flex grow w-full flex-col items-center justify-center gap-y-2">
<NoDataPlaceholder
size="small"
className="border-0"
description="It may take up to 24 hours for data to refresh"
/>
</div>
) : (
<>
{latestValue && (
<div className="pt-2 px-3 w-full text-left leading-tight">
<span className="text-xs font-mono uppercase text-foreground-light">
Most recently
</span>
<p className="text-lg text">{latestValue}</p>
</div>
)}
{hasNonPositiveValues && (
<p className="px-3 pt-1 text-xs text-foreground-light">
Log scale is unavailable because the data contains zero or negative values.
</p>
)}
<ChartContainer
ref={chartRef}
className="w-full aspect-auto px-3 py-2"
style={{
height: maxHeight ? `${maxHeight}px` : undefined,
minHeight: maxHeight ? `${maxHeight}px` : undefined,
}}
>
{chartStyle === 'bar' ? (
<BarChart accessibilityLayer margin={{ left: 0, right: 0 }} data={data}>
<CartesianGrid vertical={false} />
<XAxis
dataKey="period_start"
tickLine={false}
axisLine={false}
tickMargin={8}
minTickGap={32}
/>
<YAxis
scale={effectiveLogScale ? 'log' : 'auto'}
domain={effectiveLogScale ? [1, 'auto'] : isPercentage ? [0, 100] : undefined}
allowDataOverflow={effectiveLogScale}
width={yAxisWidth}
tickFormatter={effectiveLogScale ? formatLogTick : formatYAxisTick}
/>
<ChartTooltip
content={
<PortalChartTooltip
chartRef={chartRef}
className="min-w-[200px]"
labelSuffix={isPercentage ? '%' : ''}
labelFormatter={(x) => formatTooltipDate(x as string | number, 'DD MMM YYYY')}
/>
}
/>
<Bar dataKey={metricLabel} radius={[2, 2, 1, 1]} />
</BarChart>
) : (
<LineChart accessibilityLayer margin={{ left: 0, right: 0 }} data={data}>
<CartesianGrid vertical={false} />
<XAxis
dataKey="period_start"
tickLine={false}
axisLine={false}
tickMargin={8}
minTickGap={32}
/>
<YAxis
scale={effectiveLogScale ? 'log' : 'auto'}
domain={effectiveLogScale ? [1, 'auto'] : isPercentage ? [0, 100] : undefined}
allowDataOverflow={effectiveLogScale}
width={yAxisWidth}
tickFormatter={effectiveLogScale ? formatLogTick : formatYAxisTick}
/>
<ChartTooltip
content={
<PortalChartTooltip
chartRef={chartRef}
labelSuffix={chartData?.format === '%' ? '%' : ''}
labelFormatter={(x) => formatTooltipDate(x as string | number, 'DD MMM YYYY')}
/>
}
/>
<Line dataKey={metricLabel} stroke="var(--chart-1)" radius={4} />
</LineChart>
)}
</ChartContainer>
</>
)}
</ReportBlockContainer>
)
}