mirror of
https://github.com/ohwgiles/laminar.git
synced 2024-10-27 20:34:20 +00:00
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.
This commit is contained in:
parent
a81492e5bc
commit
8bcce4d5cc
@ -42,7 +42,10 @@ struct MonitorScope {
|
|||||||
MonitorScope(Type type = HOME, std::string job = std::string(), uint num = 0) :
|
MonitorScope(Type type = HOME, std::string job = std::string(), uint num = 0) :
|
||||||
type(type),
|
type(type),
|
||||||
job(job),
|
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
|
// whether this scope wants status information about the given job or run
|
||||||
@ -60,7 +63,10 @@ struct MonitorScope {
|
|||||||
Type type;
|
Type type;
|
||||||
std::string job;
|
std::string job;
|
||||||
uint num = 0;
|
uint num = 0;
|
||||||
|
// sorting
|
||||||
uint page = 0;
|
uint page = 0;
|
||||||
|
std::string field;
|
||||||
|
bool order_desc;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Represents a (websocket) client that wants to be notified about events
|
// 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) {
|
} else if(client->scope.type == MonitorScope::JOB) {
|
||||||
const uint runsPerPage = 10;
|
const uint runsPerPage = 10;
|
||||||
j.startArray("recent");
|
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)
|
.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){
|
.fetch<uint,time_t,time_t,int,str>([&](uint build,time_t started,time_t completed,int result,str reason){
|
||||||
j.StartObject();
|
j.StartObject();
|
||||||
@ -216,7 +231,12 @@ void Laminar::sendStatus(LaminarClient* client) {
|
|||||||
db->stmt("SELECT COUNT(*) FROM builds WHERE name = ?")
|
db->stmt("SELECT COUNT(*) FROM builds WHERE name = ?")
|
||||||
.bind(client->scope.job)
|
.bind(client->scope.job)
|
||||||
.fetch<uint>([&](uint nRuns){
|
.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");
|
j.startArray("running");
|
||||||
auto p = activeJobs.byJobName().equal_range(client->scope.job);
|
auto p = activeJobs.byJobName().equal_range(client->scope.job);
|
||||||
|
@ -63,6 +63,35 @@
|
|||||||
@keyframes spin {
|
@keyframes spin {
|
||||||
to { transform: rotate(360deg); }
|
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>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@ -170,28 +199,36 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row"><div class="col-xs-12">
|
<div class="row"><div class="col-xs-12">
|
||||||
<table class="table table-striped"><thead>
|
<table class="table table-striped">
|
||||||
<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>
|
<thead><tr>
|
||||||
|
<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 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 hidden-xs">Reason <a class="sort" :class="(sort.field=='reason'?sort.order:'')" v-on:click="do_sort('reason')"> </a></th>
|
||||||
|
</tr></thead>
|
||||||
<tr v-show="nQueued">
|
<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>
|
||||||
<tr v-for="job in jobsRunning" track-by="$index">
|
<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">{{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 hidden-xs">{{job.reason}}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr v-for="job in jobsRecent" track-by="$index">
|
<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">{{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 hidden-xs">{{job.reason}}</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
<ul class="pagination pull-right">
|
<ul class="pagination pull-right">
|
||||||
<li><button class="btn btn-default" v-on:click="page_prev" :disabled="page==0">«</button></li>
|
<li><button class="btn btn-default" v-on:click="page_prev" :disabled="sort.page==0">«</button></li>
|
||||||
<li>Page {{page+1}} of {{pages}}</li>
|
<li>Page {{sort.page+1}} of {{pages}}</li>
|
||||||
<li><button class="btn btn-default" v-on:click="page_next" :disabled="page==pages-1">»</button></li>
|
<li><button class="btn btn-default" v-on:click="page_next" :disabled="sort.page==pages-1">»</button></li>
|
||||||
</ul>
|
</ul>
|
||||||
</div></div>
|
</div></div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -402,7 +402,7 @@ var Job = function() {
|
|||||||
lastFailed: null,
|
lastFailed: null,
|
||||||
nQueued: 0,
|
nQueued: 0,
|
||||||
pages: 0,
|
pages: 0,
|
||||||
page: 0
|
sort: {}
|
||||||
};
|
};
|
||||||
return Vue.extend({
|
return Vue.extend({
|
||||||
template: '#job',
|
template: '#job',
|
||||||
@ -418,7 +418,7 @@ var Job = function() {
|
|||||||
state.lastFailed = msg.lastFailed;
|
state.lastFailed = msg.lastFailed;
|
||||||
state.nQueued = msg.nQueued;
|
state.nQueued = msg.nQueued;
|
||||||
state.pages = msg.pages;
|
state.pages = msg.pages;
|
||||||
state.page = msg.page;
|
state.sort = msg.sort;
|
||||||
|
|
||||||
var chtBt = new Chart(document.getElementById("chartBt"), {
|
var chtBt = new Chart(document.getElementById("chartBt"), {
|
||||||
type: 'bar',
|
type: 'bar',
|
||||||
@ -467,10 +467,21 @@ var Job = function() {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
page_next: 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() {
|
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) {
|
KJ_CASE_ONEOF(str, kj::String) {
|
||||||
rapidjson::Document d;
|
rapidjson::Document d;
|
||||||
d.ParseInsitu(const_cast<char*>(str.cStr()));
|
d.ParseInsitu(const_cast<char*>(str.cStr()));
|
||||||
if(d.HasMember("page") && d["page"].IsInt()) {
|
if(d.HasMember("page") && d["page"].IsInt() && d.HasMember("field") && d["field"].IsString() && d.HasMember("order") && d["order"].IsString()) {
|
||||||
int page = d["page"].GetInt();
|
lc.scope.page = d["page"].GetInt();
|
||||||
lc.scope.page = page;
|
lc.scope.field = d["field"].GetString();
|
||||||
|
lc.scope.order_desc = strcmp(d["order"].GetString(), "dsc") == 0;
|
||||||
laminar.sendStatus(&lc);
|
laminar.sendStatus(&lc);
|
||||||
return websocketRead(lc);
|
return websocketRead(lc);
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user