feat(ui): add support for multiple UI server configurations
authorJérôme Benoit <jerome.benoit@sap.com>
Mon, 26 Feb 2024 23:13:27 +0000 (00:13 +0100)
committerJérôme Benoit <jerome.benoit@sap.com>
Mon, 26 Feb 2024 23:13:27 +0000 (00:13 +0100)
closes #978

Signed-off-by: Jérôme Benoit <jerome.benoit@sap.com>
ui/web/src/composables/UIClient.ts
ui/web/src/composables/Utils.ts
ui/web/src/composables/index.ts
ui/web/src/main.ts
ui/web/src/types/ConfigurationType.ts
ui/web/src/views/ChargingStationsView.vue

index d569b93b09719e91cd8536a4ac5f315ddc5e4417..51d61f263f7788adcd45e930ef79103dbd92d883 100644 (file)
@@ -1,3 +1,4 @@
+import { useToast } from 'vue-toast-notification'
 import {
   ApplicationProtocol,
   AuthenticationType,
@@ -161,15 +162,21 @@ export class UIClient {
       `${this.uiServerConfiguration.secure === true ? ApplicationProtocol.WSS : ApplicationProtocol.WS}://${this.uiServerConfiguration.host}:${this.uiServerConfiguration.port}`,
       protocols
     )
-    this.ws.onopen = openEvent => {
-      console.info('WebSocket opened', openEvent)
+    this.ws.onopen = () => {
+      useToast().success(
+        `WebSocket to UI server '${this.uiServerConfiguration.host}' successfully opened`
+      )
     }
     this.ws.onmessage = this.responseHandler.bind(this)
     this.ws.onerror = errorEvent => {
-      console.error('WebSocket error: ', errorEvent)
+      useToast().error(`Error in WebSocket to UI server '${this.uiServerConfiguration.host}'`)
+      console.error(
+        `Error in WebSocket to UI server '${this.uiServerConfiguration.host}'`,
+        errorEvent
+      )
     }
-    this.ws.onclose = closeEvent => {
-      console.info('WebSocket closed: ', closeEvent)
+    this.ws.onclose = () => {
+      useToast().info(`WebSocket to UI server '${this.uiServerConfiguration.host}' closed`)
     }
   }
 
index 451c4155fd7a114990f28f52fb619fc4932d774d..fffd7a54966a54a2e092d4eef2a4d5360b45036b 100644 (file)
@@ -32,3 +32,12 @@ export const convertToInt = (value: unknown): number => {
   }
   return changedValue
 }
+
+export const setToLocalStorage = <T>(key: string, value: T): void => {
+  localStorage.setItem(key, JSON.stringify(value))
+}
+
+export const getFromLocalStorage = <T>(key: string, defaultValue: T): T => {
+  const item = localStorage.getItem(key)
+  return item != null ? (JSON.parse(item) as T) : defaultValue
+}
index 31e231e87056fade9f399e9d8805428ba87febeb..bc62dcb5abe4c9245c069b7f1c8e1c3d27ee71fb 100644 (file)
@@ -1,2 +1,2 @@
 export { UIClient } from './UIClient'
-export { convertToBoolean, convertToInt } from './Utils'
+export { convertToBoolean, convertToInt, getFromLocalStorage, setToLocalStorage } from './Utils'
index 6dc93195770718d467a2b03bf78934eacf126e21..f3c9ef036f6fb46445aa2f06f0c482d632e4e1cc 100644 (file)
@@ -1,38 +1,57 @@
-import { createApp } from 'vue'
+import { type App as AppType, createApp } from 'vue'
 import ToastPlugin from 'vue-toast-notification'
 import type { ConfigurationData, ResponsePayload } from '@/types'
 import { router } from '@/router'
-import { UIClient } from '@/composables'
+import { UIClient, getFromLocalStorage, setToLocalStorage } from '@/composables'
 import App from '@/App.vue'
 import 'vue-toast-notification/dist/theme-bootstrap.css'
 
-const initializeApp = (config: ConfigurationData) => {
-  const app = createApp(App)
+const app = createApp(App)
+
+const initializeApp = (app: AppType, config: ConfigurationData) => {
   app.config.errorHandler = (error, instance, info) => {
     console.error('Error:', error)
     console.info('Vue instance:', instance)
     console.info('Error info:', info)
     // TODO: add code for UI notifications or other error handling logic
   }
-  app.config.globalProperties.$configuration = config
-  app.config.globalProperties.$chargingStations = []
-  app.config.globalProperties.$uiClient = UIClient.getInstance(
-    app.config.globalProperties.$configuration.uiServer
-  )
-  app.config.globalProperties.$uiClient.registerWSEventListener('open', () => {
-    app.config.globalProperties.$uiClient
-      .listChargingStations()
-      .then((response: ResponsePayload) => {
-        app.config.globalProperties.$chargingStations = response.chargingStations
-      })
-      .catch((error: Error) => {
-        // TODO: add code for UI notifications or other error handling logic
-        console.error('Error at fetching charging stations:', error)
-      })
-      .finally(() => {
-        app.use(router).use(ToastPlugin).mount('#app')
-      })
-  })
+  if (!Array.isArray(config.uiServer)) {
+    config.uiServer = [config.uiServer]
+  }
+  if (app.config.globalProperties.$configuration == null) {
+    app.config.globalProperties.$configuration = config
+  }
+  if (!Array.isArray(app.config.globalProperties.$chargingStations)) {
+    app.config.globalProperties.$chargingStations = []
+  }
+  if (
+    getFromLocalStorage<number | undefined>('uiServerConfigurationIndex', undefined) == null ||
+    getFromLocalStorage<number>('uiServerConfigurationIndex', 0) >
+      app.config.globalProperties.$configuration.uiServer.length - 1
+  ) {
+    setToLocalStorage<number>('uiServerConfigurationIndex', 0)
+  }
+  if (app.config.globalProperties.$uiClient == null) {
+    app.config.globalProperties.$uiClient = UIClient.getInstance(
+      app.config.globalProperties.$configuration.uiServer[
+        getFromLocalStorage<number>('uiServerConfigurationIndex', 0)
+      ]
+    )
+    app.config.globalProperties.$uiClient.registerWSEventListener('open', () => {
+      app.config.globalProperties.$uiClient
+        .listChargingStations()
+        .then((response: ResponsePayload) => {
+          app.config.globalProperties.$chargingStations = response.chargingStations
+        })
+        .catch((error: Error) => {
+          // TODO: add code for UI notifications or other error handling logic
+          console.error('Error at fetching charging stations:', error)
+        })
+        .finally(() => {
+          app.use(router).use(ToastPlugin).mount('#app')
+        })
+    })
+  }
 }
 
 fetch('/config.json')
@@ -46,7 +65,7 @@ fetch('/config.json')
       .json()
       .then(config => {
         try {
-          initializeApp(config)
+          initializeApp(app, config)
         } catch (error) {
           // TODO: add code for UI notifications or other error handling logic
           console.error('Error at initializing app:', error)
index 5d0f76797141ec280f4e3cc597a3de2f52e5b36c..51bd21388b82b6c4260c5d1e2931f8611f45bac2 100644 (file)
@@ -1,7 +1,7 @@
 import type { AuthenticationType, Protocol, ProtocolVersion } from './UIProtocol'
 
 export type ConfigurationData = {
-  uiServer: UIServerConfigurationSection
+  uiServer: UIServerConfigurationSection | UIServerConfigurationSection[]
 }
 
 export type UIServerConfigurationSection = {
index d5868083d15ce558d2fcbc0842aef2d4e79e48fb..fe660111bee8fbc77137f8c2485d8bef5af9653f 100644 (file)
@@ -1,5 +1,38 @@
 <template>
   <Container id="charging-stations-container">
+    <Container id="ui-server-container">
+      <select
+        v-show="Array.isArray(uiServerConfigurations) && uiServerConfigurations.length > 1"
+        id="ui-server-selector"
+        v-model="state.uiServerIndex"
+        @change="
+          () => {
+            try {
+              if (
+                getFromLocalStorage<number>('uiServerConfigurationIndex', 0) !== state.uiServerIndex
+              ) {
+                setToLocalStorage<number>('uiServerConfigurationIndex', state.uiServerIndex)
+                app!.appContext.config.globalProperties.$uiClient.setConfiguration(
+                  app?.appContext.config.globalProperties.$configuration.uiServer[
+                    getFromLocalStorage<number>('uiServerConfigurationIndex', state.uiServerIndex)
+                  ]
+                )
+              }
+            } catch (error) {
+              $toast.error('Error at changing UI server configuration')
+              console.error('Error at changing UI server configuration:', error)
+            }
+          }
+        "
+      >
+        <option
+          v-for="uiServerConfiguration in uiServerConfigurations"
+          :value="uiServerConfiguration.index"
+        >
+          {{ uiServerConfiguration.configuration.host }}
+        </option>
+      </select>
+    </Container>
     <Container id="buttons-container">
       <Button @click="startSimulator()">Start Simulator</Button>
       <Button @click="stopSimulator()">Stop Simulator</Button>
 import { getCurrentInstance, reactive } from 'vue'
 import { useToast } from 'vue-toast-notification'
 import CSTable from '@/components/charging-stations/CSTable.vue'
-import type { ResponsePayload } from '@/types'
+import type { ResponsePayload, UIServerConfigurationSection } from '@/types'
 import Container from '@/components/Container.vue'
 import ReloadButton from '@/components/buttons/ReloadButton.vue'
 import Button from '@/components/buttons/Button.vue'
+import { getFromLocalStorage, setToLocalStorage } from '@/composables'
 
 const state = reactive({
-  isLoading: false
+  isLoading: false,
+  uiServerIndex: getFromLocalStorage<number>('uiServerConfigurationIndex', 0)
 })
 
 const app = getCurrentInstance()
 const uiClient = app?.appContext.config.globalProperties.$uiClient
+const uiServerConfigurations: { configuration: UIServerConfigurationSection; index: number }[] =
+  app?.appContext.config.globalProperties.$configuration.uiServer.map(
+    (configuration: UIServerConfigurationSection, index: number) => ({
+      configuration,
+      index
+    })
+  )
 
 const $toast = useToast()
 
@@ -98,6 +140,16 @@ const stopSimulator = (): void => {
   flex-direction: column;
 }
 
+#ui-server-container {
+  display: flex;
+  flex-direction: row;
+}
+
+#ui-server-selector {
+  width: 100%;
+  text-align: center;
+}
+
 #buttons-container {
   display: flex;
   flex-direction: row;