Skip to content

Commit fe3b44f

Browse files
gokulvijclaude
andauthored
fix: live status block overflow issues (#146)
Co-authored-by: Claude <[email protected]>
1 parent e70e69a commit fe3b44f

File tree

2 files changed

+99
-78
lines changed

2 files changed

+99
-78
lines changed

thingconnect.pulse.client/src/components/status/StatusTable.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ export function StatusTable({ items, isLoading }: StatusTableProps) {
5151

5252
void navigate(`/endpoints/${id}`);
5353
};
54-
console.log('isLoading in StatusTable:', items);
54+
5555
return (
5656
<Box borderRadius='md' overflow='hidden'>
5757
<Table.Root size='md' borderWidth={0} width='full'>

thingconnect.pulse.client/src/components/status/TrendBlocks.tsx

Lines changed: 98 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -1,119 +1,140 @@
1-
import { HStack, Box } from '@chakra-ui/react';
2-
import type { SparklinePoint } from '@/api/types';
3-
import { useEffect, useState, useRef } from 'react';
1+
import { useEffect, useMemo, useRef, useState } from "react";
2+
import { Box } from "@chakra-ui/react";
3+
import type { SparklinePoint } from "@/api/types";
44

55
const TrendBlocks = ({ data }: { data: SparklinePoint[] }) => {
66
const maxBlocks = 20;
7+
const blockPx = 12;
8+
const gapPx = 4;
9+
const totalWidth = maxBlocks * blockPx + (maxBlocks - 1) * gapPx;
10+
711
const [isSliding, setIsSliding] = useState(false);
812
const [showNewData, setShowNewData] = useState(false);
9-
const lastTimestampRef = useRef<string | null>(null);
13+
const lastTsRef = useRef<string | null>(null);
1014

11-
// Take the last 20 points, but maintain order (oldest to newest)
12-
const recentData = data.slice(-maxBlocks);
15+
// last up to maxBlocks
16+
const recent = useMemo(
17+
() => (Array.isArray(data) ? data.slice(-maxBlocks) : []),
18+
[data]
19+
);
1320

14-
// Create array of 20 blocks, fill empty ones with null
15-
const displayBlocks = Array(maxBlocks).fill(null).map((_, idx) => {
16-
const dataIdx = idx - (maxBlocks - recentData.length);
17-
return dataIdx >= 0 ? recentData[dataIdx] : null;
18-
});
21+
// always exactly maxBlocks long; earlier slots are null placeholders
22+
const display = useMemo(() => {
23+
const padded = new Array<SparklinePoint | null>(maxBlocks).fill(null);
24+
const start = maxBlocks - recent.length;
25+
for (let i = 0; i < recent.length; i++) {
26+
padded[start + i] = recent[i];
27+
}
28+
return padded;
29+
}, [recent]);
1930

20-
// Detect new data by checking if the latest timestamp changed
31+
// heartbeat / slide animation trigger when latest timestamp changes
2132
useEffect(() => {
22-
const latestTimestamp = recentData.length > 0 ? recentData[recentData.length - 1]?.ts : null;
23-
24-
if (latestTimestamp && lastTimestampRef.current && latestTimestamp !== lastTimestampRef.current) {
25-
// New data detected - trigger slide animation
33+
const latest = recent.length ? recent[recent.length - 1].ts : null;
34+
35+
if (latest && lastTsRef.current && latest !== lastTsRef.current) {
2636
setIsSliding(true);
2737
setShowNewData(false);
2838

29-
// After slide completes, show the new data
30-
setTimeout(() => {
39+
const t1 = setTimeout(() => {
3140
setShowNewData(true);
3241
setIsSliding(false);
3342
}, 500);
3443

35-
// Reset showNewData after slide-in animation completes
36-
setTimeout(() => {
44+
const t2 = setTimeout(() => {
3745
setShowNewData(false);
3846
}, 1000);
39-
} else if (latestTimestamp && !lastTimestampRef.current) {
40-
// First load - no animation needed
47+
48+
return () => {
49+
clearTimeout(t1);
50+
clearTimeout(t2);
51+
};
52+
} else if (latest && !lastTsRef.current) {
4153
setIsSliding(false);
4254
setShowNewData(false);
4355
}
4456

45-
lastTimestampRef.current = latestTimestamp;
46-
}, [recentData]);
57+
lastTsRef.current = latest;
58+
}, [recent]);
59+
60+
// debug log to help spot if parent is sending too many points
61+
useEffect(() => {
62+
// eslint-disable-next-line no-console
63+
console.debug(
64+
"[TrendBlocks]",
65+
"incoming:",
66+
data.length,
67+
"recent:",
68+
recent.length,
69+
"slots:",
70+
display.length,
71+
"lastTs:",
72+
lastTsRef.current
73+
);
74+
}, [data, recent, display]);
4775

4876
return (
49-
<>
77+
<Box
78+
width={`${totalWidth}px`}
79+
minWidth={`${totalWidth}px`}
80+
maxWidth={`${totalWidth}px`}
81+
display="grid"
82+
gridTemplateColumns={`repeat(${maxBlocks}, ${blockPx}px)`}
83+
gap={`${gapPx}px`}
84+
overflow="hidden"
85+
py="2px"
86+
pr="2px"
87+
alignItems="center"
88+
boxSizing="border-box"
89+
>
5090
<style>
5191
{`
5292
@keyframes heartbeat {
5393
0% { transform: scale(0.95); }
5494
50% { transform: scale(1.1); }
5595
100% { transform: scale(0.95); }
5696
}
57-
@keyframes slideLeftGroup {
97+
@keyframes slideLeft {
5898
0% { transform: translateX(0); }
59-
100% { transform: translateX(-16px); }
99+
100% { transform: translateX(-${blockPx + gapPx}px); }
60100
}
61-
@keyframes slideInFromRight {
62-
0% { transform: translateX(16px) scale(0.1); opacity: 0; }
63-
50% { transform: translateX(8px) scale(0.5); opacity: 0.5; }
101+
@keyframes slideIn {
102+
0% { transform: translateX(${blockPx + gapPx}px) scale(0.1); opacity: 0; }
103+
50% { transform: translateX(${Math.round((blockPx + gapPx) / 2)}px) scale(0.5); opacity: 0.5; }
64104
100% { transform: translateX(0) scale(1); opacity: 1; }
65105
}
66-
.heartbeat-animation {
67-
animation: heartbeat 1.5s ease-in-out infinite;
68-
}
69-
.slide-left-animation {
70-
animation: slideLeftGroup 500ms ease-out;
71-
}
72-
.slide-in-animation {
73-
animation: slideInFromRight 500ms ease-out;
74-
}
106+
.hb { animation: heartbeat 1.5s ease-in-out infinite; will-change: transform; }
107+
.slide-left { animation: slideLeft 500ms ease-out; will-change: transform; }
108+
.slide-in { animation: slideIn 500ms ease-out; will-change: transform; }
75109
`}
76110
</style>
77-
<HStack gap={1} alignItems="center" overflow="hidden" py={"2px"} pr={"2px"}>
78-
{displayBlocks.map((point, idx) => {
79-
const isLastElement = idx === displayBlocks.length - 1 && point !== null;
80-
const isEmpty = point === null;
81-
const isNewElement = isLastElement && showNewData;
82-
const shouldHeartbeat = isLastElement && !isSliding;
83111

112+
{display.map((pt, i) => {
113+
const isLast = i === display.length - 1;
114+
const isNew = isLast && showNewData && pt !== null;
115+
const shouldHb = isLast && !isSliding && pt !== null;
116+
const cls =
117+
isSliding && pt !== null ? "slide-left" : isNew ? "slide-in" : shouldHb ? "hb" : undefined;
118+
119+
const bg = pt === null ? "gray.200" : pt.s === "d" ? "red.500" : "green.500";
120+
const darkBg = pt === null ? "gray.700" : pt.s === "d" ? "red.600" : "green.600";
84121

85-
return (
86-
<Box
87-
key={point?.ts || `empty-${idx}`}
88-
w='3'
89-
h='5'
90-
borderRadius='sm'
91-
bg={isEmpty
92-
? 'gray.200'
93-
: point.s === 'd' ? 'red.500' : 'green.500'
94-
}
95-
_dark={{
96-
bg: isEmpty
97-
? 'gray.700'
98-
: point.s === 'd' ? 'red.600' : 'green.600'
99-
}}
100-
className={
101-
isSliding && !isEmpty
102-
? 'slide-left-animation'
103-
: isNewElement
104-
? 'slide-in-animation'
105-
: shouldHeartbeat
106-
? 'heartbeat-animation'
107-
: undefined
108-
}
109-
transformOrigin="center"
110-
position="relative"
111-
zIndex={isLastElement ? 2 : 1}
112-
/>
113-
);
114-
})}
115-
</HStack>
116-
</>
122+
return (
123+
<Box
124+
key={`slot-${i}-${pt?.ts ?? "empty"}`}
125+
w={`${blockPx}px`}
126+
h="5"
127+
borderRadius="sm"
128+
bg={bg}
129+
_dark={{ bg: darkBg }}
130+
className={cls}
131+
transformOrigin="center"
132+
position="relative"
133+
zIndex={isLast && pt ? 2 : 1}
134+
/>
135+
);
136+
})}
137+
</Box>
117138
);
118139
};
119140

0 commit comments

Comments
 (0)