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"
+ ) : (
+
+ )}
+
+ );
+
+ 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==