mirror of
https://github.com/supabase/supabase.git
synced 2026-10-06 09:55:06 +03:00
## 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>
374 lines
13 KiB
TypeScript
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>
|
|
)
|
|
}
|