From 2487c4f1eabf9e8c517ee365462ca74991943ab8 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Carlos=20Mart=C3=ADn?= <cmartin@opennebula.org>
Date: Wed, 30 Apr 2014 16:02:19 +0200
Subject: [PATCH] Feature #1607: Add accounting tab to user and group resources

---
 install.sh                                    |   1 +
 src/sunstone/public/js/plugins/groups-tab.js  |  12 +
 src/sunstone/public/js/plugins/users-tab.js   |  13 +
 src/sunstone/public/js/sunstone.js            | 475 ++++++++++++++++++
 .../vendor/flot/jquery.flot.tooltip.min.js    |  12 +
 src/sunstone/views/index.erb                  |   1 +
 6 files changed, 514 insertions(+)
 create mode 100644 src/sunstone/public/vendor/flot/jquery.flot.tooltip.min.js

diff --git a/install.sh b/install.sh
index 681c236228..6fbec3466f 100755
--- a/install.sh
+++ b/install.sh
@@ -1497,6 +1497,7 @@ src/sunstone/public/vendor/flot/jquery.flot.navigate.min.js \
 src/sunstone/public/vendor/flot/jquery.flot.pie.min.js \
 src/sunstone/public/vendor/flot/jquery.flot.resize.min.js \
 src/sunstone/public/vendor/flot/jquery.flot.stack.min.js \
+src/sunstone/public/vendor/flot/jquery.flot.tooltip.min.js \
 src/sunstone/public/vendor/flot/LICENSE.txt \
 src/sunstone/public/vendor/flot/NOTICE"
 
diff --git a/src/sunstone/public/js/plugins/groups-tab.js b/src/sunstone/public/js/plugins/groups-tab.js
index 9c35733922..b35598601f 100644
--- a/src/sunstone/public/js/plugins/groups-tab.js
+++ b/src/sunstone/public/js/plugins/groups-tab.js
@@ -703,9 +703,16 @@ function updateGroupInfo(request,group){
         </div>'
     };
 
+    var accounting_tab = {
+        title: tr("Accounting"),
+        icon: "fa-bar-chart-o",
+        content: '<div id="group_accounting"></div>'
+    };
+
     Sunstone.updateInfoPanelTab("group_info_panel","group_info_tab",info_tab);
     Sunstone.updateInfoPanelTab("group_info_panel","group_quotas_tab",quotas_tab);
     Sunstone.updateInfoPanelTab("group_info_panel","group_providers_tab",providers_tab);
+    Sunstone.updateInfoPanelTab("group_info_panel","group_accouning_tab",accounting_tab);
     Sunstone.popUpInfoPanel("group_info_panel", 'groups-tab');
 
     $("#add_rp_button", $("#group_info_panel")).click(function(){
@@ -715,6 +722,11 @@ function updateGroupInfo(request,group){
 
         return false;
     });
+
+    accountingGraphs(
+        $("#group_accounting","#group_info_panel"),
+        {   fixed_group: info.ID,
+            init_group_by: "user" });
 }
 
 function setup_group_resource_tab_content(zone_id, zone_section, str_zone_tab_id, str_datatable_id, selected_group_clusters, group) {
diff --git a/src/sunstone/public/js/plugins/users-tab.js b/src/sunstone/public/js/plugins/users-tab.js
index 98c98aeebe..f835e49dcb 100644
--- a/src/sunstone/public/js/plugins/users-tab.js
+++ b/src/sunstone/public/js/plugins/users-tab.js
@@ -715,10 +715,23 @@ function updateUserInfo(request,user){
         content : quotas_html
     };
 
+    var accounting_tab = {
+        title: tr("Accounting"),
+        icon: "fa-bar-chart-o",
+        content: '<div id="user_accounting"></div>'
+    };
+
     Sunstone.updateInfoPanelTab("user_info_panel","user_info_tab",info_tab);
     Sunstone.updateInfoPanelTab("user_info_panel","user_quotas_tab",quotas_tab);
+    Sunstone.updateInfoPanelTab("user_info_panel","user_accouning_tab",accounting_tab);
     //Sunstone.updateInfoPanelTab("user_info_panel","user_acct_tab",acct_tab);
     Sunstone.popUpInfoPanel("user_info_panel", 'users-tab');
+
+    accountingGraphs(
+        $("#user_accounting","#user_info_panel"),
+        {   fixed_user: info.ID,
+            init_group_by: "vm" });
+
 };
 
 // Used also from groups-tabs.js
