feat(packets): display payload summaries in packet rows (#46)

This commit is contained in:
n30nex
2026-09-18 06:09:59 -04:00
committed by GitHub
parent 7549bacd21
commit 34bd0948bc
10 changed files with 55 additions and 2 deletions
+2
View File
@@ -54,6 +54,8 @@ export function PacketRow({ packet, expanded, isFresh, onToggle }: PacketRowProp
</Tooltip>
</div>
{packet.summary && <div className="mt-1 truncate text-[11px] text-text-bright" title={packet.summary}>{packet.summary}</div>}
<div className="flex flex-wrap items-center gap-x-2 gap-y-1 mt-1 text-[11px] text-text-dim">
<span className="font-mono text-[11px] text-text-muted uppercase tracking-wider bg-text-muted/8 px-1.5 py-px rounded-sm">
{packet.routeTypeName || "Unknown"}
+1 -1
View File
@@ -14,7 +14,7 @@ export function PacketTableHeader() {
<span>Obs</span>
<span>Hops</span>
<span>Hash Size</span>
<span>Src → Dst</span>
<span className="truncate" title="Summary and source / destination">Summary / Src → Dst</span>
<span>IATA</span>
<span className="text-right">Age</span>
</div>
+1
View File
@@ -58,6 +58,7 @@ export function PacketTableRow({ packet, expanded, isFresh, onToggle }: PacketTa
<span className="font-mono text-text-muted">{pathLength?.hopCount ?? na}</span>
<span className="font-mono text-text-muted">{pathLength?.hashSize ?? na}</span>
<span className="min-w-0 overflow-hidden">
{packet.summary && <span className="block truncate text-text-bright" title={packet.summary}>{packet.summary}</span>}
<PacketEndpoints packet={packet} />
</span>
<span className="font-mono font-bold text-primary tracking-wider">
+1
View File
@@ -163,6 +163,7 @@ export function usePackets(frozen: boolean = false, serverFilter: PacketServerFi
lastHeardAt: data.observation.heardAt,
observationCount: data.packet.observationCount,
scope: data.packet.scope,
summary: data.packet.summary,
latestObserver: {
id: data.observation.observerId,
displayName: data.observation.observerName,
+1
View File
@@ -35,6 +35,7 @@ export interface PacketSummary {
observationCount: number;
latestObserver?: LatestObserver;
scope?: string; // matched transport scope name, e.g. "#bc"
summary?: string; // packet-derived display text, currently the advertised name
}
export interface ResolvedNode {
+1
View File
@@ -51,6 +51,7 @@ export interface WsPacketObservation {
isFirstObservation: boolean;
observationCount: number;
scope?: string; // matched transport scope name; omitted when none matched
summary?: string;
};
observation: {
observerId: string;
@@ -6,7 +6,7 @@ import { GRID_TEMPLATE } from "../../../src/features/packets/packet-grid";
describe("PacketTableHeader", () => {
it("declares every column heading", () => {
render(<PacketTableHeader />);
for (const h of ["Hash", "Type", "Route", "Obs", "Hops", "Hash Size", "Src → Dst", "IATA", "Age"]) {
for (const h of ["Hash", "Type", "Route", "Obs", "Hops", "Hash Size", "Summary / Src → Dst", "IATA", "Age"]) {
expect(screen.getByText(h)).toBeInTheDocument();
}
});
@@ -23,6 +23,13 @@ const observer = (
};
describe("PacketTableRow", () => {
it("keeps endpoints visible alongside a payload summary", () => {
render(<PacketTableRow packet={pkt({ summary: "Packet summary", latestObserver: observer({ resolvedSource: node("Source node"), resolvedDestination: node("Destination node") }) })} expanded={false} onToggle={() => {}} />);
expect(screen.getByText("Packet summary")).toBeInTheDocument();
expect(screen.getByText("Source node")).toBeInTheDocument();
expect(screen.getByText("Destination node")).toBeInTheDocument();
});
it("exposes one button carrying the expansion state", () => {
render(<PacketTableRow packet={pkt()} expanded={false} onToggle={() => {}} />);
const btn = screen.getByRole("button");
@@ -0,0 +1,26 @@
import { describe, it, expect, vi } from "vitest";
import { render, screen, fireEvent } from "@testing-library/react";
import { PacketRow } from "../../../src/features/packets/PacketRow";
import { PacketTableRow } from "../../../src/features/packets/PacketTableRow";
const packet = {
packetHash: "AA11BB22", payloadType: 4, payloadTypeName: "ADVERT",
routeType: 1, routeTypeName: "FLOOD", firstHeardAt: 1, lastHeardAt: 2, observationCount: 1,
};
describe.each([["mobile", PacketRow], ["desktop", PacketTableRow]] as const)("%s packet summary", (_name, Row) => {
it("shows the packet's advertised name and keeps row selection working", () => {
const toggle = vi.fn();
render(<Row packet={{ ...packet, summary: "MD00-Repeater 📡" }} expanded={false} onToggle={toggle} />);
const summary = screen.getByText("MD00-Repeater 📡");
expect(summary).toHaveAttribute("title", "MD00-Repeater 📡");
fireEvent.click(summary);
expect(toggle).toHaveBeenCalledTimes(1);
});
it("supports rows from an older server without summaries", () => {
render(<Row packet={packet} expanded={false} onToggle={() => {}} />);
expect(screen.getByText(/AA11/)).toBeInTheDocument();
expect(screen.queryByText("MD00-Repeater 📡")).not.toBeInTheDocument();
});
});
@@ -360,6 +360,20 @@ describe("usePackets live heard window", () => {
const flushRaf = () => rafCallbacks.splice(0).forEach((cb) => cb(0));
it("keeps summaries from history and passes summaries through live observations", async () => {
getPackets.mockResolvedValue({ items: [{ ...packet("history"), summary: "Historical advert" }], nextCursor: null });
const { result } = renderHook(() => usePackets(), { wrapper });
await waitFor(() => expect(result.current.allPackets.find((p) => p.packetHash === "history")?.summary).toBe("Historical advert"));
const event = observation("live");
event.packet.summary = "Live advert 📡";
act(() => {
result.current.handlePacketObservation(event);
flushRaf();
});
expect(result.current.allPackets.find((p) => p.packetHash === "live")?.summary).toBe("Live advert 📡");
expect(result.current.allPackets.find((p) => p.packetHash === "history")?.summary).toBe("Historical advert");
});
// Each WS message carries only its own heardAt, so a second observation of the same packet used to
// collapse the window to a single instant — the expanded row then read "spread 0.000s".
it("widens the heard window across observations instead of collapsing it", async () => {