From 8bcce4d5cc3a7b6d971c933774f7622a0625b226 Mon Sep 17 00:00:00 2001 From: Oliver Giles Date: Fri, 24 Aug 2018 12:15:40 +0300 Subject: [PATCH] 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. --- src/interface.h | 8 +++++- src/laminar.cpp | 24 ++++++++++++++++-- src/resources/index.html | 53 ++++++++++++++++++++++++++++++++++------ src/resources/js/app.js | 19 +++++++++++--- src/server.cpp | 7 +++--- 5 files changed, 93 insertions(+), 18 deletions(-) diff --git a/src/interface.h b/src/interface.h index 99c16fc..fcca41a 100644 --- a/src/interface.h +++ b/src/interface.h @@ -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 diff --git a/src/laminar.cpp b/src/laminar.cpp index f7c0213..95eb990 100644 --- a/src/laminar.cpp +++ b/src/laminar.cpp @@ -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 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 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); diff --git a/src/resources/index.html b/src/resources/index.html index 95de2d2..756e616 100644 --- a/src/resources/index.html +++ b/src/resources/index.html @@ -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; } + @@ -170,28 +199,36 @@
- - +
RunStartedDuration
+ + + + + + + - + - + + - + +
 Run  Started  Duration  
{{nQueued}} run(s) queued{{nQueued}} run(s) queued
#{{job.number}}#{{job.number}} {{formatDate(job.started)}} {{formatDuration(job.started, job.completed)}}
#{{job.number}}#{{job.number}} {{formatDate(job.started)}} {{formatDuration(job.started, job.completed)}}
    -
  • -
  • Page {{page+1}} of {{pages}}
  • -
  • +
  • +
  • Page {{sort.page+1}} of {{pages}}
  • +
diff --git a/src/resources/js/app.js b/src/resources/js/app.js index 5a70cfd..d41d737 100644 --- a/src/resources/js/app.js +++ b/src/resources/js/app.js @@ -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)); } } }); diff --git a/src/server.cpp b/src/server.cpp index ada7b8d..4713193 100644 --- a/src/server.cpp +++ b/src/server.cpp @@ -226,9 +226,10 @@ private: KJ_CASE_ONEOF(str, kj::String) { rapidjson::Document d; d.ParseInsitu(const_cast(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); }