resolves #40: implement frontend sorting

This feature allows runs to be sorted by result, number, start time
or duration, in ascending or descending order, on the Job page. Request
is processed server-side so that the correct page division can be done.
Currently running jobs are not sorted.
pull/70/head
Oliver Giles 6 years ago
parent a81492e5bc
commit 8bcce4d5cc

@ -42,7 +42,10 @@ struct MonitorScope {
MonitorScope(Type type = HOME, std::string job = std::string(), uint num = 0) :
type(type),
job(job),
num(num)
num(num),
page(0),
field("number"),
order_desc(true)
{}
// whether this scope wants status information about the given job or run
@ -60,7 +63,10 @@ struct MonitorScope {
Type type;
std::string job;
uint num = 0;
// sorting
uint page = 0;
std::string field;
bool order_desc;
};
// Represents a (websocket) client that wants to be notified about events

@ -201,7 +201,22 @@ void Laminar::sendStatus(LaminarClient* client) {
} else if(client->scope.type == MonitorScope::JOB) {
const uint runsPerPage = 10;
j.startArray("recent");
db->stmt("SELECT number,startedAt,completedAt,result,reason FROM builds WHERE name = ? ORDER BY completedAt DESC LIMIT ?,?")
// ORDER BY param cannot be bound
std::string order_by;
std::string direction = client->scope.order_desc ? "DESC" : "ASC";
if(client->scope.field == "number")
order_by = "number " + direction;
else if(client->scope.field == "result")
order_by = "result " + direction + ", number DESC";
else if(client->scope.field == "started")
order_by = "startedAt " + direction + ", number DESC";
else if(client->scope.field == "duration")
order_by = "(completedAt-startedAt) " + direction + ", number DESC";
else
order_by = "number DESC";
std::string stmt = "SELECT number,startedAt,completedAt,result,reason FROM builds WHERE name = ? ORDER BY "
+ order_by + " LIMIT ?,?";
db->stmt(stmt.c_str())
.bind(client->scope.job, client->scope.page * runsPerPage, runsPerPage)
.fetch<uint,time_t,time_t,int,str>([&](uint build,time_t started,time_t completed,int result,str reason){
j.StartObject();
@ -216,7 +231,12 @@ void Laminar::sendStatus(LaminarClient* client) {
db->stmt("SELECT COUNT(*) FROM builds WHERE name = ?")
.bind(client->scope.job)
.fetch<uint>([&](uint nRuns){
j.set("pages", (nRuns-1) / runsPerPage + 1).set("page", client->scope.page);
j.set("pages", (nRuns-1) / runsPerPage + 1);
j.startObject("sort");
j.set("page", client->scope.page)
.set("field", client->scope.field)
.set("order", client->scope.order_desc ? "dsc" : "asc")
.EndObject();
});
j.startArray("running");
auto p = activeJobs.byJobName().equal_range(client->scope.job);

@ -63,6 +63,35 @@
@keyframes spin {
to { transform: rotate(360deg); }
}
/* 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>
</head>
<body>
@ -170,28 +199,36 @@
</div>
</div>
<div class="row"><div class="col-xs-12">
<table class="table table-striped"><thead>
<tr><th>Run</th><th class="text-center">Started</th><th class="text-center">Duration</th><th class="text-center hidden-xs">Reason</th></tr></thead>
<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="4"><i>{{nQueued}} run(s) queued</i></td>
<td colspan="5"><i>{{nQueued}} run(s) queued</i></td>
</tr>
<tr v-for="job in jobsRunning" track-by="$index">
<td><span v-html="runIcon(job.result)"></span> <router-link :to="'/jobs/'+$route.params.name+'/'+job.number">#{{job.number}}</router-link></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 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><span v-html="runIcon(job.result)"></span> <router-link :to="'/jobs/'+$route.params.name+'/'+job.number">#{{job.number}}</router-link></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 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="page==0">&laquo;</button></li>
<li>Page {{page+1}} of {{pages}}</li>
<li><button class="btn btn-default" v-on:click="page_next" :disabled="page==pages-1">&raquo;</button></li>
<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>

@ -402,7 +402,7 @@ var Job = function() {
lastFailed: null,
nQueued: 0,
pages: 0,
page: 0
sort: {}
};
return Vue.extend({
template: '#job',
@ -418,7 +418,7 @@ var Job = function() {
state.lastFailed = msg.lastFailed;
state.nQueued = msg.nQueued;
state.pages = msg.pages;
state.page = msg.page;
state.sort = msg.sort;
var chtBt = new Chart(document.getElementById("chartBt"), {
type: 'bar',
@ -467,10 +467,21 @@ var Job = function() {
}
},
page_next: function() {
this.ws.send(JSON.stringify({page:++state.page}));
state.sort.page++;
this.ws.send(JSON.stringify(state.sort));
},
page_prev: function() {
this.ws.send(JSON.stringify({page:--state.page}));
state.sort.page--;
this.ws.send(JSON.stringify(state.sort));
},
do_sort: function(field) {
if(state.sort.field == field) {
state.sort.order = state.sort.order == 'asc' ? 'dsc' : 'asc';
} else {
state.sort.order = 'dsc';
state.sort.field = field;
}
this.ws.send(JSON.stringify(state.sort));
}
}
});

@ -226,9 +226,10 @@ private:
KJ_CASE_ONEOF(str, kj::String) {
rapidjson::Document d;
d.ParseInsitu(const_cast<char*>(str.cStr()));
if(d.HasMember("page") && d["page"].IsInt()) {
int page = d["page"].GetInt();
lc.scope.page = page;
if(d.HasMember("page") && d["page"].IsInt() && d.HasMember("field") && d["field"].IsString() && d.HasMember("order") && d["order"].IsString()) {
lc.scope.page = d["page"].GetInt();
lc.scope.field = d["field"].GetString();
lc.scope.order_desc = strcmp(d["order"].GetString(), "dsc") == 0;
laminar.sendStatus(&lc);
return websocketRead(lc);
}

Loading…
Cancel
Save