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
11 changed files with 707 additions and 274 deletions
|
|
@ -88,7 +88,7 @@ export interface AppsUiState {
|
|||
|
||||
const ScreenLayout = Flex.withProps({
|
||||
variant: "screen",
|
||||
h: "calc(100dvh - var(--app-shell-header-height, 0px))",
|
||||
h: "calc(100dvh - var(--app-shell-header-height, 0px) - var(--app-shell-footer-height, 0px))",
|
||||
gap: "md",
|
||||
p: "xl",
|
||||
align: "flex-start",
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ export interface ConsoleUiState {
|
|||
|
||||
const ScreenLayout = Flex.withProps({
|
||||
variant: "screen",
|
||||
h: "calc(100dvh - var(--app-shell-header-height, 0px))",
|
||||
h: "calc(100dvh - var(--app-shell-header-height, 0px) - var(--app-shell-footer-height, 0px))",
|
||||
gap: "md",
|
||||
p: "xl",
|
||||
});
|
||||
|
|
@ -127,7 +127,7 @@ export function ConsoleScreen({
|
|||
// See LoggingScreen: only override `h` when embedded, so the standalone
|
||||
// screen keeps ScreenLayout's default full-screen height (a `h={undefined}`
|
||||
// would clobber it and collapse an empty screen to its controls' height).
|
||||
<ScreenLayout {...(embedded ? { h: "100%" } : {})}>
|
||||
<ScreenLayout {...(embedded ? { h: "100%", pt: "md" } : {})}>
|
||||
{embedded ? null : (
|
||||
<Sidebar>
|
||||
<SidebarCard>
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ export interface LogsUiState {
|
|||
|
||||
const ScreenLayout = Flex.withProps({
|
||||
variant: "screen",
|
||||
h: "calc(100dvh - var(--app-shell-header-height, 0px))",
|
||||
h: "calc(100dvh - var(--app-shell-header-height, 0px) - var(--app-shell-footer-height, 0px))",
|
||||
gap: "md",
|
||||
p: "xl",
|
||||
});
|
||||
|
|
@ -82,7 +82,9 @@ export function LoggingScreen({
|
|||
// ScreenLayout's default full-screen height. Passing `h={undefined}` here
|
||||
// would clobber that default (withProps plain-spreads), collapsing an empty
|
||||
// screen to its controls' height — so only override `h` when embedded.
|
||||
<ScreenLayout {...(embedded ? { h: "100%" } : {})}>
|
||||
// Embedded also halves the top padding (`pt: md` vs the `xl` default) so the
|
||||
// panel sits closer to the sidebar's tab/search controls above it.
|
||||
<ScreenLayout {...(embedded ? { h: "100%", pt: "md" } : {})}>
|
||||
{embedded ? null : (
|
||||
<Sidebar>
|
||||
<SidebarCard>
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ export interface NetworkUiState {
|
|||
|
||||
const ScreenLayout = Flex.withProps({
|
||||
variant: "screen",
|
||||
h: "calc(100dvh - var(--app-shell-header-height, 0px))",
|
||||
h: "calc(100dvh - var(--app-shell-header-height, 0px) - var(--app-shell-footer-height, 0px))",
|
||||
gap: "md",
|
||||
p: "xl",
|
||||
});
|
||||
|
|
@ -87,7 +87,7 @@ export function NetworkScreen({
|
|||
// See LoggingScreen: only override `h` when embedded, so the standalone
|
||||
// screen keeps ScreenLayout's default full-screen height (a `h={undefined}`
|
||||
// would clobber it and collapse an empty screen to its controls' height).
|
||||
<ScreenLayout {...(embedded ? { h: "100%" } : {})}>
|
||||
<ScreenLayout {...(embedded ? { h: "100%", pt: "md" } : {})}>
|
||||
{embedded ? null : (
|
||||
<Sidebar>
|
||||
<SidebarCard>
|
||||
|
|
|
|||
|
|
@ -59,7 +59,7 @@ export interface PromptsUiState {
|
|||
|
||||
const ScreenLayout = Flex.withProps({
|
||||
variant: "screen",
|
||||
h: "calc(100dvh - var(--app-shell-header-height, 0px))",
|
||||
h: "calc(100dvh - var(--app-shell-header-height, 0px) - var(--app-shell-footer-height, 0px))",
|
||||
gap: "md",
|
||||
p: "xl",
|
||||
});
|
||||
|
|
@ -108,7 +108,7 @@ const EmptyState = Text.withProps({
|
|||
});
|
||||
|
||||
const SCROLL_MAX_HEIGHT =
|
||||
"calc(100dvh - var(--app-shell-header-height, 0px) - var(--mantine-spacing-xl) * 2)";
|
||||
"calc(100dvh - var(--app-shell-header-height, 0px) - var(--app-shell-footer-height, 0px) - var(--mantine-spacing-xl) * 2)";
|
||||
|
||||
function hasArguments(prompt: Prompt): boolean {
|
||||
return !!prompt.arguments && prompt.arguments.length > 0;
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ export interface ProtocolUiState {
|
|||
|
||||
const ScreenLayout = Flex.withProps({
|
||||
variant: "screen",
|
||||
h: "calc(100dvh - var(--app-shell-header-height, 0px))",
|
||||
h: "calc(100dvh - var(--app-shell-header-height, 0px) - var(--app-shell-footer-height, 0px))",
|
||||
gap: "md",
|
||||
p: "xl",
|
||||
});
|
||||
|
|
@ -107,7 +107,7 @@ export function ProtocolScreen({
|
|||
// See LoggingScreen: only override `h` when embedded, so the standalone
|
||||
// screen keeps ScreenLayout's default full-screen height (a `h={undefined}`
|
||||
// would clobber it and collapse an empty screen to its controls' height).
|
||||
<ScreenLayout {...(embedded ? { h: "100%" } : {})}>
|
||||
<ScreenLayout {...(embedded ? { h: "100%", pt: "md" } : {})}>
|
||||
{embedded ? null : (
|
||||
<Sidebar>
|
||||
<SidebarCard>
|
||||
|
|
|
|||
|
|
@ -74,7 +74,7 @@ export interface ResourcesUiState {
|
|||
|
||||
const ScreenLayout = Flex.withProps({
|
||||
variant: "screen",
|
||||
h: "calc(100dvh - var(--app-shell-header-height, 0px))",
|
||||
h: "calc(100dvh - var(--app-shell-header-height, 0px) - var(--app-shell-footer-height, 0px))",
|
||||
gap: "md",
|
||||
p: "xl",
|
||||
});
|
||||
|
|
@ -126,7 +126,7 @@ const EmptyState = Text.withProps({
|
|||
});
|
||||
|
||||
const SCROLL_MAX_HEIGHT =
|
||||
"calc(100dvh - var(--app-shell-header-height, 0px) - var(--mantine-spacing-xl) * 2)";
|
||||
"calc(100dvh - var(--app-shell-header-height, 0px) - var(--app-shell-footer-height, 0px) - var(--mantine-spacing-xl) * 2)";
|
||||
|
||||
export function ResourcesScreen({
|
||||
resources,
|
||||
|
|
|
|||
|
|
@ -43,6 +43,13 @@ describe("ServerListScreen", () => {
|
|||
expect(screen.getByText("Beta")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders the 'Servers' box heading", () => {
|
||||
renderWithMantine(<ServerListScreen {...baseProps} />);
|
||||
expect(
|
||||
screen.getByRole("heading", { name: "Servers" }),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders the empty state with no servers", () => {
|
||||
renderWithMantine(<ServerListScreen {...baseProps} servers={[]} />);
|
||||
expect(
|
||||
|
|
@ -157,6 +164,32 @@ describe("ServerListScreen", () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe("just-connected highlight (#1682)", () => {
|
||||
it("draws the green highlight border on the just-connected server's card", () => {
|
||||
const { container } = renderWithMantine(
|
||||
<ServerListScreen
|
||||
{...baseProps}
|
||||
activeServer="beta"
|
||||
connectedServerId="beta"
|
||||
/>,
|
||||
);
|
||||
const highlighted = container.querySelectorAll(
|
||||
'[data-variant="highlighted"]',
|
||||
);
|
||||
expect(highlighted).toHaveLength(1);
|
||||
expect(highlighted[0]).toHaveTextContent("Beta");
|
||||
});
|
||||
|
||||
it("draws no highlight border when connectedServerId is unset", () => {
|
||||
const { container } = renderWithMantine(
|
||||
<ServerListScreen {...baseProps} activeServer="beta" />,
|
||||
);
|
||||
expect(
|
||||
container.querySelectorAll('[data-variant="highlighted"]'),
|
||||
).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("freshly-added highlight", () => {
|
||||
it("draws a green highlight border on every highlighted server", () => {
|
||||
const { container } = renderWithMantine(
|
||||
|
|
|
|||
|
|
@ -1,10 +1,14 @@
|
|||
import {
|
||||
Alert,
|
||||
Code,
|
||||
Flex,
|
||||
Group,
|
||||
Paper,
|
||||
ScrollArea,
|
||||
SimpleGrid,
|
||||
Stack,
|
||||
Text,
|
||||
Title,
|
||||
} from "@mantine/core";
|
||||
import {
|
||||
DndContext,
|
||||
|
|
@ -44,6 +48,12 @@ export interface ServerListScreenProps {
|
|||
* draws a red border until another server is connected or attempted.
|
||||
*/
|
||||
erroredServerId?: string;
|
||||
/**
|
||||
* Id of the server that just connected successfully (#1682). Its card draws
|
||||
* the green highlight border and scrolls into view — the success mirror of
|
||||
* `erroredServerId`.
|
||||
*/
|
||||
connectedServerId?: string;
|
||||
onAddManually: () => void;
|
||||
onImportConfig: () => void;
|
||||
onImportServerJson: () => void;
|
||||
|
|
@ -69,21 +79,74 @@ export interface ServerListScreenProps {
|
|||
onToggleCompact: () => void;
|
||||
}
|
||||
|
||||
const PageContainer = Stack.withProps({
|
||||
// Full-height screen wrapper (matches the Protocol/Logs screens): fills the
|
||||
// viewport minus the header and footer, clips overflow, and lays the optional
|
||||
// read-only banner above the box in a column.
|
||||
const ScreenLayout = Flex.withProps({
|
||||
variant: "screen",
|
||||
direction: "column",
|
||||
h: "calc(100dvh - var(--app-shell-header-height, 0px) - var(--app-shell-footer-height, 0px))",
|
||||
gap: "md",
|
||||
p: "xl",
|
||||
});
|
||||
|
||||
// The bordered box that fills the container (#1682 follow-up), styled like the
|
||||
// "Messages" panel: a `panel`-variant Paper (display:flex column) holding the
|
||||
// title + controls header and the scrolling card grid.
|
||||
const PanelContainer = Paper.withProps({
|
||||
withBorder: true,
|
||||
p: "lg",
|
||||
flex: 1,
|
||||
variant: "panel",
|
||||
// The box takes the same surface as the header and footer (white in light
|
||||
// mode) rather than the grey app background.
|
||||
bg: "var(--mantine-color-body)",
|
||||
});
|
||||
|
||||
// Header row inside the box: "Servers" title on the left, the Export / Add /
|
||||
// collapse controls on the right.
|
||||
const PanelHeader = Group.withProps({
|
||||
justify: "space-between",
|
||||
mb: "sm",
|
||||
});
|
||||
|
||||
// Wraps the scroll region so it fills the box below the header (`flex:1/mih:0`)
|
||||
// and the inner ScrollArea can bound against it with `mah:100%`.
|
||||
const CardScrollWrap = Stack.withProps({
|
||||
flex: 1,
|
||||
mih: 0,
|
||||
gap: 0,
|
||||
});
|
||||
|
||||
// Centered in the full-height box so the empty message sits mid-panel rather
|
||||
// than clinging to the top (matches the Messages panel's empty state).
|
||||
const EmptyCenter = Stack.withProps({
|
||||
flex: 1,
|
||||
align: "center",
|
||||
justify: "center",
|
||||
});
|
||||
|
||||
const EmptyState = Text.withProps({
|
||||
c: "dimmed",
|
||||
ta: "center",
|
||||
py: "xl",
|
||||
});
|
||||
|
||||
// Override the card-surface var for the whole grid so every server card (in any
|
||||
// state — the theme's default/highlighted/errored/disabled Card variants all
|
||||
// read `--inspector-surface-card`) picks up the faintly-grey server tone,
|
||||
// without touching the shared token or the variant logic.
|
||||
const GRID_SURFACE_STYLES = {
|
||||
root: {
|
||||
"--inspector-surface-card": "var(--inspector-surface-card-server)",
|
||||
},
|
||||
} as const;
|
||||
|
||||
export function ServerListScreen({
|
||||
servers,
|
||||
writable = true,
|
||||
activeServer,
|
||||
erroredServerId,
|
||||
connectedServerId,
|
||||
onAddManually,
|
||||
onImportConfig,
|
||||
onImportServerJson,
|
||||
|
|
@ -145,6 +208,7 @@ export function ServerListScreen({
|
|||
onRemove,
|
||||
highlighted: highlightedServerIds?.includes(server.id) ?? false,
|
||||
errored: server.id === erroredServerId,
|
||||
justConnected: server.id === connectedServerId,
|
||||
scrollOnHighlight: server.id === firstHighlightedId,
|
||||
onClearHighlight: onClearHighlight
|
||||
? () => onClearHighlight(server.id)
|
||||
|
|
@ -167,6 +231,7 @@ export function ServerListScreen({
|
|||
cols={{ base: 1, "1040px": 2, "1560px": 3 }}
|
||||
spacing="lg"
|
||||
className="grid-align-start"
|
||||
styles={GRID_SURFACE_STYLES}
|
||||
>
|
||||
{servers.map((server) =>
|
||||
reorderable ? (
|
||||
|
|
@ -179,7 +244,7 @@ export function ServerListScreen({
|
|||
);
|
||||
|
||||
return (
|
||||
<PageContainer>
|
||||
<ScreenLayout>
|
||||
{!writable && (
|
||||
<Alert color="gray" variant="light" title="Read-only session">
|
||||
This server list was launched with <Code>--config</Code> or an ad-hoc
|
||||
|
|
@ -187,45 +252,63 @@ export function ServerListScreen({
|
|||
<Code>--catalog</Code> (or no flag) to manage a writable catalog.
|
||||
</Alert>
|
||||
)}
|
||||
<ServerListControls
|
||||
serverCount={servers.length}
|
||||
compact={compact}
|
||||
writable={writable}
|
||||
onToggleList={onToggleCompact}
|
||||
onAddManually={onAddManually}
|
||||
onImportConfig={onImportConfig}
|
||||
onImportServerJson={onImportServerJson}
|
||||
onExport={onExport}
|
||||
/>
|
||||
<PanelContainer>
|
||||
<PanelHeader>
|
||||
{/* Semantic h3 (visually h4) so it doesn't skip a level under the
|
||||
disconnected header's h2 "MCP Inspector" (heading-order a11y). The
|
||||
other panels render h4 directly because they only show while
|
||||
connected, when the header carries no heading to skip under. */}
|
||||
<Title order={3} size="h4">
|
||||
Servers
|
||||
</Title>
|
||||
<ServerListControls
|
||||
serverCount={servers.length}
|
||||
compact={compact}
|
||||
writable={writable}
|
||||
onToggleList={onToggleCompact}
|
||||
onAddManually={onAddManually}
|
||||
onImportConfig={onImportConfig}
|
||||
onImportServerJson={onImportServerJson}
|
||||
onExport={onExport}
|
||||
/>
|
||||
</PanelHeader>
|
||||
|
||||
<ScrollArea.Autosize
|
||||
mah="calc(100dvh - var(--app-shell-header-height, 60px) - var(--mantine-spacing-xl) * 2 - 60px)"
|
||||
// Same scrollbar treatment as the Protocol/Network/Logging list panels
|
||||
// (#1474): reserve a gutter so the bar never overlays the right edge of
|
||||
// the server cards (occluding their action icons / status badges), and
|
||||
// only show it while actively scrolling rather than popping in on hover.
|
||||
type="scroll"
|
||||
offsetScrollbars
|
||||
>
|
||||
{servers.length === 0 ? (
|
||||
<EmptyState>
|
||||
No servers configured. Add a server to get started.
|
||||
</EmptyState>
|
||||
) : reorderable ? (
|
||||
<DndContext
|
||||
sensors={sensors}
|
||||
collisionDetection={closestCenter}
|
||||
onDragEnd={handleDragEnd}
|
||||
accessibility={{ announcements }}
|
||||
>
|
||||
<SortableContext items={ids} strategy={rectSortingStrategy}>
|
||||
{grid}
|
||||
</SortableContext>
|
||||
</DndContext>
|
||||
<EmptyCenter>
|
||||
<EmptyState>
|
||||
No servers configured. Add a server to get started.
|
||||
</EmptyState>
|
||||
</EmptyCenter>
|
||||
) : (
|
||||
grid
|
||||
<CardScrollWrap>
|
||||
<ScrollArea.Autosize
|
||||
mah="100%"
|
||||
// Same scrollbar treatment as the Protocol/Network/Logging list
|
||||
// panels (#1474): reserve a gutter so the bar never overlays the
|
||||
// right edge of the server cards (occluding their action icons /
|
||||
// status badges), and only show it while actively scrolling
|
||||
// rather than popping in on hover.
|
||||
type="scroll"
|
||||
offsetScrollbars
|
||||
>
|
||||
{reorderable ? (
|
||||
<DndContext
|
||||
sensors={sensors}
|
||||
collisionDetection={closestCenter}
|
||||
onDragEnd={handleDragEnd}
|
||||
accessibility={{ announcements }}
|
||||
>
|
||||
<SortableContext items={ids} strategy={rectSortingStrategy}>
|
||||
{grid}
|
||||
</SortableContext>
|
||||
</DndContext>
|
||||
) : (
|
||||
grid
|
||||
)}
|
||||
</ScrollArea.Autosize>
|
||||
</CardScrollWrap>
|
||||
)}
|
||||
</ScrollArea.Autosize>
|
||||
</PageContainer>
|
||||
</PanelContainer>
|
||||
</ScreenLayout>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ export interface TasksUiState {
|
|||
|
||||
const ScreenLayout = Flex.withProps({
|
||||
variant: "screen",
|
||||
h: "calc(100dvh - var(--app-shell-header-height, 0px))",
|
||||
h: "calc(100dvh - var(--app-shell-header-height, 0px) - var(--app-shell-footer-height, 0px))",
|
||||
gap: "md",
|
||||
p: "xl",
|
||||
});
|
||||
|
|
@ -59,7 +59,9 @@ export function TasksScreen({
|
|||
// Embedded fills the monitoring sidebar column (100%); standalone keeps the
|
||||
// ScreenLayout's default full-screen height. Only override `h` when embedded
|
||||
// — passing `h={undefined}` would clobber the default (withProps spreads).
|
||||
<ScreenLayout {...(embedded ? { h: "100%" } : {})}>
|
||||
// Embedded also halves the top padding (`pt: md` vs `xl`) so the panel sits
|
||||
// closer to the sidebar's tab/search controls (see LoggingScreen).
|
||||
<ScreenLayout {...(embedded ? { h: "100%", pt: "md" } : {})}>
|
||||
{embedded ? null : (
|
||||
<Sidebar>
|
||||
<SidebarCard>
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ export interface ToolsScreenProps {
|
|||
// viewport minus the app-shell header and the screen's top+bottom xl padding,
|
||||
// leaving the bottom margin the overflow used to eat.
|
||||
const SCROLL_MAX_HEIGHT =
|
||||
"calc(100dvh - var(--app-shell-header-height, 0px) - var(--mantine-spacing-xl) * 2)";
|
||||
"calc(100dvh - var(--app-shell-header-height, 0px) - var(--app-shell-footer-height, 0px) - var(--mantine-spacing-xl) * 2)";
|
||||
|
||||
// No `align` override: children stretch to the row's full height, giving each
|
||||
// column pane a definite height. That definite height is what lets a column's
|
||||
|
|
@ -69,7 +69,7 @@ const SCROLL_MAX_HEIGHT =
|
|||
// see the Prompts/Resources preview panes this mirrors).
|
||||
const ScreenLayout = Flex.withProps({
|
||||
variant: "screen",
|
||||
h: "calc(100dvh - var(--app-shell-header-height, 0px))",
|
||||
h: "calc(100dvh - var(--app-shell-header-height, 0px) - var(--app-shell-footer-height, 0px))",
|
||||
gap: "md",
|
||||
p: "xl",
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue