diff --git a/clients/web/src/components/groups/ServerSettingsForm/ServerSettingsForm.stories.tsx b/clients/web/src/components/groups/ServerSettingsForm/ServerSettingsForm.stories.tsx index d82852f4..7c3939ba 100644 --- a/clients/web/src/components/groups/ServerSettingsForm/ServerSettingsForm.stories.tsx +++ b/clients/web/src/components/groups/ServerSettingsForm/ServerSettingsForm.stories.tsx @@ -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(initialSections); + + return ( + + ); +} + const meta: Meta = { 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; export const DefaultSettings: Story = { - args: { - settings: defaultSettings, - }, + render: () => , }; export const WithHeaders: Story = { - args: { - settings: { - ...defaultSettings, - headers: [ - { key: "Authorization", value: "Bearer token-abc-123" }, - { key: "X-Custom-Header", value: "custom-value" }, - ], - }, - }, + render: () => ( + + ), }; export const WithOAuth: Story = { - args: { - settings: { - ...defaultSettings, - connectionMode: "direct", - oauthClientId: "my-client-id", - oauthClientSecret: "super-secret-value", - oauthScopes: "read write admin", - }, - }, + render: () => ( + + ), }; 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: () => ( + + ), }; diff --git a/clients/web/src/components/groups/ServerSettingsForm/ServerSettingsForm.tsx b/clients/web/src/components/groups/ServerSettingsForm/ServerSettingsForm.tsx index fef1517d..d1da1e4e 100644 --- a/clients/web/src/components/groups/ServerSettingsForm/ServerSettingsForm.tsx +++ b/clients/web/src/components/groups/ServerSettingsForm/ServerSettingsForm.tsx @@ -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" > diff --git a/clients/web/src/components/groups/ServerSettingsModal/ServerSettingsModal.stories.tsx b/clients/web/src/components/groups/ServerSettingsModal/ServerSettingsModal.stories.tsx new file mode 100644 index 00000000..74a4e37d --- /dev/null +++ b/clients/web/src/components/groups/ServerSettingsModal/ServerSettingsModal.stories.tsx @@ -0,0 +1,62 @@ +import { useState } from "react"; +import type { InspectorServerSettings } from "@inspector/core/mcp/types.js"; +import type { Meta, StoryObj } from "@storybook/react-vite"; +import { ServerSettingsModal } from "./ServerSettingsModal"; + +const initialSettings: InspectorServerSettings = { + 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", +}; + +function InteractiveModal({ + startSettings, +}: { + startSettings: InspectorServerSettings; +}) { + const [settings, setSettings] = + useState(startSettings); + + return ( + {}} + onSettingsChange={setSettings} + /> + ); +} + +const meta: Meta = { + title: "Groups/ServerSettingsModal", + component: ServerSettingsModal, +}; + +export default meta; +type Story = StoryObj; + +export const FullyConfigured: Story = { + render: () => , +}; + +export const EmptySettings: Story = { + render: () => ( + + ), +}; diff --git a/clients/web/src/components/groups/ServerSettingsModal/ServerSettingsModal.tsx b/clients/web/src/components/groups/ServerSettingsModal/ServerSettingsModal.tsx new file mode 100644 index 00000000..3d2b3a25 --- /dev/null +++ b/clients/web/src/components/groups/ServerSettingsModal/ServerSettingsModal.tsx @@ -0,0 +1,139 @@ +import { useState } from "react"; +import { CloseButton, Group, Modal, Title } from "@mantine/core"; +import type { InspectorServerSettings } from "@inspector/core/mcp/types.js"; +import { ListToggle } from "../../elements/ListToggle/ListToggle"; +import { + ServerSettingsForm, + type ServerSettingsSection, +} from "../ServerSettingsForm/ServerSettingsForm"; + +const ALL_SECTIONS: ServerSettingsSection[] = [ + "connectionMode", + "headers", + "metadata", + "timeouts", + "oauth", +]; + +export interface ServerSettingsModalProps { + opened: boolean; + settings: InspectorServerSettings; + onClose: () => void; + onSettingsChange: (settings: InspectorServerSettings) => void; +} + +export function ServerSettingsModal({ + opened, + settings, + onClose, + onSettingsChange, +}: ServerSettingsModalProps) { + const [expandedSections, setExpandedSections] = useState< + ServerSettingsSection[] + >(["connectionMode"]); + + const allExpanded = expandedSections.length === ALL_SECTIONS.length; + + function handleToggleAll() { + setExpandedSections(allExpanded ? [] : ALL_SECTIONS); + } + + function handleConnectionModeChange(mode: "proxy" | "direct") { + onSettingsChange({ ...settings, connectionMode: mode }); + } + + function handleAddHeader() { + onSettingsChange({ + ...settings, + headers: [...settings.headers, { key: "", value: "" }], + }); + } + + function handleRemoveHeader(index: number) { + onSettingsChange({ + ...settings, + headers: settings.headers.filter((_, i) => i !== index), + }); + } + + function handleHeaderChange(index: number, key: string, value: string) { + const headers = settings.headers.map((h, i) => + i === index ? { key, value } : h, + ); + onSettingsChange({ ...settings, headers }); + } + + function handleAddMetadata() { + onSettingsChange({ + ...settings, + metadata: [...settings.metadata, { key: "", value: "" }], + }); + } + + function handleRemoveMetadata(index: number) { + onSettingsChange({ + ...settings, + metadata: settings.metadata.filter((_, i) => i !== index), + }); + } + + function handleMetadataChange(index: number, key: string, value: string) { + const metadata = settings.metadata.map((m, i) => + i === index ? { key, value } : m, + ); + onSettingsChange({ ...settings, metadata }); + } + + function handleTimeoutChange(field: string, value: number) { + onSettingsChange({ ...settings, [field]: value }); + } + + function handleOAuthChange(field: string, value: string) { + const oauthFieldMap: Record = { + clientId: "oauthClientId", + clientSecret: "oauthClientSecret", + scopes: "oauthScopes", + }; + const settingsKey = oauthFieldMap[field] ?? field; + onSettingsChange({ ...settings, [settingsKey]: value }); + } + + return ( + + + + Server Settings + + + + } + size="lg" + centered + styles={{ title: { flex: 1 } }} + > + + + ); +} diff --git a/clients/web/src/components/views/UnconnectedView/UnconnectedView.stories.tsx b/clients/web/src/components/views/UnconnectedView/UnconnectedView.stories.tsx index 1e7a6c17..15d198ad 100644 --- a/clients/web/src/components/views/UnconnectedView/UnconnectedView.stories.tsx +++ b/clients/web/src/components/views/UnconnectedView/UnconnectedView.stories.tsx @@ -1,9 +1,10 @@ +import { useState } from "react"; +import type { InspectorServerSettings } from "@inspector/core/mcp/types.js"; import type { Meta, StoryObj } from "@storybook/react-vite"; -import { 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 } from "../../groups/ServerSettingsForm/ServerSettingsForm"; +import { ServerSettingsModal } from "../../groups/ServerSettingsModal/ServerSettingsModal"; import type { ServerCardProps } from "../../groups/ServerCard/ServerCard"; const meta: Meta = { @@ -112,6 +113,20 @@ export const WithServers: Story = { }; function SettingsModalStory() { + const [settings, setSettings] = useState({ + 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", + }); + return ( - - - Server Settings - - - - } - size="lg" - centered - styles={{ title: { flex: 1 } }} - > - - + onSettingsChange={setSettings} + /> ); }