Skip to content

Commit

Permalink
Refactor json viewer (voxel51#4278)
Browse files Browse the repository at this point in the history
* remove unused jsonview file

* remove dep on searchable-json-view and bring in new json viewer

* add new json viewer with highlighting

* handle esc to clear search term and close json view

* linting

---------

Co-authored-by: Benjamin Kane <ben@voxel51.com>
  • Loading branch information
sashankaryal and benjaminpkane authored Apr 17, 2024
1 parent deaf08c commit de5b773
Show file tree
Hide file tree
Showing 9 changed files with 196 additions and 244 deletions.
1 change: 1 addition & 0 deletions app/packages/components/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
"@mui/icons-material": "^5.10.2",
"@mui/material": "^5.9.0",
"@react-spring/web": "^9.7.3",
"@textea/json-viewer": "^3.4.1",
"classnames": "^2.3.1",
"framer-motion": "^6.2.7",
"path-to-regexp": "^6.2.0",
Expand Down
72 changes: 48 additions & 24 deletions app/packages/components/src/components/JSONPanel/JSONPanel.tsx
Original file line number Diff line number Diff line change
@@ -1,32 +1,60 @@
/**
* Copyright 2017-2024, Voxel51, Inc.
*/
import { useState } from "react";
import { Copy as CopyIcon, Close as CloseIcon } from "@fiftyone/components";
import { Close as CloseIcon, Copy as CopyIcon } from "@fiftyone/components";
import CloseRoundedIcon from "@mui/icons-material/CloseRounded";
import { useColorScheme } from "@mui/material";
import { JsonViewer } from "@textea/json-viewer";
import React, { useEffect, useMemo, useState } from "react";
import { KeyRendererWrapper, getValueRenderersForSearch } from "./highlight";
import {
lookerCloseJSON,
lookerCopyJSON,
lookerJSONPanel,
} from "./json.module.css";
import {
lookerPanel,
lookerPanelContainer,
lookerPanelVerticalContainer,
searchCloseIcon,
searchContainer,
searchInput,
copyBtnClass,
searchCloseIcon,
} from "./panel.module.css";
import {
lookerCopyJSON,
lookerCloseJSON,
lookerJSONPanel,
} from "./json.module.css";
import ReactJson from "searchable-react-json-view";
import { useColorScheme } from "@mui/material";

export default function JSONPanel({ containerRef, onClose, onCopy, json }) {
const parsed = JSON.parse(json);
const { mode } = useColorScheme();
const isDarkMode = mode === "dark";
const [searchTerm, setSearchTerm] = useState<string>("");

useEffect(() => {
const handleEsc = (e: KeyboardEvent) => {
if (e.key === "Escape") {
if (searchTerm) {
setSearchTerm("");
return;
}

onClose();
}
};

window.addEventListener("keydown", handleEsc);
return () => {
window.removeEventListener("keydown", handleEsc);
};
}, [searchTerm, onClose]);

const keyRenderer = useMemo(
() => KeyRendererWrapper(searchTerm),
[searchTerm]
);

const valuesRenderer = useMemo(
() => getValueRenderersForSearch(searchTerm),
[searchTerm]
);

return (
<div
ref={containerRef}
Expand All @@ -51,26 +79,22 @@ export default function JSONPanel({ containerRef, onClose, onCopy, json }) {
/>
)}
</div>
<ReactJson
highlightSearch={searchTerm}
src={parsed}
theme={`ashes${!isDarkMode ? ":inverted" : ""}`}
<JsonViewer
value={parsed}
rootName={false}
objectSortKeys={true}
indentWidth={2}
keyRenderer={keyRenderer}
valueTypes={valuesRenderer}
quotesOnKeys={false}
theme={isDarkMode ? "dark" : "light"}
style={{
padding: "1rem 1rem 2rem 1rem",
overflowX: "scroll",
maxWidth: "60vw",
minWidth: "60vw",
minHeight: "70vh",
}}
iconStyle="square"
indentWidth={2}
customCopyIcon={<CopyIcon style={{ fontSize: "11px" }} />}
customCopiedIcon={
<CopyIcon
className={copyBtnClass}
style={{ fontSize: "11px" }}
/>
}
/>
</div>
)}
Expand Down
124 changes: 124 additions & 0 deletions app/packages/components/src/components/JSONPanel/highlight.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import { DataItemProps, defineEasyType } from "@textea/json-viewer";
import React, { useMemo } from "react";

