Skip to content

Commit ecc2d6d

Browse files
committed
moved the availability into seprate file
1 parent 5a27778 commit ecc2d6d

File tree

3 files changed

+177
-182
lines changed

3 files changed

+177
-182
lines changed

thingconnect.pulse.client/src/components/AvailabilityChart.tsx

Lines changed: 3 additions & 181 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,7 @@
11
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';
133
import type { RollupBucket, DailyBucket, RawCheck } from '@/api/types';
144
import type { BucketType } from '@/types/bucket';
15-
import { Database } from 'lucide-react';
165

176
export interface AvailabilityChartProps {
187
data:
@@ -167,8 +156,8 @@ export function AvailabilityChart({
167156
};
168157

169158
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%'>
172161
<g transform={`translate(${margin.left}, ${margin.top})`}>
173162
{/* Grid lines */}
174163
<defs>
@@ -264,170 +253,3 @@ export function AvailabilityChart({
264253
</Box>
265254
);
266255
}
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-
}
Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
import { useMemo } from 'react';
2+
import { SimpleGrid, Stat, HStack, Badge, Icon, Skeleton } from '@chakra-ui/react';
3+
import type { BucketType } from '@/types/bucket';
4+
import { Database } from 'lucide-react';
5+
import type { AvailabilityChartProps } from './AvailabilityChart';
6+
7+
export function AvailabilityStats({
8+
data,
9+
bucket,
10+
isLoading,
11+
}: {
12+
data: AvailabilityChartProps['data'] | null | undefined;
13+
bucket: BucketType;
14+
isLoading?: boolean;
15+
}) {
16+
const stats = useMemo(() => {
17+
if (!data) return null;
18+
19+
let totalPoints = 0;
20+
let upPoints = 0;
21+
let totalResponseTime = 0;
22+
let responseTimeCount = 0;
23+
let totalDownEvents = 0;
24+
25+
switch (bucket) {
26+
case 'raw': {
27+
totalPoints = data.raw.length;
28+
upPoints = data.raw.filter(check => check.status === 'up').length;
29+
const validRttChecks = data.raw.filter(check => check.rttMs != null);
30+
totalResponseTime = validRttChecks.reduce((sum, check) => sum + (check.rttMs || 0), 0);
31+
responseTimeCount = validRttChecks.length;
32+
break;
33+
}
34+
35+
case '15m': {
36+
totalPoints = data.rollup15m.length;
37+
const totalUptime = data.rollup15m.reduce((sum, bucket) => sum + bucket.upPct, 0);
38+
upPoints = totalUptime / 100; // Convert percentage to equivalent "up points"
39+
totalDownEvents = data.rollup15m.reduce((sum, bucket) => sum + bucket.downEvents, 0);
40+
const validRttRollups = data.rollup15m.filter(bucket => bucket.avgRttMs != null);
41+
totalResponseTime = validRttRollups.reduce(
42+
(sum, bucket) => sum + (bucket.avgRttMs || 0),
43+
0
44+
);
45+
responseTimeCount = validRttRollups.length;
46+
break;
47+
}
48+
49+
case 'daily': {
50+
totalPoints = data.rollupDaily.length;
51+
const totalDailyUptime = data.rollupDaily.reduce((sum, bucket) => sum + bucket.upPct, 0);
52+
upPoints = totalDailyUptime / 100; // Convert percentage to equivalent "up points"
53+
totalDownEvents = data.rollupDaily.reduce((sum, bucket) => sum + bucket.downEvents, 0);
54+
const validDailyRollups = data.rollupDaily.filter(bucket => bucket.avgRttMs != null);
55+
totalResponseTime = validDailyRollups.reduce(
56+
(sum, bucket) => sum + (bucket.avgRttMs || 0),
57+
0
58+
);
59+
responseTimeCount = validDailyRollups.length;
60+
break;
61+
}
62+
}
63+
64+
const availabilityPct = totalPoints > 0 ? (upPoints / totalPoints) * 100 : 0;
65+
const avgResponseTime = responseTimeCount > 0 ? totalResponseTime / responseTimeCount : null;
66+
const downEventPct = totalPoints > 0 ? (totalDownEvents / totalPoints) * 100 : 0;
67+
// const downEventPct =
68+
// totalPoints > 0
69+
// ? totalDownEvents / totalPoints // average down events per bucket
70+
// : 0;
71+
72+
return {
73+
availabilityPct,
74+
avgResponseTime,
75+
totalDownEvents,
76+
downEventPct,
77+
totalPoints,
78+
upPoints,
79+
};
80+
}, [data, bucket]);
81+
82+
if (isLoading) {
83+
return (
84+
<SimpleGrid columns={{ base: 1, sm: 2, md: 5 }} gap={2}>
85+
{Array.from({ length: 5 }).map((_, i) => (
86+
<Stat.Root key={i} p={3} borderWidth='1px' rounded='md' _dark={{ bg: 'gray.800' }}>
87+
<Skeleton height='20px' mb={2} />
88+
<Skeleton height='28px' mb={1} />
89+
<Skeleton height='16px' />
90+
</Stat.Root>
91+
))}
92+
</SimpleGrid>
93+
);
94+
}
95+
96+
if (!stats) return null;
97+
98+
return (
99+
<SimpleGrid columns={{ base: 1, sm: 2, md: 5 }} gap={2}>
100+
{/* Data Points */}
101+
<Stat.Root p={3} borderWidth='1px' rounded='md' _dark={{ bg: 'gray.800' }}>
102+
<HStack justify='space-between'>
103+
<Stat.Label>Data Points</Stat.Label>
104+
<Icon as={Database} color='fg.muted' boxSize={4} />
105+
</HStack>
106+
<Stat.ValueText>{stats.totalPoints}</Stat.ValueText>
107+
<Stat.HelpText>Checks included</Stat.HelpText>
108+
</Stat.Root>
109+
{/* Availability % */}
110+
<Stat.Root p={3} borderWidth='1px' rounded='md' _dark={{ bg: 'gray.800' }}>
111+
<Stat.Label>Availability</Stat.Label>
112+
<HStack>
113+
<Stat.ValueText
114+
color={
115+
stats.availabilityPct >= 99
116+
? 'green.600'
117+
: stats.availabilityPct >= 95
118+
? 'yellow.600'
119+
: 'red.600'
120+
}
121+
>
122+
{stats.availabilityPct.toFixed(2)}%
123+
</Stat.ValueText>
124+
{stats.availabilityPct >= 99 ? (
125+
<Stat.UpIndicator color='green.500' />
126+
) : (
127+
<Stat.DownIndicator color={stats.availabilityPct >= 95 ? 'yellow.500' : 'red.500'} />
128+
)}
129+
</HStack>
130+
<Stat.HelpText>Based on {stats.totalPoints} checks</Stat.HelpText>
131+
</Stat.Root>
132+
{/* Uptime Events */}
133+
<Stat.Root p={3} borderWidth='1px' rounded='md' _dark={{ bg: 'gray.800' }}>
134+
<Stat.Label>Uptime Events</Stat.Label>
135+
<HStack>
136+
<Stat.ValueText color='green.600'>{Math.round(stats.upPoints)}</Stat.ValueText>
137+
<Badge colorPalette='green' gap='0'>
138+
<Stat.UpIndicator />
139+
{stats.availabilityPct.toFixed(2)}%
140+
</Badge>
141+
</HStack>
142+
<Stat.HelpText>Successful checks</Stat.HelpText>
143+
</Stat.Root>
144+
{/* Down Events */}
145+
{bucket !== 'raw' && (
146+
<Stat.Root p={2} borderWidth='1px' rounded='md' _dark={{ bg: 'gray.800' }}>
147+
<Stat.Label>Down Events</Stat.Label>
148+
<HStack>
149+
<Stat.ValueText color={stats.totalDownEvents > 0 ? 'red.600' : 'green.600'}>
150+
{stats.totalDownEvents}
151+
</Stat.ValueText>
152+
<Badge colorPalette={stats.totalDownEvents > 0 ? 'red' : 'green'} gap='0'>
153+
<Stat.DownIndicator color={stats.totalDownEvents > 0 ? 'red' : 'green'} />
154+
{stats.downEventPct.toFixed(2)}%
155+
</Badge>
156+
</HStack>
157+
<Stat.HelpText>Recorded in this range</Stat.HelpText>
158+
</Stat.Root>
159+
)}
160+
{/* Response Time */}
161+
{stats.avgResponseTime && (
162+
<Stat.Root p={2} borderWidth='1px' rounded='md' _dark={{ bg: 'gray.800' }}>
163+
<Stat.Label>Avg Response</Stat.Label>
164+
<Stat.ValueText alignItems='baseline'>
165+
{stats.avgResponseTime.toFixed(2)} <Stat.ValueUnit>ms</Stat.ValueUnit>
166+
</Stat.ValueText>
167+
<Stat.HelpText>Across successful checks</Stat.HelpText>
168+
</Stat.Root>
169+
)}
170+
</SimpleGrid>
171+
);
172+
}

thingconnect.pulse.client/src/pages/History.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,12 @@ import { DateRangePicker } from '@/components/DateRangePicker';
2626
import type { DateRange } from '@/components/DateRangePicker';
2727
import { BucketSelector } from '@/components/BucketSelector';
2828
import type { BucketType } from '@/types/bucket';
29-
import { AvailabilityChart, AvailabilityStats } from '@/components/AvailabilityChart';
29+
import { AvailabilityChart } from '@/components/AvailabilityChart';
3030
import { HistoryTable } from '@/components/HistoryTable';
3131
import { HistoryService } from '@/api/services/history.service';
3232
import { StatusService } from '@/api/services/status.service';
3333
import { Tooltip } from '@/components/ui/tooltip';
34+
import { AvailabilityStats } from '@/components/AvailabilityStats';
3435

3536
export default function History() {
3637
// State for filters

0 commit comments

Comments
 (0)