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.
434 lines
11 KiB
434 lines
11 KiB
1 year ago
|
<?php
|
||
|
/*
|
||
|
** 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.
|
||
|
**/
|
||
|
?>
|
||
|
|
||
|
|
||
|
<script>
|
||
|
const view = {
|
||
|
_app: null,
|
||
|
_filter_form: null,
|
||
|
_data: null,
|
||
|
_resize_observer: null,
|
||
|
_container: null,
|
||
|
|
||
|
init({filter_form_name, data, timeline}) {
|
||
|
this._filter_form = document.querySelector(`[name="${filter_form_name}"]`);
|
||
|
this._container = document.querySelector('main');
|
||
|
this._data = data;
|
||
|
|
||
|
this.initSubfilter();
|
||
|
this.initCharts();
|
||
|
|
||
|
timeControl.addObject('charts_view', timeline, {
|
||
|
id: 'timeline_1',
|
||
|
domid: 'charts_view',
|
||
|
loadSBox: 0,
|
||
|
loadImage: 0,
|
||
|
dynamic: 0
|
||
|
});
|
||
|
|
||
|
timeControl.processObjects();
|
||
|
},
|
||
|
|
||
|
initSubfilter() {
|
||
|
this._filter_form.addEventListener('click', (e) => {
|
||
|
const link = e.target;
|
||
|
|
||
|
if (link.classList.contains('js-subfilter-set')) {
|
||
|
this.setSubfilter(link.getAttribute('data-tag'), link.getAttribute('data-value'));
|
||
|
}
|
||
|
else if (link.classList.contains('js-subfilter-unset')) {
|
||
|
this.unsetSubfilter(link.getAttribute('data-tag'), link.getAttribute('data-value'));
|
||
|
}
|
||
|
});
|
||
|
},
|
||
|
|
||
|
initCharts() {
|
||
|
this._$tmpl_row = $('<tr>').append(
|
||
|
$('<div>', {class: 'flickerfreescreen'}).append(
|
||
|
$('<div>', {class: '<?= ZBX_STYLE_CENTER ?>', style: 'min-height: 300px;'}).append(
|
||
|
$('<img>')
|
||
|
)
|
||
|
)
|
||
|
);
|
||
|
|
||
|
this._app = new ChartList( $('#charts'), this._data.timeline, this._data.config, this._container);
|
||
|
this._app.setCharts(this._data.charts);
|
||
|
this._app.refresh();
|
||
|
|
||
|
this._resize_observer = new ResizeObserver(this._app.onResize.bind(this._app));
|
||
|
this._resize_observer.observe(this._container);
|
||
|
|
||
|
$.subscribe('timeselector.rangeupdate', (e, data) => {
|
||
|
this._app.timeline = data;
|
||
|
this._app.updateCharts();
|
||
|
});
|
||
|
},
|
||
|
|
||
|
replacePaging(paging) {
|
||
|
document.querySelector('.<?= ZBX_STYLE_TABLE_PAGING ?>').outerHTML = paging;
|
||
|
},
|
||
|
|
||
|
replaceSubfilter(subfilter) {
|
||
|
if (document.getElementById('subfilter') !== null) {
|
||
|
document.getElementById('subfilter').outerHTML = subfilter;
|
||
|
}
|
||
|
},
|
||
|
|
||
|
setSubfilter(tag, value) {
|
||
|
if (value !== null) {
|
||
|
this.filterAddVar(`subfilter_tags[${tag}][]`, value);
|
||
|
}
|
||
|
else {
|
||
|
this.filterAddVar(`subfilter_tagnames[]`, tag);
|
||
|
}
|
||
|
|
||
|
this.submitSubfilter();
|
||
|
},
|
||
|
|
||
|
unsetSubfilter(tag, value) {
|
||
|
if (value !== null) {
|
||
|
document.querySelector(`[name^="subfilter_tags[${tag}]["][value="${value}"]`).remove();
|
||
|
}
|
||
|
else {
|
||
|
document.querySelector(`[name^="subfilter_tagnames["][value="${tag}"]`).remove();
|
||
|
}
|
||
|
|
||
|
this.submitSubfilter();
|
||
|
},
|
||
|
|
||
|
filterAddVar(name, value) {
|
||
|
const input = document.createElement('input');
|
||
|
|
||
|
input.type = 'hidden';
|
||
|
input.name = name;
|
||
|
input.value = value;
|
||
|
|
||
|
this._filter_form.appendChild(input);
|
||
|
},
|
||
|
|
||
|
submitSubfilter() {
|
||
|
this.filterAddVar('subfilter_set', '1');
|
||
|
this._filter_form.submit();
|
||
|
}
|
||
|
};
|
||
|
</script>
|
||
|
|
||
|
<script type="text/javascript">
|
||
|
|
||
|
/**
|
||
|
* @var {number} App will only show loading indicator, if loading takes more than DELAY_LOADING seconds.
|
||
|
*/
|
||
|
Chart.DELAY_LOADING = 3;
|
||
|
|
||
|
/**
|
||
|
* Represents chart, it can be refreshed.
|
||
|
*
|
||
|
* @param {object} chart Chart object prepared in server.
|
||
|
* @param {object} timeline Timeselector data.
|
||
|
* @param {jQuery} $tmpl Template object to be used for new chart $el.
|
||
|
* @param {Node} wrapper Dom node in respect to which resize must be done.
|
||
|
*/
|
||
|
function Chart(chart, timeline, $tmpl, wrapper) {
|
||
|
this.$el = $tmpl.clone();
|
||
|
this.$img = this.$el.find('img');
|
||
|
|
||
|
this.chartid = chart.chartid;
|
||
|
|
||
|
this.timeline = timeline;
|
||
|
this.dimensions = chart.dimensions;
|
||
|
|
||
|
this.curl = new Curl(chart.src);
|
||
|
|
||
|
if ('graphid' in chart) {
|
||
|
this.curl.setArgument('graphid', chart.graphid);
|
||
|
}
|
||
|
else {
|
||
|
this.curl.setArgument('itemids', [chart.itemid]);
|
||
|
}
|
||
|
|
||
|
this.use_sbox = !!chart.sbox;
|
||
|
this.wrapper = wrapper;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Set visual indicator of "loading state".
|
||
|
*
|
||
|
* @param {number} delay_loading (optional) Add loader only when request exceeds this many seconds.
|
||
|
*
|
||
|
* @return {function} Function that would cancel scheduled indicator or remove existing.
|
||
|
*/
|
||
|
Chart.prototype.setLoading = function(delay_loading) {
|
||
|
const timeoutid = setTimeout(function(){
|
||
|
this.$img.parent().addClass('is-loading')
|
||
|
}.bind(this), delay_loading * 1000);
|
||
|
|
||
|
return function() {
|
||
|
clearTimeout(timeoutid);
|
||
|
this.unsetLoading();
|
||
|
}.bind(this);
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Remove visual indicator of "loading state".
|
||
|
*/
|
||
|
Chart.prototype.unsetLoading = function() {
|
||
|
this.$img.parent().removeClass('is-loading');
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Remove chart.
|
||
|
*/
|
||
|
Chart.prototype.destruct = function() {
|
||
|
this.$img.off();
|
||
|
this.$el.off();
|
||
|
this.$el.remove();
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Updates image $.data for gtlc.js to handle selection box.
|
||
|
*/
|
||
|
Chart.prototype.refreshSbox = function() {
|
||
|
if (this.use_sbox) {
|
||
|
this.$img.data('zbx_sbox', {
|
||
|
left: this.dimensions.shiftXleft,
|
||
|
right: this.dimensions.shiftXright,
|
||
|
top: this.dimensions.shiftYtop,
|
||
|
height: this.dimensions.graphHeight,
|
||
|
|
||
|
from_ts: this.timeline.from_ts,
|
||
|
to_ts: this.timeline.to_ts,
|
||
|
from: this.timeline.from,
|
||
|
to: this.timeline.to
|
||
|
});
|
||
|
}
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Update chart.
|
||
|
*
|
||
|
* @param {number} delay_loading (optional) Add "loading indicator" only when request exceeds delay.
|
||
|
*
|
||
|
* @return {Promise}
|
||
|
*/
|
||
|
Chart.prototype.refresh = function(delay_loading) {
|
||
|
let width = this.wrapper.clientWidth - 20;
|
||
|
|
||
|
if (this.use_sbox) {
|
||
|
width -= this.dimensions.shiftXright + this.dimensions.shiftXleft + 1;
|
||
|
}
|
||
|
|
||
|
this.curl.setArgument('from', this.timeline.from);
|
||
|
this.curl.setArgument('to', this.timeline.to);
|
||
|
this.curl.setArgument('height', this.dimensions.graphHeight);
|
||
|
this.curl.setArgument('width', Math.max(1000, width));
|
||
|
this.curl.setArgument('profileIdx', 'web.charts.filter');
|
||
|
this.curl.setArgument('_', (+new Date).toString(34));
|
||
|
|
||
|
const unsetLoading = this.setLoading(delay_loading);
|
||
|
|
||
|
const promise = new Promise((resolve, reject) => {
|
||
|
this.$img.one('error', () => reject());
|
||
|
this.$img.one('load', () => resolve());
|
||
|
})
|
||
|
.catch(() => this.setLoading(0))
|
||
|
.finally(unsetLoading)
|
||
|
.then(() => this.refreshSbox());
|
||
|
|
||
|
this.$img.attr('src', this.curl.getUrl());
|
||
|
|
||
|
return promise;
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* @param {jQuery} $el A container where charts are maintained.
|
||
|
* @param {object} timeline Time control object.
|
||
|
* @param {object} config
|
||
|
* @param {Node} wrapper Dom node in respect to which resize must be done.
|
||
|
*/
|
||
|
function ChartList($el, timeline, config, wrapper) {
|
||
|
this.curl = new Curl('zabbix.php');
|
||
|
this.curl.setArgument('action', 'charts.view.json');
|
||
|
|
||
|
this.$el = $el;
|
||
|
this.timeline = timeline;
|
||
|
|
||
|
this.charts = [];
|
||
|
this.charts_map = {};
|
||
|
this.config = config;
|
||
|
this.wrapper = wrapper;
|
||
|
}
|
||
|
|
||
|
ChartList.prototype.updateSubfilters = function(subfilter_tagnames, subfilter_tags) {
|
||
|
this.config.subfilter_tagnames = subfilter_tagnames;
|
||
|
this.config.subfilter_tags = subfilter_tags;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Update currently listed charts.
|
||
|
*
|
||
|
* @return {Promise} Resolves once all charts are refreshed.
|
||
|
*/
|
||
|
ChartList.prototype.updateCharts = function() {
|
||
|
const updates = [];
|
||
|
|
||
|
for (const chart of this.charts) {
|
||
|
chart.timeline = this.timeline;
|
||
|
updates.push(chart.refresh());
|
||
|
}
|
||
|
|
||
|
return Promise.all(updates);
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Fetches, then sets new list, then updates each chart.
|
||
|
*
|
||
|
* @param {number} delay_loading (optional) Add "loading indicator" only when request exceeds delay.
|
||
|
*
|
||
|
* @return {Promise} Resolves once list is fetched and each of new charts is fetched.
|
||
|
*/
|
||
|
ChartList.prototype.updateListAndCharts = function(delay_loading) {
|
||
|
return this.fetchList()
|
||
|
.then(list => {
|
||
|
this.setCharts(list);
|
||
|
return this.charts;
|
||
|
})
|
||
|
.then(new_charts => {
|
||
|
const loading_charts = [];
|
||
|
for (const chart of new_charts) {
|
||
|
loading_charts.push(chart.refresh(delay_loading));
|
||
|
}
|
||
|
return Promise.all(loading_charts)
|
||
|
});
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Fetches new list of charts.
|
||
|
*
|
||
|
* @return {Promise}
|
||
|
*/
|
||
|
ChartList.prototype.fetchList = function() {
|
||
|
// Timeselector.
|
||
|
this.curl.setArgument('from', this.timeline.from);
|
||
|
this.curl.setArgument('to', this.timeline.to);
|
||
|
|
||
|
// Filter.
|
||
|
this.curl.setArgument('filter_hostids', this.config.filter_hostids);
|
||
|
this.curl.setArgument('filter_name', this.config.filter_name);
|
||
|
this.curl.setArgument('filter_show', this.config.filter_show);
|
||
|
|
||
|
this.curl.setArgument('subfilter_tagnames', this.config.subfilter_tagnames);
|
||
|
this.curl.setArgument('subfilter_tags', this.config.subfilter_tags);
|
||
|
|
||
|
this.curl.setArgument('page', this.config.page);
|
||
|
|
||
|
return fetch(this.curl.getUrl())
|
||
|
.then((response) => response.json())
|
||
|
.then((response) => {
|
||
|
this.timeline = response.timeline;
|
||
|
view.replaceSubfilter(response.subfilter);
|
||
|
view.replacePaging(response.paging);
|
||
|
|
||
|
return response.charts;
|
||
|
});
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Update app state according with configuration. Either update individual chart item schedulers or re-fetch
|
||
|
* list and update list scheduler.
|
||
|
*
|
||
|
* @param {number} delay_loading (optional) Add "loading indicator" only when request exceeds delay.
|
||
|
*/
|
||
|
ChartList.prototype.refresh = function(delay_loading) {
|
||
|
const {refresh_interval} = this.config;
|
||
|
|
||
|
if (this._timeoutid) {
|
||
|
clearTimeout(this._timeoutid);
|
||
|
}
|
||
|
|
||
|
this.updateListAndCharts(delay_loading)
|
||
|
.finally(_ => {
|
||
|
if (refresh_interval) {
|
||
|
this._timeoutid = setTimeout(_ => this.refresh(Chart.DELAY_LOADING),
|
||
|
refresh_interval * 1000
|
||
|
);
|
||
|
}
|
||
|
})
|
||
|
.catch(_ => {
|
||
|
for (const chart of this.charts) {
|
||
|
chart.setLoading();
|
||
|
}
|
||
|
});
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Constructs new charts and removes missing, reorders existing charts.
|
||
|
*
|
||
|
* @param {array} raw_charts
|
||
|
*/
|
||
|
ChartList.prototype.setCharts = function(raw_charts) {
|
||
|
const charts = [];
|
||
|
const charts_map = {};
|
||
|
|
||
|
raw_charts.forEach(function(chart) {
|
||
|
chart = this.charts_map[chart.chartid]
|
||
|
? this.charts_map[chart.chartid]
|
||
|
: new Chart(chart, this.timeline, view._$tmpl_row, view._container);
|
||
|
|
||
|
// Existing chart nodes are assured to be in correct order.
|
||
|
this.$el.append(chart.$el);
|
||
|
|
||
|
charts_map[chart.chartid] = chart;
|
||
|
charts.push(chart);
|
||
|
}.bind(this));
|
||
|
|
||
|
// Charts that was not in new list are to be deleted.
|
||
|
this.charts.forEach(function(chart) {
|
||
|
!charts_map[chart.chartid] && chart.destruct();
|
||
|
});
|
||
|
|
||
|
this.charts = charts;
|
||
|
this.charts_map = charts_map;
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* A response to horizontal window resize is to refresh charts (body min width is taken into account).
|
||
|
* Chart update is debounced for a half second.
|
||
|
*/
|
||
|
ChartList.prototype.onResize = function() {
|
||
|
const width = this.wrapper.clientWidth;
|
||
|
|
||
|
if (this._prev_width === undefined) {
|
||
|
this._prev_width = width;
|
||
|
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
clearTimeout(this._resize_timeoutid);
|
||
|
|
||
|
if (this._prev_width != width) {
|
||
|
this._resize_timeoutid = setTimeout(() => {
|
||
|
this._prev_width = width;
|
||
|
this.updateCharts();
|
||
|
}, 500);
|
||
|
}
|
||
|
};
|
||
|
</script>
|