From 90cff4db92b5a2d0ae4c7cf2bac8e15e22b36eb0 Mon Sep 17 00:00:00 2001 From: sepehr-safari Date: Wed, 8 Jul 2026 02:57:55 +0300 Subject: [PATCH] =?UTF-8?q?feat(app):=20inspector=20polish=20=E2=80=94=20l?= =?UTF-8?q?oading=20states,=20responsive,=20keyboard=20nav?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Loading state: spinner with parsing indicator, button disables - Responsive layout: mobile-friendly (flex-wrap, smaller text, adaptive grids) - Keyboard navigation: arrow up/down to move through events in the timeline - Sticky header for better UX on long traces - Error state: improved guidance for empty input - All states (loading, empty, error) properly handled Closes #29 --- CURRENT_STATE.md | 19 ++- apps/web/src/app/inspector/page.tsx | 216 +++++++++++++++++----------- 2 files changed, 148 insertions(+), 87 deletions(-) diff --git a/CURRENT_STATE.md b/CURRENT_STATE.md index 5f4b3c6..5a4681d 100644 --- a/CURRENT_STATE.md +++ b/CURRENT_STATE.md @@ -126,7 +126,7 @@ report exported. CLI and web inspector. - ✅ Workspace dependencies on `@ocpp-debugkit/core`, `scenarios`, `reporter` - ✅ `"private": true` (never publishable to npm) -### Landing Page + Inspector (in progress — this PR) +### Landing Page + Inspector (PR #43) - ✅ Landing page: hero, features, what-it's-not, architecture, quick start, footer - ✅ Inspector: trace paste textarea + file upload + sample scenario selector @@ -134,8 +134,15 @@ report exported. CLI and web inspector. - ✅ Inspector: message inspector panel (raw + normalized fields) - ✅ Inspector: failure summary (severity, description, suggested steps) - ✅ Inspector: Markdown report export (download) -- ✅ Inspector: loading/empty/error states -- ✅ Browser-local processing only (all parsing client-side) + +### Inspector Polish (in progress — this PR) + +- ✅ Loading state with spinner ("Parsing trace…") +- ✅ Error state improvements (non-sensitive messages, empty input guidance) +- ✅ Responsive layout (mobile-friendly: flex-wrap, smaller text on small screens) +- ✅ Keyboard navigation (arrow up/down to move through events) +- ✅ Sticky header for better UX on long traces +- ✅ Analyze button shows "Analyzing…" and disables during parsing ## What's Next @@ -146,9 +153,9 @@ report exported. CLI and web inspector. 5. **Issue #24** → complete (PR #40): reporter package (Markdown report generator) 6. **Issue #25** → complete (PR #41): CLI package (scaffold + inspect + report + scenario commands) 7. **Issue #26** → complete (PR #42): Next.js app scaffold (Next.js + Tailwind, routes for /, /inspector, /docs) -8. **Issue #27** (this PR) → complete: Landing page (hero, features, architecture, quick start, footer) -9. **Issue #28** (this PR) → complete: Inspector (trace input + timeline + message inspector + failures + report export) -10. **Issue #29**: Inspector polish (responsive, keyboard nav, loading states) +8. **Issue #27** → complete (PR #43): Landing page (hero, features, architecture, quick start, footer) +9. **Issue #28** → complete (PR #43): Inspector (trace input + timeline + message inspector + failures + report export) +10. **Issue #29** (this PR) → complete: Inspector polish (loading states, responsive, keyboard nav) 11. **Issue #30**: Playwright smoke tests ## Known Blockers / Decisions Pending diff --git a/apps/web/src/app/inspector/page.tsx b/apps/web/src/app/inspector/page.tsx index a9708d6..41228b0 100644 --- a/apps/web/src/app/inspector/page.tsx +++ b/apps/web/src/app/inspector/page.tsx @@ -1,6 +1,6 @@ 'use client'; -import { useState, useCallback, useMemo } from 'react'; +import { useState, useCallback, useMemo, useRef } from 'react'; import { parseTrace, buildSessionTimeline, @@ -39,6 +39,8 @@ interface AnalysisState { export default function InspectorPage() { const [input, setInput] = useState(''); const [analysis, setAnalysis] = useState(null); + const [isAnalyzing, setIsAnalyzing] = useState(false); + const timelineRef = useRef(null); const selectedEvent = useMemo(() => { if (!analysis || !analysis.selectedEventId) return null; @@ -54,39 +56,46 @@ export default function InspectorPage() { summaries: [], warnings: [], selectedEventId: null, - error: 'Input is empty.', + error: 'Input is empty. Paste a trace or select a sample scenario.', }); return; } - try { - const result = parseTrace(input); - const sessions = buildSessionTimeline(result.events); - const failures = detectFailures(result.events, sessions); - const summaries = summarizeSessions(sessions, failures); + setIsAnalyzing(true); - setAnalysis({ - events: result.events, - sessions, - failures, - summaries, - warnings: result.warnings, - selectedEventId: null, - error: null, - }); - } catch (e) { - const message = - e instanceof ParseError ? e.message : e instanceof Error ? e.message : 'Unknown error'; - setAnalysis({ - events: [], - sessions: [], - failures: [], - summaries: [], - warnings: [], - selectedEventId: null, - error: message, - }); - } + // Use setTimeout to let the loading state render before heavy parsing + setTimeout(() => { + try { + const result = parseTrace(input); + const sessions = buildSessionTimeline(result.events); + const failures = detectFailures(result.events, sessions); + const summaries = summarizeSessions(sessions, failures); + + setAnalysis({ + events: result.events, + sessions, + failures, + summaries, + warnings: result.warnings, + selectedEventId: null, + error: null, + }); + } catch (e) { + const message = + e instanceof ParseError ? e.message : e instanceof Error ? e.message : 'Unknown error'; + setAnalysis({ + events: [], + sessions: [], + failures: [], + summaries: [], + warnings: [], + selectedEventId: null, + error: message, + }); + } finally { + setIsAnalyzing(false); + } + }, 0); }, [input]); const handleFileUpload = useCallback(async (file: File) => { @@ -98,7 +107,7 @@ export default function InspectorPage() { summaries: [], warnings: [], selectedEventId: null, - error: `File size exceeds ${MAX_INPUT_SIZE_BYTES} bytes.`, + error: `File size (${file.size} bytes) exceeds the maximum allowed size (${MAX_INPUT_SIZE_BYTES} bytes).`, }); return; } @@ -128,16 +137,51 @@ export default function InspectorPage() { URL.revokeObjectURL(url); }, [analysis]); + // Keyboard navigation: arrow up/down to move between events + const handleKeyDown = useCallback( + (e: React.KeyboardEvent) => { + if (!analysis || analysis.events.length === 0) return; + if (e.key !== 'ArrowDown' && e.key !== 'ArrowUp') return; + + e.preventDefault(); + const currentIndex = analysis.events.findIndex((ev) => ev.id === analysis.selectedEventId); + + let nextIndex: number; + if (e.key === 'ArrowDown') { + nextIndex = currentIndex < 0 ? 0 : Math.min(currentIndex + 1, analysis.events.length - 1); + } else { + nextIndex = currentIndex <= 0 ? 0 : currentIndex - 1; + } + + const nextEvent = analysis.events[nextIndex]; + if (nextEvent) { + setAnalysis({ ...analysis, selectedEventId: nextEvent.id }); + // Scroll the timeline to the selected event + const btn = timelineRef.current?.querySelector( + `[data-event-id="${nextEvent.id}"]`, + ); + btn?.scrollIntoView({ block: 'nearest', behavior: 'smooth' }); + } + }, + [analysis], + ); + return ( -
+
{/* Header */} -
-
-

OCPP Inspector

+
+
+

+ OCPP Inspector +

{analysis && analysis.events.length > 0 && ( @@ -145,9 +189,9 @@ export default function InspectorPage() {
-
+
{/* Input section */} -
+
{/* Trace input */}