index.tsx182 lines · main
1'use client'
2
3import dayjs from 'dayjs'
4import { ReactNode, useState } from 'react'
5import { Bar, Cell, BarChart as RechartBarChart, XAxis, YAxis } from 'recharts'
6import type { CategoricalChartState } from 'recharts/types/chart/types'
7import { ChartConfig, ChartContainer, ChartTooltip, ChartTooltipContent, cn } from 'ui'
8
9const CHART_COLORS = {
10 TICK: 'hsl(var(--background-overlay-hover))',
11 AXIS: 'hsl(var(--background-overlay-hover))',
12 GREEN_1: 'hsl(var(--brand-default))',
13 GREEN_2: 'hsl(var(--brand-500))',
14 RED_1: 'hsl(var(--destructive-default))',
15 RED_2: 'hsl(var(--destructive-500))',
16 YELLOW_1: 'hsl(var(--warning-default))',
17 YELLOW_2: 'hsl(var(--warning-500))',
18}
19
20type LogsBarChartDatum = {
21 timestamp: string
22 error_count: number
23 ok_count: number
24 warning_count: number
25}
26
27export const LogsBarChart = ({
28 data,
29 onBarClick,
30 EmptyState,
31 DateTimeFormat = 'MMM D, YYYY, hh:mma',
32 isFullHeight = false,
33 chartConfig,
34 hideZeroValues = false,
35}: {
36 data: LogsBarChartDatum[]
37 onBarClick?: (datum: LogsBarChartDatum, tooltipData?: CategoricalChartState) => void
38 EmptyState?: ReactNode
39 DateTimeFormat?: string
40 isFullHeight?: boolean
41 chartConfig?: ChartConfig
42 hideZeroValues?: boolean
43}) => {
44 const [focusDataIndex, setFocusDataIndex] = useState<number | null>(null)
45
46 if (data.length === 0) {
47 if (EmptyState) return EmptyState
48 return null
49 }
50
51 const startDate = dayjs(data[0]['timestamp']).format(DateTimeFormat)
52 const endDate = dayjs(data[data?.length - 1]?.['timestamp']).format(DateTimeFormat)
53
54 const defaultChartConfig = {
55 error_count: {
56 label: 'Errors',
57 },
58 ok_count: {
59 label: 'Ok',
60 },
61 warning_count: {
62 label: 'Warnings',
63 },
64 } satisfies ChartConfig
65
66 return (
67 <div
68 data-testid="logs-bar-chart"
69 className={cn('flex flex-col gap-y-3', isFullHeight ? 'h-full' : 'h-24')}
70 >
71 <ChartContainer className="h-full" config={chartConfig ?? defaultChartConfig}>
72 <RechartBarChart
73 data={data}
74 onMouseMove={(e: any) => {
75 if (e.activeTooltipIndex !== focusDataIndex) {
76 setFocusDataIndex(e.activeTooltipIndex)
77 }
78 }}
79 onMouseLeave={() => setFocusDataIndex(null)}
80 onClick={(tooltipData) => {
81 const datum = tooltipData?.activePayload?.[0]?.payload
82 if (onBarClick) onBarClick(datum, tooltipData)
83 }}
84 >
85 <YAxis
86 tick={false}
87 width={0}
88 axisLine={{ stroke: CHART_COLORS.AXIS }}
89 tickLine={{ stroke: CHART_COLORS.AXIS }}
90 />
91 <XAxis
92 dataKey="timestamp"
93 interval={data.length - 2}
94 tick={false}
95 axisLine={{ stroke: CHART_COLORS.AXIS }}
96 tickLine={{ stroke: CHART_COLORS.AXIS }}
97 />
98 <ChartTooltip
99 animationDuration={0}
100 position={{ y: 16 }}
101 content={(props) => {
102 if (!props.active || !props.payload || props.payload.length === 0) {
103 return null
104 }
105
106 // Filter payload based on hideZeroValues
107 const filteredPayload = hideZeroValues
108 ? props.payload.filter((item) => Number(item.value) !== 0)
109 : props.payload
110
111 // Don't show tooltip if all values are filtered out
112 if (filteredPayload.length === 0) {
113 return null
114 }
115
116 return (
117 <ChartTooltipContent
118 active={props.active}
119 payload={filteredPayload}
120 label={props.label}
121 className="text-foreground-light -mt-5 transition-none!"
122 labelFormatter={(v: string) => dayjs(v).format(DateTimeFormat)}
123 />
124 )
125 }}
126 />
127
128 {/* Error bars */}
129 <Bar dataKey="error_count" fill={CHART_COLORS.RED_1} maxBarSize={24} stackId="stack">
130 {data?.map((_entry: LogsBarChartDatum, index: number) => (
131 <Cell
132 className="cursor-pointer transition-colors"
133 key={`error-${index}`}
134 fill={
135 focusDataIndex === index || focusDataIndex === null
136 ? CHART_COLORS.RED_1
137 : CHART_COLORS.RED_2
138 }
139 />
140 ))}
141 </Bar>
142
143 {/* Warning bars */}
144 <Bar dataKey="warning_count" fill={CHART_COLORS.YELLOW_1} maxBarSize={24} stackId="stack">
145 {data?.map((_entry: LogsBarChartDatum, index: number) => (
146 <Cell
147 className="cursor-pointer transition-colors"
148 key={`warning-${index}`}
149 fill={
150 focusDataIndex === index || focusDataIndex === null
151 ? CHART_COLORS.YELLOW_1
152 : CHART_COLORS.YELLOW_2
153 }
154 />
155 ))}
156 </Bar>
157
158 {/* Success bars */}
159 <Bar dataKey="ok_count" fill={CHART_COLORS.GREEN_1} maxBarSize={24} stackId="stack">
160 {data?.map((_entry: LogsBarChartDatum, index: number) => (
161 <Cell
162 className="cursor-pointer transition-colors"
163 key={`success-${index}`}
164 fill={
165 focusDataIndex === index || focusDataIndex === null
166 ? CHART_COLORS.GREEN_1
167 : CHART_COLORS.GREEN_2
168 }
169 />
170 ))}
171 </Bar>
172 </RechartBarChart>
173 </ChartContainer>
174 {data && (
175 <div className="text-foreground-lighter -mt-10 flex items-center justify-between text-[10px] font-mono">
176 <span>{startDate}</span>
177 <span>{endDate}</span>
178 </div>
179 )}
180 </div>
181 )
182}