From 63cd6ffcc3977d4c318567d64c1b17951335c598 Mon Sep 17 00:00:00 2001 From: Joseph Schorr Date: Wed, 28 Jan 2015 12:35:01 -0500 Subject: [PATCH 01/12] Start on adding usage charts --- endpoints/realtime.py | 33 ++++++ requirements-nover.txt | 1 + requirements.txt | 1 + static/directives/ps-usage-graph.html | 24 ++++ static/directives/realtime-line-chart.html | 3 + static/js/app.js | 123 +++++++++++++++++++++ static/lib/rickshaw.min.css | 1 + static/lib/rickshaw.min.js | 3 + static/partials/super-user.html | 3 + 9 files changed, 192 insertions(+) create mode 100644 static/directives/ps-usage-graph.html create mode 100644 static/directives/realtime-line-chart.html create mode 100644 static/lib/rickshaw.min.css create mode 100644 static/lib/rickshaw.min.js diff --git a/endpoints/realtime.py b/endpoints/realtime.py index 9f1d5c44f..cfa3ec7ad 100644 --- a/endpoints/realtime.py +++ b/endpoints/realtime.py @@ -4,13 +4,46 @@ import json from flask import request, Blueprint, abort, Response from flask.ext.login import current_user from auth.auth import require_session_login +from endpoints.common import route_show_if from app import userevents +from auth.permissions import SuperUserPermission + +import features +import psutil +import time logger = logging.getLogger(__name__) realtime = Blueprint('realtime', __name__) +@realtime.route("/ps") +@route_show_if(features.SUPER_USERS) +@require_session_login +def ps(): + if not SuperUserPermission().can(): + abort(403) + + def generator(): + while True: + data = { + 'count': { + 'cpu': psutil.cpu_percent(interval=1, percpu=True), + 'virtual_mem': psutil.virtual_memory(), + 'swap_mem': psutil.swap_memory(), + 'connections': len(psutil.net_connections()), + 'processes': len(psutil.pids()), + 'network': psutil.net_io_counters() + } + } + json_string = json.dumps(data) + yield 'data: %s\n\n' % json_string + time.sleep(0.25) + + return Response(generator(), mimetype="text/event-stream") + + + @realtime.route("/user/") @require_session_login def index(): diff --git a/requirements-nover.txt b/requirements-nover.txt index d4d21f0f5..a1b9723e7 100644 --- a/requirements-nover.txt +++ b/requirements-nover.txt @@ -41,3 +41,4 @@ git+https://github.com/DevTable/anunidecode.git git+https://github.com/DevTable/avatar-generator.git git+https://github.com/DevTable/pygithub.git gipc +psutil diff --git a/requirements.txt b/requirements.txt index 8fc83d033..542e48a6e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -37,6 +37,7 @@ marisa-trie==0.7 mixpanel-py==3.2.1 paramiko==1.15.2 peewee==2.4.5 +psutil==2.2.0 psycopg2==2.5.4 py-bcrypt==0.4 pycrypto==2.6.1 diff --git a/static/directives/ps-usage-graph.html b/static/directives/ps-usage-graph.html new file mode 100644 index 000000000..ddba677d0 --- /dev/null +++ b/static/directives/ps-usage-graph.html @@ -0,0 +1,24 @@ +
+ CPU: +
+ + Process Count: +
+ + Virtual Memory: +
+ + Swap Memory: +
+ + Network Connections: +
+ + Network Usage: +
+
\ No newline at end of file diff --git a/static/directives/realtime-line-chart.html b/static/directives/realtime-line-chart.html new file mode 100644 index 000000000..a99ba56c4 --- /dev/null +++ b/static/directives/realtime-line-chart.html @@ -0,0 +1,3 @@ +
+
+
\ No newline at end of file diff --git a/static/js/app.js b/static/js/app.js index b41e80b10..7c5fd8d1e 100644 --- a/static/js/app.js +++ b/static/js/app.js @@ -6522,6 +6522,129 @@ quayApp.directive('locationView', function () { }); +quayApp.directive('realtimeLineChart', function () { + var directiveDefinitionObject = { + priority: 0, + templateUrl: '/static/directives/realtime-line-chart.html', + replace: false, + transclude: false, + restrict: 'C', + scope: { + 'data': '=data', + 'labels': '=labels', + 'counter': '=counter', + 'labelTemplate': '@labelTemplate' + }, + controller: function($scope, $element) { + var graph = null; + var hoverDetail = null; + var series = []; + var counter = 0; + var palette = new Rickshaw.Color.Palette( { scheme: 'spectrum14' } ); + + var setupGraph = function() { + graph = new Rickshaw.Graph({ + element: $element.find('.chart')[0], + renderer: 'line', + series: series, + min: 'auto', + padding: { + 'top': 0.1, + 'left': 0.01, + 'right': 0.01, + 'bottom': 0.1 + } + }); + + hoverDetail = new Rickshaw.Graph.HoverDetail({ + graph: graph, + xFormatter: function(x) { + return x.toString(); + } + }); + }; + + var refresh = function(data) { + if (!data) { return; } + if (!graph) { + setupGraph(); + } + + if (typeof data == 'number') { + data = [data]; + } + + if ($scope.labels) { + data = data.slice(0, $scope.labels.length); + } + + if (series.length == 0){ + for (var i = 0; i < data.length; ++i) { + var title = $scope.labels ? $scope.labels[i] : $scope.labelTemplate.replace('{x}', i + 1); + series.push({ + 'color': palette.color(), + 'data': [], + 'name': title + }) + } + } + + counter++; + + for (var i = 0; i < data.length; ++i) { + var arr = series[i].data; + arr.push({ + 'x': counter, + 'y': data[i] + }) + + if (arr.length > 10) { + series[i].data = arr.slice(arr.length - 10, arr.length); + } + } + + graph.render(); + }; + + $scope.$watch('counter', function(counter) { + refresh($scope.data_raw); + }); + + $scope.$watch('data', function(data) { + $scope.data_raw = data; + }); + } + }; + return directiveDefinitionObject; +}); + + +quayApp.directive('psUsageGraph', function () { + var directiveDefinitionObject = { + priority: 0, + templateUrl: '/static/directives/ps-usage-graph.html', + replace: false, + transclude: false, + restrict: 'C', + scope: { + }, + controller: function($scope, $element) { + $scope.counter = -1; + $scope.data = null; + + var source = new EventSource('/realtime/ps'); + source.onmessage = function(e) { + $scope.$apply(function() { + $scope.counter++; + $scope.data = JSON.parse(e.data); + }); + }; + } + }; + return directiveDefinitionObject; +}); + + quayApp.directive('avatar', function () { var directiveDefinitionObject = { priority: 0, diff --git a/static/lib/rickshaw.min.css b/static/lib/rickshaw.min.css new file mode 100644 index 000000000..d1b32d8eb --- /dev/null +++ b/static/lib/rickshaw.min.css @@ -0,0 +1 @@ +.rickshaw_graph .detail{pointer-events:none;position:absolute;top:0;z-index:2;background:rgba(0,0,0,.1);bottom:0;width:1px;transition:opacity .25s linear;-moz-transition:opacity .25s linear;-o-transition:opacity .25s linear;-webkit-transition:opacity .25s linear}.rickshaw_graph .detail.inactive{opacity:0}.rickshaw_graph .detail .item.active{opacity:1}.rickshaw_graph .detail .x_label{font-family:Arial,sans-serif;border-radius:3px;padding:6px;opacity:.5;border:1px solid #e0e0e0;font-size:12px;position:absolute;background:#fff;white-space:nowrap}.rickshaw_graph .detail .x_label.left{left:0}.rickshaw_graph .detail .x_label.right{right:0}.rickshaw_graph .detail .item{position:absolute;z-index:2;border-radius:3px;padding:.25em;font-size:12px;font-family:Arial,sans-serif;opacity:0;background:rgba(0,0,0,.4);color:#fff;border:1px solid rgba(0,0,0,.4);margin-left:1em;margin-right:1em;margin-top:-1em;white-space:nowrap}.rickshaw_graph .detail .item.left{left:0}.rickshaw_graph .detail .item.right{right:0}.rickshaw_graph .detail .item.active{opacity:1;background:rgba(0,0,0,.8)}.rickshaw_graph .detail .item:after{position:absolute;display:block;width:0;height:0;content:"";border:5px solid transparent}.rickshaw_graph .detail .item.left:after{top:1em;left:-5px;margin-top:-5px;border-right-color:rgba(0,0,0,.8);border-left-width:0}.rickshaw_graph .detail .item.right:after{top:1em;right:-5px;margin-top:-5px;border-left-color:rgba(0,0,0,.8);border-right-width:0}.rickshaw_graph .detail .dot{width:4px;height:4px;margin-left:-3px;margin-top:-3.5px;border-radius:5px;position:absolute;box-shadow:0 0 2px rgba(0,0,0,.6);box-sizing:content-box;-moz-box-sizing:content-box;background:#fff;border-width:2px;border-style:solid;display:none;background-clip:padding-box}.rickshaw_graph .detail .dot.active{display:block}.rickshaw_graph{position:relative}.rickshaw_graph svg{display:block;overflow:hidden}.rickshaw_graph .x_tick{position:absolute;top:0;bottom:0;width:0;border-left:1px dotted rgba(0,0,0,.2);pointer-events:none}.rickshaw_graph .x_tick .title{position:absolute;font-size:12px;font-family:Arial,sans-serif;opacity:.5;white-space:nowrap;margin-left:3px;bottom:1px}.rickshaw_annotation_timeline{height:1px;border-top:1px solid #e0e0e0;margin-top:10px;position:relative}.rickshaw_annotation_timeline .annotation{position:absolute;height:6px;width:6px;margin-left:-2px;top:-3px;border-radius:5px;background-color:rgba(0,0,0,.25)}.rickshaw_graph .annotation_line{position:absolute;top:0;bottom:-6px;width:0;border-left:2px solid rgba(0,0,0,.3);display:none}.rickshaw_graph .annotation_line.active{display:block}.rickshaw_graph .annotation_range{background:rgba(0,0,0,.1);display:none;position:absolute;top:0;bottom:-6px}.rickshaw_graph .annotation_range.active{display:block}.rickshaw_graph .annotation_range.active.offscreen{display:none}.rickshaw_annotation_timeline .annotation .content{background:#fff;color:#000;opacity:.9;padding:5px;box-shadow:0 0 2px rgba(0,0,0,.8);border-radius:3px;position:relative;z-index:20;font-size:12px;padding:6px 8px 8px;top:18px;left:-11px;width:160px;display:none;cursor:pointer}.rickshaw_annotation_timeline .annotation .content:before{content:"\25b2";position:absolute;top:-11px;color:#fff;text-shadow:0 -1px 1px rgba(0,0,0,.8)}.rickshaw_annotation_timeline .annotation.active,.rickshaw_annotation_timeline .annotation:hover{background-color:rgba(0,0,0,.8);cursor:none}.rickshaw_annotation_timeline .annotation .content:hover{z-index:50}.rickshaw_annotation_timeline .annotation.active .content{display:block}.rickshaw_annotation_timeline .annotation:hover .content{display:block;z-index:50}.rickshaw_graph .y_axis,.rickshaw_graph .x_axis_d3{fill:none}.rickshaw_graph .y_ticks .tick line,.rickshaw_graph .x_ticks_d3 .tick{stroke:rgba(0,0,0,.16);stroke-width:2px;shape-rendering:crisp-edges;pointer-events:none}.rickshaw_graph .y_grid .tick,.rickshaw_graph .x_grid_d3 .tick{z-index:-1;stroke:rgba(0,0,0,.2);stroke-width:1px;stroke-dasharray:1 1}.rickshaw_graph .y_grid .tick[data-y-value="0"]{stroke-dasharray:1 0}.rickshaw_graph .y_grid path,.rickshaw_graph .x_grid_d3 path{fill:none;stroke:none}.rickshaw_graph .y_ticks path,.rickshaw_graph .x_ticks_d3 path{fill:none;stroke:gray}.rickshaw_graph .y_ticks text,.rickshaw_graph .x_ticks_d3 text{opacity:.5;font-size:12px;pointer-events:none}.rickshaw_graph .x_tick.glow .title,.rickshaw_graph .y_ticks.glow text{fill:#000;color:#000;text-shadow:-1px 1px 0 rgba(255,255,255,.1),1px -1px 0 rgba(255,255,255,.1),1px 1px 0 rgba(255,255,255,.1),0 1px 0 rgba(255,255,255,.1),0 -1px 0 rgba(255,255,255,.1),1px 0 0 rgba(255,255,255,.1),-1px 0 0 rgba(255,255,255,.1),-1px -1px 0 rgba(255,255,255,.1)}.rickshaw_graph .x_tick.inverse .title,.rickshaw_graph .y_ticks.inverse text{fill:#fff;color:#fff;text-shadow:-1px 1px 0 rgba(0,0,0,.8),1px -1px 0 rgba(0,0,0,.8),1px 1px 0 rgba(0,0,0,.8),0 1px 0 rgba(0,0,0,.8),0 -1px 0 rgba(0,0,0,.8),1px 0 0 rgba(0,0,0,.8),-1px 0 0 rgba(0,0,0,.8),-1px -1px 0 rgba(0,0,0,.8)}.rickshaw_legend{font-family:Arial;font-size:12px;color:#fff;background:#404040;display:inline-block;padding:12px 5px;border-radius:2px;position:relative}.rickshaw_legend:hover{z-index:10}.rickshaw_legend .swatch{width:10px;height:10px;border:1px solid rgba(0,0,0,.2)}.rickshaw_legend .line{clear:both;line-height:140%;padding-right:15px}.rickshaw_legend .line .swatch{display:inline-block;margin-right:3px;border-radius:2px}.rickshaw_legend .label{margin:0;white-space:nowrap;display:inline;font-size:inherit;background-color:transparent;color:inherit;font-weight:400;line-height:normal;padding:0;text-shadow:none}.rickshaw_legend .action:hover{opacity:.6}.rickshaw_legend .action{margin-right:.2em;font-size:10px;opacity:.2;cursor:pointer;font-size:14px}.rickshaw_legend .line.disabled{opacity:.4}.rickshaw_legend ul{list-style-type:none;margin:0;padding:0;margin:2px;cursor:pointer}.rickshaw_legend li{padding:0 0 0 2px;min-width:80px;white-space:nowrap}.rickshaw_legend li:hover{background:rgba(255,255,255,.08);border-radius:3px}.rickshaw_legend li:active{background:rgba(255,255,255,.2);border-radius:3px} \ No newline at end of file diff --git a/static/lib/rickshaw.min.js b/static/lib/rickshaw.min.js new file mode 100644 index 000000000..bd5769912 --- /dev/null +++ b/static/lib/rickshaw.min.js @@ -0,0 +1,3 @@ +(function(root,factory){if(typeof define==="function"&&define.amd){define(["d3"],function(d3){return root.Rickshaw=factory(d3)})}else if(typeof exports==="object"){module.exports=factory(require("d3"))}else{root.Rickshaw=factory(d3)}})(this,function(d3){var Rickshaw={namespace:function(namespace,obj){var parts=namespace.split(".");var parent=Rickshaw;for(var i=1,length=parts.length;i0){var x=s.data[0].x;var y=s.data[0].y;if(typeof x!="number"||typeof y!="number"&&y!==null){throw"x and y properties of points should be numbers instead of "+typeof x+" and "+typeof y}}if(s.data.length>=3){if(s.data[2].xthis.window.xMax)isInRange=false;return isInRange}return true};this.onUpdate=function(callback){this.updateCallbacks.push(callback)};this.onConfigure=function(callback){this.configureCallbacks.push(callback)};this.registerRenderer=function(renderer){this._renderers=this._renderers||{};this._renderers[renderer.name]=renderer};this.configure=function(args){this.config=this.config||{};if(args.width||args.height){this.setSize(args)}Rickshaw.keys(this.defaults).forEach(function(k){this.config[k]=k in args?args[k]:k in this?this[k]:this.defaults[k]},this);Rickshaw.keys(this.config).forEach(function(k){this[k]=this.config[k]},this);if("stack"in args)args.unstack=!args.stack;var renderer=args.renderer||this.renderer&&this.renderer.name||"stack";this.setRenderer(renderer,args);this.configureCallbacks.forEach(function(callback){callback(args)})};this.setRenderer=function(r,args){if(typeof r=="function"){this.renderer=new r({graph:self});this.registerRenderer(this.renderer)}else{if(!this._renderers[r]){throw"couldn't find renderer "+r}this.renderer=this._renderers[r]}if(typeof args=="object"){this.renderer.configure(args)}};this.setSize=function(args){args=args||{};if(typeof window!==undefined){var style=window.getComputedStyle(this.element,null);var elementWidth=parseInt(style.getPropertyValue("width"),10);var elementHeight=parseInt(style.getPropertyValue("height"),10)}this.width=args.width||elementWidth||400;this.height=args.height||elementHeight||250;this.vis&&this.vis.attr("width",this.width).attr("height",this.height)};this.initialize(args)};Rickshaw.namespace("Rickshaw.Fixtures.Color");Rickshaw.Fixtures.Color=function(){this.schemes={};this.schemes.spectrum14=["#ecb796","#dc8f70","#b2a470","#92875a","#716c49","#d2ed82","#bbe468","#a1d05d","#e7cbe6","#d8aad6","#a888c2","#9dc2d3","#649eb9","#387aa3"].reverse();this.schemes.spectrum2000=["#57306f","#514c76","#646583","#738394","#6b9c7d","#84b665","#a7ca50","#bfe746","#e2f528","#fff726","#ecdd00","#d4b11d","#de8800","#de4800","#c91515","#9a0000","#7b0429","#580839","#31082b"];this.schemes.spectrum2001=["#2f243f","#3c2c55","#4a3768","#565270","#6b6b7c","#72957f","#86ad6e","#a1bc5e","#b8d954","#d3e04e","#ccad2a","#cc8412","#c1521d","#ad3821","#8a1010","#681717","#531e1e","#3d1818","#320a1b"];this.schemes.classic9=["#423d4f","#4a6860","#848f39","#a2b73c","#ddcb53","#c5a32f","#7d5836","#963b20","#7c2626","#491d37","#2f254a"].reverse();this.schemes.httpStatus={503:"#ea5029",502:"#d23f14",500:"#bf3613",410:"#efacea",409:"#e291dc",403:"#f457e8",408:"#e121d2",401:"#b92dae",405:"#f47ceb",404:"#a82a9f",400:"#b263c6",301:"#6fa024",302:"#87c32b",307:"#a0d84c",304:"#28b55c",200:"#1a4f74",206:"#27839f",201:"#52adc9",202:"#7c979f",203:"#a5b8bd",204:"#c1cdd1"};this.schemes.colorwheel=["#b5b6a9","#858772","#785f43","#96557e","#4682b4","#65b9ac","#73c03a","#cb513a"].reverse();this.schemes.cool=["#5e9d2f","#73c03a","#4682b4","#7bc3b8","#a9884e","#c1b266","#a47493","#c09fb5"];this.schemes.munin=["#00cc00","#0066b3","#ff8000","#ffcc00","#330099","#990099","#ccff00","#ff0000","#808080","#008f00","#00487d","#b35a00","#b38f00","#6b006b","#8fb300","#b30000","#bebebe","#80ff80","#80c9ff","#ffc080","#ffe680","#aa80ff","#ee00cc","#ff8080","#666600","#ffbfff","#00ffcc","#cc6699","#999900"]};Rickshaw.namespace("Rickshaw.Fixtures.RandomData");Rickshaw.Fixtures.RandomData=function(timeInterval){var addData;timeInterval=timeInterval||1;var lastRandomValue=200;var timeBase=Math.floor((new Date).getTime()/1e3);this.addData=function(data){var randomValue=Math.random()*100+15+lastRandomValue;var index=data[0].length;var counter=1;data.forEach(function(series){var randomVariance=Math.random()*20;var v=randomValue/25+counter++ +(Math.cos(index*counter*11/960)+2)*15+(Math.cos(index/7)+2)*7+(Math.cos(index/17)+2)*1;series.push({x:index*timeInterval+timeBase,y:v+randomVariance})});lastRandomValue=randomValue*.85};this.removeData=function(data){data.forEach(function(series){series.shift()});timeBase+=timeInterval}};Rickshaw.namespace("Rickshaw.Fixtures.Time");Rickshaw.Fixtures.Time=function(){var self=this;this.months=["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"];this.units=[{name:"decade",seconds:86400*365.25*10,formatter:function(d){return parseInt(d.getUTCFullYear()/10,10)*10}},{name:"year",seconds:86400*365.25,formatter:function(d){return d.getUTCFullYear()}},{name:"month",seconds:86400*30.5,formatter:function(d){return self.months[d.getUTCMonth()]}},{name:"week",seconds:86400*7,formatter:function(d){return self.formatDate(d)}},{name:"day",seconds:86400,formatter:function(d){return d.getUTCDate()}},{name:"6 hour",seconds:3600*6,formatter:function(d){return self.formatTime(d)}},{name:"hour",seconds:3600,formatter:function(d){return self.formatTime(d)}},{name:"15 minute",seconds:60*15,formatter:function(d){return self.formatTime(d)}},{name:"minute",seconds:60,formatter:function(d){return d.getUTCMinutes()}},{name:"15 second",seconds:15,formatter:function(d){return d.getUTCSeconds()+"s"}},{name:"second",seconds:1,formatter:function(d){return d.getUTCSeconds()+"s"}},{name:"decisecond",seconds:1/10,formatter:function(d){return d.getUTCMilliseconds()+"ms"}},{name:"centisecond",seconds:1/100,formatter:function(d){return d.getUTCMilliseconds()+"ms"}}];this.unit=function(unitName){return this.units.filter(function(unit){return unitName==unit.name}).shift()};this.formatDate=function(d){return d3.time.format("%b %e")(d)};this.formatTime=function(d){return d.toUTCString().match(/(\d+:\d+):/)[1]};this.ceil=function(time,unit){var date,floor,year;if(unit.name=="month"){date=new Date(time*1e3);floor=Date.UTC(date.getUTCFullYear(),date.getUTCMonth())/1e3;if(floor==time)return time;year=date.getUTCFullYear();var month=date.getUTCMonth();if(month==11){month=0;year=year+1}else{month+=1}return Date.UTC(year,month)/1e3}if(unit.name=="year"){date=new Date(time*1e3);floor=Date.UTC(date.getUTCFullYear(),0)/1e3;if(floor==time)return time;year=date.getUTCFullYear()+1;return Date.UTC(year,0)/1e3}return Math.ceil(time/unit.seconds)*unit.seconds}};Rickshaw.namespace("Rickshaw.Fixtures.Time.Local");Rickshaw.Fixtures.Time.Local=function(){var self=this;this.months=["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"];this.units=[{name:"decade",seconds:86400*365.25*10,formatter:function(d){return parseInt(d.getFullYear()/10,10)*10}},{name:"year",seconds:86400*365.25,formatter:function(d){return d.getFullYear()}},{name:"month",seconds:86400*30.5,formatter:function(d){return self.months[d.getMonth()]}},{name:"week",seconds:86400*7,formatter:function(d){return self.formatDate(d)}},{name:"day",seconds:86400,formatter:function(d){return d.getDate()}},{name:"6 hour",seconds:3600*6,formatter:function(d){return self.formatTime(d)}},{name:"hour",seconds:3600,formatter:function(d){return self.formatTime(d)}},{name:"15 minute",seconds:60*15,formatter:function(d){return self.formatTime(d)}},{name:"minute",seconds:60,formatter:function(d){return d.getMinutes()}},{name:"15 second",seconds:15,formatter:function(d){return d.getSeconds()+"s"}},{name:"second",seconds:1,formatter:function(d){return d.getSeconds()+"s"}},{name:"decisecond",seconds:1/10,formatter:function(d){return d.getMilliseconds()+"ms"}},{name:"centisecond",seconds:1/100,formatter:function(d){return d.getMilliseconds()+"ms"}}];this.unit=function(unitName){return this.units.filter(function(unit){return unitName==unit.name}).shift()};this.formatDate=function(d){return d3.time.format("%b %e")(d)};this.formatTime=function(d){return d.toString().match(/(\d+:\d+):/)[1]};this.ceil=function(time,unit){var date,floor,year;if(unit.name=="day"){var nearFuture=new Date((time+unit.seconds-1)*1e3);var rounded=new Date(0);rounded.setMilliseconds(0);rounded.setSeconds(0);rounded.setMinutes(0);rounded.setHours(0);rounded.setDate(nearFuture.getDate());rounded.setMonth(nearFuture.getMonth());rounded.setFullYear(nearFuture.getFullYear());return rounded.getTime()/1e3}if(unit.name=="month"){date=new Date(time*1e3);floor=new Date(date.getFullYear(),date.getMonth()).getTime()/1e3;if(floor==time)return time;year=date.getFullYear();var month=date.getMonth();if(month==11){month=0;year=year+1}else{month+=1}return new Date(year,month).getTime()/1e3}if(unit.name=="year"){date=new Date(time*1e3);floor=new Date(date.getUTCFullYear(),0).getTime()/1e3;if(floor==time)return time;year=date.getFullYear()+1;return new Date(year,0).getTime()/1e3}return Math.ceil(time/unit.seconds)*unit.seconds}};Rickshaw.namespace("Rickshaw.Fixtures.Number");Rickshaw.Fixtures.Number.formatKMBT=function(y){var abs_y=Math.abs(y);if(abs_y>=1e12){return y/1e12+"T"}else if(abs_y>=1e9){return y/1e9+"B"}else if(abs_y>=1e6){return y/1e6+"M"}else if(abs_y>=1e3){return y/1e3+"K"}else if(abs_y<1&&y>0){return y.toFixed(2)}else if(abs_y===0){return""}else{return y}};Rickshaw.Fixtures.Number.formatBase1024KMGTP=function(y){var abs_y=Math.abs(y);if(abs_y>=0x4000000000000){return y/0x4000000000000+"P"}else if(abs_y>=1099511627776){return y/1099511627776+"T"}else if(abs_y>=1073741824){return y/1073741824+"G"}else if(abs_y>=1048576){return y/1048576+"M"}else if(abs_y>=1024){return y/1024+"K"}else if(abs_y<1&&y>0){return y.toFixed(2)}else if(abs_y===0){return""}else{return y}};Rickshaw.namespace("Rickshaw.Color.Palette");Rickshaw.Color.Palette=function(args){var color=new Rickshaw.Fixtures.Color;args=args||{};this.schemes={};this.scheme=color.schemes[args.scheme]||args.scheme||color.schemes.colorwheel;this.runningIndex=0;this.generatorIndex=0;if(args.interpolatedStopCount){var schemeCount=this.scheme.length-1;var i,j,scheme=[];for(i=0;iself.graph.x.range()[1]){if(annotation.element){annotation.line.classList.add("offscreen");annotation.element.style.display="none"}annotation.boxes.forEach(function(box){if(box.rangeElement)box.rangeElement.classList.add("offscreen")});return}if(!annotation.element){var element=annotation.element=document.createElement("div");element.classList.add("annotation");this.elements.timeline.appendChild(element);element.addEventListener("click",function(e){element.classList.toggle("active");annotation.line.classList.toggle("active");annotation.boxes.forEach(function(box){if(box.rangeElement)box.rangeElement.classList.toggle("active")})},false)}annotation.element.style.left=left+"px";annotation.element.style.display="block";annotation.boxes.forEach(function(box){var element=box.element;if(!element){element=box.element=document.createElement("div");element.classList.add("content");element.innerHTML=box.content;annotation.element.appendChild(element);annotation.line=document.createElement("div");annotation.line.classList.add("annotation_line");self.graph.element.appendChild(annotation.line);if(box.end){box.rangeElement=document.createElement("div");box.rangeElement.classList.add("annotation_range");self.graph.element.appendChild(box.rangeElement)}}if(box.end){var annotationRangeStart=left;var annotationRangeEnd=Math.min(self.graph.x(box.end),self.graph.x.range()[1]);if(annotationRangeStart>annotationRangeEnd){annotationRangeEnd=left;annotationRangeStart=Math.max(self.graph.x(box.end),self.graph.x.range()[0])}var annotationRangeWidth=annotationRangeEnd-annotationRangeStart;box.rangeElement.style.left=annotationRangeStart+"px";box.rangeElement.style.width=annotationRangeWidth+"px";box.rangeElement.classList.remove("offscreen")}annotation.line.classList.remove("offscreen");annotation.line.style.left=left+"px"})},this)};this.graph.onUpdate(function(){self.update()})};Rickshaw.namespace("Rickshaw.Graph.Axis.Time");Rickshaw.Graph.Axis.Time=function(args){var self=this;this.graph=args.graph;this.elements=[];this.ticksTreatment=args.ticksTreatment||"plain";this.fixedTimeUnit=args.timeUnit;var time=args.timeFixture||new Rickshaw.Fixtures.Time;this.appropriateTimeUnit=function(){var unit;var units=time.units;var domain=this.graph.x.domain();var rangeSeconds=domain[1]-domain[0];units.forEach(function(u){if(Math.floor(rangeSeconds/u.seconds)>=2){unit=unit||u}});return unit||time.units[time.units.length-1]};this.tickOffsets=function(){var domain=this.graph.x.domain();var unit=this.fixedTimeUnit||this.appropriateTimeUnit();var count=Math.ceil((domain[1]-domain[0])/unit.seconds);var runningTick=domain[0];var offsets=[];for(var i=0;iself.graph.x.range()[1])return;var element=document.createElement("div");element.style.left=self.graph.x(o.value)+"px";element.classList.add("x_tick");element.classList.add(self.ticksTreatment);var title=document.createElement("div");title.classList.add("title");title.innerHTML=o.unit.formatter(new Date(o.value*1e3));element.appendChild(title);self.graph.element.appendChild(element);self.elements.push(element)})};this.graph.onUpdate(function(){self.render()})};Rickshaw.namespace("Rickshaw.Graph.Axis.X");Rickshaw.Graph.Axis.X=function(args){var self=this;var berthRate=.1;this.initialize=function(args){this.graph=args.graph;this.orientation=args.orientation||"top";this.pixelsPerTick=args.pixelsPerTick||75;if(args.ticks)this.staticTicks=args.ticks;if(args.tickValues)this.tickValues=args.tickValues;this.tickSize=args.tickSize||4;this.ticksTreatment=args.ticksTreatment||"plain";if(args.element){this.element=args.element;this._discoverSize(args.element,args);this.vis=d3.select(args.element).append("svg:svg").attr("height",this.height).attr("width",this.width).attr("class","rickshaw_graph x_axis_d3");this.element=this.vis[0][0];this.element.style.position="relative";this.setSize({width:args.width,height:args.height})}else{this.vis=this.graph.vis}this.graph.onUpdate(function(){self.render()})};this.setSize=function(args){args=args||{};if(!this.element)return;this._discoverSize(this.element.parentNode,args);this.vis.attr("height",this.height).attr("width",this.width*(1+berthRate));var berth=Math.floor(this.width*berthRate/2);this.element.style.left=-1*berth+"px"};this.render=function(){if(this._renderWidth!==undefined&&this.graph.width!==this._renderWidth)this.setSize({auto:true});var axis=d3.svg.axis().scale(this.graph.x).orient(this.orientation);axis.tickFormat(args.tickFormat||function(x){return x});if(this.tickValues)axis.tickValues(this.tickValues);this.ticks=this.staticTicks||Math.floor(this.graph.width/this.pixelsPerTick);var berth=Math.floor(this.width*berthRate/2)||0;var transform;if(this.orientation=="top"){var yOffset=this.height||this.graph.height;transform="translate("+berth+","+yOffset+")"}else{transform="translate("+berth+", 0)"}if(this.element){this.vis.selectAll("*").remove()}this.vis.append("svg:g").attr("class",["x_ticks_d3",this.ticksTreatment].join(" ")).attr("transform",transform).call(axis.ticks(this.ticks).tickSubdivide(0).tickSize(this.tickSize));var gridSize=(this.orientation=="bottom"?1:-1)*this.graph.height;this.graph.vis.append("svg:g").attr("class","x_grid_d3").call(axis.ticks(this.ticks).tickSubdivide(0).tickSize(gridSize)).selectAll("text").each(function(){this.parentNode.setAttribute("data-x-value",this.textContent)});this._renderHeight=this.graph.height};this._discoverSize=function(element,args){if(typeof window!=="undefined"){var style=window.getComputedStyle(element,null);var elementHeight=parseInt(style.getPropertyValue("height"),10);if(!args.auto){var elementWidth=parseInt(style.getPropertyValue("width"),10)}}this.width=(args.width||elementWidth||this.graph.width)*(1+berthRate);this.height=args.height||elementHeight||40};this.initialize(args)};Rickshaw.namespace("Rickshaw.Graph.Axis.Y");Rickshaw.Graph.Axis.Y=Rickshaw.Class.create({initialize:function(args){this.graph=args.graph;this.orientation=args.orientation||"right";this.pixelsPerTick=args.pixelsPerTick||75;if(args.ticks)this.staticTicks=args.ticks;if(args.tickValues)this.tickValues=args.tickValues;this.tickSize=args.tickSize||4;this.ticksTreatment=args.ticksTreatment||"plain";this.tickFormat=args.tickFormat||function(y){return y};this.berthRate=.1;if(args.element){this.element=args.element;this.vis=d3.select(args.element).append("svg:svg").attr("class","rickshaw_graph y_axis");this.element=this.vis[0][0];this.element.style.position="relative";this.setSize({width:args.width,height:args.height})}else{this.vis=this.graph.vis}var self=this;this.graph.onUpdate(function(){self.render()})},setSize:function(args){args=args||{};if(!this.element)return;if(typeof window!=="undefined"){var style=window.getComputedStyle(this.element.parentNode,null);var elementWidth=parseInt(style.getPropertyValue("width"),10);if(!args.auto){var elementHeight=parseInt(style.getPropertyValue("height"),10)}}this.width=args.width||elementWidth||this.graph.width*this.berthRate;this.height=args.height||elementHeight||this.graph.height;this.vis.attr("width",this.width).attr("height",this.height*(1+this.berthRate));var berth=this.height*this.berthRate;if(this.orientation=="left"){this.element.style.top=-1*berth+"px"}},render:function(){if(this._renderHeight!==undefined&&this.graph.height!==this._renderHeight)this.setSize({auto:true});this.ticks=this.staticTicks||Math.floor(this.graph.height/this.pixelsPerTick);var axis=this._drawAxis(this.graph.y);this._drawGrid(axis);this._renderHeight=this.graph.height},_drawAxis:function(scale){var axis=d3.svg.axis().scale(scale).orient(this.orientation);axis.tickFormat(this.tickFormat);if(this.tickValues)axis.tickValues(this.tickValues);if(this.orientation=="left"){var berth=this.height*this.berthRate;var transform="translate("+this.width+", "+berth+")"}if(this.element){this.vis.selectAll("*").remove()}this.vis.append("svg:g").attr("class",["y_ticks",this.ticksTreatment].join(" ")).attr("transform",transform).call(axis.ticks(this.ticks).tickSubdivide(0).tickSize(this.tickSize));return axis},_drawGrid:function(axis){var gridSize=(this.orientation=="right"?1:-1)*this.graph.width;this.graph.vis.append("svg:g").attr("class","y_grid").call(axis.ticks(this.ticks).tickSubdivide(0).tickSize(gridSize)).selectAll("text").each(function(){this.parentNode.setAttribute("data-y-value",this.textContent) +})}});Rickshaw.namespace("Rickshaw.Graph.Axis.Y.Scaled");Rickshaw.Graph.Axis.Y.Scaled=Rickshaw.Class.create(Rickshaw.Graph.Axis.Y,{initialize:function($super,args){if(typeof args.scale==="undefined"){throw new Error("Scaled requires scale")}this.scale=args.scale;if(typeof args.grid==="undefined"){this.grid=true}else{this.grid=args.grid}$super(args)},_drawAxis:function($super,scale){var domain=this.scale.domain();var renderDomain=this.graph.renderer.domain().y;var extents=[Math.min.apply(Math,domain),Math.max.apply(Math,domain)];var extentMap=d3.scale.linear().domain([0,1]).range(extents);var adjExtents=[extentMap(renderDomain[0]),extentMap(renderDomain[1])];var adjustment=d3.scale.linear().domain(extents).range(adjExtents);var adjustedScale=this.scale.copy().domain(domain.map(adjustment)).range(scale.range());return $super(adjustedScale)},_drawGrid:function($super,axis){if(this.grid){$super(axis)}}});Rickshaw.namespace("Rickshaw.Graph.Behavior.Series.Highlight");Rickshaw.Graph.Behavior.Series.Highlight=function(args){this.graph=args.graph;this.legend=args.legend;var self=this;var colorSafe={};var activeLine=null;var disabledColor=args.disabledColor||function(seriesColor){return d3.interpolateRgb(seriesColor,d3.rgb("#d8d8d8"))(.8).toString()};this.addHighlightEvents=function(l){l.element.addEventListener("mouseover",function(e){if(activeLine)return;else activeLine=l;self.legend.lines.forEach(function(line){if(l===line){if(self.graph.renderer.unstack&&(line.series.renderer?line.series.renderer.unstack:true)){var seriesIndex=self.graph.series.indexOf(line.series);line.originalIndex=seriesIndex;var series=self.graph.series.splice(seriesIndex,1)[0];self.graph.series.push(series)}return}colorSafe[line.series.name]=colorSafe[line.series.name]||line.series.color;line.series.color=disabledColor(line.series.color)});self.graph.update()},false);l.element.addEventListener("mouseout",function(e){if(!activeLine)return;else activeLine=null;self.legend.lines.forEach(function(line){if(l===line&&line.hasOwnProperty("originalIndex")){var series=self.graph.series.pop();self.graph.series.splice(line.originalIndex,0,series);delete line.originalIndex}if(colorSafe[line.series.name]){line.series.color=colorSafe[line.series.name]}});self.graph.update()},false)};if(this.legend){this.legend.lines.forEach(function(l){self.addHighlightEvents(l)})}};Rickshaw.namespace("Rickshaw.Graph.Behavior.Series.Order");Rickshaw.Graph.Behavior.Series.Order=function(args){this.graph=args.graph;this.legend=args.legend;var self=this;if(typeof window.jQuery=="undefined"){throw"couldn't find jQuery at window.jQuery"}if(typeof window.jQuery.ui=="undefined"){throw"couldn't find jQuery UI at window.jQuery.ui"}jQuery(function(){jQuery(self.legend.list).sortable({containment:"parent",tolerance:"pointer",update:function(event,ui){var series=[];jQuery(self.legend.list).find("li").each(function(index,item){if(!item.series)return;series.push(item.series)});for(var i=self.graph.series.length-1;i>=0;i--){self.graph.series[i]=series.shift()}self.graph.update()}});jQuery(self.legend.list).disableSelection()});this.graph.onUpdate(function(){var h=window.getComputedStyle(self.legend.element).height;self.legend.element.style.height=h})};Rickshaw.namespace("Rickshaw.Graph.Behavior.Series.Toggle");Rickshaw.Graph.Behavior.Series.Toggle=function(args){this.graph=args.graph;this.legend=args.legend;var self=this;this.addAnchor=function(line){var anchor=document.createElement("a");anchor.innerHTML="✔";anchor.classList.add("action");line.element.insertBefore(anchor,line.element.firstChild);anchor.onclick=function(e){if(line.series.disabled){line.series.enable();line.element.classList.remove("disabled")}else{if(this.graph.series.filter(function(s){return!s.disabled}).length<=1)return;line.series.disable();line.element.classList.add("disabled")}self.graph.update()}.bind(this);var label=line.element.getElementsByTagName("span")[0];label.onclick=function(e){var disableAllOtherLines=line.series.disabled;if(!disableAllOtherLines){for(var i=0;idomainX){dataIndex=Math.abs(domainX-data[i].x)0){alignables.forEach(function(el){el.classList.remove("left");el.classList.add("right")});var rightAlignError=this._calcLayoutError(alignables);if(rightAlignError>leftAlignError){alignables.forEach(function(el){el.classList.remove("right");el.classList.add("left")})}}if(typeof this.onRender=="function"){this.onRender(args)}},_calcLayoutError:function(alignables){var parentRect=this.element.parentNode.getBoundingClientRect();var error=0;var alignRight=alignables.forEach(function(el){var rect=el.getBoundingClientRect();if(!rect.width){return}if(rect.right>parentRect.right){error+=rect.right-parentRect.right}if(rect.left=self.previewWidth){frameAfterDrag[0]-=frameAfterDrag[1]-self.previewWidth;frameAfterDrag[1]=self.previewWidth}}self.graphs.forEach(function(graph){var domainScale=d3.scale.linear().interpolate(d3.interpolateNumber).domain([0,self.previewWidth]).range(graph.dataDomain());var windowAfterDrag=[domainScale(frameAfterDrag[0]),domainScale(frameAfterDrag[1])];self.slideCallbacks.forEach(function(callback){callback(graph,windowAfterDrag[0],windowAfterDrag[1])});if(frameAfterDrag[0]===0){windowAfterDrag[0]=undefined}if(frameAfterDrag[1]===self.previewWidth){windowAfterDrag[1]=undefined}graph.window.xMin=windowAfterDrag[0];graph.window.xMax=windowAfterDrag[1];graph.update()})}function onMousedown(){drag.target=d3.event.target;drag.start=self._getClientXFromEvent(d3.event,drag);self.frameBeforeDrag=self.currentFrame.slice();d3.event.preventDefault?d3.event.preventDefault():d3.event.returnValue=false;d3.select(document).on("mousemove.rickshaw_range_slider_preview",onMousemove);d3.select(document).on("mouseup.rickshaw_range_slider_preview",onMouseup);d3.select(document).on("touchmove.rickshaw_range_slider_preview",onMousemove);d3.select(document).on("touchend.rickshaw_range_slider_preview",onMouseup);d3.select(document).on("touchcancel.rickshaw_range_slider_preview",onMouseup)}function onMousedownLeftHandle(datum,index){drag.left=true;onMousedown()}function onMousedownRightHandle(datum,index){drag.right=true;onMousedown()}function onMousedownMiddleHandle(datum,index){drag.left=true;drag.right=true;drag.rigid=true;onMousedown()}function onMouseup(datum,index){d3.select(document).on("mousemove.rickshaw_range_slider_preview",null);d3.select(document).on("mouseup.rickshaw_range_slider_preview",null);d3.select(document).on("touchmove.rickshaw_range_slider_preview",null);d3.select(document).on("touchend.rickshaw_range_slider_preview",null);d3.select(document).on("touchcancel.rickshaw_range_slider_preview",null);delete self.frameBeforeDrag;drag.left=false;drag.right=false;drag.rigid=false}element.select("rect.left_handle").on("mousedown",onMousedownLeftHandle);element.select("rect.right_handle").on("mousedown",onMousedownRightHandle);element.select("rect.middle_handle").on("mousedown",onMousedownMiddleHandle);element.select("rect.left_handle").on("touchstart",onMousedownLeftHandle);element.select("rect.right_handle").on("touchstart",onMousedownRightHandle);element.select("rect.middle_handle").on("touchstart",onMousedownMiddleHandle)},_getClientXFromEvent:function(event,drag){switch(event.type){case"touchstart":case"touchmove":var touchList=event.changedTouches;var touch=null;for(var touchIndex=0;touchIndexyMax)yMax=y});if(!series.length)return;if(series[0].xxMax)xMax=series[series.length-1].x});xMin-=(xMax-xMin)*this.padding.left;xMax+=(xMax-xMin)*this.padding.right;yMin=this.graph.min==="auto"?yMin:this.graph.min||0;yMax=this.graph.max===undefined?yMax:this.graph.max;if(this.graph.min==="auto"||yMin<0){yMin-=(yMax-yMin)*this.padding.bottom}if(this.graph.max===undefined){yMax+=(yMax-yMin)*this.padding.top}return{x:[xMin,xMax],y:[yMin,yMax]}},render:function(args){args=args||{};var graph=this.graph;var series=args.series||graph.series;var vis=args.vis||graph.vis;vis.selectAll("*").remove();var data=series.filter(function(s){return!s.disabled}).map(function(s){return s.stack});var pathNodes=vis.selectAll("path.path").data(data).enter().append("svg:path").classed("path",true).attr("d",this.seriesPathFactory());if(this.stroke){var strokeNodes=vis.selectAll("path.stroke").data(data).enter().append("svg:path").classed("stroke",true).attr("d",this.seriesStrokeFactory())}var i=0;series.forEach(function(series){if(series.disabled)return;series.path=pathNodes[0][i];if(this.stroke)series.stroke=strokeNodes[0][i];this._styleSeries(series);i++},this)},_styleSeries:function(series){var fill=this.fill?series.color:"none";var stroke=this.stroke?series.color:"none";series.path.setAttribute("fill",fill);series.path.setAttribute("stroke",stroke);series.path.setAttribute("stroke-width",this.strokeWidth);if(series.className){d3.select(series.path).classed(series.className,true)}if(series.className&&this.stroke){d3.select(series.stroke).classed(series.className,true)}},configure:function(args){args=args||{};Rickshaw.keys(this.defaults()).forEach(function(key){if(!args.hasOwnProperty(key)){this[key]=this[key]||this.graph[key]||this.defaults()[key];return}if(typeof this.defaults()[key]=="object"){Rickshaw.keys(this.defaults()[key]).forEach(function(k){this[key][k]=args[key][k]!==undefined?args[key][k]:this[key][k]!==undefined?this[key][k]:this.defaults()[key][k]},this)}else{this[key]=args[key]!==undefined?args[key]:this[key]!==undefined?this[key]:this.graph[key]!==undefined?this.graph[key]:this.defaults()[key]}},this)},setStrokeWidth:function(strokeWidth){if(strokeWidth!==undefined){this.strokeWidth=strokeWidth}},setTension:function(tension){if(tension!==undefined){this.tension=tension}}});Rickshaw.namespace("Rickshaw.Graph.Renderer.Line");Rickshaw.Graph.Renderer.Line=Rickshaw.Class.create(Rickshaw.Graph.Renderer,{name:"line",defaults:function($super){return Rickshaw.extend($super(),{unstack:true,fill:false,stroke:true})},seriesPathFactory:function(){var graph=this.graph;var factory=d3.svg.line().x(function(d){return graph.x(d.x)}).y(function(d){return graph.y(d.y)}).interpolate(this.graph.interpolation).tension(this.tension);factory.defined&&factory.defined(function(d){return d.y!==null});return factory}});Rickshaw.namespace("Rickshaw.Graph.Renderer.Stack");Rickshaw.Graph.Renderer.Stack=Rickshaw.Class.create(Rickshaw.Graph.Renderer,{name:"stack",defaults:function($super){return Rickshaw.extend($super(),{fill:true,stroke:false,unstack:false})},seriesPathFactory:function(){var graph=this.graph;var factory=d3.svg.area().x(function(d){return graph.x(d.x)}).y0(function(d){return graph.y(d.y0)}).y1(function(d){return graph.y(d.y+d.y0)}).interpolate(this.graph.interpolation).tension(this.tension);factory.defined&&factory.defined(function(d){return d.y!==null});return factory}});Rickshaw.namespace("Rickshaw.Graph.Renderer.Bar");Rickshaw.Graph.Renderer.Bar=Rickshaw.Class.create(Rickshaw.Graph.Renderer,{name:"bar",defaults:function($super){var defaults=Rickshaw.extend($super(),{gapSize:.05,unstack:false});delete defaults.tension;return defaults},initialize:function($super,args){args=args||{};this.gapSize=args.gapSize||this.gapSize;$super(args)},domain:function($super){var domain=$super();var frequentInterval=this._frequentInterval(this.graph.stackedData.slice(-1).shift());domain.x[1]+=Number(frequentInterval.magnitude);return domain},barWidth:function(series){var frequentInterval=this._frequentInterval(series.stack);var barWidth=this.graph.x.magnitude(frequentInterval.magnitude)*(1-this.gapSize);return barWidth},render:function(args){args=args||{};var graph=this.graph;var series=args.series||graph.series;var vis=args.vis||graph.vis;vis.selectAll("*").remove();var barWidth=this.barWidth(series.active()[0]);var barXOffset=0;var activeSeriesCount=series.filter(function(s){return!s.disabled}).length;var seriesBarWidth=this.unstack?barWidth/activeSeriesCount:barWidth;var transform=function(d){var matrix=[1,0,0,d.y<0?-1:1,0,d.y<0?graph.y.magnitude(Math.abs(d.y))*2:0];return"matrix("+matrix.join(",")+")"};series.forEach(function(series){if(series.disabled)return;var barWidth=this.barWidth(series);var nodes=vis.selectAll("path").data(series.stack.filter(function(d){return d.y!==null})).enter().append("svg:rect").attr("x",function(d){return graph.x(d.x)+barXOffset}).attr("y",function(d){return graph.y(d.y0+Math.abs(d.y))*(d.y<0?-1:1)}).attr("width",seriesBarWidth).attr("height",function(d){return graph.y.magnitude(Math.abs(d.y))}).attr("transform",transform);Array.prototype.forEach.call(nodes[0],function(n){n.setAttribute("fill",series.color)});if(this.unstack)barXOffset+=seriesBarWidth},this)},_frequentInterval:function(data){var intervalCounts={};for(var i=0;i0){this[0].data.forEach(function(plot){item.data.push({x:plot.x,y:0})})}else if(item.data.length===0){item.data.push({x:this.timeBase-(this.timeInterval||0),y:0})}this.push(item);if(this.legend){this.legend.addLine(this.itemByName(item.name))}},addData:function(data,x){var index=this.getIndex();Rickshaw.keys(data).forEach(function(name){if(!this.itemByName(name)){this.addItem({name:name})}},this);this.forEach(function(item){item.data.push({x:x||(index*this.timeInterval||1)+this.timeBase,y:data[item.name]||0})},this)},getIndex:function(){return this[0]&&this[0].data&&this[0].data.length?this[0].data.length:0},itemByName:function(name){for(var i=0;i1;i--){this.currentSize+=1;this.currentIndex+=1;this.forEach(function(item){item.data.unshift({x:((i-1)*this.timeInterval||1)+this.timeBase,y:0,i:i})},this)}}},addData:function($super,data,x){$super(data,x);this.currentSize+=1;this.currentIndex+=1;if(this.maxDataPoints!==undefined){while(this.currentSize>this.maxDataPoints){this.dropData()}}},dropData:function(){this.forEach(function(item){item.data.splice(0,1)});this.currentSize-=1},getIndex:function(){return this.currentIndex}});return Rickshaw}); \ No newline at end of file diff --git a/static/partials/super-user.html b/static/partials/super-user.html index 9b34cf159..567bafc4e 100644 --- a/static/partials/super-user.html +++ b/static/partials/super-user.html @@ -1,4 +1,7 @@
+ +
+
This panel provides administrator access to super users of this installation of the registry. Super users can be managed in the configuration for this installation.
From d359c849cd846b000c2b6804c18bb0efd9fd25db Mon Sep 17 00:00:00 2001 From: Joseph Schorr Date: Wed, 28 Jan 2015 17:12:33 -0500 Subject: [PATCH 02/12] Add the build worker and job count information to the charts --- buildman/component/basecomponent.py | 3 + buildman/component/buildcomponent.py | 3 + buildman/manager/enterprise.py | 4 ++ buildman/server.py | 16 ++++- data/queue.py | 16 +++-- endpoints/realtime.py | 10 ++- static/directives/ps-usage-graph.html | 13 ++++ static/directives/realtime-area-chart.html | 3 + static/js/app.js | 75 ++++++++++++++++++++++ 9 files changed, 134 insertions(+), 9 deletions(-) create mode 100644 static/directives/realtime-area-chart.html diff --git a/buildman/component/basecomponent.py b/buildman/component/basecomponent.py index 47781dff5..bd4032776 100644 --- a/buildman/component/basecomponent.py +++ b/buildman/component/basecomponent.py @@ -8,3 +8,6 @@ class BaseComponent(ApplicationSession): self.parent_manager = None self.build_logs = None self.user_files = None + + def kind(self): + raise NotImplementedError \ No newline at end of file diff --git a/buildman/component/buildcomponent.py b/buildman/component/buildcomponent.py index d518d3453..f31bf8d34 100644 --- a/buildman/component/buildcomponent.py +++ b/buildman/component/buildcomponent.py @@ -49,6 +49,9 @@ class BuildComponent(BaseComponent): BaseComponent.__init__(self, config, **kwargs) + def kind(self): + return 'builder' + def onConnect(self): self.join(self.builder_realm) diff --git a/buildman/manager/enterprise.py b/buildman/manager/enterprise.py index 6583284a8..b49ddd0f3 100644 --- a/buildman/manager/enterprise.py +++ b/buildman/manager/enterprise.py @@ -13,6 +13,9 @@ logger = logging.getLogger(__name__) class DynamicRegistrationComponent(BaseComponent): """ Component session that handles dynamic registration of the builder components. """ + def kind(self): + return 'registration' + def onConnect(self): self.join(REGISTRATION_REALM) @@ -69,6 +72,7 @@ class EnterpriseManager(BaseManager): def build_component_disposed(self, build_component, timed_out): self.build_components.remove(build_component) + self.unregister_component(build_component) def num_workers(self): return len(self.build_components) diff --git a/buildman/server.py b/buildman/server.py index e6d254536..002ccea07 100644 --- a/buildman/server.py +++ b/buildman/server.py @@ -1,5 +1,6 @@ import logging import trollius +import json from autobahn.asyncio.wamp import RouterFactory, RouterSessionFactory from autobahn.asyncio.websocket import WampWebSocketServerFactory @@ -63,7 +64,20 @@ class BuilderServer(object): @controller_app.route('/status') def status(): - return server._current_status + (running_count, available_count) = server._queue.get_metrics() + + workers = [component for component in server._current_components + if component.kind() == 'builder'] + + data = { + 'status': server._current_status, + 'running_local': server._job_count, + 'running_total': running_count, + 'workers': len(workers), + 'job_total': available_count + } + + return json.dumps(data) self._controller_app = controller_app diff --git a/data/queue.py b/data/queue.py index 5c720eed2..0e93a273f 100644 --- a/data/queue.py +++ b/data/queue.py @@ -41,10 +41,7 @@ class WorkQueue(object): def _name_match_query(self): return '%s%%' % self._canonical_name([self._queue_name] + self._canonical_name_match_list) - def update_metrics(self): - if self._reporter is None: - return - + def get_metrics(self): with self._transaction_factory(db): now = datetime.utcnow() name_match_query = self._name_match_query() @@ -52,9 +49,16 @@ class WorkQueue(object): running_query = self._running_jobs(now, name_match_query) running_count = running_query.distinct().count() - avialable_query = self._available_jobs(now, name_match_query, running_query) - available_count = avialable_query.select(QueueItem.queue_name).distinct().count() + available_query = self._available_jobs(now, name_match_query, running_query) + available_count = available_query.select(QueueItem.queue_name).distinct().count() + return (running_count, available_count) + + def update_metrics(self): + if self._reporter is None: + return + + (running_count, available_count) = self.get_metrics() self._reporter(self._currently_processing, running_count, running_count + available_count) def put(self, canonical_name_list, message, available_after=0, retries_remaining=5): diff --git a/endpoints/realtime.py b/endpoints/realtime.py index cfa3ec7ad..ac2a6c483 100644 --- a/endpoints/realtime.py +++ b/endpoints/realtime.py @@ -5,7 +5,7 @@ from flask import request, Blueprint, abort, Response from flask.ext.login import current_user from auth.auth import require_session_login from endpoints.common import route_show_if -from app import userevents +from app import app, userevents from auth.permissions import SuperUserPermission import features @@ -26,6 +26,11 @@ def ps(): def generator(): while True: + builder_data = app.config['HTTPCLIENT'].get('http://localhost:8686/status', timeout=1) + build_status = {} + if builder_data.status_code == 200: + build_status = json.loads(builder_data.text) + data = { 'count': { 'cpu': psutil.cpu_percent(interval=1, percpu=True), @@ -34,7 +39,8 @@ def ps(): 'connections': len(psutil.net_connections()), 'processes': len(psutil.pids()), 'network': psutil.net_io_counters() - } + }, + 'build': build_status } json_string = json.dumps(data) yield 'data: %s\n\n' % json_string diff --git a/static/directives/ps-usage-graph.html b/static/directives/ps-usage-graph.html index ddba677d0..9407519f0 100644 --- a/static/directives/ps-usage-graph.html +++ b/static/directives/ps-usage-graph.html @@ -1,4 +1,17 @@
+ Build Workers: +
+ +
+ CPU:
diff --git a/static/directives/realtime-area-chart.html b/static/directives/realtime-area-chart.html new file mode 100644 index 000000000..553bd30b1 --- /dev/null +++ b/static/directives/realtime-area-chart.html @@ -0,0 +1,3 @@ +
+
+
\ No newline at end of file diff --git a/static/js/app.js b/static/js/app.js index 7c5fd8d1e..0791b2f40 100644 --- a/static/js/app.js +++ b/static/js/app.js @@ -6522,6 +6522,81 @@ quayApp.directive('locationView', function () { }); +quayApp.directive('realtimeAreaChart', function () { + var directiveDefinitionObject = { + priority: 0, + templateUrl: '/static/directives/realtime-area-chart.html', + replace: false, + transclude: false, + restrict: 'C', + scope: { + 'data': '=data', + 'labels': '=labels', + 'colors': '=colors', + 'counter': '=counter' + }, + controller: function($scope, $element) { + var graph = null; + var series = []; + var palette = new Rickshaw.Color.Palette( { scheme: 'spectrum14' } ); + var colors = $scope.colors || []; + + var setupGraph = function() { + for (var i = 0; i < $scope.labels.length; ++i) { + series.push({ + name: $scope.labels[i], + color: i >= colors.length ? palette.color(): $scope.colors[i], + stroke: 'rgba(0,0,0,0.15)', + data: [] + }); + } + + graph = new Rickshaw.Graph( { + element: $element.find('.chart')[0], + renderer: 'area', + stroke: true, + series: series, + min: 0 + }); + }; + + var refresh = function(data) { + if (!data || $scope.counter < 0) { return; } + if (!graph) { + setupGraph(); + } + + for (var i = 0; i < $scope.data.length; ++i) { + series[i].data.push( + {'x': $scope.counter, 'y': $scope.data[i] } + ); + } + + hoverDetail = new Rickshaw.Graph.HoverDetail({ + graph: graph, + xFormatter: function(x) { + return x.toString(); + } + }); + + graph.renderer.unstack = true; + graph.render(); + }; + + $scope.$watch('counter', function() { + refresh($scope.data_raw); + }); + + $scope.$watch('data', function(data) { + $scope.data_raw = data; + refresh($scope.data_raw); + }); + } + }; + return directiveDefinitionObject; +}); + + quayApp.directive('realtimeLineChart', function () { var directiveDefinitionObject = { priority: 0, From 79f39697fecb7e6cbc785e025f3f290e0fcd6097 Mon Sep 17 00:00:00 2001 From: Joseph Schorr Date: Tue, 17 Feb 2015 11:31:50 -0500 Subject: [PATCH 03/12] - Fix superuser panel for debugging - Start work on the gauges panel --- endpoints/api/suconfig.py | 5 +++++ endpoints/realtime.py | 9 ++++++--- static/css/core-ui.css | 4 ++++ static/directives/ps-usage-graph.html | 24 +++++++++++++----------- static/js/core-config-setup.js | 4 ++++ static/partials/super-user.html | 8 ++++++++ 6 files changed, 40 insertions(+), 14 deletions(-) diff --git a/endpoints/api/suconfig.py b/endpoints/api/suconfig.py index daaba41ce..ee48fdf19 100644 --- a/endpoints/api/suconfig.py +++ b/endpoints/api/suconfig.py @@ -50,6 +50,11 @@ class SuperUserRegistryStatus(ApiResource): @verify_not_prod def get(self): """ Returns the status of the registry. """ + if app.config.get('DEBUGGING', False): + return { + 'status': 'ready' + } + # If there is no conf/stack volume, then report that status. if not CONFIG_PROVIDER.volume_exists(): return { diff --git a/endpoints/realtime.py b/endpoints/realtime.py index ac2a6c483..212ca6eee 100644 --- a/endpoints/realtime.py +++ b/endpoints/realtime.py @@ -26,10 +26,13 @@ def ps(): def generator(): while True: - builder_data = app.config['HTTPCLIENT'].get('http://localhost:8686/status', timeout=1) build_status = {} - if builder_data.status_code == 200: - build_status = json.loads(builder_data.text) + try: + builder_data = app.config['HTTPCLIENT'].get('http://localhost:8686/status', timeout=1) + if builder_data.status_code == 200: + build_status = json.loads(builder_data.text) + except: + pass data = { 'count': { diff --git a/static/css/core-ui.css b/static/css/core-ui.css index a89f07f39..d19e23dbc 100644 --- a/static/css/core-ui.css +++ b/static/css/core-ui.css @@ -702,4 +702,8 @@ .co-alert .co-step-bar { float: right; margin-top: 6px; +} + +.realtime-area-chart, .realtime-line-chart { + display: inline-block; } \ No newline at end of file diff --git a/static/directives/ps-usage-graph.html b/static/directives/ps-usage-graph.html index 9407519f0..88d4a2ba4 100644 --- a/static/directives/ps-usage-graph.html +++ b/static/directives/ps-usage-graph.html @@ -1,16 +1,18 @@
- Build Workers: -
+
+ Build Workers: +
-
+
+
CPU:
+ + + @@ -42,6 +45,11 @@ configuration-saved="configurationSaved()">
+ +
+
+
+
From 524705b88c41fed3b1dc04573db71adec5cf8383 Mon Sep 17 00:00:00 2001 From: Joseph Schorr Date: Tue, 17 Feb 2015 19:15:54 -0500 Subject: [PATCH 04/12] Get dashboard working and upgrade bootstrap. Note: the bootstrap fixes will be coming in the followup CL --- buildman/manager/enterprise.py | 5 + buildman/manager/ephemeral.py | 2 - buildman/server.py | 5 +- data/queue.py | 31 ++- endpoints/api/suconfig.py | 4 - endpoints/realtime.py | 7 +- external_libraries.py | 4 +- static/css/core-ui.css | 8 +- static/css/quay.css | 6 + .../directives/config/config-setup-tool.html | 52 ++-- static/directives/ps-usage-graph.html | 94 ++++--- static/directives/realtime-area-chart.html | 5 +- static/directives/realtime-line-chart.html | 5 +- static/js/app.js | 206 ++------------- static/js/controllers/superuser.js | 5 + static/js/core-config-setup.js | 4 +- static/js/core-ui.js | 241 +++++++++++++++++- static/partials/super-user.html | 5 +- 18 files changed, 429 insertions(+), 260 deletions(-) diff --git a/buildman/manager/enterprise.py b/buildman/manager/enterprise.py index c56830a1c..0ce69e508 100644 --- a/buildman/manager/enterprise.py +++ b/buildman/manager/enterprise.py @@ -25,6 +25,9 @@ class DynamicRegistrationComponent(BaseComponent): logger.debug('Registering new build component+worker with realm %s', realm) return realm + def kind(self): + return 'registration' + class EnterpriseManager(BaseManager): """ Build manager implementation for the Enterprise Registry. """ @@ -82,5 +85,7 @@ class EnterpriseManager(BaseManager): if build_component in self.ready_components: self.ready_components.remove(build_component) + self.unregister_component(build_component) + def num_workers(self): return len(self.all_components) diff --git a/buildman/manager/ephemeral.py b/buildman/manager/ephemeral.py index cfb52f8ad..473e75fb3 100644 --- a/buildman/manager/ephemeral.py +++ b/buildman/manager/ephemeral.py @@ -271,8 +271,6 @@ class EphemeralBuilderManager(BaseManager): def build_component_disposed(self, build_component, timed_out): logger.debug('Calling build_component_disposed.') - - # TODO make it so that I don't have to unregister the component if it timed out self.unregister_component(build_component) @coroutine diff --git a/buildman/server.py b/buildman/server.py index 28db129ad..f6ba9b4bc 100644 --- a/buildman/server.py +++ b/buildman/server.py @@ -70,7 +70,8 @@ class BuilderServer(object): @controller_app.route('/status') def status(): - (running_count, available_count) = server._queue.get_metrics() + metrics = server._queue.get_metrics(require_transaction=False) + (running_count, available_not_running_count, available_count) = metrics workers = [component for component in server._current_components if component.kind() == 'builder'] @@ -80,7 +81,7 @@ class BuilderServer(object): 'running_local': server._job_count, 'running_total': running_count, 'workers': len(workers), - 'job_total': available_count + 'job_total': available_count + running_count } return json.dumps(data) diff --git a/data/queue.py b/data/queue.py index 40a94c6e9..c1fb871ad 100644 --- a/data/queue.py +++ b/data/queue.py @@ -6,6 +6,12 @@ from util.morecollections import AttrDict MINIMUM_EXTENSION = timedelta(seconds=20) +class NoopWith: + def __enter__(self): + pass + + def __exit__(self, type, value, traceback): + pass class WorkQueue(object): def __init__(self, queue_name, transaction_factory, @@ -49,21 +55,32 @@ class WorkQueue(object): def _item_by_id_for_update(self, queue_id): return db_for_update(QueueItem.select().where(QueueItem.id == queue_id)).get() - def update_metrics(self): - if self._reporter is None: - return - - with self._transaction_factory(db): + def get_metrics(self, require_transaction=True): + guard = self._transaction_factory(db) if require_transaction else NoopWith() + with guard: now = datetime.utcnow() name_match_query = self._name_match_query() running_query = self._running_jobs(now, name_match_query) running_count = running_query.distinct().count() - available_query = self._available_jobs_not_running(now, name_match_query, running_query) + available_query = self._available_jobs(now, name_match_query) available_count = available_query.select(QueueItem.queue_name).distinct().count() - self._reporter(self._currently_processing, running_count, running_count + available_count) + available_not_running_query = self._available_jobs_not_running(now, name_match_query, + running_query) + available_not_running_count = (available_not_running_query.select(QueueItem.queue_name) + .distinct().count()) + + return (running_count, available_not_running_count, available_count) + + def update_metrics(self): + if self._reporter is None: + return + + (running_count, available_not_running_count, available_count) = self.get_metrics() + self._reporter(self._currently_processing, running_count, + running_count + available_not_running_count) def put(self, canonical_name_list, message, available_after=0, retries_remaining=5): """ diff --git a/endpoints/api/suconfig.py b/endpoints/api/suconfig.py index ee48fdf19..10741d9f3 100644 --- a/endpoints/api/suconfig.py +++ b/endpoints/api/suconfig.py @@ -50,10 +50,6 @@ class SuperUserRegistryStatus(ApiResource): @verify_not_prod def get(self): """ Returns the status of the registry. """ - if app.config.get('DEBUGGING', False): - return { - 'status': 'ready' - } # If there is no conf/stack volume, then report that status. if not CONFIG_PROVIDER.volume_exists(): diff --git a/endpoints/realtime.py b/endpoints/realtime.py index 212ca6eee..483a4c76c 100644 --- a/endpoints/realtime.py +++ b/endpoints/realtime.py @@ -47,9 +47,12 @@ def ps(): } json_string = json.dumps(data) yield 'data: %s\n\n' % json_string - time.sleep(0.25) + time.sleep(1) - return Response(generator(), mimetype="text/event-stream") + try: + return Response(generator(), mimetype="text/event-stream") + except: + pass diff --git a/external_libraries.py b/external_libraries.py index 3fa48c44a..3ab6bfd4a 100644 --- a/external_libraries.py +++ b/external_libraries.py @@ -6,7 +6,7 @@ LOCAL_DIRECTORY = 'static/ldn/' EXTERNAL_JS = [ 'code.jquery.com/jquery.js', - 'netdna.bootstrapcdn.com/bootstrap/3.0.0/js/bootstrap.min.js', + 'netdna.bootstrapcdn.com/bootstrap/3.3.2/js/bootstrap.min.js', 'ajax.googleapis.com/ajax/libs/angularjs/1.2.9/angular.min.js', 'ajax.googleapis.com/ajax/libs/angularjs/1.2.9/angular-route.min.js', 'ajax.googleapis.com/ajax/libs/angularjs/1.2.9/angular-sanitize.min.js', @@ -19,7 +19,7 @@ EXTERNAL_JS = [ EXTERNAL_CSS = [ 'netdna.bootstrapcdn.com/font-awesome/4.2.0/css/font-awesome.css', - 'netdna.bootstrapcdn.com/bootstrap/3.0.0/css/bootstrap.no-icons.min.css', + 'netdna.bootstrapcdn.com/bootstrap/3.3.2/css/bootstrap.min.css', 'fonts.googleapis.com/css?family=Source+Sans+Pro:400,700', ] diff --git a/static/css/core-ui.css b/static/css/core-ui.css index d19e23dbc..51a6d57c9 100644 --- a/static/css/core-ui.css +++ b/static/css/core-ui.css @@ -705,5 +705,11 @@ } .realtime-area-chart, .realtime-line-chart { - display: inline-block; + margin: 10px; + text-align: center; +} + +.rickshaw_graph { + overflow: hidden; + padding-bottom: 40px; } \ No newline at end of file diff --git a/static/css/quay.css b/static/css/quay.css index e09fce346..2e1c11136 100644 --- a/static/css/quay.css +++ b/static/css/quay.css @@ -4972,3 +4972,9 @@ i.slack-icon { left: 16px; font-size: 28px; } + +.chart-col h4, .chart-col h5 { + display: block; + text-align: center; +} + diff --git a/static/directives/config/config-setup-tool.html b/static/directives/config/config-setup-tool.html index 6b40f1fd5..7863ab1a4 100644 --- a/static/directives/config/config-setup-tool.html +++ b/static/directives/config/config-setup-tool.html @@ -1,5 +1,5 @@
-
+
@@ -289,7 +289,7 @@

Authentication for the registry can be handled by either the registry itself or LDAP. - External authentication providers (such as Github) can be used on top of this choice. + External authentication providers (such as GitHub) can be used on top of this choice.

@@ -339,20 +339,20 @@
- +
- Github (Enterprise) Authentication + GitHub (Enterprise) Authentication

- If enabled, users can use Github or Github Enterprise to authenticate to the registry. + If enabled, users can use GitHub or GitHub Enterprise to authenticate to the registry.

- Note: A registered Github (Enterprise) OAuth application is required. + Note: A registered GitHub (Enterprise) OAuth application is required. View instructions on how to - + Create an OAuth Application in GitHub

@@ -360,21 +360,21 @@
- +
- + - + @@ -402,7 +402,7 @@
Github:GitHub:
Github Endpoint:GitHub Endpoint:
- The Github Enterprise endpoint. Must start with http:// or https://. + The GitHub Enterprise endpoint. Must start with http:// or https://.
-
+
@@ -471,20 +471,20 @@
- +
- Github (Enterprise) Build Triggers + GitHub (Enterprise) Build Triggers

- If enabled, users can setup Github or Github Enterprise triggers to invoke Registry builds. + If enabled, users can setup GitHub or GitHub Enterprise triggers to invoke Registry builds.

- Note: A registered Github (Enterprise) OAuth application (separate from Github Authentication) is required. + Note: A registered GitHub (Enterprise) OAuth application (separate from GitHub Authentication) is required. View instructions on how to - + Create an OAuth Application in GitHub

@@ -492,21 +492,21 @@
- +
- + - + @@ -534,7 +534,7 @@
Github:GitHub:
Github Endpoint:GitHub Endpoint:
- The Github Enterprise endpoint. Must start with http:// or https://. + The GitHub Enterprise endpoint. Must start with http:// or https://.
-
+
diff --git a/static/directives/ps-usage-graph.html b/static/directives/ps-usage-graph.html index 88d4a2ba4..90c67c22b 100644 --- a/static/directives/ps-usage-graph.html +++ b/static/directives/ps-usage-graph.html @@ -1,39 +1,75 @@
-
- Build Workers: -
+ +
+
+ Cannot load build system status. Please restart your container. +
+
+
+

Build Queue

+
+ Running Jobs: {{ data.build.running_total }} | Total Jobs: {{ data.build.job_total }} +
+
+
-
+
+

Local Build Workers

+
+ Local Workers: {{ data.build.workers }} | Working: {{ data.build.running_local }} +
+
+
+
- CPU: -
+ +
+

CPU Usage %

+
+
- Process Count: -
+
+

Process Count

+
+
- Virtual Memory: -
+
+

Virtual Memory %

+
+
- Swap Memory: -
+
+

Swap Memory

+
+
- Network Connections: -
+
+

Network Connections

+
+
- Network Usage: -
+
+

Network Usage (Bytes)

+
+
\ No newline at end of file diff --git a/static/directives/realtime-area-chart.html b/static/directives/realtime-area-chart.html index 553bd30b1..d42d46784 100644 --- a/static/directives/realtime-area-chart.html +++ b/static/directives/realtime-area-chart.html @@ -1,3 +1,6 @@
-
+
+
+
+
\ No newline at end of file diff --git a/static/directives/realtime-line-chart.html b/static/directives/realtime-line-chart.html index a99ba56c4..74e8f748c 100644 --- a/static/directives/realtime-line-chart.html +++ b/static/directives/realtime-line-chart.html @@ -1,3 +1,6 @@
-
+
+
+
+
\ No newline at end of file diff --git a/static/js/app.js b/static/js/app.js index dd8a2e5eb..6cc07552f 100644 --- a/static/js/app.js +++ b/static/js/app.js @@ -6602,178 +6602,6 @@ quayApp.directive('locationView', function () { }); -quayApp.directive('realtimeAreaChart', function () { - var directiveDefinitionObject = { - priority: 0, - templateUrl: '/static/directives/realtime-area-chart.html', - replace: false, - transclude: false, - restrict: 'C', - scope: { - 'data': '=data', - 'labels': '=labels', - 'colors': '=colors', - 'counter': '=counter' - }, - controller: function($scope, $element) { - var graph = null; - var series = []; - var palette = new Rickshaw.Color.Palette( { scheme: 'spectrum14' } ); - var colors = $scope.colors || []; - - var setupGraph = function() { - for (var i = 0; i < $scope.labels.length; ++i) { - series.push({ - name: $scope.labels[i], - color: i >= colors.length ? palette.color(): $scope.colors[i], - stroke: 'rgba(0,0,0,0.15)', - data: [] - }); - } - - graph = new Rickshaw.Graph( { - element: $element.find('.chart')[0], - renderer: 'area', - stroke: true, - series: series, - min: 0 - }); - }; - - var refresh = function(data) { - if (!data || $scope.counter < 0) { return; } - if (!graph) { - setupGraph(); - } - - for (var i = 0; i < $scope.data.length; ++i) { - series[i].data.push( - {'x': $scope.counter, 'y': $scope.data[i] } - ); - } - - hoverDetail = new Rickshaw.Graph.HoverDetail({ - graph: graph, - xFormatter: function(x) { - return x.toString(); - } - }); - - graph.renderer.unstack = true; - graph.render(); - }; - - $scope.$watch('counter', function() { - refresh($scope.data_raw); - }); - - $scope.$watch('data', function(data) { - $scope.data_raw = data; - refresh($scope.data_raw); - }); - } - }; - return directiveDefinitionObject; -}); - - -quayApp.directive('realtimeLineChart', function () { - var directiveDefinitionObject = { - priority: 0, - templateUrl: '/static/directives/realtime-line-chart.html', - replace: false, - transclude: false, - restrict: 'C', - scope: { - 'data': '=data', - 'labels': '=labels', - 'counter': '=counter', - 'labelTemplate': '@labelTemplate' - }, - controller: function($scope, $element) { - var graph = null; - var hoverDetail = null; - var series = []; - var counter = 0; - var palette = new Rickshaw.Color.Palette( { scheme: 'spectrum14' } ); - - var setupGraph = function() { - graph = new Rickshaw.Graph({ - element: $element.find('.chart')[0], - renderer: 'line', - series: series, - min: 'auto', - padding: { - 'top': 0.1, - 'left': 0.01, - 'right': 0.01, - 'bottom': 0.1 - } - }); - - hoverDetail = new Rickshaw.Graph.HoverDetail({ - graph: graph, - xFormatter: function(x) { - return x.toString(); - } - }); - }; - - var refresh = function(data) { - if (!data) { return; } - if (!graph) { - setupGraph(); - } - - if (typeof data == 'number') { - data = [data]; - } - - if ($scope.labels) { - data = data.slice(0, $scope.labels.length); - } - - if (series.length == 0){ - for (var i = 0; i < data.length; ++i) { - var title = $scope.labels ? $scope.labels[i] : $scope.labelTemplate.replace('{x}', i + 1); - series.push({ - 'color': palette.color(), - 'data': [], - 'name': title - }) - } - } - - counter++; - - for (var i = 0; i < data.length; ++i) { - var arr = series[i].data; - arr.push({ - 'x': counter, - 'y': data[i] - }) - - if (arr.length > 10) { - series[i].data = arr.slice(arr.length - 10, arr.length); - } - } - - graph.render(); - }; - - $scope.$watch('counter', function(counter) { - refresh($scope.data_raw); - }); - - $scope.$watch('data', function(data) { - $scope.data_raw = data; - }); - } - }; - return directiveDefinitionObject; -}); - - quayApp.directive('psUsageGraph', function () { var directiveDefinitionObject = { priority: 0, @@ -6782,18 +6610,40 @@ quayApp.directive('psUsageGraph', function () { transclude: false, restrict: 'C', scope: { + 'isEnabled': '=isEnabled' }, controller: function($scope, $element) { $scope.counter = -1; $scope.data = null; - var source = new EventSource('/realtime/ps'); - source.onmessage = function(e) { - $scope.$apply(function() { - $scope.counter++; - $scope.data = JSON.parse(e.data); - }); + var source = null; + + var connect = function() { + if (source) { return; } + source = new EventSource('/realtime/ps'); + source.onmessage = function(e) { + $scope.$apply(function() { + $scope.counter++; + $scope.data = JSON.parse(e.data); + }); + }; }; + + var disconnect = function() { + if (!source) { return; } + source.close(); + source = null; + }; + + $scope.$watch('isEnabled', function(value) { + if (value) { + connect(); + } else { + disconnect(); + } + }); + + $scope.$on("$destroy", disconnect); } }; return directiveDefinitionObject; diff --git a/static/js/controllers/superuser.js b/static/js/controllers/superuser.js index ddaee7d5c..f867cd43b 100644 --- a/static/js/controllers/superuser.js +++ b/static/js/controllers/superuser.js @@ -17,6 +17,11 @@ function SuperUserAdminCtrl($scope, $timeout, ApiService, Features, UserService, $scope.pollChannel = null; $scope.logsScrolled = false; $scope.csrf_token = encodeURIComponent(window.__token); + $scope.dashboardActive = false; + + $scope.setDashboardActive = function(active) { + $scope.dashboardActive = active; + }; $scope.configurationSaved = function() { $scope.requiresRestart = true; diff --git a/static/js/core-config-setup.js b/static/js/core-config-setup.js index 0b22da9a2..569169201 100644 --- a/static/js/core-config-setup.js +++ b/static/js/core-config-setup.js @@ -307,10 +307,10 @@ angular.module("core-config-setup", ['angularFileUpload']) if (!value) { return; } ApiService.scGetConfig().then(function(resp) { - $scope.config = resp['config']; + $scope.config = resp['config'] || {}; initializeMappedLogic($scope.config); $scope.mapped['$hasChanges'] = false; - }); + }, ApiService.errorDisplay('Could not load config')); }); } }; diff --git a/static/js/core-ui.js b/static/js/core-ui.js index fc1ea029c..ed5e982e5 100644 --- a/static/js/core-ui.js +++ b/static/js/core-ui.js @@ -270,9 +270,18 @@ angular.module("core-ui", []) 'tabActive': '@tabActive', 'tabTitle': '@tabTitle', 'tabTarget': '@tabTarget', - 'tabInit': '&tabInit' + 'tabInit': '&tabInit', + 'tabShown': '&tabShown', + 'tabHidden': '&tabHidden' }, controller: function($rootScope, $scope, $element) { + $element.find('a[data-toggle="tab"]').on('hidden.bs.tab', function (e) { + $scope.tabHidden({}); + }); + + $element.find('a[data-toggle="tab"]').on('shown.bs.tab', function (e) { + $scope.tabShown({}); + }); } }; return directiveDefinitionObject; @@ -297,6 +306,236 @@ angular.module("core-ui", []) return directiveDefinitionObject; }) + .directive('realtimeAreaChart', function () { + var directiveDefinitionObject = { + priority: 0, + templateUrl: '/static/directives/realtime-area-chart.html', + replace: false, + transclude: false, + restrict: 'C', + scope: { + 'data': '=data', + 'labels': '=labels', + 'colors': '=colors', + 'counter': '=counter' + }, + controller: function($scope, $element) { + var graph = null; + var series = []; + var palette = new Rickshaw.Color.Palette( { scheme: 'spectrum14' } ); + var colors = $scope.colors || []; + + var setupGraph = function() { + for (var i = 0; i < $scope.labels.length; ++i) { + series.push({ + name: $scope.labels[i], + color: i >= colors.length ? palette.color(): $scope.colors[i], + stroke: 'rgba(0,0,0,0.15)', + data: [] + }); + } + + var options = { + element: $element.find('.chart')[0], + renderer: 'area', + stroke: true, + series: series, + min: 0, + padding: { + 'top': 0.3, + 'left': 0, + 'right': 0, + 'bottom': 0.3 + } + }; + + if ($scope.minimum != null) { + options['min'] = $scope.minimum == 'auto' ? 'auto' : $scope.minimum * 1; + } else { + options['min'] = 0; + } + + if ($scope.maximum != null) { + options['max'] = $scope.maximum == 'auto' ? 'auto' : $scope.maximum * 1; + } + + graph = new Rickshaw.Graph(options); + + xaxes = new Rickshaw.Graph.Axis.Time({ + graph: graph, + timeFixture: new Rickshaw.Fixtures.Time.Local() + }); + + yaxes = new Rickshaw.Graph.Axis.Y({ + graph: graph, + tickFormat: Rickshaw.Fixtures.Number.formatKMBT + }); + + hoverDetail = new Rickshaw.Graph.HoverDetail({ + graph: graph, + xFormatter: function(x) { + return new Date(x * 1000).toString(); + } + }); + }; + + var refresh = function(data) { + if (!data || $scope.counter < 0) { return; } + if (!graph) { + setupGraph(); + } + + var timecode = new Date().getTime() / 1000; + for (var i = 0; i < $scope.data.length; ++i) { + var arr = series[i].data; + arr.push( + {'x': timecode, 'y': $scope.data[i] } + ); + + if (arr.length > 10) { + series[i].data = arr.slice(arr.length - 10, arr.length); + } + } + + graph.renderer.unstack = true; + graph.update(); + }; + + $scope.$watch('counter', function() { + refresh($scope.data_raw); + }); + + $scope.$watch('data', function(data) { + $scope.data_raw = data; + refresh($scope.data_raw); + }); + } + }; + return directiveDefinitionObject; + }) + + + .directive('realtimeLineChart', function () { + var directiveDefinitionObject = { + priority: 0, + templateUrl: '/static/directives/realtime-line-chart.html', + replace: false, + transclude: false, + restrict: 'C', + scope: { + 'data': '=data', + 'labels': '=labels', + 'counter': '=counter', + 'labelTemplate': '@labelTemplate', + 'minimum': '@minimum', + 'maximum': '@maximum' + }, + controller: function($scope, $element) { + var graph = null; + var xaxes = null; + var yaxes = null; + var hoverDetail = null; + var series = []; + var counter = 0; + var palette = new Rickshaw.Color.Palette( { scheme: 'spectrum14' } ); + + var setupGraph = function() { + var options = { + element: $element.find('.chart')[0], + renderer: 'line', + series: series, + padding: { + 'top': 0.3, + 'left': 0, + 'right': 0, + 'bottom': 0.3 + } + }; + + if ($scope.minimum != null) { + options['min'] = $scope.minimum == 'auto' ? 'auto' : $scope.minimum * 1; + } else { + options['min'] = 0; + } + + if ($scope.maximum != null) { + options['max'] = $scope.maximum == 'auto' ? 'auto' : $scope.maximum * 1; + } + + graph = new Rickshaw.Graph(options); + xaxes = new Rickshaw.Graph.Axis.Time({ + graph: graph, + timeFixture: new Rickshaw.Fixtures.Time.Local() + }); + + yaxes = new Rickshaw.Graph.Axis.Y({ + graph: graph, + tickFormat: Rickshaw.Fixtures.Number.formatKMBT + }); + + hoverDetail = new Rickshaw.Graph.HoverDetail({ + graph: graph, + xFormatter: function(x) { + return new Date(x * 1000).toString(); + } + }); + }; + + var refresh = function(data) { + if (data == null) { return; } + if (!graph) { + setupGraph(); + } + + if (typeof data == 'number') { + data = [data]; + } + + if ($scope.labels) { + data = data.slice(0, $scope.labels.length); + } + + if (series.length == 0){ + for (var i = 0; i < data.length; ++i) { + var title = $scope.labels ? $scope.labels[i] : $scope.labelTemplate.replace('{x}', i + 1); + series.push({ + 'color': palette.color(), + 'data': [], + 'name': title + }) + } + } + + counter++; + var timecode = new Date().getTime() / 1000; + + for (var i = 0; i < data.length; ++i) { + var arr = series[i].data; + arr.push({ + 'x': timecode, + 'y': data[i] + }) + + if (arr.length > 10) { + series[i].data = arr.slice(arr.length - 10, arr.length); + } + } + + graph.update(); + }; + + $scope.$watch('counter', function(counter) { + refresh($scope.data_raw); + }); + + $scope.$watch('data', function(data) { + $scope.data_raw = data; + }); + } + }; + return directiveDefinitionObject; + }) + .directive('corStepBar', function() { var directiveDefinitionObject = { priority: 4, diff --git a/static/partials/super-user.html b/static/partials/super-user.html index 2e72bfe85..6d150634d 100644 --- a/static/partials/super-user.html +++ b/static/partials/super-user.html @@ -20,7 +20,8 @@ tab-target="#users" tab-init="loadUsers()"> - + @@ -47,7 +48,7 @@
-
+
From 854c6e8ba361aafa25eae19f15288374430712fe Mon Sep 17 00:00:00 2001 From: Joseph Schorr Date: Tue, 17 Feb 2015 19:18:56 -0500 Subject: [PATCH 05/12] Add a try-catch around the realtime logs stuff --- endpoints/realtime.py | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/endpoints/realtime.py b/endpoints/realtime.py index 483a4c76c..44b806ce1 100644 --- a/endpoints/realtime.py +++ b/endpoints/realtime.py @@ -34,17 +34,21 @@ def ps(): except: pass - data = { - 'count': { - 'cpu': psutil.cpu_percent(interval=1, percpu=True), - 'virtual_mem': psutil.virtual_memory(), - 'swap_mem': psutil.swap_memory(), - 'connections': len(psutil.net_connections()), - 'processes': len(psutil.pids()), - 'network': psutil.net_io_counters() - }, - 'build': build_status - } + try: + data = { + 'count': { + 'cpu': psutil.cpu_percent(interval=1, percpu=True), + 'virtual_mem': psutil.virtual_memory(), + 'swap_mem': psutil.swap_memory(), + 'connections': len(psutil.net_connections()), + 'processes': len(psutil.pids()), + 'network': psutil.net_io_counters() + }, + 'build': build_status + } + except psutil.AccessDenied: + data = {} + json_string = json.dumps(data) yield 'data: %s\n\n' % json_string time.sleep(1) From c642cada00ef5e0e755e9d880b168030479d6dc1 Mon Sep 17 00:00:00 2001 From: Joseph Schorr Date: Tue, 17 Feb 2015 19:37:37 -0500 Subject: [PATCH 06/12] Adjust CSS for new version of bootstrap --- static/css/quay.css | 53 ++++++++------------ static/directives/cor-tab-panel.html | 2 +- static/directives/dockerfile-build-form.html | 6 +-- static/directives/loading-status.html | 2 +- static/directives/logs-view.html | 6 +-- static/directives/notification-view.html | 2 +- static/directives/robots-manager.html | 2 +- 7 files changed, 29 insertions(+), 44 deletions(-) diff --git a/static/css/quay.css b/static/css/quay.css index 2e1c11136..1f2bbb0e9 100644 --- a/static/css/quay.css +++ b/static/css/quay.css @@ -3,6 +3,10 @@ margin: 0; } +.btn { + outline: none !important; +} + @media (max-width: 410px) { .olrk-normal { display: none; @@ -159,7 +163,7 @@ nav.navbar-default .navbar-nav>li>a.active { .notification-view-element .circle { position: absolute; - top: 14px; + top: 15px; left: 0px; width: 12px; @@ -179,13 +183,13 @@ nav.navbar-default .navbar-nav>li>a.active { margin-bottom: 4px; } -.notification-view-element .container { +.notification-view-element .notification-content { padding: 10px; border-radius: 6px; margin-left: 16px; } -.notification-view-element .container:hover { +.notification-view-element .notification-content:hover { background: rgba(66, 139, 202, 0.1); } @@ -1140,59 +1144,59 @@ i.toggle-icon:hover { } .visible-sm-inline { - display: none; + display: none !important; } .visible-md-inline { - display: none; + display: none !important; } .hidden-sm-inline { - display: inline; + display: inline !important; } .hidden-xs-inline { - display: inline; + display: inline !important; } @media (min-width: 991px) { .visible-md-inline { - display: inline; + display: inline !important; } } @media (max-width: 991px) and (min-width: 768px) { .visible-sm-inline { - display: inline; + display: inline !important; } .hidden-sm-inline { - display: none; + display: none !important; } } @media (max-width: 700px) { .hidden-xs-inline { - display: none; + display: none !important; } } .visible-xl { - display: none; + display: none !important; } .visible-xl-inline { - display: none; + display: none !important; } @media (min-width: 1200px) { .visible-xl { - display: block; + display: block !important; } .visible-xl-inline { - display: inline-block; + display: inline-block !important; } } @@ -3762,7 +3766,7 @@ p.editable:hover i { text-align: center; position: relative; color: white; - left: -42px; + left: -38px; top: -9px; font-weight: bold; font-size: .4em; @@ -3772,23 +3776,6 @@ p.editable:hover i { margin-bottom: 40px; } -.landing .social-alternate { - color: #777; - font-size: 2em; - margin-left: 43px; - line-height: 1em; -} - -.landing .social-alternate .inner-text { - text-align: center; - position: relative; - color: white; - left: -43px; - top: -9px; - font-weight: bold; - font-size: .4em; -} - .contact-options { margin-top: 60px; } diff --git a/static/directives/cor-tab-panel.html b/static/directives/cor-tab-panel.html index 57f9dfa1c..f92d683ab 100644 --- a/static/directives/cor-tab-panel.html +++ b/static/directives/cor-tab-panel.html @@ -1,3 +1,3 @@
-
+
\ No newline at end of file diff --git a/static/directives/dockerfile-build-form.html b/static/directives/dockerfile-build-form.html index 4dfba4e08..6278bd940 100644 --- a/static/directives/dockerfile-build-form.html +++ b/static/directives/dockerfile-build-form.html @@ -1,8 +1,8 @@
-
+
-
+
Uploading file {{ upload_file }}
@@ -10,7 +10,7 @@
-
+
diff --git a/static/directives/loading-status.html b/static/directives/loading-status.html index 9ed712459..1776b5235 100644 --- a/static/directives/loading-status.html +++ b/static/directives/loading-status.html @@ -1,4 +1,4 @@ -
+
diff --git a/static/directives/logs-view.html b/static/directives/logs-view.html index cc3c51d2f..28fc8edb0 100644 --- a/static/directives/logs-view.html +++ b/static/directives/logs-view.html @@ -1,6 +1,6 @@
-
+
Usage Logs @@ -20,9 +20,7 @@
-
-
-
+
diff --git a/static/directives/notification-view.html b/static/directives/notification-view.html index ef0f85de2..6b9dda7a3 100644 --- a/static/directives/notification-view.html +++ b/static/directives/notification-view.html @@ -1,5 +1,5 @@
-
+
diff --git a/static/directives/robots-manager.html b/static/directives/robots-manager.html index 6c9dd8fdd..b4bc89cab 100644 --- a/static/directives/robots-manager.html +++ b/static/directives/robots-manager.html @@ -2,7 +2,7 @@
Robot accounts allow for delegating access in multiple repositories to role-based accounts that you manage
-
+
From ee4930b4bf90310dec50ff1e613e0cee6c7053b8 Mon Sep 17 00:00:00 2001 From: Joseph Schorr Date: Tue, 17 Feb 2015 19:41:33 -0500 Subject: [PATCH 07/12] Fix checkbox padding --- static/tutorial/docker-login.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/static/tutorial/docker-login.html b/static/tutorial/docker-login.html index cb38d0da3..ff35e5d74 100644 --- a/static/tutorial/docker-login.html +++ b/static/tutorial/docker-login.html @@ -1,8 +1,8 @@
-
From f7615b2e963d2212a3e05f4ca0a4fe9b174e2eed Mon Sep 17 00:00:00 2001 From: Joseph Schorr Date: Wed, 18 Feb 2015 14:17:09 -0500 Subject: [PATCH 08/12] Add missing lib requirement --- requirements-nover.txt | 1 + requirements.txt | 1 + 2 files changed, 2 insertions(+) diff --git a/requirements-nover.txt b/requirements-nover.txt index 9b8707870..b81936ec7 100644 --- a/requirements-nover.txt +++ b/requirements-nover.txt @@ -47,3 +47,4 @@ pyOpenSSL pygpgme cachetools mock +psutil \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 4e51c6245..ee41fcc56 100644 --- a/requirements.txt +++ b/requirements.txt @@ -39,6 +39,7 @@ mixpanel-py==3.2.1 mock==1.0.1 paramiko==1.15.2 peewee==2.4.7 +psutil==2.2.1 psycopg2==2.5.4 py-bcrypt==0.4 pycrypto==2.6.1 From 9d91226cff76a3abaaae9e53d5c113237547ada5 Mon Sep 17 00:00:00 2001 From: Joseph Schorr Date: Wed, 18 Feb 2015 14:37:59 -0500 Subject: [PATCH 09/12] Fix padding on page views: the container class interacts oddly now with the new bootstrap --- static/css/core-ui.css | 5 +++++ static/css/quay.css | 4 ++++ static/partials/about.html | 2 +- static/partials/build-package.html | 4 ++-- static/partials/confirm-invite.html | 2 +- static/partials/contact.html | 2 +- static/partials/guide.html | 2 +- static/partials/image-view.html | 2 +- static/partials/landing-login.html | 4 ++-- static/partials/landing-normal.html | 16 ++++++++-------- static/partials/manage-application.html | 2 +- static/partials/new-organization.html | 2 +- static/partials/new-repo.html | 8 ++++---- static/partials/org-admin.html | 2 +- static/partials/org-member-logs.html | 2 +- static/partials/org-view.html | 2 +- static/partials/organizations.html | 2 +- static/partials/plans.html | 2 +- static/partials/repo-admin.html | 6 +++--- static/partials/repo-build.html | 4 ++-- static/partials/repo-list.html | 2 +- static/partials/security.html | 2 +- static/partials/signin.html | 2 +- static/partials/team-view.html | 2 +- static/partials/tour.html | 2 +- static/partials/tutorial.html | 2 +- static/partials/user-admin.html | 2 +- static/partials/v1-page.html | 2 +- 28 files changed, 50 insertions(+), 41 deletions(-) diff --git a/static/css/core-ui.css b/static/css/core-ui.css index 51a6d57c9..2012129c1 100644 --- a/static/css/core-ui.css +++ b/static/css/core-ui.css @@ -712,4 +712,9 @@ .rickshaw_graph { overflow: hidden; padding-bottom: 40px; +} + +.cor-container { + padding-left: 15px; + padding-right: 15px; } \ No newline at end of file diff --git a/static/css/quay.css b/static/css/quay.css index 1f2bbb0e9..e556c742e 100644 --- a/static/css/quay.css +++ b/static/css/quay.css @@ -1417,6 +1417,10 @@ i.toggle-icon:hover { background: transparent; } +.jumbotron p { + font-size: 100%; +} + .jumbotron .disclaimer-link { font-size: .3em; vertical-align: 23px; diff --git a/static/partials/about.html b/static/partials/about.html index 7440efc09..01c40f277 100644 --- a/static/partials/about.html +++ b/static/partials/about.html @@ -1,4 +1,4 @@ -
+

About Us

diff --git a/static/partials/build-package.html b/static/partials/build-package.html index 3f6df7d50..7ed70e98e 100644 --- a/static/partials/build-package.html +++ b/static/partials/build-package.html @@ -1,8 +1,8 @@
-
+
You do not have permission to view this page
-
+

diff --git a/static/partials/confirm-invite.html b/static/partials/confirm-invite.html index 807ca4540..5bf2f97ad 100644 --- a/static/partials/confirm-invite.html +++ b/static/partials/confirm-invite.html @@ -1,5 +1,5 @@
-

Dockerfile or .tar.gz or .zip: