Refactor renamed the component groups to something less obtuse:
Elements -> Groups -> Screens -> Views
This commit is contained in:
parent
ca6de91a96
commit
dd46acba87
14 changed files with 151 additions and 144 deletions
|
|
@ -0,0 +1,126 @@
|
|||
import type { Meta, StoryObj } from "@storybook/react-vite";
|
||||
import { fn } from "storybook/test";
|
||||
import { HistoryScreen } from "./HistoryScreen";
|
||||
import type { HistoryEntryProps } from "../../groups/HistoryEntry/HistoryEntry";
|
||||
|
||||
const meta: Meta<typeof HistoryScreen> = {
|
||||
title: "Screens/HistoryScreen",
|
||||
component: HistoryScreen,
|
||||
parameters: { layout: "fullscreen" },
|
||||
args: {
|
||||
searchText: "",
|
||||
onSearchChange: fn(),
|
||||
onMethodFilterChange: fn(),
|
||||
onLoadMore: fn(),
|
||||
onClearAll: fn(),
|
||||
onExport: fn(),
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof HistoryScreen>;
|
||||
|
||||
const entry1: HistoryEntryProps = {
|
||||
timestamp: "2026-03-17T10:00:00Z",
|
||||
method: "tools/call",
|
||||
target: "send_message",
|
||||
status: "success",
|
||||
durationMs: 120,
|
||||
parameters: { message: "Hello, world!" },
|
||||
response: { result: "Message sent successfully" },
|
||||
isPinned: false,
|
||||
isExpanded: false,
|
||||
onToggleExpand: fn(),
|
||||
onReplay: fn(),
|
||||
onTogglePin: fn(),
|
||||
};
|
||||
|
||||
const entry2: HistoryEntryProps = {
|
||||
timestamp: "2026-03-17T10:01:00Z",
|
||||
method: "resources/read",
|
||||
target: "config.json",
|
||||
status: "success",
|
||||
durationMs: 45,
|
||||
parameters: { uri: "file:///config.json" },
|
||||
response: { contents: [{ uri: "file:///config.json", text: "{}" }] },
|
||||
isPinned: false,
|
||||
isExpanded: false,
|
||||
onToggleExpand: fn(),
|
||||
onReplay: fn(),
|
||||
onTogglePin: fn(),
|
||||
};
|
||||
|
||||
const entry3: HistoryEntryProps = {
|
||||
timestamp: "2026-03-17T10:02:00Z",
|
||||
method: "tools/call",
|
||||
target: "delete_records",
|
||||
status: "error",
|
||||
durationMs: 350,
|
||||
parameters: { ids: [1, 2, 3] },
|
||||
response: { error: "Permission denied" },
|
||||
isPinned: false,
|
||||
isExpanded: true,
|
||||
onToggleExpand: fn(),
|
||||
onReplay: fn(),
|
||||
onTogglePin: fn(),
|
||||
};
|
||||
|
||||
const pinnedEntry1: HistoryEntryProps = {
|
||||
timestamp: "2026-03-17T09:30:00Z",
|
||||
method: "tools/list",
|
||||
status: "success",
|
||||
durationMs: 80,
|
||||
response: { tools: ["send_message", "list_users"] },
|
||||
isPinned: true,
|
||||
pinLabel: "Tool discovery",
|
||||
isExpanded: false,
|
||||
onToggleExpand: fn(),
|
||||
onReplay: fn(),
|
||||
onTogglePin: fn(),
|
||||
};
|
||||
|
||||
const pinnedEntry2: HistoryEntryProps = {
|
||||
timestamp: "2026-03-17T09:35:00Z",
|
||||
method: "prompts/get",
|
||||
target: "greeting",
|
||||
status: "success",
|
||||
durationMs: 60,
|
||||
parameters: { name: "greeting" },
|
||||
response: {
|
||||
messages: [{ role: "user", content: { type: "text", text: "Hello!" } }],
|
||||
},
|
||||
isPinned: true,
|
||||
pinLabel: "Greeting prompt",
|
||||
isExpanded: false,
|
||||
onToggleExpand: fn(),
|
||||
onReplay: fn(),
|
||||
onTogglePin: fn(),
|
||||
};
|
||||
|
||||
export const WithEntries: Story = {
|
||||
args: {
|
||||
entries: [entry1, entry2, entry3],
|
||||
pinnedEntries: [pinnedEntry1, pinnedEntry2],
|
||||
totalCount: 25,
|
||||
displayedCount: 3,
|
||||
},
|
||||
};
|
||||
|
||||
export const Empty: Story = {
|
||||
args: {
|
||||
entries: [],
|
||||
pinnedEntries: [],
|
||||
totalCount: 0,
|
||||
displayedCount: 0,
|
||||
},
|
||||
};
|
||||
|
||||
export const WithFilter: Story = {
|
||||
args: {
|
||||
entries: [entry1, entry3],
|
||||
pinnedEntries: [],
|
||||
methodFilter: "tools/call",
|
||||
totalCount: 10,
|
||||
displayedCount: 2,
|
||||
},
|
||||
};
|
||||
|
|
@ -0,0 +1,155 @@
|
|||
import {
|
||||
Button,
|
||||
Container,
|
||||
Divider,
|
||||
Group,
|
||||
Paper,
|
||||
Select,
|
||||
Stack,
|
||||
Text,
|
||||
TextInput,
|
||||
Title,
|
||||
} from "@mantine/core";
|
||||
import { HistoryEntry } from "../../groups/HistoryEntry/HistoryEntry";
|
||||
import type { HistoryEntryProps } from "../../groups/HistoryEntry/HistoryEntry";
|
||||
|
||||
export interface HistoryScreenProps {
|
||||
entries: HistoryEntryProps[];
|
||||
pinnedEntries: HistoryEntryProps[];
|
||||
searchText: string;
|
||||
methodFilter?: string;
|
||||
totalCount: number;
|
||||
displayedCount: number;
|
||||
onSearchChange: (text: string) => void;
|
||||
onMethodFilterChange: (method: string) => void;
|
||||
onLoadMore: () => void;
|
||||
onClearAll: () => void;
|
||||
onExport: () => void;
|
||||
}
|
||||
|
||||
const METHOD_OPTIONS = [
|
||||
"tools/call",
|
||||
"tools/list",
|
||||
"resources/read",
|
||||
"resources/list",
|
||||
"prompts/get",
|
||||
"prompts/list",
|
||||
"sampling/createMessage",
|
||||
"elicitation/create",
|
||||
];
|
||||
|
||||
const PageContainer = Container.withProps({
|
||||
size: "xl",
|
||||
py: "xl",
|
||||
});
|
||||
|
||||
const ContentPanel = Paper.withProps({
|
||||
withBorder: true,
|
||||
p: "md",
|
||||
});
|
||||
|
||||
const HeaderRow = Group.withProps({
|
||||
justify: "space-between",
|
||||
wrap: "wrap",
|
||||
});
|
||||
|
||||
const ToolbarButton = Button.withProps({
|
||||
variant: "light",
|
||||
size: "sm",
|
||||
});
|
||||
|
||||
const EmptyState = Text.withProps({
|
||||
c: "dimmed",
|
||||
ta: "center",
|
||||
py: "xl",
|
||||
});
|
||||
|
||||
const CountText = Text.withProps({
|
||||
size: "sm",
|
||||
c: "dimmed",
|
||||
});
|
||||
|
||||
function entryKey(entry: HistoryEntryProps): string {
|
||||
return `${entry.timestamp}-${entry.method}`;
|
||||
}
|
||||
|
||||
function formatPinnedTitle(count: number): string {
|
||||
return `Pinned Requests (${count})`;
|
||||
}
|
||||
|
||||
function formatPagination(displayed: number, total: number): string {
|
||||
return `Showing ${displayed} of ${total} entries`;
|
||||
}
|
||||
|
||||
export function HistoryScreen({
|
||||
entries,
|
||||
pinnedEntries,
|
||||
searchText,
|
||||
methodFilter,
|
||||
totalCount,
|
||||
displayedCount,
|
||||
onSearchChange,
|
||||
onMethodFilterChange,
|
||||
onLoadMore,
|
||||
onClearAll,
|
||||
onExport,
|
||||
}: HistoryScreenProps) {
|
||||
return (
|
||||
<PageContainer>
|
||||
<ContentPanel>
|
||||
<Stack gap="md">
|
||||
<HeaderRow>
|
||||
<Title order={3}>Request History</Title>
|
||||
<Group>
|
||||
<TextInput
|
||||
placeholder="Search..."
|
||||
value={searchText}
|
||||
onChange={(event) => onSearchChange(event.currentTarget.value)}
|
||||
/>
|
||||
<Select
|
||||
placeholder="Filter by method"
|
||||
data={METHOD_OPTIONS}
|
||||
value={methodFilter}
|
||||
onChange={(value) => onMethodFilterChange(value ?? "")}
|
||||
clearable
|
||||
/>
|
||||
<ToolbarButton onClick={onExport}>Export JSON</ToolbarButton>
|
||||
<ToolbarButton onClick={onClearAll}>Clear All</ToolbarButton>
|
||||
</Group>
|
||||
</HeaderRow>
|
||||
|
||||
{entries.length === 0 ? (
|
||||
<EmptyState>No request history</EmptyState>
|
||||
) : (
|
||||
<Stack gap="md">
|
||||
{entries.map((entry) => (
|
||||
<HistoryEntry key={entryKey(entry)} {...entry} />
|
||||
))}
|
||||
</Stack>
|
||||
)}
|
||||
|
||||
{pinnedEntries.length > 0 && (
|
||||
<>
|
||||
<Divider />
|
||||
<Title order={4}>{formatPinnedTitle(pinnedEntries.length)}</Title>
|
||||
<Stack gap="sm">
|
||||
{pinnedEntries.map((entry) => (
|
||||
<HistoryEntry key={entryKey(entry)} {...entry} />
|
||||
))}
|
||||
</Stack>
|
||||
</>
|
||||
)}
|
||||
|
||||
<Group justify="flex-end">
|
||||
<CountText>
|
||||
{formatPagination(displayedCount, totalCount)}
|
||||
</CountText>
|
||||
{displayedCount < totalCount && (
|
||||
<ToolbarButton onClick={onLoadMore}>Load More</ToolbarButton>
|
||||
)}
|
||||
</Group>
|
||||
</Stack>
|
||||
</ContentPanel>
|
||||
</PageContainer>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,246 @@
|
|||
import type { Meta, StoryObj } from "@storybook/react-vite";
|
||||
import { fn } from "storybook/test";
|
||||
import { LoggingScreen } from "./LoggingScreen";
|
||||
import type { LogControlsProps } from "../../groups/LogControls/LogControls";
|
||||
import type { LogEntryProps, LogLevel } from "../../elements/LogEntry/LogEntry";
|
||||
|
||||
function makeControls(): LogControlsProps {
|
||||
return {
|
||||
currentLevel: "info",
|
||||
filterText: "",
|
||||
visibleLevels: {
|
||||
debug: true,
|
||||
info: true,
|
||||
notice: true,
|
||||
warning: true,
|
||||
error: true,
|
||||
critical: true,
|
||||
alert: true,
|
||||
emergency: true,
|
||||
},
|
||||
onSetLevel: fn(),
|
||||
onFilterChange: fn(),
|
||||
onToggleLevel: fn(),
|
||||
onClear: fn(),
|
||||
onExport: fn(),
|
||||
};
|
||||
}
|
||||
|
||||
const meta: Meta<typeof LoggingScreen> = {
|
||||
title: "Screens/LoggingScreen",
|
||||
component: LoggingScreen,
|
||||
parameters: { layout: "fullscreen" },
|
||||
args: {
|
||||
onToggleAutoScroll: fn(),
|
||||
onCopyAll: fn(),
|
||||
autoScroll: true,
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof LoggingScreen>;
|
||||
|
||||
export const Empty: Story = {
|
||||
args: {
|
||||
controls: makeControls(),
|
||||
entries: [],
|
||||
},
|
||||
};
|
||||
|
||||
const logMessages: { level: LogLevel; message: string; logger?: string }[] = [
|
||||
{ level: "info", message: "Server started on port 3000" },
|
||||
{
|
||||
level: "debug",
|
||||
message: "Loading configuration from /etc/mcp/config.json",
|
||||
logger: "config",
|
||||
},
|
||||
{
|
||||
level: "warning",
|
||||
message: "Deprecated API endpoint called: /v1/tools",
|
||||
logger: "http",
|
||||
},
|
||||
{ level: "info", message: "Client connected: inspector-web-ui" },
|
||||
{
|
||||
level: "error",
|
||||
message: "Failed to read resource: file not found at /data/missing.txt",
|
||||
logger: "resources",
|
||||
},
|
||||
{
|
||||
level: "info",
|
||||
message: "Tool execution completed: search_files (245ms)",
|
||||
logger: "tools",
|
||||
},
|
||||
{
|
||||
level: "debug",
|
||||
message: "Parsing JSON-RPC request body",
|
||||
logger: "transport",
|
||||
},
|
||||
{
|
||||
level: "info",
|
||||
message: "Listing available tools for client session",
|
||||
logger: "tools",
|
||||
},
|
||||
{
|
||||
level: "warning",
|
||||
message: "Slow query detected: 1200ms for resource lookup",
|
||||
logger: "db",
|
||||
},
|
||||
{
|
||||
level: "info",
|
||||
message: "Resource subscription added: file:///config.json",
|
||||
logger: "resources",
|
||||
},
|
||||
{
|
||||
level: "debug",
|
||||
message: "Heartbeat ping received from client",
|
||||
logger: "session",
|
||||
},
|
||||
{
|
||||
level: "error",
|
||||
message: "Permission denied reading /etc/secrets.env",
|
||||
logger: "resources",
|
||||
},
|
||||
{
|
||||
level: "info",
|
||||
message: "Prompt 'summarize' resolved with 2 messages",
|
||||
logger: "prompts",
|
||||
},
|
||||
{
|
||||
level: "debug",
|
||||
message: "Cache hit for resource: db://users/42",
|
||||
logger: "cache",
|
||||
},
|
||||
{
|
||||
level: "info",
|
||||
message: "Tool 'create_record' executed successfully in 89ms",
|
||||
logger: "tools",
|
||||
},
|
||||
{
|
||||
level: "warning",
|
||||
message: "Client reconnection attempt #3",
|
||||
logger: "session",
|
||||
},
|
||||
{
|
||||
level: "info",
|
||||
message: "New resource detected: file:///data/output.csv",
|
||||
logger: "resources",
|
||||
},
|
||||
{
|
||||
level: "debug",
|
||||
message: "Serializing response payload (2.4KB)",
|
||||
logger: "transport",
|
||||
},
|
||||
{
|
||||
level: "error",
|
||||
message: "Timeout waiting for tool response after 30s",
|
||||
logger: "tools",
|
||||
},
|
||||
{
|
||||
level: "info",
|
||||
message: "Session initialized with capabilities: tools, resources, prompts",
|
||||
},
|
||||
{
|
||||
level: "debug",
|
||||
message: "Validating input schema for tool 'batch_process'",
|
||||
logger: "tools",
|
||||
},
|
||||
{
|
||||
level: "info",
|
||||
message: "Resource template resolved: db://tables/users/rows/15",
|
||||
logger: "resources",
|
||||
},
|
||||
{
|
||||
level: "warning",
|
||||
message: "Memory usage at 78% — consider increasing limits",
|
||||
logger: "system",
|
||||
},
|
||||
{
|
||||
level: "info",
|
||||
message: "Client disconnected gracefully",
|
||||
logger: "session",
|
||||
},
|
||||
{
|
||||
level: "debug",
|
||||
message: "Flushing log buffer to disk (128 entries)",
|
||||
logger: "logging",
|
||||
},
|
||||
];
|
||||
|
||||
function pad(n: number): string {
|
||||
return n.toString().padStart(2, "0");
|
||||
}
|
||||
|
||||
const mixedEntries: LogEntryProps[] = Array.from({ length: 50 }, (_, i) => {
|
||||
const src = logMessages[i % logMessages.length];
|
||||
const seconds = pad(i + 1);
|
||||
return {
|
||||
timestamp: `2026-03-17T10:00:${seconds}Z`,
|
||||
level: src.level,
|
||||
message: src.message,
|
||||
logger: src.logger,
|
||||
};
|
||||
});
|
||||
|
||||
export const WithEntries: Story = {
|
||||
args: {
|
||||
controls: makeControls(),
|
||||
entries: mixedEntries,
|
||||
},
|
||||
};
|
||||
|
||||
const allLevelEntries: LogEntryProps[] = [
|
||||
{
|
||||
timestamp: "2026-03-17T10:00:01Z",
|
||||
level: "debug",
|
||||
message: "Resolving transport handler for stdio connection",
|
||||
logger: "transport",
|
||||
},
|
||||
{
|
||||
timestamp: "2026-03-17T10:00:02Z",
|
||||
level: "info",
|
||||
message: "MCP session initialized successfully",
|
||||
},
|
||||
{
|
||||
timestamp: "2026-03-17T10:00:03Z",
|
||||
level: "notice",
|
||||
message: "Server capabilities negotiated: tools, resources, prompts",
|
||||
logger: "session",
|
||||
},
|
||||
{
|
||||
timestamp: "2026-03-17T10:00:04Z",
|
||||
level: "warning",
|
||||
message: "Rate limit approaching: 85% of quota used",
|
||||
logger: "ratelimit",
|
||||
},
|
||||
{
|
||||
timestamp: "2026-03-17T10:00:05Z",
|
||||
level: "error",
|
||||
message: "Tool execution failed: timeout after 30s",
|
||||
logger: "tools",
|
||||
},
|
||||
{
|
||||
timestamp: "2026-03-17T10:00:06Z",
|
||||
level: "critical",
|
||||
message: "Database connection pool exhausted",
|
||||
logger: "db",
|
||||
},
|
||||
{
|
||||
timestamp: "2026-03-17T10:00:07Z",
|
||||
level: "alert",
|
||||
message: "Memory usage exceeds 95% threshold",
|
||||
logger: "system",
|
||||
},
|
||||
{
|
||||
timestamp: "2026-03-17T10:00:08Z",
|
||||
level: "emergency",
|
||||
message: "System unresponsive - initiating graceful shutdown",
|
||||
logger: "system",
|
||||
},
|
||||
];
|
||||
|
||||
export const MixedLevels: Story = {
|
||||
args: {
|
||||
controls: makeControls(),
|
||||
entries: allLevelEntries,
|
||||
},
|
||||
};
|
||||
|
|
@ -0,0 +1,106 @@
|
|||
import {
|
||||
Button,
|
||||
Card,
|
||||
Checkbox,
|
||||
Container,
|
||||
Grid,
|
||||
Group,
|
||||
ScrollArea,
|
||||
Stack,
|
||||
Text,
|
||||
Title,
|
||||
} from "@mantine/core";
|
||||
import { LogControls } from "../../groups/LogControls/LogControls";
|
||||
import { LogEntry } from "../../elements/LogEntry/LogEntry";
|
||||
import type { LogControlsProps } from "../../groups/LogControls/LogControls";
|
||||
import type { LogEntryProps } from "../../elements/LogEntry/LogEntry";
|
||||
|
||||
export interface LoggingScreenProps {
|
||||
controls: LogControlsProps;
|
||||
entries: LogEntryProps[];
|
||||
autoScroll: boolean;
|
||||
onToggleAutoScroll: () => void;
|
||||
onCopyAll: () => void;
|
||||
}
|
||||
|
||||
const PageContainer = Container.withProps({
|
||||
size: "xl",
|
||||
py: "xl",
|
||||
});
|
||||
|
||||
const SidebarCard = Card.withProps({
|
||||
withBorder: true,
|
||||
padding: "lg",
|
||||
});
|
||||
|
||||
const FullHeightCard = Card.withProps({
|
||||
withBorder: true,
|
||||
padding: "lg",
|
||||
h: "100%",
|
||||
});
|
||||
|
||||
const FullHeightStack = Stack.withProps({
|
||||
gap: "sm",
|
||||
h: "100%",
|
||||
});
|
||||
|
||||
const ToolbarButton = Button.withProps({
|
||||
variant: "light",
|
||||
size: "sm",
|
||||
});
|
||||
|
||||
const EmptyCenter = Stack.withProps({
|
||||
flex: 1,
|
||||
align: "center",
|
||||
justify: "center",
|
||||
});
|
||||
|
||||
export function LoggingScreen({
|
||||
controls,
|
||||
entries,
|
||||
autoScroll,
|
||||
onToggleAutoScroll,
|
||||
onCopyAll,
|
||||
}: LoggingScreenProps) {
|
||||
return (
|
||||
<PageContainer>
|
||||
<Grid align="stretch">
|
||||
<Grid.Col span={3}>
|
||||
<SidebarCard>
|
||||
<LogControls {...controls} />
|
||||
</SidebarCard>
|
||||
</Grid.Col>
|
||||
<Grid.Col span={9}>
|
||||
<FullHeightCard>
|
||||
<FullHeightStack>
|
||||
<Group justify="space-between">
|
||||
<Title order={4}>Log Stream</Title>
|
||||
<Group>
|
||||
<Checkbox
|
||||
label="Auto-scroll"
|
||||
checked={autoScroll}
|
||||
onChange={onToggleAutoScroll}
|
||||
/>
|
||||
<ToolbarButton onClick={onCopyAll}>Copy All</ToolbarButton>
|
||||
</Group>
|
||||
</Group>
|
||||
{entries.length > 0 ? (
|
||||
<ScrollArea flex={1}>
|
||||
<Stack gap="xs">
|
||||
{entries.map((entry, index) => (
|
||||
<LogEntry key={index} {...entry} />
|
||||
))}
|
||||
</Stack>
|
||||
</ScrollArea>
|
||||
) : (
|
||||
<EmptyCenter>
|
||||
<Text c="dimmed">No log entries</Text>
|
||||
</EmptyCenter>
|
||||
)}
|
||||
</FullHeightStack>
|
||||
</FullHeightCard>
|
||||
</Grid.Col>
|
||||
</Grid>
|
||||
</PageContainer>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,84 @@
|
|||
import type { Meta, StoryObj } from "@storybook/react-vite";
|
||||
import { fn } from "storybook/test";
|
||||
import { PromptsScreen } from "./PromptsScreen";
|
||||
|
||||
const meta: Meta<typeof PromptsScreen> = {
|
||||
title: "Screens/PromptsScreen",
|
||||
component: PromptsScreen,
|
||||
parameters: { layout: "fullscreen" },
|
||||
args: {
|
||||
onRefreshList: fn(),
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof PromptsScreen>;
|
||||
|
||||
export const NoSelection: Story = {
|
||||
args: {
|
||||
promptForm: {
|
||||
prompts: [
|
||||
{ name: "summarize", description: "Summarize a document" },
|
||||
{
|
||||
name: "translate",
|
||||
description: "Translate text to another language",
|
||||
},
|
||||
{ name: "code-review", description: "Review code for issues" },
|
||||
],
|
||||
selectedPrompt: undefined,
|
||||
arguments: [],
|
||||
argumentValues: {},
|
||||
onSelectPrompt: fn(),
|
||||
onArgumentChange: fn(),
|
||||
onGetPrompt: fn(),
|
||||
},
|
||||
messages: undefined,
|
||||
listChanged: false,
|
||||
},
|
||||
};
|
||||
|
||||
export const WithResult: Story = {
|
||||
args: {
|
||||
promptForm: {
|
||||
prompts: [
|
||||
{ name: "summarize", description: "Summarize a document" },
|
||||
{
|
||||
name: "translate",
|
||||
description: "Translate text to another language",
|
||||
},
|
||||
{ name: "code-review", description: "Review code for issues" },
|
||||
],
|
||||
selectedPrompt: "translate",
|
||||
arguments: [
|
||||
{ name: "text", required: true, description: "The text to translate" },
|
||||
{
|
||||
name: "targetLanguage",
|
||||
required: true,
|
||||
description: "Target language code",
|
||||
},
|
||||
],
|
||||
argumentValues: {
|
||||
text: "Hello, how are you?",
|
||||
targetLanguage: "es",
|
||||
},
|
||||
onSelectPrompt: fn(),
|
||||
onArgumentChange: fn(),
|
||||
onGetPrompt: fn(),
|
||||
},
|
||||
messages: {
|
||||
onCopyAll: fn(),
|
||||
messages: [
|
||||
{
|
||||
role: "user",
|
||||
content:
|
||||
'Translate the following text to Spanish: "Hello, how are you?"',
|
||||
},
|
||||
{
|
||||
role: "assistant",
|
||||
content: "Hola, como estas?",
|
||||
},
|
||||
],
|
||||
},
|
||||
listChanged: false,
|
||||
},
|
||||
};
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
import { Card, Container, Grid, Stack, Text } from "@mantine/core";
|
||||
import { PromptArgumentsForm } from "../../groups/PromptArgumentsForm/PromptArgumentsForm";
|
||||
import { PromptMessagesDisplay } from "../../groups/PromptMessagesDisplay/PromptMessagesDisplay";
|
||||
import { ListChangedIndicator } from "../../elements/ListChangedIndicator/ListChangedIndicator";
|
||||
import type { PromptArgumentsFormProps } from "../../groups/PromptArgumentsForm/PromptArgumentsForm";
|
||||
import type { PromptMessagesDisplayProps } from "../../groups/PromptMessagesDisplay/PromptMessagesDisplay";
|
||||
|
||||
export interface PromptsScreenProps {
|
||||
promptForm: PromptArgumentsFormProps;
|
||||
messages?: PromptMessagesDisplayProps;
|
||||
listChanged: boolean;
|
||||
onRefreshList: () => void;
|
||||
}
|
||||
|
||||
const PageContainer = Container.withProps({
|
||||
size: "xl",
|
||||
py: "xl",
|
||||
});
|
||||
|
||||
const FullHeightCard = Card.withProps({
|
||||
withBorder: true,
|
||||
padding: "lg",
|
||||
h: "100%",
|
||||
});
|
||||
|
||||
const EmptyState = Text.withProps({
|
||||
c: "dimmed",
|
||||
ta: "center",
|
||||
py: "xl",
|
||||
});
|
||||
|
||||
export function PromptsScreen({
|
||||
promptForm,
|
||||
messages,
|
||||
listChanged,
|
||||
onRefreshList,
|
||||
}: PromptsScreenProps) {
|
||||
return (
|
||||
<PageContainer>
|
||||
<Grid align="stretch">
|
||||
<Grid.Col span={5}>
|
||||
<FullHeightCard>
|
||||
<Stack gap="md">
|
||||
<ListChangedIndicator
|
||||
visible={listChanged}
|
||||
onRefresh={onRefreshList}
|
||||
/>
|
||||
<PromptArgumentsForm {...promptForm} />
|
||||
</Stack>
|
||||
</FullHeightCard>
|
||||
</Grid.Col>
|
||||
<Grid.Col span={7}>
|
||||
<FullHeightCard>
|
||||
{messages ? (
|
||||
<PromptMessagesDisplay {...messages} />
|
||||
) : (
|
||||
<EmptyState>
|
||||
Select a prompt and click Get Prompt to see messages
|
||||
</EmptyState>
|
||||
)}
|
||||
</FullHeightCard>
|
||||
</Grid.Col>
|
||||
</Grid>
|
||||
</PageContainer>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,112 @@
|
|||
import type { Meta, StoryObj } from "@storybook/react-vite";
|
||||
import { fn } from "storybook/test";
|
||||
import { ResourcesScreen } from "./ResourcesScreen";
|
||||
import type { ResourceListItemProps } from "../../groups/ResourceListItem/ResourceListItem";
|
||||
import type { ResourceTemplateInputProps } from "../../groups/ResourceTemplateInput/ResourceTemplateInput";
|
||||
import type { ResourcePreviewPanelProps } from "../../groups/ResourcePreviewPanel/ResourcePreviewPanel";
|
||||
|
||||
const meta: Meta<typeof ResourcesScreen> = {
|
||||
title: "Screens/ResourcesScreen",
|
||||
component: ResourcesScreen,
|
||||
parameters: { layout: "fullscreen" },
|
||||
args: {
|
||||
searchText: "",
|
||||
onSearchChange: fn(),
|
||||
onRefreshList: fn(),
|
||||
onSelectResource: fn(),
|
||||
listChanged: false,
|
||||
subscriptions: [],
|
||||
templates: [],
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof ResourcesScreen>;
|
||||
|
||||
const sampleResources: ResourceListItemProps[] = [
|
||||
{
|
||||
name: "config.json",
|
||||
uri: "file:///config.json",
|
||||
annotations: { audience: "developer", priority: 0.8 },
|
||||
selected: false,
|
||||
onClick: fn(),
|
||||
},
|
||||
{
|
||||
name: "README.md",
|
||||
uri: "file:///README.md",
|
||||
selected: false,
|
||||
onClick: fn(),
|
||||
},
|
||||
{
|
||||
name: "schema.sql",
|
||||
uri: "file:///schema.sql",
|
||||
annotations: { priority: 0.5 },
|
||||
selected: false,
|
||||
onClick: fn(),
|
||||
},
|
||||
];
|
||||
|
||||
const sampleTemplates: ResourceTemplateInputProps[] = [
|
||||
{
|
||||
template: "file:///users/{userId}/profile",
|
||||
variables: { userId: "" },
|
||||
onVariableChange: fn(),
|
||||
onSubmit: fn(),
|
||||
},
|
||||
{
|
||||
template: "db://tables/{tableName}/rows/{rowId}",
|
||||
variables: { tableName: "", rowId: "" },
|
||||
onVariableChange: fn(),
|
||||
onSubmit: fn(),
|
||||
},
|
||||
];
|
||||
|
||||
const selectedResourceProps: ResourcePreviewPanelProps = {
|
||||
uri: "file:///config.json",
|
||||
mimeType: "application/json",
|
||||
annotations: { audience: "developer", priority: 0.8 },
|
||||
content: JSON.stringify(
|
||||
{
|
||||
name: "my-project",
|
||||
version: "1.0.0",
|
||||
settings: { debug: true, logLevel: "info" },
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
lastUpdated: "2026-03-17T10:30:00Z",
|
||||
isSubscribed: true,
|
||||
onSubscribe: fn(),
|
||||
onUnsubscribe: fn(),
|
||||
};
|
||||
|
||||
export const WithResources: Story = {
|
||||
args: {
|
||||
resources: sampleResources,
|
||||
},
|
||||
};
|
||||
|
||||
export const ResourceSelected: Story = {
|
||||
args: {
|
||||
resources: sampleResources.map((r) =>
|
||||
r.uri === "file:///config.json" ? { ...r, selected: true } : r,
|
||||
),
|
||||
selectedResource: selectedResourceProps,
|
||||
subscriptions: [
|
||||
{ name: "config.json", lastUpdated: "2026-03-17T10:30:00Z" },
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export const WithTemplates: Story = {
|
||||
args: {
|
||||
resources: sampleResources,
|
||||
templates: sampleTemplates,
|
||||
},
|
||||
};
|
||||
|
||||
export const Empty: Story = {
|
||||
args: {
|
||||
resources: [],
|
||||
},
|
||||
};
|
||||
|
|
@ -0,0 +1,174 @@
|
|||
import {
|
||||
Accordion,
|
||||
Card,
|
||||
Container,
|
||||
Grid,
|
||||
Stack,
|
||||
Text,
|
||||
TextInput,
|
||||
Title,
|
||||
} from "@mantine/core";
|
||||
import { ListChangedIndicator } from "../../elements/ListChangedIndicator/ListChangedIndicator";
|
||||
import { ResourceListItem } from "../../groups/ResourceListItem/ResourceListItem";
|
||||
import { ResourcePreviewPanel } from "../../groups/ResourcePreviewPanel/ResourcePreviewPanel";
|
||||
import { ResourceTemplateInput } from "../../groups/ResourceTemplateInput/ResourceTemplateInput";
|
||||
import type { ResourceListItemProps } from "../../groups/ResourceListItem/ResourceListItem";
|
||||
import type { ResourcePreviewPanelProps } from "../../groups/ResourcePreviewPanel/ResourcePreviewPanel";
|
||||
import type { ResourceTemplateInputProps } from "../../groups/ResourceTemplateInput/ResourceTemplateInput";
|
||||
|
||||
export interface SubscriptionInfo {
|
||||
name: string;
|
||||
lastUpdated?: string;
|
||||
}
|
||||
|
||||
export interface ResourcesScreenProps {
|
||||
resources: ResourceListItemProps[];
|
||||
templates: ResourceTemplateInputProps[];
|
||||
subscriptions: SubscriptionInfo[];
|
||||
selectedResource?: ResourcePreviewPanelProps;
|
||||
listChanged: boolean;
|
||||
searchText: string;
|
||||
onSearchChange: (text: string) => void;
|
||||
onRefreshList: () => void;
|
||||
onSelectResource: (uri: string) => void;
|
||||
}
|
||||
|
||||
const PageContainer = Container.withProps({
|
||||
size: "xl",
|
||||
py: "xl",
|
||||
});
|
||||
|
||||
const FullHeightCard = Card.withProps({
|
||||
withBorder: true,
|
||||
padding: "lg",
|
||||
h: "100%",
|
||||
});
|
||||
|
||||
const DetailCard = Card.withProps({
|
||||
withBorder: true,
|
||||
padding: "lg",
|
||||
});
|
||||
|
||||
const EmptyState = Text.withProps({
|
||||
c: "dimmed",
|
||||
ta: "center",
|
||||
py: "xl",
|
||||
});
|
||||
|
||||
const SubscriptionMeta = Text.withProps({
|
||||
span: true,
|
||||
c: "dimmed",
|
||||
size: "xs",
|
||||
});
|
||||
|
||||
function formatSectionCount(label: string, count: number): string {
|
||||
return `${label} (${count})`;
|
||||
}
|
||||
|
||||
function formatLastUpdated(lastUpdated: string): string {
|
||||
return ` — ${lastUpdated}`;
|
||||
}
|
||||
|
||||
export function ResourcesScreen({
|
||||
resources,
|
||||
templates,
|
||||
subscriptions,
|
||||
selectedResource,
|
||||
listChanged,
|
||||
searchText,
|
||||
onSearchChange,
|
||||
onRefreshList,
|
||||
}: ResourcesScreenProps) {
|
||||
const filteredResources = resources.filter((r) =>
|
||||
r.name.toLowerCase().includes(searchText.toLowerCase()),
|
||||
);
|
||||
|
||||
const openSections = [
|
||||
...(filteredResources.length > 0 ? ["resources"] : []),
|
||||
...(templates.length > 0 ? ["templates"] : []),
|
||||
...(subscriptions.length > 0 ? ["subscriptions"] : []),
|
||||
];
|
||||
|
||||
return (
|
||||
<PageContainer>
|
||||
<Grid align="stretch">
|
||||
<Grid.Col span={4}>
|
||||
<FullHeightCard>
|
||||
<Stack gap="sm">
|
||||
<Title order={4}>Resources</Title>
|
||||
<ListChangedIndicator
|
||||
visible={listChanged}
|
||||
onRefresh={onRefreshList}
|
||||
/>
|
||||
<TextInput
|
||||
placeholder="Search..."
|
||||
value={searchText}
|
||||
onChange={(e) => onSearchChange(e.currentTarget.value)}
|
||||
/>
|
||||
<Accordion multiple defaultValue={openSections}>
|
||||
<Accordion.Item value="resources">
|
||||
<Accordion.Control disabled={filteredResources.length === 0}>
|
||||
{formatSectionCount("URIs", filteredResources.length)}
|
||||
</Accordion.Control>
|
||||
<Accordion.Panel>
|
||||
<Stack gap="xs">
|
||||
{filteredResources.map((resource) => (
|
||||
<ResourceListItem key={resource.uri} {...resource} />
|
||||
))}
|
||||
</Stack>
|
||||
</Accordion.Panel>
|
||||
</Accordion.Item>
|
||||
|
||||
<Accordion.Item value="templates">
|
||||
<Accordion.Control disabled={templates.length === 0}>
|
||||
{formatSectionCount("Templates", templates.length)}
|
||||
</Accordion.Control>
|
||||
<Accordion.Panel>
|
||||
<Stack gap="xs">
|
||||
{templates.map((template) => (
|
||||
<ResourceTemplateInput
|
||||
key={template.template}
|
||||
{...template}
|
||||
/>
|
||||
))}
|
||||
</Stack>
|
||||
</Accordion.Panel>
|
||||
</Accordion.Item>
|
||||
|
||||
<Accordion.Item value="subscriptions">
|
||||
<Accordion.Control disabled={subscriptions.length === 0}>
|
||||
{formatSectionCount("Subscriptions", subscriptions.length)}
|
||||
</Accordion.Control>
|
||||
<Accordion.Panel>
|
||||
<Stack gap="xs">
|
||||
{subscriptions.map((sub) => (
|
||||
<Text key={sub.name} size="sm">
|
||||
{sub.name}
|
||||
{sub.lastUpdated && (
|
||||
<SubscriptionMeta>
|
||||
{formatLastUpdated(sub.lastUpdated)}
|
||||
</SubscriptionMeta>
|
||||
)}
|
||||
</Text>
|
||||
))}
|
||||
</Stack>
|
||||
</Accordion.Panel>
|
||||
</Accordion.Item>
|
||||
</Accordion>
|
||||
</Stack>
|
||||
</FullHeightCard>
|
||||
</Grid.Col>
|
||||
|
||||
<Grid.Col span={8}>
|
||||
<DetailCard>
|
||||
{selectedResource ? (
|
||||
<ResourcePreviewPanel {...selectedResource} />
|
||||
) : (
|
||||
<EmptyState>Select a resource to preview</EmptyState>
|
||||
)}
|
||||
</DetailCard>
|
||||
</Grid.Col>
|
||||
</Grid>
|
||||
</PageContainer>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,122 @@
|
|||
import type { Meta, StoryObj } from "@storybook/react-vite";
|
||||
import { fn } from "storybook/test";
|
||||
import { ServerListScreen } from "./ServerListScreen";
|
||||
import type { ServerCardProps } from "../../groups/ServerCard/ServerCard";
|
||||
|
||||
const meta: Meta<typeof ServerListScreen> = {
|
||||
title: "Screens/ServerListScreen",
|
||||
component: ServerListScreen,
|
||||
parameters: { layout: "fullscreen" },
|
||||
args: {
|
||||
onAddManually: fn(),
|
||||
onImportConfig: fn(),
|
||||
onImportServerJson: fn(),
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof ServerListScreen>;
|
||||
|
||||
function makeServerCallbacks(): Pick<
|
||||
ServerCardProps,
|
||||
| "onToggleConnection"
|
||||
| "onServerInfo"
|
||||
| "onSettings"
|
||||
| "onEdit"
|
||||
| "onClone"
|
||||
| "onRemove"
|
||||
| "onTestSampling"
|
||||
| "onTestElicitationForm"
|
||||
| "onTestElicitationUrl"
|
||||
| "onConfigureRoots"
|
||||
> {
|
||||
return {
|
||||
onToggleConnection: fn(),
|
||||
onServerInfo: fn(),
|
||||
onSettings: fn(),
|
||||
onEdit: fn(),
|
||||
onClone: fn(),
|
||||
onRemove: fn(),
|
||||
onTestSampling: fn(),
|
||||
onTestElicitationForm: fn(),
|
||||
onTestElicitationUrl: fn(),
|
||||
onConfigureRoots: fn(),
|
||||
};
|
||||
}
|
||||
|
||||
const connectedStdioServer: ServerCardProps = {
|
||||
name: "Local Dev Server",
|
||||
version: "1.2.0",
|
||||
transport: "stdio",
|
||||
connectionMode: "Direct",
|
||||
command: "npx @modelcontextprotocol/server-filesystem /home/user/projects",
|
||||
status: "connected",
|
||||
canTestClientFeatures: true,
|
||||
...makeServerCallbacks(),
|
||||
};
|
||||
|
||||
const disconnectedStdioServer: ServerCardProps = {
|
||||
name: "Database Tools",
|
||||
version: "0.9.1",
|
||||
transport: "stdio",
|
||||
connectionMode: "Direct",
|
||||
command: "python -m mcp_server_sqlite --db-path ./data.db",
|
||||
status: "disconnected",
|
||||
canTestClientFeatures: false,
|
||||
...makeServerCallbacks(),
|
||||
};
|
||||
|
||||
const failedHttpServer: ServerCardProps = {
|
||||
name: "Remote API Server",
|
||||
version: "2.0.0",
|
||||
transport: "http",
|
||||
connectionMode: "Streamable HTTP",
|
||||
command: "https://api.example.com/mcp",
|
||||
status: "failed",
|
||||
retryCount: 3,
|
||||
error: {
|
||||
message: "Connection refused",
|
||||
details: "ECONNREFUSED 127.0.0.1:8080 - The server may not be running.",
|
||||
},
|
||||
canTestClientFeatures: false,
|
||||
...makeServerCallbacks(),
|
||||
};
|
||||
|
||||
const connectingHttpServer: ServerCardProps = {
|
||||
name: "Staging Server",
|
||||
transport: "http",
|
||||
connectionMode: "Streamable HTTP",
|
||||
command: "https://staging.example.com/mcp",
|
||||
status: "connecting",
|
||||
canTestClientFeatures: false,
|
||||
...makeServerCallbacks(),
|
||||
};
|
||||
|
||||
export const MultipleServers: Story = {
|
||||
args: {
|
||||
servers: [connectedStdioServer, disconnectedStdioServer, failedHttpServer],
|
||||
},
|
||||
};
|
||||
|
||||
export const SingleServer: Story = {
|
||||
args: {
|
||||
servers: [connectedStdioServer],
|
||||
},
|
||||
};
|
||||
|
||||
export const Empty: Story = {
|
||||
args: {
|
||||
servers: [],
|
||||
},
|
||||
};
|
||||
|
||||
export const MixedStates: Story = {
|
||||
args: {
|
||||
servers: [
|
||||
connectedStdioServer,
|
||||
disconnectedStdioServer,
|
||||
failedHttpServer,
|
||||
connectingHttpServer,
|
||||
],
|
||||
},
|
||||
};
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
import { Container, Group, SimpleGrid, Stack, Text } from "@mantine/core";
|
||||
import { ServerCard } from "../../groups/ServerCard/ServerCard";
|
||||
import { AddServerMenu } from "../../groups/AddServerMenu/AddServerMenu";
|
||||
import type { ServerCardProps } from "../../groups/ServerCard/ServerCard";
|
||||
|
||||
export interface ServerListScreenProps {
|
||||
servers: ServerCardProps[];
|
||||
onAddManually: () => void;
|
||||
onImportConfig: () => void;
|
||||
onImportServerJson: () => void;
|
||||
}
|
||||
|
||||
const PageContainer = Container.withProps({
|
||||
size: "xl",
|
||||
py: "xl",
|
||||
});
|
||||
|
||||
const EmptyState = Text.withProps({
|
||||
c: "dimmed",
|
||||
ta: "center",
|
||||
py: "xl",
|
||||
});
|
||||
|
||||
export function ServerListScreen({
|
||||
servers,
|
||||
onAddManually,
|
||||
onImportConfig,
|
||||
onImportServerJson,
|
||||
}: ServerListScreenProps) {
|
||||
return (
|
||||
<PageContainer>
|
||||
<Stack gap="lg">
|
||||
<Group justify="flex-end">
|
||||
<AddServerMenu
|
||||
onAddManually={onAddManually}
|
||||
onImportConfig={onImportConfig}
|
||||
onImportServerJson={onImportServerJson}
|
||||
/>
|
||||
</Group>
|
||||
|
||||
{servers.length === 0 ? (
|
||||
<EmptyState>
|
||||
No servers configured. Add a server to get started.
|
||||
</EmptyState>
|
||||
) : (
|
||||
<SimpleGrid
|
||||
cols={{ base: 1, md: 2 }}
|
||||
spacing="lg"
|
||||
className="grid-align-start"
|
||||
>
|
||||
{servers.map((server) => (
|
||||
<ServerCard key={server.name} {...server} />
|
||||
))}
|
||||
</SimpleGrid>
|
||||
)}
|
||||
</Stack>
|
||||
</PageContainer>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,100 @@
|
|||
import type { Meta, StoryObj } from "@storybook/react-vite";
|
||||
import { fn } from "storybook/test";
|
||||
import { TasksScreen } from "./TasksScreen";
|
||||
import type { TaskCardProps } from "../../groups/TaskCard/TaskCard";
|
||||
|
||||
const meta: Meta<typeof TasksScreen> = {
|
||||
title: "Screens/TasksScreen",
|
||||
component: TasksScreen,
|
||||
parameters: { layout: "fullscreen" },
|
||||
args: {
|
||||
onRefresh: fn(),
|
||||
onClearHistory: fn(),
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof TasksScreen>;
|
||||
|
||||
const activeTask1: TaskCardProps = {
|
||||
taskId: "task-001",
|
||||
status: "running",
|
||||
method: "tools/call",
|
||||
target: "send_message",
|
||||
progress: 45,
|
||||
progressDescription: "Sending message to remote server...",
|
||||
startedAt: "2026-03-17T10:00:00Z",
|
||||
onViewDetails: fn(),
|
||||
onViewResult: fn(),
|
||||
onCancel: fn(),
|
||||
onDismiss: fn(),
|
||||
};
|
||||
|
||||
const activeTask2: TaskCardProps = {
|
||||
taskId: "task-002",
|
||||
status: "waiting",
|
||||
method: "resources/read",
|
||||
target: "config.json",
|
||||
startedAt: "2026-03-17T10:01:00Z",
|
||||
onViewDetails: fn(),
|
||||
onViewResult: fn(),
|
||||
onCancel: fn(),
|
||||
onDismiss: fn(),
|
||||
};
|
||||
|
||||
const completedTask1: TaskCardProps = {
|
||||
taskId: "task-003",
|
||||
status: "completed",
|
||||
method: "tools/call",
|
||||
target: "list_users",
|
||||
startedAt: "2026-03-17T09:50:00Z",
|
||||
completedAt: "2026-03-17T09:50:02Z",
|
||||
elapsed: "2.1s",
|
||||
onViewDetails: fn(),
|
||||
onViewResult: fn(),
|
||||
onCancel: fn(),
|
||||
onDismiss: fn(),
|
||||
};
|
||||
|
||||
const completedTask2: TaskCardProps = {
|
||||
taskId: "task-004",
|
||||
status: "failed",
|
||||
method: "tools/call",
|
||||
target: "delete_records",
|
||||
startedAt: "2026-03-17T09:45:00Z",
|
||||
completedAt: "2026-03-17T09:45:01Z",
|
||||
elapsed: "0.8s",
|
||||
error: "Permission denied: insufficient privileges",
|
||||
onViewDetails: fn(),
|
||||
onViewResult: fn(),
|
||||
onCancel: fn(),
|
||||
onDismiss: fn(),
|
||||
};
|
||||
|
||||
export const Mixed: Story = {
|
||||
args: {
|
||||
activeTasks: [activeTask1, activeTask2],
|
||||
completedTasks: [completedTask1, completedTask2],
|
||||
},
|
||||
};
|
||||
|
||||
export const ActiveOnly: Story = {
|
||||
args: {
|
||||
activeTasks: [activeTask1, activeTask2],
|
||||
completedTasks: [],
|
||||
},
|
||||
};
|
||||
|
||||
export const CompletedOnly: Story = {
|
||||
args: {
|
||||
activeTasks: [],
|
||||
completedTasks: [completedTask1, completedTask2],
|
||||
},
|
||||
};
|
||||
|
||||
export const Empty: Story = {
|
||||
args: {
|
||||
activeTasks: [],
|
||||
completedTasks: [],
|
||||
},
|
||||
};
|
||||
|
|
@ -0,0 +1,88 @@
|
|||
import {
|
||||
Button,
|
||||
Container,
|
||||
Group,
|
||||
Paper,
|
||||
Stack,
|
||||
Text,
|
||||
Title,
|
||||
} from "@mantine/core";
|
||||
import { TaskCard } from "../../groups/TaskCard/TaskCard";
|
||||
import type { TaskCardProps } from "../../groups/TaskCard/TaskCard";
|
||||
|
||||
export interface TasksScreenProps {
|
||||
activeTasks: TaskCardProps[];
|
||||
completedTasks: TaskCardProps[];
|
||||
onRefresh: () => void;
|
||||
onClearHistory: () => void;
|
||||
}
|
||||
|
||||
const PageContainer = Container.withProps({
|
||||
size: "xl",
|
||||
py: "xl",
|
||||
});
|
||||
|
||||
const ContentPanel = Paper.withProps({
|
||||
withBorder: true,
|
||||
p: "md",
|
||||
});
|
||||
|
||||
const CompactButton = Button.withProps({
|
||||
variant: "light",
|
||||
size: "xs",
|
||||
});
|
||||
|
||||
function formatSectionTitle(label: string, count: number): string {
|
||||
return `${label} (${count})`;
|
||||
}
|
||||
|
||||
export function TasksScreen({
|
||||
activeTasks,
|
||||
completedTasks,
|
||||
onRefresh,
|
||||
onClearHistory,
|
||||
}: TasksScreenProps) {
|
||||
return (
|
||||
<PageContainer>
|
||||
<ContentPanel>
|
||||
<Stack gap="lg">
|
||||
<Group justify="space-between">
|
||||
<Title order={4}>
|
||||
{formatSectionTitle("Active Tasks", activeTasks.length)}
|
||||
</Title>
|
||||
<CompactButton onClick={onRefresh}>Refresh Tasks</CompactButton>
|
||||
</Group>
|
||||
{activeTasks.length === 0 ? (
|
||||
<Text c="dimmed">No active tasks</Text>
|
||||
) : (
|
||||
<Stack gap="md">
|
||||
{activeTasks.map((task) => (
|
||||
<TaskCard key={task.taskId} {...task} />
|
||||
))}
|
||||
</Stack>
|
||||
)}
|
||||
|
||||
<Group justify="space-between">
|
||||
<Title order={4}>
|
||||
{formatSectionTitle("Completed Tasks", completedTasks.length)}
|
||||
</Title>
|
||||
{completedTasks.length > 0 && (
|
||||
<CompactButton onClick={onClearHistory}>
|
||||
Clear History
|
||||
</CompactButton>
|
||||
)}
|
||||
</Group>
|
||||
{completedTasks.length === 0 ? (
|
||||
<Text c="dimmed">No completed tasks</Text>
|
||||
) : (
|
||||
<Stack gap="md">
|
||||
{completedTasks.map((task) => (
|
||||
<TaskCard key={task.taskId} {...task} />
|
||||
))}
|
||||
</Stack>
|
||||
)}
|
||||
</Stack>
|
||||
</ContentPanel>
|
||||
</PageContainer>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,169 @@
|
|||
import type { Meta, StoryObj } from "@storybook/react-vite";
|
||||
import { fn } from "storybook/test";
|
||||
import { ToolsScreen } from "./ToolsScreen";
|
||||
import type { ToolListItemProps } from "../../groups/ToolListItem/ToolListItem";
|
||||
import type { ToolDetailPanelProps } from "../../groups/ToolDetailPanel/ToolDetailPanel";
|
||||
import type { ResultPanelProps } from "../../groups/ResultPanel/ResultPanel";
|
||||
|
||||
const meta: Meta<typeof ToolsScreen> = {
|
||||
title: "Screens/ToolsScreen",
|
||||
component: ToolsScreen,
|
||||
parameters: { layout: "fullscreen" },
|
||||
args: {
|
||||
searchText: "",
|
||||
listChanged: false,
|
||||
onSearchChange: fn(),
|
||||
onRefreshList: fn(),
|
||||
onSelectTool: fn(),
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof ToolsScreen>;
|
||||
|
||||
const sampleTools: ToolListItemProps[] = [
|
||||
{
|
||||
name: "send_message",
|
||||
title: "Send Message",
|
||||
selected: false,
|
||||
onClick: fn(),
|
||||
},
|
||||
{
|
||||
name: "create_record",
|
||||
title: "Create Record",
|
||||
selected: false,
|
||||
onClick: fn(),
|
||||
},
|
||||
{
|
||||
name: "delete_records",
|
||||
selected: false,
|
||||
onClick: fn(),
|
||||
},
|
||||
{ name: "list_users", selected: false, onClick: fn() },
|
||||
{
|
||||
name: "batch_process",
|
||||
selected: false,
|
||||
onClick: fn(),
|
||||
},
|
||||
];
|
||||
|
||||
const selectedToolData: ToolDetailPanelProps = {
|
||||
name: "create_record",
|
||||
title: "Create Record",
|
||||
description: "Creates a new record with the given parameters",
|
||||
schema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
title: { type: "string", description: "Record title" },
|
||||
count: { type: "number", description: "Number of items" },
|
||||
enabled: { type: "boolean", description: "Whether the record is active" },
|
||||
},
|
||||
required: ["title"],
|
||||
},
|
||||
formValues: {},
|
||||
isExecuting: false,
|
||||
onFormChange: fn(),
|
||||
onExecute: fn(),
|
||||
onCancel: fn(),
|
||||
};
|
||||
|
||||
const resultData: ResultPanelProps = {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: JSON.stringify(
|
||||
{
|
||||
id: 42,
|
||||
title: "New Record",
|
||||
count: 5,
|
||||
enabled: true,
|
||||
createdAt: "2026-03-17T12:00:00Z",
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
},
|
||||
],
|
||||
onClear: fn(),
|
||||
};
|
||||
|
||||
function toolsWithSelected(selectedName: string): ToolListItemProps[] {
|
||||
return sampleTools.map((tool) => ({
|
||||
...tool,
|
||||
selected: tool.name === selectedName,
|
||||
}));
|
||||
}
|
||||
|
||||
export const NoSelection: Story = {
|
||||
args: {
|
||||
tools: sampleTools,
|
||||
},
|
||||
};
|
||||
|
||||
export const ToolSelected: Story = {
|
||||
args: {
|
||||
tools: toolsWithSelected("create_record"),
|
||||
selectedTool: selectedToolData,
|
||||
},
|
||||
};
|
||||
|
||||
export const WithResult: Story = {
|
||||
args: {
|
||||
tools: toolsWithSelected("create_record"),
|
||||
selectedTool: selectedToolData,
|
||||
result: resultData,
|
||||
},
|
||||
};
|
||||
|
||||
export const LongToolName: Story = {
|
||||
args: {
|
||||
tools: [
|
||||
...sampleTools,
|
||||
{
|
||||
name: "organization_internal_database_complex_multi_table_join_query_with_aggregation_and_filtering",
|
||||
selected: true,
|
||||
onClick: fn(),
|
||||
},
|
||||
],
|
||||
selectedTool: {
|
||||
name: "organization_internal_database_complex_multi_table_join_query_with_aggregation_and_filtering",
|
||||
annotations: {
|
||||
readOnly: true,
|
||||
longRunning: true,
|
||||
},
|
||||
description:
|
||||
"Executes a complex multi-table join query across the organization's internal database with support for aggregation functions, nested filtering, and pagination of large result sets.",
|
||||
schema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
primary_table_name: {
|
||||
type: "string",
|
||||
description: "The main table to query from",
|
||||
},
|
||||
join_configuration: {
|
||||
type: "string",
|
||||
description: "JSON configuration for table joins",
|
||||
},
|
||||
aggregation_functions: {
|
||||
type: "string",
|
||||
description:
|
||||
"Comma-separated list of aggregation functions to apply",
|
||||
},
|
||||
},
|
||||
required: ["primary_table_name"],
|
||||
},
|
||||
formValues: {},
|
||||
isExecuting: false,
|
||||
onFormChange: fn(),
|
||||
onExecute: fn(),
|
||||
onCancel: fn(),
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const WithListChanged: Story = {
|
||||
args: {
|
||||
tools: sampleTools,
|
||||
listChanged: true,
|
||||
},
|
||||
};
|
||||
111
clients/web/src/components/screens/ToolsScreen/ToolsScreen.tsx
Normal file
111
clients/web/src/components/screens/ToolsScreen/ToolsScreen.tsx
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
import {
|
||||
Card,
|
||||
Container,
|
||||
Grid,
|
||||
Stack,
|
||||
Text,
|
||||
TextInput,
|
||||
Title,
|
||||
} from "@mantine/core";
|
||||
import { ListChangedIndicator } from "../../elements/ListChangedIndicator/ListChangedIndicator";
|
||||
import { ToolListItem } from "../../groups/ToolListItem/ToolListItem";
|
||||
import { ToolDetailPanel } from "../../groups/ToolDetailPanel/ToolDetailPanel";
|
||||
import { ResultPanel } from "../../groups/ResultPanel/ResultPanel";
|
||||
import type { ToolListItemProps } from "../../groups/ToolListItem/ToolListItem";
|
||||
import type { ToolDetailPanelProps } from "../../groups/ToolDetailPanel/ToolDetailPanel";
|
||||
import type { ResultPanelProps } from "../../groups/ResultPanel/ResultPanel";
|
||||
|
||||
export interface ToolsScreenProps {
|
||||
tools: ToolListItemProps[];
|
||||
selectedTool?: ToolDetailPanelProps;
|
||||
result?: ResultPanelProps;
|
||||
listChanged: boolean;
|
||||
searchText: string;
|
||||
onSearchChange: (text: string) => void;
|
||||
onRefreshList: () => void;
|
||||
onSelectTool: (name: string) => void;
|
||||
}
|
||||
|
||||
const PageContainer = Container.withProps({
|
||||
size: "xl",
|
||||
py: "xl",
|
||||
});
|
||||
|
||||
const FullHeightCard = Card.withProps({
|
||||
withBorder: true,
|
||||
padding: "lg",
|
||||
h: "100%",
|
||||
});
|
||||
|
||||
const EmptyState = Text.withProps({
|
||||
c: "dimmed",
|
||||
ta: "center",
|
||||
py: "xl",
|
||||
});
|
||||
|
||||
export function ToolsScreen({
|
||||
tools,
|
||||
selectedTool,
|
||||
result,
|
||||
listChanged,
|
||||
searchText,
|
||||
onSearchChange,
|
||||
onRefreshList,
|
||||
onSelectTool,
|
||||
}: ToolsScreenProps) {
|
||||
const filteredTools = searchText
|
||||
? tools.filter((tool) =>
|
||||
tool.name.toLowerCase().includes(searchText.toLowerCase()),
|
||||
)
|
||||
: tools;
|
||||
|
||||
return (
|
||||
<PageContainer>
|
||||
<Grid align="stretch">
|
||||
<Grid.Col span={3}>
|
||||
<FullHeightCard>
|
||||
<Stack gap="sm">
|
||||
<Title order={4}>Tools</Title>
|
||||
<ListChangedIndicator
|
||||
visible={listChanged}
|
||||
onRefresh={onRefreshList}
|
||||
/>
|
||||
<TextInput
|
||||
placeholder="Search tools..."
|
||||
value={searchText}
|
||||
onChange={(event) => onSearchChange(event.currentTarget.value)}
|
||||
/>
|
||||
{filteredTools.map((tool) => (
|
||||
<ToolListItem
|
||||
key={tool.name}
|
||||
{...tool}
|
||||
onClick={() => onSelectTool(tool.name)}
|
||||
/>
|
||||
))}
|
||||
</Stack>
|
||||
</FullHeightCard>
|
||||
</Grid.Col>
|
||||
|
||||
<Grid.Col span={5}>
|
||||
<FullHeightCard>
|
||||
{selectedTool ? (
|
||||
<ToolDetailPanel {...selectedTool} />
|
||||
) : (
|
||||
<EmptyState>Select a tool to view details</EmptyState>
|
||||
)}
|
||||
</FullHeightCard>
|
||||
</Grid.Col>
|
||||
|
||||
<Grid.Col span={4}>
|
||||
<FullHeightCard>
|
||||
{result ? (
|
||||
<ResultPanel {...result} />
|
||||
) : (
|
||||
<EmptyState>Results will appear here</EmptyState>
|
||||
)}
|
||||
</FullHeightCard>
|
||||
</Grid.Col>
|
||||
</Grid>
|
||||
</PageContainer>
|
||||
);
|
||||
}
|
||||
Loading…
Reference in a new issue