diff --git a/src/sunstone/public/js/sunstone.js b/src/sunstone/public/js/sunstone.js
index 21790d112e..f3104c4575 100644
--- a/src/sunstone/public/js/sunstone.js
+++ b/src/sunstone/public/js/sunstone.js
@@ -3902,3 +3902,478 @@ $(document).ready(function(){
       }
     })
 });
+
+// div is a jQuery selector
+// The following options can be set:
+//   fixed_user     fix an owner user ID
+//   fixed_group    fix an owner group ID
+//   init_group_by  "user", "group", "vm". init the group-by selector
+function accountingGraphs(div, opt){
+    div.append(
+    '<div class="row">\
+      <div class="large-2 columns">\
+        '+tr("Time range")+'\
+      </div>\
+      <div class="large-5 columns">\
+        <input id="acct_start_time" class="jdpicker" type="text" placeholder="2013/12/30"/>\
+      </div>\
+      <div class="large-5 columns">\
+        <input id="acct_end_time" class="jdpicker" type="text" placeholder="'+tr("Today")+'"/>\
+      </div>\
+    </div>\
+    <div class="row">\
+      <div class="large-6 columns">\
+        <input id="acct_owner_all"   type="radio" name="acct_owner" value="acct_owner_all" checked/><label for="acct_owner_all">' + tr("All") + '</label>\
+        <input id="acct_owner_group" type="radio" name="acct_owner" value="acct_owner_group" /><label for="acct_owner_group">' + tr("Group") + '</label>\
+        <input id="acct_owner_user"  type="radio" name="acct_owner" value="acct_owner_user" /><label for="acct_owner_user">' + tr("User") + '</label>\
+      </div>\
+      <div class="large-6 columns">\
+        <div id="acct_owner_select"/>\
+      </div>\
+    </div>\
+    <div class="row">\
+      <div class="large-12 columns">\
+        <label for="acct_group_by">' +  tr("Group by") + '</label>\
+        <select id="acct_group_by" name="acct_group_by">\
+          <option value="user">' + tr("User") + '</option>\
+          <option value="group">' + tr("Group") + '</option>\
+          <option value="vm">' + tr("VM") + '</option>\
+        </select>\
+      </div>\
+    </div>\
+    <div class="row">\
+      <button class="button radius left success" id="acct_submit" type="submit">'+tr("Go")+'</button>\
+    </div>\
+    <div class="row">\
+      <div class="row graph_legend">\
+        <h3 class="subheader"><small>'+tr("CPU hours")+'</small></h3>\
+      </div>\
+      <div class="row">\
+        <div class="large-10 columns centered graph" id="acct_cpu_graph" style="height: 200px;">\
+        </div>\
+      </div>\
+      <div class="row graph_legend">\
+        <div class="large-10 columns centered" id="acct_cpu_legend">\
+        </div>\
+      </div>\
+    </div>\
+    <div class="row">\
+      <div class="row graph_legend">\
+        <h3 class="subheader"><small>'+tr("Memory GB hours")+'</small></h3>\
+      </div>\
+      <div class="row">\
+        <div class="large-10 columns centered graph" id="acct_mem_graph" style="height: 200px;">\
+        </div>\
+      </div>\
+      <div class="row graph_legend">\
+        <div class="large-10 columns centered" id="acct_mem_legend">\
+        </div>\
+      </div>\
+    </div>\
+    <div class="row graph_legend">\
+      <div class="large-12 columns centered" id="acct_legend">\
+      </div>\
+    </div>');
+
+    if (opt == undefined){
+        opt = {};
+    }
+
+    //--------------------------------------------------------------------------
+    // Init start time to 1st of last month
+    //--------------------------------------------------------------------------
+    var d = new Date();
+
+    d.setDate(1);
+    d.setMonth(d.getMonth() - 1);
+
+    $("#acct_start_time", div).val(d.getFullYear() + '/' + (d.getMonth()+1) + '/' + d.getDate());
+
+    $("#acct_start_time", div).jdPicker();
+    $("#acct_end_time", div).jdPicker();
+
+    //--------------------------------------------------------------------------
+    // VM owner: all, group, user
+    //--------------------------------------------------------------------------
+
+    if (opt.fixed_user != undefined || opt.fixed_group != undefined){
+        $("input[name='acct_owner']", div).attr("disabled", "disabled");
+
+        $("#acct_owner_select", div).show();
+
+        if(opt.fixed_user != undefined){
+            var text = tr("User") +" " + opt.fixed_user;
+            $("input[value='acct_owner_user']", div).attr("checked", "checked");
+        } else {
+            var text = tr("Group") + " " + opt.fixed_group;
+            $("input[value='acct_owner_group']", div).attr("checked", "checked");
+        }
+
+        $("#acct_owner_select", div).text(text);
+    } else {
+        $("input[name='acct_owner']", div).change(function(){
+            var value = $(this).val();
+
+            switch (value){
+            case "acct_owner_all":
+                $("#acct_owner_select", div).hide();
+                break;
+
+            case "acct_owner_group":
+                $("#acct_owner_select", div).show();
+                insertSelectOptions("#acct_owner_select", div, "Group");
+                break;
+
+            case "acct_owner_user":
+                $("#acct_owner_select", div).show();
+                insertSelectOptions("#acct_owner_select", div, "User", -1, false,
+                    '<option value="-1">'+tr("<< me >>")+'</option>');
+                break;
+            }
+        });
+    }
+
+    //--------------------------------------------------------------------------
+    // Init group by select
+    //--------------------------------------------------------------------------
+
+    if(opt.init_group_by != undefined){
+        $("#acct_group_by", div).val(opt.init_group_by);
+    }
+
+    //--------------------------------------------------------------------------
+    // Submit request
+    //--------------------------------------------------------------------------
+
+    // TODO: make start_time mandatory
+
+    $("#acct_submit", div).on("click", function(){
+        var start_time = -1;
+        var end_time = -1;
+
+        var v = $("#acct_start_time", div).val();
+        if (v != ""){
+            start_time = new Date(v).getTime() / 1000;
+        }
+
+        var v = $("#acct_end_time", div).val();
+        if (v != ""){
+            end_time = new Date(v).getTime() / 1000;
+        }
+
+        var options = {
+            "start_time": start_time,
+            "end_time": end_time
+        };
+
+        if (opt.fixed_user != undefined){
+            options.userfilter = opt.fixed_user;
+        } else if (opt.fixed_group != undefined){
+            options.group = opt.fixed_group;
+        } else {
+            var select_val = $("#acct_owner_select .resource_list_select", div).val();
+
+            switch ($("input[name='acct_owner']:checked", div).val()){
+            case "acct_owner_all":
+                break;
+
+            case "acct_owner_group":
+                if(select_val != ""){
+                    options.group = select_val;
+                }
+                break;
+
+            case "acct_owner_user":
+                if(select_val != ""){
+                    options.userfilter = select_val;
+                }
+                break;
+            }
+        }
+
+        OpenNebula.VM.accounting({
+    //        timeout: true,
+            success: function(req, response){
+                fillAccounting(div, req, response);
+            },
+            error: onError,
+            data: options
+        });
+    });
+}
+
+function fillAccounting(div, req, response) {
+/*
+    console.log(req);
+    console.log(response);
+*/
+
+
+/*
+TODO More options:
+
+Granularity: month, day, hour
+Time range
+Group by: user, group, vm, [flow]
+Filter by: user, group, vm, [flow]
+
+Download csv
+*/
+
+    var options = req.request.data[0];
+
+    //--------------------------------------------------------------------------
+    // Time slots
+    //--------------------------------------------------------------------------
+
+    // start_time is mandatory
+    var start = new Date(options.start_time * 1000);
+
+    var end = new Date();
+
+    if(options.end_time != undefined && options.end_time != -1){
+        var end = new Date(options.end_time * 1000)
+    }
+
+    // granularity of 1 day
+    var times = [];
+
+    var tmp_time = start;
+
+    // End time is the start of the last time slot. We use <=, to
+    // add one extra time step
+    while (tmp_time <= end) {
+        times.push(tmp_time.getTime());
+
+        // day += 1
+        tmp_time.setDate( tmp_time.getDate() + 1 );
+    }
+
+    //--------------------------------------------------------------------------
+    // Flot options
+    //--------------------------------------------------------------------------
+
+    var options = {
+//        colors: [ "#2ba6cb", "#707D85", "#AC5A62" ],
+
+        xaxis : {
+            mode: "time",
+            timeformat: "%y/%m/%d",
+            min: times[0],
+            max: end.getTime(),
+            color: "#999",
+            size: 8
+        },
+        yaxis : { labelWidth: 50,
+                  min: 0,
+                color: "#999",
+                size: 8
+                },
+        series: {
+            bars: {
+                show: true,
+                lineWidth: 1,
+                fill: true,
+                barWidth: 24*60*60*1000 * 0.8,
+                align: "center"
+            },
+            stack: true
+        },
+        legend : {
+            show : true,
+            noColumns: 6,
+            container: $("#acct_legend", div)
+        },
+        grid: {
+            borderWidth: 1,
+            borderColor: "#cfcfcf",
+            hoverable: true
+        },
+        tooltip: true,
+        tooltipOpts: {
+            content: "%x | %s | %y"      //"%s | X: %x | Y: %y"
+/*
+            xDateFormat:    string                  //null
+            yDateFormat:    string                  //null
+            monthNames:     string                  // null
+            dayNames:       string                  // null
+            shifts: {
+                x:          int                     //10
+                y:          int                     //20
+            },
+            defaultTheme:   boolean                 //true
+            onHover:        function(flotItem, $tooltipEl)
+*/
+        }
+    };
+
+    //--------------------------------------------------------------------------
+    // Group by
+    //--------------------------------------------------------------------------
+
+    // TODO: Allow to change group by dynamically, instead of calling oned again
+
+    switch ($("#acct_group_by", div).val()){
+    case "user":
+        var group_by_fn = function(history){
+            return history.VM.UID;
+        }
+
+        var group_by_prefix = tr("User")+" ";
+
+        break;
+
+    case "group":
+        var group_by_fn = function(history){
+            return history.VM.GID;
+        }
+
+        var group_by_prefix = tr("Group")+" ";
+
+        break;
+
+    case "vm":
+        var group_by_fn = function(history){
+            return history.OID;
+        }
+
+        var group_by_prefix = tr("VM")+" ";
+
+        break;
+    }
+
+    //--------------------------------------------------------------------------
+    // Filter history entries
+    //--------------------------------------------------------------------------
+
+    // TODO filter
+    // True to proccess, false to discard
+    var filter_by_fn = function(history){
+//        return history.OID == 3605 || history.OID == 2673;
+        return true;
+    }
+
+    //--------------------------------------------------------------------------
+    // Process data series for flot
+    //--------------------------------------------------------------------------
+
+    var series = {};
+    series.CPU_HOURS = {};
+    series.MEM_HOURS = {};
+
+    // TODO: response can be an empty object
+
+    $.each(response.HISTORY_RECORDS.HISTORY, function(index, history){
+
+        if(!filter_by_fn(history)){
+            return true; //continue
+        }
+
+        var group_by = group_by_fn(history);
+
+        if (series.CPU_HOURS[group_by] == undefined){
+            series.CPU_HOURS[group_by] = {};
+        }
+
+        if (series.MEM_HOURS[group_by] == undefined){
+            series.MEM_HOURS[group_by] = {};
+        }
+
+//      TODO Optimize getting here?
+//        var serie = series.CPU_HOURS[history.VM.UID];
+
+        for (var i = 0; i<times.length-1; i++){
+
+            var t = times[i];
+            var t_next = times[i+1];
+
+            if( (history.ETIME*1000 > t || history.ETIME == 0) &&
+                (history.STIME != 0 && history.STIME*1000 <= t_next) ) {
+
+                var stime = t;
+                if(history.STIME != 0){
+                    stime = Math.max(t, history.STIME*1000);
+                }
+
+                var etime = t_next;
+                if(history.ETIME != 0){
+                    etime = Math.min(t_next, history.ETIME*1000);
+                }
+                
+                var n_hours = (etime - stime) / 1000 / 60 / 60;
+
+                // --- cpu ---
+
+                // TODO cpu may not exist
+                var val = parseFloat(history.VM.TEMPLATE.CPU) * n_hours;
+
+                var serie = series.CPU_HOURS[group_by];
+
+                if(serie[t] == undefined){
+                    serie[t] = 0;
+                }
+
+                serie[t] += val;
+
+                // --- mem ---
+
+                // TODO memory may not exist. In GB
+                var val = parseInt(history.VM.TEMPLATE.MEMORY)/1024 * n_hours;
+
+                var serie = series.MEM_HOURS[group_by];
+
+                if(serie[t] == undefined){
+                    serie[t] = 0;
+                }
+
+                serie[t] += val;
+            }
+        }
+    });
+
+//    console.log(series);
+
+    //--------------------------------------------------------------------------
+    // Create series, draw plots
+    //--------------------------------------------------------------------------
+
+    // --- cpu ---
+
+    var plot_series = [];
+
+    $.each(series.CPU_HOURS, function(key, val){
+        var data = [];
+
+        $.each(val, function(time,num){
+            data.push([parseInt(time),num]);
+        });
+
+        plot_series.push(
+        {
+            label: group_by_prefix+key,
+            data: data
+        });
+    });
+
+    $.plot($("#acct_cpu_graph", div), plot_series, options);
+
+    // --- mem ---
+
+    var plot_series = [];
+
+    $.each(series.MEM_HOURS, function(key, val){
+        var data = [];
+
+        $.each(val, function(time,num){
+            data.push([parseInt(time),num]);
+        });
+
+        plot_series.push(
+        {
+            label: group_by_prefix+key,
+            data: data
+        });
+    });
+
+    $.plot($("#acct_mem_graph", div), plot_series, options);
+}
\ No newline at end of file
diff --git a/src/sunstone/public/vendor/flot/jquery.flot.tooltip.min.js b/src/sunstone/public/vendor/flot/jquery.flot.tooltip.min.js
new file mode 100644
index 0000000000..8fdce4e4cb
--- /dev/null
+++ b/src/sunstone/public/vendor/flot/jquery.flot.tooltip.min.js
@@ -0,0 +1,12 @@
+/*
+ * jquery.flot.tooltip
+ * 
+ * description: easy-to-use tooltips for Flot charts
+ * version: 0.6.7
+ * author: Krzysztof Urbas @krzysu [myviews.pl]
+ * website: https://github.com/krzysu/flot.tooltip
+ * 
+ * build on 2014-03-26
+ * released under MIT License, 2012
+*/ 
+Array.prototype.indexOf||(Array.prototype.indexOf=function(t,i){if(void 0===this||null===this)throw new TypeError('"this" is null or not defined');var e=this.length>>>0;for(i=+i||0,1/0===Math.abs(i)&&(i=0),0>i&&(i+=e,0>i&&(i=0));e>i;i++)if(this[i]===t)return i;return-1}),function(t){var i={tooltip:!1,tooltipOpts:{content:"%s | X: %x | Y: %y",xDateFormat:null,yDateFormat:null,monthNames:null,dayNames:null,shifts:{x:10,y:20},defaultTheme:!0,onHover:function(){}}},e=function(t){this.tipPosition={x:0,y:0},this.init(t)};e.prototype.init=function(i){function e(t){var i={};i.x=t.pageX,i.y=t.pageY,s.updateTooltipPosition(i)}function o(t,i,e){var o=s.getDomElement();if(e){var n;n=s.stringFormat(s.tooltipOptions.content,e),o.html(n),s.updateTooltipPosition({x:i.pageX,y:i.pageY}),o.css({left:s.tipPosition.x+s.tooltipOptions.shifts.x,top:s.tipPosition.y+s.tooltipOptions.shifts.y}).show(),"function"==typeof s.tooltipOptions.onHover&&s.tooltipOptions.onHover(e,o)}else o.hide().html("")}var s=this,n=t.plot.plugins.length;if(this.plotPlugins=[],n)for(var r=0;n>r;r++)this.plotPlugins.push(t.plot.plugins[r].name);i.hooks.bindEvents.push(function(i,n){s.plotOptions=i.getOptions(),s.plotOptions.tooltip!==!1&&void 0!==s.plotOptions.tooltip&&(s.tooltipOptions=s.plotOptions.tooltipOpts,s.getDomElement(),t(i.getPlaceholder()).bind("plothover",o),t(n).bind("mousemove",e))}),i.hooks.shutdown.push(function(i,s){t(i.getPlaceholder()).unbind("plothover",o),t(s).unbind("mousemove",e)})},e.prototype.getDomElement=function(){var i;return t("#flotTip").length>0?i=t("#flotTip"):(i=t("<div />").attr("id","flotTip"),i.appendTo("body").hide().css({position:"absolute"}),this.tooltipOptions.defaultTheme&&i.css({background:"#fff","z-index":"1040",padding:"0.4em 0.6em","border-radius":"0.5em","font-size":"0.8em",border:"1px solid #111",display:"none","white-space":"nowrap"})),i},e.prototype.updateTooltipPosition=function(i){var e=t("#flotTip").outerWidth()+this.tooltipOptions.shifts.x,o=t("#flotTip").outerHeight()+this.tooltipOptions.shifts.y;i.x-t(window).scrollLeft()>t(window).innerWidth()-e&&(i.x-=e),i.y-t(window).scrollTop()>t(window).innerHeight()-o&&(i.y-=o),this.tipPosition.x=i.x,this.tipPosition.y=i.y},e.prototype.stringFormat=function(t,i){var e,o,s=/%p\.{0,1}(\d{0,})/,n=/%s/,r=/%lx/,a=/%ly/,p=/%x\.{0,1}(\d{0,})/,l=/%y\.{0,1}(\d{0,})/,d="%x",h="%y";if(i.series.threshold!==void 0?(e=i.datapoint[0],o=i.datapoint[1]):(e=i.series.data[i.dataIndex][0],o=i.series.data[i.dataIndex][1]),null===i.series.label&&i.series.originSeries&&(i.series.label=i.series.originSeries.label),"function"==typeof t&&(t=t(i.series.label,e,o,i)),i.series.percent!==void 0&&(t=this.adjustValPrecision(s,t,i.series.percent)),t=i.series.label!==void 0?t.replace(n,i.series.label):t.replace(n,""),t=this.hasAxisLabel("xaxis",i)?t.replace(r,i.series.xaxis.options.axisLabel):t.replace(r,""),t=this.hasAxisLabel("yaxis",i)?t.replace(a,i.series.yaxis.options.axisLabel):t.replace(a,""),this.isTimeMode("xaxis",i)&&this.isXDateFormat(i)&&(t=t.replace(p,this.timestampToDate(e,this.tooltipOptions.xDateFormat))),this.isTimeMode("yaxis",i)&&this.isYDateFormat(i)&&(t=t.replace(l,this.timestampToDate(o,this.tooltipOptions.yDateFormat))),"number"==typeof e&&(t=this.adjustValPrecision(p,t,e)),"number"==typeof o&&(t=this.adjustValPrecision(l,t,o)),i.series.xaxis.ticks!==void 0){var u;u=this.hasRotatedXAxisTicks(i)?"rotatedTicks":"ticks";var x=i.dataIndex+i.seriesIndex;i.series.xaxis[u].length>x&&!this.isTimeMode("xaxis",i)&&(t=t.replace(p,i.series.xaxis[u][x].label))}if(i.series.yaxis.ticks!==void 0)for(var c in i.series.yaxis.ticks)if(i.series.yaxis.ticks.hasOwnProperty(c)){var m=this.isCategoriesMode("yaxis",i)?i.series.yaxis.ticks[c].label:i.series.yaxis.ticks[c].v;m===o&&(t=t.replace(l,i.series.yaxis.ticks[c].label))}return i.series.xaxis.tickFormatter!==void 0&&(t=t.replace(d,i.series.xaxis.tickFormatter(e,i.series.xaxis).replace(/\$/g,"$$"))),i.series.yaxis.tickFormatter!==void 0&&(t=t.replace(h,i.series.yaxis.tickFormatter(o,i.series.yaxis).replace(/\$/g,"$$"))),t},e.prototype.isTimeMode=function(t,i){return i.series[t].options.mode!==void 0&&"time"===i.series[t].options.mode},e.prototype.isXDateFormat=function(){return this.tooltipOptions.xDateFormat!==void 0&&null!==this.tooltipOptions.xDateFormat},e.prototype.isYDateFormat=function(){return this.tooltipOptions.yDateFormat!==void 0&&null!==this.tooltipOptions.yDateFormat},e.prototype.isCategoriesMode=function(t,i){return i.series[t].options.mode!==void 0&&"categories"===i.series[t].options.mode},e.prototype.timestampToDate=function(i,e){var o=new Date(1*i);return t.plot.formatDate(o,e,this.tooltipOptions.monthNames,this.tooltipOptions.dayNames)},e.prototype.adjustValPrecision=function(t,i,e){var o,s=i.match(t);return null!==s&&""!==RegExp.$1&&(o=RegExp.$1,e=e.toFixed(o),i=i.replace(t,e)),i},e.prototype.hasAxisLabel=function(t,i){return-1!==this.plotPlugins.indexOf("axisLabels")&&i.series[t].options.axisLabel!==void 0&&i.series[t].options.axisLabel.length>0},e.prototype.hasRotatedXAxisTicks=function(i){return 1===t.grep(t.plot.plugins,function(t){return"tickRotor"===t.name}).length&&i.series.xaxis.rotatedTicks!==void 0};var o=function(t){new e(t)};t.plot.plugins.push({init:o,options:i,name:"tooltip",version:"0.6.7"})}(jQuery);
\ No newline at end of file
diff --git a/src/sunstone/views/index.erb b/src/sunstone/views/index.erb
index 315026cf98..6f084a53bf 100644
--- a/src/sunstone/views/index.erb
+++ b/src/sunstone/views/index.erb
@@ -13,6 +13,7 @@
     <script src="vendor/flot/jquery.flot.pie.min.js?v=<%= OpenNebula::VERSION %>" type="text/javascript"></script>
     <script src="vendor/flot/jquery.flot.resize.min.js?v=<%= OpenNebula::VERSION %>" type="text/javascript"></script>
     <script src="vendor/flot/jquery.flot.stack.min.js?v=<%= OpenNebula::VERSION %>" type="text/javascript"></script>
+    <script src="vendor/flot/jquery.flot.tooltip.min.js?v=<%= OpenNebula::VERSION %>" type="text/javascript"></script>
     <script src="vendor/fileuploader/fileuploader.js?v=<%= OpenNebula::VERSION %>" type="text/javascript"></script>
     <script src="vendor/4.0/nouislider/jquery.nouislider.min.js?v=<%= OpenNebula::VERSION %>" type="text/javascript"></script>
     <script src="vendor/4.0/jdpicker_1.1/jquery.jdpicker.js"?v=<%= OpenNebula::VERSION %> type="text/javascript"></script>