mirror of
https://github.com/ohwgiles/laminar.git
synced 2024-10-27 20:34:20 +00:00
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>
|
||||
<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…
Reference in New Issue
Block a user