mirror of
https://github.com/jkingsman/Remote-Terminal-for-MeshCore.git
synced 2026-07-31 22:12:34 +02:00
Add packet analyzer launch to visualizer feed. Closes #328.
This commit is contained in:
@@ -242,7 +242,12 @@ export function ConversationPane({
|
||||
if (activeConversation.type === 'visualizer') {
|
||||
return (
|
||||
<Suspense fallback={<LoadingPane label="Loading visualizer..." />}>
|
||||
<VisualizerView packets={rawPackets} contacts={contacts} config={config} />
|
||||
<VisualizerView
|
||||
packets={rawPackets}
|
||||
contacts={contacts}
|
||||
channels={channels}
|
||||
config={config}
|
||||
/>
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -49,6 +49,8 @@ interface RawPacketInspectorDialogProps {
|
||||
description: string;
|
||||
notice?: ReactNode;
|
||||
signalOverride?: SignalOverride;
|
||||
/** Portal target; see `DialogContent`. Needed when a host pane uses `requestFullscreen()`. */
|
||||
container?: HTMLElement | null;
|
||||
}
|
||||
|
||||
interface RawPacketInspectionPanelProps {
|
||||
@@ -768,6 +770,7 @@ export function RawPacketInspectorDialog({
|
||||
description,
|
||||
notice,
|
||||
signalOverride,
|
||||
container,
|
||||
}: RawPacketInspectorDialogProps) {
|
||||
const [packetInput, setPacketInput] = useState('');
|
||||
|
||||
@@ -847,7 +850,10 @@ export function RawPacketInspectorDialog({
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="flex h-[92dvh] max-w-[min(96vw,82rem)] flex-col gap-0 overflow-hidden p-0">
|
||||
<DialogContent
|
||||
container={container}
|
||||
className="flex h-[92dvh] max-w-[min(96vw,82rem)] flex-col gap-0 overflow-hidden p-0"
|
||||
>
|
||||
<DialogHeader className="border-b border-border px-5 py-3">
|
||||
<DialogTitle>{title}</DialogTitle>
|
||||
<DialogDescription className="sr-only">{description}</DialogDescription>
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { Maximize2, Minimize2 } from 'lucide-react';
|
||||
import type { Contact, RawPacket, RadioConfig } from '../types';
|
||||
import type { Channel, Contact, RawPacket, RadioConfig } from '../types';
|
||||
import { PacketVisualizer3D } from './PacketVisualizer3D';
|
||||
import { RawPacketList } from './RawPacketList';
|
||||
import { RawPacketInspectorDialog } from './RawPacketDetailModal';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from './ui/tabs';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { getVisualizerSettings, saveVisualizerSettings } from '../utils/visualizerSettings';
|
||||
@@ -10,12 +11,14 @@ import { getVisualizerSettings, saveVisualizerSettings } from '../utils/visualiz
|
||||
interface VisualizerViewProps {
|
||||
packets: RawPacket[];
|
||||
contacts: Contact[];
|
||||
channels: Channel[];
|
||||
config: RadioConfig | null;
|
||||
}
|
||||
|
||||
export function VisualizerView({ packets, contacts, config }: VisualizerViewProps) {
|
||||
export function VisualizerView({ packets, contacts, channels, config }: VisualizerViewProps) {
|
||||
const [fullScreen, setFullScreen] = useState(() => getVisualizerSettings().hidePacketFeed);
|
||||
const [paneFullScreen, setPaneFullScreen] = useState(false);
|
||||
const [selectedPacket, setSelectedPacket] = useState<RawPacket | null>(null);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// Persist packet feed visibility to localStorage
|
||||
@@ -71,7 +74,11 @@ export function VisualizerView({ packets, contacts, config }: VisualizerViewProp
|
||||
<PacketVisualizer3D packets={packets} contacts={contacts} config={config} />
|
||||
</TabsContent>
|
||||
<TabsContent value="packets" className="flex-1 m-0 overflow-hidden">
|
||||
<RawPacketList packets={packets} />
|
||||
<RawPacketList
|
||||
packets={packets}
|
||||
channels={channels}
|
||||
onPacketClick={setSelectedPacket}
|
||||
/>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
@@ -106,11 +113,31 @@ export function VisualizerView({ packets, contacts, config }: VisualizerViewProp
|
||||
Packet Feed
|
||||
</div>
|
||||
<div className="flex-1 overflow-hidden">
|
||||
<RawPacketList packets={packets} />
|
||||
<RawPacketList
|
||||
packets={packets}
|
||||
channels={channels}
|
||||
onPacketClick={setSelectedPacket}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* While natively fullscreened, portal into the fullscreen element so the
|
||||
dialog stays visible; otherwise keep the default document.body target. */}
|
||||
<RawPacketInspectorDialog
|
||||
open={selectedPacket !== null}
|
||||
onOpenChange={(isOpen) => !isOpen && setSelectedPacket(null)}
|
||||
channels={channels}
|
||||
container={paneFullScreen ? containerRef.current : undefined}
|
||||
source={
|
||||
selectedPacket
|
||||
? { kind: 'packet', packet: selectedPacket }
|
||||
: { kind: 'loading', message: 'Loading packet...' }
|
||||
}
|
||||
title="Packet Details"
|
||||
description="Detailed byte and field breakdown for the selected raw packet."
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -33,13 +33,19 @@ interface DialogContentProps extends React.ComponentPropsWithoutRef<
|
||||
typeof DialogPrimitive.Content
|
||||
> {
|
||||
hideCloseButton?: boolean;
|
||||
/**
|
||||
* Portal target. Defaults to `document.body`. Pass the active fullscreen
|
||||
* element when rendering inside `requestFullscreen()`, otherwise the dialog
|
||||
* mounts outside that subtree and is invisible while still trapping focus.
|
||||
*/
|
||||
container?: HTMLElement | null;
|
||||
}
|
||||
|
||||
const DialogContent = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Content>,
|
||||
DialogContentProps
|
||||
>(({ className, children, hideCloseButton = false, ...props }, ref) => (
|
||||
<DialogPortal>
|
||||
>(({ className, children, hideCloseButton = false, container, ...props }, ref) => (
|
||||
<DialogPortal container={container ?? undefined}>
|
||||
<DialogOverlay />
|
||||
<DialogPrimitive.Content
|
||||
ref={ref}
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
import { fireEvent, render, screen } from '@testing-library/react';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { VisualizerView } from '../components/VisualizerView';
|
||||
import type { RawPacket } from '../types';
|
||||
|
||||
// The 3D scene needs WebGL, which jsdom does not provide.
|
||||
vi.mock('../components/PacketVisualizer3D', () => ({
|
||||
PacketVisualizer3D: () => <div data-testid="packet-visualizer-3d" />,
|
||||
}));
|
||||
|
||||
function createPacket(overrides: Partial<RawPacket> = {}): RawPacket {
|
||||
return {
|
||||
id: 1,
|
||||
timestamp: 1700000000,
|
||||
data: '000000000000',
|
||||
payload_type: 'REQ',
|
||||
snr: null,
|
||||
rssi: null,
|
||||
decrypted: false,
|
||||
decrypted_info: null,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe('VisualizerView packet feed', () => {
|
||||
beforeEach(() => {
|
||||
window.localStorage.clear();
|
||||
});
|
||||
|
||||
it('opens the packet analyzer when a feed packet is clicked', () => {
|
||||
render(
|
||||
<VisualizerView
|
||||
packets={[createPacket({ id: 7, observation_id: 21 })]}
|
||||
contacts={[]}
|
||||
channels={[]}
|
||||
config={null}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.queryByText('Packet Details')).not.toBeInTheDocument();
|
||||
|
||||
// Desktop split-pane and mobile tab both render the feed, so take the first.
|
||||
fireEvent.click(screen.getAllByRole('button', { name: /TF/ })[0]);
|
||||
|
||||
expect(screen.getByText('Packet Details')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('does not render the analyzer until a packet is selected', () => {
|
||||
render(<VisualizerView packets={[]} contacts={[]} channels={[]} config={null} />);
|
||||
|
||||
expect(screen.queryByText('Packet Details')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user