luci/applications/luci-app-statistics/htdocs/luci-static/resources/view/statistics/graphs.js
Rani Hod 8fcd52f513 luci-app-statistics: regenerate graphs on window resize
Currently graphs are redrawn only based on the refresh interval
(never or every 5/30/60 seconds).
Since the image size is calculated based on the window size,
redraw graphs (once) also after resizing the window.
This also captures window resize due to orientation change
(e.g., for mobile).
Since multiple resize events are fired when dragging the window
border, there is a 250ms delay for debouncing.

Signed-off-by: Rani Hod <rani.hod@gmail.com>
Tested-by: Paul Donald <newtwen@gmail.com>
2023-12-04 16:35:15 +01:00

257 lines
7.9 KiB
JavaScript

'use strict';
'require view';
'require dom';
'require poll';
'require ui';
'require uci';
'require statistics.rrdtool as rrdtool';
var pollFn = null,
activePlugin = null,
activeInstance = null;
return view.extend({
load: function() {
return rrdtool.load();
},
updatePluginTab: function(host, span, time, ev) {
var container = ev.target,
width = Math.max(200, container.offsetWidth - 100),
plugin = ev.detail.tab,
render_instances = [],
plugin_instances = rrdtool.pluginInstances(host.value, plugin),
cache = {};
activePlugin = plugin;
dom.content(container, [
E('p', {}, [
E('em', { 'class': 'spinning' }, [ _('Loading data…') ])
])
]);
for (var i = 0; i < plugin_instances.length; i++) {
if (rrdtool.hasInstanceDetails(host.value, plugin, plugin_instances[i])) {
render_instances.push([
plugin_instances[i],
plugin_instances[i] ? '%s: %s'.format(rrdtool.pluginTitle(plugin), plugin_instances[i]) : rrdtool.pluginTitle(plugin)
]);
}
}
if (render_instances.length == 0 || render_instances.length > 1) {
render_instances.unshift([
'-',
'%s: %s'.format(rrdtool.pluginTitle(plugin), _('Overview'))
]);
}
Promise.all(render_instances.map(function(instance) {
if (instance[0] == '-') {
var tasks = [];
for (var i = 0; i < plugin_instances.length; i++)
tasks.push(rrdtool.render(plugin, plugin_instances[i], true, host.value, span.value, width, null, cache));
return Promise.all(tasks).then(function(blobs) {
return Array.prototype.concat.apply([], blobs);
});
}
else {
return rrdtool.render(plugin, instance[0], false, host.value, span.value, width, null, cache);
}
})).then(function(blobs) {
var multiple = blobs.length > 1;
dom.content(container, E('div', {}, blobs.map(function(blobs, i) {
var plugin_instance = i ? render_instances[i][0] : plugin_instances.join('|'),
title = render_instances[i][1];
return E('div', {
'class': 'center',
'data-tab': multiple ? i : null,
'data-tab-title': multiple ? title : null,
'data-plugin': plugin,
'data-plugin-instance': plugin_instance,
'data-is-index': i ? null : true,
'cbi-tab-active': function(ev) { activeInstance = ev.target.getAttribute('data-plugin-instance') }
}, blobs.map(function(blob) {
return E('img', {
'src': URL.createObjectURL(new Blob([blob], { type: 'image/png' }))
});
}));
})));
if (multiple)
ui.tabs.initTabGroup(container.lastElementChild.childNodes);
else
activeInstance = plugin_instances.join('|');
});
},
updateGraphs: function(host, span, time, container, ev) {
var plugin_names = rrdtool.pluginNames(host.value);
container.querySelectorAll('img').forEach(function(img) {
URL.revokeObjectURL(img.src);
});
dom.content(container, null);
if (container.hasAttribute('data-initialized')) {
container.removeAttribute('data-initialized');
container.parentNode.removeChild(container.previousElementSibling);
}
for (var i = 0; i < plugin_names.length; i++) {
if (!rrdtool.hasDefinition(plugin_names[i]))
continue;
container.appendChild(E('div', {
'data-tab': plugin_names[i],
'data-tab-title': rrdtool.pluginTitle(plugin_names[i]),
'cbi-tab-active': L.bind(this.updatePluginTab, this, host, span, time)
}, [
E('p', {}, [
E('em', { 'class': 'spinning' }, [ _('Loading data…') ])
])
]));
}
ui.tabs.initTabGroup(container.childNodes);
},
refreshGraphs: function(host, span, time, container) {
var div = document.querySelector('[data-plugin="%s"][data-plugin-instance="%s"]'.format(activePlugin, activeInstance || '')),
width = Math.max(200, container.offsetWidth - 100),
render_instances = activeInstance.split(/\|/);
return Promise.all(render_instances.map(function(render_instance) {
return rrdtool.render(activePlugin, render_instance || '', div.hasAttribute('data-is-index'), host.value, span.value, width);
})).then(function(blobs) {
return Array.prototype.concat.apply([], blobs);
}).then(function(blobs) {
return Promise.all(blobs.map(function(blob) {
return new Promise(function(resolveFn, rejectFn) {
var img = E('img', { 'src': URL.createObjectURL(new Blob([blob], { type: 'image/png' })) });
img.onload = function(ev) { resolveFn(img) };
img.onerror = function(ev) { resolveFn(img) };
});
})).then(function(imgs) {
while (div.childNodes.length > imgs.length)
div.removeChild(div.lastElementChild);
for (var i = 0; i < imgs.length; i++) {
if (i < div.childNodes.length) {
URL.revokeObjectURL(div.childNodes[i].src);
div.childNodes[i].src = imgs[i].src;
}
else {
div.appendChild(E('img', { 'src': imgs[i].src }));
}
}
});
});
},
togglePolling: function(host, span, time, container, ev) {
var btn = ev.currentTarget;
if (pollFn) {
poll.remove(pollFn);
pollFn = null;
}
if (time.value != '0') {
pollFn = L.bind(this.refreshGraphs, this, host, span, time, container);
poll.add(pollFn, +time.value);
}
},
render: function() {
var hosts = rrdtool.hostInstances();
return hosts.length ? this.renderGraphs() : this.renderNoData();
},
renderNoData: function() {
ui.showModal(_('No RRD data found'), [
E('p', {}, _('There is no RRD data available yet to render graphs.')),
E('p', {}, _('You need to configure <em>collectd</em> to gather data into <em>.rrd</em> files.')),
E('div', { 'class': 'right' }, [
E('button', {
'class': 'cbi-button',
'click': function(ev) { location.href = 'collectd' }
}, [ _('Set up collectd') ])
])
]);
},
renderGraphs: function() {
var hostSel = E('select', { 'style': 'max-width:170px', 'data-name': 'host' }, rrdtool.hostInstances().map(function(host) {
return E('option', {
'selected': (rrdtool.opts.host == host) ? 'selected' : null
}, [ host ])
}));
var spanSel = E('select', { 'style': 'max-width:170px', 'data-name': 'timespan' }, L.toArray(uci.get('luci_statistics', 'collectd_rrdtool', 'RRATimespans')).map(function(span) {
return E('option', {
'selected': (rrdtool.opts.timespan == span) ? 'selected' : null
}, [ span ])
}));
var timeSel = E('select', { 'style': 'max-width:170px', 'data-name': 'refresh' }, [
E('option', { 'value': 0 }, [ _('Do not refresh') ]),
E('option', { 'value': 5 }, [ _('Every 5 seconds') ]),
E('option', { 'value': 30 }, [ _('Every 30 seconds') ]),
E('option', { 'value': 60 }, [ _('Every minute') ])
]);
var graphDiv = E('div', { 'data-name': 'graphs' });
var view = E([], [
E('h2', {}, [ _('Statistics') ]),
E('div', {}, [
E('p', { 'class': 'controls' }, [
E('span', { 'class': 'nowrap' }, [
hostSel,
E('button', {
'class': 'cbi-button cbi-button-apply',
'click': ui.createHandlerFn(this, 'updateGraphs', hostSel, spanSel, timeSel, graphDiv, )
}, [ _('Display Host »') ]),
]),
' ',
E('span', { 'class': 'nowrap' }, [
spanSel,
E('button', {
'class': 'cbi-button cbi-button-apply',
'click': ui.createHandlerFn(this, 'updateGraphs', hostSel, spanSel, timeSel, graphDiv)
}, [ _('Display timespan »') ]),
]),
' ',
E('span', { 'class': 'nowrap' }, [
timeSel,
E('button', {
'class': 'cbi-button cbi-button-apply',
'click': ui.createHandlerFn(this, 'togglePolling', hostSel, spanSel, timeSel, graphDiv)
}, [ _('Apply interval »') ])
])
]),
E('hr'),
graphDiv
])
]);
requestAnimationFrame(L.bind(this.updateGraphs, this, hostSel, spanSel, timeSel, graphDiv));
var resizeTimeout;
var rgCallback = L.bind(this.refreshGraphs, this, hostSel, spanSel, timeSel, graphDiv);
addEventListener('resize', function() { clearTimeout(resizeTimeout); resizeTimeout = setTimeout(rgCallback, 250); });
return view;
},
handleSave: null,
handleSaveApply: null,
handleReset: null
});