Show development activity log.

A button in the help dialogue takes the user to the
/feed/… frontend URL, where the latest development
activity is shown, taken from the GitLab RSS feed
for the project.
This commit is contained in:
D. Berge
2021-05-08 00:46:31 +02:00
parent 983113b6cc
commit 7bb3a3910b
7 changed files with 151 additions and 2 deletions

View File

@@ -45,6 +45,14 @@
<v-icon class="d-lg-none">mdi-bug</v-icon> <v-icon class="d-lg-none">mdi-bug</v-icon>
<span class="d-none d-lg-inline">Report a bug</span> <span class="d-none d-lg-inline">Report a bug</span>
</v-btn> </v-btn>
<v-btn
color="info"
text
:href='"/feed/"+feed'
title="View development log"
>
<v-icon>mdi-rss</v-icon>
</v-btn> </v-btn>
<v-spacer></v-spacer> <v-spacer></v-spacer>
@@ -73,7 +81,8 @@ export default {
data () { data () {
return { return {
dialog: false, dialog: false,
email: "dougal-support@aaltronav.eu" email: "dougal-support@aaltronav.eu",
feed: btoa(encodeURIComponent("https://gitlab.com/wgp/dougal/software.atom?feed_token=XSPpvsYEny8YmH75Nz5W"))
}; };
} }

View File

@@ -33,6 +33,14 @@ Vue.use(VueRouter)
// which is lazy-loaded when the route is visited. // which is lazy-loaded when the route is visited.
component: () => import(/* webpackChunkName: "about" */ '../views/About.vue') component: () => import(/* webpackChunkName: "about" */ '../views/About.vue')
}, },
{
path: '/feed/:source',
name: 'Feed',
// route level code-splitting
// this generates a separate chunk (about.[hash].js) for this route
// which is lazy-loaded when the route is visited.
component: () => import(/* webpackChunkName: "about" */ '../views/Feed.vue')
},
{ {
pathToRegexpOptions: { strict: true }, pathToRegexpOptions: { strict: true },
path: "/login", path: "/login",

View File

@@ -0,0 +1,103 @@
<template>
<v-container fluid>
<v-overlay absolute opacity="1" :value="!feed.updated">
<v-progress-circular indeterminate size="64"></v-progress-circular>
</v-overlay>
<v-card>
<v-card-title>
{{feed.title}}
<a :href="feed.link"><v-icon class="ml-2">mdi-link</v-icon></a>
</v-card-title>
<v-card-subtitle>Last updated: {{feed.updated}}</v-card-subtitle>
<v-card-text>
<v-timeline align-top dense>
<v-timeline-item v-for="item in feed.items" small>
<v-card :color="item.colour">
<v-card-title primary-title>
<div class="headline">{{item.title}}</div>
</v-card-title>
<v-card-subtitle>{{item.updated}} ({{item.author}})</v-card-subtitle>
<v-card-text v-html="item.summary">
</v-card-text>
<v-card-actions>
<v-btn target="_new" :href="item.link">Complete story</v-btn>
</v-card-actions>
</v-card>
</v-timeline-item>
</v-timeline>
</v-card-text>
</v-card>
</v-container>
</template>
<script>
import { mapActions } from 'vuex';
export default {
name: "FeedViewer",
data () {
return {
timer: null,
feed: {}
}
},
methods: {
parse (text) {
const data = {items:[]};
const parser = new DOMParser();
const xml = parser.parseFromString(text, "application/xml");
const feed = xml.getElementsByTagNameNS("http://www.w3.org/2005/Atom", "feed")[0];
const entries = feed.getElementsByTagName("entry");
data.title = feed.getElementsByTagName("title")[0].childNodes[0].textContent;
data.updated = feed.getElementsByTagName("updated")[0].childNodes[0].textContent;
data.link = [...feed.getElementsByTagName("link")].filter(i =>
i.getAttribute("type") == "text/html"
).pop().getAttribute("href");
data.items = [...entries].map(entry => {
const item = {};
const link = entry.getElementsByTagName("link")[0];
if (link) {
item.link = link.getAttribute("href");
}
const author = entry.getElementsByTagName("author")[0];
if (author) {
const name = author.getElementsByTagName("name")[0];
item.author = name ? name.childNodes[0].textContent : author.innerHTML;
}
const summaries = entry.getElementsByTagName("summary");
const summary = [...summaries].find(i => i.getAttribute("type") == "xhtml") || summaries[0];
item.summary = summary.innerHTML;
item.title = entry.getElementsByTagName("title")[0].childNodes[0].textContent;
item.updated = entry.getElementsByTagName("updated")[0].childNodes[0].textContent;
return item;
});
return data;
},
async refresh () {
const text = await this.api([`/rss/?remote=${atob(this.$route.params.source)}`, {text:true}]);
this.feed = this.parse(text);
},
...mapActions(["api"])
},
async mounted () {
await this.refresh();
this.timer = setInterval(this.refresh, 300000);
},
unmounted () {
cancelInterval(this.timer);
this.timer = null;
}
}
</script>

View File

@@ -183,6 +183,9 @@ app.map({
get: [ mw.gis.navdata.get ] get: [ mw.gis.navdata.get ]
} }
}, },
'/rss/': {
get: [ mw.rss.get ]
}
// //
// '/user': { // '/user': {
// get: [ mw.user.get ], // get: [ mw.user.get ],

View File

@@ -12,5 +12,6 @@ module.exports = {
configuration: require('./configuration'), configuration: require('./configuration'),
info: require('./info'), info: require('./info'),
meta: require('./meta'), meta: require('./meta'),
openapi: require('./openapi') openapi: require('./openapi'),
rss: require('./rss')
}; };

View File

@@ -0,0 +1,21 @@
const fetch = require('node-fetch');
module.exports = async function (req, res, next) {
try {
if (req.query.remote) {
// We're being asked to fetch a remote feed
// NOTE: No, we don't limit what feeds the user can fetch
const r = await fetch(req.query.remote);
if (r && r.ok) {
res.set("Content-Type", "application/xml");
res.send(await r.text());
return;
}
}
res.status(400).send();
} catch (err) {
next(err);
}
}

View File

@@ -0,0 +1,4 @@
module.exports = {
get: require('./get')
}