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