|
1 | 1 | import { useMemo } from 'react'; |
2 | | -import { |
3 | | - Box, |
4 | | - Text, |
5 | | - VStack, |
6 | | - SimpleGrid, |
7 | | - Stat, |
8 | | - HStack, |
9 | | - Badge, |
10 | | - Icon, |
11 | | - Skeleton, |
12 | | -} from '@chakra-ui/react'; |
| 2 | +import { Box, Text, VStack, Skeleton } from '@chakra-ui/react'; |
13 | 3 | import type { RollupBucket, DailyBucket, RawCheck } from '@/api/types'; |
14 | 4 | import type { BucketType } from '@/types/bucket'; |
15 | | -import { Database } from 'lucide-react'; |
16 | 5 |
|
17 | 6 | export interface AvailabilityChartProps { |
18 | 7 | data: |
@@ -167,8 +156,8 @@ export function AvailabilityChart({ |
167 | 156 | }; |
168 | 157 |
|
169 | 158 | return ( |
170 | | - <Box height={height} position='relative'> |
171 | | - <svg width='100%' height='100%' viewBox={`0 0 800 ${height}`}> |
| 159 | + <Box height={'100%'} w='100%' position='relative'> |
| 160 | + <svg width='100%' height='100%'> |
172 | 161 | <g transform={`translate(${margin.left}, ${margin.top})`}> |
173 | 162 | {/* Grid lines */} |
174 | 163 | <defs> |
@@ -264,170 +253,3 @@ export function AvailabilityChart({ |
264 | 253 | </Box> |
265 | 254 | ); |
266 | 255 | } |
267 | | - |
268 | | -export function AvailabilityStats({ |
269 | | - data, |
270 | | - bucket, |
271 | | - isLoading, |
272 | | -}: { |
273 | | - data: AvailabilityChartProps['data'] | null | undefined; |
274 | | - bucket: BucketType; |
275 | | - isLoading?: boolean; |
276 | | -}) { |
277 | | - const stats = useMemo(() => { |
278 | | - if (!data) return null; |
279 | | - |
280 | | - let totalPoints = 0; |
281 | | - let upPoints = 0; |
282 | | - let totalResponseTime = 0; |
283 | | - let responseTimeCount = 0; |
284 | | - let totalDownEvents = 0; |
285 | | - |
286 | | - switch (bucket) { |
287 | | - case 'raw': { |
288 | | - totalPoints = data.raw.length; |
289 | | - upPoints = data.raw.filter(check => check.status === 'up').length; |
290 | | - const validRttChecks = data.raw.filter(check => check.rttMs != null); |
291 | | - totalResponseTime = validRttChecks.reduce((sum, check) => sum + (check.rttMs || 0), 0); |
292 | | - responseTimeCount = validRttChecks.length; |
293 | | - break; |
294 | | - } |
295 | | - |
296 | | - case '15m': { |
297 | | - totalPoints = data.rollup15m.length; |
298 | | - const totalUptime = data.rollup15m.reduce((sum, bucket) => sum + bucket.upPct, 0); |
299 | | - upPoints = totalUptime / 100; // Convert percentage to equivalent "up points" |
300 | | - totalDownEvents = data.rollup15m.reduce((sum, bucket) => sum + bucket.downEvents, 0); |
301 | | - const validRttRollups = data.rollup15m.filter(bucket => bucket.avgRttMs != null); |
302 | | - totalResponseTime = validRttRollups.reduce( |
303 | | - (sum, bucket) => sum + (bucket.avgRttMs || 0), |
304 | | - 0 |
305 | | - ); |
306 | | - responseTimeCount = validRttRollups.length; |
307 | | - break; |
308 | | - } |
309 | | - |
310 | | - case 'daily': { |
311 | | - totalPoints = data.rollupDaily.length; |
312 | | - const totalDailyUptime = data.rollupDaily.reduce((sum, bucket) => sum + bucket.upPct, 0); |
313 | | - upPoints = totalDailyUptime / 100; // Convert percentage to equivalent "up points" |
314 | | - totalDownEvents = data.rollupDaily.reduce((sum, bucket) => sum + bucket.downEvents, 0); |
315 | | - const validDailyRollups = data.rollupDaily.filter(bucket => bucket.avgRttMs != null); |
316 | | - totalResponseTime = validDailyRollups.reduce( |
317 | | - (sum, bucket) => sum + (bucket.avgRttMs || 0), |
318 | | - 0 |
319 | | - ); |
320 | | - responseTimeCount = validDailyRollups.length; |
321 | | - break; |
322 | | - } |
323 | | - } |
324 | | - |
325 | | - const availabilityPct = totalPoints > 0 ? (upPoints / totalPoints) * 100 : 0; |
326 | | - const avgResponseTime = responseTimeCount > 0 ? totalResponseTime / responseTimeCount : null; |
327 | | - const downEventPct = totalPoints > 0 ? (totalDownEvents / totalPoints) * 100 : 0; |
328 | | - // const downEventPct = |
329 | | - // totalPoints > 0 |
330 | | - // ? totalDownEvents / totalPoints // average down events per bucket |
331 | | - // : 0; |
332 | | - |
333 | | - return { |
334 | | - availabilityPct, |
335 | | - avgResponseTime, |
336 | | - totalDownEvents, |
337 | | - downEventPct, |
338 | | - totalPoints, |
339 | | - upPoints, |
340 | | - }; |
341 | | - }, [data, bucket]); |
342 | | - |
343 | | - if (isLoading) { |
344 | | - return ( |
345 | | - <SimpleGrid columns={{ base: 1, sm: 2, md: 5 }} gap={2}> |
346 | | - {Array.from({ length: 5 }).map((_, i) => ( |
347 | | - <Stat.Root key={i} p={3} borderWidth='1px' rounded='md' _dark={{ bg: 'gray.800' }}> |
348 | | - <Skeleton height='20px' mb={2} /> |
349 | | - <Skeleton height='28px' mb={1} /> |
350 | | - <Skeleton height='16px' /> |
351 | | - </Stat.Root> |
352 | | - ))} |
353 | | - </SimpleGrid> |
354 | | - ); |
355 | | - } |
356 | | - |
357 | | - if (!stats) return null; |
358 | | - |
359 | | - return ( |
360 | | - <SimpleGrid columns={{ base: 1, sm: 2, md: 5 }} gap={2}> |
361 | | - {/* Data Points */} |
362 | | - <Stat.Root p={3} borderWidth='1px' rounded='md' _dark={{ bg: 'gray.800' }}> |
363 | | - <HStack justify='space-between'> |
364 | | - <Stat.Label>Data Points</Stat.Label> |
365 | | - <Icon as={Database} color='fg.muted' boxSize={4} /> |
366 | | - </HStack> |
367 | | - <Stat.ValueText>{stats.totalPoints}</Stat.ValueText> |
368 | | - <Stat.HelpText>Checks included</Stat.HelpText> |
369 | | - </Stat.Root> |
370 | | - {/* Availability % */} |
371 | | - <Stat.Root p={3} borderWidth='1px' rounded='md' _dark={{ bg: 'gray.800' }}> |
372 | | - <Stat.Label>Availability</Stat.Label> |
373 | | - <HStack> |
374 | | - <Stat.ValueText |
375 | | - color={ |
376 | | - stats.availabilityPct >= 99 |
377 | | - ? 'green.600' |
378 | | - : stats.availabilityPct >= 95 |
379 | | - ? 'yellow.600' |
380 | | - : 'red.600' |
381 | | - } |
382 | | - > |
383 | | - {stats.availabilityPct.toFixed(2)}% |
384 | | - </Stat.ValueText> |
385 | | - {stats.availabilityPct >= 99 ? ( |
386 | | - <Stat.UpIndicator color='green.500' /> |
387 | | - ) : ( |
388 | | - <Stat.DownIndicator color={stats.availabilityPct >= 95 ? 'yellow.500' : 'red.500'} /> |
389 | | - )} |
390 | | - </HStack> |
391 | | - <Stat.HelpText>Based on {stats.totalPoints} checks</Stat.HelpText> |
392 | | - </Stat.Root> |
393 | | - {/* Uptime Events */} |
394 | | - <Stat.Root p={3} borderWidth='1px' rounded='md' _dark={{ bg: 'gray.800' }}> |
395 | | - <Stat.Label>Uptime Events</Stat.Label> |
396 | | - <HStack> |
397 | | - <Stat.ValueText color='green.600'>{Math.round(stats.upPoints)}</Stat.ValueText> |
398 | | - <Badge colorPalette='green' gap='0'> |
399 | | - <Stat.UpIndicator /> |
400 | | - {stats.availabilityPct.toFixed(2)}% |
401 | | - </Badge> |
402 | | - </HStack> |
403 | | - <Stat.HelpText>Successful checks</Stat.HelpText> |
404 | | - </Stat.Root> |
405 | | - {/* Down Events */} |
406 | | - {bucket !== 'raw' && ( |
407 | | - <Stat.Root p={2} borderWidth='1px' rounded='md' _dark={{ bg: 'gray.800' }}> |
408 | | - <Stat.Label>Down Events</Stat.Label> |
409 | | - <HStack> |
410 | | - <Stat.ValueText color={stats.totalDownEvents > 0 ? 'red.600' : 'green.600'}> |
411 | | - {stats.totalDownEvents} |
412 | | - </Stat.ValueText> |
413 | | - <Badge colorPalette={stats.totalDownEvents > 0 ? 'red' : 'green'} gap='0'> |
414 | | - <Stat.DownIndicator color={stats.totalDownEvents > 0 ? 'red' : 'green'} /> |
415 | | - {stats.downEventPct.toFixed(2)}% |
416 | | - </Badge> |
417 | | - </HStack> |
418 | | - <Stat.HelpText>Recorded in this range</Stat.HelpText> |
419 | | - </Stat.Root> |
420 | | - )} |
421 | | - {/* Response Time */} |
422 | | - {stats.avgResponseTime && ( |
423 | | - <Stat.Root p={2} borderWidth='1px' rounded='md' _dark={{ bg: 'gray.800' }}> |
424 | | - <Stat.Label>Avg Response</Stat.Label> |
425 | | - <Stat.ValueText alignItems='baseline'> |
426 | | - {stats.avgResponseTime.toFixed(2)} <Stat.ValueUnit>ms</Stat.ValueUnit> |
427 | | - </Stat.ValueText> |
428 | | - <Stat.HelpText>Across successful checks</Stat.HelpText> |
429 | | - </Stat.Root> |
430 | | - )} |
431 | | - </SimpleGrid> |
432 | | - ); |
433 | | -} |
0 commit comments