Skip to content

Commit 1157379

Browse files
committed
Adds loading spinner and highlights new SSE events
Improves visibility when syncing events by indicating a loading state. Highlights newly received items with a brief glow animation to help identify them quickly.
1 parent 70ca3e0 commit 1157379

File tree

2 files changed

+53
-4
lines changed

2 files changed

+53
-4
lines changed

src/components/WebhookInspector.css

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,10 @@
55
.webhook-events-status{font-size:12px;color:#999}
66
.webhook-events-status.live{color:green}
77
.webhook-events-ul{list-style:none;margin:0;padding:0;overflow:auto;flex:1}
8-
.webhook-event-item{padding:10px;border-bottom:1px solid #f0f0f0;cursor:pointer}
8+
.webhook-event-item{padding:10px;border-bottom:1px solid #f0f0f0;cursor:pointer;transition:background 0.2s}
99
.webhook-event-item.selected{background:#f7fbff}
10+
.webhook-event-item.new-event{animation:eventGlow 0.5s ease-out;box-shadow:0 0 0 2px #4a90e2}
11+
@keyframes eventGlow{0%{box-shadow:0 0 8px 4px rgba(74,144,226,0.6),0 0 0 2px #4a90e2}100%{box-shadow:0 0 0 0 rgba(74,144,226,0),0 0 0 2px #4a90e2}}
1012
.event-type{font-size:13px;color:#333}
1113
.event-meta{font-size:12px;color:#777}
1214
.event-session-indicators{display:flex;gap:6px;margin-top:6px;align-items:center}
@@ -42,3 +44,14 @@
4244
.event-type-option input[type="checkbox"]{margin-right:8px;cursor:pointer}
4345
.event-type-label{font-size:13px;color:#333;-webkit-user-select:none;user-select:none}
4446

47+
/* Loading Spinner Animation */
48+
.loading-spinner{display:inline-flex;align-items:center;gap:6px;color:#999}
49+
.loading-spinner-icon{width:14px;height:14px;border:2px solid #ddd;border-top-color:#4a90e2;border-radius:50%;animation:spin 0.8s linear infinite}
50+
@keyframes spin{to{transform:rotate(360deg)}}
51+
.loading-text{animation:loadingPulse 1.5s ease-in-out infinite}
52+
@keyframes loadingPulse{0%,100%{opacity:0.6}50%{opacity:1}}
53+
54+
/* Loading message in list */
55+
.loading-events-message{padding:20px;text-align:center;color:#666;display:flex;flex-direction:column;align-items:center;gap:12px}
56+
.loading-events-spinner{width:32px;height:32px;border:3px solid #e8e8e8;border-top-color:#4a90e2;border-radius:50%;animation:spin 0.8s linear infinite}
57+

src/components/WebhookInspector.tsx

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -88,9 +88,11 @@ const getDeviceIdFromEvent = (e: EventItem) => {
8888
const WebhookInspector: React.FC<Props> = ({ userPath, selectedDeviceId = null, selectedBayId = null, clearSignal }) => {
8989
const [allEvents, setAllEvents] = React.useState<EventItem[]>([]);
9090
const [connected, setConnected] = React.useState(false);
91+
const [isLoadingEvents, setIsLoadingEvents] = React.useState(false);
9192
const [selectedIndex, setSelectedIndex] = React.useState<number | null>(null);
9293
const [selectedEventTypes, setSelectedEventTypes] = React.useState<Set<string>>(new Set());
9394
const [showEventTypeDropdown, setShowEventTypeDropdown] = React.useState(false);
95+
const [newEventIds, setNewEventIds] = React.useState<Set<string>>(new Set());
9496
const listRef = React.useRef<HTMLUListElement | null>(null);
9597
const listContainerRef = React.useRef<HTMLDivElement | null>(null);
9698
const dropdownRef = React.useRef<HTMLDivElement | null>(null);
@@ -106,6 +108,7 @@ const WebhookInspector: React.FC<Props> = ({ userPath, selectedDeviceId = null,
106108
React.useEffect(() => {
107109
if (!userPath) return;
108110
let cancelled = false;
111+
setIsLoadingEvents(true);
109112
(async () => {
110113
try {
111114
const r = await fetch(`/api/webhook/${encodeURIComponent(userPath)}/events`);
@@ -126,6 +129,8 @@ const WebhookInspector: React.FC<Props> = ({ userPath, selectedDeviceId = null,
126129
}
127130
} catch (err) {
128131
console.warn('Failed to load events', err);
132+
} finally {
133+
if (!cancelled) setIsLoadingEvents(false);
129134
}
130135
})();
131136
return () => { cancelled = true; };
@@ -157,6 +162,19 @@ const WebhookInspector: React.FC<Props> = ({ userPath, selectedDeviceId = null,
157162
console.log('[SSE] Parsed event:', data.eventType, 'id:', data.id);
158163
const newItem: EventItem = { id: data.id, eventType: data.eventType, timestamp: data.timestamp, data: data.data, raw: data.raw, expanded: false };
159164

165+
// Mark this event as new for the glow animation
166+
if (newItem.id) {
167+
setNewEventIds(prev => new Set(prev).add(newItem.id!));
168+
// Remove the "new" flag after 500ms
169+
setTimeout(() => {
170+
setNewEventIds(prev => {
171+
const next = new Set(prev);
172+
next.delete(newItem.id!);
173+
return next;
174+
});
175+
}, 500);
176+
}
177+
160178
// Process SessionInfo events to extract activity/course information
161179
if (data.eventType === 'TPS.SessionInfo') {
162180
console.log('[SSE] Processing SessionInfo event');
@@ -584,7 +602,14 @@ const WebhookInspector: React.FC<Props> = ({ userPath, selectedDeviceId = null,
584602
<div ref={listContainerRef} className="webhook-inspector-list" tabIndex={0} onKeyDown={onListKeyDown}>
585603
<div className="webhook-events-header">
586604
<strong>Events</strong>
587-
<span className={`webhook-events-status ${connected ? 'live' : ''}`}>{connected ? 'live' : 'disconnected'}</span>
605+
<span className={`webhook-events-status ${connected ? 'live' : ''}`}>
606+
{isLoadingEvents ? (
607+
<span className="loading-spinner">
608+
<span className="loading-spinner-icon"></span>
609+
<span className="loading-text">loading</span>
610+
</span>
611+
) : (connected ? 'live' : 'disconnected')}
612+
</span>
588613

589614
{/* Event Type Filter Dropdown */}
590615
<div className="event-type-filter" ref={dropdownRef}>
@@ -627,7 +652,12 @@ const WebhookInspector: React.FC<Props> = ({ userPath, selectedDeviceId = null,
627652
</div>
628653
</div>
629654
<ul className="webhook-events-ul" ref={listRef}>
630-
{visibleEvents.length === 0 ? (
655+
{isLoadingEvents ? (
656+
<li className="loading-events-message">
657+
<div className="loading-events-spinner"></div>
658+
<div>Loading events...</div>
659+
</li>
660+
) : visibleEvents.length === 0 ? (
631661
<li className="no-events">No events yet.</li>
632662
) : (
633663
allEvents.map((e, allIdx) => {
@@ -641,8 +671,14 @@ const WebhookInspector: React.FC<Props> = ({ userPath, selectedDeviceId = null,
641671
const customerColor = getColorForId(customerSessionId, customerSessionColors);
642672
const activityColor = getColorForId(activitySessionId, activitySessionColors);
643673

674+
const isNew = e.id && newEventIds.has(e.id);
675+
644676
return (
645-
<li key={e.id || allIdx} className={`webhook-event-item ${selectedIndex === visibleIdx ? 'selected' : ''}`} onClick={() => select(visibleIdx)}>
677+
<li
678+
key={e.id || allIdx}
679+
className={`webhook-event-item ${selectedIndex === visibleIdx ? 'selected' : ''} ${isNew ? 'new-event' : ''}`}
680+
onClick={() => select(visibleIdx)}
681+
>
646682
<div className="event-type">{e.eventType}</div>
647683
<div className="event-meta">{new Date(e.timestamp).toLocaleString()}</div>
648684
<div className="event-session-indicators">

0 commit comments

Comments
 (0)