diff --git a/react-ui/package.json b/react-ui/package.json index 57c152ac44a6c74831a9c557480329db170957ad..951b1c2168a2315e80a03662f9de9c34a36f2fa3 100755 --- a/react-ui/package.json +++ b/react-ui/package.json @@ -10,6 +10,7 @@ "@fortawesome/react-fontawesome": "^0.2.2", "@reduxjs/toolkit": "^2.2.4", "bootstrap": "^5.3.3", + "dompurify": "^3.2.3", "i18next": "^23.11.5", "jwt-decode": "^4.0.0", "react": "^18.3.1", diff --git a/react-ui/src/shared/components/json_viewer/view/json_viewer.scss b/react-ui/src/shared/components/json_viewer/view/json_viewer.scss index d5b33cf98ed51ba9400c6167c9b437d5a5b1e2f8..20befdcbbcbebb9adac754c1b61dc62111dd197f 100755 --- a/react-ui/src/shared/components/json_viewer/view/json_viewer.scss +++ b/react-ui/src/shared/components/json_viewer/view/json_viewer.scss @@ -66,3 +66,7 @@ td .icon { min-width: 3em; } + +span.highlight { + background-color: darken(yellow, $amount: 0.5); +} diff --git a/react-ui/src/shared/components/json_viewer/view/json_viewer.view.tsx b/react-ui/src/shared/components/json_viewer/view/json_viewer.view.tsx index 635cad6eaf1e1d8259c78ee11708f0b9add39a10..720f75c7d26b4ffd181707c46ff42502168a72c0 100755 --- a/react-ui/src/shared/components/json_viewer/view/json_viewer.view.tsx +++ b/react-ui/src/shared/components/json_viewer/view/json_viewer.view.tsx @@ -1,5 +1,6 @@ import { faAlignRight, faPenToSquare, faTrashCan } from "@fortawesome/free-solid-svg-icons" import { FontAwesomeIcon } from "@fortawesome/react-fontawesome" +import DOMPurify from 'dompurify' import React, { Suspense, useMemo, useRef } from "react" import { Form, Table } from "react-bootstrap" import { useTranslation } from "react-i18next" @@ -29,22 +30,33 @@ export const JsonViewer = ({ json }: JsonViewerProbs) => { ) }, [breadcrumbs]) + const insertMarkTags = (text: string, search: string): string => { + const start = text.indexOf(search) + const end = start + search.length - const renderInner = (innerJson: JSON, nested: number = 0, path: string = "/network-instance/0/"): JSX.Element => { - return Object.entries(innerJson).map(([key, value]): JSX.Element => { + return DOMPurify.sanitize(text.substring(0, start)) + "<span class='highlight'>" + DOMPurify.sanitize(search) + "</span>" + DOMPurify.sanitize(text.substring(end, text.length)) + } + + const renderInner = (innerJson: JSON, nested: number = 0, parentKey: string = "", path: string = "/network-instance/0/"): JSX.Element => { + path += parentKey + (parentKey === "" ? "" : "/") + + return Object.entries(innerJson).map(([key, child]): JSX.Element => { + let collapsed = isCollapsed(key, nested); + + // display only keys and values that matches if (searchTerm !== "") { - path += key + "/" - const is = parameterizedJson.current.filter(_path => _path === path)[0] + const foundPaths = parameterizedJson.current.filter(_path => _path === path) - if (!is) { - return (<></>) - } + //collapsed = !collapsed ? !!foundPaths.length : collapsed + collapsed = !!foundPaths.length } - const isObject = value instanceof Object; - const readableValue = isObject ? '' : value; + const isObject = child instanceof Object; + let readableValue: string = isObject ? '' : DOMPurify.sanitize(child); - const collapsed = isCollapsed(key, nested); + if (searchTerm !== "" && readableValue.includes(searchTerm)) { + readableValue = insertMarkTags(readableValue, searchTerm) + } const icon = isObject ? <span className={collapsed ? 'fa-rotate-90' : ''}>></span> : <FontAwesomeIcon className="icon fa-rotate-180" icon={faAlignRight} size="xs" /> @@ -52,7 +64,21 @@ export const JsonViewer = ({ json }: JsonViewerProbs) => { // determine the margin-left: n indent let tabs = 0.0; for (let i = 0; i < nested; i++) { - tabs += 0.3; + tabs += 0.4; + } + + let concatenatedKey = key + let innerChild = child + while (innerChild.length === 1) { + const innerKey = Object.keys(innerChild)[0] + concatenatedKey += '/' + innerKey + innerChild = innerChild[innerKey] + } + + concatenatedKey = DOMPurify.sanitize(concatenatedKey) + + if (searchTerm !== "" && concatenatedKey.includes(searchTerm)) { + concatenatedKey = insertMarkTags(concatenatedKey, searchTerm) } return ( @@ -60,10 +86,10 @@ export const JsonViewer = ({ json }: JsonViewerProbs) => { <tr className={"list-item-td " + key + " " + nested + " " + (isObject ? 'object' : '')} data-copy-value={readableValue} - onClick={() => { isObject ? collapse(key, nested, value) : null }} + onClick={() => { isObject ? collapse(key, nested, child) : null }} > - <td style={{ marginLeft: tabs + 'em' }} className={"d-flex align-items-center "}>{icon}<span> {key}</span></td> - <td className="text-element text-truncate">{readableValue}</td> + <td style={{ marginLeft: tabs + 'em' }} className={"d-flex align-items-center "}>{icon}<span> <span dangerouslySetInnerHTML={{ __html: concatenatedKey }} /></span></td> + <td className="text-element text-truncate" dangerouslySetInnerHTML={{ __html: readableValue }}></td> <td className="text-end"> <div className="d-flex icons justify-content-end align-items-center"> <FontAwesomeIcon icon={faPenToSquare} size="sm" /> @@ -71,7 +97,7 @@ export const JsonViewer = ({ json }: JsonViewerProbs) => { </div> </td> </tr > - {isObject && collapsed ? renderInner(value, nested + 1, path) : ''} + {isObject && collapsed ? renderInner(innerChild, nested + 1, concatenatedKey, path) : ''} </React.Fragment > ) }) diff --git a/react-ui/src/shared/components/json_viewer/viewmodel/json_viewer.viewmodel.tsx b/react-ui/src/shared/components/json_viewer/viewmodel/json_viewer.viewmodel.tsx index 2ad0f0a6bad44e86ba4cc1815eddc3bf00b03f40..a1a9c89771d2939cbcbb1d2fddfa7707d34c1417 100644 --- a/react-ui/src/shared/components/json_viewer/viewmodel/json_viewer.viewmodel.tsx +++ b/react-ui/src/shared/components/json_viewer/viewmodel/json_viewer.viewmodel.tsx @@ -95,23 +95,30 @@ export const useJsonViewer = ({ json, search, container }: JsonViewerViewModelTy } } - const innerSearch = (json: Object, searchValue: string, path: string = "/"): boolean => { + const innerSearch = (json: Object, searchValue: string, parentKey: string = "", path: string = "/"): boolean => { let found = false; - path = JSON.parse(JSON.stringify(path)) + path += parentKey + (parentKey === "" ? "" : "/") for (const [key, childJson] of Object.entries(json)) { - path += key + "/" + // leaf if (!(childJson instanceof Object)) { const marked = key.includes(searchValue) || childJson.includes(searchValue); if (marked) { - parameterizedJsonMap.current.push(path) + parameterizedJsonMap.current.push(path + key) found = true } continue } - const marked = innerSearch(childJson, searchValue, JSON.parse(JSON.stringify(path))); + if (key.includes(searchValue)) { + parameterizedJsonMap.current.push(path + key) + found = true + } + + const marked = innerSearch(childJson, searchValue, key, path); + + // if found in some child leaf save the parent if (marked) { found = true parameterizedJsonMap.current.push(path) @@ -139,7 +146,9 @@ export const useJsonViewer = ({ json, search, container }: JsonViewerViewModelTy } return () => { - search.current!.removeEventListener('input', handleSearchInput) + if (search.current) { + search.current.removeEventListener('input', handleSearchInput) + } } }, []) diff --git a/react-ui/yarn.lock b/react-ui/yarn.lock index 5d2d968a7de0924327faef7de1466e23c6cd4f94..d60413ed6af69d4c1b18084ecf364ab95ed3c5dd 100755 --- a/react-ui/yarn.lock +++ b/react-ui/yarn.lock @@ -2676,7 +2676,7 @@ resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.3.tgz#6209321eb2c1712a7e7466422b8cb1fc0d9dd5d8" integrity sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw== -"@types/trusted-types@^2.0.2": +"@types/trusted-types@^2.0.2", "@types/trusted-types@^2.0.7": version "2.0.7" resolved "https://registry.yarnpkg.com/@types/trusted-types/-/trusted-types-2.0.7.tgz#baccb07a970b91707df3a3e8ba6896c57ead2d11" integrity sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw== @@ -4656,6 +4656,13 @@ domhandler@^4.0.0, domhandler@^4.2.0, domhandler@^4.3.1: dependencies: domelementtype "^2.2.0" +dompurify@^3.2.3: + version "3.2.3" + resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-3.2.3.tgz#05dd2175225324daabfca6603055a09b2382a4cd" + integrity sha512-U1U5Hzc2MO0oW3DF+G9qYN0aT7atAou4AgI0XjWz061nyBPbdxkfdhfy5uMgGn6+oLFCfn44ZGbdDqCzVmlOWA== + optionalDependencies: + "@types/trusted-types" "^2.0.7" + domutils@^1.7.0: version "1.7.0" resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.7.0.tgz#56ea341e834e06e6748af7a1cb25da67ea9f8c2a"