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:
parent
bcf10c75dd
commit
3550b64032
2 changed files with 325 additions and 58 deletions
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
Loading…
Reference in a new issue