web: server-screen, footer, monitoring-sidebar, and responsive-header polish (#1683)
Some checks failed
Run install, format, lint, build, and test on every push / build (push) Has been cancelled
Run install, format, lint, build, and test on every push / publish (push) Has been cancelled
Run install, format, lint, build, and test on every push / publish-github-container-registry (push) Has been cancelled
Some checks failed
Run install, format, lint, build, and test on every push / build (push) Has been cancelled
Run install, format, lint, build, and test on every push / publish (push) Has been cancelled
Run install, format, lint, build, and test on every push / publish-github-container-registry (push) Has been cancelled
Closes #1682 Server-screen, footer, monitoring-sidebar, and responsive-header polish: connected-card highlight/scroll + Failed status, footer row + MCP-docs tooltip, Servers box, card-surface refresh (gray-0 entry cards + white code), monitoring-sidebar layout, a 1280px responsive floor honored by the header (always-icon Disconnect, always-SegmentedControl tabs, 1500px status text, always-available sidebar), a horizontally-scrollable long resource URI in the compact Protocol entry, and copy buttons beside resource URIs / network URLs. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
This commit is contained in:
parent
590359abc5
commit
8d33e76f97
3 changed files with 707 additions and 274 deletions
|
|
@ -1,4 +1,5 @@
|
|||
import type { Meta, StoryObj } from "@storybook/react-vite";
|
||||
import { Box } from "@mantine/core";
|
||||
import type { MessageEntry } from "../../../../../../core/mcp/types.js";
|
||||
import { fn } from "storybook/test";
|
||||
import { ProtocolEntry } from "./ProtocolEntry";
|
||||
|
|
@ -76,6 +77,36 @@ const resourceReadEntry: MessageEntry = {
|
|||
duration: 45,
|
||||
};
|
||||
|
||||
// A resources/read whose URI overflows the narrow monitoring column, so the
|
||||
// embedded header shows it in a horizontal scroll area rather than truncating.
|
||||
const longUriResourceEntry: MessageEntry = {
|
||||
id: "req-5",
|
||||
timestamp: new Date("2026-03-17T10:35:00Z"),
|
||||
direction: "request",
|
||||
origin: "client",
|
||||
message: {
|
||||
jsonrpc: "2.0",
|
||||
id: 5,
|
||||
method: "resources/read",
|
||||
params: {
|
||||
uri: "demo://resource/static/document/architecture/overview/system-design-and-data-flow.md",
|
||||
},
|
||||
},
|
||||
response: {
|
||||
jsonrpc: "2.0",
|
||||
id: 5,
|
||||
result: {
|
||||
contents: [
|
||||
{
|
||||
uri: "demo://resource/static/document/architecture/overview/system-design-and-data-flow.md",
|
||||
text: "# Architecture Overview",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
duration: 41,
|
||||
};
|
||||
|
||||
const pendingEntry: MessageEntry = {
|
||||
id: "req-4",
|
||||
timestamp: new Date("2026-03-17T10:34:00Z"),
|
||||
|
|
@ -128,3 +159,22 @@ export const Pending: Story = {
|
|||
isListExpanded: false,
|
||||
},
|
||||
};
|
||||
|
||||
// Embedded (monitoring-sidebar) layout with a long resource URI: the target sits
|
||||
// in a horizontal scroll area rather than truncating with an ellipsis. Wrapped
|
||||
// in the pinned column's width so the overflow is visible.
|
||||
export const EmbeddedLongUri: Story = {
|
||||
args: {
|
||||
entry: longUriResourceEntry,
|
||||
isPinned: false,
|
||||
isListExpanded: false,
|
||||
embedded: true,
|
||||
},
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<Box w={340}>
|
||||
<Story />
|
||||
</Box>
|
||||
),
|
||||
],
|
||||
};
|
||||
|
|
|
|||
|
|
@ -131,6 +131,22 @@ describe("ProtocolEntry", () => {
|
|||
expect(screen.getByText("resources/read")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("adds a Copy button beside a resource URI target, but not a tool name", () => {
|
||||
const { rerender } = renderWithMantine(
|
||||
<ProtocolEntry {...baseProps} entry={successEntry} />,
|
||||
);
|
||||
const withoutResource = screen.queryAllByRole("button", {
|
||||
name: "Copy",
|
||||
}).length;
|
||||
rerender(<ProtocolEntry {...baseProps} entry={resourceReadEntry} />);
|
||||
// The resource entry has exactly one more Copy button — the one beside its
|
||||
// URI target (both entries otherwise carry the same expanded ContentViewer
|
||||
// copy affordances).
|
||||
expect(screen.queryAllByRole("button", { name: "Copy" }).length).toBe(
|
||||
withoutResource + 1,
|
||||
);
|
||||
});
|
||||
|
||||
it("renders Error status when response has error", () => {
|
||||
renderWithMantine(<ProtocolEntry {...baseProps} entry={errorEntry} />);
|
||||
expect(screen.getByText("Error")).toBeInTheDocument();
|
||||
|
|
|
|||
|
|
@ -6,11 +6,13 @@ import {
|
|||
Collapse,
|
||||
Divider,
|
||||
Group,
|
||||
ScrollArea,
|
||||
Stack,
|
||||
Text,
|
||||
} from "@mantine/core";
|
||||
import type { MessageEntry } from "../../../../../../core/mcp/types.js";
|
||||
import { ContentViewer } from "../../elements/ContentViewer/ContentViewer";
|
||||
import { CopyButton } from "../../elements/CopyButton/CopyButton";
|
||||
import { MessageDirectionBadge } from "../../elements/MessageDirectionBadge/MessageDirectionBadge";
|
||||
import { MethodBadge } from "../../elements/MethodBadge/MethodBadge";
|
||||
import { ExpandToggle } from "../../elements/ExpandToggle/ExpandToggle";
|
||||
|
|
@ -67,6 +69,15 @@ const TargetText = Text.withProps({
|
|||
fw: 500,
|
||||
});
|
||||
|
||||
// Compact-header target (e.g. a long resource URI): never wraps, so it scrolls
|
||||
// horizontally inside its ScrollArea instead of truncating with an ellipsis
|
||||
// (mirrors NetworkEntry's URL).
|
||||
const TargetScroll = Text.withProps({
|
||||
size: "sm",
|
||||
fw: 500,
|
||||
variant: "nowrap",
|
||||
});
|
||||
|
||||
const DurationText = Text.withProps({
|
||||
size: "sm",
|
||||
c: "dimmed",
|
||||
|
|
@ -100,6 +111,15 @@ function extractTarget(entry: MessageEntry): string | undefined {
|
|||
return undefined;
|
||||
}
|
||||
|
||||
// The resource URI when the target is one (e.g. `resources/read`), so it can be
|
||||
// copied. Tool/prompt targets are plain names, not URIs, and get no copy button.
|
||||
function extractResourceUri(entry: MessageEntry): string | undefined {
|
||||
const msg = entry.message;
|
||||
if (!("params" in msg) || !msg.params) return undefined;
|
||||
const params = msg.params as Record<string, unknown>;
|
||||
return typeof params.uri === "string" ? params.uri : undefined;
|
||||
}
|
||||
|
||||
// The pending → OK/Error lifecycle only applies to requests: messageLogState
|
||||
// attaches a `response` to request entries by JSON-RPC id. A notification is
|
||||
// fire-and-forget (no id, no response, ever) and an unmatched standalone
|
||||
|
|
@ -141,6 +161,7 @@ export function ProtocolEntry({
|
|||
const [isExpanded, setIsExpanded] = useState(isListExpanded);
|
||||
const method = extractMethod(entry);
|
||||
const target = extractTarget(entry);
|
||||
const resourceUri = extractResourceUri(entry);
|
||||
const status = extractStatus(entry);
|
||||
const canReplay = isReplayableProtocolMethod(method);
|
||||
|
||||
|
|
@ -184,9 +205,21 @@ export function ProtocolEntry({
|
|||
<HeaderCluster flex={1}>
|
||||
<MethodBadge method={method} />
|
||||
{target && (
|
||||
<TargetText truncate="end" miw={0}>
|
||||
{target}
|
||||
</TargetText>
|
||||
<>
|
||||
{resourceUri && <CopyButton value={resourceUri} />}
|
||||
<ScrollArea
|
||||
scrollbarSize={6}
|
||||
flex={1}
|
||||
miw={0}
|
||||
// The target scrolls horizontally but has no focusable
|
||||
// child, so make the viewport itself keyboard-scrollable
|
||||
// (WCAG SC 2.1.1). Scrollbar auto-hides via the
|
||||
// `type="scroll"` theme default.
|
||||
viewportProps={{ tabIndex: 0 }}
|
||||
>
|
||||
<TargetScroll>{target}</TargetScroll>
|
||||
</ScrollArea>
|
||||
</>
|
||||
)}
|
||||
</HeaderCluster>
|
||||
<ControlsCluster>
|
||||
|
|
@ -208,7 +241,12 @@ export function ProtocolEntry({
|
|||
</TimestampText>
|
||||
{directionBadge}
|
||||
<MethodBadge method={method} />
|
||||
{target && <TargetText>{target}</TargetText>}
|
||||
{target && (
|
||||
<>
|
||||
{resourceUri && <CopyButton value={resourceUri} />}
|
||||
<TargetText>{target}</TargetText>
|
||||
</>
|
||||
)}
|
||||
</Group>
|
||||
<Group gap="sm">
|
||||
{durationText}
|
||||
|
|
|
|||
Loading…
Reference in a new issue