diff --git a/frontend/.prettierignore b/frontend/.prettierignore
index 8d73cf19ea..1963131606 100644
--- a/frontend/.prettierignore
+++ b/frontend/.prettierignore
@@ -5,4 +5,5 @@
**/*.svg
**/*.sh
-src/auto-import.d.ts
\ No newline at end of file
+src/auto-import.d.ts
+src/assets/js
\ No newline at end of file
diff --git a/frontend/package.json b/frontend/package.json
index 01ac7a2812..0caa4e6d3d 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -49,7 +49,6 @@
"echarts": "^5.4.3",
"hotbox-minder": "1.0.15",
"jsencrypt": "^3.3.2",
- "jsonpath-picker-vanilla": "^1.2.4",
"localforage": "^1.10.0",
"lodash-es": "^4.17.21",
"mitt": "^3.0.1",
diff --git a/frontend/src/assets/js/jsonpath-picker-vanilla/jsonpath-picker-vanilla.js b/frontend/src/assets/js/jsonpath-picker-vanilla/jsonpath-picker-vanilla.js
new file mode 100644
index 0000000000..70a4f8c117
--- /dev/null
+++ b/frontend/src/assets/js/jsonpath-picker-vanilla/jsonpath-picker-vanilla.js
@@ -0,0 +1,481 @@
+/* eslint-disable */
+
+
+function _typeof(obj) { if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; } return _typeof(obj); }
+
+/**
+ * Check if arg is either an array with at least 1 element, or a dict with at least 1 key
+ * @return boolean
+ */
+function isCollapsable(arg) {
+ return arg instanceof Object && Object.keys(arg).length > 0;
+}
+/**
+ * Check if a string represents a valid url
+ * @return boolean
+ */
+
+
+function isUrl(string) {
+ const regexp = /^(ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#:.?+=&%@!\-/]))?/;
+ return regexp.test(string);
+}
+/**
+ * Transform a json object into html representation
+ * @return string
+ */
+
+
+function json2html(json, options) {
+ let html = '';
+
+ if (typeof json === 'string') {
+ // Escape tags
+ const tmp = json.replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"');
+
+ if (json.includes('Number')) {
+ html += "".concat(json.replace(/Number\(([^)]+)\)/g, "$1"), "");
+ } else if (isUrl(tmp)) {
+ html += "").concat(tmp, "");
+ } else {
+ html += "\"".concat(tmp, "\"");
+ }
+ } else if (typeof json === 'number') {
+ html += "".concat(json, "");
+ } else if (typeof json === 'boolean') {
+ html += "".concat(json, "");
+ } else if (json === null) {
+ html += 'null';
+ } else if (json instanceof Array) {
+ if (json.length > 0) {
+ html += '[
';
+
+ for (let i = 0; i < json.length; i += 1) {
+ html += "- "); // Add toggle button if item is collapsable
+
+ if (isCollapsable(json[i])) {
+ html += '';
+ }
+
+ html += json2html(json[i], options); // Add comma if item is not last
+
+ if (i < json.length - 1) {
+ html += ',';
+ }
+
+ html += '
';
+ }
+
+ html += '
]';
+ } else {
+ html += '[]';
+ }
+ } else if (_typeof(json) === 'object') {
+ let keyCount = Object.keys(json).length;
+
+ if (keyCount > 0) {
+ html += '{';
+
+ for (const key in json) {
+ if (json.hasOwnProperty(key)) {
+ html += "- ");
+ const keyRepr = options.outputWithQuotes ? "\"".concat(key, "\"") : key; // Add toggle button if item is collapsable
+
+ if (isCollapsable(json[key])) {
+ html += "".concat(keyRepr, "");
+ } else {
+ html += keyRepr;
+ }
+
+ html += `&${ options.pickerIcon };`;
+ html += ": ".concat(json2html(json[key], options)); // Add comma if item is not last
+
+ keyCount -= 1;
+
+ if (keyCount > 0) {
+ html += ',';
+ }
+
+ html += '
';
+ }
+ }
+
+ html += '
}';
+ } else {
+ html += '{}';
+ }
+ }
+
+ return html;
+}
+/**
+ * Remove an event listener
+ * @param {String} event The event type
+ * @param {Node} elem The element to remove the event to (optional, defaults to window)
+ * @param {Function} callback The callback that ran on the event
+ * @param {Boolean} capture If true, forces bubbling on non-bubbling events
+ */
+
+
+function off(event, elem, callback, capture) {
+ let captureIntern = capture;
+ let callbackIntern = callback;
+ let elemIntern = elem;
+
+ if (typeof elem === 'function') {
+ captureIntern = callback;
+ callbackIntern = elem;
+ elemIntern = window;
+ }
+
+ captureIntern = !!captureIntern;
+ elemIntern = typeof elemIntern === 'string' ? document.querySelector(elemIntern) : elemIntern;
+ if (!elemIntern) return;
+ elemIntern.removeEventListener(event, callbackIntern, captureIntern);
+}
+/**
+ * Equivalent of JQuery $().siblings(sel) with callback features
+ *
+ * Retrieve all siblings/neighbors of a node
+ * Usage:
+ * - siblings(node, '.collapse', (sib) => { });
+ * - const sibs = siblings(node);
+ * - const sibs = siblings(node, '.collapse');
+ *
+ * @param {HTMLNode} el Element to apply siblings methods
+ * @param {String} sel CSS Selector
+ * @param {Function} callback (sib) => {}
+ */
+
+
+function siblings(el, sel, callback) {
+ const sibs = [];
+
+ for (let i = 0; i < el.parentNode.children.length; i += 1) {
+ const child = el.parentNode.children[i];
+
+ if (child !== el && typeof sel === 'string' && child.matches(sel)) {
+ sibs.push(child);
+ }
+ } // If a callback is passed, call it on each sibs
+
+
+ if (callback && typeof callback === 'function') {
+ for (let _i = 0; _i < sibs.length; _i += 1) {
+ callback(sibs[_i]);
+ }
+ }
+
+ return sibs;
+}
+/**
+ * Fire a click handler to the specified node.
+ * Event handlers can detect that the event was fired programatically
+ * by testing for a 'synthetic=true' property on the event object
+ * @param {HTMLNode} node The node to fire the event handler on.
+ */
+
+
+function fireClick(node) {
+ // Make sure we use the ownerDocument from the provided node to avoid cross-window problems
+ let doc;
+
+ if (node.ownerDocument) {
+ doc = node.ownerDocument;
+ } else if (node.nodeType === 9) {
+ // the node may be the document itself, nodeType 9 = DOCUMENT_NODE
+ doc = node;
+ } else {
+ throw new Error("Invalid node passed to fireEvent: ".concat(node.id));
+ }
+
+ if (node.dispatchEvent) {
+ const eventClass = 'MouseEvents';
+ const event = doc.createEvent(eventClass);
+ event.initEvent('click', true, true); // All events created as bubbling and cancelable.
+
+ event.synthetic = true;
+ node.dispatchEvent(event, true);
+ } else if (node.fireEvent) {
+ // IE-old school style, you can drop this if you don't need to support IE8 and lower
+ const _event = doc.createEventObject();
+
+ _event.synthetic = true; // allow detection of synthetic events
+
+ node.fireEvent('onclick', _event);
+ }
+}
+/**
+ * Check if an element is visible or not
+ * @param {HTMLNode} elem Element to check
+ * @returns {Boolean}
+ */
+
+
+function isHidden(elem) {
+ const width = elem.offsetWidth;
+ const height = elem.offsetHeight;
+ return width === 0 && height === 0 || window.getComputedStyle(elem).display === 'none';
+}
+/**
+ * Method use to retrieve parents of given element
+ * @param {HTMLNode} elem Element which we want parents
+ * @param {Strign} sel selector to filter parents (CSS selectors)
+ * @returns {Array}
+ */
+
+
+function getParents(elem, sel) {
+ const result = [];
+
+ for (let p = elem && elem.parentElement; p; p = p.parentElement) {
+ if (typeof sel === 'string' && p.matches(sel)) {
+ result.push(p);
+ }
+ }
+
+ return result;
+}
+
+function HandlerEventToggle(elm, event) {
+ // Change class
+ elm.classList.toggle('collapsed'); // Fetch every json-dict and json-array to toggle them
+
+ const subTarget = siblings(elm, 'ul.json-dict, ol.json-array', function (el) {
+ el.style.display = el.style.display === '' || el.style.display === 'block' ? 'none' : 'block';
+ }); // ForEach subtarget, previous siblings return array so we parse it
+
+ for (let i = 0; i < subTarget.length; i += 1) {
+ if (!isHidden(subTarget[i])) {
+ // Parse every siblings with '.json-placehoder' and remove them (previous add by else)
+ siblings(subTarget[i], '.json-placeholder', function (el) {
+ return el.parentNode.removeChild(el);
+ });
+ } else {
+ // count item in object / array
+ const childs = subTarget[i].children;
+ let count = 0;
+
+ for (let j = 0; j < childs.length; j += 1) {
+ if (childs[j].tagName === 'LI') {
+ count += 1;
+ }
+ }
+
+ const placeholder = count + (count > 1 ? ' items' : ' item'); // Append a placeholder
+
+ subTarget[i].insertAdjacentHTML('afterend', "".concat(placeholder, ""));
+ }
+ } // Prevent propagation
+
+
+ event.stopPropagation();
+ event.preventDefault();
+}
+
+function ToggleEventListener(event) {
+ let t = event.target;
+
+ while (t && t !== this) {
+ if (t.matches('a.json-toggle')) {
+ HandlerEventToggle.call(null, t, event);
+ event.stopPropagation();
+ event.preventDefault();
+ }
+
+ t = t.parentNode;
+ }
+}
+
+; // Simulate click on toggle button when placeholder is clicked
+
+function SimulateClickHandler(elm, event) {
+ siblings(elm, 'a.json-toggle', function (el) {
+ return fireClick(el);
+ });
+ event.stopPropagation();
+ event.preventDefault();
+}
+
+function SimulateClickEventListener(event) {
+ let t = event.target;
+
+ while (t && t !== this) {
+ if (t.matches('a.json-placeholder')) {
+ SimulateClickHandler.call(null, t, event);
+ }
+
+ t = t.parentNode;
+ }
+}
+
+function PickPathHandler(elm) {
+ if (targetList.length === 0) {
+ return;
+ }
+
+ const $parentsList = getParents(elm, 'li').reverse();
+ let pathSegments = [];
+
+ for (let i = 0; i < $parentsList.length; i += 1) {
+ let {key} = $parentsList[i].dataset;
+ const {keyType} = $parentsList[i].dataset;
+
+ if (keyType === 'object' && typeof key !== 'number' && options.processKeys && options.keyReplaceRegexPattern !== undefined) {
+ const keyReplaceRegex = new RegExp(options.keyReplaceRegexPattern, options.keyReplaceRegexFlags);
+ const keyReplacementText = options.keyReplacementText === undefined ? '' : options.keyReplacementText;
+ key = key.replace(keyReplaceRegex, keyReplacementText);
+ }
+
+ pathSegments.push({
+ key,
+ keyType
+ });
+ }
+
+ const quotes = {
+ none: '',
+ single: '\'',
+ "double": '"'
+ };
+ const quote = quotes[options.pathQuotesType];
+ pathSegments = pathSegments.map(function (segment, idx) {
+ const isBracketsNotation = options.pathNotation === 'brackets';
+ const isKeyForbiddenInDotNotation = !/^\w+$/.test(segment.key) || typeof segment.key === 'number';
+
+ if (segment.keyType === 'array' || segment.isKeyANumber) {
+ return "[".concat(segment.key, "]");
+ }
+
+ if (isBracketsNotation || isKeyForbiddenInDotNotation) {
+ return "[".concat(quote).concat(segment.key).concat(quote, "]");
+ }
+
+ if (idx > 0) {
+ return ".".concat(segment.key);
+ }
+
+ return segment.key;
+ });
+ const path = pathSegments.join('');
+
+ for (let _i2 = 0; _i2 < targetList.length; _i2 += 1) {
+ if (targetList[_i2].value !== undefined) {
+ targetList[_i2].value = path;
+ }
+ }
+}
+
+function PickEventListener(event) {
+ let t = event.target;
+
+ while (t && t !== this) {
+ if (t.matches('.pick-path')) {
+ PickPathHandler.call(null, t);
+ }
+
+ t = t.parentNode;
+ }
+} // Uniq id generator
+
+
+function uuidv4() {
+ function randomString(length, chars) {
+ let result = '';
+
+ for (let i = length; i > 0; --i) {
+ result += chars[Math.floor(Math.random() * chars.length)];
+ }
+
+ return result;
+ }
+
+ return randomString(32, 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ');
+}
+
+var targetList = [];
+var options = {};
+/**
+ * Plugin method
+ * @param source: Element
+ * @param json: a javascript object
+ * @param target: NodeListOf | Element | { value: String }[] | { value: String }
+ * @param opt: an optional options hash
+ */
+
+function jsonPathPicker(source, json, target, opt) {
+ options = opt || {};
+
+ if (!(source instanceof Element)) {
+ return 1;
+ }
+
+ if (target) {
+ if (target.length) {
+ targetList = target;
+ } else if (target.value) {
+ targetList = [target];
+ } else {
+ return 3;
+ }
+ } else {
+ return 3;
+ } // Add to source unique identifier
+
+
+ const uuid = uuidv4();
+ source.id = source.id ? "".concat(source.id, " ").concat(uuid) : uuid;
+ source.setAttribute('data-jsonpath-uniq-id', uuid);
+ options.pathQuotesType = options.pathQuotesType !== undefined ? options.pathQuotesType : 'single'; // Transform to HTML
+
+ options.pickerIcon = options.pickerIcon || '#x1f4cb';
+ let html = json2html(json, options);
+ if (isCollapsable(json)) html = "".concat(html); // Insert HTML in target DOM element
+
+ source.innerHTML = html; // Bind click on toggle buttons
+
+ off('click', source);
+ source.addEventListener('click', ToggleEventListener);
+ source.addEventListener('click', SimulateClickEventListener); // Bind picker only if user didn't diseable it
+
+ if (!options.WithoutPicker) {
+ source.addEventListener('click', PickEventListener);
+ } else {
+ // Remove every picker icon
+ const sourceSelector = source.getAttribute('data-jsonpath-uniq-id'); // Prevent affect other jp-picker
+
+ document.querySelectorAll("[id*='".concat(sourceSelector, "'] .pick-path")).forEach(function (el) {
+ return el.parentNode.removeChild(el);
+ });
+ }
+
+ if (options.outputCollapsed === true) {
+ // Trigger click to collapse all nodes
+ const elms = document.querySelectorAll('a.json-toggle');
+
+ for (let i = 0; i < elms.length; i += 1) {
+ fireClick(elms[i]);
+ }
+ }
+}
+/**
+ * Plugin clear method
+ * @param source: Element
+ */
+
+
+function clearJsonPathPicker(source) {
+ if (!(source instanceof Element)) {
+ return 1;
+ } // Remove event listener
+
+
+ source.removeEventListener('click', PickEventListener);
+ source.removeEventListener('click', ToggleEventListener);
+ source.removeEventListener('click', SimulateClickEventListener);
+}
+
+export default {
+ jsonPathPicker,
+ clearJsonPathPicker
+};
\ No newline at end of file
diff --git a/frontend/src/assets/js/jsonpath-picker-vanilla/jsonpath-picker.css b/frontend/src/assets/js/jsonpath-picker-vanilla/jsonpath-picker.css
new file mode 100644
index 0000000000..83205c95ae
--- /dev/null
+++ b/frontend/src/assets/js/jsonpath-picker-vanilla/jsonpath-picker.css
@@ -0,0 +1,55 @@
+/* stylelint-disable order/properties-order */
+ul.json-dict, ol.json-array {
+ list-style-type: none;
+ margin: 0 0 0 1px;
+ border-left: 1px dotted #cccccc;
+ padding-left: 2em;
+}
+.json-string {
+ color: #0b7500;
+}
+.json-literal {
+ color: #1a01cc;
+ font-weight: bold;
+}
+
+/* Toggle button */
+a.json-toggle {
+ position: relative;
+ color: inherit;
+ text-decoration: none;
+}
+a.json-toggle:focus {
+ outline: none;
+}
+a.json-toggle::before {
+ color: #aaaaaa;
+ content: '\25BC'; /* down arrow */
+ position: absolute;
+ display: inline-block;
+ width: 1em;
+ left: -1em;
+}
+a.json-toggle.collapsed::before {
+ content: '\25B6'; /* left arrow */
+}
+
+/* Collapsable placeholder links */
+a.json-placeholder {
+ color: #aaaaaa;
+ padding: 0 1em;
+ text-decoration: none;
+}
+a.json-placeholder:hover {
+ text-decoration: underline;
+}
+
+/* Copy path icon */
+.pick-path {
+ color: lightgray;
+ cursor: pointer;
+ margin-left: 3px;
+}
+.pick-path:hover {
+ color: darkgray;
+}
diff --git a/frontend/src/components/pure/jsonpath-picker/index.vue b/frontend/src/components/pure/jsonpath-picker/index.vue
index 236a71ac7f..9a1fe2e1ce 100644
--- a/frontend/src/components/pure/jsonpath-picker/index.vue
+++ b/frontend/src/components/pure/jsonpath-picker/index.vue
@@ -6,8 +6,9 @@
diff --git a/frontend/src/components/pure/jsonpath-picker/types.ts b/frontend/src/components/pure/jsonpath-picker/types.ts
new file mode 100644
index 0000000000..17c2bd72b0
--- /dev/null
+++ b/frontend/src/components/pure/jsonpath-picker/types.ts
@@ -0,0 +1,4 @@
+export interface XpathNode {
+ content: string;
+ xpath: string;
+}
diff --git a/frontend/src/components/pure/jsonpath-picker/xpath.vue b/frontend/src/components/pure/jsonpath-picker/xpath.vue
new file mode 100644
index 0000000000..fb224e125e
--- /dev/null
+++ b/frontend/src/components/pure/jsonpath-picker/xpath.vue
@@ -0,0 +1,117 @@
+
+
+
+
+