From 4c7056c81a3bb5abdaba075ab85d261dc3560579 Mon Sep 17 00:00:00 2001
From: Matthias Feyll <matthias.feyll@@stud.h-da.de>
Date: Wed, 8 Jan 2025 12:55:13 +0100
Subject: [PATCH] (ui): change routine identification to category

---
 .../src/components/devices/api/pnd.fetch.ts   |  1 +
 .../devices/reducer/device.reducer.ts         | 34 +++++----
 .../devices/routines/device.routine.ts        |  7 +-
 .../devices/routines/mne.routine.ts           | 32 ++++++--
 .../devices/view/device.view.table.tsx        |  2 +-
 .../devices/view/device.view.tabs.tsx         |  2 +-
 .../view_model/device.tabs.viewmodel.ts       |  2 +-
 .../src/components/login/view/login.view.tsx  |  2 +-
 .../json_viewer/view/json_viewer.view.tsx     |  2 +-
 react-ui/src/shared/helper/debug.ts           |  8 +-
 .../src/shared/provider/auth.provider.tsx     |  2 +-
 .../shared/provider/menu/menu.provider.tsx    |  2 +-
 .../src/shared/provider/utils.provider.tsx    |  2 +-
 .../src/shared/reducer/routine.reducer.ts     | 75 ++++--------------
 react-ui/src/shared/types/category.type.ts    | 17 +++++
 .../interfaces.type.ts}                       |  0
 react-ui/src/shared/types/thunk.type.ts       | 12 +++
 react-ui/src/shared/utils/routine.manager.ts  | 76 ++++++++++---------
 18 files changed, 151 insertions(+), 127 deletions(-)
 create mode 100644 react-ui/src/shared/types/category.type.ts
 rename react-ui/src/shared/{helper/interfaces.ts => types/interfaces.type.ts} (100%)
 create mode 100644 react-ui/src/shared/types/thunk.type.ts

diff --git a/react-ui/src/components/devices/api/pnd.fetch.ts b/react-ui/src/components/devices/api/pnd.fetch.ts
index 6e677ab8d..fd49de636 100644
--- a/react-ui/src/components/devices/api/pnd.fetch.ts
+++ b/react-ui/src/components/devices/api/pnd.fetch.ts
@@ -2,6 +2,7 @@ import { PndServiceGetPndListApiArg, api } from "@api/api"
 import { createAsyncThunk } from "@reduxjs/toolkit"
 import { setPnds } from "../reducer/device.reducer"
 
