mirror of
https://github.com/ohwgiles/laminar.git
synced 2026-10-18 00:54:25 +00:00
Compare commits
8 Commits
tmp-better
...
1.3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dab620b01e | ||
|
|
1e7e9319c3 | ||
|
|
af4b51b3e9 | ||
|
|
458ec26943 | ||
|
|
6a20291dc4 | ||
|
|
3cc01bc45d | ||
|
|
e25b58944d | ||
|
|
1be755e323 |
@@ -87,8 +87,8 @@ file(DOWNLOAD https://cdnjs.cloudflare.com/ajax/libs/vue/2.6.12/vue.min.js
|
||||
js/vue.min.js EXPECTED_MD5 fb192338844efe86ec759a40152fcb8e)
|
||||
file(DOWNLOAD https://raw.githubusercontent.com/drudru/ansi_up/v4.0.4/ansi_up.js
|
||||
js/ansi_up.js EXPECTED_MD5 b31968e1a8fed0fa82305e978161f7f5)
|
||||
file(DOWNLOAD https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.7.2/Chart.min.js
|
||||
js/Chart.min.js EXPECTED_MD5 f6c8efa65711e0cbbc99ba72997ecd0e)
|
||||
file(DOWNLOAD https://cdnjs.cloudflare.com/ajax/libs/Chart.js/3.9.1/chart.min.js
|
||||
js/Chart.min.js EXPECTED_MD5 7dd5ea7d2cf22a1c42b43c40093d2669)
|
||||
# ...and compile them
|
||||
generate_compressed_bins(${CMAKE_BINARY_DIR} js/vue.min.js
|
||||
js/ansi_up.js js/Chart.min.js)
|
||||
|
||||
@@ -534,7 +534,7 @@ If `CONTEXTS` is empty or absent (or if `JOB.conf` doesn't exist), laminar will
|
||||
|
||||
## Adding environment to a context
|
||||
|
||||
Append desired environment variables to `/var/lib/laminar/cfg/contexts/CONTEXT_NAME.conf`:
|
||||
Append desired environment variables to `/var/lib/laminar/cfg/contexts/CONTEXT_NAME.env`:
|
||||
|
||||
```
|
||||
DUT_IP=192.168.3.2
|
||||
|
||||
@@ -7,7 +7,7 @@ Documentation=https://laminar.ohwg.net/docs.html
|
||||
[Service]
|
||||
User=laminar
|
||||
EnvironmentFile=-/etc/laminar.conf
|
||||
ExecStart=@CMAKE_INSTALL_PREFIX@/sbin/laminard
|
||||
ExecStart=@CMAKE_INSTALL_PREFIX@/sbin/laminard -v
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
|
||||
13
src/http.cpp
13
src/http.cpp
@@ -131,9 +131,16 @@ kj::Promise<void> Http::cleanupPeers(kj::Timer& timer)
|
||||
{
|
||||
return timer.afterDelay(15 * kj::SECONDS).then([&]{
|
||||
for(EventPeer* p : eventPeers) {
|
||||
// an empty SSE message is a colon followed by two newlines
|
||||
p->pendingOutput.push_back(":\n\n");
|
||||
p->fulfiller->fulfill();
|
||||
// Even single threaded, if load causes this timeout to be serviced
|
||||
// before writeEvents has created a fulfiller, or if an exception
|
||||
// caused the destruction of the promise but attach(peer) hasn't yet
|
||||
// removed it from the eventPeers list, we will see a null fulfiller
|
||||
// here
|
||||
if(p->fulfiller) {
|
||||
// an empty SSE message is a colon followed by two newlines
|
||||
p->pendingOutput.push_back(":\n\n");
|
||||
p->fulfiller->fulfill();
|
||||
}
|
||||
}
|
||||
return cleanupPeers(timer);
|
||||
}).eagerlyEvaluate(nullptr);
|
||||
|
||||
@@ -95,11 +95,31 @@ Laminar::Laminar(Server &server, Settings settings) :
|
||||
db = new Database((homePath/"laminar.sqlite").toString(true).cStr());
|
||||
// Prepare database for first use
|
||||
// TODO: error handling
|
||||
db->exec("CREATE TABLE IF NOT EXISTS builds("
|
||||
"name TEXT, number INT UNSIGNED, node TEXT, queuedAt INT, "
|
||||
"startedAt INT, completedAt INT, result INT, output TEXT, "
|
||||
"outputLen INT, parentJob TEXT, parentBuild INT, reason TEXT, "
|
||||
"PRIMARY KEY (name, number))");
|
||||
const char *create_table_stmt =
|
||||
"CREATE TABLE IF NOT EXISTS builds("
|
||||
"name TEXT, number INT UNSIGNED, node TEXT, queuedAt INT, "
|
||||
"startedAt INT, completedAt INT, result INT, output TEXT, "
|
||||
"outputLen INT, parentJob TEXT, parentBuild INT, reason TEXT, "
|
||||
"PRIMARY KEY (name, number DESC))";
|
||||
db->exec(create_table_stmt);
|
||||
|
||||
// Migrate from (name, number) primary key to (name, number DESC).
|
||||
// SQLite does not allow to alter primary key of existing table, so
|
||||
// we have to create a new table.
|
||||
db->stmt("SELECT sql LIKE '%, PRIMARY KEY (name, number))' "
|
||||
"FROM sqlite_master WHERE type = 'table' AND name = 'builds'")
|
||||
.fetch<int>([&](int has_old_index) {
|
||||
if (has_old_index) {
|
||||
LLOG(INFO, "Migrating table to the new primary key");
|
||||
db->exec("BEGIN TRANSACTION");
|
||||
db->exec("ALTER TABLE builds RENAME TO builds_old");
|
||||
db->exec(create_table_stmt);
|
||||
db->exec("INSERT INTO builds SELECT * FROM builds_old");
|
||||
db->exec("DROP TABLE builds_old");
|
||||
db->exec("COMMIT");
|
||||
}
|
||||
});
|
||||
|
||||
db->exec("CREATE INDEX IF NOT EXISTS idx_completion_time ON builds("
|
||||
"completedAt DESC)");
|
||||
|
||||
@@ -338,9 +358,8 @@ std::string Laminar::getStatus(MonitorScope scope) {
|
||||
j.set("description", desc == jobDescriptions.end() ? "" : desc->second);
|
||||
} else if(scope.type == MonitorScope::ALL) {
|
||||
j.startArray("jobs");
|
||||
db->stmt("SELECT name,number,startedAt,completedAt,result,reason FROM builds b "
|
||||
"JOIN (SELECT name n,MAX(number) latest FROM builds WHERE result IS NOT NULL GROUP BY n) q "
|
||||
"ON b.name = q.n AND b.number = latest")
|
||||
db->stmt("SELECT name, number, startedAt, completedAt, result, reason "
|
||||
"FROM builds GROUP BY name HAVING number = MAX(number)")
|
||||
.fetch<str,uint,time_t,time_t,int,str>([&](str name,uint number, time_t started, time_t completed, int result, str reason){
|
||||
j.StartObject();
|
||||
j.set("name", name);
|
||||
|
||||
@@ -158,7 +158,7 @@
|
||||
<template id="run"><div style="display: grid; grid-template-rows: auto 1fr">
|
||||
<div style="padding: 15px">
|
||||
<div style="display: grid; grid-template-columns: auto 25px auto auto 1fr 400px; gap: 5px; align-items: center">
|
||||
<h2 style="white-space: nowrap"><span v-html="runIcon(job.result)"></span> {{route.params.name}} #{{route.params.number}}</h2>
|
||||
<h2 style="white-space: nowrap"><span v-html="runIcon(job.result)"></span> <router-link :to="'jobs/'+route.params.name">{{route.params.name}}</router-link> #{{route.params.number}}</h2>
|
||||
<span></span>
|
||||
<router-link :disabled="route.params.number == 1" :to="'jobs/'+route.params.name+'/'+(route.params.number-1)" tag="button">«</router-link>
|
||||
<router-link :disabled="route.params.number == latestNum" :to="'jobs/'+route.params.name+'/'+(parseInt(route.params.number)+1)" tag="button">»</router-link>
|
||||
|
||||
@@ -129,7 +129,8 @@ const Charts = (() => {
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
hover: { mode: null }
|
||||
hover: { mode: null },
|
||||
aspectRatio: 2
|
||||
}
|
||||
});
|
||||
c.executorBusyChanged = busy => {
|
||||
@@ -159,20 +160,28 @@ const Charts = (() => {
|
||||
datasets: [{
|
||||
label: 'Failed Builds',
|
||||
backgroundColor: "#883d3d",
|
||||
data: data.map(e => e.failed || 0)
|
||||
data: data.map(e => e.failed || 0),
|
||||
fill: true,
|
||||
tension: 0.35,
|
||||
},{
|
||||
label: 'Successful Builds',
|
||||
backgroundColor: "#74af77",
|
||||
data: data.map(e => e.success || 0)
|
||||
data: data.map(e => e.success || 0),
|
||||
fill: true,
|
||||
tension: 0.35,
|
||||
}]
|
||||
},
|
||||
options:{
|
||||
title: { display: true, text: 'Runs per day' },
|
||||
tooltips:{callbacks:{title: (tip, data) => dayNames[tip[0].index].long}},
|
||||
scales:{yAxes:[{
|
||||
ticks:{userCallback: (label, index, labels) => Number.isInteger(label) ? label: null},
|
||||
stacked: true
|
||||
}]}
|
||||
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
|
||||
},
|
||||
},
|
||||
}
|
||||
});
|
||||
c.jobCompleted = success => {
|
||||
@@ -183,7 +192,7 @@ const Charts = (() => {
|
||||
},
|
||||
createRunsPerJobChart: (id, data) => {
|
||||
const c = new Chart(document.getElementById("chartBpj"), {
|
||||
type: 'horizontalBar',
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: Object.keys(data),
|
||||
datasets: [{
|
||||
@@ -193,9 +202,16 @@ const Charts = (() => {
|
||||
}]
|
||||
},
|
||||
options:{
|
||||
title: { display: true, text: 'Runs per job' },
|
||||
indexAxis: 'y',
|
||||
plugins: {
|
||||
title: { display: true, text: 'Runs per job' },
|
||||
},
|
||||
hover: { mode: null },
|
||||
scales:{xAxes:[{ticks:{userCallback: (label, index, labels)=> Number.isInteger(label) ? label: null}}]}
|
||||
scales: {
|
||||
x: {
|
||||
ticks:{callback: (label, index, labels)=> Number.isInteger(label) ? label: null}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
c.jobCompleted = name => {
|
||||
@@ -216,7 +232,7 @@ const Charts = (() => {
|
||||
createTimePerJobChart: (id, data, completedCounts) => {
|
||||
const scale = timeScale(Math.max(...Object.values(data)));
|
||||
const c = new Chart(document.getElementById(id), {
|
||||
type: 'horizontalBar',
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: Object.keys(data),
|
||||
datasets: [{
|
||||
@@ -226,18 +242,23 @@ const Charts = (() => {
|
||||
}]
|
||||
},
|
||||
options:{
|
||||
title: { display: true, text: 'Mean run time this week' },
|
||||
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()
|
||||
}}
|
||||
},
|
||||
hover: { mode: null },
|
||||
scales:{xAxes:[{
|
||||
ticks:{userCallback: scale.ticks},
|
||||
scaleLabel: {
|
||||
display: true,
|
||||
labelString: scale.label
|
||||
scales: {
|
||||
x:{
|
||||
ticks: {callback: scale.ticks},
|
||||
title: {
|
||||
display: true,
|
||||
text: scale.label
|
||||
}
|
||||
}
|
||||
}]},
|
||||
tooltips:{callbacks:{
|
||||
label: (tip, data) => data.datasets[tip.datasetIndex].label + ': ' + tip.xLabel.toFixed(2) + ' ' + scale.label.toLowerCase()
|
||||
}}
|
||||
},
|
||||
}
|
||||
});
|
||||
c.jobCompleted = (name, time) => {
|
||||
@@ -261,7 +282,8 @@ const Charts = (() => {
|
||||
label: name,
|
||||
data: durations.map(x => x * scale.factor),
|
||||
borderColor: 'hsl('+(name.hashCode() % 360)+', 27%, 57%)',
|
||||
backgroundColor: 'transparent'
|
||||
backgroundColor: 'transparent',
|
||||
tension: 0.35,
|
||||
});
|
||||
const c = new Chart(document.getElementById(id), {
|
||||
type: 'line',
|
||||
@@ -270,21 +292,21 @@ const Charts = (() => {
|
||||
datasets: data.map(e => dataValue(e.name, e.durations))
|
||||
},
|
||||
options:{
|
||||
title: { display: true, text: 'Run time changes' },
|
||||
legend:{ display: true, position: 'bottom' },
|
||||
scales:{
|
||||
xAxes:[{ticks:{display: false}}],
|
||||
yAxes:[{
|
||||
ticks:{userCallback: scale.ticks},
|
||||
scaleLabel: {
|
||||
display: true,
|
||||
labelString: scale.label
|
||||
}
|
||||
}]
|
||||
plugins: {
|
||||
legend: { display: true, position: 'bottom' },
|
||||
title: { display: true, text: 'Run time changes' },
|
||||
tooltip: { enabled: false },
|
||||
},
|
||||
scales:{
|
||||
x: {ticks: {display: false}},
|
||||
y: {
|
||||
ticks: {callback: scale.ticks},
|
||||
title: {
|
||||
display: true,
|
||||
text: scale.label
|
||||
}
|
||||
}
|
||||
},
|
||||
tooltips:{
|
||||
enabled:false
|
||||
}
|
||||
}
|
||||
});
|
||||
c.jobCompleted = (name, time) => {
|
||||
@@ -310,62 +332,65 @@ const Charts = (() => {
|
||||
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(),
|
||||
barPercentage: 1.0,
|
||||
categoryPercentage: 0.95,
|
||||
data: jobs.map(e => (e.completed - e.started) * scale.factor).reverse()
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
title: { display: true, text: 'Build time' },
|
||||
plugins: {
|
||||
title: { display: true, text: 'Build time' },
|
||||
tooltip: {
|
||||
callbacks:{
|
||||
label: (tip) => scale.ticks(tip.raw) + ' ' + scale.label.toLowerCase()
|
||||
}
|
||||
}
|
||||
},
|
||||
hover: { mode: null },
|
||||
scales:{
|
||||
xAxes:[{
|
||||
categoryPercentage: 0.95,
|
||||
barPercentage: 1.0
|
||||
},{
|
||||
id: 'avg',
|
||||
type: 'linear',
|
||||
ticks: {
|
||||
display: false
|
||||
},
|
||||
gridLines: {
|
||||
x: {
|
||||
grid: {
|
||||
display: false,
|
||||
drawBorder: false
|
||||
}
|
||||
}],
|
||||
yAxes:[{
|
||||
ticks:{userCallback: scale.ticks},
|
||||
scaleLabel:{display: true, labelString: scale.label}
|
||||
}]
|
||||
},
|
||||
y: {
|
||||
suggestedMax: avg * scale.factor,
|
||||
ticks: {callback: scale.ticks },
|
||||
title: {display: true, text: scale.label}
|
||||
}
|
||||
},
|
||||
tooltips:{callbacks:{
|
||||
label: (tip, data) => scale.ticks(tip.yLabel) + ' ' + scale.label.toLowerCase()
|
||||
}}
|
||||
}
|
||||
},
|
||||
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();
|
||||
}
|
||||
}]
|
||||
});
|
||||
c.avg = avg;
|
||||
c.jobCompleted = (num, result, time) => {
|
||||
let avg = c.data.datasets[0].data[0].y / scale.factor;
|
||||
avg = ((avg * (num - 1)) + time) / num;
|
||||
c.data.datasets[0].data[0].y = avg * scale.factor;
|
||||
c.data.datasets[0].data[1].y = avg * scale.factor;
|
||||
if(c.data.datasets[1].data.length == 20) {
|
||||
c.avg = ((c.avg * (num - 1)) + time) / num;
|
||||
c.options.scales.y.suggestedMax = avg * scale.factor;
|
||||
if(c.data.datasets[0].data.length == 20) {
|
||||
c.data.labels.shift();
|
||||
c.data.datasets[1].data.shift();
|
||||
c.data.datasets[1].backgroundColor.shift();
|
||||
c.data.datasets[0].data.shift();
|
||||
c.data.datasets[0].backgroundColor.shift();
|
||||
}
|
||||
c.data.labels.push('#' + num);
|
||||
c.data.datasets[1].data.push(time * scale.factor);
|
||||
c.data.datasets[1].backgroundColor.push(result == 'success' ? '#74af77': '#883d3d');
|
||||
c.data.datasets[0].data.push(time * scale.factor);
|
||||
c.data.datasets[0].backgroundColor.push(result == 'success' ? '#74af77': '#883d3d');
|
||||
c.update();
|
||||
};
|
||||
return c;
|
||||
@@ -374,13 +399,11 @@ const Charts = (() => {
|
||||
})();
|
||||
|
||||
// For all charts, set miniumum Y to 0
|
||||
Chart.scaleService.updateScaleDefaults('linear', {
|
||||
ticks: { suggestedMin: 0 }
|
||||
});
|
||||
Chart.defaults.scales.linear.suggestedMin = 0;
|
||||
// Don't display legend by default
|
||||
Chart.defaults.global.legend.display = false;
|
||||
Chart.defaults.plugins.legend.display = false;
|
||||
// Disable tooltip hover animations
|
||||
Chart.defaults.global.hover.animationDuration = 0;
|
||||
Chart.defaults.plugins.tooltip.animation = false;
|
||||
|
||||
// Component for the / endpoint
|
||||
const Home = templateId => {
|
||||
|
||||
Reference in New Issue
Block a user