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){ ' }; + var accounting_tab = { + title: tr("Accounting"), + icon: "fa-bar-chart-o", + content: '
' + }; + 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: '
' + }; + 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( + '
\ +
\ + '+tr("Time range")+'\ +
\ +
\ + \ +
\ +
\ + \ +
\ +
\ +
\ +
\ + \ + \ + \ +
\ +
\ +
\ +
\ +
\ +
\ +
\ + \ + \ +
\ +
\ +
\ + \ +
\ +
\ +
\ +

'+tr("CPU hours")+'

\ +
\ +
\ +
\ +
\ +
\ +
\ +
\ +
\ +
\ +
\ +
\ +
\ +

'+tr("Memory GB hours")+'

\ +
\ +
\ +
\ +
\ +
\ +
\ +
\ +
\ +
\ +
\ +
\ +
\ +
\ +
'); + + 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, + ''); + 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 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("
").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 @@ +