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:
parent
63daed5613
commit
8483b98c37
5 changed files with 264 additions and 96 deletions
|
|
@ -23,3 +23,19 @@ export const Collapsed: Story = {
|
|||
onToggle: fn(),
|
||||
},
|
||||
};
|
||||
|
||||
export const SubtleExpanded: Story = {
|
||||
args: {
|
||||
compact: false,
|
||||
variant: "subtle",
|
||||
onToggle: fn(),
|
||||
},
|
||||
};
|
||||
|
||||
export const SubtleCollapsed: Story = {
|
||||
args: {
|
||||
compact: true,
|
||||
variant: "subtle",
|
||||
onToggle: fn(),
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,14 +1,27 @@
|
|||
import { Button } from "@mantine/core";
|
||||
import { ActionIcon, Button } from "@mantine/core";
|
||||
import { RiExpandVerticalLine, RiCollapseVerticalLine } from "react-icons/ri";
|
||||
|
||||
export interface ListToggleProps {
|
||||
compact: boolean;
|
||||
onToggle: () => void;
|
||||
variant?: "default" | "subtle";
|
||||
}
|
||||
|
||||
export function ListToggle({ compact, onToggle }: ListToggleProps) {
|
||||
export function ListToggle({
|
||||
compact,
|
||||
onToggle,
|
||||
variant = "default",
|
||||
}: ListToggleProps) {
|
||||
const Icon = compact ? RiExpandVerticalLine : RiCollapseVerticalLine;
|
||||
|
||||
if (variant === "subtle") {
|
||||
return (
|
||||
<ActionIcon variant="subtle" color="gray" size="md" onClick={onToggle}>
|
||||
<Icon size={16} />
|
||||
</ActionIcon>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Button size="sm" onClick={onToggle}>
|
||||
<Icon size={20} />
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,22 @@
|
|||
import { useState } from "react";
|
||||
import type { Meta, StoryObj } from "@storybook/react-vite";
|
||||
import { Box, CloseButton, Group, Modal, Title } from "@mantine/core";
|
||||
import { fn } from "storybook/test";
|
||||
import { UnconnectedView } from "./UnconnectedView.js";
|
||||
import { ServerListScreen } from "../../screens/ServerListScreen/ServerListScreen";
|
||||
import {
|
||||
ServerSettingsForm,
|
||||
type ServerSettingsSection,
|
||||
} from "../../groups/ServerSettingsForm/ServerSettingsForm";
|
||||
|
||||
const ALL_SETTINGS_SECTIONS: ServerSettingsSection[] = [
|
||||
"connectionMode",
|
||||
"headers",
|
||||
"metadata",
|
||||
"timeouts",
|
||||
"oauth",
|
||||
];
|
||||
import { ListToggle } from "../../elements/ListToggle/ListToggle";
|
||||
import type { ServerCardProps } from "../../groups/ServerCard/ServerCard";
|
||||
|
||||
const meta: Meta<typeof UnconnectedView> = {
|
||||
|
|
@ -93,6 +108,88 @@ export const WithServers: Story = {
|
|||
},
|
||||
};
|
||||
|
||||
function SettingsModalStory() {
|
||||
const [expandedSections, setExpandedSections] = useState<
|
||||
ServerSettingsSection[]
|
||||
>(["connectionMode"]);
|
||||
const allExpanded = expandedSections.length === ALL_SETTINGS_SECTIONS.length;
|
||||
return (
|
||||
<UnconnectedView onToggleTheme={fn()}>
|
||||
<ServerListScreen
|
||||
servers={[
|
||||
{
|
||||
name: "everything-server",
|
||||
version: "1.0.0",
|
||||
transport: "stdio",
|
||||
connectionMode: "Via Proxy",
|
||||
command: "npx -y @modelcontextprotocol/server-everything",
|
||||
status: "disconnected",
|
||||
canTestClientFeatures: false,
|
||||
...makeServerCallbacks(),
|
||||
},
|
||||
]}
|
||||
onAddManually={fn()}
|
||||
onImportConfig={fn()}
|
||||
onImportServerJson={fn()}
|
||||
/>
|
||||
<Modal
|
||||
opened
|
||||
onClose={fn()}
|
||||
withCloseButton={false}
|
||||
title={
|
||||
<Group justify="space-between" wrap="nowrap" w="100%">
|
||||
<ListToggle
|
||||
compact={allExpanded}
|
||||
variant="subtle"
|
||||
onToggle={() =>
|
||||
setExpandedSections(allExpanded ? [] : ALL_SETTINGS_SECTIONS)
|
||||
}
|
||||
/>
|
||||
<Title order={4} ta="center" style={{ flex: 1 }}>
|
||||
Server Settings
|
||||
</Title>
|
||||
<Box>
|
||||
<CloseButton onClick={fn()} />
|
||||
</Box>
|
||||
</Group>
|
||||
}
|
||||
size="lg"
|
||||
centered
|
||||
styles={{ title: { flex: 1 } }}
|
||||
>
|
||||
<ServerSettingsForm
|
||||
connectionMode="proxy"
|
||||
headers={[
|
||||
{ key: "Authorization", value: "Bearer token-abc-123" },
|
||||
{ key: "X-Request-Id", value: "req-456" },
|
||||
]}
|
||||
metadata={[{ key: "userId", value: "user-789" }]}
|
||||
connectionTimeout={30000}
|
||||
requestTimeout={60000}
|
||||
oauthClientId="my-client-id"
|
||||
oauthClientSecret="super-secret-value"
|
||||
oauthScopes="read write"
|
||||
expandedSections={expandedSections}
|
||||
onExpandedSectionsChange={setExpandedSections}
|
||||
onConnectionModeChange={fn()}
|
||||
onAddHeader={fn()}
|
||||
onRemoveHeader={fn()}
|
||||
onHeaderChange={fn()}
|
||||
onAddMetadata={fn()}
|
||||
onRemoveMetadata={fn()}
|
||||
onMetadataChange={fn()}
|
||||
onTimeoutChange={fn()}
|
||||
onOAuthChange={fn()}
|
||||
/>
|
||||
</Modal>
|
||||
</UnconnectedView>
|
||||
);
|
||||
}
|
||||
|
||||
export const WithSettingsModal: Story = {
|
||||
render: () => <SettingsModalStory />,
|
||||
};
|
||||
|
||||
export const ManyServers: Story = {
|
||||
args: {
|
||||
children: (
|
||||
|
|
|
|||
Loading…
Reference in a new issue