Skip to content

Commit

Permalink
feat(be): feat(fe): add websocket for task logs
Browse files Browse the repository at this point in the history
Additionally:
- Added link for running task, #481
- Added class Listenable and unit tests for it
  • Loading branch information
fiftin committed Nov 21, 2020
1 parent b76c6d3 commit d417f95
Show file tree
Hide file tree
Showing 21 changed files with 1,080 additions and 106 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ web/public/css/*.*
web/public/html/**/*.*
web/public/fonts/*.*
web2/dist
web2/.nyc_output
config.json
.DS_Store
node_modules/
Expand Down
886 changes: 816 additions & 70 deletions web2/package-lock.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions web2/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
"eslint-plugin-import": "^2.20.2",
"eslint-plugin-vue": "^6.2.2",
"node-sass": "^4.12.0",
"nyc": "^15.1.0",
"sass": "^1.19.0",
"sass-loader": "^8.0.2",
"vue-cli-plugin-vuetify": "~2.0.7",
Expand Down
35 changes: 29 additions & 6 deletions web2/src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
v-model="taskLogDialog"
save-button-text="Delete"
:max-width="800"
@close="onTaskLogDialogClosed()"
>
<template v-slot:title={}>
<router-link
Expand Down Expand Up @@ -486,6 +487,17 @@ export default {
await this.$router.push({ path: '/project/new' });
}
},
async $route(val) {
if (val.query.t == null) {
this.taskLogDialog = false;
} else {
const taskId = parseInt(this.$route.query.t || '', 10);
if (taskId) {
EventBus.$emit('i-show-task', { taskId });
}
}
},
},
computed: {
Expand All @@ -512,7 +524,7 @@ export default {
}
try {
await this.init();
await this.reloadData();
this.state = 'success';
} catch (err) { // notify about problem and sign out
EventBus.$emit('i-snackbar', {
Expand All @@ -535,7 +547,7 @@ export default {
});
EventBus.$on('i-session-create', async () => {
await this.init();
await this.reloadData();
await this.trySelectMostSuitableProject();
});
Expand All @@ -548,6 +560,11 @@ export default {
});
EventBus.$on('i-show-task', async (e) => {
if (parseInt(this.$route.query.t || '', 10) !== e.taskId) {
const query = { ...this.$route.query, t: e.taskId };
await this.$router.replace({ query });
}
this.task = (await axios({
method: 'get',
url: `/api/project/${this.projectId}/tasks/${e.taskId}`,
Expand Down Expand Up @@ -637,7 +654,12 @@ export default {
},
methods: {
async init() {
async onTaskLogDialogClosed() {
const query = { ...this.$route.query, t: undefined };
await this.$router.replace({ query });
},
async reloadData() {
await this.loadUserInfo();
await this.loadProjects();
Expand All @@ -649,9 +671,10 @@ export default {
}
if (this.$route.query.t) {
EventBus.$emit('i-show-task', {
itemId: parseInt(this.$route.query.t || '', 10),
});
const taskId = parseInt(this.$route.query.t || '', 10);
if (taskId) {
EventBus.$emit('i-show-task', { taskId });
}
}
},
Expand Down
1 change: 1 addition & 0 deletions web2/src/components/EditDialog.vue
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ export default {
EventBus.$emit(this.eventName, e);
}
}
this.$emit('close');
},
clearFlags() {
Expand Down
25 changes: 11 additions & 14 deletions web2/src/components/IndeterminateProgressCircular.vue
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,11 @@
}
</style>
<script>
class IndeterminateTimer {
import Listenable from '@/lib/Listenable';
class IndeterminateTimer extends Listenable {
constructor() {
this.listeners = {};
super();
this.direction = 1;
this.value = 0;
this.rotate = 0;
Expand Down Expand Up @@ -47,12 +49,9 @@ class IndeterminateTimer {
self.rotate %= 360;
}
Object.keys(self.listeners).forEach((id) => {
const listener = self.listeners[id];
listener({
value: self.value,
rotate: self.rotate,
});
self.callListeners({
value: self.value,
rotate: self.rotate,
});
}, 50);
}
Expand All @@ -62,17 +61,15 @@ class IndeterminateTimer {
}
addListener(callback) {
if (Object.keys(this.listeners).length === 0) {
if (!this.hasListeners()) {
this.start();
}
const id = Math.floor(Math.random() * 100000000);
this.listeners[id] = callback;
return id;
return super.addListener(callback);
}
removeListener(id) {
delete this.listeners[id];
if (Object.keys(this.listeners).length === 0) {
super.removeListener(id);
if (!this.hasListeners()) {
this.stop();
}
}
Expand Down
11 changes: 11 additions & 0 deletions web2/src/components/TaskForm.vue
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,17 @@ export default {
props: {
templateId: Number,
},
watch: {
needReset(val) {
if (val) {
this.item.template_id = this.templateId;
}
},
templateId(val) {
this.item.template_id = val;
},
},
created() {
this.item.template_id = this.templateId;
},
Expand Down
27 changes: 25 additions & 2 deletions web2/src/components/TaskLogView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@
</v-container>
<div class="task-log-view">
<div class="task-log-view__record" v-for="record in output" :key="record.id">
<div class="task-log-view__time">{{ record.time }}</div>
<div class="task-log-view__time">{{ record.time | formatTime }}</div>
<div class="task-log-view__output">{{ record.output }}</div>
</div>
</div>
Expand All @@ -69,7 +69,7 @@
}
.task-log-view__time {
width: 250px;
width: 150px;
}
.task-log-view__output {
Expand All @@ -79,6 +79,7 @@
<script>
import axios from 'axios';
import TaskStatus from '@/components/TaskStatus.vue';
import socket from '@/socket';
export default {
components: { TaskStatus },
Expand All @@ -105,15 +106,37 @@ export default {
},
},
async created() {
socket.addListener((data) => this.onDataReceive(data));
await this.loadData();
},
methods: {
reset() {
this.item = {};
this.output = [];
this.user = {};
},
onDataReceive(data) {
if (data.project_id !== this.projectId || data.task_id !== this.itemId) {
return;
}
switch (data.type) {
case 'update':
Object.assign(this.item, {
...data,
type: undefined,
});
break;
case 'log':
this.output.push(data);
break;
default:
break;
}
},
async loadData() {
this.item = (await axios({
method: 'get',
Expand Down
2 changes: 1 addition & 1 deletion web2/src/components/TaskStatus.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<template>
<v-chip style="font-weight: bold;" :color="getStatusColor(status)">
<v-chip v-if="status" style="font-weight: bold;" :color="getStatusColor(status)">
<v-icon v-if="status !== 'running'" left>{{ getStatusIcon(status) }}</v-icon>
<IndeterminateProgressCircular v-else style="margin-left: -5px;" />
{{ humanizeStatus(status) }}
Expand Down
31 changes: 31 additions & 0 deletions web2/src/lib/Listenable.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
export default class Listenable {
constructor() {
this.listeners = {};
}

addListener(callback) {
// eslint-disable-next-line symbol-description
const id = Symbol();
this.listeners[id] = callback;
return id;
}

removeListener(id) {
if (this.listeners[id] == null) {
return false;
}
delete this.listeners[id];
return true;
}

callListeners(data) {
Object.getOwnPropertySymbols(this.listeners).forEach((id) => {
const listener = this.listeners[id];
listener(data);
});
}

hasListeners() {
return Object.keys(this.listeners).length > 0;
}
}
30 changes: 30 additions & 0 deletions web2/src/lib/PubSub.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import Listenable from '@/lib/Listenable';

export default class PubSub {
constructor() {
this.topics = {};
}

subscribe(topic, callback) {
if (this.topics[topic] == null) {
this.topics[topic] = new Listenable();
}
return this.topics[topic].addListener(callback);
}

unsubscribe(id) {
// eslint-disable-next-line no-restricted-syntax
for (const topic in this.topics) {
if (this.topics[topic].removeListener(id)) {
break;
}
}
}

publish(topic, data) {
if (!this.topics[topic]) {
return;
}
this.topics[topic].callListeners(data);
}
}
48 changes: 48 additions & 0 deletions web2/src/lib/Socket.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import Listenable from '@/lib/Listenable';

export default class Socket extends Listenable {
constructor(websocketCreator) {
super();
this.websocketCreator = websocketCreator;
}

start() {
if (this.ws != null) {
throw new Error('Websocket already started. Please stop it before starting.');
}
this.ws = this.websocketCreator();
this.ws.onclose = () => {
setTimeout(() => {
this.start();
}, 2000);
};
this.ws.onmessage = ({ data }) => {
try {
this.callListeners(JSON.parse(data));
} catch (e) {
console.error(e);
}
};
}

stop() {
this.ws.close();
delete this.ws;
}

addListener(callback) {
const isFirstListener = !this.hasListeners();
const listenerId = super.addListener(callback);
if (isFirstListener) {
this.start();
}
return listenerId;
}

removeListener(id) {
super.removeListener(id);
if (!this.hasListeners()) {
this.stop();
}
}
}
1 change: 1 addition & 0 deletions web2/src/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import vuetify from './plugins/vuetify';
Vue.config.productionTip = false;

Vue.filter('formatDate', (value) => (value ? moment(String(value)).fromNow() : '—'));
Vue.filter('formatTime', (value) => (value ? moment(String(value)).format('LTS') : '—'));
Vue.filter('formatMilliseconds', (value) => (value ? moment.duration(parseInt(value, 10), 'milliseconds').humanize() : undefined));

new Vue({
Expand Down
6 changes: 6 additions & 0 deletions web2/src/socket.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import Socket from '@/lib/Socket';

export default new Socket(() => {
const protocol = document.location.protocol === 'https:' ? 'wss' : 'ws';
return new WebSocket(`${protocol}://${document.location.host}/api/ws`);
});
2 changes: 2 additions & 0 deletions web2/src/views/project/Inventory.vue
Original file line number Diff line number Diff line change
Expand Up @@ -71,9 +71,11 @@
</template>
<script>
import ItemListPageBase from '@/components/ItemListPageBase';
import InventoryForm from '@/components/InventoryForm.vue';
export default {
mixins: [ItemListPageBase],
components: { InventoryForm },
methods: {
getHeaders() {
return [{
Expand Down
Loading

0 comments on commit d417f95

Please sign in to comment.