Skip to content

Commit b32ff05

Browse files
committed
added outage table
1 parent 09ba5f3 commit b32ff05

File tree

4 files changed

+213
-3
lines changed

4 files changed

+213
-3
lines changed

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

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -202,7 +202,6 @@ export function HistoryTable({ data, bucket, pageSize = 20, isLoading }: History
202202
overflow='hidden'
203203
textOverflow='ellipsis'
204204
whiteSpace='nowrap'
205-
title={row.error || undefined}
206205
>
207206
{row.error || '-'}
208207
</Text>
Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
import { useState, useMemo } from 'react';
2+
import {
3+
Box,
4+
Table,
5+
Text,
6+
Badge,
7+
VStack,
8+
HStack,
9+
IconButton,
10+
Pagination,
11+
ButtonGroup,
12+
Skeleton,
13+
} from '@chakra-ui/react';
14+
import { ChevronLeft, ChevronRight, Clock } from 'lucide-react';
15+
import type { Outage } from '@/api/types';
16+
import { Tooltip } from '../ui/tooltip';
17+
18+
export interface OutagesTableProps {
19+
outages?: Outage[] | null;
20+
pageSize?: number;
21+
isLoading?: boolean;
22+
}
23+
24+
function formatDuration(seconds?: number | null) {
25+
if (!seconds) return 'Unknown';
26+
if (seconds < 60) return `${seconds}s`;
27+
if (seconds < 3600) return `${Math.round(seconds / 60)}m`;
28+
const hours = Math.floor(seconds / 3600);
29+
const minutes = Math.round((seconds % 3600) / 60);
30+
return minutes > 0 ? `${hours}h ${minutes}m` : `${hours}h`;
31+
}
32+
33+
export function OutagesTable({ outages, pageSize = 20, isLoading }: OutagesTableProps) {
34+
const [currentPage, setCurrentPage] = useState(1);
35+
36+
const sortedOutages = useMemo(
37+
() =>
38+
[...(outages ?? [])].sort(
39+
(a, b) => new Date(b.startedTs).getTime() - new Date(a.startedTs).getTime()
40+
),
41+
[outages]
42+
);
43+
44+
const totalPages = Math.ceil(sortedOutages.length / pageSize);
45+
46+
const paginatedData = useMemo(() => {
47+
const startIndex = (currentPage - 1) * pageSize;
48+
return sortedOutages.slice(startIndex, startIndex + pageSize);
49+
}, [sortedOutages, currentPage, pageSize]);
50+
51+
if (!isLoading && sortedOutages.length === 0) {
52+
return (
53+
<Box
54+
p={8}
55+
textAlign='center'
56+
bg='gray.50'
57+
borderRadius='md'
58+
border='2px dashed'
59+
borderColor='gray.300'
60+
_dark={{ bg: 'gray.800', borderColor: 'gray.600' }}
61+
h='full'
62+
>
63+
<VStack gap={3}>
64+
<Clock size={32} color='#9CA3AF' />
65+
<Text color='gray.500' _dark={{ color: 'gray.400' }}>
66+
No outages recorded
67+
</Text>
68+
</VStack>
69+
</Box>
70+
);
71+
}
72+
73+
return (
74+
<VStack flex={1} minH={0} align='stretch' gap={2}>
75+
<Table.ScrollArea borderWidth='1px' rounded='md' flex={1} minH={0} overflow='auto'>
76+
<Table.Root size='sm' stickyHeader>
77+
<Table.Header>
78+
<Table.Row bg='gray.100' _dark={{ bg: 'gray.800' }}>
79+
<Table.ColumnHeader>Start Time</Table.ColumnHeader>
80+
<Table.ColumnHeader>Ended</Table.ColumnHeader>
81+
<Table.ColumnHeader>Status</Table.ColumnHeader>
82+
<Table.ColumnHeader>Duration</Table.ColumnHeader>
83+
<Table.ColumnHeader>Error</Table.ColumnHeader>
84+
</Table.Row>
85+
</Table.Header>
86+
87+
<Table.Body>
88+
{isLoading
89+
? Array.from({ length: 8 }).map((_, i) => (
90+
<Table.Row key={`skeleton-${i}`}>
91+
{Array.from({ length: 5 }).map((_, j) => (
92+
<Table.Cell key={j}>
93+
<Skeleton height='16px' w='80%' />
94+
</Table.Cell>
95+
))}
96+
</Table.Row>
97+
))
98+
: paginatedData.map((outage, idx) => (
99+
<Table.Row key={`${outage.startedTs}-${idx}`}>
100+
<Table.Cell>
101+
<Text fontSize='sm' fontFamily='mono'>
102+
{new Date(outage.startedTs).toLocaleString()}
103+
</Text>
104+
</Table.Cell>
105+
<Table.Cell>
106+
{outage.endedTs ? (
107+
<Text fontSize='sm' fontFamily='mono'>
108+
{new Date(outage.endedTs).toLocaleString()}
109+
</Text>
110+
) : (
111+
'-'
112+
)}
113+
</Table.Cell>
114+
<Table.Cell>
115+
<Badge colorPalette={outage.endedTs ? 'green' : 'red'} size='sm'>
116+
{outage.endedTs ? 'Resolved' : 'Ongoing'}
117+
</Badge>
118+
</Table.Cell>
119+
<Table.Cell>
120+
<Text fontSize='sm'>{formatDuration(outage.durationS)}</Text>
121+
</Table.Cell>
122+
<Table.Cell>
123+
<Tooltip content={outage.lastError || '-'}>
124+
<Text
125+
maxW={'3xs'}
126+
fontSize='sm'
127+
color={outage.lastError ? 'red.600' : 'gray.500'}
128+
_dark={{ color: outage.lastError ? 'red.400' : 'gray.400' }}
129+
overflow='hidden'
130+
textOverflow='ellipsis'
131+
whiteSpace='nowrap'
132+
>
133+
{outage.lastError || '-'}
134+
</Text>
135+
</Tooltip>
136+
</Table.Cell>
137+
</Table.Row>
138+
))}
139+
</Table.Body>
140+
</Table.Root>
141+
</Table.ScrollArea>
142+
143+
{!isLoading && totalPages > 1 && (
144+
<Box flexShrink={0}>
145+
<Pagination.Root
146+
count={sortedOutages.length}
147+
pageSize={pageSize}
148+
page={currentPage}
149+
onPageChange={details => setCurrentPage(details.page)}
150+
>
151+
<ButtonGroup variant='ghost' size='sm' w='full' justifyContent='center'>
152+
<Text fontSize='sm' color='gray.600' _dark={{ color: 'gray.400' }} flex='1'>
153+
{sortedOutages.length > 0
154+
? `Showing ${(currentPage - 1) * pageSize + 1} - ${Math.min(
155+
currentPage * pageSize,
156+
sortedOutages.length
157+
)} of ${sortedOutages.length} outages`
158+
: `0 of 0`}
159+
</Text>
160+
<Pagination.PrevTrigger asChild>
161+
<IconButton aria-label='Previous page'>
162+
<ChevronLeft />
163+
</IconButton>
164+
</Pagination.PrevTrigger>
165+
<Pagination.Items
166+
render={page => (
167+
<IconButton
168+
key={page.value}
169+
variant={page.value === currentPage ? 'outline' : 'ghost'}
170+
size='sm'
171+
>
172+
{page.value}
173+
</IconButton>
174+
)}
175+
/>
176+
<Pagination.NextTrigger asChild>
177+
<IconButton aria-label='Next page'>
178+
<ChevronRight />
179+
</IconButton>
180+
</Pagination.NextTrigger>
181+
</ButtonGroup>
182+
</Pagination.Root>
183+
</Box>
184+
)}
185+
</VStack>
186+
);
187+
}

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

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import { StatusService } from '@/api/services/status.service';
3333
import { Tooltip } from '@/components/ui/tooltip';
3434
import { AvailabilityStats } from '@/components/history/AvailabilityStats';
3535
import { OutagesTimeline } from '@/components/history/OutageTimeline';
36+
import { OutagesTable } from '@/components/history/OutagesTable';
3637

3738
export default function History() {
3839
const analytics = useAnalytics();
@@ -344,7 +345,7 @@ export default function History() {
344345
</Card.Body>
345346
</Card.Root>
346347
</Tabs.Content>
347-
<Tabs.Content value='outages' flex={1} display='flex' minH={0}>
348+
<Tabs.Content value='outages' flex={1} display='flex' minH={0} gap={2}>
348349
<Card.Root flex={1} display='flex' flexDirection='column' overflow='hidden' size={'sm'}>
349350
<Card.Header px={3} pt={3}>
350351
<HStack gap={2}>
@@ -368,6 +369,29 @@ export default function History() {
368369
<OutagesTimeline outages={historyData?.outages} isLoading={isHistoryDataLoading} />
369370
</Card.Body>
370371
</Card.Root>
372+
<Card.Root flex={1} display='flex' flexDirection='column' overflow='hidden' size={'sm'}>
373+
<Card.Header px={3} pt={3}>
374+
<HStack gap={2}>
375+
<Zap size={20} />
376+
<Text fontWeight='medium' fontSize='sm'>
377+
Outage History
378+
</Text>
379+
<Text fontSize='sm' color='gray.600' _dark={{ color: 'gray.400' }}>
380+
({selectedEndpointName})
381+
</Text>
382+
</HStack>
383+
</Card.Header>
384+
<Card.Body
385+
flex={1}
386+
display='flex'
387+
flexDirection='column'
388+
minH={0}
389+
p={3}
390+
overflow={'auto'}
391+
>
392+
<OutagesTable outages={historyData?.outages} isLoading={isHistoryDataLoading} />
393+
</Card.Body>
394+
</Card.Root>
371395
</Tabs.Content>
372396
</Tabs.Root>
373397
</Page>

thingconnect.pulse.client/src/providers/QueryProvider.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ export function QueryProvider({ children }: QueryProviderProps) {
1313
return (
1414
<QueryClientProvider client={queryClient}>
1515
{children}
16-
{showDevtools && <ReactQueryDevtools initialIsOpen={false} />}
16+
{!showDevtools && <ReactQueryDevtools initialIsOpen={false} />}
1717
</QueryClientProvider>
1818
);
1919
}

0 commit comments

Comments
 (0)