-
{{title}}
-
Jobs
+
+
- Home
+
+
+
+
+ {{job.name}} queued |
+
+
+ {{job.name}} #{{job.number}}
+ |
+
+
+ {{job.name}} #{{job.number}} Took {{job.duration}}s at {{formatDate(job.started)}} |
+
+
+
+
+
+
Total builds per day this week
+
+
+
+
+
+
+
+
Builds per job in the last 24 hours
+
+
+
+
+
+
+
+
Average build time per job this week
+
+
+
+
+
+
+
+
Current executor utilization
+
+
+
+
+
+
+
+
+
+
+
- Home
- Jobs
+
+
+
+
+
+
+
+
+ {{job.name}} |
+ #{{job.number}} |
+ {{formatDate(job.started)}} |
+
+
+
+
+
+
+
+
- Home
- Jobs
- {{$route.params.name}}
+
+
+
+
{{$route.params.name}}
+
+ - Last Successful Run
+ - #{{lastSuccess.number}} {{lastSuccess?' - at '+formatDate(lastSuccess.started):'never'}}
+ - Last Failed Run
+ - #{{lastFailed.number}} {{lastFailed?' - at '+formatDate(lastFailed.started):'never'}}
+
+
+
+
+
+
+ Run | Started | Duration | Reason |
+
+ {{nQueued}} run(s) queued |
+
+
+ #{{job.number}} |
+ {{formatDate(job.started)}} |
+ -- |
+ {{job.reason}} |
+
+
+ #{{job.number}} |
+ {{formatDate(job.started)}} |
+ {{job.duration + " seconds"}} |
+ {{job.reason}} |
+
+
+
-
-
- - {{n.label}}
- - {{bc.current}}
-
-
+
+
+
+
- Home
- Jobs
- {{$route.params.name}}
- #{{$route.params.number}}
+
+
+
+
{{$route.params.name}} #{{$route.params.number}}
+
+
+
+ - Reason
- {{job.reason}}
+ - Queued for
- {{job.queued}}s
+ - Started
- {{formatDate(job.started)}}
+ - Completed
- {{formatDate(job.completed)}}
+ - Duration
- {{job.duration}}s
+
+
+
+
+
+
+
Console output
+
+
+
+
+
+
+
+
+
diff --git a/src/resources/js/app.js b/src/resources/js/app.js
index f978c80..0a54cf0 100644
--- a/src/resources/js/app.js
+++ b/src/resources/js/app.js
@@ -1,340 +1,445 @@
-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'
- })
- $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() + '/log');
- ws.onmessage = function(message) {
- callback(message.data);
- };
- }
- };
-})
-.controller('mainController', function($rootScope, $scope, $ws, $interval){
- $rootScope.bc = {
- nodes: [],
- current: 'Home'
- };
+/* laminar.js
+ * frontend application for Laminar Continuous Integration
+ * https://laminar.ohwg.net
+ */
+const WebsocketHandler = function() {
+ function setupWebsocket(path, next) {
+ var ws = new WebSocket("ws://" + window.location.host + path);
+ ws.onmessage = function(msg) {
+ msg = JSON.parse(msg.data);
+ // "status" is the first message the websocket always delivers.
+ // Use this to confirm the navigation. The component is not
+ // created until next() is called, so creating a reference
+ // for other message types must be deferred
+ if (msg.type === 'status') {
+ next(comp => {
+ // Set up bidirectional reference
+ // 1. needed to reference the component for other msg types
+ this.comp = comp;
+ // 2. needed to close the ws on navigation away
+ comp.ws = this;
+ // Update html and nav titles
+ document.title = comp.$root.title = msg.title;
+ // Component-specific callback handler
+ comp[msg.type](msg.data);
+ });
+ } else {
+ // at this point, the component must be defined
+ if (!this.comp)
+ return console.error("Page component was undefined");
+ else if (typeof this.comp[msg.type] === 'function')
+ this.comp[msg.type](msg.data);
+ }
+ };
+ };
+ return {
+ beforeRouteEnter(to, from, next) {
+ setupWebsocket(to.path, (fn) => { next(fn); });
+ },
+ beforeRouteUpdate(to, from, next) {
+ this.ws.close();
+ setupWebsocket(to.path, (fn) => { fn(this); next(); });
+ },
+ beforeRouteLeave(to, from, next) {
+ this.ws.close();
+ next();
+ },
+ };
+}();
- $scope.jobsQueued = [];
- $scope.jobsRunning = [];
- $scope.jobsRecent = [];
- var chtUtilization, chtBuildsPerDay, chtBuildsPerJob, chtTimePerJob;
-
- 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) {
- $rootScope.title = data.title;
- // populate jobs
- $scope.jobsQueued = data.queued;
- data.running.forEach($rootScope.updateProgress);
- $scope.jobsRunning = data.running;
- $scope.jobsRecent = data.recent;
- $scope.$apply();
-
- // setup charts
- chtUtilization = new Chart(document.getElementById("chartUtil").getContext("2d")).Pie(
- [{value: data.executorsBusy, color:"tan", label: "Busy"},
- {value: data.executorsTotal, color: "darkseagreen", 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: "lightsteelblue",
- 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) {
- $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();
+const Utils = {
+ methods: {
+ runIcon(result) {
+ return result === "success" ? '
✔' : result === "failed" || result === "aborted" ? '
✘' : '';
+ },
+ formatDate: function(unix) {
+ // TODO: reimplement when toLocaleDateString() accepts formatting options on most browsers
+ var d = new Date(1000 * unix);
+ var m = d.getMinutes();
+ if (m < 10) m = '0' + m;
+ return d.getHours() + ':' + m + ' 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();
+ },
+ }
+};
- 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;
- }
- }
- 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;
- }
- }
- }
- });
- var timeUpdater = $interval(function() {
- $scope.jobsRunning.forEach($rootScope.updateProgress);
- }, 1000);
- $scope.$on('$destroy', function() {
- $interval.cancel(timeUpdater);
- });
-})
-.controller('BrowseController', function($rootScope, $scope, $ws, $interval){
- $rootScope.bc = {
- nodes: [{ href: '/', label: 'Home' }],
- current: 'Jobs'
- };
+const ProgressUpdater = {
+ data() { return { jobsRunning: [] }; },
+ methods: {
+ updateProgress(o) {
+ if (o.etc) {
+ var p = ((new Date()).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;
+ }
+ }
+ }
+ },
+ beforeDestroy() {
+ clearInterval(this.updateTimer);
+ },
+ watch: {
+ jobsRunning(val) {
+ // this function handles several cases:
+ // - the route has changed to a different run of the same job
+ // - the current job has ended
+ // - the current job has started (practically hard to reach)
+ clearInterval(this.updateTimer);
+ if (val.length) {
+ // TODO: first, a non-animated progress update
+ this.updateTimer = setInterval(() => {
+ this.jobsRunning.forEach(this.updateProgress);
+ this.$forceUpdate();
+ }, 1000);
+ }
+ }
+ }
+};
- $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;
- };
+const Home = function() {
+ var state = {
+ jobsQueued: [],
+ jobsRecent: []
+ };
- $scope.jobs = [];
- $ws.statusListener({
- status: function(data) {
- $rootScope.title = data.title;
- $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();
- },
- job_completed: function(data) {
- for(var i in $scope.jobs) {
- if($scope.jobs[i].name === data.name) {
- $scope.jobs[i] = data;
- $scope.$apply;
- break;
- }
- }
- }
- });
-})
-.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.jobsRunning = [];
- $scope.jobsRecent = [];
+ var chtUtilization, chtBuildsPerDay, chtBuildsPerJob, chtTimePerJob;
- $ws.statusListener({
- status: function(data) {
- $rootScope.title = data.title;
+ var updateUtilization = function(busy) {
+ chtUtilization.segments[0].value += busy ? 1 : -1;
+ chtUtilization.segments[1].value -= busy ? 1 : -1;
+ chtUtilization.update();
+ }
- $scope.jobsRunning = data.running;
- $scope.jobsRecent = data.recent;
- $scope.lastSuccess = data.lastSuccess;
- $scope.lastFailed = data.lastFailed;
- $scope.$apply();
-
- var chtBt = new Chart(document.getElementById("chartBt").getContext("2d")).Bar({
- labels: data.recent.map(function(e){return '#' + e.number;}).reverse(),
- datasets: [{
- fillColor: "darkseagreen",
- strokeColor: "forestgreen",
- data: data.recent.map(function(e){return e.duration;}).reverse()
- }]
- },
- {barValueSpacing: 1,barStrokeWidth: 1,barDatasetSpacing:0}
- );
+ return {
+ template: '#home',
+ mixins: [WebsocketHandler, Utils, ProgressUpdater],
+ data: function() {
+ return state;
+ },
+ methods: {
+ status: function(msg) {
+ state.jobsQueued = msg.queued;
+ state.jobsRunning = msg.running;
+ state.jobsRecent = msg.recent;
+ this.$forceUpdate();
- for(var i = 0, n = data.recent.length; i < n; ++i) {
- if(data.recent[i].result != "success") {
- chtBt.datasets[0].bars[n-i-1].fillColor = "darksalmon";
- chtBt.datasets[0].bars[n-i-1].strokeColor = "crimson";
- }
- }
- chtBt.update();
-
- },
- job_queued: function() {
- $scope.nQueued++;
- },
- job_started: function(data) {
- $scope.nQueued--;
- $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.number === data.number) {
- $scope.jobsRunning.splice(i,1);
- $scope.jobsRecent.splice(0,0,data);
- $scope.$apply();
- break;
- }
- }
- }
- });
-})
-.controller('RunController', function($rootScope, $scope, $routeParams, $ws, $interval) {
- $rootScope.bc = {
- nodes: [{ href: '/', label: 'Home' },
- { href: '/jobs', label: 'Jobs' },
- { href: '/jobs/'+$routeParams.name, label: $routeParams.name }
- ],
- current: '#' + $routeParams.num
- };
+ // setup charts
+ chtUtilization = new Chart(document.getElementById("chartUtil").getContext("2d")).Pie(
+ [{
+ value: msg.executorsBusy,
+ color: "tan",
+ label: "Busy"
+ },
+ {
+ value: msg.executorsTotal,
+ color: "darkseagreen",
+ 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: msg.buildsPerDay.map(function(e) {
+ return e.success || 0;
+ })
+ }, {
+ label: "Failed Bulids",
+ fillColor: "darksalmon",
+ strokeColor: "crimson",
+ data: msg.buildsPerDay.map(function(e) {
+ return e.failed || 0;
+ })
+ }]
+ }, {
+ showTooltips: false
+ });
+ chtBuildsPerJob = new Chart(document.getElementById("chartBpj").getContext("2d")).HorizontalBar({
+ labels: Object.keys(msg.buildsPerJob),
+ datasets: [{
+ fillColor: "lightsteelblue",
+ data: Object.keys(msg.buildsPerJob).map(function(e) {
+ return msg.buildsPerJob[e];
+ })
+ }]
+ }, {});
+ chtTimePerJob = new Chart(document.getElementById("chartTpj").getContext("2d")).HorizontalBar({
+ labels: Object.keys(msg.timePerJob),
+ datasets: [{
+ fillColor: "lightsteelblue",
+ data: Object.keys(msg.timePerJob).map(function(e) {
+ return msg.timePerJob[e];
+ })
+ }]
+ }, {});
- $scope.name = $routeParams.name;
- $scope.num = parseInt($routeParams.num);
- $ws.statusListener({
- status: function(data) {
- $rootScope.title = data.title;
- $rootScope.updateProgress(data);
- $scope.job = data;
- $scope.latestNum = data.latestNum;
- $scope.$apply();
- },
- job_started: function() {
- $scope.latestNum++;
- $scope.$apply();
- },
- job_completed: function(data) {
- $scope.job = data;
- $scope.$apply();
- }
- });
-
- $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);
- }
- });
-
- var timeUpdater = $interval(function() {
- $rootScope.updateProgress($scope.job);
- }, 1000);
- $scope.$on('$destroy', function() {
- $interval.cancel(timeUpdater);
- });
-})
-.run(function($rootScope) {
- angular.extend($rootScope, {
- runIcon: function(result) {
- return result === "success" ? '
✔' : result === "failed" || result === "aborted" ? '
✘' : '';
- },
- runComplete: function(run) {
- return !!run && (run.result === 'aborted' || run.result === 'failed' || run.result === 'success');
- },
- formatDate: function(unix) {
- // TODO reimplement when toLocaleDateString() accepts formatting
- // options on most browsers
- var d = new Date(1000 * unix);
- var m = d.getMinutes();
- if(m < 10) m = '0' + m;
- return d.getHours() + ':' + m + ' 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();
- },
- updateProgress: 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;
- }
- }
- }
- });
+ },
+ job_queued: function(data) {
+ state.jobsQueued.splice(0, 0, data);
+ this.$forceUpdate();
+ },
+ job_started: function(data) {
+ state.jobsQueued.splice(state.jobsQueued.length - data.queueIndex - 1, 1);
+ state.jobsRunning.splice(0, 0, data);
+ this.$forceUpdate();
+ 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 < state.jobsRunning.length; ++i) {
+ var job = state.jobsRunning[i];
+ if (job.name == data.name && job.number == data.number) {
+ state.jobsRunning.splice(i, 1);
+ state.jobsRecent.splice(0, 0, data);
+ this.$forceUpdate();
+ 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;
+ }
+ }
+ }
+ }
+ };
+}();
+
+const Jobs = function() {
+ var state = {
+ jobs: [],
+ search: '',
+ tags: [],
+ tag: null
+ };
+ return {
+ template: '#jobs',
+ mixins: [WebsocketHandler, Utils],
+ data: function() { return state; },
+ computed: {
+ filteredJobs() {
+ var ret = this.jobs;
+ var tag = this.tag;
+ if (tag) {
+ ret = ret.filter(function(job) {
+ return job.tags.indexOf(tag) >= 0;
+ });
+ }
+ var search = this.search;
+ if (search) {
+ ret = ret.filter(function(job) {
+ return job.name.indexOf(search) > -1;
+ });
+ }
+ return ret;
+ }
+ },
+ methods: {
+ status: function(msg) {
+ state.jobs = msg.jobs;
+ var tags = {};
+ for (var i in state.jobs) {
+ for (var j in state.jobs[i].tags) {
+ tags[state.jobs[i].tags[j]] = true;
+ }
+ }
+ state.tags = Object.keys(tags);
+ },
+ job_completed: function(data) {
+ for (var i in state.jobs) {
+ if (state.jobs[i].name === data.name) {
+ state.jobs[i] = data;
+ this.$forceUpdate();
+ break;
+ }
+ }
+ }
+ }
+ };
+}();
+
+var Job = function() {
+ var state = {
+ jobsRunning: [],
+ jobsRecent: [],
+ lastSuccess: null,
+ lastFailed: null,
+ nQueued: 0,
+ };
+ return Vue.extend({
+ template: '#job',
+ mixins: [WebsocketHandler, Utils],
+ data: function() {
+ return state;
+ },
+ methods: {
+ status: function(msg) {
+ state.jobsRunning = msg.running;
+ state.jobsRecent = msg.recent;
+ state.lastSuccess = msg.lastSuccess;
+ state.lastFailed = msg.lastFailed;
+
+ var chtBt = new Chart(document.getElementById("chartBt").getContext("2d")).Bar({
+ labels: msg.recent.map(function(e) {
+ return '#' + e.number;
+ }).reverse(),
+ datasets: [{
+ fillColor: "darkseagreen",
+ strokeColor: "forestgreen",
+ data: msg.recent.map(function(e) {
+ return e.duration;
+ }).reverse()
+ }]
+ }, {
+ barValueSpacing: 1,
+ barStrokeWidth: 1,
+ barDatasetSpacing: 0
+ });
+
+ for (var i = 0, n = msg.recent.length; i < n; ++i) {
+ if (msg.recent[i].result != "success") {
+ chtBt.datasets[0].bars[n - i - 1].fillColor = "darksalmon";
+ chtBt.datasets[0].bars[n - i - 1].strokeColor = "crimson";
+ }
+ }
+ chtBt.update();
+
+ },
+ job_queued: function() {
+ state.nQueued++;
+ },
+ job_started: function(data) {
+ state.nQueued--;
+ state.jobsRunning.splice(0, 0, data);
+ this.$forceUpdate();
+ },
+ job_completed: function(data) {
+ for (var i = 0; i < state.jobsRunning.length; ++i) {
+ var job = state.jobsRunning[i];
+ if (job.number === data.number) {
+ state.jobsRunning.splice(i, 1);
+ state.jobsRecent.splice(0, 0, data);
+ this.$forceUpdate();
+ // TODO: update the chart
+ break;
+ }
+ }
+ }
+ }
+ });
+}();
+
+const Run = function() {
+ var state = {
+ job: { artifacts: [] },
+ latestNum: null,
+ log: '',
+ autoscroll: false
+ };
+ var firstLog = false;
+ var logHandler = function(vm, d) {
+ state.log += d;
+ vm.$forceUpdate();
+ if (!firstLog) {
+ firstLog = true;
+ } else if (state.autoscroll) {
+ window.scrollTo(0, document.body.scrollHeight);
+ }
+ };
+
+ return {
+ template: '#run',
+ mixins: [WebsocketHandler, Utils, ProgressUpdater],
+ data: function() {
+ return state;
+ },
+ methods: {
+ status: function(data) {
+ state.log = '';
+ state.job = data;
+ state.latestNum = data.latestNum;
+ if (!!state.job.etc)
+ state.jobsRunning = [data];
+ },
+ job_started: function(data) {
+ state.latestNum++;
+ this.$forceUpdate();
+ },
+ job_completed: function(data) {
+ state.job = data;
+ state.jobsRunning = [];
+ this.$forceUpdate();
+ },
+ runComplete: function(run) {
+ return !!run && (run.result === 'aborted' || run.result === 'failed' || run.result === 'success');
+ },
+ },
+ beforeRouteEnter(to, from, next) {
+ next(vm => {
+ vm.logws = new WebSocket("ws://" + location.host + to.path + '/log');
+ vm.logws.onmessage = function(msg) {
+ logHandler(vm, msg.data);
+ }
+ });
+ },
+ beforeRouteUpdate(to, from, next) {
+ var vm = this;
+ state.jobsRunning = [];
+ vm.logws.close();
+ vm.logws = new WebSocket("ws://" + location.host + to.path + '/log');
+ vm.logws.onmessage = function(msg) {
+ logHandler(vm, msg.data);
+ }
+ next();
+ },
+ beforeRouteLeave(to, from, next) {
+ this.logws.close();
+ next();
+ }
+ };
+}();
+
+new Vue({
+ el: '#app',
+ data: {
+ title: '' // populated by status ws message
+ },
+ router: new VueRouter({
+ mode: 'history',
+ routes: [
+ { path: '/', component: Home },
+ { path: '/jobs', component: Jobs },
+ { path: '/jobs/:name', component: Job },
+ { path: '/jobs/:name/:number', component: Run }
+ ],
+ }),
});
diff --git a/src/resources/progress.gif b/src/resources/progress.gif
new file mode 100644
index 0000000..b6cab7e
Binary files /dev/null and b/src/resources/progress.gif differ
diff --git a/src/resources/tpl/browse.html b/src/resources/tpl/browse.html
deleted file mode 100644
index dc75ec9..0000000
--- a/src/resources/tpl/browse.html
+++ /dev/null
@@ -1,21 +0,0 @@
-
diff --git a/src/resources/tpl/home.html b/src/resources/tpl/home.html
deleted file mode 100644
index c2388ef..0000000
--- a/src/resources/tpl/home.html
+++ /dev/null
@@ -1,58 +0,0 @@
-
-
-
-
-
-
-
-
Total builds per day this week
-
-
-
-
-
-
-
-
Builds per job in the last 24 hours
-
-
-
-
-
-
-
-
Average build time per job this week
-
-
-
-
-
-
-
-
-
Current executor utilization
-
-
-
-
-
-
-
-
-
-
diff --git a/src/resources/tpl/job.html b/src/resources/tpl/job.html
deleted file mode 100644
index 87f85e7..0000000
--- a/src/resources/tpl/job.html
+++ /dev/null
@@ -1,51 +0,0 @@
-
-
-
-
-
{{name}}
-
- - Last Successful Run
-
- #{{lastSuccess.number}}
- {{lastSuccess?" - at "+formatDate(lastSuccess.started):"never"}}
- - Last Failed Run
-
- #{{lastFailed.number}}
- {{lastFailed?" - at "+formatDate(lastFailed.started):"never"}}
-
-
-
-
-
-
-
-
-
-
- Run | Started | Duration | Reason |
-
- {{nQueued}} run(s) queued |
-
-
- #{{job.number}} |
- {{formatDate(job.started)}} |
- -- |
- {{job.reason}} |
-
-
- #{{job.number}} |
- {{formatDate(job.started)}} |
- {{job.duration + " seconds"}} |
- {{job.reason}} |
-
-
-
-
-
-
-
diff --git a/src/resources/tpl/run.html b/src/resources/tpl/run.html
deleted file mode 100644
index 1e8f635..0000000
--- a/src/resources/tpl/run.html
+++ /dev/null
@@ -1,43 +0,0 @@
-
-
-
-
{{name}} #{{num}}
-
-
-
- - Reason
- {{job.reason}}
- - Queued for
- {{job.queued}}s
- - Started
- {{formatDate(job.started)}}
- - Completed
- {{formatDate(job.completed)}}
- - Duration
- {{job.duration}}s
-
-
-
-
-
-
-
-
Console output
-
-
-
-