From 6798f179451a4daf8ae6bbaaebfbde42f45f35a8 Mon Sep 17 00:00:00 2001 From: turbocrime <134443988+turbocrime@users.noreply.github.com> Date: Mon, 25 Mar 2024 12:30:49 -0700 Subject: [PATCH] too fancy json view (#813) --- packages/ui/components/ui/json-viewer.tsx | 122 +++++++++++++++++++--- packages/ui/package.json | 4 +- pnpm-lock.yaml | 8 +- 3 files changed, 116 insertions(+), 18 deletions(-) diff --git a/packages/ui/components/ui/json-viewer.tsx b/packages/ui/components/ui/json-viewer.tsx index 9c690ba4da..7bad92c4b8 100644 --- a/packages/ui/components/ui/json-viewer.tsx +++ b/packages/ui/components/ui/json-viewer.tsx @@ -1,30 +1,128 @@ import type { JsonObject, JsonValue } from '@bufbuild/protobuf'; import { JsonView } from 'react-json-view-lite'; +import { cn } from '../../lib/utils'; +import { CopyToClipboardIconButton } from './copy-to-clipboard-icon-button'; +import { DoubleArrowDownIcon, DoubleArrowUpIcon } from '@radix-ui/react-icons'; +import { Button } from './button'; +import { useCallback, useState } from 'react'; + +const objectDepth = (o: JsonValue): number => + o && typeof o === 'object' ? 1 + Math.max(-1, ...Object.values(o).map(objectDepth)) : 0; + +const objectLength = (o: JsonValue): number => + o && typeof o === 'object' ? Object.entries(o).length : 0; export const JsonViewer = ({ jsonObj }: { jsonObj: JsonObject | JsonValue[] }) => { + const [expandAll, setExpandAll] = useState(false); + + const shouldExpandNode = useCallback( + (level: number, value: JsonValue, field?: string) => { + const collapseLevel = 3; + if (expandAll) return true; + if ( + // expand all empty, they render small + objectLength(value) === 0 || + objectDepth(value) === 0 || + // expand arrays + Array.isArray(value) || + // always expand below minimum + level < collapseLevel + ) + return true; + // begin to collapse, small objects stay open + if (level === collapseLevel) return !field && objectLength(value) < 2; + if (level === collapseLevel + 1) return !field && objectLength(value) < 2; + // close all objects + if (level === collapseLevel + 2) return false; + + // nested hidden objects stay open + return true; + }, + [expandAll], + ); + return (
+
+
+
+ + +
+
+
level < 2} + shouldExpandNode={shouldExpandNode} style={{ - container: 'bg-black whitespace-pre-wrap break-words font-mono -mx-4', - basicChildStyle: 'mx-4 py-[2px]', - label: 'font-semibold mr-1.5 text-gray-200', + container: cn( + 'font-mono', + 'mr-[3em]', // pad on the right side, for balance + 'text-ellipsis', // visually truncate long strings on the right side + '[&>div>span:first-child]:hidden', // hide first collapse arrow + ), + basicChildStyle: cn( + 'text-gray-500', + 'break-keep truncate', // don't break strings, visually truncate + 'pl-[1em]', // indent each level + // compact display if no children + '[&:not(:has(div))]:block', + 'only-of-type:[&:not(:has(div))]:p-0', + 'only-of-type:[&:not(:has(div>div))]:inline', + 'only-of-type:[&:not(:has(div>div))]:*:inline', + '[&>span:last-child]:mr-2', // space after last item, for compact display + ), + label: cn( + 'text-gray-200', + 'inline', + 'mr-2', // space after label + ), nullValue: 'text-red-600', undefinedValue: 'text-red-600', - stringValue: 'text-amber-600', + noQuotesForStringValues: true, // quotes will be styled + stringValue: cn( + 'text-amber-600', + 'select-all', // entire string selected on click + // quotes from style, so they don't get selected + "before:content-['“'] before:text-gray-500", + "after:content-['”'] after:text-gray-500", + ), booleanValue: 'text-purple-600', - numberValue: 'text-teal-600', + numberValue: 'text-teal-200', otherValue: 'text-blue-600', - punctuation: 'text-gray-500 mr-1.5', - collapseIcon: - 'text-teal-200 text-[16px] p-1 mr-1.5 select-none cursor-pointer after:content-["▼"]', - expandIcon: - 'text-teal-200 text-[16px] p-1 mr-1.5 select-none cursor-pointer after:content-["▶"]', - collapsedContent: 'text-amber-600 mr-1.5 after:content-["..."] after:text-xs', + punctuation: cn( + 'text-gray-500', + 'inline text-sm', + 'has-[~div]:mr-2', // space if next item is a label + ), + collapseIcon: cn( + 'text-teal-600 text-lg', + "has-[~div>div:nth-of-type(2)]:after:content-['▼']", + 'leading-[0] w-0 inline-block relative -left-3', + ), + expandIcon: 'hidden', // use collapsedContent ellipsis to expand + collapsedContent: "text-teal-600 text-xs leading-[0] after:content-['•••'] after:mx-2", }} />
); }; + +const ExpandAllIconButton = ({ + expandAll, + setExpandAll, +}: { + expandAll?: boolean; + setExpandAll: (e: boolean) => void; +}) => ( + +); diff --git a/packages/ui/package.json b/packages/ui/package.json index 97f07a5177..030ba9334b 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -12,8 +12,8 @@ "build-storybook": "storybook build" }, "dependencies": { - "@penumbra-zone/getters": "workspace:*", "@penumbra-zone/bech32": "workspace:*", + "@penumbra-zone/getters": "workspace:*", "@penumbra-zone/perspective": "workspace:*", "@penumbra-zone/types": "workspace:*", "@radix-ui/react-checkbox": "^1.0.4", @@ -37,7 +37,7 @@ "humanize-duration": "^3.31.0", "lucide-react": "^0.354.0", "react-dom": "^18.2.0", - "react-json-view-lite": "^1.2.1", + "react-json-view-lite": "^1.3.0", "react-loader-spinner": "^6.1.6", "react-router-dom": "^6.22.3", "sonner": "1.4.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3421f5fdee..5709346ec5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -754,8 +754,8 @@ importers: specifier: ^18.2.0 version: 18.2.0(react@18.2.0) react-json-view-lite: - specifier: ^1.2.1 - version: 1.2.1(react@18.2.0) + specifier: ^1.3.0 + version: 1.3.0(react@18.2.0) react-loader-spinner: specifier: ^6.1.6 version: 6.1.6(react-dom@18.2.0)(react@18.2.0) @@ -14168,8 +14168,8 @@ packages: /react-is@18.2.0: resolution: {integrity: sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==} - /react-json-view-lite@1.2.1(react@18.2.0): - resolution: {integrity: sha512-Itc0g86fytOmKZoIoJyGgvNqohWSbh3NXIKNgH6W6FT9PC1ck4xas1tT3Rr/b3UlFXyA9Jjaw9QSXdZy2JwGMQ==} + /react-json-view-lite@1.3.0(react@18.2.0): + resolution: {integrity: sha512-aN1biKC5v4DQkmQBlZjuMFR09MKZGMPtIg+cut8zEeg2HXd6gl2gRy0n4HMacHf0dznQgo0SVXN7eT8zV3hEuQ==} engines: {node: '>=14'} peerDependencies: react: ^16.13.1 || ^17.0.0 || ^18.0.0