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:
parent
2e533925a2
commit
359e8a1bd3
109
src/App.tsx
109
src/App.tsx
@ -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',
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user