Collapse ServerSettingsForm into accordion and add subtle ListToggle variant

Wraps every section of ServerSettingsForm (Connection Mode, Custom
Headers, Request Metadata, Timeouts, OAuth Settings) in a controlled
Mantine Accordion. The accordion is driven by new required
`expandedSections` / `onExpandedSectionsChange` props so the parent
owns expand/collapse state — consistent with the dumb-component
principle.

Adds a `variant?: "default" | "subtle"` prop to ListToggle. In subtle
mode it renders as an ActionIcon with `variant="subtle"`, `color="gray"`,
and a 16px icon so it visually matches Mantine's CloseButton in modal
headers (transparent until hover).

Adds a WithSettingsModal story on UnconnectedView that presents the
form inside a Mantine Modal with a custom three-column header: subtle
ListToggle on the left (expands/collapses all sections at once),
"Server Settings" Title centered, CloseButton on the right.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
cliffhall 2026-04-05 09:36:14 -04:00
commit 8483b98c37
2 changed files with 264 additions and 96 deletions

View file

@ -6,6 +6,8 @@ const meta: Meta<typeof ServerSettingsForm> = {
title: "Groups/ServerSettingsForm",
component: ServerSettingsForm,
args: {
expandedSections: ["connectionMode"],
onExpandedSectionsChange: fn(),
onConnectionModeChange: fn(),
onAddHeader: fn(),
onRemoveHeader: fn(),

View file

@ -1,14 +1,13 @@
import {
Accordion,
ActionIcon,
Button,
Divider,
Group,
NumberInput,
Select,
Stack,
Text,
TextInput,
Title,
} from "@mantine/core";
export interface KeyValuePair {
@ -16,6 +15,13 @@ export interface KeyValuePair {
value: string;
}
export type ServerSettingsSection =
| "connectionMode"
| "headers"
| "metadata"
| "timeouts"
| "oauth";
export interface ServerSettingsFormProps {
connectionMode: "proxy" | "direct";
headers: KeyValuePair[];
@ -25,6 +31,8 @@ export interface ServerSettingsFormProps {
oauthClientId?: string;
oauthClientSecret?: string;
oauthScopes?: string;
expandedSections: ServerSettingsSection[];
onExpandedSectionsChange: (sections: ServerSettingsSection[]) => void;
onConnectionModeChange: (mode: string) => void;
onAddHeader: () => void;
onRemoveHeader: (index: number) => void;
@ -105,6 +113,8 @@ export function ServerSettingsForm({
oauthClientId,
oauthClientSecret,
oauthScopes,
expandedSections,
onExpandedSectionsChange,
onConnectionModeChange,
onAddHeader,
onRemoveHeader,
@ -122,97 +132,127 @@ export function ServerSettingsForm({
};
return (
<Stack gap="md">
<Title order={5}>Connection Mode</Title>
<Select
data={CONNECTION_MODE_OPTIONS}
value={connectionMode}
onChange={(value) => {
if (value) onConnectionModeChange(value);
}}
description={
connectionMode === "proxy"
? "Route through inspector proxy (required for STDIO)"
: undefined
}
/>
<Accordion
multiple
value={expandedSections}
onChange={(value) =>
onExpandedSectionsChange(value as ServerSettingsSection[])
}
variant="separated"
>
<Accordion.Item value="connectionMode">
<Accordion.Control>Connection Mode</Accordion.Control>
<Accordion.Panel>
<Select
data={CONNECTION_MODE_OPTIONS}
value={connectionMode}
onChange={(value) => {
if (value) onConnectionModeChange(value);
}}
description={
connectionMode === "proxy"
? "Route through inspector proxy (required for STDIO)"
: undefined
}
/>
</Accordion.Panel>
</Accordion.Item>
<Divider />
<Accordion.Item value="headers">
<Accordion.Control>Custom Headers</Accordion.Control>
<Accordion.Panel>
<Stack gap="md">
<Group justify="space-between">
<HintText>
Headers sent with every HTTP request to this server
</HintText>
<AddButton onClick={onAddHeader}>+ Add Header</AddButton>
</Group>
{headers.length === 0 ? (
<EmptyHint>No custom headers configured</EmptyHint>
) : (
<KeyValueRows
items={headers}
onChange={onHeaderChange}
onRemove={onRemoveHeader}
/>
)}
</Stack>
</Accordion.Panel>
</Accordion.Item>
<Group justify="space-between">
<Title order={5}>Custom Headers</Title>
<AddButton onClick={onAddHeader}>+ Add Header</AddButton>
</Group>
<HintText>Headers sent with every HTTP request to this server</HintText>
{headers.length === 0 ? (
<EmptyHint>No custom headers configured</EmptyHint>
) : (
<KeyValueRows
items={headers}
onChange={onHeaderChange}
onRemove={onRemoveHeader}
/>
)}
<Accordion.Item value="metadata">
<Accordion.Control>Request Metadata</Accordion.Control>
<Accordion.Panel>
<Stack gap="md">
<Group justify="space-between">
<HintText>
Metadata sent with every MCP request (included in _meta field)
</HintText>
<AddButton onClick={onAddMetadata}>+ Add Metadata</AddButton>
</Group>
{metadata.length === 0 ? (
<EmptyHint>No request metadata configured</EmptyHint>
) : (
<KeyValueRows
items={metadata}
onChange={onMetadataChange}
onRemove={onRemoveMetadata}
/>
)}
</Stack>
</Accordion.Panel>
</Accordion.Item>
<Divider />
<Accordion.Item value="timeouts">
<Accordion.Control>Timeouts</Accordion.Control>
<Accordion.Panel>
<Group>
<NumberInput
label="Connection Timeout"
suffix=" ms"
value={connectionTimeout}
onChange={handleTimeoutChange("connectionTimeout")}
/>
<NumberInput
label="Request Timeout"
suffix=" ms"
value={requestTimeout}
onChange={handleTimeoutChange("requestTimeout")}
/>
</Group>
</Accordion.Panel>
</Accordion.Item>
<Group justify="space-between">
<Title order={5}>Request Metadata</Title>
<AddButton onClick={onAddMetadata}>+ Add Metadata</AddButton>
</Group>
<HintText>
Metadata sent with every MCP request (included in _meta field)
</HintText>
{metadata.length === 0 ? (
<EmptyHint>No request metadata configured</EmptyHint>
) : (
<KeyValueRows
items={metadata}
onChange={onMetadataChange}
onRemove={onRemoveMetadata}
/>
)}
<Divider />
<Title order={5}>Timeouts</Title>
<Group>
<NumberInput
label="Connection Timeout"
suffix=" ms"
value={connectionTimeout}
onChange={handleTimeoutChange("connectionTimeout")}
/>
<NumberInput
label="Request Timeout"
suffix=" ms"
value={requestTimeout}
onChange={handleTimeoutChange("requestTimeout")}
/>
</Group>
<Divider />
<Title order={5}>OAuth Settings</Title>
<HintText>
Pre-configure OAuth credentials for servers requiring authentication
</HintText>
<TextInput
label="Client ID"
value={oauthClientId ?? ""}
onChange={(e) => onOAuthChange("clientId", e.currentTarget.value)}
/>
<TextInput
label="Client Secret"
value={oauthClientSecret ?? ""}
type="password"
onChange={(e) => onOAuthChange("clientSecret", e.currentTarget.value)}
/>
<TextInput
label="Scopes"
value={oauthScopes ?? ""}
onChange={(e) => onOAuthChange("scopes", e.currentTarget.value)}
/>
</Stack>
<Accordion.Item value="oauth">
<Accordion.Control>OAuth Settings</Accordion.Control>
<Accordion.Panel>
<Stack gap="md">
<HintText>
Pre-configure OAuth credentials for servers requiring
authentication
</HintText>
<TextInput
label="Client ID"
value={oauthClientId ?? ""}
onChange={(e) => onOAuthChange("clientId", e.currentTarget.value)}
/>
<TextInput
label="Client Secret"
value={oauthClientSecret ?? ""}
type="password"
onChange={(e) =>
onOAuthChange("clientSecret", e.currentTarget.value)
}
/>
<TextInput
label="Scopes"
value={oauthScopes ?? ""}
onChange={(e) => onOAuthChange("scopes", e.currentTarget.value)}
/>
</Stack>
</Accordion.Panel>
</Accordion.Item>
</Accordion>
);
}