2017-07-13 18:57:28 +00:00
|
|
|
/* laminar.js
|
|
|
|
* frontend application for Laminar Continuous Integration
|
|
|
|
* https://laminar.ohwg.net
|
|
|
|
*/
|
2018-06-01 07:14:59 +00:00
|
|
|
|
2020-10-16 08:19:09 +00:00
|
|
|
// A hash function added to String helps generating consistent
|
|
|
|
// colours from job names for use in charts
|
2018-09-08 15:16:23 +00:00
|
|
|
String.prototype.hashCode = function() {
|
|
|
|
for(var r=0, i=0; i<this.length; i++)
|
|
|
|
r=(r<<5)-r+this.charCodeAt(i),r&=r;
|
|
|
|
return r;
|
|
|
|
};
|
|
|
|
|
2020-10-16 08:19:09 +00:00
|
|
|
// Filter to pretty-print the size of artifacts
|
|
|
|
Vue.filter('iecFileSize', bytes => {
|
|
|
|
const exp = Math.floor(Math.log(bytes) / Math.log(1024));
|
2018-06-01 07:14:59 +00:00
|
|
|
return (bytes / Math.pow(1024, exp)).toFixed(1) + ' ' +
|
|
|
|
['B', 'KiB', 'MiB', 'GiB', 'TiB'][exp];
|
|
|
|
});
|
|
|
|
|
2015-09-26 20:54:27 +00:00
|
|
|
|
2020-10-16 08:19:09 +00:00
|
|
|
// Mixin for periodically updating a progress bar
|
|
|
|
Vue.mixin({
|
|
|
|
data: () => ({ jobsRunning: [] }),
|
2017-07-13 18:57:28 +00:00
|
|
|
methods: {
|
|
|
|
updateProgress(o) {
|
|
|
|
if (o.etc) {
|
2020-10-16 08:19:09 +00:00
|
|
|
const p = (Math.floor(Date.now()/1000) + this.$root.clockSkew - o.started) / (o.etc - o.started);
|
|
|
|
if (p > 1.2)
|
2017-07-13 18:57:28 +00:00
|
|
|
o.overtime = true;
|
2020-10-16 08:19:09 +00:00
|
|
|
o.progress = (p >= 1) ? 99 : 100 * p;
|
2017-07-13 18:57:28 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
},
|
2020-10-16 08:19:09 +00:00
|
|
|
beforeDestroy: () => {
|
2017-07-13 18:57:28 +00:00
|
|
|
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) {
|
2020-11-20 01:08:02 +00:00
|
|
|
// set the current progress update first
|
|
|
|
this.jobsRunning.forEach(this.updateProgress);
|
|
|
|
this.$forceUpdate();
|
|
|
|
// then update with animation every second
|
2017-07-13 18:57:28 +00:00
|
|
|
this.updateTimer = setInterval(() => {
|
|
|
|
this.jobsRunning.forEach(this.updateProgress);
|
|
|
|
this.$forceUpdate();
|
|
|
|
}, 1000);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2020-10-16 08:19:09 +00:00
|
|
|
});
|
2015-09-26 20:54:27 +00:00
|
|
|
|
2020-10-16 08:19:09 +00:00
|
|
|
// 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">
|
|
|
|
<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" />
|
|
|
|
</svg>`
|
|
|
|
: (result == 'failed' || result == 'aborted') ? /* cross */
|
|
|
|
`<svg class="status failed" viewBox="0 0 100 100">
|
|
|
|
<path d="m 19,20 c 2,8 12,29 15,32 -5,5 -18,21 -21,26 2,3 8,15 11,18 4,-6 17,-21
|
|
|
|
21,-26 5,5 11,15 15,20 8,-2 15,-9 20,-15 -3,-3 -17,-18 -20,-24 3,-5 23,-26 30,-33 -3,-5 -8,-9
|
|
|
|
-12,-12 -6,5 -26,26 -29,30 -6,-8 -11,-15 -15,-23 -3,0 -12,5 -15,7 z" />
|
|
|
|
</svg>`
|
|
|
|
: (result == 'queued') ? /* clock */
|
|
|
|
`<svg class="status queued" viewBox="0 0 100 100">
|
|
|
|
<circle r="50" cy="50" cx="50" />
|
|
|
|
<path d="m 50,15 0,35 17,17" stroke-width="10" fill="none" />
|
|
|
|
</svg>`
|
|
|
|
: /* spinner */
|
|
|
|
`<svg class="status running" viewBox="0 0 100 100">
|
|
|
|
<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>
|
|
|
|
</circle>
|
|
|
|
</svg>`,
|
|
|
|
// Pretty-print a unix date
|
|
|
|
formatDate: unix => {
|
|
|
|
// TODO: reimplement when toLocaleDateString() accepts formatting options on most browsers
|
|
|
|
const d = new Date(1000 * unix);
|
|
|
|
let m = d.getMinutes();
|
|
|
|
if (m < 10)
|
|
|
|
m = '0' + m;
|
2021-07-08 22:03:30 +00:00
|
|
|
return d.getHours() + ':' + m + ' on ' +
|
2020-10-16 08:19:09 +00:00
|
|
|
['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'][d.getDay()] + ' ' + d.getDate() + '. ' +
|
2021-07-08 22:03:30 +00:00
|
|
|
['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'][d.getMonth()] + ' ' +
|
2020-10-16 08:19:09 +00:00
|
|
|
d.getFullYear();
|
|
|
|
},
|
|
|
|
// Pretty-print a duration
|
|
|
|
formatDuration: function(start, end) {
|
|
|
|
if(!end)
|
|
|
|
end = Math.floor(Date.now()/1000) + this.$root.clockSkew;
|
|
|
|
if(end - start > 3600)
|
|
|
|
return Math.floor((end-start)/3600) + ' hours, ' + Math.floor(((end-start)%3600)/60) + ' minutes';
|
|
|
|
else if(end - start > 60)
|
|
|
|
return Math.floor((end-start)/60) + ' minutes, ' + ((end-start)%60) + ' seconds';
|
|
|
|
else
|
|
|
|
return (end-start) + ' seconds';
|
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
// 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 {
|
|
|
|
createExecutorUtilizationChart: (id, nBusy, nTotal) => {
|
|
|
|
const c = new Chart(document.getElementById(id), {
|
|
|
|
type: 'pie',
|
|
|
|
data: {
|
|
|
|
labels: [ "Busy", "Idle" ],
|
|
|
|
datasets: [{
|
|
|
|
data: [ nBusy, nTotal - nBusy ],
|
|
|
|
backgroundColor: [ "#afa674", "#7483af" ]
|
|
|
|
}]
|
|
|
|
},
|
|
|
|
options: {
|
2022-10-04 21:20:23 +00:00
|
|
|
hover: { mode: null },
|
|
|
|
aspectRatio: 2
|
2020-10-16 08:19:09 +00:00
|
|
|
}
|
|
|
|
});
|
|
|
|
c.executorBusyChanged = busy => {
|
|
|
|
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();
|
|
|
|
for (var i = 6; i >= 0; --i) {
|
|
|
|
var then = new Date(now.getTime() - i * 86400000);
|
|
|
|
res.push({
|
|
|
|
short: ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"][then.getDay()],
|
|
|
|
long: then.toLocaleDateString()}
|
|
|
|
);
|
|
|
|
}
|
|
|
|
return res;
|
|
|
|
})();
|
|
|
|
const c = new Chart(document.getElementById(id), {
|
|
|
|
type: 'line',
|
|
|
|
data: {
|
|
|
|
labels: dayNames.map(e => e.short),
|
|
|
|
datasets: [{
|
|
|
|
label: 'Failed Builds',
|
|
|
|
backgroundColor: "#883d3d",
|
2022-09-23 18:03:49 +00:00
|
|
|
data: data.map(e => e.failed || 0),
|
|
|
|
fill: true,
|
2022-10-04 21:20:23 +00:00
|
|
|
tension: 0.35,
|
2020-10-16 08:19:09 +00:00
|
|
|
},{
|
|
|
|
label: 'Successful Builds',
|
|
|
|
backgroundColor: "#74af77",
|
2022-09-23 18:03:49 +00:00
|
|
|
data: data.map(e => e.success || 0),
|
|
|
|
fill: true,
|
2022-10-04 21:20:23 +00:00
|
|
|
tension: 0.35,
|
2020-10-16 08:19:09 +00:00
|
|
|
}]
|
|
|
|
},
|
|
|
|
options:{
|
2022-09-23 18:03:49 +00:00
|
|
|
plugins: {
|
|
|
|
title: { display: true, text: 'Runs per day' },
|
|
|
|
tooltip:{callbacks:{title: (tip) => dayNames[tip[0].dataIndex].long}},
|
|
|
|
},
|
|
|
|
scales: {
|
|
|
|
y: {
|
|
|
|
ticks:{callback: (label, index, labels) => Number.isInteger(label) ? label: null},
|
|
|
|
stacked: true
|
|
|
|
},
|
|
|
|
},
|
2020-10-16 08:19:09 +00:00
|
|
|
}
|
|
|
|
});
|
|
|
|
c.jobCompleted = success => {
|
2020-11-13 01:00:59 +00:00
|
|
|
c.data.datasets[success ? 1 : 0].data[6]++;
|
2020-10-16 08:19:09 +00:00
|
|
|
c.update();
|
|
|
|
}
|
|
|
|
return c;
|
|
|
|
},
|
|
|
|
createRunsPerJobChart: (id, data) => {
|
|
|
|
const c = new Chart(document.getElementById("chartBpj"), {
|
2022-09-23 18:03:49 +00:00
|
|
|
type: 'bar',
|
2020-10-16 08:19:09 +00:00
|
|
|
data: {
|
|
|
|
labels: Object.keys(data),
|
|
|
|
datasets: [{
|
|
|
|
label: 'Runs in last 24 hours',
|
|
|
|
backgroundColor: "#7483af",
|
|
|
|
data: Object.keys(data).map(e => data[e])
|
|
|
|
}]
|
|
|
|
},
|
|
|
|
options:{
|
2022-09-23 18:03:49 +00:00
|
|
|
indexAxis: 'y',
|
|
|
|
plugins: {
|
|
|
|
title: { display: true, text: 'Runs per job' },
|
|
|
|
},
|
2020-10-16 08:19:09 +00:00
|
|
|
hover: { mode: null },
|
2022-09-23 18:03:49 +00:00
|
|
|
scales: {
|
|
|
|
x: {
|
|
|
|
ticks:{callback: (label, index, labels)=> Number.isInteger(label) ? label: null}
|
|
|
|
}
|
|
|
|
}
|
2020-10-16 08:19:09 +00:00
|
|
|
}
|
|
|
|
});
|
|
|
|
c.jobCompleted = name => {
|
|
|
|
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();
|
2021-07-08 22:03:30 +00:00
|
|
|
return;
|
2020-10-16 08:19:09 +00:00
|
|
|
}
|
|
|
|
}
|
2021-07-08 22:03:30 +00:00
|
|
|
// if we get here, it's a new/unknown job
|
|
|
|
c.data.labels.push(name);
|
|
|
|
c.data.datasets[0].data.push(1);
|
|
|
|
c.update();
|
2020-10-16 08:19:09 +00:00
|
|
|
}
|
|
|
|
return c;
|
|
|
|
},
|
2021-07-08 22:03:30 +00:00
|
|
|
createTimePerJobChart: (id, data, completedCounts) => {
|
2020-10-16 08:19:09 +00:00
|
|
|
const scale = timeScale(Math.max(...Object.values(data)));
|
2021-07-08 22:03:30 +00:00
|
|
|
const c = new Chart(document.getElementById(id), {
|
2022-09-23 18:03:49 +00:00
|
|
|
type: 'bar',
|
2020-10-16 08:19:09 +00:00
|
|
|
data: {
|
|
|
|
labels: Object.keys(data),
|
|
|
|
datasets: [{
|
|
|
|
label: 'Mean run time this week',
|
|
|
|
backgroundColor: "#7483af",
|
|
|
|
data: Object.keys(data).map(e => data[e] * scale.factor)
|
|
|
|
}]
|
|
|
|
},
|
|
|
|
options:{
|
2022-09-23 18:03:49 +00:00
|
|
|
indexAxis: 'y',
|
|
|
|
plugins: {
|
|
|
|
title: { display: true, text: 'Mean run time this week' },
|
|
|
|
tooltip:{callbacks:{
|
|
|
|
label: (tip) => tip.dataset.label + ': ' + tip.raw.toFixed(2) + ' ' + scale.label.toLowerCase()
|
|
|
|
}}
|
|
|
|
},
|
2020-10-16 08:19:09 +00:00
|
|
|
hover: { mode: null },
|
2022-09-23 18:03:49 +00:00
|
|
|
scales: {
|
|
|
|
x:{
|
|
|
|
ticks: {callback: scale.ticks},
|
|
|
|
title: {
|
|
|
|
display: true,
|
|
|
|
text: scale.label
|
|
|
|
}
|
2020-10-16 08:19:09 +00:00
|
|
|
}
|
2022-09-23 18:03:49 +00:00
|
|
|
},
|
2020-10-16 08:19:09 +00:00
|
|
|
}
|
|
|
|
});
|
2021-07-08 22:03:30 +00:00
|
|
|
c.jobCompleted = (name, time) => {
|
|
|
|
for (var j = 0; j < c.data.datasets[0].data.length; ++j) {
|
|
|
|
if (c.data.labels[j] == name) {
|
|
|
|
c.data.datasets[0].data[j] = ((completedCounts[name]-1) * c.data.datasets[0].data[j] + time * scale.factor) / completedCounts[name];
|
|
|
|
c.update();
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// if we get here, it's a new/unknown job
|
|
|
|
c.data.labels.push(name);
|
|
|
|
c.data.datasets[0].data.push(time * scale.factor);
|
|
|
|
c.update();
|
|
|
|
};
|
|
|
|
return c;
|
2020-10-16 08:19:09 +00:00
|
|
|
},
|
|
|
|
createRunTimeChangesChart: (id, data) => {
|
|
|
|
const scale = timeScale(Math.max(...data.map(e => Math.max(...e.durations))));
|
2021-07-08 22:03:30 +00:00
|
|
|
const dataValue = (name, durations) => ({
|
|
|
|
label: name,
|
|
|
|
data: durations.map(x => x * scale.factor),
|
|
|
|
borderColor: 'hsl('+(name.hashCode() % 360)+', 27%, 57%)',
|
2022-10-04 21:20:23 +00:00
|
|
|
backgroundColor: 'transparent',
|
|
|
|
tension: 0.35,
|
2021-07-08 22:03:30 +00:00
|
|
|
});
|
|
|
|
const c = new Chart(document.getElementById(id), {
|
2020-10-16 08:19:09 +00:00
|
|
|
type: 'line',
|
|
|
|
data: {
|
|
|
|
labels: [...Array(10).keys()],
|
2021-07-08 22:03:30 +00:00
|
|
|
datasets: data.map(e => dataValue(e.name, e.durations))
|
2020-10-16 08:19:09 +00:00
|
|
|
},
|
|
|
|
options:{
|
2022-09-23 18:03:49 +00:00
|
|
|
plugins: {
|
|
|
|
legend: { display: true, position: 'bottom' },
|
|
|
|
title: { display: true, text: 'Run time changes' },
|
|
|
|
tooltip: { enabled: false },
|
|
|
|
},
|
2020-10-16 08:19:09 +00:00
|
|
|
scales:{
|
2022-09-23 18:03:49 +00:00
|
|
|
x: {ticks: {display: false}},
|
|
|
|
y: {
|
|
|
|
ticks: {callback: scale.ticks},
|
|
|
|
title: {
|
2020-10-16 08:19:09 +00:00
|
|
|
display: true,
|
2022-09-23 18:03:49 +00:00
|
|
|
text: scale.label
|
2020-10-16 08:19:09 +00:00
|
|
|
}
|
2022-09-23 18:03:49 +00:00
|
|
|
}
|
2020-10-16 08:19:09 +00:00
|
|
|
},
|
|
|
|
}
|
|
|
|
});
|
2021-07-08 22:03:30 +00:00
|
|
|
c.jobCompleted = (name, time) => {
|
|
|
|
for (var j = 0; j < c.data.datasets.length; ++j) {
|
|
|
|
if (c.data.datasets[j].label == name) {
|
|
|
|
if(c.data.datasets[j].data.length == 10)
|
|
|
|
c.data.datasets[j].data.shift();
|
|
|
|
c.data.datasets[j].data.push(time * scale.factor);
|
|
|
|
c.update();
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// if we get here, it's a new/unknown job
|
|
|
|
c.data.datasets.push(dataValue(name, [time]));
|
|
|
|
c.update();
|
|
|
|
};
|
|
|
|
return c;
|
2020-10-16 08:19:09 +00:00
|
|
|
},
|
|
|
|
createRunTimeChart: (id, jobs, avg) => {
|
|
|
|
const scale = timeScale(Math.max(...jobs.map(v=>v.completed-v.started)));
|
2021-07-08 22:03:30 +00:00
|
|
|
const c = new Chart(document.getElementById(id), {
|
2020-10-16 08:19:09 +00:00
|
|
|
type: 'bar',
|
|
|
|
data: {
|
|
|
|
labels: jobs.map(e => '#' + e.number).reverse(),
|
|
|
|
datasets: [{
|
|
|
|
label: 'Build time',
|
|
|
|
backgroundColor: jobs.map(e => e.result == 'success' ? '#74af77': '#883d3d').reverse(),
|
2022-09-23 18:03:49 +00:00
|
|
|
barPercentage: 1.0,
|
|
|
|
categoryPercentage: 0.95,
|
2020-10-16 08:19:09 +00:00
|
|
|
data: jobs.map(e => (e.completed - e.started) * scale.factor).reverse()
|
|
|
|
}]
|
|
|
|
},
|
|
|
|
options: {
|
2022-09-23 18:03:49 +00:00
|
|
|
plugins: {
|
|
|
|
title: { display: true, text: 'Build time' },
|
|
|
|
tooltip: {
|
|
|
|
callbacks:{
|
|
|
|
label: (tip) => scale.ticks(tip.raw) + ' ' + scale.label.toLowerCase()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
},
|
2020-10-16 08:19:09 +00:00
|
|
|
hover: { mode: null },
|
|
|
|
scales:{
|
2022-09-23 18:03:49 +00:00
|
|
|
x: {
|
|
|
|
grid: {
|
2020-10-16 08:19:09 +00:00
|
|
|
display: false,
|
|
|
|
drawBorder: false
|
|
|
|
}
|
2022-09-23 18:03:49 +00:00
|
|
|
},
|
|
|
|
y: {
|
2022-10-26 07:12:23 +00:00
|
|
|
suggestedMax: avg * scale.factor,
|
|
|
|
ticks: {callback: scale.ticks },
|
2022-09-23 18:03:49 +00:00
|
|
|
title: {display: true, text: scale.label}
|
|
|
|
}
|
2020-10-16 08:19:09 +00:00
|
|
|
},
|
2022-10-26 07:12:23 +00:00
|
|
|
},
|
|
|
|
plugins: [{
|
|
|
|
afterDraw: (chart, args, options) => {
|
|
|
|
const {ctx, avg, chartArea, scales:{y:yaxis}} = chart;
|
|
|
|
const y = chartArea.top + yaxis.height - avg * scale.factor * yaxis.height / yaxis.end;
|
|
|
|
ctx.save();
|
|
|
|
ctx.beginPath();
|
|
|
|
ctx.translate(chartArea.left, y);
|
|
|
|
ctx.moveTo(0,0);
|
|
|
|
ctx.lineTo(chartArea.width, 0);
|
|
|
|
ctx.lineWidth = 2;
|
|
|
|
ctx.strokeStyle = '#7483af';
|
|
|
|
ctx.stroke();
|
|
|
|
ctx.restore();
|
|
|
|
}
|
|
|
|
}]
|
2020-10-16 08:19:09 +00:00
|
|
|
});
|
2022-10-26 07:12:23 +00:00
|
|
|
c.avg = avg;
|
2021-07-08 22:03:30 +00:00
|
|
|
c.jobCompleted = (num, result, time) => {
|
2022-10-26 07:12:23 +00:00
|
|
|
c.avg = ((c.avg * (num - 1)) + time) / num;
|
|
|
|
c.options.scales.y.suggestedMax = avg * scale.factor;
|
|
|
|
if(c.data.datasets[0].data.length == 20) {
|
2021-07-08 22:03:30 +00:00
|
|
|
c.data.labels.shift();
|
2022-10-26 07:12:23 +00:00
|
|
|
c.data.datasets[0].data.shift();
|
|
|
|
c.data.datasets[0].backgroundColor.shift();
|
2021-07-08 22:03:30 +00:00
|
|
|
}
|
|
|
|
c.data.labels.push('#' + num);
|
2022-10-26 07:12:23 +00:00
|
|
|
c.data.datasets[0].data.push(time * scale.factor);
|
|
|
|
c.data.datasets[0].backgroundColor.push(result == 'success' ? '#74af77': '#883d3d');
|
2021-07-08 22:03:30 +00:00
|
|
|
c.update();
|
|
|
|
};
|
|
|
|
return c;
|
2020-10-16 08:19:09 +00:00
|
|
|
}
|
|
|
|
};
|
|
|
|
})();
|
|
|
|
|
|
|
|
// For all charts, set miniumum Y to 0
|
2022-10-04 21:20:23 +00:00
|
|
|
Chart.defaults.scales.linear.suggestedMin = 0;
|
2020-10-16 08:19:09 +00:00
|
|
|
// Don't display legend by default
|
2022-09-23 18:03:49 +00:00
|
|
|
Chart.defaults.plugins.legend.display = false;
|
2020-10-16 08:19:09 +00:00
|
|
|
// Disable tooltip hover animations
|
2022-09-23 18:03:49 +00:00
|
|
|
Chart.defaults.plugins.tooltip.animation = false;
|
2020-10-16 08:19:09 +00:00
|
|
|
|
|
|
|
// Component for the / endpoint
|
|
|
|
const Home = templateId => {
|
|
|
|
const state = {
|
2017-07-13 18:57:28 +00:00
|
|
|
jobsQueued: [],
|
2018-09-08 15:16:23 +00:00
|
|
|
jobsRecent: [],
|
2020-06-26 22:45:47 +00:00
|
|
|
resultChanged: [],
|
|
|
|
lowPassRates: [],
|
2017-07-13 18:57:28 +00:00
|
|
|
};
|
2020-10-16 08:19:09 +00:00
|
|
|
let chtUtilization, chtBuildsPerDay, chtBuildsPerJob, chtTimePerJob;
|
2021-07-08 22:03:30 +00:00
|
|
|
let completedCounts;
|
2017-07-13 18:57:28 +00:00
|
|
|
return {
|
2020-10-16 08:19:09 +00:00
|
|
|
template: templateId,
|
|
|
|
data: () => state,
|
2017-07-13 18:57:28 +00:00
|
|
|
methods: {
|
|
|
|
status: function(msg) {
|
2021-12-05 00:41:05 +00:00
|
|
|
state.jobsQueued = msg.queued.reverse();
|
|
|
|
state.jobsRunning = msg.running.reverse();
|
2017-07-13 18:57:28 +00:00
|
|
|
state.jobsRecent = msg.recent;
|
2018-09-08 15:16:23 +00:00
|
|
|
state.resultChanged = msg.resultChanged;
|
2020-06-26 22:45:47 +00:00
|
|
|
state.lowPassRates = msg.lowPassRates;
|
2021-07-08 22:03:30 +00:00
|
|
|
completedCounts = msg.completedCounts;
|
2017-07-13 18:57:28 +00:00
|
|
|
this.$forceUpdate();
|
2015-09-26 20:54:27 +00:00
|
|
|
|
2020-11-20 01:34:26 +00:00
|
|
|
// defer charts to nextTick because they get DOM elements which aren't rendered yet
|
|
|
|
this.$nextTick(() => {
|
|
|
|
chtUtilization = Charts.createExecutorUtilizationChart("chartUtil", msg.executorsBusy, msg.executorsTotal);
|
|
|
|
chtBuildsPerDay = Charts.createRunsPerDayChart("chartBpd", msg.buildsPerDay);
|
|
|
|
chtBuildsPerJob = Charts.createRunsPerJobChart("chartBpj", msg.buildsPerJob);
|
2021-07-08 22:03:30 +00:00
|
|
|
chtTimePerJob = Charts.createTimePerJobChart("chartTpj", msg.timePerJob, completedCounts);
|
2020-11-20 01:34:26 +00:00
|
|
|
chtBuildTimeChanges = Charts.createRunTimeChangesChart("chartBuildTimeChanges", msg.buildTimeChanges);
|
|
|
|
});
|
2017-07-13 18:57:28 +00:00
|
|
|
},
|
|
|
|
job_queued: function(data) {
|
2022-01-21 20:07:08 +00:00
|
|
|
state.jobsQueued.splice(state.jobsQueued.length - data.queueIndex, 0, data);
|
2017-07-13 18:57:28 +00:00
|
|
|
this.$forceUpdate();
|
|
|
|
},
|
|
|
|
job_started: function(data) {
|
|
|
|
state.jobsQueued.splice(state.jobsQueued.length - data.queueIndex - 1, 1);
|
|
|
|
state.jobsRunning.splice(0, 0, data);
|
|
|
|
this.$forceUpdate();
|
2020-10-16 08:19:09 +00:00
|
|
|
chtUtilization.executorBusyChanged(true);
|
2017-07-13 18:57:28 +00:00
|
|
|
},
|
|
|
|
job_completed: function(data) {
|
2021-07-08 22:03:30 +00:00
|
|
|
if(!(job.name in completedCounts))
|
|
|
|
completedCounts[job.name] = 0;
|
|
|
|
for(let i = 0; i < state.jobsRunning.length; ++i) {
|
|
|
|
const job = state.jobsRunning[i];
|
2017-07-13 18:57:28 +00:00
|
|
|
if (job.name == data.name && job.number == data.number) {
|
|
|
|
state.jobsRunning.splice(i, 1);
|
|
|
|
state.jobsRecent.splice(0, 0, data);
|
|
|
|
this.$forceUpdate();
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
2021-07-08 22:03:30 +00:00
|
|
|
for(let i = 0; i < state.resultChanged.length; ++i) {
|
|
|
|
const job = state.resultChanged[i];
|
|
|
|
if(job.name == data.name) {
|
|
|
|
job[data.result === 'success' ? 'lastSuccess' : 'lastFailure'] = data.number;
|
|
|
|
this.$forceUpdate();
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
for(let i = 0; i < state.lowPassRates.length; ++i) {
|
|
|
|
const job = state.lowPassRates[i];
|
|
|
|
if(job.name == data.name) {
|
|
|
|
job.passRate = ((completedCounts[job.name] - 1) * job.passRate + (data.result === 'success' ? 1 : 0)) / completedCounts[job.name];
|
|
|
|
this.$forceUpdate();
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
completedCounts[job.name]++;
|
2020-10-16 08:19:09 +00:00
|
|
|
chtBuildsPerDay.jobCompleted(data.result === 'success')
|
|
|
|
chtUtilization.executorBusyChanged(false);
|
2021-07-08 22:03:30 +00:00
|
|
|
chtBuildsPerJob.jobCompleted(data.name);
|
|
|
|
chtTimePerJob.jobCompleted(data.name, data.completed - data.started);
|
|
|
|
chtBuildTimeChanges.jobCompleted(data.name, data.completed - data.started);
|
2017-07-13 18:57:28 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
2020-10-16 08:19:09 +00:00
|
|
|
};
|
2017-07-13 18:57:28 +00:00
|
|
|
|
2020-10-16 08:19:09 +00:00
|
|
|
// Component for the /jobs and /wallboard endpoints
|
|
|
|
const All = templateId => {
|
|
|
|
const state = {
|
2017-07-13 18:57:28 +00:00
|
|
|
jobs: [],
|
|
|
|
search: '',
|
2019-12-13 08:42:22 +00:00
|
|
|
groups: {},
|
2020-10-09 00:06:03 +00:00
|
|
|
regexps: {},
|
2019-12-13 08:42:22 +00:00
|
|
|
group: null,
|
|
|
|
ungrouped: []
|
2017-07-13 18:57:28 +00:00
|
|
|
};
|
|
|
|
return {
|
2020-10-09 00:06:03 +00:00
|
|
|
template: templateId,
|
2020-10-16 08:19:09 +00:00
|
|
|
data: () => state,
|
2017-07-13 18:57:28 +00:00
|
|
|
methods: {
|
|
|
|
status: function(msg) {
|
|
|
|
state.jobs = msg.jobs;
|
2017-11-07 06:21:01 +00:00
|
|
|
state.jobsRunning = msg.running;
|
|
|
|
// mix running and completed jobs
|
2020-10-16 08:19:09 +00:00
|
|
|
msg.running.forEach(job => {
|
2022-09-03 07:54:08 +00:00
|
|
|
job.result = 'running';
|
2020-10-16 08:19:09 +00:00
|
|
|
const idx = state.jobs.findIndex(j => j.name === job.name);
|
2017-11-07 06:21:01 +00:00
|
|
|
if (idx > -1)
|
2020-10-16 08:19:09 +00:00
|
|
|
state.jobs[idx] = job;
|
2017-12-02 15:55:21 +00:00
|
|
|
else {
|
|
|
|
// special case: first run of a job.
|
2022-09-03 07:51:29 +00:00
|
|
|
state.jobs.unshift(job);
|
2020-10-16 08:19:09 +00:00
|
|
|
state.jobs.sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
|
2017-12-02 15:55:21 +00:00
|
|
|
}
|
2020-10-16 08:19:09 +00:00
|
|
|
});
|
2019-12-13 08:42:22 +00:00
|
|
|
state.groups = {};
|
2020-10-09 00:06:03 +00:00
|
|
|
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);
|
2019-12-13 08:42:22 +00:00
|
|
|
state.group = state.ungrouped.length ? null : Object.keys(state.groups)[0];
|
2017-07-13 18:57:28 +00:00
|
|
|
},
|
2017-11-07 06:21:01 +00:00
|
|
|
job_started: function(data) {
|
2020-10-09 00:06:03 +00:00
|
|
|
data.result = 'running'; // for wallboard css
|
2018-08-24 10:10:00 +00:00
|
|
|
// jobsRunning must be maintained for ProgressUpdater
|
2020-10-16 08:19:09 +00:00
|
|
|
let updAt = state.jobsRunning.findIndex(j => j.name === data.name);
|
|
|
|
if (updAt === -1) {
|
2017-11-07 06:21:01 +00:00
|
|
|
state.jobsRunning.unshift(data);
|
|
|
|
} else {
|
|
|
|
state.jobsRunning[updAt] = data;
|
|
|
|
}
|
2020-10-16 08:19:09 +00:00
|
|
|
updAt = state.jobs.findIndex(j => j.name === data.name);
|
|
|
|
if (updAt === -1) {
|
2017-12-02 15:55:21 +00:00
|
|
|
// first execution of new job. TODO insert without resort
|
|
|
|
state.jobs.unshift(data);
|
2020-10-16 08:19:09 +00:00
|
|
|
state.jobs.sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
|
2020-10-09 00:06:03 +00:00
|
|
|
if(!Object.values(state.regexps).some(r => r.test(data.name)))
|
2019-12-13 08:42:22 +00:00
|
|
|
state.ungrouped.push(data.name);
|
2017-12-02 15:55:21 +00:00
|
|
|
} else {
|
|
|
|
state.jobs[updAt] = data;
|
|
|
|
}
|
|
|
|
this.$forceUpdate();
|
2017-11-07 06:21:01 +00:00
|
|
|
},
|
2017-07-13 18:57:28 +00:00
|
|
|
job_completed: function(data) {
|
2020-10-16 08:19:09 +00:00
|
|
|
let updAt = state.jobs.findIndex(j => j.name === data.name);
|
|
|
|
if (updAt > -1)
|
|
|
|
state.jobs[updAt] = data;
|
|
|
|
updAt = state.jobsRunning.findIndex(j => j.name === data.name);
|
|
|
|
if (updAt > -1) {
|
|
|
|
state.jobsRunning.splice(updAt, 1);
|
|
|
|
this.$forceUpdate();
|
2017-07-13 18:57:28 +00:00
|
|
|
}
|
2018-08-24 10:10:00 +00:00
|
|
|
},
|
|
|
|
filteredJobs: function() {
|
2019-12-13 08:42:22 +00:00
|
|
|
let ret = [];
|
|
|
|
if (state.group)
|
2020-10-09 00:06:03 +00:00
|
|
|
ret = state.jobs.filter(job => state.regexps[state.group].test(job.name));
|
2019-12-13 08:42:22 +00:00
|
|
|
else
|
|
|
|
ret = state.jobs.filter(job => state.ungrouped.includes(job.name));
|
|
|
|
if (this.search)
|
|
|
|
ret = ret.filter(job => job.name.indexOf(this.search) > -1);
|
2018-08-24 10:10:00 +00:00
|
|
|
return ret;
|
|
|
|
},
|
2020-10-09 00:06:03 +00:00
|
|
|
wallboardJobs: function() {
|
|
|
|
let ret = [];
|
|
|
|
const expr = (new URLSearchParams(window.location.search)).get('filter');
|
|
|
|
if (expr)
|
|
|
|
ret = state.jobs.filter(job => (new RegExp(expr)).test(job.name));
|
|
|
|
else
|
2022-09-03 07:53:14 +00:00
|
|
|
ret = [...state.jobs];
|
2020-10-09 00:06:03 +00:00
|
|
|
// sort failed before success, newest first
|
|
|
|
ret.sort((a,b) => a.result == b.result ? a.started - b.started : 2*(b.result == 'success')-1);
|
|
|
|
return ret;
|
|
|
|
},
|
|
|
|
wallboardLink: function() {
|
2021-03-17 08:21:16 +00:00
|
|
|
return 'wallboard' + (state.group ? '?filter=' + state.groups[state.group] : '');
|
2020-10-09 00:06:03 +00:00
|
|
|
}
|
2017-07-13 18:57:28 +00:00
|
|
|
}
|
|
|
|
};
|
2020-10-09 00:06:03 +00:00
|
|
|
};
|
2017-07-13 18:57:28 +00:00
|
|
|
|
2020-10-16 08:19:09 +00:00
|
|
|
// Component for the /job/:name endpoint
|
|
|
|
const Job = templateId => {
|
|
|
|
const state = {
|
2019-12-24 20:10:16 +00:00
|
|
|
description: '',
|
2021-12-05 00:41:05 +00:00
|
|
|
jobsQueued: [],
|
2017-07-13 18:57:28 +00:00
|
|
|
jobsRunning: [],
|
|
|
|
jobsRecent: [],
|
|
|
|
lastSuccess: null,
|
|
|
|
lastFailed: null,
|
2018-06-01 11:51:34 +00:00
|
|
|
pages: 0,
|
2018-08-24 09:15:40 +00:00
|
|
|
sort: {}
|
2017-07-13 18:57:28 +00:00
|
|
|
};
|
2021-07-08 22:03:30 +00:00
|
|
|
let chtBuildTime = null;
|
2020-10-16 08:19:09 +00:00
|
|
|
return {
|
|
|
|
template: templateId,
|
2021-01-02 08:26:13 +00:00
|
|
|
props: ['route'],
|
2020-10-16 08:19:09 +00:00
|
|
|
data: () => state,
|
2017-07-13 18:57:28 +00:00
|
|
|
methods: {
|
|
|
|
status: function(msg) {
|
2019-12-24 20:10:16 +00:00
|
|
|
state.description = msg.description;
|
2021-12-05 00:41:05 +00:00
|
|
|
state.jobsQueued = msg.queued.reverse();
|
|
|
|
state.jobsRunning = msg.running.reverse();
|
2017-07-13 18:57:28 +00:00
|
|
|
state.jobsRecent = msg.recent;
|
|
|
|
state.lastSuccess = msg.lastSuccess;
|
|
|
|
state.lastFailed = msg.lastFailed;
|
2018-06-01 11:51:34 +00:00
|
|
|
state.pages = msg.pages;
|
2018-08-24 09:15:40 +00:00
|
|
|
state.sort = msg.sort;
|
2017-07-13 18:57:28 +00:00
|
|
|
|
2018-08-24 10:32:07 +00:00
|
|
|
// "status" comes again if we change page/sorting. Delete the
|
|
|
|
// old chart and recreate it to prevent flickering of old data
|
2021-07-08 22:03:30 +00:00
|
|
|
if(chtBuildTime)
|
|
|
|
chtBuildTime.destroy();
|
2020-11-20 01:34:26 +00:00
|
|
|
|
|
|
|
// defer chart to nextTick because they get DOM elements which aren't rendered yet
|
|
|
|
this.$nextTick(() => {
|
2021-07-08 22:03:30 +00:00
|
|
|
chtBuildTime = Charts.createRunTimeChart("chartBt", msg.recent, msg.averageRuntime);
|
2020-11-20 01:34:26 +00:00
|
|
|
});
|
2017-07-13 18:57:28 +00:00
|
|
|
},
|
2021-12-05 00:41:05 +00:00
|
|
|
job_queued: function(data) {
|
2022-01-21 20:07:08 +00:00
|
|
|
state.jobsQueued.splice(state.jobsQueued.length - data.queueIndex, 0, data);
|
2021-12-05 00:41:05 +00:00
|
|
|
this.$forceUpdate();
|
2017-07-13 18:57:28 +00:00
|
|
|
},
|
|
|
|
job_started: function(data) {
|
2021-12-05 00:41:05 +00:00
|
|
|
state.jobsQueued.splice(state.jobsQueued.length - data.queueIndex - 1, 1);
|
2017-07-13 18:57:28 +00:00
|
|
|
state.jobsRunning.splice(0, 0, data);
|
|
|
|
this.$forceUpdate();
|
|
|
|
},
|
|
|
|
job_completed: function(data) {
|
2020-10-16 08:19:09 +00:00
|
|
|
const i = state.jobsRunning.findIndex(j => j.number === data.number);
|
|
|
|
if (i > -1) {
|
2017-07-13 18:57:28 +00:00
|
|
|
state.jobsRunning.splice(i, 1);
|
|
|
|
state.jobsRecent.splice(0, 0, data);
|
|
|
|
this.$forceUpdate();
|
|
|
|
}
|
2021-07-08 22:03:30 +00:00
|
|
|
chtBuildTime.jobCompleted(data.number, data.result, data.completed - data.started);
|
2018-06-01 11:51:34 +00:00
|
|
|
},
|
|
|
|
page_next: function() {
|
2018-08-24 09:15:40 +00:00
|
|
|
state.sort.page++;
|
2019-10-05 17:06:35 +00:00
|
|
|
this.query(state.sort)
|
2018-06-01 11:51:34 +00:00
|
|
|
},
|
|
|
|
page_prev: function() {
|
2018-08-24 09:15:40 +00:00
|
|
|
state.sort.page--;
|
2019-10-05 17:06:35 +00:00
|
|
|
this.query(state.sort)
|
2018-08-24 09:15:40 +00:00
|
|
|
},
|
|
|
|
do_sort: function(field) {
|
|
|
|
if(state.sort.field == field) {
|
|
|
|
state.sort.order = state.sort.order == 'asc' ? 'dsc' : 'asc';
|
|
|
|
} else {
|
|
|
|
state.sort.order = 'dsc';
|
|
|
|
state.sort.field = field;
|
|
|
|
}
|
2019-10-05 17:06:35 +00:00
|
|
|
this.query(state.sort)
|
2021-01-02 08:26:13 +00:00
|
|
|
},
|
|
|
|
query: function(q) {
|
|
|
|
this.$root.$emit('navigate', q);
|
2017-07-13 18:57:28 +00:00
|
|
|
}
|
|
|
|
}
|
2020-10-16 08:19:09 +00:00
|
|
|
};
|
|
|
|
};
|
2017-07-13 18:57:28 +00:00
|
|
|
|
2020-10-16 08:19:09 +00:00
|
|
|
// Component for the /job/:name/:number endpoint
|
|
|
|
const Run = templateId => {
|
2019-02-18 20:06:37 +00:00
|
|
|
const utf8decoder = new TextDecoder('utf-8');
|
2021-01-09 08:51:12 +00:00
|
|
|
const ansi_up = new AnsiUp;
|
2021-06-11 03:41:16 +00:00
|
|
|
ansi_up.use_classes = true;
|
2020-10-16 08:19:09 +00:00
|
|
|
const state = {
|
2018-09-30 06:04:17 +00:00
|
|
|
job: { artifacts: [], upstream: {} },
|
2017-07-13 18:57:28 +00:00
|
|
|
latestNum: null,
|
2022-01-01 08:30:38 +00:00
|
|
|
logComplete: false,
|
2017-07-13 18:57:28 +00:00
|
|
|
};
|
2019-02-15 17:05:44 +00:00
|
|
|
const logFetcher = (vm, name, num) => {
|
|
|
|
const abort = new AbortController();
|
2019-03-29 19:43:16 +00:00
|
|
|
fetch('log/'+name+'/'+num, {signal:abort.signal}).then(res => {
|
2019-02-18 20:06:37 +00:00
|
|
|
// ATOW pipeThrough not supported in Firefox
|
|
|
|
//const reader = res.body.pipeThrough(new TextDecoderStream).getReader();
|
|
|
|
const reader = res.body.getReader();
|
2022-01-01 08:30:38 +00:00
|
|
|
const target = document.getElementsByTagName('code')[0];
|
|
|
|
let logToRender = '';
|
|
|
|
let logComplete = false;
|
|
|
|
let tid = null;
|
2022-01-22 07:07:23 +00:00
|
|
|
let lastUiUpdate = 0;
|
2022-01-01 08:30:38 +00:00
|
|
|
|
|
|
|
function updateUI() {
|
|
|
|
// output may contain private ANSI CSI escape sequence to point to
|
|
|
|
// downstream jobs. ansi_up (correctly) discards unknown sequences,
|
|
|
|
// so they must be matched before passing through ansi_up. ansi_up
|
|
|
|
// also (correctly) escapes HTML, so they need to be converted back
|
|
|
|
// to links after going through ansi_up.
|
|
|
|
// A better solution one day would be if ansi_up were to provide
|
|
|
|
// a callback interface for handling unknown sequences.
|
|
|
|
// Also, update the DOM directly rather than using a binding through
|
|
|
|
// Vue, the performance is noticeably better with large logs.
|
2022-01-22 07:08:23 +00:00
|
|
|
target.insertAdjacentHTML('beforeend', ansi_up.ansi_to_html(
|
2022-01-01 08:30:38 +00:00
|
|
|
logToRender.replace(/\033\[\{([^:]+):(\d+)\033\\/g, (m, $1, $2) =>
|
|
|
|
'~~~~LAMINAR_RUN~'+$1+':'+$2+'~'
|
|
|
|
)
|
|
|
|
).replace(/~~~~LAMINAR_RUN~([^:]+):(\d+)~/g, (m, $1, $2) =>
|
|
|
|
'<a href="jobs/'+$1+'" onclick="return LaminarApp.navigate(this.href);">'+$1+'</a>:'+
|
|
|
|
'<a href="jobs/'+$1+'/'+$2+'" onclick="return LaminarApp.navigate(this.href);">#'+$2+'</a>'
|
2022-01-22 07:08:23 +00:00
|
|
|
));
|
2022-01-01 08:30:38 +00:00
|
|
|
logToRender = '';
|
|
|
|
if (logComplete) {
|
|
|
|
// output finished
|
|
|
|
state.logComplete = true;
|
|
|
|
}
|
2022-01-22 07:07:23 +00:00
|
|
|
|
|
|
|
lastUiUpdate = Date.now();
|
|
|
|
tid = null;
|
2022-01-01 08:30:38 +00:00
|
|
|
}
|
|
|
|
|
2019-02-15 17:05:44 +00:00
|
|
|
return function pump() {
|
|
|
|
return reader.read().then(({done, value}) => {
|
2022-01-01 08:30:38 +00:00
|
|
|
if (done) {
|
|
|
|
// do not set state.logComplete directly, because rendering
|
|
|
|
// may take some time, and we don't want the progress indicator
|
|
|
|
// to disappear before rendering is complete. Instead, delay
|
2022-01-22 07:07:23 +00:00
|
|
|
// it until after the entire log has been rendered
|
2022-01-01 08:30:38 +00:00
|
|
|
logComplete = true;
|
2022-01-22 07:07:23 +00:00
|
|
|
// if no render update is pending, schedule one immediately
|
|
|
|
// (do not use the delayed buffering mechanism from below), so
|
|
|
|
// that for the common case of short logs, the loading spinner
|
|
|
|
// disappears immediately as the log is rendered
|
|
|
|
if(tid === null)
|
|
|
|
setTimeout(updateUI, 0);
|
2019-02-15 17:05:44 +00:00
|
|
|
return;
|
2022-01-01 08:30:38 +00:00
|
|
|
}
|
|
|
|
// sometimes logs can be very large, and we are calling pump()
|
|
|
|
// furiously to get all the data to the client. To prevent straining
|
|
|
|
// the client renderer, buffer the data and delay the UI updates.
|
2022-01-22 07:07:23 +00:00
|
|
|
logToRender += utf8decoder.decode(value);
|
|
|
|
if(tid === null)
|
|
|
|
tid = setTimeout(updateUI, Math.max(500 - (Date.now() - lastUiUpdate), 0));
|
2019-02-15 17:05:44 +00:00
|
|
|
return pump();
|
|
|
|
});
|
2019-02-18 20:06:37 +00:00
|
|
|
}();
|
2019-02-15 17:05:44 +00:00
|
|
|
}).catch(e => {});
|
|
|
|
return abort;
|
|
|
|
}
|
2017-07-13 18:57:28 +00:00
|
|
|
return {
|
2020-10-16 08:19:09 +00:00
|
|
|
template: templateId,
|
|
|
|
data: () => state,
|
2021-01-02 08:26:13 +00:00
|
|
|
props: ['route'],
|
2017-07-13 18:57:28 +00:00
|
|
|
methods: {
|
2021-01-02 08:26:13 +00:00
|
|
|
status: function(data) {
|
2020-09-25 03:29:30 +00:00
|
|
|
// Check for the /latest endpoint
|
2021-01-02 08:26:13 +00:00
|
|
|
const params = this._props.route.params;
|
2020-09-25 03:29:30 +00:00
|
|
|
if(params.number === 'latest')
|
2021-03-17 08:21:16 +00:00
|
|
|
return this.$router.replace('jobs/' + params.name + '/' + data.latestNum);
|
2019-02-18 21:06:11 +00:00
|
|
|
|
2020-09-25 03:29:30 +00:00
|
|
|
state.number = parseInt(params.number);
|
2017-08-14 05:45:28 +00:00
|
|
|
state.jobsRunning = [];
|
2017-07-13 18:57:28 +00:00
|
|
|
state.job = data;
|
|
|
|
state.latestNum = data.latestNum;
|
2017-11-18 09:26:04 +00:00
|
|
|
state.jobsRunning = [data];
|
2022-01-01 08:30:38 +00:00
|
|
|
state.logComplete = false;
|
|
|
|
// DOM is used directly for performance
|
|
|
|
document.getElementsByTagName('code')[0].innerHTML = '';
|
2020-09-25 03:29:30 +00:00
|
|
|
if(this.logstream)
|
|
|
|
this.logstream.abort();
|
|
|
|
if(data.started)
|
|
|
|
this.logstream = logFetcher(this, params.name, params.number);
|
2017-07-13 18:57:28 +00:00
|
|
|
},
|
2020-09-25 03:29:30 +00:00
|
|
|
job_queued: function(data) {
|
|
|
|
state.latestNum = data.number;
|
2017-07-13 18:57:28 +00:00
|
|
|
this.$forceUpdate();
|
|
|
|
},
|
2020-09-25 03:29:30 +00:00
|
|
|
job_started: function(data) {
|
|
|
|
if(data.number === state.number) {
|
|
|
|
state.job = Object.assign(state.job, data);
|
|
|
|
state.job.result = 'running';
|
|
|
|
if(this.logstream)
|
|
|
|
this.logstream.abort();
|
|
|
|
this.logstream = logFetcher(this, data.name, data.number);
|
|
|
|
this.$forceUpdate();
|
|
|
|
}
|
|
|
|
},
|
2017-07-13 18:57:28 +00:00
|
|
|
job_completed: function(data) {
|
2020-09-25 03:29:30 +00:00
|
|
|
if(data.number === state.number) {
|
|
|
|
state.job = Object.assign(state.job, data);
|
|
|
|
state.jobsRunning = [];
|
|
|
|
this.$forceUpdate();
|
|
|
|
}
|
2017-07-13 18:57:28 +00:00
|
|
|
},
|
|
|
|
runComplete: function(run) {
|
|
|
|
return !!run && (run.result === 'aborted' || run.result === 'failed' || run.result === 'success');
|
|
|
|
},
|
|
|
|
}
|
|
|
|
};
|
2020-10-16 08:19:09 +00:00
|
|
|
};
|
2018-08-24 10:31:29 +00:00
|
|
|
|
2021-01-02 08:26:13 +00:00
|
|
|
Vue.component('RouterLink', {
|
|
|
|
name: 'router-link',
|
|
|
|
props: {
|
|
|
|
to: { type: String },
|
|
|
|
tag: { type: String, default: 'a' }
|
|
|
|
},
|
|
|
|
template: `<component :is="tag" @click="navigate" :href="to"><slot></slot></component>`,
|
|
|
|
methods: {
|
|
|
|
navigate: function(e) {
|
|
|
|
e.preventDefault();
|
|
|
|
history.pushState(null, null, this.to);
|
|
|
|
this.$root.$emit('navigate');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
Vue.component('RouterView', (() => {
|
|
|
|
const routes = [
|
|
|
|
{ path: /^$/, component: Home('#home') },
|
|
|
|
{ path: /^jobs$/, component: All('#jobs') },
|
|
|
|
{ path: /^wallboard$/, component: All('#wallboard') },
|
|
|
|
{ path: /^jobs\/(?<name>[^\/]+)$/, component: Job('#job') },
|
|
|
|
{ path: /^jobs\/(?<name>[^\/]+)\/(?<number>\d+)$/, component: Run('#run') }
|
|
|
|
];
|
|
|
|
|
|
|
|
const resolveRoute = path => {
|
|
|
|
for(i in routes) {
|
|
|
|
const r = routes[i].path.exec(path);
|
|
|
|
if(r)
|
|
|
|
return [routes[i].component, r.groups];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
let eventSource = null;
|
|
|
|
|
|
|
|
const setupEventSource = (view, query) => {
|
|
|
|
// drop any existing event source
|
|
|
|
if(eventSource)
|
|
|
|
eventSource.close();
|
|
|
|
|
|
|
|
const path = (location.origin+location.pathname).substr(document.head.baseURI.length);
|
|
|
|
const search = query ? '?' + Object.entries(query).map(([k,v])=>`${k}=${v}`).join('&') : '';
|
|
|
|
|
|
|
|
eventSource = new EventSource(document.head.baseURI + path + search);
|
|
|
|
eventSource.reconnectInterval = 500;
|
|
|
|
eventSource.onmessage = msg => {
|
|
|
|
msg = JSON.parse(msg.data);
|
|
|
|
if(msg.type === 'status') {
|
|
|
|
// Event source is connected. Update static data
|
|
|
|
document.title = view.$root.title = msg.title;
|
|
|
|
view.$root.version = msg.version;
|
|
|
|
// Calculate clock offset (used by ProgressUpdater)
|
|
|
|
view.$root.clockSkew = msg.time - Math.floor((new Date()).getTime()/1000);
|
|
|
|
view.$root.connected = true;
|
|
|
|
[view.currentView, route.params] = resolveRoute(path);
|
|
|
|
// the component won't be instantiated until nextTick
|
|
|
|
view.$nextTick(() => {
|
|
|
|
// component is ready, update it with the data from the eventsource
|
|
|
|
eventSource.comp = view.$children[0];
|
|
|
|
// and finally run the component handler
|
|
|
|
eventSource.comp[msg.type](msg.data);
|
|
|
|
});
|
|
|
|
} else {
|
|
|
|
// at this point, the component must be defined
|
|
|
|
if (!eventSource.comp)
|
|
|
|
return console.error("Page component was undefined");
|
|
|
|
view.$root.connected = true;
|
|
|
|
view.$root.showNotify(msg.type, msg.data);
|
|
|
|
if(typeof eventSource.comp[msg.type] === 'function')
|
|
|
|
eventSource.comp[msg.type](msg.data);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
eventSource.onerror = err => {
|
|
|
|
let ri = eventSource.reconnectInterval;
|
|
|
|
view.$root.connected = false;
|
|
|
|
setTimeout(() => {
|
|
|
|
setupEventSource(view);
|
|
|
|
if(ri < 7500)
|
|
|
|
ri *= 1.5;
|
|
|
|
eventSource.reconnectInterval = ri
|
|
|
|
}, ri);
|
|
|
|
eventSource.close();
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
let route = {};
|
|
|
|
|
|
|
|
return {
|
|
|
|
name: 'router-view',
|
|
|
|
template: `<component :is="currentView" :route="route"></component>`,
|
|
|
|
data: () => ({
|
|
|
|
currentView: routes[0].component, // default to home
|
|
|
|
route: route
|
|
|
|
}),
|
|
|
|
created: function() {
|
|
|
|
this.$root.$on('navigate', query => {
|
|
|
|
setupEventSource(this, query);
|
|
|
|
});
|
|
|
|
window.addEventListener('popstate', () => {
|
|
|
|
this.$root.$emit('navigate');
|
|
|
|
});
|
|
|
|
// initial navigation
|
|
|
|
this.$root.$emit('navigate');
|
|
|
|
}
|
|
|
|
};
|
|
|
|
})());
|
|
|
|
|
2021-09-25 07:07:36 +00:00
|
|
|
const LaminarApp = new Vue({
|
2017-07-13 18:57:28 +00:00
|
|
|
el: '#app',
|
|
|
|
data: {
|
2020-10-16 08:19:09 +00:00
|
|
|
title: '', // populated by status message
|
2020-07-03 03:13:11 +00:00
|
|
|
version: '',
|
2018-02-03 14:52:46 +00:00
|
|
|
clockSkew: 0,
|
2017-12-29 15:18:43 +00:00
|
|
|
connected: false,
|
2021-01-02 08:26:13 +00:00
|
|
|
notify: 'localStorage' in window && localStorage.getItem('showNotifications') == 1,
|
|
|
|
route: { path: '', params: {} }
|
2017-12-29 15:18:43 +00:00
|
|
|
},
|
|
|
|
computed: {
|
2020-10-16 08:19:09 +00:00
|
|
|
supportsNotifications: () =>
|
|
|
|
'Notification' in window && Notification.permission !== 'denied'
|
2017-12-29 15:18:43 +00:00
|
|
|
},
|
|
|
|
methods: {
|
2020-10-16 08:19:09 +00:00
|
|
|
toggleNotifications: function(en) {
|
2017-12-29 15:18:43 +00:00
|
|
|
if(Notification.permission !== 'granted')
|
|
|
|
Notification.requestPermission(p => this.notify = (p === 'granted'))
|
|
|
|
else
|
|
|
|
this.notify = en;
|
|
|
|
},
|
2020-10-16 08:19:09 +00:00
|
|
|
showNotify: function(msg, data) {
|
2017-12-29 15:18:43 +00:00
|
|
|
if(this.notify && msg === 'job_completed')
|
2018-01-05 08:50:50 +00:00
|
|
|
new Notification('Job ' + data.result, {
|
|
|
|
body: data.name + ' ' + '#' + data.number + ': ' + data.result
|
|
|
|
});
|
2021-09-25 07:07:36 +00:00
|
|
|
},
|
|
|
|
navigate: function(path) {
|
|
|
|
history.pushState(null, null, path);
|
|
|
|
this.$emit('navigate');
|
|
|
|
return false;
|
2020-10-16 08:19:09 +00:00
|
|
|
}
|
2017-12-29 15:18:43 +00:00
|
|
|
},
|
|
|
|
watch: {
|
2020-10-16 08:19:09 +00:00
|
|
|
notify: e => localStorage.setItem('showNotifications', e ? 1 : 0)
|
2021-01-02 08:26:13 +00:00
|
|
|
}
|
2015-09-26 20:54:27 +00:00
|
|
|
});
|