import { useCallback, useMemo, useState } from "react" import jp from "jsonpath" import JsonTreeNode from "@/components/json-tree-node" /** * JSON Viewer page component that displays the JSON visualisation tool in DevLab. */ export default function JsonViewer() { 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 [copied, setCopied] = useState(false) // Compute matching results const result = useMemo(() => { let parsed try { parsed = JSON.parse(jsonInput) } catch (e) { return { parsed: null, matchedPaths: [], matchedValues: [], error: (e as Error).message, queryError: null } } try { const nodes = jp.nodes(parsed, query) return { parsed, matchedPaths: nodes.map((n) => jp.stringify(n.path)), matchedValues: nodes.map((n) => n.value), error: null, queryError: null, } } catch (e) { // When JSONPath expression is invalid, still display the JSON tree but with no matches return { parsed, matchedPaths: [], matchedValues: [], error: null, queryError: (e as Error).message } } }, [jsonInput, query]) // Copy as CSV const copyAsCsv = useCallback(() => { if (result.matchedValues.length === 0) return const escapeCsvValue = (val: unknown): string => { const str = typeof val === "object" ? JSON.stringify(val) : String(val) if (str.includes(",") || str.includes('"') || str.includes("\n")) { return `"${str.replace(/"/g, '""')}"` } return str } const header = query const rows = result.matchedValues.map(escapeCsvValue) const csv = [header, ...rows].join("\n") navigator.clipboard.writeText(csv).then(() => { setCopied(true) setTimeout(() => setCopied(false), 2000) }) }, [query, result.matchedValues]) return (
{/* Left panel - 30% */}
{/* JSON Source - fills remaining height */}