Added UI for creating link tokens, and improved component search api.
This commit is contained in:
parent
ed3f6bd6b9
commit
e45b942f34
|
@ -48,6 +48,27 @@ export function getComponent(rs, id) {
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Searches through the rail system's components.
|
||||
* @param {RailSystem} rs
|
||||
* @param {string|null} searchQuery
|
||||
* @return {Promise<Object>}
|
||||
*/
|
||||
export function searchComponents(rs, searchQuery) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const params = {
|
||||
page: 0,
|
||||
size: 25
|
||||
};
|
||||
if (searchQuery) params.q = searchQuery;
|
||||
axios.get(`${API_URL}/rs/${rs.id}/c/search`, {params: params})
|
||||
.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)
|
||||
|
|
|
@ -0,0 +1,57 @@
|
|||
import {API_URL} from "./constants";
|
||||
import axios from "axios";
|
||||
|
||||
/**
|
||||
* A token that's used by components to provide real-time up and down links.
|
||||
*/
|
||||
export class LinkToken {
|
||||
constructor(data) {
|
||||
this.id = data.id;
|
||||
this.label = data.label;
|
||||
this.components = data.components;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the list of link tokens in a rail system.
|
||||
* @param {RailSystem} rs
|
||||
* @return {Promise<LinkToken[]>}
|
||||
*/
|
||||
export function getTokens(rs) {
|
||||
return new Promise((resolve, reject) => {
|
||||
axios.get(`${API_URL}/rs/${rs.id}/lt`)
|
||||
.then(response => {
|
||||
resolve(response.data.map(obj => new LinkToken(obj)));
|
||||
})
|
||||
.catch(reject);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new link token.
|
||||
* @param {RailSystem} rs
|
||||
* @param {LinkToken} data
|
||||
* @return {Promise<string>} A promise that resolves to the token that was created.
|
||||
*/
|
||||
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);
|
||||
})
|
||||
.catch(reject);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a link token.
|
||||
* @param {RailSystem} rs
|
||||
* @param {Number} tokenId
|
||||
*/
|
||||
export function deleteToken(rs, tokenId) {
|
||||
return new Promise((resolve, reject) => {
|
||||
axios.delete(`${API_URL}/rs/${rs.id}/lt/${tokenId}`)
|
||||
.then(resolve)
|
||||
.catch(reject);
|
||||
});
|
||||
}
|
|
@ -2,6 +2,12 @@ import axios from "axios";
|
|||
import {API_URL} from "./constants";
|
||||
import {refreshSomeComponents} from "./components";
|
||||
|
||||
/**
|
||||
* Updates the connections to a path node.
|
||||
* @param {RailSystem} rs The rail system to which the node belongs.
|
||||
* @param {Object} node The node to update.
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export function updateConnections(rs, node) {
|
||||
return new Promise((resolve, reject) => {
|
||||
axios.patch(
|
||||
|
@ -16,11 +22,25 @@ export function updateConnections(rs, node) {
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a connection to a path node.
|
||||
* @param {RailSystem} rs
|
||||
* @param {Object} node
|
||||
* @param {Object} other
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export function addConnection(rs, node, other) {
|
||||
node.connectedNodes.push(other);
|
||||
return updateConnections(rs, node);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a connection from a path node.
|
||||
* @param {RailSystem} rs
|
||||
* @param {Object} node
|
||||
* @param {Object} other
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export function removeConnection(rs, node, other) {
|
||||
const idx = node.connectedNodes.findIndex(n => n.id === other.id);
|
||||
return new Promise((resolve, reject) => {
|
||||
|
|
|
@ -1,11 +1,26 @@
|
|||
import axios from "axios";
|
||||
import {API_URL} from "./constants";
|
||||
|
||||
export class RailSystem {
|
||||
constructor(data) {
|
||||
this.id = data.id;
|
||||
this.name = data.name;
|
||||
this.segments = [];
|
||||
this.components = [];
|
||||
this.websocket = null;
|
||||
this.selectedComponent = null;
|
||||
}
|
||||
}
|
||||
|
||||
export function refreshRailSystems(rsStore) {
|
||||
return new Promise(resolve => {
|
||||
axios.get(`${API_URL}/rs`)
|
||||
.then(response => {
|
||||
rsStore.railSystems = response.data;
|
||||
const rsItems = response.data;
|
||||
rsStore.railSystems.length = 0;
|
||||
for (let i = 0; i < rsItems.length; i++) {
|
||||
rsStore.railSystems.push(new RailSystem(rsItems[i]));
|
||||
}
|
||||
resolve();
|
||||
})
|
||||
.catch(error => console.error(error));
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
import {WS_URL} from "./constants";
|
||||
|
||||
/**
|
||||
* Establishes a websocket connection to the given rail system.
|
||||
* @param {RailSystem} rs
|
||||
*/
|
||||
export function establishWebsocketConnection(rs) {
|
||||
if (rs.websocket) {
|
||||
rs.websocket.close();
|
||||
}
|
||||
closeWebsocketConnection(rs);
|
||||
rs.websocket = new WebSocket(`${WS_URL}/${rs.id}`);
|
||||
rs.websocket.onopen = () => {
|
||||
console.log("Opened websocket connection to rail system " + rs.id);
|
||||
|
@ -25,6 +27,10 @@ export function establishWebsocketConnection(rs) {
|
|||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the websocket connection to a rail system, if possible.
|
||||
* @param {RailSystem} rs
|
||||
*/
|
||||
export function closeWebsocketConnection(rs) {
|
||||
if (rs.websocket) {
|
||||
rs.websocket.close();
|
||||
|
|
|
@ -15,6 +15,7 @@
|
|||
id="railSystemSelect"
|
||||
v-model="rsStore.selectedRailSystem"
|
||||
class="form-select form-select-sm"
|
||||
@change="rsStore.onSelectedRailSystemChanged()"
|
||||
>
|
||||
<option v-for="rs in rsStore.railSystems" :key="rs.id" :value="rs">
|
||||
{{rs.name}}
|
||||
|
|
|
@ -0,0 +1,115 @@
|
|||
<template>
|
||||
<div class="modal fade" tabindex="-1" id="createLinkTokenModal">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Create Link Token</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>
|
||||
Create a <em>link token</em> to link components in this rail system
|
||||
to actual devices in your system, so your world can talk to this
|
||||
system. Each link token should have a unique label that can be used
|
||||
to identify it, and a list of components that it's linked to.
|
||||
</p>
|
||||
<p>
|
||||
Note that for security purposes, the raw token that's generated is
|
||||
only shown once, and is never available again. If you lose the
|
||||
token, you must create a new one instead and delete the old one.
|
||||
</p>
|
||||
<form>
|
||||
<div class="mb-3">
|
||||
<label for="createLinkTokenLabel" class="form-label">Label</label>
|
||||
<input
|
||||
id="createLinkTokenLabel"
|
||||
class="form-control"
|
||||
type="text"
|
||||
v-model="formData.label"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label" for="createLinkTokenComponentSelect">Select Components to Link</label>
|
||||
<ComponentSelector id="createLinkTokenComponentSelect" :railSystem="railSystem" v-model="components"/>
|
||||
</div>
|
||||
</form>
|
||||
<div v-if="warnings.length > 0">
|
||||
<div v-for="msg in warnings" :key="msg" class="alert alert-danger mt-2">
|
||||
{{msg}}
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="token !== null" class="alert alert-success mt-2">
|
||||
Created token: {{token}}
|
||||
<br>
|
||||
<small>Copy this token now; it will not be shown again.</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal" @click="reset()">Close</button>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-primary"
|
||||
@click="formSubmitted()"
|
||||
v-if="token == null"
|
||||
>
|
||||
Add
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {createLinkToken} from "../../api/linkTokens";
|
||||
import {RailSystem} from "../../api/railSystems";
|
||||
import ComponentSelector from "./util/ComponentSelector.vue";
|
||||
|
||||
export default {
|
||||
name: "CreateLinkTokenModal",
|
||||
components: {ComponentSelector},
|
||||
props: {
|
||||
railSystem: {
|
||||
type: RailSystem,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
formData: {
|
||||
label: "",
|
||||
componentIds: []
|
||||
},
|
||||
components: [],
|
||||
warnings: [],
|
||||
token: null
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
formSubmitted() {
|
||||
this.formData.componentIds = this.components.map(c => c.id);
|
||||
createLinkToken(this.railSystem, this.formData)
|
||||
.then(token => {
|
||||
this.token = token;
|
||||
})
|
||||
.catch(error => {
|
||||
this.warnings.length = 0;
|
||||
this.warnings.push("Couldn't create token: " + error.response.data.message);
|
||||
});
|
||||
},
|
||||
reset() {
|
||||
// TODO: Fix this!! Reset doesn't work.
|
||||
this.formData.label = "";
|
||||
this.formData.componentIds = [];
|
||||
this.components = [];
|
||||
this.warnings = [];
|
||||
this.token = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
<h3>Rail System: <em>{{railSystem.name}}</em></h3>
|
||||
<SegmentsView :segments="railSystem.segments" v-if="railSystem.segments"/>
|
||||
<div class="dropdown">
|
||||
<div class="dropdown d-inline-block me-2">
|
||||
<button class="btn btn-success btn-sm dropdown-toggle" type="button" id="railSystemAddComponentsToggle" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
Add Component
|
||||
</button>
|
||||
|
@ -48,6 +48,17 @@
|
|||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div v-if="createLinkTokenAllowed()" class="d-inline-block">
|
||||
<button
|
||||
class="btn btn-success btn-sm"
|
||||
type="button"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#createLinkTokenModal"
|
||||
>
|
||||
Create Link Token
|
||||
</button>
|
||||
<CreateLinkTokenModal :railSystem="railSystem" />
|
||||
</div>
|
||||
<AddSegmentModal />
|
||||
<AddSignalModal v-if="addSignalAllowed()" />
|
||||
<AddSegmentBoundaryModal v-if="addSegmentBoundaryAllowed()" />
|
||||
|
@ -60,10 +71,13 @@ import AddSegmentModal from "./AddSegmentModal.vue";
|
|||
import AddSignalModal from "./component/AddSignalModal.vue";
|
||||
import AddSegmentBoundaryModal from "./component/AddSegmentBoundaryModal.vue";
|
||||
import AddSwitchModal from "./component/AddSwitchModal.vue";
|
||||
import CreateLinkTokenModal from "./CreateLinkTokenModal.vue";
|
||||
import {RailSystem} from "../../api/railSystems";
|
||||
|
||||
export default {
|
||||
name: "RailSystemPropertiesView",
|
||||
components: {
|
||||
CreateLinkTokenModal,
|
||||
AddSwitchModal,
|
||||
AddSegmentBoundaryModal,
|
||||
AddSignalModal,
|
||||
|
@ -72,19 +86,22 @@ export default {
|
|||
},
|
||||
props: {
|
||||
railSystem: {
|
||||
type: Object,
|
||||
type: RailSystem,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
addSignalAllowed() {
|
||||
return this.railSystem.segments && this.railSystem.segments.length > 0
|
||||
return this.railSystem.segments && this.railSystem.segments.length > 0;
|
||||
},
|
||||
addSegmentBoundaryAllowed() {
|
||||
return this.railSystem.segments && this.railSystem.segments.length > 1
|
||||
return this.railSystem.segments && this.railSystem.segments.length > 1;
|
||||
},
|
||||
addSwitchAllowed() {
|
||||
return this.railSystem.components && this.railSystem.components.length > 1
|
||||
return this.railSystem.components && this.railSystem.components.length > 1;
|
||||
},
|
||||
createLinkTokenAllowed() {
|
||||
return this.railSystem.components && this.railSystem.components.length > 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,106 @@
|
|||
<template>
|
||||
<div class="border p-2">
|
||||
<input type="text" class="form-control mb-2" placeholder="Search for components" v-model="searchQuery" @keyup="refreshComponents()" />
|
||||
<ul class="list-group list-group-flush" style="overflow: auto; max-height: 200px">
|
||||
<li
|
||||
class="list-group-item"
|
||||
v-for="component in possibleComponents"
|
||||
:key="component.id"
|
||||
>
|
||||
{{component.name}}
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="badge btn btn-sm btn-success float-end"
|
||||
@click="selectComponent(component)"
|
||||
v-if="!isComponentSelected(component)"
|
||||
>
|
||||
Select
|
||||
</button>
|
||||
<span v-if="isComponentSelected(component)" class="badge bg-secondary float-end">Selected</span>
|
||||
</li>
|
||||
</ul>
|
||||
<div v-if="selectedComponents.length > 0" class="mt-2">
|
||||
<span
|
||||
class="badge bg-secondary me-1 mb-1"
|
||||
v-for="component in selectedComponents"
|
||||
:key="component.id"
|
||||
>
|
||||
{{component.name}}
|
||||
<button class="badge rounded-pill bg-danger" @click="deselectComponent(component)">
|
||||
X
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
class="btn btn-sm btn-secondary"
|
||||
type="button"
|
||||
v-if="selectedComponents.length > 1"
|
||||
@click="selectedComponents.length = 0"
|
||||
>
|
||||
Clear All
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {RailSystem} from "../../../api/railSystems";
|
||||
import {searchComponents} from "../../../api/components";
|
||||
|
||||
export default {
|
||||
name: "ComponentSelector",
|
||||
props: {
|
||||
railSystem: {
|
||||
type: RailSystem,
|
||||
required: true
|
||||
},
|
||||
modelValue: {
|
||||
type: Array,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
emits: ["update:modelValue"],
|
||||
data() {
|
||||
return {
|
||||
searchQuery: null,
|
||||
selectedComponents: [],
|
||||
possibleComponents: []
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.refreshComponents();
|
||||
},
|
||||
methods: {
|
||||
refreshComponents() {
|
||||
searchComponents(this.railSystem, this.searchQuery)
|
||||
.then(page => this.possibleComponents = page.content);
|
||||
},
|
||||
isComponentSelected(component) {
|
||||
return this.selectedComponents.some(c => c.id === component.id);
|
||||
},
|
||||
selectComponent(component) {
|
||||
if (this.isComponentSelected(component)) return;
|
||||
this.selectedComponents.push(component);
|
||||
this.selectedComponents.sort((a, b) => {
|
||||
const nameA = a.name.toUpperCase();
|
||||
const nameB = b.name.toUpperCase();
|
||||
if (nameA < nameB) return -1;
|
||||
if (nameA > nameB) return 1;
|
||||
return 0;
|
||||
});
|
||||
this.$emit("update:modelValue", this.selectedComponents);
|
||||
},
|
||||
deselectComponent(component) {
|
||||
const idx = this.selectedComponents.findIndex(c => c.id === component.id);
|
||||
if (idx > -1) {
|
||||
this.selectedComponents.splice(idx, 1);
|
||||
this.$emit("update:modelValue", this.selectedComponents);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
|
@ -3,20 +3,9 @@ import { createPinia } from 'pinia';
|
|||
import App from './App.vue'
|
||||
import "bootstrap/dist/css/bootstrap.min.css";
|
||||
import "bootstrap";
|
||||
import {useRailSystemsStore} from "./stores/railSystemsStore";
|
||||
|
||||
|
||||
const pinia = createPinia();
|
||||
const app = createApp(App);
|
||||
app.use(pinia);
|
||||
|
||||
// Configure rail system updates.
|
||||
const rsStore = useRailSystemsStore();
|
||||
rsStore.$subscribe(mutation => {
|
||||
const evt = mutation.events;
|
||||
if (evt.key === "selectedRailSystem" && evt.newValue !== null) {
|
||||
rsStore.onSelectedRailSystemChanged();
|
||||
}
|
||||
});
|
||||
|
||||
app.mount('#app')
|
||||
|
|
|
@ -5,14 +5,12 @@ import {closeWebsocketConnection, establishWebsocketConnection} from "../api/web
|
|||
|
||||
export const useRailSystemsStore = defineStore('RailSystemsStore', {
|
||||
state: () => ({
|
||||
/**
|
||||
* @type {RailSystem[]}
|
||||
*/
|
||||
railSystems: [],
|
||||
/**
|
||||
* @type {{
|
||||
* segments: [Object],
|
||||
* components: [Object],
|
||||
* selectedComponent: Object | null,
|
||||
* websocket: WebSocket | null
|
||||
* } | null}
|
||||
* @type {RailSystem | null}
|
||||
*/
|
||||
selectedRailSystem: null
|
||||
}),
|
||||
|
|
|
@ -29,11 +29,12 @@ public class ComponentsApiController {
|
|||
|
||||
@GetMapping(path = "/search")
|
||||
public Page<SimpleComponentResponse> searchComponents(
|
||||
@PathVariable long rsId,
|
||||
@RequestParam(name = "q", required = false) String searchQuery,
|
||||
@PageableDefault(sort = "name")
|
||||
Pageable pageable
|
||||
) {
|
||||
return componentService.search(searchQuery, pageable);
|
||||
return componentService.search(rsId, searchQuery, pageable);
|
||||
}
|
||||
|
||||
@GetMapping(path = "/{cId}")
|
||||
|
|
|
@ -16,6 +16,8 @@ import org.springframework.stereotype.Service;
|
|||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
import javax.persistence.criteria.Predicate;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
@ -42,12 +44,14 @@ public class ComponentService {
|
|||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public Page<SimpleComponentResponse> search(String searchQuery, Pageable pageable) {
|
||||
public Page<SimpleComponentResponse> search(long rsId, String searchQuery, Pageable pageable) {
|
||||
return componentRepository.findAll((root, query, cb) -> {
|
||||
List<Predicate> predicates = new ArrayList<>(2);
|
||||
predicates.add(cb.equal(root.get("railSystem").get("id"), rsId));
|
||||
if (searchQuery != null && !searchQuery.isBlank()) {
|
||||
return cb.like(cb.lower(root.get("name")), '%' + searchQuery.toLowerCase() + '%');
|
||||
predicates.add(cb.like(cb.lower(root.get("name")), '%' + searchQuery.toLowerCase() + '%'));
|
||||
}
|
||||
return cb.and();
|
||||
return cb.and(predicates.toArray(new Predicate[0]));
|
||||
}, pageable).map(SimpleComponentResponse::new);
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue