Cleaned up JS and improved link token stuff.
This commit is contained in:
parent
4abd39cbe1
commit
ed3f6bd6b9
|
@ -0,0 +1,80 @@
|
||||||
|
import axios from "axios";
|
||||||
|
import {API_URL} from "./constants";
|
||||||
|
|
||||||
|
export function refreshComponents(rs) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
axios.get(`${API_URL}/rs/${rs.id}/c`)
|
||||||
|
.then(response => {
|
||||||
|
const previousSelectedComponentId = rs.selectedComponent ? rs.selectedComponent.id : null;
|
||||||
|
rs.components = response.data;
|
||||||
|
if (previousSelectedComponentId !== null) {
|
||||||
|
const previousComponent = rs.components.find(c => c.id === previousSelectedComponentId);
|
||||||
|
if (previousComponent) {
|
||||||
|
rs.selectedComponent = previousComponent;
|
||||||
|
} else {
|
||||||
|
rs.selectedComponent = null;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
rs.selectedComponent = null;
|
||||||
|
}
|
||||||
|
resolve();
|
||||||
|
})
|
||||||
|
.catch(reject);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function refreshSomeComponents(rs, components) {
|
||||||
|
const promises = [];
|
||||||
|
for (let i = 0; i < components.length; i++) {
|
||||||
|
const promise = new Promise((resolve, reject) => {
|
||||||
|
axios.get(`${API_URL}/rs/${rs.id}/c/${components[i].id}`)
|
||||||
|
.then(resp => {
|
||||||
|
const idx = rs.components.findIndex(c => c.id === resp.data.id);
|
||||||
|
if (idx > -1) rs.components[idx] = resp.data;
|
||||||
|
resolve();
|
||||||
|
})
|
||||||
|
.catch(reject);
|
||||||
|
});
|
||||||
|
promises.push(promise);
|
||||||
|
}
|
||||||
|
return Promise.all(promises);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getComponent(rs, id) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
axios.get(`${this.apiUrl}/rs/${rs.id}/c/${id}`)
|
||||||
|
.then(response => resolve(response.data))
|
||||||
|
.catch(reject);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createComponent(rs, data) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
axios.post(`${API_URL}/rs/${rs.id}/c`, data)
|
||||||
|
.then(response => {
|
||||||
|
const newComponentId = response.data.id;
|
||||||
|
refreshComponents(rs)
|
||||||
|
.then(() => {
|
||||||
|
const newComponent = rs.components.find(c => c.id === newComponentId);
|
||||||
|
if (newComponent) {
|
||||||
|
rs.selectedComponent = newComponent;
|
||||||
|
}
|
||||||
|
resolve();
|
||||||
|
})
|
||||||
|
.catch(reject);
|
||||||
|
})
|
||||||
|
.catch(reject);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function removeComponent(rs, id) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
axios.delete(`${API_URL}/rs/${rs.id}/c/${id}`)
|
||||||
|
.then(() => {
|
||||||
|
refreshComponents(rs)
|
||||||
|
.then(resolve)
|
||||||
|
.catch(reject);
|
||||||
|
})
|
||||||
|
.catch(reject);
|
||||||
|
});
|
||||||
|
}
|
|
@ -0,0 +1,2 @@
|
||||||
|
export const API_URL = import.meta.env.VITE_API_URL;
|
||||||
|
export const WS_URL = import.meta.env.VITE_WS_URL;
|
|
@ -0,0 +1,44 @@
|
||||||
|
import axios from "axios";
|
||||||
|
import {API_URL} from "./constants";
|
||||||
|
import {refreshSomeComponents} from "./components";
|
||||||
|
|
||||||
|
export function updateConnections(rs, node) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
axios.patch(
|
||||||
|
`${API_URL}/rs/${rs.id}/c/${node.id}/connectedNodes`,
|
||||||
|
node
|
||||||
|
)
|
||||||
|
.then(response => {
|
||||||
|
node.connectedNodes = response.data.connectedNodes;
|
||||||
|
resolve();
|
||||||
|
})
|
||||||
|
.catch(reject);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function addConnection(rs, node, other) {
|
||||||
|
node.connectedNodes.push(other);
|
||||||
|
return updateConnections(rs, node);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function removeConnection(rs, node, other) {
|
||||||
|
const idx = node.connectedNodes.findIndex(n => n.id === other.id);
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
if (idx > -1) {
|
||||||
|
node.connectedNodes.splice(idx, 1);
|
||||||
|
updateConnections(rs, node)
|
||||||
|
.then(() => {
|
||||||
|
const nodes = [];
|
||||||
|
nodes.push(...node.connectedNodes);
|
||||||
|
nodes.push(other);
|
||||||
|
refreshSomeComponents(rs, nodes)
|
||||||
|
.then(resolve)
|
||||||
|
.catch(reject);
|
||||||
|
})
|
||||||
|
.catch(reject);
|
||||||
|
} else {
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,38 @@
|
||||||
|
import axios from "axios";
|
||||||
|
import {API_URL} from "./constants";
|
||||||
|
|
||||||
|
export function refreshRailSystems(rsStore) {
|
||||||
|
return new Promise(resolve => {
|
||||||
|
axios.get(`${API_URL}/rs`)
|
||||||
|
.then(response => {
|
||||||
|
rsStore.railSystems = response.data;
|
||||||
|
resolve();
|
||||||
|
})
|
||||||
|
.catch(error => console.error(error));
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createRailSystem(rsStore, name) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
axios.post(`${API_URL}/rs`, {name: name})
|
||||||
|
.then(response => {
|
||||||
|
const newId = response.data.id;
|
||||||
|
refreshRailSystems(rsStore)
|
||||||
|
.then(() => resolve(rsStore.railSystems.find(rs => rs.id === newId)))
|
||||||
|
.catch(error => reject(error));
|
||||||
|
})
|
||||||
|
.catch(error => reject(error));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function removeRailSystem(rsStore, id) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
axios.delete(`${API_URL}/rs/${id}`)
|
||||||
|
.then(() => {
|
||||||
|
rsStore.selectedRailSystem = null;
|
||||||
|
refreshRailSystems(rsStore)
|
||||||
|
.then(() => resolve)
|
||||||
|
.catch(error => reject(error));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
|
@ -0,0 +1,50 @@
|
||||||
|
import axios from "axios";
|
||||||
|
import {API_URL} from "./constants";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches the set of segments for a rail system.
|
||||||
|
* @param {Number} rsId
|
||||||
|
* @returns {Promise<[Object]>}
|
||||||
|
*/
|
||||||
|
export function getSegments(rsId) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
axios.get(`${API_URL}/rs/${rsId}/s`)
|
||||||
|
.then(response => resolve(response.data))
|
||||||
|
.catch(error => reject(error));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function refreshSegments(rs) {
|
||||||
|
return new Promise(resolve => {
|
||||||
|
getSegments(rs.id)
|
||||||
|
.then(segments => {
|
||||||
|
rs.segments = segments;
|
||||||
|
resolve();
|
||||||
|
})
|
||||||
|
.catch(error => console.error(error));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createSegment(rs, name) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
axios.post(`${API_URL}/rs/${rs.id}/s`, {name: name})
|
||||||
|
.then(() => {
|
||||||
|
refreshSegments(rs)
|
||||||
|
.then(() => resolve())
|
||||||
|
.catch(error => reject(error));
|
||||||
|
})
|
||||||
|
.catch(error => reject(error));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function removeSegment(rs, segmentId) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
axios.delete(`${API_URL}/rs/${rs.id}/${segmentId}`)
|
||||||
|
.then(() => {
|
||||||
|
refreshSegments(rs)
|
||||||
|
.then(() => resolve())
|
||||||
|
.catch(error => reject(error));
|
||||||
|
})
|
||||||
|
.catch(error => reject(error));
|
||||||
|
});
|
||||||
|
}
|
|
@ -0,0 +1,33 @@
|
||||||
|
import {WS_URL} from "./constants";
|
||||||
|
|
||||||
|
export function establishWebsocketConnection(rs) {
|
||||||
|
if (rs.websocket) {
|
||||||
|
rs.websocket.close();
|
||||||
|
}
|
||||||
|
rs.websocket = new WebSocket(`${WS_URL}/${rs.id}`);
|
||||||
|
rs.websocket.onopen = () => {
|
||||||
|
console.log("Opened websocket connection to rail system " + rs.id);
|
||||||
|
};
|
||||||
|
rs.websocket.onclose = event => {
|
||||||
|
if (event.code !== 1000) {
|
||||||
|
console.warn("Lost websocket connection. Attempting to reestablish.");
|
||||||
|
setTimeout(() => {
|
||||||
|
establishWebsocketConnection(rs);
|
||||||
|
}, 3000);
|
||||||
|
}
|
||||||
|
console.log("Closed websocket connection to rail system " + rs.id);
|
||||||
|
};
|
||||||
|
rs.websocket.onmessage = msg => {
|
||||||
|
console.log(msg);
|
||||||
|
};
|
||||||
|
rs.websocket.onerror = error => {
|
||||||
|
console.log(error);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function closeWebsocketConnection(rs) {
|
||||||
|
if (rs.websocket) {
|
||||||
|
rs.websocket.close();
|
||||||
|
rs.websocket = null;
|
||||||
|
}
|
||||||
|
}
|
|
@ -23,7 +23,7 @@
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item me-2" v-if="rsStore.selectedRailSystem !== null">
|
<li class="nav-item me-2" v-if="rsStore.selectedRailSystem !== null">
|
||||||
<button
|
<button
|
||||||
@click="removeRailSystem()"
|
@click="remove()"
|
||||||
class="btn btn-danger btn-sm"
|
class="btn btn-danger btn-sm"
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
|
@ -57,6 +57,7 @@
|
||||||
import {useRailSystemsStore} from "../stores/railSystemsStore";
|
import {useRailSystemsStore} from "../stores/railSystemsStore";
|
||||||
import AddRailSystemModal from "./railsystem/AddRailSystemModal.vue";
|
import AddRailSystemModal from "./railsystem/AddRailSystemModal.vue";
|
||||||
import ConfirmModal from "./ConfirmModal.vue";
|
import ConfirmModal from "./ConfirmModal.vue";
|
||||||
|
import {refreshRailSystems, removeRailSystem} from "../api/railSystems";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "AppNavbar",
|
name: "AppNavbar",
|
||||||
|
@ -68,12 +69,12 @@ export default {
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.rsStore.refreshRailSystems();
|
refreshRailSystems(this.rsStore);
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
removeRailSystem() {
|
remove() {
|
||||||
this.$refs.confirmModal.showConfirm()
|
this.$refs.confirmModal.showConfirm()
|
||||||
.then(() => this.rsStore.removeRailSystem(this.rsStore.selectedRailSystem));
|
.then(() => removeRailSystem(this.rsStore, this.rsStore.selectedRailSystem.id));
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -36,6 +36,7 @@
|
||||||
<script>
|
<script>
|
||||||
import {useRailSystemsStore} from "../../stores/railSystemsStore";
|
import {useRailSystemsStore} from "../../stores/railSystemsStore";
|
||||||
import {Modal} from "bootstrap";
|
import {Modal} from "bootstrap";
|
||||||
|
import {createRailSystem} from "../../api/railSystems";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "AddRailSystem",
|
name: "AddRailSystem",
|
||||||
|
@ -55,7 +56,7 @@ export default {
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
formSubmitted() {
|
formSubmitted() {
|
||||||
this.rsStore.createRailSystem(this.formData.rsName)
|
createRailSystem(this.rsStore, this.formData.rsName)
|
||||||
.then(rs => {
|
.then(rs => {
|
||||||
this.formData.rsName = "";
|
this.formData.rsName = "";
|
||||||
const modal = Modal.getInstance(document.getElementById("addRailSystemModal"));
|
const modal = Modal.getInstance(document.getElementById("addRailSystemModal"));
|
||||||
|
|
|
@ -47,6 +47,7 @@
|
||||||
<script>
|
<script>
|
||||||
import {useRailSystemsStore} from "../../stores/railSystemsStore";
|
import {useRailSystemsStore} from "../../stores/railSystemsStore";
|
||||||
import {Modal} from "bootstrap";
|
import {Modal} from "bootstrap";
|
||||||
|
import {createSegment} from "../../api/segments";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "AddSegmentModal",
|
name: "AddSegmentModal",
|
||||||
|
@ -67,7 +68,7 @@ export default {
|
||||||
methods: {
|
methods: {
|
||||||
formSubmitted() {
|
formSubmitted() {
|
||||||
const modal = Modal.getInstance(document.getElementById("addSegmentModal"));
|
const modal = Modal.getInstance(document.getElementById("addSegmentModal"));
|
||||||
this.rsStore.addSegment(this.formData.name)
|
createSegment(this.rsStore.selectedRailSystem, this.formData.name)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.formData.name = "";
|
this.formData.name = "";
|
||||||
modal.hide();
|
modal.hide();
|
||||||
|
|
|
@ -1,46 +1,57 @@
|
||||||
<template>
|
<template>
|
||||||
<h3>Rail System: <em>{{railSystem.name}}</em></h3>
|
<h3>Rail System: <em>{{railSystem.name}}</em></h3>
|
||||||
<SegmentsView />
|
<SegmentsView :segments="railSystem.segments" v-if="railSystem.segments"/>
|
||||||
<button
|
<div class="dropdown">
|
||||||
type="button"
|
<button class="btn btn-success btn-sm dropdown-toggle" type="button" id="railSystemAddComponentsToggle" data-bs-toggle="dropdown" aria-expanded="false">
|
||||||
class="btn btn-success btn-sm me-2"
|
Add Component
|
||||||
data-bs-toggle="modal"
|
</button>
|
||||||
data-bs-target="#addSegmentModal"
|
<ul class="dropdown-menu" aria-labelledby="railSystemAddComponentsToggle">
|
||||||
>
|
<li>
|
||||||
Add Segment
|
<button
|
||||||
</button>
|
type="button"
|
||||||
|
class="dropdown-item"
|
||||||
|
data-bs-toggle="modal"
|
||||||
|
data-bs-target="#addSegmentModal"
|
||||||
|
>
|
||||||
|
Add Segment
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li v-if="addSignalAllowed()">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="dropdown-item"
|
||||||
|
data-bs-toggle="modal"
|
||||||
|
data-bs-target="#addSignalModal"
|
||||||
|
>
|
||||||
|
Add Signal
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li v-if="addSegmentBoundaryAllowed()">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="dropdown-item"
|
||||||
|
data-bs-toggle="modal"
|
||||||
|
data-bs-target="#addSegmentBoundaryModal"
|
||||||
|
>
|
||||||
|
Add Segment Boundary
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li v-if="addSwitchAllowed()">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="dropdown-item"
|
||||||
|
data-bs-toggle="modal"
|
||||||
|
data-bs-target="#addSwitchModal"
|
||||||
|
>
|
||||||
|
Add Switch
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
<AddSegmentModal />
|
<AddSegmentModal />
|
||||||
<span v-if="railSystem.segments && railSystem.segments.length > 0">
|
<AddSignalModal v-if="addSignalAllowed()" />
|
||||||
<button
|
<AddSegmentBoundaryModal v-if="addSegmentBoundaryAllowed()" />
|
||||||
type="button"
|
<AddSwitchModal v-if="addSwitchAllowed()" />
|
||||||
class="btn btn-success btn-sm me-2"
|
|
||||||
data-bs-toggle="modal"
|
|
||||||
data-bs-target="#addSignalModal"
|
|
||||||
>
|
|
||||||
Add Signal
|
|
||||||
</button>
|
|
||||||
<AddSignalModal />
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="btn btn-success btn-sm me-2"
|
|
||||||
data-bs-toggle="modal"
|
|
||||||
data-bs-target="#addSegmentBoundaryModal"
|
|
||||||
>
|
|
||||||
Add Segment Boundary
|
|
||||||
</button>
|
|
||||||
<AddSegmentBoundaryModal />
|
|
||||||
</span>
|
|
||||||
<span v-if="railSystem.components && railSystem.components.length > 1">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="btn btn-success btn-sm"
|
|
||||||
data-bs-toggle="modal"
|
|
||||||
data-bs-target="#addSwitchModal"
|
|
||||||
>
|
|
||||||
Add Switch
|
|
||||||
</button>
|
|
||||||
<AddSwitchModal />
|
|
||||||
</span>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
@ -64,6 +75,17 @@ export default {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: true
|
required: true
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
addSignalAllowed() {
|
||||||
|
return this.railSystem.segments && this.railSystem.segments.length > 0
|
||||||
|
},
|
||||||
|
addSegmentBoundaryAllowed() {
|
||||||
|
return this.railSystem.segments && this.railSystem.segments.length > 1
|
||||||
|
},
|
||||||
|
addSwitchAllowed() {
|
||||||
|
return this.railSystem.components && this.railSystem.components.length > 1
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -1,26 +1,47 @@
|
||||||
<template>
|
<template>
|
||||||
<h5>Segments</h5>
|
<h5>Segments</h5>
|
||||||
|
<input type="search" class="form-control-sm w-100 mb-1" placeholder="Filter by name" v-model="segmentNameFilter" />
|
||||||
<ul class="list-group overflow-auto mb-2" style="max-height: 200px;">
|
<ul class="list-group overflow-auto mb-2" style="max-height: 200px;">
|
||||||
<li
|
<li
|
||||||
v-for="segment in rsStore.selectedRailSystem.segments"
|
v-for="segment in filteredSegments()"
|
||||||
:key="segment.id"
|
:key="segment.id"
|
||||||
class="list-group-item"
|
class="list-group-item"
|
||||||
>
|
>
|
||||||
{{segment.name}}
|
{{segment.name}}
|
||||||
<button @click.prevent="rsStore.removeSegment(segment.id)" class="btn btn-sm btn-danger float-end">Remove</button>
|
<button @click.prevent="removeSegment(rsStore.selectedRailSystem, segment.id)" class="btn btn-sm btn-danger float-end">Remove</button>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import {useRailSystemsStore} from "../../stores/railSystemsStore";
|
import {useRailSystemsStore} from "../../stores/railSystemsStore";
|
||||||
|
import {removeSegment} from "../../api/segments";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "SegmentsView.vue",
|
name: "SegmentsView.vue",
|
||||||
setup() {
|
setup() {
|
||||||
const rsStore = useRailSystemsStore();
|
const rsStore = useRailSystemsStore();
|
||||||
return {
|
return {
|
||||||
rsStore
|
rsStore,
|
||||||
|
removeSegment
|
||||||
|
}
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
segments: {
|
||||||
|
type: Array,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
segmentNameFilter: null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
filteredSegments() {
|
||||||
|
if (this.segmentNameFilter === null || this.segmentNameFilter.trim().length === 0) return this.segments;
|
||||||
|
const filterString = this.segmentNameFilter.trim().toLowerCase();
|
||||||
|
return this.segments.filter(segment => segment.name.toLowerCase().includes(filterString));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -64,6 +64,7 @@
|
||||||
<script>
|
<script>
|
||||||
import {useRailSystemsStore} from "../../../stores/railSystemsStore";
|
import {useRailSystemsStore} from "../../../stores/railSystemsStore";
|
||||||
import {Modal} from "bootstrap";
|
import {Modal} from "bootstrap";
|
||||||
|
import {createComponent} from "../../../api/components";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "AddSegmentBoundaryModal",
|
name: "AddSegmentBoundaryModal",
|
||||||
|
@ -94,7 +95,7 @@ export default {
|
||||||
formSubmitted() {
|
formSubmitted() {
|
||||||
const modal = Modal.getInstance(document.getElementById("addSegmentBoundaryModal"));
|
const modal = Modal.getInstance(document.getElementById("addSegmentBoundaryModal"));
|
||||||
this.formData.segments = [this.formData.segmentA, this.formData.segmentB];
|
this.formData.segments = [this.formData.segmentA, this.formData.segmentB];
|
||||||
this.rsStore.addComponent(this.formData)
|
createComponent(this.rsStore.selectedRailSystem, this.formData)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
modal.hide();
|
modal.hide();
|
||||||
})
|
})
|
||||||
|
|
|
@ -53,6 +53,7 @@
|
||||||
<script>
|
<script>
|
||||||
import {useRailSystemsStore} from "../../../stores/railSystemsStore";
|
import {useRailSystemsStore} from "../../../stores/railSystemsStore";
|
||||||
import {Modal} from "bootstrap";
|
import {Modal} from "bootstrap";
|
||||||
|
import {createComponent} from "../../../api/components";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "AddSignalModal",
|
name: "AddSignalModal",
|
||||||
|
@ -80,7 +81,7 @@ export default {
|
||||||
methods: {
|
methods: {
|
||||||
formSubmitted() {
|
formSubmitted() {
|
||||||
const modal = Modal.getInstance(document.getElementById("addSignalModal"));
|
const modal = Modal.getInstance(document.getElementById("addSignalModal"));
|
||||||
this.rsStore.addComponent(this.formData)
|
createComponent(this.rsStore.selectedRailSystem, this.formData)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
modal.hide();
|
modal.hide();
|
||||||
})
|
})
|
||||||
|
|
|
@ -56,6 +56,7 @@
|
||||||
<script>
|
<script>
|
||||||
import {useRailSystemsStore} from "../../../stores/railSystemsStore";
|
import {useRailSystemsStore} from "../../../stores/railSystemsStore";
|
||||||
import {Modal} from "bootstrap";
|
import {Modal} from "bootstrap";
|
||||||
|
import {createComponent} from "../../../api/components";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "AddSwitchModal",
|
name: "AddSwitchModal",
|
||||||
|
@ -84,7 +85,7 @@ export default {
|
||||||
methods: {
|
methods: {
|
||||||
formSubmitted() {
|
formSubmitted() {
|
||||||
const modal = Modal.getInstance(document.getElementById("addSwitchModal"));
|
const modal = Modal.getInstance(document.getElementById("addSwitchModal"));
|
||||||
this.rsStore.addComponent(this.formData)
|
createComponent(this.rsStore.selectedRailSystem, this.formData)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
modal.hide();
|
modal.hide();
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<h3>{{component.name}}</h3>
|
<h3>{{component.name}}</h3>
|
||||||
<small class="text-muted">{{component.type}}</small>
|
<small class="text-muted">
|
||||||
|
{{component.type}}
|
||||||
|
</small>
|
||||||
<table class="table">
|
<table class="table">
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
|
@ -21,16 +23,13 @@
|
||||||
</table>
|
</table>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
|
||||||
<th>Online</th><td>{{component.online}}</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
<SignalComponentView v-if="component.type === 'SIGNAL'" :signal="component" />
|
<SignalComponentView v-if="component.type === 'SIGNAL'" :signal="component" />
|
||||||
<SegmentBoundaryNodeComponentView v-if="component.type === 'SEGMENT_BOUNDARY'" :node="component" />
|
<SegmentBoundaryNodeComponentView v-if="component.type === 'SEGMENT_BOUNDARY'" :node="component" />
|
||||||
<SwitchComponentView v-if="component.type === 'SWITCH'" :sw="component"/>
|
<SwitchComponentView v-if="component.type === 'SWITCH'" :sw="component"/>
|
||||||
<PathNodeComponentView v-if="component.connectedNodes" :pathNode="component" :railSystem="railSystem" />
|
<PathNodeComponentView v-if="component.connectedNodes" :pathNode="component" :railSystem="railSystem" />
|
||||||
<button @click="removeComponent()" class="btn btn-sm btn-danger">Remove</button>
|
<button @click="remove()" class="btn btn-sm btn-danger">Remove</button>
|
||||||
</div>
|
</div>
|
||||||
<ConfirmModal
|
<ConfirmModal
|
||||||
ref="removeConfirm"
|
ref="removeConfirm"
|
||||||
|
@ -47,6 +46,7 @@ import SegmentBoundaryNodeComponentView from "./SegmentBoundaryNodeComponentView
|
||||||
import {useRailSystemsStore} from "../../../stores/railSystemsStore";
|
import {useRailSystemsStore} from "../../../stores/railSystemsStore";
|
||||||
import ConfirmModal from "../../ConfirmModal.vue";
|
import ConfirmModal from "../../ConfirmModal.vue";
|
||||||
import SwitchComponentView from "./SwitchComponentView.vue";
|
import SwitchComponentView from "./SwitchComponentView.vue";
|
||||||
|
import {removeComponent} from "../../../api/components";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
|
@ -73,9 +73,12 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
removeComponent() {
|
remove() {
|
||||||
this.$refs.removeConfirm.showConfirm()
|
this.$refs.removeConfirm.showConfirm()
|
||||||
.then(() => this.rsStore.removeComponent(this.component.id));
|
.then(() => {
|
||||||
|
removeComponent(this.rsStore.selectedRailSystem, this.component.id)
|
||||||
|
.catch(console.error);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
class="list-group-item"
|
class="list-group-item"
|
||||||
>
|
>
|
||||||
{{node.name}}
|
{{node.name}}
|
||||||
<button @click="rsStore.removeConnection(pathNode, node)" class="btn btn-sm btn-danger float-end">
|
<button @click="remove(node)" class="btn btn-sm btn-danger float-end">
|
||||||
Remove
|
Remove
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
|
@ -16,7 +16,7 @@
|
||||||
There are no connected nodes.
|
There are no connected nodes.
|
||||||
</p>
|
</p>
|
||||||
<form
|
<form
|
||||||
@submit.prevent="rsStore.addConnection(pathNode, formData.nodeToAdd)"
|
@submit.prevent="add(formData.nodeToAdd)"
|
||||||
v-if="getEligibleConnections().length > 0"
|
v-if="getEligibleConnections().length > 0"
|
||||||
class="input-group mb-3"
|
class="input-group mb-3"
|
||||||
>
|
>
|
||||||
|
@ -30,16 +30,10 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import {useRailSystemsStore} from "../../../stores/railSystemsStore";
|
import {addConnection, removeConnection} from "../../../api/paths";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "PathNodeComponentView",
|
name: "PathNodeComponentView",
|
||||||
setup() {
|
|
||||||
const rsStore = useRailSystemsStore();
|
|
||||||
return {
|
|
||||||
rsStore
|
|
||||||
};
|
|
||||||
},
|
|
||||||
props: {
|
props: {
|
||||||
pathNode: {
|
pathNode: {
|
||||||
type: Object,
|
type: Object,
|
||||||
|
@ -74,6 +68,12 @@ export default {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nodes;
|
return nodes;
|
||||||
|
},
|
||||||
|
remove(node) {
|
||||||
|
removeConnection(this.railSystem, this.pathNode, node);
|
||||||
|
},
|
||||||
|
add(node) {
|
||||||
|
addConnection(this.railSystem, this.pathNode, node);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,19 +1,14 @@
|
||||||
<template>
|
<template>
|
||||||
<h5>Segments Connected</h5>
|
<h5>Segments Connected</h5>
|
||||||
<table class="table">
|
<div class="mb-2">
|
||||||
<thead>
|
<span
|
||||||
<tr>
|
v-for="segment in node.segments"
|
||||||
<th>Name</th>
|
:key="segment.id"
|
||||||
<th>Occupied</th>
|
class="badge bg-secondary me-1"
|
||||||
</tr>
|
>
|
||||||
</thead>
|
{{segment.name}}
|
||||||
<tbody>
|
</span>
|
||||||
<tr v-for="segment in node.segments" :key="segment.id">
|
</div>
|
||||||
<td>{{segment.name}}</td>
|
|
||||||
<td>{{segment.occupied}}</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
|
|
@ -1,6 +1,10 @@
|
||||||
<template>
|
<template>
|
||||||
<h5>Switch Configurations</h5>
|
<h5>Switch Configurations</h5>
|
||||||
<ul class="list-group list-group-flush border" v-if="sw.possibleConfigurations.length > 0" style="overflow: auto; max-height: 150px;">
|
<ul
|
||||||
|
class="list-group list-group-flush border mb-2"
|
||||||
|
v-if="sw.possibleConfigurations.length > 0"
|
||||||
|
style="overflow: auto; max-height: 150px;"
|
||||||
|
>
|
||||||
<li
|
<li
|
||||||
v-for="config in sw.possibleConfigurations"
|
v-for="config in sw.possibleConfigurations"
|
||||||
:key="config.id"
|
:key="config.id"
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
import { defineStore } from "pinia";
|
import {defineStore} from "pinia";
|
||||||
import axios from "axios";
|
import {refreshSegments} from "../api/segments"
|
||||||
|
import {refreshComponents} from "../api/components";
|
||||||
|
import {closeWebsocketConnection, establishWebsocketConnection} from "../api/websocket";
|
||||||
|
|
||||||
export const useRailSystemsStore = defineStore('RailSystemsStore', {
|
export const useRailSystemsStore = defineStore('RailSystemsStore', {
|
||||||
state: () => ({
|
state: () => ({
|
||||||
|
@ -12,173 +14,21 @@ export const useRailSystemsStore = defineStore('RailSystemsStore', {
|
||||||
* websocket: WebSocket | null
|
* websocket: WebSocket | null
|
||||||
* } | null}
|
* } | null}
|
||||||
*/
|
*/
|
||||||
selectedRailSystem: null,
|
selectedRailSystem: null
|
||||||
websocket: null,
|
|
||||||
apiUrl: import.meta.env.VITE_API_URL,
|
|
||||||
wsUrl: import.meta.env.VITE_WS_URL
|
|
||||||
}),
|
}),
|
||||||
actions: {
|
actions: {
|
||||||
refreshRailSystems() {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
axios.get(this.apiUrl + "/rs")
|
|
||||||
.then(response => {
|
|
||||||
this.railSystems = response.data;
|
|
||||||
resolve();
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
reject(error);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
},
|
|
||||||
createRailSystem(name) {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
axios.post(this.apiUrl + "/rs", {name: name})
|
|
||||||
.then(response => {
|
|
||||||
const newId = response.data.id;
|
|
||||||
this.refreshRailSystems()
|
|
||||||
.then(() => resolve(this.railSystems.find(rs => rs.id === newId)))
|
|
||||||
.catch(error => reject(error));
|
|
||||||
})
|
|
||||||
.catch(error => reject(error));
|
|
||||||
});
|
|
||||||
},
|
|
||||||
removeRailSystem(rs) {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
axios.delete(this.apiUrl + "/rs/" + rs.id)
|
|
||||||
.then(() => {
|
|
||||||
this.selectedRailSystem = null;
|
|
||||||
this.refreshRailSystems()
|
|
||||||
.then(() => resolve)
|
|
||||||
.catch(error => reject(error));
|
|
||||||
})
|
|
||||||
})
|
|
||||||
},
|
|
||||||
refreshSegments(rs) {
|
|
||||||
return new Promise(resolve => {
|
|
||||||
axios.get(`${this.apiUrl}/rs/${rs.id}/s`)
|
|
||||||
.then(response => {
|
|
||||||
rs.segments = response.data;
|
|
||||||
resolve();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
},
|
|
||||||
refreshAllComponents(rs) {
|
|
||||||
return new Promise(resolve => {
|
|
||||||
axios.get(`${this.apiUrl}/rs/${rs.id}/c`)
|
|
||||||
.then(response => {
|
|
||||||
rs.selectedComponent = null;
|
|
||||||
rs.components = response.data;
|
|
||||||
resolve();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
},
|
|
||||||
onSelectedRailSystemChanged() {
|
onSelectedRailSystemChanged() {
|
||||||
|
this.railSystems.forEach(rs => closeWebsocketConnection(rs));
|
||||||
if (!this.selectedRailSystem) return;
|
if (!this.selectedRailSystem) return;
|
||||||
this.refreshSegments(this.selectedRailSystem);
|
refreshSegments(this.selectedRailSystem);
|
||||||
this.refreshAllComponents(this.selectedRailSystem);
|
refreshComponents(this.selectedRailSystem);
|
||||||
if (this.websocket !== null) {
|
establishWebsocketConnection(this.selectedRailSystem);
|
||||||
this.websocket.close();
|
}
|
||||||
}
|
},
|
||||||
console.log(this.wsUrl);
|
getters: {
|
||||||
this.websocket = new WebSocket(this.wsUrl + "/" + this.selectedRailSystem.id);
|
rsId() {
|
||||||
this.websocket.onopen = event => {
|
if (this.selectedRailSystem === null) return null;
|
||||||
console.log("Opened websocket connection.");
|
return this.selectedRailSystem.id;
|
||||||
};
|
|
||||||
this.websocket.onclose = event => {
|
|
||||||
console.log("Closed websocket connection.");
|
|
||||||
};
|
|
||||||
this.websocket.onmessage = (msg) => {
|
|
||||||
console.log(msg);
|
|
||||||
};
|
|
||||||
},
|
|
||||||
addSegment(name) {
|
|
||||||
const rs = this.selectedRailSystem;
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
axios.post(`${this.apiUrl}/rs/${rs.id}/s`, {name: name})
|
|
||||||
.then(() => {
|
|
||||||
this.refreshSegments(rs)
|
|
||||||
.then(() => resolve())
|
|
||||||
.catch(error => reject(error));
|
|
||||||
})
|
|
||||||
.catch(error => reject(error));
|
|
||||||
});
|
|
||||||
},
|
|
||||||
removeSegment(id) {
|
|
||||||
const rs = this.selectedRailSystem;
|
|
||||||
axios.delete(`${this.apiUrl}/rs/${rs.id}/s/${id}`)
|
|
||||||
.then(() => this.refreshSegments(rs))
|
|
||||||
.catch(error => console.log(error));
|
|
||||||
},
|
|
||||||
addComponent(data) {
|
|
||||||
const rs = this.selectedRailSystem;
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
axios.post(`${this.apiUrl}/rs/${rs.id}/c`, data)
|
|
||||||
.then(() => {
|
|
||||||
this.refreshAllComponents(rs)
|
|
||||||
.then(() => resolve())
|
|
||||||
.catch(error => reject(error));
|
|
||||||
})
|
|
||||||
.catch(error => reject(error));
|
|
||||||
});
|
|
||||||
},
|
|
||||||
removeComponent(id) {
|
|
||||||
const rs = this.selectedRailSystem;
|
|
||||||
axios.delete(`${this.apiUrl}/rs/${rs.id}/c/${id}`)
|
|
||||||
.then(() => this.refreshAllComponents(rs))
|
|
||||||
.catch(error => console.log(error));
|
|
||||||
},
|
|
||||||
fetchComponentData(component) {
|
|
||||||
return new Promise(resolve => {
|
|
||||||
const rs = this.selectedRailSystem;
|
|
||||||
axios.get(`${this.apiUrl}/rs/${rs.id}/c/${component.id}`)
|
|
||||||
.then(response => resolve(response.data))
|
|
||||||
.catch(error => console.log(error));
|
|
||||||
});
|
|
||||||
},
|
|
||||||
refreshComponents(components) {
|
|
||||||
const rs = this.selectedRailSystem;
|
|
||||||
for (let i = 0; i < components.length; i++) {
|
|
||||||
axios.get(`${this.apiUrl}/rs/${rs.id}/c/${components[i].id}`)
|
|
||||||
.then(resp => {
|
|
||||||
const idx = this.selectedRailSystem.components.findIndex(c => c.id === resp.data.id);
|
|
||||||
if (idx > -1) this.selectedRailSystem.components[idx] = resp.data;
|
|
||||||
})
|
|
||||||
.catch(error => console.log(error));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
updateConnections(pathNode) {
|
|
||||||
const rs = this.selectedRailSystem;
|
|
||||||
return new Promise(resolve => {
|
|
||||||
axios.patch(
|
|
||||||
`${this.apiUrl}/rs/${rs.id}/c/${pathNode.id}/connectedNodes`,
|
|
||||||
pathNode
|
|
||||||
)
|
|
||||||
.then(response => {
|
|
||||||
pathNode.connectedNodes = response.data.connectedNodes;
|
|
||||||
resolve();
|
|
||||||
})
|
|
||||||
.catch(error => console.log(error));
|
|
||||||
});
|
|
||||||
},
|
|
||||||
addConnection(pathNode, other) {
|
|
||||||
pathNode.connectedNodes.push(other);
|
|
||||||
this.updateConnections(pathNode)
|
|
||||||
.then(() => {
|
|
||||||
this.refreshComponents(pathNode.connectedNodes);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
removeConnection(pathNode, other) {
|
|
||||||
const idx = pathNode.connectedNodes.findIndex(n => n.id === other.id);
|
|
||||||
if (idx > -1) {
|
|
||||||
pathNode.connectedNodes.splice(idx, 1);
|
|
||||||
this.updateConnections(pathNode)
|
|
||||||
.then(() => {
|
|
||||||
const nodes = [];
|
|
||||||
nodes.push(pathNode.connectedNodes);
|
|
||||||
nodes.push(other);
|
|
||||||
this.refreshComponents(nodes);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
|
@ -1,12 +1,17 @@
|
||||||
package nl.andrewl.railsignalapi.dao;
|
package nl.andrewl.railsignalapi.dao;
|
||||||
|
|
||||||
import nl.andrewl.railsignalapi.model.LinkToken;
|
import nl.andrewl.railsignalapi.model.LinkToken;
|
||||||
|
import nl.andrewl.railsignalapi.model.RailSystem;
|
||||||
import org.springframework.data.jpa.repository.JpaRepository;
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
import org.springframework.stereotype.Repository;
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
@Repository
|
@Repository
|
||||||
public interface LinkTokenRepository extends JpaRepository<LinkToken, Long> {
|
public interface LinkTokenRepository extends JpaRepository<LinkToken, Long> {
|
||||||
Iterable<LinkToken> findAllByTokenPrefix(String prefix);
|
Iterable<LinkToken> findAllByTokenPrefix(String prefix);
|
||||||
boolean existsByLabel(String label);
|
boolean existsByLabel(String label);
|
||||||
|
List<LinkToken> findAllByRailSystem(RailSystem rs);
|
||||||
|
Optional<LinkToken> findByIdAndRailSystemId(long ltId, long rsId);
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,7 +12,7 @@ import java.util.Optional;
|
||||||
public interface SegmentRepository extends JpaRepository<Segment, Long> {
|
public interface SegmentRepository extends JpaRepository<Segment, Long> {
|
||||||
boolean existsByNameAndRailSystem(String name, RailSystem rs);
|
boolean existsByNameAndRailSystem(String name, RailSystem rs);
|
||||||
|
|
||||||
List<Segment> findAllByRailSystemId(long rsId);
|
List<Segment> findAllByRailSystemIdOrderByName(long rsId);
|
||||||
|
|
||||||
Optional<Segment> findByIdAndRailSystemId(long id, long rsId);
|
Optional<Segment> findByIdAndRailSystemId(long id, long rsId);
|
||||||
|
|
||||||
|
|
|
@ -15,6 +15,7 @@ public abstract class ComponentDownlink {
|
||||||
}
|
}
|
||||||
|
|
||||||
public abstract void send(Object msg) throws Exception;
|
public abstract void send(Object msg) throws Exception;
|
||||||
|
public abstract void shutdown() throws Exception;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean equals(Object o) {
|
public boolean equals(Object o) {
|
||||||
|
|
|
@ -2,8 +2,9 @@ package nl.andrewl.railsignalapi.live;
|
||||||
|
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import nl.andrewl.railsignalapi.dao.LinkTokenRepository;
|
|
||||||
import nl.andrewl.railsignalapi.dao.ComponentRepository;
|
import nl.andrewl.railsignalapi.dao.ComponentRepository;
|
||||||
|
import nl.andrewl.railsignalapi.dao.LinkTokenRepository;
|
||||||
|
import nl.andrewl.railsignalapi.live.websocket.AppUpdateService;
|
||||||
import nl.andrewl.railsignalapi.model.component.Component;
|
import nl.andrewl.railsignalapi.model.component.Component;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
@ -23,6 +24,7 @@ public class ComponentDownlinkService {
|
||||||
|
|
||||||
private final LinkTokenRepository tokenRepository;
|
private final LinkTokenRepository tokenRepository;
|
||||||
private final ComponentRepository<Component> componentRepository;
|
private final ComponentRepository<Component> componentRepository;
|
||||||
|
private final AppUpdateService appUpdateService;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Registers a new active downlink to one or more components.
|
* Registers a new active downlink to one or more components.
|
||||||
|
@ -34,10 +36,11 @@ public class ComponentDownlinkService {
|
||||||
componentDownlinks.put(downlink, components.stream().map(Component::getId).collect(Collectors.toSet()));
|
componentDownlinks.put(downlink, components.stream().map(Component::getId).collect(Collectors.toSet()));
|
||||||
for (var c : components) {
|
for (var c : components) {
|
||||||
c.setOnline(true);
|
c.setOnline(true);
|
||||||
|
componentRepository.save(c);
|
||||||
|
appUpdateService.sendComponentUpdate(c.getRailSystem().getId(), c.getId());
|
||||||
Set<ComponentDownlink> downlinks = downlinksByCId.computeIfAbsent(c.getId(), aLong -> new HashSet<>());
|
Set<ComponentDownlink> downlinks = downlinksByCId.computeIfAbsent(c.getId(), aLong -> new HashSet<>());
|
||||||
downlinks.add(downlink);
|
downlinks.add(downlink);
|
||||||
}
|
}
|
||||||
componentRepository.saveAll(components);
|
|
||||||
log.info("Registered downlink with token id {}.", downlink.getTokenId());
|
log.info("Registered downlink with token id {}.", downlink.getTokenId());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -54,6 +57,7 @@ public class ComponentDownlinkService {
|
||||||
componentRepository.findById(cId).ifPresent(component -> {
|
componentRepository.findById(cId).ifPresent(component -> {
|
||||||
component.setOnline(false);
|
component.setOnline(false);
|
||||||
componentRepository.save(component);
|
componentRepository.save(component);
|
||||||
|
appUpdateService.sendComponentUpdate(component.getRailSystem().getId(), component.getId());
|
||||||
});
|
});
|
||||||
Set<ComponentDownlink> downlinks = downlinksByCId.get(cId);
|
Set<ComponentDownlink> downlinks = downlinksByCId.get(cId);
|
||||||
if (downlinks != null) {
|
if (downlinks != null) {
|
||||||
|
@ -64,6 +68,11 @@ public class ComponentDownlinkService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
try {
|
||||||
|
downlink.shutdown();
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("An error occurred while shutting down a component downlink.", e);
|
||||||
|
}
|
||||||
log.info("De-registered downlink with token id {}.", downlink.getTokenId());
|
log.info("De-registered downlink with token id {}.", downlink.getTokenId());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -13,7 +13,9 @@ import java.io.IOException;
|
||||||
import java.net.Socket;
|
import java.net.Socket;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This link manager is started when a TCP link is established.
|
* This link manager is started when a TCP link is established, and acts as
|
||||||
|
* both a downlink to the component that we can send messages through, and an
|
||||||
|
* uplink that receives messages from the component.
|
||||||
*/
|
*/
|
||||||
@Slf4j
|
@Slf4j
|
||||||
public class TcpLinkManager extends ComponentDownlink implements Runnable {
|
public class TcpLinkManager extends ComponentDownlink implements Runnable {
|
||||||
|
@ -53,12 +55,9 @@ public class TcpLinkManager extends ComponentDownlink implements Runnable {
|
||||||
downlinkService.deregisterDownlink(this);
|
downlinkService.deregisterDownlink(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void shutdown() {
|
@Override
|
||||||
try {
|
public void shutdown() throws IOException {
|
||||||
this.socket.close();
|
socket.close();
|
||||||
} catch (IOException e) {
|
|
||||||
log.warn("An error occurred while closing TCP socket.", e);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -1,15 +1,14 @@
|
||||||
package nl.andrewl.railsignalapi.live.tcp_socket;
|
package nl.andrewl.railsignalapi.live.tcp_socket;
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import nl.andrewl.railsignalapi.dao.LinkTokenRepository;
|
|
||||||
import nl.andrewl.railsignalapi.live.ComponentDownlinkService;
|
import nl.andrewl.railsignalapi.live.ComponentDownlinkService;
|
||||||
import nl.andrewl.railsignalapi.live.ComponentUplinkMessageHandler;
|
import nl.andrewl.railsignalapi.live.ComponentUplinkMessageHandler;
|
||||||
import nl.andrewl.railsignalapi.model.LinkToken;
|
import nl.andrewl.railsignalapi.model.LinkToken;
|
||||||
|
import nl.andrewl.railsignalapi.service.LinkTokenService;
|
||||||
import nl.andrewl.railsignalapi.util.JsonUtils;
|
import nl.andrewl.railsignalapi.util.JsonUtils;
|
||||||
import org.springframework.boot.context.event.ApplicationReadyEvent;
|
import org.springframework.boot.context.event.ApplicationReadyEvent;
|
||||||
import org.springframework.context.event.ContextClosedEvent;
|
import org.springframework.context.event.ContextClosedEvent;
|
||||||
import org.springframework.context.event.EventListener;
|
import org.springframework.context.event.EventListener;
|
||||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
import java.io.DataInputStream;
|
import java.io.DataInputStream;
|
||||||
|
@ -19,6 +18,7 @@ import java.net.InetSocketAddress;
|
||||||
import java.net.ServerSocket;
|
import java.net.ServerSocket;
|
||||||
import java.net.Socket;
|
import java.net.Socket;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
|
import java.util.Optional;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -43,19 +43,16 @@ public class TcpSocketServer {
|
||||||
private final ServerSocket serverSocket;
|
private final ServerSocket serverSocket;
|
||||||
private final Set<TcpLinkManager> linkManagers;
|
private final Set<TcpLinkManager> linkManagers;
|
||||||
|
|
||||||
private final LinkTokenRepository tokenRepository;
|
private final LinkTokenService tokenService;
|
||||||
private final PasswordEncoder passwordEncoder;
|
|
||||||
private final ComponentDownlinkService componentDownlinkService;
|
private final ComponentDownlinkService componentDownlinkService;
|
||||||
private final ComponentUplinkMessageHandler uplinkMessageHandler;
|
private final ComponentUplinkMessageHandler uplinkMessageHandler;
|
||||||
|
|
||||||
public TcpSocketServer(
|
public TcpSocketServer(
|
||||||
LinkTokenRepository tokenRepository,
|
LinkTokenService linkTokenService,
|
||||||
PasswordEncoder passwordEncoder,
|
|
||||||
ComponentDownlinkService componentDownlinkService,
|
ComponentDownlinkService componentDownlinkService,
|
||||||
ComponentUplinkMessageHandler uplinkMessageHandler
|
ComponentUplinkMessageHandler uplinkMessageHandler
|
||||||
) throws IOException {
|
) throws IOException {
|
||||||
this.tokenRepository = tokenRepository;
|
this.tokenService = linkTokenService;
|
||||||
this.passwordEncoder = passwordEncoder;
|
|
||||||
this.componentDownlinkService = componentDownlinkService;
|
this.componentDownlinkService = componentDownlinkService;
|
||||||
this.uplinkMessageHandler = uplinkMessageHandler;
|
this.uplinkMessageHandler = uplinkMessageHandler;
|
||||||
|
|
||||||
|
@ -86,7 +83,9 @@ public class TcpSocketServer {
|
||||||
@EventListener(ContextClosedEvent.class)
|
@EventListener(ContextClosedEvent.class)
|
||||||
public void closeServer() throws IOException {
|
public void closeServer() throws IOException {
|
||||||
serverSocket.close();
|
serverSocket.close();
|
||||||
for (var linkManager : linkManagers) linkManager.shutdown();
|
for (var linkManager : linkManagers) {
|
||||||
|
linkManager.shutdown();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void initializeConnection(Socket socket) throws IOException {
|
private void initializeConnection(Socket socket) throws IOException {
|
||||||
|
@ -98,18 +97,17 @@ public class TcpSocketServer {
|
||||||
JsonUtils.writeJsonString(out, new ConnectMessage(false, "Invalid or missing token."));
|
JsonUtils.writeJsonString(out, new ConnectMessage(false, "Invalid or missing token."));
|
||||||
socket.close();
|
socket.close();
|
||||||
} else {
|
} else {
|
||||||
Iterable<LinkToken> tokens = tokenRepository.findAllByTokenPrefix(rawToken.substring(0, LinkToken.PREFIX_SIZE));
|
Optional<LinkToken> optionalToken = tokenService.validateToken(rawToken);
|
||||||
for (var token : tokens) {
|
if (optionalToken.isPresent()) {
|
||||||
if (passwordEncoder.matches(rawToken, token.getTokenHash())) {
|
LinkToken token = optionalToken.get();
|
||||||
JsonUtils.writeJsonString(out, new ConnectMessage(true, "Connection established."));
|
JsonUtils.writeJsonString(out, new ConnectMessage(true, "Connection established."));
|
||||||
var linkManager = new TcpLinkManager(token.getId(), socket, componentDownlinkService, uplinkMessageHandler);
|
var linkManager = new TcpLinkManager(token.getId(), socket, componentDownlinkService, uplinkMessageHandler);
|
||||||
new Thread(linkManager, "linkManager-" + token.getId()).start();
|
new Thread(linkManager, "LinkManager-" + token.getId()).start();
|
||||||
linkManagers.add(linkManager);
|
linkManagers.add(linkManager);
|
||||||
return;
|
} else {
|
||||||
}
|
JsonUtils.writeJsonString(out, new ConnectMessage(false, "Invalid token."));
|
||||||
|
socket.close();
|
||||||
}
|
}
|
||||||
JsonUtils.writeJsonString(out, new ConnectMessage(false, "Invalid token."));
|
|
||||||
socket.close();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,6 +32,7 @@ public class AppUpdateService {
|
||||||
public synchronized void registerSession(long rsId, WebSocketSession session) {
|
public synchronized void registerSession(long rsId, WebSocketSession session) {
|
||||||
Set<WebSocketSession> sessionsForRs = sessions.computeIfAbsent(rsId, x -> new HashSet<>());
|
Set<WebSocketSession> sessionsForRs = sessions.computeIfAbsent(rsId, x -> new HashSet<>());
|
||||||
sessionsForRs.add(session);
|
sessionsForRs.add(session);
|
||||||
|
log.info("Registered a new app websocket session for rail system id " + rsId);
|
||||||
}
|
}
|
||||||
|
|
||||||
public synchronized void deregisterSession(WebSocketSession session) {
|
public synchronized void deregisterSession(WebSocketSession session) {
|
||||||
|
@ -49,6 +50,7 @@ public class AppUpdateService {
|
||||||
for (var orphanRsId : orphans) {
|
for (var orphanRsId : orphans) {
|
||||||
sessions.remove(orphanRsId);
|
sessions.remove(orphanRsId);
|
||||||
}
|
}
|
||||||
|
log.info("De-registered an app websocket session.");
|
||||||
}
|
}
|
||||||
|
|
||||||
public synchronized void sendUpdate(long rsId, Object msg) {
|
public synchronized void sendUpdate(long rsId, Object msg) {
|
||||||
|
|
|
@ -17,4 +17,9 @@ public class WebsocketDownlink extends ComponentDownlink {
|
||||||
public void send(Object msg) throws Exception {
|
public void send(Object msg) throws Exception {
|
||||||
webSocketSession.sendMessage(new TextMessage(JsonUtils.toJson(msg)));
|
webSocketSession.sendMessage(new TextMessage(JsonUtils.toJson(msg)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void shutdown() throws Exception {
|
||||||
|
webSocketSession.close();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,38 @@
|
||||||
|
package nl.andrewl.railsignalapi.rest;
|
||||||
|
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import nl.andrewl.railsignalapi.rest.dto.LinkTokenCreatedResponse;
|
||||||
|
import nl.andrewl.railsignalapi.rest.dto.LinkTokenPayload;
|
||||||
|
import nl.andrewl.railsignalapi.rest.dto.LinkTokenResponse;
|
||||||
|
import nl.andrewl.railsignalapi.service.LinkTokenService;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import javax.validation.Valid;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Controller for endpoints regarding link tokens.
|
||||||
|
*/
|
||||||
|
@RestController
|
||||||
|
@RequestMapping(path = "/api/rs/{rsId}/lt")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class LinkTokensApiController {
|
||||||
|
private final LinkTokenService tokenService;
|
||||||
|
|
||||||
|
@GetMapping
|
||||||
|
public List<LinkTokenResponse> getTokens(@PathVariable long rsId) {
|
||||||
|
return tokenService.getTokens(rsId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping
|
||||||
|
public LinkTokenCreatedResponse createToken(@PathVariable long rsId, @RequestBody @Valid LinkTokenPayload payload) {
|
||||||
|
return tokenService.createToken(rsId, payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
@DeleteMapping(path = "/{ltId}")
|
||||||
|
public ResponseEntity<Void> deleteToken(@PathVariable long rsId, @PathVariable long ltId) {
|
||||||
|
tokenService.deleteToken(rsId, ltId);
|
||||||
|
return ResponseEntity.noContent().build();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
package nl.andrewl.railsignalapi.rest.dto;
|
||||||
|
|
||||||
|
public record LinkTokenCreatedResponse(
|
||||||
|
String token
|
||||||
|
) {}
|
|
@ -0,0 +1,12 @@
|
||||||
|
package nl.andrewl.railsignalapi.rest.dto;
|
||||||
|
|
||||||
|
import javax.validation.constraints.NotBlank;
|
||||||
|
import javax.validation.constraints.NotEmpty;
|
||||||
|
import javax.validation.constraints.NotNull;
|
||||||
|
|
||||||
|
public record LinkTokenPayload(
|
||||||
|
@NotNull @NotBlank
|
||||||
|
String label,
|
||||||
|
@NotEmpty
|
||||||
|
long[] componentIds
|
||||||
|
) {}
|
|
@ -0,0 +1,24 @@
|
||||||
|
package nl.andrewl.railsignalapi.rest.dto;
|
||||||
|
|
||||||
|
import nl.andrewl.railsignalapi.model.LinkToken;
|
||||||
|
import nl.andrewl.railsignalapi.model.component.Component;
|
||||||
|
import nl.andrewl.railsignalapi.rest.dto.component.out.SimpleComponentResponse;
|
||||||
|
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public record LinkTokenResponse(
|
||||||
|
long id,
|
||||||
|
String label,
|
||||||
|
List<SimpleComponentResponse> components
|
||||||
|
) {
|
||||||
|
public LinkTokenResponse(LinkToken token) {
|
||||||
|
this(
|
||||||
|
token.getId(),
|
||||||
|
token.getLabel(),
|
||||||
|
token.getComponents().stream()
|
||||||
|
.sorted(Comparator.comparing(Component::getName))
|
||||||
|
.map(SimpleComponentResponse::new).toList()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,85 @@
|
||||||
|
package nl.andrewl.railsignalapi.service;
|
||||||
|
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import nl.andrewl.railsignalapi.dao.ComponentRepository;
|
||||||
|
import nl.andrewl.railsignalapi.dao.LinkTokenRepository;
|
||||||
|
import nl.andrewl.railsignalapi.dao.RailSystemRepository;
|
||||||
|
import nl.andrewl.railsignalapi.live.ComponentDownlinkService;
|
||||||
|
import nl.andrewl.railsignalapi.model.LinkToken;
|
||||||
|
import nl.andrewl.railsignalapi.model.component.Component;
|
||||||
|
import nl.andrewl.railsignalapi.model.component.ComponentType;
|
||||||
|
import nl.andrewl.railsignalapi.rest.dto.LinkTokenCreatedResponse;
|
||||||
|
import nl.andrewl.railsignalapi.rest.dto.LinkTokenPayload;
|
||||||
|
import nl.andrewl.railsignalapi.rest.dto.LinkTokenResponse;
|
||||||
|
import nl.andrewl.railsignalapi.util.StringUtils;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
import org.springframework.web.server.ResponseStatusException;
|
||||||
|
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class LinkTokenService {
|
||||||
|
private final LinkTokenRepository tokenRepository;
|
||||||
|
private final RailSystemRepository railSystemRepository;
|
||||||
|
private final ComponentRepository<Component> componentRepository;
|
||||||
|
private final PasswordEncoder passwordEncoder;
|
||||||
|
private final ComponentDownlinkService componentDownlinkService;
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public LinkTokenCreatedResponse createToken(long rsId, LinkTokenPayload payload) {
|
||||||
|
var rs = railSystemRepository.findById(rsId).orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
|
||||||
|
if (tokenRepository.existsByLabel(payload.label())) {
|
||||||
|
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "There is already a token with that label.");
|
||||||
|
}
|
||||||
|
final Set<ComponentType> validTypes = Set.of(ComponentType.SIGNAL, ComponentType.SWITCH, ComponentType.SEGMENT_BOUNDARY);
|
||||||
|
Set<Component> components = new HashSet<>();
|
||||||
|
for (var cId : payload.componentIds()) {
|
||||||
|
var c = componentRepository.findByIdAndRailSystemId(cId, rsId)
|
||||||
|
.orElseThrow(() -> new ResponseStatusException(HttpStatus.BAD_REQUEST, "Component with id " + cId + " was not found in this rail system."));
|
||||||
|
if (!validTypes.contains(c.getType())) {
|
||||||
|
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Component with id " + cId + " is not a valid type which allows linking.");
|
||||||
|
}
|
||||||
|
components.add(c);
|
||||||
|
}
|
||||||
|
final String token = StringUtils.randomString(32, StringUtils.ALPHA_NUM);
|
||||||
|
tokenRepository.save(new LinkToken(
|
||||||
|
rs,
|
||||||
|
payload.label(),
|
||||||
|
token.substring(0, LinkToken.PREFIX_SIZE),
|
||||||
|
passwordEncoder.encode(token),
|
||||||
|
components
|
||||||
|
));
|
||||||
|
return new LinkTokenCreatedResponse(token);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public Optional<LinkToken> validateToken(String rawToken) {
|
||||||
|
for (var token : tokenRepository.findAllByTokenPrefix(rawToken.substring(0, LinkToken.PREFIX_SIZE))) {
|
||||||
|
if (passwordEncoder.matches(rawToken, token.getTokenHash())) {
|
||||||
|
return Optional.of(token);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public List<LinkTokenResponse> getTokens(long rsId) {
|
||||||
|
var rs = railSystemRepository.findById(rsId)
|
||||||
|
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
|
||||||
|
return tokenRepository.findAllByRailSystem(rs).stream()
|
||||||
|
.sorted(Comparator.comparing(LinkToken::getLabel))
|
||||||
|
.map(LinkTokenResponse::new).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public void deleteToken(long rsId, long ltId) {
|
||||||
|
var token = tokenRepository.findByIdAndRailSystemId(ltId, rsId)
|
||||||
|
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
|
||||||
|
componentDownlinkService.deregisterDownlink(token.getId());
|
||||||
|
tokenRepository.delete(token);
|
||||||
|
}
|
||||||
|
}
|
|
@ -35,7 +35,7 @@ public class SegmentService {
|
||||||
|
|
||||||
@Transactional(readOnly = true)
|
@Transactional(readOnly = true)
|
||||||
public List<SegmentResponse> getSegments(long rsId) {
|
public List<SegmentResponse> getSegments(long rsId) {
|
||||||
return segmentRepository.findAllByRailSystemId(rsId).stream().map(SegmentResponse::new).toList();
|
return segmentRepository.findAllByRailSystemIdOrderByName(rsId).stream().map(SegmentResponse::new).toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Transactional(readOnly = true)
|
@Transactional(readOnly = true)
|
||||||
|
|
|
@ -0,0 +1,19 @@
|
||||||
|
package nl.andrewl.railsignalapi.util;
|
||||||
|
|
||||||
|
import java.security.SecureRandom;
|
||||||
|
import java.util.Random;
|
||||||
|
|
||||||
|
public final class StringUtils {
|
||||||
|
private StringUtils() {}
|
||||||
|
|
||||||
|
public static final String ALPHA_NUM = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
|
||||||
|
private static final Random random = new SecureRandom();
|
||||||
|
|
||||||
|
public static String randomString(int length, String alphabet) {
|
||||||
|
StringBuilder sb = new StringBuilder(length);
|
||||||
|
for (int i = 0; i < length; i++) {
|
||||||
|
sb.append(alphabet.charAt(random.nextInt(alphabet.length())));
|
||||||
|
}
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue