Skip to content

Commit 4e087a2

Browse files
hemanandrclaude
andcommitted
feat: implement Issue #18 - Live Board Page with real-time monitoring dashboard
- Add API types aligned with OpenAPI spec for /api/status/live - Create StatusService for live endpoint data fetching - Build React Query hook with 5-second auto-refresh - Implement responsive StatusTable for desktop with sortable columns - Create StatusCard component for mobile-friendly card layout - Add StatusFilters for group filtering and endpoint search - Build StatusPagination with page size controls - Create MiniSparkline component for status history visualization - Update Dashboard page with complete live monitoring interface - Add TypeScript strict compliance and ESLint fixes - Install date-fns for time formatting utilities 🎯 Ready for backend API integration with /api/status/live endpoint 🚀 Generated with Claude Code Co-Authored-By: Claude <[email protected]>
1 parent 6d4e0a9 commit 4e087a2

File tree

12 files changed

+733
-50
lines changed

12 files changed

+733
-50
lines changed

.claude/settings.local.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@
99
"Bash(dotnet clean:*)",
1010
"mcp__chakra-ui__get_component_example",
1111
"Bash(npm run build:*)",
12-
"Bash(npm run lint)"
12+
"Bash(npm run lint)",
13+
"mcp__chakra-ui__list_components"
1314
],
1415
"deny": [],
1516
"ask": []

thingconnect.pulse.client/package-lock.json

Lines changed: 15 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

thingconnect.pulse.client/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
"@monaco-editor/react": "^4.7.0",
1919
"@tanstack/react-query": "^5.84.2",
2020
"axios": "^1.11.0",
21+
"date-fns": "^4.1.0",
2122
"lodash": "^4.17.21",
2223
"lucide-react": "^0.541.0",
2324
"luxon": "^3.7.1",
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { apiClient } from '../client'
2+
import type { PagedLive, LiveStatusParams } from '../types'
3+
4+
export class StatusService {
5+
static async getLiveStatus(params: LiveStatusParams = {}): Promise<PagedLive> {
6+
const searchParams = new URLSearchParams()
7+
8+
if (params.group) {
9+
searchParams.append('group', params.group)
10+
}
11+
12+
if (params.search) {
13+
searchParams.append('search', params.search)
14+
}
15+
16+
if (params.page) {
17+
searchParams.append('page', params.page.toString())
18+
}
19+
20+
if (params.pageSize) {
21+
searchParams.append('pageSize', params.pageSize.toString())
22+
}
23+
24+
const queryString = searchParams.toString()
25+
const url = `/api/status/live${queryString ? `?${queryString}` : ''}`
26+
27+
return apiClient.get<PagedLive>(url)
28+
}
29+
}

thingconnect.pulse.client/src/api/types.ts

Lines changed: 37 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,50 @@
11
// API Types based on OpenAPI specification
22
// This file contains TypeScript interfaces for the Pulse API responses
33

4-
export interface LiveEndpoint {
4+
export interface Group {
55
id: string
66
name: string
7+
parent_id?: string | null
8+
color?: string | null
9+
}
10+
11+
export interface Endpoint {
12+
id: string
13+
name: string
14+
group: Group
15+
type: 'icmp' | 'tcp' | 'http'
716
host: string
8-
group?: string
9-
status: 'UP' | 'DOWN' | 'FLAPPING'
10-
rtt?: number
11-
lastCheck?: string
12-
sparkline?: number[]
13-
config: {
14-
type: 'ICMP' | 'TCP' | 'HTTP' | 'HTTPS'
15-
port?: number
16-
path?: string
17-
timeout?: number
18-
interval?: number
19-
}
17+
port?: number | null
18+
http_path?: string | null
19+
http_match?: string | null
20+
interval_seconds: number
21+
timeout_ms: number
22+
retries: number
23+
enabled: boolean
2024
}
2125

22-
export interface PagedLive {
23-
data: LiveEndpoint[]
26+
export interface SparklinePoint {
27+
ts: string
28+
s: 'u' | 'd'
29+
}
30+
31+
export interface LiveStatusItem {
32+
endpoint: Endpoint
33+
status: 'up' | 'down' | 'flapping'
34+
rtt_ms?: number | null
35+
last_change_ts: string
36+
sparkline: SparklinePoint[]
37+
}
38+
39+
export interface PageMeta {
2440
page: number
2541
pageSize: number
26-
totalCount: number
27-
totalPages: number
42+
total: number
43+
}
44+
45+
export interface PagedLive {
46+
meta: PageMeta
47+
items: LiveStatusItem[]
2848
}
2949

3050
export interface StateChange {
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import { Box } from '@chakra-ui/react'
2+
import type { SparklinePoint } from '@/api/types'
3+
4+
interface MiniSparklineProps {
5+
data: SparklinePoint[]
6+
width?: number
7+
height?: number
8+
}
9+
10+
export function MiniSparkline({ data, width = 80, height = 20 }: MiniSparklineProps) {
11+
if (!data || data.length === 0) {
12+
return (
13+
<Box w={width} h={height} bg="gray.100" _dark={{ bg: 'gray.700' }} borderRadius="sm" />
14+
)
15+
}
16+
17+
// Create points for the SVG path
18+
const points = data.map((point, index) => {
19+
const x = (index / (data.length - 1)) * width
20+
const y = point.s === 'u' ? height * 0.2 : height * 0.8 // Up points near top, down points near bottom
21+
return `${x},${y}`
22+
}).join(' ')
23+
24+
return (
25+
<Box w={width} h={height}>
26+
<svg width={width} height={height} viewBox={`0 0 ${width} ${height}`}>
27+
{/* Background */}
28+
<rect width={width} height={height} fill="transparent" />
29+
30+
{/* Status line */}
31+
<polyline
32+
points={points}
33+
fill="none"
34+
stroke={data[data.length - 1]?.s === 'u' ? '#10B981' : '#EF4444'}
35+
strokeWidth="1.5"
36+
strokeLinecap="round"
37+
strokeLinejoin="round"
38+
/>
39+
40+
{/* Status dots */}
41+
{data.map((point, index) => {
42+
const x = (index / (data.length - 1)) * width
43+
const y = point.s === 'u' ? height * 0.2 : height * 0.8
44+
return (
45+
<circle
46+
key={`${point.ts}-${index}`}
47+
cx={x}
48+
cy={y}
49+
r="1.5"
50+
fill={point.s === 'u' ? '#10B981' : '#EF4444'}
51+
/>
52+
)
53+
})}
54+
</svg>
55+
</Box>
56+
)
57+
}
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import { Text, Badge, HStack, VStack, Card } from '@chakra-ui/react'
2+
import { MiniSparkline } from '@/components/charts/MiniSparkline'
3+
import { formatDistanceToNow } from 'date-fns'
4+
import { useNavigate } from 'react-router-dom'
5+
import type { LiveStatusItem } from '@/api/types'
6+
7+
interface StatusCardProps {
8+
item: LiveStatusItem
9+
}
10+
11+
export function StatusCard({ item }: StatusCardProps) {
12+
const navigate = useNavigate()
13+
14+
const getStatusColor = (status: string) => {
15+
switch (status) {
16+
case 'up': return 'green'
17+
case 'down': return 'red'
18+
case 'flapping': return 'yellow'
19+
default: return 'gray'
20+
}
21+
}
22+
23+
const formatRTT = (rtt?: number | null) => {
24+
if (rtt == null) return '-'
25+
return `${rtt}ms`
26+
}
27+
28+
const formatLastChange = (timestamp: string) => {
29+
try {
30+
return formatDistanceToNow(new Date(timestamp), { addSuffix: true })
31+
} catch {
32+
return 'Unknown'
33+
}
34+
}
35+
36+
const handleClick = () => {
37+
void navigate(`/endpoints/${item.endpoint.id}`)
38+
}
39+
40+
return (
41+
<Card.Root
42+
cursor="pointer"
43+
_hover={{
44+
transform: 'translateY(-2px)',
45+
shadow: 'md',
46+
borderColor: 'blue.200',
47+
_dark: { borderColor: 'blue.600' }
48+
}}
49+
onClick={handleClick}
50+
data-testid={`status-card-${item.endpoint.id}`}
51+
transition="all 0.2s"
52+
size="sm"
53+
>
54+
<Card.Body>
55+
<VStack align="stretch" gap={3}>
56+
{/* Header with status and name */}
57+
<HStack justify="space-between" align="start">
58+
<VStack align="start" gap={1} flex="1">
59+
<Text fontWeight="semibold" fontSize="sm" data-testid="card-endpoint-name">
60+
{item.endpoint.name}
61+
</Text>
62+
<Text fontSize="xs" color="gray.600" _dark={{ color: 'gray.400' }}>
63+
{item.endpoint.group.name}
64+
</Text>
65+
</VStack>
66+
67+
<Badge
68+
colorPalette={getStatusColor(item.status)}
69+
variant="subtle"
70+
textTransform="uppercase"
71+
fontSize="xs"
72+
data-testid={`card-status-badge-${item.status}`}
73+
>
74+
{item.status}
75+
</Badge>
76+
</HStack>
77+
78+
{/* Host and RTT */}
79+
<HStack justify="space-between" align="center">
80+
<HStack gap={1} flex="1">
81+
<Text fontFamily="mono" fontSize="xs" data-testid="card-endpoint-host">
82+
{item.endpoint.host}
83+
</Text>
84+
{item.endpoint.port && (
85+
<Text fontSize="xs" color="gray.500">
86+
:{item.endpoint.port}
87+
</Text>
88+
)}
89+
</HStack>
90+
91+
<Text
92+
fontFamily="mono"
93+
fontSize="xs"
94+
color={item.rtt_ms ? 'inherit' : 'gray.400'}
95+
data-testid="card-endpoint-rtt"
96+
>
97+
{formatRTT(item.rtt_ms)}
98+
</Text>
99+
</HStack>
100+
101+
{/* Sparkline and last change */}
102+
<HStack justify="space-between" align="center">
103+
<MiniSparkline data={item.sparkline} width={60} height={16} />
104+
<Text fontSize="xs" color="gray.500">
105+
{formatLastChange(item.last_change_ts)}
106+
</Text>
107+
</HStack>
108+
</VStack>
109+
</Card.Body>
110+
</Card.Root>
111+
)
112+
}

0 commit comments

Comments
 (0)