1
0
mirror of https://github.com/ohwgiles/laminar.git synced 2026-03-02 03:40:21 +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:
Oliver Giles
2020-06-27 10:45:47 +12:00
parent 4554039703
commit ae560b9de4
6 changed files with 465 additions and 374 deletions

View File

@@ -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>
<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>
</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>
<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)}}&nbsp;%</td></tr>
</table>
</div>
<div>
<h3>Utilization</h3>
<div><canvas id="chartUtil"></canvas></div>
</div>
</div>
<div class="col-sm-7 col-md-8 col-lg-9"><div class="row">
<div class="col-md-6">
<div class="panel panel-default">
<div class="panel-heading">Total runs per day this week</div>
<div class="panel-body">
<canvas id="chartBpd"></canvas>
</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 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>
</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>
</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>
</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')">&nbsp;</a></th>
<th>Run <a class="sort" :class="(sort.field=='number'?sort.order:'')" v-on:click="do_sort('number')">&nbsp;</a></th>
<th class="text-center">Started <a class="sort" :class="(sort.field=='started'?sort.order:'')" v-on:click="do_sort('started')">&nbsp;</a></th>
<th class="text-center">Duration <a class="sort" :class="(sort.field=='duration'?sort.order:'')" v-on:click="do_sort('duration')">&nbsp;</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')">&nbsp;</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">&laquo;</button>
<span>Page {{sort.page+1}} of {{pages}}</span>
<button class="btn" v-on:click="page_next" :disabled="sort.page==pages-1">&raquo;</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')">&nbsp;</a></th>
<th>Run <a class="sort" :class="(sort.field=='number'?sort.order:'')" v-on:click="do_sort('number')">&nbsp;</a></th>
<th class="text-center">Started <a class="sort" :class="(sort.field=='started'?sort.order:'')" v-on:click="do_sort('started')">&nbsp;</a></th>
<th class="text-center">Duration <a class="sort" :class="(sort.field=='duration'?sort.order:'')" v-on:click="do_sort('duration')">&nbsp;</a></th>
<th class="text-center hidden-xs">Reason <a class="sort" :class="(sort.field=='reason'?sort.order:'')" v-on:click="do_sort('reason')">&nbsp;</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">&laquo;</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">&raquo;</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)">&laquo;</router-link></li>
<li v-show="latestNum > $route.params.number"><router-link :to="'/jobs/'+$route.params.name+'/'+(parseInt($route.params.number)+1)">&raquo;</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">&laquo;</router-link>
<router-link :disabled="$route.params.number == latestNum" :to="'/jobs/'+$route.params.name+'/'+(parseInt($route.params.number)+1)" tag="button">&raquo;</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'">&#128276;</a>
<router-view></router-view>
<div v-show="!connected" id="popup-connecting"><span class="status spin">&#xfe0e;</span>&nbsp;Connecting...</div>
</div>
<div id="connecting-overlay" :class="{shown:!connected}">
<div><span v-html="runIcon('running')"></span> Connecting...</div>
</div>
</main>
</body>
</html>