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
* @return {Promise<LinkToken[]>}
* @return {Promise}
*/
export function getLinkTokens(rs) {
export function refreshLinkTokens(rs) {
return new Promise((resolve, reject) => {
axios.get(`${API_URL}/rs/${rs.id}/lt`)
.then(response => {
resolve(response.data.map(obj => new LinkToken(obj)));
rs.linkTokens = response.data;
resolve();
})
.catch(reject);
});
}
/**
* Creates a new link token.
* @param {RailSystem} rs
@ -37,7 +39,8 @@ export function createLinkToken(rs, data) {
return new Promise((resolve, reject) => {
axios.post(`${API_URL}/rs/${rs.id}/lt`, data)
.then(response => {
resolve(response.data.token);
const token = response.data.token;
refreshLinkTokens(rs).then(() => resolve(token)).catch(reject);
})
.catch(reject);
});
@ -51,7 +54,9 @@ export function createLinkToken(rs, data) {
export function deleteToken(rs, tokenId) {
return new Promise((resolve, reject) => {
axios.delete(`${API_URL}/rs/${rs.id}/lt/${tokenId}`)
.then(resolve)
.then(() => {
refreshLinkTokens(rs).then(resolve).catch(reject);
})
.catch(reject);
});
}

View File

@ -7,8 +7,12 @@ export class RailSystem {
constructor(data) {
this.id = data.id;
this.name = data.name;
this.settings = null;
this.segments = [];
this.components = [];
this.linkTokens = [];
this.websocket = null;
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,23 +1,26 @@
import {WS_URL} from "./constants";
import { WS_URL } from "./constants";
const WS_RECONNECT_TIMEOUT = 3000;
/**
* Establishes a websocket connection to the given rail system.
* @param {RailSystem} rs
* @return {Promise} A promise that resolves when a connection is established.
*/
export function establishWebsocketConnection(rs) {
closeWebsocketConnection(rs);
return new Promise(resolve => {
rs.websocket = new WebSocket(`${WS_URL}/${rs.id}`);
rs.websocket.onopen = () => {
console.log("Opened websocket connection to rail system " + rs.id);
};
rs.websocket.onopen = resolve;
rs.websocket.onclose = event => {
if (event.code !== 1000) {
console.warn("Lost websocket connection. Attempting to reestablish.");
if (event.code === 1000) {
console.log(`Closed websocket connection to rail system "${rs.name}" (${rs.id})`);
} else {
console.warn(`Unexpectedly lost websocket connection to rail system "${rs.name}" (${rs.id}). Attempting to reestablish in ${WS_RECONNECT_TIMEOUT} ms.`);
setTimeout(() => {
establishWebsocketConnection(rs);
}, 3000);
establishWebsocketConnection(rs)
.then(() => console.log("Successfully reestablished connection."));
}, WS_RECONNECT_TIMEOUT);
}
console.log("Closed websocket connection to rail system " + rs.id);
};
rs.websocket.onmessage = msg => {
console.log(msg);
@ -25,15 +28,22 @@ export function establishWebsocketConnection(rs) {
rs.websocket.onerror = error => {
console.log(error);
};
});
}
/**
* Closes the websocket connection to a rail system, if possible.
* @param {RailSystem} rs
* @return {Promise} A promise that resolves when the connection is closed.
*/
export function closeWebsocketConnection(rs) {
return new Promise(resolve => {
if (rs.websocket) {
rs.websocket.onclose = resolve;
rs.websocket.close();
rs.websocket = null;
} else {
resolve();
}
});
}

View File

@ -1,12 +1,41 @@
<template>
<div class="row">
<div class="col-sm-4">
<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>
<script>
import { RailSystem } from "src/api/railSystems";
import LinkTokensView from "components/rs/settings/LinkTokensView.vue";
import { useQuasar } from "quasar";
import { updateSettings } from "src/api/settings";
export default {
name: "SettingsView",
@ -16,6 +45,44 @@ export default {
type: RailSystem,
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>

View File

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

View File

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

View File

@ -1,8 +1,10 @@
import {defineStore} from "pinia";
import {refreshSegments} from "../api/segments"
import {refreshComponents} from "../api/components";
import {closeWebsocketConnection, establishWebsocketConnection} from "../api/websocket";
import { defineStore } from "pinia";
import { refreshSegments } from "../api/segments";
import { refreshComponents } from "../api/components";
import { closeWebsocketConnection, establishWebsocketConnection } from "../api/websocket";
import { refreshRailSystems } from "src/api/railSystems";
import { refreshLinkTokens } from "src/api/linkTokens";
import { refreshSettings } from "src/api/settings";
export const useRailSystemsStore = defineStore('RailSystemsStore', {
state: () => ({
@ -13,26 +15,38 @@ export const useRailSystemsStore = defineStore('RailSystemsStore', {
/**
* @type {RailSystem | null}
*/
selectedRailSystem: null,
loaded: false
selectedRailSystem: null
}),
actions: {
async selectRailSystem(rsId) {
if (!this.loaded) {
await refreshRailSystems(this);
/**
* Updates the selected rail system.
* @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 => {
r.components.length = 0;
r.segments.length = 0;
closeWebsocketConnection(r);
});
if (!rsId) return;
return new Promise(resolve => {
Promise.all(wsClosePromises).then(() => {
refreshRailSystems(this).then(() => {
const rs = this.railSystems.find(r => r.id === rsId);
await refreshSegments(rs);
await refreshComponents(rs);
establishWebsocketConnection(rs);
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();
});
});
});
});
}
},
getters: {