mirror of
https://github.com/ohwgiles/laminar.git
synced 2024-10-27 20:34:20 +00:00
frontend refactor
use a more modern style, reduce variable scopes where possible, fix several minor bugs such as pagination and scales in chart tooltips
This commit is contained in:
parent
c6b60646f6
commit
bd489bdbb0
@ -3,30 +3,27 @@
|
|||||||
* https://laminar.ohwg.net
|
* https://laminar.ohwg.net
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
// A hash function added to String helps generating consistent
|
||||||
|
// colours from job names for use in charts
|
||||||
String.prototype.hashCode = function() {
|
String.prototype.hashCode = function() {
|
||||||
for(var r=0, i=0; i<this.length; i++)
|
for(var r=0, i=0; i<this.length; i++)
|
||||||
r=(r<<5)-r+this.charCodeAt(i),r&=r;
|
r=(r<<5)-r+this.charCodeAt(i),r&=r;
|
||||||
return r;
|
return r;
|
||||||
};
|
};
|
||||||
|
|
||||||
Vue.filter('iecFileSize', function(bytes) {
|
// Filter to pretty-print the size of artifacts
|
||||||
var exp = Math.floor(Math.log(bytes) / Math.log(1024));
|
Vue.filter('iecFileSize', bytes => {
|
||||||
|
const exp = Math.floor(Math.log(bytes) / Math.log(1024));
|
||||||
return (bytes / Math.pow(1024, exp)).toFixed(1) + ' ' +
|
return (bytes / Math.pow(1024, exp)).toFixed(1) + ' ' +
|
||||||
['B', 'KiB', 'MiB', 'GiB', 'TiB'][exp];
|
['B', 'KiB', 'MiB', 'GiB', 'TiB'][exp];
|
||||||
});
|
});
|
||||||
|
|
||||||
const timeScale = function(max){
|
// Mixin handling retrieving dynamic updates from the backend
|
||||||
return max > 3600
|
Vue.mixin((() => {
|
||||||
? { scale:function(v){return Math.round(v/360)/10}, label:'Hours' }
|
const setupEventSource = (to, query, next, comp) => {
|
||||||
: max > 60
|
|
||||||
? { scale:function(v){return Math.round(v/6)/10}, label:'Minutes' }
|
|
||||||
: { scale:function(v){return v;}, label:'Seconds' };
|
|
||||||
}
|
|
||||||
const ServerEventHandler = function() {
|
|
||||||
function setupEventSource(to, query, next, comp) {
|
|
||||||
const es = new EventSource(document.head.baseURI + to.path.substr(1) + query);
|
const es = new EventSource(document.head.baseURI + to.path.substr(1) + query);
|
||||||
es.comp = comp;
|
es.comp = comp; // When reconnecting, we already have a component. Usually this will be null.
|
||||||
es.path = to.path; // save for later in case we need to add query params
|
es.to = to; // Save a ref, needed for adding query params for pagination.
|
||||||
es.onmessage = function(msg) {
|
es.onmessage = function(msg) {
|
||||||
msg = JSON.parse(msg.data);
|
msg = JSON.parse(msg.data);
|
||||||
// "status" is the first message the server always delivers.
|
// "status" is the first message the server always delivers.
|
||||||
@ -39,7 +36,7 @@ const ServerEventHandler = function() {
|
|||||||
// should not be handled here, but treated the same as any other
|
// should not be handled here, but treated the same as any other
|
||||||
// message. An exception is if the connection has been lost - in
|
// message. An exception is if the connection has been lost - in
|
||||||
// that case we should treat this as a "first-time" status message.
|
// that case we should treat this as a "first-time" status message.
|
||||||
// this.comp.es is used as a proxy for this.
|
// !this.comp.es is used to test this condition.
|
||||||
if (msg.type === 'status' && (!this.comp || !this.comp.es)) {
|
if (msg.type === 'status' && (!this.comp || !this.comp.es)) {
|
||||||
next(comp => {
|
next(comp => {
|
||||||
// Set up bidirectional reference
|
// Set up bidirectional reference
|
||||||
@ -72,6 +69,7 @@ const ServerEventHandler = function() {
|
|||||||
es.onerror = function(e) {
|
es.onerror = function(e) {
|
||||||
this.comp.$root.connected = false;
|
this.comp.$root.connected = false;
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
// Recrate the EventSource, passing in the existing component
|
||||||
this.comp.es = setupEventSource(to, query, null, this.comp);
|
this.comp.es = setupEventSource(to, query, null, this.comp);
|
||||||
}, this.comp.esReconnectInterval);
|
}, this.comp.esReconnectInterval);
|
||||||
if(this.comp.esReconnectInterval < 7500)
|
if(this.comp.esReconnectInterval < 7500)
|
||||||
@ -95,16 +93,52 @@ const ServerEventHandler = function() {
|
|||||||
methods: {
|
methods: {
|
||||||
query(q) {
|
query(q) {
|
||||||
this.es.close();
|
this.es.close();
|
||||||
setupEventSource(this.es.path, '?' + Object.entries(q).map(([k,v])=>`${k}=${v}`).join('&'), (fn) => { fn(this); });
|
setupEventSource(this.es.to, '?' + Object.entries(q).map(([k,v])=>`${k}=${v}`).join('&'), fn => fn(this));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}();
|
})());
|
||||||
|
|
||||||
const Utils = {
|
// Mixin for periodically updating a progress bar
|
||||||
|
Vue.mixin({
|
||||||
|
data: () => ({ jobsRunning: [] }),
|
||||||
methods: {
|
methods: {
|
||||||
runIcon(result) {
|
updateProgress(o) {
|
||||||
return (result == 'success') ? /* checkmark */
|
if (o.etc) {
|
||||||
|
const p = (Math.floor(Date.now()/1000) + this.$root.clockSkew - o.started) / (o.etc - o.started);
|
||||||
|
if (p > 1.2)
|
||||||
|
o.overtime = true;
|
||||||
|
o.progress = (p >= 1) ? 99 : 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Utility methods
|
||||||
|
Vue.mixin({
|
||||||
|
methods: {
|
||||||
|
// Get an svg icon given a run result
|
||||||
|
runIcon: result =>
|
||||||
|
(result == 'success') ? /* checkmark */
|
||||||
`<svg class="status success" viewBox="0 0 100 100">
|
`<svg class="status success" viewBox="0 0 100 100">
|
||||||
<path d="m 23,46 c -6,0 -17,3 -17,11 0,8 9,30 12,32 3,2 14,5 20,-2 6,-6 24,-36
|
<path d="m 23,46 c -6,0 -17,3 -17,11 0,8 9,30 12,32 3,2 14,5 20,-2 6,-6 24,-36
|
||||||
56,-71 5,-3 -9,-8 -23,-2 -13,6 -33,42 -41,47 -6,-3 -5,-12 -8,-15 z" />
|
56,-71 5,-3 -9,-8 -23,-2 -13,6 -33,42 -41,47 -6,-3 -5,-12 -8,-15 z" />
|
||||||
@ -125,20 +159,20 @@ const Utils = {
|
|||||||
<circle cx="50" cy="50" r="40" stroke-width="15" fill="none" stroke-dasharray="175">
|
<circle cx="50" cy="50" r="40" stroke-width="15" fill="none" stroke-dasharray="175">
|
||||||
<animateTransform attributeName="transform" type="rotate" repeatCount="indefinite" dur="2s" values="0 50 50;360 50 50"></animateTransform>
|
<animateTransform attributeName="transform" type="rotate" repeatCount="indefinite" dur="2s" values="0 50 50;360 50 50"></animateTransform>
|
||||||
</circle>
|
</circle>
|
||||||
</svg>`
|
</svg>`,
|
||||||
;
|
// Pretty-print a unix date
|
||||||
},
|
formatDate: unix => {
|
||||||
formatDate: function(unix) {
|
|
||||||
// TODO: reimplement when toLocaleDateString() accepts formatting options on most browsers
|
// TODO: reimplement when toLocaleDateString() accepts formatting options on most browsers
|
||||||
var d = new Date(1000 * unix);
|
const d = new Date(1000 * unix);
|
||||||
var m = d.getMinutes();
|
let m = d.getMinutes();
|
||||||
if (m < 10) m = '0' + m;
|
if (m < 10)
|
||||||
return d.getHours() + ':' + m + ' on ' + ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'][d.getDay()] + ' ' +
|
m = '0' + m;
|
||||||
d.getDate() + '. ' + ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
|
return d.getHours() + ':' + m + ' on ' +
|
||||||
'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'
|
['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'][d.getDay()] + ' ' + d.getDate() + '. ' +
|
||||||
][d.getMonth()] + ' ' +
|
['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'][d.getMonth()] + ' ' +
|
||||||
d.getFullYear();
|
d.getFullYear();
|
||||||
},
|
},
|
||||||
|
// Pretty-print a duration
|
||||||
formatDuration: function(start, end) {
|
formatDuration: function(start, end) {
|
||||||
if(!end)
|
if(!end)
|
||||||
end = Math.floor(Date.now()/1000) + this.$root.clockSkew;
|
end = Math.floor(Date.now()/1000) + this.$root.clockSkew;
|
||||||
@ -150,93 +184,41 @@ const Utils = {
|
|||||||
return (end-start) + ' seconds';
|
return (end-start) + ' seconds';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
});
|
||||||
|
|
||||||
const ProgressUpdater = {
|
|
||||||
data() { return { jobsRunning: [] }; },
|
|
||||||
methods: {
|
|
||||||
updateProgress(o) {
|
|
||||||
if (o.etc) {
|
|
||||||
var p = (Math.floor(Date.now()/1000) + this.$root.clockSkew - o.started) / (o.etc - o.started);
|
|
||||||
if (p > 1.2) {
|
|
||||||
o.overtime = true;
|
|
||||||
}
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const Home = function() {
|
|
||||||
var state = {
|
|
||||||
jobsQueued: [],
|
|
||||||
jobsRecent: [],
|
|
||||||
resultChanged: [],
|
|
||||||
lowPassRates: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
var chtUtilization, chtBuildsPerDay, chtBuildsPerJob, chtTimePerJob;
|
|
||||||
|
|
||||||
var updateUtilization = function(busy) {
|
|
||||||
chtUtilization.data.datasets[0].data[0] += busy ? 1 : -1;
|
|
||||||
chtUtilization.data.datasets[0].data[1] -= busy ? 1 : -1;
|
|
||||||
chtUtilization.update();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// Chart factory
|
||||||
|
const Charts = (() => {
|
||||||
|
// TODO usage is broken!
|
||||||
|
const timeScale = max => max > 3600
|
||||||
|
? { factor: 1/3600, ticks: v => v.toFixed(1), label:'Hours' }
|
||||||
|
: max > 60
|
||||||
|
? { factor: 1/60, ticks: v => v.toFixed(1), label:'Minutes' }
|
||||||
|
: { factor: 1, ticks: v => v, label:'Seconds' };
|
||||||
return {
|
return {
|
||||||
template: '#home',
|
createExecutorUtilizationChart: (id, nBusy, nTotal) => {
|
||||||
mixins: [ServerEventHandler, Utils, ProgressUpdater],
|
const c = new Chart(document.getElementById(id), {
|
||||||
data: function() {
|
|
||||||
return state;
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
status: function(msg) {
|
|
||||||
state.jobsQueued = msg.queued;
|
|
||||||
state.jobsRunning = msg.running;
|
|
||||||
state.jobsRecent = msg.recent;
|
|
||||||
state.resultChanged = msg.resultChanged;
|
|
||||||
state.lowPassRates = msg.lowPassRates;
|
|
||||||
this.$forceUpdate();
|
|
||||||
|
|
||||||
// setup charts
|
|
||||||
chtUtilization = new Chart(document.getElementById("chartUtil"), {
|
|
||||||
type: 'pie',
|
type: 'pie',
|
||||||
data: {
|
data: {
|
||||||
labels: ["Busy", "Idle"],
|
labels: [ "Busy", "Idle" ],
|
||||||
datasets: [{
|
datasets: [{
|
||||||
data: [ msg.executorsBusy, msg.executorsTotal - msg.executorsBusy ],
|
data: [ nBusy, nTotal - nBusy ],
|
||||||
backgroundColor: ["#afa674", "#7483af"]
|
backgroundColor: [ "#afa674", "#7483af" ]
|
||||||
}]
|
}]
|
||||||
},
|
},
|
||||||
options: {
|
options: {
|
||||||
hover: { mode: null }
|
hover: { mode: null }
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
var buildsPerDayDates = function(){
|
c.executorBusyChanged = busy => {
|
||||||
res = [];
|
c.data.datasets[0].data[0] += busy ? 1 : -1;
|
||||||
|
c.data.datasets[0].data[1] -= busy ? 1 : -1;
|
||||||
|
c.update();
|
||||||
|
}
|
||||||
|
return c;
|
||||||
|
},
|
||||||
|
createRunsPerDayChart: (id, data) => {
|
||||||
|
const dayNames = (() => {
|
||||||
|
const res = [];
|
||||||
var now = new Date();
|
var now = new Date();
|
||||||
for (var i = 6; i >= 0; --i) {
|
for (var i = 6; i >= 0; --i) {
|
||||||
var then = new Date(now.getTime() - i * 86400000);
|
var then = new Date(now.getTime() - i * 86400000);
|
||||||
@ -246,91 +228,104 @@ const Home = function() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
return res;
|
return res;
|
||||||
}();
|
})();
|
||||||
chtBuildsPerDay = new Chart(document.getElementById("chartBpd"), {
|
const c = new Chart(document.getElementById(id), {
|
||||||
type: 'line',
|
type: 'line',
|
||||||
data: {
|
data: {
|
||||||
labels: buildsPerDayDates.map((e)=>{ return e.short; }),
|
labels: dayNames.map(e => e.short),
|
||||||
datasets: [{
|
datasets: [{
|
||||||
label: 'Failed Builds',
|
label: 'Failed Builds',
|
||||||
backgroundColor: "#883d3d",
|
backgroundColor: "#883d3d",
|
||||||
data: msg.buildsPerDay.map((e)=>{ return e.failed || 0; })
|
data: data.map(e => e.failed || 0)
|
||||||
},{
|
},{
|
||||||
label: 'Successful Builds',
|
label: 'Successful Builds',
|
||||||
backgroundColor: "#74af77",
|
backgroundColor: "#74af77",
|
||||||
data: msg.buildsPerDay.map((e)=>{ return e.success || 0; })
|
data: data.map(e => e.success || 0)
|
||||||
}]
|
}]
|
||||||
},
|
},
|
||||||
options:{
|
options:{
|
||||||
title: { display: true, text: 'Builds per day' },
|
title: { display: true, text: 'Builds per day' },
|
||||||
tooltips:{callbacks:{title: function(tip, data) {
|
tooltips:{callbacks:{title: (tip, data) => dayNames[tip[0].index].long}},
|
||||||
return buildsPerDayDates[tip[0].index].long;
|
|
||||||
}}},
|
|
||||||
scales:{yAxes:[{
|
scales:{yAxes:[{
|
||||||
ticks:{userCallback: (label, index, labels)=>{
|
ticks:{userCallback: (label, index, labels) => Number.isInteger(label) ? label: null},
|
||||||
if(Number.isInteger(label))
|
|
||||||
return label;
|
|
||||||
}},
|
|
||||||
stacked: true
|
stacked: true
|
||||||
}]}
|
}]}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
chtBuildsPerJob = new Chart(document.getElementById("chartBpj"), {
|
c.jobCompleted = success => {
|
||||||
|
c.data.datasets[success ? 0 : 1].data[6]++;
|
||||||
|
c.update();
|
||||||
|
}
|
||||||
|
return c;
|
||||||
|
},
|
||||||
|
createRunsPerJobChart: (id, data) => {
|
||||||
|
const c = new Chart(document.getElementById("chartBpj"), {
|
||||||
type: 'horizontalBar',
|
type: 'horizontalBar',
|
||||||
data: {
|
data: {
|
||||||
labels: Object.keys(msg.buildsPerJob),
|
labels: Object.keys(data),
|
||||||
datasets: [{
|
datasets: [{
|
||||||
label: 'Runs in last 24 hours',
|
label: 'Runs in last 24 hours',
|
||||||
backgroundColor: "#7483af",
|
backgroundColor: "#7483af",
|
||||||
data: Object.keys(msg.buildsPerJob).map((e)=>{ return msg.buildsPerJob[e]; })
|
data: Object.keys(data).map(e => data[e])
|
||||||
}]
|
}]
|
||||||
},
|
},
|
||||||
options:{
|
options:{
|
||||||
title: { display: true, text: 'Builds per job' },
|
title: { display: true, text: 'Builds per job' },
|
||||||
hover: { mode: null },
|
hover: { mode: null },
|
||||||
scales:{xAxes:[{ticks:{userCallback: (label, index, labels)=>{
|
scales:{xAxes:[{ticks:{userCallback: (label, index, labels)=> Number.isInteger(label) ? label: null}}]}
|
||||||
if(Number.isInteger(label))
|
|
||||||
return label;
|
|
||||||
}}}]}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
var tpjScale = timeScale(Math.max(...Object.values(msg.timePerJob)));
|
c.jobCompleted = name => {
|
||||||
chtTimePerJob = new Chart(document.getElementById("chartTpj"), {
|
for (var j = 0; j < c.data.datasets[0].data.length; ++j) {
|
||||||
|
if (c.data.labels[j] == name) {
|
||||||
|
c.data.datasets[0].data[j]++;
|
||||||
|
c.update();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return c;
|
||||||
|
},
|
||||||
|
createTimePerJobChart: (id, data) => {
|
||||||
|
const scale = timeScale(Math.max(...Object.values(data)));
|
||||||
|
return new Chart(document.getElementById(id), {
|
||||||
type: 'horizontalBar',
|
type: 'horizontalBar',
|
||||||
data: {
|
data: {
|
||||||
labels: Object.keys(msg.timePerJob),
|
labels: Object.keys(data),
|
||||||
datasets: [{
|
datasets: [{
|
||||||
label: 'Mean run time this week',
|
label: 'Mean run time this week',
|
||||||
backgroundColor: "#7483af",
|
backgroundColor: "#7483af",
|
||||||
data: Object.keys(msg.timePerJob).map((e)=>{ return msg.timePerJob[e]; })
|
data: Object.keys(data).map(e => data[e] * scale.factor)
|
||||||
}]
|
}]
|
||||||
},
|
},
|
||||||
options:{
|
options:{
|
||||||
title: { display: true, text: 'Mean run time this week' },
|
title: { display: true, text: 'Mean run time this week' },
|
||||||
hover: { mode: null },
|
hover: { mode: null },
|
||||||
scales:{xAxes:[{
|
scales:{xAxes:[{
|
||||||
ticks:{userCallback: tpjScale.scale},
|
ticks:{userCallback: scale.ticks},
|
||||||
scaleLabel: {
|
scaleLabel: {
|
||||||
display: true,
|
display: true,
|
||||||
labelString: tpjScale.label
|
labelString: scale.label
|
||||||
}
|
}
|
||||||
}]},
|
}]},
|
||||||
tooltips:{callbacks:{label:(tip, data)=>{
|
tooltips:{callbacks:{
|
||||||
return data.datasets[tip.datasetIndex].label + ': ' + tip.xLabel + ' ' + tpjScale.label.toLowerCase();
|
label: (tip, data) => data.datasets[tip.datasetIndex].label + ': ' + tip.xLabel + ' ' + scale.label.toLowerCase()
|
||||||
}}}
|
}}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
const btcScale = timeScale(Math.max(...msg.buildTimeChanges.map(e=>Math.max(...e.durations))));
|
},
|
||||||
var chtBuildTimeChanges = new Chart(document.getElementById("chartBuildTimeChanges"), {
|
createRunTimeChangesChart: (id, data) => {
|
||||||
|
const scale = timeScale(Math.max(...data.map(e => Math.max(...e.durations))));
|
||||||
|
return new Chart(document.getElementById(id), {
|
||||||
type: 'line',
|
type: 'line',
|
||||||
data: {
|
data: {
|
||||||
labels: [...Array(10).keys()],
|
labels: [...Array(10).keys()],
|
||||||
datasets: msg.buildTimeChanges.map((e)=>{return {
|
datasets: data.map(e => ({
|
||||||
label: e.name,
|
label: e.name,
|
||||||
data: e.durations,
|
data: e.durations.map(x => x * scale.factor),
|
||||||
borderColor: 'hsl('+(e.name.hashCode() % 360)+', 27%, 57%)',
|
borderColor: 'hsl('+(e.name.hashCode() % 360)+', 27%, 57%)',
|
||||||
backgroundColor: 'transparent'
|
backgroundColor: 'transparent'
|
||||||
}})
|
}))
|
||||||
},
|
},
|
||||||
options:{
|
options:{
|
||||||
title: { display: true, text: 'Build time changes' },
|
title: { display: true, text: 'Build time changes' },
|
||||||
@ -338,10 +333,10 @@ const Home = function() {
|
|||||||
scales:{
|
scales:{
|
||||||
xAxes:[{ticks:{display: false}}],
|
xAxes:[{ticks:{display: false}}],
|
||||||
yAxes:[{
|
yAxes:[{
|
||||||
ticks:{userCallback: btcScale.scale},
|
ticks:{userCallback: scale.ticks},
|
||||||
scaleLabel: {
|
scaleLabel: {
|
||||||
display: true,
|
display: true,
|
||||||
labelString: btcScale.label
|
labelString: scale.label
|
||||||
}
|
}
|
||||||
}]
|
}]
|
||||||
},
|
},
|
||||||
@ -351,6 +346,96 @@ const Home = function() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
createRunTimeChart: (id, jobs, avg) => {
|
||||||
|
const scale = timeScale(Math.max(...jobs.map(v=>v.completed-v.started)));
|
||||||
|
return new Chart(document.getElementById(id), {
|
||||||
|
type: 'bar',
|
||||||
|
data: {
|
||||||
|
labels: jobs.map(e => '#' + e.number).reverse(),
|
||||||
|
datasets: [{
|
||||||
|
label: 'Average',
|
||||||
|
type: 'line',
|
||||||
|
data: [{x:0, y:avg * scale.factor}, {x:1, y:avg * scale.factor}],
|
||||||
|
borderColor: '#7483af',
|
||||||
|
backgroundColor: 'transparent',
|
||||||
|
xAxisID: 'avg',
|
||||||
|
pointRadius: 0,
|
||||||
|
pointHitRadius: 0,
|
||||||
|
pointHoverRadius: 0,
|
||||||
|
},{
|
||||||
|
label: 'Build time',
|
||||||
|
backgroundColor: jobs.map(e => e.result == 'success' ? '#74af77': '#883d3d').reverse(),
|
||||||
|
data: jobs.map(e => (e.completed - e.started) * scale.factor).reverse()
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
title: { display: true, text: 'Build time' },
|
||||||
|
hover: { mode: null },
|
||||||
|
scales:{
|
||||||
|
xAxes:[{
|
||||||
|
categoryPercentage: 0.95,
|
||||||
|
barPercentage: 1.0
|
||||||
|
},{
|
||||||
|
id: 'avg',
|
||||||
|
type: 'linear',
|
||||||
|
ticks: {
|
||||||
|
display: false
|
||||||
|
},
|
||||||
|
gridLines: {
|
||||||
|
display: false,
|
||||||
|
drawBorder: false
|
||||||
|
}
|
||||||
|
}],
|
||||||
|
yAxes:[{
|
||||||
|
ticks:{userCallback: scale.ticks},
|
||||||
|
scaleLabel:{display: true, labelString: scale.label}
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
tooltips:{callbacks:{
|
||||||
|
label: (tip, data) => scale.ticks(tip.yLabel) + ' ' + scale.label.toLowerCase()
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
|
||||||
|
// For all charts, set miniumum Y to 0
|
||||||
|
Chart.scaleService.updateScaleDefaults('linear', {
|
||||||
|
ticks: { suggestedMin: 0 }
|
||||||
|
});
|
||||||
|
// Don't display legend by default
|
||||||
|
Chart.defaults.global.legend.display = false;
|
||||||
|
// Disable tooltip hover animations
|
||||||
|
Chart.defaults.global.hover.animationDuration = 0;
|
||||||
|
|
||||||
|
// Component for the / endpoint
|
||||||
|
const Home = templateId => {
|
||||||
|
const state = {
|
||||||
|
jobsQueued: [],
|
||||||
|
jobsRecent: [],
|
||||||
|
resultChanged: [],
|
||||||
|
lowPassRates: [],
|
||||||
|
};
|
||||||
|
let chtUtilization, chtBuildsPerDay, chtBuildsPerJob, chtTimePerJob;
|
||||||
|
return {
|
||||||
|
template: templateId,
|
||||||
|
data: () => state,
|
||||||
|
methods: {
|
||||||
|
status: function(msg) {
|
||||||
|
state.jobsQueued = msg.queued;
|
||||||
|
state.jobsRunning = msg.running;
|
||||||
|
state.jobsRecent = msg.recent;
|
||||||
|
state.resultChanged = msg.resultChanged;
|
||||||
|
state.lowPassRates = msg.lowPassRates;
|
||||||
|
this.$forceUpdate();
|
||||||
|
|
||||||
|
chtUtilization = Charts.createExecutorUtilizationChart("chartUtil", msg.executorsBusy, msg.executorsTotal);
|
||||||
|
chtBuildsPerDay = Charts.createRunsPerDayChart("chartBpd", msg.buildsPerDay);
|
||||||
|
chtBuildsPerJob = Charts.createRunsPerJobChart("chartBpj", msg.buildsPerJob);
|
||||||
|
chtTimePerJob = Charts.createTimePerJobChart("chartTpj", msg.timePerJob);
|
||||||
|
chtBuildTimeChanges = Charts.createRunTimeChangesChart("chartBuildTimeChanges", msg.buildTimeChanges);
|
||||||
|
},
|
||||||
job_queued: function(data) {
|
job_queued: function(data) {
|
||||||
state.jobsQueued.splice(0, 0, data);
|
state.jobsQueued.splice(0, 0, data);
|
||||||
this.$forceUpdate();
|
this.$forceUpdate();
|
||||||
@ -359,15 +444,9 @@ const Home = function() {
|
|||||||
state.jobsQueued.splice(state.jobsQueued.length - data.queueIndex - 1, 1);
|
state.jobsQueued.splice(state.jobsQueued.length - data.queueIndex - 1, 1);
|
||||||
state.jobsRunning.splice(0, 0, data);
|
state.jobsRunning.splice(0, 0, data);
|
||||||
this.$forceUpdate();
|
this.$forceUpdate();
|
||||||
updateUtilization(true);
|
chtUtilization.executorBusyChanged(true);
|
||||||
},
|
},
|
||||||
job_completed: function(data) {
|
job_completed: function(data) {
|
||||||
if (data.result === "success")
|
|
||||||
chtBuildsPerDay.data.datasets[0].data[6]++;
|
|
||||||
else
|
|
||||||
chtBuildsPerDay.data.datasets[1].data[6]++;
|
|
||||||
chtBuildsPerDay.update();
|
|
||||||
|
|
||||||
for (var i = 0; i < state.jobsRunning.length; ++i) {
|
for (var i = 0; i < state.jobsRunning.length; ++i) {
|
||||||
var job = state.jobsRunning[i];
|
var job = state.jobsRunning[i];
|
||||||
if (job.name == data.name && job.number == data.number) {
|
if (job.name == data.name && job.number == data.number) {
|
||||||
@ -377,21 +456,17 @@ const Home = function() {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
updateUtilization(false);
|
chtBuildsPerDay.jobCompleted(data.result === 'success')
|
||||||
for (var j = 0; j < chtBuildsPerJob.data.datasets[0].data.length; ++j) {
|
chtUtilization.executorBusyChanged(false);
|
||||||
if (chtBuildsPerJob.data.labels[j] == job.name) {
|
chtBuildsPerJob.jobCompleted(data.name)
|
||||||
chtBuildsPerJob.data.datasets[0].data[j]++;
|
|
||||||
chtBuildsPerJob.update();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}();
|
};
|
||||||
|
|
||||||
const All = function(templateId) {
|
// Component for the /jobs and /wallboard endpoints
|
||||||
var state = {
|
const All = templateId => {
|
||||||
|
const state = {
|
||||||
jobs: [],
|
jobs: [],
|
||||||
search: '',
|
search: '',
|
||||||
groups: {},
|
groups: {},
|
||||||
@ -401,23 +476,22 @@ const All = function(templateId) {
|
|||||||
};
|
};
|
||||||
return {
|
return {
|
||||||
template: templateId,
|
template: templateId,
|
||||||
mixins: [ServerEventHandler, Utils, ProgressUpdater],
|
data: () => state,
|
||||||
data: function() { return state; },
|
|
||||||
methods: {
|
methods: {
|
||||||
status: function(msg) {
|
status: function(msg) {
|
||||||
state.jobs = msg.jobs;
|
state.jobs = msg.jobs;
|
||||||
state.jobsRunning = msg.running;
|
state.jobsRunning = msg.running;
|
||||||
// mix running and completed jobs
|
// mix running and completed jobs
|
||||||
for (var i in msg.running) {
|
msg.running.forEach(job => {
|
||||||
var idx = state.jobs.findIndex(job => job.name === msg.running[i].name);
|
const idx = state.jobs.findIndex(j => j.name === job.name);
|
||||||
if (idx > -1)
|
if (idx > -1)
|
||||||
state.jobs[idx] = msg.running[i];
|
state.jobs[idx] = job;
|
||||||
else {
|
else {
|
||||||
// special case: first run of a job.
|
// special case: first run of a job.
|
||||||
state.jobs.unshift(msg.running[i]);
|
state.jobs.unshift(j);
|
||||||
state.jobs.sort(function(a, b){return a.name < b.name ? -1 : a.name > b.name ? 1 : 0;});
|
state.jobs.sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
});
|
||||||
state.groups = {};
|
state.groups = {};
|
||||||
Object.keys(msg.groups).forEach(k => state.regexps[k] = new RegExp(state.groups[k] = msg.groups[k]));
|
Object.keys(msg.groups).forEach(k => state.regexps[k] = new RegExp(state.groups[k] = msg.groups[k]));
|
||||||
state.ungrouped = state.jobs.filter(j => !Object.values(state.regexps).some(r => r.test(j.name))).map(j => j.name);
|
state.ungrouped = state.jobs.filter(j => !Object.values(state.regexps).some(r => r.test(j.name))).map(j => j.name);
|
||||||
@ -425,30 +499,18 @@ const All = function(templateId) {
|
|||||||
},
|
},
|
||||||
job_started: function(data) {
|
job_started: function(data) {
|
||||||
data.result = 'running'; // for wallboard css
|
data.result = 'running'; // for wallboard css
|
||||||
var updAt = null;
|
|
||||||
// jobsRunning must be maintained for ProgressUpdater
|
// jobsRunning must be maintained for ProgressUpdater
|
||||||
for (var i in state.jobsRunning) {
|
let updAt = state.jobsRunning.findIndex(j => j.name === data.name);
|
||||||
if (state.jobsRunning[i].name === data.name) {
|
if (updAt === -1) {
|
||||||
updAt = i;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (updAt === null) {
|
|
||||||
state.jobsRunning.unshift(data);
|
state.jobsRunning.unshift(data);
|
||||||
} else {
|
} else {
|
||||||
state.jobsRunning[updAt] = data;
|
state.jobsRunning[updAt] = data;
|
||||||
}
|
}
|
||||||
updAt = null;
|
updAt = state.jobs.findIndex(j => j.name === data.name);
|
||||||
for (var i in state.jobs) {
|
if (updAt === -1) {
|
||||||
if (state.jobs[i].name === data.name) {
|
|
||||||
updAt = i;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (updAt === null) {
|
|
||||||
// first execution of new job. TODO insert without resort
|
// first execution of new job. TODO insert without resort
|
||||||
state.jobs.unshift(data);
|
state.jobs.unshift(data);
|
||||||
state.jobs.sort(function(a, b){return a.name < b.name ? -1 : a.name > b.name ? 1 : 0;});
|
state.jobs.sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
|
||||||
if(!Object.values(state.regexps).some(r => r.test(data.name)))
|
if(!Object.values(state.regexps).some(r => r.test(data.name)))
|
||||||
state.ungrouped.push(data.name);
|
state.ungrouped.push(data.name);
|
||||||
} else {
|
} else {
|
||||||
@ -457,19 +519,13 @@ const All = function(templateId) {
|
|||||||
this.$forceUpdate();
|
this.$forceUpdate();
|
||||||
},
|
},
|
||||||
job_completed: function(data) {
|
job_completed: function(data) {
|
||||||
for (var i in state.jobs) {
|
let updAt = state.jobs.findIndex(j => j.name === data.name);
|
||||||
if (state.jobs[i].name === data.name) {
|
if (updAt > -1)
|
||||||
state.jobs[i] = data;
|
state.jobs[updAt] = data;
|
||||||
// forceUpdate in second loop
|
updAt = state.jobsRunning.findIndex(j => j.name === data.name);
|
||||||
break;
|
if (updAt > -1) {
|
||||||
}
|
state.jobsRunning.splice(updAt, 1);
|
||||||
}
|
|
||||||
for (var i in state.jobsRunning) {
|
|
||||||
if (state.jobsRunning[i].name === data.name) {
|
|
||||||
state.jobsRunning.splice(i, 1);
|
|
||||||
this.$forceUpdate();
|
this.$forceUpdate();
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
filteredJobs: function() {
|
filteredJobs: function() {
|
||||||
@ -500,8 +556,9 @@ const All = function(templateId) {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
var Job = function() {
|
// Component for the /job/:name endpoint
|
||||||
var state = {
|
const Job = templateId => {
|
||||||
|
const state = {
|
||||||
description: '',
|
description: '',
|
||||||
jobsRunning: [],
|
jobsRunning: [],
|
||||||
jobsRecent: [],
|
jobsRecent: [],
|
||||||
@ -511,13 +568,10 @@ var Job = function() {
|
|||||||
pages: 0,
|
pages: 0,
|
||||||
sort: {}
|
sort: {}
|
||||||
};
|
};
|
||||||
var chtBt = null;
|
let chtBt = null;
|
||||||
return Vue.extend({
|
return {
|
||||||
template: '#job',
|
template: templateId,
|
||||||
mixins: [ServerEventHandler, Utils, ProgressUpdater],
|
data: () => state,
|
||||||
data: function() {
|
|
||||||
return state;
|
|
||||||
},
|
|
||||||
methods: {
|
methods: {
|
||||||
status: function(msg) {
|
status: function(msg) {
|
||||||
state.description = msg.description;
|
state.description = msg.description;
|
||||||
@ -533,59 +587,7 @@ var Job = function() {
|
|||||||
// old chart and recreate it to prevent flickering of old data
|
// old chart and recreate it to prevent flickering of old data
|
||||||
if(chtBt)
|
if(chtBt)
|
||||||
chtBt.destroy();
|
chtBt.destroy();
|
||||||
const btScale = timeScale(Math.max(...msg.recent.map(v=>v.completed-v.started)));
|
chtBt = Charts.createRunTimeChart("chartBt", msg.recent, msg.averageRuntime);
|
||||||
chtBt = new Chart(document.getElementById("chartBt"), {
|
|
||||||
type: 'bar',
|
|
||||||
data: {
|
|
||||||
labels: msg.recent.map(function(e) {
|
|
||||||
return '#' + e.number;
|
|
||||||
}).reverse(),
|
|
||||||
datasets: [{
|
|
||||||
label: 'Average',
|
|
||||||
type: 'line',
|
|
||||||
data: [{x:0,y:msg.averageRuntime},{x:1,y:msg.averageRuntime}],
|
|
||||||
borderColor: '#7483af',
|
|
||||||
backgroundColor: 'transparent',
|
|
||||||
xAxisID: 'avg',
|
|
||||||
pointRadius: 0,
|
|
||||||
pointHitRadius: 0,
|
|
||||||
pointHoverRadius: 0,
|
|
||||||
},{
|
|
||||||
label: 'Build time',
|
|
||||||
backgroundColor: msg.recent.map(e => e.result == 'success' ? '#74af77': '#883d3d').reverse(),
|
|
||||||
data: msg.recent.map(function(e) {
|
|
||||||
return e.completed - e.started;
|
|
||||||
}).reverse()
|
|
||||||
}]
|
|
||||||
},
|
|
||||||
options: {
|
|
||||||
title: { display: true, text: 'Build time' },
|
|
||||||
hover: { mode: null },
|
|
||||||
scales:{
|
|
||||||
xAxes:[{
|
|
||||||
categoryPercentage: 1.0,
|
|
||||||
barPercentage: 1.0
|
|
||||||
},{
|
|
||||||
id: 'avg',
|
|
||||||
type: 'linear',
|
|
||||||
ticks: {
|
|
||||||
display: false
|
|
||||||
},
|
|
||||||
gridLines: {
|
|
||||||
display: false,
|
|
||||||
drawBorder: false
|
|
||||||
}
|
|
||||||
}],
|
|
||||||
yAxes:[{
|
|
||||||
ticks:{userCallback: btScale.scale},
|
|
||||||
scaleLabel:{display: true, labelString: btScale.label}
|
|
||||||
}]
|
|
||||||
},
|
|
||||||
tooltips:{callbacks:{label:(tip, data)=>{
|
|
||||||
return data.datasets[tip.datasetIndex].label + ': ' + tip.yLabel + ' ' + btScale.label.toLowerCase();
|
|
||||||
}}}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
job_queued: function() {
|
job_queued: function() {
|
||||||
state.nQueued++;
|
state.nQueued++;
|
||||||
@ -596,15 +598,12 @@ var Job = function() {
|
|||||||
this.$forceUpdate();
|
this.$forceUpdate();
|
||||||
},
|
},
|
||||||
job_completed: function(data) {
|
job_completed: function(data) {
|
||||||
for (var i = 0; i < state.jobsRunning.length; ++i) {
|
const i = state.jobsRunning.findIndex(j => j.number === data.number);
|
||||||
var job = state.jobsRunning[i];
|
if (i > -1) {
|
||||||
if (job.number === data.number) {
|
|
||||||
state.jobsRunning.splice(i, 1);
|
state.jobsRunning.splice(i, 1);
|
||||||
state.jobsRecent.splice(0, 0, data);
|
state.jobsRecent.splice(0, 0, data);
|
||||||
this.$forceUpdate();
|
this.$forceUpdate();
|
||||||
// TODO: update the chart
|
// TODO: update the chart
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
page_next: function() {
|
page_next: function() {
|
||||||
@ -625,12 +624,13 @@ var Job = function() {
|
|||||||
this.query(state.sort)
|
this.query(state.sort)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
};
|
||||||
}();
|
};
|
||||||
|
|
||||||
const Run = function() {
|
// Component for the /job/:name/:number endpoint
|
||||||
|
const Run = templateId => {
|
||||||
const utf8decoder = new TextDecoder('utf-8');
|
const utf8decoder = new TextDecoder('utf-8');
|
||||||
var state = {
|
const state = {
|
||||||
job: { artifacts: [], upstream: {} },
|
job: { artifacts: [], upstream: {} },
|
||||||
latestNum: null,
|
latestNum: null,
|
||||||
log: '',
|
log: '',
|
||||||
@ -641,13 +641,19 @@ const Run = function() {
|
|||||||
// ATOW pipeThrough not supported in Firefox
|
// ATOW pipeThrough not supported in Firefox
|
||||||
//const reader = res.body.pipeThrough(new TextDecoderStream).getReader();
|
//const reader = res.body.pipeThrough(new TextDecoderStream).getReader();
|
||||||
const reader = res.body.getReader();
|
const reader = res.body.getReader();
|
||||||
let total = 0;
|
|
||||||
return function pump() {
|
return function pump() {
|
||||||
return reader.read().then(({done, value}) => {
|
return reader.read().then(({done, value}) => {
|
||||||
value = utf8decoder.decode(value);
|
value = utf8decoder.decode(value);
|
||||||
if (done)
|
if (done)
|
||||||
return;
|
return;
|
||||||
state.log += ansi_up.ansi_to_html(value.replace(/</g,'<').replace(/>/g,'>').replace(/\033\[\{([^:]+):(\d+)\033\\/g, (m,$1,$2)=>{return '<a href="jobs/'+$1+'" onclick="return vroute(this);">'+$1+'</a>:<a href="jobs/'+$1+'/'+$2+'" onclick="return vroute(this);">#'+$2+'</a>';}));
|
state.log += ansi_up.ansi_to_html(
|
||||||
|
value.replace(/</g,'<')
|
||||||
|
.replace(/>/g,'>')
|
||||||
|
.replace(/\033\[\{([^:]+):(\d+)\033\\/g, (m, $1, $2) =>
|
||||||
|
'<a href="jobs/'+$1+'" onclick="return vroute(this);">'+$1+'</a>:'+
|
||||||
|
'<a href="jobs/'+$1+'/'+$2+'" onclick="return vroute(this);">#'+$2+'</a>'
|
||||||
|
)
|
||||||
|
);
|
||||||
vm.$forceUpdate();
|
vm.$forceUpdate();
|
||||||
return pump();
|
return pump();
|
||||||
});
|
});
|
||||||
@ -655,13 +661,9 @@ const Run = function() {
|
|||||||
}).catch(e => {});
|
}).catch(e => {});
|
||||||
return abort;
|
return abort;
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
template: '#run',
|
template: templateId,
|
||||||
mixins: [ServerEventHandler, Utils, ProgressUpdater],
|
data: () => state,
|
||||||
data: function() {
|
|
||||||
return state;
|
|
||||||
},
|
|
||||||
methods: {
|
methods: {
|
||||||
status: function(data, params) {
|
status: function(data, params) {
|
||||||
// Check for the /latest endpoint
|
// Check for the /latest endpoint
|
||||||
@ -705,72 +707,47 @@ const Run = function() {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}();
|
};
|
||||||
|
|
||||||
// For all charts, set miniumum Y to 0
|
|
||||||
Chart.scaleService.updateScaleDefaults('linear', {
|
|
||||||
ticks: { suggestedMin: 0 }
|
|
||||||
});
|
|
||||||
// Don't display legend by default
|
|
||||||
Chart.defaults.global.legend.display = false;
|
|
||||||
// Disable tooltip hover animations
|
|
||||||
Chart.defaults.global.hover.animationDuration = 0;
|
|
||||||
// Plugin to move a DOM item on top of a chart element
|
|
||||||
Chart.plugins.register({
|
|
||||||
afterDatasetsDraw: (chart) => {
|
|
||||||
chart.data.datasets.forEach((dataset, i) => {
|
|
||||||
var meta = chart.getDatasetMeta(i);
|
|
||||||
if(dataset.itemid)
|
|
||||||
meta.data.forEach((e,j) => {
|
|
||||||
var pos = e.getCenterPoint();
|
|
||||||
var node = document.getElementById(dataset.itemid[j]);
|
|
||||||
node.style.top = (pos.y - node.clientHeight/2) + 'px';
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
new Vue({
|
new Vue({
|
||||||
el: '#app',
|
el: '#app',
|
||||||
data: {
|
data: {
|
||||||
title: '', // populated by status ws message
|
title: '', // populated by status message
|
||||||
version: '',
|
version: '',
|
||||||
clockSkew: 0,
|
clockSkew: 0,
|
||||||
connected: false,
|
connected: false,
|
||||||
notify: 'localStorage' in window && localStorage.getItem('showNotifications') == 1
|
notify: 'localStorage' in window && localStorage.getItem('showNotifications') == 1
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
supportsNotifications() {
|
supportsNotifications: () =>
|
||||||
return 'Notification' in window && Notification.permission !== 'denied';
|
'Notification' in window && Notification.permission !== 'denied'
|
||||||
}
|
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
toggleNotifications(en) {
|
toggleNotifications: function(en) {
|
||||||
if(Notification.permission !== 'granted')
|
if(Notification.permission !== 'granted')
|
||||||
Notification.requestPermission(p => this.notify = (p === 'granted'))
|
Notification.requestPermission(p => this.notify = (p === 'granted'))
|
||||||
else
|
else
|
||||||
this.notify = en;
|
this.notify = en;
|
||||||
},
|
},
|
||||||
showNotify(msg, data) {
|
showNotify: function(msg, data) {
|
||||||
if(this.notify && msg === 'job_completed')
|
if(this.notify && msg === 'job_completed')
|
||||||
new Notification('Job ' + data.result, {
|
new Notification('Job ' + data.result, {
|
||||||
body: data.name + ' ' + '#' + data.number + ': ' + data.result
|
body: data.name + ' ' + '#' + data.number + ': ' + data.result
|
||||||
});
|
});
|
||||||
},
|
}
|
||||||
runIcon: Utils.methods.runIcon
|
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
notify(e) { localStorage.setItem('showNotifications', e ? 1 : 0); }
|
notify: e => localStorage.setItem('showNotifications', e ? 1 : 0)
|
||||||
},
|
},
|
||||||
router: new VueRouter({
|
router: new VueRouter({
|
||||||
mode: 'history',
|
mode: 'history',
|
||||||
base: document.head.baseURI.substr(location.origin.length),
|
base: document.head.baseURI.substr(location.origin.length),
|
||||||
routes: [
|
routes: [
|
||||||
{ path: '/', component: Home },
|
{ path: '/', component: Home('#home') },
|
||||||
{ path: '/jobs', component: All('#jobs') },
|
{ path: '/jobs', component: All('#jobs') },
|
||||||
{ path: '/wallboard', component: All('#wallboard') },
|
{ path: '/wallboard', component: All('#wallboard') },
|
||||||
{ path: '/jobs/:name', component: Job },
|
{ path: '/jobs/:name', component: Job('#job') },
|
||||||
{ path: '/jobs/:name/:number', component: Run }
|
{ path: '/jobs/:name/:number', component: Run('#run') }
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
Loading…
Reference in New Issue
Block a user