implement wallboard view

resolves #51
pull/137/head
Oliver Giles 4 years ago
parent 746ab24676
commit 1ea9713536

@ -78,14 +78,14 @@ kj::Maybe<MonitorScope> fromUrl(std::string resource, char* query) {
return kj::mv(scope);
}
if(resource.substr(0, 5) != "/jobs")
return nullptr;
if(resource.length() == 5) {
if(resource == "/jobs" || resource == "/wallboard") {
scope.type = MonitorScope::ALL;
return kj::mv(scope);
}
if(resource.substr(0, 5) != "/jobs")
return nullptr;
resource = resource.substr(5);
size_t split = resource.find('/',1);
std::string job = resource.substr(1,split-1);

@ -325,16 +325,17 @@ std::string Laminar::getStatus(MonitorScope scope) {
j.set("description", desc == jobDescriptions.end() ? "" : desc->second);
} else if(scope.type == MonitorScope::ALL) {
j.startArray("jobs");
db->stmt("SELECT name,number,startedAt,completedAt,result FROM builds b "
db->stmt("SELECT name,number,startedAt,completedAt,result,reason FROM builds b "
"JOIN (SELECT name n,MAX(number) latest FROM builds WHERE result IS NOT NULL GROUP BY n) q "
"ON b.name = q.n AND b.number = latest")
.fetch<str,uint,time_t,time_t,int>([&](str name,uint number, time_t started, time_t completed, int result){
.fetch<str,uint,time_t,time_t,int,str>([&](str name,uint number, time_t started, time_t completed, int result, str reason){
j.StartObject();
j.set("name", name);
j.set("number", number);
j.set("result", to_string(RunState(result)));
j.set("started", started);
j.set("completed", completed);
j.set("reason", reason);
j.EndObject();
});
j.EndArray();

@ -122,7 +122,7 @@ inline bool beginsWith(std::string haystack, const char* needle) {
bool Resources::handleRequest(std::string path, const char** start, const char** end, const char** content_type) {
// need to keep the list of "application links" synchronised with the angular
// application. We cannot return a 404 for any of these
auto it = beginsWith(path,"/jobs")
auto it = beginsWith(path,"/jobs") || path == "/wallboard"
? resources.find("/")
: resources.find(path);

@ -81,7 +81,19 @@
<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>
<input class="form-control" id="jobFilter" v-model="search" placeholder="Filter...">
<div style="display: grid; grid-auto-flow: column; align-items: center; gap: 15px">
<router-link :to="wallboardLink()" style="display: inherit;" title="Wallboard">
<svg width="18" viewBox="0 0 13 13">
<g fill="#728494">
<rect x="0" y="2" width="6" height="4" />
<rect x="0" y="7" width="6" height="4" />
<rect x="7" y="2" width="6" height="4" />
<rect x="7" y="7" width="6" height="4" />
</g>
</svg>
</router-link>
<input class="form-control" id="jobFilter" v-model="search" placeholder="Filter...">
</div>
</nav>
<table class="striped" id="job-list">
<tr v-for="job in filteredJobs()">
@ -93,6 +105,14 @@
</table>
</div></template>
<template id="wallboard"><div class="wallboard">
<router-link :to="'/jobs/'+job.name+'/'+job.number" tag="div" v-for="job in wallboardJobs()" :data-result="job.result">
<span style="font-size: 36px; font-weight: bold;">{{job.name}} #{{job.number}}</span><br>
<span style="font-size: 30px;">{{formatDate(job.started)}}</span><br>
<span style="font-size: 26px;">{{job.reason}}</span>
</router-link>
</div></template>
<template id="job"><div id="page-job-main">
<div style="padding: 15px;">
<h2>{{$route.params.name}}</h2>

@ -390,16 +390,17 @@ const Home = function() {
};
}();
const Jobs = function() {
const All = function(templateId) {
var state = {
jobs: [],
search: '',
groups: {},
regexps: {},
group: null,
ungrouped: []
};
return {
template: '#jobs',
template: templateId,
mixins: [ServerEventHandler, Utils, ProgressUpdater],
data: function() { return state; },
methods: {
@ -418,11 +419,12 @@ const Jobs = function() {
}
}
state.groups = {};
Object.keys(msg.groups).map(k => state.groups[k] = new RegExp(msg.groups[k]));
state.ungrouped = state.jobs.filter(j => !Object.values(state.groups).some(r => r.test(j.name))).map(j => j.name);
Object.keys(msg.groups).forEach(k => state.regexps[k] = new RegExp(state.groups[k] = msg.groups[k]));
state.ungrouped = state.jobs.filter(j => !Object.values(state.regexps).some(r => r.test(j.name))).map(j => j.name);
state.group = state.ungrouped.length ? null : Object.keys(state.groups)[0];
},
job_started: function(data) {
data.result = 'running'; // for wallboard css
var updAt = null;
// jobsRunning must be maintained for ProgressUpdater
for (var i in state.jobsRunning) {
@ -447,7 +449,7 @@ const Jobs = function() {
// first execution of new job. TODO insert without resort
state.jobs.unshift(data);
state.jobs.sort(function(a, b){return a.name < b.name ? -1 : a.name > b.name ? 1 : 0;});
if(!Object.values(state.groups).some(r => r.test(data.name)))
if(!Object.values(state.regexps).some(r => r.test(data.name)))
state.ungrouped.push(data.name);
} else {
state.jobs[updAt] = data;
@ -473,16 +475,30 @@ const Jobs = function() {
filteredJobs: function() {
let ret = [];
if (state.group)
ret = state.jobs.filter(job => state.groups[state.group].test(job.name));
ret = state.jobs.filter(job => state.regexps[state.group].test(job.name));
else
ret = state.jobs.filter(job => state.ungrouped.includes(job.name));
if (this.search)
ret = ret.filter(job => job.name.indexOf(this.search) > -1);
return ret;
},
wallboardJobs: function() {
let ret = [];
const expr = (new URLSearchParams(window.location.search)).get('filter');
if (expr)
ret = state.jobs.filter(job => (new RegExp(expr)).test(job.name));
else
ret = state.jobs;
// sort failed before success, newest first
ret.sort((a,b) => a.result == b.result ? a.started - b.started : 2*(b.result == 'success')-1);
return ret;
},
wallboardLink: function() {
return '/wallboard' + (state.group ? '?filter=' + state.groups[state.group] : '');
}
}
};
}();
};
var Job = function() {
var state = {
@ -751,7 +767,8 @@ new Vue({
base: document.head.baseURI.substr(location.origin.length),
routes: [
{ path: '/', component: Home },
{ path: '/jobs', component: Jobs },
{ path: '/jobs', component: All('#jobs') },
{ path: '/wallboard', component: All('#wallboard') },
{ path: '/jobs/:name', component: Job },
{ path: '/jobs/:name/:number', component: Run }
],

@ -179,6 +179,41 @@ button:not([disabled]) { cursor: pointer; color: var(--main-fg); }
from { background-position: 1rem 0; } to { background-position: 0 0; }
}
/* wallboard */
.wallboard {
display: flex;
flex-wrap: wrap-reverse;
flex-direction: row-reverse;
gap: 20px;
padding: 20px;
position: fixed;
height: 100%;
width: 100%;
overflow: auto;
background-color: #000
}
.wallboard > div {
padding: 30px;
flex-grow: 1;
background-color: var(--failure);
color: var(--nav-fg-light);
}
.wallboard > div:hover {
cursor: pointer;
}
.wallboard > div[data-result="running"] {
animation: wallboard-bg-fade 2s ease infinite;
}
@keyframes wallboard-bg-fade {
from { background-color: #4786ab; }
50% { background-color: #446597; }
to { background-color: #4786ab; }
}
.wallboard > div[data-result="success"] {
background-color: var(--success);
color: var(--main-fg);
}
/* connecting overlay */
#connecting-overlay {
position: fixed;

Loading…
Cancel
Save