Skip to content

Commit 29461df

Browse files
committed
added pagination to RecentChecksTable and move code to a seprate component
1 parent e169d5a commit 29461df

File tree

5 files changed

+284
-254
lines changed

5 files changed

+284
-254
lines changed
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import { Text, VStack, Badge, HStack, Card } from '@chakra-ui/react';
2+
import { CloudOff } from 'lucide-react';
3+
import { formatDistanceToNow } from 'date-fns';
4+
import type { Outage } from '@/api/types';
5+
6+
interface OutagesListProps {
7+
outages: Outage[];
8+
}
9+
10+
function formatDuration(seconds?: number | null): string {
11+
if (!seconds) return 'Unknown';
12+
13+
if (seconds < 60) return `${seconds}s`;
14+
if (seconds < 3600) return `${Math.round(seconds / 60)}m`;
15+
16+
const hours = Math.floor(seconds / 3600);
17+
const minutes = Math.round((seconds % 3600) / 60);
18+
return minutes > 0 ? `${hours}h ${minutes}m` : `${hours}h`;
19+
}
20+
21+
export function OutagesList({ outages }: OutagesListProps) {
22+
if (outages.length === 0) {
23+
return (
24+
<VStack color='gray.300' textAlign='center' gap={1} py={5}>
25+
<CloudOff size={'40px'} />
26+
<Text textAlign='center' color='gray.500'>
27+
No recent outages
28+
</Text>
29+
</VStack>
30+
);
31+
}
32+
33+
return (
34+
<VStack gap={4} align='stretch'>
35+
{outages.slice(0, 5).map((outage, index) => (
36+
<Card.Root key={`${outage.startedTs}-${index}`} variant='outline'>
37+
<Card.Body>
38+
<VStack gap={2} align='stretch'>
39+
<HStack justify='space-between'>
40+
<Text fontSize='sm' fontWeight='medium'>
41+
{formatDistanceToNow(new Date(outage.startedTs), { addSuffix: true })}
42+
</Text>
43+
<Badge colorPalette={outage.endedTs ? 'gray' : 'red'} size='sm'>
44+
{outage.endedTs ? 'Resolved' : 'Ongoing'}
45+
</Badge>
46+
</HStack>
47+
<HStack justify='space-between'>
48+
<Text fontSize='sm' color='gray.600'>
49+
Duration: {formatDuration(outage.durationS)}
50+
</Text>
51+
{outage.endedTs && (
52+
<Text fontSize='sm' color='gray.600'>
53+
Ended: {formatDistanceToNow(new Date(outage.endedTs), { addSuffix: true })}
54+
</Text>
55+
)}
56+
</HStack>
57+
{outage.lastError && (
58+
<Text fontSize='sm' color='red.600' _dark={{ color: 'red.400' }} lineClamp={2}>
59+
{outage.lastError}
60+
</Text>
61+
)}
62+
</VStack>
63+
</Card.Body>
64+
</Card.Root>
65+
))}
66+
{outages.length > 5 && (
67+
<Text fontSize='sm' color='gray.500' textAlign='center'>
68+
Showing 5 of {outages.length} recent outages
69+
</Text>
70+
)}
71+
</VStack>
72+
);
73+
}
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
import {
2+
Box,
3+
Text,
4+
VStack,
5+
Badge,
6+
Table,
7+
Pagination,
8+
ButtonGroup,
9+
IconButton,
10+
} from '@chakra-ui/react';
11+
import { ChevronLeft, ChevronRight, CloudOff } from 'lucide-react';
12+
import { formatDistanceToNow } from 'date-fns';
13+
import type { RawCheck } from '@/api/types';
14+
import { useMemo, useState } from 'react';
15+
interface RecentChecksTableProps {
16+
checks: RawCheck[];
17+
pageSize?: number;
18+
}
19+
20+
export function RecentChecksTable({ checks, pageSize = 10 }: RecentChecksTableProps) {
21+
const [currentPage, setCurrentPage] = useState(1);
22+
const pageCount = Math.ceil(checks.length / pageSize);
23+
const pagedChecks = useMemo(() => {
24+
const start = (currentPage - 1) * pageSize;
25+
return checks.slice(start, start + pageSize);
26+
}, [checks, currentPage, pageSize]);
27+
28+
if (checks.length === 0) {
29+
return (
30+
<VStack color='gray.300' textAlign='center' gap={1} py={5}>
31+
<CloudOff size={'40px'} />
32+
<Text textAlign='center' color='gray.500'>
33+
No recent checks available
34+
</Text>
35+
</VStack>
36+
);
37+
}
38+
39+
return (
40+
<VStack gap={2} align='stretch'>
41+
<Table.ScrollArea borderWidth='1px' rounded='md' flex={1} minH={0} overflow='auto'>
42+
<Table.Root size='sm' stickyHeader>
43+
<Table.Header>
44+
<Table.Row bg='gray.100' _dark={{ bg: 'gray.800' }}>
45+
<Table.ColumnHeader w='30%'>Time</Table.ColumnHeader>
46+
<Table.ColumnHeader w='20%'>Status</Table.ColumnHeader>
47+
<Table.ColumnHeader w='25%'>RTT</Table.ColumnHeader>
48+
<Table.ColumnHeader w='25%'>Error</Table.ColumnHeader>
49+
</Table.Row>
50+
</Table.Header>
51+
<Table.Body>
52+
{pagedChecks.map((check, index) => (
53+
<Table.Row key={`${check.ts}-${index}`}>
54+
<Table.Cell w='30%'>
55+
<Text flex='1' fontSize='sm'>
56+
{formatDistanceToNow(new Date(check.ts), { addSuffix: true })}
57+
</Text>
58+
</Table.Cell>
59+
<Table.Cell w='20%'>
60+
<Badge colorPalette={check.status === 'up' ? 'green' : 'red'} size='sm'>
61+
{check.status.toUpperCase()}
62+
</Badge>
63+
</Table.Cell>
64+
<Table.Cell w='25%'>
65+
<Text fontSize='sm'>{check.rttMs ? `${check.rttMs}ms` : '-'}</Text>
66+
</Table.Cell>
67+
<Table.Cell w='25%'>
68+
<Text flex='1' fontSize='sm' color='gray.500' lineClamp={1}>
69+
{check.error || '-'}
70+
</Text>
71+
</Table.Cell>
72+
</Table.Row>
73+
))}
74+
</Table.Body>
75+
</Table.Root>
76+
</Table.ScrollArea>
77+
78+
{/* Pagination */}
79+
{pageCount > 1 && (
80+
<Box flexShrink={0}>
81+
<Pagination.Root
82+
count={checks.length}
83+
pageSize={pageSize}
84+
page={currentPage}
85+
onPageChange={details => setCurrentPage(details.page)}
86+
>
87+
<ButtonGroup variant='ghost' size='sm' w='full' justifyContent='center'>
88+
<Text fontSize='sm' color='gray.600' _dark={{ color: 'gray.400' }} flex='1'>
89+
{`Showing ${(currentPage - 1) * pageSize + 1} - ${Math.min(
90+
currentPage * pageSize,
91+
checks.length
92+
)} of ${checks.length} entries`}
93+
</Text>
94+
<Pagination.PrevTrigger asChild>
95+
<IconButton aria-label='Previous page'>
96+
<ChevronLeft />
97+
</IconButton>
98+
</Pagination.PrevTrigger>
99+
<Pagination.Items
100+
render={page => (
101+
<IconButton
102+
key={page.value}
103+
variant={page.value === currentPage ? 'outline' : 'ghost'}
104+
size='sm'
105+
>
106+
{page.value}
107+
</IconButton>
108+
)}
109+
/>
110+
<Pagination.NextTrigger asChild>
111+
<IconButton aria-label='Next page'>
112+
<ChevronRight />
113+
</IconButton>
114+
</Pagination.NextTrigger>
115+
</ButtonGroup>
116+
</Pagination.Root>
117+
</Box>
118+
)}
119+
</VStack>
120+
);
121+
}

thingconnect.pulse.client/src/components/layout/Page.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { type LucideIcon } from 'lucide-react';
66

77
export interface PageProps {
88
title: string;
9-
description?: string;
9+
description?: ReactNode;
1010
testId?: string;
1111
children: ReactNode;
1212
isLoading?: boolean;

thingconnect.pulse.client/src/components/layout/PageHeader.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import { Link as RouterLink } from 'react-router-dom';
1515

1616
export interface PageHeaderProps {
1717
title: string;
18-
description?: string;
18+
description?: ReactNode;
1919
breadcrumbs?: (string | null | undefined)[];
2020
tos?: string[];
2121
actions?: ReactNode;

0 commit comments

Comments
 (0)