import dayjs from 'dayjs' import { Activity, BarChartIcon, Loader2 } from 'lucide-react' import { useRouter } from 'next/router' import { ReactNode, useCallback, useEffect, useMemo, useState } from 'react' import { Bar, BarChart, CartesianGrid, Line, LineChart, XAxis, YAxis } from 'recharts' import { ChartContainer, ChartTooltip, ChartTooltipContent, 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 { checkHasNonPositiveValues, computeYAxisWidth, formatLogTick, formatYAxisTick, } from '@/components/ui/QueryBlock/QueryBlock.utils' 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 { 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 chartConfig?: Partial }) => 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 [chartStyle, setChartStyle] = useState(defaultChartStyle) const logScale = useMemo(() => defaultLogScale, [defaultLogScale]) const [latestValue, setLatestValue] = useState() 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 = mapMultiResponseToAnalyticsData(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 'hsl(var(--chart-1))' if (threshold.check === 'gt') { return value >= threshold.danger ? 'hsl(var(--chart-destructive))' : value >= threshold.warning ? 'hsl(var(--chart-warning))' : 'hsl(var(--chart-1))' } else { return value <= threshold.danger ? 'hsl(var(--chart-destructive))' : value <= threshold.warning ? 'hsl(var(--chart-warning))' : 'hsl(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 ( : } 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'}`, }, }} /> Log} 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 ? (

Loading data for {label}

) : chartData === undefined ? (

Unable to load data for {label}

) : data.length === 0 ? (
) : ( <> {latestValue && (
Most recently

{latestValue}

)} {hasNonPositiveValues && (

Log scale is unavailable because the data contains zero or negative values.

)} {chartStyle === 'bar' ? ( formatTooltipDate(x as string | number, 'DD MMM YYYY')} /> } /> ) : ( formatTooltipDate(x as string | number, 'DD MMM YYYY')} /> } /> )} )}
) }