| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368 |
- import dayjs from 'dayjs'
- import { Activity, BarChartIcon, Loader2 } from 'lucide-react'
- import { useRouter } from 'next/router'
- import { ReactNode, useCallback, useEffect, useMemo, useState } from 'react'
- import { Bar, BarChart, CartesianGrid, Line, LineChart, XAxis, YAxis } from 'recharts'
- import { ChartContainer, ChartTooltip, ChartTooltipContent, WarningIcon } from 'ui'
- import { METRIC_THRESHOLDS } from './ReportBlock.constants'
- import { ReportBlockContainer } from './ReportBlockContainer'
- import { ChartConfig } from '@/components/interfaces/SQLEditor/UtilityPanel/ChartConfig'
- import { ButtonTooltip } from '@/components/ui/ButtonTooltip'
- import { timestampFormatter } from '@/components/ui/Charts/Charts.utils'
- import NoDataPlaceholder from '@/components/ui/Charts/NoDataPlaceholder'
- import {
- checkHasNonPositiveValues,
- computeYAxisWidth,
- formatLogTick,
- formatYAxisTick,
- } from '@/components/ui/QueryBlock/QueryBlock.utils'
- import { AnalyticsInterval } from '@/data/analytics/constants'
- import { mapMultiResponseToAnalyticsData } from '@/data/analytics/infra-monitoring-queries'
- import {
- InfraMonitoringAttribute,
- useInfraMonitoringAttributesQuery,
- } from '@/data/analytics/infra-monitoring-query'
- import {
- ProjectDailyStatsAttribute,
- useProjectDailyStatsQuery,
- } from '@/data/analytics/project-daily-stats-query'
- import { METRICS } from '@/lib/constants/metrics'
- import { useFormatDateTime } from '@/lib/datetime'
- import { useDatabaseSelectorStateSnapshot } from '@/state/database-selector'
- import type { Dashboards } from '@/types'
- interface ChartBlockProps {
- label: string
- attribute: string
- provider: 'infra-monitoring' | 'daily-stats'
- startDate: string
- endDate: string
- interval?: AnalyticsInterval
- defaultChartStyle?: 'bar' | 'line'
- defaultLogScale?: boolean
- isLoading?: boolean
- actions?: ReactNode
- maxHeight?: number
- onUpdateChartConfig?: ({
- chart,
- chartConfig,
- }: {
- chart?: Partial<Dashboards.Chart>
- chartConfig?: Partial<ChartConfig>
- }) => void
- }
- export const ChartBlock = ({
- label,
- attribute,
- provider,
- startDate,
- endDate,
- interval = '1d',
- defaultChartStyle = 'bar',
- defaultLogScale = false,
- isLoading = false,
- actions,
- maxHeight,
- onUpdateChartConfig,
- }: ChartBlockProps) => {
- const router = useRouter()
- const { ref } = router.query
- const state = useDatabaseSelectorStateSnapshot()
- const [chartStyle, setChartStyle] = useState<string>(defaultChartStyle)
- const logScale = useMemo(() => defaultLogScale, [defaultLogScale])
- const [latestValue, setLatestValue] = useState<string | undefined>()
- const formatChartDate = useFormatDateTime()
- const formatTooltipDate = useCallback(
- (value: string | number, format: string) =>
- /^\d{4}-\d{2}-\d{2}$/.test(String(value))
- ? timestampFormatter(String(value), format, true)
- : formatChartDate(value, format),
- [formatChartDate]
- )
- const databaseIdentifier = state.selectedDatabaseId
- const {
- data: dailyStatsData,
- isFetching: isFetchingDailyStats,
- isPending: isLoadingDailyStats,
- } = useProjectDailyStatsQuery(
- {
- projectRef: ref as string,
- attribute: attribute as ProjectDailyStatsAttribute,
- startDate: dayjs(startDate).format('YYYY-MM-DD'),
- endDate: dayjs(endDate).format('YYYY-MM-DD'),
- },
- { enabled: provider === 'daily-stats' }
- )
- const {
- data: infraMonitoringRawData,
- isFetching: isFetchingInfraMonitoring,
- isPending: isLoadingInfraMonitoring,
- } = useInfraMonitoringAttributesQuery(
- {
- projectRef: ref as string,
- attributes: [attribute as InfraMonitoringAttribute],
- startDate,
- endDate,
- interval: interval as AnalyticsInterval,
- databaseIdentifier,
- },
- { enabled: provider === 'infra-monitoring' }
- )
- const infraMonitoringData = useMemo(() => {
- if (!infraMonitoringRawData) return undefined
- const mapped = mapMultiResponseToAnalyticsData(infraMonitoringRawData, [
- attribute as InfraMonitoringAttribute,
- ])
- return mapped[attribute]
- }, [infraMonitoringRawData, attribute])
- const chartData =
- provider === 'infra-monitoring'
- ? infraMonitoringData
- : provider === 'daily-stats'
- ? dailyStatsData
- : undefined
- const isFetching =
- provider === 'infra-monitoring'
- ? isFetchingInfraMonitoring
- : provider === 'daily-stats'
- ? isFetchingDailyStats
- : false
- const loading =
- isLoading ||
- attribute.startsWith('new_snippet_') ||
- (provider === 'infra-monitoring'
- ? isLoadingInfraMonitoring
- : provider === 'daily-stats'
- ? isLoadingDailyStats
- : isLoading)
- const metric = METRICS.find((x) => x.key === attribute)
- const metricLabel = metric?.label ?? attribute
- const getCellColor = (attribute: string, value: number) => {
- const threshold = METRIC_THRESHOLDS[attribute as keyof typeof METRIC_THRESHOLDS]
- if (!threshold) return 'hsl(var(--chart-1))'
- if (threshold.check === 'gt') {
- return value >= threshold.danger
- ? 'hsl(var(--chart-destructive))'
- : value >= threshold.warning
- ? 'hsl(var(--chart-warning))'
- : 'hsl(var(--chart-1))'
- } else {
- return value <= threshold.danger
- ? 'hsl(var(--chart-destructive))'
- : value <= threshold.warning
- ? 'hsl(var(--chart-warning))'
- : 'hsl(var(--chart-1))'
- }
- }
- const isPercentage = chartData?.format === '%'
- const data = (chartData?.data ?? []).map((x: any) => {
- const value = isPercentage ? x[attribute] : x[attribute]
- const color = getCellColor(attribute, x[attribute])
- return {
- ...x,
- period_start: dayjs(x.period_start).utc().format('YYYY-MM-DD'),
- [attribute]: value,
- [metricLabel]: value,
- fill: color,
- stroke: color,
- }
- })
- const hasNonPositiveValues = useMemo(() => {
- if (!logScale || !data.length) return false
- return checkHasNonPositiveValues(data, metricLabel)
- }, [logScale, data, metricLabel])
- const effectiveLogScale = logScale && !hasNonPositiveValues
- const yAxisWidth = computeYAxisWidth(data, metricLabel, {
- isLogScale: effectiveLogScale,
- isPercentage,
- })
- const getInitialHighlightedValue = useCallback(() => {
- if (!chartData?.data?.length) return undefined
- const lastDataPoint = chartData.data[chartData.data.length - 1]
- const value = lastDataPoint[attribute]
- return isPercentage
- ? `${typeof value === 'number' ? value.toFixed(1) : value}%`
- : typeof value === 'number'
- ? value.toLocaleString()
- : value
- }, [chartData?.data, chartData?.format, attribute])
- useEffect(() => {
- if (defaultChartStyle) setChartStyle(defaultChartStyle)
- }, [defaultChartStyle])
- useEffect(() => {
- setLatestValue(getInitialHighlightedValue())
- }, [chartData, getInitialHighlightedValue])
- return (
- <ReportBlockContainer
- draggable
- showDragHandle
- loading={isFetching}
- icon={metric?.category?.icon('text-foreground-muted')}
- label={label}
- actions={
- <>
- <ButtonTooltip
- type="text"
- size="tiny"
- disabled={loading}
- className="w-7 h-7"
- icon={chartStyle === 'bar' ? <Activity /> : <BarChartIcon />}
- onClick={() => {
- const style = chartStyle === 'bar' ? 'line' : 'bar'
- if (onUpdateChartConfig) onUpdateChartConfig({ chart: { chart_type: style } })
- setChartStyle(style)
- }}
- tooltip={{
- content: {
- side: 'bottom',
- className: 'max-w-56 text-center',
- text: `View as ${chartStyle === 'bar' ? 'line chart' : 'bar chart'}`,
- },
- }}
- />
- <ButtonTooltip
- type={logScale ? 'default' : 'text'}
- size="tiny"
- disabled={loading}
- className="h-7 px-1.5 font-mono text-[10px]"
- icon={<span className="font-mono text-[10px] leading-none">Log</span>}
- onClick={() => {
- const next = !logScale
- if (onUpdateChartConfig) onUpdateChartConfig({ chartConfig: { logScale: next } })
- }}
- tooltip={{
- content: {
- side: 'bottom',
- className: 'max-w-56 text-center',
- text: `Switch to ${logScale ? 'linear' : 'logarithmic'} scale`,
- },
- }}
- />
- {actions}
- </>
- }
- >
- {loading ? (
- <div className="flex grow w-full flex-col items-center justify-center gap-y-2 px-4">
- <Loader2 size={18} className="animate-spin text-border-strong" />
- <p className="text-xs text-foreground-lighter text-center">Loading data for {label}</p>
- </div>
- ) : chartData === undefined ? (
- <div className="flex grow w-full flex-col items-center justify-center gap-y-2 px-4">
- <WarningIcon />
- <p className="text-xs text-foreground-lighter text-center">
- Unable to load data for {label}
- </p>
- </div>
- ) : data.length === 0 ? (
- <div className="flex grow w-full flex-col items-center justify-center gap-y-2">
- <NoDataPlaceholder
- size="small"
- className="border-0"
- description="It may take up to 24 hours for data to refresh"
- />
- </div>
- ) : (
- <>
- {latestValue && (
- <div className="pt-2 px-3 w-full text-left leading-tight">
- <span className="text-xs font-mono uppercase text-foreground-light">
- Most recently
- </span>
- <p className="text-lg text">{latestValue}</p>
- </div>
- )}
- {hasNonPositiveValues && (
- <p className="px-3 pt-1 text-xs text-foreground-light">
- Log scale is unavailable because the data contains zero or negative values.
- </p>
- )}
- <ChartContainer
- className="w-full aspect-auto px-3 py-2"
- style={{
- height: maxHeight ? `${maxHeight}px` : undefined,
- minHeight: maxHeight ? `${maxHeight}px` : undefined,
- }}
- >
- {chartStyle === 'bar' ? (
- <BarChart accessibilityLayer margin={{ left: 0, right: 0 }} data={data}>
- <CartesianGrid vertical={false} />
- <XAxis
- dataKey="period_start"
- tickLine={false}
- axisLine={false}
- tickMargin={8}
- minTickGap={32}
- />
- <YAxis
- scale={effectiveLogScale ? 'log' : 'auto'}
- domain={effectiveLogScale ? [1, 'auto'] : isPercentage ? [0, 100] : undefined}
- allowDataOverflow={effectiveLogScale}
- width={yAxisWidth}
- tickFormatter={effectiveLogScale ? formatLogTick : formatYAxisTick}
- />
- <ChartTooltip
- content={
- <ChartTooltipContent
- className="min-w-[200px]"
- labelSuffix={isPercentage ? '%' : ''}
- labelFormatter={(x) => formatTooltipDate(x as string | number, 'DD MMM YYYY')}
- />
- }
- />
- <Bar dataKey={metricLabel} radius={[2, 2, 1, 1]} />
- </BarChart>
- ) : (
- <LineChart accessibilityLayer margin={{ left: 0, right: 0 }} data={data}>
- <CartesianGrid vertical={false} />
- <XAxis
- dataKey="period_start"
- tickLine={false}
- axisLine={false}
- tickMargin={8}
- minTickGap={32}
- />
- <YAxis
- scale={effectiveLogScale ? 'log' : 'auto'}
- domain={effectiveLogScale ? [1, 'auto'] : isPercentage ? [0, 100] : undefined}
- allowDataOverflow={effectiveLogScale}
- width={yAxisWidth}
- tickFormatter={effectiveLogScale ? formatLogTick : formatYAxisTick}
- />
- <ChartTooltip
- content={
- <ChartTooltipContent
- labelSuffix={chartData?.format === '%' ? '%' : ''}
- labelFormatter={(x) => formatTooltipDate(x as string | number, 'DD MMM YYYY')}
- />
- }
- />
- <Line dataKey={metricLabel} stroke="hsl(var(--chart-1))" radius={4} />
- </LineChart>
- )}
- </ChartContainer>
- </>
- )}
- </ReportBlockContainer>
- )
- }
|