diff --git a/src/App.tsx b/src/App.tsx index 1a2040a..31574d8 100644 --- a/src/App.tsx +++ b/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('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() {