const HighlightedText = ({
text,
searchTerm,
}: {
text: string | number;
searchTerm: string;
}) => {
const parts = useMemo(
() => String(text ?? "").split(searchTerm),
[text, searchTerm]
);

if (!String(text).includes(searchTerm)) {
return <>{text}</>;
}

return (
<>
{parts.map((part, index) => (
<React.Fragment key={index}>
{part}
{index !== parts.length - 1 && (
<span style={{ backgroundColor: "yellow", color: "black" }}>
{searchTerm}
</span>
)}
</React.Fragment>
))}
</>
);
};

export const KeyRenderer = (props: DataItemProps & { searchTerm: string }) => {
const { searchTerm, path } = props;
const leafPath = String(path.at(-1));

if (leafPath?.includes(searchTerm)) {
return <HighlightedText text={leafPath} searchTerm={searchTerm} />;
}

return <>{leafPath}</>;
};

export const KeyRendererWrapper = (searchTerm: string) => {
const wrapper = (props: DataItemProps) => {
return <KeyRenderer searchTerm={searchTerm} {...props} />;
};

// show all keys (required in runtime by json-viewer)
wrapper.when = () => true;
return wrapper;
};

const getHighlightedComponentString = (searchTerm: string) =>
defineEasyType({
is: (value) => {
if (searchTerm?.length === 0 || typeof value !== "string") {
return false;
}

return String(value).includes(searchTerm);
},
type: "string",
colorKey: "base09",
Renderer: (props) => {
const value = props.value as string;
return <HighlightedText text={`"${value}"`} searchTerm={searchTerm} />;
},
});

const isInt = (n: number) => n % 1 === 0;

const getHighlightedComponentFloat = (searchTerm: string) =>
defineEasyType({
is: (value) => {
if (
searchTerm?.length === 0 ||
typeof value !== "number" ||
isInt(value) ||
isNaN(value)
) {
return false;
}

return String(value).includes(searchTerm);
},
type: "float",
colorKey: "base0B",
Renderer: (props) => {
const value = props.value as number;
return <HighlightedText text={value} searchTerm={searchTerm} />;
},
});

const getHighlightedComponentInt = (searchTerm: string) =>
defineEasyType({
is: (value) => {
if (
searchTerm?.length === 0 ||
typeof value !== "number" ||
isNaN(value) ||
!isInt(value)
) {
return false;
}

return String(value).includes(searchTerm);
},
type: "int",
colorKey: "base0F",
Renderer: (props) => {
const value = props.value as number;
return <HighlightedText text={value} searchTerm={searchTerm} />;
},
});

export const getValueRenderersForSearch = (searchTerm: string) => [
getHighlightedComponentString(searchTerm),
getHighlightedComponentFloat(searchTerm),
getHighlightedComponentInt(searchTerm),
];
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/**
* Copyright 2017-2022, Voxel51, Inc.
* Copyright 2017-2024, Voxel51, Inc.
*/

.lookerJSONPanel {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/**
* Copyright 2017-2022, Voxel51, Inc.
* Copyright 2017-2024, Voxel51, Inc.
*/

.lookerPanel {
Expand Down
1 change: 0 additions & 1 deletion app/packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,6 @@
"recoil-relay": "^0.1.0",
"remark-gfm": "^3.0.1",
"resize-observer-polyfill": "^1.5.1",
"searchable-react-json-view": "^0.0.8",
"styled-components": "^6.1.8",
"uuid": "^8.3.2",
"xstate": "^4.14.0"
Expand Down
55 changes: 0 additions & 55 deletions app/packages/core/src/plugins/SchemaIO/components/JSONView.tsx

This file was deleted.

1 change: 0 additions & 1 deletion app/packages/core/src/plugins/SchemaIO/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ export { default as HelpTooltip } from "./HelpTooltip";
export { default as HiddenView } from "./HiddenView";
export { default as ImageView } from "./ImageView";
export { default as InferredView } from "./InferredView";
export { default as JSONView } from "./JSONView";
export { default as KeyValueView } from "./KeyValueView";
export { default as LabelValueView } from "./LabelValueView";
export { default as LinkView } from "./LinkView";
Expand Down
Loading

0 comments on commit de5b773

Please sign in to comment.