inspector/clients/web/src/components/screens/ResourcesScreen/ResourcesScreen.tsx
cliffhall d5559b4ea6 Make ResourcesScreen template/resource selection mutually exclusive
Selecting a resource now clears the active template selection (and vice
versa), so the right-hand panel reflects what the user just clicked.
The template's Read Resource button still only sets the resource URI,
preserving the template-with-preview layout for the read flow.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 11:38:11 -04:00

217 lines
5.9 KiB
TypeScript

import { useState } from "react";
import {
Alert,
Card,
Flex,
Group,
Loader,
ScrollArea,
Stack,
Text,
} from "@mantine/core";
import type {
ReadResourceResult,
Resource,
ResourceTemplate,
} from "@modelcontextprotocol/sdk/types.js";
import type { InspectorResourceSubscription } from "../../../../../../core/mcp/types.js";
import { ResourceControls } from "../../groups/ResourceControls/ResourceControls";
import { ResourcePreviewPanel } from "../../groups/ResourcePreviewPanel/ResourcePreviewPanel";
import { ResourceTemplatePanel } from "../../groups/ResourceTemplatePanel/ResourceTemplatePanel";
export interface ReadResourceState {
status: "idle" | "pending" | "ok" | "error";
uri?: string;
result?: ReadResourceResult;
error?: string;
lastUpdated?: Date;
isSubscribed?: boolean;
}
export interface ResourcesScreenProps {
resources: Resource[];
templates: ResourceTemplate[];
subscriptions: InspectorResourceSubscription[];
readState?: ReadResourceState;
listChanged: boolean;
onRefreshList: () => void;
onReadResource: (uri: string) => void;
onSubscribeResource: (uri: string) => void;
onUnsubscribeResource: (uri: string) => void;
}
const ScreenLayout = Flex.withProps({
variant: "screen",
h: "calc(100vh - var(--app-shell-header-height, 0px))",
gap: "md",
p: "xl",
});
const Sidebar = Stack.withProps({
w: 340,
flex: "0 0 auto",
});
const SidebarCard = Card.withProps({
withBorder: true,
padding: "lg",
});
const DetailCard = Card.withProps({
withBorder: true,
padding: "lg",
});
const EmptyState = Text.withProps({
c: "dimmed",
ta: "center",
py: "xl",
});
const SCROLL_MAX_HEIGHT =
"calc(100vh - var(--app-shell-header-height, 0px) - var(--mantine-spacing-xl) * 2)";
export function ResourcesScreen({
resources,
templates,
subscriptions,
readState,
listChanged,
onRefreshList,
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;
const selectedTemplate = selectedTemplateUri
? templates.find((t) => t.uriTemplate === selectedTemplateUri)
: undefined;
// For template-expanded URIs that don't appear in the resources list,
// construct a synthetic Resource so the preview panel can render.
const readResource: Resource | undefined =
selectedResource ??
(readState?.uri && readState.uri === selectedResourceUri
? { name: readState.uri, uri: readState.uri }
: undefined);
function handleSelectResource(uri: string) {
setSelectedTemplateUri(undefined);
setSelectedResourceUri(uri);
}
function handleSelectTemplate(uriTemplate: string) {
setSelectedResourceUri(undefined);
setSelectedTemplateUri(uriTemplate);
}
function handleReadResource(uri: string) {
setSelectedResourceUri(uri);
onReadResource(uri);
}
function renderReadState() {
if (!readState) return null;
if (readState.status === "pending") {
return (
<DetailCard>
<Stack align="center" py="xl">
<Loader size="sm" />
<Text c="dimmed">Reading resource...</Text>
</Stack>
</DetailCard>
);
}
if (readState.status === "error") {
return (
<DetailCard>
<Alert color="red" variant="light" title="Read Error">
{readState.error ?? "Failed to read resource"}
</Alert>
</DetailCard>
);
}
if (readState.result && readResource) {
return (
<DetailCard>
<ResourcePreviewPanel
resource={readResource}
contents={readState.result.contents}
lastUpdated={readState.lastUpdated}
isSubscribed={readState.isSubscribed ?? false}
onRefresh={() => handleReadResource(readResource.uri)}
onSubscribe={() => onSubscribeResource(readResource.uri)}
onUnsubscribe={() => onUnsubscribeResource(readResource.uri)}
/>
</DetailCard>
);
}
return null;
}
return (
<ScreenLayout>
<Sidebar>
<SidebarCard>
<ResourceControls
resources={resources}
templates={templates}
subscriptions={subscriptions}
selectedUri={selectedResourceUri}
selectedTemplateUri={selectedTemplateUri}
listChanged={listChanged}
onRefreshList={onRefreshList}
onSelectUri={handleSelectResource}
onSelectTemplate={handleSelectTemplate}
onUnsubscribeResource={onUnsubscribeResource}
/>
</SidebarCard>
</Sidebar>
{selectedTemplate ? (
<Group flex={1} gap="md" align="flex-start" wrap="nowrap">
<ScrollArea.Autosize flex={1} mah={SCROLL_MAX_HEIGHT}>
<DetailCard>
<ResourceTemplatePanel
template={selectedTemplate}
onReadResource={handleReadResource}
/>
</DetailCard>
</ScrollArea.Autosize>
<ScrollArea.Autosize flex={1} mah={SCROLL_MAX_HEIGHT}>
{renderReadState() ?? (
<DetailCard>
<EmptyState>Enter a URI and click Read to preview</EmptyState>
</DetailCard>
)}
</ScrollArea.Autosize>
</Group>
) : selectedResource ? (
<ScrollArea.Autosize flex={1} mah={SCROLL_MAX_HEIGHT}>
{renderReadState() ?? (
<DetailCard>
<EmptyState>Click to read this resource</EmptyState>
</DetailCard>
)}
</ScrollArea.Autosize>
) : (
<DetailCard flex={1}>
<EmptyState>Select a resource to preview</EmptyState>
</DetailCard>
)}
</ScreenLayout>
);
}