Skip to content

Commit ed31f98

Browse files
authored
Merge branch 'main' into perf_useTheme
2 parents f09d2b6 + 38fb710 commit ed31f98

File tree

11 files changed

+344
-113
lines changed

11 files changed

+344
-113
lines changed

client/src/components/DynamicJsonForm.tsx

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,21 @@ const DynamicJsonForm = ({
108108
}
109109
};
110110

111+
const formatJson = () => {
112+
try {
113+
const jsonStr = rawJsonValue.trim();
114+
if (!jsonStr) {
115+
return;
116+
}
117+
const formatted = JSON.stringify(JSON.parse(jsonStr), null, 2);
118+
setRawJsonValue(formatted);
119+
debouncedUpdateParent(formatted);
120+
setJsonError(undefined);
121+
} catch (err) {
122+
setJsonError(err instanceof Error ? err.message : "Invalid JSON");
123+
}
124+
};
125+
111126
const renderFormFields = (
112127
propSchema: JsonSchemaType,
113128
currentValue: JsonValue,
@@ -353,7 +368,12 @@ const DynamicJsonForm = ({
353368

354369
return (
355370
<div className="space-y-4">
356-
<div className="flex justify-end">
371+
<div className="flex justify-end space-x-2">
372+
{isJsonMode && (
373+
<Button variant="outline" size="sm" onClick={formatJson}>
374+
Format JSON
375+
</Button>
376+
)}
357377
<Button variant="outline" size="sm" onClick={handleSwitchToFormMode}>
358378
{isJsonMode ? "Switch to Form" : "Switch to JSON"}
359379
</Button>

client/src/components/History.tsx

Lines changed: 12 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { ServerNotification } from "@modelcontextprotocol/sdk/types.js";
22
import { Copy } from "lucide-react";
33
import { useState } from "react";
4+
import JsonView from "./JsonView";
45

56
const HistoryAndNotifications = ({
67
requestHistory,
@@ -74,9 +75,9 @@ const HistoryAndNotifications = ({
7475
<Copy size={16} />
7576
</button>
7677
</div>
77-
<pre className="whitespace-pre-wrap break-words bg-background p-2 rounded">
78-
{JSON.stringify(JSON.parse(request.request), null, 2)}
79-
</pre>
78+
<div className="bg-background p-2 rounded">
79+
<JsonView data={request.request} />
80+
</div>
8081
</div>
8182
{request.response && (
8283
<div className="mt-2">
@@ -91,13 +92,9 @@ const HistoryAndNotifications = ({
9192
<Copy size={16} />
9293
</button>
9394
</div>
94-
<pre className="whitespace-pre-wrap break-words bg-background p-2 rounded">
95-
{JSON.stringify(
96-
JSON.parse(request.response),
97-
null,
98-
2,
99-
)}
100-
</pre>
95+
<div className="bg-background p-2 rounded">
96+
<JsonView data={request.response} />
97+
</div>
10198
</div>
10299
)}
103100
</>
@@ -146,9 +143,11 @@ const HistoryAndNotifications = ({
146143
<Copy size={16} />
147144
</button>
148145
</div>
149-
<pre className="whitespace-pre-wrap break-words bg-background p-2 rounded">
150-
{JSON.stringify(notification, null, 2)}
151-
</pre>
146+
<div className="bg-background p-2 rounded">
147+
<JsonView
148+
data={JSON.stringify(notification, null, 2)}
149+
/>
150+
</div>
152151
</div>
153152
)}
154153
</li>

client/src/components/JsonEditor.tsx

Lines changed: 3 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import Editor from "react-simple-code-editor";
33
import Prism from "prismjs";
44
import "prismjs/components/prism-json";
55
import "prismjs/themes/prism.css";
6-
import { Button } from "@/components/ui/button";
76

87
interface JsonEditorProps {
98
value: string;
@@ -16,49 +15,25 @@ const JsonEditor = ({
1615
onChange,
1716
error: externalError,
1817
}: JsonEditorProps) => {
19-
const [editorContent, setEditorContent] = useState(value);
18+
const [editorContent, setEditorContent] = useState(value || "");
2019
const [internalError, setInternalError] = useState<string | undefined>(
2120
undefined,
2221
);
2322

2423
useEffect(() => {
25-
setEditorContent(value);
24+
setEditorContent(value || "");
2625
}, [value]);
2726

28-
const formatJson = (json: string): string => {
29-
try {
30-
return JSON.stringify(JSON.parse(json), null, 2);
31-
} catch {
32-
return json;
33-
}
34-
};
35-
3627
const handleEditorChange = (newContent: string) => {
3728
setEditorContent(newContent);
3829
setInternalError(undefined);
3930
onChange(newContent);
4031
};
4132

42-
const handleFormatJson = () => {
43-
try {
44-
const formatted = formatJson(editorContent);
45-
setEditorContent(formatted);
46-
onChange(formatted);
47-
setInternalError(undefined);
48-
} catch (err) {
49-
setInternalError(err instanceof Error ? err.message : "Invalid JSON");
50-
}
51-
};
52-
5333
const displayError = internalError || externalError;
5434

5535
return (
56-
<div className="relative space-y-2">
57-
<div className="flex justify-end">
58-
<Button variant="outline" size="sm" onClick={handleFormatJson}>
59-
Format JSON
60-
</Button>
61-
</div>
36+
<div className="relative">
6237
<div
6338
className={`border rounded-md ${
6439
displayError

client/src/components/JsonView.tsx

Lines changed: 228 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,228 @@
1+
import { useState, memo } from "react";
2+
import { JsonValue } from "./DynamicJsonForm";
3+
import clsx from "clsx";
4+
5+
interface JsonViewProps {
6+
data: unknown;
7+
name?: string;
8+
initialExpandDepth?: number;
9+
}
10+
11+
function tryParseJson(str: string): { success: boolean; data: JsonValue } {
12+
const trimmed = str.trim();
13+
if (
14+
!(trimmed.startsWith("{") && trimmed.endsWith("}")) &&
15+
!(trimmed.startsWith("[") && trimmed.endsWith("]"))
16+
) {
17+
return { success: false, data: str };
18+
}
19+
try {
20+
return { success: true, data: JSON.parse(str) };
21+
} catch {
22+
return { success: false, data: str };
23+
}
24+
}
25+
26+
const JsonView = memo(
27+
({ data, name, initialExpandDepth = 3 }: JsonViewProps) => {
28+
const normalizedData =
29+
typeof data === "string"
30+
? tryParseJson(data).success
31+
? tryParseJson(data).data
32+
: data
33+
: data;
34+
35+
return (
36+
<div className="font-mono text-sm transition-all duration-300 ">
37+
<JsonNode
38+
data={normalizedData as JsonValue}
39+
name={name}
40+
depth={0}
41+
initialExpandDepth={initialExpandDepth}
42+
/>
43+
</div>
44+
);
45+
},
46+
);
47+
48+
JsonView.displayName = "JsonView";
49+
50+
interface JsonNodeProps {
51+
data: JsonValue;
52+
name?: string;
53+
depth: number;
54+
initialExpandDepth: number;
55+
}
56+
57+
const JsonNode = memo(
58+
({ data, name, depth = 0, initialExpandDepth }: JsonNodeProps) => {
59+
const [isExpanded, setIsExpanded] = useState(depth < initialExpandDepth);
60+
61+
const getDataType = (value: JsonValue): string => {
62+
if (Array.isArray(value)) return "array";
63+
if (value === null) return "null";
64+
return typeof value;
65+
};
66+
67+
const dataType = getDataType(data);
68+
69+
const typeStyleMap: Record<string, string> = {
70+
number: "text-blue-600",
71+
boolean: "text-amber-600",
72+
null: "text-purple-600",
73+
undefined: "text-gray-600",
74+
string: "text-green-600 break-all whitespace-pre-wrap",
75+
default: "text-gray-700",
76+
};
77+
78+
const renderCollapsible = (isArray: boolean) => {
79+
const items = isArray
80+
? (data as JsonValue[])
81+
: Object.entries(data as Record<string, JsonValue>);
82+
const itemCount = items.length;
83+
const isEmpty = itemCount === 0;
84+
85+
const symbolMap = {
86+
open: isArray ? "[" : "{",
87+
close: isArray ? "]" : "}",
88+
collapsed: isArray ? "[ ... ]" : "{ ... }",
89+
empty: isArray ? "[]" : "{}",
90+
};
91+
92+
if (isEmpty) {
93+
return (
94+
<div className="flex items-center">
95+
{name && (
96+
<span className="mr-1 text-gray-600 dark:text-gray-400">
97+
{name}:
98+
</span>
99+
)}
100+
<span className="text-gray-500">{symbolMap.empty}</span>
101+
</div>
102+
);
103+
}
104+
105+
return (
106+
<div className="flex flex-col">
107+
<div
108+
className="flex items-center mr-1 rounded cursor-pointer group hover:bg-gray-800/10 dark:hover:bg-gray-800/20"
109+
onClick={() => setIsExpanded(!isExpanded)}
110+
>
111+
{name && (
112+
<span className="mr-1 text-gray-600 dark:text-gray-400 dark:group-hover:text-gray-100 group-hover:text-gray-400">
113+
{name}:
114+
</span>
115+
)}
116+
{isExpanded ? (
117+
<span className="text-gray-600 dark:text-gray-400 dark:group-hover:text-gray-100 group-hover:text-gray-400">
118+
{symbolMap.open}
119+
</span>
120+
) : (
121+
<>
122+
<span className="text-gray-600 dark:group-hover:text-gray-100 group-hover:text-gray-400">
123+
{symbolMap.collapsed}
124+
</span>
125+
<span className="ml-1 text-gray-700 dark:group-hover:text-gray-100 group-hover:text-gray-400">
126+
{itemCount} {itemCount === 1 ? "item" : "items"}
127+
</span>
128+
</>
129+
)}
130+
</div>
131+
{isExpanded && (
132+
<>
133+
<div className="pl-2 ml-4 border-l border-gray-200 dark:border-gray-800">
134+
{isArray
135+
? (items as JsonValue[]).map((item, index) => (
136+
<div key={index} className="my-1">
137+
<JsonNode
138+
data={item}
139+
name={`${index}`}
140+
depth={depth + 1}
141+
initialExpandDepth={initialExpandDepth}
142+
/>
143+
</div>
144+
))
145+
: (items as [string, JsonValue][]).map(([key, value]) => (
146+
<div key={key} className="my-1">
147+
<JsonNode
148+
data={value}
149+
name={key}
150+
depth={depth + 1}
151+
initialExpandDepth={initialExpandDepth}
152+
/>
153+
</div>
154+
))}
155+
</div>
156+
<div className="text-gray-600 dark:text-gray-400">
157+
{symbolMap.close}
158+
</div>
159+
</>
160+
)}
161+
</div>
162+
);
163+
};
164+
165+
const renderString = (value: string) => {
166+
const maxLength = 100;
167+
const isTooLong = value.length > maxLength;
168+
169+
if (!isTooLong) {
170+
return (
171+
<div className="flex mr-1 rounded hover:bg-gray-800/20">
172+
{name && (
173+
<span className="mr-1 text-gray-600 dark:text-gray-400">
174+
{name}:
175+
</span>
176+
)}
177+
<pre className={typeStyleMap.string}>"{value}"</pre>
178+
</div>
179+
);
180+
}
181+
182+
return (
183+
<div className="flex mr-1 rounded group hover:bg-gray-800/20">
184+
{name && (
185+
<span className="mr-1 text-gray-600 dark:text-gray-400 dark:group-hover:text-gray-100 group-hover:text-gray-400">
186+
{name}:
187+
</span>
188+
)}
189+
<pre
190+
className={clsx(
191+
typeStyleMap.string,
192+
"cursor-pointer group-hover:text-green-500",
193+
)}
194+
onClick={() => setIsExpanded(!isExpanded)}
195+
title={isExpanded ? "Click to collapse" : "Click to expand"}
196+
>
197+
{isExpanded ? `"${value}"` : `"${value.slice(0, maxLength)}..."`}
198+
</pre>
199+
</div>
200+
);
201+
};
202+
203+
switch (dataType) {
204+
case "object":
205+
case "array":
206+
return renderCollapsible(dataType === "array");
207+
case "string":
208+
return renderString(data as string);
209+
default:
210+
return (
211+
<div className="flex items-center mr-1 rounded hover:bg-gray-800/20">
212+
{name && (
213+
<span className="mr-1 text-gray-600 dark:text-gray-400">
214+
{name}:
215+
</span>
216+
)}
217+
<span className={typeStyleMap[dataType] || typeStyleMap.default}>
218+
{data === null ? "null" : String(data)}
219+
</span>
220+
</div>
221+
);
222+
}
223+
},
224+
);
225+
226+
JsonNode.displayName = "JsonNode";
227+
228+
export default JsonView;

0 commit comments

Comments
 (0)