perfViz/src/App.tsx
Michael Mainguy 359e8a1bd3 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>
2025-08-11 12:18:31 -05:00

303 lines
9.7 KiB
TypeScript

import { useState, useEffect, useCallback } from 'react'
import './App.css'
import TraceViewer from './components/TraceViewer'
import PhaseViewer from './components/PhaseViewer'
import HTTPRequestViewer from './components/httprequestviewer/HTTPRequestViewer'
import RequestDebugger from './components/RequestDebugger'
import TraceUpload from './components/TraceUpload'
import TraceSelector from './components/TraceSelector'
import { traceDatabase } from './utils/traceDatabase'
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')
const [currentView, setCurrentView] = useState<AppView>('http')
const [selectedTraceId, setSelectedTraceId] = useState<string | null>(null)
const [, setHasTraces] = useState<boolean>(false)
const [dbInitialized, setDbInitialized] = useState(false)
// 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 {
// Initialize the database
await traceDatabase.init()
setDbInitialized(true)
// Check if we have any traces
const traces = await traceDatabase.getAllTraces()
setHasTraces(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) {
console.error('Failed to initialize database:', error)
setMode('upload') // Fallback to upload mode
setDbInitialized(true)
}
}
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 = () => {
setMode('upload')
}
if (!dbInitialized) {
return (
<div style={{
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
minHeight: '100vh',
fontFamily: 'system-ui, sans-serif'
}}>
<div>Initializing database...</div>
</div>
)
}
if (mode === 'upload') {
return <TraceUpload onUploadSuccess={handleUploadSuccess} />
}
if (mode === 'selector') {
return (
<TraceSelector
onTraceSelect={handleTraceSelect}
onUploadNew={handleUploadNew}
/>
)
}
// Analysis mode - show the main interface
return (
<>
<div>
<div style={{
padding: '10px 20px',
background: '#f8f9fa',
borderBottom: '1px solid #dee2e6',
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center'
}}>
<div style={{ display: 'flex', alignItems: 'center', gap: '15px' }}>
<button
onClick={handleBackToSelector}
style={{
background: 'transparent',
border: '1px solid #6c757d',
color: '#6c757d',
padding: '6px 12px',
borderRadius: '4px',
cursor: 'pointer',
fontSize: '12px'
}}
>
Back to Traces
</button>
<h1 style={{ margin: '0', color: '#495057' }}>Perf Viz</h1>
</div>
<nav style={{ display: 'flex', gap: '10px' }}>
<button
onClick={() => {
setCurrentView('trace')
updateUrlWithTraceId(selectedTraceId, 'trace', null)
}}
style={{
background: currentView === 'trace' ? '#007bff' : '#6c757d',
color: 'white',
border: 'none',
padding: '8px 16px',
borderRadius: '4px',
cursor: 'pointer',
fontSize: '14px'
}}
>
Trace Stats
</button>
<button
onClick={() => {
setCurrentView('phases')
updateUrlWithTraceId(selectedTraceId, 'phases', null)
}}
style={{
background: currentView === 'phases' ? '#007bff' : '#6c757d',
color: 'white',
border: 'none',
padding: '8px 16px',
borderRadius: '4px',
cursor: 'pointer',
fontSize: '14px'
}}
>
Phase Events
</button>
<button
onClick={() => {
setCurrentView('http')
updateUrlWithTraceId(selectedTraceId, 'http', null)
}}
style={{
background: currentView === 'http' ? '#007bff' : '#6c757d',
color: 'white',
border: 'none',
padding: '8px 16px',
borderRadius: '4px',
cursor: 'pointer',
fontSize: '14px'
}}
>
HTTP Requests
</button>
<button
onClick={() => {
setCurrentView('debug')
updateUrlWithTraceId(selectedTraceId, 'debug', null)
}}
style={{
background: currentView === 'debug' ? '#007bff' : '#6c757d',
color: 'white',
border: 'none',
padding: '8px 16px',
borderRadius: '4px',
cursor: 'pointer',
fontSize: '14px'
}}
>
Request Debug
</button>
</nav>
</div>
{currentView === 'trace' && (
<TraceViewer traceId={selectedTraceId} />
)}
{currentView === 'phases' && (
<PhaseViewer traceId={selectedTraceId} />
)}
{currentView === 'http' && (
<HTTPRequestViewer traceId={selectedTraceId} />
)}
{currentView === 'debug' && traceData && (
<RequestDebugger traceEvents={traceData.traceEvents} />
)}
</div>
</>
)
}
export default App