mirror of
https://github.com/ohwgiles/laminar.git
synced 2024-10-27 20:34:20 +00:00
webui refresh
WebUI rewritten in a more modern style, bootstrap is dropped in favour of plain css/grid. Hand-crafted svgs replace utf-8 glyphs for a more uniform look and smoother animation. webmanifest added for better mobile behaviour. No doubt minor tweaks will follow... resolves #57
This commit is contained in:
parent
4554039703
commit
ae560b9de4
@ -62,7 +62,7 @@ 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
|
||||||
favicon.ico favicon-152.png icon.png)
|
style.css manifest.webmanifest favicon.ico favicon-152.png icon.png)
|
||||||
|
|
||||||
# The code that allows dynamic modifying of index.html requires knowing its original size
|
# The code that allows dynamic modifying of index.html requires knowing its original size
|
||||||
add_custom_command(OUTPUT index_html_size.h
|
add_custom_command(OUTPUT index_html_size.h
|
||||||
@ -78,11 +78,9 @@ 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/2.7.2/Chart.min.js
|
file(DOWNLOAD https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.7.2/Chart.min.js
|
||||||
js/Chart.min.js EXPECTED_MD5 f6c8efa65711e0cbbc99ba72997ecd0e)
|
js/Chart.min.js EXPECTED_MD5 f6c8efa65711e0cbbc99ba72997ecd0e)
|
||||||
file(DOWNLOAD https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css
|
|
||||||
css/bootstrap.min.css EXPECTED_MD5 5d5357cb3704e1f43a1f5bfed2aebf42)
|
|
||||||
# ...and compile them
|
# ...and compile them
|
||||||
generate_compressed_bins(${CMAKE_BINARY_DIR} js/vue-router.min.js js/vue.min.js
|
generate_compressed_bins(${CMAKE_BINARY_DIR} js/vue-router.min.js js/vue.min.js
|
||||||
js/ansi_up.js js/Chart.min.js css/bootstrap.min.css)
|
js/ansi_up.js js/Chart.min.js)
|
||||||
# (see resources.cpp where these are fetched)
|
# (see resources.cpp where these are fetched)
|
||||||
|
|
||||||
set(LAMINARD_CORE_SOURCES
|
set(LAMINARD_CORE_SOURCES
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
///
|
///
|
||||||
/// Copyright 2015-2019 Oliver Giles
|
/// Copyright 2015-2020 Oliver Giles
|
||||||
///
|
///
|
||||||
/// This file is part of Laminar
|
/// This file is part of Laminar
|
||||||
///
|
///
|
||||||
@ -32,6 +32,7 @@
|
|||||||
#define CONTENT_TYPE_PNG "image/png"
|
#define CONTENT_TYPE_PNG "image/png"
|
||||||
#define CONTENT_TYPE_JS "application/javascript; charset=utf-8"
|
#define CONTENT_TYPE_JS "application/javascript; charset=utf-8"
|
||||||
#define CONTENT_TYPE_CSS "text/css; charset=utf-8"
|
#define CONTENT_TYPE_CSS "text/css; charset=utf-8"
|
||||||
|
#define CONTENT_TYPE_MANIFEST "application/manifest+json; charset=utf-8"
|
||||||
|
|
||||||
#define GZIP_FORMAT 16
|
#define GZIP_FORMAT 16
|
||||||
|
|
||||||
@ -46,7 +47,8 @@ Resources::Resources()
|
|||||||
INIT_RESOURCE("/js/vue-router.min.js", js_vue_router_min_js, CONTENT_TYPE_JS);
|
INIT_RESOURCE("/js/vue-router.min.js", js_vue_router_min_js, CONTENT_TYPE_JS);
|
||||||
INIT_RESOURCE("/js/ansi_up.js", js_ansi_up_js, CONTENT_TYPE_JS);
|
INIT_RESOURCE("/js/ansi_up.js", js_ansi_up_js, CONTENT_TYPE_JS);
|
||||||
INIT_RESOURCE("/js/Chart.min.js", js_Chart_min_js, CONTENT_TYPE_JS);
|
INIT_RESOURCE("/js/Chart.min.js", js_Chart_min_js, CONTENT_TYPE_JS);
|
||||||
INIT_RESOURCE("/css/bootstrap.min.css", css_bootstrap_min_css, CONTENT_TYPE_CSS);
|
INIT_RESOURCE("/style.css", style_css, CONTENT_TYPE_CSS);
|
||||||
|
INIT_RESOURCE("/manifest.webmanifest", manifest_webmanifest, CONTENT_TYPE_MANIFEST);
|
||||||
// Configure the default template
|
// Configure the default template
|
||||||
setHtmlTemplate(std::string());
|
setHtmlTemplate(std::string());
|
||||||
}
|
}
|
||||||
|
@ -8,293 +8,151 @@
|
|||||||
<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">
|
||||||
<link rel="icon" href="favicon.ico">
|
<link rel="icon" href="favicon.ico">
|
||||||
|
<link rel="manifest" href="/manifest.webmanifest">
|
||||||
<title>Laminar</title>
|
<title>Laminar</title>
|
||||||
<script src="js/vue.min.js"></script>
|
<script src="js/vue.min.js"></script>
|
||||||
<script src="js/vue-router.min.js"></script>
|
<script src="js/vue-router.min.js"></script>
|
||||||
<script src="js/ansi_up.js"></script>
|
<script src="js/ansi_up.js"></script>
|
||||||
<script src="js/Chart.min.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>
|
<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>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<template id="home"><div>
|
<template id="home"><div id="page-home-main">
|
||||||
<ol class="breadcrumb"><li class="active">Home</li></ol>
|
<nav>
|
||||||
<div class="container-fluid"><div class="row">
|
<table class="table striped">
|
||||||
<div class="col-sm-5 col-md-4 col-lg-3 dash">
|
|
||||||
<table class="table table-bordered">
|
|
||||||
<tr v-for="job in jobsQueued">
|
<tr v-for="job in jobsQueued">
|
||||||
<td><router-link :to="'/jobs/'+job.name">{{job.name}}</router-link> <i>queued</i></td>
|
<td><router-link :to="'/jobs/'+job.name">{{job.name}}</router-link> <i>queued</i></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr v-for="job in jobsRunning">
|
<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>
|
<td>
|
||||||
<small class="pull-right">{{formatDuration(job.started, job.completed)}}</small>
|
<span v-html="runIcon(job.result)"></span>
|
||||||
<div class="progress">
|
<router-link :to="'/jobs/'+job.name">{{job.name}}</router-link>
|
||||||
<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>
|
<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>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr v-for="job in jobsRecent">
|
<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>
|
<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>
|
</tr>
|
||||||
</table>
|
</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>
|
||||||
<div class="col-sm-7 col-md-8 col-lg-9"><div class="row">
|
<div>
|
||||||
<div class="col-md-6">
|
<h3>Low pass rates</h3>
|
||||||
<div class="panel panel-default">
|
<table>
|
||||||
<div class="panel-heading">Total runs per day this week</div>
|
<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>
|
||||||
<div class="panel-body">
|
</table>
|
||||||
<canvas id="chartBpd"></canvas>
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3>Utilization</h3>
|
||||||
|
<div><canvas id="chartUtil"></canvas></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div>
|
||||||
<div class="col-md-6">
|
<div style="display: grid; grid-template-columns: repeat(auto-fill, minmax(380px, 1fr)); gap: 5px; padding: 5px;">
|
||||||
<div class="panel panel-default">
|
<div><canvas id="chartBpd"></canvas></div>
|
||||||
<div class="panel-heading">Most runs per job in the last 24 hours</div>
|
<div><canvas id="chartBpj"></canvas></div>
|
||||||
<div class="panel-body" id="chartStatus">
|
<div><canvas id="chartTpj"></canvas></div>
|
||||||
<canvas id="chartBpj"></canvas>
|
<div><canvas id="chartBuildTimeChanges"></canvas></div>
|
||||||
|
<div><canvas id="chartBuildTimeDist"></canvas></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div></div><div class="row">
|
</section>
|
||||||
<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>
|
|
||||||
</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>
|
|
||||||
<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>
|
|
||||||
<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>
|
|
||||||
</div></div>
|
|
||||||
</div></template>
|
</div></template>
|
||||||
|
|
||||||
<template id="jobs"><div>
|
<template id="jobs"><div>
|
||||||
<ol class="breadcrumb"><li><router-link to="/">Home</router-link></li><li class="active">Jobs</li></ol>
|
<nav style="display: grid; grid-auto-flow: column; justify-content: space-between; align-items: end; padding: 10px 15px;">
|
||||||
<div class="container-fluid"><div class="row">
|
<div style="display:grid; grid-auto-flow: column; grid-gap: 15px; padding: 5px 0;">
|
||||||
<div class="col-xs-12">
|
<a v-show="ungrouped.length" :class="{'active':group==null}" href v-on:click.prevent="group = null">Ungrouped Jobs</a>
|
||||||
<div class="pull-right">
|
<a v-for="g in Object.keys(groups)" :class="{'active':g==group}" href v-on:click.prevent="group = g">{{g}}</a>
|
||||||
<input class="form-control" id="jobFilter" v-model="search" placeholder="Filter...">
|
|
||||||
</div>
|
</div>
|
||||||
<ul class="nav nav-tabs">
|
<input class="form-control" id="jobFilter" v-model="search" placeholder="Filter...">
|
||||||
<li v-show="ungrouped.length" :class="{'active':group==null}"><a href v-on:click.prevent="group = null">Ungrouped Jobs</a></li>
|
</nav>
|
||||||
<li v-for="g in Object.keys(groups)" :class="{'active':g==group}"><a href v-on:click.prevent="group = g">{{g}}</a></li>
|
<table class="striped" id="job-list">
|
||||||
</ul>
|
|
||||||
<table class="table table-striped" id="joblist">
|
|
||||||
<tr v-for="job in filteredJobs()">
|
<tr v-for="job in filteredJobs()">
|
||||||
<td><router-link :to="'/jobs/'+job.name">{{job.name}}</router-link></td>
|
<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 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 class="text-center">{{formatDate(job.started)}}</td>
|
<td>{{formatDate(job.started)}}</td>
|
||||||
<td class="text-center">{{formatDuration(job.started,job.completed)}}</td>
|
<td>{{formatDuration(job.started,job.completed)}}</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
|
||||||
</div></div>
|
|
||||||
</div></template>
|
</div></template>
|
||||||
|
|
||||||
<template id="job"><div>
|
<template id="job"><div id="page-job-main">
|
||||||
<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 style="padding: 15px;">
|
||||||
<div class="container-fluid">
|
<h2>{{$route.params.name}}</h2>
|
||||||
<div class="row">
|
|
||||||
<div class="col-sm-5 col-md-6 col-lg-7">
|
|
||||||
<h3>{{$route.params.name}}</h3>
|
|
||||||
<div v-html="description"></div>
|
<div v-html="description"></div>
|
||||||
<dl class="dl-horizontal">
|
<dl>
|
||||||
<dt>Last Successful Run</dt>
|
<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>
|
<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>
|
<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>
|
<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>
|
</dl>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-sm-7 col-md-6 col-lg-5">
|
<div style="display: grid; justify-content: center; padding: 15px;">
|
||||||
<div class="panel panel-default">
|
|
||||||
<div class="panel-heading">Build time</div>
|
|
||||||
<div class="panel-body">
|
|
||||||
<canvas id="chartBt"></canvas>
|
<canvas id="chartBt"></canvas>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div style="grid-column: 1/-1">
|
||||||
</div>
|
<table class="striped">
|
||||||
</div>
|
|
||||||
<div class="row"><div class="col-xs-12">
|
|
||||||
<table class="table table-striped">
|
|
||||||
<thead><tr>
|
<thead><tr>
|
||||||
<th><a class="sort" :class="(sort.field=='result'?sort.order:'')" v-on:click="do_sort('result')"> </a></th>
|
<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>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">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">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>
|
<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></thead>
|
||||||
<tr v-show="nQueued">
|
<tr v-show="nQueued">
|
||||||
<td colspan="5"><i>{{nQueued}} run(s) queued</i></td>
|
<td colspan="5"><i>{{nQueued}} run(s) queued</i></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr v-for="job in jobsRunning" track-by="$index">
|
<tr v-for="job in jobsRunning.concat(jobsRecent)" track-by="$index">
|
||||||
<td style="width:1px"><span v-html="runIcon(job.result)"></span></td>
|
<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><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">{{formatDate(job.started)}}</td>
|
||||||
<td class="text-center">{{formatDuration(job.started, job.completed)}}</td>
|
<td class="text-center">{{formatDuration(job.started, job.completed)}}</td>
|
||||||
<td class="text-center hidden-xs">{{job.reason}}</td>
|
<td class="text-center vp-sm-hide">{{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>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
<ul class="pagination pull-right">
|
<div style="float: right; margin: 15px; display: inline-grid; grid-auto-flow: column; gap: 10px; align-items: center">
|
||||||
<li><button class="btn btn-default" v-on:click="page_prev" :disabled="sort.page==0">«</button></li>
|
<button v-on:click="page_prev" :disabled="sort.page==0">«</button>
|
||||||
<li>Page {{sort.page+1}} of {{pages}}</li>
|
<span>Page {{sort.page+1}} of {{pages}}</span>
|
||||||
<li><button class="btn btn-default" v-on:click="page_next" :disabled="sort.page==pages-1">»</button></li>
|
<button class="btn" v-on:click="page_next" :disabled="sort.page==pages-1">»</button>
|
||||||
</ul>
|
</div>
|
||||||
</div></div>
|
|
||||||
</div>
|
</div>
|
||||||
</div></template>
|
</div></template>
|
||||||
|
|
||||||
<template id="run"><div>
|
<template id="run"><div style="display: grid; grid-template-rows: auto 1fr">
|
||||||
<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 style="padding: 15px">
|
||||||
<div class="container-fluid">
|
<div style="display: grid; grid-template-columns: auto 1fr 400px auto auto; gap: 5px; align-items: center">
|
||||||
<div class="row">
|
<h2 style="white-space: nowrap"><span v-html="runIcon(job.result)"></span> {{$route.params.name}} #{{$route.params.number}}</h2>
|
||||||
<div class="col-sm-5 col-md-6 col-lg-7">
|
<span></span>
|
||||||
<h3 style="float:left"><span v-html="runIcon(job.result)"></span> {{$route.params.name}} #{{$route.params.number}}</h3>
|
<div><!-- extra div to preserve grid columns when v-show hides the progress bar -->
|
||||||
<nav class="pull-left">
|
<div class="progress" v-show="job.result == 'running'">
|
||||||
<ul class="pagination" style="margin:15px 20px">
|
<div class="progress-bar" :class="{overtime:job.overtime,indeterminate:!job.etc}" :style="job.etc && {width:job.progress+'%'}"></div>
|
||||||
<li v-show="$route.params.number > 1"><router-link :to="'/jobs/'+$route.params.name+'/'+($route.params.number-1)">«</router-link></li>
|
</div>
|
||||||
<li v-show="latestNum > $route.params.number"><router-link :to="'/jobs/'+$route.params.name+'/'+(parseInt($route.params.number)+1)">»</router-link></li>
|
</div>
|
||||||
</ul>
|
<router-link :disabled="$route.params.number == 1" :to="'/jobs/'+$route.params.name+'/'+($route.params.number-1)" tag="button">«</router-link>
|
||||||
</nav>
|
<router-link :disabled="$route.params.number == latestNum" :to="'/jobs/'+$route.params.name+'/'+(parseInt($route.params.number)+1)" tag="button">»</router-link>
|
||||||
<div style="clear:both;"></div>
|
</div>
|
||||||
<dl class="dl-horizontal">
|
<div id="page-run-detail">
|
||||||
|
<dl>
|
||||||
<dt>Reason</dt><dd>{{job.reason}}</dd>
|
<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 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>Queued for</dt><dd>{{job.queued}}s</dd>
|
||||||
@ -302,40 +160,48 @@
|
|||||||
<dt v-show="runComplete(job)">Completed</dt><dd v-show="job.completed">{{formatDate(job.completed)}}</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>
|
<dt>Duration</dt><dd>{{formatDuration(job.started, job.completed)}}</dd>
|
||||||
</dl>
|
</dl>
|
||||||
</div>
|
<dl v-show="job.artifacts.length">
|
||||||
<div class="col-sm-7 col-md-6 col-lg-5">
|
<dt>Artifacts</dt>
|
||||||
<div class="progress" v-show="job.result == 'running'">
|
<dd>
|
||||||
<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>
|
<ul style="margin-bottom: 0">
|
||||||
</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>
|
<li v-for="art in job.artifacts"><a :href="art.url" target="_self">{{art.filename}}</a> [{{ art.size | iecFileSize }}]</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
</dd>
|
||||||
|
</dl>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div class="console-log">
|
||||||
</div>
|
<code v-html="log"></code>
|
||||||
<div class="row"><div class="col-xs-12">
|
<span v-show="job.result == 'running'" v-html="runIcon('running')" style="display: block;"></span>
|
||||||
<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>
|
||||||
</div></template>
|
</div></template>
|
||||||
|
|
||||||
<div id="app">
|
<main id="app" style="display: grid; grid-template-rows: auto 1fr auto; height: 100%;">
|
||||||
<nav class="navbar navbar-inverse">
|
<nav id="nav-top" style="display: grid; grid-template-columns: auto auto 1fr auto; grid-gap: 15px;">
|
||||||
<div>
|
<router-link to="/" style="display: grid; grid-auto-flow: column; align-items: center; margin: 5px; font-size: 20px;">
|
||||||
<router-link to="/" class="navbar-brand"><img src="icon.png">{{title}}</router-link>
|
<img src="icon.png"> {{title}}
|
||||||
<router-link to="/jobs" class="btn navbar-btn pull-right">Jobs</router-link>
|
</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>
|
</div>
|
||||||
</nav>
|
</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>
|
<router-view></router-view>
|
||||||
<div v-show="!connected" id="popup-connecting"><span class="status spin">⚙︎</span> Connecting...</div>
|
<div id="connecting-overlay" :class="{shown:!connected}">
|
||||||
|
<div><span v-html="runIcon('running')"></span> Connecting...</div>
|
||||||
</div>
|
</div>
|
||||||
|
</main>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
||||||
|
@ -103,15 +103,24 @@ const ServerEventHandler = function() {
|
|||||||
const Utils = {
|
const Utils = {
|
||||||
methods: {
|
methods: {
|
||||||
runIcon(result) {
|
runIcon(result) {
|
||||||
var marker = '⚙';
|
return (result == 'success') ? /* checkmark */
|
||||||
var classname = result;
|
`<svg class="status success" viewBox="0 0 100 100">
|
||||||
if (result === 'success')
|
<path d="m 23,46 c -6,0 -17,3 -17,11 0,8 9,30 12,32 3,2 14,5 20,-2 6,-6 24,-36
|
||||||
marker = '✔';
|
56,-71 5,-3 -9,-8 -23,-2 -13,6 -33,42 -41,47 -6,-3 -5,-12 -8,-15 z" />
|
||||||
else if (result === 'failed' || result === 'aborted')
|
</svg>`
|
||||||
marker = '✘';
|
: (result == 'failed' || result == 'aborted') ? /* cross */
|
||||||
else
|
`<svg class="status failed" viewBox="0 0 100 100">
|
||||||
classname = 'spin';
|
<path d="m 19,20 c 2,8 12,29 15,32 -5,5 -18,21 -21,26 2,3 8,15 11,18 4,-6 17,-21
|
||||||
return '<span title="' + result + '" class="status ' + classname + '">' + marker + '︎</span>';
|
21,-26 5,5 11,15 15,20 8,-2 15,-9 20,-15 -3,-3 -17,-18 -20,-24 3,-5 23,-26 30,-33 -3,-5 -8,-9
|
||||||
|
-12,-12 -6,5 -26,26 -29,30 -6,-8 -11,-15 -15,-23 -3,0 -12,5 -15,7 z" />
|
||||||
|
</svg>`
|
||||||
|
: /* spinner */
|
||||||
|
`<svg class="status running" viewBox="0 0 100 100">
|
||||||
|
<circle cx="50" cy="50" r="40" stroke-width="15" fill="none" stroke-dasharray="175">
|
||||||
|
<animateTransform attributeName="transform" type="rotate" repeatCount="indefinite" dur="2s" values="0 50 50;360 50 50"></animateTransform>
|
||||||
|
</circle>
|
||||||
|
</svg>`
|
||||||
|
;
|
||||||
},
|
},
|
||||||
formatDate: function(unix) {
|
formatDate: function(unix) {
|
||||||
// TODO: reimplement when toLocaleDateString() accepts formatting options on most browsers
|
// TODO: reimplement when toLocaleDateString() accepts formatting options on most browsers
|
||||||
@ -145,7 +154,8 @@ const ProgressUpdater = {
|
|||||||
var p = (Math.floor(Date.now()/1000) + this.$root.clockSkew - o.started) / (o.etc - o.started);
|
var p = (Math.floor(Date.now()/1000) + this.$root.clockSkew - o.started) / (o.etc - o.started);
|
||||||
if (p > 1.2) {
|
if (p > 1.2) {
|
||||||
o.overtime = true;
|
o.overtime = true;
|
||||||
} else if (p >= 1) {
|
}
|
||||||
|
if (p >= 1) {
|
||||||
o.progress = 99;
|
o.progress = 99;
|
||||||
} else {
|
} else {
|
||||||
o.progress = 100 * p;
|
o.progress = 100 * p;
|
||||||
@ -178,7 +188,8 @@ const Home = function() {
|
|||||||
var state = {
|
var state = {
|
||||||
jobsQueued: [],
|
jobsQueued: [],
|
||||||
jobsRecent: [],
|
jobsRecent: [],
|
||||||
resultChanged: []
|
resultChanged: [],
|
||||||
|
lowPassRates: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
var chtUtilization, chtBuildsPerDay, chtBuildsPerJob, chtTimePerJob;
|
var chtUtilization, chtBuildsPerDay, chtBuildsPerJob, chtTimePerJob;
|
||||||
@ -201,6 +212,7 @@ const Home = function() {
|
|||||||
state.jobsRunning = msg.running;
|
state.jobsRunning = msg.running;
|
||||||
state.jobsRecent = msg.recent;
|
state.jobsRecent = msg.recent;
|
||||||
state.resultChanged = msg.resultChanged;
|
state.resultChanged = msg.resultChanged;
|
||||||
|
state.lowPassRates = msg.lowPassRates;
|
||||||
this.$forceUpdate();
|
this.$forceUpdate();
|
||||||
|
|
||||||
// setup charts
|
// setup charts
|
||||||
@ -243,6 +255,7 @@ const Home = function() {
|
|||||||
}]
|
}]
|
||||||
},
|
},
|
||||||
options:{
|
options:{
|
||||||
|
title: { display: true, text: 'Builds per day' },
|
||||||
tooltips:{callbacks:{title: function(tip, data) {
|
tooltips:{callbacks:{title: function(tip, data) {
|
||||||
return buildsPerDayDates[tip[0].index].long;
|
return buildsPerDayDates[tip[0].index].long;
|
||||||
}}},
|
}}},
|
||||||
@ -263,6 +276,7 @@ const Home = function() {
|
|||||||
}]
|
}]
|
||||||
},
|
},
|
||||||
options:{
|
options:{
|
||||||
|
title: { display: true, text: 'Builds per job' },
|
||||||
scales:{xAxes:[{ticks:{userCallback: (label, index, labels)=>{
|
scales:{xAxes:[{ticks:{userCallback: (label, index, labels)=>{
|
||||||
if(Number.isInteger(label))
|
if(Number.isInteger(label))
|
||||||
return label;
|
return label;
|
||||||
@ -281,6 +295,7 @@ const Home = function() {
|
|||||||
}]
|
}]
|
||||||
},
|
},
|
||||||
options:{
|
options:{
|
||||||
|
title: { display: true, text: 'Mean run time this week' },
|
||||||
scales:{xAxes:[{
|
scales:{xAxes:[{
|
||||||
ticks:{userCallback: tpjScale.scale},
|
ticks:{userCallback: tpjScale.scale},
|
||||||
scaleLabel: {
|
scaleLabel: {
|
||||||
@ -293,52 +308,6 @@ const Home = function() {
|
|||||||
}}}
|
}}}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
var chtResultChanges = new Chart(document.getElementById("chartResultChanges"), {
|
|
||||||
type: 'horizontalBar',
|
|
||||||
data: {
|
|
||||||
labels: msg.resultChanged.map((e)=>{ return e.name; }),
|
|
||||||
datasets: [{
|
|
||||||
//label: '% Passed',
|
|
||||||
backgroundColor: msg.resultChanged.map((e)=>{return e.lastFailure > e.lastSuccess ? 'firebrick' : 'forestgreen';}),
|
|
||||||
data: msg.resultChanged.map((e)=>{ return e.lastSuccess - e.lastFailure; }),
|
|
||||||
itemid: msg.resultChanged.map((e)=> { return 'rcd_' + e.name; })
|
|
||||||
}]
|
|
||||||
},
|
|
||||||
options:{
|
|
||||||
scales:{
|
|
||||||
xAxes:[{ticks:{display: false}}],
|
|
||||||
yAxes:[{ticks:{display: false}}]
|
|
||||||
},
|
|
||||||
tooltips:{
|
|
||||||
enabled:false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
var chtPassRates = new Chart(document.getElementById("chartPassRates"), {
|
|
||||||
type: 'horizontalBar',
|
|
||||||
data: {
|
|
||||||
labels: msg.lowPassRates.map((e)=>{ return e.name }),
|
|
||||||
datasets: [{
|
|
||||||
stack: 'passrate',
|
|
||||||
label: '% Passed',
|
|
||||||
backgroundColor: "forestgreen",
|
|
||||||
data: msg.lowPassRates.map((e)=>{ return e.passRate*100; })
|
|
||||||
},{
|
|
||||||
stack:'passrate',
|
|
||||||
label: '% Failed',
|
|
||||||
backgroundColor: "firebrick",
|
|
||||||
data: msg.lowPassRates.map((e)=>{ return (1-e.passRate)*100; })
|
|
||||||
}],
|
|
||||||
},
|
|
||||||
options:{
|
|
||||||
scales:{xAxes:[{ticks:{callback:(val,idx,values)=>{
|
|
||||||
return val + '%';
|
|
||||||
}}}]},
|
|
||||||
tooltips:{
|
|
||||||
enabled:false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
var btcScale = timeScale(Math.max(msg.buildTimeChanges.map((e)=>{return Math.max(e.durations)})));
|
var btcScale = timeScale(Math.max(msg.buildTimeChanges.map((e)=>{return Math.max(e.durations)})));
|
||||||
var chtBuildTimeChanges = new Chart(document.getElementById("chartBuildTimeChanges"), {
|
var chtBuildTimeChanges = new Chart(document.getElementById("chartBuildTimeChanges"), {
|
||||||
type: 'line',
|
type: 'line',
|
||||||
@ -352,6 +321,7 @@ const Home = function() {
|
|||||||
}})
|
}})
|
||||||
},
|
},
|
||||||
options:{
|
options:{
|
||||||
|
title: { display: true, text: 'Build time changes' },
|
||||||
legend:{display:true},
|
legend:{display:true},
|
||||||
scales:{
|
scales:{
|
||||||
xAxes:[{ticks:{display: false}}],
|
xAxes:[{ticks:{display: false}}],
|
||||||
@ -377,6 +347,9 @@ const Home = function() {
|
|||||||
data: msg.buildTimeDist,
|
data: msg.buildTimeDist,
|
||||||
backgroundColor: "steelblue",
|
backgroundColor: "steelblue",
|
||||||
}]
|
}]
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
title: { display: true, text: 'Build time distribution' }
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@ -573,6 +546,7 @@ var Job = function() {
|
|||||||
}]
|
}]
|
||||||
},
|
},
|
||||||
options: {
|
options: {
|
||||||
|
title: { display: true, text: 'Build time' },
|
||||||
scales:{
|
scales:{
|
||||||
xAxes:[{},{
|
xAxes:[{},{
|
||||||
id: 'avg',
|
id: 'avg',
|
||||||
@ -651,9 +625,7 @@ const Run = function() {
|
|||||||
job: { artifacts: [], upstream: {} },
|
job: { artifacts: [], upstream: {} },
|
||||||
latestNum: null,
|
latestNum: null,
|
||||||
log: '',
|
log: '',
|
||||||
autoscroll: false
|
|
||||||
};
|
};
|
||||||
var firstLog = false;
|
|
||||||
const logFetcher = (vm, name, num) => {
|
const logFetcher = (vm, name, num) => {
|
||||||
const abort = new AbortController();
|
const abort = new AbortController();
|
||||||
fetch('log/'+name+'/'+num, {signal:abort.signal}).then(res => {
|
fetch('log/'+name+'/'+num, {signal:abort.signal}).then(res => {
|
||||||
@ -668,11 +640,6 @@ const Run = function() {
|
|||||||
return;
|
return;
|
||||||
state.log += ansi_up.ansi_to_html(value.replace(/</g,'<').replace(/>/g,'>').replace(/\033\[\{([^:]+):(\d+)\033\\/g, (m,$1,$2)=>{return '<a href="jobs/'+$1+'" onclick="return vroute(this);">'+$1+'</a>:<a href="jobs/'+$1+'/'+$2+'" onclick="return vroute(this);">#'+$2+'</a>';}));
|
state.log += ansi_up.ansi_to_html(value.replace(/</g,'<').replace(/>/g,'>').replace(/\033\[\{([^:]+):(\d+)\033\\/g, (m,$1,$2)=>{return '<a href="jobs/'+$1+'" onclick="return vroute(this);">'+$1+'</a>:<a href="jobs/'+$1+'/'+$2+'" onclick="return vroute(this);">#'+$2+'</a>';}));
|
||||||
vm.$forceUpdate();
|
vm.$forceUpdate();
|
||||||
if (!firstLog) {
|
|
||||||
firstLog = true;
|
|
||||||
} else if (state.autoscroll) {
|
|
||||||
window.scrollTo(0, document.body.scrollHeight);
|
|
||||||
}
|
|
||||||
return pump();
|
return pump();
|
||||||
});
|
});
|
||||||
}();
|
}();
|
||||||
@ -780,7 +747,8 @@ new Vue({
|
|||||||
new Notification('Job ' + data.result, {
|
new Notification('Job ' + data.result, {
|
||||||
body: data.name + ' ' + '#' + data.number + ': ' + data.result
|
body: data.name + ' ' + '#' + data.number + ': ' + data.result
|
||||||
});
|
});
|
||||||
}
|
},
|
||||||
|
runIcon: Utils.methods.runIcon
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
notify(e) { localStorage.setItem('showNotifications', e ? 1 : 0); }
|
notify(e) { localStorage.setItem('showNotifications', e ? 1 : 0); }
|
||||||
|
21
src/resources/manifest.webmanifest
Normal file
21
src/resources/manifest.webmanifest
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"short_name": "Laminar",
|
||||||
|
"name": "Laminar",
|
||||||
|
"description": "Lightweight Continuous Integration",
|
||||||
|
"icons": [
|
||||||
|
{
|
||||||
|
"src": "/icon.png",
|
||||||
|
"type": "image/png",
|
||||||
|
"sizes": "36x36"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "/favicon-152.png",
|
||||||
|
"type": "image/png",
|
||||||
|
"sizes": "152x152"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"start_url": "/",
|
||||||
|
"background_color": "#2F3340",
|
||||||
|
"display": "standalone",
|
||||||
|
"scope": "/"
|
||||||
|
}
|
236
src/resources/style.css
Normal file
236
src/resources/style.css
Normal file
@ -0,0 +1,236 @@
|
|||||||
|
/* colour scheme */
|
||||||
|
:root {
|
||||||
|
--main-bg: #fff;
|
||||||
|
--main-fg: #333;
|
||||||
|
--nav-bg: #2F3340;
|
||||||
|
--nav-bg-darker: #292b33;
|
||||||
|
--nav-fg: #d0d0d0;
|
||||||
|
--nav-fg-light: #fafafa;
|
||||||
|
--icon-enabled: #d8cb83;
|
||||||
|
--success: #74af77;
|
||||||
|
--failure: #883d3d;
|
||||||
|
--running: #4786ab;
|
||||||
|
--warning: #de9a34;
|
||||||
|
--link-fg: #2f4579;
|
||||||
|
--console-bg: #313235;
|
||||||
|
--console-fg: #fff;
|
||||||
|
--alt-row-bg: #fafafa;
|
||||||
|
--border-grey: #d0d0d0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* basic resets */
|
||||||
|
html { box-sizing: border-box; }
|
||||||
|
*, *:before, *:after { box-sizing: inherit; }
|
||||||
|
body, h1, h2, h3, h4, h5, h6, p, ol, ul {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
|
ol, ul { list-style: none; }
|
||||||
|
body, html { height: 100%; }
|
||||||
|
body {
|
||||||
|
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||||
|
font-size: 16px;
|
||||||
|
color: var(--main-fg);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* main header bar */
|
||||||
|
#nav-top { background-color: var(--nav-bg); }
|
||||||
|
#nav-top-links { background-color: var(--nav-bg-darker); }
|
||||||
|
#nav-top a { color: var(--nav-fg); }
|
||||||
|
#nav-top a:hover { color: white; text-decoration: none; }
|
||||||
|
|
||||||
|
/* navbar svg icons (enable notifications) */
|
||||||
|
.nav-icon { display: inherit; }
|
||||||
|
.nav-icon svg { fill: var(--nav-fg); stroke: #000; }
|
||||||
|
.nav-icon:hover { cursor: pointer; }
|
||||||
|
.nav-icon:hover svg { fill: var(--nav-fg-light); }
|
||||||
|
.nav-icon.active svg { fill: var(--icon-enabled); }
|
||||||
|
|
||||||
|
/* anchors */
|
||||||
|
a { color: var(--link-fg); text-decoration: none; }
|
||||||
|
a:visited { color: var(--link-fg); }
|
||||||
|
a:active { color: var(--link-fg); }
|
||||||
|
a:hover { text-decoration: underline; }
|
||||||
|
|
||||||
|
/* charts */
|
||||||
|
canvas {
|
||||||
|
width: 100% !important;
|
||||||
|
max-width: 800px;
|
||||||
|
height: auto !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#popup-connecting {
|
||||||
|
position: fixed;
|
||||||
|
background: white;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
bottom: 10px;
|
||||||
|
right: 10px;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* status icons */
|
||||||
|
.status {
|
||||||
|
display: inline-block;
|
||||||
|
width: 1em;
|
||||||
|
vertical-align: middle;
|
||||||
|
margin-top: -2px; /* pixel-pushing */
|
||||||
|
}
|
||||||
|
svg.success path { fill: var(--success); }
|
||||||
|
svg.failed path { fill: var(--failure); }
|
||||||
|
svg.running circle { stroke: var(--running); }
|
||||||
|
|
||||||
|
/* 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: var(--border-grey);
|
||||||
|
margin-top: -9px;
|
||||||
|
}
|
||||||
|
a.sort:after {
|
||||||
|
border-top-color: var(--border-grey);
|
||||||
|
margin-top: 1px;
|
||||||
|
}
|
||||||
|
a.sort.dsc:after { border-top-color: var(--main-fg); }
|
||||||
|
a.sort.asc:before { border-bottom-color: var(--main-fg); }
|
||||||
|
a.sort:hover { text-decoration: none; cursor:pointer; }
|
||||||
|
a.sort:not(.asc):hover:before { border-bottom-color: var(--main-fg); }
|
||||||
|
a.sort:not(.dsc):hover:after { border-top-color: var(--main-fg); }
|
||||||
|
|
||||||
|
/* job group tabs */
|
||||||
|
a.active { color: var(--main-fg); }
|
||||||
|
a.active:hover { text-decoration: none; }
|
||||||
|
|
||||||
|
/* run console output */
|
||||||
|
.console-log { padding: 15px; background-color: var(--console-bg); }
|
||||||
|
.console-log code { white-space: pre-wrap; color: var(--console-fg); }
|
||||||
|
.console-log a { color: var(--console-fg); }
|
||||||
|
|
||||||
|
/* text input (job filtering) */
|
||||||
|
input { padding: 5px 8px; }
|
||||||
|
|
||||||
|
/* description list (run detail) */
|
||||||
|
dl { display: grid; grid-template-columns: auto 1fr; }
|
||||||
|
dt { text-align: right; font-weight: bold; min-width: 85px; }
|
||||||
|
dt,dd { line-height: 2; }
|
||||||
|
|
||||||
|
/* tables */
|
||||||
|
table { border-spacing: 0; width: 100%; }
|
||||||
|
th { text-align: left; border-bottom: 1px solid var(--border-grey); }
|
||||||
|
td, th { padding: 8px; }
|
||||||
|
table.striped td { border-top: 1px solid var(--border-grey); }
|
||||||
|
table.striped tr:nth-child(even) { background-color: var(--alt-row-bg); }
|
||||||
|
td:first-child, th:first-child { padding-left: 15px; }
|
||||||
|
td:last-child, th:last-child { padding-right: 15px; }
|
||||||
|
|
||||||
|
/* next/prev navigation buttons */
|
||||||
|
button {
|
||||||
|
border: 1px solid var(--border-grey);
|
||||||
|
background-color: var(--alt-row-bg);
|
||||||
|
padding: 6px;
|
||||||
|
min-width: 29px;
|
||||||
|
}
|
||||||
|
button[disabled] { cursor: not-allowed; color: var(--border-grey); }
|
||||||
|
button:not([disabled]) { cursor: pointer; color: var(--main-fg); }
|
||||||
|
|
||||||
|
/* progress bar */
|
||||||
|
.progress {
|
||||||
|
width: 100%;
|
||||||
|
height: 8px;
|
||||||
|
border: 1px solid;
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow: hidden;
|
||||||
|
border-color: var(--border-grey);
|
||||||
|
background-color: var(--alt-row-bg);
|
||||||
|
}
|
||||||
|
.progress-bar {
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
background-color: var(--running);
|
||||||
|
background-image: linear-gradient(45deg, transparent 35%, rgba(255,255,255,0.18) 35% 65%, transparent 65%);
|
||||||
|
background-size: 1rem;
|
||||||
|
transition: width .6s linear;
|
||||||
|
}
|
||||||
|
.progress-bar.overtime { background-color: var(--warning); }
|
||||||
|
.progress-bar.indeterminate {
|
||||||
|
animation: animate-stripes 1s linear infinite;
|
||||||
|
}
|
||||||
|
@keyframes animate-stripes {
|
||||||
|
from { background-position: 1rem 0; } to { background-position: 0 0; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* connecting overlay */
|
||||||
|
#connecting-overlay {
|
||||||
|
position: fixed;
|
||||||
|
top: 0; right: 0; bottom: 0; left: 0;
|
||||||
|
display: grid;
|
||||||
|
align-content: end; justify-content: end;
|
||||||
|
color: var(--nav-fg-light);
|
||||||
|
font-size: 18px;
|
||||||
|
padding: 30px;
|
||||||
|
visibility: hidden;
|
||||||
|
background-color: rgba(0,0,0,0.75);
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.5s ease, visibility 0s 0.5s;
|
||||||
|
}
|
||||||
|
#connecting-overlay.shown {
|
||||||
|
visibility: visible;
|
||||||
|
opacity: 1;
|
||||||
|
transition: opacity 0.5s ease 2s;
|
||||||
|
}
|
||||||
|
#connecting-overlay > div { opacity: 1; }
|
||||||
|
|
||||||
|
/* responsive layout */
|
||||||
|
#page-home-main {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: auto 1fr;
|
||||||
|
}
|
||||||
|
@media (max-width: 865px) {
|
||||||
|
#page-home-main {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
.vp-sm-hide { display: none; }
|
||||||
|
}
|
||||||
|
#page-home-stats {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr 1fr;
|
||||||
|
padding: 15px;
|
||||||
|
gap: 15px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
@media (max-width: 650px) {
|
||||||
|
#page-home-stats {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#page-job-main {
|
||||||
|
display: grid;
|
||||||
|
grid-template: auto 1fr / minmax(550px, 1fr) 1fr;
|
||||||
|
}
|
||||||
|
@media (max-width: 965px) {
|
||||||
|
#page-job-main {
|
||||||
|
grid-template: auto auto 1fr / 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#page-run-detail {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(400px, auto) 1fr;
|
||||||
|
gap: 5px;
|
||||||
|
}
|
||||||
|
@media (max-width: 780px) {
|
||||||
|
#page-run-detail {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user