feat(web): compact two-line HistoryEntry/NetworkEntry for the narrow column (#1616)

In the pinned monitoring column (embedded), the single-line entry headers wrap
badly. Add compact two-line variants gated on `embedded`:

- HistoryEntry: line 1 = time + client/server badge + elapsed + status; line 2 =
  method (and target, truncated) on the left with the controls on the right,
  where Replay becomes an icon (new ReplayButton element) next to Pin/Expand.
- NetworkEntry: line 1 = time + HTTP method + category + elapsed + status; line 2
  = the URL in a horizontal ScrollArea (new Text `nowrap` variant) with the
  expand toggle on the right.

The full-size (non-embedded) layouts are unchanged. `embedded` is threaded from
the History/Network panels into the entries.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
cliffhall 2026-07-08 20:53:23 -04:00
commit 3550b64032
2 changed files with 325 additions and 58 deletions

View file

@ -31,6 +31,20 @@ describe("NetworkEntry", () => {
expect(screen.getByText("transport")).toBeInTheDocument();
});
it("renders the compact two-line layout (line 1 meta, line 2 URL) when embedded", () => {
renderWithMantine(
<NetworkEntry entry={baseEntry} isListExpanded={false} embedded />,
);
// Line 1: method, category, duration, status.
expect(screen.getByText("POST")).toBeInTheDocument();
expect(screen.getByText("transport")).toBeInTheDocument();
expect(screen.getByText("45ms")).toBeInTheDocument();
expect(screen.getByText("200 OK")).toBeInTheDocument();
// Line 2: the URL (in its scroll area) and the expand toggle.
expect(screen.getByText("https://example.com/mcp")).toBeInTheDocument();
expect(screen.getByRole("button", { name: "Expand" })).toBeInTheDocument();
});
it("shows body / header detail when expanded", async () => {
const user = userEvent.setup();
renderWithMantine(

View file

@ -6,6 +6,7 @@ import {
Collapse,
Divider,
Group,
ScrollArea,
Stack,
Table,
Text,
@ -19,6 +20,12 @@ import { maskSecretsInBody } from "../../../utils/maskSecrets";
export interface NetworkEntryProps {
entry: FetchRequestEntry;
isListExpanded: boolean;
/**
* Compact two-line header for the narrow monitoring column (#1616): line 1 is
* time + method + category + duration + status; line 2 is the URL in a
* horizontal scroll area with the expand toggle on the right.
*/
embedded?: boolean;
}
const EntryContainer = Card.withProps({
@ -43,6 +50,26 @@ const UrlText = Text.withProps({
truncate: "end",
});
// Compact-header URL: never wraps, so a long URL scrolls horizontally inside its
// ScrollArea instead of wrapping to many lines.
const UrlScroll = Text.withProps({
size: "sm",
fw: 500,
variant: "nowrap",
});
// Left / right clusters for a compact header line (mirrors HistoryEntry).
const HeaderCluster = Group.withProps({
gap: "sm",
wrap: "nowrap",
miw: 0,
});
const ControlsCluster = Group.withProps({
gap: "sm",
wrap: "nowrap",
});
const DurationText = Text.withProps({
size: "sm",
c: "dimmed",
@ -197,7 +224,11 @@ function BodyPreview({
);
}
export function NetworkEntry({ entry, isListExpanded }: NetworkEntryProps) {
export function NetworkEntry({
entry,
isListExpanded,
embedded = false,
}: NetworkEntryProps) {
const [isExpanded, setIsExpanded] = useState(isListExpanded);
// The list-level Expand/Collapse toggle is authoritative: each time the
@ -210,33 +241,70 @@ export function NetworkEntry({ entry, isListExpanded }: NetworkEntryProps) {
setIsExpanded(isListExpanded);
}, [isListExpanded]);
const metaBadges = (
<>
{entry.duration != null && (
<DurationText>{formatDuration(entry.duration)}</DurationText>
)}
{isLongLivedStream(entry) && <Badge color="orange">SSE</Badge>}
<Badge color={statusColor(entry)}>{statusLabel(entry)}</Badge>
</>
);
const expandToggle = (
<ExpandToggle
expanded={isExpanded}
onToggle={() => setIsExpanded((v) => !v)}
/>
);
return (
<EntryContainer>
<Stack gap="sm">
<HeaderRow>
<Group gap="sm" wrap="nowrap" miw={0} flex={1}>
<TimestampText>{formatTimestamp(entry.timestamp)}</TimestampText>
<Badge color="dark">{entry.method}</Badge>
<Badge color={categoryColor(entry.category)} variant="light">
{entry.category}
</Badge>
<UrlText>{entry.url}</UrlText>
</Group>
<Group gap="sm" wrap="nowrap">
{entry.duration != null && (
<DurationText>{formatDuration(entry.duration)}</DurationText>
)}
{isLongLivedStream(entry) && <Badge color="orange">SSE</Badge>}
<Badge color={statusColor(entry)}>{statusLabel(entry)}</Badge>
</Group>
</HeaderRow>
{embedded ? (
// Compact two-line header for the narrow column.
<Stack gap="xs">
<HeaderRow>
<HeaderCluster>
<TimestampText>
{formatTimestamp(entry.timestamp)}
</TimestampText>
<Badge color="dark">{entry.method}</Badge>
<Badge color={categoryColor(entry.category)} variant="light">
{entry.category}
</Badge>
</HeaderCluster>
<ControlsCluster>{metaBadges}</ControlsCluster>
</HeaderRow>
<Group gap="xs" wrap="nowrap" justify="space-between">
<ScrollArea type="hover" scrollbarSize={6} flex={1} miw={0}>
<UrlScroll>{entry.url}</UrlScroll>
</ScrollArea>
{expandToggle}
</Group>
</Stack>
) : (
<>
<HeaderRow>
<Group gap="sm" wrap="nowrap" miw={0} flex={1}>
<TimestampText>
{formatTimestamp(entry.timestamp)}
</TimestampText>
<Badge color="dark">{entry.method}</Badge>
<Badge color={categoryColor(entry.category)} variant="light">
{entry.category}
</Badge>
<UrlText>{entry.url}</UrlText>
</Group>
<Group gap="sm" wrap="nowrap">
{metaBadges}
</Group>
</HeaderRow>
<Group gap="xs" justify="flex-end">
<ExpandToggle
expanded={isExpanded}
onToggle={() => setIsExpanded((v) => !v)}
/>
</Group>
<Group gap="xs" justify="flex-end">
{expandToggle}
</Group>
</>
)}
<Collapse in={isExpanded}>
<Stack gap="sm">