diff --git a/react-ui/src/components/devices/api/pnd.fetch.ts b/react-ui/src/components/devices/api/pnd.fetch.ts index 6e677ab8d7456df132a54cf04f3131b6c5c3d636..fd49de636e53fccf73bddc19741d71ab0472c71d 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 f574509be8821fe987dbe28344f27fee1c49fb49..9d91700c4dcd32c886001628cc794e820b0f9cc1 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 10caffa2faa1eb0bdcba5f616c23f36c1721cb30..f9b3e1ee243279a111b67e0b133aeb261cfe0a03 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 7de1d18149c35c2bfbe47582d479a5736fa7f1b1..a3ea43d2feb14b5267349bc09241af0df1c1dd01 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 312caab60d858b0931e50154c2c6e796e0405e47..9b731fef5ae0ff067e14455f15bedbbd066aa82f 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 2929f9b64855359fa19ea9c0e17d90f5af79bb61..a2768a0ea0d76d96e507d334234d9b2db126f996 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 4a60567b68e78e571577d7f74fb0729df47008cb..af4cc3abb9f0b2e207acd5c71fbb48f4a0562ca7 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 03d7406f42bc37a78f9e6047f4aa70758e8b8258..38afc83a127189d2583a76f99bf89a4de7a97b8e 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 720f75c7d26b4ffd181707c46ff42502168a72c0..b8358c686994263e5c731504806da2c139b3f648 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 6628989b8f08fc4c4cdce9f74b0291c0dfbb7726..db10a0979ac9b5acfab21af71fc4c1dc2595fbe3 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 69bdccbdfbb3b6db3295d20a137e56a57d8504f2..77219bdce1547fc11b889eb444ae6d6a9eb5eb6a 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 a91f46639af28e2db261a3089f62e0a79fc13c41..b2525692b9f463312158d6443a17320e2bed19e7 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 ca6aa1d3235efe3e1f48441fc7a39a47e9ce7fb7..3a76bbe69430ca8f5031935f8e1d11925c620610 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 fc5a4b35b39031c02d8ea1bebef7b8e81cd718d0..873d7d6a21500aed1bdd9c8abbd3eeb2992bed5d 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 0000000000000000000000000000000000000000..e08282341e803cf8b62bf0deec423fb0ebe62234 --- /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 0000000000000000000000000000000000000000..ef0a92b8774f79fab8fd3a33937d50fa07954ee1 --- /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 2f1a8086ad5255fd63b2a13b149b6e3d5c708b31..427043e24671fa32935ccf4d4728cc4859132752 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