mirror of
https://github.com/ohwgiles/laminar.git
synced 2026-03-02 03:40:21 +00:00
Initial commit
This commit is contained in:
45
src/resources/index.html
Normal file
45
src/resources/index.html
Normal file
@@ -0,0 +1,45 @@
|
||||
<!doctype html>
|
||||
<html ng-app="laminar">
|
||||
<head>
|
||||
<base href="/">
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Laminar</title>
|
||||
<script src="/js/angular.min.js"></script>
|
||||
<script src="/js/angular-route.min.js"></script>
|
||||
<script src="/js/angular-sanitize.min.js"></script>
|
||||
<script src="/js/ansi_up.js" type="text/javascript"></script>
|
||||
<script src="/js/Chart.min.js"></script>
|
||||
<script src="/js/Chart.HorizontalBar.js"></script>
|
||||
<link href="/css/bootstrap.min.css" rel="stylesheet">
|
||||
<script src="/js/app.js"></script>
|
||||
<style>
|
||||
body, html { height: 100%; }
|
||||
.navbar { margin-bottom: 0; }
|
||||
.navbar-brand { padding: 13px 15px; font-family: 'Cantarell';}
|
||||
.navbar-inverse { border: 0; }
|
||||
canvas {
|
||||
width: 100% !important;
|
||||
max-width: 800px;
|
||||
height: auto !important;
|
||||
}
|
||||
.progress {
|
||||
height: 10px;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<nav class="navbar navbar-inverse">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand" href="/">laminar</a>
|
||||
<ul class="nav navbar-nav">
|
||||
<li ng-class="{active:active('/')}"><a href="/jobs">Jobs</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
<div ng-view></div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
250
src/resources/js/app.js
Normal file
250
src/resources/js/app.js
Normal file
@@ -0,0 +1,250 @@
|
||||
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'])
|
||||
.config(function($routeProvider, $locationProvider, $sceProvider) {
|
||||
$routeProvider
|
||||
.when('/', {
|
||||
templateUrl: 'tpl/home.html',
|
||||
controller: 'mainController'
|
||||
})
|
||||
.when('/jobs', {
|
||||
templateUrl: 'tpl/browse.html',
|
||||
controller: 'BrowseController',
|
||||
})
|
||||
.when('/jobs/:name', {
|
||||
templateUrl: 'tpl/job.html',
|
||||
controller: 'JobController'
|
||||
})
|
||||
.when('/jobs/:name/:num', {
|
||||
templateUrl: 'tpl/run.html',
|
||||
controller: 'RunController'
|
||||
})
|
||||
.when('/jobs/:name/:num/log', {
|
||||
templateUrl: 'tpl/log.html',
|
||||
controller: 'LogController'
|
||||
})
|
||||
$locationProvider.html5Mode(true);
|
||||
$sceProvider.enabled(false);
|
||||
})
|
||||
.factory('$ws',function($q,$location){
|
||||
return {
|
||||
statusListener: function(callbacks) {
|
||||
var ws = new WebSocket("ws://" + location.host + $location.path());
|
||||
ws.onmessage = function(message) {
|
||||
message = JSON.parse(message.data);
|
||||
callbacks[message.type](message.data);
|
||||
};
|
||||
},
|
||||
logListener: function(callback) {
|
||||
var ws = new WebSocket("ws://" + location.host + $location.path());
|
||||
ws.onmessage = function(message) {
|
||||
callback(message.data);
|
||||
};
|
||||
}
|
||||
};
|
||||
})
|
||||
.controller('mainController', function($scope, $ws, $interval){
|
||||
$scope.jobsQueued = [];
|
||||
$scope.jobsRunning = [];
|
||||
$scope.jobsRecent = [];
|
||||
|
||||
var chtUtilization, chtBuildsPerDay, chtBuildsPerJob;
|
||||
|
||||
var updateUtilization = function(busy) {
|
||||
chtUtilization.segments[0].value += busy ? 1 : -1;
|
||||
chtUtilization.segments[1].value -= busy ? 1 : -1;
|
||||
chtUtilization.update();
|
||||
}
|
||||
|
||||
$ws.statusListener({
|
||||
status: function(data) {
|
||||
// populate jobs
|
||||
$scope.jobsQueued = data.queued;
|
||||
$scope.jobsRunning = data.running;
|
||||
$scope.jobsRecent = data.recent.map(Laminar.jobFormatter);
|
||||
$scope.$apply();
|
||||
|
||||
// setup charts
|
||||
chtUtilization = new Chart(document.getElementById("chartUtil").getContext("2d")).Pie(
|
||||
[{value: data.executorsBusy, color:"sandybrown", label: "Busy"},
|
||||
{value: data.executorsTotal, color: "steelblue", label: "Idle"}],
|
||||
{animationEasing: 'easeInOutQuad'}
|
||||
);
|
||||
chtBuildsPerDay = new Chart(document.getElementById("chartBpd").getContext("2d")).Line({
|
||||
labels: function(){
|
||||
res = [];
|
||||
var now = new Date();
|
||||
for(var i = 6; i >= 0; --i) {
|
||||
var then = new Date(now.getTime() - i*86400000);
|
||||
res.push(["Sun","Mon","Tue","Wed","Thu","Fri","Sat"][then.getDay()]);
|
||||
}
|
||||
return res;
|
||||
}(),
|
||||
datasets: [{
|
||||
label: "Successful Builds",
|
||||
fillColor: "darkseagreen",
|
||||
strokeColor: "forestgreen",
|
||||
data: data.buildsPerDay.map(function(e){return e.success||0;})
|
||||
},{
|
||||
label: "Failed Bulids",
|
||||
fillColor: "darksalmon",
|
||||
strokeColor: "crimson",
|
||||
data: data.buildsPerDay.map(function(e){return e.failed||0;})
|
||||
}]},
|
||||
{ showTooltips: false }
|
||||
);
|
||||
chtBuildsPerJob = new Chart(document.getElementById("chartBpj").getContext("2d")).HorizontalBar({
|
||||
labels: Object.keys(data.buildsPerJob),
|
||||
datasets: [{
|
||||
fillColor: "steelblue",
|
||||
data: Object.keys(data.buildsPerJob).map(function(e){return data.buildsPerJob[e];})
|
||||
}]
|
||||
},{});
|
||||
},
|
||||
job_queued: function(data) {
|
||||
$scope.jobsQueued.splice(0,0,data);
|
||||
$scope.$apply();
|
||||
},
|
||||
job_started: function(data) {
|
||||
$scope.jobsQueued.splice($scope.jobsQueued.length - data.queueIndex - 1,1);
|
||||
$scope.jobsRunning.splice(0,0,data);
|
||||
$scope.$apply();
|
||||
updateUtilization(true);
|
||||
},
|
||||
job_completed: function(data) {
|
||||
if(data.result === "success")
|
||||
chtBuildsPerDay.datasets[0].points[6].value++;
|
||||
else
|
||||
chtBuildsPerDay.datasets[1].points[6].value++;
|
||||
chtBuildsPerDay.update();
|
||||
|
||||
for(var i = 0; i < $scope.jobsRunning.length; ++i) {
|
||||
var job = $scope.jobsRunning[i];
|
||||
if(job.name == data.name && job.number == data.number) {
|
||||
$scope.jobsRunning.splice(i,1);
|
||||
$scope.jobsRecent.splice(0,0,Laminar.jobFormatter(data));
|
||||
$scope.$apply();
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
updateUtilization(false);
|
||||
for(var j = 0; j < chtBuildsPerJob.datasets[0].bars.length; ++j) {
|
||||
if(chtBuildsPerJob.datasets[0].bars[j].label == job.name) {
|
||||
chtBuildsPerJob.datasets[0].bars[j].value++;
|
||||
chtBuildsPerJob.update();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
$scope.active = function(url) {
|
||||
return false;
|
||||
}
|
||||
$scope.runIcon = Laminar.runIcon;
|
||||
timeUpdater = $interval(function() {
|
||||
$scope.jobsRunning.forEach(function(o){
|
||||
if(o.etc) {
|
||||
var d = new Date();
|
||||
var p = (d.getTime()/1000 - o.started) / (o.etc - o.started);
|
||||
if(p > 1.2) {
|
||||
o.overtime = true;
|
||||
} else if(p >= 1) {
|
||||
o.progress = 99;
|
||||
} else {
|
||||
o.progress = 100 * p;
|
||||
}
|
||||
}
|
||||
});
|
||||
}, 1000);
|
||||
$scope.$on('$destroy', function() {
|
||||
$interval.cancel(timeUpdater);
|
||||
});
|
||||
})
|
||||
.controller('BrowseController', function($scope, $ws, $interval){
|
||||
$scope.jobs = [];
|
||||
$ws.statusListener({
|
||||
status: function(data) {
|
||||
$scope.jobs = data.jobs;
|
||||
$scope.$apply();
|
||||
},
|
||||
});
|
||||
})
|
||||
.controller('JobController', function($scope, $routeParams, $ws) {
|
||||
$scope.name = $routeParams.name;
|
||||
$scope.jobsQueued = [];
|
||||
$scope.jobsRunning = [];
|
||||
$scope.jobsRecent = [];
|
||||
$ws.statusListener({
|
||||
status: function(data) {
|
||||
$scope.jobsQueued = data.queued.filter(function(e){return e.name == $routeParams.name;});
|
||||
$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.$apply();
|
||||
},
|
||||
job_queued: function(data) {
|
||||
if(data.name == $routeParams.name) {
|
||||
$scope.jobsQueued.splice(0,0,data);
|
||||
$scope.$apply();
|
||||
}
|
||||
},
|
||||
job_started: function(data) {
|
||||
if(data.name == $routeParams.name) {
|
||||
$scope.jobsQueued.splice($scope.jobsQueued.length - 1,1);
|
||||
$scope.jobsRunning.splice(0,0,data);
|
||||
$scope.$apply();
|
||||
}
|
||||
},
|
||||
job_completed: function(data) {
|
||||
for(var i = 0; i < $scope.jobsRunning.length; ++i) {
|
||||
var job = $scope.jobsRunning[i];
|
||||
if(job.name == data.name && job.number == data.number) {
|
||||
$scope.jobsRunning.splice(i,1);
|
||||
$scope.jobsRecent.splice(0,0,data);
|
||||
$scope.$apply();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
$scope.runIcon = Laminar.runIcon;
|
||||
})
|
||||
.controller('RunController', function($scope, $routeParams, $ws) {
|
||||
$scope.name = $routeParams.name;
|
||||
$scope.num = $routeParams.num;
|
||||
$ws.statusListener({
|
||||
status: function(data) {
|
||||
$scope.job = Laminar.jobFormatter(data);
|
||||
$scope.$apply();
|
||||
},
|
||||
job_completed: function(data) {
|
||||
$scope.job = Laminar.jobFormatter(data);
|
||||
$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);
|
||||
}
|
||||
|
||||
})
|
||||
.run(function() {});
|
||||
16
src/resources/tpl/browse.html
Normal file
16
src/resources/tpl/browse.html
Normal file
@@ -0,0 +1,16 @@
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<div class="col-xs-12">
|
||||
<h3>Browse jobs</h3>
|
||||
<div class="form-inline form-group">
|
||||
<label for="jobFilter">Filter</label>
|
||||
<input id="jobFilter" ng-model="search.name">
|
||||
</div>
|
||||
<table class="table table-bordered">
|
||||
<tr class="animate-repeat" ng-repeat="job in jobs | filter:search:strict">
|
||||
<td><a href="jobs/{{job.name}}">{{job.name}}</a></td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
58
src/resources/tpl/home.html
Normal file
58
src/resources/tpl/home.html
Normal file
@@ -0,0 +1,58 @@
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<div class="col-sm-5 col-md-4 col-lg-3 dash">
|
||||
<h3>Recent Builds</h3>
|
||||
<table class="table table-bordered">
|
||||
<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>
|
||||
</tr>
|
||||
<tr class="animate-repeat" ng-repeat="job in jobsRunning track by $index">
|
||||
<td><a href="jobs/{{job.name}}">{{job.name}}</a> <a href="jobs/{{job.name}}/{{job.number}}">#{{job.number}}</a> <div class="progress">
|
||||
<div class="progress-bar progress-bar-{{job.overtime?'warning':'info'}} progress-bar-striped {{job.etc?'':'active'}}" style="width:{{!job.etc?'100':job.progress}}%"></div>
|
||||
</div>
|
||||
</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/{{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>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<div class="col-sm-7 col-md-8 col-lg-9">
|
||||
<h3>Dashboard</h3>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">Builds per day</div>
|
||||
<div class="panel-body">
|
||||
<canvas id="chartBpd"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">Builds per job in the last 24 hours</div>
|
||||
<div class="panel-body" id="chartStatus">
|
||||
<canvas id="chartBpj"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">Current executor utilization</div>
|
||||
<div class="panel-body">
|
||||
<canvas id="chartUtil"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">what to put here?</div>
|
||||
<div class="panel-body">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
22
src/resources/tpl/job.html
Normal file
22
src/resources/tpl/job.html
Normal file
@@ -0,0 +1,22 @@
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<div class="col-xs-12">
|
||||
<h3>{{name}}</h3>
|
||||
<table class="table table-bordered">
|
||||
<tr class="animate-repeat" ng-repeat="job in jobsQueued track by $index">
|
||||
<td><i>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/{{job.name}}/{{job.number}}">#{{job.number}}</a></td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<div class="col-sm-7 col-md-8 col-lg-9">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
8
src/resources/tpl/log.html
Normal file
8
src/resources/tpl/log.html
Normal file
@@ -0,0 +1,8 @@
|
||||
<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>
|
||||
13
src/resources/tpl/run.html
Normal file
13
src/resources/tpl/run.html
Normal file
@@ -0,0 +1,13 @@
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<div class="col-xs-12">
|
||||
<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>Started</dt><dd>{{job.when}}</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
Reference in New Issue
Block a user