* feat(sort): add SortToggle for Logs, History, and Network (#1371) Introduces a reusable SortToggle element and wires it into the Logs, History, and Network screens so users can flip chronological order. Selection is persisted per-screen to localStorage under the inspector.sortDirection.* namespace, with invalid stored values falling back to the newest-first default. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(logs): move auto-scroll checkbox left of the sort selector Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor(logs): remove auto-scroll checkbox; ListToggle left of SortToggle - Drops the Logs auto-scroll checkbox and its plumbing through LoggingScreen / InspectorView. - Moves the ListToggle to the left of the SortToggle on the History and Network panel toolbars. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor(sort): address review on #1372 - Hydrate sort direction synchronously (getInitialValueInEffect: false) to remove the one-frame newest-first flicker on first paint. - Drop the redundant `[...matched].sort()` copy across the three panels; `.filter()` already returns a fresh array. - Document why serializeSortDirection bypasses Mantine's default JSON stringify (raw enum value in storage). - MemoryStorage.getItem: collapse `has` + `!` to `?? null`. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(history): default the request list to collapsed for parity with Network The History list opened expanded while Network opened collapsed; align History with Network so both panels start in the same compact view. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(history,network): persist list compact state to localStorage Lifts the per-panel `compact` flag out of HistoryListPanel / NetworkStreamPanel and into InspectorView's localStorage hooks, mirroring the sort-direction shape: inspector.listCompact.history inspector.listCompact.network Defaults to collapsed (compact=true) on both, with a corrupted-value deserializer that clamps unknown strings back to the default. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(servers,resources): persist ListToggle state to localStorage Lifts the per-screen compact preference out of ServerListScreen and ResourceControls into InspectorView's localStorage hooks, matching the History/Network shape: inspector.listCompact.servers inspector.listCompact.resources Servers and Resources both default to expanded (compact=false). For ResourceControls the displayed compact is still derived from openSections (so per-section accordion clicks still work locally), and only explicit ListToggle clicks update the persisted preference — the handler computes the next compact value from the click outcome rather than blindly flipping, so a half-open accordion still records the right post-state. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor(sort-toggle): switch to icon button matching ListToggle style Replaces the Mantine Select with a subtle Button (or ActionIcon for the subtle variant), using TbSortDescending2 for newest-first and TbSortAscending2 for oldest-first. Clicking the button flips between the two values. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor(sort-toggle): restore Select with sort icon in rightSection Reverts SortToggle to a Mantine Select with simplified labels ("Newest First" / "Oldest First") and the corresponding TbSortDescending2 / TbSortAscending2 icon in the rightSection, replacing the default chevron. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(sort-toggle): swap icons so they match label direction Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(sort-toggle): use down arrow for newest, up arrow for oldest Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor(inspector-view): extract useSortDirection / useListCompact Collapses seven near-identical useLocalStorage call sites in InspectorView into two scoped helpers. Each helper carries the shared namespace, serialize/deserialize adapters, and getInitialValueInEffect: false; the call sites now read as "this screen has sort persisted to this scope" rather than re-stating the storage shape every time. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(sort-toggle): apply prettier formatting Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
a7178280f0
commit
dd0f5de820
33 changed files with 812 additions and 117 deletions
|
|
@ -0,0 +1,41 @@
|
||||||
|
import type { Meta, StoryObj } from "@storybook/react-vite";
|
||||||
|
import { expect, fn, userEvent, within } from "storybook/test";
|
||||||
|
import { SortToggle } from "./SortToggle";
|
||||||
|
|
||||||
|
const meta: Meta<typeof SortToggle> = {
|
||||||
|
title: "Elements/SortToggle",
|
||||||
|
component: SortToggle,
|
||||||
|
args: {
|
||||||
|
onChange: fn(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
type Story = StoryObj<typeof SortToggle>;
|
||||||
|
|
||||||
|
export const NewestFirst: Story = {
|
||||||
|
args: {
|
||||||
|
value: "newest-first",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const OldestFirst: Story = {
|
||||||
|
args: {
|
||||||
|
value: "oldest-first",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const FlipsDirection: Story = {
|
||||||
|
args: {
|
||||||
|
value: "newest-first",
|
||||||
|
},
|
||||||
|
play: async ({ canvasElement, args }) => {
|
||||||
|
const body = within(canvasElement.ownerDocument.body);
|
||||||
|
const select = await body.findByRole("textbox", {
|
||||||
|
name: "Sort direction",
|
||||||
|
});
|
||||||
|
await userEvent.click(select);
|
||||||
|
await userEvent.click(await body.findByText("Oldest First"));
|
||||||
|
await expect(args.onChange).toHaveBeenCalledWith("oldest-first");
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,45 @@
|
||||||
|
import { describe, it, expect, vi } from "vitest";
|
||||||
|
import userEvent from "@testing-library/user-event";
|
||||||
|
import { renderWithMantine, screen } from "../../../test/renderWithMantine";
|
||||||
|
import { SortToggle } from "./SortToggle";
|
||||||
|
|
||||||
|
describe("SortToggle", () => {
|
||||||
|
it("renders the current value as the selected option", () => {
|
||||||
|
renderWithMantine(<SortToggle value="newest-first" onChange={() => {}} />);
|
||||||
|
expect(screen.getByDisplayValue("Newest First")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders 'Oldest First' when value is oldest-first", () => {
|
||||||
|
renderWithMantine(<SortToggle value="oldest-first" onChange={() => {}} />);
|
||||||
|
expect(screen.getByDisplayValue("Oldest First")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("uses the default aria-label", () => {
|
||||||
|
renderWithMantine(<SortToggle value="newest-first" onChange={() => {}} />);
|
||||||
|
expect(
|
||||||
|
screen.getByRole("textbox", { name: "Sort direction" }),
|
||||||
|
).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("honors a custom aria-label", () => {
|
||||||
|
renderWithMantine(
|
||||||
|
<SortToggle
|
||||||
|
value="newest-first"
|
||||||
|
onChange={() => {}}
|
||||||
|
aria-label="Logs sort"
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
screen.getByRole("textbox", { name: "Logs sort" }),
|
||||||
|
).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("invokes onChange with the new direction when the user picks another option", async () => {
|
||||||
|
const user = userEvent.setup();
|
||||||
|
const onChange = vi.fn();
|
||||||
|
renderWithMantine(<SortToggle value="newest-first" onChange={onChange} />);
|
||||||
|
await user.click(screen.getByRole("textbox", { name: "Sort direction" }));
|
||||||
|
await user.click(await screen.findByText("Oldest First"));
|
||||||
|
expect(onChange).toHaveBeenCalledWith("oldest-first");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,42 @@
|
||||||
|
import { Select } from "@mantine/core";
|
||||||
|
import { TbSortAscending2, TbSortDescending2 } from "react-icons/tb";
|
||||||
|
|
||||||
|
export type SortDirection = "oldest-first" | "newest-first";
|
||||||
|
|
||||||
|
export interface SortToggleProps {
|
||||||
|
value: SortDirection;
|
||||||
|
onChange: (next: SortDirection) => void;
|
||||||
|
"aria-label"?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const OPTIONS: { value: SortDirection; label: string }[] = [
|
||||||
|
{ value: "newest-first", label: "Newest First" },
|
||||||
|
{ value: "oldest-first", label: "Oldest First" },
|
||||||
|
];
|
||||||
|
|
||||||
|
function isSortDirection(value: string | null): value is SortDirection {
|
||||||
|
return value === "oldest-first" || value === "newest-first";
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SortToggle({
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
"aria-label": ariaLabel = "Sort direction",
|
||||||
|
}: SortToggleProps) {
|
||||||
|
const Icon = value === "newest-first" ? TbSortDescending2 : TbSortAscending2;
|
||||||
|
return (
|
||||||
|
<Select
|
||||||
|
size="sm"
|
||||||
|
w={150}
|
||||||
|
data={OPTIONS}
|
||||||
|
value={value}
|
||||||
|
onChange={(next) => {
|
||||||
|
if (isSortDirection(next)) onChange(next);
|
||||||
|
}}
|
||||||
|
allowDeselect={false}
|
||||||
|
withCheckIcon={false}
|
||||||
|
rightSection={<Icon size={16} />}
|
||||||
|
aria-label={ariaLabel}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -13,6 +13,10 @@ const meta: Meta<typeof HistoryListPanel> = {
|
||||||
onExport: fn(),
|
onExport: fn(),
|
||||||
onReplay: fn(),
|
onReplay: fn(),
|
||||||
onTogglePin: fn(),
|
onTogglePin: fn(),
|
||||||
|
sortDirection: "newest-first",
|
||||||
|
onSortChange: fn(),
|
||||||
|
compact: true,
|
||||||
|
onToggleCompact: fn(),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -66,6 +66,10 @@ const baseProps = {
|
||||||
onExport: vi.fn(),
|
onExport: vi.fn(),
|
||||||
onReplay: vi.fn(),
|
onReplay: vi.fn(),
|
||||||
onTogglePin: vi.fn(),
|
onTogglePin: vi.fn(),
|
||||||
|
sortDirection: "newest-first" as const,
|
||||||
|
onSortChange: vi.fn(),
|
||||||
|
compact: true,
|
||||||
|
onToggleCompact: vi.fn(),
|
||||||
};
|
};
|
||||||
|
|
||||||
describe("HistoryListPanel", () => {
|
describe("HistoryListPanel", () => {
|
||||||
|
|
@ -219,22 +223,82 @@ describe("HistoryListPanel", () => {
|
||||||
expect(onTogglePin).toHaveBeenCalledWith("req-1");
|
expect(onTogglePin).toHaveBeenCalledWith("req-1");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("toggles compact list state when ListToggle is clicked", async () => {
|
it("renders entries newest-first by default", () => {
|
||||||
const user = userEvent.setup();
|
|
||||||
renderWithMantine(
|
renderWithMantine(
|
||||||
<HistoryListPanel {...baseProps} entries={sampleEntries} />,
|
<HistoryListPanel {...baseProps} entries={sampleEntries} />,
|
||||||
);
|
);
|
||||||
// Initially expanded — Collapse buttons exist on each entry, and the
|
const methods = screen.getAllByText(
|
||||||
// ListToggle exposes its aria-label as "Collapse all".
|
/tools\/call|resources\/read|tools\/list/,
|
||||||
expect(
|
);
|
||||||
screen.getAllByRole("button", { name: "Collapse" }).length,
|
expect(methods[0]).toHaveTextContent("tools/list");
|
||||||
).toBeGreaterThan(0);
|
expect(methods[methods.length - 1]).toHaveTextContent("tools/call");
|
||||||
|
});
|
||||||
|
|
||||||
await user.click(screen.getByRole("button", { name: "Collapse all" }));
|
it("reorders entries when sortDirection is oldest-first", () => {
|
||||||
|
renderWithMantine(
|
||||||
|
<HistoryListPanel
|
||||||
|
{...baseProps}
|
||||||
|
entries={sampleEntries}
|
||||||
|
sortDirection="oldest-first"
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
const methods = screen.getAllByText(
|
||||||
|
/tools\/call|resources\/read|tools\/list/,
|
||||||
|
);
|
||||||
|
expect(methods[0]).toHaveTextContent("tools/call");
|
||||||
|
expect(methods[methods.length - 1]).toHaveTextContent("tools/list");
|
||||||
|
});
|
||||||
|
|
||||||
// After toggle, entries collapsed — they show Expand
|
it("invokes onSortChange when the user picks a new sort", async () => {
|
||||||
|
const user = userEvent.setup();
|
||||||
|
const onSortChange = vi.fn();
|
||||||
|
renderWithMantine(
|
||||||
|
<HistoryListPanel
|
||||||
|
{...baseProps}
|
||||||
|
entries={sampleEntries}
|
||||||
|
onSortChange={onSortChange}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
await user.click(
|
||||||
|
screen.getByRole("textbox", { name: "History sort direction" }),
|
||||||
|
);
|
||||||
|
await user.click(await screen.findByText("Oldest First"));
|
||||||
|
expect(onSortChange).toHaveBeenCalledWith("oldest-first");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders entries collapsed when compact is true (default parity with Network)", () => {
|
||||||
|
renderWithMantine(
|
||||||
|
<HistoryListPanel {...baseProps} entries={sampleEntries} compact />,
|
||||||
|
);
|
||||||
expect(
|
expect(
|
||||||
screen.getAllByRole("button", { name: "Expand" }).length,
|
screen.getAllByRole("button", { name: "Expand" }).length,
|
||||||
).toBeGreaterThan(0);
|
).toBeGreaterThan(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("renders entries expanded when compact is false", () => {
|
||||||
|
renderWithMantine(
|
||||||
|
<HistoryListPanel
|
||||||
|
{...baseProps}
|
||||||
|
entries={sampleEntries}
|
||||||
|
compact={false}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
screen.getAllByRole("button", { name: "Collapse" }).length,
|
||||||
|
).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("invokes onToggleCompact when the ListToggle is clicked", async () => {
|
||||||
|
const user = userEvent.setup();
|
||||||
|
const onToggleCompact = vi.fn();
|
||||||
|
renderWithMantine(
|
||||||
|
<HistoryListPanel
|
||||||
|
{...baseProps}
|
||||||
|
entries={sampleEntries}
|
||||||
|
onToggleCompact={onToggleCompact}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
await user.click(screen.getByRole("button", { name: "Expand all" }));
|
||||||
|
expect(onToggleCompact).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { useMemo, useState } from "react";
|
import { useMemo } from "react";
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Group,
|
Group,
|
||||||
|
|
@ -11,6 +11,10 @@ import {
|
||||||
import type { MessageEntry, MessageMethod } from "@inspector/core/mcp/types.js";
|
import type { MessageEntry, MessageMethod } from "@inspector/core/mcp/types.js";
|
||||||
import { HistoryEntry } from "../HistoryEntry/HistoryEntry";
|
import { HistoryEntry } from "../HistoryEntry/HistoryEntry";
|
||||||
import { ListToggle } from "../../elements/ListToggle/ListToggle";
|
import { ListToggle } from "../../elements/ListToggle/ListToggle";
|
||||||
|
import {
|
||||||
|
SortToggle,
|
||||||
|
type SortDirection,
|
||||||
|
} from "../../elements/SortToggle/SortToggle";
|
||||||
import { extractMethod } from "../historyUtils.js";
|
import { extractMethod } from "../historyUtils.js";
|
||||||
|
|
||||||
export interface HistoryListPanelProps {
|
export interface HistoryListPanelProps {
|
||||||
|
|
@ -22,6 +26,10 @@ export interface HistoryListPanelProps {
|
||||||
onExport: () => void;
|
onExport: () => void;
|
||||||
onReplay: (id: string) => void;
|
onReplay: (id: string) => void;
|
||||||
onTogglePin: (id: string) => void;
|
onTogglePin: (id: string) => void;
|
||||||
|
sortDirection: SortDirection;
|
||||||
|
onSortChange: (next: SortDirection) => void;
|
||||||
|
compact: boolean;
|
||||||
|
onToggleCompact: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const PanelContainer = Paper.withProps({
|
const PanelContainer = Paper.withProps({
|
||||||
|
|
@ -71,13 +79,19 @@ export function HistoryListPanel({
|
||||||
onExport,
|
onExport,
|
||||||
onReplay,
|
onReplay,
|
||||||
onTogglePin,
|
onTogglePin,
|
||||||
|
sortDirection,
|
||||||
|
onSortChange,
|
||||||
|
compact,
|
||||||
|
onToggleCompact,
|
||||||
}: HistoryListPanelProps) {
|
}: HistoryListPanelProps) {
|
||||||
const [compact, setCompact] = useState(false);
|
const filteredEntries = useMemo(() => {
|
||||||
|
// `.filter()` returns a fresh array, so sorting in-place is safe.
|
||||||
const filteredEntries = useMemo(
|
const sorted = entries
|
||||||
() => entries.filter((e) => matchesFilters(e, searchText, methodFilter)),
|
.filter((e) => matchesFilters(e, searchText, methodFilter))
|
||||||
[entries, searchText, methodFilter],
|
.sort((a, b) => a.timestamp.getTime() - b.timestamp.getTime());
|
||||||
);
|
if (sortDirection === "newest-first") sorted.reverse();
|
||||||
|
return sorted;
|
||||||
|
}, [entries, searchText, methodFilter, sortDirection]);
|
||||||
|
|
||||||
const pinnedEntries = useMemo(
|
const pinnedEntries = useMemo(
|
||||||
() => filteredEntries.filter((e) => pinnedIds.has(e.id)),
|
() => filteredEntries.filter((e) => pinnedIds.has(e.id)),
|
||||||
|
|
@ -97,11 +111,13 @@ export function HistoryListPanel({
|
||||||
<Title order={4}>Requests</Title>
|
<Title order={4}>Requests</Title>
|
||||||
<Group gap="xs">
|
<Group gap="xs">
|
||||||
{hasResults && (
|
{hasResults && (
|
||||||
<ListToggle
|
<ListToggle compact={compact} onToggle={onToggleCompact} />
|
||||||
compact={compact}
|
|
||||||
onToggle={() => setCompact((c) => !c)}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
|
<SortToggle
|
||||||
|
value={sortDirection}
|
||||||
|
onChange={onSortChange}
|
||||||
|
aria-label="History sort direction"
|
||||||
|
/>
|
||||||
<Button
|
<Button
|
||||||
variant="default"
|
variant="default"
|
||||||
onClick={onClearAll}
|
onClick={onClearAll}
|
||||||
|
|
|
||||||
|
|
@ -19,10 +19,10 @@ const meta: Meta<typeof LogStreamPanel> = {
|
||||||
alert: true,
|
alert: true,
|
||||||
emergency: true,
|
emergency: true,
|
||||||
},
|
},
|
||||||
autoScroll: true,
|
|
||||||
onToggleAutoScroll: fn(),
|
|
||||||
onClear: fn(),
|
onClear: fn(),
|
||||||
onExport: fn(),
|
onExport: fn(),
|
||||||
|
sortDirection: "newest-first",
|
||||||
|
onSortChange: fn(),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -43,10 +43,10 @@ const baseProps = {
|
||||||
entries,
|
entries,
|
||||||
filterText: "",
|
filterText: "",
|
||||||
visibleLevels: allVisible,
|
visibleLevels: allVisible,
|
||||||
autoScroll: true,
|
|
||||||
onToggleAutoScroll: vi.fn(),
|
|
||||||
onClear: vi.fn(),
|
onClear: vi.fn(),
|
||||||
onExport: vi.fn(),
|
onExport: vi.fn(),
|
||||||
|
sortDirection: "newest-first" as const,
|
||||||
|
onSortChange: vi.fn(),
|
||||||
};
|
};
|
||||||
|
|
||||||
describe("LogStreamPanel", () => {
|
describe("LogStreamPanel", () => {
|
||||||
|
|
@ -149,18 +149,39 @@ describe("LogStreamPanel", () => {
|
||||||
expect(onExport).toHaveBeenCalledTimes(1);
|
expect(onExport).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("invokes onToggleAutoScroll when the auto-scroll checkbox is clicked", async () => {
|
it("renders entries newest-first by default", () => {
|
||||||
const user = userEvent.setup();
|
renderWithMantine(<LogStreamPanel {...baseProps} />);
|
||||||
const onToggleAutoScroll = vi.fn();
|
const items = screen.getAllByText(
|
||||||
renderWithMantine(
|
/Server started|Failed to read|Loading config|deprecated/,
|
||||||
<LogStreamPanel {...baseProps} onToggleAutoScroll={onToggleAutoScroll} />,
|
|
||||||
);
|
);
|
||||||
await user.click(screen.getByLabelText("Auto-scroll"));
|
// Newest receivedAt is the warning entry; oldest is "Server started".
|
||||||
expect(onToggleAutoScroll).toHaveBeenCalledTimes(1);
|
expect(items[0]).toHaveTextContent('{"code":42,"msg":"deprecated"}');
|
||||||
|
expect(items[items.length - 1]).toHaveTextContent("Server started");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("renders auto-scroll checkbox with correct checked state", () => {
|
it("reorders entries when sortDirection is oldest-first", () => {
|
||||||
renderWithMantine(<LogStreamPanel {...baseProps} autoScroll={false} />);
|
renderWithMantine(
|
||||||
expect(screen.getByLabelText("Auto-scroll")).not.toBeChecked();
|
<LogStreamPanel {...baseProps} sortDirection="oldest-first" />,
|
||||||
|
);
|
||||||
|
const items = screen.getAllByText(
|
||||||
|
/Server started|Failed to read|Loading config|deprecated/,
|
||||||
|
);
|
||||||
|
expect(items[0]).toHaveTextContent("Server started");
|
||||||
|
expect(items[items.length - 1]).toHaveTextContent(
|
||||||
|
'{"code":42,"msg":"deprecated"}',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("invokes onSortChange when the user picks a new sort", async () => {
|
||||||
|
const user = userEvent.setup();
|
||||||
|
const onSortChange = vi.fn();
|
||||||
|
renderWithMantine(
|
||||||
|
<LogStreamPanel {...baseProps} onSortChange={onSortChange} />,
|
||||||
|
);
|
||||||
|
await user.click(
|
||||||
|
screen.getByRole("textbox", { name: "Logs sort direction" }),
|
||||||
|
);
|
||||||
|
await user.click(await screen.findByText("Oldest First"));
|
||||||
|
expect(onSortChange).toHaveBeenCalledWith("oldest-first");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Checkbox,
|
|
||||||
Group,
|
Group,
|
||||||
Paper,
|
Paper,
|
||||||
ScrollArea,
|
ScrollArea,
|
||||||
|
|
@ -12,15 +11,19 @@ import {
|
||||||
import type { LoggingLevel } from "@modelcontextprotocol/sdk/types.js";
|
import type { LoggingLevel } from "@modelcontextprotocol/sdk/types.js";
|
||||||
import { LogEntry } from "../../elements/LogEntry/LogEntry";
|
import { LogEntry } from "../../elements/LogEntry/LogEntry";
|
||||||
import type { LogEntryData } from "../../elements/LogEntry/LogEntry";
|
import type { LogEntryData } from "../../elements/LogEntry/LogEntry";
|
||||||
|
import {
|
||||||
|
SortToggle,
|
||||||
|
type SortDirection,
|
||||||
|
} from "../../elements/SortToggle/SortToggle";
|
||||||
|
|
||||||
export interface LogStreamPanelProps {
|
export interface LogStreamPanelProps {
|
||||||
entries: LogEntryData[];
|
entries: LogEntryData[];
|
||||||
filterText: string;
|
filterText: string;
|
||||||
visibleLevels: Record<LoggingLevel, boolean>;
|
visibleLevels: Record<LoggingLevel, boolean>;
|
||||||
autoScroll: boolean;
|
|
||||||
onToggleAutoScroll: () => void;
|
|
||||||
onClear: () => void;
|
onClear: () => void;
|
||||||
onExport: () => void;
|
onExport: () => void;
|
||||||
|
sortDirection: SortDirection;
|
||||||
|
onSortChange: (next: SortDirection) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const PanelContainer = Paper.withProps({
|
const PanelContainer = Paper.withProps({
|
||||||
|
|
@ -61,25 +64,29 @@ export function LogStreamPanel({
|
||||||
entries,
|
entries,
|
||||||
filterText,
|
filterText,
|
||||||
visibleLevels,
|
visibleLevels,
|
||||||
autoScroll,
|
|
||||||
onToggleAutoScroll,
|
|
||||||
onClear,
|
onClear,
|
||||||
onExport,
|
onExport,
|
||||||
|
sortDirection,
|
||||||
|
onSortChange,
|
||||||
}: LogStreamPanelProps) {
|
}: LogStreamPanelProps) {
|
||||||
const filteredEntries = useMemo(
|
const filteredEntries = useMemo(() => {
|
||||||
() => entries.filter((e) => matchesFilters(e, filterText, visibleLevels)),
|
// `.filter()` returns a fresh array, so sorting in-place is safe.
|
||||||
[entries, filterText, visibleLevels],
|
const sorted = entries
|
||||||
);
|
.filter((e) => matchesFilters(e, filterText, visibleLevels))
|
||||||
|
.sort((a, b) => a.receivedAt.getTime() - b.receivedAt.getTime());
|
||||||
|
if (sortDirection === "newest-first") sorted.reverse();
|
||||||
|
return sorted;
|
||||||
|
}, [entries, filterText, visibleLevels, sortDirection]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PanelContainer>
|
<PanelContainer>
|
||||||
<Group justify="space-between" mb="sm">
|
<Group justify="space-between" mb="sm">
|
||||||
<Title order={4}>Log Stream</Title>
|
<Title order={4}>Log Stream</Title>
|
||||||
<Group>
|
<Group>
|
||||||
<Checkbox
|
<SortToggle
|
||||||
label="Auto-scroll"
|
value={sortDirection}
|
||||||
checked={autoScroll}
|
onChange={onSortChange}
|
||||||
onChange={onToggleAutoScroll}
|
aria-label="Logs sort direction"
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
variant="default"
|
variant="default"
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,10 @@ const meta: Meta<typeof NetworkStreamPanel> = {
|
||||||
visibleCategories: { auth: true, transport: true },
|
visibleCategories: { auth: true, transport: true },
|
||||||
onClear: fn(),
|
onClear: fn(),
|
||||||
onExport: fn(),
|
onExport: fn(),
|
||||||
|
sortDirection: "newest-first",
|
||||||
|
onSortChange: fn(),
|
||||||
|
compact: true,
|
||||||
|
onToggleCompact: fn(),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,10 @@ const baseProps = {
|
||||||
visibleCategories: { auth: true, transport: true } as const,
|
visibleCategories: { auth: true, transport: true } as const,
|
||||||
onClear: vi.fn(),
|
onClear: vi.fn(),
|
||||||
onExport: vi.fn(),
|
onExport: vi.fn(),
|
||||||
|
sortDirection: "newest-first" as const,
|
||||||
|
onSortChange: vi.fn(),
|
||||||
|
compact: true,
|
||||||
|
onToggleCompact: vi.fn(),
|
||||||
};
|
};
|
||||||
|
|
||||||
describe("NetworkStreamPanel", () => {
|
describe("NetworkStreamPanel", () => {
|
||||||
|
|
@ -124,14 +128,88 @@ describe("NetworkStreamPanel", () => {
|
||||||
expect(screen.getByRole("button", { name: "Export" })).toBeDisabled();
|
expect(screen.getByRole("button", { name: "Export" })).toBeDisabled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("toggles between compact and expanded list views", async () => {
|
it("renders entries collapsed when compact is true", () => {
|
||||||
const user = userEvent.setup();
|
renderWithMantine(<NetworkStreamPanel {...baseProps} compact />);
|
||||||
renderWithMantine(<NetworkStreamPanel {...baseProps} />);
|
|
||||||
// List starts compact -> entry should show Expand button
|
|
||||||
expect(screen.getByRole("button", { name: "Expand" })).toBeInTheDocument();
|
expect(screen.getByRole("button", { name: "Expand" })).toBeInTheDocument();
|
||||||
await user.click(screen.getByRole("button", { name: "Expand all" }));
|
});
|
||||||
|
|
||||||
|
it("renders entries expanded when compact is false", () => {
|
||||||
|
renderWithMantine(<NetworkStreamPanel {...baseProps} compact={false} />);
|
||||||
expect(
|
expect(
|
||||||
screen.getByRole("button", { name: "Collapse" }),
|
screen.getByRole("button", { name: "Collapse" }),
|
||||||
).toBeInTheDocument();
|
).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("invokes onToggleCompact when the ListToggle is clicked", async () => {
|
||||||
|
const user = userEvent.setup();
|
||||||
|
const onToggleCompact = vi.fn();
|
||||||
|
renderWithMantine(
|
||||||
|
<NetworkStreamPanel {...baseProps} onToggleCompact={onToggleCompact} />,
|
||||||
|
);
|
||||||
|
await user.click(screen.getByRole("button", { name: "Expand all" }));
|
||||||
|
expect(onToggleCompact).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders entries newest-first by default", () => {
|
||||||
|
const older: FetchRequestEntry = {
|
||||||
|
...entry,
|
||||||
|
id: "older",
|
||||||
|
url: "https://example.com/older",
|
||||||
|
timestamp: new Date("2026-03-17T09:00:00Z"),
|
||||||
|
};
|
||||||
|
const newer: FetchRequestEntry = {
|
||||||
|
...entry,
|
||||||
|
id: "newer",
|
||||||
|
url: "https://example.com/newer",
|
||||||
|
timestamp: new Date("2026-03-17T11:00:00Z"),
|
||||||
|
};
|
||||||
|
renderWithMantine(
|
||||||
|
<NetworkStreamPanel {...baseProps} entries={[older, newer]} />,
|
||||||
|
);
|
||||||
|
const urls = screen.getAllByText(/example\.com\//);
|
||||||
|
expect(urls[0]).toHaveTextContent("https://example.com/newer");
|
||||||
|
expect(urls[urls.length - 1]).toHaveTextContent(
|
||||||
|
"https://example.com/older",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("reorders entries when sortDirection is oldest-first", () => {
|
||||||
|
const older: FetchRequestEntry = {
|
||||||
|
...entry,
|
||||||
|
id: "older",
|
||||||
|
url: "https://example.com/older",
|
||||||
|
timestamp: new Date("2026-03-17T09:00:00Z"),
|
||||||
|
};
|
||||||
|
const newer: FetchRequestEntry = {
|
||||||
|
...entry,
|
||||||
|
id: "newer",
|
||||||
|
url: "https://example.com/newer",
|
||||||
|
timestamp: new Date("2026-03-17T11:00:00Z"),
|
||||||
|
};
|
||||||
|
renderWithMantine(
|
||||||
|
<NetworkStreamPanel
|
||||||
|
{...baseProps}
|
||||||
|
entries={[newer, older]}
|
||||||
|
sortDirection="oldest-first"
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
const urls = screen.getAllByText(/example\.com\//);
|
||||||
|
expect(urls[0]).toHaveTextContent("https://example.com/older");
|
||||||
|
expect(urls[urls.length - 1]).toHaveTextContent(
|
||||||
|
"https://example.com/newer",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("invokes onSortChange when the user picks a new sort", async () => {
|
||||||
|
const user = userEvent.setup();
|
||||||
|
const onSortChange = vi.fn();
|
||||||
|
renderWithMantine(
|
||||||
|
<NetworkStreamPanel {...baseProps} onSortChange={onSortChange} />,
|
||||||
|
);
|
||||||
|
await user.click(
|
||||||
|
screen.getByRole("textbox", { name: "Network sort direction" }),
|
||||||
|
);
|
||||||
|
await user.click(await screen.findByText("Oldest First"));
|
||||||
|
expect(onSortChange).toHaveBeenCalledWith("oldest-first");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { useMemo, useState } from "react";
|
import { useMemo } from "react";
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Group,
|
Group,
|
||||||
|
|
@ -14,6 +14,10 @@ import type {
|
||||||
} from "@inspector/core/mcp/types.js";
|
} from "@inspector/core/mcp/types.js";
|
||||||
import { NetworkEntry } from "../NetworkEntry/NetworkEntry";
|
import { NetworkEntry } from "../NetworkEntry/NetworkEntry";
|
||||||
import { ListToggle } from "../../elements/ListToggle/ListToggle";
|
import { ListToggle } from "../../elements/ListToggle/ListToggle";
|
||||||
|
import {
|
||||||
|
SortToggle,
|
||||||
|
type SortDirection,
|
||||||
|
} from "../../elements/SortToggle/SortToggle";
|
||||||
|
|
||||||
export interface NetworkStreamPanelProps {
|
export interface NetworkStreamPanelProps {
|
||||||
entries: FetchRequestEntry[];
|
entries: FetchRequestEntry[];
|
||||||
|
|
@ -21,6 +25,10 @@ export interface NetworkStreamPanelProps {
|
||||||
visibleCategories: Record<FetchRequestCategory, boolean>;
|
visibleCategories: Record<FetchRequestCategory, boolean>;
|
||||||
onClear: () => void;
|
onClear: () => void;
|
||||||
onExport: () => void;
|
onExport: () => void;
|
||||||
|
sortDirection: SortDirection;
|
||||||
|
onSortChange: (next: SortDirection) => void;
|
||||||
|
compact: boolean;
|
||||||
|
onToggleCompact: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const PanelContainer = Paper.withProps({
|
const PanelContainer = Paper.withProps({
|
||||||
|
|
@ -82,14 +90,19 @@ export function NetworkStreamPanel({
|
||||||
visibleCategories,
|
visibleCategories,
|
||||||
onClear,
|
onClear,
|
||||||
onExport,
|
onExport,
|
||||||
|
sortDirection,
|
||||||
|
onSortChange,
|
||||||
|
compact,
|
||||||
|
onToggleCompact,
|
||||||
}: NetworkStreamPanelProps) {
|
}: NetworkStreamPanelProps) {
|
||||||
const [compact, setCompact] = useState(true);
|
const filteredEntries = useMemo(() => {
|
||||||
|
// `.filter()` returns a fresh array, so sorting in-place is safe.
|
||||||
const filteredEntries = useMemo(
|
const sorted = entries
|
||||||
() =>
|
.filter((e) => matchesFilters(e, filterText, visibleCategories))
|
||||||
entries.filter((e) => matchesFilters(e, filterText, visibleCategories)),
|
.sort((a, b) => a.timestamp.getTime() - b.timestamp.getTime());
|
||||||
[entries, filterText, visibleCategories],
|
if (sortDirection === "newest-first") sorted.reverse();
|
||||||
);
|
return sorted;
|
||||||
|
}, [entries, filterText, visibleCategories, sortDirection]);
|
||||||
|
|
||||||
const hasEntries = entries.length > 0;
|
const hasEntries = entries.length > 0;
|
||||||
const hasResults = filteredEntries.length > 0;
|
const hasResults = filteredEntries.length > 0;
|
||||||
|
|
@ -100,11 +113,13 @@ export function NetworkStreamPanel({
|
||||||
<Title order={4}>{formatTitle(filteredEntries.length)}</Title>
|
<Title order={4}>{formatTitle(filteredEntries.length)}</Title>
|
||||||
<Group gap="xs">
|
<Group gap="xs">
|
||||||
{hasResults && (
|
{hasResults && (
|
||||||
<ListToggle
|
<ListToggle compact={compact} onToggle={onToggleCompact} />
|
||||||
compact={compact}
|
|
||||||
onToggle={() => setCompact((c) => !c)}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
|
<SortToggle
|
||||||
|
value={sortDirection}
|
||||||
|
onChange={onSortChange}
|
||||||
|
aria-label="Network sort direction"
|
||||||
|
/>
|
||||||
<Button variant="default" onClick={onClear} disabled={!hasEntries}>
|
<Button variant="default" onClick={onClear} disabled={!hasEntries}>
|
||||||
Clear
|
Clear
|
||||||
</Button>
|
</Button>
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,8 @@ const meta: Meta<typeof ResourceControls> = {
|
||||||
onSelectTemplate: fn(),
|
onSelectTemplate: fn(),
|
||||||
onUnsubscribeResource: fn(),
|
onUnsubscribeResource: fn(),
|
||||||
listChanged: false,
|
listChanged: false,
|
||||||
|
compact: false,
|
||||||
|
onCompactChange: fn(),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,8 @@ const baseProps = {
|
||||||
onSelectUri: vi.fn(),
|
onSelectUri: vi.fn(),
|
||||||
onSelectTemplate: vi.fn(),
|
onSelectTemplate: vi.fn(),
|
||||||
onUnsubscribeResource: vi.fn(),
|
onUnsubscribeResource: vi.fn(),
|
||||||
|
compact: false,
|
||||||
|
onCompactChange: vi.fn(),
|
||||||
};
|
};
|
||||||
|
|
||||||
describe("ResourceControls", () => {
|
describe("ResourceControls", () => {
|
||||||
|
|
@ -150,23 +152,40 @@ describe("ResourceControls", () => {
|
||||||
expect(screen.getByText("Subscriptions (0)")).toBeInTheDocument();
|
expect(screen.getByText("Subscriptions (0)")).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("toggles all sections via the ListToggle button", async () => {
|
it("seeds the accordion to all-open when compact is false", () => {
|
||||||
|
renderWithMantine(<ResourceControls {...baseProps} compact={false} />);
|
||||||
|
// All three sections open → ListToggle reads "Collapse all".
|
||||||
|
expect(
|
||||||
|
screen.getByRole("button", { name: "Collapse all" }),
|
||||||
|
).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("seeds the accordion to all-closed when compact is true", () => {
|
||||||
|
renderWithMantine(<ResourceControls {...baseProps} compact />);
|
||||||
|
// All three sections closed → ListToggle reads "Expand all".
|
||||||
|
expect(
|
||||||
|
screen.getByRole("button", { name: "Expand all" }),
|
||||||
|
).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("invokes onCompactChange with the new preference when the ListToggle is clicked", async () => {
|
||||||
const user = userEvent.setup();
|
const user = userEvent.setup();
|
||||||
renderWithMantine(<ResourceControls {...baseProps} />);
|
const onCompactChange = vi.fn();
|
||||||
const toggleButtons = screen.getAllByRole("button");
|
renderWithMantine(
|
||||||
// The list toggle is the last in the search row; just exercise it.
|
<ResourceControls
|
||||||
// Since all three sections start open, clicking it should collapse them.
|
{...baseProps}
|
||||||
const toggle = toggleButtons.find(
|
compact={false}
|
||||||
(btn) => btn.querySelector("svg") && btn !== toggleButtons[0],
|
onCompactChange={onCompactChange}
|
||||||
|
/>,
|
||||||
);
|
);
|
||||||
if (toggle) {
|
// All sections start open → ListToggle reads "Collapse all"; clicking
|
||||||
await user.click(toggle);
|
// it collapses everything and records compact=true.
|
||||||
}
|
await user.click(screen.getByRole("button", { name: "Collapse all" }));
|
||||||
// After collapse, click again to re-expand.
|
expect(onCompactChange).toHaveBeenCalledWith(true);
|
||||||
if (toggle) {
|
// Now collapsed → ListToggle reads "Expand all"; clicking re-expands
|
||||||
await user.click(toggle);
|
// and records compact=false.
|
||||||
}
|
await user.click(screen.getByRole("button", { name: "Expand all" }));
|
||||||
expect(screen.getByText("URIs (2)")).toBeInTheDocument();
|
expect(onCompactChange).toHaveBeenLastCalledWith(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("filters by resource title when title is set", async () => {
|
it("filters by resource title when title is set", async () => {
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,13 @@ export interface ResourceControlsProps {
|
||||||
onSelectUri: (uri: string) => void;
|
onSelectUri: (uri: string) => void;
|
||||||
onSelectTemplate: (uriTemplate: string) => void;
|
onSelectTemplate: (uriTemplate: string) => void;
|
||||||
onUnsubscribeResource: (uri: string) => void;
|
onUnsubscribeResource: (uri: string) => void;
|
||||||
|
/**
|
||||||
|
* Persisted preference for the ListToggle. Seeds initial accordion state
|
||||||
|
* on mount; the user can still toggle individual sections during a session
|
||||||
|
* without affecting this value. Only an explicit ListToggle click updates it.
|
||||||
|
*/
|
||||||
|
compact: boolean;
|
||||||
|
onCompactChange: (next: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
function panelMaxHeight(openCount: number): string {
|
function panelMaxHeight(openCount: number): string {
|
||||||
|
|
@ -52,6 +59,8 @@ export function ResourceControls({
|
||||||
onSelectUri,
|
onSelectUri,
|
||||||
onSelectTemplate,
|
onSelectTemplate,
|
||||||
onUnsubscribeResource,
|
onUnsubscribeResource,
|
||||||
|
compact: initialCompact,
|
||||||
|
onCompactChange,
|
||||||
}: ResourceControlsProps) {
|
}: ResourceControlsProps) {
|
||||||
const [searchText, setSearchText] = useState("");
|
const [searchText, setSearchText] = useState("");
|
||||||
const query = searchText.toLowerCase();
|
const query = searchText.toLowerCase();
|
||||||
|
|
@ -74,17 +83,27 @@ export function ResourceControls({
|
||||||
s.resource.uri.toLowerCase().includes(query),
|
s.resource.uri.toLowerCase().includes(query),
|
||||||
);
|
);
|
||||||
|
|
||||||
const defaultOpen = [
|
|
||||||
...(filteredResources.length > 0 ? ["resources"] : []),
|
|
||||||
...(filteredTemplates.length > 0 ? ["templates"] : []),
|
|
||||||
...(filteredSubscriptions.length > 0 ? ["subscriptions"] : []),
|
|
||||||
];
|
|
||||||
|
|
||||||
const allSections = ["resources", "templates", "subscriptions"];
|
const allSections = ["resources", "templates", "subscriptions"];
|
||||||
const [openSections, setOpenSections] = useState<string[]>(defaultOpen);
|
// Seed the accordion from the persisted preference: empty when the user
|
||||||
|
// last left the panel compact, all three open when they explicitly
|
||||||
|
// expanded. Per-section accordion clicks during a session update the
|
||||||
|
// local list but don't change the persisted preference.
|
||||||
|
const [openSections, setOpenSections] = useState<string[]>(
|
||||||
|
initialCompact ? [] : [...allSections],
|
||||||
|
);
|
||||||
const allExpanded = openSections.length === allSections.length;
|
const allExpanded = openSections.length === allSections.length;
|
||||||
const maxHeight = panelMaxHeight(openSections.length);
|
const maxHeight = panelMaxHeight(openSections.length);
|
||||||
|
|
||||||
|
function handleToggleList() {
|
||||||
|
// Compute the next compact value from what the click will produce so a
|
||||||
|
// half-open accordion (user toggled a single section) still persists the
|
||||||
|
// right preference: clicking "expand all" should record `compact=false`
|
||||||
|
// even if the visible state was already partially expanded.
|
||||||
|
const nextCompact = allExpanded;
|
||||||
|
setOpenSections(nextCompact ? [] : [...allSections]);
|
||||||
|
onCompactChange(nextCompact);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack gap="sm">
|
<Stack gap="sm">
|
||||||
<Group justify="space-between">
|
<Group justify="space-between">
|
||||||
|
|
@ -98,10 +117,7 @@ export function ResourceControls({
|
||||||
value={searchText}
|
value={searchText}
|
||||||
onChange={(e) => setSearchText(e.currentTarget.value)}
|
onChange={(e) => setSearchText(e.currentTarget.value)}
|
||||||
/>
|
/>
|
||||||
<ListToggle
|
<ListToggle compact={!allExpanded} onToggle={handleToggleList} />
|
||||||
compact={!allExpanded}
|
|
||||||
onToggle={() => setOpenSections(allExpanded ? [] : [...allSections])}
|
|
||||||
/>
|
|
||||||
</Group>
|
</Group>
|
||||||
<Accordion multiple value={openSections} onChange={setOpenSections}>
|
<Accordion multiple value={openSections} onChange={setOpenSections}>
|
||||||
<Accordion.Item value="resources">
|
<Accordion.Item value="resources">
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,10 @@ const meta: Meta<typeof HistoryScreen> = {
|
||||||
onExport: fn(),
|
onExport: fn(),
|
||||||
onReplay: fn(),
|
onReplay: fn(),
|
||||||
onTogglePin: fn(),
|
onTogglePin: fn(),
|
||||||
|
sortDirection: "newest-first",
|
||||||
|
onSortChange: fn(),
|
||||||
|
compact: true,
|
||||||
|
onToggleCompact: fn(),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,10 @@ const baseProps = {
|
||||||
onExport: vi.fn(),
|
onExport: vi.fn(),
|
||||||
onReplay: vi.fn(),
|
onReplay: vi.fn(),
|
||||||
onTogglePin: vi.fn(),
|
onTogglePin: vi.fn(),
|
||||||
|
sortDirection: "newest-first" as const,
|
||||||
|
onSortChange: vi.fn(),
|
||||||
|
compact: true,
|
||||||
|
onToggleCompact: vi.fn(),
|
||||||
};
|
};
|
||||||
|
|
||||||
describe("HistoryScreen", () => {
|
describe("HistoryScreen", () => {
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import type { MessageEntry, MessageMethod } from "@inspector/core/mcp/types.js";
|
||||||
import { HistoryControls } from "../../groups/HistoryControls/HistoryControls";
|
import { HistoryControls } from "../../groups/HistoryControls/HistoryControls";
|
||||||
import { HistoryListPanel } from "../../groups/HistoryListPanel/HistoryListPanel.js";
|
import { HistoryListPanel } from "../../groups/HistoryListPanel/HistoryListPanel.js";
|
||||||
import { extractMethod } from "../../groups/historyUtils.js";
|
import { extractMethod } from "../../groups/historyUtils.js";
|
||||||
|
import type { SortDirection } from "../../elements/SortToggle/SortToggle";
|
||||||
|
|
||||||
export interface HistoryScreenProps {
|
export interface HistoryScreenProps {
|
||||||
entries: MessageEntry[];
|
entries: MessageEntry[];
|
||||||
|
|
@ -12,6 +13,10 @@ export interface HistoryScreenProps {
|
||||||
onExport: () => void;
|
onExport: () => void;
|
||||||
onReplay: (id: string) => void;
|
onReplay: (id: string) => void;
|
||||||
onTogglePin: (id: string) => void;
|
onTogglePin: (id: string) => void;
|
||||||
|
sortDirection: SortDirection;
|
||||||
|
onSortChange: (next: SortDirection) => void;
|
||||||
|
compact: boolean;
|
||||||
|
onToggleCompact: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ScreenLayout = Flex.withProps({
|
const ScreenLayout = Flex.withProps({
|
||||||
|
|
@ -38,6 +43,10 @@ export function HistoryScreen({
|
||||||
onExport,
|
onExport,
|
||||||
onReplay,
|
onReplay,
|
||||||
onTogglePin,
|
onTogglePin,
|
||||||
|
sortDirection,
|
||||||
|
onSortChange,
|
||||||
|
compact,
|
||||||
|
onToggleCompact,
|
||||||
}: HistoryScreenProps) {
|
}: HistoryScreenProps) {
|
||||||
const [searchText, setSearchText] = useState("");
|
const [searchText, setSearchText] = useState("");
|
||||||
const [methodFilter, setMethodFilter] = useState<MessageMethod | undefined>();
|
const [methodFilter, setMethodFilter] = useState<MessageMethod | undefined>();
|
||||||
|
|
@ -74,6 +83,10 @@ export function HistoryScreen({
|
||||||
onExport={onExport}
|
onExport={onExport}
|
||||||
onReplay={onReplay}
|
onReplay={onReplay}
|
||||||
onTogglePin={onTogglePin}
|
onTogglePin={onTogglePin}
|
||||||
|
sortDirection={sortDirection}
|
||||||
|
onSortChange={onSortChange}
|
||||||
|
compact={compact}
|
||||||
|
onToggleCompact={onToggleCompact}
|
||||||
/>
|
/>
|
||||||
</ScreenLayout>
|
</ScreenLayout>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -13,8 +13,8 @@ const meta: Meta<typeof LoggingScreen> = {
|
||||||
onSetLevel: fn(),
|
onSetLevel: fn(),
|
||||||
onClear: fn(),
|
onClear: fn(),
|
||||||
onExport: fn(),
|
onExport: fn(),
|
||||||
autoScroll: true,
|
sortDirection: "newest-first",
|
||||||
onToggleAutoScroll: fn(),
|
onSortChange: fn(),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,8 +9,8 @@ const baseProps = {
|
||||||
onSetLevel: vi.fn(),
|
onSetLevel: vi.fn(),
|
||||||
onClear: vi.fn(),
|
onClear: vi.fn(),
|
||||||
onExport: vi.fn(),
|
onExport: vi.fn(),
|
||||||
autoScroll: true,
|
sortDirection: "newest-first" as const,
|
||||||
onToggleAutoScroll: vi.fn(),
|
onSortChange: vi.fn(),
|
||||||
};
|
};
|
||||||
|
|
||||||
describe("LoggingScreen", () => {
|
describe("LoggingScreen", () => {
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue