ChartHandler.tsx208 lines · main
1import dayjs from 'dayjs'
2import { Activity, BarChartIcon, Loader2 } from 'lucide-react'
3import { useRouter } from 'next/router'
4import { PropsWithChildren, useMemo, useState } from 'react'
5import { Button, Tooltip, TooltipContent, TooltipTrigger, WarningIcon } from 'ui'
6
7import type { ChartData } from './Charts.types'
8import AreaChart from '@/components/ui/Charts/AreaChart'
9import BarChart from '@/components/ui/Charts/BarChart'
10import { AnalyticsInterval } from '@/data/analytics/constants'
11import { mapMultiResponseToAnalyticsData } from '@/data/analytics/infra-monitoring-queries'
12import {
13 InfraMonitoringAttribute,
14 useInfraMonitoringAttributesQuery,
15} from '@/data/analytics/infra-monitoring-query'
16import {
17 ProjectDailyStatsAttribute,
18 useProjectDailyStatsQuery,
19} from '@/data/analytics/project-daily-stats-query'
20import { useDatabaseSelectorStateSnapshot } from '@/state/database-selector'
21
22interface 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 */
49const 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
208export default ChartHandler