refine InlineError, ServerCard, HistoryEntry, and LoggingScreen
- InlineError: show more/less toggle on same line as message, collapsible details, doc link as button - ServerCard: use ContentViewer with CopyButton for command, remove onCopyCommand prop, combine action rows, add responsive Card variant - HistoryEntry: use ContentViewer with CopyButton for JSON blocks, swap duration and status badge positions - LoggingScreen: stretch log stream panel to full height, center empty message vertically Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
1da76c53ef
commit
3c25bdfbd5
9 changed files with 136 additions and 105 deletions
|
|
@ -103,3 +103,15 @@
|
||||||
.inspector-spin {
|
.inspector-spin {
|
||||||
animation: inspector-spin 1s linear infinite;
|
animation: inspector-spin 1s linear infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── Responsive card ────────────────────────────────────── */
|
||||||
|
|
||||||
|
.card-responsive {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 62em) {
|
||||||
|
.card-responsive {
|
||||||
|
width: 50%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
import { Alert, Anchor, Group, Spoiler, Stack, Text } from "@mantine/core";
|
import { Alert, Button, Collapse, Group, Stack, Text } from "@mantine/core";
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
export interface InlineErrorProps {
|
export interface InlineErrorProps {
|
||||||
message: string;
|
message: string;
|
||||||
details?: string;
|
details?: string;
|
||||||
retryCount?: number;
|
retryCount?: number;
|
||||||
maxRetries?: number;
|
maxRetries?: number;
|
||||||
onShowMore?: () => void;
|
|
||||||
docLink?: string;
|
docLink?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -16,33 +16,52 @@ export function InlineError({
|
||||||
maxRetries,
|
maxRetries,
|
||||||
docLink,
|
docLink,
|
||||||
}: InlineErrorProps) {
|
}: InlineErrorProps) {
|
||||||
const hasDetails =
|
const [expanded, setExpanded] = useState(false);
|
||||||
retryCount !== undefined || details !== undefined || docLink !== undefined;
|
const hasExpandable = details !== undefined || docLink !== undefined;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Alert color="red" variant="light" title={message}>
|
<Alert color="red" variant="light">
|
||||||
{hasDetails && (
|
<Stack gap="xs">
|
||||||
<Stack gap="xs">
|
<Group justify="space-between" wrap="nowrap">
|
||||||
{retryCount !== undefined && (
|
<Group gap="sm" wrap="nowrap">
|
||||||
<Text size="sm">
|
<Text size="sm" fw={500} c="red">
|
||||||
Retry attempt {retryCount}
|
{message}
|
||||||
{maxRetries !== undefined && ` of ${maxRetries}`}
|
|
||||||
</Text>
|
</Text>
|
||||||
|
{retryCount !== undefined && (
|
||||||
|
<Text size="sm" c="dimmed">
|
||||||
|
Retry attempt {retryCount}
|
||||||
|
{maxRetries !== undefined && ` of ${maxRetries}`}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</Group>
|
||||||
|
{hasExpandable && (
|
||||||
|
<Button
|
||||||
|
variant="transparent"
|
||||||
|
size="compact-xs"
|
||||||
|
c="dark"
|
||||||
|
onClick={() => setExpanded((v) => !v)}
|
||||||
|
>
|
||||||
|
{expanded ? "Show less" : "Show more"}
|
||||||
|
</Button>
|
||||||
)}
|
)}
|
||||||
{details && (
|
</Group>
|
||||||
<Spoiler maxHeight={0} showLabel="Show more" hideLabel="Show less">
|
<Collapse in={expanded}>
|
||||||
<Text size="sm">{details}</Text>
|
<Stack gap="xs">
|
||||||
</Spoiler>
|
{details && <Text size="sm">{details}</Text>}
|
||||||
)}
|
{docLink && (
|
||||||
{docLink && (
|
<Button
|
||||||
<Group>
|
variant="light"
|
||||||
<Anchor href={docLink} target="_blank" size="sm">
|
size="xs"
|
||||||
View Troubleshooting Guide →
|
component="a"
|
||||||
</Anchor>
|
href={docLink}
|
||||||
</Group>
|
target="_blank"
|
||||||
)}
|
>
|
||||||
</Stack>
|
View Troubleshooting Guide
|
||||||
)}
|
</Button>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
</Collapse>
|
||||||
|
</Stack>
|
||||||
</Alert>
|
</Alert>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,6 @@ type Story = StoryObj<typeof HomeLayout>;
|
||||||
function makeServerCallbacks(): Pick<
|
function makeServerCallbacks(): Pick<
|
||||||
ServerCardProps,
|
ServerCardProps,
|
||||||
| "onToggleConnection"
|
| "onToggleConnection"
|
||||||
| "onCopyCommand"
|
|
||||||
| "onServerInfo"
|
| "onServerInfo"
|
||||||
| "onSettings"
|
| "onSettings"
|
||||||
| "onEdit"
|
| "onEdit"
|
||||||
|
|
@ -29,7 +28,6 @@ function makeServerCallbacks(): Pick<
|
||||||
> {
|
> {
|
||||||
return {
|
return {
|
||||||
onToggleConnection: fn(),
|
onToggleConnection: fn(),
|
||||||
onCopyCommand: fn(),
|
|
||||||
onServerInfo: fn(),
|
onServerInfo: fn(),
|
||||||
onSettings: fn(),
|
onSettings: fn(),
|
||||||
onEdit: fn(),
|
onEdit: fn(),
|
||||||
|
|
|
||||||
|
|
@ -2,12 +2,12 @@ import {
|
||||||
Badge,
|
Badge,
|
||||||
Button,
|
Button,
|
||||||
Card,
|
Card,
|
||||||
Code,
|
|
||||||
Collapse,
|
Collapse,
|
||||||
Group,
|
Group,
|
||||||
Stack,
|
Stack,
|
||||||
Text,
|
Text,
|
||||||
} from "@mantine/core";
|
} from "@mantine/core";
|
||||||
|
import { ContentViewer } from "../../atoms/ContentViewer/ContentViewer";
|
||||||
|
|
||||||
export interface HistoryChildEntry {
|
export interface HistoryChildEntry {
|
||||||
timestamp: string;
|
timestamp: string;
|
||||||
|
|
@ -66,12 +66,12 @@ export function HistoryEntry({
|
||||||
)}
|
)}
|
||||||
</Group>
|
</Group>
|
||||||
<Group gap="sm">
|
<Group gap="sm">
|
||||||
<Badge color={status === "success" ? "green" : "red"}>
|
|
||||||
{status === "success" ? "OK" : "Error"}
|
|
||||||
</Badge>
|
|
||||||
<Text size="sm" c="dimmed">
|
<Text size="sm" c="dimmed">
|
||||||
{durationMs}ms
|
{durationMs}ms
|
||||||
</Text>
|
</Text>
|
||||||
|
<Badge color={status === "success" ? "green" : "red"}>
|
||||||
|
{status === "success" ? "OK" : "Error"}
|
||||||
|
</Badge>
|
||||||
</Group>
|
</Group>
|
||||||
</Group>
|
</Group>
|
||||||
|
|
||||||
|
|
@ -93,13 +93,21 @@ export function HistoryEntry({
|
||||||
{parameters && (
|
{parameters && (
|
||||||
<Stack gap="xs">
|
<Stack gap="xs">
|
||||||
<Text size="sm">Parameters:</Text>
|
<Text size="sm">Parameters:</Text>
|
||||||
<Code block>{JSON.stringify(parameters, null, 2)}</Code>
|
<ContentViewer
|
||||||
|
type="json"
|
||||||
|
content={JSON.stringify(parameters)}
|
||||||
|
copyable
|
||||||
|
/>
|
||||||
</Stack>
|
</Stack>
|
||||||
)}
|
)}
|
||||||
{response && (
|
{response && (
|
||||||
<Stack gap="xs">
|
<Stack gap="xs">
|
||||||
<Text size="sm">Response:</Text>
|
<Text size="sm">Response:</Text>
|
||||||
<Code block>{JSON.stringify(response, null, 2)}</Code>
|
<ContentViewer
|
||||||
|
type="json"
|
||||||
|
content={JSON.stringify(response)}
|
||||||
|
copyable
|
||||||
|
/>
|
||||||
</Stack>
|
</Stack>
|
||||||
)}
|
)}
|
||||||
{childEntries && childEntries.length > 0 && (
|
{childEntries && childEntries.length > 0 && (
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,6 @@ const meta: Meta<typeof ServerCard> = {
|
||||||
component: ServerCard,
|
component: ServerCard,
|
||||||
args: {
|
args: {
|
||||||
onToggleConnection: fn(),
|
onToggleConnection: fn(),
|
||||||
onCopyCommand: fn(),
|
|
||||||
onServerInfo: fn(),
|
onServerInfo: fn(),
|
||||||
onSettings: fn(),
|
onSettings: fn(),
|
||||||
onEdit: fn(),
|
onEdit: fn(),
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@ import {
|
||||||
Badge,
|
Badge,
|
||||||
Button,
|
Button,
|
||||||
Card,
|
Card,
|
||||||
Code,
|
|
||||||
Group,
|
Group,
|
||||||
Menu,
|
Menu,
|
||||||
Stack,
|
Stack,
|
||||||
|
|
@ -11,7 +10,7 @@ import {
|
||||||
import { StatusIndicator } from "../../atoms/StatusIndicator/StatusIndicator";
|
import { StatusIndicator } from "../../atoms/StatusIndicator/StatusIndicator";
|
||||||
import { TransportBadge } from "../../atoms/TransportBadge/TransportBadge";
|
import { TransportBadge } from "../../atoms/TransportBadge/TransportBadge";
|
||||||
import { ConnectionToggle } from "../../atoms/ConnectionToggle/ConnectionToggle";
|
import { ConnectionToggle } from "../../atoms/ConnectionToggle/ConnectionToggle";
|
||||||
import { CopyButton } from "../../atoms/CopyButton/CopyButton";
|
import { ContentViewer } from "../../atoms/ContentViewer/ContentViewer";
|
||||||
import { InlineError } from "../../atoms/InlineError/InlineError";
|
import { InlineError } from "../../atoms/InlineError/InlineError";
|
||||||
|
|
||||||
export interface ServerCardProps {
|
export interface ServerCardProps {
|
||||||
|
|
@ -25,7 +24,6 @@ export interface ServerCardProps {
|
||||||
error?: { message: string; details?: string };
|
error?: { message: string; details?: string };
|
||||||
canTestClientFeatures: boolean;
|
canTestClientFeatures: boolean;
|
||||||
onToggleConnection: (connect: boolean) => void;
|
onToggleConnection: (connect: boolean) => void;
|
||||||
onCopyCommand: () => void;
|
|
||||||
onServerInfo: () => void;
|
onServerInfo: () => void;
|
||||||
onSettings: () => void;
|
onSettings: () => void;
|
||||||
onEdit: () => void;
|
onEdit: () => void;
|
||||||
|
|
@ -76,7 +74,7 @@ export function ServerCard({
|
||||||
return (
|
return (
|
||||||
<Card withBorder padding="lg">
|
<Card withBorder padding="lg">
|
||||||
<Stack gap="sm">
|
<Stack gap="sm">
|
||||||
<Group justify="space-between">
|
<Group justify="space-between" wrap="wrap">
|
||||||
<HeaderLeft>
|
<HeaderLeft>
|
||||||
<Text fw={600} size="lg">
|
<Text fw={600} size="lg">
|
||||||
{name}
|
{name}
|
||||||
|
|
@ -101,14 +99,7 @@ export function ServerCard({
|
||||||
</Text>
|
</Text>
|
||||||
</Group>
|
</Group>
|
||||||
|
|
||||||
<Group gap="xs">
|
<ContentViewer type="text" content={command} copyable />
|
||||||
<Code>
|
|
||||||
<Text lineClamp={1} size="sm">
|
|
||||||
{command}
|
|
||||||
</Text>
|
|
||||||
</Code>
|
|
||||||
<CopyButton value={command} />
|
|
||||||
</Group>
|
|
||||||
|
|
||||||
{error && (
|
{error && (
|
||||||
<InlineError
|
<InlineError
|
||||||
|
|
@ -118,49 +109,50 @@ export function ServerCard({
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<ActionsRow>
|
<Group justify="space-between">
|
||||||
<Button variant="subtle" size="xs" onClick={onServerInfo}>
|
<ActionsRow>
|
||||||
Server Info
|
<Button variant="subtle" size="xs" onClick={onClone}>
|
||||||
</Button>
|
Clone
|
||||||
<Button variant="subtle" size="xs" onClick={onSettings}>
|
</Button>
|
||||||
Settings
|
<Button variant="subtle" size="xs" onClick={onEdit}>
|
||||||
</Button>
|
Edit
|
||||||
{canTestClientFeatures && (
|
</Button>
|
||||||
<Menu>
|
<Button variant="subtle" size="xs" color="red" onClick={onRemove}>
|
||||||
<Menu.Target>
|
Remove
|
||||||
<Button variant="subtle" size="xs">
|
</Button>
|
||||||
Test Client Features ▾
|
</ActionsRow>
|
||||||
</Button>
|
<ActionsRow>
|
||||||
</Menu.Target>
|
<Button variant="subtle" size="xs" onClick={onServerInfo}>
|
||||||
<Menu.Dropdown>
|
Server Info
|
||||||
<Menu.Item onClick={onTestSampling}>
|
</Button>
|
||||||
Simulate Sampling Request
|
<Button variant="subtle" size="xs" onClick={onSettings}>
|
||||||
</Menu.Item>
|
Settings
|
||||||
<Menu.Item onClick={onTestElicitationForm}>
|
</Button>
|
||||||
Simulate Elicitation (Form)
|
{canTestClientFeatures && (
|
||||||
</Menu.Item>
|
<Menu>
|
||||||
<Menu.Item onClick={onTestElicitationUrl}>
|
<Menu.Target>
|
||||||
Simulate Elicitation (URL)
|
<Button variant="subtle" size="xs">
|
||||||
</Menu.Item>
|
Test Client Features ▾
|
||||||
<Menu.Item onClick={onConfigureRoots}>
|
</Button>
|
||||||
Configure Roots
|
</Menu.Target>
|
||||||
</Menu.Item>
|
<Menu.Dropdown>
|
||||||
</Menu.Dropdown>
|
<Menu.Item onClick={onTestSampling}>
|
||||||
</Menu>
|
Simulate Sampling Request
|
||||||
)}
|
</Menu.Item>
|
||||||
</ActionsRow>
|
<Menu.Item onClick={onTestElicitationForm}>
|
||||||
|
Simulate Elicitation (Form)
|
||||||
<ActionsRow>
|
</Menu.Item>
|
||||||
<Button variant="subtle" size="xs" onClick={onClone}>
|
<Menu.Item onClick={onTestElicitationUrl}>
|
||||||
Clone
|
Simulate Elicitation (URL)
|
||||||
</Button>
|
</Menu.Item>
|
||||||
<Button variant="subtle" size="xs" onClick={onEdit}>
|
<Menu.Item onClick={onConfigureRoots}>
|
||||||
Edit
|
Configure Roots
|
||||||
</Button>
|
</Menu.Item>
|
||||||
<Button variant="subtle" size="xs" color="red" onClick={onRemove}>
|
</Menu.Dropdown>
|
||||||
Remove
|
</Menu>
|
||||||
</Button>
|
)}
|
||||||
</ActionsRow>
|
</ActionsRow>
|
||||||
|
</Group>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -30,15 +30,15 @@ export function LoggingScreen({
|
||||||
onCopyAll,
|
onCopyAll,
|
||||||
}: LoggingScreenProps) {
|
}: LoggingScreenProps) {
|
||||||
return (
|
return (
|
||||||
<Grid>
|
<Grid align="stretch">
|
||||||
<Grid.Col span={3}>
|
<Grid.Col span={3}>
|
||||||
<Paper withBorder p="md">
|
<Paper withBorder p="md" h="100%">
|
||||||
<LogControls {...controls} />
|
<LogControls {...controls} />
|
||||||
</Paper>
|
</Paper>
|
||||||
</Grid.Col>
|
</Grid.Col>
|
||||||
<Grid.Col span={9}>
|
<Grid.Col span={9}>
|
||||||
<Paper withBorder p="md">
|
<Paper withBorder p="md" h="100%">
|
||||||
<Stack gap="sm">
|
<Stack gap="sm" h="100%">
|
||||||
<Group justify="space-between">
|
<Group justify="space-between">
|
||||||
<Title order={4}>Log Stream</Title>
|
<Title order={4}>Log Stream</Title>
|
||||||
<Group>
|
<Group>
|
||||||
|
|
@ -52,17 +52,18 @@ export function LoggingScreen({
|
||||||
</Button>
|
</Button>
|
||||||
</Group>
|
</Group>
|
||||||
</Group>
|
</Group>
|
||||||
<ScrollArea h={500}>
|
{entries.length > 0 ? (
|
||||||
<Stack gap="xs">
|
<ScrollArea flex={1}>
|
||||||
{entries.map((entry, index) => (
|
<Stack gap="xs">
|
||||||
<LogEntry key={index} {...entry} />
|
{entries.map((entry, index) => (
|
||||||
))}
|
<LogEntry key={index} {...entry} />
|
||||||
|
))}
|
||||||
|
</Stack>
|
||||||
|
</ScrollArea>
|
||||||
|
) : (
|
||||||
|
<Stack flex={1} align="center" justify="center">
|
||||||
|
<Text c="dimmed">No log entries</Text>
|
||||||
</Stack>
|
</Stack>
|
||||||
</ScrollArea>
|
|
||||||
{entries.length === 0 && (
|
|
||||||
<Text c="dimmed" ta="center">
|
|
||||||
No log entries
|
|
||||||
</Text>
|
|
||||||
)}
|
)}
|
||||||
</Stack>
|
</Stack>
|
||||||
</Paper>
|
</Paper>
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,6 @@ type Story = StoryObj<typeof ServerListScreen>;
|
||||||
function makeServerCallbacks(): Pick<
|
function makeServerCallbacks(): Pick<
|
||||||
ServerCardProps,
|
ServerCardProps,
|
||||||
| "onToggleConnection"
|
| "onToggleConnection"
|
||||||
| "onCopyCommand"
|
|
||||||
| "onServerInfo"
|
| "onServerInfo"
|
||||||
| "onSettings"
|
| "onSettings"
|
||||||
| "onEdit"
|
| "onEdit"
|
||||||
|
|
@ -32,7 +31,6 @@ function makeServerCallbacks(): Pick<
|
||||||
> {
|
> {
|
||||||
return {
|
return {
|
||||||
onToggleConnection: fn(),
|
onToggleConnection: fn(),
|
||||||
onCopyCommand: fn(),
|
|
||||||
onServerInfo: fn(),
|
onServerInfo: fn(),
|
||||||
onSettings: fn(),
|
onSettings: fn(),
|
||||||
onEdit: fn(),
|
onEdit: fn(),
|
||||||
|
|
|
||||||
|
|
@ -6,4 +6,8 @@ export const ThemeCard = Card.extend({
|
||||||
radius: "md",
|
radius: "md",
|
||||||
withBorder: true,
|
withBorder: true,
|
||||||
},
|
},
|
||||||
|
classNames: (_theme, props) => {
|
||||||
|
if (props.variant === "responsive") return { root: "card-responsive" };
|
||||||
|
return {};
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue