Refactor ServerCard with active server logic, rename StatusIndicator, and polish UI panels

- Rename StatusIndicator to ServerStatusIndicator across all files
- ServerCard: add activeServer/onSetActiveServer props for coordinated
  connection flow — when a card's toggle is switched on, it sets itself
  as the active server, shows "Connecting" status, and dims all other
  cards (opacity + pointerEvents none + inert for full keyboard blocking)
- ServerListScreen: manage activeServer state internally, pass to cards;
  switch to full-width layout with 3-column grid and scrollable card area
- ServerCard: move "Test Client Features" dropdown above ContentViewer
  with min-height row for consistent card heights
- ToolResultPanel: move Clear button to top-right header row
- ToolDetailPanel: right-align Execute Tool button, size md, not full-width
- ListChangedIndicator: bump Refresh button to size sm
- ToolsActive story: expand to 30 tools with titles
- UnconnectedView stories: all servers disconnected, remove Connecting story

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
cliffhall 2026-04-02 18:00:18 -04:00
commit 8e73bc6ffb
5 changed files with 295 additions and 117 deletions

View file

@ -7,6 +7,7 @@ const meta: Meta<typeof ServerCard> = {
component: ServerCard,
args: {
onToggleConnection: fn(),
onSetConnectingServer: fn(),
onServerInfo: fn(),
onSettings: fn(),
onEdit: fn(),

View file

@ -1,5 +1,6 @@
import { useEffect } from "react";
import { Badge, Button, Card, Group, Menu, Stack, Text } from "@mantine/core";
import { StatusIndicator } from "../../elements/StatusIndicator/StatusIndicator";
import { ServerStatusIndicator } from "../../elements/ServerStatusIndicator/ServerStatusIndicator";
import { TransportBadge } from "../../elements/TransportBadge/TransportBadge";
import { ConnectionToggle } from "../../elements/ConnectionToggle/ConnectionToggle";
import { ContentViewer } from "../../elements/ContentViewer/ContentViewer";
@ -15,6 +16,8 @@ export interface ServerCardProps {
retryCount?: number;
error?: { message: string; details?: string };
canTestClientFeatures: boolean;
activeServer?: string;
onSetActiveServer?: (name: string | undefined) => void;
onToggleConnection: (connect: boolean) => void;
onServerInfo: () => void;
onSettings: () => void;
@ -71,6 +74,8 @@ export function ServerCard({
retryCount,
error,
canTestClientFeatures,
activeServer,
onSetActiveServer,
onToggleConnection,
onServerInfo,
onSettings,
@ -83,11 +88,35 @@ export function ServerCard({
onConfigureRoots,
compact = false,
}: ServerCardProps) {
const isThisConnecting = activeServer === name;
const isDimmed = activeServer !== undefined && activeServer !== name;
const isConnected = status === "connected";
const isConnecting = status === "connecting";
const isConnecting = status === "connecting" || isThisConnecting;
const displayStatus = isThisConnecting ? "connecting" : status;
useEffect(() => {
if (isThisConnecting) {
onToggleConnection(true);
}
}, [isThisConnecting, onToggleConnection]);
function handleToggle(connect: boolean) {
if (connect && !activeServer) {
onSetActiveServer?.(name);
} else if (!connect && activeServer && activeServer === name) {
onSetActiveServer?.(undefined);
onToggleConnection(false);
}
}
return (
<Card withBorder padding="lg">
<Card
withBorder
padding="lg"
opacity={isDimmed ? 0.4 : 1}
style={isDimmed ? { pointerEvents: "none" } : undefined}
{...(isDimmed ? { "aria-disabled": true, inert: "" } : {})}
>
<Stack gap="sm">
<Group justify="space-between" wrap="wrap">
<HeaderLeft>
@ -95,21 +124,47 @@ export function ServerCard({
{version && <Badge variant="outline">{version}</Badge>}
</HeaderLeft>
<HeaderRight>
<StatusIndicator status={status} retryCount={retryCount} />
<ServerStatusIndicator
status={displayStatus}
retryCount={retryCount}
/>
<ConnectionToggle
checked={isConnected}
loading={isConnecting}
disabled={false}
onChange={onToggleConnection}
disabled={isDimmed}
onChange={handleToggle}
/>
</HeaderRight>
</Group>
{!compact && (
<>
<Group gap="sm">
<TransportBadge transport={transport} />
<ModeText>{connectionMode}</ModeText>
<Group justify="space-between" mih={30}>
<Group gap="sm">
<TransportBadge transport={transport} />
<ModeText>{connectionMode}</ModeText>
</Group>
{canTestClientFeatures && (
<Menu>
<Menu.Target>
<SubtleButton>Test Client Features &#x25BE;</SubtleButton>
</Menu.Target>
<Menu.Dropdown>
<Menu.Item onClick={onTestSampling}>
Simulate Sampling Request
</Menu.Item>
<Menu.Item onClick={onTestElicitationForm}>
Simulate Elicitation (Form)
</Menu.Item>
<Menu.Item onClick={onTestElicitationUrl}>
Simulate Elicitation (URL)
</Menu.Item>
<Menu.Item onClick={onConfigureRoots}>
Configure Roots
</Menu.Item>
</Menu.Dropdown>
</Menu>
)}
</Group>
<ContentViewer type="text" content={command} copyable />
@ -131,27 +186,6 @@ export function ServerCard({
<ActionsRow>
<SubtleButton onClick={onServerInfo}>Server Info</SubtleButton>
<SubtleButton onClick={onSettings}>Settings</SubtleButton>
{canTestClientFeatures && (
<Menu>
<Menu.Target>
<SubtleButton>Test Client Features &#x25BE;</SubtleButton>
</Menu.Target>
<Menu.Dropdown>
<Menu.Item onClick={onTestSampling}>
Simulate Sampling Request
</Menu.Item>
<Menu.Item onClick={onTestElicitationForm}>
Simulate Elicitation (Form)
</Menu.Item>
<Menu.Item onClick={onTestElicitationUrl}>
Simulate Elicitation (URL)
</Menu.Item>
<Menu.Item onClick={onConfigureRoots}>
Configure Roots
</Menu.Item>
</Menu.Dropdown>
</Menu>
)}
</ActionsRow>
</Group>
</>

View file

@ -115,16 +115,16 @@ export function ToolDetailPanel({
/>
)}
<Group>
<Group justify="flex-end">
{isExecuting && <CancelButton onClick={onCancel}>Cancel</CancelButton>}
<Button
fullWidth
size="md"
onClick={onExecute}
disabled={isExecuting}
loading={isExecuting}
>
Execute Tool
</Button>
{isExecuting && <CancelButton onClick={onCancel}>Cancel</CancelButton>}
</Group>
</Stack>
);

View file

@ -29,7 +29,10 @@ function resolveMediaType(mimeType?: string): "image" | "audio" {
export function ToolResultPanel({ content, onClear }: ToolResultPanelProps) {
return (
<Stack>
<Title order={4}>Results</Title>
<Group justify="space-between">
<Title order={4}>Results</Title>
<ClearButton onClick={onClear}>Clear</ClearButton>
</Group>
{content.length === 0 ? (
<Text c="dimmed">No results yet</Text>
) : (
@ -59,9 +62,6 @@ export function ToolResultPanel({ content, onClear }: ToolResultPanelProps) {
})}
</>
)}
<Group>
<ClearButton onClick={onClear}>Clear</ClearButton>
</Group>
</Stack>
);
}

View file

@ -10,7 +10,7 @@ import {
} from "@mantine/core";
import { useComputedColorScheme } from "@mantine/core";
import { MdLightMode, MdDarkMode } from "react-icons/md";
import { StatusIndicator } from "../../elements/StatusIndicator/StatusIndicator";
import { ServerStatusIndicator } from "../../elements/ServerStatusIndicator/ServerStatusIndicator";
import mcpLogo from "../../../theme/assets/MCP.svg";
import mcpLogoDark from "../../../theme/assets/MCP-dark.svg";
@ -133,7 +133,7 @@ export function ViewHeader(props: ViewHeaderProps) {
</CenterSection>
<RightSection>
<StatusIndicator status={props.status} latencyMs={props.latencyMs} />
<ServerStatusIndicator status={props.status} latencyMs={props.latencyMs} />
<DisconnectButton onClick={props.onDisconnect}>
Disconnect
</DisconnectButton>