diff --git a/src/App.tsx b/src/App.tsx index 28fcb79..a39ee0b 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -256,6 +256,23 @@ function AppInner() { if (id === null) dropSelectionParam("observer"); }, [dropSelectionParam]); + // Jump from a node's detail panel to its location on the Map tab, framed close on that node. + // Bypasses handleTabChange (like handleViewObserverStats) so selectedNodeId survives the switch on + // mobile too — the point of this action is to keep the same node's panel open, now over the map. + const handleViewOnMap = useCallback( + (lat: number, lng: number) => { + setSearchParams((prev) => { + const next = new URLSearchParams(prev); + next.set("tab", "Map"); + next.set("lat", lat.toFixed(5)); + next.set("lng", lng.toFixed(5)); + next.set("zoom", "14"); + return next; + }); + }, + [setSearchParams], + ); + // Jump from an observer's detail panel to its telemetry on the Stats tab (Stats → Observer, preselected). const handleViewObserverStats = useCallback( (id: string) => { @@ -340,6 +357,7 @@ function AppInner() { }} onViewNode={setSelectedNodeId} onAnalyzePacket={setOverlayPacketHash} + onViewOnMap={handleViewOnMap} /> )} {overlayNodeId && ( @@ -351,6 +369,15 @@ function AppInner() { setSelectedObserverId(observerId); }} onViewNode={setOverlayNodeId} + onViewOnMap={(lat, lng) => { + // handleViewOnMap bypasses handleTabChange (see its own comment), so the overlay + // won't be cleared as a side effect here the way onViewObserver's handleTabChange + // call clears it above — close it and promote the node into the shared selection + // explicitly, so NodeDetailPanel (not this overlay) is what reopens on the Map tab. + handleViewOnMap(lat, lng); + setSelectedNodeId(overlayNodeId); + setOverlayNodeId(null); + }} /> )} {overlayPacketHash && ( diff --git a/src/features/nodes/NodeDetailOverlay.tsx b/src/features/nodes/NodeDetailOverlay.tsx index 514c949..ef448de 100644 --- a/src/features/nodes/NodeDetailOverlay.tsx +++ b/src/features/nodes/NodeDetailOverlay.tsx @@ -4,11 +4,12 @@ import { ModalOverlay } from "../../components/ModalOverlay"; // Node detail shown as a modal over the packet analyzer: the panel sits where the packet drawer is // (right side) and the rest dims, so a user can peek at a path hop's node and close back to the packet. -export function NodeDetailOverlay({ nodeId, onClose, onViewObserver, onViewNode }: { +export function NodeDetailOverlay({ nodeId, onClose, onViewObserver, onViewNode, onViewOnMap }: { nodeId: string; onClose: () => void; onViewObserver: (observerId: string) => void; onViewNode?: (nodeId: string) => void; + onViewOnMap?: (lat: number, lng: number) => void; }) { useEffect(() => { function onKey(e: KeyboardEvent) { @@ -20,7 +21,7 @@ export function NodeDetailOverlay({ nodeId, onClose, onViewObserver, onViewNode return ( - + ); } diff --git a/src/features/nodes/NodeDetailPanel.tsx b/src/features/nodes/NodeDetailPanel.tsx index 2073738..72245c3 100644 --- a/src/features/nodes/NodeDetailPanel.tsx +++ b/src/features/nodes/NodeDetailPanel.tsx @@ -73,9 +73,10 @@ interface NodeDetailPanelProps { onViewObserver: (observerId: string) => void; onViewNode?: (nodeId: string) => void; onAnalyzePacket?: (hash: string) => void; + onViewOnMap?: (lat: number, lng: number) => void; } -export function NodeDetailPanel({ nodeId, onClose, onViewObserver, onViewNode, onAnalyzePacket }: NodeDetailPanelProps) { +export function NodeDetailPanel({ nodeId, onClose, onViewObserver, onViewNode, onAnalyzePacket, onViewOnMap }: NodeDetailPanelProps) { const { data: node, isLoading } = useQuery({ queryKey: ["node", nodeId], queryFn: () => getNode(nodeId), @@ -145,6 +146,19 @@ export function NodeDetailPanel({ nodeId, onClose, onViewObserver, onViewNode, o {node.lng != null && } {node.locationSource && } + {hasLocation && onViewOnMap && ( + + )} )}