feat(design-system): first pass at db report chart colours (#46787)

## 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>
This commit is contained in:
authored and GitHub committed 2026-09-16 09:18:40 +01:00
1 parent 92fdb1d3c5
commit 7fce0a12d9
26 files changed
+540 -219

No files matched your search

@@ -49,27 +49,7 @@ We do not wrap Recharts. This means you're not locked into an abstraction. When
</Callout>
Add the following colors to your CSS file in your app.
```css
@layer base {
:root {
--chart-1: 12 76% 61%;
--chart-2: 173 58% 39%;
--chart-3: 197 37% 24%;
--chart-4: 43 74% 66%;
--chart-5: 27 87% 67%;
}
.dark {
--chart-1: 220 70% 50%;
--chart-2: 160 60% 45%;
--chart-3: 30 80% 55%;
--chart-4: 280 65% 60%;
--chart-5: 340 75% 55%;
}
}
```
Chart colors are already defined for every app in `packages/config/css/charts.css`, which ships through the shared Tailwind config. It provides eight categorical slots, `--chart-1` through `--chart-8`, each with a matching `-fill` token, resolved per theme. See the [Charts](/docs/ui-patterns/charts) pattern page for the palette and the rules for assigning slots.
## Your First Chart
@@ -327,25 +307,19 @@ Charts has built-in support for theming. You can use css variables (recommended)
<Steps>
<Step>Define your colors in your css file</Step>
<Step>Pick a slot from the shared palette</Step>
```css {6-7,14-15} title="globals.css"
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 240 10% 3.9%;
// ...
--chart-1: 12 76% 61%;
--chart-2: 173 58% 39%;
}
```css title="packages/config/css/charts.css"
:root {
--chart-1: var(--color-brand-800);
--chart-2: var(--color-blue-900);
/* ... */
}
.dark: {
--background: 240 10% 3.9%;
--foreground: 0 0% 100%;
// ...
--chart-1: 220 70% 50%;
--chart-2: 160 60% 45%;
}
[data-theme*='dark'] {
--chart-1: var(--color-brand-900);
--chart-2: var(--color-blue-1100);
/* ... */
}
```
@@ -355,28 +329,18 @@ Charts has built-in support for theming. You can use css variables (recommended)
const chartConfig = {
desktop: {
label: 'Desktop',
color: 'hsl(var(--chart-1))',
color: 'var(--chart-1)',
},
mobile: {
label: 'Mobile',
color: 'hsl(var(--chart-2))',
color: 'var(--chart-2)',
},
} satisfies ChartConfig
```
<Callout className="mt-4">
We're wrapping the value in `hsl()` here because we define the colors without color space function.
This is not required. You can use full color values, such as hex, hsl or oklch.
```css
--chart-1: oklch(70% 0.227 154.59);
```
```tsx
color: "var(--chart-1)",
```
The slots are full color values, so pass them as `var(--chart-1)`. Do not wrap them in `hsl()`; that form is for bare HSL triplets and produces an invalid color here.
</Callout>
@@ -472,11 +436,11 @@ const chartConfig = {
},
chrome: {
label: 'Chrome',
color: 'hsl(var(--chart-1))',
color: 'var(--chart-1)',
},
safari: {
label: 'Safari',
color: 'hsl(var(--chart-2))',
color: 'var(--chart-2)',
},
} satisfies ChartConfig
```
@@ -516,11 +480,11 @@ const chartData = [
const chartConfig = {
chrome: {
label: 'Chrome',
color: 'hsl(var(--chart-1))',
color: 'var(--chart-1)',
},
safari: {
label: 'Safari',
color: 'hsl(var(--chart-2))',
color: 'var(--chart-2)',
},
} satisfies ChartConfig
```
@@ -23,6 +23,29 @@ Our charts use a combination of our own presentational components and [Recharts]
3. **Keep it simple**: Try to avoid abstracting the chart content too much. These components should cover most of your presentational needs.
## Color
Series colors come from eight categorical slots, `--chart-1` through `--chart-8`, defined in
`packages/config/css/charts.css`. Assign them in order and never cycle: a ninth series folds
into "Other" or becomes small multiples. Each slot has a matching `-fill` token. Slots resolve
per theme, so pass `var(--chart-n)` and never branch on light/dark in code. Adjacent slots
alternate hue families and clear colorblind separation in both themes.
Reference lines use `--chart-reference`. Headroom, idle and unused capacity use `--chart-muted`.
Directional pairs use `--chart-in` / `--chart-out` so read and write keep the same hue across
charts.
Status colors (`--chart-status-success`, `-warning`, `-destructive`, each with a `-muted` tier)
are reserved for state and always ship with an icon or label. Never use one as a series color:
amber on a neutral metric reads as a problem. Warm hues are otherwise limited to tomato, slot 5,
because no amber or yellow step is legible on the dark surface.
<ComponentPreview name="chart-palette" wide />
Every slot stacked together, to check adjacent segments stay separable in both themes.
<ComponentPreview name="chart-palette-stress" wide />
## Examples
### Basic Chart Types
+16
View File
@@ -57,4 +57,20 @@ export const charts: Registry = [
category: 'Charts',
subcategory: 'Composed',
},
{
name: 'chart-palette',
type: 'components:block',
registryDependencies: ['chart'],
files: ['block/chart-palette.tsx'],
category: 'Charts',
subcategory: 'Palette',
},
{
name: 'chart-palette-stress',
type: 'components:block',
registryDependencies: ['chart'],
files: ['block/chart-palette-stress.tsx'],
category: 'Charts',
subcategory: 'Palette',
},
]
@@ -116,11 +116,11 @@ const chartConfig = {
},
desktop: {
label: 'Desktop',
color: 'hsl(var(--chart-1))',
color: 'var(--chart-1)',
},
mobile: {
label: 'Mobile',
color: 'hsl(var(--chart-2))',
color: 'var(--chart-2)',
},
} satisfies ChartConfig
@@ -52,11 +52,11 @@ export default function ComposedChartBasic() {
},
performance: {
label: 'Performance',
color: 'hsl(var(--chart-2))',
color: 'var(--chart-2)',
},
efficiency: {
label: 'Efficiency',
color: 'hsl(var(--chart-5))',
color: 'var(--chart-5)',
},
}
@@ -0,0 +1,72 @@
'use client'
import {
Chart,
ChartBar,
ChartCard,
ChartContent,
ChartHeader,
ChartTitle,
type ChartBarTick,
type ChartConfig,
} from 'ui-patterns/Chart'
const SERIES = [
{ key: 'postgres', label: 'Postgres' },
{ key: 'postgrest', label: 'PostgREST' },
{ key: 'reserved', label: 'Reserved' },
{ key: 'auth', label: 'Auth' },
{ key: 'storage', label: 'Storage' },
{ key: 'realtime', label: 'Realtime' },
{ key: 'cron', label: 'Cron' },
{ key: 'other', label: 'Other roles' },
]
const config: ChartConfig = Object.fromEntries(
SERIES.map((s, i) => [s.key, { label: s.label, color: `var(--chart-${i + 1})` }])
)
export default function ChartPaletteStress() {
const data: ChartBarTick[] = Array.from({ length: 40 }, (_, i) => {
const date = new Date()
date.setMinutes(date.getMinutes() - (40 - i) * 3)
const row: ChartBarTick = { timestamp: date.toISOString() }
const trend = Math.sin((i / 40) * Math.PI * 2)
SERIES.forEach((s, idx) => {
const phase = Math.sin(i / 3.5 + idx * 1.7)
const jitter = Math.sin(i * 2.3 + idx * 0.9) * 1.5
row[s.key] = Math.max(1, Math.round(5 + idx * 1.8 + phase * 3 + trend * 2 + jitter))
})
return row
})
return (
<div className="flex flex-col gap-6 w-8/12">
<Chart>
<ChartCard>
<ChartHeader>
<ChartTitle tooltip="Every categorical slot on screen at once">
Client connections by role
</ChartTitle>
</ChartHeader>
<ChartContent>
<div className="h-40">
<ChartBar
data={data}
dataKey={SERIES[0].key}
dataKeys={SERIES.map((s) => s.key)}
config={config}
isStacked
isFullHeight
showGrid
showYAxis
YAxisProps={{ width: 36 }}
/>
</div>
</ChartContent>
</ChartCard>
</Chart>
</div>
)
}
@@ -0,0 +1,135 @@
import { ReactNode } from 'react'
const SLOTS = [1, 2, 3, 4, 5, 6, 7, 8]
const STATUS = [
{ name: '--chart-status-success', muted: '--chart-status-success-muted', note: 'Healthy, ok' },
{
name: '--chart-status-warning',
muted: '--chart-status-warning-muted',
note: 'Threshold breach',
},
{
name: '--chart-status-destructive',
muted: '--chart-status-destructive-muted',
note: 'Error, failure',
},
]
const DEFAULTS = [
{ name: '--chart-in', note: 'Pinned: network in, disk read' },
{ name: '--chart-out', note: 'Pinned: network out, disk write' },
{ name: '--chart-reference', note: 'Reference lines, max values' },
{ name: '--chart-muted', note: 'Headroom, idle, unused capacity' },
]
function Swatch({ token, label }: { token: string; label: string }) {
return (
<div className="flex min-w-0 flex-1 flex-col gap-1.5">
<div
className="border-default h-12 w-full rounded-md border"
style={{ background: `var(${token})` }}
/>
<span className="text-foreground-lighter text-xs">{label}</span>
</div>
)
}
function TokenCard({
title,
token,
note,
children,
}: {
title: ReactNode
token: string
note?: string
children: ReactNode
}) {
return (
<div className="border-default bg-surface-100 flex flex-col gap-4 rounded-lg border p-4">
<div className="flex flex-col gap-1">
<div className="text-foreground text-sm">{title}</div>
<code className="text-foreground-lighter break-all font-mono text-xs">{token}</code>
</div>
<div className="flex gap-3">{children}</div>
{note && <p className="text-foreground-lighter text-xs">{note}</p>}
</div>
)
}
function Section({
title,
description,
children,
className,
}: {
title: string
description: string
children: ReactNode
className: string
}) {
return (
<section className="flex flex-col gap-4">
<div className="flex flex-col gap-1">
<h3 className="text-foreground text-sm">{title}</h3>
<p className="text-foreground-lighter max-w-prose text-xs">{description}</p>
</div>
<div className={className}>{children}</div>
</section>
)
}
export default function ChartPalette() {
return (
<div className="flex w-full flex-col gap-10 p-6">
<Section
title="Categorical slots"
description="Assigned in fixed order, never cycled. A ninth series folds into “Other”."
className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4"
>
{SLOTS.map((n) => (
<TokenCard key={n} title={`Slot ${n}`} token={`--chart-${n}`}>
<Swatch token={`--chart-${n}`} label="Stroke" />
<Swatch token={`--chart-${n}-fill`} label="Fill" />
</TokenCard>
))}
</Section>
<Section
title="Status"
description="Reserved meaning. These point at the same tokens the rest of the UI uses and always ship with an icon or label, so state is never carried by color alone. Never assign one to a series."
className="grid grid-cols-1 gap-4 sm:grid-cols-3"
>
{STATUS.map((d) => (
<TokenCard
key={d.name}
title={d.name.replace('--chart-status-', '')}
token={d.name}
note={d.note}
>
<Swatch token={d.name} label="Base" />
<Swatch token={d.muted} label="Muted" />
</TokenCard>
))}
</Section>
<Section
title="Pinned pairs and rendering defaults"
description="Chart authors do not pick these. Reference lines and headroom are applied by the chart."
className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4"
>
{DEFAULTS.map((d) => (
<TokenCard
key={d.name}
title={d.name.replace('--chart-', '')}
token={d.name}
note={d.note}
>
<Swatch token={d.name} label="Color" />
</TokenCard>
))}
</Section>
</div>
)
}
@@ -32,8 +32,8 @@ export default function Component() {
<TooltipDemo
label="Page Views"
payload={[
{ name: 'Desktop', value: 186, fill: 'hsl(var(--chart-1))' },
{ name: 'Mobile', value: 80, fill: 'hsl(var(--chart-2))' },
{ name: 'Desktop', value: 186, fill: 'var(--chart-1)' },
{ name: 'Mobile', value: 80, fill: 'var(--chart-2)' },
]}
className="w-32"
/>
@@ -64,8 +64,8 @@ export default function Component() {
label="Browser"
hideLabel
payload={[
{ name: 'Chrome', value: 1286, fill: 'hsl(var(--chart-3))' },
{ name: 'Firefox', value: 1000, fill: 'hsl(var(--chart-4))' },
{ name: 'Chrome', value: 1286, fill: 'var(--chart-3)' },
{ name: 'Firefox', value: 1000, fill: 'var(--chart-4)' },
]}
indicator="dashed"
className="w-32"
@@ -74,7 +74,7 @@ export default function Component() {
<div className="hidden! md:flex!">
<TooltipDemo
label="Page Views"
payload={[{ name: 'Desktop', value: 12486, fill: 'hsl(var(--chart-3))' }]}
payload={[{ name: 'Desktop', value: 12486, fill: 'var(--chart-3)' }]}
className="w-36"
indicator="line"
/>
@@ -84,7 +84,7 @@ export default function Component() {
<TooltipDemo
label="Browser"
hideLabel
payload={[{ name: 'Chrome', value: 1286, fill: 'hsl(var(--chart-1))' }]}
payload={[{ name: 'Chrome', value: 1286, fill: 'var(--chart-1)' }]}
indicator="dot"
className="w-32"
/>
-16
View File
@@ -36,22 +36,6 @@
}
@layer base {
:root {
--chart-1: 12 76% 61%;
--chart-2: 173 58% 39%;
--chart-3: 197 37% 24%;
--chart-4: 43 74% 66%;
--chart-5: 27 87% 67%;
}
.dark {
--chart-1: 220 70% 50%;
--chart-2: 160 60% 45%;
--chart-3: 30 80% 55%;
--chart-4: 280 65% 60%;
--chart-5: 340 75% 55%;
}
* {
@apply border-border;
}
-15
View File
@@ -30,11 +30,6 @@
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
--color-chart-1: hsl(var(--chart-1));
--color-chart-2: hsl(var(--chart-2));
--color-chart-3: hsl(var(--chart-3));
--color-chart-4: hsl(var(--chart-4));
--color-chart-5: hsl(var(--chart-5));
}
@layer base {
@@ -58,11 +53,6 @@
--border: hsl(220 13% 91%);
--input: hsl(220 13% 91%);
--ring: hsl(220 13% 70.8%);
--chart-1: 12 76% 61%;
--chart-2: 173 58% 39%;
--chart-3: 197 37% 24%;
--chart-4: 43 74% 66%;
--chart-5: 27 87% 67%;
--radius: 0.625rem;
--sidebar: hsl(210 20% 98.5%);
--sidebar-foreground: hsl(224 71.4% 4.1%);
@@ -94,11 +84,6 @@
--border: hsl(215 27.9% 16.9%);
--input: hsl(215 27.9% 16.9%);
--ring: hsl(217.9 10.6% 55.6%);
--chart-1: 220 70% 50%;
--chart-2: 160 60% 45%;
--chart-3: 30 80% 55%;
--chart-4: 280 65% 60%;
--chart-5: 340 75% 55%;
--sidebar: hsl(220.9 39.3% 11%);
--sidebar-foreground: hsl(210 20% 98%);
--sidebar-primary: hsl(220 70% 50%);
@@ -17,11 +17,7 @@ interface QueryResultChartProps {
result?: QueryResult
}
const Y_SERIES_COLORS = [
'hsl(var(--brand-default))',
'hsl(var(--chart-blue))',
'hsl(var(--chart-3))',
]
const Y_SERIES_COLORS = ['hsl(var(--brand-default))', 'var(--chart-2)', 'var(--chart-3)']
const toChartValue = (value: unknown): string | number => {
if (typeof value === 'number' || typeof value === 'string') return value
@@ -88,7 +88,7 @@ export const INVOCATION_CHART_CONFIG = {
},
warning_count: {
label: 'Warnings',
color: 'var(--chart-warning)',
color: 'var(--chart-status-warning)',
},
error_count: {
label: 'Errors',
@@ -9,7 +9,7 @@ export const CHART_TABS = [
export const LEGEND_ITEMS: Record<string, { label: string; color: string; dataKey: string }[]> = {
query_latency: [
{ label: 'P50', color: 'hsl(var(--chart-4))', dataKey: 'p50' },
{ label: 'P50', color: 'var(--chart-4)', dataKey: 'p50' },
{ label: 'P95', color: CHART_COLORS.GREEN_1, dataKey: 'p95' },
],
rows_read: [{ label: 'Rows Read', color: CHART_COLORS.GREEN_1, dataKey: 'rows_read' }],
@@ -19,4 +19,4 @@ export const LEGEND_ITEMS: Record<string, { label: string; color: string; dataKe
export const CHART_TYPE = 'linear'
export const SEL_COLOR = 'hsl(var(--chart-blue))'
export const SEL_COLOR = 'var(--chart-2)'
@@ -156,15 +156,15 @@ export const ChartBlock = ({
if (!threshold) return 'var(--chart-1)'
if (threshold.check === 'gt') {
return value >= threshold.danger
? 'var(--chart-destructive)'
? 'var(--chart-status-destructive)'
: value >= threshold.warning
? 'var(--chart-warning)'
? 'var(--chart-status-warning)'
: 'var(--chart-1)'
} else {
return value <= threshold.danger
? 'var(--chart-destructive)'
? 'var(--chart-status-destructive)'
: value <= threshold.warning
? 'var(--chart-warning)'
? 'var(--chart-status-warning)'
: 'var(--chart-1)'
}
}
@@ -77,11 +77,11 @@ import { useShortcut } from '@/state/shortcuts/useShortcut'
export const CHART_CONFIG = {
success: {
label: <TooltipLabel level="success" />,
color: 'var(--chart-success)',
color: 'var(--chart-muted)',
},
warning: {
label: <TooltipLabel level="warning" />,
color: 'var(--chart-warning)',
color: 'var(--chart-status-warning)',
},
error: {
label: <TooltipLabel level="error" />,
@@ -40,6 +40,7 @@ import {
CustomTooltip,
getStackId,
MultiAttribute,
resolveChartColor,
} from './ComposedChart.utils'
import NoDataPlaceholder from './NoDataPlaceholder'
import { ChartHighlight } from './useChartHighlight'
@@ -307,7 +308,7 @@ export function ComposedChart({
const maxAttribute = attributes.find((a) => a.isMaxValue)
const maxAttributeData = {
name: maxAttribute?.attribute,
color: CHART_COLORS.REFERENCE_LINE,
color: resolveChartColor(maxAttribute?.color, isDarkMode) ?? CHART_COLORS.REFERENCE_LINE,
}
const referenceLines = attributes.filter((attribute) => {
@@ -342,16 +343,12 @@ export function ComposedChart({
const attribute = attributes.find((attr) => attr.attribute === att.name)
return {
...att,
color: attribute?.color
? isDarkMode
? attribute.color.dark
: attribute.color.light
: STACKED_CHART_COLORS[index % STACKED_CHART_COLORS.length],
fill: attribute?.fill
? isDarkMode
? attribute.fill.dark
: attribute.fill.light
: STACKED_CHART_FILLS[index % STACKED_CHART_FILLS.length],
color:
resolveChartColor(attribute?.color, isDarkMode) ??
STACKED_CHART_COLORS[index % STACKED_CHART_COLORS.length],
fill:
resolveChartColor(attribute?.fill, isDarkMode) ??
STACKED_CHART_FILLS[index % STACKED_CHART_FILLS.length],
}
})
: []
@@ -600,7 +597,9 @@ export function ComposedChart({
key={maxAttribute.attribute}
type="linear"
dataKey={maxAttribute.attribute}
stroke={CHART_COLORS.REFERENCE_LINE}
stroke={
resolveChartColor(maxAttribute.color, isDarkMode) ?? CHART_COLORS.REFERENCE_LINE
}
strokeWidth={2}
strokeDasharray={maxAttribute.strokeDasharray ?? '3 3'}
dot={false}
@@ -616,7 +615,7 @@ export function ComposedChart({
key={line.attribute}
y={line.value}
strokeWidth={1}
stroke={isDarkMode ? line.color?.dark : line.color?.light}
stroke={resolveChartColor(line.color, isDarkMode)}
strokeDasharray={line.strokeDasharray ?? '3 3'}
label={undefined}
>
@@ -40,18 +40,25 @@ export interface ReportAttributes {
export type Provider = 'infra-monitoring' | 'daily-stats' | 'mock' | 'reference-line' | 'logs'
export type ThemedColor = {
light?: string
dark?: string
}
export const resolveChartColor = (
color: string | ThemedColor | undefined,
isDarkMode: boolean | undefined
) => {
if (typeof color === 'string') return color
return isDarkMode ? color?.dark : color?.light
}
export type MultiAttribute = {
attribute: string
provider?: Provider
label?: string
color?: {
light: string
dark: string
}
fill?: {
light?: string
dark?: string
}
color?: string | { light: string; dark: string }
fill?: string | ThemedColor
statusCode?: string
grantType?: string
providerType?: string
@@ -63,9 +63,9 @@ export function getLevelColor(
case 'success':
return {
text: 'text-muted',
bg: 'bg-[var(--chart-success)] group-data-[state=selected]/row:bg-foreground-lighter',
bg: 'bg-[var(--chart-muted)] group-data-[state=selected]/row:bg-foreground-lighter',
border:
'border-[var(--chart-success)] group-data-[state=selected]/row:border-foreground-lighter',
'border-[var(--chart-muted)] group-data-[state=selected]/row:border-foreground-lighter',
}
case 'warning':
return {
+81 -10
View File
@@ -75,6 +75,8 @@ export const getReportAttributesV2: (
attribute: 'ram_usage_used',
provider: 'infra-monitoring',
label: 'Used',
color: 'var(--chart-1)',
fill: 'var(--chart-1-fill)',
tooltip:
'RAM in use by Postgres and the operating system. Sustained high usage may indicate memory pressure',
},
@@ -82,6 +84,8 @@ export const getReportAttributesV2: (
attribute: 'ram_usage_cache_and_buffers',
provider: 'infra-monitoring',
label: 'Cache + Buffers',
color: 'var(--chart-2)',
fill: 'var(--chart-2-fill)',
tooltip:
'RAM used by the operating system page cache and PostgreSQL buffers to accelerate disk reads/writes',
},
@@ -89,6 +93,8 @@ export const getReportAttributesV2: (
attribute: 'ram_usage_free',
provider: 'infra-monitoring',
label: 'Free',
color: 'var(--chart-muted)',
fill: 'var(--chart-muted-fill)',
tooltip:
'Unallocated memory available for use. A small portion is always reserved by the operating system',
},
@@ -105,6 +111,7 @@ export const getReportAttributesV2: (
provider: 'infra-monitoring',
label: 'Swap',
omitFromTotal: true,
color: 'var(--chart-3)',
tooltip:
'Swap space in use by the operating system. Sustained swap usage indicates memory pressure and may degrade database performance',
},
@@ -132,6 +139,8 @@ export const getReportAttributesV2: (
attribute: 'ram_commit_used',
provider: 'infra-monitoring',
label: 'Committed',
color: 'var(--chart-1)',
fill: 'var(--chart-1-fill)',
tooltip:
'Total memory the kernel has promised to processes (RAM plus swap). Sustained values near or above the commit limit indicate overcommitment and a high risk of out-of-memory failures',
},
@@ -139,6 +148,7 @@ export const getReportAttributesV2: (
attribute: 'ram_commit_limit',
provider: 'infra-monitoring',
label: 'Commit limit',
color: 'var(--chart-reference)',
isMaxValue: true,
omitFromTotal: true,
tooltip:
@@ -201,8 +211,8 @@ export const getReportAttributesV2: (
provider: 'infra-monitoring',
label: 'System',
format: '%',
color: { light: '#EDC35E', dark: '#EDD35E' },
fill: { light: '#F6D99F', dark: '#5C5230' },
color: 'var(--chart-2)',
fill: 'var(--chart-2-fill)',
tooltip:
'CPU time spent on kernel operations (e.g., process scheduling, memory management). High values may indicate system overhead',
},
@@ -211,8 +221,8 @@ export const getReportAttributesV2: (
provider: 'infra-monitoring',
label: 'User',
format: '%',
color: { light: '#0063E8', dark: '#65BCD9' },
fill: { light: '#80B1F4', dark: '#2A3D45' },
color: 'var(--chart-1)',
fill: 'var(--chart-1-fill)',
tooltip:
'CPU time used by database queries and user-space processes. High values may suggest CPU-intensive queries',
},
@@ -221,8 +231,8 @@ export const getReportAttributesV2: (
provider: 'infra-monitoring',
label: 'IOwait',
format: '%',
color: { light: '#DB3A34', dark: '#FF6B6B' },
fill: { light: '#F2A7A3', dark: '#5C2A2A' },
color: 'var(--chart-3)',
fill: 'var(--chart-3-fill)',
tooltip:
'CPU time waiting for disk or network I/O. High values may indicate disk bottlenecks',
},
@@ -231,8 +241,8 @@ export const getReportAttributesV2: (
provider: 'infra-monitoring',
label: 'IRQs',
format: '%',
color: { light: '#DA760B', dark: '#DA760B' },
fill: { light: '#FFB885', dark: '#5C3D0A' },
color: 'var(--chart-4)',
fill: 'var(--chart-4-fill)',
tooltip: 'CPU time handling hardware interrupt requests (IRQ)',
},
{
@@ -240,11 +250,30 @@ export const getReportAttributesV2: (
provider: 'infra-monitoring',
label: 'Other',
format: '%',
color: { light: '#B616A6', dark: '#DB8DF9' },
fill: { light: '#DB8BD3', dark: '#4A3D5C' },
color: 'var(--chart-5)',
fill: 'var(--chart-5-fill)',
tooltip:
'CPU time spent on other tasks (e.g., background processes, software interrupts)',
},
{
attribute: 'cpu_usage_busy_idle',
provider: 'infra-monitoring',
label: 'Idle',
format: '%',
omitFromTotal: true,
color: 'var(--chart-muted)',
fill: 'var(--chart-muted-fill)',
tooltip: 'CPU time spent idle and available for new work',
},
{
attribute: 'cpu_usage_max',
provider: 'reference-line',
label: 'Max',
value: 100,
color: 'var(--chart-reference)',
tooltip: 'Max CPU usage',
isMaxValue: true,
},
],
},
{
@@ -269,12 +298,16 @@ export const getReportAttributesV2: (
attribute: 'network_receive_bytes',
provider: 'infra-monitoring',
label: 'Network in',
color: 'var(--chart-in)',
fill: 'var(--chart-in-fill)',
tooltip: 'Inbound network throughput (bytes per second)',
},
{
attribute: 'network_transmit_bytes',
provider: 'infra-monitoring',
label: 'Network out',
color: 'var(--chart-out)',
fill: 'var(--chart-out-fill)',
tooltip: 'Outbound network throughput (bytes per second)',
},
],
@@ -301,6 +334,8 @@ export const getReportAttributesV2: (
attribute: 'disk_iops_write',
provider: 'infra-monitoring',
label: 'Write IOPS',
color: 'var(--chart-out)',
fill: 'var(--chart-out-fill)',
tooltip:
'Number of write operations per second. High values indicate frequent data writes, logging, or transaction activity',
},
@@ -308,6 +343,8 @@ export const getReportAttributesV2: (
attribute: 'disk_iops_read',
provider: 'infra-monitoring',
label: 'Read IOPS',
color: 'var(--chart-in)',
fill: 'var(--chart-in-fill)',
tooltip:
'Number of read operations per second. High values suggest frequent disk reads due to queries or poor caching',
},
@@ -315,6 +352,7 @@ export const getReportAttributesV2: (
attribute: 'disk_iops_max',
provider: 'reference-line',
label: 'Max IOPS',
color: 'var(--chart-reference)',
value: effectiveMaxIops,
tooltip:
'Effective maximum IOPS for your current compute and disk configuration. Equal to the lower of the compute IOPS limit and the provisioned disk IOPS',
@@ -345,18 +383,23 @@ export const getReportAttributesV2: (
attribute: 'disk_bytes_read',
provider: 'infra-monitoring',
label: 'Read throughput',
color: 'var(--chart-in)',
fill: 'var(--chart-in-fill)',
tooltip: 'Disk read throughput (bytes per second)',
},
{
attribute: 'disk_bytes_written',
provider: 'infra-monitoring',
label: 'Write throughput',
color: 'var(--chart-out)',
fill: 'var(--chart-out-fill)',
tooltip: 'Disk write throughput (bytes per second)',
},
{
attribute: 'disk_throughput_max',
provider: 'reference-line',
label: 'Max throughput',
color: 'var(--chart-reference)',
value:
diskConfig?.attributes?.type === 'gp3' &&
typeof diskConfig.attributes.throughput_mbps === 'number'
@@ -418,12 +461,15 @@ export const getReportAttributesV2: (
attribute: 'pg_stat_database_num_backends',
provider: 'infra-monitoring',
label: 'Total connections',
color: 'var(--chart-1)',
fill: 'var(--chart-1-fill)',
tooltip: 'Total number of active database connections',
},
{
attribute: 'max_db_connections',
provider: 'reference-line',
label: 'Max connections',
color: 'var(--chart-reference)',
value: maxConnections?.maxConnections,
tooltip: 'Max available connections for your current compute size',
isMaxValue: true,
@@ -452,6 +498,8 @@ export const getReportAttributesV2: (
attribute: 'client_connections_postgres',
provider: 'infra-monitoring',
label: 'Postgres',
color: 'var(--chart-1)',
fill: 'var(--chart-1-fill)',
tooltip:
'Direct connections to the Postgres database from your application and external clients',
},
@@ -459,12 +507,16 @@ export const getReportAttributesV2: (
attribute: 'client_connections_authenticator',
provider: 'infra-monitoring',
label: 'PostgREST',
color: 'var(--chart-2)',
fill: 'var(--chart-2-fill)',
tooltip: 'Connection pool managed by PostgREST',
},
{
attribute: 'client_connections_supabase_admin',
provider: 'infra-monitoring',
label: 'Reserved',
color: 'var(--chart-3)',
fill: 'var(--chart-3-fill)',
tooltip:
'Administrative connections used by various Supabase services for internal operations and maintenance tasks',
},
@@ -472,24 +524,31 @@ export const getReportAttributesV2: (
attribute: 'client_connections_supabase_auth_admin',
provider: 'infra-monitoring',
label: 'Auth',
color: 'var(--chart-4)',
fill: 'var(--chart-4-fill)',
tooltip: 'Connection pool managed by Supabase Auth',
},
{
attribute: 'client_connections_supabase_storage_admin',
provider: 'infra-monitoring',
label: 'Storage',
color: 'var(--chart-5)',
fill: 'var(--chart-5-fill)',
tooltip: 'Connection pool managed by Supabase Storage',
},
{
attribute: 'client_connections_other',
provider: 'infra-monitoring',
label: 'Other roles',
color: 'var(--chart-6)',
fill: 'var(--chart-6-fill)',
tooltip: "Miscellaneous database connections that don't fall into other categories.",
},
{
attribute: 'max_db_connections',
provider: 'reference-line',
label: 'Max connections',
color: 'var(--chart-reference)',
value: maxConnections?.maxConnections,
tooltip: 'Max available connections for your current compute size',
isMaxValue: true,
@@ -522,12 +581,15 @@ export const getReportAttributesV2: (
attribute: 'client_connections_pgbouncer',
provider: 'infra-monitoring',
label: isHighAvailability ? 'multipooler' : 'pgbouncer',
color: 'var(--chart-1)',
fill: 'var(--chart-1-fill)',
tooltip: isHighAvailability ? 'Multipooler connections' : 'PgBouncer connections',
},
{
attribute: 'pg_pooler_max_connections',
provider: 'reference-line',
label: 'Max pooler connections',
color: 'var(--chart-reference)',
value: pgBouncerMaxConnections,
tooltip: 'Maximum allowed pooler connections for your current compute size',
isMaxValue: true,
@@ -555,6 +617,8 @@ export const getReportAttributesV2: (
attribute: 'supavisor_connections_active',
provider: 'infra-monitoring',
label: 'supavisor',
color: 'var(--chart-1)',
fill: 'var(--chart-1-fill)',
tooltip: 'Supavisor connections',
},
],
@@ -582,6 +646,8 @@ export const getReportAttributesV2: (
provider: 'infra-monitoring',
format: 'bytes',
label: 'System',
color: 'var(--chart-3)',
fill: 'var(--chart-3-fill)',
tooltip: 'Reserved space for the system to ensure your database runs smoothly',
},
{
@@ -589,6 +655,8 @@ export const getReportAttributesV2: (
provider: 'infra-monitoring',
format: 'bytes',
label: 'WAL',
color: 'var(--chart-2)',
fill: 'var(--chart-2-fill)',
tooltip:
'Disk usage by the write-ahead log. The usage depends on your WAL settings and the amount of data being written to the database',
},
@@ -597,6 +665,8 @@ export const getReportAttributesV2: (
provider: 'infra-monitoring',
format: 'bytes',
label: 'Database',
color: 'var(--chart-1)',
fill: 'var(--chart-1-fill)',
tooltip: 'Disk usage by your database (tables, indexes, data, ...)',
},
{
@@ -605,6 +675,7 @@ export const getReportAttributesV2: (
isMaxValue: true,
format: 'bytes',
label: 'Disk Size',
color: 'var(--chart-reference)',
tooltip: 'Disk Size refers to the total space your project occupies on disk',
},
entitledFeatures.includes('database') &&
-20
View File
@@ -138,16 +138,6 @@
}
:root {
--chart-1: hsl(var(--brand-default));
--chart-2: 173 58% 39%;
--chart-3: 197 37% 24%;
--chart-4: 43 74% 66%;
--chart-5: 27 87% 67%;
--chart-blue: 217 91% 60%;
--chart-warning: hsl(var(--warning-500));
--chart-warning-muted: hsl(var(--warning-400));
--chart-destructive: hsl(var(--destructive-default));
--chart-success: color-mix(in oklch, var(--foreground-muted) 50%, white);
--sidebar-background: var(--background-dash-sidebar);
--sidebar-foreground: var(--foreground-default);
--sidebar-primary: var(--foreground-default);
@@ -161,16 +151,6 @@
[data-theme='dark'],
.dark {
--chart-1: hsl(var(--brand-default));
--chart-2: 160 60% 45%;
--chart-3: 30 80% 55%;
--chart-4: 280 65% 60%;
--chart-5: 340 75% 55%;
--chart-blue: 217 91% 65%;
--chart-warning: hsl(var(--warning-default));
--chart-warning-muted: hsl(var(--warning-500));
--chart-destructive: hsl(var(--destructive-default));
--chart-success: color-mix(in oklch, var(--foreground-muted) 55%, var(--background));
--sidebar-background: var(--background-dash-sidebar);
--sidebar-foreground: var(--foreground-default);
--sidebar-primary: var(--foreground-default);
-15
View File
@@ -37,11 +37,6 @@
/* --color-border: var(--border); */
--color-input: var(--input);
--color-ring: var(--ring);
--color-chart-1: hsl(var(--chart-1));
--color-chart-2: hsl(var(--chart-2));
--color-chart-3: hsl(var(--chart-3));
--color-chart-4: hsl(var(--chart-4));
--color-chart-5: hsl(var(--chart-5));
}
@layer base {
@@ -65,11 +60,6 @@
--border: hsl(0 0% 92.2%);
--input: hsl(0 0% 92.2%);
--ring: hsl(0 0% 70.8%);
--chart-1: 41.1 22.2% 64.6%;
--chart-2: 184.7 11.8% 60%;
--chart-3: 227.4 7% 39.8%;
--chart-4: 84.4 18.9% 82.8%;
--chart-5: 70.1 18.8% 76.9%;
--radius: 0.625rem;
--sidebar: hsl(0 0% 98.5%);
--sidebar-foreground: hsl(0 0% 14.5%);
@@ -102,11 +92,6 @@
--border: hsl(0 0% 26.9%);
--input: hsl(0 0% 26.9%);
--ring: hsl(0 0% 55.6%);
--chart-1: 264.4 24.3% 48.8%;
--chart-2: 162.5 17% 69.6%;
--chart-3: 70.1 18.8% 76.9%;
--chart-4: 303.9 26.5% 62.7%;
--chart-5: 16.4 24.6% 64.5%;
--sidebar: hsl(0 0% 20.5%);
--sidebar-foreground: hsl(0 0% 98.5%);
--sidebar-primary: hsl(264.4 24.3% 48.8%);
+48
View File
@@ -0,0 +1,48 @@
:root {
--chart-1: var(--color-brand-800);
--chart-2: var(--color-blue-900);
--chart-3: var(--color-pink-1000);
--chart-4: var(--color-violet-900);
--chart-5: var(--color-tomato-1000);
--chart-6: var(--color-indigo-1000);
--chart-7: var(--color-green-1100);
--chart-8: var(--color-purple-1000);
--chart-1-fill: var(--color-brand-500);
--chart-2-fill: var(--color-blue-500);
--chart-3-fill: var(--color-pink-500);
--chart-4-fill: var(--color-violet-500);
--chart-5-fill: var(--color-tomato-500);
--chart-6-fill: var(--color-indigo-500);
--chart-7-fill: var(--color-green-500);
--chart-8-fill: var(--color-purple-500);
--chart-in: var(--chart-2);
--chart-out: var(--chart-5);
--chart-in-fill: var(--chart-2-fill);
--chart-out-fill: var(--chart-5-fill);
--chart-reference: var(--color-scale-1000);
--chart-muted: var(--color-scale-700);
--chart-muted-fill: var(--color-scale-400);
--chart-status-success: hsl(var(--brand-default));
--chart-status-success-muted: hsl(var(--brand-500));
--chart-status-warning: var(--warning);
--chart-status-warning-muted: hsl(var(--warning-500));
--chart-status-destructive: var(--destructive);
--chart-status-destructive-muted: hsl(var(--destructive-500));
}
[data-theme*='dark'] {
--chart-1: var(--color-brand-900);
--chart-2: var(--color-blue-1100);
--chart-3: var(--color-pink-1100);
--chart-4: var(--color-violet-1100);
--chart-5: var(--color-tomato-1100);
--chart-6: var(--color-indigo-1100);
--chart-7: var(--color-green-1100);
--chart-8: var(--color-purple-1100);
--chart-muted: var(--color-scale-800);
}
+1
View File
@@ -26,6 +26,7 @@
* consumes them from @theme inline so Tailwind generates the utilities. */
@import './css/colors.css';
@import './css/theme.css';
@import './css/charts.css';
@import './css/animations.css';
@import './css/utilities.css';
@import './tailwind-plugins/hit-area.css';
@@ -15,6 +15,8 @@ import {
import type { CategoricalChartState } from 'recharts/types/chart/types'
import { ChartConfig, ChartContainer, ChartTooltip, ChartTooltipContent, cn } from 'ui'
const DIMMED_FILL_OPACITY = 0.35
const CHART_COLORS = {
TICK: 'var(--background-overlay-hover)',
AXIS: 'var(--background-overlay-hover)',
@@ -66,6 +68,8 @@ export interface ChartBarProps {
showGrid?: boolean
showYAxis?: boolean
showXAxis?: boolean
isStacked?: boolean
margin?: { top?: number; right?: number; bottom?: number; left?: number }
XAxisProps?: {
tick?: boolean
tickFormatter?: (value: any) => string
@@ -102,6 +106,8 @@ export const ChartBar = ({
showGrid = false,
showYAxis = false,
showXAxis = false,
isStacked = false,
margin: marginProp,
XAxisProps,
YAxisProps,
}: ChartBarProps) => {
@@ -146,23 +152,28 @@ export const ChartBar = ({
...XAxisProps,
}
const yAxisWidth = showYAxis ? (YAxisProps?.width ?? 60) : 0
const yAxisConfig = {
tick: showYAxis
? { fill: 'var(--color-foreground-lighter)', fontSize: 10, fontFamily: 'var(--font-mono)' }
: false,
hide: !showYAxis,
tickMargin: showYAxis ? (YAxisProps?.tickMargin ?? 4) : 0,
width: showYAxis ? (YAxisProps?.width ?? 60) : 0,
width: yAxisWidth,
axisLine: { stroke: CHART_COLORS.AXIS },
tickLine: { stroke: CHART_COLORS.AXIS },
...YAxisProps,
}
const hasDateRangeFooter = xKey === 'timestamp' && data.length > 0
const margin = {
top: 0,
right: 0,
left: 0,
bottom: 0,
...marginProp,
}
return (
@@ -170,7 +181,10 @@ export const ChartBar = ({
data-testid="chart-bar"
className={cn('flex flex-col gap-y-3 w-full', isFullHeight ? 'h-full' : 'h-24', className)}
>
<ChartContainer className="w-full! h-full" config={chartConfig}>
<ChartContainer
className={cn('w-full!', hasDateRangeFooter ? 'h-[calc(100%-28px)]' : 'h-full')}
config={chartConfig}
>
<RechartBarChart
data={data}
syncId={syncId}
@@ -230,7 +244,6 @@ export const ChartBar = ({
/>
}
/>
{/* Selection highlight area */}
{showHighlightActions && (
<ReferenceArea
x1={chartHighlight?.coordinates.left}
@@ -251,7 +264,28 @@ export const ChartBar = ({
? keyConfig.theme.dark
: keyConfig.theme.light
: color)
return <Bar key={key} dataKey={key} fill={barColor} maxBarSize={24} />
return (
<Bar
key={key}
dataKey={key}
fill={barColor}
maxBarSize={24}
stackId={isStacked ? 'stack' : undefined}
>
{data.map((_entry: ChartBarTick, dataIndex: number) => (
<Cell
key={`${key}-${dataIndex}`}
className="cursor-pointer transition-opacity"
fill={barColor}
fillOpacity={
focusDataIndex === null || focusDataIndex === dataIndex
? 1
: DIMMED_FILL_OPACITY
}
/>
))}
</Bar>
)
})
) : (
<Bar dataKey={dataKey} fill={color} maxBarSize={24}>
@@ -267,8 +301,11 @@ export const ChartBar = ({
</RechartBarChart>
</ChartContainer>
{xKey === 'timestamp' && data && data.length > 0 && (
<div className="text-foreground-lighter -mt-6 flex items-center justify-between text-[10px] font-mono">
{hasDateRangeFooter && (
<div
className="text-foreground-lighter flex h-4 items-center justify-between text-[10px] font-mono"
style={{ paddingLeft: yAxisWidth + margin.left }}
>
<span>{dayjs(data[0][xKey]).format(DateTimeFormat)}</span>
<span>{dayjs(data[data.length - 1]?.[xKey]).format(DateTimeFormat)}</span>
</div>
@@ -2,7 +2,7 @@
import dayjs from 'dayjs'
import { useTheme } from 'next-themes'
import { ReactNode, useState } from 'react'
import { ReactNode, useId, useState } from 'react'
import {
Area,
CartesianGrid,
@@ -118,6 +118,7 @@ export const ChartLine = ({
referenceLines,
}: ChartLineProps) => {
const [focusDataIndex, setFocusDataIndex] = useState<number | null>(null)
const gradientPrefix = `chart-line-fill-${useId().replace(/:/g, '')}`
const { resolvedTheme } = useTheme()
const isDarkMode = resolvedTheme?.includes('dark')
@@ -157,18 +158,30 @@ export const ChartLine = ({
...XAxisProps,
}
const yAxisWidth = showYAxis ? (YAxisProps?.width ?? 60) : 0
const yAxisConfig = {
tick: showYAxis
? { fill: 'var(--color-foreground-lighter)', fontSize: 10, fontFamily: 'var(--font-mono)' }
: false,
hide: !showYAxis,
tickMargin: showYAxis ? (YAxisProps?.tickMargin ?? 4) : 0,
width: showYAxis ? (YAxisProps?.width ?? 60) : 0,
width: yAxisWidth,
axisLine: { stroke: CHART_COLORS.AXIS },
tickLine: { stroke: CHART_COLORS.AXIS },
...YAxisProps,
}
const hasDateRangeFooter = xKey === 'timestamp' && data.length > 0
const resolveSeriesColor = (key: string) => {
const keyConfig = chartConfig[key]
return (
keyConfig?.color ||
(keyConfig?.theme ? (isDarkMode ? keyConfig.theme.dark : keyConfig.theme.light) : color)
)
}
const margin = {
top: 0,
right: 0,
@@ -189,7 +202,10 @@ export const ChartLine = ({
data-testid="chart-line"
className={cn('flex flex-col gap-y-3 w-full', isFullHeight ? 'h-full' : 'h-24', className)}
>
<ChartContainer className="w-full! h-full" config={chartConfig}>
<ChartContainer
className={cn('w-full!', hasDateRangeFooter ? 'h-[calc(100%-28px)]' : 'h-full')}
config={chartConfig}
>
<RechartAreaChart
data={data}
syncId={syncId}
@@ -236,6 +252,21 @@ export const ChartLine = ({
if (onLineClick) onLineClick(datum, tooltipData)
}}
>
<defs>
{keysToRender.map((key, index) => (
<linearGradient
key={key}
id={`${gradientPrefix}-${index}`}
x1="0"
y1="0"
x2="0"
y2="1"
>
<stop offset="5%" stopColor={resolveSeriesColor(key)} stopOpacity={0.8} />
<stop offset="95%" stopColor={resolveSeriesColor(key)} stopOpacity={0} />
</linearGradient>
))}
</defs>
{showGrid && <CartesianGrid vertical={false} stroke={CHART_COLORS.AXIS} />}
<YAxis {...yAxisConfig} />
<XAxis {...xAxisConfig} />
@@ -275,7 +306,6 @@ export const ChartLine = ({
/>
}
/>
{/* Selection highlight area */}
{showHighlightActions && (
<ReferenceArea
x1={chartHighlight?.coordinates.left}
@@ -295,38 +325,26 @@ export const ChartLine = ({
strokeDasharray={line.strokeDasharray ?? '4 4'}
/>
))}
{keysToRender.map((key, index) => {
const keyConfig = chartConfig[key]
const lineColor =
keyConfig?.color ||
(keyConfig?.theme
? isDarkMode
? keyConfig.theme.dark
: keyConfig.theme.light
: color)
const baseOpacity = 0.2
const opacityIncrement = 0.1
const maxOpacity = 0.6
const fillOpacity = Math.min(baseOpacity + index * opacityIncrement, maxOpacity)
return (
<Area
key={key}
type="step"
dataKey={key}
fill={lineColor}
fillOpacity={fillOpacity}
stroke={lineColor}
strokeWidth={strokeWidth}
stackId={keysToRender.length > 1 ? `stack-${key}` : undefined}
/>
)
})}
{keysToRender.map((key, index) => (
<Area
key={key}
type="step"
dataKey={key}
fill={`url(#${gradientPrefix}-${index})`}
fillOpacity={0.1}
stroke={resolveSeriesColor(key)}
strokeWidth={strokeWidth}
stackId={keysToRender.length > 1 ? `stack-${key}` : undefined}
/>
))}
</RechartAreaChart>
</ChartContainer>
{xKey === 'timestamp' && data && data.length > 0 && (
<div className="text-foreground-lighter -mt-6 flex items-center justify-between text-[10px] font-mono">
{hasDateRangeFooter && (
<div
className="text-foreground-lighter flex h-4 items-center justify-between text-[10px] font-mono"
style={{ paddingLeft: yAxisWidth + margin.left }}
>
<span>{dayjs(data[0][xKey]).format(DateTimeFormat)}</span>
<span>{dayjs(data[data.length - 1]?.[xKey]).format(DateTimeFormat)}</span>
</div>
@@ -13,8 +13,8 @@ const CHART_COLORS = {
GREEN_2: 'hsl(var(--brand-500))',
RED_1: 'hsl(var(--destructive-default))',
RED_2: 'hsl(var(--destructive-500))',
YELLOW_1: 'var(--chart-warning, hsl(var(--warning-default)))',
YELLOW_2: 'var(--chart-warning-muted, hsl(var(--warning-500)))',
YELLOW_1: 'var(--chart-status-warning)',
YELLOW_2: 'var(--chart-status-warning-muted)',
}
type LogsBarChartDatum = {