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 {
|
||||
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 {
|
||||
message: string;
|
||||
details?: string;
|
||||
retryCount?: number;
|
||||
maxRetries?: number;
|
||||
onShowMore?: () => void;
|
||||
docLink?: string;
|
||||
}
|
||||
|
||||
|
|
@ -16,33 +16,52 @@ export function InlineError({
|
|||
maxRetries,
|
||||
docLink,
|
||||
}: InlineErrorProps) {
|
||||
const hasDetails =
|
||||
retryCount !== undefined || details !== undefined || docLink !== undefined;
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
const hasExpandable = details !== undefined || docLink !== undefined;
|
||||
|
||||
return (
|
||||
<Alert color="red" variant="light" title={message}>
|
||||
{hasDetails && (
|
||||
<Stack gap="xs">
|
||||
{retryCount !== undefined && (
|
||||
<Text size="sm">
|
||||
Retry attempt {retryCount}
|
||||
{maxRetries !== undefined && ` of ${maxRetries}`}
|
||||
<Alert color="red" variant="light">
|
||||
<Stack gap="xs">
|
||||
<Group justify="space-between" wrap="nowrap">
|
||||
<Group gap="sm" wrap="nowrap">
|
||||
<Text size="sm" fw={500} c="red">
|
||||
{message}
|
||||
</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 && (
|
||||
<Spoiler maxHeight={0} showLabel="Show more" hideLabel="Show less">
|
||||
<Text size="sm">{details}</Text>
|
||||
</Spoiler>
|
||||
)}
|
||||
{docLink && (
|
||||
<Group>
|
||||
<Anchor href={docLink} target="_blank" size="sm">
|
||||
View Troubleshooting Guide →
|
||||
</Anchor>
|
||||
</Group>
|
||||
)}
|
||||
</Stack>
|
||||
)}
|
||||
</Group>
|
||||
<Collapse in={expanded}>
|
||||
<Stack gap="xs">
|
||||
{details && <Text size="sm">{details}</Text>}
|
||||
{docLink && (
|
||||
<Button
|
||||
variant="light"
|
||||
size="xs"
|
||||
component="a"
|
||||
href={docLink}
|
||||
target="_blank"
|
||||
>
|
||||
View Troubleshooting Guide
|
||||
</Button>
|
||||
)}
|
||||
</Stack>
|
||||
</Collapse>
|
||||
</Stack>
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,7 +20,6 @@ type Story = StoryObj<typeof HomeLayout>;
|
|||
function makeServerCallbacks(): Pick<
|
||||
ServerCardProps,
|
||||
| "onToggleConnection"
|
||||
| "onCopyCommand"
|
||||
| "onServerInfo"
|
||||
| "onSettings"
|
||||
| "onEdit"
|
||||
|
|
@ -29,7 +28,6 @@ function makeServerCallbacks(): Pick<
|
|||
> {
|
||||
return {
|
||||
onToggleConnection: fn(),
|
||||
onCopyCommand: fn(),
|
||||
onServerInfo: fn(),
|
||||
onSettings: fn(),
|
||||
onEdit: fn(),
|
||||
|
|
|
|||
|
|
@ -2,12 +2,12 @@ import {
|
|||
Badge,
|
||||
Button,
|
||||
Card,
|
||||
Code,
|
||||
Collapse,
|
||||
Group,
|
||||
Stack,
|
||||
Text,
|
||||
} from "@mantine/core";
|
||||
import { ContentViewer } from "../../atoms/ContentViewer/ContentViewer";
|
||||
|
||||
export interface HistoryChildEntry {
|
||||
timestamp: string;
|
||||
|
|
@ -66,12 +66,12 @@ export function HistoryEntry({
|
|||
)}
|
||||
</Group>
|
||||
<Group gap="sm">
|
||||
<Badge color={status === "success" ? "green" : "red"}>
|
||||
{status === "success" ? "OK" : "Error"}
|
||||
</Badge>
|
||||
<Text size="sm" c="dimmed">
|
||||
{durationMs}ms
|
||||
</Text>
|
||||
<Badge color={status === "success" ? "green" : "red"}>
|
||||
{status === "success" ? "OK" : "Error"}
|
||||
</Badge>
|
||||
</Group>
|
||||
</Group>
|
||||
|
||||
|
|
@ -93,13 +93,21 @@ export function HistoryEntry({
|
|||
{parameters && (
|
||||
<Stack gap="xs">
|
||||
<Text size="sm">Parameters:</Text>
|
||||
<Code block>{JSON.stringify(parameters, null, 2)}</Code>
|
||||
<ContentViewer
|
||||
type="json"
|
||||
content={JSON.stringify(parameters)}
|
||||
copyable
|
||||
/>
|
||||
</Stack>
|
||||
)}
|
||||
{response && (
|
||||
<Stack gap="xs">
|
||||
<Text size="sm">Response:</Text>
|
||||
<Code block>{JSON.stringify(response, null, 2)}</Code>
|
||||
<ContentViewer
|
||||
type="json"
|
||||
content={JSON.stringify(response)}
|
||||
copyable
|
||||
/>
|
||||
</Stack>
|
||||
)}
|
||||
{childEntries && childEntries.length > 0 && (
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ const meta: Meta<typeof ServerCard> = {
|
|||
component: ServerCard,
|
||||
args: {
|
||||
onToggleConnection: fn(),
|
||||
onCopyCommand: fn(),
|
||||
onServerInfo: fn(),
|
||||
onSettings: fn(),
|
||||
onEdit: fn(),
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ import {
|
|||
Badge,
|
||||
Button,
|
||||
Card,
|
||||
Code,
|
||||
Group,
|
||||
Menu,
|
||||
Stack,
|
||||
|
|
@ -11,7 +10,7 @@ import {
|
|||
import { StatusIndicator } from "../../atoms/StatusIndicator/StatusIndicator";
|
||||
import { TransportBadge } from "../../atoms/TransportBadge/TransportBadge";
|
||||
import { ConnectionToggle } from "../../atoms/ConnectionToggle/ConnectionToggle";
|
||||
import { CopyButton } from "../../atoms/CopyButton/CopyButton";
|
||||
import { ContentViewer } from "../../atoms/ContentViewer/ContentViewer";
|
||||
import { InlineError } from "../../atoms/InlineError/InlineError";
|
||||
|
||||
export interface ServerCardProps {
|
||||
|
|
@ -25,7 +24,6 @@ export interface ServerCardProps {
|
|||
error?: { message: string; details?: string };
|
||||
canTestClientFeatures: boolean;
|
||||
onToggleConnection: (connect: boolean) => void;
|
||||
onCopyCommand: () => void;
|
||||
onServerInfo: () => void;
|
||||
onSettings: () => void;
|
||||
onEdit: () => void;
|
||||
|
|
@ -76,7 +74,7 @@ export function ServerCard({
|
|||
return (
|
||||
<Card withBorder padding="lg">
|
||||
<Stack gap="sm">
|
||||
<Group justify="space-between">
|
||||
<Group justify="space-between" wrap="wrap">
|
||||
<HeaderLeft>
|
||||
<Text fw={600} size="lg">
|
||||
{name}
|
||||
|
|
@ -101,14 +99,7 @@ export function ServerCard({
|
|||
</Text>
|
||||
</Group>
|
||||
|
||||
<Group gap="xs">
|
||||
<Code>
|
||||
<Text lineClamp={1} size="sm">
|
||||
{command}
|
||||
</Text>
|
||||
</Code>
|
||||
<CopyButton value={command} />
|
||||
</Group>
|
||||
<ContentViewer type="text" content={command} copyable />
|
||||
|
||||
{error && (
|
||||
<InlineError
|
||||
|
|
@ -118,49 +109,50 @@ export function ServerCard({
|
|||
/>
|
||||
)}
|
||||
|
||||
<ActionsRow>
|
||||
<Button variant="subtle" size="xs" onClick={onServerInfo}>
|
||||
Server Info
|
||||
</Button>
|
||||
<Button variant="subtle" size="xs" onClick={onSettings}>
|
||||
Settings
|
||||
</Button>
|
||||
{canTestClientFeatures && (
|
||||
<Menu>
|
||||
<Menu.Target>
|
||||
<Button variant="subtle" size="xs">
|
||||
Test Client Features ▾
|
||||
</Button>
|
||||
</Menu.Target>
|
||||
<Menu.Dropdown>
|
||||
<Menu.Item onClick={onTestSampling}>
|
||||
Simulate Sampling Request
|
||||
</Menu.Item>
|
||||
<Menu.Item onClick={onTestElicitationForm}>
|
||||
Simulate Elicitation (Form)
|
||||
</Menu.Item>
|
||||
<Menu.Item onClick={onTestElicitationUrl}>
|
||||
Simulate Elicitation (URL)
|
||||
</Menu.Item>
|
||||
<Menu.Item onClick={onConfigureRoots}>
|
||||
Configure Roots
|
||||
</Menu.Item>
|
||||
</Menu.Dropdown>
|
||||
</Menu>
|
||||
)}
|
||||
</ActionsRow>
|
||||
|
||||
<ActionsRow>
|
||||
<Button variant="subtle" size="xs" onClick={onClone}>
|
||||
Clone
|
||||
</Button>
|
||||
<Button variant="subtle" size="xs" onClick={onEdit}>
|
||||
Edit
|
||||
</Button>
|
||||
<Button variant="subtle" size="xs" color="red" onClick={onRemove}>
|
||||
Remove
|
||||
</Button>
|
||||
</ActionsRow>
|
||||
<Group justify="space-between">
|
||||
<ActionsRow>
|
||||
<Button variant="subtle" size="xs" onClick={onClone}>
|
||||
Clone
|
||||
</Button>
|
||||
<Button variant="subtle" size="xs" onClick={onEdit}>
|
||||
Edit
|
||||
</Button>
|
||||
<Button variant="subtle" size="xs" color="red" onClick={onRemove}>
|
||||
Remove
|
||||
</Button>
|
||||
</ActionsRow>
|
||||
<ActionsRow>
|
||||
<Button variant="subtle" size="xs" onClick={onServerInfo}>
|
||||
Server Info
|
||||
</Button>
|
||||
<Button variant="subtle" size="xs" onClick={onSettings}>
|
||||
Settings
|
||||
</Button>
|
||||
{canTestClientFeatures && (
|
||||
<Menu>
|
||||
<Menu.Target>
|
||||
<Button variant="subtle" size="xs">
|
||||
Test Client Features ▾
|
||||
</Button>
|
||||
</Menu.Target>
|
||||
<Menu.Dropdown>
|
||||
<Menu.Item onClick={onTestSampling}>
|
||||
Simulate Sampling Request
|
||||
</Menu.Item>
|
||||
<Menu.Item onClick={onTestElicitationForm}>
|
||||
Simulate Elicitation (Form)
|
||||
</Menu.Item>
|
||||
<Menu.Item onClick={onTestElicitationUrl}>
|
||||
Simulate Elicitation (URL)
|
||||
</Menu.Item>
|
||||
<Menu.Item onClick={onConfigureRoots}>
|
||||
Configure Roots
|
||||
</Menu.Item>
|
||||
</Menu.Dropdown>
|
||||
</Menu>
|
||||
)}
|
||||
</ActionsRow>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Card>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -30,15 +30,15 @@ export function LoggingScreen({
|
|||
onCopyAll,
|
||||
}: LoggingScreenProps) {
|
||||
return (
|
||||
<Grid>
|
||||
<Grid align="stretch">
|
||||
<Grid.Col span={3}>
|
||||
<Paper withBorder p="md">
|
||||
<Paper withBorder p="md" h="100%">
|
||||
<LogControls {...controls} />
|
||||
</Paper>
|
||||
</Grid.Col>
|
||||
<Grid.Col span={9}>
|
||||
<Paper withBorder p="md">
|
||||
<Stack gap="sm">
|
||||
<Paper withBorder p="md" h="100%">
|
||||
<Stack gap="sm" h="100%">
|
||||
<Group justify="space-between">
|
||||
<Title order={4}>Log Stream</Title>
|
||||
<Group>
|
||||
|
|
@ -52,17 +52,18 @@ export function LoggingScreen({
|
|||
</Button>
|
||||
</Group>
|
||||
</Group>
|
||||
<ScrollArea h={500}>
|
||||
<Stack gap="xs">
|
||||
{entries.map((entry, index) => (
|
||||
<LogEntry key={index} {...entry} />
|
||||
))}
|
||||
{entries.length > 0 ? (
|
||||
<ScrollArea flex={1}>
|
||||
<Stack gap="xs">
|
||||
{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>
|
||||
</ScrollArea>
|
||||
{entries.length === 0 && (
|
||||
<Text c="dimmed" ta="center">
|
||||
No log entries
|
||||
</Text>
|
||||
)}
|
||||
</Stack>
|
||||
</Paper>
|
||||
|
|
|
|||
|
|
@ -19,7 +19,6 @@ type Story = StoryObj<typeof ServerListScreen>;
|
|||
function makeServerCallbacks(): Pick<
|
||||
ServerCardProps,
|
||||
| "onToggleConnection"
|
||||
| "onCopyCommand"
|
||||
| "onServerInfo"
|
||||
| "onSettings"
|
||||
| "onEdit"
|
||||
|
|
@ -32,7 +31,6 @@ function makeServerCallbacks(): Pick<
|
|||
> {
|
||||
return {
|
||||
onToggleConnection: fn(),
|
||||
onCopyCommand: fn(),
|
||||
onServerInfo: fn(),
|
||||
onSettings: fn(),
|
||||
onEdit: fn(),
|
||||
|
|
|
|||
|
|
@ -6,4 +6,8 @@ export const ThemeCard = Card.extend({
|
|||
radius: "md",
|
||||
withBorder: true,
|
||||
},
|
||||
classNames: (_theme, props) => {
|
||||
if (props.variant === "responsive") return { root: "card-responsive" };
|
||||
return {};
|
||||
},
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue