mirror of
https://github.com/MeshCore-Beacon/beacon-web.git
synced 2026-10-18 04:39:53 +00:00
feat(packets): display payload summaries in packet rows (#46)
This commit is contained in:
@@ -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"}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
Reference in New Issue
Block a user