Skip to content
Snippets Groups Projects
Commit 112113b4 authored by matthiasf's avatar matthiasf
Browse files

ui: implement json viewer search

parent 543cd2e4
Branches
Tags
2 merge requests!1162Draft: Ui integration,!1128UI: Implement yang model view
Pipeline #242016 failed
...@@ -10,6 +10,7 @@ ...@@ -10,6 +10,7 @@
"@fortawesome/react-fontawesome": "^0.2.2", "@fortawesome/react-fontawesome": "^0.2.2",
"@reduxjs/toolkit": "^2.2.4", "@reduxjs/toolkit": "^2.2.4",
"bootstrap": "^5.3.3", "bootstrap": "^5.3.3",
"dompurify": "^3.2.3",
"i18next": "^23.11.5", "i18next": "^23.11.5",
"jwt-decode": "^4.0.0", "jwt-decode": "^4.0.0",
"react": "^18.3.1", "react": "^18.3.1",
......
...@@ -66,3 +66,7 @@ td .icon { ...@@ -66,3 +66,7 @@ td .icon {
min-width: 3em; min-width: 3em;
} }
span.highlight {
background-color: darken(yellow, $amount: 0.5);
}
import { faAlignRight, faPenToSquare, faTrashCan } from "@fortawesome/free-solid-svg-icons" import { faAlignRight, faPenToSquare, faTrashCan } from "@fortawesome/free-solid-svg-icons"
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome" import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
import DOMPurify from 'dompurify'
import React, { Suspense, useMemo, useRef } from "react" import React, { Suspense, useMemo, useRef } from "react"
import { Form, Table } from "react-bootstrap" import { Form, Table } from "react-bootstrap"
import { useTranslation } from "react-i18next" import { useTranslation } from "react-i18next"
...@@ -29,22 +30,33 @@ export const JsonViewer = ({ json }: JsonViewerProbs) => { ...@@ -29,22 +30,33 @@ export const JsonViewer = ({ json }: JsonViewerProbs) => {
) )
}, [breadcrumbs]) }, [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 DOMPurify.sanitize(text.substring(0, start)) + "<span class='highlight'>" + DOMPurify.sanitize(search) + "</span>" + DOMPurify.sanitize(text.substring(end, text.length))
return Object.entries(innerJson).map(([key, value]): JSX.Element => { }
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 !== "") { if (searchTerm !== "") {
path += key + "/" const foundPaths = parameterizedJson.current.filter(_path => _path === path)
const is = parameterizedJson.current.filter(_path => _path === path)[0]
if (!is) { //collapsed = !collapsed ? !!foundPaths.length : collapsed
return (<></>) collapsed = !!foundPaths.length
}
} }
const isObject = value instanceof Object; const isObject = child instanceof Object;
const readableValue = isObject ? '' : value; let readableValue: string = isObject ? '' : DOMPurify.sanitize(child);
const collapsed = isCollapsed(key, nested); if (searchTerm !== "" && readableValue.includes(searchTerm)) {
readableValue = insertMarkTags(readableValue, searchTerm)
}
const icon = isObject ? const icon = isObject ?
<span className={collapsed ? 'fa-rotate-90' : ''}>&gt;</span> : <FontAwesomeIcon className="icon fa-rotate-180" icon={faAlignRight} size="xs" /> <span className={collapsed ? 'fa-rotate-90' : ''}>&gt;</span> : <FontAwesomeIcon className="icon fa-rotate-180" icon={faAlignRight} size="xs" />
...@@ -52,7 +64,21 @@ export const JsonViewer = ({ json }: JsonViewerProbs) => { ...@@ -52,7 +64,21 @@ export const JsonViewer = ({ json }: JsonViewerProbs) => {
// determine the margin-left: n indent // determine the margin-left: n indent
let tabs = 0.0; let tabs = 0.0;
for (let i = 0; i < nested; i++) { 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 ( return (
...@@ -60,10 +86,10 @@ export const JsonViewer = ({ json }: JsonViewerProbs) => { ...@@ -60,10 +86,10 @@ export const JsonViewer = ({ json }: JsonViewerProbs) => {
<tr <tr
className={"list-item-td " + key + " " + nested + " " + (isObject ? 'object' : '')} className={"list-item-td " + key + " " + nested + " " + (isObject ? 'object' : '')}
data-copy-value={readableValue} 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>&ensp;{key}</span></td> <td style={{ marginLeft: tabs + 'em' }} className={"d-flex align-items-center "}>{icon}<span>&ensp;<span dangerouslySetInnerHTML={{ __html: concatenatedKey }} /></span></td>
<td className="text-element text-truncate">{readableValue}</td> <td className="text-element text-truncate" dangerouslySetInnerHTML={{ __html: readableValue }}></td>
<td className="text-end"> <td className="text-end">
<div className="d-flex icons justify-content-end align-items-center"> <div className="d-flex icons justify-content-end align-items-center">
<FontAwesomeIcon icon={faPenToSquare} size="sm" /> <FontAwesomeIcon icon={faPenToSquare} size="sm" />
...@@ -71,7 +97,7 @@ export const JsonViewer = ({ json }: JsonViewerProbs) => { ...@@ -71,7 +97,7 @@ export const JsonViewer = ({ json }: JsonViewerProbs) => {
</div> </div>
</td> </td>
</tr > </tr >
{isObject && collapsed ? renderInner(value, nested + 1, path) : ''} {isObject && collapsed ? renderInner(innerChild, nested + 1, concatenatedKey, path) : ''}
</React.Fragment > </React.Fragment >
) )
}) })
......
...@@ -95,23 +95,30 @@ export const useJsonViewer = ({ json, search, container }: JsonViewerViewModelTy ...@@ -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; let found = false;
path = JSON.parse(JSON.stringify(path)) path += parentKey + (parentKey === "" ? "" : "/")
for (const [key, childJson] of Object.entries(json)) { for (const [key, childJson] of Object.entries(json)) {
path += key + "/" // leaf
if (!(childJson instanceof Object)) { if (!(childJson instanceof Object)) {
const marked = key.includes(searchValue) || childJson.includes(searchValue); const marked = key.includes(searchValue) || childJson.includes(searchValue);
if (marked) { if (marked) {
parameterizedJsonMap.current.push(path) parameterizedJsonMap.current.push(path + key)
found = true found = true
} }
continue 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) { if (marked) {
found = true found = true
parameterizedJsonMap.current.push(path) parameterizedJsonMap.current.push(path)
...@@ -139,7 +146,9 @@ export const useJsonViewer = ({ json, search, container }: JsonViewerViewModelTy ...@@ -139,7 +146,9 @@ export const useJsonViewer = ({ json, search, container }: JsonViewerViewModelTy
} }
return () => { return () => {
search.current!.removeEventListener('input', handleSearchInput) if (search.current) {
search.current.removeEventListener('input', handleSearchInput)
}
} }
}, []) }, [])
......
...@@ -2676,7 +2676,7 @@ ...@@ -2676,7 +2676,7 @@
resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.3.tgz#6209321eb2c1712a7e7466422b8cb1fc0d9dd5d8" resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.3.tgz#6209321eb2c1712a7e7466422b8cb1fc0d9dd5d8"
integrity sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw== 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" version "2.0.7"
resolved "https://registry.yarnpkg.com/@types/trusted-types/-/trusted-types-2.0.7.tgz#baccb07a970b91707df3a3e8ba6896c57ead2d11" resolved "https://registry.yarnpkg.com/@types/trusted-types/-/trusted-types-2.0.7.tgz#baccb07a970b91707df3a3e8ba6896c57ead2d11"
integrity sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw== integrity sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==
...@@ -4656,6 +4656,13 @@ domhandler@^4.0.0, domhandler@^4.2.0, domhandler@^4.3.1: ...@@ -4656,6 +4656,13 @@ domhandler@^4.0.0, domhandler@^4.2.0, domhandler@^4.3.1:
dependencies: dependencies:
domelementtype "^2.2.0" 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: domutils@^1.7.0:
version "1.7.0" version "1.7.0"
resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.7.0.tgz#56ea341e834e06e6748af7a1cb25da67ea9f8c2a" resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.7.0.tgz#56ea341e834e06e6748af7a1cb25da67ea9f8c2a"
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment