Skip to content

Commit ae022fc

Browse files
authored
feat: update UI, add loading state, pagination, and moved table component to separate file
2 parents 4039948 + 3e74485 commit ae022fc

File tree

7 files changed

+384
-298
lines changed

7 files changed

+384
-298
lines changed

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

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
import { ChevronLeft, ChevronRight, AlertCircle, CheckCircle, Clock } from 'lucide-react';
1515
import type { RollupBucket, DailyBucket, RawCheck } from '@/api/types';
1616
import type { BucketType } from '@/types/bucket';
17+
import { Tooltip } from './ui/tooltip';
1718

1819
export interface HistoryTableProps {
1920
data:
@@ -193,18 +194,20 @@ export function HistoryTable({ data, bucket, pageSize = 20, isLoading }: History
193194
</Text>
194195
</Table.Cell>
195196
<Table.Cell>
196-
<Text
197-
fontSize='sm'
198-
color={row.error ? 'red.600' : 'gray.500'}
199-
_dark={{ color: row.error ? 'red.400' : 'gray.400' }}
200-
maxW='200px'
201-
overflow='hidden'
202-
textOverflow='ellipsis'
203-
whiteSpace='nowrap'
204-
title={row.error || undefined}
205-
>
206-
{row.error || '-'}
207-
</Text>
197+
<Tooltip content={row.error || '-'}>
198+
<Text
199+
fontSize='sm'
200+
color={row.error ? 'red.600' : 'gray.500'}
201+
_dark={{ color: row.error ? 'red.400' : 'gray.400' }}
202+
maxW='200px'
203+
overflow='hidden'
204+
textOverflow='ellipsis'
205+
whiteSpace='nowrap'
206+
title={row.error || undefined}
207+
>
208+
{row.error || '-'}
209+
</Text>
210+
</Tooltip>
208211
</Table.Cell>
209212
</>
210213
) : (
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
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
25+
justify='center'
26+
align='center'
27+
color='gray.300'
28+
textAlign='center'
29+
gap={1}
30+
py={5}
31+
h='100%'
32+
>
33+
<CloudOff size={'40px'} />
34+
<Text textAlign='center' color='gray.500'>
35+
No recent outages
36+
</Text>
37+
</VStack>
38+
);
39+
}
40+
41+
return (
42+
<VStack gap={4} align='stretch'>
43+
{outages.slice(0, 5).map((outage, index) => (
44+
<Card.Root key={`${outage.startedTs}-${index}`} variant='outline'>
45+
<Card.Body>
46+
<VStack gap={2} align='stretch'>
47+
<HStack justify='space-between'>
48+
<Text fontSize='sm' fontWeight='medium'>
49+
{formatDistanceToNow(new Date(outage.startedTs), { addSuffix: true })}
50+
</Text>
51+
<Badge colorPalette={outage.endedTs ? 'gray' : 'red'} size='sm'>
52+
{outage.endedTs ? 'Resolved' : 'Ongoing'}
53+
</Badge>
54+
</HStack>
55+
<HStack justify='space-between'>
56+
<Text fontSize='sm' color='gray.600'>
57+
Duration: {formatDuration(outage.durationS)}
58+
</Text>
59+
{outage.endedTs && (
60+
<Text fontSize='sm' color='gray.600'>
61+
Ended: {formatDistanceToNow(new Date(outage.endedTs), { addSuffix: true })}
62+
</Text>
63+
)}
64+
</HStack>
65+
{outage.lastError && (
66+
<Text fontSize='sm' color='red.600' _dark={{ color: 'red.400' }} lineClamp={2}>
67+
{outage.lastError}
68+
</Text>
69+
)}
70+
</VStack>
71+
</Card.Body>
72+
</Card.Root>
73+
))}
74+
{outages.length > 5 && (
75+
<Text fontSize='sm' color='gray.500' textAlign='center'>
76+
Showing 5 of {outages.length} recent outages
77+
</Text>
78+
)}
79+
</VStack>
80+
);
81+
}
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
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+
import { Tooltip } from './ui/tooltip';
16+
interface RecentChecksTableProps {
17+
checks: RawCheck[];
18+
pageSize?: number;
19+
}
20+
21+
export function RecentChecksTable({ checks, pageSize = 10 }: RecentChecksTableProps) {
22+
const [currentPage, setCurrentPage] = useState(1);
23+
const pageCount = Math.ceil(checks.length / pageSize);
24+
const pagedChecks = useMemo(() => {
25+
const start = (currentPage - 1) * pageSize;
26+
return checks.slice(start, start + pageSize);
27+
}, [checks, currentPage, pageSize]);
28+
29+
if (checks.length === 0) {
30+
return (
31+
<VStack
32+
justify='center'
33+
align='center'
34+
color='gray.300'
35+
textAlign='center'
36+
gap={1}
37+
py={5}
38+
h='100%'
39+
>
40+
<CloudOff size={'40px'} />
41+
<Text textAlign='center' color='gray.500'>
42+
No recent checks available
43+
</Text>
44+
</VStack>
45+
);
46+
}
47+
48+
return (
49+
<VStack gap={2} align='stretch' h='100%' flex='1'>
50+
<Box flex='1' display='flex' flexDirection='column' minH={0}>
51+
<Table.ScrollArea borderWidth='1px' rounded='md' flex='1' minH={0} overflow='auto'>
52+
<Table.Root size='sm' stickyHeader>
53+
<Table.Header>
54+
<Table.Row bg='gray.100' _dark={{ bg: 'gray.800' }}>
55+
<Table.ColumnHeader w='30%'>Time</Table.ColumnHeader>
56+
<Table.ColumnHeader w='20%'>Status</Table.ColumnHeader>
57+
<Table.ColumnHeader w='25%'>RTT</Table.ColumnHeader>
58+
<Table.ColumnHeader w='25%'>Error</Table.ColumnHeader>
59+
</Table.Row>
60+
</Table.Header>
61+
<Table.Body>
62+
{pagedChecks.map((check, index) => (
63+
<Table.Row key={`${check.ts}-${index}`}>
64+
<Table.Cell w='30%'>
65+
<Text flex='1' fontSize='sm'>
66+
{formatDistanceToNow(new Date(check.ts), { addSuffix: true })}
67+
</Text>
68+
</Table.Cell>
69+
<Table.Cell w='20%'>
70+
<Badge colorPalette={check.status === 'up' ? 'green' : 'red'} size='sm'>
71+
{check.status.toUpperCase()}
72+
</Badge>
73+
</Table.Cell>
74+
<Table.Cell w='25%'>
75+
<Text fontSize='sm'>{check.rttMs ? `${check.rttMs}ms` : '-'}</Text>
76+
</Table.Cell>
77+
<Table.Cell w='25%'>
78+
<Tooltip content={check.error || '-'}>
79+
<Text flex='1' fontSize='sm' color='gray.500' lineClamp={1}>
80+
{check.error || '-'}
81+
</Text>
82+
</Tooltip>
83+
</Table.Cell>
84+
</Table.Row>
85+
))}
86+
</Table.Body>
87+
</Table.Root>
88+
</Table.ScrollArea>
89+
90+
{/* Pagination fixed at bottom */}
91+
{pageCount > 1 && (
92+
<Box flexShrink={0} py={2}>
93+
<Pagination.Root
94+
count={checks.length}
95+
pageSize={pageSize}
96+
page={currentPage}
97+
onPageChange={details => setCurrentPage(details.page)}
98+
>
99+
<ButtonGroup variant='ghost' size='sm' w='full' justifyContent='center'>
100+
<Text fontSize='sm' color='gray.600' _dark={{ color: 'gray.400' }} flex='1'>
101+
{`Showing ${(currentPage - 1) * pageSize + 1} - ${Math.min(
102+
currentPage * pageSize,
103+
checks.length
104+
)} of ${checks.length} entries`}
105+
</Text>
106+
<Pagination.PrevTrigger asChild>
107+
<IconButton aria-label='Previous page'>
108+
<ChevronLeft />
109+
</IconButton>
110+
</Pagination.PrevTrigger>
111+
<Pagination.Items
112+
render={page => (
113+
<IconButton
114+
key={page.value}
115+
variant={page.value === currentPage ? 'outline' : 'ghost'}
116+
size='sm'
117+
>
118+
{page.value}
119+
</IconButton>
120+
)}
121+
/>
122+
<Pagination.NextTrigger asChild>
123+
<IconButton aria-label='Next page'>
124+
<ChevronRight />
125+
</IconButton>
126+
</Pagination.NextTrigger>
127+
</ButtonGroup>
128+
</Pagination.Root>
129+
</Box>
130+
)}
131+
</Box>
132+
</VStack>
133+
);
134+
}

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;

thingconnect.pulse.client/src/components/ui/empty-state.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import * as React from 'react';
33

44
export interface EmptyStateProps extends ChakraEmptyState.RootProps {
55
title: string;
6-
description?: string;
6+
description?: React.ReactElement;
77
icon?: React.ReactNode;
88
}
99

0 commit comments

Comments
 (0)