ReportChartV2.tsx192 lines · main
1import { useQuery } from '@tanstack/react-query'
2import { Loader2 } from 'lucide-react'
3import { useState } from 'react'
4import { Card, CardContent, cn } from 'ui'
5
6import { ReportChartUpsell } from './ReportChartUpsell'
7import type { ChartHighlightAction } from '@/components/ui/Charts/ChartHighlightActions'
8import { ComposedChart } from '@/components/ui/Charts/ComposedChart'
9import type { MultiAttribute } from '@/components/ui/Charts/ComposedChart.utils'
10import { useChartHighlight } from '@/components/ui/Charts/useChartHighlight'
11import type { AnalyticsInterval } from '@/data/analytics/constants'
12import type { ReportConfig } from '@/data/reports/v2/reports.types'
13import { useFillTimeseriesSorted } from '@/hooks/analytics/useFillTimeseriesSorted'
14import { useCheckEntitlements } from '@/hooks/misc/useCheckEntitlements'
15import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization'
16
17export interface ReportChartV2Props {
18 report: ReportConfig
19 projectRef: string
20 startDate: string
21 endDate: string
22 interval: AnalyticsInterval
23 updateDateRange: (from: string, to: string) => void
24 /**
25 * Group ID used to invalidate React Query caches
26 */
27 queryGroup?: string
28 className?: string
29 syncId?: string
30 filters?: any
31 highlightActions?: ChartHighlightAction[]
32}
33
34// Compute total across entire period over unique attribute keys.
35// Excludes attributes that are disabled, reference lines, max values, or marked omitFromTotal.
36export function computePeriodTotal(
37 chartData: Record<string, unknown>[],
38 dynamicAttributes: MultiAttribute[]
39): number {
40 const attributeKeys = Array.from(
41 new Set(
42 dynamicAttributes
43 .filter(
44 (a) =>
45 a?.enabled !== false &&
46 a?.provider !== 'reference-line' &&
47 !a?.isMaxValue &&
48 !a?.omitFromTotal
49 )
50 .map((a) => a.attribute)
51 )
52 )
53
54 return chartData.reduce((sum: number, row: Record<string, unknown>) => {
55 const rowTotal = attributeKeys.reduce((acc: number, key: string) => {
56 const value = row?.[key]
57 return acc + (typeof value === 'number' ? value : 0)
58 }, 0)
59 return sum + rowTotal
60 }, 0)
61}
62
63export const ReportChartV2 = ({
64 report,
65 projectRef,
66 startDate,
67 endDate,
68 interval,
69 updateDateRange,
70 className,
71 syncId,
72 filters,
73 highlightActions,
74 queryGroup,
75}: ReportChartV2Props) => {
76 const { data: org } = useSelectedOrganizationQuery()
77 const { getEntitlementSetValues, isLoading: isEntitlementLoading } = useCheckEntitlements(
78 'observability.dashboard_advanced_metrics',
79 undefined,
80 { enabled: !!report.entitlement }
81 )
82
83 const entitledFeatures = getEntitlementSetValues()
84 const isAvailable = !report.entitlement || entitledFeatures.includes(report.entitlement)
85
86 const canFetch = isAvailable
87
88 const {
89 data: queryResult,
90 isLoading: isLoadingChart,
91 error,
92 isFetching,
93 } = useQuery({
94 queryKey: [
95 'projects',
96 projectRef,
97 'report-v2',
98 { reportId: report.id, queryGroup, startDate, endDate, interval, filters },
99 ],
100 queryFn: async () => {
101 return await report.dataProvider(projectRef, startDate, endDate, interval, filters)
102 },
103 enabled: Boolean(projectRef && canFetch && isAvailable && !report.hide),
104 refetchOnWindowFocus: false,
105 staleTime: 0,
106 })
107
108 const chartData = queryResult?.data || []
109 const dynamicAttributes = queryResult?.attributes || []
110
111 const showSumAsDefaultHighlight = report.showSumAsDefaultHighlight ?? true
112 const headerTotal = showSumAsDefaultHighlight
113 ? computePeriodTotal(chartData, dynamicAttributes)
114 : undefined
115
116 /**
117 * Depending on the source the timestamp key could be 'timestamp' or 'period_start'
118 */
119 const firstItem = chartData[0]
120 const timestampKey = firstItem?.hasOwnProperty('timestamp') ? 'timestamp' : 'period_start'
121
122 const { data: filledChartData } = useFillTimeseriesSorted({
123 data: chartData,
124 timestampKey,
125 valueKey: dynamicAttributes.map((attr) => attr.attribute),
126 defaultValue: 0,
127 startDate,
128 endDate,
129 minPointsToFill: undefined,
130 interval,
131 })
132
133 const [chartStyle, setChartStyle] = useState<string>(report.defaultChartStyle)
134 const chartHighlight = useChartHighlight()
135
136 if (!isAvailable && !isEntitlementLoading) {
137 return <ReportChartUpsell report={report} orgSlug={org?.slug ?? ''} />
138 }
139
140 const isErrorState = error && !isLoadingChart
141
142 if (report.hide) return null
143
144 return (
145 <Card id={report.id} className={cn('relative w-full overflow-hidden scroll-mt-16', className)}>
146 <CardContent
147 className={cn(
148 'flex flex-col gap-4 min-h-[280px] items-center justify-center',
149 isFetching && 'opacity-50'
150 )}
151 >
152 {isLoadingChart ? (
153 <Loader2 className="size-5 animate-spin text-foreground-light" />
154 ) : isErrorState ? (
155 <p className="text-sm text-foreground-light text-center h-full flex items-center justify-center">
156 Error loading chart data
157 </p>
158 ) : (
159 <div className="w-full relative">
160 <ComposedChart
161 chartId={report.id}
162 attributes={dynamicAttributes}
163 data={filledChartData}
164 format={report.format ?? undefined}
165 xAxisKey={report.xAxisKey ?? 'timestamp'}
166 yAxisKey={report.yAxisKey ?? dynamicAttributes[0]?.attribute}
167 hideHighlightedValue={report.hideHighlightedValue}
168 highlightedValue={headerTotal}
169 title={report.label}
170 customDateFormat={undefined}
171 chartStyle={chartStyle}
172 chartHighlight={chartHighlight}
173 showTooltip={report.showTooltip}
174 showLegend={report.showLegend}
175 showTotal={false}
176 showMaxValue={report.showMaxValue}
177 onChartStyleChange={setChartStyle}
178 updateDateRange={updateDateRange}
179 valuePrecision={report.valuePrecision}
180 hideChartType={report.hideChartType}
181 titleTooltip={report.titleTooltip}
182 syncId={syncId}
183 sql={queryResult?.query}
184 highlightActions={highlightActions}
185 showNewBadge={report.showNewBadge}
186 />
187 </div>
188 )}
189 </CardContent>
190 </Card>
191 )
192}