You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

833 lines
24 KiB

/*
** Zabbix
** Copyright (C) 2001-2023 Zabbix SIA
**
** This program is free software; you can redistribute it and/or modify
** it under the terms of the GNU General Public License as published by
** the Free Software Foundation; either version 2 of the License, or
** (at your option) any later version.
**
** This program is distributed in the hope that it will be useful,
** but WITHOUT ANY WARRANTY; without even the implied warranty of
** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
** GNU General Public License for more details.
**
** You should have received a copy of the GNU General Public License
** along with this program; if not, write to the Free Software
** Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
**/
/**
* JQuery class that initializes interactivity for SVG graph.
*
* Supported options:
* - SBox - time range selector;
* - show_problems - show problems in hintbox when mouse is moved over the problem zone;
* - min_period - min period in seconds that must be s-boxed to change the data in dashboard timeselector.
*/
(function ($) {
"use strict";
// Makes SBox selection cancelable pressing Esc.
function sBoxKeyboardInteraction(e) {
if (e.keyCode == 27) {
destroySBox(e, e.data.graph);
}
}
// Disable text selection in document when move mouse pressed cursor.
function disableSelect(e) {
e.preventDefault();
}
// Cancel SBox and unset its variables.
function destroySBox(e, graph) {
var graph = graph || e.data.graph,
data = graph.data('options');
if (data) {
if (!data.isHintBoxFrozen && !data.isTriggerHintBoxFrozen) {
graph.data('widget')._resumeUpdating();
}
jQuery('.svg-graph-selection', graph).attr({'width': 0, 'height': 0});
jQuery('.svg-graph-selection-text', graph).text('');
graph.data('options').boxing = false;
}
dropDocumentListeners(e, graph);
}
/**
* Function removes SBox related $(document) event listeners:
* - if no other widget have active SBox;
* - to avoid another call of destroySBox on 'mouseup' (in case if user has pressed ESC).
*/
function dropDocumentListeners(e, graph) {
let widgets_boxing = 0; // Number of widgets with active SBox.
ZABBIX.Dashboard.getSelectedDashboardPage().getWidgets().forEach((widget) => {
if (widget.getType() === 'svggraph' && widget._svg !== null) {
const options = jQuery(widget._svg).data('options');
if (options !== undefined && options.boxing) {
widgets_boxing++;
}
}
});
if (widgets_boxing == 0 || (e && 'keyCode' in e && e.keyCode == 27)) {
jQuery(document)
.off('selectstart', disableSelect)
.off('keydown', sBoxKeyboardInteraction)
.off('mousemove', moveSBoxMouse)
.off('mouseup', destroySBox)
.off('mouseup', endSBoxDrag);
}
}
// Destroy hintbox, unset its variables and event listeners.
function destroyHintbox(graph) {
var data = graph.data('options'),
hbox = graph.data('hintbox') || null;
if (hbox !== null && (data.isHintBoxFrozen === false && data.isTriggerHintBoxFrozen === false)) {
graph.removeAttr('data-expanded');
removeFromOverlaysStack(graph.hintboxid);
graph.off('mouseup', makeHintboxStatic);
graph.removeData('hintbox');
hbox.remove();
}
}
// Hide vertical helper line and highlighted data points.
function hideHelper(graph) {
graph.find('.svg-helper').attr({'x1': -10, 'x2': -10});
graph.find('.svg-point-highlight').attr({'cx': -10, 'cy': -10});
}
// Create a new hintbox and stick it to certain position where user has clicked.
function makeHintboxStatic(e, graph) {
var graph = graph || e.data.graph,
data = graph.data('options'),
hbox = graph.data('hintbox'),
content = hbox ? hbox.find('> div') : null;
// Destroy old hintbox to make new one with close button.
destroyHintbox(graph);
if (content) {
// Should be put inside hintBoxItem to use functionality of hintBox.
graph.hintBoxItem = hintBox.createBox(e, graph, content, '', true, 'top: 0; left: 0',
graph.closest('.dashboard-grid-widget-container')
);
if (graph.data('simpleTriggersHintbox')) {
data.isTriggerHintBoxFrozen = true;
}
else {
data.isHintBoxFrozen = true;
}
graph.data('widget')._pauseUpdating();
graph.hintBoxItem.on('onDeleteHint.hintBox', function(e) {
graph.data('widget')._resumeUpdating();
data.isTriggerHintBoxFrozen = false;
data.isHintBoxFrozen = false; // Unfreeze because only onfrozen hintboxes can be removed.
graph.off('mouseup', hintboxSilentMode);
destroyHintbox(graph);
});
repositionHintBox(e, graph);
Overlay.prototype.recoverFocus.call({'$dialogue': graph.hintBoxItem});
Overlay.prototype.containFocus.call({'$dialogue': graph.hintBoxItem});
graph
.off('mouseup', hintboxSilentMode)
.on('mouseup', {graph: graph}, hintboxSilentMode);
graph.data('hintbox', graph.hintBoxItem);
}
}
/**
* Silent mode means that hintbox is waiting for click to be repositionated. Once user clicks on graph, existing
* hintbox will be repositionated with a new values in the place where user clicked on.
*/
function hintboxSilentMode(e) {
var graph = e.data.graph,
data = graph.data('options');
if (data.isHintBoxFrozen) {
data.isHintBoxFrozen = false;
showHintbox(e, graph);
makeHintboxStatic(e, graph);
}
if (data.isTriggerHintBoxFrozen) {
data.isTriggerHintBoxFrozen = false;
showSimpleTriggerHintbox(e, graph);
makeHintboxStatic(e, graph);
}
}
// Method to start selection of some horizontal area in graph.
function startSBoxDrag(e) {
e.stopPropagation();
var graph = e.data.graph,
offsetX = e.clientX - graph.offset().left,
data = graph.data('options');
if (data.dimX <= offsetX && offsetX <= data.dimX + data.dimW && data.dimY <= e.offsetY
&& e.offsetY <= data.dimY + data.dimH) {
jQuery(document)
.on('selectstart', disableSelect)
.on('keydown', {graph: graph}, sBoxKeyboardInteraction)
.on('mousemove', {graph: graph}, moveSBoxMouse)
.on('mouseup', {graph: graph}, endSBoxDrag);
data.start = offsetX - data.dimX;
}
}
// Method to recalculate selected area during mouse move.
function moveSBoxMouse(e) {
e.stopPropagation();
var graph = e.data.graph,
data = graph.data('options'),
$sbox = jQuery('.svg-graph-selection', graph),
$stxt = jQuery('.svg-graph-selection-text', graph),
offsetX = e.clientX - graph.offset().left;
data.end = offsetX - data.dimX;
// If mouse movement detected (SBox has dragged), destroy opened hintbox and pause widget refresh.
if (data.start != data.end && !data.boxing) {
graph.data('widget')._pauseUpdating();
data.isHintBoxFrozen = false;
data.isTriggerHintBoxFrozen = false;
data.boxing = true;
destroyHintbox(graph);
hideHelper(graph);
}
if (data.boxing) {
data.end = Math.min(offsetX - data.dimX, data.dimW);
data.end = (data.end > 0) ? data.end : 0;
$sbox.attr({
'x': (Math.min(data.start, data.end) + data.dimX) + 'px',
'y': data.dimY + 'px',
'width': Math.abs(data.end - data.start) + 'px',
'height': data.dimH
});
var seconds = Math.round(Math.abs(data.end - data.start) * data.spp),
label = formatTimestamp(seconds, false, true)
+ (seconds < data.minPeriod ? ' [min 1' + t('S_MINUTE_SHORT') + ']' : '');
$stxt
.text(label)
.attr({
'x': (Math.min(data.start, data.end) + data.dimX + 5) + 'px',
'y': (data.dimY + 15) + 'px'
});
}
}
// Method to end selection of horizontal area in graph.
function endSBoxDrag(e) {
e.stopPropagation();
var graph = e.data.graph,
data = graph.data('options'),
offsetX = e.clientX - graph.offset().left,
set_date = data && data.boxing;
destroySBox(e, graph);
if (set_date) {
data.end = Math.min(offsetX - data.dimX, data.dimW);
var seconds = Math.round(Math.abs(data.end - data.start) * data.spp),
from_offset = Math.floor(Math.min(data.start, data.end)) * data.spp,
to_offset = Math.floor(data.dimW - Math.max(data.start, data.end)) * data.spp;
if (seconds > data.minPeriod && (from_offset > 0 || to_offset > 0)) {
jQuery.publish('timeselector.rangeoffset', {
from_offset: Math.max(0, Math.ceil(from_offset)),
to_offset: Math.ceil(to_offset)
});
}
}
}
// Read SVG nodes and find closest past value to the given x in each data set.
function findValues(graph, x) {
var data_sets = [],
nodes = graph.querySelectorAll('[data-set]');
for (var i = 0, l = nodes.length; l > i; i++) {
var px = -10,
py = -10,
pv = null,
pp = 0,
ps = 0;
// Find matching X points.
switch (nodes[i].getAttribute('data-set')) {
case 'points':
var test_x = Math.min(x, +nodes[i].lastChild.getAttribute('cx')),
circle_nodes = nodes[i].querySelectorAll('circle'),
points = [];
for (var c = 0, cl = circle_nodes.length; cl > c; c++) {
if (test_x >= parseInt(circle_nodes[c].getAttribute('cx'))) {
points.push(circle_nodes[c]);
}
}
var point = points.slice(-1)[0];
if (typeof point !== 'undefined') {
px = point.getAttribute('cx');
py = point.getAttribute('cy');
pv = point.getAttribute('label');
}
break;
case 'bar':
var polygons_nodes = nodes[i].querySelectorAll('polygon');
var points = [];
var pp = 0;
for (var c = 0, cl = polygons_nodes.length; cl > c; c++) {
var coord = polygons_nodes[c].getAttribute('points').split(' ').map(function (val) {
return val.split(',');
});
if (polygons_nodes[c].getAttribute('data-px') == coord[0][0]) {
if (x >= parseInt(coord[0][0])) {
points.push(polygons_nodes[c]);
}
}
else {
if (x >= parseInt(polygons_nodes[c].getAttribute('data-px'))) {
points.push(polygons_nodes[c]);
}
}
}
px = 0;
py = 0;
var point = points.slice(-1)[0];
if (typeof point !== 'undefined') {
var coord = point.getAttribute('points').split(' ').map(function (val) {
return val.split(',');
});
px = coord[0][0];
py = coord[1][1];
pv = point.getAttribute('label');
pp = (coord[2][0] - coord[0][0]) / 2;
ps = point.getAttribute('data-px');
}
break;
case 'staircase':
case 'line':
var direction_string = '',
label = [],
data_set = nodes[i].getAttribute('data-set'),
data_nodes = nodes[i].childNodes,
elmnt_label,
cx,
cy;
for (var index = 0, len = data_nodes.length; index < len; index++) {
elmnt_label = data_nodes[index].getAttribute('label');
if (elmnt_label) {
label.push(elmnt_label);
if (data_nodes[index].tagName.toLowerCase() === 'circle') {
cx = data_nodes[index].getAttribute('cx');
cy = data_nodes[index].getAttribute('cy');
direction_string += ' _' + cx + ',' + cy;
}
else {
direction_string += ' ' + data_nodes[index].getAttribute('d');
}
}
}
label = label.join(',').split(',');
var direction = ED // Edge transforms 'd' attribute.
? direction_string.substr(1).replace(/([ML])\s(\d+)\s(\d+)/g, '$1$2\,$3').split(' ')
: direction_string.substr(1).split(' '),
index = direction.length,
point,
point_label;
while (index) {
index--;
point = direction[index].substr(1).split(',');
point_label = label[data_set === 'line' ? index : Math.ceil(index / 2)];
if (x >= parseInt(point[0]) && point_label !== '') {
px = point[0];
py = point[1];
pv = point_label;
break;
}
}
break;
}
data_sets.push({g: nodes[i], x: px, y: py, v: pv, p: pp, s: ps});
}
return data_sets;
}
// Find what problems matches in time to the given x.
function findProblems(graph, x) {
var problems = [],
problem_start,
problem_width,
nodes = graph.querySelectorAll('[data-info]');
for (var i = 0, l = nodes.length; l > i; i++) {
problem_start = +nodes[i].getAttribute('x');
problem_width = +nodes[i].getAttribute('width');
if (x > problem_start && problem_start + problem_width > x) {
problems.push(JSON.parse(nodes[i].getAttribute('data-info')));
}
}
return problems;
}
// Set position of vertical helper line.
function setHelperPosition(e, graph) {
var data = graph.data('options');
graph.find('.svg-helper').attr({
'x1': e.clientX - graph.offset().left,
'y1': data.dimY,
'x2': e.clientX - graph.offset().left,
'y2': data.dimY + data.dimH
});
}
/**
* Get tolerance for given data set. Tolerance is used to find which elements are hovered by mouse. Script takes
* actual data point and adds N pixels to all sides. Then looks if mouse is in calculated area. N is calculated by
* this function. Tolerance is used to find exactly matched point only.
*/
function getDataPointTolerance(ds) {
var data_tag = ds.querySelector(':not(.svg-point-highlight)');
if (data_tag.tagName.toLowerCase() === 'circle') {
return +ds.childNodes[1].getAttribute('r');
}
else {
return +window.getComputedStyle(data_tag)['strokeWidth'];
}
}
// Position hintbox near current mouse position.
function repositionHintBox(e, graph) {
// Use closest positioned ancestor for offset calculation.
var offset = graph.closest('.dashboard-grid-widget-container').offsetParent().offset(),
hbox = jQuery(graph.hintBoxItem),
page_bottom = jQuery(window.top).scrollTop() + jQuery(window.top).height(),
mouse_distance = 15,
l = (document.body.clientWidth >= e.clientX + hbox.outerWidth() + mouse_distance)
? e.clientX + mouse_distance - offset.left
: e.clientX - mouse_distance - hbox.outerWidth() - offset.left,
t = e.pageY - offset.top,
t = page_bottom >= t + offset.top + hbox.outerHeight() + mouse_distance
? t + mouse_distance
: t - mouse_distance - hbox.outerHeight(),
t = (t + offset.top < 0) ? -offset.top : t;
hbox.css({'left': l, 'top': t});
}
// Show problem or value hintbox.
function showHintbox(e, graph) {
var graph = graph || e.data.graph,
data = graph.data('options'),
hbox = graph.data('hintbox') || null,
offsetX = e.clientX - graph.offset().left,
html = null,
in_x = false,
in_values_area = false,
in_problem_area = false;
if (graph.data('simpleTriggersHintbox') || data.isTriggerHintBoxFrozen === true) {
return;
}
if (data.boxing === true) {
hideHelper(graph);
return;
}
// Check if mouse in the horizontal area in which hintbox must be shown.
in_x = (data.dimX <= offsetX && offsetX <= data.dimX + data.dimW);
in_problem_area = in_x && (data.dimY + data.dimH <= e.offsetY && e.offsetY <= data.dimY + data.dimH + 15);
in_values_area = in_x && (data.dimY <= e.offsetY && e.offsetY <= data.dimY + data.dimH);
// Show problems when mouse is in the 15px high area under the graph canvas.
if (data.showProblems && data.isHintBoxFrozen === false && in_problem_area) {
hideHelper(graph);
var problems = findProblems(graph[0], e.offsetX),
problems_total = problems.length;
if (problems_total > 0) {
var tbody = jQuery('<tbody>');
problems.slice(0, data.hintMaxRows).forEach(function(val, i) {
tbody.append(
jQuery('<tr>')
.append(jQuery('<td>').append(jQuery('<a>', {'href': val.url}).text(val.clock)))
.append(jQuery('<td>').append(val.r_eventid
? jQuery('<a>', {'href': val.url}).text(val.r_clock)
: val.r_clock)
)
.append(jQuery('<td>').append(
jQuery('<span>', { 'class': val.status_color }).text(val.status))
)
.append(jQuery('<td>', {'class': val.severity}).text(val.name))
);
});
html = jQuery('<div>')
.addClass('svg-graph-hintbox')
.append(
jQuery('<table>')
.addClass('list-table compact-view')
.append(tbody)
)
.append(problems_total > data.hintMaxRows
? makeHintBoxFooter(data.hintMaxRows, problems_total)
: null
);
}
}
// Show graph values if mouse is over the graph canvas.
else if (in_values_area) {
// Set position of mouse following helper line.
setHelperPosition(e, graph);
// Find values.
var points = findValues(graph[0], offsetX),
points_total = points.length,
show_hint = false,
xy_point = false,
tolerance;
/**
* Decide if one specific value or list of all matching Xs should be highlighted and either to show or
* hide hintbox.
*/
if (data.isHintBoxFrozen === false) {
points.forEach(function(point) {
if (!show_hint && point.v !== null) {
show_hint = true;
}
tolerance = getDataPointTolerance(point.g);
if (!xy_point && point.v !== null
&& (+point.x + tolerance) > e.offsetX && e.offsetX > (+point.x - tolerance)
&& (+point.y + tolerance) > e.offsetY && e.offsetY > (+point.y - tolerance)) {
xy_point = point;
points_total = 1;
}
});
}
// Make html for hintbox.
if (show_hint) {
html = jQuery('<ul>');
}
var rows_added = 0;
points.forEach(function(point) {
var point_highlight = point.g.querySelectorAll('.svg-point-highlight')[0];
if (point.v !== null && (xy_point === false || xy_point === point)) {
point_highlight.setAttribute('cx', point.x);
point_highlight.setAttribute('cy', point.y);
if (point.p > 0) {
point_highlight.setAttribute('cx', parseInt(point.x) + parseInt(point.p));
}
if (show_hint && data.hintMaxRows > rows_added) {
jQuery('<li>')
.text(point.g.getAttribute('data-metric') + ': ' + point.v)
.append(
jQuery('<span>')
.css('background-color', point.g.getAttribute('data-color'))
.addClass('svg-graph-hintbox-item-color')
)
.appendTo(html);
rows_added++;
}
}
else {
point_highlight.setAttribute('cx', -10);
point_highlight.setAttribute('cy', -10);
}
});
if (show_hint) {
// Calculate time at mouse position.
const time = new CDate((data.timeFrom + (offsetX - data.dimX) * data.spp) * 1000);
html = jQuery('<div>')
.addClass('svg-graph-hintbox')
.append(
jQuery('<div>')
.addClass('header')
.html(time.format(PHP_ZBX_FULL_DATE_TIME))
)
.append(html)
.append(points_total > data.hintMaxRows
? makeHintBoxFooter(data.hintMaxRows, points_total)
: null
);
}
}
else {
hideHelper(graph);
}
if (html !== null) {
if (hbox === null) {
hbox = hintBox.createBox(e, graph, html, '', false, false,
graph.closest('.dashboard-grid-widget-container')
);
graph
.off('mouseup', makeHintboxStatic)
.on('mouseup', {graph: graph}, makeHintboxStatic);
graph.data('hintbox', hbox);
}
else {
hbox.find('> div').replaceWith(html);
}
graph.hintBoxItem = hbox;
repositionHintBox(e, graph);
}
if (html === null && (in_values_area || in_problem_area)) {
destroyHintbox(graph);
}
}
// Function creates hintbox footer.
function makeHintBoxFooter(num_displayed, num_total) {
return jQuery('<div>')
.addClass('table-paging')
.append(
jQuery('<div>')
.addClass('paging-btn-container')
.append(
jQuery('<div>')
.text(sprintf(t('S_DISPLAYING_FOUND'), num_displayed, num_total))
.addClass('table-stats')
)
);
}
var methods = {
init: function(widget) {
this.each(function() {
jQuery(widget._svg)
.data('options', {
dimX: widget._svg_options.dims.x,
dimY: widget._svg_options.dims.y,
dimW: widget._svg_options.dims.w,
dimH: widget._svg_options.dims.h,
showProblems: widget._svg_options.show_problems,
showSimpleTriggers: widget._svg_options.show_simple_triggers,
hintMaxRows: widget._svg_options.hint_max_rows,
isHintBoxFrozen: false,
isTriggerHintBoxFrozen: false,
spp: widget._svg_options.spp || null,
timeFrom: widget._svg_options.time_from,
minPeriod: widget._svg_options.min_period,
boxing: false
})
.data('widget', widget)
.attr('unselectable', 'on')
.css('user-select', 'none');
if (widget._svg_options.sbox) {
dropDocumentListeners(null, jQuery(widget._svg));
}
});
},
activate: function () {
const widget = jQuery(this).data('widget');
const graph = jQuery(widget._svg);
graph
.on('mousemove', (e) => {
showSimpleTriggerHintbox(e, graph);
showHintbox(e, graph);
})
.on('mouseleave', function() {
destroyHintbox(graph);
hideHelper(graph);
})
.on('selectstart', false);
if (widget._svg_options.sbox) {
graph
.on('dblclick', function() {
hintBox.hideHint(graph, true);
jQuery.publish('timeselector.zoomout');
return false;
})
.on('mousedown', {graph}, startSBoxDrag);
}
},
deactivate: function (e) {
const widget = jQuery(this).data('widget');
const graph = jQuery(widget._svg);
destroySBox(e, graph);
graph.off('mousemove mouseleave dblclick mousedown selectstart');
}
};
function showSimpleTriggerHintbox(e, graph) {
graph = graph || e.data.graph;
const data = graph.data('options');
let hbox = graph.data('hintbox') || null;
graph.data('simpleTriggersHintbox', false);
let html = null;
if (!data.showSimpleTriggers || data.isHintBoxFrozen === true) {
return;
}
if (data.boxing === true) {
hideHelper(graph);
return;
}
// Check if mouse in the horizontal area in which hintbox must be shown.
const triggers = findTriggers(graph[0], e.offsetY);
if (data.isTriggerHintBoxFrozen === false && triggers) {
hideHelper(graph);
const triggers_length = triggers.length;
if (triggers_length > 0) {
const hint_body = jQuery('<ul></ul>');
const trigger_areas = triggers.filter((val) => {
if (val.begin_position > e.offsetX) {
return false;
}
if (e.offsetX > val.end_position) {
return false;
}
return true;
});
if (!trigger_areas.length) {
return;
}
triggers.slice(0, data.hintMaxRows).forEach((val, i) => {
hint_body.append(
jQuery('<li>')
.text(val.trigger + ' [' + val.constant + ']')
.append(
jQuery('<span>')
.css('background-color', val.color)
.addClass('svg-graph-hintbox-trigger-color')
)
)
val.elem.classList.toggle('svg-graph-simple-trigger-hover', true)
});
html = jQuery('<div>')
.addClass('svg-graph-hintbox')
.append(hint_body)
.append(triggers_length > data.hintMaxRows
? makeHintBoxFooter(data.hintMaxRows, triggers_length)
: null
);
graph.data('simpleTriggersHintbox', true);
}
}
if (html !== null) {
if (hbox === null) {
hbox = hintBox.createBox(e, graph, html, '', false, false,
graph.closest('.dashboard-grid-widget-container')
);
graph
.off('mouseup', makeHintboxStatic)
.on('mouseup', {graph: graph}, makeHintboxStatic);
graph.data('hintbox', hbox);
}
else {
hbox.find('> div').replaceWith(html);
}
graph.hintBoxItem = hbox;
repositionHintBox(e, graph);
}
else {
destroyHintbox(graph);
}
}
function findTriggers(graph, y) {
const triggers = [];
const nodes = graph.querySelectorAll('.svg-graph-simple-trigger line');
[...graph.querySelectorAll('.svg-graph-simple-trigger.svg-graph-simple-trigger-hover')].map(
(elem) => elem.classList.toggle('svg-graph-simple-trigger-hover', false)
);
for (var i = 0, l = nodes.length; l > i; i++) {
const trigger_y = parseInt(nodes[i].getAttribute('y1'));
if (y < trigger_y + 10 && y > trigger_y - 10) {
triggers.push({
begin_position: parseInt(nodes[i].getAttribute('x1')),
end_position: parseInt(nodes[i].getAttribute('x2')),
color: nodes[i].parentElement.getAttribute('severity-color'),
constant: nodes[i].parentElement.getAttribute('constant'),
trigger: nodes[i].parentElement.getAttribute('description'),
elem: nodes[i].parentElement
});
}
}
return triggers;
}
jQuery.fn.svggraph = function(method) {
if (methods[method]) {
return methods[method].apply(this, Array.prototype.slice.call(arguments, 1));
}
return methods.init.apply(this, arguments);
};
})(jQuery);