Project

General

Profile

Statistics
| Branch: | Tag: | Revision:

vigigraph / vigigraph / public / js / graph.js @ 56b77667

History | View | Annotate | Download (19.3 KB)

1
// Copyright (C) 2006-2020 CS-SI
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
});