diff --git a/react-ui/src/index.tsx b/react-ui/src/index.tsx index 48f57dcff5b972dc1eec466f219251c483c2b943..186138b891b54367e8c4d6d9f9de976ae8f13845 100755 --- a/react-ui/src/index.tsx +++ b/react-ui/src/index.tsx @@ -16,12 +16,6 @@ import { router } from './routes' import './shared/icons/icons' import { persistor, store } from './stores' -const installToastify = () => { - return ( - <ToastContainer /> - ) -}; - ReactDOM.createRoot(document.getElementById("root")).render( <React.StrictMode> <Provider store={store}> @@ -29,7 +23,7 @@ ReactDOM.createRoot(document.getElementById("root")).render( <I18nextProvider i18n={i18next}> <UtilsProvider> <MenuProvider> - {installToastify()} + <ToastContainer /> <RouterProvider router={router} /> </MenuProvider> </UtilsProvider> 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 649f57e398e7fd4ea1908d3b410ea52ed5cfda46..d5b33cf98ed51ba9400c6167c9b437d5a5b1e2f8 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 @@ -34,6 +34,10 @@ padding-top: 0 !important; padding-right: 5px !important; } + + & > .text-element { + max-width: 100px; + } } .list-item-td.object { 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 9462fa4dbcb34870255d1c41aa4dd58334e4831d..635cad6eaf1e1d8259c78ee11708f0b9add39a10 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,9 +1,7 @@ -import { faAlignRight, faCopy, 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 { useMenu } from "@provider/menu/menu.provider" -import { useUtils } from "@provider/utils.provider" -import React, { Suspense, useEffect, useMemo, useRef } from "react" -import { Table } from "react-bootstrap" +import React, { Suspense, useMemo, useRef } from "react" +import { Form, Table } from "react-bootstrap" import { useTranslation } from "react-i18next" import { useJsonViewer } from "../viewmodel/json_viewer.viewmodel" import './json_viewer.scss' @@ -13,38 +11,11 @@ type JsonViewerProbs = { } export const JsonViewer = ({ json }: JsonViewerProbs) => { - const { getSubset, breadcrumbs, isCollapsed, collapseable, collapse } = useJsonViewer(); - const { subscribe } = useMenu(); - const htmlContainer = useRef(null); const { t } = useTranslation('common'); - const { toClipboard } = useUtils(); + const htmlContainer = useRef(null); + const search = useRef<HTMLInputElement>(null); - useEffect(() => { - if (htmlContainer.current) { - const subscription = subscribe({ - target: htmlContainer.current, - actions: [ - { - key: t('json_viewer.copy'), - icon: faCopy, - action: (clickedElement) => { - let parent = clickedElement; - while (parent && parent.tagName !== 'TR') { - parent = parent.parentNode; - } - - const copyValue = parent.dataset.copyValue - toClipboard(copyValue) - } - } - ] - }) - - return () => { - subscription.unsubscribe(); - } - } - }, []) + const { getSubset, breadcrumbs, isCollapsed, collapseable, collapse, parameterizedJson, searchTerm } = useJsonViewer({ json, search, container: htmlContainer }); const breadcrumbHTML = useMemo(() => { return ( @@ -59,8 +30,17 @@ export const JsonViewer = ({ json }: JsonViewerProbs) => { }, [breadcrumbs]) - const renderInner = (innerJson: JSON, nested: number = 0): JSX.Element => { + const renderInner = (innerJson: JSON, nested: number = 0, path: string = "/network-instance/0/"): JSX.Element => { return Object.entries(innerJson).map(([key, value]): JSX.Element => { + if (searchTerm !== "") { + path += key + "/" + const is = parameterizedJson.current.filter(_path => _path === path)[0] + + if (!is) { + return (<></>) + } + } + const isObject = value instanceof Object; const readableValue = isObject ? '' : value; @@ -69,7 +49,6 @@ export const JsonViewer = ({ json }: JsonViewerProbs) => { const icon = isObject ? <span className={collapsed ? 'fa-rotate-90' : ''}>></span> : <FontAwesomeIcon className="icon fa-rotate-180" icon={faAlignRight} size="xs" /> - // determine the margin-left: n indent let tabs = 0.0; for (let i = 0; i < nested; i++) { @@ -78,9 +57,13 @@ export const JsonViewer = ({ json }: JsonViewerProbs) => { return ( <React.Fragment key={`${nested}-${key}`}> - <tr className={"list-item-td " + key + " " + nested + " " + (isObject ? 'object' : '')} data-copy-value={readableValue} onClick={() => { isObject ? collapse(key, nested, value) : null }} > + <tr + className={"list-item-td " + key + " " + nested + " " + (isObject ? 'object' : '')} + data-copy-value={readableValue} + onClick={() => { isObject ? collapse(key, nested, value) : null }} + > <td style={{ marginLeft: tabs + 'em' }} className={"d-flex align-items-center "}>{icon}<span> {key}</span></td> - <td>{readableValue}</td> + <td className="text-element text-truncate">{readableValue}</td> <td className="text-end"> <div className="d-flex icons justify-content-end align-items-center"> <FontAwesomeIcon icon={faPenToSquare} size="sm" /> @@ -88,13 +71,12 @@ export const JsonViewer = ({ json }: JsonViewerProbs) => { </div> </td> </tr > - {isObject && collapsed ? renderInner(value, nested + 1) : ''} + {isObject && collapsed ? renderInner(value, nested + 1, path) : ''} </React.Fragment > ) }) } - const renderJson = (json: JSON): JSX.Element => { return ( <Table className="list-group-tr"> @@ -117,10 +99,21 @@ export const JsonViewer = ({ json }: JsonViewerProbs) => { </Suspense> </> ) - }, [json, collapseable]) + }, [json, collapseable, searchTerm]) + + const searchHTML = () => { + return ( + <> + <Form.Group controlId='device.search' className='p-0 mx-1 pt-2'> + <Form.Control type="text" placeholder={t('device.search.placeholder')} ref={search} /> + </Form.Group> + </> + ) + } return ( <div ref={htmlContainer}> + {searchHTML()} {breadcrumbHTML} {hierarchyHTML} </div> 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 d7ccd5d5a0dceb88248e3d000e15c80b9c07b522..2ad0f0a6bad44e86ba4cc1815eddc3bf00b03f40 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 @@ -1,4 +1,9 @@ +import { faCopy } from "@fortawesome/free-solid-svg-icons"; import { useAppDispatch, useAppSelector } from "@hooks"; +import { useMenu } from "@provider/menu/menu.provider"; +import { useUtils } from "@provider/utils.provider"; +import React, { MutableRefObject, useEffect, useMemo, useRef, useState } from "react"; +import { useTranslation } from "react-i18next"; import { compareIdentifier, toggleCollapse } from "../reducer/json_viewer.reducer"; export enum CollapseValues { @@ -7,10 +12,25 @@ export enum CollapseValues { TRUE } +interface JsonViewerViewModelType { + json: JSON, + search: React.RefObject<HTMLInputElement>, + container: React.RefObject<HTMLElement> +} + +export const MARKED = "?marked" -export const useJsonViewer = () => { +export const useJsonViewer = ({ json, search, container }: JsonViewerViewModelType) => { const { breadcrumbs, collapseContainer } = useAppSelector(state => state.json_viwer) const dispatch = useAppDispatch(); + const [searchTerm, setSearchTerm] = useState(''); + const { toClipboard } = useUtils(); + const { t } = useTranslation('common'); + const { subscribe } = useMenu(); + + // Map that contains a filtered key list with all keys that are found by the searchTerm + // The number represents + const parameterizedJsonMap = useRef<Array<string>>([]); const getSubset = (json: JSON) => { @@ -42,14 +62,94 @@ export const useJsonViewer = () => { if (keys.length === 1) { collapse(keys[0], nested + 1, json[keys[0]], CollapseValues.TRUE) } + } + + const handleSearchInput = () => { + setSearchTerm(search.current!.value) + } + + const registerMenuOptions = () => { + if (container.current) { + const subscription = subscribe({ + target: container.current, + actions: [ + { + key: t('json_viewer.copy'), + icon: faCopy, + action: (clickedElement) => { + let parent = clickedElement; + while (parent && parent.tagName !== 'TR') { + parent = parent.parentNode; + } + + const copyValue = parent.dataset.copyValue + toClipboard(copyValue) + } + } + ] + }) + + return () => { + subscription.unsubscribe(); + } + } + } + + const innerSearch = (json: Object, searchValue: string, path: string = "/"): boolean => { + let found = false; + path = JSON.parse(JSON.stringify(path)) + + for (const [key, childJson] of Object.entries(json)) { + path += key + "/" + if (!(childJson instanceof Object)) { + const marked = key.includes(searchValue) || childJson.includes(searchValue); + if (marked) { + parameterizedJsonMap.current.push(path) + found = true + } + + continue + } + + const marked = innerSearch(childJson, searchValue, JSON.parse(JSON.stringify(path))); + if (marked) { + found = true + parameterizedJsonMap.current.push(path) + } + } + return found } + const parameterizedJson = useMemo<MutableRefObject<Array<string>>>(() => { + parameterizedJsonMap.current = [] + if (searchTerm === "") { + return json + } + + innerSearch(json, searchTerm); + return parameterizedJsonMap + }, [searchTerm]) + + useEffect(() => { + registerMenuOptions(); + + if (search.current) { + search.current.addEventListener('input', handleSearchInput) + } + + return () => { + search.current!.removeEventListener('input', handleSearchInput) + } + }, []) + return { getSubset, breadcrumbs, collapseable: collapseContainer, isCollapsed, - collapse + collapse, + searchTerm, + parameterizedJson } } \ No newline at end of file diff --git a/react-ui/src/shared/helper/hash.ts b/react-ui/src/shared/helper/hash.ts new file mode 100644 index 0000000000000000000000000000000000000000..e185c87a5704a9f83f15b31a3d479a1e316d6dca --- /dev/null +++ b/react-ui/src/shared/helper/hash.ts @@ -0,0 +1,11 @@ +export const stringToHash = (text: string): number => { + let hash = 0; + + if (text.length === 0) return hash; + + for (const char of text) { + hash ^= char.charCodeAt(0); + } + + return hash; +} \ No newline at end of file