feat(sort): add SortToggle for Logs, History, and Network (#1371) (#1372)

* 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:
Cliff Hall 2026-05-29 14:47:19 -04:00 • committed by GitHub
commit dd0f5de820
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 812 additions and 117 deletions

View file

@ -13,6 +13,10 @@ const meta: Meta<typeof HistoryScreen> = {
onExport: fn(),
onReplay: fn(),
onTogglePin: fn(),
sortDirection: "newest-first",
onSortChange: fn(),
compact: true,
onToggleCompact: fn(),
},
};

View file

@ -26,6 +26,10 @@ const baseProps = {
onExport: vi.fn(),
onReplay: vi.fn(),
onTogglePin: vi.fn(),
sortDirection: "newest-first" as const,
onSortChange: vi.fn(),
compact: true,
onToggleCompact: vi.fn(),
};
describe("HistoryScreen", () => {

View file

@ -4,6 +4,7 @@ import type { MessageEntry, MessageMethod } from "@inspector/core/mcp/types.js";
import { HistoryControls } from "../../groups/HistoryControls/HistoryControls";
import { HistoryListPanel } from "../../groups/HistoryListPanel/HistoryListPanel.js";
import { extractMethod } from "../../groups/historyUtils.js";
import type { SortDirection } from "../../elements/SortToggle/SortToggle";
export interface HistoryScreenProps {
entries: MessageEntry[];
@ -12,6 +13,10 @@ export interface HistoryScreenProps {
onExport: () => void;
onReplay: (id: string) => void;
onTogglePin: (id: string) => void;
sortDirection: SortDirection;
onSortChange: (next: SortDirection) => void;
compact: boolean;
onToggleCompact: () => void;
}
const ScreenLayout = Flex.withProps({
@ -38,6 +43,10 @@ export function HistoryScreen({
onExport,
onReplay,
onTogglePin,
sortDirection,
onSortChange,
compact,
onToggleCompact,
}: HistoryScreenProps) {
const [searchText, setSearchText] = useState("");
const [methodFilter, setMethodFilter] = useState<MessageMethod | undefined>();
@ -74,6 +83,10 @@ export function HistoryScreen({
onExport={onExport}
onReplay={onReplay}
onTogglePin={onTogglePin}
sortDirection={sortDirection}
onSortChange={onSortChange}
compact={compact}
onToggleCompact={onToggleCompact}
/>
</ScreenLayout>
);

View file

@ -13,8 +13,8 @@ const meta: Meta<typeof LoggingScreen> = {
onSetLevel: fn(),
onClear: fn(),
onExport: fn(),
autoScroll: true,
onToggleAutoScroll: fn(),
sortDirection: "newest-first",
onSortChange: fn(),
},
};

View file

@ -9,8 +9,8 @@ const baseProps = {
onSetLevel: vi.fn(),
onClear: vi.fn(),
onExport: vi.fn(),
autoScroll: true,
onToggleAutoScroll: vi.fn(),
sortDirection: "newest-first" as const,
onSortChange: vi.fn(),
};
describe("LoggingScreen", () => {

View file

@ -4,6 +4,7 @@ import type { LoggingLevel } from "@modelcontextprotocol/sdk/types.js";
import { LogControls } from "../../groups/LogControls/LogControls";
import { LogStreamPanel } from "../../groups/LogStreamPanel/LogStreamPanel";
import type { LogEntryData } from "../../elements/LogEntry/LogEntry";
import type { SortDirection } from "../../elements/SortToggle/SortToggle";
export interface LoggingScreenProps {
entries: LogEntryData[];
@ -11,8 +12,8 @@ export interface LoggingScreenProps {
onSetLevel: (level: LoggingLevel) => void;
onClear: () => void;
onExport: () => void;
autoScroll: boolean;
onToggleAutoScroll: () => void;
sortDirection: SortDirection;
onSortChange: (next: SortDirection) => void;
}
const ALL_LEVELS_VISIBLE: Record<LoggingLevel, boolean> = {
@ -60,8 +61,8 @@ export function LoggingScreen({
onSetLevel,
onClear,
onExport,
autoScroll,
onToggleAutoScroll,
sortDirection,
onSortChange,
}: LoggingScreenProps) {
const [filterText, setFilterText] = useState("");
const [visibleLevels, setVisibleLevels] =
@ -95,10 +96,10 @@ export function LoggingScreen({
entries={entries}
filterText={filterText}
visibleLevels={visibleLevels}
autoScroll={autoScroll}
onToggleAutoScroll={onToggleAutoScroll}
onClear={onClear}
onExport={onExport}
sortDirection={sortDirection}
onSortChange={onSortChange}
/>
</ScreenLayout>
);

View file

@ -10,6 +10,10 @@ const meta: Meta<typeof NetworkScreen> = {
args: {
onClear: fn(),
onExport: fn(),
sortDirection: "newest-first",
onSortChange: fn(),
compact: true,
onToggleCompact: fn(),
},
};

View file

@ -43,6 +43,10 @@ const baseProps = {
entries: [transportEntry, authEntry, errorEntry],
onClear: vi.fn(),
onExport: vi.fn(),
sortDirection: "newest-first" as const,
onSortChange: vi.fn(),
compact: true,
onToggleCompact: vi.fn(),
};
describe("NetworkScreen", () => {

View file

@ -6,11 +6,16 @@ import type {
} from "@inspector/core/mcp/types.js";
import { NetworkControls } from "../../groups/NetworkControls/NetworkControls";
import { NetworkStreamPanel } from "../../groups/NetworkStreamPanel/NetworkStreamPanel";
import type { SortDirection } from "../../elements/SortToggle/SortToggle";
export interface NetworkScreenProps {
entries: FetchRequestEntry[];
onClear: () => void;
onExport: () => void;
sortDirection: SortDirection;
onSortChange: (next: SortDirection) => void;
compact: boolean;
onToggleCompact: () => void;
}
const ALL_CATEGORIES_VISIBLE: Record<FetchRequestCategory, boolean> = {
@ -44,6 +49,10 @@ export function NetworkScreen({
entries,
onClear,
onExport,
sortDirection,
onSortChange,
compact,
onToggleCompact,
}: NetworkScreenProps) {
const [filterText, setFilterText] = useState("");
const [visibleCategories, setVisibleCategories] = useState<
@ -83,6 +92,10 @@ export function NetworkScreen({
visibleCategories={visibleCategories}
onClear={onClear}
onExport={onExport}
sortDirection={sortDirection}
onSortChange={onSortChange}
compact={compact}
onToggleCompact={onToggleCompact}
/>
</ScreenLayout>
);

View file

@ -20,6 +20,8 @@ const meta: Meta<typeof ResourcesScreen> = {
listChanged: false,
subscriptions: [],
templates: [],
compact: false,
onCompactChange: fn(),
},
};

View file

@ -26,6 +26,8 @@ const baseProps = {
onReadResource: vi.fn(),
onSubscribeResource: vi.fn(),
onUnsubscribeResource: vi.fn(),
compact: false,
onCompactChange: vi.fn(),
};
const okResult: ReadResourceResult = {

View file

@ -47,6 +47,8 @@ export interface ResourcesScreenProps {
argumentValue: string,
context: Record<string, string>,
) => Promise<string[]>;
compact: boolean;
onCompactChange: (next: boolean) => void;
}
const ScreenLayout = Flex.withProps({
@ -112,6 +114,8 @@ export function ResourcesScreen({
onSubscribeResource,
onUnsubscribeResource,
onCompleteArgument,
compact,
onCompactChange,
}: ResourcesScreenProps) {
const [selectedResourceUri, setSelectedResourceUri] = useState<
string | undefined
@ -252,6 +256,8 @@ export function ResourcesScreen({
onSelectUri={handleSelectResource}
onSelectTemplate={handleSelectTemplate}
onUnsubscribeResource={onUnsubscribeResource}
compact={compact}
onCompactChange={onCompactChange}
/>
</SidebarCard>
</Sidebar>

View file

@ -18,6 +18,8 @@ const meta: Meta<typeof ServerListScreen> = {
onEdit: fn(),
onClone: fn(),
onRemove: fn(),
compact: false,
onToggleCompact: fn(),
},
};

View file

@ -25,6 +25,8 @@ const baseProps = {
onEdit: vi.fn(),
onClone: vi.fn(),
onRemove: vi.fn(),
compact: false,
onToggleCompact: vi.fn(),
};
describe("ServerListScreen", () => {

View file

@ -1,4 +1,3 @@
import { useState } from "react";
import { ScrollArea, SimpleGrid, Stack, Text } from "@mantine/core";
import type { ServerEntry } from "@inspector/core/mcp/types.js";
import { ServerCard } from "../../groups/ServerCard/ServerCard";
@ -19,6 +18,8 @@ export interface ServerListScreenProps {
onEdit: (id: string) => void;
onClone: (id: string) => void;
onRemove: (id: string) => void;
compact: boolean;
onToggleCompact: () => void;
}
const PageContainer = Stack.withProps({
@ -44,19 +45,15 @@ export function ServerListScreen({
onEdit,
onClone,
onRemove,
compact,
onToggleCompact,
}: ServerListScreenProps) {
const [compact, setCompact] = useState<boolean>(false);
function handleToggleList() {
setCompact((prev) => !prev);
}
return (
<PageContainer>
<ServerListControls
serverCount={servers.length}
compact={compact}
onToggleList={handleToggleList}
onToggleList={onToggleCompact}
onAddManually={onAddManually}
onImportConfig={onImportConfig}
onImportServerJson={onImportServerJson}