ChartHandler.tsx208 lines · main
| 1 | import dayjs from 'dayjs' |
| 2 | import { Activity, BarChartIcon, Loader2 } from 'lucide-react' |
| 3 | import { useRouter } from 'next/router' |
| 4 | import { PropsWithChildren, useMemo, useState } from 'react' |
| 5 | import { Button, Tooltip, TooltipContent, TooltipTrigger, WarningIcon } from 'ui' |
| 6 | |
| 7 | import type { ChartData } from './Charts.types' |
| 8 | import AreaChart from '@/components/ui/Charts/AreaChart' |
| 9 | import BarChart from '@/components/ui/Charts/BarChart' |
| 10 | import { AnalyticsInterval } from '@/data/analytics/constants' |
| 11 | import { mapMultiResponseToAnalyticsData } from '@/data/analytics/infra-monitoring-queries' |
| 12 | import { |
| 13 | InfraMonitoringAttribute, |
| 14 | useInfraMonitoringAttributesQuery, |
| 15 | } from '@/data/analytics/infra-monitoring-query' |
| 16 | import { |
| 17 | ProjectDailyStatsAttribute, |
| 18 | useProjectDailyStatsQuery, |
| 19 | } from '@/data/analytics/project-daily-stats-query' |
| 20 | import { useDatabaseSelectorStateSnapshot } from '@/state/database-selector' |
| 21 | |
| 22 | interface ChartHandlerProps { |
| 23 | id?: string |
| 24 | label: string |
| 25 | attribute: string |
| 26 | provider: 'infra-monitoring' | 'daily-stats' |
| 27 | startDate: string |
| 28 | endDate: string |
| 29 | interval: string |
| 30 | customDateFormat?: string |
| 31 | defaultChartStyle?: 'bar' | 'line' |
| 32 | hideChartType?: boolean |
| 33 | data?: ChartData |
| 34 | isLoading?: boolean |
| 35 | format?: string |
| 36 | highlightedValue?: string | number |
| 37 | syncId?: string |
| 38 | } |
| 39 | |
| 40 | /** |
| 41 | * Controls chart display state. Optionally fetches static chart data if data is not provided. |
| 42 | * |
| 43 | * If the `data` prop is provided, it will disable automatic chart data fetching and pass the data directly to the chart render. |
| 44 | * - loading state can also be provided through the `isLoading` prop, to display loading placeholders. Ignored if `data` key not provided. |
| 45 | * - if `isLoading=true` and `data` is `undefined`, loading error message will be shown. |
| 46 | * |
| 47 | * Provided data must be in the expected chart format. |
| 48 | */ |
| 49 | const ChartHandler = ({ |
| 50 | label, |
| 51 | attribute, |
| 52 | provider, |
| 53 | startDate, |
| 54 | endDate, |
| 55 | interval, |
| 56 | customDateFormat, |
| 57 | children = null, |
| 58 | defaultChartStyle = 'bar', |
| 59 | hideChartType = false, |
| 60 | data, |
| 61 | isLoading, |
| 62 | format, |
| 63 | highlightedValue, |
| 64 | syncId, |
| 65 | ...otherProps |
| 66 | }: PropsWithChildren<ChartHandlerProps>) => { |
| 67 | const router = useRouter() |
| 68 | const { ref } = router.query |
| 69 | |
| 70 | const state = useDatabaseSelectorStateSnapshot() |
| 71 | const [chartStyle, setChartStyle] = useState<string>(defaultChartStyle) |
| 72 | |
| 73 | const databaseIdentifier = state.selectedDatabaseId |
| 74 | |
| 75 | const { data: dailyStatsData, isPending: isFetchingDailyStats } = useProjectDailyStatsQuery( |
| 76 | { |
| 77 | projectRef: ref as string, |
| 78 | attribute: attribute as ProjectDailyStatsAttribute, |
| 79 | startDate: dayjs(startDate).format('YYYY-MM-DD'), |
| 80 | endDate: dayjs(endDate).format('YYYY-MM-DD'), |
| 81 | }, |
| 82 | { enabled: provider === 'daily-stats' && data === undefined } |
| 83 | ) |
| 84 | |
| 85 | const { data: infraMonitoringData, isPending: isFetchingInfraMonitoring } = |
| 86 | useInfraMonitoringAttributesQuery( |
| 87 | { |
| 88 | projectRef: ref as string, |
| 89 | attributes: [attribute as InfraMonitoringAttribute], |
| 90 | startDate, |
| 91 | endDate, |
| 92 | interval: interval as AnalyticsInterval, |
| 93 | databaseIdentifier, |
| 94 | }, |
| 95 | { enabled: provider === 'infra-monitoring' && data === undefined } |
| 96 | ) |
| 97 | |
| 98 | const transformedInfraData = useMemo(() => { |
| 99 | if (!infraMonitoringData) return undefined |
| 100 | const mapped = mapMultiResponseToAnalyticsData(infraMonitoringData, [ |
| 101 | attribute as InfraMonitoringAttribute, |
| 102 | ]) |
| 103 | return mapped[attribute] |
| 104 | }, [infraMonitoringData, attribute]) |
| 105 | |
| 106 | const chartData = |
| 107 | data || |
| 108 | (provider === 'infra-monitoring' |
| 109 | ? transformedInfraData |
| 110 | : provider === 'daily-stats' |
| 111 | ? dailyStatsData |
| 112 | : undefined) |
| 113 | |
| 114 | const loading = |
| 115 | isLoading || |
| 116 | (provider === 'infra-monitoring' |
| 117 | ? isFetchingInfraMonitoring |
| 118 | : provider === 'daily-stats' |
| 119 | ? isFetchingDailyStats |
| 120 | : isLoading) |
| 121 | |
| 122 | const shouldHighlightMaxValue = |
| 123 | provider === 'daily-stats' && |
| 124 | !attribute.includes('ingress') && |
| 125 | !attribute.includes('egress') && |
| 126 | chartData !== undefined && |
| 127 | 'maximum' in chartData |
| 128 | const shouldHighlightTotalGroupedValue = chartData !== undefined && 'totalGrouped' in chartData |
| 129 | |
| 130 | const _highlightedValue = |
| 131 | highlightedValue !== undefined |
| 132 | ? highlightedValue |
| 133 | : shouldHighlightMaxValue |
| 134 | ? chartData?.maximum |
| 135 | : provider === 'daily-stats' |
| 136 | ? chartData?.total |
| 137 | : shouldHighlightTotalGroupedValue |
| 138 | ? chartData?.totalGrouped?.[attribute] |
| 139 | : (chartData?.data[chartData?.data.length - 1] as any)?.[attribute as any] |
| 140 | |
| 141 | if (loading) { |
| 142 | return ( |
| 143 | <div className="flex h-52 w-full flex-col items-center justify-center gap-y-2"> |
| 144 | <Loader2 size={18} className="animate-spin text-border-strong" /> |
| 145 | <p className="text-xs text-foreground-lighter">Loading data for {label}</p> |
| 146 | </div> |
| 147 | ) |
| 148 | } |
| 149 | |
| 150 | if (chartData === undefined) { |
| 151 | return ( |
| 152 | <div className="flex h-52 w-full flex-col items-center justify-center gap-y-2"> |
| 153 | <WarningIcon /> |
| 154 | <p className="text-xs text-foreground-lighter">Unable to load data for {label}</p> |
| 155 | </div> |
| 156 | ) |
| 157 | } |
| 158 | |
| 159 | return ( |
| 160 | <div className="h-full w-full"> |
| 161 | <div className="absolute right-6 z-10 flex justify-between"> |
| 162 | {!hideChartType && ( |
| 163 | <Tooltip> |
| 164 | <TooltipTrigger asChild> |
| 165 | <Button |
| 166 | type="default" |
| 167 | className="px-1.5" |
| 168 | icon={chartStyle === 'bar' ? <Activity /> : <BarChartIcon />} |
| 169 | onClick={() => setChartStyle(chartStyle === 'bar' ? 'line' : 'bar')} |
| 170 | /> |
| 171 | </TooltipTrigger> |
| 172 | <TooltipContent side="left" align="center"> |
| 173 | View as {chartStyle === 'bar' ? 'line chart' : 'bar chart'} |
| 174 | </TooltipContent> |
| 175 | </Tooltip> |
| 176 | )} |
| 177 | {children} |
| 178 | </div> |
| 179 | {chartStyle === 'bar' ? ( |
| 180 | <BarChart |
| 181 | data={(chartData?.data ?? []) as any} |
| 182 | format={format || chartData?.format} |
| 183 | xAxisKey={'period_start'} |
| 184 | yAxisKey={attribute} |
| 185 | highlightedValue={_highlightedValue} |
| 186 | title={label} |
| 187 | customDateFormat={customDateFormat} |
| 188 | syncId={syncId} |
| 189 | {...otherProps} |
| 190 | /> |
| 191 | ) : ( |
| 192 | <AreaChart |
| 193 | data={(chartData?.data ?? []) as any} |
| 194 | format={format || chartData?.format} |
| 195 | xAxisKey="period_start" |
| 196 | yAxisKey={attribute} |
| 197 | highlightedValue={_highlightedValue} |
| 198 | title={label} |
| 199 | customDateFormat={customDateFormat} |
| 200 | syncId={syncId} |
| 201 | {...otherProps} |
| 202 | /> |
| 203 | )} |
| 204 | </div> |
| 205 | ) |
| 206 | } |
| 207 | |
| 208 | export default ChartHandler |