// SPDX-FileCopyrightText: 2026 CERN
// SPDX-License-Identifier: LGPL-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;
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: 'process/' + 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 = {};
/**
* Static dictionary storing all modules loaded in the client
* side of Adaptyst Analyser.
*
* @static
*/
static modules_loaded = {};
/**
* Static variable storing an instance of Sigma for displaying
* a system graph of the current session.
*
* @static
*/
static system_graph_view = undefined;
// 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 {
if (!Window.isInCompactMode()) {
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) {
if (event != undefined) {
event.stopPropagation();
event.preventDefault();
}
}
/**
* Returns whether Adaptyst Analyser is run in compact mode.
*
* @return {bool} Whether compact mode is enabled.
*
* @static
*/
static isInCompactMode() {
return $('body').attr('data-compact') === '1';
}
/**
* Creates a window from a JSON-able dictionary produced by `serialize()`
* or an equivalent JSON string.
*
* The resulting window object can be accessed in Window.instances
* using the ID stored in the JSON string under "id".
*
* @static
* @param [json] A JSON-able dictionary produced by `serialize()` or
* an equivalent JSON string.
* @param {Function} [ready_handler] Function to be called
* when a window finishes loading. It should have zero parameters and
* no return value. This is not run if an error occurs. It can be
* undefined.
*/
static deserialize(json, ready_handler) {
let obj = typeof json == 'string' || json instanceof String ? JSON.parse(json) : json;
let createWindow = window_class => {
let session = undefined;
if (obj.session != undefined) {
if (obj.session in Session.instances) {
session = Session.instances[obj.session];
} else {
session =
new Session(obj.session,
$('#results_combobox').find(
'option[value="' + obj.session + '"]').attr(
'data-label'));
}
}
let window_obj = new window_class(true, ...obj.constr);
window_obj.init(window_obj, session,
obj.entity,
obj.analysable,
obj.module,
obj.init_data,
obj.x,
obj.y,
obj.dependencies,
obj.id, obj.width,
obj.height,
w => {
if (obj.data != undefined) {
w._importData(obj.data);
}
w.#editTitle(obj.custom_title);
if (obj.collapsed && !Window.isInCompactMode()) {
w.onVisibilityClick();
}
if (ready_handler != undefined) {
ready_handler();
}
});
};
if (obj.module == undefined) {
if (obj.type === 'link') {
createWindow(LinkWindow);
} else if (obj.type === 'settings') {
createWindow(SettingsWindow);
} else if (obj.type === 'open_arrangement') {
createWindow(OpenArrangementWindow);
} else {
window.alert('Could not find window type ' + obj.type +
' (no module is specified)!');
}
} else {
import('./modules/' + obj.module + '/backend.js')
.then(backend => {
if (!(obj.module in Window.modules_loaded)) {
$('<link type="text/css" rel="stylesheet" href="/static/' +
'modules/' + obj.module + '/backend.css" />').appendTo('head');
Window.modules_loaded[obj.module] = true;
}
let window_class = backend.getWindowClass(obj.type);
if (window_class == undefined) {
window.alert('Could not find window type ' + obj.type + ' in module ' +
obj.module + '!');
return;
}
createWindow(window_class);
}, () => {
window.alert('Could not load module ' + obj.module + '! ' +
'Are you sure it is installed?');
});
}
}
// Private, should not be called by any external code.
static sendArrgmtRequest(req_data, done_func, fail_func) {
$.ajax({
url: '/arrgmt',
method: 'POST',
dataType: 'json',
data: req_data
}).done((data, status, xhr) => {
done_func(data, status);
}).fail(fail_func);
}
#id;
#session;
#entity_id;
#analysable_id;
#data;
#module_name;
#being_resized;
#collapsed;
#last_focus;
#dom;
#first_resize_call;
#loading_jquery;
#setup_data;
#min_height;
#last_height;
#content;
#custom_title;
#window_dependencies;
#constructor_data;
#ready_handler;
#centered;
/**
* Constructs a Window object. This doesn't do anything
* else, including displaying a window: you need to call
* `init()` for this unless the window is deserialized
* (i.e. created by Window.deserialize(), this will
* be indicated by the first argument to the subclass
* constructor set to true).
*
* @constructor
*/
constructor() {
}
/**
* Initialises a Window object and displays a window
* corresponding to the object. This function must
* be called by all subclasses from their constructor,
* with the first argument being JavaScript "this".
*
* The method should not be overridden: otherwise,
* the serialising/deserialising feature won't work
* properly if at all.
*
* @param {Object} [instance] A Window subclass object
* referred to by JavaScript "this" inside the subclass
* 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} [analysable_id] The ID of an analysable 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()` and
* `getTitle()`. It can be undefined. If window serialisation should be
* supported, the provided value must be either undefined or serialisable
* to JSON.
* @param {float} [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. This is always
* ignored in the compact mode.
* @param {float} [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. This is always
* ignored in the compact mode.
* @param {Array} [window_dependencies] Array of all Window
* objects or string IDs this window depends on, e.g. for obtaining data.
* It can be undefined.
* @param {String} [custom_id] The ID to be assigned to a window.
* All whitespaces are automatically replaced with an underscore.
* Leave this undefined unless you know what you're doing.
* @param {float} [width] Width of a window. It cannot be smaller
* than the minimum width specified in the CSS stylesheet of the
* module. If undefined, the default value will be used. This is
* always ignored in the compact mode.
* @param {float} [height] Height of a window. It cannot be smaller
* than the minimum height specified in the CSS stylesheet of the
* module. If undefined, the default value will be used. This is
* always ignored in the compact mode.
* @param {Function} [ready_handler] Function to be called
* when a window finishes loading. It should have one parameter
* corresponding to the loaded Window subclass object and no return value.
* This is not run if an error occurs. It can be undefined.
*/
init(instance, session, entity_id, analysable_id,
module_name, data, x, y,
window_dependencies, custom_id,
width, height, ready_handler) {
let index = 0;
let id = undefined;
if (custom_id == undefined) {
if (session == undefined) {
id = `w_${this.getType()}_${index}`.replace(/\s/g, '_');
while (id in Window.instances) {
index++;
id = `w_${this.getType()}_${index}`.replace(/\s/g, '_');
}
} else {
id = `w_${session.label}_${this.getType()}_${index}`.replace(/\s/g, '_');
while (id in Window.instances) {
index++;
id = `w_${session.label}_${this.getType()}_${index}`.replace(/\s/g, '_');
}
}
} else {
id = custom_id.replace(/\s/g, '_');
}
Window.instances[id] = instance;
this.#id = id;
this.#session = session;
this.#entity_id = entity_id;
this.#analysable_id = analysable_id;
this.#data = {};
this.#constructor_data = data;
this.#module_name = module_name;
this.#being_resized = false;
this.#collapsed = false;
this.#last_focus = Date.now();
this.#dom = this.#createWindowDOM(width, height);
this.#custom_title = undefined;
this.#ready_handler = ready_handler;
if (window_dependencies != undefined &&
window_dependencies.length > 0 &&
(typeof window_dependencies[0] == 'string' ||
window_dependencies[0] instanceof String)) {
this.#window_dependencies = [];
for (const s of window_dependencies) {
if (!(s in Window.instances)) {
console.error("Could not find a window dependency with ID " + s + " " +
"when constructing a window of type " + this.getType() + "!");
continue;
}
this.#window_dependencies.push(Window.instances[s]);
}
} else {
this.#window_dependencies = window_dependencies;
}
if (!Window.isInCompactMode()) {
if (x != undefined && y != undefined) {
this.#dom.css('left', x + 'px');
this.#dom.css('top', y + 'px');
this.#centered = false;
} else {
this.#dom.css('top', '50%');
this.#dom.css('left', '50%');
this.#dom.css('transform', 'translate(-50%, -50%)');
this.#centered = true;
}
} else {
this.#centered = false;
}
this.#first_resize_call = true;
new ResizeObserver(Window.onResize).observe(this.#dom[0]);
if (data == undefined) {
this.#setup({});
} else {
this.#setup(data);
}
}
/**
* Gets a Window subclass object. This is equivalent to the
* "this"/"self" keyword in an object-oriented programming (OOP)
* language.
*
* Please note that JavaScript's "this" is NOT equivalent
* and you should not rely on it unless you know the difference
* between JavaScript and an OOP language in handling the "this"
* keyword.
*/
inst() {
return Window.instances[this.#id];
}
/**
* 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.getAnalysableId(),
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 ID of the analysable of a window. It can be
* undefined.
*
* @return {String} ID of the analysable of a window.
*/
getAnalysableId() {
return this.#analysable_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(width, height) {
const window_header = `
<div class="window_header">
<span class="window_title"></span>
<div class="window_buttons">
<!-- 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/Refresh 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 id="share" xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960" height="24px" width="24px"
class="window_share" onclick="">
<title>Share window</title>
<path d="M684-80q-48.33 0-82.17-33.83Q568-147.67 568-196q0-7.33 4.33-32l-293-171.33q-16.18 16.56-37.42 25.94Q220.67-364 196-364q-48.33 0-82.17-33.67Q80-431.33 80-480t33.83-82.33Q147.67-596 196-596q24 0 45 9.03T278-562l294.33-170q-2-7.67-3.16-15.5Q568-755.33 568-764q0-48.33 33.83-82.17Q635.67-880 684-880t82.17 33.83Q800-812.33 800-764t-33.83 82.17Q732.33-648 684-648q-23.52 0-44.09-8.83-20.58-8.84-36.58-23.84L307-513.33q2.67 7.66 3.83 16.16 1.17 8.5 1.17 16.84 0 8.33-.83 15.5-.84 7.16-2.84 14.83L604-280q16-15 36.4-23.5 20.39-8.5 43.7-8.5 48.57 0 82.23 33.83Q800-244.33 800-196t-33.83 82.17Q732.33-80 684-80Zm.02-66.67q20.98 0 35.15-14.19 14.16-14.19 14.16-35.16 0-20.98-14.19-35.15-14.19-14.16-35.16-14.16-20.98 0-35.15 14.19-14.16 14.19-14.16 35.16 0 20.98 14.19 35.15 14.19 14.16 35.16 14.16Zm-488-284q20.98 0 35.15-14.19 14.16-14.19 14.16-35.16 0-20.98-14.19-35.15-14.19-14.16-35.16-14.16-20.98 0-35.15 14.19-14.16 14.19-14.16 35.16 0 20.98 14.19 35.15 14.19 14.16 35.16 14.16Zm523.15-298.19q14.16-14.19 14.16-35.16 0-20.98-14.19-35.15-14.19-14.16-35.16-14.16-20.98 0-35.15 14.19-14.16 14.19-14.16 35.16 0 20.98 14.19 35.15 14.19 14.16 35.16 14.16 20.98 0 35.15-14.19ZM684-196ZM196-480Zm488-284Z"/>
</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>
</div>
`;
let root = $('<div></div>');
root.attr('class', 'window ' + this.getType() + '_window');
let header = $(window_header);
header.attr('id', this.#id + '_header');
root.append(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);
if (Window.isInCompactMode()) {
header.attr('onclick', `Window.instances['${this.#id}'].focus()`);
header.attr('onauxclick', `Window.instances['${this.#id}'].onAuxClick(event)`);
} else {
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_share').attr(
'onclick', `Window.instances['${this.#id}'].onShareClick(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)`);
if (width != undefined) {
root.css('width', width + 'px');
}
if (height != undefined) {
root.css('height', height + 'px');
}
return root;
}
// Private, not meant to be called by any external code.
onAuxClick(event) {
if (event.button === 1) {
this.close(event);
}
}
// Private, not meant to be called by any external code.
#editTitle(new_title) {
if (new_title == undefined || new_title === '') {
return;
}
if (this.#session == undefined || Window.isInCompactMode()) {
$('#' + this.#id + '_header').find('.window_title').text(new_title);
$('#' + this.#id + '_header').attr('title', new_title);
} else {
$('#' + this.#id + '_header').find('.window_title').text(
'[Session: ' + this.#session.label + '] ' +
new_title);
$('#' + this.#id + '_header').attr('title',
'[Session: ' +
this.#session.label +
'] ' +
new_title);
}
this.#custom_title = new_title;
}
// Private, not meant to be called by any external code.
onShareClick(event) {
Window.stopPropagation(event);
let getName = () => {
return window.prompt(
"You're about to save the single window/tab (along with its dependencies) for " +
"opening later or sharing with others using the same Adaptyst Analyser instance.\n\n" +
"This is called a single window arrangement: it is defined as your chosen window/tab, " +
"its dependencies if any, and " +
"the session formally associated with the " +
"window/tab if any.\n\n" +
"The window/tab content is also saved if the content export is supported by a corresponding module.\n\n" +
"What name would you like to give to your arrangement? Leave it empty to get a random human-friendly name.");
};
let name = getName();
if (name == undefined) {
return;
} else if (name === '') {
name = undefined;
}
this.#dom.find('.window_share').addClass('disabled');
this.#dom.find('.window_share').attr('onclick', '');
this.showLoading();
Window.sendArrgmtRequest({
'type': 'check_name',
'name': name
}, (data, status) => {
if (data.exists) {
window.alert('The arrangement "' + name + '" already exists!');
this.#dom.find('.window_share').removeClass('disabled');
this.#dom.find('.window_share').attr('onclick', `Window.instances['${this.#id}'].onShareClick(event)`);
this.hideLoading();
return;
}
let session_id = this.#session != undefined ? this.#session.id : undefined;
let windows = {};
let cur_x = 10;
let cur_y = 10;
let cur_dependencies = new Set(this.getDependencyObjects());
let all_dependencies = new Set(cur_dependencies);
while (cur_dependencies.size > 0) {
let new_dependencies = new Set();
for (const d of cur_dependencies) {
for (const wd of d.getDependencyObjects()) {
new_dependencies.add(wd);
}
}
for (const d of new_dependencies) {
all_dependencies.add(d);
}
cur_dependencies = new_dependencies;
}
let instances = Array.from(all_dependencies);
instances.sort((a, b) => {
return a.getLastFocusTime() - b.getLastFocusTime();
});
for (const w of instances) {
windows[w.getId()] = w.serialize(cur_x, cur_y, true);
cur_x += 20;
cur_y += 20;
}
let arrangement = {
"session": session_id,
"main_window": this.serialize(cur_x, cur_y),
"other_windows": windows
};
Window.sendArrgmtRequest({
'type': 'save',
'name': name,
'data': JSON.stringify(arrangement)
}, (data, status) => {
if (name == undefined) {
name = data.name;
}
window.prompt('The arrangement "' + name + '" has been ' +
'saved successfully!\n\n' +
"Here's the auth token for changing the " +
"arrangement name or deleting the arrangement. " +
"You'll see it only once, save it in a safe place.",
data.token);
this.#dom.find('.window_share').removeClass('disabled');
this.#dom.find('.window_share').attr('onclick', `Window.instances['${this.#id}'].onShareClick(event)`);
this.hideLoading();
new LinkWindow(false, undefined, undefined, undefined, undefined, {
'arrgmt': data.id,
'name': name,
'compact': true,
'hide_header': true,
'hide_footer': true
});
}, (xhr, txt, error) => {
window.alert('The arrangement "' + name + '" could not be saved ' +
'due to an error! (save stage, ' +
'error type: ' + txt + '/"' + error + '"/' + xhr.status + ')');
this.#dom.find('.window_share').removeClass('disabled');
this.#dom.find('.window_share').attr('onclick', `Window.instances['${this.#id}'].onShareClick(event)`);
this.hideLoading();
});
}, (xhr, txt, error) => {
window.alert('The arrangement "' + name + '" could not be saved ' +
'due to an error! (check name stage, ' +
'error type: ' + txt + '/"' + error + '"/' + xhr.status + ')');
this.#dom.find('.window_share').removeClass('disabled');
this.#dom.find('.window_share').attr('onclick', `Window.instances['${this.#id}'].onShareClick(event)`);
this.hideLoading();
});
this.#dom.find('.window_share').removeClass('disabled');
this.#dom.find('.window_share').attr('onclick', `Window.instances['${this.#id}'].onShareClick(event)`);
this.hideLoading();
}
// Private, not meant to be called by any external code.
onEditTitleClick(event) {
Window.stopPropagation(event);
let title = undefined;
if (Window.isInCompactMode()) {
title = window.prompt('Enter a new title for the window.',
this.getCurrentTitle());
} else {
title = window.prompt('Enter a new title for the window. ' +
'The session prefix will remain ' +
'unchanged if present.',
this.getCurrentTitle());
}
this.#editTitle(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.#id + '_header');
let window_buttons = window_header.find('.window_buttons');
if (Window.#current_focused_id !== this.#id) {
if (Window.isInCompactMode()) {
this.#first_resize_call = true;
$('#footer_text').hide();
let current_content = $('#block').children();
current_content.hide();
this.#dom.show();
if (this.#being_resized) {
this.finishResize();
this.#being_resized = false;
}
$('head').find('title').text('Adaptyst Analyser: ' +
this.getCurrentTitle());
} else {
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');
window_buttons.css('background-color', 'black');
window_buttons.css('color', 'white');
window_buttons.css('fill', 'white');
window_buttons.css('border-left-color', '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.#id + '_header');
let unfocused_buttons = unfocused_header.find('.window_buttons');
unfocused_header.css('background-color', 'lightgray');
unfocused_header.css('color', 'black');
unfocused_header.css('fill', 'black');
unfocused_buttons.css('background-color', 'lightgray');
unfocused_buttons.css('color', 'black');
unfocused_buttons.css('fill', 'black');
unfocused_buttons.css('border-left-color', 'black');
if (Window.#current_focused_id === this.#id) {
Window.#current_focused_id = undefined;
}
}
// 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() {
if (this.#being_resized) {
return;
}
if (Window.isInCompactMode()) {
if (this.startResize()) {
if (Window.#current_focused_id === this.#id) {
this.finishResize();
} else {
this.#being_resized = true;
}
}
} else {
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();
this.#centered = false;
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);
if (Window.isInCompactMode()) {
this.focus();
}
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() {
if (this.#loading_jquery == undefined) {
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();
$('#' + this.#id + '_header').find('.window_refresh').addClass('disabled');
$('#' + this.#id + '_header').find('.window_share').addClass('disabled');
$('#' + this.#id + '_header').find('.window_refresh').attr('onclick', '');
$('#' + this.#id + '_header').find('.window_share').attr('onclick', '');
}
}
/**
* Hides the loading indicator in a window.
*/
hideLoading() {
if (this.#loading_jquery != undefined) {
this.#loading_jquery.remove();
this.#loading_jquery = undefined;
$('#' + this.#id + '_header').find('.window_refresh').removeClass('disabled');
$('#' + this.#id + '_header').find('.window_share').removeClass('disabled');
$('#' + this.#id + '_header').find('.window_refresh').attr('onclick',
`Window.instances['${this.#id}'].onRefreshClick(event)`);
$('#' + this.#id + '_header').find('.window_share').attr('onclick',
`Window.instances['${this.#id}'].onShareClick(event)`);
if (this.#ready_handler != undefined) {
this.#ready_handler(this.inst());
this.#ready_handler = undefined;
}
}
}
// 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) {
let title = undefined;
if (this.#session == undefined || Window.isInCompactMode()) {
title = this.getTitle(data);
} else {
title = '[Session: ' + this.#session.label + '] ' +
this.getTitle(data);
}
let header = existing_window ? $('#' + this.#id + '_header') :
this.#dom.find('.window_header');
header.find('.window_title').text(title);
header.attr('title', title);
}
this.showLoading();
if (!existing_window) {
if (Window.isInCompactMode()) {
$('#tabs_content').append(this.#dom.find('.window_header'));
$('#tabs').prop('scrollLeft', $('#tabs').prop('scrollWidth'));
this.#dom.css('height', '100%');
this.#dom.css('width', '100%');
$('#graph').hide();
$('#block').append(this.#dom);
} else {
this.#dom.appendTo('body');
}
this.focus();
}
this.#setup_data = data;
this._setup(data, existing_window);
}
/**
* Gets the expected title of a window. A user is free
* to change it by using the "edit title" feature.
*
* @param [data] Arbitrary data passed to the constructor, e.g.
* a dictionary.
*
* @abstract
* @return {String} Expected title of a window.
*/
getTitle(data) {
throw new Error('This is an abstract method!');
}
/**
* Gets the title of a window.
*
* @return {String} Title of a window.
*/
getCurrentTitle() {
if (this.#custom_title == undefined) {
return this.getTitle(this.#constructor_data);
} else {
return this.#custom_title;
}
}
/**
* 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!');
}
/**
* Gets an array containing the ID strings of all windows this window
* depends on, e.g. for obtaining data.
*
* This method returns an empty array if no window dependencies
* have been provided in the constructor.
*
* undefined should never be returned.
*
* @return {Array} Array of window dependencies in form of ID strings.
*/
getDependencies() {
if (this.#window_dependencies == undefined) {
return [];
}
let array = [];
for (const w of this.#window_dependencies) {
array.push(w.getId());
}
return array;
}
/**
* Same as `getDependencies()`, but an array of Window objects
* is returned instead of an array of window ID strings.
*
* @return {Array} Array of window dependencies in form of Window
* objects.
*/
getDependencyObjects() {
if (this.#window_dependencies == undefined) {
return [];
}
return this.#window_dependencies;
}
/**
* Serialises the window so that it can be reconstructed
* later using `Window.deserialize()`. This is useful e.g.
* for saving the window for opening later or sharing with
* other users of the same Adaptyst Analyser instance.
*
* The return format is a JSON-able dictionary in the following form:
* ```
* {
* "id": <window ID: always present>,
* "module": <module name: not always present>,
* "type": <window type: always present>,
* "constr": <array of arguments to be passed to the window constructor: always present>,
* "session": <session ID: not always present>,
* "entity": <entity ID: not always present>,
* "analysable": <ID of analysable: not always present>,
* "init_data": <arbitrary data passed to _setup() and getTitle(): not always present>,
* "x": <x-part of the initial upper-left corner position of the window: not always present>,
* "y": <y-part of the initial upper-left corner position of the window: not always present>,
* "dependencies": <window dependencies as returned by getDependencies(): always present>,
* "collapsed": <whether the window is collapsed: always present>,
* "custom_title": <custom title if any: not always present>,
* "data": <window data returned by _exportData(): not always present>
* }
* ```
*
* You should not assume that `Window.deserialize()` will be always called
* in the standard mode only or in the compact mode only.
*
* @param {int} [x] x-part of the initial upper-left corner position
* of the window in case it cannot be extracted automatically (e.g.
* due to being in the compact mode). If undefined, the window will
* be centered in a screen when deserialised unless the coordinate
* can be obtained automatically.
* @param {int} [y] y-part of the initial upper-left corner position
* of the window in case it cannot be extracted automatically (e.g.
* due to being in the compact mode). If undefined, the window will
* be centered in a screen when deserialised unless the coordinate
* can be obtained automatically.
* @param {bool} [collapsed] Whether the window should be serialised
* in the collapsed state. If undefined, the current collapsed status
* will be used instead (which is always false if the compact mode is on).
* @return Window serialised in form of a JSON-able dictionary.
*/
serialize(x, y, collapsed) {
if (!Window.isInCompactMode()) {
if (this.#centered) {
x = undefined;
y = undefined;
} else {
let x_tmp = Number.parseFloat(this.#dom.css('left'));
let y_tmp = Number.parseFloat(this.#dom.css('top'));
if (!isNaN(x_tmp) && !isNaN(y_tmp)) {
x = x_tmp;
y = y_tmp;
}
}
}
return {
"id": this.#id,
"module": this.#module_name,
"type": this.getType(),
"constr": this.getConstructorArgs(),
"session": this.#session != undefined ? this.#session.id : undefined,
"entity": this.#entity_id,
"analysable": this.#analysable_id,
"init_data": this.#constructor_data,
"x": x,
"y": y,
"dependencies": this.getDependencies(),
"collapsed": collapsed != undefined ? collapsed : (
Window.isInCompactMode() ? false : this.#collapsed),
"custom_title": this.#custom_title,
"data": this._exportData(),
"width": Window.isInCompactMode() ? undefined : this.#dom.outerWidth(),
"height": Window.isInCompactMode() ? undefined :
(this.#collapsed ? this.#last_height : this.#dom.outerHeight())
};
}
/**
* Gets the array of arguments passed to the constructor (not init()) except for
* the first argument indicating whether the window is being deserialised. This is
* useful for serialising/deserialising a window.
*
* The default implementation returns an empty array. undefined should
* never be returned.
*
* @return Array of constructor arguments, starting from the second one.
*/
getConstructorArgs() {
return [];
}
/**
* Returns window content data to be used by `serialize()`.
* The extent to which the contents are saved in `_exportData()`
* is decided by your implementation.
*
* When `Window.deserialize()` is called with a dictionary
* produced by `serialize()`, the part there generated by
* `_exportData()` is passed to `_importData()` to restore the window.
*
* There are no specific guidelines on the format of the return data
* as long as it is JSON-compatible and accepted by the implementation
* of `_importData()`.
*
* The default implementation returns undefined. You should override
* this method if you want a different behaviour.
*
* @return Serialised content data of the window.
*/
_exportData() {
return undefined;
}
/**
* Restores the window content using data produced by `_exportData()`. This
* is used by `Window.deserialize()`, called e.g. when a user opens
* a window arrangement saved by another user of the same Adaptyst
* Analyser instance.
*
* If used, this is guaranteed to be called after the window
* is constructed and `_setup()` is executed.
*
* The default implementation does nothing. You should override
* this method if you want a different behaviour.
*
* @param [data] Serialised content data of the window to be restored.
*/
_importData(data) {
}
// Private, not meant to be called by any external code.
close(event) {
Window.stopPropagation(event);
this.prepareClose();
$('#' + this.#id + '_header').remove();
this.#dom.remove();
delete Window.instances[this.#id];
if (Window.#current_focused_id === this.#id) {
Window.#current_focused_id = undefined;
}
let keys = Object.keys(Window.instances);
if (keys.length === 0) {
if (Window.isInCompactMode()) {
openSystemGraph();
}
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.#id + '_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.css('height', window_header.outerHeight() + 'px');
} else {
this.#collapsed = false;
this.#dom.css('height', this.#last_height + 'px');
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`.
* @param {bool} [html] Whether option texts should be
* treated as an HTML code. This is false by default.
*/
static createMenu(name_prefix, x, y, options, html) {
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);
}
});
if (html) {
item.html(k);
} else {
item.text(k);
}
menu.append(item);
}
menu.css('top', y);
menu.css('left', x);
menu.outerHeight('auto');
menu.css('display', 'none');
menu.css('z-index', '10001');
Menu.closeMenu();
$('body').append(menu);
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.css('display', 'flex');
}
/**
* 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) {
// SVGs below are from Google Material Icons, licensing:
// SPDX-FileCopyrightText: Google
// SPDX-License-Identifier: Apache-2.0
// ************************
let setUpIcon = (target, code) => {
let obj = v.item.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');
// ************************
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', 'none');
menu.css('z-index', '10001');
Menu.closeMenu();
$('body').append(menu);
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.css('display', 'flex');
}
/**
* Closes a menu.
*/
static closeMenu() {
$('#menu').remove();
}
}
// Private, not meant to be used by any external code.
class LinkWindow extends Window {
constructor(deserialized, ...args) {
super();
if (!deserialized) {
this.init(this, ...args);
}
}
getType() {
return 'link';
}
getContentCode() {
return `
<div class="link_header">
<input class="link_box" type="text" readonly />
<!-- This SVG is from Google Material Icons, licensing:
SPDX-FileCopyrightText: Google
SPDX-License-Identifier: Apache-2.0 -->
<a href="" target="_blank" class="link_open">
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#000000">
<title>Open in new browser tab/window</title>
<path d="M200-120q-33 0-56.5-23.5T120-200v-560q0-33 23.5-56.5T200-840h280v80H200v560h560v-280h80v280q0 33-23.5 56.5T760-120H200Zm188-212-56-56 372-372H560v-80h280v280h-80v-144L388-332Z"/>
</svg>
</a>
</div>
<div class="link_setting">
<input type="checkbox" name="link_compact" class="link_compact" />
<label title="Suitable for tablets, smaller desktop screens, and embedding in websites. May be *not* suitable for phones." class="help_pointer">Compact mode</label>
</div>
<div class="link_compact_setting">
<input type="checkbox" name="link_hide_header" class="link_hide_header" />
<label title="This saves space. A user can always toggle the header if needed." class="help_pointer">Hide header by default</label>
</div>
<div class="link_compact_setting">
<input type="checkbox" name="link_hide_footer" class="link_hide_footer" />
<label title="This saves space. A user can always toggle the footer if needed." class="help_pointer">Hide footer by default</label>
</div>
`;
}
startResize() {
return false;
}
finishResize() {
}
getTitle(data) {
let title = 'Link to share for ';
if ('session' in data) {
title += 'session: ' +
$('#results_combobox').find("[value='" + data.session + "']").attr('data-label');
} else if ('arrgmt' in data) {
title += 'arrangement';
if ('name' in data) {
title += ': ' + data.name;
}
} else {
return 'Link to share';
}
return title;
}
prepareRefresh() {
}
prepareClose() {
}
_exportData() {
return {
'compact': this.inst().getContent().find('.link_compact').prop('checked'),
'hide_header': this.inst().getContent().find('.link_hide_header').prop('checked'),
'hide_footer': this.inst().getContent().find('.link_hide_footer').prop('checked')
}
}
_importData(data) {
if (data.hide_header) {
this.inst().getContent().find('.link_hide_header').prop('checked', true);
} else {
this.inst().getContent().find('.link_hide_header').prop('checked', false);
}
if (data.hide_footer) {
this.inst().getContent().find('.link_hide_footer').prop('checked', true);
} else {
this.inst().getContent().find('.link_hide_footer').prop('checked', false);
}
if (data.compact) {
this.inst().getContent().find('.link_compact').prop('checked', true);
} else {
this.inst().getContent().find('.link_compact').prop('checked', false);
}
this.inst().getContent().find('.link_compact').trigger('change');
}
_setup(data, existing_window) {
let url_txt = './?';
if ('session' in data) {
url_txt += 'session=' + data.session;
} else if ('arrgmt' in data) {
url_txt += 'arrgmt=' + data.arrgmt;
}
if ('compact' in data && data.compact) {
url_txt += '&compact=1';
this.inst().getContent().find('.link_compact').prop('checked', true);
if ('hide_header' in data && data.hide_header) {
url_txt += '&hide_header=1';
this.inst().getContent().find('.link_hide_header').prop('checked', true);
}
if ('hide_footer' in data && data.hide_footer) {
url_txt += '&hide_footer=1';
this.inst().getContent().find('.link_hide_footer').prop('checked', true);
}
} else {
this.inst().getContent().find('.link_hide_header').prop('disabled', true);
this.inst().getContent().find('.link_hide_footer').prop('disabled', true);
}
let url = new URL(url_txt, document.baseURI);
this.inst().getContent().find('.link_box').val(url.href);
this.inst().getContent().find('.link_box').select();
this.inst().getContent().find('.link_open').attr('href', url.href);
let refresh = () => {
let url_txt = './?';
if ('session' in data) {
url_txt += 'session=' + data.session;
} else if ('arrgmt' in data) {
url_txt += 'arrgmt=' + data.arrgmt;
}
if (this.inst().getContent().find('.link_compact').prop('checked')) {
url_txt += '&compact=1';
this.inst().getContent().find('.link_hide_header').prop('disabled', false);
this.inst().getContent().find('.link_hide_footer').prop('disabled', false);
if (this.inst().getContent().find('.link_hide_header').prop('checked')) {
url_txt += '&hide_header=1';
}
if (this.inst().getContent().find('.link_hide_footer').prop('checked')) {
url_txt += '&hide_footer=1';
}
} else {
this.inst().getContent().find('.link_hide_header').prop('disabled', true);
this.inst().getContent().find('.link_hide_footer').prop('disabled', true);
}
let url = new URL(url_txt, document.baseURI);
this.inst().getContent().find('.link_box').val(url.href);
this.inst().getContent().find('.link_box').select();
this.inst().getContent().find('.link_open').attr('href', url.href);
};
this.inst().getContent().find('.link_compact').on('change', refresh);
this.inst().getContent().find('.link_hide_header').on('change', refresh);
this.inst().getContent().find('.link_hide_footer').on('change', refresh);
this.inst().hideLoading();
}
}
// Private, not meant to be used by any external code.
class SettingsWindow extends Window {
#current_backend;
constructor(deserialized, ...args) {
super();
if (!deserialized) {
this.init(this, ...args);
}
}
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() {
return false;
}
finishResize() {
}
getTitle() {
return 'Settings';
}
prepareRefresh() {
if (this.inst().#current_backend != undefined) {
this.inst().#current_backend.hide();
this.inst().#current_backend.appendTo('body');
this.inst().#current_backend = undefined;
}
}
prepareClose() {
if (this.inst().#current_backend != undefined) {
this.inst().#current_backend.hide();
this.inst().#current_backend.appendTo('body');
}
$('#settings_settings').attr('class', 'pointer');
$('#settings_settings').find('title').text('Settings');
$('#settings_settings').attr('onclick', 'onSettingsClick(event)');
}
_setup(data, existing_window) {
if (!existing_window) {
$('#settings_settings').attr('class', 'disabled');
$('#settings_settings').find('title').text('Settings (already open)');
$('#settings_settings').attr('onclick', '');
}
let names = [];
$('.settings_block').each((i, elem) => {
names.push([$(elem).attr('data-backend'), $(elem).attr('id')]);
});
names.sort((a, b) => {
if (a[0].toLowerCase() < b[0].toLowerCase()) {
return -1;
} else if (a[0].toLowerCase() > b[0].toLowerCase()) {
return 1;
} else {
return 0;
}
});
names.forEach(entry => {
this.inst().getContent().find('.settings_backends_combobox').append(
new Option(entry[0], entry[1]));
});
this.inst().getContent().find('.settings_backends_combobox').on('change', event => {
this.inst().getContent().find('.settings_backends_combobox option:selected').each((i, elem) => {
let id = $(elem).val();
if (this.inst().#current_backend == undefined) {
this.inst().getContent().find('.settings_space').html('');
} else {
this.inst().#current_backend.hide();
this.inst().#current_backend.appendTo('body');
}
this.inst().#current_backend = $('#' + id);
this.inst().#current_backend.appendTo(this.inst().getContent().find('.settings_space'));
this.inst().#current_backend.show();
});
});
this.inst().hideLoading();
}
_exportData() {
return {
'module': this.inst().getContent().find('.settings_backends_combobox option:selected').val()
};
}
_importData(data) {
this.inst().getContent().find('.settings_backends_combobox').val(data.module);
this.inst().getContent().find('.settings_backends_combobox').trigger('change');
}
}
class OpenArrangementWindow extends Window {
#types
#sort
#search
#page
#last_page
constructor(deserialized, ...args) {
super();
if (!deserialized) {
this.init(this, ...args);
}
}
getType() {
return 'open_arrangement';
}
getContentCode() {
return `
<!-- All SVGs used in this initial window code are from Google Material Icons, licensing:
SPDX-FileCopyrightText: Google
SPDX-License-Identifier: Apache-2.0 -->
<div class="open_arrangement_header">
<div class="open_arrangement_page">
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#000000"
class="pointer open_arrangement_first_page" onclick="Window.instances['${this.inst().getId()}'].onGoToFirstPageClick(event)">
<title>Go to the first page</title>
<path d="M440-240 200-480l240-240 56 56-183 184 183 184-56 56Zm264 0L464-480l240-240 56 56-183 184 183 184-56 56Z"/>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#000000"
class="pointer open_arrangement_prev_page" onclick="Window.instances['${this.inst().getId()}'].onGoToPrevPageClick(event)">
<title>Go to the previous page</title>
<path d="M560-240 320-480l240-240 56 56-184 184 184 184-56 56Z"/>
</svg>
<span class="open_arrangement_cur_page">
Page <span class="open_arrangement_cur_page_num">1</span> of <span class="open_arrangement_last_page_num">1</span> (<span class="open_arrangement_cnt">0</span> arrang.)
</span>
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#000000"
class="pointer open_arrangement_next_page" onclick="Window.instances['${this.inst().getId()}'].onGoToNextPageClick(event)">
<title>Go to the next page</title>
<path d="M504-480 320-664l56-56 240 240-240 240-56-56 184-184Z"/>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#000000"
class="pointer open_arrangement_last_page" onclick="Window.instances['${this.inst().getId()}'].onGoToLastPageClick(event)">
<title>Go to the last page</title>
<path d="M383-480 200-664l56-56 240 240-240 240-56-56 183-184Zm264 0L464-664l56-56 240 240-240 240-56-56 183-184Z"/>
</svg>
</div>
<div class="open_arrangement_table_actions">
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#000000"
class="pointer" onclick="Window.instances['${this.inst().getId()}'].onSearchClick(event)">
<title>Search</title>
<path d="M784-120 532-372q-30 24-69 38t-83 14q-109 0-184.5-75.5T120-580q0-109 75.5-184.5T380-840q109 0 184.5 75.5T640-580q0 44-14 83t-38 69l252 252-56 56ZM380-400q75 0 127.5-52.5T560-580q0-75-52.5-127.5T380-760q-75 0-127.5 52.5T200-580q0 75 52.5 127.5T380-400Z"/>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#000000"
class="pointer" onclick="Window.instances['${this.inst().getId()}'].onFilterClick(event)">
<title>Filter types</title>
<path d="M440-160q-17 0-28.5-11.5T400-200v-240L168-736q-15-20-4.5-42t36.5-22h560q26 0 36.5 22t-4.5 42L560-440v240q0 17-11.5 28.5T520-160h-80Zm40-308 198-252H282l198 252Zm0 0Z"/>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#000000"
class="pointer" onclick="Window.instances['${this.inst().getId()}'].onSortByClick(event)">
<title>Sort by...</title>
<path d="M120-240v-80h240v80H120Zm0-200v-80h480v80H120Zm0-200v-80h720v80H120Z"/>
</svg>
</div>
</div>
<table class="open_arrangement_table">
<tr>
<th>Name</th>
<th>Type</th>
<th>Last update (UTC)</th>
<th>Actions</th>
</tr>
</table>`;
}
// Private, not meant to be called by any external code.
onGoToFirstPageClick(event) {
this.inst().showLoading();
Window.sendArrgmtRequest({
'type': 'list',
'types': this.inst().#types,
'sort': this.inst().#sort,
'search': this.inst().#search,
'page': 1
}, (data, status) => {
this.inst().#populateTable(data, 1);
this.inst().hideLoading();
}, (xhr, txt, error) => {
window.alert('Could not load the arrangements!\n\n' +
'Error type: ' + txt + '/"' + error + '"/' + xhr.status);
this.inst().hideLoading();
});
Window.stopPropagation(event);
}
// Private, not meant to be called by any external code.
onGoToPrevPageClick(event) {
this.inst().showLoading();
Window.sendArrgmtRequest({
'type': 'list',
'types': this.inst().#types,
'sort': this.inst().#sort,
'search': this.inst().#search,
'page': this.inst().#page - 1,
}, (data, status) => {
this.inst().#populateTable(data, this.inst().#page - 1);
this.inst().hideLoading();
}, (xhr, txt, error) => {
window.alert('Could not load the arrangements!\n\n' +
'Error type: ' + txt + '/"' + error + '"/' + xhr.status);
this.inst().hideLoading();
});
Window.stopPropagation(event);
}
// Private, not meant to be called by any external code.
onGoToNextPageClick(event) {
this.inst().showLoading();
Window.sendArrgmtRequest({
'type': 'list',
'types': this.inst().#types,
'sort': this.inst().#sort,
'search': this.inst().#search,
'page': this.inst().#page + 1,
}, (data, status) => {
this.inst().#populateTable(data, this.inst().#page + 1);
this.inst().hideLoading();
}, (xhr, txt, error) => {
window.alert('Could not load the arrangements!\n\n' +
'Error type: ' + txt + '/"' + error + '"/' + xhr.status);
this.inst().hideLoading();
});
Window.stopPropagation(event);
}
// Private, not meant to be called by any external code.
onGoToLastPageClick(event) {
this.inst().showLoading();
Window.sendArrgmtRequest({
'type': 'list',
'types': this.inst().#types,
'sort': this.inst().#sort,
'search': this.inst().#search,
'page': this.inst().#last_page,
}, (data, status) => {
this.inst().#populateTable(data, this.inst().#last_page);
this.inst().hideLoading();
}, (xhr, txt, error) => {
window.alert('Could not load the arrangements!\n\n' +
'Error type: ' + txt + '/"' + error + '"/' + xhr.status);
this.inst().hideLoading();
});
Window.stopPropagation(event);
}
// Private, not meant to be called by any external code.
onSearchClick(event) {
let search = window.prompt('Please enter your search ' +
'query for an arrangement name. Use regex.\n\n' +
'To clear search later, click "Search" and leave ' +
'the field empty.', this.inst().#search);
if (search == undefined) {
return;
}
if (search == '') {
if (this.inst().#search != undefined) {
this.inst().showLoading();
Window.sendArrgmtRequest({
'type': 'list',
'types': this.inst().#types,
'sort': this.inst().#sort
}, (data, status) => {
this.inst().#search = undefined;
this.inst().#populateTable(data, 1);
this.inst().hideLoading();
}, (xhr, txt, error) => {
window.alert('Could not load the arrangements!\n\n' +
'Error type: ' + txt + '/"' + error + '"/' + xhr.status);
this.inst().hideLoading();
});
}
return;
}
this.inst().showLoading();
Window.sendArrgmtRequest({
'type': 'list',
'types': this.inst().#types,
'sort': this.inst().#sort,
'search': search
}, (data, status) => {
this.inst().#search = search;
this.inst().#populateTable(data, 1);
this.inst().hideLoading();
}, (xhr, txt, error) => {
window.alert('Could not load the arrangements!\n\n' +
'Error type: ' + txt + '/"' + error + '"/' + xhr.status);
this.inst().hideLoading();
});
Window.stopPropagation(event);
}
// Private, not meant to be called by any external code.
onFilterClick(event) {
let reload = types => {
this.inst().showLoading();
Window.sendArrgmtRequest({
'type': 'list',
'types': types,
'sort': this.inst().#sort,
'search': this.inst().#search
}, (data, status) => {
this.inst().#types = types;
this.inst().#populateTable(data, 1);
this.inst().hideLoading();
}, (xhr, txt, error) => {
window.alert('Could not load the arrangements!\n\n' +
'Error type: ' + txt + '/"' + error + '"/' + xhr.status);
this.inst().hideLoading();
});
};
let make_bold_if_needed = (txt, val) => {
if (this.inst().#types == val) {
return '<b>' + txt + '</b>';
} else {
return txt;
}
};
let options = [
[make_bold_if_needed('Show W only', 'W'),
[undefined, () => {
reload('W');
}]],
[make_bold_if_needed('Show SW only', 'SW'),
[undefined, () => {
reload('SW');
}]],
[make_bold_if_needed('Show both W and SW', 'both'),
[undefined, () => {
reload('both');
}]]
];
Menu.createMenu('open_arrangement_filter_menu',
event.pageX,
event.pageY,
options, true);
Window.stopPropagation(event);
}
// Private, not meant to be called by any external code.
onSortByClick(event) {
let page = Number.parseInt(
this.inst().getContent().find('.open_arrangement_cur_page_num').text());
if (isNaN(page)) {
page = 1;
}
let reload = sort => {
this.inst().showLoading();
Window.sendArrgmtRequest({
'type': 'list',
'sort': sort,
'types': this.inst().#types,
'search': this.inst().#search,
'page': page
}, (data, status) => {
this.inst().#sort = sort;
this.inst().#populateTable(data, page);
this.inst().hideLoading();
}, (xhr, txt, error) => {
window.alert('Could not load the arrangements!\n\n' +
'Error type: ' + txt + '/"' + error + '"/' + xhr.status);
this.inst().hideLoading();
});
};
let make_bold_if_needed = (txt, val) => {
if (this.inst().#sort == val) {
return '<b>' + txt + '</b>';
} else {
return txt;
}
};
let options = [
[make_bold_if_needed('Sort by name (asc.)', 'name_asc'),
[undefined, () => {
reload('name_asc');
}]],
[make_bold_if_needed('Sort by name (desc.)', 'name_desc'),
[undefined, () => {
reload('name_desc');
}]],
[make_bold_if_needed('Sort by last update (asc.)', 'last_update_asc'),
[undefined, () => {
reload('last_update_asc');
}]],
[make_bold_if_needed('Sort by last update (desc.)', 'last_update_desc'),
[undefined, () => {
reload('last_update_desc');
}]]
];
Menu.createMenu('open_arrangement_sort_by_menu',
event.pageX,
event.pageY,
options, true);
Window.stopPropagation(event);
}
startResize() {
return false;
}
finishResize() {
}
getTitle() {
return 'Open an arrangement';
}
prepareRefresh() {
}
prepareClose() {
}
#getActionsToolbarCode() {
return `
<td class="open_arrangement_actions_toolbar">
<!-- All SVGs used in this actions toolbar code are 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" fill="#000000"
class="pointer open_arrangement_get_link">
<title>Get link</title>
<path d="M440-280H280q-83 0-141.5-58.5T80-480q0-83 58.5-141.5T280-680h160v80H280q-50 0-85 35t-35 85q0 50 35 85t85 35h160v80ZM320-440v-80h320v80H320Zm200 160v-80h160q50 0 85-35t35-85q0-50-35-85t-85-35H520v-80h160q83 0 141.5 58.5T880-480q0 83-58.5 141.5T680-280H520Z"/>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#000000"
class="pointer open_arrangement_edit_name">
<title>Edit name</title>
<path d="M200-200h57l391-391-57-57-391 391v57Zm-80 80v-170l528-527q12-11 26.5-17t30.5-6q16 0 31 6t26 18l55 56q12 11 17.5 26t5.5 30q0 16-5.5 30.5T817-647L290-120H120Zm640-584-56-56 56 56Zm-141 85-28-29 57 57-29-28Z"/>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#000000"
class="pointer open_arrangement_delete">
<title>Delete</title>
<path d="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"/>
</svg>
</td>
`;
}
onGetLinkClick(event, arrgmt) {
Window.stopPropagation(event);
new LinkWindow(false, undefined, undefined, undefined, undefined, {
'arrgmt': arrgmt.id,
'name': arrgmt.name,
'compact': arrgmt.type === 'SW',
'hide_header': arrgmt.type === 'SW',
'hide_footer': arrgmt.type === 'SW'
});
}
onEditNameClick(event, arrgmt) {
Window.stopPropagation(event);
let name = window.prompt('Please enter the new name of your ' +
'arrangement.', arrgmt.name);
if (name == '' || name == undefined || name === arrgmt.name) {
return;
}
let token = window.prompt('Please enter your auth token. ' +
"If you don't have/remember one, " +
'contact your Adaptyst Analyser ' +
'instance administrator.');
if (token == '' || token == undefined) {
return;
}
this.inst().showLoading();
Window.sendArrgmtRequest({
'type': 'edit_name',
'name': arrgmt.name,
'new_name': name,
'token': token
}, (data, status) => {
window.alert('The new name has just been saved!');
this.inst().#reloadTable();
}, (xhr, txt, error) => {
if (xhr.status === 403) {
window.alert('Invalid auth token!');
} else if (xhr.status === 409) {
window.alert('The new name is already taken by an ' +
'existing arrangement!');
} else {
window.alert('Could not save the new name!\n\n' +
'Error type: ' + txt + '/"' + error +
'"/' + xhr.status);
}
this.inst().hideLoading();
});
}
onDeleteClick(event, arrgmt) {
Window.stopPropagation(event);
let token = window.prompt('Please enter your auth token to confirm ' +
'deleting the arrangement "' + arrgmt.name + '". ' +
"If you don't have/remember one, " +
'contact your Adaptyst Analyser ' +
'instance administrator.');
if (token == '' || token == undefined) {
return;
}
this.inst().showLoading();
Window.sendArrgmtRequest({
'type': 'delete',
'name': arrgmt.name,
'token': token
}, (data, status) => {
window.alert('The arrangement "' + arrgmt.name + '" ' +
'has just been deleted!');
this.inst().#reloadTable();
}, (xhr, txt, error) => {
if (xhr.status === 403) {
window.alert('Invalid auth token!');
} else {
window.alert('Could not save the new name!\n\n' +
'Error type: ' + txt + '/"' + error +
+ '"/' + xhr.status);
}
this.inst().hideLoading();
});
}
#reloadTable() {
this.inst().showLoading();
Window.sendArrgmtRequest({
'type': 'list',
'sort': this.inst().#sort,
'types': this.inst().#types,
'search': this.inst().#search,
'page': this.inst().#page
}, (data, status) => {
this.inst().#populateTable(data, this.inst().#page);
this.inst().hideLoading();
}, (xhr, txt, error) => {
window.alert('Could not load the arrangements!\n\n' +
'Error type: ' + txt + '/"' + error + '"/' + xhr.status);
this.inst().hideLoading();
});
}
#populateTable(data, page) {
let numf = new Intl.NumberFormat('en-US');
this.inst().getContent().find('.open_arrangement_cur_page_num').text(
numf.format(page));
this.inst().getContent().find('.open_arrangement_last_page_num').text(
numf.format(data.general_total_pages));
this.inst().getContent().find('.open_arrangement_cnt').text(
numf.format(data.general_total_cnt));
this.inst().#page = page;
this.inst().#last_page = data.general_total_pages;
if (page == 1) {
this.inst().getContent().find('.open_arrangement_first_page').removeClass('pointer');
this.inst().getContent().find('.open_arrangement_first_page').addClass('disabled');
this.inst().getContent().find('.open_arrangement_first_page').attr('onclick', '');
this.inst().getContent().find('.open_arrangement_prev_page').removeClass('pointer');
this.inst().getContent().find('.open_arrangement_prev_page').addClass('disabled');
this.inst().getContent().find('.open_arrangement_prev_page').attr('onclick', '');
} else {
this.inst().getContent().find('.open_arrangement_first_page').removeClass('disabled');
this.inst().getContent().find('.open_arrangement_first_page').addClass('pointer');
this.inst().getContent().find(
'.open_arrangement_first_page').attr('onclick',
`Window.instances['${this.inst().getId()}'].` +
`onGoToFirstPageClick(event)`);
this.inst().getContent().find('.open_arrangement_prev_page').removeClass('disabled');
this.inst().getContent().find('.open_arrangement_prev_page').addClass('pointer');
this.inst().getContent().find(
'.open_arrangement_prev_page').attr('onclick',
`Window.instances['${this.inst().getId()}'].` +
`onGoToPrevPageClick(event)`);
}
if (page == data.general_total_pages) {
this.inst().getContent().find('.open_arrangement_last_page').removeClass('pointer');
this.inst().getContent().find('.open_arrangement_last_page').addClass('disabled');
this.inst().getContent().find('.open_arrangement_last_page').attr('onclick', '');
this.inst().getContent().find('.open_arrangement_next_page').removeClass('pointer');
this.inst().getContent().find('.open_arrangement_next_page').addClass('disabled');
this.inst().getContent().find('.open_arrangement_next_page').attr('onclick', '');
} else if (page > data.general_total_pages) {
this.inst().onGoToLastPageClick();
return;
} else {
this.inst().getContent().find('.open_arrangement_last_page').removeClass('disabled');
this.inst().getContent().find('.open_arrangement_last_page').addClass('pointer');
this.inst().getContent().find(
'.open_arrangement_last_page').attr('onclick',
`Window.instances['${this.inst().getId()}'].` +
`onGoToLastPageClick(event)`);
this.inst().getContent().find('.open_arrangement_next_page').removeClass('disabled');
this.inst().getContent().find('.open_arrangement_next_page').addClass('pointer');
this.inst().getContent().find(
'.open_arrangement_next_page').attr('onclick',
`Window.instances['${this.inst().getId()}'].` +
`onGoToNextPageClick(event)`);
}
let table = this.inst().getContent().find('.open_arrangement_table');
table.find('.open_arrangement_data_row').remove()
for (const arrgmt of data.list) {
let row = $('<tr class="open_arrangement_data_row"></tr>');
let type_title = arrgmt.type == 'W' ? "Window arrangement" :
"Single window arrangement";
row.append($(`<td>${arrgmt.name}</td>`));
row.append($(`<td><span class="open_arrangement_type" ` +
`title="${type_title}">${arrgmt.type}</span></td>`));
row.append($(`<td>${arrgmt.last_update}</td>`));
let toolbar = $(this.inst().#getActionsToolbarCode());
toolbar.find('.open_arrangement_get_link').click(event => {
this.inst().onGetLinkClick(event, arrgmt);
});
toolbar.find('.open_arrangement_edit_name').click(event => {
this.inst().onEditNameClick(event, arrgmt);
});
toolbar.find('.open_arrangement_delete').click(event => {
this.inst().onDeleteClick(event, arrgmt);
});
row.append(toolbar);
table.append(row);
}
}
_exportData() {
return {
'types': this.inst().#types,
'sort': this.inst().#sort,
'search': this.inst().#search,
'page': this.inst().#page
};
}
_importData(data) {
this.inst().#types = data.types;
this.inst().#sort = data.sort;
this.inst().#search = data.search;
this.inst().#page = data.page;
this.inst().#reloadTable();
}
_setup(data, existing_window) {
Window.sendArrgmtRequest({
'type': 'list'
}, (data, status) => {
this.inst().#sort = 'last_update_desc';
this.inst().#types = 'both';
this.inst().#populateTable(data, 1);
this.inst().hideLoading();
}, (xhr, txt, error) => {
window.alert('Could not load the arrangements!\n\n' +
'Error type: ' + txt + '/"' + error + '"/' + xhr.status);
this.inst().close();
});
}
}
// Private, not meant to be called by any external code.
function openSystemGraph() {
if ($('#graph').is(':hidden')) {
let current_content = $('#block').children();
current_content.hide();
$('#graph').show();
$('#footer_text').show();
for (const w of Object.values(Window.instances)) {
w.unfocus();
}
if (Window.system_graph_view != undefined) {
Window.system_graph_view.refresh();
}
$('head').find('title').text('Adaptyst Analyser');
}
}
// Private, not meant to be called by any external code.
function loadCurrentSession(ready_handler) {
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').removeClass('pointer');
$('#refresh').addClass('disabled');
$('#share').removeClass('pointer');
$('#share').attr('onclick', '');
$('#share').addClass('disabled');
$('#refresh').attr('onclick', '');
$('#graph').html('');
if (Window.isInCompactMode()) {
$('#loading').addClass('loading');
$('#block').append($('#loading'));
$('#loading').css('display', 'flex');
} else {
$('#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: 'get/' + id + '/',
method: 'GET'
}).done(ajax_obj => {
let response = JSON.parse(ajax_obj);
let graph = graphology.Graph.from(response.system);
let positions = forceAtlas2(graph, {
iterations: 50,
settings: {
adjustSizes: true,
gravity: 100
}
});
for (let node of Object.keys(positions)) {
graph.mergeNodeAttributes(node, positions[node]);
}
Window.system_graph_view = new Sigma(graph, $('#graph')[0], {
renderEdgeLabels: true,
defaultEdgeType: 'curve',
edgeProgramClasses: {
curve: SigmaEdgeCurve.EdgeCurvedArrowProgram
},
labelSize: 20,
edgeLabelSize: 20,
allowInvalidContainer: true
});
let view = Window.system_graph_view;
view.getCamera().setState({ratio: 2});
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: graph.getNodeAttribute(node.node, 'server_id'),
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
Window.modules_loaded)) {
$('<link type="text/css" rel="stylesheet" href="/static/' +
'modules/' + event.data.data.backend_name + '/backend.css" />').appendTo('head');
Window.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();
if (Window.isInCompactMode()) {
$('#main_tab').attr('title', 'System graph (session: ' +
label + ')');
$('#main_tab_title').text('System graph (session: ' +
label + ')');
$('#loading').removeClass('loading');
$('#footer').append($('#loading'));
}
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').html('You can see a graph describing your computer system. ' +
'Double-click any node and select a module to open an internal ' +
(Window.isInCompactMode() ? 'tab' : '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('Double-click any node and select a module to open an internal ' +
(Window.isInCompactMode() ? 'tab' : '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').removeClass('disabled');
$('#refresh').addClass('pointer');
$('#refresh').attr('onclick', 'loadCurrentSession()');
$('#footer_text').show();
$('#share').removeClass('disabled');
$('#share').addClass('pointer');
$('#share').attr('onclick', 'onShareClick(event)');
if (ready_handler != undefined) {
ready_handler();
}
}).fail((xhr, txt, error) => {
$('#loading').hide();
if (Window.isInCompactMode()) {
$('#loading').removeClass('loading');
$('#footer').append($('#loading'));
}
if (xhr.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! (error type: ' +
txt + '/"' + error + '"/' + xhr.status + ')</font></b>');
}
$('#footer_text').show();
});
});
}
// Private, not meant to be called by any external code.
function onOpenClick(event) {
new OpenArrangementWindow(false, undefined, undefined, undefined, undefined, {});
}
// Private, not meant to be called by any external code.
function openSessionLinkDialogs() {
new LinkWindow(false, undefined, undefined, undefined, undefined, {
'session': $('#results_combobox option:selected').attr('value'),
'compact': false
});
// let id = $('#results_combobox option:selected').attr('value');
// let url = new URL('./?session=' + id, document.baseURI);
// let open_compact = window.prompt(
// "Here's the URL you can share with others to let them quickly " +
// "open your session and explore it themselves.\n\n" +
// "You can also get the compact mode URL by clicking OK. " +
// "Compact mode is " +
// "suitable for tablets, smaller desktop " +
// "screens, and embedding in websites. This does *not* include phones.",
// url.href);
// if (open_compact != undefined) {
// url = new URL('./?compact=1&session=' + id, document.baseURI);
// let open_hide_footer = window.prompt(
// "Here's the requested compact mode URL you can share with others.\n\n" +
// "You can also get the compact mode URL, where the footer is hidden " +
// "by default to make more space (it can be still be toggled by users " +
// "if needed). Click OK if you want this.", url.href);
// if (open_hide_footer != undefined) {
// url = new URL('./?compact=1&hide_footer=1&session=' + id, document.baseURI);
// window.prompt("Here's the requested compact mode URL with the footer " +
// "hidden by default.", url.href);
// }
// }
}
// Private, not meant to be called by any external code.
function saveWindowArrangement() {
let getName = () => {
return window.prompt(
"You're about to save the current window arrangement for " +
"opening later or sharing with others using the same Adaptyst Analyser instance.\n\n" +
"A window arrangement is defined as your current session choice, the camera state of the system graph, " +
"and all of your windows/tabs " +
"along with their content if the content export is supported by a corresponding module.\n\n" +
"What name would you like to give to your arrangement? Leave it empty to get a random human-friendly name.");
};
let name = getName();
if (name == undefined) {
return;
} else if (name === '') {
name = undefined;
}
$('#share').removeClass('pointer');
$('#share').addClass('disabled_wait');
$('#share').attr('onclick', '');
$('html').css('cursor', 'progress');
Window.sendArrgmtRequest({
'type': 'check_name',
'name': name
}, (data, status) => {
if (data.exists) {
window.alert('The arrangement "' + name + '" already exists!');
$('#share').removeClass('disabled_wait');
$('#share').addClass('pointer');
$('#share').attr('onclick', 'onShareClick(event)');
$('html').css('cursor', '');
return;
}
let session_id = $('#results_combobox option:selected').attr('value');
let camera_state = Window.system_graph_view.getCamera().getState();
let windows = {};
let cur_x = 10;
let cur_y = 10;
let instances = Object.values(Window.instances);
instances.sort((a, b) => {
return a.getLastFocusTime() - b.getLastFocusTime();
});
for (const w of instances) {
windows[w.getId()] = w.serialize(cur_x, cur_y);
cur_x += 20;
cur_y += 20;
}
let arrangement = {
"session": session_id,
"camera_state": camera_state,
"windows": windows
};
Window.sendArrgmtRequest({
'type': 'save',
'name': name,
'data': JSON.stringify(arrangement)
}, (data, status) => {
if (name == undefined) {
name = data.name;
}
window.prompt('The arrangement "' + name + '" has been ' +
'saved successfully!\n\n' +
"Here's the auth token for changing the " +
"arrangement name or deleting the arrangement. " +
"You'll see it only once, save it in a safe place.",
data.token);
$('#share').removeClass('disabled_wait');
$('#share').addClass('pointer');
$('#share').attr('onclick', 'onShareClick(event)');
$('html').css('cursor', '');
new LinkWindow(false, undefined, undefined, undefined, undefined, {
'arrgmt': data.id,
'name': name
});
}, (xhr, txt, error) => {
window.alert('The arrangement "' + name + '" could not be saved ' +
'due to an error! (save stage, ' +
'error type: ' + txt + '/"' + error + '"/' + xhr.status + ')');
$('#share').removeClass('disabled_wait');
$('#share').addClass('pointer');
$('#share').attr('onclick', 'onShareClick(event)');
$('html').css('cursor', '');
});
}, (xhr, txt, error) => {
window.alert('The arrangement "' + name + '" could not be saved ' +
'due to an error! (check name stage, ' +
'error type: ' + txt + '/"' + error + '"/' + xhr.status + ')');
$('#share').removeClass('disabled_wait');
$('#share').addClass('pointer');
$('#share').attr('onclick', 'onShareClick(event)');
$('html').css('cursor', '');
});
}
// Private, not meant to be called by any external code.
function loadArrangement(data) {
let openWindows = () => {
let windows_to_open = [];
let windows_visited = new Set();
let error = false;
let processDependency = (cur_w, window_dict) => {
if (windows_visited.has(cur_w)) {
return;
}
windows_visited.add(cur_w);
if (cur_w.dependencies != undefined) {
for (const w of cur_w.dependencies) {
if (error) {
return;
}
if (window_dict[w] == undefined) {
error = true;
return;
}
processDependency(window_dict[w],
window_dict);
}
}
windows_to_open.push(cur_w);
};
if (data.other_windows != undefined) {
for (const w of Object.values(data.other_windows)) {
if (error) {
break;
}
processDependency(w, data.other_windows);
}
}
if (data.windows != undefined) {
for (const w of Object.values(data.windows)) {
if (error) {
break;
}
processDependency(w, data.windows);
}
}
if (error) {
window.alert('The arrangement refers to non-existing ' +
'windows! This means it is malformed and cannot ' +
'be opened.');
return;
}
if (data.main_window != undefined) {
windows_to_open.push(data.main_window);
}
let index = 0;
let deserializeNextWindow = () => {
if (++index < windows_to_open.length) {
Window.deserialize(windows_to_open[index],
deserializeNextWindow);
} else {
$('#arrgmt_loading').remove();
}
};
if (windows_to_open.length > 0) {
Window.deserialize(windows_to_open[0],
deserializeNextWindow);
}
};
if (data.session != undefined) {
$('#results_combobox').val(data.session);
loadCurrentSession(() => {
if (data.camera_state != undefined) {
Window.system_graph_view.getCamera().setState(data.camera_state);
}
openWindows();
});
} else {
openWindows();
}
}
// 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(false, undefined, undefined, undefined, undefined, {});
}
// Private, not meant to be called by any external code.
function onShareClick(event) {
let options = [
['Get session link', [undefined, openSessionLinkDialogs]],
['Save window arrangement', [undefined, saveWindowArrangement]]
];
Menu.createMenu('share_menu',
event.pageX,
event.pageY,
options);
Window.stopPropagation(event);
}
// Private, not meant to be called by any external code.
function onShowHeaderClick(event) {
$('#header_container').removeClass('header_hidden');
$('#header_container').addClass('header_shown');
if ($('#graph').is(':visible')) {
Window.system_graph_view.refresh();
}
}
// Private, not meant to be called by any external code.
function onHideHeaderClick(event) {
$('#header_container').removeClass('header_shown');
$('#header_container').addClass('header_hidden');
if ($('#graph').is(':visible')) {
Window.system_graph_view.refresh();
}
}
// Private, not meant to be called by any external code.
function onShowFooterClick(event) {
$('#footer').removeClass('footer_hidden');
$('#footer').addClass('footer_shown');
if ($('#graph').is(':visible')) {
Window.system_graph_view.refresh();
}
}
// Private, not meant to be called by any external code.
function onHideFooterClick(event) {
$('#footer').removeClass('footer_shown');
$('#footer').addClass('footer_hidden');
if ($('#graph').is(':visible')) {
Window.system_graph_view.refresh();
}
}
$(document).ready(() => {
if ($('body').attr('data-arrgmt') != undefined) {
let id = $('body').attr('data-arrgmt');
Window.sendArrgmtRequest({
'type': 'get',
'id': id
}, (data, status) => {
loadArrangement(data);
}, (xhr, txt, error) => {
if (xhr.status === 404) {
window.alert('The arrangement with ID ' + id +
"can't be found!");
} else if (xhr.status === 422) {
window.alert('The arrangement with ID ' + id +
' refers to at least one performance ' +
'analysis session for which the fingerprint ' +
'verification has failed! This means that the ' +
'session is not present or has been tampered with.\n\n' +
'The first arrangement session encountering ' +
'this problem: ' +
JSON.parse(xhr.responseText).session_invalid);
} else {
window.alert('Could not load the arrangement with ID ' +
id + '!\n\n' +
'Error type: ' + error + '/"' +
txt + '"/' + xhr.status);
}
});
} else if ($('body').attr('data-session') != undefined) {
$('#results_combobox').val($('body').attr('data-session'));
loadCurrentSession();
}
if (Window.isInCompactMode()) {
$('#tabs')[0].addEventListener('wheel', event => {
let coefficient = undefined;
if (event.deltaMode === WheelEvent.DOM_DELTA_PIXEL) {
coefficient = 1;
} else if (event.deltaMode === WheelEvent.DOM_DELTA_LINE) {
coefficient = 10;
} else if (event.deltaMode === WheelEvent.DOM_DELTA_PAGE) {
coefficient = 20;
} else {
coefficient = 0;
}
$('#tabs').prop('scrollLeft',
Math.max(0, $('#tabs').prop('scrollLeft') + coefficient * event.deltaY));
});
} else {
$(document).on('change', '#results_combobox', () => {
loadCurrentSession();
});
}
});