diff --git a/src/server/webapp/static/agents-graph.js b/src/server/webapp/static/agents-graph.js
index 48d2dd0..d350ea2 100644
--- a/src/server/webapp/static/agents-graph.js
+++ b/src/server/webapp/static/agents-graph.js
@@ -2,9 +2,11 @@ let agentData = [];
let isCyInitialized = false;
let cy;
+
function initializeCytoscape() {
if (isCyInitialized) return;
+ // cytoscape.use(cxtmenu);
cy = cytoscape({
container: document.getElementById('cyto-graph'),
@@ -58,7 +60,8 @@ function initializeCytoscape() {
'width' : 3,
'line-color' : '#ccc',
'target-arrow-color': '#ccc',
- 'target-arrow-shape': 'triangle'
+ 'target-arrow-shape': 'triangle',
+ 'curve-style': 'round-segments'
}
},
{
@@ -80,7 +83,7 @@ document.addEventListener('DOMContentLoaded', function () {
document.body.addEventListener('htmx:afterSwap', function (event) {
if (event.target.id === 'agentList') {
- initializeCytoscape();
+ // initializeCytoscape();
loadGraphData();
}
});
@@ -139,10 +142,66 @@ async function updateGraph(agentData) {
/* --- relayout --------------------------------------------------- */
cy.layout({ name: 'grid', rows: 2 }).run();
+
+ /* --- CxtMenu --- */
+ if (window.nodeContextMenu) {
+ window.nodeContextMenu.destroy();
+ }
+ window.nodeContextMenu = cy.cxtmenu({
+ selector: 'node',
+ commands: [
+ {
+ content: 'Details',
+ select: function (ele) {
+ const data = ele.data();
+ alert(`Node Details:\n\nName: ${data.name}\nID: ${data.id}\nType: ${data.type}\nIP: ${data.ip}`);
+ }
+ },
+ {
+ content: 'Ping',
+ select: function(ele) {
+ const ip = ele.data().ip;
+ console.log(`Ping to ${ip} triggered!`);
+ }
+ }
+ ],
+ menuRadius: 100,
+ fillColor: `rgba(0, 0, 0, 0.75)`,
+ activeFillColor: `rgba(255, 255, 255, 0.3)`,
+ activePadding: 10,
+ indicatorSize: 18,
+ separatorWidth: 3,
+ spotlightPadding: 4,
+ minSpotlightRadius: 20,
+ maxSpotlightRadius: 40,
+ openMenuEvents: 'cxttap',
+ });
+
+ window.edgeContextMenu = cy.cxtmenu({
+ selector: 'edge',
+ commands: [
+ {
+ content: 'View Info',
+ select: function (ele) {
+ const source = ele.source().data().name;
+ const target = ele.target().data().name;
+ alert(`Edge from "${source}" to "${target}"`);
+ }
+ },
+ {
+ content: 'Delete Edge',
+ select: function (ele) {
+ ele.remove();
+ }
+ }
+ ],
+ menuRadius: 100,
+ });
+
}
async function fetchData() {
- const url = "http://localhost:3333/agents";
+ const url = "/agents";
try {
const response = await fetch(url, {headers: {Accept: 'application/json'}});
if (!response.ok) {
@@ -160,14 +219,19 @@ async function fetchData() {
async function loadGraphData() {
// console.log("Function loadGraphData()");
+ var agentDataCurrent = ""
+ if (agentData.length > 0) {
+ agentDataCurrent = JSON.stringify(agentData);
+ }
// Fetch agent data asynchronously
agentData = await fetchData();
- // Check if the data is valid
- // console.log('Extracted agent data:', agentData);
+ console.log('Extracted agent data:', agentData);
// Only update the graph if agent data is available
- if (agentData && agentData.length > 0) {
+ if (JSON.stringify(agentData) != agentDataCurrent) {
+ // if (agentData && agentData.length > 0) {
+ console.log("updating graph");
await updateGraph(agentData);
} else {
console.log('No agent data found or extracted.');
diff --git a/src/server/webapp/static/cytoscape-cxtmenu.js b/src/server/webapp/static/cytoscape-cxtmenu.js
new file mode 100644
index 0000000..82b1440
--- /dev/null
+++ b/src/server/webapp/static/cytoscape-cxtmenu.js
@@ -0,0 +1,849 @@
+(function webpackUniversalModuleDefinition(root, factory) {
+ if(typeof exports === 'object' && typeof module === 'object')
+ module.exports = factory();
+ else if(typeof define === 'function' && define.amd)
+ define([], factory);
+ else if(typeof exports === 'object')
+ exports["cytoscapeCxtmenu"] = factory();
+ else
+ root["cytoscapeCxtmenu"] = factory();
+})(this, function() {
+return /******/ (function(modules) { // webpackBootstrap
+/******/ // The module cache
+/******/ var installedModules = {};
+/******/
+/******/ // The require function
+/******/ function __webpack_require__(moduleId) {
+/******/
+/******/ // Check if module is in cache
+/******/ if(installedModules[moduleId]) {
+/******/ return installedModules[moduleId].exports;
+/******/ }
+/******/ // Create a new module (and put it into the cache)
+/******/ var module = installedModules[moduleId] = {
+/******/ i: moduleId,
+/******/ l: false,
+/******/ exports: {}
+/******/ };
+/******/
+/******/ // Execute the module function
+/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
+/******/
+/******/ // Flag the module as loaded
+/******/ module.l = true;
+/******/
+/******/ // Return the exports of the module
+/******/ return module.exports;
+/******/ }
+/******/
+/******/
+/******/ // expose the modules object (__webpack_modules__)
+/******/ __webpack_require__.m = modules;
+/******/
+/******/ // expose the module cache
+/******/ __webpack_require__.c = installedModules;
+/******/
+/******/ // identity function for calling harmony imports with the correct context
+/******/ __webpack_require__.i = function(value) { return value; };
+/******/
+/******/ // define getter function for harmony exports
+/******/ __webpack_require__.d = function(exports, name, getter) {
+/******/ if(!__webpack_require__.o(exports, name)) {
+/******/ Object.defineProperty(exports, name, {
+/******/ configurable: false,
+/******/ enumerable: true,
+/******/ get: getter
+/******/ });
+/******/ }
+/******/ };
+/******/
+/******/ // getDefaultExport function for compatibility with non-harmony modules
+/******/ __webpack_require__.n = function(module) {
+/******/ var getter = module && module.__esModule ?
+/******/ function getDefault() { return module['default']; } :
+/******/ function getModuleExports() { return module; };
+/******/ __webpack_require__.d(getter, 'a', getter);
+/******/ return getter;
+/******/ };
+/******/
+/******/ // Object.prototype.hasOwnProperty.call
+/******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };
+/******/
+/******/ // __webpack_public_path__
+/******/ __webpack_require__.p = "";
+/******/
+/******/ // Load entry module and return exports
+/******/ return __webpack_require__(__webpack_require__.s = 4);
+/******/ })
+/************************************************************************/
+/******/ ([
+/* 0 */
+/***/ (function(module, exports, __webpack_require__) {
+
+"use strict";
+
+
+var defaults = __webpack_require__(2);
+var assign = __webpack_require__(1);
+
+var _require = __webpack_require__(3),
+ removeEles = _require.removeEles,
+ setStyles = _require.setStyles,
+ createElement = _require.createElement,
+ getPixelRatio = _require.getPixelRatio,
+ getOffset = _require.getOffset;
+
+var cxtmenu = function cxtmenu(params) {
+ var options = assign({}, defaults, params);
+ var cy = this;
+ var container = cy.container();
+ var target = void 0;
+
+ var data = {
+ options: options,
+ handlers: [],
+ container: createElement({ class: 'cxtmenu' })
+ };
+
+ var wrapper = data.container;
+ var parent = createElement();
+ var canvas = createElement({ tag: 'canvas' });
+ var commands = [];
+ var c2d = canvas.getContext('2d');
+
+ var r = 100; // defailt radius;
+ var containerSize = (r + options.activePadding) * 2;
+ var activeCommandI = void 0;
+ var offset = void 0;
+
+ container.insertBefore(wrapper, container.firstChild);
+ wrapper.appendChild(parent);
+ parent.appendChild(canvas);
+
+ setStyles(wrapper, {
+ position: 'absolute',
+ zIndex: options.zIndex,
+ userSelect: 'none',
+ pointerEvents: 'none' // prevent events on menu in modern browsers
+ });
+
+ // prevent events on menu in legacy browsers
+ ['mousedown', 'mousemove', 'mouseup', 'contextmenu'].forEach(function (evt) {
+ wrapper.addEventListener(evt, function (e) {
+ e.preventDefault();
+
+ return false;
+ });
+ });
+
+ setStyles(parent, {
+ display: 'none',
+ width: containerSize + 'px',
+ height: containerSize + 'px',
+ position: 'absolute',
+ zIndex: 1,
+ marginLeft: -options.activePadding + 'px',
+ marginTop: -options.activePadding + 'px',
+ userSelect: 'none'
+ });
+
+ canvas.width = containerSize;
+ canvas.height = containerSize;
+
+ function createMenuItems(r, rs) {
+ removeEles('.cxtmenu-item', parent);
+ var dtheta = 2 * Math.PI / commands.length;
+ var theta1 = Math.PI / 2;
+ var theta2 = theta1 + dtheta;
+
+ for (var i = 0; i < commands.length; i++) {
+ var command = commands[i];
+
+ var midtheta = (theta1 + theta2) / 2;
+ var rx1 = (r + rs) / 2 * Math.cos(midtheta);
+ var ry1 = (r + rs) / 2 * Math.sin(midtheta);
+
+ // Arbitrary multiplier to increase the sizing of the space
+ // available for the item.
+ var width = 1 * Math.abs((r - rs) * Math.cos(midtheta));
+ var height = 1 * Math.abs((r - rs) * Math.sin(midtheta));
+ width = Math.max(width, height);
+
+ var item = createElement({ class: 'cxtmenu-item' });
+ setStyles(item, {
+ color: options.itemColor,
+ cursor: 'default',
+ display: 'table',
+ 'text-align': 'center',
+ //background: 'red',
+ position: 'absolute',
+ 'text-shadow': '-1px -1px 2px ' + options.itemTextShadowColor + ', 1px -1px 2px ' + options.itemTextShadowColor + ', -1px 1px 2px ' + options.itemTextShadowColor + ', 1px 1px 1px ' + options.itemTextShadowColor,
+ left: '50%',
+ top: '50%',
+ 'min-height': width + 'px',
+ width: width + 'px',
+ height: width + 'px',
+ marginLeft: rx1 - width / 2 + 'px',
+ marginTop: -ry1 - width / 2 + 'px'
+ });
+
+ var content = createElement({ class: 'cxtmenu-content' });
+
+ if (command.content instanceof HTMLElement) {
+ content.appendChild(command.content);
+ } else {
+ content.innerHTML = command.content;
+ }
+
+ setStyles(content, {
+ 'width': width + 'px',
+ 'height': width + 'px',
+ 'vertical-align': 'middle',
+ 'display': 'table-cell'
+ });
+
+ setStyles(content, command.contentStyle || {});
+
+ if (command.disabled === true || command.enabled === false) {
+ content.setAttribute('class', 'cxtmenu-content cxtmenu-disabled');
+ }
+
+ parent.appendChild(item);
+ item.appendChild(content);
+
+ theta1 += dtheta;
+ theta2 += dtheta;
+ }
+ }
+
+ function queueDrawBg(radius, rspotlight) {
+ redrawQueue.drawBg = [radius, rspotlight];
+ }
+
+ function drawBg(radius, rspotlight) {
+ c2d.globalCompositeOperation = 'source-over';
+
+ c2d.clearRect(0, 0, containerSize, containerSize);
+
+ // draw background items
+ c2d.fillStyle = options.fillColor;
+ var dtheta = 2 * Math.PI / commands.length;
+ var theta1 = Math.PI / 2;
+ var theta2 = theta1 + dtheta;
+
+ for (var index = 0; index < commands.length; index++) {
+ var command = commands[index];
+
+ if (command.fillColor) {
+ c2d.fillStyle = command.fillColor;
+ }
+ c2d.beginPath();
+ c2d.moveTo(radius + options.activePadding, radius + options.activePadding);
+ c2d.arc(radius + options.activePadding, radius + options.activePadding, radius, 2 * Math.PI - theta1, 2 * Math.PI - theta2, true);
+ c2d.closePath();
+ c2d.fill();
+
+ theta1 += dtheta;
+ theta2 += dtheta;
+
+ c2d.fillStyle = options.fillColor;
+ }
+
+ // draw separators between items
+ c2d.globalCompositeOperation = 'destination-out';
+ c2d.strokeStyle = 'white';
+ c2d.lineWidth = options.separatorWidth;
+ theta1 = Math.PI / 2;
+ theta2 = theta1 + dtheta;
+
+ for (var i = 0; i < commands.length; i++) {
+ var rx1 = radius * Math.cos(theta1);
+ var ry1 = radius * Math.sin(theta1);
+ c2d.beginPath();
+ c2d.moveTo(radius + options.activePadding, radius + options.activePadding);
+ c2d.lineTo(radius + options.activePadding + rx1, radius + options.activePadding - ry1);
+ c2d.closePath();
+ c2d.stroke();
+
+ theta1 += dtheta;
+ theta2 += dtheta;
+ }
+
+ c2d.fillStyle = 'white';
+ c2d.globalCompositeOperation = 'destination-out';
+ c2d.beginPath();
+ c2d.arc(radius + options.activePadding, radius + options.activePadding, rspotlight + options.spotlightPadding, 0, Math.PI * 2, true);
+ c2d.closePath();
+ c2d.fill();
+
+ c2d.globalCompositeOperation = 'source-over';
+ }
+
+ function queueDrawCommands(rx, ry, radius, theta, rspotlight) {
+ redrawQueue.drawCommands = [rx, ry, radius, theta, rspotlight];
+ }
+
+ function drawCommands(rx, ry, radius, theta, rs) {
+ var dtheta = 2 * Math.PI / commands.length;
+ var theta1 = Math.PI / 2;
+ var theta2 = theta1 + dtheta;
+
+ theta1 += dtheta * activeCommandI;
+ theta2 += dtheta * activeCommandI;
+
+ c2d.fillStyle = options.activeFillColor;
+ c2d.strokeStyle = 'black';
+ c2d.lineWidth = 1;
+ c2d.beginPath();
+ c2d.moveTo(radius + options.activePadding, radius + options.activePadding);
+ c2d.arc(radius + options.activePadding, radius + options.activePadding, radius + options.activePadding, 2 * Math.PI - theta1, 2 * Math.PI - theta2, true);
+ c2d.closePath();
+ c2d.fill();
+
+ c2d.fillStyle = 'white';
+ c2d.globalCompositeOperation = 'destination-out';
+
+ var tx = radius + options.activePadding + rx / radius * (rs + options.spotlightPadding - options.indicatorSize / 4);
+ var ty = radius + options.activePadding + ry / radius * (rs + options.spotlightPadding - options.indicatorSize / 4);
+ var rot = Math.PI / 4 - theta;
+
+ c2d.translate(tx, ty);
+ c2d.rotate(rot);
+
+ // clear the indicator
+ // The indicator size (arrow) depends on the node size as well. If the indicator size is bigger and the rendered node size + padding,
+ // use the rendered node size + padding as the indicator size.
+ var indicatorSize = options.indicatorSize > rs + options.spotlightPadding ? rs + options.spotlightPadding : options.indicatorSize;
+ c2d.beginPath();
+ c2d.fillRect(-indicatorSize / 2, -indicatorSize / 2, indicatorSize, indicatorSize);
+ c2d.closePath();
+ c2d.fill();
+
+ c2d.rotate(-rot);
+ c2d.translate(-tx, -ty);
+
+ // c2d.setTransform( 1, 0, 0, 1, 0, 0 );
+
+ // clear the spotlight
+ c2d.beginPath();
+ c2d.arc(radius + options.activePadding, radius + options.activePadding, rs + options.spotlightPadding, 0, Math.PI * 2, true);
+ c2d.closePath();
+ c2d.fill();
+
+ c2d.globalCompositeOperation = 'source-over';
+ }
+
+ function updatePixelRatio() {
+ var pxr = getPixelRatio();
+ var w = containerSize;
+ var h = containerSize;
+
+ canvas.width = w * pxr;
+ canvas.height = h * pxr;
+
+ canvas.style.width = w + 'px';
+ canvas.style.height = h + 'px';
+
+ c2d.setTransform(1, 0, 0, 1, 0, 0);
+ c2d.scale(pxr, pxr);
+ }
+
+ var redrawing = true;
+ var redrawQueue = {};
+
+ var raf = window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || window.msRequestAnimationFrame || function (fn) {
+ return setTimeout(fn, 16);
+ };
+
+ var redraw = function redraw() {
+ if (redrawQueue.drawBg) {
+ drawBg.apply(null, redrawQueue.drawBg);
+ }
+
+ if (redrawQueue.drawCommands) {
+ drawCommands.apply(null, redrawQueue.drawCommands);
+ }
+
+ redrawQueue = {};
+
+ if (redrawing) {
+ raf(redraw);
+ }
+ };
+
+ // kick off
+ updatePixelRatio();
+ redraw();
+
+ var ctrx = void 0,
+ ctry = void 0,
+ rs = void 0;
+
+ var bindings = {
+ on: function on(events, selector, fn) {
+
+ var _fn = fn;
+ if (selector === 'core') {
+ _fn = function _fn(e) {
+ if (e.cyTarget === cy || e.target === cy) {
+ // only if event target is directly core
+ return fn.apply(this, [e]);
+ }
+ };
+ }
+
+ data.handlers.push({
+ events: events,
+ selector: selector,
+ fn: _fn
+ });
+
+ if (selector === 'core') {
+ cy.on(events, _fn);
+ } else {
+ cy.on(events, selector, _fn);
+ }
+
+ return this;
+ }
+ };
+
+ function addEventListeners() {
+ var grabbable = void 0;
+ var inGesture = false;
+ var dragHandler = void 0;
+ var zoomEnabled = void 0;
+ var panEnabled = void 0;
+ var boxEnabled = void 0;
+ var gestureStartEvent = void 0;
+ var hoverOn = void 0;
+
+ var restoreZoom = function restoreZoom() {
+ if (zoomEnabled) {
+ cy.userZoomingEnabled(true);
+ }
+ };
+
+ var restoreGrab = function restoreGrab() {
+ if (grabbable) {
+ target.grabify();
+ }
+ };
+
+ var restorePan = function restorePan() {
+ if (panEnabled) {
+ cy.userPanningEnabled(true);
+ }
+ };
+
+ var restoreBoxSeln = function restoreBoxSeln() {
+ if (boxEnabled) {
+ cy.boxSelectionEnabled(true);
+ }
+ };
+
+ var restoreGestures = function restoreGestures() {
+ restoreGrab();
+ restoreZoom();
+ restorePan();
+ restoreBoxSeln();
+ };
+
+ window.addEventListener('resize', updatePixelRatio);
+
+ bindings.on('resize', function () {
+ updatePixelRatio();
+ }).on(options.openMenuEvents, options.selector, function (e) {
+ target = this; // Remember which node the context menu is for
+ var ele = this;
+ var isCy = this === cy;
+
+ if (inGesture) {
+ parent.style.display = 'none';
+
+ inGesture = false;
+
+ restoreGestures();
+ }
+
+ if (typeof options.commands === 'function') {
+ var res = options.commands(target);
+ if (res.then) {
+ res.then(function (_commands) {
+ commands = _commands;
+ openMenu();
+ });
+ } else {
+ commands = res;
+ openMenu();
+ }
+ } else {
+ commands = options.commands;
+ openMenu();
+ }
+
+ function openMenu() {
+ if (!commands || commands.length === 0) {
+ return;
+ }
+
+ zoomEnabled = cy.userZoomingEnabled();
+ cy.userZoomingEnabled(false);
+
+ panEnabled = cy.userPanningEnabled();
+ cy.userPanningEnabled(false);
+
+ boxEnabled = cy.boxSelectionEnabled();
+ cy.boxSelectionEnabled(false);
+
+ grabbable = target.grabbable && target.grabbable();
+ if (grabbable) {
+ target.ungrabify();
+ }
+
+ var rp = void 0,
+ rw = void 0,
+ rh = void 0,
+ rs = void 0;
+ if (!isCy && ele && ele.isNode instanceof Function && ele.isNode() && !ele.isParent() && !options.atMouse) {
+ // If it's a node, the default spotlight radius for a node is the node width
+ rp = ele.renderedPosition();
+ rw = ele.renderedOuterWidth();
+ rh = ele.renderedOuterHeight();
+ rs = rw / 2;
+ // If adaptativeNodespotlightRadius is not enabled and min|maxSpotlighrRadius is defined, use those instead
+ rs = !options.adaptativeNodeSpotlightRadius && options.minSpotlightRadius ? Math.max(rs, options.minSpotlightRadius) : rs;
+ rs = !options.adaptativeNodeSpotlightRadius && options.maxSpotlightRadius ? Math.min(rs, options.maxSpotlightRadius) : rs;
+ } else {
+ // If it's the background or an edge, the spotlight radius is the min|maxSpotlightRadius
+ rp = e.renderedPosition || e.cyRenderedPosition;
+ rw = 1;
+ rh = 1;
+ rs = rw / 2;
+ rs = options.minSpotlightRadius ? Math.max(rs, options.minSpotlightRadius) : rs;
+ rs = options.maxSpotlightRadius ? Math.min(rs, options.maxSpotlightRadius) : rs;
+ }
+
+ offset = getOffset(container);
+
+ ctrx = rp.x;
+ ctry = rp.y;
+ r = rw / 2 + (options.menuRadius instanceof Function ? options.menuRadius(target) : Number(options.menuRadius));
+ containerSize = (r + options.activePadding) * 2;
+ updatePixelRatio();
+
+ setStyles(parent, {
+ width: containerSize + 'px',
+ height: containerSize + 'px',
+ display: 'block',
+ left: rp.x - r + 'px',
+ top: rp.y - r + 'px'
+ });
+ createMenuItems(r, rs);
+ queueDrawBg(r, rs);
+
+ activeCommandI = undefined;
+
+ inGesture = true;
+ gestureStartEvent = e;
+ }
+ }).on('cxtdrag tapdrag', options.selector, dragHandler = function dragHandler(e) {
+
+ if (!inGesture) {
+ return;
+ }
+ e.preventDefault(); // Otherwise, on mobile, the pull-down refresh gesture gets activated
+
+ var origE = e.originalEvent;
+ var isTouch = origE.touches && origE.touches.length > 0;
+
+ var pageX = (isTouch ? origE.touches[0].pageX : origE.pageX) - window.pageXOffset;
+ var pageY = (isTouch ? origE.touches[0].pageY : origE.pageY) - window.pageYOffset;
+
+ activeCommandI = undefined;
+
+ var dx = pageX - offset.left - ctrx;
+ var dy = pageY - offset.top - ctry;
+
+ if (dx === 0) {
+ dx = 0.01;
+ }
+
+ var d = Math.sqrt(dx * dx + dy * dy);
+ var cosTheta = (dy * dy - d * d - dx * dx) / (-2 * d * dx);
+ var theta = Math.acos(cosTheta);
+
+ var rw = void 0;
+ if (target && target.isNode instanceof Function && target.isNode() && !target.isParent() && !options.atMouse) {
+ // If it's a node, the default spotlight radius for a node is the node width
+ rw = target.renderedOuterWidth();
+ rs = rw / 2;
+ // If adaptativeNodespotlightRadius is not enabled and min|maxSpotlighrRadius is defined, use those instead
+ rs = !options.adaptativeNodeSpotlightRadius && options.minSpotlightRadius ? Math.max(rs, options.minSpotlightRadius) : rs;
+ rs = !options.adaptativeNodeSpotlightRadius && options.maxSpotlightRadius ? Math.min(rs, options.maxSpotlightRadius) : rs;
+ } else {
+ // If it's the background or an edge, the spotlight radius is the min|maxSpotlightRadius
+ rw = 1;
+ rs = rw / 2;
+ rs = options.minSpotlightRadius ? Math.max(rs, options.minSpotlightRadius) : rs;
+ rs = options.maxSpotlightRadius ? Math.min(rs, options.maxSpotlightRadius) : rs;
+ }
+
+ r = rw / 2 + (options.menuRadius instanceof Function ? options.menuRadius(target) : Number(options.menuRadius));
+ if (d < rs + options.spotlightPadding || typeof options.outsideMenuCancel === "number" && d > r + options.activePadding + options.outsideMenuCancel) {
+ //
+
+ queueDrawBg(r, rs);
+ return;
+ }
+ queueDrawBg(r, rs);
+
+ var rx = dx * r / d;
+ var ry = dy * r / d;
+
+ if (dy > 0) {
+ theta = Math.PI + Math.abs(theta - Math.PI);
+ }
+
+ var dtheta = 2 * Math.PI / commands.length;
+ var theta1 = Math.PI / 2;
+ var theta2 = theta1 + dtheta;
+
+ for (var i = 0; i < commands.length; i++) {
+ var command = commands[i];
+
+ var inThisCommand = theta1 <= theta && theta <= theta2 || theta1 <= theta + 2 * Math.PI && theta + 2 * Math.PI <= theta2;
+
+ if (command.disabled === true || command.enabled === false) {
+ inThisCommand = false;
+ }
+
+ if (inThisCommand) {
+ activeCommandI = i;
+ break;
+ }
+
+ theta1 += dtheta;
+ theta2 += dtheta;
+ }
+ queueDrawCommands(rx, ry, r, theta, rs);
+ }).on('tapdrag', dragHandler).on('mousemove', function () {
+ if (activeCommandI !== undefined) {
+ var hovered = commands[activeCommandI].hover;
+ if (hovered) {
+ if (hoverOn !== activeCommandI) {
+ hovered.apply(target, [target, gestureStartEvent]);
+ }
+ hoverOn = activeCommandI;
+ }
+ }
+ }).on('cxttapend tapend', function () {
+ parent.style.display = 'none';
+ if (activeCommandI !== undefined) {
+ var select = commands[activeCommandI].select;
+
+ if (select) {
+ select.apply(target, [target, gestureStartEvent]);
+ activeCommandI = undefined;
+ }
+ }
+
+ hoverOn = undefined;
+
+ inGesture = false;
+
+ restoreGestures();
+ });
+ }
+
+ function removeEventListeners() {
+ var handlers = data.handlers;
+
+ for (var i = 0; i < handlers.length; i++) {
+ var h = handlers[i];
+
+ if (h.selector === 'core') {
+ cy.off(h.events, h.fn);
+ } else {
+ cy.off(h.events, h.selector, h.fn);
+ }
+ }
+
+ window.removeEventListener('resize', updatePixelRatio);
+ }
+
+ function destroyInstance() {
+ redrawing = false;
+
+ removeEventListeners();
+
+ wrapper.remove();
+ }
+
+ addEventListeners();
+
+ return {
+ destroy: function destroy() {
+ destroyInstance();
+ }
+ };
+};
+
+module.exports = cxtmenu;
+
+/***/ }),
+/* 1 */
+/***/ (function(module, exports, __webpack_require__) {
+
+"use strict";
+
+
+// Simple, internal Object.assign() polyfill for options objects etc.
+
+module.exports = Object.assign != null ? Object.assign.bind(Object) : function (tgt) {
+ for (var _len = arguments.length, srcs = Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) {
+ srcs[_key - 1] = arguments[_key];
+ }
+
+ srcs.filter(function (src) {
+ return src != null;
+ }).forEach(function (src) {
+ Object.keys(src).forEach(function (k) {
+ return tgt[k] = src[k];
+ });
+ });
+
+ return tgt;
+};
+
+/***/ }),
+/* 2 */
+/***/ (function(module, exports, __webpack_require__) {
+
+"use strict";
+
+
+var defaults = {
+ menuRadius: function menuRadius(ele) {
+ return 100;
+ }, // the radius of the circular menu in pixels
+ selector: 'node', // elements matching this Cytoscape.js selector will trigger cxtmenus
+ commands: [// an array of commands to list in the menu or a function that returns the array
+ /*
+ { // example command
+ fillColor: 'rgba(200, 200, 200, 0.75)', // optional: custom background color for item
+ content: 'a command name' // html/text content to be displayed in the menu
+ contentStyle: {}, // css key:value pairs to set the command's css in js if you want
+ hover: function(ele){ // a function to execute when the command is hovered
+ console.log( ele.id() ) // `ele` holds the reference to the active element
+ },
+ select: function(ele){ // a function to execute when the command is selected
+ console.log( ele.id() ) // `ele` holds the reference to the active element
+ },
+ enabled: true // whether the command is selectable
+ }
+ */
+ ], // function( ele ){ return [ /*...*/ ] }, // example function for commands
+ fillColor: 'rgba(0, 0, 0, 0.75)', // the background colour of the menu
+ activeFillColor: 'rgba(1, 105, 217, 0.75)', // the colour used to indicate the selected command
+ activePadding: 20, // additional size in pixels for the active command
+ indicatorSize: 24, // the size in pixels of the pointer to the active command, will default to the node size if the node size is smaller than the indicator size,
+ separatorWidth: 3, // the empty spacing in pixels between successive commands
+ spotlightPadding: 4, // extra spacing in pixels between the element and the spotlight
+ adaptativeNodeSpotlightRadius: false, // specify whether the spotlight radius should adapt to the node size
+ minSpotlightRadius: 24, // the minimum radius in pixels of the spotlight (ignored for the node if adaptativeNodeSpotlightRadius is enabled but still used for the edge & background)
+ maxSpotlightRadius: 38, // the maximum radius in pixels of the spotlight (ignored for the node if adaptativeNodeSpotlightRadius is enabled but still used for the edge & background)
+ openMenuEvents: 'cxttapstart taphold', // space-separated cytoscape events that will open the menu; only `cxttapstart` and/or `taphold` work here
+ itemColor: 'white', // the colour of text in the command's content
+ itemTextShadowColor: 'transparent', // the text shadow colour of the command's content
+ zIndex: 9999, // the z-index of the ui div
+ atMouse: false, // draw menu at mouse position
+ outsideMenuCancel: false // if set to a number, this will cancel the command if the pointer is released outside of the spotlight, padded by the number given
+};
+
+module.exports = defaults;
+
+/***/ }),
+/* 3 */
+/***/ (function(module, exports, __webpack_require__) {
+
+"use strict";
+
+
+var removeEles = function removeEles(query) {
+ var ancestor = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : document;
+
+ var els = ancestor.querySelectorAll(query);
+
+ for (var i = 0; i < els.length; i++) {
+ var el = els[i];
+
+ el.parentNode.removeChild(el);
+ }
+};
+
+var setStyles = function setStyles(el, style) {
+ var props = Object.keys(style);
+
+ for (var i = 0, l = props.length; i < l; i++) {
+ el.style[props[i]] = style[props[i]];
+ }
+};
+
+var createElement = function createElement(options) {
+ options = options || {};
+
+ var el = document.createElement(options.tag || 'div');
+
+ el.className = options.class || '';
+
+ if (options.style) {
+ setStyles(el, options.style);
+ }
+
+ return el;
+};
+
+var getPixelRatio = function getPixelRatio() {
+ return window.devicePixelRatio || 1;
+};
+
+var getOffset = function getOffset(el) {
+ var offset = el.getBoundingClientRect();
+
+ return {
+ left: offset.left + document.body.scrollLeft + parseFloat(getComputedStyle(document.body)['padding-left']) + parseFloat(getComputedStyle(document.body)['border-left-width']),
+ top: offset.top + document.body.scrollTop + parseFloat(getComputedStyle(document.body)['padding-top']) + parseFloat(getComputedStyle(document.body)['border-top-width'])
+ };
+};
+
+module.exports = { removeEles: removeEles, setStyles: setStyles, createElement: createElement, getPixelRatio: getPixelRatio, getOffset: getOffset };
+
+/***/ }),
+/* 4 */
+/***/ (function(module, exports, __webpack_require__) {
+
+"use strict";
+
+
+var cxtmenu = __webpack_require__(0);
+
+// registers the extension on a cytoscape lib ref
+var register = function register(cytoscape) {
+ if (!cytoscape) {
+ return;
+ } // can't register if cytoscape unspecified
+
+ cytoscape('core', 'cxtmenu', cxtmenu); // register with cytoscape.js
+};
+
+if (typeof cytoscape !== 'undefined') {
+ // expose to global cytoscape (i.e. window.cytoscape)
+ register(cytoscape);
+}
+
+module.exports = register;
+
+/***/ })
+/******/ ]);
+});
\ No newline at end of file
diff --git a/src/server/webapp/static/gontrol-stylesheet.css b/src/server/webapp/static/gontrol-stylesheet.css
index 15f6068..fcdfa9b 100644
--- a/src/server/webapp/static/gontrol-stylesheet.css
+++ b/src/server/webapp/static/gontrol-stylesheet.css
@@ -19,8 +19,9 @@
/* background-color: #f8f9fa; /1* optional: subtle background for log readability *1/ */
}
-.log-info, .log-warning, .log-error, .log-fatal, .log-debug{ font-family: "Roboto Mono", "Consolas", "Courier New", monospace;
- font-size: 14px;
+.log-info, .log-warning, .log-error, .log-fatal, .log-debug{
+ font-family: "Roboto Mono", "Consolas", "Courier New", monospace;
+ font-size: 14px;
}
.log-info {
diff --git a/src/server/webapp/templates/index.html b/src/server/webapp/templates/index.html
index a7803a7..aa1c3df 100644
--- a/src/server/webapp/templates/index.html
+++ b/src/server/webapp/templates/index.html
@@ -9,6 +9,7 @@
+