From 8e73bc6ffbf239eec6605760690757a9b6e44e6b Mon Sep 17 00:00:00 2001 From: cliffhall Date: Thu, 2 Apr 2026 18:00:18 -0400 Subject: [PATCH] Refactor ServerCard with active server logic, rename StatusIndicator, and polish UI panels MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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) --- .../ConnectionToggle/ConnectionToggle.tsx | 2 +- .../ListChangedIndicator.tsx | 2 +- .../ServerStatusIndicator.stories.tsx} | 10 +- .../ServerStatusIndicator.tsx} | 10 +- .../groups/ServerCard/ServerCard.stories.tsx | 1 + .../groups/ServerCard/ServerCard.tsx | 94 +++++++--- .../ToolDetailPanel/ToolDetailPanel.tsx | 6 +- .../ToolResultPanel/ToolResultPanel.tsx | 8 +- .../groups/ViewHeader/ViewHeader.tsx | 4 +- .../ServerListScreen/ServerListScreen.tsx | 40 ++-- .../ConnectedView/ConnectedView.stories.tsx | 171 +++++++++++++++++- .../UnconnectedView.stories.tsx | 64 ++----- 12 files changed, 295 insertions(+), 117 deletions(-) rename clients/web/src/components/elements/{StatusIndicator/StatusIndicator.stories.tsx => ServerStatusIndicator/ServerStatusIndicator.stories.tsx} (67%) rename clients/web/src/components/elements/{StatusIndicator/StatusIndicator.tsx => ServerStatusIndicator/ServerStatusIndicator.tsx} (82%) diff --git a/clients/web/src/components/elements/ConnectionToggle/ConnectionToggle.tsx b/clients/web/src/components/elements/ConnectionToggle/ConnectionToggle.tsx index 3e210c67..486dca61 100644 --- a/clients/web/src/components/elements/ConnectionToggle/ConnectionToggle.tsx +++ b/clients/web/src/components/elements/ConnectionToggle/ConnectionToggle.tsx @@ -16,7 +16,7 @@ export function ConnectionToggle({ return ( onChange(event.currentTarget.checked)} /> diff --git a/clients/web/src/components/elements/ListChangedIndicator/ListChangedIndicator.tsx b/clients/web/src/components/elements/ListChangedIndicator/ListChangedIndicator.tsx index 4e28acf7..a9704b11 100644 --- a/clients/web/src/components/elements/ListChangedIndicator/ListChangedIndicator.tsx +++ b/clients/web/src/components/elements/ListChangedIndicator/ListChangedIndicator.tsx @@ -18,7 +18,7 @@ const UpdateLabel = Text.withProps({ }); const RefreshButton = Button.withProps({ - size: "xs", + size: "sm", variant: "light", }); diff --git a/clients/web/src/components/elements/StatusIndicator/StatusIndicator.stories.tsx b/clients/web/src/components/elements/ServerStatusIndicator/ServerStatusIndicator.stories.tsx similarity index 67% rename from clients/web/src/components/elements/StatusIndicator/StatusIndicator.stories.tsx rename to clients/web/src/components/elements/ServerStatusIndicator/ServerStatusIndicator.stories.tsx index c50a5e6e..87385cf1 100644 --- a/clients/web/src/components/elements/StatusIndicator/StatusIndicator.stories.tsx +++ b/clients/web/src/components/elements/ServerStatusIndicator/ServerStatusIndicator.stories.tsx @@ -1,13 +1,13 @@ import type { Meta, StoryObj } from "@storybook/react-vite"; -import { StatusIndicator } from "./StatusIndicator"; +import { ServerStatusIndicator } from "./ServerStatusIndicator"; -const meta: Meta = { - title: "Elements/StatusIndicator", - component: StatusIndicator, +const meta: Meta = { + title: "Elements/ServerStatusIndicator", + component: ServerStatusIndicator, }; export default meta; -type Story = StoryObj; +type Story = StoryObj; export const Connected: Story = { args: { diff --git a/clients/web/src/components/elements/StatusIndicator/StatusIndicator.tsx b/clients/web/src/components/elements/ServerStatusIndicator/ServerStatusIndicator.tsx similarity index 82% rename from clients/web/src/components/elements/StatusIndicator/StatusIndicator.tsx rename to clients/web/src/components/elements/ServerStatusIndicator/ServerStatusIndicator.tsx index 26c56078..b9ace767 100644 --- a/clients/web/src/components/elements/StatusIndicator/StatusIndicator.tsx +++ b/clients/web/src/components/elements/ServerStatusIndicator/ServerStatusIndicator.tsx @@ -1,12 +1,12 @@ import { Group, Paper, Text } from "@mantine/core"; -export interface StatusIndicatorProps { +export interface ServerStatusIndicatorProps { status: "connected" | "connecting" | "disconnected" | "failed"; latencyMs?: number; retryCount?: number; } -const statusColorVar: Record = { +const statusColorVar: Record = { connected: "var(--inspector-status-connected)", connecting: "var(--inspector-status-connecting)", disconnected: "var(--inspector-status-disconnected)", @@ -14,7 +14,7 @@ const statusColorVar: Record = { }; function getLabel( - status: StatusIndicatorProps["status"], + status: ServerStatusIndicatorProps["status"], latencyMs?: number, retryCount?: number, ): string { @@ -36,11 +36,11 @@ const Dot = Paper.withProps({ radius: "xl", }); -export function StatusIndicator({ +export function ServerStatusIndicator({ status, latencyMs, retryCount, -}: StatusIndicatorProps) { +}: ServerStatusIndicatorProps) { return ( = { component: ServerCard, args: { onToggleConnection: fn(), + onSetConnectingServer: fn(), onServerInfo: fn(), onSettings: fn(), onEdit: fn(), diff --git a/clients/web/src/components/groups/ServerCard/ServerCard.tsx b/clients/web/src/components/groups/ServerCard/ServerCard.tsx index 241726ad..e61359b1 100644 --- a/clients/web/src/components/groups/ServerCard/ServerCard.tsx +++ b/clients/web/src/components/groups/ServerCard/ServerCard.tsx @@ -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 ( - + @@ -95,21 +124,47 @@ export function ServerCard({ {version && {version}} - + {!compact && ( <> - - - {connectionMode} + + + + {connectionMode} + + {canTestClientFeatures && ( + + + Test Client Features ▾ + + + + Simulate Sampling Request + + + Simulate Elicitation (Form) + + + Simulate Elicitation (URL) + + + Configure Roots + + + + )} @@ -131,27 +186,6 @@ export function ServerCard({ Server Info Settings - {canTestClientFeatures && ( - - - Test Client Features ▾ - - - - Simulate Sampling Request - - - Simulate Elicitation (Form) - - - Simulate Elicitation (URL) - - - Configure Roots - - - - )} diff --git a/clients/web/src/components/groups/ToolDetailPanel/ToolDetailPanel.tsx b/clients/web/src/components/groups/ToolDetailPanel/ToolDetailPanel.tsx index d0d5ecc5..bf1ebecc 100644 --- a/clients/web/src/components/groups/ToolDetailPanel/ToolDetailPanel.tsx +++ b/clients/web/src/components/groups/ToolDetailPanel/ToolDetailPanel.tsx @@ -115,16 +115,16 @@ export function ToolDetailPanel({ /> )} - + + {isExecuting && Cancel} - {isExecuting && Cancel} ); diff --git a/clients/web/src/components/groups/ToolResultPanel/ToolResultPanel.tsx b/clients/web/src/components/groups/ToolResultPanel/ToolResultPanel.tsx index 46657a7a..f78281e1 100644 --- a/clients/web/src/components/groups/ToolResultPanel/ToolResultPanel.tsx +++ b/clients/web/src/components/groups/ToolResultPanel/ToolResultPanel.tsx @@ -29,7 +29,10 @@ function resolveMediaType(mimeType?: string): "image" | "audio" { export function ToolResultPanel({ content, onClear }: ToolResultPanelProps) { return ( - Results + + Results + Clear + {content.length === 0 ? ( No results yet ) : ( @@ -59,9 +62,6 @@ export function ToolResultPanel({ content, onClear }: ToolResultPanelProps) { })} )} - - Clear - ); } diff --git a/clients/web/src/components/groups/ViewHeader/ViewHeader.tsx b/clients/web/src/components/groups/ViewHeader/ViewHeader.tsx index b2e9f85a..0f397c13 100644 --- a/clients/web/src/components/groups/ViewHeader/ViewHeader.tsx +++ b/clients/web/src/components/groups/ViewHeader/ViewHeader.tsx @@ -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) { - + Disconnect diff --git a/clients/web/src/components/screens/ServerListScreen/ServerListScreen.tsx b/clients/web/src/components/screens/ServerListScreen/ServerListScreen.tsx index be0eedc9..f386e4b0 100644 --- a/clients/web/src/components/screens/ServerListScreen/ServerListScreen.tsx +++ b/clients/web/src/components/screens/ServerListScreen/ServerListScreen.tsx @@ -1,5 +1,5 @@ import { useState } from "react"; -import { Container, SimpleGrid, Stack, Text } from "@mantine/core"; +import { ScrollArea, SimpleGrid, Stack, Text } from "@mantine/core"; import { ServerCard } from "../../groups/ServerCard/ServerCard"; import { ServerListControls } from "../../groups/ServerListControls/ServerListControls"; import type { ServerCardProps } from "../../groups/ServerCard/ServerCard"; @@ -11,9 +11,8 @@ export interface ServerListScreenProps { onImportServerJson: () => void; } -const PageContainer = Container.withProps({ - size: "xl", - py: "xl", +const PageContainer = Stack.withProps({ + p: "xl", }); const EmptyState = Text.withProps({ @@ -29,6 +28,9 @@ export function ServerListScreen({ onImportServerJson, }: ServerListScreenProps) { const [compact, setCompact] = useState(false); + const [activeServer, setActiveServer] = useState< + string | undefined + >(); function handleToggleList() { setCompact((prev) => (prev === true ? false : true)); @@ -36,32 +38,38 @@ export function ServerListScreen({ return ( - - + + {servers.length === 0 ? ( No servers configured. Add a server to get started. ) : ( {servers.map((server) => ( - + ))} )} - + ); } diff --git a/clients/web/src/components/views/ConnectedView/ConnectedView.stories.tsx b/clients/web/src/components/views/ConnectedView/ConnectedView.stories.tsx index e0efa376..cc47773a 100644 --- a/clients/web/src/components/views/ConnectedView/ConnectedView.stories.tsx +++ b/clients/web/src/components/views/ConnectedView/ConnectedView.stories.tsx @@ -184,9 +184,174 @@ export const ToolsActive: Story = { selected: true, onClick: fn(), }, - { name: "delete_records", selected: false, onClick: fn() }, - { name: "list_users", selected: false, onClick: fn() }, - { name: "batch_process", selected: false, onClick: fn() }, + { + name: "delete_records", + title: "Delete Records", + selected: false, + onClick: fn(), + }, + { + name: "list_users", + title: "List Users", + selected: false, + onClick: fn(), + }, + { + name: "batch_process", + title: "Batch Process", + selected: false, + onClick: fn(), + }, + { + name: "search_files", + title: "Search Files", + selected: false, + onClick: fn(), + }, + { + name: "read_file", + title: "Read File", + selected: false, + onClick: fn(), + }, + { + name: "write_file", + title: "Write File", + selected: false, + onClick: fn(), + }, + { + name: "get_schema", + title: "Get Schema", + selected: false, + onClick: fn(), + }, + { + name: "run_query", + title: "Run Query", + selected: false, + onClick: fn(), + }, + { + name: "create_migration", + title: "Create Migration", + selected: false, + onClick: fn(), + }, + { + name: "validate_config", + title: "Validate Config", + selected: false, + onClick: fn(), + }, + { + name: "deploy_service", + title: "Deploy Service", + selected: false, + onClick: fn(), + }, + { + name: "rollback_deploy", + title: "Rollback Deploy", + selected: false, + onClick: fn(), + }, + { + name: "check_health", + title: "Check Health", + selected: false, + onClick: fn(), + }, + { + name: "generate_report", + title: "Generate Report", + selected: false, + onClick: fn(), + }, + { + name: "export_csv", + title: "Export CSV", + selected: false, + onClick: fn(), + }, + { + name: "import_data", + title: "Import Data", + selected: false, + onClick: fn(), + }, + { + name: "send_notification", + title: "Send Notification", + selected: false, + onClick: fn(), + }, + { + name: "schedule_task", + title: "Schedule Task", + selected: false, + onClick: fn(), + }, + { + name: "cancel_task", + title: "Cancel Task", + selected: false, + onClick: fn(), + }, + { + name: "list_connections", + title: "List Connections", + selected: false, + onClick: fn(), + }, + { + name: "refresh_cache", + title: "Refresh Cache", + selected: false, + onClick: fn(), + }, + { + name: "purge_logs", + title: "Purge Logs", + selected: false, + onClick: fn(), + }, + { + name: "encrypt_secret", + title: "Encrypt Secret", + selected: false, + onClick: fn(), + }, + { + name: "rotate_keys", + title: "Rotate Keys", + selected: false, + onClick: fn(), + }, + { + name: "sync_resources", + title: "Sync Resources", + selected: false, + onClick: fn(), + }, + { + name: "compile_template", + title: "Compile Template", + selected: false, + onClick: fn(), + }, + { + name: "analyze_performance", + title: "Analyze Performance", + selected: false, + onClick: fn(), + }, + { + name: "resize_image", + title: "Resize Image", + selected: false, + onClick: fn(), + }, ]} selectedTool={{ name: "create_record", diff --git a/clients/web/src/components/views/UnconnectedView/UnconnectedView.stories.tsx b/clients/web/src/components/views/UnconnectedView/UnconnectedView.stories.tsx index d32b5010..2707aa53 100644 --- a/clients/web/src/components/views/UnconnectedView/UnconnectedView.stories.tsx +++ b/clients/web/src/components/views/UnconnectedView/UnconnectedView.stories.tsx @@ -59,8 +59,8 @@ export const WithServers: Story = { transport: "stdio", connectionMode: "Via Proxy", command: "npx -y @modelcontextprotocol/server-everything", - status: "connected", - canTestClientFeatures: true, + status: "disconnected", + canTestClientFeatures: false, ...makeServerCallbacks(), }, { @@ -80,9 +80,7 @@ export const WithServers: Story = { transport: "http", connectionMode: "Direct", command: "https://api.example.com/mcp", - status: "failed", - retryCount: 3, - error: { message: "Connection timeout after 20s" }, + status: "disconnected", canTestClientFeatures: false, ...makeServerCallbacks(), }, @@ -106,8 +104,8 @@ export const ManyServers: Story = { transport: "stdio", connectionMode: "Via Proxy", command: "npx -y @modelcontextprotocol/server-everything", - status: "connected", - canTestClientFeatures: true, + status: "disconnected", + canTestClientFeatures: false, ...makeServerCallbacks(), }, { @@ -127,8 +125,8 @@ export const ManyServers: Story = { transport: "http", connectionMode: "Direct", command: "https://api.example.com/mcp", - status: "connected", - canTestClientFeatures: true, + status: "disconnected", + canTestClientFeatures: false, ...makeServerCallbacks(), }, { @@ -137,7 +135,7 @@ export const ManyServers: Story = { transport: "stdio", connectionMode: "Via Proxy", command: "npx -y @modelcontextprotocol/server-postgres", - status: "connecting", + status: "disconnected", canTestClientFeatures: false, ...makeServerCallbacks(), }, @@ -147,8 +145,8 @@ export const ManyServers: Story = { transport: "stdio", connectionMode: "Via Proxy", command: "npx -y @modelcontextprotocol/server-github", - status: "connected", - canTestClientFeatures: true, + status: "disconnected", + canTestClientFeatures: false, ...makeServerCallbacks(), }, { @@ -157,9 +155,7 @@ export const ManyServers: Story = { transport: "http", connectionMode: "Direct", command: "https://slack-mcp.example.com/mcp", - status: "failed", - retryCount: 5, - error: { message: "Authentication failed" }, + status: "disconnected", canTestClientFeatures: false, ...makeServerCallbacks(), }, @@ -169,7 +165,7 @@ export const ManyServers: Story = { transport: "stdio", connectionMode: "Via Proxy", command: "npx -y @modelcontextprotocol/server-memory", - status: "connected", + status: "disconnected", canTestClientFeatures: false, ...makeServerCallbacks(), }, @@ -189,8 +185,8 @@ export const ManyServers: Story = { transport: "stdio", connectionMode: "Via Proxy", command: "npx -y @modelcontextprotocol/server-sqlite ./data.db", - status: "connected", - canTestClientFeatures: true, + status: "disconnected", + canTestClientFeatures: false, ...makeServerCallbacks(), }, { @@ -199,9 +195,7 @@ export const ManyServers: Story = { transport: "stdio", connectionMode: "Via Proxy", command: "npx -y @modelcontextprotocol/server-brave-search", - status: "failed", - retryCount: 2, - error: { message: "API key expired" }, + status: "disconnected", canTestClientFeatures: false, ...makeServerCallbacks(), }, @@ -221,7 +215,7 @@ export const ManyServers: Story = { transport: "stdio", connectionMode: "Via Proxy", command: "npx -y @modelcontextprotocol/server-sequential-thinking", - status: "connecting", + status: "disconnected", canTestClientFeatures: false, ...makeServerCallbacks(), }, @@ -232,28 +226,4 @@ export const ManyServers: Story = { /> ), }, -}; - -export const Connecting: Story = { - args: { - children: ( - - ), - }, -}; +}; \ No newline at end of file