Files
supabase/apps/studio/components/ui/Charts/ChartHandler.tsx
kemal.earth 8531064ad7 chore(studio): small visual update to empty observability charts (#46728)
## 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?

Subtle improvement to charts that cannot load data. Less floaty.

| Before | After |
|--------|--------|
| <img width="731" height="323" alt="Screenshot 2026-06-08 at 11 46 21"
src="https://github.com/user-attachments/assets/14c87766-dfe3-448d-8c9c-f3176b658d08"
/> | <img width="791" height="333" alt="Screenshot 2026-06-08 at 11 46
01"
src="https://github.com/user-attachments/assets/d0e33b91-2990-41d2-b14c-25065b1f0c12"
/> |






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

## Summary by CodeRabbit

## Release Notes

**Style**
- Improved visual styling for chart error and loading states. Chart
placeholders now feature increased height and enhanced border
presentation with dashed borders and rounded corners, providing clearer
visual distinction when data is unavailable or unable to load.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-06-08 14:12:47 +01:00

209 lines
6.5 KiB
TypeScript

import dayjs from 'dayjs'
import { Activity, BarChartIcon, Loader2 } from 'lucide-react'
import { useRouter } from 'next/router'
import { PropsWithChildren, useMemo, useState } from 'react'
import { Button, Tooltip, TooltipContent, TooltipTrigger, WarningIcon } from 'ui'
import type { ChartData } from './Charts.types'
import AreaChart from '@/components/ui/Charts/AreaChart'
import BarChart from '@/components/ui/Charts/BarChart'
import { AnalyticsInterval } from '@/data/analytics/constants'
import { mapMultiResponseToAnalyticsData } 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 { useDatabaseSelectorStateSnapshot } from '@/state/database-selector'
interface ChartHandlerProps {
id?: string
label: string
attribute: string
provider: 'infra-monitoring' | 'daily-stats'
startDate: string
endDate: string
interval: string
customDateFormat?: string
defaultChartStyle?: 'bar' | 'line'
hideChartType?: boolean
data?: ChartData
isLoading?: boolean
format?: string
highlightedValue?: string | number
syncId?: string
}
/**
* Controls chart display state. Optionally fetches static chart data if data is not provided.
*
* If the `data` prop is provided, it will disable automatic chart data fetching and pass the data directly to the chart render.
* - loading state can also be provided through the `isLoading` prop, to display loading placeholders. Ignored if `data` key not provided.
* - if `isLoading=true` and `data` is `undefined`, loading error message will be shown.
*
* Provided data must be in the expected chart format.
*/
const ChartHandler = ({
label,
attribute,
provider,
startDate,
endDate,
interval,
customDateFormat,
children = null,
defaultChartStyle = 'bar',
hideChartType = false,
data,
isLoading,
format,
highlightedValue,
syncId,
...otherProps
}: PropsWithChildren<ChartHandlerProps>) => {
const router = useRouter()
const { ref } = router.query
const state = useDatabaseSelectorStateSnapshot()
const [chartStyle, setChartStyle] = useState<string>(defaultChartStyle)
const databaseIdentifier = state.selectedDatabaseId
const { data: dailyStatsData, isPending: isFetchingDailyStats } = 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' && data === undefined }
)
const { data: infraMonitoringData, isPending: isFetchingInfraMonitoring } =
useInfraMonitoringAttributesQuery(
{
projectRef: ref as string,
attributes: [attribute as InfraMonitoringAttribute],
startDate,
endDate,
interval: interval as AnalyticsInterval,
databaseIdentifier,
},
{ enabled: provider === 'infra-monitoring' && data === undefined }
)
const transformedInfraData = useMemo(() => {
if (!infraMonitoringData) return undefined
const mapped = mapMultiResponseToAnalyticsData(infraMonitoringData, [
attribute as InfraMonitoringAttribute,
])
return mapped[attribute]
}, [infraMonitoringData, attribute])
const chartData =
data ||
(provider === 'infra-monitoring'
? transformedInfraData
: provider === 'daily-stats'
? dailyStatsData
: undefined)
const loading =
isLoading ||
(provider === 'infra-monitoring'
? isFetchingInfraMonitoring
: provider === 'daily-stats'
? isFetchingDailyStats
: isLoading)
const shouldHighlightMaxValue =
provider === 'daily-stats' &&
!attribute.includes('ingress') &&
!attribute.includes('egress') &&
chartData !== undefined &&
'maximum' in chartData
const shouldHighlightTotalGroupedValue = chartData !== undefined && 'totalGrouped' in chartData
const _highlightedValue =
highlightedValue !== undefined
? highlightedValue
: shouldHighlightMaxValue
? chartData?.maximum
: provider === 'daily-stats'
? chartData?.total
: shouldHighlightTotalGroupedValue
? chartData?.totalGrouped?.[attribute]
: (chartData?.data[chartData?.data.length - 1] as any)?.[attribute as any]
if (loading) {
return (
<div className="flex h-52 w-full flex-col items-center justify-center gap-y-2">
<Loader2 size={18} className="animate-spin text-border-strong" />
<p className="text-xs text-foreground-lighter">Loading data for {label}</p>
</div>
)
}
if (chartData === undefined) {
return (
<div className="flex h-64 w-full flex-col items-center justify-center gap-y-2 border border-dashed rounded-md">
<WarningIcon />
<p className="text-xs text-foreground-lighter">Unable to load data for {label}</p>
</div>
)
}
return (
<div className="h-full w-full">
<div className="absolute right-6 z-10 flex justify-between">
{!hideChartType && (
<Tooltip>
<TooltipTrigger asChild>
<Button
type="default"
className="px-1.5"
icon={chartStyle === 'bar' ? <Activity /> : <BarChartIcon />}
onClick={() => setChartStyle(chartStyle === 'bar' ? 'line' : 'bar')}
/>
</TooltipTrigger>
<TooltipContent side="left" align="center">
View as {chartStyle === 'bar' ? 'line chart' : 'bar chart'}
</TooltipContent>
</Tooltip>
)}
{children}
</div>
{chartStyle === 'bar' ? (
<BarChart
data={(chartData?.data ?? []) as any}
format={format || chartData?.format}
xAxisKey={'period_start'}
yAxisKey={attribute}
highlightedValue={_highlightedValue}
title={label}
customDateFormat={customDateFormat}
syncId={syncId}
{...otherProps}
/>
) : (
<AreaChart
data={(chartData?.data ?? []) as any}
format={format || chartData?.format}
xAxisKey="period_start"
yAxisKey={attribute}
highlightedValue={_highlightedValue}
title={label}
customDateFormat={customDateFormat}
syncId={syncId}
{...otherProps}
/>
)}
</div>
)
}
export default ChartHandler