Added settings and improved websocket management.

This commit is contained in:
Andrew Lalis 2022-05-25 13:15:21 +02:00
parent e1a756ffc9
commit 2a05e26d6d
8 changed files with 204 additions and 75 deletions

View File

@ -13,20 +13,22 @@ export class LinkToken {
} }
/** /**
* Gets the list of link tokens in a rail system. * Refreshes the list of link tokens in a rail system.
* @param {RailSystem} rs * @param {RailSystem} rs
* @return {Promise<LinkToken[]>} * @return {Promise}
*/ */
export function getLinkTokens(rs) { export function refreshLinkTokens(rs) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
axios.get(`${API_URL}/rs/${rs.id}/lt`) axios.get(`${API_URL}/rs/${rs.id}/lt`)
.then(response => { .then(response => {
resolve(response.data.map(obj => new LinkToken(obj))); rs.linkTokens = response.data;
}) resolve();
.catch(reject); })
}); .catch(reject);
});
} }
/** /**
* Creates a new link token. * Creates a new link token.
* @param {RailSystem} rs * @param {RailSystem} rs
@ -34,13 +36,14 @@ export function getLinkTokens(rs) {
* @return {Promise<string>} A promise that resolves to the token that was created. * @return {Promise<string>} A promise that resolves to the token that was created.
*/ */
export function createLinkToken(rs, data) { export function createLinkToken(rs, data) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
axios.post(`${API_URL}/rs/${rs.id}/lt`, data) axios.post(`${API_URL}/rs/${rs.id}/lt`, data)
.then(response => { .then(response => {
resolve(response.data.token); const token = response.data.token;
}) refreshLinkTokens(rs).then(() => resolve(token)).catch(reject);
.catch(reject); })
}); .catch(reject);
});
} }
/** /**
@ -51,7 +54,9 @@ export function createLinkToken(rs, data) {
export function deleteToken(rs, tokenId) { export function deleteToken(rs, tokenId) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
axios.delete(`${API_URL}/rs/${rs.id}/lt/${tokenId}`) axios.delete(`${API_URL}/rs/${rs.id}/lt/${tokenId}`)
.then(resolve) .then(() => {
refreshLinkTokens(rs).then(resolve).catch(reject);
})
.catch(reject); .catch(reject);
}); });
} }

View File

@ -7,8 +7,12 @@ export class RailSystem {
constructor(data) { constructor(data) {
this.id = data.id; this.id = data.id;
this.name = data.name; this.name = data.name;
this.settings = null;
this.segments = []; this.segments = [];
this.components = []; this.components = [];
this.linkTokens = [];
this.websocket = null; this.websocket = null;
this.selectedComponents = []; this.selectedComponents = [];
} }

View File

@ -0,0 +1,23 @@
import {API_URL} from "src/api/constants";
import axios from "axios";
export function refreshSettings(rs) {
return new Promise((resolve, reject) => {
axios.get(`${API_URL}/rs/${rs.id}/settings`)
.then(response => {
rs.settings = response.data;
resolve();
})
.catch(reject);
});
}
export function updateSettings(rs, newSettings) {
return new Promise((resolve, reject) => {
axios.post(`${API_URL}/rs/${rs.id}/settings`, newSettings)
.then(() => {
refreshSettings(rs).then(resolve).catch(reject);
})
.catch(reject);
});
}

View File

@ -1,39 +1,49 @@
import {WS_URL} from "./constants"; import { WS_URL } from "./constants";
const WS_RECONNECT_TIMEOUT = 3000;
/** /**
* Establishes a websocket connection to the given rail system. * Establishes a websocket connection to the given rail system.
* @param {RailSystem} rs * @param {RailSystem} rs
* @return {Promise} A promise that resolves when a connection is established.
*/ */
export function establishWebsocketConnection(rs) { export function establishWebsocketConnection(rs) {
closeWebsocketConnection(rs); return new Promise(resolve => {
rs.websocket = new WebSocket(`${WS_URL}/${rs.id}`); rs.websocket = new WebSocket(`${WS_URL}/${rs.id}`);
rs.websocket.onopen = () => { rs.websocket.onopen = resolve;
console.log("Opened websocket connection to rail system " + rs.id);
};
rs.websocket.onclose = event => { rs.websocket.onclose = event => {
if (event.code !== 1000) { if (event.code === 1000) {
console.warn("Lost websocket connection. Attempting to reestablish."); console.log(`Closed websocket connection to rail system "${rs.name}" (${rs.id})`);
setTimeout(() => { } else {
establishWebsocketConnection(rs); console.warn(`Unexpectedly lost websocket connection to rail system "${rs.name}" (${rs.id}). Attempting to reestablish in ${WS_RECONNECT_TIMEOUT} ms.`);
}, 3000); setTimeout(() => {
} establishWebsocketConnection(rs)
console.log("Closed websocket connection to rail system " + rs.id); .then(() => console.log("Successfully reestablished connection."));
}, WS_RECONNECT_TIMEOUT);
}
}; };
rs.websocket.onmessage = msg => { rs.websocket.onmessage = msg => {
console.log(msg); console.log(msg);
}; };
rs.websocket.onerror = error => { rs.websocket.onerror = error => {
console.log(error); console.log(error);
}; };
});
} }
/** /**
* Closes the websocket connection to a rail system, if possible. * Closes the websocket connection to a rail system, if possible.
* @param {RailSystem} rs * @param {RailSystem} rs
* @return {Promise} A promise that resolves when the connection is closed.
*/ */
export function closeWebsocketConnection(rs) { export function closeWebsocketConnection(rs) {
return new Promise(resolve => {
if (rs.websocket) { if (rs.websocket) {
rs.websocket.close(); rs.websocket.onclose = resolve;
rs.websocket = null; rs.websocket.close();
} else {
resolve();
} }
});
} }

