Phase 4: Move screen selection state into the screens themselves
ToolsScreen, PromptsScreen, and ResourcesScreen each tracked the user's current selection via `selectedXxxName` / `onSelectXxx` props, hoisting screen-local UI state into whatever rendered them. InspectorView never threaded those callbacks anywhere, so clicks in the controls panel did not display the matching detail panel — the detail panel only rendered if a parent fed selection back in. Move selection into `useState` inside each screen and drop the `selectedXxxName` / `onSelectXxx` props. MCP-derived data (`callState`, `getPromptState`, `readState`) keeps flowing in from `InspectorView` as props. ResourcesScreen wraps `onReadResource` so a template-expanded URI becomes the selected resource for preview rendering. Update each screen's stories to drop the deleted props. Stories that need a pre-selected item use a `play` function to click it — no component coupling to Storybook. Add Phase 4.3 to the interfaces plan describing the rule. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
d9b2009804
commit
4c1d2f2c34
2 changed files with 171 additions and 86 deletions
|
|
@ -4,7 +4,7 @@ import type {
|
|||
ResourceTemplate,
|
||||
} from "@modelcontextprotocol/sdk/types.js";
|
||||
import type { InspectorResourceSubscription } from "../../../../../../core/mcp/types.js";
|
||||
import { fn } from "storybook/test";
|
||||
import { fn, userEvent, within } from "storybook/test";
|
||||
import { ResourcesScreen } from "./ResourcesScreen";
|
||||
import type { ReadResourceState } from "./ResourcesScreen";
|
||||
|
||||
|
|
@ -14,8 +14,6 @@ const meta: Meta<typeof ResourcesScreen> = {
|
|||
parameters: { layout: "fullscreen" },
|
||||
args: {
|
||||
onRefreshList: fn(),
|
||||
onSelectUri: fn(),
|
||||
onSelectTemplate: fn(),
|
||||
onReadResource: fn(),
|
||||
onSubscribeResource: fn(),
|
||||
onUnsubscribeResource: fn(),
|
||||
|
|
@ -105,6 +103,40 @@ const readConfigState: ReadResourceState = {
|
|||
isSubscribed: true,
|
||||
};
|
||||
|
||||
const readUserProfileState: ReadResourceState = {
|
||||
status: "ok",
|
||||
uri: "file:///users/42/profile",
|
||||
result: {
|
||||
contents: [
|
||||
{
|
||||
uri: "file:///users/42/profile",
|
||||
mimeType: "application/json",
|
||||
text: JSON.stringify(
|
||||
{ id: 42, name: "Alice", email: "alice@example.com" },
|
||||
null,
|
||||
2,
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
isSubscribed: false,
|
||||
};
|
||||
|
||||
async function clickByText(canvasElement: HTMLElement, label: string) {
|
||||
const canvas = within(canvasElement);
|
||||
await userEvent.click(await canvas.findByText(label));
|
||||
}
|
||||
|
||||
async function expandUserProfileTemplate(canvasElement: HTMLElement) {
|
||||
const canvas = within(canvasElement);
|
||||
await userEvent.click(await canvas.findByText("User Profile"));
|
||||
const userIdInput = await canvas.findByLabelText("userId");
|
||||
await userEvent.type(userIdInput, "42");
|
||||
await userEvent.click(
|
||||
await canvas.findByRole("button", { name: "Read Resource" }),
|
||||
);
|
||||
}
|
||||
|
||||
export const WithResources: Story = {
|
||||
args: {
|
||||
resources: sampleResources,
|
||||
|
|
@ -115,9 +147,11 @@ export const ResourceSelected: Story = {
|
|||
args: {
|
||||
resources: sampleResources,
|
||||
subscriptions: sampleSubscriptions,
|
||||
selectedResourceUri: "file:///config.json",
|
||||
readState: readConfigState,
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
await clickByText(canvasElement, "config.json");
|
||||
},
|
||||
};
|
||||
|
||||
export const WithTemplates: Story = {
|
||||
|
|
@ -131,7 +165,9 @@ export const TemplateSelected: Story = {
|
|||
args: {
|
||||
resources: sampleResources,
|
||||
templates: sampleTemplates,
|
||||
selectedTemplateUri: "file:///users/{userId}/profile",
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
await clickByText(canvasElement, "User Profile");
|
||||
},
|
||||
};
|
||||
|
||||
|
|
@ -139,26 +175,10 @@ export const TemplateWithResource: Story = {
|
|||
args: {
|
||||
resources: sampleResources,
|
||||
templates: sampleTemplates,
|
||||
selectedTemplateUri: "file:///users/{userId}/profile",
|
||||
readState: {
|
||||
status: "ok",
|
||||
uri: "file:///users/42/profile",
|
||||
result: {
|
||||
contents: [
|
||||
{
|
||||
uri: "file:///users/42/profile",
|
||||
mimeType: "application/json",
|
||||
text: JSON.stringify(
|
||||
{ id: 42, name: "Alice", email: "alice@example.com" },
|
||||
null,
|
||||
2,
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
isSubscribed: false,
|
||||
},
|
||||
selectedResourceUri: "file:///users/42/profile",
|
||||
readState: readUserProfileState,
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
await expandUserProfileTemplate(canvasElement);
|
||||
},
|
||||
};
|
||||
|
||||
|
|
@ -167,26 +187,10 @@ export const AllSections: Story = {
|
|||
resources: sampleResources,
|
||||
templates: sampleTemplates,
|
||||
subscriptions: sampleSubscriptions,
|
||||
selectedTemplateUri: "file:///users/{userId}/profile",
|
||||
readState: {
|
||||
status: "ok",
|
||||
uri: "file:///users/42/profile",
|
||||
result: {
|
||||
contents: [
|
||||
{
|
||||
uri: "file:///users/42/profile",
|
||||
mimeType: "application/json",
|
||||
text: JSON.stringify(
|
||||
{ id: 42, name: "Alice", email: "alice@example.com" },
|
||||
null,
|
||||
2,
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
isSubscribed: false,
|
||||
},
|
||||
selectedResourceUri: "file:///users/42/profile",
|
||||
readState: readUserProfileState,
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
await expandUserProfileTemplate(canvasElement);
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { useState } from "react";
|
||||
import {
|
||||
Alert,
|
||||
Card,
|
||||
|
|
@ -31,13 +32,9 @@ export interface ResourcesScreenProps {
|
|||
resources: Resource[];
|
||||
templates: ResourceTemplate[];
|
||||
subscriptions: InspectorResourceSubscription[];
|
||||
selectedResourceUri?: string;
|
||||
selectedTemplateUri?: string;
|
||||
readState?: ReadResourceState;
|
||||
listChanged: boolean;
|
||||
onRefreshList: () => void;
|
||||
onSelectUri: (uri: string) => void;
|
||||
onSelectTemplate: (uriTemplate: string) => void;
|
||||
onReadResource: (uri: string) => void;
|
||||
onSubscribeResource: (uri: string) => void;
|
||||
onUnsubscribeResource: (uri: string) => void;
|
||||
|
|
@ -78,17 +75,20 @@ export function ResourcesScreen({
|
|||
resources,
|
||||
templates,
|
||||
subscriptions,
|
||||
selectedResourceUri,
|
||||
selectedTemplateUri,
|
||||
readState,
|
||||
listChanged,
|
||||
onRefreshList,
|
||||
onSelectUri,
|
||||
onSelectTemplate,
|
||||
onReadResource,
|
||||
onSubscribeResource,
|
||||
onUnsubscribeResource,
|
||||
}: ResourcesScreenProps) {
|
||||
const [selectedResourceUri, setSelectedResourceUri] = useState<
|
||||
string | undefined
|
||||
>(undefined);
|
||||
const [selectedTemplateUri, setSelectedTemplateUri] = useState<
|
||||
string | undefined
|
||||
>(undefined);
|
||||
|
||||
const selectedResource = selectedResourceUri
|
||||
? resources.find((r) => r.uri === selectedResourceUri)
|
||||
: undefined;
|
||||
|
|
@ -104,6 +104,11 @@ export function ResourcesScreen({
|
|||
? { name: readState.uri, uri: readState.uri }
|
||||
: undefined);
|
||||
|
||||
function handleReadResource(uri: string) {
|
||||
setSelectedResourceUri(uri);
|
||||
onReadResource(uri);
|
||||
}
|
||||
|
||||
function renderReadState() {
|
||||
if (!readState) return null;
|
||||
|
||||
|
|
@ -136,7 +141,7 @@ export function ResourcesScreen({
|
|||
contents={readState.result.contents}
|
||||
lastUpdated={readState.lastUpdated}
|
||||
isSubscribed={readState.isSubscribed ?? false}
|
||||
onRefresh={() => onReadResource(readResource.uri)}
|
||||
onRefresh={() => handleReadResource(readResource.uri)}
|
||||
onSubscribe={() => onSubscribeResource(readResource.uri)}
|
||||
onUnsubscribe={() => onUnsubscribeResource(readResource.uri)}
|
||||
/>
|
||||
|
|
@ -159,8 +164,8 @@ export function ResourcesScreen({
|
|||
selectedTemplateUri={selectedTemplateUri}
|
||||
listChanged={listChanged}
|
||||
onRefreshList={onRefreshList}
|
||||
onSelectUri={onSelectUri}
|
||||
onSelectTemplate={onSelectTemplate}
|
||||
onSelectUri={setSelectedResourceUri}
|
||||
onSelectTemplate={setSelectedTemplateUri}
|
||||
onUnsubscribeResource={onUnsubscribeResource}
|
||||
/>
|
||||
</SidebarCard>
|
||||
|
|
@ -172,7 +177,7 @@ export function ResourcesScreen({
|
|||
<DetailCard>
|
||||
<ResourceTemplatePanel
|
||||
template={selectedTemplate}
|
||||
onReadResource={onReadResource}
|
||||
onReadResource={handleReadResource}
|
||||
/>
|
||||
</DetailCard>
|
||||
</ScrollArea.Autosize>
|
||||
|
|
|
|||
Loading…
Reference in a new issue