diff --git a/src/mqueryfront/package.json b/src/mqueryfront/package.json index 804e0bb8..50d4ddc3 100644 --- a/src/mqueryfront/package.json +++ b/src/mqueryfront/package.json @@ -23,6 +23,8 @@ "react": "^18.3.1", "react-copy-to-clipboard": "^5.1.0", "react-dom": "^18.3.1", + "react-draggable": "^4.4.6", + "react-html-parser": "^2.0.2", "react-router-dom": "^6.26.2", "react-select": "^5.8.1", "replace-js-pagination": "^1.0.5", diff --git a/src/mqueryfront/src/App.css b/src/mqueryfront/src/App.css index 1d67e328..1184edcf 100644 --- a/src/mqueryfront/src/App.css +++ b/src/mqueryfront/src/App.css @@ -87,3 +87,29 @@ .cursor-pointer { cursor: pointer; } + +.modal-container { + position: absolute; + offset-distance: 10px; + z-index: auto; + right: 5vw; +} + +.modal-block { + position: relative; + block-size: "fit-content"; + right: 5vw; +} + +.modal-dialog { + margin: 0; +} + +.modal-header:hover { + cursor: grab; +} + +.modal-table { + overflow-y: scroll; + max-height: 50vh; +} diff --git a/src/mqueryfront/src/components/ActionShowMatchContext.js b/src/mqueryfront/src/components/ActionShowMatchContext.js new file mode 100644 index 00000000..68e89552 --- /dev/null +++ b/src/mqueryfront/src/components/ActionShowMatchContext.js @@ -0,0 +1,193 @@ +import React, { useState, useRef, useEffect } from "react"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { faLightbulb } from "@fortawesome/free-solid-svg-icons"; +import Draggable from "react-draggable"; +import ReactHtmlParser from "react-html-parser"; + +const useClickOutsideModal = (ref, callback) => { + const handleClick = (event) => { + if (ref.current && !ref.current.contains(event.target)) { + // lose focus (higher z-index) only if other modal was clicked + const modals = document.querySelectorAll(".modal"); + const wasClicked = (modal) => modal.contains(event.target); + if (Array.from(modals).some(wasClicked)) { + callback(); + } + } + }; + + useEffect(() => { + document.addEventListener("click", handleClick); + + return () => { + document.removeEventListener("click", handleClick); + }; + }); +}; + +function base64ToHex(str64) { + return atob(str64) + .split("") + .map(function (aChar) { + return ("0" + aChar.charCodeAt(0).toString(16)).slice(-2); + }) + .join("") + .toUpperCase(); +} + +function base64ToSanitizedUtf8(str64) { + return atob(str64) + .split("") + .map(function (aChar) { + if (32 <= aChar.charCodeAt(0) && aChar.charCodeAt(0) < 127) { + return aChar; + } + return "."; + }) + .join(""); +} + +function insertEveryNChars(str, insert, n) { + return str + .split(new RegExp(`(.{${n}})`)) + .filter((x) => x) + .join(insert); +} + +function cellHTML(foundSample, lineLength, transformFunc) { + const hexBefore = transformFunc(foundSample["before"]); + const hexMatching = transformFunc(foundSample["matching"]); + const hexAfter = transformFunc(foundSample["after"]); + const basicStr = hexBefore + hexMatching + hexAfter; + const strWithBreakLines = insertEveryNChars(basicStr, "
", lineLength); + const breaksInBefore = Math.floor(hexBefore.length / lineLength); + const breaksInBeforeAndMatching = Math.floor( + (hexBefore.length + hexMatching.length) / lineLength + ); + const BoldOpeningTagIndex = hexBefore.length + 4 * breaksInBefore; + const BoldClosingTagIndex = + hexBefore.length + hexMatching.length + 4 * breaksInBeforeAndMatching; + let boldedStr = + strWithBreakLines.slice(0, BoldClosingTagIndex) + + "" + + strWithBreakLines.slice(BoldClosingTagIndex); + boldedStr = + boldedStr.slice(0, BoldOpeningTagIndex) + + "" + + boldedStr.slice(BoldOpeningTagIndex); + return boldedStr; +} + +const ActionShowMatchContext = (props) => { + const ref = useRef(null); + const [showModal, setShowModal] = useState(false); + const [focus, setFocus] = useState(true); + useClickOutsideModal(ref, () => setFocus(false)); + + const modalHeader = ( +
+
{`Match context for ${props.filename}`}
+
+ ); + + const tableRows = Object.keys(props.context).map((rulename, index) => { + const rulenameRows = Object.keys(props.context[rulename]).map( + (identifier) => { + const foundSample = props.context[rulename][identifier]; + return ( + <> + + + {identifier} + + + + {ReactHtmlParser( + cellHTML(foundSample, 20, base64ToHex) + )} + + + {ReactHtmlParser( + cellHTML(foundSample, 10, base64ToSanitizedUtf8) + )} + + + ); + } + ); + + return ( + <> + + + + {rulename} + + + {rulenameRows[0]} + + {rulenameRows.slice(1).map((row) => ( + {row} + ))} + + ); + }); + + const modalBody = ( +
+ {!Object.keys(props.context).length ? ( + "No context available" + ) : ( + + {tableRows} +
+ )} +
+ ); + + return ( +
+ + {showModal && ( + +
setFocus(true)} + > +
+
+
+ {modalHeader} + {modalBody} +
+
+
+
+
+ )} +
+ ); +}; + +export default ActionShowMatchContext; diff --git a/src/mqueryfront/src/query/QueryMatchesItem.js b/src/mqueryfront/src/query/QueryMatchesItem.js index 1cbf4525..719ecdd3 100644 --- a/src/mqueryfront/src/query/QueryMatchesItem.js +++ b/src/mqueryfront/src/query/QueryMatchesItem.js @@ -2,10 +2,11 @@ import React from "react"; import path from "path-browserify"; import ActionDownload from "../components/ActionDownload"; import ActionCopyToClipboard from "../components/ActionCopyToClipboard"; +import ActionShowMatchContext from "../components/ActionShowMatchContext"; const QueryMatchesItem = (props) => { const { match, download_url } = props; - const { matches, meta, file } = match; + const { matches, meta, file, context } = match; const fileBasename = path.basename(file); @@ -68,6 +69,12 @@ const QueryMatchesItem = (props) => { tooltipMessage="Copy file name to clipboard" /> + + + {matchBadges} {metadataBadges} diff --git a/src/mqueryfront/yarn.lock b/src/mqueryfront/yarn.lock index 51cf021b..3d76482e 100644 --- a/src/mqueryfront/yarn.lock +++ b/src/mqueryfront/yarn.lock @@ -1033,6 +1033,11 @@ classnames@^2.2.5: resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.5.1.tgz#ba774c614be0f016da105c858e7159eae8e7687b" integrity sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow== +clsx@^1.1.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.2.1.tgz#0ddc4a20a549b59c93a4116bb26f5294ca17dc12" + integrity sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg== + color-convert@^1.9.0: version "1.9.3" resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" @@ -1233,6 +1238,39 @@ dom-helpers@^5.0.1: "@babel/runtime" "^7.8.7" csstype "^3.0.2" +dom-serializer@0: + version "0.2.2" + resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.2.2.tgz#1afb81f533717175d478655debc5e332d9f9bb51" + integrity sha512-2/xPb3ORsQ42nHYiSunXkDjPLBaEj/xTwUO4B7XCZQTRk7EBtTOPaygh10YAAh2OI1Qrp6NWfpAhzswj0ydt9g== + dependencies: + domelementtype "^2.0.1" + entities "^2.0.0" + +domelementtype@1, domelementtype@^1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-1.3.1.tgz#d048c44b37b0d10a7f2a3d5fee3f4333d790481f" + integrity sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w== + +domelementtype@^2.0.1: + version "2.3.0" + resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.3.0.tgz#5c45e8e869952626331d7aab326d01daf65d589d" + integrity sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw== + +domhandler@^2.3.0: + version "2.4.2" + resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-2.4.2.tgz#8805097e933d65e85546f726d60f5eb88b44f803" + integrity sha512-JiK04h0Ht5u/80fdLMCEmV4zkNh2BcoMFBmZ/91WtYZ8qVXSKjiw7fXMgFPnHcSZgOo3XdinHvmnDUeMf5R4wA== + dependencies: + domelementtype "1" + +domutils@^1.5.1: + version "1.7.0" + resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.7.0.tgz#56ea341e834e06e6748af7a1cb25da67ea9f8c2a" + integrity sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg== + dependencies: + dom-serializer "0" + domelementtype "1" + dot-case@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/dot-case/-/dot-case-3.0.4.tgz#9b2b670d00a431667a8a75ba29cd1b98809ce751" @@ -1261,6 +1299,16 @@ encodeurl@~2.0.0: resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-2.0.0.tgz#7b8ea898077d7e409d3ac45474ea38eaf0857a58" integrity sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg== +entities@^1.1.1: + version "1.1.2" + resolved "https://registry.yarnpkg.com/entities/-/entities-1.1.2.tgz#bdfa735299664dfafd34529ed4f8522a275fea56" + integrity sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w== + +entities@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/entities/-/entities-2.2.0.tgz#098dc90ebb83d8dffa089d55256b351d34c4da55" + integrity sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A== + entities@^4.4.0: version "4.5.0" resolved "https://registry.yarnpkg.com/entities/-/entities-4.5.0.tgz#5d268ea5e7113ec74c4d033b79ea5a35a488fb48" @@ -1567,6 +1615,18 @@ html-entities@^2.4.0: resolved "https://registry.yarnpkg.com/html-entities/-/html-entities-2.5.2.tgz#201a3cf95d3a15be7099521620d19dfb4f65359f" integrity sha512-K//PSRMQk4FZ78Kyau+mZurHn3FH0Vwr+H36eE0rPbeYkRRi9YxceYPhuN60UwWorxyKHhqoAJl2OFKa4BVtaA== +htmlparser2@^3.9.0: + version "3.10.1" + resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-3.10.1.tgz#bd679dc3f59897b6a34bb10749c855bb53a9392f" + integrity sha512-IgieNijUMbkDovyoKObU1DUhm1iwNYE/fuifEoEHfd1oZKZDaONBSkal7Y01shxsM49R4XaMdGez3WnF9UfiCQ== + dependencies: + domelementtype "^1.3.1" + domhandler "^2.3.0" + domutils "^1.5.1" + entities "^1.1.1" + inherits "^2.0.1" + readable-stream "^3.1.1" + http-deceiver@^1.2.7: version "1.2.7" resolved "https://registry.yarnpkg.com/http-deceiver/-/http-deceiver-1.2.7.tgz#fa7168944ab9a519d337cb0bec7284dc3e723d87" @@ -2119,6 +2179,21 @@ react-dom@^18.3.1: loose-envify "^1.1.0" scheduler "^0.23.2" +react-draggable@^4.4.6: + version "4.4.6" + resolved "https://registry.yarnpkg.com/react-draggable/-/react-draggable-4.4.6.tgz#63343ee945770881ca1256a5b6fa5c9f5983fe1e" + integrity sha512-LtY5Xw1zTPqHkVmtM3X8MUOxNDOUhv/khTgBgrUvwaS064bwVvxT+q5El0uUFNx5IEPKXuRejr7UqLwBIg5pdw== + dependencies: + clsx "^1.1.1" + prop-types "^15.8.1" + +react-html-parser@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/react-html-parser/-/react-html-parser-2.0.2.tgz#6dbe1ddd2cebc1b34ca15215158021db5fc5685e" + integrity sha512-XeerLwCVjTs3njZcgCOeDUqLgNIt/t+6Jgi5/qPsO/krUWl76kWKXMeVs2LhY2gwM6X378DkhLjur0zUQdpz0g== + dependencies: + htmlparser2 "^3.9.0" + react-is@^16.13.1, react-is@^16.7.0: version "16.13.1" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" @@ -2189,7 +2264,7 @@ readable-stream@^2.0.1: string_decoder "~1.1.1" util-deprecate "~1.0.1" -readable-stream@^3.0.6: +readable-stream@^3.0.6, readable-stream@^3.1.1: version "3.6.2" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.2.tgz#56a9b36ea965c00c5a93ef31eb111a0f11056967" integrity sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==