vigigraph / vigigraph / public / js / graph.js @ 696cffc9
History | View | Annotate | Download (19.3 KB)
1 |
// Copyright (C) 2006-2020 CS GROUP - France
|
---|---|
2 |
// License: GNU GPL v2 <http://www.gnu.org/licenses/gpl-2.0.html>
|
3 |
|
4 |
var refresh_delay = 30; |
5 |
var graphs = [];
|
6 |
|
7 |
var old_fragment = ''; |
8 |
var skip_detection = 0; |
9 |
|
10 |
var logger = new Log(); |
11 |
logger.enableLog(); |
12 |
|
13 |
var Graph = new Class({ |
14 |
Implements: [Events, Options],
|
15 |
|
16 |
options: {
|
17 |
duration: 86400, |
18 |
start: null, |
19 |
autoRefresh: 0, |
20 |
refreshDelay: null, |
21 |
left: null, |
22 |
top: null, |
23 |
overlap: 10 // Pourcentage de recouvrement (voir #730) |
24 |
}, |
25 |
|
26 |
initialize: function (options, host, graph) { |
27 |
this.setOptions(options);
|
28 |
this.host = host;
|
29 |
this.graph = graph;
|
30 |
this.refreshTimer = null; |
31 |
this.destroyed = false; |
32 |
|
33 |
new Request.JSON({
|
34 |
url: app_path + 'rpc/startTime', |
35 |
onSuccess: function (data) { |
36 |
this.startTime = data.starttime.toInt();
|
37 |
}.bind(this)
|
38 |
}).get({'host': this.host}); |
39 |
|
40 |
var toolbar = new Jx.Toolbar({position:'top'}); |
41 |
var ngettext = function (singular, plural, n) { |
42 |
// Retourne une version pluralisée de la période de temps,
|
43 |
// tout en supportant les substitutions au format de Python.
|
44 |
// En particulier, "%(qtty)d" est remplacé par le nombre d'unités.
|
45 |
return window.ngettext(singular, plural, n).substitute(
|
46 |
{ |
47 |
'qtty': n
|
48 |
}, |
49 |
/%\(([a-z]+)\)[dui]/g
|
50 |
); |
51 |
}; |
52 |
|
53 |
// Périodes de temps disponibles.
|
54 |
// Voir aussi RpcController.presets pour l'équivalent côté Python.
|
55 |
var periods = [
|
56 |
[ngettext('Last %(qtty)d hour', 'Last %(qtty)d hours', 12), 12], |
57 |
[ngettext('Last %(qtty)d hour', 'Last %(qtty)d hours', 24), 24], |
58 |
[ngettext('Last %(qtty)d hour', 'Last %(qtty)d hours', 48), 48], |
59 |
[ngettext('Last %(qtty)d day', 'Last %(qtty)d days', 7), 7*24], |
60 |
[ngettext('Last %(qtty)d day', 'Last %(qtty)d days', 14), 14*24], |
61 |
[ngettext('Last %(qtty)d month', 'Last %(qtty)d months', 1), 30*24], |
62 |
[ngettext('Last %(qtty)d month', 'Last %(qtty)d months', 3), 3*30*24], |
63 |
[ngettext('Last %(qtty)d month', 'Last %(qtty)d months', 6), 6*30*24], |
64 |
[ngettext('Last %(qtty)d year', 'Last %(qtty)d years', 1), 365*24] |
65 |
]; |
66 |
|
67 |
var timeframe = new Jx.Menu({ |
68 |
label: _("Timeframe"), |
69 |
image: app_path + 'images/history.png', |
70 |
tooltip: _("Timeframe menu") |
71 |
}); |
72 |
|
73 |
periods.each(function (period) {
|
74 |
var menuItem = new Jx.Menu.Item({ |
75 |
label: period[0] |
76 |
}); |
77 |
menuItem.options.period = period[1] * 60 * 60; |
78 |
menuItem.addEvent('click', function () { |
79 |
this[0].options.start = null; |
80 |
this[0].options.duration = (this[1].options.period).toInt(); |
81 |
this[0].updateGraph(); |
82 |
}.bind([this, menuItem]));
|
83 |
timeframe.add(menuItem); |
84 |
}, this);
|
85 |
|
86 |
// Indicateur d'alerte en cas d'erreur
|
87 |
var alert_msg = _(
|
88 |
'Could not load the graph for "%(graph)s" on "%(host)s". ' +
|
89 |
'Make sure VigiRRD is running and receives performance data.'
|
90 |
); |
91 |
// Le pattern donné à substitute permet de garder une syntaxe
|
92 |
// cohérente avec Python (facilite le travail des traducteurs).
|
93 |
alert_msg = alert_msg.substitute({ |
94 |
'graph': this.graph, |
95 |
'host': this.host |
96 |
}, (/\\?%\(([^()]+)\)s/g));
|
97 |
this.alert_indicator = new Element("img"); |
98 |
this.alert_indicator.addClass("alert-indicator"); |
99 |
this.alert_indicator.setProperty("src", app_path + 'images/messagebox_warning.png'); |
100 |
this.alert_indicator.setProperty("title", alert_msg); |
101 |
|
102 |
this.indicators = new Jx.Menu({ |
103 |
label: _("Export to CSV"), |
104 |
image: app_path + 'images/document-export.png', |
105 |
tooltip: _("Export the content of this graph to CSV") |
106 |
}); |
107 |
|
108 |
new Request.JSON({
|
109 |
url: app_path + 'rpc/getIndicators', |
110 |
onSuccess: function (data) { |
111 |
data.items.each(function (item) {
|
112 |
this.indicators.add(new Jx.Menu.Item({ |
113 |
name: item[0], |
114 |
label: item[1], |
115 |
onClick: this.exportCSV.bind(this), |
116 |
indicator: true |
117 |
})); |
118 |
}, this);
|
119 |
|
120 |
this.indicators.add(new Jx.Menu.Item({ |
121 |
label: _('All'), |
122 |
onClick: this.exportCSV.bind(this), |
123 |
indicator: false |
124 |
})); |
125 |
}.bind(this)
|
126 |
}).get({ |
127 |
'host': this.host, |
128 |
'graph': this.graph |
129 |
}); |
130 |
|
131 |
this.refresh_button = new Jx.Button({ |
132 |
image: app_path + 'images/refresh.png', |
133 |
tooltip: _("Automatically refresh the graph"), |
134 |
toggle: true, |
135 |
onDown: function() { |
136 |
// On s'assure qu'il n'y a pas déjà un timer lancé.
|
137 |
if ($chk(this.refreshTimer)) |
138 |
return;
|
139 |
var delay =
|
140 |
this.options.refreshDelay ||
|
141 |
window.refresh_delay; |
142 |
this.refreshTimer =
|
143 |
this.updateGraph.periodical(delay * 1000, this); |
144 |
this.options.autoRefresh = 1; |
145 |
logger.log( |
146 |
'Auto-refresh enabled on graph "{graph}" for host "{host}"'
|
147 |
.substitute({ |
148 |
'graph': this.graph, |
149 |
'host': this.host |
150 |
}) |
151 |
); |
152 |
window.updateURI(); |
153 |
}.bind(this),
|
154 |
onUp: function() { |
155 |
clearInterval(this.refreshTimer);
|
156 |
// clearInterval arrête le timer, mais n'invalide pas
|
157 |
// la référence, ce dont on a besoin (cf. onDown).
|
158 |
this.refreshTimer = null; |
159 |
this.options.autoRefresh = 0; |
160 |
logger.log( |
161 |
'Auto-refresh disabled on graph "{graph}" for host "{host}"'
|
162 |
.substitute({ |
163 |
'graph': this.graph, |
164 |
'host': this.host |
165 |
}) |
166 |
); |
167 |
window.updateURI(); |
168 |
}.bind(this)
|
169 |
}); |
170 |
|
171 |
this.zoom_in = new Jx.Button({ |
172 |
image: app_path + 'images/zoom-in.png', |
173 |
tooltip: _("Zoom in"), |
174 |
onClick: function() { |
175 |
this.updateZoom(true); |
176 |
}.bind(this)
|
177 |
}); |
178 |
|
179 |
this.zoom_out = new Jx.Button({ |
180 |
image: app_path + 'images/zoom-out.png', |
181 |
tooltip: _("Zoom out"), |
182 |
onClick: function() { |
183 |
this.updateZoom(false); |
184 |
}.bind(this)
|
185 |
}); |
186 |
|
187 |
toolbar.add( |
188 |
this.refresh_button,
|
189 |
timeframe, |
190 |
new Jx.Button({
|
191 |
image: app_path + 'images/start.png', |
192 |
tooltip: _("Graph start"), |
193 |
onClick: function() { |
194 |
this.options.start = this.startTime; |
195 |
this.updateGraph();
|
196 |
}.bind(this)
|
197 |
}), |
198 |
new Jx.Button({
|
199 |
image: app_path + 'images/previous.png', |
200 |
tooltip: _("Previous section"), |
201 |
onClick: function() { |
202 |
this.options.start =
|
203 |
this.getStartTime() -
|
204 |
this.options.duration +
|
205 |
(this.options.overlap * this.options.duration / 100).toInt(); |
206 |
this.updateGraph();
|
207 |
}.bind(this)
|
208 |
}), |
209 |
new Jx.Button({
|
210 |
image: app_path + 'images/next.png', |
211 |
tooltip: _("Next section"), |
212 |
onClick: function() { |
213 |
this.options.start =
|
214 |
this.getStartTime() +
|
215 |
this.options.duration -
|
216 |
(this.options.overlap * this.options.duration / 100).toInt(); |
217 |
this.updateGraph();
|
218 |
}.bind(this)
|
219 |
}), |
220 |
new Jx.Button({
|
221 |
image: app_path + 'images/end.png', |
222 |
tooltip: _("Graph end"), |
223 |
onClick: function() { |
224 |
this.options.start = null; |
225 |
this.updateGraph();
|
226 |
}.bind(this)
|
227 |
}), |
228 |
this.zoom_in,
|
229 |
this.zoom_out,
|
230 |
this.indicators,
|
231 |
new Jx.Button({
|
232 |
image: app_path + 'images/document-print-small.png', |
233 |
tooltip: _("Print graph"), |
234 |
onClick: this.print.bind(this) |
235 |
}) |
236 |
); |
237 |
|
238 |
var label = _("Graph for \"%(graph)s\" on \"%(host)s\""); |
239 |
// Le pattern donné à substitute permet de garder une syntaxe
|
240 |
// cohérente avec Python (facilite le travail des traducteurs).
|
241 |
label = label.substitute({ |
242 |
'graph': this.graph, |
243 |
'host': this.host |
244 |
}, (/\\?%\(([^()]+)\)s/g));
|
245 |
|
246 |
this.graph_window = new Jx.Dialog({ |
247 |
label: label,
|
248 |
modal: false, |
249 |
move: true, |
250 |
close: true, |
251 |
horizontal: this.options.left + ' left', |
252 |
vertical: this.options.top + ' top', |
253 |
width: 575, |
254 |
height: 75, |
255 |
vertical_overflow: true, |
256 |
horizontal_overflow: true, |
257 |
toolbars: [toolbar]
|
258 |
}); |
259 |
// Empêche l'affichage d'une barre de défilement vertical
|
260 |
// désactivée sous Chromium.
|
261 |
this.graph_window.content.setStyle('overflow', 'hidden'); |
262 |
|
263 |
this.alert_indicator.inject(this.graph_window.content.parentNode); |
264 |
|
265 |
// mise à jour
|
266 |
this.updateGraph();
|
267 |
this.graph_window.open();
|
268 |
|
269 |
this.refresh_button.setActive(parseInt(this.options.autoRefresh, 10)); |
270 |
|
271 |
var onClose = function () { |
272 |
if (this.destroyed) return; |
273 |
this.destroyed = true; |
274 |
this.graph_window.domObj.dispose();
|
275 |
window.graphs.erase(this);
|
276 |
window.updateURI(); |
277 |
}; |
278 |
|
279 |
this.graph_window.addEvent('close', onClose.bind(this)); |
280 |
// sizeChange est déclenché à la fois après un redimensionnement
|
281 |
// et après un déplacement. Ce cas est mal documenté dans JxLib.
|
282 |
this.graph_window.addEvent('sizeChange', this.dialogMoved.bind(this)); |
283 |
|
284 |
// Simule un déplacement de la fenêtre,
|
285 |
// pour mettre à jour les coordonnées.
|
286 |
this.dialogMoved();
|
287 |
window.graphs.push(this);
|
288 |
return this; |
289 |
}, |
290 |
|
291 |
destroy: function () { |
292 |
this.graph_window.close();
|
293 |
}, |
294 |
|
295 |
dialogMoved: function () { |
296 |
// Repris de l'API interne de JxLib (création du Drag).
|
297 |
this.options.left = parseInt(this.graph_window.domObj.style.left, 10); |
298 |
this.options.top = parseInt(this.graph_window.domObj.style.top, 10); |
299 |
window.updateURI(); |
300 |
}, |
301 |
|
302 |
updateZoom: function (zoom_in) { |
303 |
var start = this.options.start; |
304 |
var factor = zoom_in ? 0.5 : 2; |
305 |
this.options.duration = parseInt(this.options.duration, 10); |
306 |
if (start !== null) { |
307 |
// On zoom sur la partie centrale du graphe.
|
308 |
if (zoom_in) start += this.options.duration * 0.25; |
309 |
// On dézoome "par les 2 côtés".
|
310 |
else start -= this.options.duration * 0.5; |
311 |
this.options.start = start;
|
312 |
} |
313 |
this.options.duration *= factor;
|
314 |
// Période minimale d'affichage : 1 minute.
|
315 |
if (this.options.duration < 60) |
316 |
this.options.duration = 60; |
317 |
// On (dés)active le bouton de zoom en avant au besoin.
|
318 |
this.zoom_in.setEnabled(this.options.duration != 60); |
319 |
this.updateGraph();
|
320 |
}, |
321 |
|
322 |
getStartTime: function () { |
323 |
var start = this.options.start; |
324 |
if (start === null) |
325 |
// On génère un horodatage UNIX correspondant
|
326 |
// à l'heure courante en UTC.
|
327 |
start = (new Date() / 1000).toInt() - this.options.duration; |
328 |
if (start < 0) |
329 |
return 0; |
330 |
return start;
|
331 |
}, |
332 |
|
333 |
exportCSV: function (menuItem) { |
334 |
var uri = new URI(app_path + 'vigirrd/' + |
335 |
encodeURIComponent(this.host) + '/export'); |
336 |
|
337 |
var start = this.getStartTime(); |
338 |
|
339 |
uri.setData({ |
340 |
host: this.host, |
341 |
graphtemplate: this.graph, |
342 |
start: start,
|
343 |
end: start + this.options.duration, |
344 |
// Décalage par rapport à UTC, ex: 60 = UTC+01:00.
|
345 |
timezone: (new Date()).getTimezoneOffset(), |
346 |
nocache: (new Date() / 1) |
347 |
}); |
348 |
|
349 |
if (menuItem.options.indicator)
|
350 |
uri.setData({ds: menuItem.options.name}, true); |
351 |
|
352 |
window.open(uri.toString()); |
353 |
}, |
354 |
|
355 |
// Cette fonction est aussi utilisée dans print.js
|
356 |
// pour gérer l'impression globale.
|
357 |
getPrintParams: function () { |
358 |
var img = this.graph_window.content.getElement('img'); |
359 |
var img_uri = new URI(img.src); |
360 |
var params = img_uri.getData();
|
361 |
var res = $H({ |
362 |
host: params.host,
|
363 |
start: params.start,
|
364 |
duration: params.duration,
|
365 |
graph: params.graphtemplate,
|
366 |
nocache: params. nocache,
|
367 |
}); |
368 |
return res.toQueryString();
|
369 |
}, |
370 |
|
371 |
print: function () { |
372 |
var uri = new URI(app_path + 'rpc/graphsList'); |
373 |
uri.setData({graphs: [this.getPrintParams()]}); |
374 |
var print_window = window.open(uri.toString());
|
375 |
print_window.onload = function () { |
376 |
this.print();
|
377 |
}.bind(print_window); |
378 |
}, |
379 |
|
380 |
updateGraph: function () { |
381 |
logger.log("Updating graph for '" + this.graph + |
382 |
"' on '" + this.host + "'"); |
383 |
|
384 |
var uri = new URI(app_path + 'vigirrd/' + |
385 |
encodeURIComponent(this.host) + '/graph.png'); |
386 |
|
387 |
var start = this.getStartTime(); |
388 |
|
389 |
uri.setData({ |
390 |
host: this.host, |
391 |
start: start,
|
392 |
duration: this.options.duration, |
393 |
graphtemplate: this.graph, |
394 |
// Permet d'empêcher la mise en cache du graphe.
|
395 |
// Nécessaire car le graphe évolue dynamiquement au fil de l'eau.
|
396 |
nocache: (new Date() / 1) |
397 |
}); |
398 |
|
399 |
// Si possible, on remplace la précédente image.
|
400 |
var img = this.graph_window.content.getElement('img'); |
401 |
if (img !== null) { |
402 |
img.set('src', uri.toString());
|
403 |
return;
|
404 |
} |
405 |
|
406 |
// On génère dynamiquement une balise "img" pour charger le graphe.
|
407 |
this.graph_window.setContent(
|
408 |
'<img src="' + uri.toString() + '"/' + '>'); |
409 |
img = this.graph_window.content.getElement('img'); |
410 |
|
411 |
img.addEvent('load', function () { |
412 |
// On ne peut pas réutiliser "img" directement ici,
|
413 |
// car on génère une boucle dans les références JS
|
414 |
// (ce qui donne lieu à une fuite mémoire).
|
415 |
var content_img = this.graph_window.content.getElement('img'); |
416 |
this.graph_window.resize(
|
417 |
content_img.width + 25,
|
418 |
content_img.height + 73
|
419 |
); |
420 |
this.hideAlert();
|
421 |
}.bind(this));
|
422 |
|
423 |
img.addEvent('error', function () { |
424 |
this.showAlert();
|
425 |
}.bind(this));
|
426 |
}, |
427 |
|
428 |
showAlert: function() { |
429 |
this.alert_indicator.setStyle("display", "block"); |
430 |
var zindex = parseInt(this.graph_window.domObj.getStyle("z-index"), 10) + 1; |
431 |
this.alert_indicator.setStyle("z-index", zindex); |
432 |
return;
|
433 |
}, |
434 |
|
435 |
hideAlert: function() { |
436 |
this.alert_indicator.setStyle("display", "none"); |
437 |
} |
438 |
}); |
439 |
|
440 |
var updateURI = function () { |
441 |
logger.log("Updating the current window's URI");
|
442 |
|
443 |
var graphs_uri = [];
|
444 |
var uri = new URI(); |
445 |
|
446 |
// Section critique.
|
447 |
window.skip_detection++; |
448 |
uri.set('fragment', ''); |
449 |
|
450 |
window.graphs.each(function (graph) {
|
451 |
var props = new Hash(graph.options); |
452 |
props.extend({host: graph.host, graph: graph.graph}); |
453 |
// Sous Firefox, l'apostrophe n'est pas échappée via JavaScript,
|
454 |
// alors qu'elle l'est par le navigateur dans l'URL.
|
455 |
// Afin d'éviter une boucle de rechargement infinie, on échappe
|
456 |
// manuellement l'apostrophe.
|
457 |
this.push(props.toQueryString().replace(/'/, '%27')); |
458 |
}, graphs_uri); |
459 |
|
460 |
uri.setData({'graphs': graphs_uri, safety: 1}, false, 'fragment'); |
461 |
uri.go(); |
462 |
window.old_fragment = uri.toString(); |
463 |
|
464 |
// Fin de section critique.
|
465 |
window.skip_detection--; |
466 |
}; |
467 |
|
468 |
var update_visible_graphs = function (new_fragment) { |
469 |
logger.log("Updating visible graphs");
|
470 |
|
471 |
// On réouvre les graphes précédemment chargés.
|
472 |
var new_graphs = [];
|
473 |
var qs = new Hash(new_fragment.get('fragment').parseQueryString()); |
474 |
if (qs.has('graphs')) { |
475 |
new_graphs = $splat(qs.get('graphs')); |
476 |
} |
477 |
|
478 |
// Section critique.
|
479 |
window.skip_detection++; |
480 |
|
481 |
var prev_graphs = window.graphs;
|
482 |
window.graphs = []; |
483 |
prev_graphs.each(function (graph) { graph.destroy(); });
|
484 |
|
485 |
new_graphs.each(function (graph) {
|
486 |
var uri = new URI('?' + graph); |
487 |
var qs = new Hash(uri.getData()); |
488 |
if (qs.has('host') && qs.has('graph')) { |
489 |
var options = new Hash(); |
490 |
var params = [
|
491 |
'start',
|
492 |
'duration',
|
493 |
'left',
|
494 |
'top',
|
495 |
'autoRefresh',
|
496 |
'refreshDelay'
|
497 |
]; |
498 |
|
499 |
params.each(function (param) {
|
500 |
if (this[0].has(param)) |
501 |
this[1].set(param, (this[0].get(param)).toInt()); |
502 |
}, [qs, options]); |
503 |
|
504 |
new Graph(
|
505 |
options.getClean(), |
506 |
qs.get('host'),
|
507 |
qs.get('graph')
|
508 |
); |
509 |
} |
510 |
}); |
511 |
|
512 |
window.updateURI(); |
513 |
|
514 |
// Fin de section critique.
|
515 |
window.skip_detection--; |
516 |
|
517 |
if (window.graphs.length == 1) { |
518 |
new Request.JSON({
|
519 |
url: app_path + 'rpc/selectHostAndGraph', |
520 |
onSuccess: function (results) { |
521 |
window.toolbar.host_picker.setItem(results.idhost, this[0]); |
522 |
window.toolbar.graph_picker.idselection = results.idgraph; |
523 |
window.toolbar.graph_picker.setLabel(this[1]); |
524 |
}.bind([ |
525 |
window.graphs[0].host,
|
526 |
window.graphs[0].graph
|
527 |
]) |
528 |
}).get({ |
529 |
host: window.graphs[0].host, |
530 |
graph: window.graphs[0].graph |
531 |
}); |
532 |
} |
533 |
}; |
534 |
|
535 |
var hash_change_detector = function() { |
536 |
var new_fragment;
|
537 |
|
538 |
// Pour les moments où on a besoin de mettre à jour
|
539 |
// volontairement l'URI (à l'ouverture d'un graphe).
|
540 |
if (window.skip_detection) return; |
541 |
|
542 |
// Force mootools à analyser l'URL courante de nouveau,
|
543 |
// ce qui mettra à jour la partie "fragment" de l'URI.
|
544 |
URI.base = new URI(
|
545 |
document.getElements('base[href]', true).getLast(), |
546 |
{base: document.location}
|
547 |
); |
548 |
|
549 |
new_fragment = new URI();
|
550 |
if (old_fragment.toString() != new_fragment.toString()) {
|
551 |
update_visible_graphs(new_fragment, old_fragment); |
552 |
} |
553 |
}; |
554 |
|
555 |
if ('onhashchange' in window) { |
556 |
window.onhashchange = hash_change_detector; |
557 |
} else {
|
558 |
hash_change_detector.periodical(100);
|
559 |
} |
560 |
|
561 |
window.addEvent('load', function () { |
562 |
new Request.JSON({
|
563 |
url: app_path + 'rpc/tempoDelayRefresh', |
564 |
onSuccess: function (data) { |
565 |
window.refresh_delay = data.delay; |
566 |
} |
567 |
}).get(); |
568 |
|
569 |
hash_change_detector(); |
570 |
}); |