mirror of
https://gitlab.com/wgp/dougal/software.git
synced 2025-12-06 13:17:08 +00:00
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:
@@ -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"))
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
103
lib/www/client/source/src/views/Feed.vue
Normal file
103
lib/www/client/source/src/views/Feed.vue
Normal 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>
|
||||||
@@ -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 ],
|
||||||
|
|||||||
@@ -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')
|
||||||
};
|
};
|
||||||
|
|||||||
21
lib/www/server/api/middleware/rss/get.js
Normal file
21
lib/www/server/api/middleware/rss/get.js
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
4
lib/www/server/api/middleware/rss/index.js
Normal file
4
lib/www/server/api/middleware/rss/index.js
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
|
||||||
|
module.exports = {
|
||||||
|
get: require('./get')
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user