mirror of
				https://github.com/ohwgiles/laminar.git
				synced 2025-06-13 12:54:29 +00:00 
			
		
		
		
	frontend: replace angular with vue
This commit is contained in:
		
							parent
							
								
									56bc2581bf
								
							
						
					
					
						commit
						ad9837fd96
					
				| @ -58,14 +58,13 @@ add_custom_command(OUTPUT laminar.capnp.c++ laminar.capnp.h | ||||
| # Zip and compile statically served resources | ||||
| generate_compressed_bins(${CMAKE_SOURCE_DIR}/src/resources index.html js/app.js | ||||
|     tpl/home.html tpl/job.html tpl/run.html tpl/browse.html | ||||
|     favicon.ico favicon-152.png icon.png progress.png) | ||||
|     favicon.ico favicon-152.png icon.png progress.gif) | ||||
| 
 | ||||
| # Download 3rd-party frontend JS libs... | ||||
| file(DOWNLOAD https://ajax.googleapis.com/ajax/libs/angularjs/1.3.14/angular.min.js | ||||
|         js/angular.min.js EXPECTED_MD5 b1137641dbb512a60e83d673f7e2d98f) | ||||
| file(DOWNLOAD https://ajax.googleapis.com/ajax/libs/angularjs/1.3.14/angular-route.min.js | ||||
|         js/angular-route.min.js EXPECTED_MD5 28ef7d7b4349ae0dce602748185ef32a) | ||||
| file(DOWNLOAD https://ajax.googleapis.com/ajax/libs/angularjs/1.3.14/angular-sanitize.min.js | ||||
|         js/angular-sanitize.min.js EXPECTED_MD5 0854eae86bcdf5f92b1ab2b458d8d054) | ||||
| file(DOWNLOAD https://cdnjs.cloudflare.com/ajax/libs/vue/2.3.4/vue.min.js | ||||
|         js/vue.min.js EXPECTED_MD5 ae2fca1cfa0e31377819b1b0ffef704c) | ||||
| file(DOWNLOAD https://cdnjs.cloudflare.com/ajax/libs/vue-router/2.7.0/vue-router.min.js | ||||
|         js/vue-router.min.js EXPECTED_MD5 5d3e35710dbe02de78c39e3e439b8d4e) | ||||
| file(DOWNLOAD https://raw.githubusercontent.com/drudru/ansi_up/v1.3.0/ansi_up.js | ||||
|         js/ansi_up.js EXPECTED_MD5 158566dc1ff8f2804de972f7e841e2f6) | ||||
| file(DOWNLOAD https://cdnjs.cloudflare.com/ajax/libs/Chart.js/1.0.2/Chart.min.js | ||||
| @ -75,9 +74,8 @@ file(DOWNLOAD https://raw.githubusercontent.com/tomsouthall/Chart.HorizontalBar. | ||||
| file(DOWNLOAD https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css | ||||
|         css/bootstrap.min.css EXPECTED_MD5 5d5357cb3704e1f43a1f5bfed2aebf42) | ||||
| # ...and compile them | ||||
| generate_compressed_bins(${CMAKE_BINARY_DIR} js/angular.min.js js/angular-route.min.js | ||||
|     js/angular-sanitize.min.js js/ansi_up.js js/Chart.min.js js/Chart.HorizontalBar.js | ||||
|     css/bootstrap.min.css) | ||||
| generate_compressed_bins(${CMAKE_BINARY_DIR} js/vue-router.min.js js/vue.min.js | ||||
|     js/ansi_up.js js/Chart.min.js js/Chart.HorizontalBar.js css/bootstrap.min.css) | ||||
| # (see resources.cpp where these are fetched) | ||||
| 
 | ||||
| ## Server | ||||
|  | ||||
| @ -30,7 +30,7 @@ Resources::Resources() | ||||
|     INIT_RESOURCE("/", index_html); | ||||
|     INIT_RESOURCE("/favicon.ico", favicon_ico); | ||||
|     INIT_RESOURCE("/favicon-152.png", favicon_152_png); | ||||
|     INIT_RESOURCE("/progress.png", progress_png); | ||||
|     INIT_RESOURCE("/progress.gif", progress_gif); | ||||
|     INIT_RESOURCE("/icon.png", icon_png); | ||||
|     INIT_RESOURCE("/js/app.js", js_app_js); | ||||
|     INIT_RESOURCE("/js/Chart.HorizontalBar.js", js_Chart_HorizontalBar_js); | ||||
| @ -39,9 +39,8 @@ Resources::Resources() | ||||
|     INIT_RESOURCE("/tpl/job.html", tpl_job_html); | ||||
|     INIT_RESOURCE("/tpl/run.html", tpl_run_html); | ||||
|     INIT_RESOURCE("/tpl/browse.html", tpl_browse_html); | ||||
|     INIT_RESOURCE("/js/angular.min.js", js_angular_min_js); | ||||
|     INIT_RESOURCE("/js/angular-route.min.js", js_angular_route_min_js); | ||||
|     INIT_RESOURCE("/js/angular-sanitize.min.js", js_angular_sanitize_min_js); | ||||
|     INIT_RESOURCE("/js/vue.min.js", js_vue_min_js); | ||||
|     INIT_RESOURCE("/js/vue-router.min.js", js_vue_router_min_js); | ||||
|     INIT_RESOURCE("/js/ansi_up.js", js_ansi_up_js); | ||||
|     INIT_RESOURCE("/js/Chart.min.js", js_Chart_min_js); | ||||
|     INIT_RESOURCE("/js/Chart.HorizontalBar.js", js_Chart_HorizontalBar_js); | ||||
|  | ||||
| @ -1,5 +1,5 @@ | ||||
| <!doctype html> | ||||
| <html ng-app="laminar"> | ||||
| <html> | ||||
| <head> | ||||
|  <base href="/"> | ||||
|  <meta charset="utf-8"> | ||||
| @ -8,14 +8,13 @@ | ||||
|  <meta name="apple-mobile-web-app-capable" content="yes" /> | ||||
|  <link rel="apple-touch-icon-precomposed" href="/favicon-152.png"> | ||||
|  <title>Laminar</title> | ||||
|  <script src="/js/angular.min.js"></script> | ||||
|  <script src="/js/angular-route.min.js"></script> | ||||
|  <script src="/js/angular-sanitize.min.js"></script> | ||||
|  <script src="/js/ansi_up.js" type="text/javascript"></script> | ||||
|  <script src="/js/vue.min.js"></script> | ||||
|  <script src="/js/vue-router.min.js"></script> | ||||
|  <script src="/js/ansi_up.js"></script> | ||||
|  <script src="/js/Chart.min.js"></script> | ||||
|  <script src="/js/Chart.HorizontalBar.js"></script> | ||||
|  <link href="/css/bootstrap.min.css" rel="stylesheet"> | ||||
|  <script src="/js/app.js"></script> | ||||
|  <script src="/js/app.js" defer></script> | ||||
|  <style> | ||||
|   body, html { height: 100%; } | ||||
|   .navbar { margin-bottom: 0; } | ||||
| @ -36,42 +35,187 @@ | ||||
|    margin-top: 5px; | ||||
|    margin-bottom: 0; | ||||
|   } | ||||
|   .spin { | ||||
| 	  -webkit-animation: rotation 2s infinite linear; | ||||
|   } | ||||
|   @-webkit-keyframes rotation { | ||||
|     from {-webkit-transform: rotate(0deg);} | ||||
|     to   {-webkit-transform: rotate(359deg);} | ||||
|   } | ||||
|   img.spin.small { | ||||
| 	  width: 11px; | ||||
| 	  height: 11px; | ||||
|   } | ||||
|   img.spin { | ||||
|     -webkit-animation:spin 4s linear infinite; | ||||
|     -moz-animation:spin 4s linear infinite; | ||||
|     animation:spin 4s linear infinite; | ||||
| } | ||||
| @-moz-keyframes spin { 100% { -moz-transform: rotate(360deg); } } | ||||
| @-webkit-keyframes spin { 100% { -webkit-transform: rotate(360deg); } } | ||||
| @keyframes spin { 100% { -webkit-transform: rotate(360deg); transform:rotate(360deg); } } | ||||
| 
 | ||||
|   table#joblist tr:first-child td { border-top: 0; } | ||||
|  </style> | ||||
| </head> | ||||
| <body> | ||||
|  <nav class="navbar navbar-inverse"> | ||||
|   <div class="container-fluid"> | ||||
|    <div> | ||||
|     <a class="navbar-brand" href="/"><img src="/icon.png">{{title}}</a> | ||||
|     <a class="btn navbar-btn pull-right" href="/jobs">Jobs</a> | ||||
|  <template id="home"><div> | ||||
|   <ol class="breadcrumb"><li class="active">Home</li></ol> | ||||
|   <div class="container-fluid"><div class="row"> | ||||
|    <div class="col-sm-5 col-md-4 col-lg-3 dash"> | ||||
|     <table class="table table-bordered"> | ||||
|      <tr v-for="job in jobsQueued"> | ||||
|       <td><router-link :to="'/jobs/'+job.name">{{job.name}}</router-link> <i>queued</i></td> | ||||
|      </tr> | ||||
|      <tr v-for="job in jobsRunning"> | ||||
|       <td><img class="spin small" src="/progress.gif"> <router-link :to="'/jobs/'+job.name">{{job.name}}</router-link> <router-link :to="'/jobs/'+job.name+'/'+job.number">#{{job.number}}</router-link> <div class="progress"> | ||||
|        <div class="progress-bar progress-bar-striped" :class="'progress-bar-'+(job.overtime?'warning':'info')" :class="job.etc?'':'active'" :style="'width:'+(!job.etc?'100':job.progress)+'%'"></div> | ||||
|        </div> | ||||
|       </td> | ||||
|      </tr> | ||||
|      <tr v-for="job in jobsRecent"> | ||||
|       <td><span v-html="runIcon(job.result)"></span> <router-link :to="'/jobs/'+job.name">{{job.name}}</router-link> <router-link :to="'/jobs/'+job.name+'/'+job.number">#{{job.number}}</router-link><br><small>Took {{job.duration}}s at {{formatDate(job.started)}}</small></td> | ||||
|      </tr> | ||||
|     </table> | ||||
|    </div> | ||||
|    <div class="col-sm-7 col-md-8 col-lg-9"><div class="row"> | ||||
|     <div class="col-md-6"> | ||||
|      <div class="panel panel-default"> | ||||
|       <div class="panel-heading">Total builds per day this week</div> | ||||
|       <div class="panel-body"> | ||||
|        <canvas id="chartBpd"></canvas> | ||||
|       </div> | ||||
|      </div> | ||||
|     </div> | ||||
|     <div class="col-md-6"> | ||||
|      <div class="panel panel-default"> | ||||
|       <div class="panel-heading">Builds per job in the last 24 hours</div> | ||||
|       <div class="panel-body" id="chartStatus"> | ||||
|        <canvas id="chartBpj"></canvas> | ||||
|       </div> | ||||
|      </div> | ||||
|     </div> | ||||
|     <div class="col-md-6"> | ||||
|      <div class="panel panel-default"> | ||||
|       <div class="panel-heading">Average build time per job this week</div> | ||||
|       <div class="panel-body"> | ||||
|        <canvas id="chartTpj"></canvas> | ||||
|       </div> | ||||
|      </div> | ||||
|     </div> | ||||
|     <div class="col-md-6"> | ||||
|      <div class="panel panel-default"> | ||||
|       <div class="panel-heading">Current executor utilization</div> | ||||
|       <div class="panel-body"> | ||||
|        <canvas id="chartUtil"></canvas> | ||||
|       </div> | ||||
|      </div> | ||||
|     </div> | ||||
|    </div></div> | ||||
|   </div></div>  | ||||
|  </div></template> | ||||
|   | ||||
|  <template id="jobs"><div> | ||||
|   <ol class="breadcrumb"><li><router-link to="/">Home</router-link></li><li class="active">Jobs</li></ol> | ||||
|   <div class="container-fluid"><div class="row"> | ||||
|    <div class="col-xs-12"> | ||||
|     <div class="pull-right"> | ||||
|      <input class="form-control" id="jobFilter" v-model="search" placeholder="Filter..."> | ||||
|     </div> | ||||
|     <ul class="nav nav-tabs"> | ||||
|      <li :class="{'active':tag==null}"><a href v-on:click.prevent="tag = null">All Jobs</a></li> | ||||
|      <li v-for="t in tags" :class="{'active':t==tag}"><a href v-on:click.prevent="tag = t">{{t}}</a></li> | ||||
|     </ul> | ||||
|     <table class="table table-striped" id="joblist"> | ||||
|      <tr   v-for="job in filteredJobs"> | ||||
|       <td><router-link :to="'/jobs/'+job.name">{{job.name}}</router-link></td> | ||||
|       <td class="text-center"><span v-html="runIcon(job.result)"></span> <router-link :to="'/jobs/'+job.name+'/'+job.number">#{{job.number}}</router-link></td> | ||||
|       <td class="text-center">{{formatDate(job.started)}}</td> | ||||
|      </tr> | ||||
|     </table> | ||||
|    </div> | ||||
|   </div></div> | ||||
|  </div></template> | ||||
|   | ||||
|  <template id="job"><div> | ||||
|   <ol class="breadcrumb"><li><router-link to="/">Home</router-link></li><li><router-link to="/jobs">Jobs</router-link></li><li class="active">{{$route.params.name}}</li></ol></ol> | ||||
|   <div class="container-fluid"> | ||||
|    <div class="row"> | ||||
|     <div class="col-sm-5 col-md-6 col-lg-7"> | ||||
|     <h3>{{$route.params.name}}</h3> | ||||
|     <dl class="dl-horizontal"> | ||||
|      <dt>Last Successful Run</dt> | ||||
|      <dd><router-link v-if="lastSuccess" :to="'/jobs/'+$route.params.name+'/'+lastSuccess.number">#{{lastSuccess.number}}</router-link> {{lastSuccess?' - at '+formatDate(lastSuccess.started):'never'}}</dd> | ||||
|      <dt>Last Failed Run</dt> | ||||
|      <dd><router-link v-if="lastFailed" :to="'/jobs/'+$route.params.name+'/'+lastFailed.number">#{{lastFailed.number}}</router-link> {{lastFailed?' - at '+formatDate(lastFailed.started):'never'}}</dd> | ||||
|     </dl> | ||||
|     </div> | ||||
|     <div class="col-sm-7 col-md-6 col-lg-5"> | ||||
|     <div class="panel panel-default"> | ||||
|       <div class="panel-heading">Build time</div> | ||||
|       <div class="panel-body"> | ||||
|       <canvas id="chartBt"></canvas> | ||||
|       </div> | ||||
|     </div> | ||||
|     </div> | ||||
|    </div> | ||||
|    <div class="row"><div class="col-xs-12"> | ||||
|     <table class="table table-striped"><thead> | ||||
|      <tr><th>Run</th><th class="text-center">Started</th><th class="text-center">Duration</th><th class="text-center hidden-xs">Reason</th></tr></thead> | ||||
|      <tr v-show="nQueued"> | ||||
|       <td colspan="4"><i>{{nQueued}} run(s) queued</i></td> | ||||
|      </tr> | ||||
|      <tr v-for="job in jobsRunning" track-by="$index"> | ||||
|       <td><img class="spin small" src="/progress.gif"> <router-link :to="'/jobs/'+$route.params.name+'/'+job.number">#{{job.number}}</router-link></td> | ||||
|       <td class="text-center">{{formatDate(job.started)}}</td> | ||||
|       <td class="text-center">--</td> | ||||
|       <td class="text-center hidden-xs">{{job.reason}}</td> | ||||
|      </tr> | ||||
|      <tr v-for="job in jobsRecent" track-by="$index"> | ||||
|       <td><span v-html="runIcon(job.result)"></span> <router-link :to="'/jobs/'+$route.params.name+'/'+job.number">#{{job.number}}</router-link></td> | ||||
|       <td class="text-center">{{formatDate(job.started)}}</td> | ||||
|       <td class="text-center">{{job.duration + " seconds"}}</td> | ||||
|       <td class="text-center hidden-xs">{{job.reason}}</td> | ||||
|      </tr> | ||||
|     </table> | ||||
|    </div></div> | ||||
|   </div> | ||||
|  </nav> | ||||
|  <ol class="breadcrumb"> | ||||
|   <li ng-repeat="n in bc.nodes track by $index"><a href="{{n.href}}">{{n.label}}</a></li> | ||||
|   <li class="active">{{bc.current}}</li> | ||||
|  </ol> | ||||
|  <div ng-view></div> | ||||
|  </div></template> | ||||
|   | ||||
|  <template id="run"><div> | ||||
|   <ol class="breadcrumb"><li><router-link to="/">Home</router-link></li><li><router-link to="/jobs">Jobs</router-link></li><li><router-link :to="'/jobs/'+$route.params.name">{{$route.params.name}}</router-link></li><li class="active">#{{$route.params.number}}</li></ol></ol> | ||||
|   <div class="container-fluid"> | ||||
|    <div class="row"> | ||||
|     <div class="col-sm-5 col-md-6 col-lg-7"> | ||||
|      <h3 style="float:left"><img class="spin" src="/progress.gif" v-show="job.result === 'running'"><span v-html="runIcon(job.result)"></span> {{$route.params.name}} #{{$route.params.number}}</h3> | ||||
|      <nav class="pull-left"> | ||||
|       <ul class="pagination" style="margin:15px 20px"> | ||||
|        <li v-show="$route.params.number > 1"><router-link :to="'/jobs/'+$route.params.name+'/'+($route.params.number-1)">«</router-link></li> | ||||
|        <li v-show="latestNum > $route.params.number"><router-link :to="'/jobs/'+$route.params.name+'/'+(parseInt($route.params.number)+1)">»</router-link></li> | ||||
|       </ul> | ||||
|      </nav> | ||||
|      <div style="clear:both;"></div> | ||||
|      <dl class="dl-horizontal"> | ||||
|       <dt>Reason</dt><dd>{{job.reason}}</dd> | ||||
|       <dt>Queued for</dt><dd>{{job.queued}}s</dd> | ||||
|       <dt>Started</dt><dd>{{formatDate(job.started)}}</dd> | ||||
|       <dt v-show="runComplete(job)">Completed</dt><dd v-show="job.completed">{{formatDate(job.completed)}}</dd> | ||||
|       <dt v-show="runComplete(job)">Duration</dt><dd v-show="runComplete(job)">{{job.duration}}s</dd> | ||||
|      </dl> | ||||
|     </div> | ||||
|     <div class="col-sm-7 col-md-6 col-lg-5"> | ||||
|      <div class="progress" v-show="job.result == 'running'"> | ||||
|       <div class="progress-bar  progress-bar-striped" :class="'progress-bar-'+(job.overtime?'warning':'info')" :class="job.etc?'':'active'" :style="{width:!job.etc?100:job.progress + '%'}"></div> | ||||
|      </div> | ||||
|      <div class="panel panel-default" v-show="job.artifacts.length"> | ||||
|       <div class="panel-heading">Artifacts</div> | ||||
|       <div class="panel-body"> | ||||
|        <ul class="list-unstyled" style="margin-bottom: 0"> | ||||
|         <li v-for="art in job.artifacts"><router-link :to="art.url" target="_self">{{art.filename}}</router-link></li> | ||||
|        </ul> | ||||
|       </div> | ||||
|      </div> | ||||
|     </div> | ||||
|    </div> | ||||
|    <div class="row"><div class="col-xs-12"> | ||||
|     <button type="button" class="btn btn-default btn-xs pull-right" :class="{'active':autoscroll}" v-on:click="autoscroll = !autoscroll" style="margin-top:10px">Autoscroll</button> | ||||
|     <h4>Console output</h4> | ||||
|     <pre v-html="log"></pre> | ||||
|    </div></div> | ||||
|   </div> | ||||
|  </div></template> | ||||
| 
 | ||||
|  <div id="app"> | ||||
|   <nav class="navbar navbar-inverse"> | ||||
|    <div class="container-fluid"> | ||||
|     <div> | ||||
|      <router-link to="/" class="navbar-brand"><img src="/icon.png">{{title}}</router-link> | ||||
|      <router-link to="/jobs" class="btn navbar-btn pull-right">Jobs</router-link> | ||||
|     </div> | ||||
|    </div> | ||||
|   </nav> | ||||
|   <router-view></router-view> | ||||
|  </div> | ||||
| </body> | ||||
| </html> | ||||
| 
 | ||||
|  | ||||
| @ -1,340 +1,445 @@ | ||||
| angular.module('laminar',['ngRoute','ngSanitize']) | ||||
| .config(function($routeProvider, $locationProvider, $sceProvider) { | ||||
| 	$routeProvider | ||||
| 	.when('/', { | ||||
| 		templateUrl: 'tpl/home.html', | ||||
| 		controller: 'mainController' | ||||
| 	}) | ||||
| 	.when('/jobs', { | ||||
| 		templateUrl: 'tpl/browse.html', | ||||
| 		controller: 'BrowseController', | ||||
| 	}) | ||||
| 	.when('/jobs/:name', { | ||||
| 		templateUrl: 'tpl/job.html', | ||||
| 		controller: 'JobController' | ||||
| 	}) | ||||
| 	.when('/jobs/:name/:num', { | ||||
| 		templateUrl: 'tpl/run.html', | ||||
| 		controller: 'RunController' | ||||
| 	}) | ||||
| 	$locationProvider.html5Mode(true); | ||||
| 	$sceProvider.enabled(false); | ||||
| }) | ||||
| .factory('$ws',function($q,$location){ | ||||
| 	return { | ||||
| 		statusListener: function(callbacks) { | ||||
| 			var ws = new WebSocket("ws://" + location.host + $location.path()); | ||||
| 			ws.onmessage = function(message) { | ||||
| 				message = JSON.parse(message.data); | ||||
| 				callbacks[message.type](message.data); | ||||
| 			}; | ||||
| 		}, | ||||
| 		logListener: function(callback) { | ||||
| 			var ws = new WebSocket("ws://" + location.host + $location.path() + '/log'); | ||||
| 			ws.onmessage = function(message) { | ||||
| 				callback(message.data); | ||||
| 			}; | ||||
| 		} | ||||
| 	}; | ||||
| }) | ||||
| .controller('mainController', function($rootScope, $scope, $ws, $interval){ | ||||
| 	$rootScope.bc = { | ||||
| 		nodes: [], | ||||
| 		current: 'Home' | ||||
| 	}; | ||||
| /* laminar.js | ||||
|  * frontend application for Laminar Continuous Integration | ||||
|  * https://laminar.ohwg.net
 | ||||
|  */ | ||||
| const WebsocketHandler = function() { | ||||
|   function setupWebsocket(path, next) { | ||||
|     var ws = new WebSocket("ws://" + window.location.host + path); | ||||
|     ws.onmessage = function(msg) { | ||||
|       msg = JSON.parse(msg.data); | ||||
|       // "status" is the first message the websocket always delivers.
 | ||||
|       // Use this to confirm the navigation. The component is not
 | ||||
|       // created until next() is called, so creating a reference
 | ||||
|       // for other message types must be deferred
 | ||||
|       if (msg.type === 'status') { | ||||
|         next(comp => { | ||||
|           // Set up bidirectional reference
 | ||||
|           // 1. needed to reference the component for other msg types
 | ||||
|           this.comp = comp; | ||||
|           // 2. needed to close the ws on navigation away
 | ||||
|           comp.ws = this; | ||||
|           // Update html and nav titles
 | ||||
|           document.title = comp.$root.title = msg.title; | ||||
|           // Component-specific callback handler
 | ||||
|           comp[msg.type](msg.data); | ||||
|         }); | ||||
|       } else { | ||||
|         // at this point, the component must be defined
 | ||||
|         if (!this.comp) | ||||
|           return console.error("Page component was undefined"); | ||||
|         else if (typeof this.comp[msg.type] === 'function') | ||||
|           this.comp[msg.type](msg.data); | ||||
|       } | ||||
|     }; | ||||
|   }; | ||||
|   return { | ||||
|     beforeRouteEnter(to, from, next) { | ||||
|       setupWebsocket(to.path, (fn) => { next(fn); }); | ||||
|     }, | ||||
|     beforeRouteUpdate(to, from, next) { | ||||
|       this.ws.close(); | ||||
|       setupWebsocket(to.path, (fn) => { fn(this); next(); }); | ||||
|     }, | ||||
|     beforeRouteLeave(to, from, next) { | ||||
|       this.ws.close(); | ||||
|       next(); | ||||
|     }, | ||||
|   }; | ||||
| }(); | ||||
| 
 | ||||
| 	$scope.jobsQueued = []; | ||||
| 	$scope.jobsRunning = []; | ||||
| 	$scope.jobsRecent = []; | ||||
| 	var chtUtilization, chtBuildsPerDay, chtBuildsPerJob, chtTimePerJob; | ||||
| 	 | ||||
| 	var updateUtilization = function(busy) { | ||||
| 		chtUtilization.segments[0].value += busy ? 1 : -1; | ||||
| 		chtUtilization.segments[1].value -= busy ? 1 : -1; | ||||
| 		chtUtilization.update(); | ||||
| 	} | ||||
| 			 | ||||
| 	$ws.statusListener({ | ||||
| 		status: function(data) { | ||||
| 			$rootScope.title = data.title; | ||||
| 			// populate jobs
 | ||||
| 			$scope.jobsQueued = data.queued; | ||||
| 			data.running.forEach($rootScope.updateProgress); | ||||
| 			$scope.jobsRunning = data.running; | ||||
| 			$scope.jobsRecent = data.recent; | ||||
| 			$scope.$apply(); | ||||
| 			 | ||||
| 			// setup charts
 | ||||
| 			chtUtilization = new Chart(document.getElementById("chartUtil").getContext("2d")).Pie( | ||||
| 				[{value: data.executorsBusy, color:"tan", label: "Busy"}, | ||||
| 				 {value: data.executorsTotal, color: "darkseagreen", label: "Idle"}], | ||||
| 				{animationEasing: 'easeInOutQuad'} | ||||
| 			); | ||||
| 			chtBuildsPerDay = new Chart(document.getElementById("chartBpd").getContext("2d")).Line({ | ||||
| 				labels: function(){ | ||||
| 					res = []; | ||||
| 					var now = new Date(); | ||||
| 					for(var i = 6; i >= 0; --i) { | ||||
| 						var then = new Date(now.getTime() - i*86400000); | ||||
| 						res.push(["Sun","Mon","Tue","Wed","Thu","Fri","Sat"][then.getDay()]); | ||||
| 					} | ||||
| 					return res; | ||||
| 				}(), | ||||
| 				datasets: [{ | ||||
| 					label: "Successful Builds", | ||||
| 					fillColor: "darkseagreen", | ||||
| 					strokeColor: "forestgreen", | ||||
| 					data: data.buildsPerDay.map(function(e){return e.success||0;}) | ||||
| 				},{ | ||||
| 					label: "Failed Bulids", | ||||
| 					fillColor: "darksalmon", | ||||
| 					strokeColor: "crimson", | ||||
| 					data: data.buildsPerDay.map(function(e){return e.failed||0;}) | ||||
| 				}]}, | ||||
| 				{ showTooltips: false } | ||||
| 			); | ||||
| 			chtBuildsPerJob = new Chart(document.getElementById("chartBpj").getContext("2d")).HorizontalBar({ | ||||
| 				labels: Object.keys(data.buildsPerJob), | ||||
| 				datasets: [{ | ||||
| 					fillColor: "lightsteelblue", | ||||
| 					data: Object.keys(data.buildsPerJob).map(function(e){return data.buildsPerJob[e];}) | ||||
| 				}] | ||||
| 			},{}); | ||||
| 			chtTimePerJob = new Chart(document.getElementById("chartTpj").getContext("2d")).HorizontalBar({ | ||||
| 				labels: Object.keys(data.timePerJob), | ||||
| 				datasets: [{ | ||||
| 					fillColor: "lightsteelblue", | ||||
| 					data: Object.keys(data.timePerJob).map(function(e){return data.timePerJob[e];}) | ||||
| 				}] | ||||
| 			},{}); | ||||
| 		}, | ||||
| 		job_queued: function(data) { | ||||
| 			$scope.jobsQueued.splice(0,0,data); | ||||
| 			$scope.$apply(); | ||||
| 		}, | ||||
| 		job_started: function(data) { | ||||
| 			$scope.jobsQueued.splice($scope.jobsQueued.length - data.queueIndex - 1,1); | ||||
| 			$scope.jobsRunning.splice(0,0,data); | ||||
| 			$scope.$apply(); | ||||
| 			updateUtilization(true); | ||||
| 		}, | ||||
| 		job_completed: function(data) { | ||||
| 			if(data.result === "success") | ||||
| 				chtBuildsPerDay.datasets[0].points[6].value++; | ||||
| 			else | ||||
| 				chtBuildsPerDay.datasets[1].points[6].value++; | ||||
| 			chtBuildsPerDay.update(); | ||||
| const Utils = { | ||||
|   methods: { | ||||
|     runIcon(result) { | ||||
|       return result === "success" ? '<span style="color:forestgreen;font-family:\'Zapf Dingbats\';">✔</span>' : result === "failed" || result === "aborted" ? '<span style="color:crimson;">✘</span>' : ''; | ||||
|     }, | ||||
|     formatDate: function(unix) { | ||||
|       // TODO: reimplement when toLocaleDateString() accepts formatting options on most browsers
 | ||||
|       var d = new Date(1000 * unix); | ||||
|       var m = d.getMinutes(); | ||||
|       if (m < 10) m = '0' + m; | ||||
|       return d.getHours() + ':' + m + ' on ' + ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'][d.getDay()] + ' ' + | ||||
|         d.getDate() + '. ' + ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', | ||||
|           'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec' | ||||
|         ][d.getMonth()] + ' ' + | ||||
|         d.getFullYear(); | ||||
|     }, | ||||
|   } | ||||
| }; | ||||
| 
 | ||||
| 			for(var i = 0; i < $scope.jobsRunning.length; ++i) { | ||||
| 				var job = $scope.jobsRunning[i]; | ||||
| 				if(job.name == data.name && job.number == data.number) { | ||||
| 					$scope.jobsRunning.splice(i,1); | ||||
| 					$scope.jobsRecent.splice(0,0,data); | ||||
| 					$scope.$apply(); | ||||
| 					 | ||||
| 					break; | ||||
| 				} | ||||
| 			} | ||||
| 			updateUtilization(false); | ||||
| 			for(var j = 0; j < chtBuildsPerJob.datasets[0].bars.length; ++j) { | ||||
| 				if(chtBuildsPerJob.datasets[0].bars[j].label == job.name) { | ||||
| 					chtBuildsPerJob.datasets[0].bars[j].value++; | ||||
| 					chtBuildsPerJob.update(); | ||||
| 					break; | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
| 	}); | ||||
| 	var timeUpdater = $interval(function() { | ||||
| 		$scope.jobsRunning.forEach($rootScope.updateProgress); | ||||
| 	}, 1000); | ||||
| 	$scope.$on('$destroy', function() { | ||||
| 		$interval.cancel(timeUpdater); | ||||
| 	}); | ||||
| }) | ||||
| .controller('BrowseController', function($rootScope, $scope, $ws, $interval){ | ||||
| 	$rootScope.bc = { | ||||
| 		nodes: [{ href: '/', label: 'Home' }], | ||||
| 		current: 'Jobs' | ||||
| 	}; | ||||
| const ProgressUpdater = { | ||||
|   data() { return { jobsRunning: [] }; }, | ||||
|   methods: { | ||||
|     updateProgress(o) { | ||||
|       if (o.etc) { | ||||
|         var p = ((new Date()).getTime() / 1000 - o.started) / (o.etc - o.started); | ||||
|         if (p > 1.2) { | ||||
|           o.overtime = true; | ||||
|         } else if (p >= 1) { | ||||
|           o.progress = 99; | ||||
|         } else { | ||||
|           o.progress = 100 * p; | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   }, | ||||
|   beforeDestroy() { | ||||
|     clearInterval(this.updateTimer); | ||||
|   }, | ||||
|   watch: { | ||||
|     jobsRunning(val) { | ||||
|       // this function handles several cases:
 | ||||
|       // - the route has changed to a different run of the same job
 | ||||
|       // - the current job has ended
 | ||||
|       // - the current job has started (practically hard to reach)
 | ||||
|       clearInterval(this.updateTimer); | ||||
|       if (val.length) { | ||||
|         // TODO: first, a non-animated progress update
 | ||||
|         this.updateTimer = setInterval(() => { | ||||
|           this.jobsRunning.forEach(this.updateProgress); | ||||
|           this.$forceUpdate(); | ||||
|         }, 1000); | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| }; | ||||
| 
 | ||||
| 	$scope.currentTag = null; | ||||
| 	$scope.activeTag = function(t) { | ||||
| 		return $scope.currentTag === t; | ||||
| 	}; | ||||
| 	$scope.bytag = function(job) { | ||||
| 		if($scope.currentTag === null) return true; | ||||
| 		return job.tags.indexOf($scope.currentTag) >= 0; | ||||
| 	}; | ||||
| const Home = function() { | ||||
|   var state = { | ||||
|     jobsQueued: [], | ||||
|     jobsRecent: [] | ||||
|   }; | ||||
| 
 | ||||
| 	$scope.jobs = []; | ||||
| 	$ws.statusListener({ | ||||
| 		status: function(data) { | ||||
| 			$rootScope.title = data.title; | ||||
| 			$scope.jobs = data.jobs; | ||||
| 			var tags = {}; | ||||
| 			for(var i in data.jobs) { | ||||
| 				for(var j in data.jobs[i].tags) { | ||||
| 					tags[data.jobs[i].tags[j]] = true; | ||||
| 				} | ||||
| 			} | ||||
| 			$scope.tags = Object.keys(tags); | ||||
| 			$scope.$apply(); | ||||
| 		}, | ||||
| 		job_completed: function(data) { | ||||
| 			for(var i in $scope.jobs) { | ||||
| 				if($scope.jobs[i].name === data.name) { | ||||
| 					$scope.jobs[i] = data; | ||||
| 					$scope.$apply; | ||||
| 					break; | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
| 	}); | ||||
| }) | ||||
| .controller('JobController', function($rootScope, $scope, $routeParams, $ws) { | ||||
| 	$rootScope.bc = { | ||||
| 		nodes: [{ href: '/', label: 'Home' },{ href: '/jobs', label: 'Jobs' }], | ||||
| 		current: $routeParams.name | ||||
| 	}; | ||||
| 	 | ||||
| 	$scope.name = $routeParams.name; | ||||
| 	$scope.jobsRunning = []; | ||||
| 	$scope.jobsRecent = []; | ||||
|   var chtUtilization, chtBuildsPerDay, chtBuildsPerJob, chtTimePerJob; | ||||
| 
 | ||||
| 	$ws.statusListener({ | ||||
| 		status: function(data) { | ||||
| 			$rootScope.title = data.title; | ||||
|   var updateUtilization = function(busy) { | ||||
|     chtUtilization.segments[0].value += busy ? 1 : -1; | ||||
|     chtUtilization.segments[1].value -= busy ? 1 : -1; | ||||
|     chtUtilization.update(); | ||||
|   } | ||||
| 
 | ||||
| 			$scope.jobsRunning = data.running; | ||||
| 			$scope.jobsRecent = data.recent; | ||||
| 			$scope.lastSuccess = data.lastSuccess; | ||||
| 			$scope.lastFailed = data.lastFailed; | ||||
| 			$scope.$apply(); | ||||
| 			 | ||||
| 			var chtBt = new Chart(document.getElementById("chartBt").getContext("2d")).Bar({ | ||||
| 				labels: data.recent.map(function(e){return '#' + e.number;}).reverse(), | ||||
| 				datasets: [{ | ||||
| 					fillColor: "darkseagreen", | ||||
| 					strokeColor: "forestgreen", | ||||
| 					data: data.recent.map(function(e){return e.duration;}).reverse() | ||||
| 				}] | ||||
| 			}, | ||||
| 			{barValueSpacing: 1,barStrokeWidth: 1,barDatasetSpacing:0} | ||||
| 			); | ||||
|   return { | ||||
|     template: '#home', | ||||
|     mixins: [WebsocketHandler, Utils, ProgressUpdater], | ||||
|     data: function() { | ||||
|       return state; | ||||
|     }, | ||||
|     methods: { | ||||
|       status: function(msg) { | ||||
|         state.jobsQueued = msg.queued; | ||||
|         state.jobsRunning = msg.running; | ||||
|         state.jobsRecent = msg.recent; | ||||
|         this.$forceUpdate(); | ||||
| 
 | ||||
| 			for(var i = 0, n = data.recent.length; i < n; ++i) { | ||||
| 				if(data.recent[i].result != "success") { | ||||
| 					chtBt.datasets[0].bars[n-i-1].fillColor = "darksalmon"; | ||||
| 					chtBt.datasets[0].bars[n-i-1].strokeColor = "crimson"; | ||||
| 				} | ||||
| 			} | ||||
| 			chtBt.update(); | ||||
| 			 | ||||
| 		}, | ||||
| 		job_queued: function() { | ||||
| 			$scope.nQueued++; | ||||
| 		}, | ||||
| 		job_started: function(data) { | ||||
| 			$scope.nQueued--; | ||||
| 			$scope.jobsRunning.splice(0,0,data); | ||||
| 			$scope.$apply(); | ||||
| 		}, | ||||
| 		job_completed: function(data) { | ||||
| 			for(var i = 0; i < $scope.jobsRunning.length; ++i) { | ||||
| 				var job = $scope.jobsRunning[i]; | ||||
| 				if(job.number === data.number) { | ||||
| 					$scope.jobsRunning.splice(i,1); | ||||
| 					$scope.jobsRecent.splice(0,0,data); | ||||
| 					$scope.$apply(); | ||||
| 					break; | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
| 	}); | ||||
| }) | ||||
| .controller('RunController', function($rootScope, $scope, $routeParams, $ws, $interval) { | ||||
| 	$rootScope.bc = { | ||||
| 		nodes: [{ href: '/', label: 'Home' }, | ||||
| 		{ href: '/jobs', label: 'Jobs' }, | ||||
| 		{ href: '/jobs/'+$routeParams.name, label: $routeParams.name } | ||||
| 		], | ||||
| 		current: '#' + $routeParams.num | ||||
| 	}; | ||||
|         // setup charts
 | ||||
|         chtUtilization = new Chart(document.getElementById("chartUtil").getContext("2d")).Pie( | ||||
|           [{ | ||||
|               value: msg.executorsBusy, | ||||
|               color: "tan", | ||||
|               label: "Busy" | ||||
|             }, | ||||
|             { | ||||
|               value: msg.executorsTotal, | ||||
|               color: "darkseagreen", | ||||
|               label: "Idle" | ||||
|             } | ||||
|           ], { | ||||
|             animationEasing: 'easeInOutQuad' | ||||
|           } | ||||
|         ); | ||||
|         chtBuildsPerDay = new Chart(document.getElementById("chartBpd").getContext("2d")).Line({ | ||||
|           labels: function() { | ||||
|             res = []; | ||||
|             var now = new Date(); | ||||
|             for (var i = 6; i >= 0; --i) { | ||||
|               var then = new Date(now.getTime() - i * 86400000); | ||||
|               res.push(["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"][then.getDay()]); | ||||
|             } | ||||
|             return res; | ||||
|           }(), | ||||
|           datasets: [{ | ||||
|             label: "Successful Builds", | ||||
|             fillColor: "darkseagreen", | ||||
|             strokeColor: "forestgreen", | ||||
|             data: msg.buildsPerDay.map(function(e) { | ||||
|               return e.success || 0; | ||||
|             }) | ||||
|           }, { | ||||
|             label: "Failed Bulids", | ||||
|             fillColor: "darksalmon", | ||||
|             strokeColor: "crimson", | ||||
|             data: msg.buildsPerDay.map(function(e) { | ||||
|               return e.failed || 0; | ||||
|             }) | ||||
|           }] | ||||
|         }, { | ||||
|           showTooltips: false | ||||
|         }); | ||||
|         chtBuildsPerJob = new Chart(document.getElementById("chartBpj").getContext("2d")).HorizontalBar({ | ||||
|           labels: Object.keys(msg.buildsPerJob), | ||||
|           datasets: [{ | ||||
|             fillColor: "lightsteelblue", | ||||
|             data: Object.keys(msg.buildsPerJob).map(function(e) { | ||||
|               return msg.buildsPerJob[e]; | ||||
|             }) | ||||
|           }] | ||||
|         }, {}); | ||||
|         chtTimePerJob = new Chart(document.getElementById("chartTpj").getContext("2d")).HorizontalBar({ | ||||
|           labels: Object.keys(msg.timePerJob), | ||||
|           datasets: [{ | ||||
|             fillColor: "lightsteelblue", | ||||
|             data: Object.keys(msg.timePerJob).map(function(e) { | ||||
|               return msg.timePerJob[e]; | ||||
|             }) | ||||
|           }] | ||||
|         }, {}); | ||||
| 
 | ||||
| 	$scope.name = $routeParams.name; | ||||
| 	$scope.num = parseInt($routeParams.num); | ||||
| 
 | ||||
| 	$ws.statusListener({ | ||||
| 		status: function(data) { | ||||
| 			$rootScope.title = data.title; | ||||
| 			$rootScope.updateProgress(data); | ||||
| 			$scope.job = data; | ||||
| 			$scope.latestNum = data.latestNum; | ||||
| 			$scope.$apply(); | ||||
| 		}, | ||||
| 		job_started: function() { | ||||
| 			$scope.latestNum++; | ||||
| 			$scope.$apply(); | ||||
| 		}, | ||||
| 		job_completed: function(data) { | ||||
| 			$scope.job = data; | ||||
| 			$scope.$apply(); | ||||
| 		} | ||||
| 	}); | ||||
| 	 | ||||
| 	$scope.log = "" | ||||
| 	$scope.autoscroll = false; | ||||
| 	var firstLog = false; | ||||
| 	$ws.logListener(function(data) { | ||||
| 		$scope.log += ansi_up.ansi_to_html(data.replace('<','<').replace('>','>')); | ||||
| 		$scope.$apply(); | ||||
| 		if(!firstLog) { | ||||
| 			firstLog = true; | ||||
| 		} else if($scope.autoscroll) { | ||||
| 			window.scrollTo(0, document.body.scrollHeight); | ||||
| 		} | ||||
| 	}); | ||||
| 	 | ||||
| 	var timeUpdater = $interval(function() { | ||||
| 		$rootScope.updateProgress($scope.job); | ||||
| 	}, 1000); | ||||
| 	$scope.$on('$destroy', function() { | ||||
| 		$interval.cancel(timeUpdater); | ||||
| 	}); | ||||
| }) | ||||
| .run(function($rootScope) { | ||||
| 	angular.extend($rootScope, { | ||||
| 		runIcon: function(result) { | ||||
| 			return result === "success" ? '<span style="color:forestgreen;font-family:\'Zapf Dingbats\';">✔</span>' : result === "failed" || result === "aborted" ? '<span style="color:crimson;">✘</span>' : ''; | ||||
| 		}, | ||||
| 		runComplete: function(run) { | ||||
| 			return !!run && (run.result === 'aborted' || run.result === 'failed' || run.result === 'success'); | ||||
| 		}, | ||||
| 		formatDate: function(unix) { | ||||
| 			// TODO reimplement when toLocaleDateString() accepts formatting
 | ||||
| 			// options on most browsers
 | ||||
| 			var d = new Date(1000 * unix); | ||||
| 			var m = d.getMinutes(); | ||||
| 			if(m < 10) m = '0' + m; | ||||
| 			return d.getHours() + ':' + m + ' on ' +  | ||||
| 				['Sun','Mon','Tue','Wed','Thu','Fri','Sat'][d.getDay()] + ' ' | ||||
| 				+ d.getDate() + '. ' + ['Jan','Feb','Mar','Apr','May','Jun', | ||||
| 				'Jul','Aug','Sep', 'Oct','Nov','Dec'][d.getMonth()] + ' ' | ||||
| 				+ d.getFullYear(); | ||||
| 		}, | ||||
| 		updateProgress: function(o){ | ||||
| 			if(o.etc) { | ||||
| 				var d = new Date(); | ||||
| 				var p = (d.getTime()/1000 - o.started) / (o.etc - o.started); | ||||
| 				if(p > 1.2) { | ||||
| 					o.overtime = true; | ||||
| 				} else if(p >= 1) { | ||||
| 					o.progress = 99; | ||||
| 				} else { | ||||
| 					o.progress = 100 * p; | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
| 	}); | ||||
|       }, | ||||
|       job_queued: function(data) { | ||||
|         state.jobsQueued.splice(0, 0, data); | ||||
|         this.$forceUpdate(); | ||||
|       }, | ||||
|       job_started: function(data) { | ||||
|         state.jobsQueued.splice(state.jobsQueued.length - data.queueIndex - 1, 1); | ||||
|         state.jobsRunning.splice(0, 0, data); | ||||
|         this.$forceUpdate(); | ||||
|         updateUtilization(true); | ||||
|       }, | ||||
|       job_completed: function(data) { | ||||
|         if (data.result === "success") | ||||
|           chtBuildsPerDay.datasets[0].points[6].value++; | ||||
|         else | ||||
|           chtBuildsPerDay.datasets[1].points[6].value++; | ||||
|         chtBuildsPerDay.update(); | ||||
| 
 | ||||
|         for (var i = 0; i < state.jobsRunning.length; ++i) { | ||||
|           var job = state.jobsRunning[i]; | ||||
|           if (job.name == data.name && job.number == data.number) { | ||||
|             state.jobsRunning.splice(i, 1); | ||||
|             state.jobsRecent.splice(0, 0, data); | ||||
|             this.$forceUpdate(); | ||||
|             break; | ||||
|           } | ||||
|         } | ||||
|         updateUtilization(false); | ||||
|         for (var j = 0; j < chtBuildsPerJob.datasets[0].bars.length; ++j) { | ||||
|           if (chtBuildsPerJob.datasets[0].bars[j].label == job.name) { | ||||
|             chtBuildsPerJob.datasets[0].bars[j].value++; | ||||
|             chtBuildsPerJob.update(); | ||||
|             break; | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   }; | ||||
| }(); | ||||
| 
 | ||||
| const Jobs = function() { | ||||
|   var state = { | ||||
|     jobs: [], | ||||
|     search: '', | ||||
|     tags: [], | ||||
|     tag: null | ||||
|   }; | ||||
|   return { | ||||
|     template: '#jobs', | ||||
|     mixins: [WebsocketHandler, Utils], | ||||
|     data: function() { return state; }, | ||||
|     computed: { | ||||
|       filteredJobs() { | ||||
|         var ret = this.jobs; | ||||
|         var tag = this.tag; | ||||
|         if (tag) { | ||||
|           ret = ret.filter(function(job) { | ||||
|             return job.tags.indexOf(tag) >= 0; | ||||
|           }); | ||||
|         } | ||||
|         var search = this.search; | ||||
|         if (search) { | ||||
|           ret = ret.filter(function(job) { | ||||
|             return job.name.indexOf(search) > -1; | ||||
|           }); | ||||
|         } | ||||
|         return ret; | ||||
|       } | ||||
|     }, | ||||
|     methods: { | ||||
|       status: function(msg) { | ||||
|         state.jobs = msg.jobs; | ||||
|         var tags = {}; | ||||
|         for (var i in state.jobs) { | ||||
|           for (var j in state.jobs[i].tags) { | ||||
|             tags[state.jobs[i].tags[j]] = true; | ||||
|           } | ||||
|         } | ||||
|         state.tags = Object.keys(tags); | ||||
|       }, | ||||
|       job_completed: function(data) { | ||||
|         for (var i in state.jobs) { | ||||
|           if (state.jobs[i].name === data.name) { | ||||
|             state.jobs[i] = data; | ||||
|             this.$forceUpdate(); | ||||
|             break; | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   }; | ||||
| }(); | ||||
| 
 | ||||
| var Job = function() { | ||||
|   var state = { | ||||
|     jobsRunning: [], | ||||
|     jobsRecent: [], | ||||
|     lastSuccess: null, | ||||
|     lastFailed: null, | ||||
|     nQueued: 0, | ||||
|   }; | ||||
|   return Vue.extend({ | ||||
|     template: '#job', | ||||
|     mixins: [WebsocketHandler, Utils], | ||||
|     data: function() { | ||||
|       return state; | ||||
|     }, | ||||
|     methods: { | ||||
|       status: function(msg) { | ||||
|         state.jobsRunning = msg.running; | ||||
|         state.jobsRecent = msg.recent; | ||||
|         state.lastSuccess = msg.lastSuccess; | ||||
|         state.lastFailed = msg.lastFailed; | ||||
| 
 | ||||
|         var chtBt = new Chart(document.getElementById("chartBt").getContext("2d")).Bar({ | ||||
|           labels: msg.recent.map(function(e) { | ||||
|             return '#' + e.number; | ||||
|           }).reverse(), | ||||
|           datasets: [{ | ||||
|             fillColor: "darkseagreen", | ||||
|             strokeColor: "forestgreen", | ||||
|             data: msg.recent.map(function(e) { | ||||
|               return e.duration; | ||||
|             }).reverse() | ||||
|           }] | ||||
|         }, { | ||||
|           barValueSpacing: 1, | ||||
|           barStrokeWidth: 1, | ||||
|           barDatasetSpacing: 0 | ||||
|         }); | ||||
| 
 | ||||
|         for (var i = 0, n = msg.recent.length; i < n; ++i) { | ||||
|           if (msg.recent[i].result != "success") { | ||||
|             chtBt.datasets[0].bars[n - i - 1].fillColor = "darksalmon"; | ||||
|             chtBt.datasets[0].bars[n - i - 1].strokeColor = "crimson"; | ||||
|           } | ||||
|         } | ||||
|         chtBt.update(); | ||||
| 
 | ||||
|       }, | ||||
|       job_queued: function() { | ||||
|         state.nQueued++; | ||||
|       }, | ||||
|       job_started: function(data) { | ||||
|         state.nQueued--; | ||||
|         state.jobsRunning.splice(0, 0, data); | ||||
|         this.$forceUpdate(); | ||||
|       }, | ||||
|       job_completed: function(data) { | ||||
|         for (var i = 0; i < state.jobsRunning.length; ++i) { | ||||
|           var job = state.jobsRunning[i]; | ||||
|           if (job.number === data.number) { | ||||
|             state.jobsRunning.splice(i, 1); | ||||
|             state.jobsRecent.splice(0, 0, data); | ||||
|             this.$forceUpdate(); | ||||
|             // TODO: update the chart
 | ||||
|             break; | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   }); | ||||
| }(); | ||||
| 
 | ||||
| const Run = function() { | ||||
|   var state = { | ||||
|     job: { artifacts: [] }, | ||||
|     latestNum: null, | ||||
|     log: '', | ||||
|     autoscroll: false | ||||
|   }; | ||||
|   var firstLog = false; | ||||
|   var logHandler = function(vm, d) { | ||||
|     state.log += d; | ||||
|     vm.$forceUpdate(); | ||||
|     if (!firstLog) { | ||||
|       firstLog = true; | ||||
|     } else if (state.autoscroll) { | ||||
|       window.scrollTo(0, document.body.scrollHeight); | ||||
|     } | ||||
|   }; | ||||
| 
 | ||||
|   return { | ||||
|     template: '#run', | ||||
|     mixins: [WebsocketHandler, Utils, ProgressUpdater], | ||||
|     data: function() { | ||||
|       return state; | ||||
|     }, | ||||
|     methods: { | ||||
|       status: function(data) { | ||||
|         state.log = ''; | ||||
|         state.job = data; | ||||
|         state.latestNum = data.latestNum; | ||||
|         if (!!state.job.etc) | ||||
|           state.jobsRunning = [data]; | ||||
|       }, | ||||
|       job_started: function(data) { | ||||
|         state.latestNum++; | ||||
|         this.$forceUpdate(); | ||||
|       }, | ||||
|       job_completed: function(data) { | ||||
|         state.job = data; | ||||
|         state.jobsRunning = []; | ||||
|         this.$forceUpdate(); | ||||
|       }, | ||||
|       runComplete: function(run) { | ||||
|         return !!run && (run.result === 'aborted' || run.result === 'failed' || run.result === 'success'); | ||||
|       }, | ||||
|     }, | ||||
|     beforeRouteEnter(to, from, next) { | ||||
|       next(vm => { | ||||
|         vm.logws = new WebSocket("ws://" + location.host + to.path + '/log'); | ||||
|         vm.logws.onmessage = function(msg) { | ||||
|           logHandler(vm, msg.data); | ||||
|         } | ||||
|       }); | ||||
|     }, | ||||
|     beforeRouteUpdate(to, from, next) { | ||||
|       var vm = this; | ||||
|       state.jobsRunning = []; | ||||
|       vm.logws.close(); | ||||
|       vm.logws = new WebSocket("ws://" + location.host + to.path + '/log'); | ||||
|       vm.logws.onmessage = function(msg) { | ||||
|         logHandler(vm, msg.data); | ||||
|       } | ||||
|       next(); | ||||
|     }, | ||||
|     beforeRouteLeave(to, from, next) { | ||||
|       this.logws.close(); | ||||
|       next(); | ||||
|     } | ||||
|   }; | ||||
| }(); | ||||
| 
 | ||||
| new Vue({ | ||||
|   el: '#app', | ||||
|   data: { | ||||
|     title: '' // populated by status ws message
 | ||||
|   }, | ||||
|   router: new VueRouter({ | ||||
|     mode: 'history', | ||||
|     routes: [ | ||||
|       { path: '/',                   component: Home }, | ||||
|       { path: '/jobs',               component: Jobs }, | ||||
|       { path: '/jobs/:name',         component: Job }, | ||||
|       { path: '/jobs/:name/:number', component: Run } | ||||
|     ], | ||||
|   }), | ||||
| }); | ||||
|  | ||||
							
								
								
									
										
											BIN
										
									
								
								src/resources/progress.gif
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								src/resources/progress.gif
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 2.5 KiB | 
| @ -1,21 +0,0 @@ | ||||
| <div class="container-fluid"> | ||||
| <div class="row"> | ||||
| <div class="col-xs-12"> | ||||
|  <div class="pull-right"> | ||||
|   <input class="form-control" id="jobFilter" ng-model="search.name" placeholder="Filter..."> | ||||
|  </div> | ||||
|  <ul class="nav nav-tabs"> | ||||
|   <li ng-class="{active:activeTag(null)}"><a href ng-click="currentTag = null">All Jobs</a></li> | ||||
|   <li ng-repeat="tag in tags" ng-class="{active:activeTag(tag)}"><a href ng-click="$parent.currentTag = tag">{{tag}}</a></li> | ||||
|  </ul> | ||||
|  <style>table#joblist tr:first-child td { border-top: 0; }</style> | ||||
|  <table class="table table-striped" id="joblist"> | ||||
|   <tr class="animate-repeat" ng-repeat="job in jobs | filter:bytag | filter:search"> | ||||
|    <td><a href="jobs/{{job.name}}">{{job.name}}</a></td> | ||||
|    <td class="text-center"><span ng-bind-html="runIcon(job.result)"></span> <a href="jobs/{{job.name}}/{{job.number}}">#{{job.number}}</a></td> | ||||
|    <td class="text-center">{{formatDate(job.started)}}</a></td> | ||||
|   </tr> | ||||
|  </table> | ||||
| </div> | ||||
| </div> | ||||
| </div> | ||||
| @ -1,58 +0,0 @@ | ||||
| <div class="container-fluid"> | ||||
| <div class="row"> | ||||
| <div class="col-sm-5 col-md-4 col-lg-3 dash"> | ||||
|  <table class="table table-bordered"> | ||||
|   <tr class="animate-repeat" ng-repeat="job in jobsQueued track by $index"> | ||||
|    <td><a href="jobs/{{job.name}}">{{job.name}}</a> <i>queued</i></td> | ||||
|   </tr> | ||||
|   <tr class="animate-repeat" ng-repeat="job in jobsRunning track by $index"> | ||||
|    <td><img class="spin small" src="/progress.png"> <a href="jobs/{{job.name}}">{{job.name}}</a> <a href="jobs/{{job.name}}/{{job.number}}">#{{job.number}}</a> <div class="progress"> | ||||
|     <div class="progress-bar progress-bar-{{job.overtime?'warning':'info'}} progress-bar-striped {{job.etc?'':'active'}}" style="width:{{!job.etc?'100':job.progress}}%"></div> | ||||
|     </div> | ||||
|    </td> | ||||
|   </tr> | ||||
|   <tr class="animate-repeat" ng-repeat="job in jobsRecent track by $index"> | ||||
|    <td><span ng-bind-html="runIcon(job.result)"></span> <a href="jobs/{{job.name}}">{{job.name}}</a> <a href="jobs/{{job.name}}/{{job.number}}">#{{job.number}}</a><br><small>Took {{job.duration}}s at {{formatDate(job.started)}}</small></td> | ||||
|   </tr> | ||||
|  </table> | ||||
| </div> | ||||
| <div class="col-sm-7 col-md-8 col-lg-9"> | ||||
| <div class="row"> | ||||
| <div class="col-md-6"> | ||||
|  <div class="panel panel-default"> | ||||
|   <div class="panel-heading">Total builds per day this week</div> | ||||
|   <div class="panel-body"> | ||||
|    <canvas id="chartBpd"></canvas> | ||||
|   </div> | ||||
|  </div> | ||||
| </div> | ||||
| <div class="col-md-6"> | ||||
|  <div class="panel panel-default"> | ||||
|   <div class="panel-heading">Builds per job in the last 24 hours</div> | ||||
|   <div class="panel-body" id="chartStatus"> | ||||
|    <canvas id="chartBpj"></canvas> | ||||
|   </div> | ||||
|  </div> | ||||
| </div> | ||||
| <div class="col-md-6"> | ||||
|  <div class="panel panel-default"> | ||||
|   <div class="panel-heading">Average build time per job this week</div> | ||||
|   <div class="panel-body"> | ||||
|    <canvas id="chartTpj"></canvas> | ||||
|   </div> | ||||
| 
 | ||||
|  </div> | ||||
| </div> | ||||
| <div class="col-md-6"> | ||||
|  <div class="panel panel-default"> | ||||
|   <div class="panel-heading">Current executor utilization</div> | ||||
|   <div class="panel-body"> | ||||
|    <canvas id="chartUtil"></canvas> | ||||
|   </div> | ||||
|  </div> | ||||
| </div> | ||||
| </div> | ||||
| </div> | ||||
| </div> | ||||
| </div> | ||||
| 
 | ||||
| @ -1,51 +0,0 @@ | ||||
| <div class="container-fluid"> | ||||
| <div class="row"> | ||||
| 	 | ||||
|  <div class="col-sm-5 col-md-6 col-lg-7"> | ||||
|   <h3>{{name}}</h3> | ||||
|   <dl class="dl-horizontal"> | ||||
|    <dt>Last Successful Run</dt><dd> | ||||
| 	   <a ng-show="lastSuccess" href="jobs/{{name}}/{{lastSuccess.number}}">#{{lastSuccess.number}}</a>    | ||||
| 	   {{lastSuccess?" - at "+formatDate(lastSuccess.started):"never"}}</dd> | ||||
|      <dt>Last Failed Run</dt><dd> | ||||
| 	   <a ng-show="lastFailed" href="jobs/{{name}}/{{lastFailed.number}}">#{{lastFailed.number}}</a>    | ||||
| 	   {{lastFailed?" - at "+formatDate(lastFailed.started):"never"}}</dd> | ||||
|   </dl> | ||||
| 
 | ||||
|  </div> | ||||
|  <div class="col-sm-7 col-md-6 col-lg-5"> | ||||
|   <div class="panel panel-default"> | ||||
|    <div class="panel-heading">Build time</div> | ||||
|    <div class="panel-body"> | ||||
|     <canvas id="chartBt"></canvas> | ||||
|    </div> | ||||
|   </div> | ||||
|  </div> | ||||
|   | ||||
| </div> | ||||
| <div class="row"> | ||||
| 
 | ||||
|  <div class="col-xs-12"> | ||||
|   <table class="table table-striped"><thead> | ||||
|    <tr><th>Run</th><th class="text-center">Started</th><th class="text-center">Duration</th><th class="text-center hidden-xs">Reason</th></tr></thead> | ||||
|    <tr ng-show="nQueued"> | ||||
|     <td colspan="4"><i>{{nQueued}} run(s) queued</i></td> | ||||
|    </tr> | ||||
|    <tr class="animate-repeat" ng-repeat="job in jobsRunning track by $index"> | ||||
|     <td><img class="spin small" src="/progress.png"> <a href="jobs/{{name}}/{{job.number}}">#{{job.number}}</a></td> | ||||
|     <td class="text-center">{{formatDate(job.started)}}</td> | ||||
|     <td class="text-center">--</td> | ||||
|     <td class="text-center hidden-xs">{{job.reason}}</td> | ||||
|    </tr> | ||||
|    <tr class="animate-repeat" ng-repeat="job in jobsRecent track by $index"> | ||||
|     <td><span ng-bind-html="runIcon(job.result)"></span> <a href="jobs/{{name}}/{{job.number}}">#{{job.number}}</a></td> | ||||
|     <td class="text-center">{{formatDate(job.started)}}</td> | ||||
|     <td class="text-center">{{job.duration + " seconds"}}</td> | ||||
|     <td class="text-center hidden-xs">{{job.reason}}</td> | ||||
|    </tr> | ||||
|   </table> | ||||
|  </div> | ||||
|   | ||||
| </div> | ||||
| </div> | ||||
| 
 | ||||
| @ -1,43 +0,0 @@ | ||||
| <div class="container-fluid"> | ||||
| <div class="row"> | ||||
| <div class="col-sm-5 col-md-6 col-lg-7"> | ||||
|  <h3 style="float:left"><img class="spin" src="/progress.png" ng-show="job.result === 'running'"><span ng-bind-html="runIcon(job.result)"></span> {{name}} #{{num}}</h3> | ||||
|  <nav class="pull-left"> | ||||
|   <ul class="pagination" style="margin:15px 20px"> | ||||
|    <li ng-show="num > 1"><a href="jobs/{{name}}/{{num-1}}">«</a></li> | ||||
|    <li ng-show="latestNum > num"><a ng-href="jobs/{{name}}/{{num+1}}">»</a></li> | ||||
|   </ul> | ||||
|  </nav> | ||||
|  <div style="clear:both;"></div> | ||||
|  <dl class="dl-horizontal"> | ||||
|   <dt>Reason</dt><dd>{{job.reason}}</dd> | ||||
|   <dt>Queued for</dt><dd>{{job.queued}}s</dd> | ||||
|   <dt>Started</dt><dd>{{formatDate(job.started)}}</dd> | ||||
|   <dt ng-show="runComplete(job)">Completed</dt><dd ng-show="runComplete(job)">{{formatDate(job.completed)}}</dd> | ||||
|   <dt ng-show="runComplete(job)">Duration</dt><dd ng-show="runComplete(job)">{{job.duration}}s</dd> | ||||
|  </dl> | ||||
| </div> | ||||
| <div class="col-sm-7 col-md-6 col-lg-5"> | ||||
|  <div class="progress" ng-show="job.result == 'running'"> | ||||
|   <div class="progress-bar progress-bar-{{job.overtime?'warning':'info'}} progress-bar-striped {{job.etc?'':'active'}}" style="width:{{!job.etc?'100':job.progress}}%;"></div> | ||||
|  </div> | ||||
|  <div class="panel panel-default" ng-show="job.artifacts.length"> | ||||
|   <div class="panel-heading">Artifacts</div> | ||||
|   <div class="panel-body"> | ||||
|    <ul class="list-unstyled" style="margin-bottom: 0"> | ||||
| 	<li ng-repeat="art in job.artifacts"> | ||||
| 	 <a href="{{art.url}}" target="_self">{{art.filename}}</a> | ||||
| 	</li> | ||||
|    </ul> | ||||
|   </div> | ||||
|  </div> | ||||
| </div> | ||||
| </div> | ||||
| <div class="row"> | ||||
| <div class="col-xs-12"> | ||||
|  <button type="button" class="btn btn-default btn-xs pull-right" ng-class="{active:autoscroll}" ng-click="autoscroll = !autoscroll" style="margin-top:10px">Autoscroll</button> | ||||
|  <h4>Console output</h4> | ||||
|  <pre ng-bind-html="log"></pre> | ||||
| </div> | ||||
| </div> | ||||
| </div> | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user