mirror of
https://github.com/ohwgiles/laminar.git
synced 2024-10-27 20:34:20 +00:00
frontend love
This commit is contained in:
parent
0df97e95fd
commit
1e0a2ebc36
@ -57,7 +57,7 @@ add_custom_command(OUTPUT laminar.capnp.c++ laminar.capnp.h
|
|||||||
|
|
||||||
# Zip and compile statically served resources
|
# Zip and compile statically served resources
|
||||||
generate_compressed_bins(${CMAKE_SOURCE_DIR}/src/resources index.html js/app.js
|
generate_compressed_bins(${CMAKE_SOURCE_DIR}/src/resources index.html js/app.js
|
||||||
tpl/home.html tpl/job.html tpl/run.html tpl/log.html tpl/browse.html
|
tpl/home.html tpl/job.html tpl/run.html tpl/browse.html
|
||||||
favicon.ico favicon-152.png icon.png)
|
favicon.ico favicon-152.png icon.png)
|
||||||
# Download 3rd-party frontend JS libs...
|
# Download 3rd-party frontend JS libs...
|
||||||
file(DOWNLOAD https://ajax.googleapis.com/ajax/libs/angularjs/1.3.14/angular.min.js
|
file(DOWNLOAD https://ajax.googleapis.com/ajax/libs/angularjs/1.3.14/angular.min.js
|
||||||
|
@ -8,6 +8,13 @@
|
|||||||
###
|
###
|
||||||
#LAMINAR_HOME=/var/lib/laminar
|
#LAMINAR_HOME=/var/lib/laminar
|
||||||
|
|
||||||
|
###
|
||||||
|
### LAMINAR_TITLE
|
||||||
|
###
|
||||||
|
### Page title to show in web frontend
|
||||||
|
###
|
||||||
|
#LAMINAR_TITLE=
|
||||||
|
|
||||||
###
|
###
|
||||||
### LAMINAR_KEEP_WORKDIR
|
### LAMINAR_KEEP_WORKDIR
|
||||||
###
|
###
|
||||||
|
117
src/laminar.cpp
117
src/laminar.cpp
@ -118,48 +118,52 @@ void Laminar::sendStatus(LaminarClient* client) {
|
|||||||
client->sendMessage(log);
|
client->sendMessage(log);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else if(client->scope.type == MonitorScope::RUN) {
|
return;
|
||||||
Json j;
|
}
|
||||||
j.set("type", "status");
|
|
||||||
j.startObject("data");
|
Json j;
|
||||||
db->stmt("SELECT startedAt, result, reason FROM builds WHERE name = ? AND number = ?")
|
j.set("type", "status");
|
||||||
|
j.startObject("data");
|
||||||
|
j.set("title", getenv("LAMINAR_TITLE") ?: "");
|
||||||
|
if(client->scope.type == MonitorScope::RUN) {
|
||||||
|
db->stmt("SELECT queuedAt,startedAt,completedAt, result, reason FROM builds WHERE name = ? AND number = ?")
|
||||||
.bind(client->scope.job, client->scope.num)
|
.bind(client->scope.job, client->scope.num)
|
||||||
.fetch<time_t, int, std::string>([&](time_t started, int result, std::string reason) {
|
.fetch<time_t, time_t, time_t, int, std::string>([&](time_t queued, time_t started, time_t completed, int result, std::string reason) {
|
||||||
|
j.set("queued", started-queued);
|
||||||
j.set("started", started);
|
j.set("started", started);
|
||||||
|
j.set("completed", completed);
|
||||||
|
j.set("duration", completed-started);
|
||||||
j.set("result", to_string(RunState(result)));
|
j.set("result", to_string(RunState(result)));
|
||||||
j.set("reason", reason);
|
j.set("reason", reason);
|
||||||
});
|
});
|
||||||
|
j.set("latestNum", int(buildNums[client->scope.job]));
|
||||||
j.startArray("artifacts");
|
j.startArray("artifacts");
|
||||||
fs::path dir(fs::path(homeDir)/"archive"/client->scope.job/std::to_string(client->scope.num));
|
fs::path dir(fs::path(homeDir)/"archive"/client->scope.job/std::to_string(client->scope.num));
|
||||||
fs::recursive_directory_iterator rdt(dir);
|
if(fs::is_directory(dir)) {
|
||||||
int prefixLen = (fs::path(homeDir)/"archive").string().length();
|
fs::recursive_directory_iterator rdt(dir);
|
||||||
int scopeLen = dir.string().length();
|
int prefixLen = (fs::path(homeDir)/"archive").string().length();
|
||||||
for(fs::directory_entry e : rdt) {
|
int scopeLen = dir.string().length();
|
||||||
if(!fs::is_regular_file(e))
|
for(fs::directory_entry e : rdt) {
|
||||||
continue;
|
if(!fs::is_regular_file(e))
|
||||||
j.StartObject();
|
continue;
|
||||||
j.set("url", archiveUrl + e.path().string().substr(prefixLen));
|
j.StartObject();
|
||||||
j.set("filename", e.path().string().substr(scopeLen+1));
|
j.set("url", archiveUrl + e.path().string().substr(prefixLen));
|
||||||
j.EndObject();
|
j.set("filename", e.path().string().substr(scopeLen+1));
|
||||||
|
j.EndObject();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
j.EndArray();
|
j.EndArray();
|
||||||
j.EndObject();
|
|
||||||
client->sendMessage(j.str());
|
|
||||||
} else if(client->scope.type == MonitorScope::JOB) {
|
} else if(client->scope.type == MonitorScope::JOB) {
|
||||||
Json j;
|
|
||||||
j.set("type", "status");
|
|
||||||
j.startObject("data");
|
|
||||||
j.startArray("recent");
|
j.startArray("recent");
|
||||||
db->stmt("SELECT * FROM builds WHERE name = ? ORDER BY completedAt DESC LIMIT 5")
|
db->stmt("SELECT number,startedAt,completedAt,result,reason FROM builds WHERE name = ? ORDER BY completedAt DESC LIMIT 25")
|
||||||
.bind(client->scope.job)
|
.bind(client->scope.job)
|
||||||
.fetch<str,int,str,time_t,time_t,time_t,int>([&](str name,int build,str node,time_t,time_t started,time_t completed,int result){
|
.fetch<int,time_t,time_t,int,str>([&](int build,time_t started,time_t completed,int result,str reason){
|
||||||
j.StartObject();
|
j.StartObject();
|
||||||
j.set("name", name)
|
j.set("number", build)
|
||||||
.set("number", build)
|
|
||||||
.set("node", node)
|
|
||||||
.set("duration", completed - started)
|
.set("duration", completed - started)
|
||||||
.set("started", started)
|
.set("started", started)
|
||||||
.set("result", to_string(RunState(result)))
|
.set("result", to_string(RunState(result)))
|
||||||
|
.set("reason", reason)
|
||||||
.EndObject();
|
.EndObject();
|
||||||
});
|
});
|
||||||
j.EndArray();
|
j.EndArray();
|
||||||
@ -168,45 +172,52 @@ void Laminar::sendStatus(LaminarClient* client) {
|
|||||||
for(auto it = p.first; it != p.second; ++it) {
|
for(auto it = p.first; it != p.second; ++it) {
|
||||||
const std::shared_ptr<Run> run = *it;
|
const std::shared_ptr<Run> run = *it;
|
||||||
j.StartObject();
|
j.StartObject();
|
||||||
j.set("name", run->name);
|
|
||||||
j.set("number", run->build);
|
j.set("number", run->build);
|
||||||
j.set("node", run->node->name);
|
j.set("node", run->node->name);
|
||||||
j.set("started", run->startedAt);
|
j.set("started", run->startedAt);
|
||||||
j.EndObject();
|
j.EndObject();
|
||||||
}
|
}
|
||||||
j.EndArray();
|
j.EndArray();
|
||||||
j.startArray("queued");
|
int nQueued = 0;
|
||||||
for(const std::shared_ptr<Run> run : queuedJobs) {
|
for(const std::shared_ptr<Run> run : queuedJobs) {
|
||||||
if (run->name == client->scope.job) {
|
if (run->name == client->scope.job) {
|
||||||
j.StartObject();
|
nQueued++;
|
||||||
j.set("name", run->name);
|
|
||||||
j.EndObject();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
j.EndArray();
|
j.set("nQueued", nQueued);
|
||||||
j.EndObject();
|
db->stmt("SELECT number,startedAt FROM builds WHERE name = ? AND result = ? ORDER BY completedAt DESC LIMIT 1")
|
||||||
client->sendMessage(j.str());
|
.bind(client->scope.job, int(RunState::SUCCESS))
|
||||||
|
.fetch<int,time_t>([&](int build, time_t started){
|
||||||
|
j.startObject("lastSuccess");
|
||||||
|
j.set("number", build).set("started", started);
|
||||||
|
j.EndObject();
|
||||||
|
});
|
||||||
|
db->stmt("SELECT number,startedAt FROM builds WHERE name = ? AND result <> ? ORDER BY completedAt DESC LIMIT 1")
|
||||||
|
.bind(client->scope.job, int(RunState::SUCCESS))
|
||||||
|
.fetch<int,time_t>([&](int build, time_t started){
|
||||||
|
j.startObject("lastFailed");
|
||||||
|
j.set("number", build).set("started", started);
|
||||||
|
j.EndObject();
|
||||||
|
});
|
||||||
|
|
||||||
} else if(client->scope.type == MonitorScope::ALL) {
|
} else if(client->scope.type == MonitorScope::ALL) {
|
||||||
Json j;
|
|
||||||
j.set("type", "status");
|
|
||||||
j.startObject("data");
|
|
||||||
j.startArray("jobs");
|
j.startArray("jobs");
|
||||||
db->stmt("SELECT name FROM builds GROUP BY name")
|
db->stmt("SELECT name,number,startedAt,result FROM builds GROUP BY name ORDER BY number DESC")
|
||||||
.fetch<str>([&](str name){
|
.fetch<str,int,time_t,int>([&](str name,int number, time_t started, int result){
|
||||||
j.StartObject();
|
j.StartObject();
|
||||||
j.set("name", name)
|
j.set("name", name);
|
||||||
.EndObject();
|
j.set("number", number).set("result", to_string(RunState(result))).set("started", started);
|
||||||
|
j.startArray("tags");
|
||||||
|
for(const str& t: jobTags[name]) {
|
||||||
|
j.String(t.c_str());
|
||||||
|
}
|
||||||
|
j.EndArray();
|
||||||
|
j.EndObject();
|
||||||
});
|
});
|
||||||
j.EndArray();
|
j.EndArray();
|
||||||
j.EndObject();
|
|
||||||
client->sendMessage(j.str());
|
|
||||||
} else { // Home page
|
} else { // Home page
|
||||||
Json j;
|
|
||||||
j.set("type", "status");
|
|
||||||
j.startObject("data");
|
|
||||||
j.startArray("recent");
|
j.startArray("recent");
|
||||||
db->stmt("SELECT * FROM builds ORDER BY completedAt DESC LIMIT 5")
|
db->stmt("SELECT * FROM builds ORDER BY completedAt DESC LIMIT 15")
|
||||||
.fetch<str,int,str,time_t,time_t,time_t,int>([&](str name,int build,str node,time_t,time_t started,time_t completed,int result){
|
.fetch<str,int,str,time_t,time_t,time_t,int>([&](str name,int build,str node,time_t,time_t started,time_t completed,int result){
|
||||||
j.StartObject();
|
j.StartObject();
|
||||||
j.set("name", name)
|
j.set("name", name)
|
||||||
@ -262,10 +273,18 @@ void Laminar::sendStatus(LaminarClient* client) {
|
|||||||
j.set(job.c_str(), count);
|
j.set(job.c_str(), count);
|
||||||
});
|
});
|
||||||
j.EndObject();
|
j.EndObject();
|
||||||
|
j.startObject("timePerJob");
|
||||||
|
db->stmt("SELECT name, AVG(completedAt-startedAt) FROM builds WHERE completedAt > ? GROUP BY name")
|
||||||
|
.bind(time(0) - 7 * 86400)
|
||||||
|
.fetch<str, int>([&](str job, int time){
|
||||||
|
j.set(job.c_str(), time);
|
||||||
|
});
|
||||||
j.EndObject();
|
j.EndObject();
|
||||||
client->sendMessage(j.str());
|
|
||||||
}
|
}
|
||||||
|
j.EndObject();
|
||||||
|
client->sendMessage(j.str());
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Laminar::~Laminar() {
|
Laminar::~Laminar() {
|
||||||
|
@ -37,7 +37,6 @@ Resources::Resources()
|
|||||||
INIT_RESOURCE("/tpl/home.html", tpl_home_html);
|
INIT_RESOURCE("/tpl/home.html", tpl_home_html);
|
||||||
INIT_RESOURCE("/tpl/job.html", tpl_job_html);
|
INIT_RESOURCE("/tpl/job.html", tpl_job_html);
|
||||||
INIT_RESOURCE("/tpl/run.html", tpl_run_html);
|
INIT_RESOURCE("/tpl/run.html", tpl_run_html);
|
||||||
INIT_RESOURCE("/tpl/log.html", tpl_log_html);
|
|
||||||
INIT_RESOURCE("/tpl/browse.html", tpl_browse_html);
|
INIT_RESOURCE("/tpl/browse.html", tpl_browse_html);
|
||||||
INIT_RESOURCE("/js/angular.min.js", js_angular_min_js);
|
INIT_RESOURCE("/js/angular.min.js", js_angular_min_js);
|
||||||
INIT_RESOURCE("/js/angular-route.min.js", js_angular_route_min_js);
|
INIT_RESOURCE("/js/angular-route.min.js", js_angular_route_min_js);
|
||||||
|
@ -17,11 +17,14 @@
|
|||||||
<script src="/js/app.js"></script>
|
<script src="/js/app.js"></script>
|
||||||
<style>
|
<style>
|
||||||
body, html { height: 100%; }
|
body, html { height: 100%; }
|
||||||
|
.navbar { margin-bottom: 0; }
|
||||||
.navbar-brand { margin: 0 -15px; padding: 7px 15px }
|
.navbar-brand { margin: 0 -15px; padding: 7px 15px }
|
||||||
|
.navbar-brand>img { display: inline; }
|
||||||
a.navbar-btn { color: #9d9d9d; }
|
a.navbar-btn { color: #9d9d9d; }
|
||||||
a.navbar-btn.active { color: #fff; }
|
a.navbar-btn.active { color: #fff; }
|
||||||
a.navbar-btn:hover { color: #fff; text-decoration: none; }
|
a.navbar-btn:hover { color: #fff; text-decoration: none; }
|
||||||
a.navbar-btn:focus { color: #fff; }
|
a.navbar-btn:focus { color: #fff; }
|
||||||
|
dt,dd { line-height: 2; }
|
||||||
canvas {
|
canvas {
|
||||||
width: 100% !important;
|
width: 100% !important;
|
||||||
max-width: 800px;
|
max-width: 800px;
|
||||||
@ -37,11 +40,15 @@
|
|||||||
<nav class="navbar navbar-inverse">
|
<nav class="navbar navbar-inverse">
|
||||||
<div class="container-fluid">
|
<div class="container-fluid">
|
||||||
<div>
|
<div>
|
||||||
<a class="navbar-brand" href="/"><img src="/icon.png"></a>
|
<a class="navbar-brand" href="/"><img src="/icon.png">{{title}}</a>
|
||||||
<a class="btn navbar-btn" href="/jobs">Jobs</a>
|
<a class="btn navbar-btn pull-right" href="/jobs">Jobs</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
<ol class="breadcrumb">
|
||||||
|
<li ng-repeat="n in bc.nodes track by $index"><a href="{{n.href}}">{{n.label}}</a></li>
|
||||||
|
<li class="active">{{bc.current}}</li>
|
||||||
|
</ol>
|
||||||
<div ng-view></div>
|
<div ng-view></div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
@ -1,13 +1,3 @@
|
|||||||
Laminar = {
|
|
||||||
runIcon: function(result) {
|
|
||||||
return result === "success" ? '<span style="color:forestgreen">✔</span>' : '<span style="color:crimson;">✘</span>';
|
|
||||||
},
|
|
||||||
jobFormatter: function(o) {
|
|
||||||
o.duration = o.duration + "s"
|
|
||||||
o.when = (new Date(1000 * o.started)).toLocaleString();
|
|
||||||
return o;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
angular.module('laminar',['ngRoute','ngSanitize'])
|
angular.module('laminar',['ngRoute','ngSanitize'])
|
||||||
.config(function($routeProvider, $locationProvider, $sceProvider) {
|
.config(function($routeProvider, $locationProvider, $sceProvider) {
|
||||||
$routeProvider
|
$routeProvider
|
||||||
@ -27,10 +17,6 @@ angular.module('laminar',['ngRoute','ngSanitize'])
|
|||||||
templateUrl: 'tpl/run.html',
|
templateUrl: 'tpl/run.html',
|
||||||
controller: 'RunController'
|
controller: 'RunController'
|
||||||
})
|
})
|
||||||
.when('/jobs/:name/:num/log', {
|
|
||||||
templateUrl: 'tpl/log.html',
|
|
||||||
controller: 'LogController'
|
|
||||||
})
|
|
||||||
$locationProvider.html5Mode(true);
|
$locationProvider.html5Mode(true);
|
||||||
$sceProvider.enabled(false);
|
$sceProvider.enabled(false);
|
||||||
})
|
})
|
||||||
@ -44,19 +30,23 @@ angular.module('laminar',['ngRoute','ngSanitize'])
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
logListener: function(callback) {
|
logListener: function(callback) {
|
||||||
var ws = new WebSocket("ws://" + location.host + $location.path());
|
var ws = new WebSocket("ws://" + location.host + $location.path() + '/log');
|
||||||
ws.onmessage = function(message) {
|
ws.onmessage = function(message) {
|
||||||
callback(message.data);
|
callback(message.data);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
.controller('mainController', function($scope, $ws, $interval){
|
.controller('mainController', function($rootScope, $scope, $ws, $interval){
|
||||||
|
$rootScope.bc = {
|
||||||
|
nodes: [],
|
||||||
|
current: 'Home'
|
||||||
|
};
|
||||||
|
|
||||||
$scope.jobsQueued = [];
|
$scope.jobsQueued = [];
|
||||||
$scope.jobsRunning = [];
|
$scope.jobsRunning = [];
|
||||||
$scope.jobsRecent = [];
|
$scope.jobsRecent = [];
|
||||||
|
var chtUtilization, chtBuildsPerDay, chtBuildsPerJob, chtTimePerJob;
|
||||||
var chtUtilization, chtBuildsPerDay, chtBuildsPerJob;
|
|
||||||
|
|
||||||
var updateUtilization = function(busy) {
|
var updateUtilization = function(busy) {
|
||||||
chtUtilization.segments[0].value += busy ? 1 : -1;
|
chtUtilization.segments[0].value += busy ? 1 : -1;
|
||||||
@ -66,16 +56,18 @@ angular.module('laminar',['ngRoute','ngSanitize'])
|
|||||||
|
|
||||||
$ws.statusListener({
|
$ws.statusListener({
|
||||||
status: function(data) {
|
status: function(data) {
|
||||||
|
$rootScope.title = data.title;
|
||||||
// populate jobs
|
// populate jobs
|
||||||
$scope.jobsQueued = data.queued;
|
$scope.jobsQueued = data.queued;
|
||||||
$scope.jobsRunning = data.running;
|
$scope.jobsRunning = data.running;
|
||||||
$scope.jobsRecent = data.recent.map(Laminar.jobFormatter);
|
$scope.jobsRecent = data.recent;
|
||||||
|
|
||||||
$scope.$apply();
|
$scope.$apply();
|
||||||
|
|
||||||
// setup charts
|
// setup charts
|
||||||
chtUtilization = new Chart(document.getElementById("chartUtil").getContext("2d")).Pie(
|
chtUtilization = new Chart(document.getElementById("chartUtil").getContext("2d")).Pie(
|
||||||
[{value: data.executorsBusy, color:"sandybrown", label: "Busy"},
|
[{value: data.executorsBusy, color:"tan", label: "Busy"},
|
||||||
{value: data.executorsTotal, color: "steelblue", label: "Idle"}],
|
{value: data.executorsTotal, color: "darkseagreen", label: "Idle"}],
|
||||||
{animationEasing: 'easeInOutQuad'}
|
{animationEasing: 'easeInOutQuad'}
|
||||||
);
|
);
|
||||||
chtBuildsPerDay = new Chart(document.getElementById("chartBpd").getContext("2d")).Line({
|
chtBuildsPerDay = new Chart(document.getElementById("chartBpd").getContext("2d")).Line({
|
||||||
@ -104,10 +96,17 @@ angular.module('laminar',['ngRoute','ngSanitize'])
|
|||||||
chtBuildsPerJob = new Chart(document.getElementById("chartBpj").getContext("2d")).HorizontalBar({
|
chtBuildsPerJob = new Chart(document.getElementById("chartBpj").getContext("2d")).HorizontalBar({
|
||||||
labels: Object.keys(data.buildsPerJob),
|
labels: Object.keys(data.buildsPerJob),
|
||||||
datasets: [{
|
datasets: [{
|
||||||
fillColor: "steelblue",
|
fillColor: "lightsteelblue",
|
||||||
data: Object.keys(data.buildsPerJob).map(function(e){return data.buildsPerJob[e];})
|
data: Object.keys(data.buildsPerJob).map(function(e){return data.buildsPerJob[e];})
|
||||||
}]
|
}]
|
||||||
},{});
|
},{});
|
||||||
|
chtTimePerJob = new Chart(document.getElementById("chartTpj").getContext("2d")).HorizontalBar({
|
||||||
|
labels: Object.keys(data.timePerJob),
|
||||||
|
datasets: [{
|
||||||
|
fillColor: "lightsteelblue",
|
||||||
|
data: Object.keys(data.timePerJob).map(function(e){return data.timePerJob[e];})
|
||||||
|
}]
|
||||||
|
},{});
|
||||||
},
|
},
|
||||||
job_queued: function(data) {
|
job_queued: function(data) {
|
||||||
$scope.jobsQueued.splice(0,0,data);
|
$scope.jobsQueued.splice(0,0,data);
|
||||||
@ -130,7 +129,7 @@ angular.module('laminar',['ngRoute','ngSanitize'])
|
|||||||
var job = $scope.jobsRunning[i];
|
var job = $scope.jobsRunning[i];
|
||||||
if(job.name == data.name && job.number == data.number) {
|
if(job.name == data.name && job.number == data.number) {
|
||||||
$scope.jobsRunning.splice(i,1);
|
$scope.jobsRunning.splice(i,1);
|
||||||
$scope.jobsRecent.splice(0,0,Laminar.jobFormatter(data));
|
$scope.jobsRecent.splice(0,0,data);
|
||||||
$scope.$apply();
|
$scope.$apply();
|
||||||
|
|
||||||
break;
|
break;
|
||||||
@ -146,7 +145,6 @@ angular.module('laminar',['ngRoute','ngSanitize'])
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
$scope.runIcon = Laminar.runIcon;
|
|
||||||
timeUpdater = $interval(function() {
|
timeUpdater = $interval(function() {
|
||||||
$scope.jobsRunning.forEach(function(o){
|
$scope.jobsRunning.forEach(function(o){
|
||||||
if(o.etc) {
|
if(o.etc) {
|
||||||
@ -166,34 +164,82 @@ angular.module('laminar',['ngRoute','ngSanitize'])
|
|||||||
$interval.cancel(timeUpdater);
|
$interval.cancel(timeUpdater);
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.controller('BrowseController', function($scope, $ws, $interval){
|
.controller('BrowseController', function($rootScope, $scope, $ws, $interval){
|
||||||
|
$rootScope.bc = {
|
||||||
|
nodes: [{ href: '/', label: 'Home' }],
|
||||||
|
current: 'Jobs'
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.currentTag = null;
|
||||||
|
$scope.activeTag = function(t) {
|
||||||
|
return $scope.currentTag === t;
|
||||||
|
};
|
||||||
|
$scope.bytag = function(job) {
|
||||||
|
if($scope.currentTag === null) return true;
|
||||||
|
return job.tags.indexOf($scope.currentTag) >= 0;
|
||||||
|
};
|
||||||
|
|
||||||
$scope.jobs = [];
|
$scope.jobs = [];
|
||||||
$ws.statusListener({
|
$ws.statusListener({
|
||||||
status: function(data) {
|
status: function(data) {
|
||||||
|
$rootScope.title = data.title;
|
||||||
$scope.jobs = data.jobs;
|
$scope.jobs = data.jobs;
|
||||||
|
var tags = {};
|
||||||
|
for(var i in data.jobs) {
|
||||||
|
for(var j in data.jobs[i].tags) {
|
||||||
|
tags[data.jobs[i].tags[j]] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$scope.tags = Object.keys(tags);
|
||||||
$scope.$apply();
|
$scope.$apply();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.controller('JobController', function($scope, $routeParams, $ws) {
|
.controller('JobController', function($rootScope, $scope, $routeParams, $ws) {
|
||||||
|
$rootScope.bc = {
|
||||||
|
nodes: [{ href: '/', label: 'Home' },{ href: '/jobs', label: 'Jobs' }],
|
||||||
|
current: $routeParams.name
|
||||||
|
};
|
||||||
|
|
||||||
$scope.name = $routeParams.name;
|
$scope.name = $routeParams.name;
|
||||||
$scope.jobsQueued = [];
|
|
||||||
$scope.jobsRunning = [];
|
$scope.jobsRunning = [];
|
||||||
$scope.jobsRecent = [];
|
$scope.jobsRecent = [];
|
||||||
|
|
||||||
$ws.statusListener({
|
$ws.statusListener({
|
||||||
status: function(data) {
|
status: function(data) {
|
||||||
$scope.jobsQueued = data.queued.filter(function(e){return e.name == $routeParams.name;});
|
$rootScope.title = data.title;
|
||||||
$scope.jobsRunning = data.running.filter(function(e){return e.name == $routeParams.name;});
|
|
||||||
$scope.jobsRecent = data.recent.filter(function(e){return e.name == $routeParams.name;});
|
$scope.jobsRunning = data.running;
|
||||||
|
$scope.jobsRecent = data.recent;
|
||||||
|
$scope.lastSuccess = data.lastSuccess;
|
||||||
|
$scope.lastFailed = data.lastFailed;
|
||||||
$scope.$apply();
|
$scope.$apply();
|
||||||
},
|
|
||||||
job_queued: function(data) {
|
var chtBt = new Chart(document.getElementById("chartBt").getContext("2d")).Bar({
|
||||||
if(data.name == $routeParams.name) {
|
labels: data.recent.map(function(e){return '#' + e.number;}),
|
||||||
$scope.jobsQueued.splice(0,0,data);
|
datasets: [{
|
||||||
$scope.$apply();
|
fillColor: "darkseagreen",
|
||||||
|
strokeColor: "forestgreen",
|
||||||
|
data: data.recent.map(function(e){return e.duration;})
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
{barValueSpacing: 1,barStrokeWidth: 1,barDatasetSpacing:0}
|
||||||
|
);
|
||||||
|
|
||||||
|
for(var i = 0; i < data.recent.length; ++i) {
|
||||||
|
if(data.recent[i].result != "success") {
|
||||||
|
chtBt.datasets[0].bars[i].fillColor = "darksalmon";
|
||||||
|
chtBt.datasets[0].bars[i].strokeColor = "crimson";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
chtBt.update();
|
||||||
|
|
||||||
|
},
|
||||||
|
job_queued: function() {
|
||||||
|
$scope.nQueued++;
|
||||||
},
|
},
|
||||||
job_started: function(data) {
|
job_started: function(data) {
|
||||||
|
$scope.nQueued--;
|
||||||
if(data.name == $routeParams.name) {
|
if(data.name == $routeParams.name) {
|
||||||
$scope.jobsQueued.splice($scope.jobsQueued.length - 1,1);
|
$scope.jobsQueued.splice($scope.jobsQueued.length - 1,1);
|
||||||
$scope.jobsRunning.splice(0,0,data);
|
$scope.jobsRunning.splice(0,0,data);
|
||||||
@ -212,36 +258,62 @@ angular.module('laminar',['ngRoute','ngSanitize'])
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
$scope.runIcon = Laminar.runIcon;
|
|
||||||
})
|
})
|
||||||
.controller('RunController', function($scope, $routeParams, $ws) {
|
.controller('RunController', function($rootScope, $scope, $routeParams, $ws) {
|
||||||
|
$rootScope.bc = {
|
||||||
|
nodes: [{ href: '/', label: 'Home' },
|
||||||
|
{ href: '/jobs', label: 'Jobs' },
|
||||||
|
{ href: '/jobs/'+$routeParams.name, label: $routeParams.name }
|
||||||
|
],
|
||||||
|
current: '#' + $routeParams.num
|
||||||
|
};
|
||||||
|
|
||||||
$scope.name = $routeParams.name;
|
$scope.name = $routeParams.name;
|
||||||
$scope.num = $routeParams.num;
|
$scope.num = parseInt($routeParams.num);
|
||||||
$ws.statusListener({
|
$ws.statusListener({
|
||||||
status: function(data) {
|
status: function(data) {
|
||||||
$scope.job = Laminar.jobFormatter(data);
|
$rootScope.title = data.title;
|
||||||
|
$scope.job = data;
|
||||||
|
$scope.$apply();
|
||||||
|
},
|
||||||
|
job_started: function() {
|
||||||
|
$scope.job.latestNum++;
|
||||||
$scope.$apply();
|
$scope.$apply();
|
||||||
},
|
},
|
||||||
job_completed: function(data) {
|
job_completed: function(data) {
|
||||||
$scope.job = Laminar.jobFormatter(data);
|
$scope.job = data;
|
||||||
$scope.$apply();
|
$scope.$apply();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
$scope.runIcon = Laminar.runIcon;
|
|
||||||
})
|
|
||||||
.controller('LogController', function($scope, $routeParams, $ws) {
|
|
||||||
$scope.name = $routeParams.name;
|
|
||||||
$scope.num = $routeParams.num;
|
|
||||||
$scope._log = ""
|
|
||||||
$ws.logListener(function(data) {
|
|
||||||
$scope._log += ansi_up.ansi_to_html(data);
|
|
||||||
$scope.$apply();
|
|
||||||
window.scrollTo(0, document.body.scrollHeight);
|
|
||||||
});
|
|
||||||
$scope.log = function() {
|
|
||||||
// TODO sanitize
|
|
||||||
return ansi_up.ansi_to_html($scope._log);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
$scope.log = ""
|
||||||
|
$scope.autoscroll = false;
|
||||||
|
var firstLog = false;
|
||||||
|
$ws.logListener(function(data) {
|
||||||
|
$scope.log += ansi_up.ansi_to_html(data.replace('<','<').replace('>','>'));
|
||||||
|
$scope.$apply();
|
||||||
|
if(!firstLog) {
|
||||||
|
firstLog = true;
|
||||||
|
} else if($scope.autoscroll) {
|
||||||
|
window.scrollTo(0, document.body.scrollHeight);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
})
|
})
|
||||||
.run(function() {});
|
.run(function($rootScope) {
|
||||||
|
angular.extend($rootScope, {
|
||||||
|
runIcon: function(result) {
|
||||||
|
return result === "success" ? '<span style="color:forestgreen;font-family:\'Zapf Dingbats\';">✔</span>' : result === "failed" ? '<span style="color:crimson;">✘</span>' : '';
|
||||||
|
},
|
||||||
|
formatDate: function(unix) {
|
||||||
|
// TODO reimplement when toLocaleDateString() accepts formatting
|
||||||
|
// options on most browsers
|
||||||
|
var d = new Date(1000 * unix);
|
||||||
|
return d.getHours() + ':' + d.getMinutes() + ' on ' +
|
||||||
|
['Sun','Mon','Tue','Wed','Thu','Fri','Sat'][d.getDay()] + ' '
|
||||||
|
+ d.getDate() + '. ' + ['Jan','Feb','Mar','Apr','May','Jun',
|
||||||
|
'Jul','Aug','Sep', 'Oct','Nov','Dec'][d.getMonth()] + ' '
|
||||||
|
+ d.getFullYear();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
@ -1,14 +1,19 @@
|
|||||||
<div class="container-fluid">
|
<div class="container-fluid">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-xs-12">
|
<div class="col-xs-12">
|
||||||
<h3>Browse jobs</h3>
|
<div class="pull-right">
|
||||||
<div class="form-inline form-group">
|
<input class="form-control" id="jobFilter" ng-model="search.name" placeholder="Filter...">
|
||||||
<label for="jobFilter">Filter</label>
|
|
||||||
<input id="jobFilter" ng-model="search.name">
|
|
||||||
</div>
|
</div>
|
||||||
<table class="table table-bordered">
|
<ul class="nav nav-tabs">
|
||||||
<tr class="animate-repeat" ng-repeat="job in jobs | filter:search:strict">
|
<li ng-class="{active:activeTag(null)}"><a href ng-click="currentTag = null">All Jobs</a></li>
|
||||||
|
<li ng-repeat="tag in tags" ng-class="{active:activeTag(tag)}"><a href ng-click="$parent.currentTag = tag">{{tag}}</a></li>
|
||||||
|
</ul>
|
||||||
|
<style>table#joblist tr:first-child td { border-top: 0; }</style>
|
||||||
|
<table class="table table-striped" id="joblist">
|
||||||
|
<tr class="animate-repeat" ng-repeat="job in jobs | filter:bytag | filter:search">
|
||||||
<td><a href="jobs/{{job.name}}">{{job.name}}</a></td>
|
<td><a href="jobs/{{job.name}}">{{job.name}}</a></td>
|
||||||
|
<td class="text-center"><span ng-bind-html="runIcon(job.result)"></span> <a href="jobs/{{job.name}}/{{job.numberumber}}">#{{job.number}}</a></td>
|
||||||
|
<td class="text-center">{{formatDate(job.started)}}</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
<div class="container-fluid">
|
<div class="container-fluid">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-sm-5 col-md-4 col-lg-3 dash">
|
<div class="col-sm-5 col-md-4 col-lg-3 dash">
|
||||||
<h3>Recent Builds</h3>
|
|
||||||
<table class="table table-bordered">
|
<table class="table table-bordered">
|
||||||
<tr class="animate-repeat" ng-repeat="job in jobsQueued track by $index">
|
<tr class="animate-repeat" ng-repeat="job in jobsQueued track by $index">
|
||||||
<td><a href="jobs/{{job.name}}">{{job.name}}</a> <i>queued</i></td>
|
<td><a href="jobs/{{job.name}}">{{job.name}}</a> <i>queued</i></td>
|
||||||
@ -13,16 +12,15 @@
|
|||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr class="animate-repeat" ng-repeat="job in jobsRecent track by $index">
|
<tr class="animate-repeat" ng-repeat="job in jobsRecent track by $index">
|
||||||
<td><span ng-bind-html="runIcon(job.result)"></span> <a href="jobs/{{job.name}}">{{job.name}}</a> <a href="jobs/{{job.name}}/{{job.number}}">#{{job.number}}</a><br><small>Took {{job.duration}} at {{job.when}}</small></td>
|
<td><span ng-bind-html="runIcon(job.result)"></span> <a href="jobs/{{job.name}}">{{job.name}}</a> <a href="jobs/{{job.name}}/{{job.number}}">#{{job.number}}</a><br><small>Took {{job.duration}}s at {{formatDate(job.started)}}</small></td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-sm-7 col-md-8 col-lg-9">
|
<div class="col-sm-7 col-md-8 col-lg-9">
|
||||||
<h3>Dashboard</h3>
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
<div class="panel-heading">Builds per day</div>
|
<div class="panel-heading">Total builds per day this week</div>
|
||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
<canvas id="chartBpd"></canvas>
|
<canvas id="chartBpd"></canvas>
|
||||||
</div>
|
</div>
|
||||||
@ -38,16 +36,18 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
<div class="panel-heading">Current executor utilization</div>
|
<div class="panel-heading">Average build time per job this week</div>
|
||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
<canvas id="chartUtil"></canvas>
|
<canvas id="chartTpj"></canvas>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
<div class="panel-heading">what to put here?</div>
|
<div class="panel-heading">Current executor utilization</div>
|
||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
|
<canvas id="chartUtil"></canvas>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,22 +1,48 @@
|
|||||||
<div class="container-fluid">
|
<div class="container-fluid">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-xs-12">
|
|
||||||
<h3>{{name}}</h3>
|
<div class="col-sm-5 col-md-6 col-lg-7">
|
||||||
<table class="table table-bordered">
|
<h3>{{name}}</h3>
|
||||||
<tr class="animate-repeat" ng-repeat="job in jobsQueued track by $index">
|
<dl class="dl-horizontal">
|
||||||
<td><i>queued</i></td>
|
<dt>Last Successful Run</dt><dd>
|
||||||
</tr>
|
<a ng-show="lastSuccess" href="jobs/{{name}}/{{lastSuccess.number}}">#{{lastSuccess.number}}</a>
|
||||||
<tr class="animate-repeat" ng-repeat="job in jobsRunning track by $index">
|
{{lastSuccess?" - at "+formatDate(lastSuccess.started):"never"}}</dd>
|
||||||
<td><a href="jobs/{{job.name}}/{{job.number}}">#{{job.number}}</a> progressbar?</td>
|
<dt>Last Failed Run</dt><dd>
|
||||||
</tr>
|
<a ng-show="lastFailed" href="jobs/{{name}}/{{lastFailed.number}}">#{{lastFailed.number}}</a>
|
||||||
<tr class="animate-repeat" ng-repeat="job in jobsRecent track by $index">
|
{{lastFailed?" - at "+formatDate(lastFailed.started):"never"}}</dd>
|
||||||
<td><span ng-bind-html="runIcon(job.result)"></span> <a href="jobs/{{job.name}}/{{job.number}}">#{{job.number}}</a></td>
|
</dl>
|
||||||
</tr>
|
|
||||||
</table>
|
</div>
|
||||||
|
<div class="col-sm-7 col-md-6 col-lg-5">
|
||||||
|
<div class="panel panel-default">
|
||||||
|
<div class="panel-heading">Build time</div>
|
||||||
|
<div class="panel-body">
|
||||||
|
<canvas id="chartBt"></canvas>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div class="col-sm-7 col-md-8 col-lg-9">
|
<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>
|
||||||
|
<tr ng-show="nQueued">
|
||||||
|
<td colspan="4"><i>{{nQueued}} run(s) queued</i></td>
|
||||||
|
</tr>
|
||||||
|
<tr class="animate-repeat" ng-repeat="job in jobsRunning track by $index">
|
||||||
|
<td><a href="jobs/{{job.name}}/{{job.number}}">#{{job.number}}</a> progressbar?</td>
|
||||||
|
</tr>
|
||||||
|
<tr class="animate-repeat" ng-repeat="job in jobsRecent track by $index">
|
||||||
|
<td><span ng-bind-html="runIcon(job.result)"></span> <a href="jobs/{{name}}/{{job.number}}">#{{job.number}}</a></td>
|
||||||
|
<td class="text-center">{{formatDate(job.started)}}</td>
|
||||||
|
<td class="text-center">{{job.duration + " seconds"}}</td>
|
||||||
|
<td class="text-center hidden-xs">{{job.reason}}</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
|
@ -1,8 +0,0 @@
|
|||||||
<div class="container-fluid">
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-xs-12">
|
|
||||||
<h3>Log output for {{name}} #{{num}}</h3>
|
|
||||||
<pre ng-bind-html="log()"></pre>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
@ -1,14 +1,40 @@
|
|||||||
<div class="container-fluid">
|
<div class="container-fluid">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-xs-12">
|
<div class="col-sm-5 col-md-6 col-lg-7">
|
||||||
|
<h3 style="float:left"><span ng-bind-html="runIcon(job.result)"></span> {{name}} #{{num}}</h3>
|
||||||
|
<nav class="pull-left">
|
||||||
|
<ul class="pagination" style="margin:15px 20px">
|
||||||
|
<li><a href="jobs/{{name}}/{{num-1}}">«</a></li>
|
||||||
|
<li ng-show="job.latestNum > num"><a ng-href="jobs/{{name}}/{{num+1}}">»</a></li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
<div style="clear:both;"></div>
|
||||||
<dl class="dl-horizontal">
|
<dl class="dl-horizontal">
|
||||||
<dt style="vertical-align:bottom;"></dt><dd><h3><span ng-bind-html="runIcon(job.result)"></span> {{name}} #{{num}}</h3></dd>
|
|
||||||
<dt><a class="btn btn-default" href="jobs/{{name}}">< Job</a></dt><dd><a class="btn btn-default" href="jobs/{{name}}/{{num}}/log">Log output</a></dd>
|
|
||||||
<dt></dt><dd> </dd>
|
|
||||||
<dt>Reason</dt><dd>{{job.reason}}</dd>
|
<dt>Reason</dt><dd>{{job.reason}}</dd>
|
||||||
<dt>Started</dt><dd>{{job.when}}</dd>
|
<dt>Queued for</dt><dd>{{job.queued}}s</dd>
|
||||||
<dt>Artifacts</dt><dd><ul><li ng-repeat="art in job.artifacts"><a href="{{art.url}}" target="_self">{{art.filename}}</a></li></ul></dd>
|
<dt>Started</dt><dd>{{formatDate(job.started)}}</dd>
|
||||||
|
<dt>Completed</dt><dd>{{formatDate(job.completed)}}</dd>
|
||||||
|
<dt>Duration</dt><dd>{{job.duration}}s</dd>
|
||||||
</dl>
|
</dl>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="col-sm-7 col-md-6 col-lg-5">
|
||||||
|
<div class="panel panel-default" ng-show="job.artifacts.length">
|
||||||
|
<div class="panel-heading">Artifacts</div>
|
||||||
|
<div class="panel-body">
|
||||||
|
<ul class="list-unstyled" style="margin-bottom: 0">
|
||||||
|
<li ng-repeat="art in job.artifacts">
|
||||||
|
<a href="{{art.url}}" target="_self">{{art.filename}}</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-xs-12">
|
||||||
|
<button type="button" class="btn btn-default btn-xs pull-right" ng-class="{active:autoscroll}" ng-click="autoscroll = !autoscroll" style="margin-top:10px">Autoscroll</button>
|
||||||
|
<h4>Console output</h4>
|
||||||
|
<pre ng-bind-html="log"></pre>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
Loading…
Reference in New Issue
Block a user