Add the two-line packet table row

This commit is contained in:
MrAlders0n
2026-07-27 19:10:07 -04:00
parent dbed149679
commit 8795c360ff
2 changed files with 140 additions and 0 deletions
+72
View File
@@ -0,0 +1,72 @@
import { formatHex } from "../../lib/formatters";
import { Timestamp } from "../../components/Timestamp";
import { Badge } from "../../components/Badge";
import { ScopeTag } from "../../components/ScopeTag";
import { payloadTypeVariant } from "../../components/badge-utils";
import { PAYLOAD_TYPE_NAMES, type PayloadTypeValue } from "../../types/enums";
import type { PacketSummary } from "../../types/api";
import { GRID_TEMPLATE } from "./packet-grid";
import { PacketPathLine } from "./PacketPathLine";
interface PacketTableRowProps {
packet: PacketSummary;
expanded: boolean;
isFresh?: boolean;
onToggle: () => void;
}
// two-line table row: line 1 is the identity grid (shares GRID_TEMPLATE with the header), line 2
// is the latest path. The whole row is the expansion click target.
export function PacketTableRow({ packet, expanded, isFresh, onToggle }: PacketTableRowProps) {
const observer = packet.latestObserver;
return (
<div
className={`border-b ${
expanded
? "border-primary bg-primary/10"
: isFresh
? "packet-fresh border-border-subtle"
: "border-border-subtle hover:bg-bg-raised/50"
}`}
>
<button
type="button"
onClick={onToggle}
aria-expanded={expanded}
className="grid w-full items-center gap-x-3 px-3 py-1.5 text-left text-[11px] cursor-pointer"
style={{ gridTemplateColumns: GRID_TEMPLATE }}
>
<span className={`text-text-dim transition-transform ${expanded ? "rotate-90" : ""}`} aria-hidden>
</span>
<span className="font-mono text-xs font-semibold text-primary tracking-wider">
{formatHex(packet.packetHash)}
</span>
<span>
<Badge variant={payloadTypeVariant(packet.payloadType)}>
{PAYLOAD_TYPE_NAMES[packet.payloadType as PayloadTypeValue] ?? packet.payloadTypeName}
</Badge>
</span>
<span className="font-mono text-[10px] text-text-muted uppercase tracking-wider">
{packet.routeTypeName || "Unknown"}
{packet.scope && <ScopeTag>{packet.scope}</ScopeTag>}
</span>
<span className="font-mono text-text-muted">×{packet.observationCount}</span>
<span className="text-text-normal truncate">
{observer ? (observer.displayName ?? observer.id.slice(0, 8)) : <span className="text-text-dim"></span>}
</span>
<span className="font-mono font-bold text-primary tracking-wider">
{observer?.iata ?? <span className="text-text-dim font-normal"></span>}
</span>
<span className="text-right text-text-muted">
<Timestamp value={packet.lastHeardAt} />
</span>
</button>
<div className="px-3 pb-1.5 pl-[2.25rem]">
<PacketPathLine packet={packet} />
</div>
</div>
);
}
@@ -0,0 +1,68 @@
import { describe, it, expect, vi } from "vitest";
import { render, screen, fireEvent } from "@testing-library/react";
import { PacketTableRow } from "../../../src/features/packets/PacketTableRow";
import type { PacketSummary } from "../../../src/types/api";
const pkt = (over: Partial<PacketSummary> = {}): PacketSummary => ({
packetHash: "AA11BB22", payloadType: 1, payloadTypeName: "ADVERT",
routeType: 1, routeTypeName: "FLOOD",
firstHeardAt: 1700000000, lastHeardAt: 1700000000, observationCount: 3, ...over,
});
describe("PacketTableRow", () => {
it("exposes one button carrying the expansion state", () => {
render(<PacketTableRow packet={pkt()} expanded={false} onToggle={() => {}} />);
const btn = screen.getByRole("button");
expect(btn).toHaveAttribute("aria-expanded", "false");
});
it("toggles on click", () => {
const onToggle = vi.fn();
render(<PacketTableRow packet={pkt()} expanded={false} onToggle={onToggle} />);
fireEvent.click(screen.getByRole("button"));
expect(onToggle).toHaveBeenCalledOnce();
});
it("renders line 2 even when there is no path data, so row height is constant", () => {
render(<PacketTableRow packet={pkt()} expanded={false} onToggle={() => {}} />);
expect(screen.getByText("n/a")).toBeInTheDocument();
});
it("falls back to the observer id when there is no display name", () => {
render(<PacketTableRow packet={pkt({ latestObserver: { id: "abcdef1234", iata: "YVR" } })} expanded={false} onToggle={() => {}} />);
expect(screen.getByText("abcdef12")).toBeInTheDocument();
expect(screen.getByText("YVR")).toBeInTheDocument();
});
it("prefers the observer display name over the id", () => {
render(<PacketTableRow packet={pkt({ latestObserver: { id: "abcdef1234", displayName: "Cypress Peak", iata: "YVR" } })} expanded={false} onToggle={() => {}} />);
expect(screen.getByText("Cypress Peak")).toBeInTheDocument();
expect(screen.queryByText("abcdef12")).not.toBeInTheDocument();
});
it("reflects the expanded state on the button and chevron", () => {
render(<PacketTableRow packet={pkt()} expanded={true} onToggle={() => {}} />);
expect(screen.getByRole("button")).toHaveAttribute("aria-expanded", "true");
expect(screen.getByText("")).toHaveClass("rotate-90");
});
it("marks a fresh row with the pulse class", () => {
const { container } = render(<PacketTableRow packet={pkt()} expanded={false} isFresh onToggle={() => {}} />);
expect(container.querySelector(".packet-fresh")).toBeInTheDocument();
});
it("renders a scope tag when the packet has a scope", () => {
render(<PacketTableRow packet={pkt({ scope: "#bc" })} expanded={false} onToggle={() => {}} />);
expect(screen.getByText("#bc")).toBeInTheDocument();
});
it("falls back to the raw payload type name for an unrecognized payload type", () => {
render(<PacketTableRow packet={pkt({ payloadType: 99, payloadTypeName: "CUSTOM_99" })} expanded={false} onToggle={() => {}} />);
expect(screen.getByText("CUSTOM_99")).toBeInTheDocument();
});
it("falls back to Unknown when routeTypeName is empty", () => {
render(<PacketTableRow packet={pkt({ routeTypeName: "" })} expanded={false} onToggle={() => {}} />);
expect(screen.getByText("Unknown")).toBeInTheDocument();
});
});