@ -8,334 +8,200 @@
< meta name = "apple-mobile-web-app-capable" content = "yes" / >
< link rel = "apple-touch-icon-precomposed" href = "favicon-152.png" >
< link rel = "icon" href = "favicon.ico" >
< link rel = "manifest" href = "/manifest.webmanifest" >
< title > Laminar< / title >
< script src = "js/vue.min.js" > < / script >
< script src = "js/vue-router.min.js" > < / script >
< script src = "js/ansi_up.js" > < / script >
< script src = "js/Chart.min.js" > < / script >
< link href = "css/bootstrap.min.css" rel = "stylesheet" >
< link href = "custom/style.css" rel = "stylesheet" >
< script src = "js/app.js" defer > < / script >
< style >
body, html { height: 100%; }
.navbar { margin-bottom: 0; border-radius: 0; }
.navbar-brand { margin: 0 -15px; padding: 7px 15px }
.navbar-brand>img { display: inline; }
a.navbar-btn { color: #9d9d9d; }
a.navbar-btn.active { color: #fff; }
a.navbar-btn:hover { color: #fff; text-decoration: none; }
a.navbar-btn:focus { color: #fff; }
.bell { margin: 8px 15px; color: #9d9d9d; }
.bell:hover { text-decoration: none; color: #9d9d9d; cursor: pointer; }
.bell.active { color: #333; }
dt,dd { line-height: 2; }
canvas {
width: 100% !important;
max-width: 800px;
height: auto !important;
}
.progress {
height: 10px;
margin-top: 5px;
margin-bottom: 0;
}
table#joblist tr:first-child td { border-top: 0; }
#popup-connecting {
position: fixed;
background: white;
border: 1px solid #ddd;
bottom: 10px;
right: 10px;
padding: 20px;
}
/* status icons */
span.status {
display: inline-block;
width: 1em;
text-align: center;
font-family: sans-serif;
}
span.success { color: forestgreen; }
span.failed { color: firebrick; }
span.aborted { color: indigo; }
span.spin {
color: steelblue;
animation: 2s linear infinite spin;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
/* chart overlay */
li.chart-overlay {
position: absolute;
display: inline-block;
background: white;
border: 1px solid lightgray;
padding: 3px;
}
/* sort indicators */
a.sort {
position: relative;
margin-left: 7px;
}
a.sort:before, a.sort:after {
border: 4px solid transparent;
content: "";
position: absolute;
display: block;
height: 0;
width: 0;
right: 0;
top: 50%;
}
a.sort:before {
border-bottom-color: #ccc;
margin-top: -9px;
}
a.sort:after {
border-top-color: #ccc;
margin-top: 1px;
}
a.sort.dsc:after { border-top-color: #000; }
a.sort.asc:before { border-bottom-color: #000; }
a.sort:hover { text-decoration: none; cursor:pointer; }
a.sort:not(.asc):hover:before { border-bottom-color: #777; }
a.sort:not(.dsc):hover:after { border-top-color: #777; }
< / style >
< link href = "style.css" rel = "stylesheet" >
< link href = "custom/style.css" rel = "stylesheet" >
< / head >
< 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 > < 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 >
< small class = "pull-right" > {{formatDuration(job.started, job.completed)}}< / small >
< 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 {{formatDuration(job.started, job.completed)}} 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 runs 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" > Most runs per job in the last 24 hours< / div >
< div class = "panel-body" id = "chartStatus" >
< canvas id = "chartBpj" > < / canvas >
< template id = "home" > < div id = "page-home-main" >
< nav >
< table class = "table striped" >
< 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 >
< 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 >
< small style = "float:right;" > {{formatDuration(job.started, job.completed)}}< / small >
< div class = "progress" style = "margin-top: 5px;" >
< div class = "progress-bar" :class = "{overtime:job.overtime,indeterminate:!job.etc}" :style = "job.etc && {width:job.progress+'%'}" > < / div >
< / div >
< / div >
< / div > < / div > < div class = "row" >
< div class = "col-md-6" >
< div class = "panel panel-default" >
< div class = "panel-heading" > Longest average run time per job this week< / div >
< div class = "panel-body" >
< canvas id = "chartTpj" > < / canvas >
< / 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 {{formatDuration(job.started, job.completed)}} at {{formatDate(job.started)}}< / small >
< / td >
< / tr >
< / table >
< / nav >
< section style = "border-left: 1px solid #d0d0d0;" >
< div id = "page-home-stats" >
< div >
< h3 > Recent regressions< / h3 >
< table >
< tr v-for = "job in resultChanged" v-if = "job.lastFailure>job.lastSuccess" > < td > < router-link :to = "'/jobs/'+job.name+'/'+job.lastFailure" > {{job.name}} #{{job.lastFailure}}< / router-link > since < router-link :to = "'/jobs/'+job.name+'/'+job.lastSuccess" > #{{job.lastSuccess}}< / router-link > < / tr >
< / table >
< / 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 class = "row" >
< div class = "col-md-6" >
< div class = "panel panel-default" >
< div class = "panel-heading" > Regressions and recoveries< / div >
< div class = "panel-body" > < div style = "position:relative" >
< canvas id = "chartResultChanges" > < / canvas >
< ul v-for = "job in resultChanged" >
< li v-if = "job.lastFailure>job.lastSuccess" :id = "'rcd_'+job.name" class = "chart-overlay" > < router-link :to = "'/jobs/'+job.name" > {{job.name}}< / router-link > : < span v-html = "runIcon('failed')" > < / span > < router-link :to = "'/jobs/'+job.name+'/'+job.lastFailure" > #{{job.lastFailure}}< / router-link > since < span v-html = "runIcon('success')" > < / span > < router-link :to = "'/jobs/'+job.name+'/'+job.lastSuccess" > #{{job.lastSuccess}}< / router-link > < / li >
< li v-if = "job.lastFailure<job.lastSuccess" :id = "'rcd_'+job.name" class = "chart-overlay" > < router-link :to = "'/jobs/'+job.name" > {{job.name}}< / router-link > : < span v-html = "runIcon('success')" > < / span > < router-link :to = "'/jobs/'+job.name+'/'+job.lastSuccess" > #{{job.lastSuccess}}< / router-link > since < span v-html = "runIcon('failed')" > < / span > < router-link :to = "'/jobs/'+job.name+'/'+job.lastFailure" > #{{job.lastFailure}}< / router-link > < / li >
< / ul >
< / div > < / div >
< / div >
< div >
< h3 > Low pass rates< / h3 >
< table >
< tr v-for = "job in lowPassRates" > < td > < router-link :to = "'/jobs/'+job.name" > {{job.name}}< / router-link > < / td > < td > {{Math.round(job.passRate*100)}} %< / td > < / tr >
< / table >
< / div >
< div class = "col-md-6" >
< div class = "panel panel-default" >
< div class = "panel-heading" > Low pass rates< / div >
< div class = "panel-body" >
< canvas id = "chartPassRates" > < / canvas >
< / div >
< / div >
< / div > < / div > < div class = "row" >
< div class = "col-md-6" >
< div class = "panel panel-default" >
< div class = "panel-heading" > Run time changes< / div >
< div class = "panel-body" >
< canvas id = "chartBuildTimeChanges" > < / canvas >
< / div >
< / div >
< div >
< h3 > Utilization< / h3 >
< div > < canvas id = "chartUtil" > < / canvas > < / div >
< / div >
< div class = "col-md-6" >
< div class = "panel panel-default" >
< div class = "panel-heading" > Average run time distribution< / div >
< div class = "panel-body" >
< canvas id = "chartBuildTimeDist" > < / canvas >
< / div >
< / div >
< / div >
< div >
< div style = "display: grid; grid-template-columns: repeat(auto-fill, minmax(380px, 1fr)); gap: 5px; padding: 5px;" >
< div > < canvas id = "chartBpd" > < / canvas > < / div >
< div > < canvas id = "chartBpj" > < / canvas > < / div >
< div > < canvas id = "chartTpj" > < / canvas > < / div >
< div > < canvas id = "chartBuildTimeChanges" > < / canvas > < / div >
< div > < canvas id = "chartBuildTimeDist" > < / canvas > < / div >
< / div >
< / div > < / div >
< / div> < / div >
< / div >
< / section >
< / 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 v-show = "ungrouped.length" :class = "{'active':group==null}" > < a href v-on:click . prevent = "group = null" > Ungrouped Jobs< / a > < / li >
< li v-for = "g in Object.keys(groups)" :class = "{'active':g==group}" > < a href v-on:click . prevent = "group = g" > {{g}}< / 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 >
< td class = "text-center" > {{formatDuration(job.started,job.completed)}}< / td >
< / tr >
< / table >
< nav style = "display: grid; grid-auto-flow: column; justify-content: space-between; align-items: end; padding: 10px 15px;" >
< div style = "display:grid; grid-auto-flow: column; grid-gap: 15px; padding: 5px 0;" >
< a v-show = "ungrouped.length" :class = "{'active':group==null}" href v-on:click . prevent = "group = null" > Ungrouped Jobs< / a >
< a v-for = "g in Object.keys(groups)" :class = "{'active':g==group}" href v-on:click . prevent = "group = g" > {{g}}< / a >
< / div >
< / div > < / div >
< input class = "form-control" id = "jobFilter" v-model = "search" placeholder = "Filter..." >
< / nav >
< table class = "striped" id = "job-list" >
< tr v-for = "job in filteredJobs()" >
< td > < router-link :to = "'/jobs/'+job.name" > {{job.name}}< / router-link > < / td >
< td style = "white-space: nowrap;" > < span v-html = "runIcon(job.result)" > < / span > < router-link :to = "'/jobs/'+job.name+'/'+job.number" > #{{job.number}}< / router-link > < / td >
< td > {{formatDate(job.started)}}< / td >
< td > {{formatDuration(job.started,job.completed)}}< / td >
< / tr >
< / table >
< / 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 >
< div v-html = "description" > < / div >
< 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 >
< template id = "job" > < div id = "page-job-main" >
< div style = "padding: 15px;" >
< h2 > {{$route.params.name}}< / h2 >
< div v-html = "description" > < / div >
< dl >
< 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 style = "display: grid; justify-content: center; padding: 15px;" >
< canvas id = "chartBt" > < / canvas >
< / div >
< div style = "grid-column: 1/-1" >
< table class = "striped" >
< thead > < tr >
< th > < a class = "sort" :class = "(sort.field=='result'?sort.order:'')" v-on:click = "do_sort('result')" > < / a > < / th >
< th > Run < a class = "sort" :class = "(sort.field=='number'?sort.order:'')" v-on:click = "do_sort('number')" > < / a > < / th >
< th class = "text-center" > Started < a class = "sort" :class = "(sort.field=='started'?sort.order:'')" v-on:click = "do_sort('started')" > < / a > < / th >
< th class = "text-center" > Duration < a class = "sort" :class = "(sort.field=='duration'?sort.order:'')" v-on:click = "do_sort('duration')" > < / a > < / th >
< th class = "text-center vp-sm-hide" > Reason < a class = "sort" :class = "(sort.field=='reason'?sort.order:'')" v-on:click = "do_sort('reason')" > < / a > < / th >
< / tr > < / thead >
< tr v-show = "nQueued" >
< td colspan = "5" > < i > {{nQueued}} run(s) queued< / i > < / td >
< / tr >
< tr v-for = "job in jobsRunning.concat(jobsRecent)" track-by = "$index" >
< td style = "width:1px" > < span v-html = "runIcon(job.result)" > < / span > < / td >
< td > < 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" > {{formatDuration(job.started, job.completed)}}< / td >
< td class = "text-center vp-sm-hide" > {{job.reason}}< / td >
< / tr >
< / table >
< div style = "float: right; margin: 15px; display: inline-grid; grid-auto-flow: column; gap: 10px; align-items: center" >
< button v-on:click = "page_prev" :disabled = "sort.page==0" > « < / button >
< span > Page {{sort.page+1}} of {{pages}}< / span >
< button class = "btn" v-on:click = "page_next" :disabled = "sort.page==pages-1" > » < / button >
< / div >
< div class = "row" > < div class = "col-xs-12" >
< table class = "table table-striped" >
< thead > < tr >
< th > < a class = "sort" :class = "(sort.field=='result'?sort.order:'')" v-on:click = "do_sort('result')" > < / a > < / th >
< th > Run < a class = "sort" :class = "(sort.field=='number'?sort.order:'')" v-on:click = "do_sort('number')" > < / a > < / th >
< th class = "text-center" > Started < a class = "sort" :class = "(sort.field=='started'?sort.order:'')" v-on:click = "do_sort('started')" > < / a > < / th >
< th class = "text-center" > Duration < a class = "sort" :class = "(sort.field=='duration'?sort.order:'')" v-on:click = "do_sort('duration')" > < / a > < / th >
< th class = "text-center hidden-xs" > Reason < a class = "sort" :class = "(sort.field=='reason'?sort.order:'')" v-on:click = "do_sort('reason')" > < / a > < / th >
< / tr > < / thead >
< tr v-show = "nQueued" >
< td colspan = "5" > < i > {{nQueued}} run(s) queued< / i > < / td >
< / tr >
< tr v-for = "job in jobsRunning" track-by = "$index" >
< td style = "width:1px" > < span v-html = "runIcon(job.result)" > < / span > < / td >
< td > < 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" > {{formatDuration(job.started, job.completed)}}< / td >
< td class = "text-center hidden-xs" > {{job.reason}}< / td >
< / tr >
< tr v-for = "job in jobsRecent" track-by = "$index" >
< td style = "width:1px" > < span v-html = "runIcon(job.result)" > < / span > < / td >
< td > < 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" > {{formatDuration(job.started, job.completed)}}< / td >
< td class = "text-center hidden-xs" > {{job.reason}}< / td >
< / tr >
< / table >
< ul class = "pagination pull-right" >
< li > < button class = "btn btn-default" v-on:click = "page_prev" :disabled = "sort.page==0" > « < / button > < / li >
< li > Page {{sort.page+1}} of {{pages}}< / li >
< li > < button class = "btn btn-default" v-on:click = "page_next" :disabled = "sort.page==pages-1" > » < / button > < / li >
< / ul >
< / 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" > < 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 v-show = "job.upstream.num > 0" > Upstream< / dt > < dd v-show = "job.upstream.num > 0" > < router-link :to = "'/jobs/'+job.upstream.name" > {{job.upstream.name}}< / router-link > < router-link :to = "'/jobs/'+job.upstream.name+'/'+job.upstream.num" > #{{job.upstream.num}}< / router-link > < / li > < / 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 > Duration< / dt > < dd > {{formatDuration(job.started, job.completed)}}< / dd >
< / dl >
< / div >
< div class = "col-sm-7 col-md-6 col-lg-5" >
< template id = "run" > < div style = "display: grid; grid-template-rows: auto 1fr" >
< div style = "padding: 15px" >
< div style = "display: grid; grid-template-columns: auto 1fr 400px auto auto; gap: 5px; align-items: center" >
< h2 style = "white-space: nowrap" > < span v-html = "runIcon(job.result)" > < / span > {{$route.params.name}} #{{$route.params.number}}< / h2 >
< span > < / span >
< div > <!-- extra div to preserve grid columns when v - show hides the progress bar -->
< 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" > < a :href = "art.url" target = "_self" > {{art.filename}}< / a > [{{ art.size | iecFileSize }}]< / li >
< / ul >
< / div >
< div class = "progress-bar" :class = "{overtime:job.overtime,indeterminate:!job.etc}" :style = "job.etc && {width:job.progress+'%'}" > < / div >
< / div >
< / div >
< router-link :disabled = "$route.params.number == 1" :to = "'/jobs/'+$route.params.name+'/'+($route.params.number-1)" tag = "button" > « < / router-link >
< router-link :disabled = "$route.params.number == latestNum" :to = "'/jobs/'+$route.params.name+'/'+(parseInt($route.params.number)+1)" tag = "button" > » < / router-link >
< / 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 id = "page-run-detail" >
< dl >
< dt > Reason< / dt > < dd > {{job.reason}}< / dd >
< dt v-show = "job.upstream.num > 0" > Upstream< / dt > < dd v-show = "job.upstream.num > 0" > < router-link :to = "'/jobs/'+job.upstream.name" > {{job.upstream.name}}< / router-link > < router-link :to = "'/jobs/'+job.upstream.name+'/'+job.upstream.num" > #{{job.upstream.num}}< / router-link > < / li > < / 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 > Duration< / dt > < dd > {{formatDuration(job.started, job.completed)}}< / dd >
< / dl >
< dl v-show = "job.artifacts.length" >
< dt > Artifacts< / dt >
< dd >
< ul style = "margin-bottom: 0" >
< li v-for = "art in job.artifacts" > < a :href = "art.url" target = "_self" > {{art.filename}}< / a > [{{ art.size | iecFileSize }}]< / li >
< / ul >
< / dd >
< / dl >
< / div >
< / div >
< div class = "console-log" >
< code v-html = "log" > < / code >
< span v-show = "job.result == 'running'" v-html = "runIcon('running')" style = "display: block;" > < / span >
< / div >
< / div > < / template >
< div id = "app" >
< nav class = "navbar navbar-inverse" >
< 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 >
< main id = "app" style = "display: grid; grid-template-rows: auto 1fr auto; height: 100%;" >
< nav id = "nav-top" style = "display: grid; grid-template-columns: auto auto 1fr auto; grid-gap: 15px;" >
< router-link to = "/" style = "display: grid; grid-auto-flow: column; align-items: center; margin: 5px; font-size: 20px;" >
< img src = "icon.png" > {{title}}
< / router-link >
< div id = "nav-top-links" style = "display: grid; grid-auto-flow: column; justify-content: start; gap: 15px; padding: 0 15px; align-items: center; font-size: 16px;" >
< router-link to = "/jobs" > Jobs< / router-link >
< router-link v-for = "(crumb,i) in _route.path.slice(1).split('/').slice(1,-1)" :to = "_route.path.split('/').slice(0,i+3).join('/')" > {{crumb}}< / router-link >
< / div >
< div > < / div >
< div style = "display: grid; align-items: center; padding: 0 15px" >
< a v-on:click = "toggleNotifications(!notify)" class = "nav-icon" :class = "{active:notify}" v-show = "supportsNotifications" :title = "(notify?'Disable':'Enable')+' notifications'" >
< svg width = "18" viewBox = "0 0 12 12" >
< g stroke-width = "0.5" >
< path d = "m 6,9 c -1,0 -1,0 -1,1 0,1 2,1 2,0 0,-1 0,-1 -1,-1 z" / >
< path d = "m 1,10 c 3,-3 1,-9 5,-9 4,0 2,6 5,9 1,1 -3,-1 -5,-1 -2,0 -6,2 -5,1 z" / >
< / g >
< / svg >
< / a >
< / div >
< / nav >
< a v-on:click = "toggleNotifications(!notify)" v-show = "supportsNotifications" class = "bell pull-right" :class = "{'active':notify}" :title = "(notify?'Disable':'Enable')+' notifications'" > 🔔 < / a >
< router-view > < / router-view >
< div v-show = "!connected" id = "popup-connecting" > < span class = "status spin" > ⚙︎ < / span > Connecting...< / div >
< / div >
< div id = "connecting-overlay" :class = "{shown:!connected}" >
< div > < span v-html = "runIcon('running')" > < / span > Connecting...< / div >
< / div >
< / main >
< / body >
< / html >