Add URL routing for 3D visualization views

- Extend path routing to support /[traceid]/[view]/[3dview] format
- Add 'network' and 'timeline' as 3D view parameters in URL
- Update HTTPRequestViewer to sync 3D view state with URL
- Handle browser back/forward navigation for 3D views
- Ensure only one 3D view is active at a time
- URLs now reflect selected 3D visualization state for sharing

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Michael Mainguy 2025-08-11 12:18:31 -05:00
parent 2e533925a2
commit 359e8a1bd3
2 changed files with 140 additions and 12 deletions

View File

@ -1,4 +1,4 @@
import { useState, useEffect } from 'react'
import { useState, useEffect, useCallback } from 'react'
import './App.css'
import TraceViewer from './components/TraceViewer'
import PhaseViewer from './components/PhaseViewer'
@ -11,6 +11,51 @@ import { useDatabaseTraceData } from './hooks/useDatabaseTraceData'
type AppView = 'trace' | 'phases' | 'http' | 'debug'
type AppMode = 'selector' | 'upload' | 'analysis'
type ThreeDView = 'network' | 'timeline' | null
// URL path utilities for /[traceid]/[view]/[3dview] routing
const updateUrlWithTraceId = (traceId: string | null, view: AppView = 'http', threeDView: ThreeDView = null) => {
if (!traceId) {
window.history.pushState({}, '', '/')
return
}
const path = threeDView ? `/${traceId}/${view}/${threeDView}` : `/${traceId}/${view}`
window.history.pushState({}, '', path)
}
const getUrlParams = () => {
const path = window.location.pathname
const segments = path.split('/').filter(Boolean) // Remove empty segments
if (segments.length >= 3) {
const traceId = segments[0]
const view = segments[1] as AppView
const threeDView = segments[2] as ThreeDView
// Validate view and 3D view values
const validViews: AppView[] = ['trace', 'phases', 'http', 'debug']
const validThreeDViews: (ThreeDView)[] = ['network', 'timeline']
const validatedView = validViews.includes(view) ? view : 'http'
const validatedThreeDView = validThreeDViews.includes(threeDView) ? threeDView : null
return { traceId, view: validatedView, threeDView: validatedThreeDView }
} else if (segments.length >= 2) {
const traceId = segments[0]
const view = segments[1] as AppView
// Validate view is one of the allowed values
const validViews: AppView[] = ['trace', 'phases', 'http', 'debug']
const validatedView = validViews.includes(view) ? view : 'http'
return { traceId, view: validatedView, threeDView: null }
} else if (segments.length === 1) {
// Only trace ID provided, default to http view
return { traceId: segments[0], view: 'http' as AppView, threeDView: null }
}
// Root path or invalid format
return { traceId: null, view: 'http' as AppView, threeDView: null }
}
// Export utility functions for use in other components
export { getUrlParams, updateUrlWithTraceId, type ThreeDView }
function App() {
const [mode, setMode] = useState<AppMode>('selector')
@ -22,9 +67,27 @@ function App() {
// Always call hooks at the top level
const { traceData } = useDatabaseTraceData(selectedTraceId)
// Handle browser back/forward navigation
const handlePopState = useCallback(() => {
const { traceId, view } = getUrlParams()
if (traceId && traceId !== selectedTraceId) {
setSelectedTraceId(traceId)
setCurrentView(view)
setMode('analysis')
// Note: threeDView will be handled by HTTPRequestViewer component
} else if (!traceId && selectedTraceId) {
setSelectedTraceId(null)
setMode('selector')
}
}, [selectedTraceId])
useEffect(() => {
initializeApp()
}, [])
// Listen for browser navigation (back/forward)
window.addEventListener('popstate', handlePopState)
return () => window.removeEventListener('popstate', handlePopState)
}, [handlePopState])
const initializeApp = async () => {
try {
@ -36,10 +99,23 @@ function App() {
const traces = await traceDatabase.getAllTraces()
setHasTraces(traces.length > 0)
// If no traces, show upload screen
if (traces.length === 0) {
// Check URL for existing trace parameter
const { traceId, view } = getUrlParams()
if (traceId && traces.some(t => t.id === traceId)) {
// Valid trace ID in URL, load it
setSelectedTraceId(traceId)
setCurrentView(view)
setMode('analysis')
// Note: threeDView will be handled by HTTPRequestViewer component
} else if (traceId) {
// Invalid trace ID in URL, clear it and show selector
window.history.replaceState({}, '', '/')
setMode(traces.length > 0 ? 'selector' : 'upload')
} else if (traces.length === 0) {
// No traces, show upload screen
setMode('upload')
} else {
// Has traces, show selector
setMode('selector')
}
} catch (error) {
@ -51,18 +127,23 @@ function App() {
const handleTraceSelect = (traceId: string) => {
setSelectedTraceId(traceId)
setCurrentView('http') // Default to HTTP view
setMode('analysis')
updateUrlWithTraceId(traceId, 'http', null)
}
const handleUploadSuccess = (traceId: string) => {
setSelectedTraceId(traceId)
setCurrentView('http')
setMode('analysis')
setHasTraces(true)
updateUrlWithTraceId(traceId, 'http', null)
}
const handleBackToSelector = () => {
setSelectedTraceId(null)
setMode('selector')
window.history.pushState({}, '', '/')
}
const handleUploadNew = () => {
@ -128,7 +209,10 @@ function App() {
<nav style={{ display: 'flex', gap: '10px' }}>
<button
onClick={() => setCurrentView('trace')}
onClick={() => {
setCurrentView('trace')
updateUrlWithTraceId(selectedTraceId, 'trace', null)
}}
style={{
background: currentView === 'trace' ? '#007bff' : '#6c757d',
color: 'white',
@ -142,7 +226,10 @@ function App() {
Trace Stats
</button>
<button
onClick={() => setCurrentView('phases')}
onClick={() => {
setCurrentView('phases')
updateUrlWithTraceId(selectedTraceId, 'phases', null)
}}
style={{
background: currentView === 'phases' ? '#007bff' : '#6c757d',
color: 'white',
@ -156,7 +243,10 @@ function App() {
Phase Events
</button>
<button
onClick={() => setCurrentView('http')}
onClick={() => {
setCurrentView('http')
updateUrlWithTraceId(selectedTraceId, 'http', null)
}}
style={{
background: currentView === 'http' ? '#007bff' : '#6c757d',
color: 'white',
@ -170,7 +260,10 @@ function App() {
HTTP Requests
</button>
<button
onClick={() => setCurrentView('debug')}
onClick={() => {
setCurrentView('debug')
updateUrlWithTraceId(selectedTraceId, 'debug', null)
}}
style={{
background: currentView === 'debug' ? '#007bff' : '#6c757d',
color: 'white',

View File

@ -1,5 +1,6 @@
import { useState, useMemo, useEffect } from 'react'
import { useDatabaseTraceData } from '../../hooks/useDatabaseTraceData'
import { getUrlParams, updateUrlWithTraceId } from '../../App'
import BabylonViewer from '../../BabylonViewer'
import BabylonTimelineViewer from '../../BabylonTimelineViewer'
import RequestFilters from './RequestFilters'
@ -33,8 +34,42 @@ export default function HTTPRequestViewer({ traceId }: HTTPRequestViewerProps) {
const [priorityFilter, setPriorityFilter] = useState<string>('all')
const [showQueueAnalysis, setShowQueueAnalysis] = useState(false)
const [showScreenshots, setShowScreenshots] = useState(false)
// 3D viewer state - initialized from URL
const [show3DViewer, setShow3DViewer] = useState(false)
const [showTimelineViewer, setShowTimelineViewer] = useState(false)
// Initialize 3D view state from URL on component mount and handle URL changes
useEffect(() => {
const updateFrom3DUrl = () => {
const { threeDView } = getUrlParams()
setShow3DViewer(threeDView === 'network')
setShowTimelineViewer(threeDView === 'timeline')
}
// Set initial state
updateFrom3DUrl()
// Listen for URL changes (back/forward navigation)
const handlePopState = () => updateFrom3DUrl()
window.addEventListener('popstate', handlePopState)
return () => window.removeEventListener('popstate', handlePopState)
}, [])
// Handle 3D view changes and update URL
const handle3DViewerToggle = (enabled: boolean) => {
const { traceId, view } = getUrlParams()
setShow3DViewer(enabled)
setShowTimelineViewer(false) // Ensure only one 3D view is active
updateUrlWithTraceId(traceId, view, enabled ? 'network' : null)
}
const handleTimelineViewerToggle = (enabled: boolean) => {
const { traceId, view } = getUrlParams()
setShowTimelineViewer(enabled)
setShow3DViewer(false) // Ensure only one 3D view is active
updateUrlWithTraceId(traceId, view, enabled ? 'timeline' : null)
}
const [ssimThreshold, setSsimThreshold] = useState(SSIM_SIMILARITY_THRESHOLD)
const [pendingSSIMThreshold, setPendingSSIMThreshold] = useState(SSIM_SIMILARITY_THRESHOLD)
const [expandedRows, setExpandedRows] = useState<Set<string>>(new Set())
@ -264,8 +299,8 @@ export default function HTTPRequestViewer({ traceId }: HTTPRequestViewerProps) {
setCurrentPage={setCurrentPage}
setShowQueueAnalysis={setShowQueueAnalysis}
setShowScreenshots={setShowScreenshots}
setShow3DViewer={setShow3DViewer}
setShowTimelineViewer={setShowTimelineViewer}
setShow3DViewer={handle3DViewerToggle}
setShowTimelineViewer={handleTimelineViewerToggle}
setPendingSSIMThreshold={setPendingSSIMThreshold}
handleSSIMRecalculate={handleSSIMRecalculate}
/>
@ -304,7 +339,7 @@ export default function HTTPRequestViewer({ traceId }: HTTPRequestViewerProps) {
3D Network Visualization ({filteredRequests.length} requests)
</h3>
<button
onClick={() => setShow3DViewer(false)}
onClick={() => handle3DViewerToggle(false)}
className={styles.modalCloseButton}
>
Close
@ -343,7 +378,7 @@ export default function HTTPRequestViewer({ traceId }: HTTPRequestViewerProps) {
3D Timeline Visualization ({filteredRequests.length} requests)
</h3>
<button
onClick={() => setShowTimelineViewer(false)}
onClick={() => handleTimelineViewerToggle(false)}
className={styles.modalCloseButton}
>
Close