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>
 | 
				
			||||||
 | 
					 <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>
 | 
				
			||||||
 | 
					 </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">
 | 
					  <nav class="navbar navbar-inverse">
 | 
				
			||||||
   <div class="container-fluid">
 | 
					   <div class="container-fluid">
 | 
				
			||||||
    <div>
 | 
					    <div>
 | 
				
			||||||
    <a class="navbar-brand" href="/"><img src="/icon.png">{{title}}</a>
 | 
					     <router-link to="/" class="navbar-brand"><img src="/icon.png">{{title}}</router-link>
 | 
				
			||||||
    <a class="btn navbar-btn pull-right" href="/jobs">Jobs</a>
 | 
					     <router-link to="/jobs" class="btn navbar-btn pull-right">Jobs</router-link>
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
   </div>
 | 
					   </div>
 | 
				
			||||||
  </nav>
 | 
					  </nav>
 | 
				
			||||||
 <ol class="breadcrumb">
 | 
					  <router-view></router-view>
 | 
				
			||||||
  <li ng-repeat="n in bc.nodes track by $index"><a href="{{n.href}}">{{n.label}}</a></li>
 | 
					 </div>
 | 
				
			||||||
  <li class="active">{{bc.current}}</li>
 | 
					 | 
				
			||||||
 </ol>
 | 
					 | 
				
			||||||
 <div ng-view></div>
 | 
					 | 
				
			||||||
</body>
 | 
					</body>
 | 
				
			||||||
</html>
 | 
					</html>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -1,51 +1,114 @@
 | 
				
			|||||||
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);
 | 
					 | 
				
			||||||
			};
 | 
					 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
})
 | 
					  };
 | 
				
			||||||
.controller('mainController', function($rootScope, $scope, $ws, $interval){
 | 
					  return {
 | 
				
			||||||
	$rootScope.bc = {
 | 
					    beforeRouteEnter(to, from, next) {
 | 
				
			||||||
		nodes: [],
 | 
					      setupWebsocket(to.path, (fn) => { next(fn); });
 | 
				
			||||||
		current: 'Home'
 | 
					    },
 | 
				
			||||||
 | 
					    beforeRouteUpdate(to, from, next) {
 | 
				
			||||||
 | 
					      this.ws.close();
 | 
				
			||||||
 | 
					      setupWebsocket(to.path, (fn) => { fn(this); next(); });
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    beforeRouteLeave(to, from, next) {
 | 
				
			||||||
 | 
					      this.ws.close();
 | 
				
			||||||
 | 
					      next();
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					}();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					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();
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					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);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const Home = function() {
 | 
				
			||||||
 | 
					  var state = {
 | 
				
			||||||
 | 
					    jobsQueued: [],
 | 
				
			||||||
 | 
					    jobsRecent: []
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	$scope.jobsQueued = [];
 | 
					 | 
				
			||||||
	$scope.jobsRunning = [];
 | 
					 | 
				
			||||||
	$scope.jobsRecent = [];
 | 
					 | 
				
			||||||
  var chtUtilization, chtBuildsPerDay, chtBuildsPerJob, chtTimePerJob;
 | 
					  var chtUtilization, chtBuildsPerDay, chtBuildsPerJob, chtTimePerJob;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  var updateUtilization = function(busy) {
 | 
					  var updateUtilization = function(busy) {
 | 
				
			||||||
@ -54,21 +117,34 @@ angular.module('laminar',['ngRoute','ngSanitize'])
 | 
				
			|||||||
    chtUtilization.update();
 | 
					    chtUtilization.update();
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	$ws.statusListener({
 | 
					  return {
 | 
				
			||||||
		status: function(data) {
 | 
					    template: '#home',
 | 
				
			||||||
			$rootScope.title = data.title;
 | 
					    mixins: [WebsocketHandler, Utils, ProgressUpdater],
 | 
				
			||||||
			// populate jobs
 | 
					    data: function() {
 | 
				
			||||||
			$scope.jobsQueued = data.queued;
 | 
					      return state;
 | 
				
			||||||
			data.running.forEach($rootScope.updateProgress);
 | 
					    },
 | 
				
			||||||
			$scope.jobsRunning = data.running;
 | 
					    methods: {
 | 
				
			||||||
			$scope.jobsRecent = data.recent;
 | 
					      status: function(msg) {
 | 
				
			||||||
			$scope.$apply();
 | 
					        state.jobsQueued = msg.queued;
 | 
				
			||||||
 | 
					        state.jobsRunning = msg.running;
 | 
				
			||||||
 | 
					        state.jobsRecent = msg.recent;
 | 
				
			||||||
 | 
					        this.$forceUpdate();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        // setup charts
 | 
					        // setup charts
 | 
				
			||||||
        chtUtilization = new Chart(document.getElementById("chartUtil").getContext("2d")).Pie(
 | 
					        chtUtilization = new Chart(document.getElementById("chartUtil").getContext("2d")).Pie(
 | 
				
			||||||
				[{value: data.executorsBusy, color:"tan", label: "Busy"},
 | 
					          [{
 | 
				
			||||||
				 {value: data.executorsTotal, color: "darkseagreen", label: "Idle"}],
 | 
					              value: msg.executorsBusy,
 | 
				
			||||||
				{animationEasing: 'easeInOutQuad'}
 | 
					              color: "tan",
 | 
				
			||||||
 | 
					              label: "Busy"
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					              value: msg.executorsTotal,
 | 
				
			||||||
 | 
					              color: "darkseagreen",
 | 
				
			||||||
 | 
					              label: "Idle"
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					          ], {
 | 
				
			||||||
 | 
					            animationEasing: 'easeInOutQuad'
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
        );
 | 
					        );
 | 
				
			||||||
        chtBuildsPerDay = new Chart(document.getElementById("chartBpd").getContext("2d")).Line({
 | 
					        chtBuildsPerDay = new Chart(document.getElementById("chartBpd").getContext("2d")).Line({
 | 
				
			||||||
          labels: function() {
 | 
					          labels: function() {
 | 
				
			||||||
@ -84,38 +160,49 @@ angular.module('laminar',['ngRoute','ngSanitize'])
 | 
				
			|||||||
            label: "Successful Builds",
 | 
					            label: "Successful Builds",
 | 
				
			||||||
            fillColor: "darkseagreen",
 | 
					            fillColor: "darkseagreen",
 | 
				
			||||||
            strokeColor: "forestgreen",
 | 
					            strokeColor: "forestgreen",
 | 
				
			||||||
					data: data.buildsPerDay.map(function(e){return e.success||0;})
 | 
					            data: msg.buildsPerDay.map(function(e) {
 | 
				
			||||||
 | 
					              return e.success || 0;
 | 
				
			||||||
 | 
					            })
 | 
				
			||||||
          }, {
 | 
					          }, {
 | 
				
			||||||
            label: "Failed Bulids",
 | 
					            label: "Failed Bulids",
 | 
				
			||||||
            fillColor: "darksalmon",
 | 
					            fillColor: "darksalmon",
 | 
				
			||||||
            strokeColor: "crimson",
 | 
					            strokeColor: "crimson",
 | 
				
			||||||
					data: data.buildsPerDay.map(function(e){return e.failed||0;})
 | 
					            data: msg.buildsPerDay.map(function(e) {
 | 
				
			||||||
				}]},
 | 
					              return e.failed || 0;
 | 
				
			||||||
				{ showTooltips: false }
 | 
					            })
 | 
				
			||||||
			);
 | 
					          }]
 | 
				
			||||||
 | 
					        }, {
 | 
				
			||||||
 | 
					          showTooltips: false
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
        chtBuildsPerJob = new Chart(document.getElementById("chartBpj").getContext("2d")).HorizontalBar({
 | 
					        chtBuildsPerJob = new Chart(document.getElementById("chartBpj").getContext("2d")).HorizontalBar({
 | 
				
			||||||
				labels: Object.keys(data.buildsPerJob),
 | 
					          labels: Object.keys(msg.buildsPerJob),
 | 
				
			||||||
          datasets: [{
 | 
					          datasets: [{
 | 
				
			||||||
            fillColor: "lightsteelblue",
 | 
					            fillColor: "lightsteelblue",
 | 
				
			||||||
					data: Object.keys(data.buildsPerJob).map(function(e){return data.buildsPerJob[e];})
 | 
					            data: Object.keys(msg.buildsPerJob).map(function(e) {
 | 
				
			||||||
 | 
					              return msg.buildsPerJob[e];
 | 
				
			||||||
 | 
					            })
 | 
				
			||||||
          }]
 | 
					          }]
 | 
				
			||||||
        }, {});
 | 
					        }, {});
 | 
				
			||||||
        chtTimePerJob = new Chart(document.getElementById("chartTpj").getContext("2d")).HorizontalBar({
 | 
					        chtTimePerJob = new Chart(document.getElementById("chartTpj").getContext("2d")).HorizontalBar({
 | 
				
			||||||
				labels: Object.keys(data.timePerJob),
 | 
					          labels: Object.keys(msg.timePerJob),
 | 
				
			||||||
          datasets: [{
 | 
					          datasets: [{
 | 
				
			||||||
            fillColor: "lightsteelblue",
 | 
					            fillColor: "lightsteelblue",
 | 
				
			||||||
					data: Object.keys(data.timePerJob).map(function(e){return data.timePerJob[e];})
 | 
					            data: Object.keys(msg.timePerJob).map(function(e) {
 | 
				
			||||||
 | 
					              return msg.timePerJob[e];
 | 
				
			||||||
 | 
					            })
 | 
				
			||||||
          }]
 | 
					          }]
 | 
				
			||||||
        }, {});
 | 
					        }, {});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
      job_queued: function(data) {
 | 
					      job_queued: function(data) {
 | 
				
			||||||
			$scope.jobsQueued.splice(0,0,data);
 | 
					        state.jobsQueued.splice(0, 0, data);
 | 
				
			||||||
			$scope.$apply();
 | 
					        this.$forceUpdate();
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
      job_started: function(data) {
 | 
					      job_started: function(data) {
 | 
				
			||||||
			$scope.jobsQueued.splice($scope.jobsQueued.length - data.queueIndex - 1,1);
 | 
					        state.jobsQueued.splice(state.jobsQueued.length - data.queueIndex - 1, 1);
 | 
				
			||||||
			$scope.jobsRunning.splice(0,0,data);
 | 
					        state.jobsRunning.splice(0, 0, data);
 | 
				
			||||||
			$scope.$apply();
 | 
					        this.$forceUpdate();
 | 
				
			||||||
        updateUtilization(true);
 | 
					        updateUtilization(true);
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
      job_completed: function(data) {
 | 
					      job_completed: function(data) {
 | 
				
			||||||
@ -125,13 +212,12 @@ angular.module('laminar',['ngRoute','ngSanitize'])
 | 
				
			|||||||
          chtBuildsPerDay.datasets[1].points[6].value++;
 | 
					          chtBuildsPerDay.datasets[1].points[6].value++;
 | 
				
			||||||
        chtBuildsPerDay.update();
 | 
					        chtBuildsPerDay.update();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			for(var i = 0; i < $scope.jobsRunning.length; ++i) {
 | 
					        for (var i = 0; i < state.jobsRunning.length; ++i) {
 | 
				
			||||||
				var job = $scope.jobsRunning[i];
 | 
					          var job = state.jobsRunning[i];
 | 
				
			||||||
          if (job.name == data.name && job.number == data.number) {
 | 
					          if (job.name == data.name && job.number == data.number) {
 | 
				
			||||||
					$scope.jobsRunning.splice(i,1);
 | 
					            state.jobsRunning.splice(i, 1);
 | 
				
			||||||
					$scope.jobsRecent.splice(0,0,data);
 | 
					            state.jobsRecent.splice(0, 0, data);
 | 
				
			||||||
					$scope.$apply();
 | 
					            this.$forceUpdate();
 | 
				
			||||||
					
 | 
					 | 
				
			||||||
            break;
 | 
					            break;
 | 
				
			||||||
          }
 | 
					          }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
@ -144,87 +230,103 @@ angular.module('laminar',['ngRoute','ngSanitize'])
 | 
				
			|||||||
          }
 | 
					          }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
	});
 | 
					    }
 | 
				
			||||||
	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'
 | 
					 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					}();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	$scope.currentTag = null;
 | 
					const Jobs = function() {
 | 
				
			||||||
	$scope.activeTag = function(t) {
 | 
					  var state = {
 | 
				
			||||||
		return $scope.currentTag === t;
 | 
					    jobs: [],
 | 
				
			||||||
 | 
					    search: '',
 | 
				
			||||||
 | 
					    tags: [],
 | 
				
			||||||
 | 
					    tag: null
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
	$scope.bytag = function(job) {
 | 
					  return {
 | 
				
			||||||
		if($scope.currentTag === null) return true;
 | 
					    template: '#jobs',
 | 
				
			||||||
		return job.tags.indexOf($scope.currentTag) >= 0;
 | 
					    mixins: [WebsocketHandler, Utils],
 | 
				
			||||||
	};
 | 
					    data: function() { return state; },
 | 
				
			||||||
 | 
					    computed: {
 | 
				
			||||||
	$scope.jobs = [];
 | 
					      filteredJobs() {
 | 
				
			||||||
	$ws.statusListener({
 | 
					        var ret = this.jobs;
 | 
				
			||||||
		status: function(data) {
 | 
					        var tag = this.tag;
 | 
				
			||||||
			$rootScope.title = data.title;
 | 
					        if (tag) {
 | 
				
			||||||
			$scope.jobs = data.jobs;
 | 
					          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 = {};
 | 
					        var tags = {};
 | 
				
			||||||
			for(var i in data.jobs) {
 | 
					        for (var i in state.jobs) {
 | 
				
			||||||
				for(var j in data.jobs[i].tags) {
 | 
					          for (var j in state.jobs[i].tags) {
 | 
				
			||||||
					tags[data.jobs[i].tags[j]] = true;
 | 
					            tags[state.jobs[i].tags[j]] = true;
 | 
				
			||||||
          }
 | 
					          }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
			$scope.tags = Object.keys(tags);
 | 
					        state.tags = Object.keys(tags);
 | 
				
			||||||
			$scope.$apply();
 | 
					 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
      job_completed: function(data) {
 | 
					      job_completed: function(data) {
 | 
				
			||||||
			for(var i in $scope.jobs) {
 | 
					        for (var i in state.jobs) {
 | 
				
			||||||
				if($scope.jobs[i].name === data.name) {
 | 
					          if (state.jobs[i].name === data.name) {
 | 
				
			||||||
					$scope.jobs[i] = data;
 | 
					            state.jobs[i] = data;
 | 
				
			||||||
					$scope.$apply;
 | 
					            this.$forceUpdate();
 | 
				
			||||||
            break;
 | 
					            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;
 | 
					var Job = function() {
 | 
				
			||||||
	$scope.jobsRunning = [];
 | 
					  var state = {
 | 
				
			||||||
	$scope.jobsRecent = [];
 | 
					    jobsRunning: [],
 | 
				
			||||||
 | 
					    jobsRecent: [],
 | 
				
			||||||
	$ws.statusListener({
 | 
					    lastSuccess: null,
 | 
				
			||||||
		status: function(data) {
 | 
					    lastFailed: null,
 | 
				
			||||||
			$rootScope.title = data.title;
 | 
					    nQueued: 0,
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
			$scope.jobsRunning = data.running;
 | 
					  return Vue.extend({
 | 
				
			||||||
			$scope.jobsRecent = data.recent;
 | 
					    template: '#job',
 | 
				
			||||||
			$scope.lastSuccess = data.lastSuccess;
 | 
					    mixins: [WebsocketHandler, Utils],
 | 
				
			||||||
			$scope.lastFailed = data.lastFailed;
 | 
					    data: function() {
 | 
				
			||||||
			$scope.$apply();
 | 
					      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({
 | 
					        var chtBt = new Chart(document.getElementById("chartBt").getContext("2d")).Bar({
 | 
				
			||||||
				labels: data.recent.map(function(e){return '#' + e.number;}).reverse(),
 | 
					          labels: msg.recent.map(function(e) {
 | 
				
			||||||
 | 
					            return '#' + e.number;
 | 
				
			||||||
 | 
					          }).reverse(),
 | 
				
			||||||
          datasets: [{
 | 
					          datasets: [{
 | 
				
			||||||
            fillColor: "darkseagreen",
 | 
					            fillColor: "darkseagreen",
 | 
				
			||||||
            strokeColor: "forestgreen",
 | 
					            strokeColor: "forestgreen",
 | 
				
			||||||
					data: data.recent.map(function(e){return e.duration;}).reverse()
 | 
					            data: msg.recent.map(function(e) {
 | 
				
			||||||
 | 
					              return e.duration;
 | 
				
			||||||
 | 
					            }).reverse()
 | 
				
			||||||
          }]
 | 
					          }]
 | 
				
			||||||
			},
 | 
					        }, {
 | 
				
			||||||
			{barValueSpacing: 1,barStrokeWidth: 1,barDatasetSpacing:0}
 | 
					          barValueSpacing: 1,
 | 
				
			||||||
			);
 | 
					          barStrokeWidth: 1,
 | 
				
			||||||
 | 
					          barDatasetSpacing: 0
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			for(var i = 0, n = data.recent.length; i < n; ++i) {
 | 
					        for (var i = 0, n = msg.recent.length; i < n; ++i) {
 | 
				
			||||||
				if(data.recent[i].result != "success") {
 | 
					          if (msg.recent[i].result != "success") {
 | 
				
			||||||
            chtBt.datasets[0].bars[n - i - 1].fillColor = "darksalmon";
 | 
					            chtBt.datasets[0].bars[n - i - 1].fillColor = "darksalmon";
 | 
				
			||||||
            chtBt.datasets[0].bars[n - i - 1].strokeColor = "crimson";
 | 
					            chtBt.datasets[0].bars[n - i - 1].strokeColor = "crimson";
 | 
				
			||||||
          }
 | 
					          }
 | 
				
			||||||
@ -233,108 +335,111 @@ angular.module('laminar',['ngRoute','ngSanitize'])
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
      job_queued: function() {
 | 
					      job_queued: function() {
 | 
				
			||||||
			$scope.nQueued++;
 | 
					        state.nQueued++;
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
      job_started: function(data) {
 | 
					      job_started: function(data) {
 | 
				
			||||||
			$scope.nQueued--;
 | 
					        state.nQueued--;
 | 
				
			||||||
			$scope.jobsRunning.splice(0,0,data);
 | 
					        state.jobsRunning.splice(0, 0, data);
 | 
				
			||||||
			$scope.$apply();
 | 
					        this.$forceUpdate();
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
      job_completed: function(data) {
 | 
					      job_completed: function(data) {
 | 
				
			||||||
			for(var i = 0; i < $scope.jobsRunning.length; ++i) {
 | 
					        for (var i = 0; i < state.jobsRunning.length; ++i) {
 | 
				
			||||||
				var job = $scope.jobsRunning[i];
 | 
					          var job = state.jobsRunning[i];
 | 
				
			||||||
          if (job.number === data.number) {
 | 
					          if (job.number === data.number) {
 | 
				
			||||||
					$scope.jobsRunning.splice(i,1);
 | 
					            state.jobsRunning.splice(i, 1);
 | 
				
			||||||
					$scope.jobsRecent.splice(0,0,data);
 | 
					            state.jobsRecent.splice(0, 0, data);
 | 
				
			||||||
					$scope.$apply();
 | 
					            this.$forceUpdate();
 | 
				
			||||||
 | 
					            // TODO: update the chart
 | 
				
			||||||
            break;
 | 
					            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
 | 
					 | 
				
			||||||
	};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	$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 = ""
 | 
					const Run = function() {
 | 
				
			||||||
	$scope.autoscroll = false;
 | 
					  var state = {
 | 
				
			||||||
 | 
					    job: { artifacts: [] },
 | 
				
			||||||
 | 
					    latestNum: null,
 | 
				
			||||||
 | 
					    log: '',
 | 
				
			||||||
 | 
					    autoscroll: false
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
  var firstLog = false;
 | 
					  var firstLog = false;
 | 
				
			||||||
	$ws.logListener(function(data) {
 | 
					  var logHandler = function(vm, d) {
 | 
				
			||||||
		$scope.log += ansi_up.ansi_to_html(data.replace('<','<').replace('>','>'));
 | 
					    state.log += d;
 | 
				
			||||||
		$scope.$apply();
 | 
					    vm.$forceUpdate();
 | 
				
			||||||
    if (!firstLog) {
 | 
					    if (!firstLog) {
 | 
				
			||||||
      firstLog = true;
 | 
					      firstLog = true;
 | 
				
			||||||
		} else if($scope.autoscroll) {
 | 
					    } else if (state.autoscroll) {
 | 
				
			||||||
      window.scrollTo(0, document.body.scrollHeight);
 | 
					      window.scrollTo(0, document.body.scrollHeight);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
	});
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	var timeUpdater = $interval(function() {
 | 
					  return {
 | 
				
			||||||
		$rootScope.updateProgress($scope.job);
 | 
					    template: '#run',
 | 
				
			||||||
	}, 1000);
 | 
					    mixins: [WebsocketHandler, Utils, ProgressUpdater],
 | 
				
			||||||
	$scope.$on('$destroy', function() {
 | 
					    data: function() {
 | 
				
			||||||
		$interval.cancel(timeUpdater);
 | 
					      return state;
 | 
				
			||||||
	});
 | 
					    },
 | 
				
			||||||
})
 | 
					    methods: {
 | 
				
			||||||
.run(function($rootScope) {
 | 
					      status: function(data) {
 | 
				
			||||||
	angular.extend($rootScope, {
 | 
					        state.log = '';
 | 
				
			||||||
		runIcon: function(result) {
 | 
					        state.job = data;
 | 
				
			||||||
			return result === "success" ? '<span style="color:forestgreen;font-family:\'Zapf Dingbats\';">✔</span>' : result === "failed" || result === "aborted" ? '<span style="color:crimson;">✘</span>' : '';
 | 
					        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) {
 | 
					      runComplete: function(run) {
 | 
				
			||||||
        return !!run && (run.result === 'aborted' || run.result === 'failed' || run.result === 'success');
 | 
					        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){
 | 
					    beforeRouteEnter(to, from, next) {
 | 
				
			||||||
			if(o.etc) {
 | 
					      next(vm => {
 | 
				
			||||||
				var d = new Date();
 | 
					        vm.logws = new WebSocket("ws://" + location.host + to.path + '/log');
 | 
				
			||||||
				var p = (d.getTime()/1000 - o.started) / (o.etc - o.started);
 | 
					        vm.logws.onmessage = function(msg) {
 | 
				
			||||||
				if(p > 1.2) {
 | 
					          logHandler(vm, msg.data);
 | 
				
			||||||
					o.overtime = true;
 | 
					 | 
				
			||||||
				} else if(p >= 1) {
 | 
					 | 
				
			||||||
					o.progress = 99;
 | 
					 | 
				
			||||||
				} else {
 | 
					 | 
				
			||||||
					o.progress = 100 * p;
 | 
					 | 
				
			||||||
				}
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    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