fix(web): keep wide network-entry URLs from overflowing the column (#1623)
Long request URLs stretched every Network card to their own max-content width, bleeding the cards (and their status badges) past the pinned monitoring column. Mantine's ScrollArea `content` slot defaults to `min-width: max-content`, so a non-wrapping URL forced the whole list wider than its viewport. Add an opt-in `constrainContentWidth` prop to EmbeddableScrollArea that relaxes that slot to `min-width: 0`, so the list can't grow past its viewport and each row manages its own overflow — the URL already lives in a horizontal ScrollArea, which now actually scrolls. Scoped to the Network stream panel (Logs/Protocol rows wrap or truncate, so they keep the default where a long line scrolls the list). Verified in Storybook (embedded 380px column): cards clamp to the viewport, no list/page horizontal overflow, and the URL scrolls within its own area. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BrihGWcrM9JGRyu41nzZYw
This commit is contained in:
parent
9824055425
commit
20e4ce7b43
4 changed files with 83 additions and 1 deletions
|
|
@ -31,4 +31,27 @@ describe("EmbeddableScrollArea", () => {
|
||||||
);
|
);
|
||||||
expect(ref.current).toBeInstanceOf(HTMLElement);
|
expect(ref.current).toBeInstanceOf(HTMLElement);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it.each([false, true])(
|
||||||
|
"constrains the content width when asked (embedded=%s)",
|
||||||
|
(embedded) => {
|
||||||
|
const ref = createRef<HTMLDivElement>();
|
||||||
|
renderWithMantine(
|
||||||
|
<EmbeddableScrollArea
|
||||||
|
embedded={embedded}
|
||||||
|
viewportRef={ref}
|
||||||
|
constrainContentWidth
|
||||||
|
>
|
||||||
|
<div>constrained content</div>
|
||||||
|
</EmbeddableScrollArea>,
|
||||||
|
);
|
||||||
|
expect(screen.getByText("constrained content")).toBeInTheDocument();
|
||||||
|
// The Mantine ScrollArea `content` slot has its default
|
||||||
|
// `min-width: max-content` relaxed to 0 so a long non-wrapping row
|
||||||
|
// (e.g. a network URL) can't stretch the list past its viewport (#1623).
|
||||||
|
const content = document.querySelector(".mantine-ScrollArea-content");
|
||||||
|
expect(content).not.toBeNull();
|
||||||
|
expect((content as HTMLElement).style.minWidth).toBe("0");
|
||||||
|
},
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -17,8 +17,23 @@ export interface EmbeddableScrollAreaProps {
|
||||||
embedded: boolean;
|
embedded: boolean;
|
||||||
viewportRef: Ref<HTMLDivElement>;
|
viewportRef: Ref<HTMLDivElement>;
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
|
/**
|
||||||
|
* Constrain the scrolled content to the viewport width instead of letting it
|
||||||
|
* grow to its own `max-content`. Mantine's ScrollArea `content` slot defaults
|
||||||
|
* to `min-width: max-content`, so a row with non-wrapping content (e.g. a long
|
||||||
|
* network URL) stretches every card past the column and it bleeds out (#1623).
|
||||||
|
* When true, the content can shrink to the viewport and each row must manage
|
||||||
|
* its own overflow (the Network URL scrolls inside its own inner ScrollArea).
|
||||||
|
* Left off for panels whose rows already wrap/truncate (Logs, Protocol), where
|
||||||
|
* the default lets a long line scroll the list horizontally instead.
|
||||||
|
*/
|
||||||
|
constrainContentWidth?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Relax the `content` slot's default `min-width: max-content` so the list can't
|
||||||
|
// grow wider than its viewport; see `constrainContentWidth`.
|
||||||
|
const CONSTRAIN_CONTENT_STYLES = { content: { minWidth: 0 } } as const;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The scroll region shared by the Logs / Protocol / Network stream panels, which
|
* The scroll region shared by the Logs / Protocol / Network stream panels, which
|
||||||
* render both full-size (their own tab) and embedded (the monitoring column).
|
* render both full-size (their own tab) and embedded (the monitoring column).
|
||||||
|
|
@ -28,7 +43,9 @@ export function EmbeddableScrollArea({
|
||||||
embedded,
|
embedded,
|
||||||
viewportRef,
|
viewportRef,
|
||||||
children,
|
children,
|
||||||
|
constrainContentWidth = false,
|
||||||
}: EmbeddableScrollAreaProps) {
|
}: EmbeddableScrollAreaProps) {
|
||||||
|
const styles = constrainContentWidth ? CONSTRAIN_CONTENT_STYLES : undefined;
|
||||||
if (embedded) {
|
if (embedded) {
|
||||||
return (
|
return (
|
||||||
<Stack flex={1} mih={0} gap={0}>
|
<Stack flex={1} mih={0} gap={0}>
|
||||||
|
|
@ -37,6 +54,7 @@ export function EmbeddableScrollArea({
|
||||||
mah="100%"
|
mah="100%"
|
||||||
type="scroll"
|
type="scroll"
|
||||||
offsetScrollbars
|
offsetScrollbars
|
||||||
|
styles={styles}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</ScrollArea.Autosize>
|
</ScrollArea.Autosize>
|
||||||
|
|
@ -49,6 +67,7 @@ export function EmbeddableScrollArea({
|
||||||
mah={FULLSIZE_MAH}
|
mah={FULLSIZE_MAH}
|
||||||
type="scroll"
|
type="scroll"
|
||||||
offsetScrollbars
|
offsetScrollbars
|
||||||
|
styles={styles}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</ScrollArea.Autosize>
|
</ScrollArea.Autosize>
|
||||||
|
|
|
||||||
|
|
@ -57,3 +57,39 @@ export const WithEntries: Story = {
|
||||||
export const Empty: Story = {
|
export const Empty: Story = {
|
||||||
args: { entries: [] },
|
args: { entries: [] },
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const longUrlSample: FetchRequestEntry[] = [
|
||||||
|
{
|
||||||
|
id: "n-long-1",
|
||||||
|
timestamp: new Date("2026-03-17T10:00:00Z"),
|
||||||
|
method: "GET",
|
||||||
|
url: "https://example-server.modelcontextprotocol.io/.well-known/oauth-protected-resource/mcp",
|
||||||
|
requestHeaders: {},
|
||||||
|
responseStatus: 404,
|
||||||
|
duration: 84,
|
||||||
|
category: "auth",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "n-long-2",
|
||||||
|
timestamp: new Date("2026-03-17T10:00:05Z"),
|
||||||
|
method: "GET",
|
||||||
|
url: "https://example-server.modelcontextprotocol.io/.well-known/oauth-authorization-server",
|
||||||
|
requestHeaders: {},
|
||||||
|
responseStatus: 200,
|
||||||
|
duration: 79,
|
||||||
|
category: "auth",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// Reproduces the pinned-column overflow: a long URL must scroll horizontally
|
||||||
|
// inside its own area rather than widening the card past the column (#1623).
|
||||||
|
export const EmbeddedLongUrls: Story = {
|
||||||
|
args: { entries: longUrlSample, embedded: true },
|
||||||
|
decorators: [
|
||||||
|
(Story) => (
|
||||||
|
<div style={{ width: 380, height: 500, display: "flex" }}>
|
||||||
|
<Story />
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
|
||||||
|
|
@ -140,7 +140,11 @@ export function NetworkStreamPanel({
|
||||||
{!hasResults ? (
|
{!hasResults ? (
|
||||||
<EmptyState>No network requests</EmptyState>
|
<EmptyState>No network requests</EmptyState>
|
||||||
) : (
|
) : (
|
||||||
<EmbeddableScrollArea embedded={embedded} viewportRef={viewportRef}>
|
<EmbeddableScrollArea
|
||||||
|
embedded={embedded}
|
||||||
|
viewportRef={viewportRef}
|
||||||
|
constrainContentWidth
|
||||||
|
>
|
||||||
<Stack gap="md">
|
<Stack gap="md">
|
||||||
{filteredEntries.map((entry) => (
|
{filteredEntries.map((entry) => (
|
||||||
<NetworkEntry
|
<NetworkEntry
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue