mirror of
https://github.com/jkingsman/Remote-Terminal-for-MeshCore.git
synced 2026-11-03 08:02:19 +01:00
Add packet feed filters; closes #169.
This commit is contained in:
2 files changed
+180
-27
No files matched your search
@@ -11,11 +11,14 @@ import {
|
|||||||
Cell,
|
Cell,
|
||||||
} from 'recharts';
|
} from 'recharts';
|
||||||
|
|
||||||
|
import { MeshCoreDecoder, Utils } from '@michaelhart/meshcore-decoder';
|
||||||
|
|
||||||
import { RawPacketList } from './RawPacketList';
|
import { RawPacketList } from './RawPacketList';
|
||||||
import { RawPacketInspectorDialog } from './RawPacketDetailModal';
|
import { RawPacketInspectorDialog } from './RawPacketDetailModal';
|
||||||
import { Button } from './ui/button';
|
import { Button } from './ui/button';
|
||||||
import type { Channel, Contact, RawPacket } from '../types';
|
import type { Channel, Contact, RawPacket } from '../types';
|
||||||
import {
|
import {
|
||||||
|
KNOWN_PAYLOAD_TYPES,
|
||||||
RAW_PACKET_STATS_WINDOWS,
|
RAW_PACKET_STATS_WINDOWS,
|
||||||
buildRawPacketStatsSnapshot,
|
buildRawPacketStatsSnapshot,
|
||||||
type NeighborStat,
|
type NeighborStat,
|
||||||
@@ -24,9 +27,26 @@ import {
|
|||||||
type RawPacketStatsSessionState,
|
type RawPacketStatsSessionState,
|
||||||
type RawPacketStatsWindow,
|
type RawPacketStatsWindow,
|
||||||
} from '../utils/rawPacketStats';
|
} from '../utils/rawPacketStats';
|
||||||
|
import { createDecoderOptions } from '../utils/rawPacketInspector';
|
||||||
import { getContactDisplayName } from '../utils/pubkey';
|
import { getContactDisplayName } from '../utils/pubkey';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
|
const KNOWN_PAYLOAD_TYPE_SET = new Set<string>(KNOWN_PAYLOAD_TYPES);
|
||||||
|
|
||||||
|
function getPacketTypeName(
|
||||||
|
packet: RawPacket,
|
||||||
|
decoderOptions?: ReturnType<typeof createDecoderOptions>
|
||||||
|
): string {
|
||||||
|
try {
|
||||||
|
const decoded = MeshCoreDecoder.decode(packet.data, decoderOptions);
|
||||||
|
if (!decoded.isValid) return 'Unknown';
|
||||||
|
const name = Utils.getPayloadTypeName(decoded.payloadType);
|
||||||
|
return KNOWN_PAYLOAD_TYPE_SET.has(name) ? name : 'Unknown';
|
||||||
|
} catch {
|
||||||
|
return 'Unknown';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
interface RawPacketFeedViewProps {
|
interface RawPacketFeedViewProps {
|
||||||
packets: RawPacket[];
|
packets: RawPacket[];
|
||||||
rawPacketStatsSession: RawPacketStatsSessionState;
|
rawPacketStatsSession: RawPacketStatsSessionState;
|
||||||
@@ -428,6 +448,48 @@ export function RawPacketFeedView({
|
|||||||
const [nowSec, setNowSec] = useState(() => Math.floor(Date.now() / 1000));
|
const [nowSec, setNowSec] = useState(() => Math.floor(Date.now() / 1000));
|
||||||
const [selectedPacket, setSelectedPacket] = useState<RawPacket | null>(null);
|
const [selectedPacket, setSelectedPacket] = useState<RawPacket | null>(null);
|
||||||
const [analyzeModalOpen, setAnalyzeModalOpen] = useState(false);
|
const [analyzeModalOpen, setAnalyzeModalOpen] = useState(false);
|
||||||
|
const [mobileFiltersOpen, setMobileFiltersOpen] = useState(false);
|
||||||
|
const [enabledTypes, setEnabledTypes] = useState<Set<string>>(() => new Set(KNOWN_PAYLOAD_TYPES));
|
||||||
|
|
||||||
|
const decoderOptions = useMemo(() => createDecoderOptions(channels), [channels]);
|
||||||
|
|
||||||
|
const packetsWithTypes = useMemo(
|
||||||
|
() =>
|
||||||
|
packets.map((packet) => ({
|
||||||
|
packet,
|
||||||
|
payloadType: getPacketTypeName(packet, decoderOptions),
|
||||||
|
})),
|
||||||
|
[packets, decoderOptions]
|
||||||
|
);
|
||||||
|
|
||||||
|
const allTypesEnabled = enabledTypes.size === KNOWN_PAYLOAD_TYPES.length;
|
||||||
|
|
||||||
|
const filteredPackets = useMemo(() => {
|
||||||
|
if (allTypesEnabled) return packets;
|
||||||
|
return packetsWithTypes
|
||||||
|
.filter(({ payloadType }) => enabledTypes.has(payloadType))
|
||||||
|
.map(({ packet }) => packet);
|
||||||
|
}, [packetsWithTypes, enabledTypes, packets, allTypesEnabled]);
|
||||||
|
|
||||||
|
const handleToggleAll = () => {
|
||||||
|
setEnabledTypes(allTypesEnabled ? new Set() : new Set(KNOWN_PAYLOAD_TYPES));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleToggleType = (type: string) => {
|
||||||
|
setEnabledTypes((prev) => {
|
||||||
|
const next = new Set(prev);
|
||||||
|
if (next.has(type)) {
|
||||||
|
next.delete(type);
|
||||||
|
} else {
|
||||||
|
next.add(type);
|
||||||
|
}
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleOnly = (type: string) => {
|
||||||
|
setEnabledTypes(new Set([type]));
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const interval = window.setInterval(() => {
|
const interval = window.setInterval(() => {
|
||||||
@@ -468,38 +530,129 @@ export function RawPacketFeedView({
|
|||||||
);
|
);
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="flex items-center justify-between gap-3 border-b border-border px-4 py-2.5">
|
<div className="border-b border-border px-4 py-2.5">
|
||||||
<div>
|
<div className="flex items-center justify-between gap-3">
|
||||||
<h2 className="font-semibold text-base text-foreground">Raw Packet Feed</h2>
|
<div>
|
||||||
<p className="text-xs text-muted-foreground">
|
<h2 className="font-semibold text-base text-foreground">Raw Packet Feed</h2>
|
||||||
Collecting stats since {formatTimestamp(rawPacketStatsSession.sessionStartedAt)}
|
<p className="hidden md:block text-xs text-muted-foreground">
|
||||||
</p>
|
Collecting stats since {formatTimestamp(rawPacketStatsSession.sessionStartedAt)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => setAnalyzeModalOpen(true)}
|
||||||
|
>
|
||||||
|
Analyze Packet
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => setStatsOpen((current) => !current)}
|
||||||
|
aria-expanded={statsOpen}
|
||||||
|
>
|
||||||
|
{statsOpen ? (
|
||||||
|
<ChevronRight className="h-4 w-4" />
|
||||||
|
) : (
|
||||||
|
<ChevronLeft className="h-4 w-4" />
|
||||||
|
)}
|
||||||
|
{statsOpen ? 'Hide Stats' : 'Show Stats'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<p className="md:hidden text-xs text-muted-foreground">
|
||||||
<Button
|
Collecting stats since {formatTimestamp(rawPacketStatsSession.sessionStartedAt)}
|
||||||
type="button"
|
{!mobileFiltersOpen && (
|
||||||
variant="outline"
|
<>
|
||||||
size="sm"
|
{' · '}
|
||||||
onClick={() => setAnalyzeModalOpen(true)}
|
<button
|
||||||
>
|
type="button"
|
||||||
Analyze Packet
|
className="text-primary hover:text-primary/80 transition-colors"
|
||||||
</Button>
|
onClick={() => setMobileFiltersOpen(true)}
|
||||||
<Button
|
>
|
||||||
type="button"
|
Show Filters
|
||||||
variant="outline"
|
</button>
|
||||||
size="sm"
|
</>
|
||||||
onClick={() => setStatsOpen((current) => !current)}
|
)}
|
||||||
aria-expanded={statsOpen}
|
</p>
|
||||||
>
|
|
||||||
{statsOpen ? <ChevronRight className="h-4 w-4" /> : <ChevronLeft className="h-4 w-4" />}
|
{mobileFiltersOpen && (
|
||||||
{statsOpen ? 'Hide Stats' : 'Show Stats'}
|
<div className="mt-1.5 md:hidden flex flex-wrap items-center gap-x-3 gap-y-1">
|
||||||
</Button>
|
<label className="flex items-center gap-1 text-xs text-muted-foreground cursor-pointer">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={allTypesEnabled}
|
||||||
|
onChange={handleToggleAll}
|
||||||
|
className="rounded"
|
||||||
|
/>
|
||||||
|
All
|
||||||
|
</label>
|
||||||
|
{KNOWN_PAYLOAD_TYPES.map((type) => (
|
||||||
|
<span key={type} className="inline-flex items-center gap-1 text-xs">
|
||||||
|
<label className="flex items-center gap-1 text-foreground cursor-pointer">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={enabledTypes.has(type)}
|
||||||
|
onChange={() => handleToggleType(type)}
|
||||||
|
className="rounded"
|
||||||
|
/>
|
||||||
|
{type}
|
||||||
|
</label>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="text-[0.625rem] text-muted-foreground hover:text-primary transition-colors"
|
||||||
|
onClick={() => handleOnly(type)}
|
||||||
|
>
|
||||||
|
(only)
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="mt-1.5 hidden md:flex flex-wrap items-center gap-x-3 gap-y-1">
|
||||||
|
<label className="flex items-center gap-1 text-xs text-muted-foreground cursor-pointer">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={allTypesEnabled}
|
||||||
|
onChange={handleToggleAll}
|
||||||
|
className="rounded"
|
||||||
|
/>
|
||||||
|
All
|
||||||
|
</label>
|
||||||
|
{KNOWN_PAYLOAD_TYPES.map((type) => (
|
||||||
|
<span key={type} className="inline-flex items-center gap-1 text-xs">
|
||||||
|
<label className="flex items-center gap-1 text-foreground cursor-pointer">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={enabledTypes.has(type)}
|
||||||
|
onChange={() => handleToggleType(type)}
|
||||||
|
className="rounded"
|
||||||
|
/>
|
||||||
|
{type}
|
||||||
|
</label>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="text-[0.625rem] text-muted-foreground hover:text-primary transition-colors"
|
||||||
|
onClick={() => handleOnly(type)}
|
||||||
|
>
|
||||||
|
(only)
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex min-h-0 flex-1 flex-col md:flex-row">
|
<div className="flex min-h-0 flex-1 flex-col md:flex-row">
|
||||||
<div className={cn('min-h-0 min-w-0 flex-1', statsOpen && 'md:border-r md:border-border')}>
|
<div className={cn('min-h-0 min-w-0 flex-1', statsOpen && 'md:border-r md:border-border')}>
|
||||||
<RawPacketList packets={packets} channels={channels} onPacketClick={setSelectedPacket} />
|
<RawPacketList
|
||||||
|
packets={filteredPackets}
|
||||||
|
channels={channels}
|
||||||
|
onPacketClick={setSelectedPacket}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<aside
|
<aside
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ const RAW_PACKET_STATS_WINDOW_SECONDS: Record<Exclude<RawPacketStatsWindow, 'ses
|
|||||||
|
|
||||||
export const MAX_RAW_PACKET_STATS_OBSERVATIONS = 20000;
|
export const MAX_RAW_PACKET_STATS_OBSERVATIONS = 20000;
|
||||||
|
|
||||||
const KNOWN_PAYLOAD_TYPES = [
|
export const KNOWN_PAYLOAD_TYPES = [
|
||||||
'Advert',
|
'Advert',
|
||||||
'GroupText',
|
'GroupText',
|
||||||
'TextMessage',
|
'TextMessage',
|
||||||
|
|||||||
Reference in new issue
Block a user