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:
parent
5594e940c9
commit
a68933464b
11 changed files with 80 additions and 69 deletions
|
|
@ -15,8 +15,6 @@ export const Default: Story = {
|
||||||
searchText: "",
|
searchText: "",
|
||||||
onSearchChange: fn(),
|
onSearchChange: fn(),
|
||||||
onMethodFilterChange: fn(),
|
onMethodFilterChange: fn(),
|
||||||
onClearAll: fn(),
|
|
||||||
onExport: fn(),
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -26,7 +24,5 @@ export const WithFilters: Story = {
|
||||||
methodFilter: "tools/call",
|
methodFilter: "tools/call",
|
||||||
onSearchChange: fn(),
|
onSearchChange: fn(),
|
||||||
onMethodFilterChange: fn(),
|
onMethodFilterChange: fn(),
|
||||||
onClearAll: fn(),
|
|
||||||
onExport: fn(),
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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 = [
|
const METHOD_OPTIONS = [
|
||||||
"tools/call",
|
"tools/call",
|
||||||
|
|
@ -11,18 +11,11 @@ const METHOD_OPTIONS = [
|
||||||
"elicitation/create",
|
"elicitation/create",
|
||||||
];
|
];
|
||||||
|
|
||||||
const ToolbarButton = Button.withProps({
|
|
||||||
variant: "light",
|
|
||||||
size: "sm",
|
|
||||||
});
|
|
||||||
|
|
||||||
export interface HistoryControlsProps {
|
export interface HistoryControlsProps {
|
||||||
searchText: string;
|
searchText: string;
|
||||||
methodFilter?: string;
|
methodFilter?: string;
|
||||||
onSearchChange: (text: string) => void;
|
onSearchChange: (text: string) => void;
|
||||||
onMethodFilterChange: (method: string) => void;
|
onMethodFilterChange: (method: string) => void;
|
||||||
onClearAll: () => void;
|
|
||||||
onExport: () => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function HistoryControls({
|
export function HistoryControls({
|
||||||
|
|
@ -30,14 +23,10 @@ export function HistoryControls({
|
||||||
methodFilter,
|
methodFilter,
|
||||||
onSearchChange,
|
onSearchChange,
|
||||||
onMethodFilterChange,
|
onMethodFilterChange,
|
||||||
onClearAll,
|
|
||||||
onExport,
|
|
||||||
}: HistoryControlsProps) {
|
}: HistoryControlsProps) {
|
||||||
return (
|
return (
|
||||||
<Stack gap="md">
|
<Stack gap="md">
|
||||||
<Title order={4}>History</Title>
|
<Title order={4}>History</Title>
|
||||||
|
|
||||||
<Title order={5}>Search</Title>
|
|
||||||
<TextInput
|
<TextInput
|
||||||
placeholder="Search..."
|
placeholder="Search..."
|
||||||
value={searchText}
|
value={searchText}
|
||||||
|
|
@ -52,11 +41,6 @@ export function HistoryControls({
|
||||||
onChange={(value) => onMethodFilterChange(value ?? "")}
|
onChange={(value) => onMethodFilterChange(value ?? "")}
|
||||||
clearable
|
clearable
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Group>
|
|
||||||
<ToolbarButton onClick={onExport}>Export JSON</ToolbarButton>
|
|
||||||
<ToolbarButton onClick={onClearAll}>Clear All</ToolbarButton>
|
|
||||||
</Group>
|
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,8 @@ const meta: Meta<typeof HistoryListPanel> = {
|
||||||
component: HistoryListPanel,
|
component: HistoryListPanel,
|
||||||
args: {
|
args: {
|
||||||
searchText: "",
|
searchText: "",
|
||||||
|
onClearAll: fn(),
|
||||||
|
onExport: fn(),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,13 @@
|
||||||
import { useMemo, useState } from "react";
|
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 { HistoryEntry } from "../HistoryEntry/HistoryEntry";
|
||||||
import { ListToggle } from "../../elements/ListToggle/ListToggle";
|
import { ListToggle } from "../../elements/ListToggle/ListToggle";
|
||||||
import type { HistoryEntryProps } from "../HistoryEntry/HistoryEntry";
|
import type { HistoryEntryProps } from "../HistoryEntry/HistoryEntry";
|
||||||
|
|
@ -9,8 +17,15 @@ export interface HistoryRequestsPanelProps {
|
||||||
pinnedEntries: HistoryEntryProps[];
|
pinnedEntries: HistoryEntryProps[];
|
||||||
searchText: string;
|
searchText: string;
|
||||||
methodFilter?: string;
|
methodFilter?: string;
|
||||||
|
onClearAll: () => void;
|
||||||
|
onExport: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const ToolbarButton = Button.withProps({
|
||||||
|
variant: "light",
|
||||||
|
size: "sm",
|
||||||
|
});
|
||||||
|
|
||||||
const PanelContainer = Paper.withProps({
|
const PanelContainer = Paper.withProps({
|
||||||
withBorder: true,
|
withBorder: true,
|
||||||
p: "lg",
|
p: "lg",
|
||||||
|
|
@ -56,6 +71,8 @@ export function HistoryListPanel({
|
||||||
pinnedEntries,
|
pinnedEntries,
|
||||||
searchText,
|
searchText,
|
||||||
methodFilter,
|
methodFilter,
|
||||||
|
onClearAll,
|
||||||
|
onExport,
|
||||||
}: HistoryRequestsPanelProps) {
|
}: HistoryRequestsPanelProps) {
|
||||||
const [compact, setCompact] = useState(false);
|
const [compact, setCompact] = useState(false);
|
||||||
|
|
||||||
|
|
@ -76,12 +93,16 @@ export function HistoryListPanel({
|
||||||
<PanelContainer>
|
<PanelContainer>
|
||||||
<Group justify="space-between" mb="sm">
|
<Group justify="space-between" mb="sm">
|
||||||
<Title order={4}>Requests</Title>
|
<Title order={4}>Requests</Title>
|
||||||
{hasResults && (
|
<Group gap="xs">
|
||||||
<ListToggle
|
<ToolbarButton onClick={onExport}>Export JSON</ToolbarButton>
|
||||||
compact={compact}
|
<ToolbarButton onClick={onClearAll}>Clear All</ToolbarButton>
|
||||||
onToggle={() => setCompact((c) => !c)}
|
{hasResults && (
|
||||||
/>
|
<ListToggle
|
||||||
)}
|
compact={compact}
|
||||||
|
onToggle={() => setCompact((c) => !c)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Group>
|
||||||
</Group>
|
</Group>
|
||||||
|
|
||||||
{!hasResults ? (
|
{!hasResults ? (
|
||||||
|
|
|
||||||
|
|
@ -59,6 +59,12 @@ export function LogControls({
|
||||||
<Stack gap="md">
|
<Stack gap="md">
|
||||||
<Title order={4}>Logging</Title>
|
<Title order={4}>Logging</Title>
|
||||||
|
|
||||||
|
<TextInput
|
||||||
|
placeholder="Search..."
|
||||||
|
value={filterText}
|
||||||
|
onChange={(e) => onFilterChange(e.currentTarget.value)}
|
||||||
|
/>
|
||||||
|
|
||||||
<Title order={5}>Active Level</Title>
|
<Title order={5}>Active Level</Title>
|
||||||
<Group wrap="nowrap">
|
<Group wrap="nowrap">
|
||||||
<Select
|
<Select
|
||||||
|
|
@ -74,13 +80,6 @@ export function LogControls({
|
||||||
</Button>
|
</Button>
|
||||||
</Group>
|
</Group>
|
||||||
|
|
||||||
<Title order={5}>Search</Title>
|
|
||||||
<TextInput
|
|
||||||
placeholder="Search logs..."
|
|
||||||
value={filterText}
|
|
||||||
onChange={(e) => onFilterChange(e.currentTarget.value)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Group justify="space-between">
|
<Group justify="space-between">
|
||||||
<Title order={5}>Filter by Level</Title>
|
<Title order={5}>Filter by Level</Title>
|
||||||
<SubtleButton onClick={onToggleAllLevels}>
|
<SubtleButton onClick={onToggleAllLevels}>
|
||||||
|
|
|
||||||
|
|
@ -6,8 +6,6 @@ const meta: Meta<typeof ResourceControls> = {
|
||||||
title: "Groups/ResourceControls",
|
title: "Groups/ResourceControls",
|
||||||
component: ResourceControls,
|
component: ResourceControls,
|
||||||
args: {
|
args: {
|
||||||
searchText: "",
|
|
||||||
onSearchChange: fn(),
|
|
||||||
onRefreshList: fn(),
|
onRefreshList: fn(),
|
||||||
onSelectUri: fn(),
|
onSelectUri: fn(),
|
||||||
onSelectTemplate: fn(),
|
onSelectTemplate: fn(),
|
||||||
|
|
@ -59,7 +57,6 @@ export const Default: Story = {
|
||||||
export const WithSearch: Story = {
|
export const WithSearch: Story = {
|
||||||
args: {
|
args: {
|
||||||
...Default.args,
|
...Default.args,
|
||||||
searchText: "config",
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import {
|
||||||
Title,
|
Title,
|
||||||
} from "@mantine/core";
|
} from "@mantine/core";
|
||||||
import { ListChangedIndicator } from "../../elements/ListChangedIndicator/ListChangedIndicator";
|
import { ListChangedIndicator } from "../../elements/ListChangedIndicator/ListChangedIndicator";
|
||||||
|
import { ListToggle } from "../../elements/ListToggle/ListToggle";
|
||||||
import { ResourceListItem } from "../ResourceListItem/ResourceListItem";
|
import { ResourceListItem } from "../ResourceListItem/ResourceListItem";
|
||||||
import { ResourceSubscribedItem } from "../ResourceSubscribedItem/ResourceSubscribedItem";
|
import { ResourceSubscribedItem } from "../ResourceSubscribedItem/ResourceSubscribedItem";
|
||||||
import type {
|
import type {
|
||||||
|
|
@ -21,8 +22,6 @@ export interface ResourceControlsProps {
|
||||||
templates: TemplateListItem[];
|
templates: TemplateListItem[];
|
||||||
subscriptions: SubscriptionItem[];
|
subscriptions: SubscriptionItem[];
|
||||||
listChanged: boolean;
|
listChanged: boolean;
|
||||||
searchText: string;
|
|
||||||
onSearchChange: (text: string) => void;
|
|
||||||
onRefreshList: () => void;
|
onRefreshList: () => void;
|
||||||
onSelectUri: (uri: string) => void;
|
onSelectUri: (uri: string) => void;
|
||||||
onSelectTemplate: (uriTemplate: string) => void;
|
onSelectTemplate: (uriTemplate: string) => void;
|
||||||
|
|
@ -53,24 +52,38 @@ export function ResourceControls({
|
||||||
templates,
|
templates,
|
||||||
subscriptions,
|
subscriptions,
|
||||||
listChanged,
|
listChanged,
|
||||||
searchText,
|
|
||||||
onSearchChange,
|
|
||||||
onRefreshList,
|
onRefreshList,
|
||||||
onSelectUri,
|
onSelectUri,
|
||||||
onSelectTemplate,
|
onSelectTemplate,
|
||||||
onUnsubscribeResource,
|
onUnsubscribeResource,
|
||||||
}: ResourceControlsProps) {
|
}: ResourceControlsProps) {
|
||||||
const filteredResources = resources.filter((r) =>
|
const [searchText, setSearchText] = useState("");
|
||||||
r.name.toLowerCase().includes(searchText.toLowerCase()),
|
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 = [
|
const defaultOpen = [
|
||||||
...(filteredResources.length > 0 ? ["resources"] : []),
|
...(filteredResources.length > 0 ? ["resources"] : []),
|
||||||
...(templates.length > 0 ? ["templates"] : []),
|
...(filteredTemplates.length > 0 ? ["templates"] : []),
|
||||||
...(subscriptions.length > 0 ? ["subscriptions"] : []),
|
...(filteredSubscriptions.length > 0 ? ["subscriptions"] : []),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const allSections = ["resources", "templates", "subscriptions"];
|
||||||
const [openSections, setOpenSections] = useState<string[]>(defaultOpen);
|
const [openSections, setOpenSections] = useState<string[]>(defaultOpen);
|
||||||
|
const allExpanded = openSections.length === allSections.length;
|
||||||
const maxHeight = panelMaxHeight(openSections.length);
|
const maxHeight = panelMaxHeight(openSections.length);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -79,11 +92,20 @@ export function ResourceControls({
|
||||||
<Title order={4}>Resources</Title>
|
<Title order={4}>Resources</Title>
|
||||||
<ListChangedIndicator visible={listChanged} onRefresh={onRefreshList} />
|
<ListChangedIndicator visible={listChanged} onRefresh={onRefreshList} />
|
||||||
</Group>
|
</Group>
|
||||||
<TextInput
|
<Group gap="xs" wrap="nowrap">
|
||||||
placeholder="Search..."
|
<ListToggle
|
||||||
value={searchText}
|
compact={!allExpanded}
|
||||||
onChange={(e) => onSearchChange(e.currentTarget.value)}
|
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 multiple value={openSections} onChange={setOpenSections}>
|
||||||
<Accordion.Item value="resources">
|
<Accordion.Item value="resources">
|
||||||
<Accordion.Control disabled={filteredResources.length === 0}>
|
<Accordion.Control disabled={filteredResources.length === 0}>
|
||||||
|
|
@ -108,13 +130,13 @@ export function ResourceControls({
|
||||||
</Accordion.Item>
|
</Accordion.Item>
|
||||||
|
|
||||||
<Accordion.Item value="templates">
|
<Accordion.Item value="templates">
|
||||||
<Accordion.Control disabled={templates.length === 0}>
|
<Accordion.Control disabled={filteredTemplates.length === 0}>
|
||||||
{formatSectionCount("Templates", templates.length)}
|
{formatSectionCount("Templates", filteredTemplates.length)}
|
||||||
</Accordion.Control>
|
</Accordion.Control>
|
||||||
<Accordion.Panel>
|
<Accordion.Panel>
|
||||||
<ScrollArea.Autosize mah={maxHeight}>
|
<ScrollArea.Autosize mah={maxHeight}>
|
||||||
<Stack gap="xs">
|
<Stack gap="xs">
|
||||||
{templates.map((template) => (
|
{filteredTemplates.map((template) => (
|
||||||
<ResourceListItem
|
<ResourceListItem
|
||||||
key={template.uriTemplate}
|
key={template.uriTemplate}
|
||||||
name={templateDisplayName(template)}
|
name={templateDisplayName(template)}
|
||||||
|
|
@ -129,13 +151,13 @@ export function ResourceControls({
|
||||||
</Accordion.Item>
|
</Accordion.Item>
|
||||||
|
|
||||||
<Accordion.Item value="subscriptions">
|
<Accordion.Item value="subscriptions">
|
||||||
<Accordion.Control disabled={subscriptions.length === 0}>
|
<Accordion.Control disabled={filteredSubscriptions.length === 0}>
|
||||||
{formatSectionCount("Subscriptions", subscriptions.length)}
|
{formatSectionCount("Subscriptions", filteredSubscriptions.length)}
|
||||||
</Accordion.Control>
|
</Accordion.Control>
|
||||||
<Accordion.Panel>
|
<Accordion.Panel>
|
||||||
<ScrollArea.Autosize mah={maxHeight}>
|
<ScrollArea.Autosize mah={maxHeight}>
|
||||||
<Stack gap="xs">
|
<Stack gap="xs">
|
||||||
{subscriptions.map((sub) => (
|
{filteredSubscriptions.map((sub) => (
|
||||||
<ResourceSubscribedItem
|
<ResourceSubscribedItem
|
||||||
key={sub.name}
|
key={sub.name}
|
||||||
name={sub.name}
|
name={sub.name}
|
||||||
|
|
|
||||||
|
|
@ -48,8 +48,6 @@ export function HistoryScreen({
|
||||||
onMethodFilterChange={(value) =>
|
onMethodFilterChange={(value) =>
|
||||||
setMethodFilter(value || undefined)
|
setMethodFilter(value || undefined)
|
||||||
}
|
}
|
||||||
onClearAll={onClearAll}
|
|
||||||
onExport={onExport}
|
|
||||||
/>
|
/>
|
||||||
</SidebarCard>
|
</SidebarCard>
|
||||||
</Sidebar>
|
</Sidebar>
|
||||||
|
|
@ -58,6 +56,8 @@ export function HistoryScreen({
|
||||||
pinnedEntries={pinnedEntries}
|
pinnedEntries={pinnedEntries}
|
||||||
searchText={searchText}
|
searchText={searchText}
|
||||||
methodFilter={methodFilter}
|
methodFilter={methodFilter}
|
||||||
|
onClearAll={onClearAll}
|
||||||
|
onExport={onExport}
|
||||||
/>
|
/>
|
||||||
</ScreenLayout>
|
</ScreenLayout>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -13,8 +13,6 @@ const meta: Meta<typeof ResourcesScreen> = {
|
||||||
component: ResourcesScreen,
|
component: ResourcesScreen,
|
||||||
parameters: { layout: "fullscreen" },
|
parameters: { layout: "fullscreen" },
|
||||||
args: {
|
args: {
|
||||||
searchText: "",
|
|
||||||
onSearchChange: fn(),
|
|
||||||
onRefreshList: fn(),
|
onRefreshList: fn(),
|
||||||
onSelectUri: fn(),
|
onSelectUri: fn(),
|
||||||
onSelectTemplate: fn(),
|
onSelectTemplate: fn(),
|
||||||
|
|
|
||||||
|
|
@ -47,8 +47,6 @@ export interface ResourcesScreenProps {
|
||||||
selectedResource?: SelectedResource;
|
selectedResource?: SelectedResource;
|
||||||
selectedTemplate?: SelectedTemplate;
|
selectedTemplate?: SelectedTemplate;
|
||||||
listChanged: boolean;
|
listChanged: boolean;
|
||||||
searchText: string;
|
|
||||||
onSearchChange: (text: string) => void;
|
|
||||||
onRefreshList: () => void;
|
onRefreshList: () => void;
|
||||||
onSelectUri: (uri: string) => void;
|
onSelectUri: (uri: string) => void;
|
||||||
onSelectTemplate: (uriTemplate: string) => void;
|
onSelectTemplate: (uriTemplate: string) => void;
|
||||||
|
|
@ -92,8 +90,6 @@ export function ResourcesScreen({
|
||||||
selectedResource,
|
selectedResource,
|
||||||
selectedTemplate,
|
selectedTemplate,
|
||||||
listChanged,
|
listChanged,
|
||||||
searchText,
|
|
||||||
onSearchChange,
|
|
||||||
onRefreshList,
|
onRefreshList,
|
||||||
onSelectUri,
|
onSelectUri,
|
||||||
onSelectTemplate,
|
onSelectTemplate,
|
||||||
|
|
@ -110,8 +106,6 @@ export function ResourcesScreen({
|
||||||
templates={templates}
|
templates={templates}
|
||||||
subscriptions={subscriptions}
|
subscriptions={subscriptions}
|
||||||
listChanged={listChanged}
|
listChanged={listChanged}
|
||||||
searchText={searchText}
|
|
||||||
onSearchChange={onSearchChange}
|
|
||||||
onRefreshList={onRefreshList}
|
onRefreshList={onRefreshList}
|
||||||
onSelectUri={onSelectUri}
|
onSelectUri={onSelectUri}
|
||||||
onSelectTemplate={onSelectTemplate}
|
onSelectTemplate={onSelectTemplate}
|
||||||
|
|
|
||||||
|
|
@ -447,8 +447,6 @@ export const ResourcesActive: Story = {
|
||||||
isSubscribed: false,
|
isSubscribed: false,
|
||||||
}}
|
}}
|
||||||
listChanged={true}
|
listChanged={true}
|
||||||
searchText=""
|
|
||||||
onSearchChange={fn()}
|
|
||||||
onRefreshList={fn()}
|
onRefreshList={fn()}
|
||||||
onSelectUri={fn()}
|
onSelectUri={fn()}
|
||||||
onSelectTemplate={fn()}
|
onSelectTemplate={fn()}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue