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 | # Zip and compile statically served resources | ||||||
| generate_compressed_bins(${CMAKE_SOURCE_DIR}/src/resources index.html js/app.js | 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 |     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... | # Download 3rd-party frontend JS libs... | ||||||
| file(DOWNLOAD https://ajax.googleapis.com/ajax/libs/angularjs/1.3.14/angular.min.js | file(DOWNLOAD https://cdnjs.cloudflare.com/ajax/libs/vue/2.3.4/vue.min.js | ||||||
|         js/angular.min.js EXPECTED_MD5 b1137641dbb512a60e83d673f7e2d98f) |         js/vue.min.js EXPECTED_MD5 ae2fca1cfa0e31377819b1b0ffef704c) | ||||||
| file(DOWNLOAD https://ajax.googleapis.com/ajax/libs/angularjs/1.3.14/angular-route.min.js | file(DOWNLOAD https://cdnjs.cloudflare.com/ajax/libs/vue-router/2.7.0/vue-router.min.js | ||||||
|         js/angular-route.min.js EXPECTED_MD5 28ef7d7b4349ae0dce602748185ef32a) |         js/vue-router.min.js EXPECTED_MD5 5d3e35710dbe02de78c39e3e439b8d4e) | ||||||
| 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://raw.githubusercontent.com/drudru/ansi_up/v1.3.0/ansi_up.js | file(DOWNLOAD https://raw.githubusercontent.com/drudru/ansi_up/v1.3.0/ansi_up.js | ||||||
|         js/ansi_up.js EXPECTED_MD5 158566dc1ff8f2804de972f7e841e2f6) |         js/ansi_up.js EXPECTED_MD5 158566dc1ff8f2804de972f7e841e2f6) | ||||||
| file(DOWNLOAD https://cdnjs.cloudflare.com/ajax/libs/Chart.js/1.0.2/Chart.min.js | 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 | file(DOWNLOAD https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css | ||||||
|         css/bootstrap.min.css EXPECTED_MD5 5d5357cb3704e1f43a1f5bfed2aebf42) |         css/bootstrap.min.css EXPECTED_MD5 5d5357cb3704e1f43a1f5bfed2aebf42) | ||||||
| # ...and compile them | # ...and compile them | ||||||
| generate_compressed_bins(${CMAKE_BINARY_DIR} js/angular.min.js js/angular-route.min.js | generate_compressed_bins(${CMAKE_BINARY_DIR} js/vue-router.min.js js/vue.min.js | ||||||
|     js/angular-sanitize.min.js js/ansi_up.js js/Chart.min.js js/Chart.HorizontalBar.js |     js/ansi_up.js js/Chart.min.js js/Chart.HorizontalBar.js css/bootstrap.min.css) | ||||||
|     css/bootstrap.min.css) |  | ||||||
| # (see resources.cpp where these are fetched) | # (see resources.cpp where these are fetched) | ||||||
| 
 | 
 | ||||||
| ## Server | ## Server | ||||||
|  | |||||||
| @ -30,7 +30,7 @@ Resources::Resources() | |||||||
|     INIT_RESOURCE("/", index_html); |     INIT_RESOURCE("/", index_html); | ||||||
|     INIT_RESOURCE("/favicon.ico", favicon_ico); |     INIT_RESOURCE("/favicon.ico", favicon_ico); | ||||||
|     INIT_RESOURCE("/favicon-152.png", favicon_152_png); |     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("/icon.png", icon_png); | ||||||
|     INIT_RESOURCE("/js/app.js", js_app_js); |     INIT_RESOURCE("/js/app.js", js_app_js); | ||||||
|     INIT_RESOURCE("/js/Chart.HorizontalBar.js", js_Chart_HorizontalBar_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/job.html", tpl_job_html); | ||||||
|     INIT_RESOURCE("/tpl/run.html", tpl_run_html); |     INIT_RESOURCE("/tpl/run.html", tpl_run_html); | ||||||
|     INIT_RESOURCE("/tpl/browse.html", tpl_browse_html); |     INIT_RESOURCE("/tpl/browse.html", tpl_browse_html); | ||||||
|     INIT_RESOURCE("/js/angular.min.js", js_angular_min_js); |     INIT_RESOURCE("/js/vue.min.js", js_vue_min_js); | ||||||
|     INIT_RESOURCE("/js/angular-route.min.js", js_angular_route_min_js); |     INIT_RESOURCE("/js/vue-router.min.js", js_vue_router_min_js); | ||||||
|     INIT_RESOURCE("/js/angular-sanitize.min.js", js_angular_sanitize_min_js); |  | ||||||
|     INIT_RESOURCE("/js/ansi_up.js", js_ansi_up_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.min.js", js_Chart_min_js); | ||||||
|     INIT_RESOURCE("/js/Chart.HorizontalBar.js", js_Chart_HorizontalBar_js); |     INIT_RESOURCE("/js/Chart.HorizontalBar.js", js_Chart_HorizontalBar_js); | ||||||
|  | |||||||
| @ -1,5 +1,5 @@ | |||||||
| <!doctype html> | <!doctype html> | ||||||
| <html ng-app="laminar"> | <html> | ||||||
| <head> | <head> | ||||||
|  <base href="/"> |  <base href="/"> | ||||||
|  <meta charset="utf-8"> |  <meta charset="utf-8"> | ||||||
| @ -8,14 +8,13 @@ | |||||||
|  <meta name="apple-mobile-web-app-capable" content="yes" /> |  <meta name="apple-mobile-web-app-capable" content="yes" /> | ||||||
|  <link rel="apple-touch-icon-precomposed" href="/favicon-152.png"> |  <link rel="apple-touch-icon-precomposed" href="/favicon-152.png"> | ||||||
|  <title>Laminar</title> |  <title>Laminar</title> | ||||||
|  <script src="/js/angular.min.js"></script> |  <script src="/js/vue.min.js"></script> | ||||||
|  <script src="/js/angular-route.min.js"></script> |  <script src="/js/vue-router.min.js"></script> | ||||||
|  <script src="/js/angular-sanitize.min.js"></script> |  <script src="/js/ansi_up.js"></script> | ||||||
|  <script src="/js/ansi_up.js" type="text/javascript"></script> |  | ||||||
|  <script src="/js/Chart.min.js"></script> |  <script src="/js/Chart.min.js"></script> | ||||||
|  <script src="/js/Chart.HorizontalBar.js"></script> |  <script src="/js/Chart.HorizontalBar.js"></script> | ||||||
|  <link href="/css/bootstrap.min.css" rel="stylesheet"> |  <link href="/css/bootstrap.min.css" rel="stylesheet"> | ||||||
|  <script src="/js/app.js"></script> |  <script src="/js/app.js" defer></script> | ||||||
|  <style> |  <style> | ||||||
|   body, html { height: 100%; } |   body, html { height: 100%; } | ||||||
|   .navbar { margin-bottom: 0; } |   .navbar { margin-bottom: 0; } | ||||||
| @ -36,42 +35,187 @@ | |||||||
|    margin-top: 5px; |    margin-top: 5px; | ||||||
|    margin-bottom: 0; |    margin-bottom: 0; | ||||||
|   } |   } | ||||||
|   .spin { |   table#joblist tr:first-child td { border-top: 0; } | ||||||
| 	  -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); } } |  | ||||||
| 
 |  | ||||||
|  </style> |  </style> | ||||||
| </head> | </head> | ||||||
| <body> | <body> | ||||||
|  <nav class="navbar navbar-inverse"> |  <template id="home"><div> | ||||||
|   <div class="container-fluid"> |   <ol class="breadcrumb"><li class="active">Home</li></ol> | ||||||
|    <div> |   <div class="container-fluid"><div class="row"> | ||||||
|     <a class="navbar-brand" href="/"><img src="/icon.png">{{title}}</a> |    <div class="col-sm-5 col-md-4 col-lg-3 dash"> | ||||||
|     <a class="btn navbar-btn pull-right" href="/jobs">Jobs</a> |     <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> | ||||||
|  |    <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> |   </div> | ||||||
|  </nav> |  </div></template> | ||||||
|  <ol class="breadcrumb"> |   | ||||||
|   <li ng-repeat="n in bc.nodes track by $index"><a href="{{n.href}}">{{n.label}}</a></li> |  <template id="run"><div> | ||||||
|   <li class="active">{{bc.current}}</li> |   <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> | ||||||
|  </ol> |   <div class="container-fluid"> | ||||||
|  <div ng-view></div> |    <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> | </body> | ||||||
| </html> | </html> | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -1,340 +1,445 @@ | |||||||
| angular.module('laminar',['ngRoute','ngSanitize']) | /* laminar.js | ||||||
| .config(function($routeProvider, $locationProvider, $sceProvider) { |  * frontend application for Laminar Continuous Integration | ||||||
| 	$routeProvider |  * https://laminar.ohwg.net
 | ||||||
| 	.when('/', { |  */ | ||||||
| 		templateUrl: 'tpl/home.html', | const WebsocketHandler = function() { | ||||||
| 		controller: 'mainController' |   function setupWebsocket(path, next) { | ||||||
| 	}) |     var ws = new WebSocket("ws://" + window.location.host + path); | ||||||
| 	.when('/jobs', { |     ws.onmessage = function(msg) { | ||||||
| 		templateUrl: 'tpl/browse.html', |       msg = JSON.parse(msg.data); | ||||||
| 		controller: 'BrowseController', |       // "status" is the first message the websocket always delivers.
 | ||||||
| 	}) |       // Use this to confirm the navigation. The component is not
 | ||||||
| 	.when('/jobs/:name', { |       // created until next() is called, so creating a reference
 | ||||||
| 		templateUrl: 'tpl/job.html', |       // for other message types must be deferred
 | ||||||
| 		controller: 'JobController' |       if (msg.type === 'status') { | ||||||
| 	}) |         next(comp => { | ||||||
| 	.when('/jobs/:name/:num', { |           // Set up bidirectional reference
 | ||||||
| 		templateUrl: 'tpl/run.html', |           // 1. needed to reference the component for other msg types
 | ||||||
| 		controller: 'RunController' |           this.comp = comp; | ||||||
| 	}) |           // 2. needed to close the ws on navigation away
 | ||||||
| 	$locationProvider.html5Mode(true); |           comp.ws = this; | ||||||
| 	$sceProvider.enabled(false); |           // Update html and nav titles
 | ||||||
| }) |           document.title = comp.$root.title = msg.title; | ||||||
| .factory('$ws',function($q,$location){ |           // Component-specific callback handler
 | ||||||
| 	return { |           comp[msg.type](msg.data); | ||||||
| 		statusListener: function(callbacks) { |         }); | ||||||
| 			var ws = new WebSocket("ws://" + location.host + $location.path()); |       } else { | ||||||
| 			ws.onmessage = function(message) { |         // at this point, the component must be defined
 | ||||||
| 				message = JSON.parse(message.data); |         if (!this.comp) | ||||||
| 				callbacks[message.type](message.data); |           return console.error("Page component was undefined"); | ||||||
| 			}; |         else if (typeof this.comp[msg.type] === 'function') | ||||||
| 		}, |           this.comp[msg.type](msg.data); | ||||||
| 		logListener: function(callback) { |       } | ||||||
| 			var ws = new WebSocket("ws://" + location.host + $location.path() + '/log'); |     }; | ||||||
| 			ws.onmessage = function(message) { |   }; | ||||||
| 				callback(message.data); |   return { | ||||||
| 			}; |     beforeRouteEnter(to, from, next) { | ||||||
| 		} |       setupWebsocket(to.path, (fn) => { next(fn); }); | ||||||
| 	}; |     }, | ||||||
| }) |     beforeRouteUpdate(to, from, next) { | ||||||
| .controller('mainController', function($rootScope, $scope, $ws, $interval){ |       this.ws.close(); | ||||||
| 	$rootScope.bc = { |       setupWebsocket(to.path, (fn) => { fn(this); next(); }); | ||||||
| 		nodes: [], |     }, | ||||||
| 		current: 'Home' |     beforeRouteLeave(to, from, next) { | ||||||
| 	}; |       this.ws.close(); | ||||||
|  |       next(); | ||||||
|  |     }, | ||||||
|  |   }; | ||||||
|  | }(); | ||||||
| 
 | 
 | ||||||
