* 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
3 changed files with 812 additions and 117 deletions
|
|
@ -19,10 +19,10 @@ const meta: Meta<typeof LogStreamPanel> = {
|
|||
alert: true,
|
||||
emergency: true,
|
||||
},
|
||||
autoScroll: true,
|
||||
onToggleAutoScroll: fn(),
|
||||
onClear: fn(),
|
||||
onExport: fn(),
|
||||
sortDirection: "newest-first",
|
||||
onSortChange: fn(),
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -43,10 +43,10 @@ const baseProps = {
|
|||
entries,
|
||||
filterText: "",
|
||||
visibleLevels: allVisible,
|
||||
autoScroll: true,
|
||||
onToggleAutoScroll: vi.fn(),
|
||||
onClear: vi.fn(),
|
||||
onExport: vi.fn(),
|
||||
sortDirection: "newest-first" as const,
|
||||
onSortChange: vi.fn(),
|
||||
};
|
||||
|
||||
describe("LogStreamPanel", () => {
|
||||
|
|
@ -149,18 +149,39 @@ describe("LogStreamPanel", () => {
|
|||
expect(onExport).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("invokes onToggleAutoScroll when the auto-scroll checkbox is clicked", async () => {
|
||||
const user = userEvent.setup();
|
||||
const onToggleAutoScroll = vi.fn();
|
||||
renderWithMantine(
|
||||
<LogStreamPanel {...baseProps} onToggleAutoScroll={onToggleAutoScroll} />,
|
||||
it("renders entries newest-first by default", () => {
|
||||
renderWithMantine(<LogStreamPanel {...baseProps} />);
|
||||
const items = screen.getAllByText(
|
||||
/Server started|Failed to read|Loading config|deprecated/,
|
||||
);
|
||||
await user.click(screen.getByLabelText("Auto-scroll"));
|
||||
expect(onToggleAutoScroll).toHaveBeenCalledTimes(1);
|
||||
// Newest receivedAt is the warning entry; oldest is "Server started".
|
||||
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", () => {
|
||||
renderWithMantine(<LogStreamPanel {...baseProps} autoScroll={false} />);
|
||||
expect(screen.getByLabelText("Auto-scroll")).not.toBeChecked();
|
||||
it("reorders entries when sortDirection is oldest-first", () => {
|
||||
renderWithMantine(
|
||||
<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 {
|
||||
Button,
|
||||
Checkbox,
|
||||
Group,
|
||||
Paper,
|
||||
ScrollArea,
|
||||
|
|
@ -12,15 +11,19 @@ import {
|
|||
import type { LoggingLevel } from "@modelcontextprotocol/sdk/types.js";
|
||||
import { LogEntry } from "../../elements/LogEntry/LogEntry";
|
||||
import type { LogEntryData } from "../../elements/LogEntry/LogEntry";
|
||||
import {
|
||||
SortToggle,
|
||||
type SortDirection,
|
||||
} from "../../elements/SortToggle/SortToggle";
|
||||
|
||||
export interface LogStreamPanelProps {
|
||||
entries: LogEntryData[];
|
||||
filterText: string;
|
||||
visibleLevels: Record<LoggingLevel, boolean>;
|
||||
autoScroll: boolean;
|
||||
onToggleAutoScroll: () => void;
|
||||
onClear: () => void;
|
||||
onExport: () => void;
|
||||
sortDirection: SortDirection;
|
||||
onSortChange: (next: SortDirection) => void;
|
||||
}
|
||||
|
||||
const PanelContainer = Paper.withProps({
|
||||
|
|
@ -61,25 +64,29 @@ export function LogStreamPanel({
|
|||
entries,
|
||||
filterText,
|
||||
visibleLevels,
|
||||
autoScroll,
|
||||
onToggleAutoScroll,
|
||||
onClear,
|
||||
onExport,
|
||||
sortDirection,
|
||||
onSortChange,
|
||||
}: LogStreamPanelProps) {
|
||||
const filteredEntries = useMemo(
|
||||
() => entries.filter((e) => matchesFilters(e, filterText, visibleLevels)),
|
||||
[entries, filterText, visibleLevels],
|
||||
);
|
||||
const filteredEntries = useMemo(() => {
|
||||
// `.filter()` returns a fresh array, so sorting in-place is safe.
|
||||
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 (
|
||||
<PanelContainer>
|
||||
<Group justify="space-between" mb="sm">
|
||||
<Title order={4}>Log Stream</Title>
|
||||
<Group>
|
||||
<Checkbox
|
||||
label="Auto-scroll"
|
||||
checked={autoScroll}
|
||||
onChange={onToggleAutoScroll}
|
||||
<SortToggle
|
||||
value={sortDirection}
|
||||
onChange={onSortChange}
|
||||
aria-label="Logs sort direction"
|
||||
/>
|
||||
<Button
|
||||
variant="default"
|
||||
|
|
|
|||
Loading…
Reference in a new issue