ReportChartV2.tsx 6.2 KB

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