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 './App.css'
|
||||||
import TraceViewer from './components/TraceViewer'
|
import TraceViewer from './components/TraceViewer'
|
||||||
import PhaseViewer from './components/PhaseViewer'
|
import PhaseViewer from './components/PhaseViewer'
|
||||||
@ -11,6 +11,51 @@ import { useDatabaseTraceData } from './hooks/useDatabaseTraceData'
|
|||||||
|
|
||||||
type AppView = 'trace' | 'phases' | 'http' | 'debug'
|
type AppView = 'trace' | 'phases' | 'http' | 'debug'
|
||||||
type AppMode = 'selector' | 'upload' | 'analysis'
|
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() {
|
function App() {
|
||||||
const [mode, setMode] = useState<AppMode>('selector')
|
const [mode, setMode] = useState<AppMode>('selector')
|
||||||
@ -22,9 +67,27 @@ function App() {
|
|||||||
// Always call hooks at the top level
|
// Always call hooks at the top level
|
||||||
const { traceData } = useDatabaseTraceData(selectedTraceId)
|
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(() => {
|
useEffect(() => {
|
||||||
initializeApp()
|
initializeApp()
|
||||||
}, [])
|
|
||||||
|
// Listen for browser navigation (back/forward)
|
||||||
|
window.addEventListener('popstate', handlePopState)
|
||||||
|
return () => window.removeEventListener('popstate', handlePopState)
|
||||||
|
}, [handlePopState])
|
||||||
|
|
||||||
const initializeApp = async () => {
|
const initializeApp = async () => {
|
||||||
try {
|
try {
|
||||||
@ -36,10 +99,23 @@ function App() {
|
|||||||
const traces = await traceDatabase.getAllTraces()
|
const traces = await traceDatabase.getAllTraces()
|
||||||
setHasTraces(traces.length > 0)
|
setHasTraces(traces.length > 0)
|
||||||
|
|
||||||
// If no traces, show upload screen
|
// Check URL for existing trace parameter
|
||||||
if (traces.length === 0) {
|
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')
|
setMode('upload')
|
||||||
} else {
|
} else {
|
||||||
|
// Has traces, show selector
|
||||||
setMode('selector')
|
setMode('selector')
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -51,18 +127,23 @@ function App() {
|
|||||||
|
|
||||||
const handleTraceSelect = (traceId: string) => {
|
const handleTraceSelect = (traceId: string) => {
|
||||||
setSelectedTraceId(traceId)
|
setSelectedTraceId(traceId)
|
||||||
|
setCurrentView('http') // Default to HTTP view
|
||||||
setMode('analysis')
|
setMode('analysis')
|
||||||
|
updateUrlWithTraceId(traceId, 'http', null)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleUploadSuccess = (traceId: string) => {
|
const handleUploadSuccess = (traceId: string) => {
|
||||||
setSelectedTraceId(traceId)
|
setSelectedTraceId(traceId)
|
||||||
|
setCurrentView('http')
|
||||||
setMode('analysis')
|
setMode('analysis')
|
||||||
setHasTraces(true)
|
setHasTraces(true)
|
||||||
|
updateUrlWithTraceId(traceId, 'http', null)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleBackToSelector = () => {
|
const handleBackToSelector = () => {
|
||||||
setSelectedTraceId(null)
|
setSelectedTraceId(null)
|
||||||
setMode('selector')
|
setMode('selector')
|
||||||
|
window.history.pushState({}, '', '/')
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleUploadNew = () => {
|
const handleUploadNew = () => {
|
||||||
@ -128,7 +209,10 @@ function App() {
|
|||||||
|
|
||||||
<nav style={{ display: 'flex', gap: '10px' }}>
|
<nav style={{ display: 'flex', gap: '10px' }}>
|
||||||
<button
|
<button
|
||||||
onClick={() => setCurrentView('trace')}
|
onClick={() => {
|
||||||
|
setCurrentView('trace')
|
||||||
|
updateUrlWithTraceId(selectedTraceId, 'trace', null)
|
||||||
|
}}
|
||||||
style={{
|
style={{
|
||||||
background: currentView === 'trace' ? '#007bff' : '#6c757d',
|
background: currentView === 'trace' ? '#007bff' : '#6c757d',
|
||||||
color: 'white',
|
color: 'white',
|
||||||
@ -142,7 +226,10 @@ function App() {
|
|||||||
Trace Stats
|
Trace Stats
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => setCurrentView('phases')}
|
onClick={() => {
|
||||||
|
setCurrentView('phases')
|
||||||
|
updateUrlWithTraceId(selectedTraceId, 'phases', null)
|
||||||
|
}}
|
||||||
style={{
|
style={{
|
||||||
background: currentView === 'phases' ? '#007bff' : '#6c757d',
|
background: currentView === 'phases' ? '#007bff' : '#6c757d',
|
||||||
color: 'white',
|
color: 'white',
|
||||||
@ -156,7 +243,10 @@ function App() {
|
|||||||
Phase Events
|
Phase Events
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => setCurrentView('http')}
|
onClick={() => {
|
||||||
|
setCurrentView('http')
|
||||||
|
updateUrlWithTraceId(selectedTraceId, 'http', null)
|
||||||
|
}}
|
||||||
style={{
|
style={{
|
||||||
background: currentView === 'http' ? '#007bff' : '#6c757d',
|
background: currentView === 'http' ? '#007bff' : '#6c757d',
|
||||||
color: 'white',
|
color: 'white',
|
||||||
@ -170,7 +260,10 @@ function App() {
|
|||||||
HTTP Requests
|
HTTP Requests
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => setCurrentView('debug')}
|
onClick={() => {
|
||||||
|
setCurrentView('debug')
|
||||||
|
updateUrlWithTraceId(selectedTraceId, 'debug', null)
|
||||||
|
}}
|
||||||
style={{
|
style={{
|
||||||
background: currentView === 'debug' ? '#007bff' : '#6c757d',
|
background: currentView === 'debug' ? '#007bff' : '#6c757d',
|
||||||
color: 'white',
|
color: 'white',
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { useState, useMemo, useEffect } from 'react'
|
import { useState, useMemo, useEffect } from 'react'
|
||||||
import { useDatabaseTraceData } from '../../hooks/useDatabaseTraceData'
|
import { useDatabaseTraceData } from '../../hooks/useDatabaseTraceData'
|
||||||
|
import { getUrlParams, updateUrlWithTraceId } from '../../App'
|
||||||
import BabylonViewer from '../../BabylonViewer'
|
import BabylonViewer from '../../BabylonViewer'
|
||||||
import BabylonTimelineViewer from '../../BabylonTimelineViewer'
|
import BabylonTimelineViewer from '../../BabylonTimelineViewer'
|
||||||
import RequestFilters from './RequestFilters'
|
import RequestFilters from './RequestFilters'
|
||||||
@ -33,8 +34,42 @@ export default function HTTPRequestViewer({ traceId }: HTTPRequestViewerProps) {
|
|||||||
const [priorityFilter, setPriorityFilter] = useState<string>('all')
|
const [priorityFilter, setPriorityFilter] = useState<string>('all')
|
||||||
const [showQueueAnalysis, setShowQueueAnalysis] = useState(false)
|
const [showQueueAnalysis, setShowQueueAnalysis] = useState(false)
|
||||||
const [showScreenshots, setShowScreenshots] = useState(false)
|
const [showScreenshots, setShowScreenshots] = useState(false)
|
||||||
|
// 3D viewer state - initialized from URL
|
||||||
const [show3DViewer, setShow3DViewer] = useState(false)
|
const [show3DViewer, setShow3DViewer] = useState(false)
|
||||||
const [showTimelineViewer, setShowTimelineViewer] = 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 [ssimThreshold, setSsimThreshold] = useState(SSIM_SIMILARITY_THRESHOLD)
|
||||||
const [pendingSSIMThreshold, setPendingSSIMThreshold] = useState(SSIM_SIMILARITY_THRESHOLD)
|
const [pendingSSIMThreshold, setPendingSSIMThreshold] = useState(SSIM_SIMILARITY_THRESHOLD)
|
||||||
const [expandedRows, setExpandedRows] = useState<Set<string>>(new Set())
|
const [expandedRows, setExpandedRows] = useState<Set<string>>(new Set())
|
||||||
@ -264,8 +299,8 @@ export default function HTTPRequestViewer({ traceId }: HTTPRequestViewerProps) {
|
|||||||
setCurrentPage={setCurrentPage}
|
setCurrentPage={setCurrentPage}
|
||||||
setShowQueueAnalysis={setShowQueueAnalysis}
|
setShowQueueAnalysis={setShowQueueAnalysis}
|
||||||
setShowScreenshots={setShowScreenshots}
|
setShowScreenshots={setShowScreenshots}
|
||||||
setShow3DViewer={setShow3DViewer}
|
setShow3DViewer={handle3DViewerToggle}
|
||||||
setShowTimelineViewer={setShowTimelineViewer}
|
setShowTimelineViewer={handleTimelineViewerToggle}
|
||||||
setPendingSSIMThreshold={setPendingSSIMThreshold}
|
setPendingSSIMThreshold={setPendingSSIMThreshold}
|
||||||
handleSSIMRecalculate={handleSSIMRecalculate}
|
handleSSIMRecalculate={handleSSIMRecalculate}
|
||||||
/>
|
/>
|
||||||
@ -304,7 +339,7 @@ export default function HTTPRequestViewer({ traceId }: HTTPRequestViewerProps) {
|
|||||||
3D Network Visualization ({filteredRequests.length} requests)
|
3D Network Visualization ({filteredRequests.length} requests)
|
||||||
</h3>
|
</h3>
|
||||||
<button
|
<button
|
||||||
onClick={() => setShow3DViewer(false)}
|
onClick={() => handle3DViewerToggle(false)}
|
||||||
className={styles.modalCloseButton}
|
className={styles.modalCloseButton}
|
||||||
>
|
>
|
||||||
✕ Close
|
✕ Close
|
||||||
@ -343,7 +378,7 @@ export default function HTTPRequestViewer({ traceId }: HTTPRequestViewerProps) {
|
|||||||
3D Timeline Visualization ({filteredRequests.length} requests)
|
3D Timeline Visualization ({filteredRequests.length} requests)
|
||||||
</h3>
|
</h3>
|
||||||
<button
|
<button
|
||||||
onClick={() => setShowTimelineViewer(false)}
|
onClick={() => handleTimelineViewerToggle(false)}
|
||||||
className={styles.modalCloseButton}
|
className={styles.modalCloseButton}
|
||||||
>
|
>
|
||||||
✕ Close
|
✕ Close
|
||||||
|
Loading…
Reference in New Issue
Block a user