import React, { PropsWithChildren, useState, useMemo, useEffect, useRef } from 'react' import { useRouter } from 'next/router' import { Loader2 } from 'lucide-react' import { cn, WarningIcon } from 'ui' import Panel from 'components/ui/Panel' import ComposedChart from './ComposedChart' import { AnalyticsInterval, DataPoint } from 'data/analytics/constants' import { InfraMonitoringAttribute } from 'data/analytics/infra-monitoring-query' import { useInfraMonitoringQueries } from 'data/analytics/infra-monitoring-queries' import { ProjectDailyStatsAttribute } from 'data/analytics/project-daily-stats-query' import { useProjectDailyStatsQueries } from 'data/analytics/project-daily-stats-queries' import { useDatabaseSelectorStateSnapshot } from 'state/database-selector' import { useChartHighlight } from './useChartHighlight' import type { ChartData } from './Charts.types' import type { UpdateDateRange } from 'pages/project/[ref]/reports/database' import { MultiAttribute } from './ComposedChart.utils' export interface ComposedChartHandlerProps { id?: string label: string attributes: MultiAttribute[] startDate: string endDate: string interval: string customDateFormat?: string defaultChartStyle?: 'bar' | 'line' | 'stackedAreaLine' hideChartType?: boolean data?: ChartData isLoading?: boolean format?: string highlightedValue?: string | number className?: string showTooltip?: boolean showLegend?: boolean showTotal?: boolean showMaxValue?: boolean updateDateRange?: UpdateDateRange valuePrecision?: number isVisible?: boolean docsUrl?: string } /** * Wrapper component that handles intersection observer logic for lazy loading */ const LazyChartWrapper = ({ children }: PropsWithChildren) => { const [isVisible, setIsVisible] = useState(false) const ref = useRef(null) useEffect(() => { const observer = new IntersectionObserver( ([entry]) => { if (entry.isIntersecting) { setIsVisible(true) observer.disconnect() } }, { rootMargin: '150px 0px', // Start loading before the component enters viewport threshold: 0, } ) const currentRef = ref.current if (currentRef) { observer.observe(currentRef) } return () => { if (currentRef) { observer.unobserve(currentRef) } } }, []) return
{React.cloneElement(children as React.ReactElement, { isVisible })}
} /** * 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 ComposedChartHandler = ({ label, attributes, startDate, endDate, interval, customDateFormat, children = null, defaultChartStyle = 'bar', hideChartType = false, data, isLoading, format, highlightedValue, className, showTooltip, showLegend, showMaxValue, showTotal, updateDateRange, valuePrecision, isVisible = true, id, ...otherProps }: PropsWithChildren) => { const router = useRouter() const { ref } = router.query const state = useDatabaseSelectorStateSnapshot() const [chartStyle, setChartStyle] = useState(defaultChartStyle) const chartHighlight = useChartHighlight() const databaseIdentifier = state.selectedDatabaseId // Use the custom hook at the top level of the component const attributeQueries = useAttributeQueries( attributes, ref, startDate, endDate, interval as AnalyticsInterval, databaseIdentifier, data, isVisible ) // Combine all the data into a single dataset const combinedData = useMemo(() => { if (data) return data const isLoading = attributeQueries.some((query: any) => query.isLoading) if (isLoading) return undefined const hasError = attributeQueries.some((query: any) => !query.data) if (hasError) return undefined // Get all unique timestamps from all datasets const timestamps = new Set() attributeQueries.forEach((query: any) => { query.data?.data?.forEach((point: any) => { if (point?.period_start) { timestamps.add(point.period_start) } }) }) const referenceLineQueries = attributeQueries.filter( (_, index) => attributes[index].provider === 'reference-line' ) // Combine data points for each timestamp const combined = Array.from(timestamps) .sort() .map((timestamp) => { const point: any = { timestamp } // Add regular attributes attributes.forEach((attr, index) => { if (!attr) return // Handle custom value attributes (like disk size) if (attr.customValue !== undefined) { point[attr.attribute] = attr.customValue return } // Skip reference line attributes here, we'll add them below if (attr.provider === 'reference-line') return const queryData = attributeQueries[index]?.data?.data const matchingPoint = queryData?.find((p: any) => p.period_start === timestamp) let value = matchingPoint?.[attr.attribute] ?? 0 // Apply value manipulation if provided if (attr.manipulateValue && typeof attr.manipulateValue === 'function') { // Ensure value is a number before manipulation const numericValue = typeof value === 'number' ? value : Number(value) || 0 value = attr.manipulateValue(numericValue) } point[attr.attribute] = value }) // Add reference line values for each timestamp referenceLineQueries.forEach((query: any) => { const attr = query.data.attribute const value = query.data.total point[attr] = value }) return point as DataPoint }) return combined as DataPoint[] }, [data, attributeQueries, attributes]) const loading = isLoading || attributeQueries.some((query: any) => query.isLoading) // Calculate highlighted value based on the first attribute's data const _highlightedValue = useMemo(() => { if (highlightedValue !== undefined) return highlightedValue const firstAttr = attributes[0] const firstQuery = attributeQueries[0] const firstData = firstQuery?.data if (!firstData) return undefined const shouldHighlightMaxValue = firstAttr.provider === 'daily-stats' && !firstAttr.attribute.includes('ingress') && !firstAttr.attribute.includes('egress') && 'maximum' in firstData const shouldHighlightTotalGroupedValue = 'totalGrouped' in firstData return shouldHighlightMaxValue ? firstData.maximum : firstAttr.provider === 'daily-stats' ? firstData.total : shouldHighlightTotalGroupedValue ? firstData.totalGrouped?.[firstAttr.attribute as keyof typeof firstData.totalGrouped] : (firstData.data[firstData.data.length - 1] as any)?.[firstAttr.attribute] }, [highlightedValue, attributes, attributeQueries]) if (loading) { return (

Loading data for {label}

) } if (!combinedData) { return (

Unable to load data for {label}

) } // Rest of the component remains similar, but pass all attributes to charts return (
{children}
) } const useAttributeQueries = ( attributes: MultiAttribute[], ref: string | string[] | undefined, startDate: string, endDate: string, interval: AnalyticsInterval, databaseIdentifier: string | undefined, data: ChartData | undefined, isVisible: boolean ) => { const infraAttributes = attributes .filter((attr) => attr?.provider === 'infra-monitoring') .map((attr) => attr.attribute as InfraMonitoringAttribute) const dailyStatsAttributes = attributes .filter((attr) => attr?.provider === 'daily-stats') .map((attr) => attr.attribute as ProjectDailyStatsAttribute) const referenceLines = attributes.filter((attr) => attr?.provider === 'reference-line') const infraQueries = useInfraMonitoringQueries( infraAttributes, ref, startDate, endDate, interval, databaseIdentifier, data, isVisible ) const dailyStatsQueries = useProjectDailyStatsQueries( dailyStatsAttributes, ref, startDate, endDate, interval, databaseIdentifier, data, isVisible ) const referenceLineQueries = referenceLines.map((line) => { let value = line.value || 0 return { data: { data: [], // Will be populated in combinedData attribute: line.attribute, total: value, maximum: value, totalGrouped: { [line.attribute]: value }, }, isLoading: false, isError: false, } }) return [...infraQueries, ...dailyStatsQueries, ...referenceLineQueries] } export default function LazyComposedChartHandler(props: ComposedChartHandlerProps) { return ( ) }