Add ServerSettingsModal group, revert form to controlled component

Revert ServerSettingsForm to a controlled component with
expandedSections/onExpandedSectionsChange props. The form is a
dumb component — accordion state belongs to its container.

Create ServerSettingsModal group that manages:
- Accordion expand/collapse state with ListToggle expand-all button
- Add/remove/edit header pairs
- Add/remove/edit metadata pairs
- Connection mode, timeout, and OAuth changes
- All changes flow through a single onSettingsChange callback

Update ServerSettingsForm stories with useState render functions
so accordion sections are interactive.

Update UnconnectedView WithSettingsModal story to use the new
ServerSettingsModal component.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
cliffhall 2026-04-21 17:13:57 -04:00
commit a6fc900271
2 changed files with 307 additions and 100 deletions

View file

@ -1,7 +1,11 @@
import { useState } from "react";
import type { InspectorServerSettings } from "@inspector/core/mcp/types.js";
import type { Meta, StoryObj } from "@storybook/react-vite";
import { fn } from "storybook/test";
import { ServerSettingsForm } from "./ServerSettingsForm";
import {
ServerSettingsForm,
type ServerSettingsSection,
} from "./ServerSettingsForm";
const defaultSettings: InspectorServerSettings = {
connectionMode: "proxy",
@ -11,72 +15,97 @@ const defaultSettings: InspectorServerSettings = {
requestTimeout: 60000,
};
const defaultHandlers = {
onConnectionModeChange: fn(),
onAddHeader: fn(),
onRemoveHeader: fn(),
onHeaderChange: fn(),
onAddMetadata: fn(),
onRemoveMetadata: fn(),
onMetadataChange: fn(),
onTimeoutChange: fn(),
onOAuthChange: fn(),
};
function InteractiveForm({
settings,
initialSections = ["connectionMode"],
}: {
settings: InspectorServerSettings;
initialSections?: ServerSettingsSection[];
}) {
const [expandedSections, setExpandedSections] =
useState<ServerSettingsSection[]>(initialSections);
return (
<ServerSettingsForm
settings={settings}
expandedSections={expandedSections}
onExpandedSectionsChange={setExpandedSections}
{...defaultHandlers}
/>
);
}
const meta: Meta<typeof ServerSettingsForm> = {
title: "Groups/ServerSettingsForm",
component: ServerSettingsForm,
args: {
onConnectionModeChange: fn(),
onAddHeader: fn(),
onRemoveHeader: fn(),
onHeaderChange: fn(),
onAddMetadata: fn(),
onRemoveMetadata: fn(),
onMetadataChange: fn(),
onTimeoutChange: fn(),
onOAuthChange: fn(),
},
};
export default meta;
type Story = StoryObj<typeof ServerSettingsForm>;
export const DefaultSettings: Story = {
args: {
settings: defaultSettings,
},
render: () => <InteractiveForm settings={defaultSettings} />,
};
export const WithHeaders: Story = {
args: {
settings: {
...defaultSettings,
headers: [
{ key: "Authorization", value: "Bearer token-abc-123" },
{ key: "X-Custom-Header", value: "custom-value" },
],
},
},
render: () => (
<InteractiveForm
settings={{
...defaultSettings,
headers: [
{ key: "Authorization", value: "Bearer token-abc-123" },
{ key: "X-Custom-Header", value: "custom-value" },
],
}}
/>
),
};
export const WithOAuth: Story = {
args: {
settings: {
...defaultSettings,
connectionMode: "direct",
oauthClientId: "my-client-id",
oauthClientSecret: "super-secret-value",
oauthScopes: "read write admin",
},
},
render: () => (
<InteractiveForm
settings={{
...defaultSettings,
connectionMode: "direct",
oauthClientId: "my-client-id",
oauthClientSecret: "super-secret-value",
oauthScopes: "read write admin",
}}
/>
),
};
export const AllConfigured: Story = {
args: {
settings: {
connectionMode: "proxy",
headers: [
{ key: "Authorization", value: "Bearer token-abc-123" },
{ key: "X-Request-Id", value: "req-456" },
],
metadata: [
{ key: "userId", value: "user-789" },
{ key: "sessionId", value: "session-012" },
],
connectionTimeout: 15000,
requestTimeout: 45000,
oauthClientId: "my-client-id",
oauthClientSecret: "super-secret-value",
oauthScopes: "read write",
},
},
render: () => (
<InteractiveForm
settings={{
connectionMode: "proxy",
headers: [
{ key: "Authorization", value: "Bearer token-abc-123" },
{ key: "X-Request-Id", value: "req-456" },
],
metadata: [
{ key: "userId", value: "user-789" },
{ key: "sessionId", value: "session-012" },
],
connectionTimeout: 15000,
requestTimeout: 45000,
oauthClientId: "my-client-id",
oauthClientSecret: "super-secret-value",
oauthScopes: "read write",
}}
/>
),
};

View file

@ -1,4 +1,3 @@
import { useState } from "react";
import {
Accordion,
ActionIcon,
@ -21,7 +20,8 @@ export type ServerSettingsSection =
export interface ServerSettingsFormProps {
settings: InspectorServerSettings;
defaultExpandedSections?: ServerSettingsSection[];
expandedSections: ServerSettingsSection[];
onExpandedSectionsChange: (sections: ServerSettingsSection[]) => void;
onConnectionModeChange: (mode: "proxy" | "direct") => void;
onAddHeader: () => void;
onRemoveHeader: (index: number) => void;
@ -95,7 +95,8 @@ function KeyValueRows({
export function ServerSettingsForm({
settings,
defaultExpandedSections = ["connectionMode"],
expandedSections,
onExpandedSectionsChange,
onConnectionModeChange,
onAddHeader,
onRemoveHeader,
@ -106,10 +107,6 @@ export function ServerSettingsForm({
onTimeoutChange,
onOAuthChange,
}: ServerSettingsFormProps) {
const [expandedSections, setExpandedSections] = useState<
ServerSettingsSection[]
>(defaultExpandedSections);
const handleTimeoutChange = (field: string) => (value: number | string) => {
const numValue =
typeof value === "string" ? parseInt(value, 10) || 0 : value;
@ -121,7 +118,7 @@ export function ServerSettingsForm({
multiple
value={expandedSections}
onChange={(value) =>
setExpandedSections(value as ServerSettingsSection[])
onExpandedSectionsChange(value as ServerSettingsSection[])
}
variant="separated"
>