// SPDX-FileCopyrightText: 2025 CERN
// SPDX-License-Identifier: GPL-3.0-or-later
/**
* This class represents a performance analysis session.
*/
class Session {
/**
* Static dictionary storing all instances of a Session
* class under their IDs.
*
* @static
*/
static instances = {};
/**
* Constructs a Session object, which is the main
* place for storing all information about a performance
* analysis session.
*
* @constructor
* @param {String} id ID of a session as known by the server.
* @param {String} label Human-readable label of a session.
*/
constructor(id, label) {
this.id = id;
this.label = label;
this.modules_loaded = {};
Session.instances[id] = this;
}
/**
* Sends a request to the server side of Adaptyst Analyser.
* The request will be handled by the Python code of a
* corresponding Adaptyst Analyser module.
*
* Use Window.sendRequest() to send requests if you can.
* Calling Session.sendRequest() is preferred only in case
* you don't have an eligible Window-inheriting object and
* don't want to create one.
*
* @param {String} entity ID of an entity.
* @param {String} node ID of a node.
* @param {String} module Name of a module.
* @param {Object} data Data to be sent in form of JSON.
* @param done_func Function to be called when the request
* succeeds. The function must take exactly one argument
* which is the content returned by the server side.
* @param fail_func Function to be called when the request
* fails for any reason. The function must take exactly the
* arguments described in the "error" entry of "settings"
* in the jQuery.ajax() documentation
* [here](https://api.jquery.com/jQuery.ajax).
* @param {String} content_type Content type expected from
* the server side. Use one of the values explained in
* the "dataType" entry in "settings" in the jQuery.ajax
* documentation [here](https://api.jquery.com/jQuery.ajax).
* It can be undefined, this is then interpreted as 'json'.
*/
sendRequest(entity, node, module, data, done_func, fail_func,
content_type) {
if (content_type === undefined) {
content_type = 'json';
}
$.ajax({
url: this.id + '/' + entity + '/' + node + '/' + module,
method: 'POST',
dataType: content_type,
data: data
}).done((data, status, xhr) => {
done_func(data);
}).fail(fail_func);
}
}
/**
* This abstract class represents an internal window.
*
* If you want to display a window in Adaptyst Analyser,
* you must implement a new class inheriting from this class
* and implementing its abstract methods.
*/
class Window {
/**
* Static dictionary storing all instances of a Window
* class (or one of its subclasses) under their IDs as returned
* by `getId()`.
*
* @static
*/
static instances = {};
// Private, not meant to be used by any external code.
static #current_focused_id = undefined;
// Private, not meant to be used by any external code.
static #largest_z_index = 0;
// Private, not meant to be called by any external code.
static onResize(windows) {
for (const window of windows) {
let target = window.target;
if (target === null) {
continue;
}
let window_id = $(target).attr('id');
while (target !== null && !(window_id in Window.instances)) {
target = target.parentElement;
window_id = $(target).attr('id');
}
if (target === null) {
continue;
}
if (Window.instances[window_id].first_resize_call) {
Window.instances[window_id].first_resize_call = false;
} else {
let position = $(target).position();
target.style.transform = '';
target.style.top = position.top + 'px';
target.style.left = position.left + 'px';
Window.instances[window_id].triggerResize();
}
}
}
/**
* Gets the path to the server folder where JavaScript
* and CSS dependencies of modules are stored. Use the
* value returned by this method for constructing
* URLs to the dependencies.
*
* @return {String} Path to the folder with JavaScript
* and CSS module dependencies.
*
* @static
*/
static getDepsPath() {
return '/static/deps';
}
/**
* Stops further propagation of an event. It may be useful
* e.g. for handling mouse clicks.
*
* @param {Object} event Event which propagation should be stopped.
*
* @static
*/
static stopPropagation(event) {
event.stopPropagation();
event.preventDefault();
}
#id;
#session;
#entity_id;
#node_id;
#data;
#module_name;
#being_resized;
#collapsed;
#last_focus;
#dom;
#first_resize_call;
#loading_jquery;
#setup_data;
#min_height;
#last_height;
#content;
#custom_title;
/**
* Constructs a Window object and displays a window
* corresponding to the object. All subclasses
* must call this constructor.
*
* @constructor
* @param {Object} [session] `Session` object corresponding
* to a window. This is provided by a parameter of
* `createRootWindow()`. It can be undefined.
* @param {String} [entity_id] The ID of an entity corresponding
* to a window. This is provided by a parameter of
* `createRootWindow()`. It can be undefined.
* @param {String} [node_id] The ID of a node corresponding
* to a window. This is provided by a parameter of
* `createRootWindow()`. It can be undefined.
* @param {String} [module_name] The name of a module within
* a node corresponding to a window. It can be undefined.
* @param [data] Arbitrary data to be passed to `_setup()`.
* It can be undefined.
* @param {int} [x] x-part of the initial upper-left corner
* position of a window. If undefined, the value of `y` will
* be ignored and the window will be centered.
* @param {int} [y] y-part of the initial upper-left corner
* position of a window. If undefined, the value of `x` will
* be ignored and the window will be centered.
*/
constructor(session, entity_id, node_id,
module_name, data, x, y) {
let index = 0;
let id = undefined;
if (session == undefined) {
id = `w_${this.getType()}_${index}`;
while (id in Window.instances) {
index++;
id = `w_${this.getType()}_${index}`;
}
} else {
id = `w_${session.label}_${this.getType()}_${index}`;
while (id in Window.instances) {
index++;
id = `w_${session.label}_${this.getType()}_${index}`;
}
}
Window.instances[id] = this;
this.#id = id;
this.#session = session;
this.#entity_id = entity_id;
this.#node_id = node_id;
this.#data = {};
this.#module_name = module_name;
this.#being_resized = false;
this.#collapsed = false;
this.#last_focus = Date.now();
this.#dom = this.#createWindowDOM();
this.#custom_title = undefined;
if (x !== undefined && y !== undefined) {
this.#dom.css('left', x + 'px');
this.#dom.css('top', y + 'px');
} else {
this.#dom.css('top', '50%');
this.#dom.css('left', '50%');
this.#dom.css('transform', 'translate(-50%, -50%)');
}
this.#first_resize_call = true;
new ResizeObserver(Window.onResize).observe(this.#dom[0]);
if (data === undefined) {
this.#setup({});
} else {
this.#setup(data);
}
}
/**
* Sends a request to the server side of Adaptyst Analyser.
* The request will be handled by the Python code of a
* corresponding Adaptyst Analyser module.
*
* Use this function only if you have constructed your
* object with all of an entity ID, a node ID, and a module
* name.
*
* @param {Object} data Data to be sent in form of JSON.
* @param done_func Function to be called when the request
* succeeds. The function must take exactly one argument
* which is a JSON object returned by the server side.
* @param fail_func Function to be called when the request
* fails for any reason. The function must take exactly the
* arguments described in the "error" entry of "settings"
* in the jQuery.ajax() documentation
* [here](https://api.jquery.com/jQuery.ajax).
* @param {String} content_type Content type expected from
* the server side. Use one of the values explained in
* the "dataType" entry in "settings" in the jQuery.ajax
* documentation [here](https://api.jquery.com/jQuery.ajax).
* It can be undefined, this is then interpreted as 'json'.
*/
sendRequest(data, done_func, fail_func, content_type) {
this.getSession().sendRequest(this.getEntityId(),
this.getNodeId(),
this.getModuleName(),
data, done_func, fail_func,
content_type);
}
/**
* Gets the last time a window was focused.
*
* @return Last time a window was focused,
* as a number representing a Unix timestamp in
* milliseconds.
*/
getLastFocusTime() {
return this.#last_focus;
}
/**
* Gets the ID of a window.
*
* @return {String} ID of a window.
*/
getId() {
return this.#id;
}
/**
* Gets the entity ID of a window. It can be
* undefined.
*
* @return {String} Entity ID of a window.
*/
getEntityId() {
return this.#entity_id;
}
/**
* Gets the node ID of a window. It can be
* undefined.
*
* @return {String} Node ID of a window.
*/
getNodeId() {
return this.#node_id;
}
/**
* Gets the module name of a window. It can be
* undefined.
*
* @return {String} Module name of a window.
*/
getModuleName() {
return this.#module_name;
}
/**
* Gets the dictionary of a window. It can be
* filled with arbitrary data.
*
* @return {Object} Dictionary of a window.
*/
getData() {
return this.#data;
}
/**
* Gets the performance session a window is part of.
*
* @return {Object} Session of a window.
*/
getSession() {
return this.#session;
}
/**
* Gets the type of a window. This is used in the ID
* of a window, an HTML class of the window
* (i.e. `<type>_window`), and an HTML class of
* the window content (i.e. `<type>_content`).
*
* @abstract
* @return {String} Type of a window.
*/
getType() {
throw new Error('This is an abstract method!');
}
// Private, not meant to be called by any external code.
#getProcessedContentObject() {
let content = $(this.getContentCode());
// SVGs below are from Google Material Icons, licensing:
// SPDX-FileCopyrightText: Google
// SPDX-License-Identifier: Apache-2.0
// ************************
let setUpIcon = (target, code) => {
let obj = content.find('[data-icon="' + target + '"]');
let path = $(document.createElementNS('http://www.w3.org/2000/svg', 'path'));
path.attr('d', code);
obj.append(path);
obj.attr('xmlns', '');
obj.attr('viewBox', '0 -960 960 960');
};
setUpIcon('general', 'M280-280h80v-200h-80v200Zm320 0h80v-400h-80v400Zm-160 0h80v-120h-80v120Zm0-200h80v-80h-80v80ZM200-120q-33 0-56.5-23.5T120-200v-560q0-33 23.5-56.5T200-840h560q33 0 56.5 23.5T840-760v560q0 33-23.5 56.5T760-120H200Zm0-80h560v-560H200v560Zm0-560v560-560Z');
setUpIcon('warning', 'm40-120 440-760 440 760H40Zm138-80h604L480-720 178-200Zm302-40q17 0 28.5-11.5T520-280q0-17-11.5-28.5T480-320q-17 0-28.5 11.5T440-280q0 17 11.5 28.5T480-240Zm-40-120h80v-200h-80v200Zm40-100Z');
setUpIcon('replace', 'M164-560q14-103 91.5-171.5T440-800q59 0 110.5 22.5T640-716v-84h80v240H480v-80h120q-29-36-69.5-58T440-720q-72 0-127 45.5T244-560h-80Zm620 440L608-296q-36 27-78.5 41.5T440-240q-59 0-110.5-22.5T240-324v84h-80v-240h240v80H280q29 36 69.5 58t90.5 22q72 0 127-45.5T636-480h80q-5 36-18 67.5T664-352l176 176-56 56Z');
setUpIcon('download', 'M480-320 280-520l56-58 104 104v-326h80v326l104-104 56 58-200 200ZM240-160q-33 0-56.5-23.5T160-240v-120h80v120h480v-120h80v120q0 33-23.5 56.5T720-160H240Z');
setUpIcon('delete', 'M280-120q-33 0-56.5-23.5T200-200v-520h-40v-80h200v-40h240v40h200v80h-40v520q0 33-23.5 56.5T680-120H280Zm400-600H280v520h400v-520ZM360-280h80v-360h-80v360Zm160 0h80v-360h-80v360ZM280-720v520-520Z');
setUpIcon('copy', 'M120-220v-80h80v80h-80Zm0-140v-80h80v80h-80Zm0-140v-80h80v80h-80ZM260-80v-80h80v80h-80Zm100-160q-33 0-56.5-23.5T280-320v-480q0-33 23.5-56.5T360-880h360q33 0 56.5 23.5T800-800v480q0 33-23.5 56.5T720-240H360Zm0-80h360v-480H360v480Zm40 240v-80h80v80h-80Zm-200 0q-33 0-56.5-23.5T120-160h80v80Zm340 0v-80h80q0 33-23.5 56.5T540-80ZM120-640q0-33 23.5-56.5T200-720v80h-80Zm420 80Z');
// ************************
return content;
}
/**
* Gets the content of a window in form of an HTML code.
*
* Window header and border rendering along with basic window
* operations except resizing is fully handled by Adaptyst
* Analyser and you shouldn't implement it yourself. Resizing
* is partially handled by Adaptyst Analyser and should also
* not be implemented here, see `startResize()` and
* `finishResize()` instead.
*
* A padding of 5 pixels is applied to the content of
* all windows in Adaptyst Analyser.
*
* @abstract
* @return {String} HTML code of the content of a window.
*/
getContentCode() {
throw new Error('This is an abstract method!');
}
/**
* Gets the content of a window in form of a jQuery object.
* The return value of this function is based on your implementation
* of `getContentCode()`.
*
* @return {Object} jQuery object representing the content of a window.
*/
getContent() {
if (this.#content === undefined) {
this.#content = this.#dom.find('.window_content');
}
return this.#content;
}
// Private, not meant to be called by any external code.
#createWindowDOM() {
const window_header = `
<div class="window_header">
<span class="window_title"></span>
<!-- This SVG is from Google Material Icons, licensing:
SPDX-FileCopyrightText: Google
SPDX-License-Identifier: Apache-2.0 -->
<svg xmlns="http://www.w3.org/2000/svg" class="window_refresh" height="24px"
viewBox="0 -960 960 960" width="24px" onmousedown="Window.stopPropagation(event)">
<title>Reset window contents</title>
<path d="M480-160q-134 0-227-93t-93-227q0-134 93-227t227-93q69 0 132 28.5T720-690v-110h80v280H520v-80h168q-32-56-87.5-88T480-720q-100 0-170 70t-70 170q0 100
70 170t170 70q77 0 139-44t87-116h84q-28 106-114 173t-196 67Z"/>
</svg>
<!-- This SVG is from Google Material Icons, licensing:
SPDX-FileCopyrightText: Google
SPDX-License-Identifier: Apache-2.0 -->
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960"
width="24px" class="window_edit_title" onmousedown="Window.stopPropagation(event)">
<title>Edit title</title>
<path d="M160-400v-80h280v80H160Zm0-160v-80h440v80H160Zm0-160v-80h440v80H160Zm360 560v-123l221-220q9-9 20-13t22-4q12 0 23 4.5t20 13.5l37 37q8 9 12.5 20t4.5 22q0 11-4 22.5T863-380L643-160H520Zm300-263-37-37 37 37ZM580-220h38l121-122-18-19-19-18-122 121v38Zm141-141-19-18 37 37-18-19Z"/>
</svg>
<!-- This SVG is from Google Material Icons, licensing:
SPDX-FileCopyrightText: Google
SPDX-License-Identifier: Apache-2.0 -->
<svg xmlns="http://www.w3.org/2000/svg" class="window_visibility" height="24px"
viewBox="0 -960 960 960" width="24px" onmousedown="Window.stopPropagation(event)">
<title>Toggle visibility</title>
<path d="M480-320q75 0 127.5-52.5T660-500q0-75-52.5-127.5T480-680q-75 0-127.5 52.5T300-500q0 75 52.5 127.5T480-320Zm0-72q-45 0-76.5-31.5T372-500q0-45
31.5-76.5T480-608q45 0 76.5 31.5T588-500q0 45-31.5 76.5T480-392Zm0 192q-146 0-266-81.5T40-500q54-137 174-218.5T480-800q146 0 266 81.5T920-500q-54
137-174 218.5T480-200Zm0-300Zm0 220q113 0 207.5-59.5T832-500q-50-101-144.5-160.5T480-720q-113 0-207.5 59.5T128-500q50 101 144.5 160.5T480-280Z"/>
</svg>
<!-- This SVG is from Google Material Icons, licensing:
SPDX-FileCopyrightText: Google
SPDX-License-Identifier: Apache-2.0 -->
<svg xmlns="http://www.w3.org/2000/svg" class="window_close" height="24px"
viewBox="0 -960 960 960" width="24px" onmousedown="Window.stopPropagation(event)">
<title>Close</title>
<path d="m256-200-56-56 224-224-224-224 56-56 224 224 224-224 56 56-224 224 224 224-56 56-224-224-224 224Z"/>
</svg>
</div>
`;
let root = $('<div></div>');
root.attr('class', 'window ' + this.getType() + '_window');
root.append($(window_header));
let content = $('<div></div>');
content.attr('class', 'window_content ' + this.getType() + '_content');
content.html(this.#getProcessedContentObject());
root.append(content);
root.attr('id', this.#id);
root.attr('onclick', `Window.instances['${this.#id}'].focus()`);
root.attr('onmouseup', `Window.instances['${this.#id}'].onMouseUp()`);
root.find('.window_header').attr('onmousedown', `Window.instances['${this.#id}'].startDrag(event)`);
root.find('.window_refresh').attr(
'onclick', `Window.instances['${this.#id}'].onRefreshClick(event)`);
root.find('.window_visibility').attr(
'onclick', `Window.instances['${this.#id}'].onVisibilityClick(event)`);
root.find('.window_edit_title').attr(
'onclick', `Window.instances['${this.#id}'].onEditTitleClick(event)`);
root.find('.window_close').attr(
'onclick', `Window.instances['${this.#id}'].close(event)`);
return root;
}
// Private, not meant to be called by any external code.
onEditTitleClick(event) {
let title = window.prompt('Enter a new title for the window. ' +
'The session prefix will remain ' +
'unchanged if present.',
this.#custom_title === undefined ?
this.getTitle() : this.#custom_title);
if (title == undefined || title === '') {
return;
}
if (this.#session === undefined) {
this.#dom.find('.window_title').text(title);
} else {
this.#dom.find('.window_title').text(
'[Session: ' + this.#session.label + '] ' +
title);
}
this.#custom_title = title;
}
/**
* Downloads a given SVG object in a window as an SVG file.
*
* @param {String} [class_name] The class name of an SVG object.
* It is expected that the object has a unique class name within
* the window. Otherwise, the behaviour is undefined.
* @param {String} [css] The path to a CSS stylesheet to be
* applied to the SVG before downloading.
*/
downloadSvg(class_name, css) {
$.ajax({
url: css,
method: 'GET',
dataType: 'text'
}).done(res => {
let svg = this.getContent().find('.' + class_name).children()[0].cloneNode(true);
let style = document.createElement('style');
style.innerHTML = res;
svg.insertBefore(style, svg.firstChild);
let data = document.createElement('a');
data.download = 'flamegraph.svg';
data.href = 'data:image/svg+xml;charset=utf-8,' +
encodeURIComponent((new XMLSerializer()).serializeToString(svg));
data.addEventListener('click', event => {
event.stopPropagation();
});
data.click();
}).fail(res => {
window.alert('Could not download a stylesheet for the SVG!');
});
}
/**
* Focuses a window.
*/
focus() {
let window_header = this.#dom.find('.window_header');
if (Window.#current_focused_id !== this.#id) {
if (Window.#largest_z_index >= 10000) {
let z_index_arr = [];
for (const w of Object.values(Window.instances)) {
z_index_arr.push({'index': w.getZIndex(),
'id': w.getId()});
}
z_index_arr.sort((a, b) => {
if (a.index === undefined) {
return -1;
} else if (b.index === undefined) {
return 1;
} else {
return a.index - b.index;
}
});
let index = 1;
for (const obj of z_index_arr) {
$('#' + obj.id).css('z-index', index);
index += 1;
}
this.#dom.css('z-index', index);
Window.#largest_z_index = index;
} else {
Window.#largest_z_index += 1;
this.#dom.css('z-index', Window.#largest_z_index);
}
window_header.css('background-color', 'black');
window_header.css('color', 'white');
window_header.css('fill', 'white');
for (const w of Object.values(Window.instances)) {
if (w.getId() !== this.getId()) {
w.unfocus();
}
}
Window.#current_focused_id = this.getId();
this.#last_focus = Date.now();
}
}
/**
* Unfocuses a window.
*/
unfocus() {
let unfocused_header = this.#dom.find('.window_header');
unfocused_header.css('background-color', 'lightgray');
unfocused_header.css('color', 'black');
unfocused_header.css('fill', 'black');
}
// Private, not meant to be called by any external code.
onMouseUp() {
if (this.#being_resized) {
this.finishResize();
this.#being_resized = false;
}
}
/**
* Triggers window resizing event-wise.
*/
triggerResize() {
this.#being_resized = this.startResize();
}
/**
* Called when a user starts resizing a window.
*
* @abstract
* @return {bool} Whether finishResize() should be
* called after resizing is complete.
*/
startResize() {
throw new Error('This is an abstract method!');
}
/**
* Called when a user finishes resizing a window.
*
* @abstract
*/
finishResize() {
throw new Error('This is an abstract method!');
}
// Private, not meant to be called by any external code.
startDrag(event) {
Window.stopPropagation(event);
this.focus();
let dragged = document.getElementById(this.#id);
let startX = event.offsetX;
let startY = event.offsetY;
$('body').mousemove(event => {
event.stopPropagation();
event.preventDefault();
let newX = event.pageX - startX;
let newY = event.pageY - startY;
let dragged_rect = dragged.getBoundingClientRect();
dragged.style.transform = '';
dragged.style.left = newX + 'px';
dragged.style.top = newY + 'px';
});
$('body').mouseup(event => {
$('body').off('mousemove');
$('body').off('mouseup');
});
}
// Private, not meant to be called by any external code.
onRefreshClick(event) {
Window.stopPropagation(event);
this.prepareRefresh(this.#setup_data);
this.#dom.find('.window_content').html(this.#getProcessedContentObject());
this.#setup();
}
/**
* Gets the value of the z-index CSS property of a window.
*
* @return Value of z-index of a window.
*/
getZIndex() {
return this.#dom.css('z-index');
}
/**
* Called when a user refreshes a window, before the proper
* refresh process with the content resetup takes place.
*
* The old window content is still available when this
* method is called.
*
* @abstract
* @param data Arbitrary data that have been passed to the
* constructor and will be available in _setup(), e.g. a dictionary.
*/
prepareRefresh(data) {
throw new Error('This is an abstract method!');
}
/**
* Called when a user closes a window, before it is actually
* closed.
* @abstract
*/
prepareClose() {
throw new Error('This is an abstract method!');
}
/**
* Shows the loading indicator in a window.
*/
showLoading() {
this.#loading_jquery = $('#loading').clone();
this.#loading_jquery.removeAttr('id');
this.#loading_jquery.attr('class', 'loading');
this.#loading_jquery.prependTo(this.#dom.find('.window_content'));
this.#loading_jquery.show();
}
/**
* Hides the loading indicator in a window.
*/
hideLoading() {
this.#loading_jquery.hide();
}
// Private, not meant to be called by any external code.
#setup(data) {
let existing_window = false;
if (data === undefined) {
data = this.#setup_data;
existing_window = true;
}
if (this.#custom_title === undefined) {
if (this.#session === undefined) {
this.#dom.find('.window_title').text(this.getTitle());
} else {
this.#dom.find('.window_title').text(
'[Session: ' + this.#session.label + '] ' +
this.getTitle());
}
}
this.showLoading();
if (!existing_window) {
this.#dom.appendTo('body');
this.focus();
}
this.#setup_data = data;
this._setup(data, existing_window);
}
/**
* Gets the title of a window.
* @abstract
* @return {String} Title of a window.
*/
getTitle() {
throw new Error('This is an abstract method!');
}
/**
* Sets up a window. This is always called when the window is opened or
* refreshed. Use `getContent()` to get a jQuery object for manipulating
* the window content.
*
* **Important:** This function must call `hideLoading()` at some point.
* Otherwise, there will be a loading indicator in the window instead
* of a desired content.
*
* @abstract
* @param data Arbitrary data passed to the constructor, e.g.
* a dictionary.
* @param {bool} existing_window Whether a window is already displayed.
*/
_setup(data, existing_window) {
throw new Error('This is an abstract method!');
}
// Private, not meant to be called by any external code.
close(event) {
Window.stopPropagation(event);
this.prepareClose();
this.#dom.remove();
delete Window.instances[this.#id];
let keys = Object.keys(Window.instances);
if (keys.length === 0) {
return;
}
keys.sort((a, b) => {
return Window.instances[b].getLastFocusTime() - Window.instances[a].getLastFocusTime();
});
Window.instances[keys[0]].focus();
}
// Private, not meant to be called by any external code.
onVisibilityClick(event) {
Window.stopPropagation(event);
let window_content = this.#dom.find('.window_content');
let window_header = this.#dom.find('.window_header');
if (!this.#collapsed) {
let position = this.#dom.position();
this.#dom.css('transform', '');
this.#dom.css('left', position.left);
this.#dom.css('top', position.top);
this.#collapsed = true;
this.#min_height = this.#dom.css('min-height');
this.#last_height = this.#dom.outerHeight();
this.#dom.css('min-height', '0');
this.#dom.css('resize', 'horizontal');
this.#dom.height(window_header.outerHeight());
} else {
this.#collapsed = false;
this.#dom.height(this.#last_height);
this.#dom.css('min-height', this.#min_height);
this.#dom.css('resize', 'both');
this.#dom.css('opacity', '');
}
}
}
/**
* This class contains static methods for managing menus.
* It is not meant to be constructed.
*
* **Only one menu can be open at a time.**
*/
class Menu {
/**
* Creates and displays a menu.
*
* If you want to refer to the menu block in CSS, use
* .<name_prefix>_menu.
*
* @static
* @param {String} name_prefix Prefix to use for the menu
* block class in CSS.
* @param {int} x x-part of the upper-left corner position
* of a menu.
* @param {int} y y-part of the upper-left corner position
* of a menu.
* @param {Array} options Array of menu items of type
* `[k, v]`, where `k` is the label of a menu item to be
* displayed and `v` is of form `[<arbitrary data>,
* <click event handler>]`. Click event handlers must
* accept `event` (corresponding to a JavaScript
* click event object) as the first argument. `<arbitrary
* data>` will be accessible in a click handler through
* `event.data.data`.
*/
static createMenu(name_prefix, x, y, options) {
if (name_prefix.includes('"') || name_prefix.includes(' ')) {
window.alert('Illegal characters in ' +
'name_prefix in createMenu()!');
return;
}
let menu = $('<div id="menu" class="menu_block ' + name_prefix + '_menu"></div>');
let first = true;
for (const [k, v] of options) {
let item = $('<div></div>');
if (first) {
item.addClass('menu_item_first');
first = false;
} else {
item.addClass('menu_item');
}
item.addClass('menu_item_with_hover');
item.on('click', {
'data': v[0],
'handler': v[1]
}, event => {
Window.stopPropagation(event);
Menu.closeMenu();
if (event.data.handler !== undefined) {
event.data.handler(event);
}
});
item.text(k);
menu.append(item);
}
menu.css('top', y);
menu.css('left', x);
menu.outerHeight('auto');
menu.css('display', 'flex');
menu.css('z-index', '10001');
let height = menu.outerHeight();
let width = menu.outerWidth();
if (y + height > $(window).outerHeight() - 30) {
menu.outerHeight($(window).outerHeight() - y - 30);
}
if (x + width > $(window).outerWidth() - 20) {
menu.css('left', Math.max(0, x - width));
}
Menu.closeMenu();
$('body').append(menu);
}
/**
* Creates and displays a menu with custom-made blocks.
*
* If you want to refer to the menu block in CSS, use
* .<name_prefix>_menu.
*
* @static
* @param {String} name_prefix Prefix to use for the menu
* block class in CSS.
* @param {int} x x-part of the upper-left corner position
* of a menu.
* @param {int} y y-part of the upper-left corner position
* of a menu.
* @param {Array} blocks Array of custom menu items of type
* `{item: <item>, hover: <hover>, click_handler: [<arbitrary
* data>, <click event handler>]}`, where `<item>` is
* a jQuery object representing a custom menu item element
* (e.g. `$('<div>Hello World!</div>')`), `<hover>` is a
* boolean indicating whether the item should be highlighted
* on hover, and `[<arbitrary data>, <click event handler>]`
* is the same as in `createMenu()`. `<hover>` is optional,
* its default value is false.
*/
static createMenuWithCustomBlocks(name_prefix, x, y, blocks) {
Menu.closeMenu();
if (name_prefix.includes('"') || name_prefix.includes(' ')) {
window.alert('Illegal characters in ' +
'name_prefix in createMenuWithCustomBlocks()!');
return;
}
let menu = $('<div id="menu" class="menu_block ' + name_prefix + '_menu"></div>');
let first = true;
for (const v of blocks) {
if (first) {
v.item.addClass('menu_item_first');
first = false;
} else {
v.item.addClass('menu_item');
}
if ('hover' in v && v.hover) {
v.item.addClass('menu_item_with_hover');
}
if (v.click_handler === undefined) {
v.item.on('click', event => {
Window.stopPropagation(event);
});
} else {
v.item.on('click', {
'data': v.click_handler[0],
'handler': v.click_handler[1]
}, event => {
Window.stopPropagation(event);
Menu.closeMenu();
if (event.data.handler !== undefined) {
event.data.handler(event);
}
});
}
menu.append(v.item);
}
menu.css('top', y);
menu.css('left', x);
menu.outerHeight('auto');
menu.css('display', 'flex');
menu.css('z-index', '10001');
let height = menu.outerHeight();
let width = menu.outerWidth();
if (y + height > $(window).outerHeight() - 30) {
menu.outerHeight($(window).outerHeight() - y - 30);
}
if (x + width > $(window).outerWidth() - 20) {
menu.css('left', Math.max(0, x - width));
}
Menu.closeMenu();
$('body').append(menu);
}
/**
* Closes a menu.
*/
static closeMenu() {
$('#menu').remove();
}
}
// Private, not meant to be used by any external code.
class SettingsWindow extends Window {
#current_backend;
getType() {
return 'settings';
}
getContentCode() {
return `
<div class="toolbar">
<select name="settings_backends" class="settings_backends_combobox" autocomplete="off">
<option value="" selected="selected" disabled="disabled">
Please select a backend...
</option>
</select>
</div>
<div class="window_space settings_space">
<div class="centered">Select a backend first to be able to change its settings.</div>
</div>`;
}
startResize() {
}
finishResize() {
}
getTitle() {
return 'Settings';
}
prepareRefresh() {
if (this.#current_backend !== undefined) {
this.#current_backend.hide();
this.#current_backend.appendTo('body');
}
}
prepareClose() {
if (this.#current_backend !== undefined) {
this.#current_backend.hide();
this.#current_backend.appendTo('body');
}
}
_setup(data, existing_window) {
$('.settings_block').each((i, elem) => {
this.getContent().find('.settings_backends_combobox').append(
new Option($(elem).attr('data-backend'), $(elem).attr('id')));
});
this.getContent().find('.settings_backends_combobox').on('change', event => {
this.getContent().find('.settings_backends_combobox option:selected').each((i, elem) => {
let id = $(elem).val();
if (this.#current_backend === undefined) {
this.getContent().find('.settings_space').html('');
} else {
this.#current_backend.hide();
this.#current_backend.appendTo('body');
}
this.#current_backend = $('#' + id);
this.#current_backend.appendTo(this.getContent().find('.settings_space'));
this.#current_backend.show();
});
});
this.hideLoading();
}
}
// Private, not meant to be called by any external code.
function loadCurrentSession() {
let versionLessThan = (a, b) => {
for (var i = 0; i < Math.min(a.length, b.length); i++) {
if (a[i] > b[i]) {
return false;
}
if (a[i] < b[i]) {
return true;
}
}
if (a.length >= b.length) {
return false;
} else {
return true;
}
};
$('#refresh').attr('class', 'disabled');
$('#refresh').attr('onclick', '');
$('#block').html('');
$('#loading').css('display', 'flex');
$('#footer_text').text('Please wait...');
$('#results_combobox option:selected').each(function() {
let id = $(this).val();
let label = $(this).attr('data-label');
let session = id in Session.instances ? Session.instances[id] :
new Session(id, label);
let min_mod_vers = JSON.parse($('#viewer_script').attr('data-min-mod-vers'));
$.ajax({
url: id + '/',
method: 'GET'
}).done(ajax_obj => {
let response = JSON.parse(ajax_obj);
let graph = graphology.Graph.from(response.system);
let view = new Sigma(graph, $('#block')[0], {
});
view.on('doubleClickNode', (node) => {
node.event.preventSigmaDefault();
let backends = graph.getNodeAttribute(node.node, 'backends');
let entity = graph.getNodeAttribute(node.node, 'entity');
let options = [];
for (let [name, version] of backends) {
let item_label = name;
if (version.length === 0) {
item_label += ' (unknown version)';
}
options.push([item_label,
[{
backend_name: name,
entity: entity,
node: node.node,
session: session
}, (event) => {
if (version.length > 0 && (name in min_mod_vers) &&
min_mod_vers[name].length > 0 &&
versionLessThan(version, min_mod_vers[name])) {
let proceed =
window.confirm('The Adaptyst module used for producing the results is ' +
'older than the minimum version supported by the ' +
'corresponding Adaptyst Analyser module!\n\n' +
'Expect errors and incorrect behaviours. Click ' +
'OK if you want to proceed.');
if (!proceed) {
return;
}
}
import('./modules/' + event.data.data.backend_name + '/backend.js')
.then(backend => {
if (!(event.data.data.backend_name in
event.data.data.session.modules_loaded)) {
$('<link type="text/css" rel="stylesheet" href="/static/' +
'modules/' + event.data.data.backend_name + '/backend.css" />').appendTo('head');
event.data.data.session.modules_loaded[event.data.data.backend_name] = true;
}
backend.createRootWindow(event.data.data.entity,
event.data.data.node,
event.data.data.session);
}, () => {
window.alert('Could not load the module! ' +
'Are you sure it is installed?');
});
}]]);
}
Menu.createMenu('system_graph',
node.event.original.pageX,
node.event.original.pageY,
options);
});
$('#loading').hide();
let non_zero_exit_codes = [];
for (const [entity, data] of Object.entries(response.entities)) {
if (data[0] > 0) {
non_zero_exit_codes.push([entity, data[0]]);
}
}
if (non_zero_exit_codes.length === 0) {
$('#footer_text').text('You can see a graph describing your computer system. ' +
'Double-click any node and select a module to open an internal window ' +
'with a detailed analysis of the node done by the module.');
} else {
let entity_cnt_hover = '';
for (let i = 0; i < non_zero_exit_codes.length; i++) {
entity_cnt_hover += non_zero_exit_codes[i][0] + ': exit code ' +
non_zero_exit_codes[i][1] +
(non_zero_exit_codes[i][1] === 210 ? ' (may suggest a fatal error, ' +
'e.g. a seg fault)' : '') +
(i < non_zero_exit_codes.length - 1 ? '\n' : '');
}
$('#footer_text').html('You can see a graph describing your computer system. ' +
'Double-click any node and select a module to open an internal window ' +
'with a detailed analysis of the node done by the module.<br />' +
'<b><font color="#ff9900">WARNING:</font></b> The workflow in ' +
'<span style="cursor:help; text-decoration:underline" ' +
'title="' + entity_cnt_hover + '">' +
(non_zero_exit_codes.length === 1 ? '1 entity' :
(non_zero_exit_codes.length) + ' entities') + '</span> ' +
'returned a non-zero exit code. Hover over the underlined text to ' +
'see more details.');
}
$('#refresh').attr('class', '');
$('#refresh').attr('onclick', 'loadCurrentSession()');
}).fail(ajax_obj => {
$('#loading').hide();
if (ajax_obj.status === 500) {
$('#footer_text').html('<b><font color="red">Could not load the session because of an ' +
'error on the server side!</font></b>');
} else {
$('#footer_text').html('<b><font color="red">Could not load the session! (HTTP code ' +
ajax_obj.status + ')</font></b>');
}
});
});
}
$(document).on('change', '#results_combobox', loadCurrentSession);
// Private, not meant to be called by any external code.
function onSessionRefreshClick(event) {
loadCurrentSession();
}
// Private, not meant to be called by any external code.
function onSettingsClick(event) {
new SettingsWindow(undefined, undefined, undefined, {});
}