- 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>
303 lines
9.7 KiB
TypeScript
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
|