From c06dacb38e3406bf88d186da0843fe8ccdfbaf0a Mon Sep 17 00:00:00 2001 From: zihluwang Date: Thu, 15 Jan 2026 15:22:17 +0800 Subject: [PATCH] feat: complete a simple json visualiser --- package.json | 4 + pnpm-lock.yaml | 147 ++++++++++++++++ src/components/json-tree-node/index.tsx | 52 ++++++ src/page/home/index.tsx | 215 +++++++++++------------- 4 files changed, 297 insertions(+), 121 deletions(-) create mode 100644 src/components/json-tree-node/index.tsx diff --git a/package.json b/package.json index ac01840..e54868a 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,8 @@ "@tailwindcss/vite": "^4.1.18", "axios": "^1.13.2", "dayjs": "^1.11.19", + "jsonpath": "^1.1.1", + "lodash": "^4.17.21", "react": "^19.2.3", "react-dom": "^19.2.3", "react-redux": "^9.2.0", @@ -25,6 +27,8 @@ "tailwindcss": "^4.1.18" }, "devDependencies": { + "@types/jsonpath": "^0.2.4", + "@types/lodash": "^4.17.23", "@types/node": "^22.19.2", "@types/react": "^19.2.7", "@types/react-dom": "^19.2.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 60060f1..ef7136a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -20,6 +20,12 @@ importers: dayjs: specifier: ^1.11.19 version: 1.11.19 + jsonpath: + specifier: ^1.1.1 + version: 1.1.1 + lodash: + specifier: ^4.17.21 + version: 4.17.21 react: specifier: ^19.2.3 version: 19.2.3 @@ -42,6 +48,12 @@ importers: specifier: ^4.1.18 version: 4.1.18 devDependencies: + '@types/jsonpath': + specifier: ^0.2.4 + version: 0.2.4 + '@types/lodash': + specifier: ^4.17.23 + version: 4.17.23 '@types/node': specifier: ^22.19.2 version: 22.19.2 @@ -559,6 +571,12 @@ packages: '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + '@types/jsonpath@0.2.4': + resolution: {integrity: sha512-K3hxB8Blw0qgW6ExKgMbXQv2UPZBoE2GqLpVY+yr7nMD2Pq86lsuIzyAaiQ7eMqFL5B6di6pxSkogLJEyEHoGA==} + + '@types/lodash@4.17.23': + resolution: {integrity: sha512-RDvF6wTulMPjrNdCoYRC8gNR880JNGT8uB+REUpC2Ns4pRqQJhGz90wh7rgdXDPpCczF3VGktDuFGVnz8zP7HA==} + '@types/node@22.19.2': resolution: {integrity: sha512-LPM2G3Syo1GLzXLGJAKdqoU35XvrWzGJ21/7sgZTUpbkBaOasTj8tjwn6w+hCkqaa1TfJ/w67rJSwYItlJ2mYw==} @@ -627,6 +645,9 @@ packages: supports-color: optional: true + deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + delayed-stream@1.0.0: resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} engines: {node: '>=0.4.0'} @@ -671,6 +692,32 @@ packages: resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} engines: {node: '>=6'} + escodegen@1.14.3: + resolution: {integrity: sha512-qFcX0XJkdg+PB3xjZZG/wKSuT1PnQWx57+TVSjIMmILd2yC/6ByYElPwJnslDsuWuSAp4AwJGumarAAmJch5Kw==} + engines: {node: '>=4.0'} + hasBin: true + + esprima@1.2.2: + resolution: {integrity: sha512-+JpPZam9w5DuJ3Q67SqsMGtiHKENSMRVoxvArfJZK01/BfLEObtZ6orJa/MtoGNR/rfMgp5837T41PAmTwAv/A==} + engines: {node: '>=0.4.0'} + hasBin: true + + esprima@4.0.1: + resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} + engines: {node: '>=4'} + hasBin: true + + estraverse@4.3.0: + resolution: {integrity: sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==} + engines: {node: '>=4.0'} + + esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + + fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + fdir@6.5.0: resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} engines: {node: '>=12.0.0'} @@ -756,6 +803,13 @@ packages: engines: {node: '>=6'} hasBin: true + jsonpath@1.1.1: + resolution: {integrity: sha512-l6Cg7jRpixfbgoWgkrl77dgEj8RPvND0wMH6TwQmi9Qs4TFfS9u5cUFnbeKTwj5ga5Y3BTGGNI28k117LJ009w==} + + levn@0.3.0: + resolution: {integrity: sha512-0OO4y2iOHix2W6ujICbKIaEQXvFQHue65vUG3pb5EUomzPI90z9hsA1VsO/dbIIpC53J8gxM9Q4Oho0jrCM/yA==} + engines: {node: '>= 0.8.0'} + lightningcss-android-arm64@1.30.2: resolution: {integrity: sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==} engines: {node: '>= 12.0.0'} @@ -826,6 +880,9 @@ packages: resolution: {integrity: sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==} engines: {node: '>= 12.0.0'} + lodash@4.17.21: + resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} @@ -855,6 +912,10 @@ packages: node-releases@2.0.27: resolution: {integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==} + optionator@0.8.3: + resolution: {integrity: sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==} + engines: {node: '>= 0.8.0'} + picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -866,6 +927,10 @@ packages: resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} engines: {node: ^10 || ^12 || >=14} + prelude-ls@1.1.2: + resolution: {integrity: sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w==} + engines: {node: '>= 0.8.0'} + prettier@3.7.4: resolution: {integrity: sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA==} engines: {node: '>=14'} @@ -955,6 +1020,13 @@ packages: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} + source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + + static-eval@2.0.2: + resolution: {integrity: sha512-N/D219Hcr2bPjLxPiV+TQE++Tsmrady7TqAJugLy7Xk1EumfDWS/f5dtBbkRCGE7wKKXuYockQoj8Rm2/pVKyg==} + tailwindcss@4.1.18: resolution: {integrity: sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==} @@ -966,11 +1038,18 @@ packages: resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} engines: {node: '>=12.0.0'} + type-check@0.3.2: + resolution: {integrity: sha512-ZCmOJdvOWDBYJlzAoFkC+Q0+bUyEOS1ltgp1MGU03fqHG+dbi9tBFU2Rd9QKiDZFAYrhPh2JUf7rZRIuHRKtOg==} + engines: {node: '>= 0.8.0'} + typescript@5.9.3: resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} engines: {node: '>=14.17'} hasBin: true + underscore@1.12.1: + resolution: {integrity: sha512-hEQt0+ZLDVUMhebKxL4x1BTtDY7bavVofhZ9KZ4aI26X9SRaE+Y3m83XUL1UP2jn8ynjndwCCpEHdUG+9pP1Tw==} + undici-types@6.21.0: resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} @@ -1025,6 +1104,10 @@ packages: yaml: optional: true + word-wrap@1.2.5: + resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} + engines: {node: '>=0.10.0'} + yallist@3.1.1: resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} @@ -1414,6 +1497,10 @@ snapshots: '@types/estree@1.0.8': {} + '@types/jsonpath@0.2.4': {} + + '@types/lodash@4.17.23': {} + '@types/node@22.19.2': dependencies: undici-types: 6.21.0 @@ -1483,6 +1570,8 @@ snapshots: dependencies: ms: 2.1.3 + deep-is@0.1.4: {} + delayed-stream@1.0.0: {} detect-libc@2.1.2: {} @@ -1546,6 +1635,25 @@ snapshots: escalade@3.2.0: {} + escodegen@1.14.3: + dependencies: + esprima: 4.0.1 + estraverse: 4.3.0 + esutils: 2.0.3 + optionator: 0.8.3 + optionalDependencies: + source-map: 0.6.1 + + esprima@1.2.2: {} + + esprima@4.0.1: {} + + estraverse@4.3.0: {} + + esutils@2.0.3: {} + + fast-levenshtein@2.0.6: {} + fdir@6.5.0(picomatch@4.0.3): optionalDependencies: picomatch: 4.0.3 @@ -1611,6 +1719,17 @@ snapshots: json5@2.2.3: {} + jsonpath@1.1.1: + dependencies: + esprima: 1.2.2 + static-eval: 2.0.2 + underscore: 1.12.1 + + levn@0.3.0: + dependencies: + prelude-ls: 1.1.2 + type-check: 0.3.2 + lightningcss-android-arm64@1.30.2: optional: true @@ -1660,6 +1779,8 @@ snapshots: lightningcss-win32-arm64-msvc: 1.30.2 lightningcss-win32-x64-msvc: 1.30.2 + lodash@4.17.21: {} + lru-cache@5.1.1: dependencies: yallist: 3.1.1 @@ -1682,6 +1803,15 @@ snapshots: node-releases@2.0.27: {} + optionator@0.8.3: + dependencies: + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.3.0 + prelude-ls: 1.1.2 + type-check: 0.3.2 + word-wrap: 1.2.5 + picocolors@1.1.1: {} picomatch@4.0.3: {} @@ -1692,6 +1822,8 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 + prelude-ls@1.1.2: {} + prettier@3.7.4: {} proxy-from-env@1.1.0: {} @@ -1778,6 +1910,13 @@ snapshots: source-map-js@1.2.1: {} + source-map@0.6.1: + optional: true + + static-eval@2.0.2: + dependencies: + escodegen: 1.14.3 + tailwindcss@4.1.18: {} tapable@2.3.0: {} @@ -1787,8 +1926,14 @@ snapshots: fdir: 6.5.0(picomatch@4.0.3) picomatch: 4.0.3 + type-check@0.3.2: + dependencies: + prelude-ls: 1.1.2 + typescript@5.9.3: {} + underscore@1.12.1: {} + undici-types@6.21.0: {} update-browserslist-db@1.2.2(browserslist@4.28.1): @@ -1815,4 +1960,6 @@ snapshots: jiti: 2.6.1 lightningcss: 1.30.2 + word-wrap@1.2.5: {} + yallist@3.1.1: {} diff --git a/src/components/json-tree-node/index.tsx b/src/components/json-tree-node/index.tsx new file mode 100644 index 0000000..2ab982d --- /dev/null +++ b/src/components/json-tree-node/index.tsx @@ -0,0 +1,52 @@ +import { useMemo } from "react" +import jp, { PathComponent } from "jsonpath" +import _ from "lodash" + +export interface JsonTreeNodeProps { + data: unknown + path: PathComponent[] + matchedPaths: string[] +} + +export default function JsonTreeNode({ data, path, matchedPaths }: JsonTreeNodeProps) { + const currentPathString = useMemo(() => jp.stringify(path), [path]) + const isMatched = matchedPaths.includes(currentPathString) + + const highlightClass = isMatched ? "bg-yellow-200 ring-2 ring-yellow-400 rounded-sm" : "" + + if (_.isObject(data) && data != null) { + const isArray = Array.isArray(data) + const entries = Object.entries(data) + + return ( +
+ {isArray ? "[" : "{"} + {entries.map(([key, value], index) => { + const nextPath = [...path, isArray ? Number(key) : key] + + return ( +
+ {!isArray && "{key}": } + + {index < entries.length - 1 && ,} +
+ ) + })} + {isArray ? "]" : "}"} +
+ ) + } + + const renderValue = () => { + if (typeof data === "string") return "{data}" + if (typeof data === "number") return {data} + if (typeof data === "boolean") return {String(data)} + return null + } + + return ( + + {renderValue()} + + ) +} diff --git a/src/page/home/index.tsx b/src/page/home/index.tsx index b86cad1..c8fcce2 100644 --- a/src/page/home/index.tsx +++ b/src/page/home/index.tsx @@ -1,138 +1,111 @@ -import { Link } from "react-router-dom" +import { useMemo, useState } from "react" +import jp from "jsonpath" +import JsonTreeNode from "@/components/json-tree-node" /** * Home page component that displays the main landing content. */ export default function Home() { + const initialData = { + centre_id: "LON-01", + location: "London", + is_active: true, + staff_members: [ + { id: 101, name: "Alice", roles: ["Admin", "Manager"] }, + { id: 102, name: "Bob", roles: ["Developer"] }, + ], + config: { + colour_scheme: "Dark Mode", + retention_days: 30, + }, + } + + const [jsonInput, setJsonInput] = useState(JSON.stringify(initialData, null, 2)) + const [query, setQuery] = useState("$.staff_members[*].name") + + // 计算匹配结果 + const result = useMemo(() => { + try { + const parsed = JSON.parse(jsonInput) + const nodes = jp.nodes(parsed, query) + return { + parsed, + matchedPaths: nodes.map((n) => jp.stringify(n.path)), + error: null, + } + } catch (e) { + return { parsed: null, matchedPaths: [], error: (e as Error).message } + } + }, [jsonInput, query]) + return ( -
- {/* Hero Section */} -
-

- Welcome to OnixByte React Template -

-

- A modern React application template with TypeScript, Tailwind CSS,{" "} - Redux, and React Router for seamless navigation. -

-
+
+
+
+

+ TypeScript JSONPath Explorer +

+

+ Debug and visualises your JSONPath queries with real-time highlighting. +

+
- {/* Features Grid */} -
-
- {/* Feature 1 */} -
-
-
-
- - - -
-
-

Fast Development

-
-
-
-

- Built with Vite for lightning-fast development experience and hot module - replacement. -

+
+ {/* 控制面板 */} +
+
+
+ + JSON Source +
+