View File

@ -1,12 +1,41 @@
<template> <template>
<q-list> <div class="row">
<link-tokens-view :rail-system="railSystem"/> <div class="col-sm-4">
</q-list> <q-list>
<q-item-label header>General Settings</q-item-label>
<q-item>
<q-item-section>
<q-item-label>Read Only</q-item-label>
<q-item-label caption>Freeze this rail system and prevent all updates.</q-item-label>
</q-item-section>
<q-item-section side>
<q-toggle v-model="settings.readOnly"/>
</q-item-section>
</q-item>
<q-item>
<q-item-section>
<q-item-label>Authentication Required</q-item-label>
<q-item-label caption>Require users to login to view and manage the system.</q-item-label>
</q-item-section>
<q-item-section side>
<q-toggle v-model="settings.authenticationRequired"/>
</q-item-section>
</q-item>
<q-separator/>
<link-tokens-view :rail-system="railSystem"/>
</q-list>
</div>
</div>
</template> </template>
<script> <script>
import { RailSystem } from "src/api/railSystems"; import { RailSystem } from "src/api/railSystems";
import LinkTokensView from "components/rs/settings/LinkTokensView.vue"; import LinkTokensView from "components/rs/settings/LinkTokensView.vue";
import { useQuasar } from "quasar";
import { updateSettings } from "src/api/settings";
export default { export default {
name: "SettingsView", name: "SettingsView",
@ -16,6 +45,44 @@ export default {
type: RailSystem, type: RailSystem,
required: true required: true
} }
},
setup() {
const quasar = useQuasar();
return {quasar};
},
data() {
return {
settings: {
readOnly: this.railSystem.settings.readOnly,
authenticationRequired: this.railSystem.settings.authenticationRequired
}
}
},
watch: {
settings: {
handler(newValue, oldValue) {
this.update();
},
deep: true
}
},
methods: {
update() {
updateSettings(this.railSystem, this.settings)
.then(() => {
this.quasar.notify({
color: "positive",
message: "Settings have been updated.",
closeBtn: true
});
})
.catch(error => {
this.quasar.notify({
color: "negative",
message: "Settings could not be updated: " + error.response.data.message
});
});
}
} }
}; };
</script> </script>

View File

