Rename History file to HistoryAndNotifications
Rename History.tsx to HistoryAndNotifications.tsx for consistency with component name and update imports. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
7eaa948e98
commit
78eaa177a4
3 changed files with 50 additions and 40 deletions
|
|
@ -52,7 +52,7 @@ import { z } from "zod";
|
|||
import "./App.css";
|
||||
import AuthDebugger from "./components/AuthDebugger";
|
||||
import ConsoleTab from "./components/ConsoleTab";
|
||||
import HistoryAndNotifications from "./components/History";
|
||||
import HistoryAndNotifications from "./components/HistoryAndNotifications";
|
||||
import PingTab from "./components/PingTab";
|
||||
import PromptsTab, { Prompt } from "./components/PromptsTab";
|
||||
import ResourcesTab from "./components/ResourcesTab";
|
||||
|
|
|
|||
155
client/src/components/HistoryAndNotifications.tsx
Normal file
155
client/src/components/HistoryAndNotifications.tsx
Normal file
|
|
@ -0,0 +1,155 @@
|
|||
import { ServerNotification } from "@modelcontextprotocol/sdk/types.js";
|
||||
import { useState } from "react";
|
||||
import JsonView from "./JsonView";
|
||||
|
||||
const HistoryAndNotifications = ({
|
||||
requestHistory,
|
||||
serverNotifications,
|
||||
}: {
|
||||
requestHistory: Array<{ request: string; response?: string }>;
|
||||
serverNotifications: ServerNotification[];
|
||||
}) => {
|
||||
const [expandedRequests, setExpandedRequests] = useState<{
|
||||
[key: number]: boolean;
|
||||
}>({});
|
||||
const [expandedNotifications, setExpandedNotifications] = useState<{
|
||||
[key: number]: boolean;
|
||||
}>({});
|
||||
|
||||
const toggleRequestExpansion = (index: number) => {
|
||||
setExpandedRequests((prev) => ({ ...prev, [index]: !prev[index] }));
|
||||
};
|
||||
|
||||
const toggleNotificationExpansion = (index: number) => {
|
||||
setExpandedNotifications((prev) => ({ ...prev, [index]: !prev[index] }));
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-card overflow-hidden flex h-full">
|
||||
<div className="flex-1 overflow-y-auto p-4 border-r">
|
||||
<h2 className="text-lg font-semibold mb-4">History</h2>
|
||||
{requestHistory.length === 0 ? (
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400 italic">
|
||||
No history yet
|
||||
</p>
|
||||
) : (
|
||||
<ul className="space-y-3">
|
||||
{requestHistory
|
||||
.slice()
|
||||
.reverse()
|
||||
.map((request, index) => (
|
||||
<li
|
||||
key={index}
|
||||
className="text-sm text-foreground bg-secondary py-2 px-3 rounded"
|
||||
>
|
||||
<div
|
||||
className="flex justify-between items-center cursor-pointer"
|
||||
onClick={() =>
|
||||
toggleRequestExpansion(requestHistory.length - 1 - index)
|
||||
}
|
||||
>
|
||||
<span className="font-mono">
|
||||
{requestHistory.length - index}.{" "}
|
||||
{JSON.parse(request.request).method}
|
||||
</span>
|
||||
<span>
|
||||
{expandedRequests[requestHistory.length - 1 - index]
|
||||
? "▼"
|
||||
: "▶"}
|
||||
</span>
|
||||
</div>
|
||||
{expandedRequests[requestHistory.length - 1 - index] && (
|
||||
<>
|
||||
<div className="mt-2">
|
||||
<div className="flex justify-between items-center mb-1">
|
||||
<span className="font-semibold text-blue-600">
|
||||
Request:
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<JsonView
|
||||
data={request.request}
|
||||
className="bg-background"
|
||||
/>
|
||||
</div>
|
||||
{request.response && (
|
||||
<div className="mt-2">
|
||||
<div className="flex justify-between items-center mb-1">
|
||||
<span className="font-semibold text-green-600">
|
||||
Response:
|
||||
</span>
|
||||
</div>
|
||||
<JsonView
|
||||
data={request.response}
|
||||
className="bg-background"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex-1 overflow-y-auto p-4">
|
||||
<h2 className="text-lg font-semibold mb-4">Server Notifications</h2>
|
||||
{serverNotifications.length === 0 ? (
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400 italic">
|
||||
No notifications yet
|
||||
</p>
|
||||
) : (
|
||||
<ul className="space-y-3">
|
||||
{serverNotifications
|
||||
.slice()
|
||||
.reverse()
|
||||
.map((notification, index) => (
|
||||
<li
|
||||
key={index}
|
||||
className="text-sm text-foreground bg-secondary py-2 px-3 rounded"
|
||||
>
|
||||
<div
|
||||
className="flex justify-between items-center cursor-pointer"
|
||||
onClick={() =>
|
||||
toggleNotificationExpansion(
|
||||
serverNotifications.length - 1 - index,
|
||||
)
|
||||
}
|
||||
>
|
||||
<span className="font-mono">
|
||||
{serverNotifications.length - index}.{" "}
|
||||
{notification.method}
|
||||
</span>
|
||||
<span>
|
||||
{expandedNotifications[
|
||||
serverNotifications.length - 1 - index
|
||||
]
|
||||
? "▼"
|
||||
: "▶"}
|
||||
</span>
|
||||
</div>
|
||||
{expandedNotifications[
|
||||
serverNotifications.length - 1 - index
|
||||
] && (
|
||||
<div className="mt-2">
|
||||
<div className="flex justify-between items-center mb-1">
|
||||
<span className="font-semibold text-purple-600">
|
||||
Details:
|
||||
</span>
|
||||
</div>
|
||||
<JsonView
|
||||
data={JSON.stringify(notification, null, 2)}
|
||||
className="bg-background"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default HistoryAndNotifications;
|
||||
226
client/src/components/__tests__/HistoryAndNotifications.test.tsx
Normal file
226
client/src/components/__tests__/HistoryAndNotifications.test.tsx
Normal file
|
|
@ -0,0 +1,226 @@
|
|||
import { render, screen, fireEvent } from "@testing-library/react";
|
||||
import { describe, it, expect, jest } from "@jest/globals";
|
||||
import HistoryAndNotifications from "../HistoryAndNotifications";
|
||||
import { ServerNotification } from "@modelcontextprotocol/sdk/types.js";
|
||||
|
||||
// Mock JsonView component
|
||||
jest.mock("../JsonView", () => {
|
||||
return function JsonView({ data }: { data: string }) {
|
||||
return <div data-testid="json-view">{data}</div>;
|
||||
};
|
||||
});
|
||||
|
||||
describe("HistoryAndNotifications", () => {
|
||||
const mockRequestHistory = [
|
||||
{
|
||||
request: JSON.stringify({ method: "test/method1", params: {} }),
|
||||
response: JSON.stringify({ result: "success" }),
|
||||
},
|
||||
{
|
||||
request: JSON.stringify({ method: "test/method2", params: {} }),
|
||||
response: JSON.stringify({ result: "success" }),
|
||||
},
|
||||
];
|
||||
|
||||
const mockNotifications: ServerNotification[] = [
|
||||
{
|
||||
method: "notifications/message",
|
||||
params: {
|
||||
level: "info" as const,
|
||||
message: "First notification",
|
||||
},
|
||||
},
|
||||
{
|
||||
method: "notifications/progress",
|
||||
params: {
|
||||
progressToken: "test-token",
|
||||
progress: 50,
|
||||
message: "Second notification",
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
it("renders history and notifications sections", () => {
|
||||
render(
|
||||
<HistoryAndNotifications
|
||||
requestHistory={mockRequestHistory}
|
||||
serverNotifications={mockNotifications}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByText("History")).toBeTruthy();
|
||||
expect(screen.getByText("Server Notifications")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("displays request history items with correct numbering", () => {
|
||||
render(
|
||||
<HistoryAndNotifications
|
||||
requestHistory={mockRequestHistory}
|
||||
serverNotifications={[]}
|
||||
/>,
|
||||
);
|
||||
|
||||
// Items should be numbered in reverse order (newest first)
|
||||
expect(screen.getByText("2. test/method2")).toBeTruthy();
|
||||
expect(screen.getByText("1. test/method1")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("displays server notifications with correct numbering", () => {
|
||||
render(
|
||||
<HistoryAndNotifications
|
||||
requestHistory={[]}
|
||||
serverNotifications={mockNotifications}
|
||||
/>,
|
||||
);
|
||||
|
||||
// Items should be numbered in reverse order (newest first)
|
||||
expect(screen.getByText("2. notifications/progress")).toBeTruthy();
|
||||
expect(screen.getByText("1. notifications/message")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("expands and collapses request items when clicked", () => {
|
||||
render(
|
||||
<HistoryAndNotifications
|
||||
requestHistory={mockRequestHistory}
|
||||
serverNotifications={[]}
|
||||
/>,
|
||||
);
|
||||
|
||||
const firstRequestHeader = screen.getByText("2. test/method2");
|
||||
|
||||
// Initially collapsed - should show ▶ arrows (there are multiple)
|
||||
expect(screen.getAllByText("▶")).toHaveLength(2);
|
||||
expect(screen.queryByText("Request:")).toBeNull();
|
||||
|
||||
// Click to expand
|
||||
fireEvent.click(firstRequestHeader);
|
||||
|
||||
// Should now be expanded - one ▼ and one ▶
|
||||
expect(screen.getByText("▼")).toBeTruthy();
|
||||
expect(screen.getAllByText("▶")).toHaveLength(1);
|
||||
expect(screen.getByText("Request:")).toBeTruthy();
|
||||
expect(screen.getByText("Response:")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("expands and collapses notification items when clicked", () => {
|
||||
render(
|
||||
<HistoryAndNotifications
|
||||
requestHistory={[]}
|
||||
serverNotifications={mockNotifications}
|
||||
/>,
|
||||
);
|
||||
|
||||
const firstNotificationHeader = screen.getByText(
|
||||
"2. notifications/progress",
|
||||
);
|
||||
|
||||
// Initially collapsed
|
||||
expect(screen.getAllByText("▶")).toHaveLength(2);
|
||||
expect(screen.queryByText("Details:")).toBeNull();
|
||||
|
||||
// Click to expand
|
||||
fireEvent.click(firstNotificationHeader);
|
||||
|
||||
// Should now be expanded
|
||||
expect(screen.getByText("▼")).toBeTruthy();
|
||||
expect(screen.getAllByText("▶")).toHaveLength(1);
|
||||
expect(screen.getByText("Details:")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("maintains expanded state when new notifications are added", () => {
|
||||
const { rerender } = render(
|
||||
<HistoryAndNotifications
|
||||
requestHistory={[]}
|
||||
serverNotifications={mockNotifications}
|
||||
/>,
|
||||
);
|
||||
|
||||
// Find and expand the older notification (should be "1. notifications/message")
|
||||
const olderNotificationHeader = screen.getByText(
|
||||
"1. notifications/message",
|
||||
);
|
||||
fireEvent.click(olderNotificationHeader);
|
||||
|
||||
// Verify it's expanded
|
||||
expect(screen.getByText("Details:")).toBeTruthy();
|
||||
|
||||
// Add a new notification at the beginning (simulating real behavior)
|
||||
const newNotifications: ServerNotification[] = [
|
||||
{
|
||||
method: "notifications/resources/updated",
|
||||
params: { uri: "file://test.txt" },
|
||||
},
|
||||
...mockNotifications,
|
||||
];
|
||||
|
||||
// Re-render with new notifications
|
||||
rerender(
|
||||
<HistoryAndNotifications
|
||||
requestHistory={[]}
|
||||
serverNotifications={newNotifications}
|
||||
/>,
|
||||
);
|
||||
|
||||
// The original notification should still be expanded
|
||||
// It's now numbered as "2. notifications/message" due to the new item
|
||||
expect(screen.getByText("3. notifications/progress")).toBeTruthy();
|
||||
expect(screen.getByText("2. notifications/message")).toBeTruthy();
|
||||
expect(screen.getByText("1. notifications/resources/updated")).toBeTruthy();
|
||||
|
||||
// The originally expanded notification should still show its details
|
||||
expect(screen.getByText("Details:")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("maintains expanded state when new requests are added", () => {
|
||||
const { rerender } = render(
|
||||
<HistoryAndNotifications
|
||||
requestHistory={mockRequestHistory}
|
||||
serverNotifications={[]}
|
||||
/>,
|
||||
);
|
||||
|
||||
// Find and expand the older request (should be "1. test/method1")
|
||||
const olderRequestHeader = screen.getByText("1. test/method1");
|
||||
fireEvent.click(olderRequestHeader);
|
||||
|
||||
// Verify it's expanded
|
||||
expect(screen.getByText("Request:")).toBeTruthy();
|
||||
expect(screen.getByText("Response:")).toBeTruthy();
|
||||
|
||||
// Add a new request at the beginning
|
||||
const newRequestHistory = [
|
||||
{
|
||||
request: JSON.stringify({ method: "test/new_method", params: {} }),
|
||||
response: JSON.stringify({ result: "new success" }),
|
||||
},
|
||||
...mockRequestHistory,
|
||||
];
|
||||
|
||||
// Re-render with new request history
|
||||
rerender(
|
||||
<HistoryAndNotifications
|
||||
requestHistory={newRequestHistory}
|
||||
serverNotifications={[]}
|
||||
/>,
|
||||
);
|
||||
|
||||
// The original request should still be expanded
|
||||
// It's now numbered as "2. test/method1" due to the new item
|
||||
expect(screen.getByText("3. test/method2")).toBeTruthy();
|
||||
expect(screen.getByText("2. test/method1")).toBeTruthy();
|
||||
expect(screen.getByText("1. test/new_method")).toBeTruthy();
|
||||
|
||||
// The originally expanded request should still show its details
|
||||
expect(screen.getByText("Request:")).toBeTruthy();
|
||||
expect(screen.getByText("Response:")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("displays empty state messages when no data is available", () => {
|
||||
render(
|
||||
<HistoryAndNotifications requestHistory={[]} serverNotifications={[]} />,
|
||||
);
|
||||
|
||||
expect(screen.getByText("No history yet")).toBeTruthy();
|
||||
expect(screen.getByText("No notifications yet")).toBeTruthy();
|
||||
});
|
||||
});
|
||||
Loading…
Reference in a new issue