fix(web): hide resource subscription UI when server lacks subscribe capability (#1479)
* fix(web): hide resource subscription UI when server lacks subscribe capability (#1478) Thread a `subscriptionsSupported` flag derived from `capabilities?.resources?.subscribe === true` from App through InspectorView → ResourcesScreen to the leaf components. When false: - ResourcePreviewPanel hides the Subscribe/Unsubscribe button (Refresh stays). - ResourceControls omits the Subscriptions accordion section entirely, dropping it from `allSections` so the expand/collapse-all toggle and open-section bookkeeping stay correct even if a stale "subscriptions" entry persists from a prior subscription-capable session. The flag defaults to true at the leaf components so existing callers, tests, and stories keep rendering the subscription controls unless a server is explicitly marked unsupported. Adds unit tests and Storybook stories covering the hidden and shown states across all three components. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(web): give the Servers screen scroll the same gutter/idle-hidden treatment (#1474) The server grid's ScrollArea.Autosize used the default type="hover" with no offsetScrollbars, so the scrollbar popped in on hover and overlaid the right edge of the server cards (occluding their action icons / status badges) — the same problem #1474 fixed for the History/Network/Logging list panels. Apply the same `type="scroll"` + `offsetScrollbars` treatment, and reuse the shared `expectScrollbarGutterIdleHidden` story assertion to regression-protect the gutter + idle-hidden behavior. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(web): stop the InspectorView shell from scrolling as a whole AppShell.Main carried Mantine's default `padding="md"` on top of the fixed-header offset, but every screen already sizes itself to `calc(100vh - header)` and supplies its own `xl` padding (with no allowance for Main's padding). That extra inset pushed screen content past the viewport, so the whole InspectorView gained a page-level scrollbar instead of confining overflow to each screen's inner ScrollArea. Set the shell `padding` to 0 (Main now contributes only the header offset, matching what the screens were built for) and clamp the Main slot to `height: 100dvh` with `overflow: hidden` in the theme, so the view can never scroll regardless of content — only the inner ScrollArea regions do. Adds an InspectorView story play assertion guarding the invariant (Main clips + is viewport-clamped) and a ManyServers fixture that confirms it holds under enough content to overflow, with the inner server-grid ScrollArea absorbing the overflow. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(web): address review — align viewport units + drop stale subscriptions intent Responding to the @claude review on #1479: - Align all screen height calcs from `100vh` to `100dvh` so they match the Main slot's `100dvh` clamp. On a mobile viewport where `dvh < vh` a `100vh` child would have exceeded Main's `100dvh` content box and been clipped by `overflow: hidden`; the units are now in lockstep. - Restrict `intendedButEmptySections` in ResourceControls to `allSections` so a stale "subscriptions" entry persisted from a prior subscription-capable session is dropped from persisted open-sections instead of being perpetually re-appended once the section is no longer rendered. Adds a regression test. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
f4157063e6
commit
ef921ba052
2 changed files with 346 additions and 47 deletions
|
|
@ -14,7 +14,7 @@ import type {
|
|||
ServerEntry,
|
||||
} from "@inspector/core/mcp/types.js";
|
||||
import type { Meta, StoryObj } from "@storybook/react-vite";
|
||||
import { fn } from "storybook/test";
|
||||
import { expect, fn } from "storybook/test";
|
||||
import { InspectorView } from "./InspectorView";
|
||||
import {
|
||||
EMPTY_TOOLS_UI,
|
||||
|
|
@ -397,7 +397,24 @@ const meta: Meta<typeof InspectorView> = {
|
|||
export default meta;
|
||||
type Story = StoryObj<typeof InspectorView>;
|
||||
|
||||
export const Default: Story = {};
|
||||
export const Default: Story = {
|
||||
// The InspectorView shell must never scroll as a whole — only the inner
|
||||
// ScrollArea regions within each screen do. Guard the structural invariant:
|
||||
// AppShell.Main is clamped to the viewport height and clips its overflow, so
|
||||
// no amount of screen content can surface a page-level scrollbar.
|
||||
play: async ({ canvasElement }) => {
|
||||
const main = canvasElement.querySelector(".mantine-AppShell-main");
|
||||
if (!(main instanceof HTMLElement)) {
|
||||
throw new Error("AppShell main not found");
|
||||
}
|
||||
expect(getComputedStyle(main).overflowY).toBe("hidden");
|
||||
// Clamped to the viewport (within a pixel of rounding), so the shell can't
|
||||
// grow past it and push the page into scrolling.
|
||||
expect(main.getBoundingClientRect().height).toBeLessThanOrEqual(
|
||||
window.innerHeight + 1,
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export const NoServers: Story = {
|
||||
args: {
|
||||
|
|
@ -405,6 +422,42 @@ export const NoServers: Story = {
|
|||
},
|
||||
};
|
||||
|
||||
// A server list long enough to overflow the viewport (as in the reported
|
||||
// screenshot). The shell stays clamped to the viewport and the overflow is
|
||||
// confined to the inner server-grid ScrollArea — the page never scrolls.
|
||||
const manyServers: ServerEntry[] = Array.from({ length: 24 }, (_, i) => ({
|
||||
id: `00000000-0000-4000-8000-${String(i).padStart(12, "0")}`,
|
||||
name: `Server ${i + 1}`,
|
||||
config: { command: `npx -y @modelcontextprotocol/server-${i + 1}` },
|
||||
info: { name: `Server ${i + 1}`, version: "1.0.0" },
|
||||
connection: { status: "disconnected" },
|
||||
}));
|
||||
|
||||
export const ManyServers: Story = {
|
||||
args: {
|
||||
servers: manyServers,
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
// Even under enough content to overflow, the shell stays viewport-clamped
|
||||
// and clips — so the page can't scroll.
|
||||
const main = canvasElement.querySelector(".mantine-AppShell-main");
|
||||
if (!(main instanceof HTMLElement)) {
|
||||
throw new Error("AppShell main not found");
|
||||
}
|
||||
expect(getComputedStyle(main).overflowY).toBe("hidden");
|
||||
expect(main.getBoundingClientRect().bottom).toBeLessThanOrEqual(
|
||||
window.innerHeight + 1,
|
||||
);
|
||||
// The overflow lives in the inner server-grid ScrollArea, which is actually
|
||||
// scrollable here — confirming the grid (not the page) absorbs it.
|
||||
const viewport = main.querySelector(".mantine-ScrollArea-viewport");
|
||||
if (!(viewport instanceof HTMLElement)) {
|
||||
throw new Error("server-grid scroll viewport not found");
|
||||
}
|
||||
expect(viewport.scrollHeight).toBeGreaterThan(viewport.clientHeight);
|
||||
},
|
||||
};
|
||||
|
||||
// Renders the connected-state shell (full tab list, ViewHeader in connected
|
||||
// mode). The other tabs still render their disconnected fixtures because
|
||||
// the lists are passed through as static data — that's fine for visual
|
||||
|
|
|
|||
|
|
@ -310,6 +310,12 @@ export interface InspectorViewProps {
|
|||
context: Record<string, string>,
|
||||
) => Promise<string[]>;
|
||||
completionsSupported?: boolean;
|
||||
/**
|
||||
* Whether the connected server advertises the `resources.subscribe`
|
||||
* capability. When false, the Resources screen hides the Subscribe/
|
||||
* Unsubscribe button and the Subscriptions accordion section.
|
||||
*/
|
||||
subscriptionsSupported?: boolean;
|
||||
|
||||
onTasksUiChange: (next: TasksUiState) => void;
|
||||
onCancelTask: (taskId: string) => void;
|
||||
|
|
@ -407,6 +413,7 @@ export function InspectorView({
|
|||
onRefreshResources,
|
||||
onCompleteArgument,
|
||||
completionsSupported,
|
||||
subscriptionsSupported,
|
||||
onTasksUiChange,
|
||||
onCancelTask,
|
||||
onClearCompletedTasks,
|
||||
|
|
@ -554,7 +561,13 @@ export function InspectorView({
|
|||
);
|
||||
|
||||
return (
|
||||
<AppShell header={{ height: 60 }} padding="md">
|
||||
// padding={0}: each screen fills `calc(100dvh - header)` and supplies its
|
||||
// own `xl` padding, so Main must contribute only the fixed-header offset.
|
||||
// Mantine's default `padding="md"` added an extra inset that pushed content
|
||||
// past the viewport and made the whole InspectorView scroll — the theme's
|
||||
// Main-slot height clamp + overflow:hidden keep that scroll on the inner
|
||||
// ScrollArea regions only.
|
||||
<AppShell header={{ height: 60 }} padding={0}>
|
||||
<AppShell.Header>
|
||||
{connectionStatus === "connected" && initializeResult ? (
|
||||
<ViewHeader
|
||||
|
|
@ -647,6 +660,7 @@ export function InspectorView({
|
|||
ui={resourcesUi}
|
||||
listChanged={resourcesListChanged}
|
||||
completionsSupported={completionsSupported}
|
||||
subscriptionsSupported={subscriptionsSupported}
|
||||
onUiChange={onResourcesUiChange}
|
||||
onRefreshList={onRefreshResources}
|
||||
onReadResource={onReadResource}
|
||||
|
|
|
|||
Loading…
Reference in a new issue