+// TODO rethink this. This should be in the shared part bc its getting invoked in the procteded layout
 export const fetchPnds = createAsyncThunk('device/fetchPnds', (_, thunkApi) => {
     const payload: PndServiceGetPndListApiArg = {
         timestamp: new Date().getTime().toString(),
diff --git a/react-ui/src/components/devices/reducer/device.reducer.ts b/react-ui/src/components/devices/reducer/device.reducer.ts
index f574509be..9d91700c4 100755
--- a/react-ui/src/components/devices/reducer/device.reducer.ts
+++ b/react-ui/src/components/devices/reducer/device.reducer.ts
@@ -11,27 +11,25 @@ import { startListening } from '/src/stores/middleware/listener.middleware'
 
 export type Device = NetworkelementFlattenedManagedNetworkElement
 
-interface SelectedDeviceInterface {
+interface SelectedObject {
     device: Device
     mne: NetworkelementManagedNetworkElement | null
     json: JSON | null
 }
 
-type SelectedDeviceType = SelectedDeviceInterface | null
-
 export interface DeviceSliceState {
     devices: Device[]
     pnds: PndPrincipalNetworkDomain[]
 
     activeTab: DeviceViewTabValues
-    selectedDevice: SelectedDeviceType
+    selected: SelectedObject | null
 }
 
 const initialState: DeviceSliceState = {
     devices: [],
     pnds: [],
     activeTab: DeviceViewTabValues.METADATA,
-    selectedDevice: null,
+    selected: null,
 }
 
 const deviceSlice = createSlice({
@@ -48,29 +46,33 @@ const deviceSlice = createSlice({
             state.activeTab = action.payload
         },
         setSelectedDevice: (state, action: PayloadAction<Device | null>) => {
-            let selectedDevice: SelectedDeviceType = null
+            let selectedObject = null;
             if (action.payload) {
-                selectedDevice = { device: action.payload, mne: null, json: null }
+                selectedObject = { device: action.payload, mne: null, json: null }
             }
 
-            state.selectedDevice = selectedDevice
+            state.selected = selectedObject
         },
         setSelectedMne: (state, action: PayloadAction<NetworkelementManagedNetworkElement>) => {
-            if (!state.selectedDevice) {
-                throw new Error('Selected Device is null where it shouldn´t be null')
+            if (!state.selected) {
+                throw new Error('Can not find corresponding device')
+            }
+
+            // safety check to prevent possible race conditions
+            if (state.selected.device.id !== action.payload.id) {
+                // TODO proper error handling by retry fetching the device object
+                throw new Error('Device and mne id does not match')
             }
 
-            state.selectedDevice.mne = action.payload
-            // TODO maybe don´t take the device of "selected device" instead search in the devices array for the proper device
-            // should not make a diffrence due to pointer but dunno
+            state.selected.mne = action.payload
         },
 
         setSelectedJson: (state, action: PayloadAction<JSON>) => {
-            if (!state.selectedDevice) {
+            if (!state.selected) {
                 throw new Error('Selected Device is null where it shouldn´t be null')
             }
 
-            state.selectedDevice.json = action.payload || null
+            state.selected.json = action.payload || null
         },
     },
 })
@@ -86,7 +88,7 @@ startListening({
     predicate: (action) => setDevices.match(action),
     effect: async (action, listenerApi) => {
         const { device: state } = listenerApi.getOriginalState() as RootState
-        if (state.selectedDevice) {
+        if (state.selected) {
             return
         }
 
diff --git a/react-ui/src/components/devices/routines/device.routine.ts b/react-ui/src/components/devices/routines/device.routine.ts
index 10caffa2f..f9b3e1ee2 100755
--- a/react-ui/src/components/devices/routines/device.routine.ts
+++ b/react-ui/src/components/devices/routines/device.routine.ts
@@ -19,8 +19,13 @@ startListening({
 export const fetchDevicesThunk = createAsyncThunk(FETCH_DEVICE_ACTION, (_, thunkApi) => {
     const { user } = thunkApi.getState() as RootState
 
+    if (!user.user?.roles) {
+        throw new Error('Background MNE fetching failed! User data is missing. Reload page or logout and login again')
+        // TODO
+    }
+
     const payload: NetworkElementServiceGetAllFlattenedApiArg = {
-        pid: Object.keys(user?.user.roles)[0],
+        pid: Object.keys(user.user.roles)[0],
         timestamp: new Date().getTime().toString(),
     }
 
diff --git a/react-ui/src/components/devices/routines/mne.routine.ts b/react-ui/src/components/devices/routines/mne.routine.ts
index 7de1d1814..a3ea43d2f 100755
--- a/react-ui/src/components/devices/routines/mne.routine.ts
+++ b/react-ui/src/components/devices/routines/mne.routine.ts
@@ -6,26 +6,38 @@ import {
     setSelectedMne,
 } from '@component/devices/reducer/device.reducer'
 import { createAsyncThunk } from '@reduxjs/toolkit'
-import { addRoutine, CATEGORIES } from '@shared/reducer/routine.reducer'
+import { addRoutine } from '@shared/reducer/routine.reducer'
+import { Category } from '@shared/types/category.type'
 import { RootState } from 'src/stores'
 import { startListening } from '../../../stores/middleware/listener.middleware'
 
 export const FETCH_MNE_ACTION = 'subscription/device/fetchSelectedMNE'
 
-// fetch mne if selected device is set
+/**
+ * #0
+ * Trigger fetch MNE (#1)
+ * 
+ * Triggered by a selectedDevice
+ */
 startListening({
     predicate: (action) => setSelectedDevice.match(action) && !!action.payload,
     effect: async (action, listenerApi) => {
         listenerApi.dispatch(
             addRoutine({
                 thunk: fetchSelectedMneThunk,
-                category: CATEGORIES.TAB,
+                category: Category.TAB,
                 payload: action.payload,
             })
         )
     },
 })
 
+/**
+ * #1
+ * Fetch MNE
+ * 
+ * Triggered by #0
+ */
 const FETCH_MNE_INTERVAL = 5000 // in ms
 export const fetchSelectedMneThunk = createAsyncThunk(
     FETCH_MNE_ACTION,
@@ -56,7 +68,12 @@ export const fetchSelectedMneThunk = createAsyncThunk(
     }
 )
 
-// save fetched mne
+/**
+ * #2
+ * Received MNE
+ * 
+ * Triggered by #1
+ */
 startListening({
     predicate: (action) => api.endpoints.networkElementServiceGet.matchFulfilled(action),
     effect: async (action, listenerApi) => {
@@ -64,7 +81,12 @@ startListening({
     },
 })
 
-// save fetched mne
+/**
+ * #3
+ * Fetch & receive json
+ * 
+ * Triggered by #2
+ */
 startListening({
     predicate: (action) => setSelectedMne.match(action),
     effect: async (action, listenerApi) => {
diff --git a/react-ui/src/components/devices/view/device.view.table.tsx b/react-ui/src/components/devices/view/device.view.table.tsx
index 312caab60..9b731fef5 100755
--- a/react-ui/src/components/devices/view/device.view.table.tsx
+++ b/react-ui/src/components/devices/view/device.view.table.tsx
@@ -5,7 +5,7 @@ import { useTranslation } from "react-i18next";
 import { useDeviceTableViewModel } from "../view_model/device.table.viewmodel";
 
 export const DeviceViewTable = (searchRef: MutableRefObject<HTMLInputElement>) => {
-    const { devices, pnds, selectedDevice } = useAppSelector(state => state.device);
+    const { devices, pnds, selected: selectedDevice } = useAppSelector(state => state.device);
     const { t } = useTranslation('common');
     const { trClickHandler } = useDeviceTableViewModel(searchRef);
 
diff --git a/react-ui/src/components/devices/view/device.view.tabs.tsx b/react-ui/src/components/devices/view/device.view.tabs.tsx
index 2929f9b64..a2768a0ea 100755
--- a/react-ui/src/components/devices/view/device.view.tabs.tsx
+++ b/react-ui/src/components/devices/view/device.view.tabs.tsx
@@ -8,7 +8,7 @@ export enum DeviceViewTabValues {
 }
 
 export const DeviceViewTabs = (activeTab: DeviceViewTabValues) => {
-    const { selectedDevice } = useAppSelector(state => state.device);
+    const { selected: selectedDevice } = useAppSelector(state => state.device);
     const { jsonYang } = useDeviceTabsViewModel();
 
     const metadataTab = () => {
diff --git a/react-ui/src/components/devices/view_model/device.tabs.viewmodel.ts b/react-ui/src/components/devices/view_model/device.tabs.viewmodel.ts
index 4a60567b6..af4cc3abb 100755
--- a/react-ui/src/components/devices/view_model/device.tabs.viewmodel.ts
+++ b/react-ui/src/components/devices/view_model/device.tabs.viewmodel.ts
@@ -7,7 +7,7 @@ export enum DeviceViewTabValues {
 }
 
 export const useDeviceTabsViewModel = () => {
-    const { selectedDevice } = useAppSelector((state) => state.device)
+    const { selected: selectedDevice } = useAppSelector((state) => state.device)
 
     const getYangModelJSON = (): JSON | null => {
         if (!selectedDevice?.json) {
diff --git a/react-ui/src/components/login/view/login.view.tsx b/react-ui/src/components/login/view/login.view.tsx
index 03d7406f4..38afc83a1 100755
--- a/react-ui/src/components/login/view/login.view.tsx
+++ b/react-ui/src/components/login/view/login.view.tsx
@@ -1,5 +1,5 @@
 import logo from '@assets/logo.svg'
-import { BasicProp } from '@helper/interfaces'
+import { BasicProp } from '@shared/types/interfaces.type'
 import React, { useRef } from 'react'
 import { Alert, Button, Col, Container, Form, Image, Row, Spinner } from 'react-bootstrap'
 import { useTranslation } from 'react-i18next'
diff --git a/react-ui/src/shared/components/json_viewer/view/json_viewer.view.tsx b/react-ui/src/shared/components/json_viewer/view/json_viewer.view.tsx
index 720f75c7d..b8358c686 100755
--- a/react-ui/src/shared/components/json_viewer/view/json_viewer.view.tsx
+++ b/react-ui/src/shared/components/json_viewer/view/json_viewer.view.tsx
@@ -130,7 +130,7 @@ export const JsonViewer = ({ json }: JsonViewerProbs) => {
     const searchHTML = () => {
         return (
             <>
-                <Form.Group controlId='device.search' className='p-0 mx-1 pt-2'>
+                <Form.Group controlId='json_viewer.search' className='p-0 mx-1 pt-2'>
                     <Form.Control type="text" placeholder={t('device.search.placeholder')} ref={search} />
                 </Form.Group>
             </>
diff --git a/react-ui/src/shared/helper/debug.ts b/react-ui/src/shared/helper/debug.ts
index 6628989b8..db10a0979 100644
--- a/react-ui/src/shared/helper/debug.ts
+++ b/react-ui/src/shared/helper/debug.ts
@@ -1,5 +1,11 @@
-export const debugMessage = (message: string) => {
+export const warnMessage = (message: string) => {
     if (window?.env === 'development') {
         console.warn("Debug: \n" + message)
     }
+}
+
+export const infoMessage = (message: string) => {
+    if (window?.env === 'development') {
+        console.info("Info: \n" + message)
+    }
 }
\ No newline at end of file
diff --git a/react-ui/src/shared/provider/auth.provider.tsx b/react-ui/src/shared/provider/auth.provider.tsx
index 69bdccbdf..77219bdce 100755
--- a/react-ui/src/shared/provider/auth.provider.tsx
+++ b/react-ui/src/shared/provider/auth.provider.tsx
@@ -1,8 +1,8 @@
 import { AuthServiceLoginApiArg, AuthServiceLoginApiResponse, useAuthServiceLoginMutation } from "@api/api";
 import { getCookieValue } from "@helper/coookie";
-import { BasicProp } from "@helper/interfaces";
 import { useAppDispatch, useAppSelector } from "@hooks";
 import { DEVICE_URL, LOGIN_URL } from "@routes";
+import { BasicProp } from "@shared/types/interfaces.type";
 import { jwtDecode } from "jwt-decode";
 import { createContext, useContext, useEffect, useMemo } from "react";
 import { useNavigate } from "react-router-dom";
diff --git a/react-ui/src/shared/provider/menu/menu.provider.tsx b/react-ui/src/shared/provider/menu/menu.provider.tsx
index a91f46639..b2525692b 100644
--- a/react-ui/src/shared/provider/menu/menu.provider.tsx
+++ b/react-ui/src/shared/provider/menu/menu.provider.tsx
@@ -1,7 +1,7 @@
 import { faRightFromBracket, IconDefinition } from "@fortawesome/free-solid-svg-icons";
 import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import { BasicProp } from "@helper/interfaces";
 import { useAuth } from "@provider/auth.provider";
+import { BasicProp } from "@shared/types/interfaces.type";
 import React, { createContext, useContext, useEffect, useMemo, useState } from "react";
 import { useTranslation } from "react-i18next";
 import './menu.provider.scss';
diff --git a/react-ui/src/shared/provider/utils.provider.tsx b/react-ui/src/shared/provider/utils.provider.tsx
index ca6aa1d32..3a76bbe69 100644
--- a/react-ui/src/shared/provider/utils.provider.tsx
+++ b/react-ui/src/shared/provider/utils.provider.tsx
@@ -1,4 +1,4 @@
-import { BasicProp } from "@helper/interfaces";
+import { BasicProp } from "@shared/types/interfaces.type";
 import React, { createContext, useContext, useMemo } from "react";
 import { useTranslation } from "react-i18next";
 import { toast } from "react-toastify";
diff --git a/react-ui/src/shared/reducer/routine.reducer.ts b/react-ui/src/shared/reducer/routine.reducer.ts
index fc5a4b35b..873d7d6a2 100755
--- a/react-ui/src/shared/reducer/routine.reducer.ts
+++ b/react-ui/src/shared/reducer/routine.reducer.ts
@@ -1,72 +1,31 @@
-import { debugMessage } from '@helper/debug'
 import { PayloadAction, createSlice } from '@reduxjs/toolkit'
+import { CategoryType } from '@shared/types/category.type'
+import { ThunkEntityDTO } from '@shared/types/thunk.type'
 import { RoutineManager } from '@utils/routine.manager'
 import { RootState } from '../../stores'
 import { startListening } from '../../stores/middleware/listener.middleware'
 import { setToken } from './user.reducer'
 
-// ---------------- thunk types ---------------- 
-
-interface ThunkEntityDTO {
-    thunk: any
-    payload: Object
-
-    /**
-     * Only one subscription per category is allowed.
-     * New subscription will unsubscribe and overwrite the old one
-     */
-    category: CATEGORIES
-}
-
-/**
- *  This Wrapper holds the actual thunk information 
- *  as well as additional information 
- */
-interface ThunkWrapper extends ThunkEntityDTO {
-    id?: number
-    locked: boolean
-}
-
-export enum CATEGORIES {
-    TABLE,
-    TAB,
-}
-
-// ---------------- reducer types ---------------- 
-
 export interface ReducerState {
-    thunks: { [key in keyof typeof CATEGORIES]: ThunkWrapper | null }
+    thunks: Record<CategoryType, ThunkEntityDTO | null>
 }
 
 const initialState: ReducerState = {
     thunks: {
+        DEVICE: null,
         TABLE: null,
-        TAB: null,
+        TAB: null
     },
 }
 
+
 const RoutineSlice = createSlice({
     name: 'routine',
     initialState,
     reducers: {
         addRoutine: (state: any, { payload }: PayloadAction<ThunkEntityDTO>) => {
-            if (state.thunks[CATEGORIES[payload.category]]?.locked) {
-
-            }
-
-            const newThunk: ThunkWrapper = { ...payload, locked: true }
-            state.thunks[CATEGORIES[payload.category]] = newThunk
-        },
-
-        setThunkId: (state, { payload }: PayloadAction<{ id: number; category: CATEGORIES }>) => {
-            const thunk = state.thunks[CATEGORIES[payload.category] as any]
-
-            if (!thunk) {
-                debugMessage("Desired thunk of category " + payload.category + " is not available")
-                return
-            }
-
-            state.thunks[CATEGORIES[payload.category] as any] = { ...thunk, id: payload.id, locked: false }
+            const newThunk: ThunkEntityDTO = payload
+            state.thunks[payload.category] = newThunk
         },
 
         removeAll: (state) => {
@@ -113,12 +72,9 @@ startListening({
 startListening({
     predicate: (action) => addRoutine.match(action),
     effect: async (action, listenerApi) => {
-        const { thunk } = action.payload as ThunkWrapper
+        const { thunk } = action.payload as ThunkEntityDTO
         const subscription = await listenerApi.dispatch(thunk(action.payload.payload))
-        const thunkId = await RoutineManager.add(subscription.payload)
-        listenerApi.dispatch(
-            RoutineSlice.actions.setThunkId({ id: thunkId, category: action.payload.category })
-        )
+        RoutineManager.add(subscription.payload, action.payload.category)
     },
 })
 
@@ -127,14 +83,11 @@ startListening({
     predicate: (action) => addRoutine.match(action),
     effect: async (action, listenerApi) => {
         const { routine } = listenerApi.getOriginalState() as RootState
-        const lastThunk = routine.thunks[CATEGORIES[action.payload.category] as any]
-        if (lastThunk) {
-            if (!lastThunk.id) {
-                throw new Error()
-                // TODO
-            }
+        const category = action.payload.category;
 
-            RoutineManager.unsubscribe(lastThunk.id)
+        const lastThunk = routine.thunks[category as CategoryType]
+        if (lastThunk) {
+            RoutineManager.unsubscribe(category)
         }
     },
 })
diff --git a/react-ui/src/shared/types/category.type.ts b/react-ui/src/shared/types/category.type.ts
new file mode 100644
index 000000000..e08282341
--- /dev/null
+++ b/react-ui/src/shared/types/category.type.ts
@@ -0,0 +1,17 @@
+
+const DeviceListView = {
+    TABLE: "device_list/table",
+    TAB: "device_list/tab",
+}
+
+const Shared = {
+    DEVICE: 'objects/device'
+}
+
+
+export const Category = {
+    ...DeviceListView,
+    ...Shared
+}
+
+export type CategoryType = keyof typeof Category
\ No newline at end of file
diff --git a/react-ui/src/shared/helper/interfaces.ts b/react-ui/src/shared/types/interfaces.type.ts
similarity index 100%
rename from react-ui/src/shared/helper/interfaces.ts
rename to react-ui/src/shared/types/interfaces.type.ts
diff --git a/react-ui/src/shared/types/thunk.type.ts b/react-ui/src/shared/types/thunk.type.ts
new file mode 100644
index 000000000..ef0a92b87
--- /dev/null
+++ b/react-ui/src/shared/types/thunk.type.ts
@@ -0,0 +1,12 @@
+import { CategoryType } from "./category.type"
+
+export interface ThunkEntityDTO {
+    thunk: any
+    payload: Object
+
+    /**
+     * Only one subscription per category is allowed.
+     * New subscription will unsubscribe and overwrite the old one
+     */
+    category: CategoryType
+}
\ No newline at end of file
diff --git a/react-ui/src/shared/utils/routine.manager.ts b/react-ui/src/shared/utils/routine.manager.ts
index 2f1a8086a..427043e24 100755
--- a/react-ui/src/shared/utils/routine.manager.ts
+++ b/react-ui/src/shared/utils/routine.manager.ts
@@ -1,67 +1,73 @@
+import { infoMessage, warnMessage } from '@helper/debug'
 import { QueryActionCreatorResult } from '@reduxjs/toolkit/query'
+import { Category, CategoryType } from '@shared/types/category.type'
 
 type Routine = QueryActionCreatorResult<any>
 
 interface Entity {
     routine: Routine
-    id: number
 }
 
-const initialState = {
-    routines: [] as Entity[],
+
+interface RoutineState {
+    routines: Record<CategoryType, Entity | null>
+}
+
+const initalState: RoutineState = {
+    routines: {
+        DEVICE: null,
+        TABLE: null,
+        TAB: null
+    }
 }
 
 /**
- * Routine manager is a singleton that holds all running routines.
- * The redux store holds any persistable information about the routines.
- * The routine objects itself are stored in the RoutineManager.
- */
+* Routine manager is a singleton that holds all running routines.
+* The redux store holds any persistable information about the routines.
+* The routine objects itself are stored in the RoutineManager.
+*/
 export const RoutineManager = (() => {
-    const state = initialState
-    const add = (routine: Routine): number => {
-        const id = state.routines.length
+    let state = initalState
 
-        const newEntity: Entity = {
+    const add = (routine: Routine, category: CategoryType): boolean => {
+        const entity: Entity = {
             routine: routine,
-            id,
         }
 
-        state.routines = [...state.routines, newEntity]
-
-        return id
+        state.routines = {
+            ...state.routines,
+            [category]: entity
+        }
+        return true
     }
 
     const unsubscribeAll = () => {
-        state.routines.forEach(({ routine: subscription }) => {
-            _unsubscribe(subscription)
-        })
+        Object.keys(state.routines)
+            .forEach((category) => {
+                unsubscribe(category as CategoryType)
+            })
 
-        state.routines = initialState.routines
+        state = initalState
     }
 
     /**
      * @param id
      * @returns returns true if the routine was stopped, false if it was not found
      */
-    const unsubscribe = (id: number): boolean => {
-        const routine = state.routines.find(({ id: routineId }) => routineId === id)
+    const unsubscribe = (category: CategoryType): boolean => {
+        const entity = state.routines[category]
 
-        if (routine) {
-            _unsubscribe(routine.routine)
+        if (entity) {
+            entity.routine.unsubscribe()
+            state.routines[category] = null
+            infoMessage("Routine unsubscribed from category " + category)
         }
 
-        return !!routine
-    }
+        if (!!entity) {
+            warnMessage("Desired routine to unsubscribe does not exist in category " + Category[category])
+        }
 
-    /**
-     * Actual unsubscribe process.
-     * This process is extracted to have a single process of unsubscribing.
-     *
-     * @param subscription
-     */
-    const _unsubscribe = (subscription: Routine) => {
-        subscription.unsubscribe()
-        // TODO remove from state
+        return !!entity
     }
 
     return {
@@ -69,4 +75,4 @@ export const RoutineManager = (() => {
         unsubscribe,
         unsubscribeAll,
     }
-})()
+})()
\ No newline at end of file
-- 
GitLab