Open the analyzer from an observation and add a hash size column

Drops the Open analyzer and Copy link buttons from the expansion; the analyzer popup keeps its own copy link. Table rows butt up against each other now, so the whole strip is a click target.
This commit is contained in:
MrAlders0n committed 2026-07-27 22:23:53 -04:00
1 parent 1f3fb5b7c9
commit b2adc5276f
10 files changed
+76 -78

No files matched your search

+8 -10
View File
@@ -2,7 +2,6 @@ import { useCallback, useMemo } from "react";
import type { PacketSummary } from "../../types/api";
import { formatPropagation } from "../../lib/formatters";
import { Timestamp } from "../../components/Timestamp";
import { CopyLinkButton } from "../../components/CopyLinkButton";
import { usePacketDetail } from "./usePacketDetail";
import { ObservationTable } from "./ObservationTable";
import { buildPacketPaths } from "../map/packet-path";
@@ -35,10 +34,13 @@ export function PacketExpansion({ packet, onOpenAnalyzer, onViewPath, selectedOb
// than showing a blank (0-row) skeleton while it loads.
const noObservations = packet.observationCount === 0;
const emptyState = <div className="text-[10px] text-text-dim py-2">No observations</div>;
// null drops ?analyze, so a link copied while the drawer is open still restores just the row
const copyParams = useCallback(
() => ({ tab: "Packets", hash: packet.packetHash, analyze: null }),
[packet.packetHash],
// Picking an observation is the way into the analyzer — it opens on the one you clicked.
const handleSelectObservation = useCallback(
(id: number) => {
onSelectObservation(id);
onOpenAnalyzer();
},
[onSelectObservation, onOpenAnalyzer],
);
return (
@@ -53,9 +55,6 @@ export function PacketExpansion({ packet, onOpenAnalyzer, onViewPath, selectedOb
<span>first <Timestamp value={packet.firstHeardAt} /></span>
<span>last <Timestamp value={packet.lastHeardAt} /></span>
<span>spread {formatPropagation(spread)}</span>
<button type="button" onClick={onOpenAnalyzer} disabled={!ready} className={ACTION_BUTTON_CLASS}>
Open analyzer
</button>
<button
type="button"
onClick={onViewPath}
@@ -65,7 +64,6 @@ export function PacketExpansion({ packet, onOpenAnalyzer, onViewPath, selectedOb
>
View path on map
</button>
<CopyLinkButton params={copyParams} ariaLabel="Copy row link" />
</div>
<div className="max-h-[360px] overflow-y-auto">
@@ -91,7 +89,7 @@ export function PacketExpansion({ packet, onOpenAnalyzer, onViewPath, selectedOb
) : data && data.observations.length === 0 ? (
emptyState
) : data ? (
<ObservationTable observations={data.observations} selectedId={selectedObservationId} onSelect={onSelectObservation} />
<ObservationTable observations={data.observations} selectedId={selectedObservationId} onSelect={handleSelectObservation} />
) : null}
</div>
</div>
@@ -13,6 +13,7 @@ export function PacketTableHeader() {
<span>Route</span>
<span>Obs</span>
<span>Hops</span>
<span>Hash Size</span>
<span>Src → Dst</span>
<span>IATA</span>
<span className="text-right">Age</span>
+5 -5
View File
@@ -19,7 +19,8 @@ interface PacketTableRowProps {
// expansion instead, which frees the wide column for the packet's endpoints.
export function PacketTableRow({ packet, expanded, isFresh, onToggle }: PacketTableRowProps) {
// ?? not ||, so a legitimate 0-hop direct packet still shows its count
const hopCount = packet.latestObserver?.pathLength?.hopCount;
const pathLength = packet.latestObserver?.pathLength;
const na = <span className="text-text-dim">n/a</span>;
return (
<div
@@ -54,14 +55,13 @@ export function PacketTableRow({ packet, expanded, isFresh, onToggle }: PacketTa
{packet.scope && <ScopeTag>{packet.scope}</ScopeTag>}
</span>
<span className="font-mono text-text-muted">×{packet.observationCount}</span>
<span className="font-mono text-text-muted">
{hopCount ?? <span className="text-text-dim">n/a</span>}
</span>
<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">
<PacketEndpoints packet={packet} />
</span>
<span className="font-mono font-bold text-primary tracking-wider">
{packet.latestObserver?.iata ?? <span className="text-text-dim font-normal">n/a</span>}
{packet.latestObserver?.iata ?? <span className="font-normal">{na}</span>}
</span>
<span className="text-right text-text-muted">
<Timestamp value={packet.lastHeardAt} />
+3 -2
View File
@@ -55,7 +55,7 @@ export function PacketVirtualList({
count: packets.length,
getScrollElement: () => parentRef.current,
// a collapsed row: one grid line on desktop, a taller card below md. Expanded rows are remeasured.
estimateSize: () => (isMobile ? 64 : 43),
estimateSize: () => (isMobile ? 64 : 37),
overscan: 10,
getItemKey: (index) => packets[index]?.packetHash ?? index,
});
@@ -121,7 +121,8 @@ export function PacketVirtualList({
transform: `translateY(${virtualRow.start}px)`,
}}
>
<div className="pt-1.5">
{/* cards need breathing room; table rows butt up so the whole strip is a click target */}
<div className={isMobile ? "pt-1.5" : ""}>
{isMobile ? (
<PacketRow
packet={packet}
+3 -1
View File
@@ -2,4 +2,6 @@
// track font- and content-independent: the header and the rows are separate grids, so a `ch` track
// resolves against each one's own font size (9px vs 11px) and an `auto` track against its own text
// ("HASH" vs "4AE77F09") — either silently drifts the two apart.
export const GRID_TEMPLATE = "1.25rem 5rem 6rem 5rem 3rem 3rem minmax(6rem,1fr) 3.5rem 5rem";
// Endpoints take minmax(0,1fr) so they absorb every bit of squeeze and truncate, rather than
// pushing IATA and Age off the edge on a narrow viewport.
export const GRID_TEMPLATE = "1.25rem 5rem 6rem 5rem 3rem 3rem 4rem minmax(0,1fr) 3.5rem 5rem";