mirror of
https://github.com/ohwgiles/laminar.git
synced 2024-10-27 20:34:20 +00:00
remove dependency on vue-router
only a small subset of vue-router is used, and integration is complicated by each route having its own EventSource request. Implementing the routing directly allows simplification of the EventSource logic. Another motivating factor is that the vue-router packages in debian have been unreliable, making the dependence on vue-router a hinderance for packaging laminar in debian.
This commit is contained in:
parent
c140fb51eb
commit
907f3926ce
@ -85,8 +85,6 @@ add_custom_command(OUTPUT index_html_size.h
|
|||||||
# Download 3rd-party frontend JS libs...
|
# Download 3rd-party frontend JS libs...
|
||||||
file(DOWNLOAD https://cdnjs.cloudflare.com/ajax/libs/vue/2.6.12/vue.min.js
|
file(DOWNLOAD https://cdnjs.cloudflare.com/ajax/libs/vue/2.6.12/vue.min.js
|
||||||
js/vue.min.js EXPECTED_MD5 fb192338844efe86ec759a40152fcb8e)
|
js/vue.min.js EXPECTED_MD5 fb192338844efe86ec759a40152fcb8e)
|
||||||
file(DOWNLOAD https://cdnjs.cloudflare.com/ajax/libs/vue-router/3.4.8/vue-router.min.js
|
|
||||||
js/vue-router.min.js EXPECTED_MD5 5f51d4dbbf68fd6725956a5a2b865f3b)
|
|
||||||
file(DOWNLOAD https://raw.githubusercontent.com/drudru/ansi_up/v1.3.0/ansi_up.js
|
file(DOWNLOAD https://raw.githubusercontent.com/drudru/ansi_up/v1.3.0/ansi_up.js
|
||||||
js/ansi_up.js EXPECTED_MD5 158566dc1ff8f2804de972f7e841e2f6)
|
js/ansi_up.js EXPECTED_MD5 158566dc1ff8f2804de972f7e841e2f6)
|
||||||
file(DOWNLOAD https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.7.2/Chart.min.js
|
file(DOWNLOAD https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.7.2/Chart.min.js
|
||||||
|
@ -11,7 +11,6 @@
|
|||||||
<link rel="manifest" href="/manifest.webmanifest">
|
<link rel="manifest" href="/manifest.webmanifest">
|
||||||
<title>Laminar</title>
|
<title>Laminar</title>
|
||||||
<script src="js/vue.min.js"></script>
|
<script src="js/vue.min.js"></script>
|
||||||
<script src="js/vue-router.min.js"></script>
|
|
||||||
<script src="js/ansi_up.js"></script>
|
<script src="js/ansi_up.js"></script>
|
||||||
<script src="js/Chart.min.js"></script>
|
<script src="js/Chart.min.js"></script>
|
||||||
<script src="js/app.js" defer></script>
|
<script src="js/app.js" defer></script>
|
||||||
@ -115,13 +114,13 @@
|
|||||||
|
|
||||||
<template id="job"><div id="page-job-main">
|
<template id="job"><div id="page-job-main">
|
||||||
<div style="padding: 15px;">
|
<div style="padding: 15px;">
|
||||||
<h2>{{$route.params.name}}</h2>
|
<h2>{{route.params.name}}</h2>
|
||||||
<div v-html="description"></div>
|
<div v-html="description"></div>
|
||||||
<dl>
|
<dl>
|
||||||
<dt>Last Successful Run</dt>
|
<dt>Last Successful Run</dt>
|
||||||
<dd><router-link v-if="lastSuccess" :to="'/jobs/'+$route.params.name+'/'+lastSuccess.number">#{{lastSuccess.number}}</router-link> {{lastSuccess?' - at '+formatDate(lastSuccess.started):'never'}}</dd>
|
<dd><router-link v-if="lastSuccess" :to="'/jobs/'+route.params.name+'/'+lastSuccess.number">#{{lastSuccess.number}}</router-link> {{lastSuccess?' - at '+formatDate(lastSuccess.started):'never'}}</dd>
|
||||||
<dt>Last Failed Run</dt>
|
<dt>Last Failed Run</dt>
|
||||||
<dd><router-link v-if="lastFailed" :to="'/jobs/'+$route.params.name+'/'+lastFailed.number">#{{lastFailed.number}}</router-link> {{lastFailed?' - at '+formatDate(lastFailed.started):'never'}}</dd>
|
<dd><router-link v-if="lastFailed" :to="'/jobs/'+route.params.name+'/'+lastFailed.number">#{{lastFailed.number}}</router-link> {{lastFailed?' - at '+formatDate(lastFailed.started):'never'}}</dd>
|
||||||
</dl>
|
</dl>
|
||||||
</div>
|
</div>
|
||||||
<div style="display: grid; justify-content: center; padding: 15px;">
|
<div style="display: grid; justify-content: center; padding: 15px;">
|
||||||
@ -142,7 +141,7 @@
|
|||||||
</tr>
|
</tr>
|
||||||
<tr v-for="job in jobsRunning.concat(jobsRecent)" track-by="$index">
|
<tr v-for="job in jobsRunning.concat(jobsRecent)" track-by="$index">
|
||||||
<td style="width:1px"><span v-html="runIcon(job.result)"></span></td>
|
<td style="width:1px"><span v-html="runIcon(job.result)"></span></td>
|
||||||
<td><router-link :to="'/jobs/'+$route.params.name+'/'+job.number">#{{job.number}}</router-link></td>
|
<td><router-link :to="'/jobs/'+route.params.name+'/'+job.number">#{{job.number}}</router-link></td>
|
||||||
<td class="text-center">{{formatDate(job.started)}}</td>
|
<td class="text-center">{{formatDate(job.started)}}</td>
|
||||||
<td class="text-center">{{formatDuration(job.started, job.completed)}}</td>
|
<td class="text-center">{{formatDuration(job.started, job.completed)}}</td>
|
||||||
<td class="text-center vp-sm-hide">{{job.reason}}</td>
|
<td class="text-center vp-sm-hide">{{job.reason}}</td>
|
||||||
@ -159,10 +158,10 @@
|
|||||||
<template id="run"><div style="display: grid; grid-template-rows: auto 1fr">
|
<template id="run"><div style="display: grid; grid-template-rows: auto 1fr">
|
||||||
<div style="padding: 15px">
|
<div style="padding: 15px">
|
||||||
<div style="display: grid; grid-template-columns: auto 25px auto auto 1fr 400px; gap: 5px; align-items: center">
|
<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> {{route.params.name}} #{{route.params.number}}</h2>
|
||||||
<span></span>
|
<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 == 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>
|
<router-link :disabled="route.params.number == latestNum" :to="'/jobs/'+route.params.name+'/'+(parseInt(route.params.number)+1)" tag="button">»</router-link>
|
||||||
<span></span>
|
<span></span>
|
||||||
<div class="progress" v-show="job.result == 'running'">
|
<div class="progress" v-show="job.result == 'running'">
|
||||||
<div class="progress-bar" :class="{overtime:job.overtime,indeterminate:!job.etc}" :style="job.etc && {width:job.progress+'%'}"></div>
|
<div class="progress-bar" :class="{overtime:job.overtime,indeterminate:!job.etc}" :style="job.etc && {width:job.progress+'%'}"></div>
|
||||||
@ -200,7 +199,7 @@
|
|||||||
</router-link>
|
</router-link>
|
||||||
<div id="nav-top-links" style="display: grid; grid-auto-flow: column; justify-content: start; gap: 15px; padding: 0 15px; align-items: center; font-size: 16px;">
|
<div id="nav-top-links" style="display: grid; grid-auto-flow: column; justify-content: start; gap: 15px; padding: 0 15px; align-items: center; font-size: 16px;">
|
||||||
<router-link to="/jobs">Jobs</router-link>
|
<router-link to="/jobs">Jobs</router-link>
|
||||||
<router-link v-for="(crumb,i) in _route.path.slice(1).split('/').slice(1,-1)" :to="_route.path.split('/').slice(0,i+3).join('/')">{{crumb}}</router-link>
|
<router-link v-for="(crumb,i) in route.path.slice(1).split('/').slice(1,-1)" :to="route.path.split('/').slice(0,i+3).join('/')">{{crumb}}</router-link>
|
||||||
</div>
|
</div>
|
||||||
<div></div>
|
<div></div>
|
||||||
<span class="version">{{version}}</span>
|
<span class="version">{{version}}</span>
|
||||||
|
@ -18,86 +18,6 @@ Vue.filter('iecFileSize', bytes => {
|
|||||||
['B', 'KiB', 'MiB', 'GiB', 'TiB'][exp];
|
['B', 'KiB', 'MiB', 'GiB', 'TiB'][exp];
|
||||||
});
|
});
|
||||||
|
|
||||||
// Mixin handling retrieving dynamic updates from the backend
|
|
||||||
Vue.mixin((() => {
|
|
||||||
const setupEventSource = (to, query, next, comp) => {
|
|
||||||
const es = new EventSource(document.head.baseURI + to.path.substr(1) + query);
|
|
||||||
es.comp = comp; // When reconnecting, we already have a component. Usually this will be null.
|
|
||||||
es.to = to; // Save a ref, needed for adding query params for pagination.
|
|
||||||
es.onmessage = function(msg) {
|
|
||||||
msg = JSON.parse(msg.data);
|
|
||||||
// "status" is the first message the server always delivers.
|
|
||||||
// Use this to confirm the navigation. The component is not
|
|
||||||
// created until next() is called, so creating a reference
|
|
||||||
// for other message types must be deferred. There are some extra
|
|
||||||
// subtle checks here. If this eventsource already has a component,
|
|
||||||
// then this is not the first time the status message has been
|
|
||||||
// received. If the frontend requests an update, the status message
|
|
||||||
// should not be handled here, but treated the same as any other
|
|
||||||
// message. An exception is if the connection has been lost - in
|
|
||||||
// that case we should treat this as a "first-time" status message.
|
|
||||||
// !this.comp.es is used to test this condition.
|
|
||||||
if (msg.type === 'status' && (!this.comp || !this.comp.es)) {
|
|
||||||
next(comp => {
|
|
||||||
// Set up bidirectional reference
|
|
||||||
// 1. needed to reference the component for other msg types
|
|
||||||
this.comp = comp;
|
|
||||||
// 2. needed to close the ws on navigation away
|
|
||||||
comp.es = this;
|
|
||||||
comp.esReconnectInterval = 500;
|
|
||||||
// Update html and nav titles
|
|
||||||
document.title = comp.$root.title = msg.title;
|
|
||||||
comp.$root.version = msg.version;
|
|
||||||
// Calculate clock offset (used by ProgressUpdater)
|
|
||||||
comp.$root.clockSkew = msg.time - Math.floor((new Date()).getTime()/1000);
|
|
||||||
comp.$root.connected = true;
|
|
||||||
// Component-specific callback handler
|
|
||||||
comp[msg.type](msg.data, to.params);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// at this point, the component must be defined
|
|
||||||
if (!this.comp)
|
|
||||||
return console.error("Page component was undefined");
|
|
||||||
else {
|
|
||||||
this.comp.$root.connected = true;
|
|
||||||
this.comp.$root.showNotify(msg.type, msg.data);
|
|
||||||
if(typeof this.comp[msg.type] === 'function')
|
|
||||||
this.comp[msg.type](msg.data);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
es.onerror = function(e) {
|
|
||||||
this.comp.$root.connected = false;
|
|
||||||
setTimeout(() => {
|
|
||||||
// Recrate the EventSource, passing in the existing component
|
|
||||||
this.comp.es = setupEventSource(to, query, null, this.comp);
|
|
||||||
}, this.comp.esReconnectInterval);
|
|
||||||
if(this.comp.esReconnectInterval < 7500)
|
|
||||||
this.comp.esReconnectInterval *= 1.5;
|
|
||||||
this.close();
|
|
||||||
}
|
|
||||||
return es;
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
beforeRouteEnter(to, from, next) {
|
|
||||||
setupEventSource(to, '', (fn) => { next(fn); });
|
|
||||||
},
|
|
||||||
beforeRouteUpdate(to, from, next) {
|
|
||||||
this.es.close();
|
|
||||||
setupEventSource(to, '', (fn) => { fn(this); next(); });
|
|
||||||
},
|
|
||||||
beforeRouteLeave(to, from, next) {
|
|
||||||
this.es.close();
|
|
||||||
next();
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
query(q) {
|
|
||||||
this.es.close();
|
|
||||||
setupEventSource(this.es.to, '?' + Object.entries(q).map(([k,v])=>`${k}=${v}`).join('&'), fn => fn(this));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
})());
|
|
||||||
|
|
||||||
// Mixin for periodically updating a progress bar
|
// Mixin for periodically updating a progress bar
|
||||||
Vue.mixin({
|
Vue.mixin({
|
||||||
@ -577,6 +497,7 @@ const Job = templateId => {
|
|||||||
let chtBt = null;
|
let chtBt = null;
|
||||||
return {
|
return {
|
||||||
template: templateId,
|
template: templateId,
|
||||||
|
props: ['route'],
|
||||||
data: () => state,
|
data: () => state,
|
||||||
methods: {
|
methods: {
|
||||||
status: function(msg) {
|
status: function(msg) {
|
||||||
@ -632,6 +553,9 @@ const Job = templateId => {
|
|||||||
state.sort.field = field;
|
state.sort.field = field;
|
||||||
}
|
}
|
||||||
this.query(state.sort)
|
this.query(state.sort)
|
||||||
|
},
|
||||||
|
query: function(q) {
|
||||||
|
this.$root.$emit('navigate', q);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -674,9 +598,11 @@ const Run = templateId => {
|
|||||||
return {
|
return {
|
||||||
template: templateId,
|
template: templateId,
|
||||||
data: () => state,
|
data: () => state,
|
||||||
|
props: ['route'],
|
||||||
methods: {
|
methods: {
|
||||||
status: function(data, params) {
|
status: function(data) {
|
||||||
// Check for the /latest endpoint
|
// Check for the /latest endpoint
|
||||||
|
const params = this._props.route.params;
|
||||||
if(params.number === 'latest')
|
if(params.number === 'latest')
|
||||||
return this.$router.replace('/jobs/' + params.name + '/' + data.latestNum);
|
return this.$router.replace('/jobs/' + params.name + '/' + data.latestNum);
|
||||||
|
|
||||||
@ -719,6 +645,115 @@ const Run = templateId => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
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 {
|
||||||
|
console.log('another msg!')
|
||||||
|
console.log(msg)
|
||||||
|
// 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');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
})());
|
||||||
|
|
||||||
new Vue({
|
new Vue({
|
||||||
el: '#app',
|
el: '#app',
|
||||||
data: {
|
data: {
|
||||||
@ -726,7 +761,8 @@ new Vue({
|
|||||||
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,
|
||||||
|
route: { path: '', params: {} }
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
supportsNotifications: () =>
|
supportsNotifications: () =>
|
||||||
@ -748,16 +784,5 @@ new Vue({
|
|||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
notify: e => localStorage.setItem('showNotifications', e ? 1 : 0)
|
notify: e => localStorage.setItem('showNotifications', e ? 1 : 0)
|
||||||
},
|
}
|
||||||
router: new VueRouter({
|
|
||||||
mode: 'history',
|
|
||||||
base: document.head.baseURI.substr(location.origin.length),
|
|
||||||
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', component: Run('#run') }
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
});
|
});
|
||||||
|
Loading…
Reference in New Issue
Block a user