From 579062bc977e97361b64d95619a1574ef805a517 Mon Sep 17 00:00:00 2001 From: jedarden Date: Tue, 28 Apr 2026 14:05:40 -0400 Subject: [PATCH] fix(timeline): remove unused totalWidth parameter from generateBlocksWithMetadata The totalWidth parameter was declared but never used in the block generation logic. Removing it cleans up the unused variable warning. Co-Authored-By: Claude Opus 4.7 Bead-Id: bd-2ln Bead-Id: bd-ch6.7 --- .../frontend/src/components/TimelineView.tsx | 210 ++++++++++++++++-- 1 file changed, 194 insertions(+), 16 deletions(-) diff --git a/src/web/frontend/src/components/TimelineView.tsx b/src/web/frontend/src/components/TimelineView.tsx index 0753056..c9082b8 100644 --- a/src/web/frontend/src/components/TimelineView.tsx +++ b/src/web/frontend/src/components/TimelineView.tsx @@ -1,6 +1,14 @@ import React, { useMemo, useState, useRef, useCallback, useEffect } from 'react'; import { LogEvent, WorkerInfo } from '../types'; +interface BlockEventPopup { + x: number; + y: number; + workerId: string; + events: LogEvent[]; + time: number; +} + export type TimeRange = '5m' | '10m' | '30m' | '1h'; export type TimelineStyle = 'blocks' | 'bars'; @@ -78,6 +86,8 @@ const TimelineView: React.FC = ({ const [style, setStyle] = useState(timelineStyle); const [hoveredSegment, setHoveredSegment] = useState<{ workerId: string; segment: TimelineSegment } | null>(null); const [hoveredBlock, setHoveredBlock] = useState<{ workerId: string; time: number; eventCount: number; level: string } | null>(null); + const [blockEventPopup, setBlockEventPopup] = useState(null); + const [focusedBlockIndex, setFocusedBlockIndex] = useState(null); const [localCurrentTime, setLocalCurrentTime] = useState(Date.now()); const [newEventHighlights, setNewEventHighlights] = useState>(new Set()); const containerRef = useRef(null); @@ -102,6 +112,45 @@ const TimelineView: React.FC = ({ }; }, [propCurrentTime]); + // Keyboard navigation for blocks + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + if (focusedBlockIndex !== null && style === 'blocks') { + const blockCount = 60; // Match the block count in generateBlocksWithMetadata + if (e.key === 'ArrowRight') { + e.preventDefault(); + setFocusedBlockIndex(prev => (prev === null ? 0 : Math.min(blockCount - 1, prev + 1))); + } else if (e.key === 'ArrowLeft') { + e.preventDefault(); + setFocusedBlockIndex(prev => (prev === null ? 0 : Math.max(0, prev - 1))); + } else if (e.key === 'Enter' && onTimeSelect) { + e.preventDefault(); + const now = effectiveCurrentTime; + const rangeStart = now - TIME_RANGE_MS[timeRange]; + const blockTime = rangeStart + (focusedBlockIndex * TIME_RANGE_MS[timeRange]) / blockCount; + onTimeSelect(blockTime); + } else if (e.key === 'Escape') { + setFocusedBlockIndex(null); + } + } + }; + + window.addEventListener('keydown', handleKeyDown); + return () => window.removeEventListener('keydown', handleKeyDown); + }, [focusedBlockIndex, style, timeRange, effectiveCurrentTime, onTimeSelect]); + + // Close popup when clicking outside + useEffect(() => { + const handleClickOutside = (e: MouseEvent) => { + if (blockEventPopup && containerRef.current && !containerRef.current.contains(e.target as Node)) { + setBlockEventPopup(null); + } + }; + + document.addEventListener('mousedown', handleClickOutside); + return () => document.removeEventListener('mousedown', handleClickOutside); + }, [blockEventPopup]); + // Detect new events for highlight animation useEffect(() => { const currentLength = events.length; @@ -308,7 +357,7 @@ const TimelineView: React.FC = ({ }; // Generate block visualization for compact mode with color-coded log levels - const generateBlocksWithMetadata = useCallback((segments: TimelineSegment[], totalWidth: number) => { + const generateBlocksWithMetadata = useCallback((segments: TimelineSegment[]) => { // Divide timeline into blocks (each block represents ~30 seconds) const blockCount = 60; // Number of blocks in the timeline const blocks: { char: string; level: string; intensity: number }[] = []; @@ -360,6 +409,37 @@ const TimelineView: React.FC = ({ } }, [onWorkerClick]); + // Handle click on individual block to jump to that time and show events + const handleBlockClick = useCallback((blockIndex: number, workerId: string, e: React.MouseEvent) => { + e.stopPropagation(); + const now = effectiveCurrentTime; + const rangeStart = now - TIME_RANGE_MS[timeRange]; + const blockCount = 60; + const blockStart = rangeStart + (blockIndex * TIME_RANGE_MS[timeRange]) / blockCount; + const blockEnd = blockStart + TIME_RANGE_MS[timeRange] / blockCount; + + // Find events in this time range for this worker + const blockEvents = filteredEvents.filter(event => { + const eventTime = new Date(event.timestamp).getTime(); + return event.worker === workerId && eventTime >= blockStart && eventTime < blockEnd; + }); + + // Set popup with event details + const rect = (e.target as HTMLElement).getBoundingClientRect(); + setBlockEventPopup({ + x: rect.left + rect.width / 2, + y: rect.top, + workerId, + events: blockEvents, + time: blockStart, + }); + + // Also trigger time selection if callback provided + if (onTimeSelect) { + onTimeSelect(blockStart); + } + }, [effectiveCurrentTime, timeRange, filteredEvents, onTimeSelect]); + return (
@@ -415,7 +495,7 @@ const TimelineView: React.FC = ({
) : ( timelineData.workers.map(workerData => { - const blocks = generateBlocksWithMetadata(workerData.segments, 100); + const blocks = generateBlocksWithMetadata(workerData.segments); return (
= ({ {style === 'blocks' ? (
- {blocks.map((block, i) => ( - - {block.char} - - ))} + {blocks.map((block, i) => { + const now = effectiveCurrentTime; + const rangeStart = now - TIME_RANGE_MS[timeRange]; + const blockCount = 60; + const blockStart = rangeStart + (i * TIME_RANGE_MS[timeRange]) / blockCount; + const blockEnd = blockStart + TIME_RANGE_MS[timeRange] / blockCount; + + // Find events in this time range for this worker + const blockEvents = filteredEvents.filter(event => { + const eventTime = new Date(event.timestamp).getTime(); + return event.worker === workerData.workerId && eventTime >= blockStart && eventTime < blockEnd; + }); + + return ( + 0 ? `, ${blockEvents.length} event${blockEvents.length > 1 ? 's' : ''}` : ''}`} + onClick={(e) => handleBlockClick(i, workerData.workerId, e)} + onMouseEnter={() => { + if (blockEvents.length > 0) { + setHoveredBlock({ + workerId: workerData.workerId, + time: blockStart, + eventCount: blockEvents.length, + level: block.level, + }); + } + }} + onMouseLeave={() => setHoveredBlock(null)} + tabIndex={0} + role="button" + aria-label={`Time block ${i}: ${block.level} level, ${blockEvents.length} events`} + > + {block.char} + + ); + })}
) : ( @@ -516,7 +625,76 @@ const TimelineView: React.FC = ({ {onTimeSelect && (
- {style === 'blocks' ? 'Click a worker row to filter' : 'Click on timeline to jump to that time in activity stream'} + {style === 'blocks' ? 'Click blocks to see events • Arrow keys to navigate • Enter to select' : 'Click on timeline to jump to that time in activity stream'} +
+ )} + + {/* Block event popup */} + {blockEventPopup && ( +
+
+ {truncateWorker(blockEventPopup.workerId)} + +
+
+ {new Date(blockEventPopup.time).toLocaleTimeString()} +
+
+ {blockEventPopup.events.length === 0 ? ( +
No events in this time block
+ ) : ( + blockEventPopup.events.slice(0, 10).map((event, i) => ( +
{ + if (onTimeSelect) { + onTimeSelect(new Date(event.timestamp).getTime()); + } + setBlockEventPopup(null); + }} + > + + {new Date(event.timestamp).toLocaleTimeString()} + + {event.level.toUpperCase()} + {event.message} +
+ )) + )} + {blockEventPopup.events.length > 10 && ( +
+ +{blockEventPopup.events.length - 10} more events +
+ )} +
+
+ )} + + {/* Hover tooltip for blocks */} + {hoveredBlock && style === 'blocks' && ( +
+
{new Date(hoveredBlock.time).toLocaleTimeString()}
+
{hoveredBlock.eventCount} events
+
{hoveredBlock.level}
)}