Add ListToggle and built-in search filtering to ResourceControls, move Export/Clear buttons to HistoryListPanel

ResourceControls now manages its own search state internally and filters
all three accordion sections (URIs, Templates, Subscriptions) by name
and URI. Added ListToggle to expand/collapse all sections. Moved Export
JSON and Clear All buttons from HistoryControls sidebar to the
HistoryListPanel header for better proximity to the data they act on.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
cliffhall 2026-04-01 19:24:11 -04:00
commit a68933464b
11 changed files with 80 additions and 69 deletions

View file

@ -15,8 +15,6 @@ export const Default: Story = {
searchText: "",
onSearchChange: fn(),
onMethodFilterChange: fn(),
onClearAll: fn(),
onExport: fn(),
},
};
@ -26,7 +24,5 @@ export const WithFilters: Story = {
methodFilter: "tools/call",
onSearchChange: fn(),
onMethodFilterChange: fn(),
onClearAll: fn(),
onExport: fn(),
},
};

View file

@ -1,4 +1,4 @@
import { Button, Group, Select, Stack, TextInput, Title } from "@mantine/core";
import { Select, Stack, TextInput, Title } from "@mantine/core";
const METHOD_OPTIONS = [
"tools/call",
@ -11,18 +11,11 @@ const METHOD_OPTIONS = [
"elicitation/create",
];
const ToolbarButton = Button.withProps({
variant: "light",
size: "sm",
});
export interface HistoryControlsProps {
searchText: string;
methodFilter?: string;
onSearchChange: (text: string) => void;
onMethodFilterChange: (method: string) => void;
onClearAll: () => void;
onExport: () => void;
}
export function HistoryControls({
@ -30,14 +23,10 @@ export function HistoryControls({
methodFilter,
onSearchChange,
onMethodFilterChange,
onClearAll,
onExport,
}: HistoryControlsProps) {
return (
<Stack gap="md">
<Title order={4}>History</Title>
<Title order={5}>Search</Title>
<TextInput
placeholder="Search..."
value={searchText}
@ -52,11 +41,6 @@ export function HistoryControls({
onChange={(value) => onMethodFilterChange(value ?? "")}
clearable
/>
<Group>
<ToolbarButton onClick={onExport}>Export JSON</ToolbarButton>
<ToolbarButton onClick={onClearAll}>Clear All</ToolbarButton>
</Group>
</Stack>
);
}

View file

@ -8,6 +8,8 @@ const meta: Meta<typeof HistoryListPanel> = {
component: HistoryListPanel,
args: {
searchText: "",
onClearAll: fn(),
onExport: fn(),
},
};

View file

@ -1,5 +1,13 @@
import { useMemo, useState } from "react";
import { Group, Paper, ScrollArea, Stack, Text, Title } from "@mantine/core";
import {
Button,
Group,
Paper,
ScrollArea,
Stack,
Text,
Title,
} from "@mantine/core";
import { HistoryEntry } from "../HistoryEntry/HistoryEntry";
import { ListToggle } from "../../elements/ListToggle/ListToggle";
import type { HistoryEntryProps } from "../HistoryEntry/HistoryEntry";
@ -9,8 +17,15 @@ export interface HistoryRequestsPanelProps {
pinnedEntries: HistoryEntryProps[];
searchText: string;
methodFilter?: string;
onClearAll: () => void;
onExport: () => void;
}
const ToolbarButton = Button.withProps({
variant: "light",
size: "sm",
});
const PanelContainer = Paper.withProps({
withBorder: true,
p: "lg",
@ -56,6 +71,8 @@ export function HistoryListPanel({
pinnedEntries,
searchText,
methodFilter,
onClearAll,
onExport,
}: HistoryRequestsPanelProps) {
const [compact, setCompact] = useState(false);
@ -76,12 +93,16 @@ export function HistoryListPanel({
<PanelContainer>
<Group justify="space-between" mb="sm">
<Title order={4}>Requests</Title>
{hasResults && (
<ListToggle
compact={compact}
onToggle={() => setCompact((c) => !c)}
/>
)}
<Group gap="xs">
<ToolbarButton onClick={onExport}>Export JSON</ToolbarButton>
<ToolbarButton onClick={onClearAll}>Clear All</ToolbarButton>
{hasResults && (
<ListToggle
compact={compact}
onToggle={() => setCompact((c) => !c)}
/>
)}
</Group>
</Group>
{!hasResults ? (

View file

@ -59,6 +59,12 @@ export function LogControls({
<Stack gap="md">
<Title order={4}>Logging</Title>
<TextInput
placeholder="Search..."
value={filterText}
onChange={(e) => onFilterChange(e.currentTarget.value)}
/>
<Title order={5}>Active Level</Title>
<Group wrap="nowrap">
<Select
@ -74,13 +80,6 @@ export function LogControls({
</Button>
</Group>
<Title order={5}>Search</Title>
<TextInput
placeholder="Search logs..."
value={filterText}
onChange={(e) => onFilterChange(e.currentTarget.value)}
/>
<Group justify="space-between">
<Title order={5}>Filter by Level</Title>
<SubtleButton onClick={onToggleAllLevels}>

View file

@ -6,8 +6,6 @@ const meta: Meta<typeof ResourceControls> = {
title: "Groups/ResourceControls",
component: ResourceControls,
args: {
searchText: "",
onSearchChange: fn(),
onRefreshList: fn(),
onSelectUri: fn(),
onSelectTemplate: fn(),
@ -59,7 +57,6 @@ export const Default: Story = {
export const WithSearch: Story = {
args: {
...Default.args,
searchText: "config",
},
};

View file

@ -8,6 +8,7 @@ import {
Title,
} from "@mantine/core";
import { ListChangedIndicator } from "../../elements/ListChangedIndicator/ListChangedIndicator";
import { ListToggle } from "../../elements/ListToggle/ListToggle";
import { ResourceListItem } from "../ResourceListItem/ResourceListItem";
import { ResourceSubscribedItem } from "../ResourceSubscribedItem/ResourceSubscribedItem";
import type {
@ -21,8 +22,6 @@ export interface ResourceControlsProps {
templates: TemplateListItem[];
subscriptions: SubscriptionItem[];
listChanged: boolean;
searchText: string;
onSearchChange: (text: string) => void;
onRefreshList: () => void;
onSelectUri: (uri: string) => void;
onSelectTemplate: (uriTemplate: string) => void;
@ -53,24 +52,38 @@ export function ResourceControls({
templates,
subscriptions,
listChanged,
searchText,
onSearchChange,
onRefreshList,
onSelectUri,
onSelectTemplate,
onUnsubscribeResource,
}: ResourceControlsProps) {
const filteredResources = resources.filter((r) =>
r.name.toLowerCase().includes(searchText.toLowerCase()),
const [searchText, setSearchText] = useState("");
const query = searchText.toLowerCase();
const filteredResources = resources.filter(
(r) =>
r.name.toLowerCase().includes(query) ||
r.uri.toLowerCase().includes(query),
);
const filteredTemplates = templates.filter(
(t) =>
templateDisplayName(t).toLowerCase().includes(query) ||
t.uriTemplate.toLowerCase().includes(query),
);
const filteredSubscriptions = subscriptions.filter(
(s) =>
s.name.toLowerCase().includes(query) ||
s.uri.toLowerCase().includes(query),
);
const defaultOpen = [
...(filteredResources.length > 0 ? ["resources"] : []),
...(templates.length > 0 ? ["templates"] : []),
...(subscriptions.length > 0 ? ["subscriptions"] : []),
...(filteredTemplates.length > 0 ? ["templates"] : []),
...(filteredSubscriptions.length > 0 ? ["subscriptions"] : []),
];
const allSections = ["resources", "templates", "subscriptions"];
const [openSections, setOpenSections] = useState<string[]>(defaultOpen);
const allExpanded = openSections.length === allSections.length;
const maxHeight = panelMaxHeight(openSections.length);
return (
@ -79,11 +92,20 @@ export function ResourceControls({
<Title order={4}>Resources</Title>
<ListChangedIndicator visible={listChanged} onRefresh={onRefreshList} />
</Group>
<TextInput
placeholder="Search..."
value={searchText}
onChange={(e) => onSearchChange(e.currentTarget.value)}
/>
<Group gap="xs" wrap="nowrap">
<ListToggle
compact={!allExpanded}
onToggle={() =>
setOpenSections(allExpanded ? [] : [...allSections])
}
/>
<TextInput
flex={1}
placeholder="Search..."
value={searchText}
onChange={(e) => setSearchText(e.currentTarget.value)}
/>
</Group>
<Accordion multiple value={openSections} onChange={setOpenSections}>
<Accordion.Item value="resources">
<Accordion.Control disabled={filteredResources.length === 0}>
@ -108,13 +130,13 @@ export function ResourceControls({
</Accordion.Item>
<Accordion.Item value="templates">
<Accordion.Control disabled={templates.length === 0}>
{formatSectionCount("Templates", templates.length)}
<Accordion.Control disabled={filteredTemplates.length === 0}>
{formatSectionCount("Templates", filteredTemplates.length)}
</Accordion.Control>
<Accordion.Panel>
<ScrollArea.Autosize mah={maxHeight}>
<Stack gap="xs">
{templates.map((template) => (
{filteredTemplates.map((template) => (
<ResourceListItem
key={template.uriTemplate}
name={templateDisplayName(template)}
@ -129,13 +151,13 @@ export function ResourceControls({
</Accordion.Item>
<Accordion.Item value="subscriptions">
<Accordion.Control disabled={subscriptions.length === 0}>
{formatSectionCount("Subscriptions", subscriptions.length)}
<Accordion.Control disabled={filteredSubscriptions.length === 0}>
{formatSectionCount("Subscriptions", filteredSubscriptions.length)}
</Accordion.Control>
<Accordion.Panel>
<ScrollArea.Autosize mah={maxHeight}>
<Stack gap="xs">
{subscriptions.map((sub) => (
{filteredSubscriptions.map((sub) => (
<ResourceSubscribedItem
key={sub.name}
name={sub.name}

View file

@ -48,8 +48,6 @@ export function HistoryScreen({
onMethodFilterChange={(value) =>
setMethodFilter(value || undefined)
}
onClearAll={onClearAll}
onExport={onExport}
/>
</SidebarCard>
</Sidebar>
@ -58,6 +56,8 @@ export function HistoryScreen({
pinnedEntries={pinnedEntries}
searchText={searchText}
methodFilter={methodFilter}
onClearAll={onClearAll}
onExport={onExport}
/>
</ScreenLayout>
);

View file

@ -13,8 +13,6 @@ const meta: Meta<typeof ResourcesScreen> = {
component: ResourcesScreen,
parameters: { layout: "fullscreen" },
args: {
searchText: "",
onSearchChange: fn(),
onRefreshList: fn(),
onSelectUri: fn(),
onSelectTemplate: fn(),

View file

@ -47,8 +47,6 @@ export interface ResourcesScreenProps {
selectedResource?: SelectedResource;
selectedTemplate?: SelectedTemplate;
listChanged: boolean;
searchText: string;
onSearchChange: (text: string) => void;
onRefreshList: () => void;
onSelectUri: (uri: string) => void;
onSelectTemplate: (uriTemplate: string) => void;
@ -92,8 +90,6 @@ export function ResourcesScreen({
selectedResource,
selectedTemplate,
listChanged,
searchText,
onSearchChange,
onRefreshList,
onSelectUri,
onSelectTemplate,
@ -110,8 +106,6 @@ export function ResourcesScreen({
templates={templates}
subscriptions={subscriptions}
listChanged={listChanged}
searchText={searchText}
onSearchChange={onSearchChange}
onRefreshList={onRefreshList}
onSelectUri={onSelectUri}
onSelectTemplate={onSelectTemplate}

View file

@ -447,8 +447,6 @@ export const ResourcesActive: Story = {
isSubscribed: false,
}}
listChanged={true}
searchText=""
onSearchChange={fn()}
onRefreshList={fn()}
onSelectUri={fn()}
onSelectTemplate={fn()}