| 	$scope.jobsQueued = []; | const Utils = { | ||||||
| 	$scope.jobsRunning = []; |   methods: { | ||||||
| 	$scope.jobsRecent = []; |     runIcon(result) { | ||||||
| 	var chtUtilization, chtBuildsPerDay, chtBuildsPerJob, chtTimePerJob; |       return result === "success" ? '<span style="color:forestgreen;font-family:\'Zapf Dingbats\';">✔</span>' : result === "failed" || result === "aborted" ? '<span style="color:crimson;">✘</span>' : ''; | ||||||
| 	 |     }, | ||||||
| 	var updateUtilization = function(busy) { |     formatDate: function(unix) { | ||||||
| 		chtUtilization.segments[0].value += busy ? 1 : -1; |       // TODO: reimplement when toLocaleDateString() accepts formatting options on most browsers
 | ||||||
| 		chtUtilization.segments[1].value -= busy ? 1 : -1; |       var d = new Date(1000 * unix); | ||||||
| 		chtUtilization.update(); |       var m = d.getMinutes(); | ||||||
| 	} |       if (m < 10) m = '0' + m; | ||||||
| 			 |       return d.getHours() + ':' + m + ' on ' + ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'][d.getDay()] + ' ' + | ||||||
| 	$ws.statusListener({ |         d.getDate() + '. ' + ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', | ||||||
| 		status: function(data) { |           'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec' | ||||||
| 			$rootScope.title = data.title; |         ][d.getMonth()] + ' ' + | ||||||
| 			// populate jobs
 |         d.getFullYear(); | ||||||
| 			$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(); |  | ||||||
| 
 | 
 | ||||||
| 			for(var i = 0; i < $scope.jobsRunning.length; ++i) { | const ProgressUpdater = { | ||||||
| 				var job = $scope.jobsRunning[i]; |   data() { return { jobsRunning: [] }; }, | ||||||
| 				if(job.name == data.name && job.number == data.number) { |   methods: { | ||||||
| 					$scope.jobsRunning.splice(i,1); |     updateProgress(o) { | ||||||
| 					$scope.jobsRecent.splice(0,0,data); |       if (o.etc) { | ||||||
| 					$scope.$apply(); |         var p = ((new Date()).getTime() / 1000 - o.started) / (o.etc - o.started); | ||||||
| 					 |         if (p > 1.2) { | ||||||
| 					break; |           o.overtime = true; | ||||||
| 				} |         } else if (p >= 1) { | ||||||
| 			} |           o.progress = 99; | ||||||
| 			updateUtilization(false); |         } else { | ||||||
| 			for(var j = 0; j < chtBuildsPerJob.datasets[0].bars.length; ++j) { |           o.progress = 100 * p; | ||||||
| 				if(chtBuildsPerJob.datasets[0].bars[j].label == job.name) { |         } | ||||||
| 					chtBuildsPerJob.datasets[0].bars[j].value++; |       } | ||||||
| 					chtBuildsPerJob.update(); |     } | ||||||
| 					break; |   }, | ||||||
| 				} |   beforeDestroy() { | ||||||
| 			} |     clearInterval(this.updateTimer); | ||||||
| 		} |   }, | ||||||
| 	}); |   watch: { | ||||||
| 	var timeUpdater = $interval(function() { |     jobsRunning(val) { | ||||||
| 		$scope.jobsRunning.forEach($rootScope.updateProgress); |       // this function handles several cases:
 | ||||||
| 	}, 1000); |       // - the route has changed to a different run of the same job
 | ||||||
| 	$scope.$on('$destroy', function() { |       // - the current job has ended
 | ||||||
| 		$interval.cancel(timeUpdater); |       // - the current job has started (practically hard to reach)
 | ||||||
| 	}); |       clearInterval(this.updateTimer); | ||||||
| }) |       if (val.length) { | ||||||
| .controller('BrowseController', function($rootScope, $scope, $ws, $interval){ |         // TODO: first, a non-animated progress update
 | ||||||
| 	$rootScope.bc = { |         this.updateTimer = setInterval(() => { | ||||||
| 		nodes: [{ href: '/', label: 'Home' }], |           this.jobsRunning.forEach(this.updateProgress); | ||||||
| 		current: 'Jobs' |           this.$forceUpdate(); | ||||||
| 	}; |         }, 1000); | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | }; | ||||||
| 
 | 
 | ||||||
| 	$scope.currentTag = null; | const Home = function() { | ||||||
| 	$scope.activeTag = function(t) { |   var state = { | ||||||
| 		return $scope.currentTag === t; |     jobsQueued: [], | ||||||
| 	}; |     jobsRecent: [] | ||||||
| 	$scope.bytag = function(job) { |   }; | ||||||
| 		if($scope.currentTag === null) return true; |  | ||||||
| 		return job.tags.indexOf($scope.currentTag) >= 0; |  | ||||||
| 	}; |  | ||||||
| 
 | 
 | ||||||
| 	$scope.jobs = []; |   var chtUtilization, chtBuildsPerDay, chtBuildsPerJob, chtTimePerJob; | ||||||
| 	$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 = []; |  | ||||||
| 
 | 
 | ||||||
| 	$ws.statusListener({ |   var updateUtilization = function(busy) { | ||||||
| 		status: function(data) { |     chtUtilization.segments[0].value += busy ? 1 : -1; | ||||||
| 			$rootScope.title = data.title; |     chtUtilization.segments[1].value -= busy ? 1 : -1; | ||||||
|  |     chtUtilization.update(); | ||||||
|  |   } | ||||||
| 
 | 
 | ||||||
| 			$scope.jobsRunning = data.running; |   return { | ||||||
| 			$scope.jobsRecent = data.recent; |     template: '#home', | ||||||
| 			$scope.lastSuccess = data.lastSuccess; |     mixins: [WebsocketHandler, Utils, ProgressUpdater], | ||||||
| 			$scope.lastFailed = data.lastFailed; |     data: function() { | ||||||
| 			$scope.$apply(); |       return state; | ||||||
| 			 |     }, | ||||||
| 			var chtBt = new Chart(document.getElementById("chartBt").getContext("2d")).Bar({ |     methods: { | ||||||
| 				labels: data.recent.map(function(e){return '#' + e.number;}).reverse(), |       status: function(msg) { | ||||||
| 				datasets: [{ |         state.jobsQueued = msg.queued; | ||||||
| 					fillColor: "darkseagreen", |         state.jobsRunning = msg.running; | ||||||
| 					strokeColor: "forestgreen", |         state.jobsRecent = msg.recent; | ||||||
| 					data: data.recent.map(function(e){return e.duration;}).reverse() |         this.$forceUpdate(); | ||||||
| 				}] |  | ||||||
| 			}, |  | ||||||
| 			{barValueSpacing: 1,barStrokeWidth: 1,barDatasetSpacing:0} |  | ||||||
| 			); |  | ||||||
| 
 | 
 | ||||||
| 			for(var i = 0, n = data.recent.length; i < n; ++i) { |         // setup charts
 | ||||||
| 				if(data.recent[i].result != "success") { |         chtUtilization = new Chart(document.getElementById("chartUtil").getContext("2d")).Pie( | ||||||
| 					chtBt.datasets[0].bars[n-i-1].fillColor = "darksalmon"; |           [{ | ||||||
| 					chtBt.datasets[0].bars[n-i-1].strokeColor = "crimson"; |               value: msg.executorsBusy, | ||||||
| 				} |               color: "tan", | ||||||
| 			} |               label: "Busy" | ||||||
| 			chtBt.update(); |             }, | ||||||
| 			 |             { | ||||||
| 		}, |               value: msg.executorsTotal, | ||||||
| 		job_queued: function() { |               color: "darkseagreen", | ||||||
| 			$scope.nQueued++; |               label: "Idle" | ||||||
| 		}, |             } | ||||||
| 		job_started: function(data) { |           ], { | ||||||
| 			$scope.nQueued--; |             animationEasing: 'easeInOutQuad' | ||||||
| 			$scope.jobsRunning.splice(0,0,data); |           } | ||||||
| 			$scope.$apply(); |         ); | ||||||
| 		}, |         chtBuildsPerDay = new Chart(document.getElementById("chartBpd").getContext("2d")).Line({ | ||||||
| 		job_completed: function(data) { |           labels: function() { | ||||||
| 			for(var i = 0; i < $scope.jobsRunning.length; ++i) { |             res = []; | ||||||
| 				var job = $scope.jobsRunning[i]; |             var now = new Date(); | ||||||
| 				if(job.number === data.number) { |             for (var i = 6; i >= 0; --i) { | ||||||
| 					$scope.jobsRunning.splice(i,1); |               var then = new Date(now.getTime() - i * 86400000); | ||||||
| 					$scope.jobsRecent.splice(0,0,data); |               res.push(["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"][then.getDay()]); | ||||||
| 					$scope.$apply(); |             } | ||||||
| 					break; |             return res; | ||||||
| 				} |           }(), | ||||||
| 			} |           datasets: [{ | ||||||
| 		} |             label: "Successful Builds", | ||||||
| 	}); |             fillColor: "darkseagreen", | ||||||
| }) |             strokeColor: "forestgreen", | ||||||
| .controller('RunController', function($rootScope, $scope, $routeParams, $ws, $interval) { |             data: msg.buildsPerDay.map(function(e) { | ||||||
| 	$rootScope.bc = { |               return e.success || 0; | ||||||
| 		nodes: [{ href: '/', label: 'Home' }, |             }) | ||||||
| 		{ href: '/jobs', label: 'Jobs' }, |           }, { | ||||||
| 		{ href: '/jobs/'+$routeParams.name, label: $routeParams.name } |             label: "Failed Bulids", | ||||||
| 		], |             fillColor: "darksalmon", | ||||||
| 		current: '#' + $routeParams.num |             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) { |       job_queued: function(data) { | ||||||
| 			$rootScope.title = data.title; |         state.jobsQueued.splice(0, 0, data); | ||||||
| 			$rootScope.updateProgress(data); |         this.$forceUpdate(); | ||||||
| 			$scope.job = data; |       }, | ||||||
| 			$scope.latestNum = data.latestNum; |       job_started: function(data) { | ||||||
| 			$scope.$apply(); |         state.jobsQueued.splice(state.jobsQueued.length - data.queueIndex - 1, 1); | ||||||
| 		}, |         state.jobsRunning.splice(0, 0, data); | ||||||
| 		job_started: function() { |         this.$forceUpdate(); | ||||||
| 			$scope.latestNum++; |         updateUtilization(true); | ||||||
| 			$scope.$apply(); |       }, | ||||||
| 		}, |       job_completed: function(data) { | ||||||
| 		job_completed: function(data) { |         if (data.result === "success") | ||||||
| 			$scope.job = data; |           chtBuildsPerDay.datasets[0].points[6].value++; | ||||||
| 			$scope.$apply(); |         else | ||||||
| 		} |           chtBuildsPerDay.datasets[1].points[6].value++; | ||||||
| 	}); |         chtBuildsPerDay.update(); | ||||||
| 	 | 
 | ||||||
| 	$scope.log = "" |         for (var i = 0; i < state.jobsRunning.length; ++i) { | ||||||
| 	$scope.autoscroll = false; |           var job = state.jobsRunning[i]; | ||||||
| 	var firstLog = false; |           if (job.name == data.name && job.number == data.number) { | ||||||
| 	$ws.logListener(function(data) { |             state.jobsRunning.splice(i, 1); | ||||||
| 		$scope.log += ansi_up.ansi_to_html(data.replace('<','<').replace('>','>')); |             state.jobsRecent.splice(0, 0, data); | ||||||
| 		$scope.$apply(); |             this.$forceUpdate(); | ||||||
| 		if(!firstLog) { |             break; | ||||||
| 			firstLog = true; |           } | ||||||
| 		} else if($scope.autoscroll) { |         } | ||||||
| 			window.scrollTo(0, document.body.scrollHeight); |         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++; | ||||||
| 	var timeUpdater = $interval(function() { |             chtBuildsPerJob.update(); | ||||||
| 		$rootScope.updateProgress($scope.job); |             break; | ||||||
| 	}, 1000); |           } | ||||||
| 	$scope.$on('$destroy', function() { |         } | ||||||
| 		$interval.cancel(timeUpdater); |       } | ||||||
| 	}); |     } | ||||||
| }) |   }; | ||||||
| .run(function($rootScope) { | }(); | ||||||
| 	angular.extend($rootScope, { | 
 | ||||||
| 		runIcon: function(result) { | const Jobs = function() { | ||||||
| 			return result === "success" ? '<span style="color:forestgreen;font-family:\'Zapf Dingbats\';">✔</span>' : result === "failed" || result === "aborted" ? '<span style="color:crimson;">✘</span>' : ''; |   var state = { | ||||||
| 		}, |     jobs: [], | ||||||
| 		runComplete: function(run) { |     search: '', | ||||||
| 			return !!run && (run.result === 'aborted' || run.result === 'failed' || run.result === 'success'); |     tags: [], | ||||||
| 		}, |     tag: null | ||||||
| 		formatDate: function(unix) { |   }; | ||||||
| 			// TODO reimplement when toLocaleDateString() accepts formatting
 |   return { | ||||||
| 			// options on most browsers
 |     template: '#jobs', | ||||||
| 			var d = new Date(1000 * unix); |     mixins: [WebsocketHandler, Utils], | ||||||
| 			var m = d.getMinutes(); |     data: function() { return state; }, | ||||||
| 			if(m < 10) m = '0' + m; |     computed: { | ||||||
| 			return d.getHours() + ':' + m + ' on ' +  |       filteredJobs() { | ||||||
| 				['Sun','Mon','Tue','Wed','Thu','Fri','Sat'][d.getDay()] + ' ' |         var ret = this.jobs; | ||||||
| 				+ d.getDate() + '. ' + ['Jan','Feb','Mar','Apr','May','Jun', |         var tag = this.tag; | ||||||
| 				'Jul','Aug','Sep', 'Oct','Nov','Dec'][d.getMonth()] + ' ' |         if (tag) { | ||||||
| 				+ d.getFullYear(); |           ret = ret.filter(function(job) { | ||||||
| 		}, |             return job.tags.indexOf(tag) >= 0; | ||||||
| 		updateProgress: function(o){ |           }); | ||||||
| 			if(o.etc) { |         } | ||||||
| 				var d = new Date(); |         var search = this.search; | ||||||
| 				var p = (d.getTime()/1000 - o.started) / (o.etc - o.started); |         if (search) { | ||||||
| 				if(p > 1.2) { |           ret = ret.filter(function(job) { | ||||||
| 					o.overtime = true; |             return job.name.indexOf(search) > -1; | ||||||
| 				} else if(p >= 1) { |           }); | ||||||
| 					o.progress = 99; |         } | ||||||
| 				} else { |         return ret; | ||||||
| 					o.progress = 100 * p; |       } | ||||||
| 				} |     }, | ||||||
| 			} |     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