@ -3,21 +3,32 @@
expand-separator expand-separator
label="Component Links" label="Component Links"
caption="Link components to your system." caption="Link components to your system."
@before-show="refreshLinkTokens"
switch-toggle-side switch-toggle-side
:content-inset-level="0.5" :content-inset-level="0.5"
> >
<q-list> <q-list>
<q-item <q-item
v-for="token in linkTokens" v-for="token in railSystem.linkTokens"
:key="token.id" :key="token.id"
> >
<q-item-section> <q-item-section>
<q-item-label>{{token.label}}</q-item-label> <q-item-label>{{token.label}}</q-item-label>
<q-item-label caption>{{token.id}}</q-item-label> <q-item-label caption>{{token.id}}</q-item-label>
</q-item-section> </q-item-section>
<q-item-section>
<q-item-label caption>Components</q-item-label>
<q-item-label>
<q-chip
v-for="component in token.components"
:key="component.id"
:label="component.name"
dense
size="sm"
/>
</q-item-label>
</q-item-section>
</q-item> </q-item>
<q-item v-if="linkTokens.length === 0"> <q-item v-if="railSystem.linkTokens.length === 0">
<q-item-section> <q-item-section>
<q-item-label caption>There are no link tokens. Add one via the <em>Add Link</em> button below.</q-item-label> <q-item-label caption>There are no link tokens. Add one via the <em>Add Link</em> button below.</q-item-label>
</q-item-section> </q-item-section>
@ -91,7 +102,7 @@
<script> <script>
import { RailSystem } from "src/api/railSystems"; import { RailSystem } from "src/api/railSystems";
import { createLinkToken, getLinkTokens } from "src/api/linkTokens"; import { createLinkToken } from "src/api/linkTokens";
import { useQuasar } from "quasar"; import { useQuasar } from "quasar";
export default { export default {
@ -108,7 +119,6 @@ export default {
}, },
data() { data() {
return { return {
linkTokens: [],
addTokenDialog: false, addTokenDialog: false,
addTokenData: { addTokenData: {
label: "", label: "",
@ -119,12 +129,6 @@ export default {
}; };
}, },
methods: { methods: {
refreshLinkTokens() {
getLinkTokens(this.railSystem)
.then(tokens => {
this.linkTokens = tokens;
});
},
getEligibleComponents() { getEligibleComponents() {
return this.railSystem.components.filter(component => { return this.railSystem.components.filter(component => {
return component.type === "SIGNAL" || component.type === "SEGMENT_BOUNDARY" || component.type === "SWITCH"; return component.type === "SIGNAL" || component.type === "SEGMENT_BOUNDARY" || component.type === "SWITCH";
@ -144,7 +148,7 @@ export default {
}); });
}, },
onReset() { onReset() {
this.addTokenData.name = ""; this.addTokenData.label = "";
this.addTokenData.selectedComponents.length = 0; this.addTokenData.selectedComponents.length = 0;
this.addTokenData.componentIds.length = 0; this.addTokenData.componentIds.length = 0;
this.token = null; this.token = null;

View File

@ -45,17 +45,19 @@ export default {
linkTokens: [] linkTokens: []
} }
}, },
async beforeRouteEnter(to, from, next) { beforeRouteEnter(to, from, next) {
const id = parseInt(to.params.id); const id = parseInt(to.params.id);
const rsStore = useRailSystemsStore(); const rsStore = useRailSystemsStore();
await rsStore.selectRailSystem(id); rsStore.selectRailSystem(id).then(() => {
next(vm => vm.railSystem = rsStore.selectedRailSystem); next(vm => vm.railSystem = rsStore.selectedRailSystem);
});
}, },
async beforeRouteUpdate(to, from) { beforeRouteUpdate(to, from) {
const id = parseInt(to.params.id); const id = parseInt(to.params.id);
const rsStore = useRailSystemsStore(); const rsStore = useRailSystemsStore();
await rsStore.selectRailSystem(id); rsStore.selectRailSystem(id).then(() => {
this.railSystem = rsStore.selectedRailSystem; this.railSystem = rsStore.selectedRailSystem;
});
} }
}; };
</script> </script>

View File

@ -1,8 +1,10 @@
import {defineStore} from "pinia"; import { defineStore } from "pinia";
import {refreshSegments} from "../api/segments" import { refreshSegments } from "../api/segments";
import {refreshComponents} from "../api/components"; import { refreshComponents } from "../api/components";
import {closeWebsocketConnection, establishWebsocketConnection} from "../api/websocket"; import { closeWebsocketConnection, establishWebsocketConnection } from "../api/websocket";
import { refreshRailSystems } from "src/api/railSystems"; import { refreshRailSystems } from "src/api/railSystems";
import { refreshLinkTokens } from "src/api/linkTokens";
import { refreshSettings } from "src/api/settings";
export const useRailSystemsStore = defineStore('RailSystemsStore', { export const useRailSystemsStore = defineStore('RailSystemsStore', {
state: () => ({ state: () => ({
@ -13,26 +15,38 @@ export const useRailSystemsStore = defineStore('RailSystemsStore', {
/** /**
* @type {RailSystem | null} * @type {RailSystem | null}
*/ */
selectedRailSystem: null, selectedRailSystem: null
loaded: false
}), }),
actions: { actions: {
async selectRailSystem(rsId) { /**
if (!this.loaded) { * Updates the selected rail system.
await refreshRailSystems(this); * @param rsId {Number} The new rail system id.
* @returns {Promise} A promise that resolves when the new rail system is
* fully loaded and ready.
*/
selectRailSystem(rsId) {
// Close any existing websocket connections prior to refreshing.
const wsClosePromises = [];
if (this.selectedRailSystem) {
wsClosePromises.push(closeWebsocketConnection(this.selectedRailSystem));
} }
this.railSystems.forEach(r => { return new Promise(resolve => {
r.components.length = 0; Promise.all(wsClosePromises).then(() => {
r.segments.length = 0; refreshRailSystems(this).then(() => {
closeWebsocketConnection(r); const rs = this.railSystems.find(r => r.id === rsId);
const updatePromises = [];
updatePromises.push(refreshSegments(rs));
updatePromises.push(refreshComponents(rs));
updatePromises.push(refreshLinkTokens(rs));
updatePromises.push(refreshSettings(rs));
updatePromises.push(establishWebsocketConnection(rs));
Promise.all(updatePromises).then(() => {
this.selectedRailSystem = rs;
resolve();
});
});
});
}); });
if (!rsId) return;
const rs = this.railSystems.find(r => r.id === rsId);
await refreshSegments(rs);
await refreshComponents(rs);
establishWebsocketConnection(rs);
this.selectedRailSystem = rs;
} }
}, },
getters: { getters: {