From d4e6b5c81e2e75b6c7385b7682d12894c52317eb Mon Sep 17 00:00:00 2001 From: Matthias Feyll <matthias.feyll@stud.h-da.de> Date: Tue, 27 Aug 2024 10:43:23 +0200 Subject: [PATCH] (fix) fix race conidtion on login | refetching persist in store now --- react-ui/scripts/modify-api.sh | 32 -------- react-ui/scripts/template.js | 18 ----- .../landingpage.scss => device/device.scss} | 0 .../components/view/device/device.view.tsx | 50 +++++++++++++ .../view/landingpage/landingpage.tsx | 11 --- .../src/components/view/login/login.view.tsx | 4 +- .../src/components/view/splash/splash.view.ts | 0 .../components/view_model/device.viewmodel.ts | 48 ++++++++++++ .../components/view_model/login.viewmodel.ts | 1 - react-ui/src/index.tsx | 29 +------- react-ui/src/routes.tsx | 16 ++++ react-ui/src/stores/api.store.ts | 8 +- react-ui/src/stores/persist.store.ts | 2 + react-ui/src/stores/reducer/device.reducer.ts | 74 +++++++++++++++++++ .../src/stores/reducer/schedule.reducer.ts | 50 +++++++------ react-ui/src/stores/reducer/user.reducer.ts | 57 +++++++------- react-ui/src/utils/helper/coookie.ts | 14 ++++ .../{auth.layout.tsx => basic.layout.tsx} | 9 +-- react-ui/src/utils/layouts/login.layout.tsx | 4 +- .../protected.layout/protected.layout.scss | 8 +- .../protected.layout/protected.layout.tsx | 14 ++-- react-ui/src/utils/provider/auth.provider.tsx | 16 ++-- .../src/utils/provider/fetch.provider.tsx | 21 ------ react-ui/src/utils/scheduler.tsx | 20 ----- react-ui/tsconfig.json | 4 +- react-ui/tsconfig.node.json | 2 +- react-ui/vite.config.mjs | 2 + 27 files changed, 302 insertions(+), 212 deletions(-) delete mode 100755 react-ui/scripts/modify-api.sh delete mode 100644 react-ui/scripts/template.js rename react-ui/src/components/view/{landingpage/landingpage.scss => device/device.scss} (100%) create mode 100644 react-ui/src/components/view/device/device.view.tsx delete mode 100644 react-ui/src/components/view/landingpage/landingpage.tsx delete mode 100644 react-ui/src/components/view/splash/splash.view.ts create mode 100644 react-ui/src/components/view_model/device.viewmodel.ts create mode 100644 react-ui/src/routes.tsx create mode 100644 react-ui/src/stores/reducer/device.reducer.ts create mode 100644 react-ui/src/utils/helper/coookie.ts rename react-ui/src/utils/layouts/{auth.layout.tsx => basic.layout.tsx} (52%) delete mode 100644 react-ui/src/utils/provider/fetch.provider.tsx delete mode 100644 react-ui/src/utils/scheduler.tsx diff --git a/react-ui/scripts/modify-api.sh b/react-ui/scripts/modify-api.sh deleted file mode 100755 index d5b9fdf5b..000000000 --- a/react-ui/scripts/modify-api.sh +++ /dev/null @@ -1,32 +0,0 @@ -#!/usr/bin/env sh - -# Verzeichnis mit den generierten Dateien -GENERATED_DIR="../src/utils/api/" - -# Template-Datei -TEMPLATE_FILE="./template.js" - -# Stelle sicher, dass das Template existiert -if [[ ! -f "$TEMPLATE_FILE" ]]; then - echo "Template file not found: $TEMPLATE_FILE" - exit 1 -fi - -# Füge den Inhalt des Templates in jede Query-Funktion ein -for file in "$GENERATED_DIR"/*.ts; do - if grep -q 'builder.query' "$file"; then - echo "Processing $file..." - - # Überprüfen, ob onCacheEntryAdded bereits vorhanden ist - if ! grep -q 'onCacheEntryAdded' "$file"; then - # Füge das Template in die Query-Funktion ein - sed -i.bak '/builder.query.*{/r '"$TEMPLATE_FILE" "$file" - - echo "Extended $file with onCacheEntryAdded." - else - echo "$file already contains onCacheEntryAdded, skipping." - fi - fi -done - -echo "All applicable files processed." \ No newline at end of file diff --git a/react-ui/scripts/template.js b/react-ui/scripts/template.js deleted file mode 100644 index 2fa7b2993..000000000 --- a/react-ui/scripts/template.js +++ /dev/null @@ -1,18 +0,0 @@ -onCacheEntryAdded: async (arg, { updateCachedData, cacheDataLoaded, cacheEntryRemoved }) => { - try { - // Warte, bis der Cache geladen ist - await cacheDataLoaded; - - // Beobachte kontinuierlich Änderungen am Cache - const unsubscribe = updateCachedData((draft) => { - console.log('Updated data:', draft); - // Hier kannst du auf die Daten zugreifen und z.B. weitere Aktionen auslösen - }); - - // Aufräumen, wenn der Cache entfernt wird - await cacheEntryRemoved; - unsubscribe(); - } catch (err) { - console.error('Error in onCacheEntryAdded:', err); - } - }, \ No newline at end of file diff --git a/react-ui/src/components/view/landingpage/landingpage.scss b/react-ui/src/components/view/device/device.scss similarity index 100% rename from react-ui/src/components/view/landingpage/landingpage.scss rename to react-ui/src/components/view/device/device.scss diff --git a/react-ui/src/components/view/device/device.view.tsx b/react-ui/src/components/view/device/device.view.tsx new file mode 100644 index 000000000..8a664c4d8 --- /dev/null +++ b/react-ui/src/components/view/device/device.view.tsx @@ -0,0 +1,50 @@ +import { useAppSelector } from '@hooks'; +import { Col, Container, Row, Table } from 'react-bootstrap'; +import './device.scss'; +import { useDeviceViewModel } from '@viewmodel/device.viewmodel'; + +function DeviceView() { + const { devices } = useAppSelector(state => state.device); + useDeviceViewModel(); + + + const getDeviceTable = () => { + return devices.map((device, index) => ( + <tr key={index}> + <td>{device.name}</td> + <td>{device.id}</td> + <td>{device.pid}</td> + </tr> + )) + } + + return ( + <div className='m-4 pt-4'> + <Container className="bg-white rounded c-box"> + <Row > + <Col><h3>Device list</h3></Col> + </Row> + + <Row className='mt-2'> + <Col> + <Table striped bordered hover className='table-primary'> + <thead> + <tr> + <th>Name</th> + <th>UUID</th> + <th>User</th> + <th>Last updated</th> + </tr> + </thead> + <tbody> + {getDeviceTable()} + </tbody> + </Table> + </Col> + </Row> + </Container> + </div> + ) +} + +export default DeviceView diff --git a/react-ui/src/components/view/landingpage/landingpage.tsx b/react-ui/src/components/view/landingpage/landingpage.tsx deleted file mode 100644 index 62c358639..000000000 --- a/react-ui/src/components/view/landingpage/landingpage.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import './landingpage.scss' - -function Landingpage() { - return ( - <div className="App"> - - </div> - ) -} - -export default Landingpage diff --git a/react-ui/src/components/view/login/login.view.tsx b/react-ui/src/components/view/login/login.view.tsx index ba8da826e..405a42f84 100644 --- a/react-ui/src/components/view/login/login.view.tsx +++ b/react-ui/src/components/view/login/login.view.tsx @@ -6,7 +6,7 @@ import logo from '@assets/logo.svg' import useLoginViewModel from '@viewmodel/login.viewmodel' import React, { useRef } from 'react' -const LoginPage = ({ children }) => { +const LoginView = ({ children }) => { const { t } = useTranslation('common') const { login, handleErrorMessageRendering, displayFormFieldChecks, loginLoading } = useLoginViewModel(); @@ -82,4 +82,4 @@ const LoginPage = ({ children }) => { ) } -export default LoginPage +export default LoginView diff --git a/react-ui/src/components/view/splash/splash.view.ts b/react-ui/src/components/view/splash/splash.view.ts deleted file mode 100644 index e69de29bb..000000000 diff --git a/react-ui/src/components/view_model/device.viewmodel.ts b/react-ui/src/components/view_model/device.viewmodel.ts new file mode 100644 index 000000000..7ceb78809 --- /dev/null +++ b/react-ui/src/components/view_model/device.viewmodel.ts @@ -0,0 +1,48 @@ +import { api, NetworkelementFlattenedManagedNetworkElement, NetworkelementGetAllFlattenedResponse, NetworkElementServiceGetAllFlattenedApiArg } from "@api/api"; +import { useAppSelector } from "@hooks"; +import { setDevices } from "@reducer/device.reducer"; +import { QueryActionCreatorResult } from "@reduxjs/toolkit/query"; +import { useEffect } from "react"; +import { useDispatch } from "react-redux"; +import { AppDispatch } from "src/stores"; + +const FETCH_DEVICES_INTERVAL = 15000; // in ms + + +export const useDeviceViewModel = () => { + const { user } = useAppSelector(state => state.user); + const [triggerFetchDevices] = api.endpoints.networkElementServiceGetAllFlattened.useLazyQuerySubscription({ + pollingInterval: FETCH_DEVICES_INTERVAL, + skipPollingIfUnfocused: true + }); + const dispatch = useDispatch<AppDispatch>(); + + + // TODO figure out how we get the proper response type here + let fetchDevicesSubscription: QueryActionCreatorResult<any> | undefined; + + + useEffect(() => { + fetchDevices(); + + return () => { + fetchDevicesSubscription?.unsubscribe(); + } + }, []) + + const fetchDevices = () => { + const payload: NetworkElementServiceGetAllFlattenedApiArg = { + pid: Object.keys(user?.roles)[0], + timestamp: new Date().getTime().toString(), + } + + fetchDevicesSubscription = triggerFetchDevices(payload); + fetchDevicesSubscription.then((response) => { + const { mne } = response.data as NetworkelementGetAllFlattenedResponse; + dispatch(setDevices(mne)); + }); + } + + return { + } +} \ No newline at end of file diff --git a/react-ui/src/components/view_model/login.viewmodel.ts b/react-ui/src/components/view_model/login.viewmodel.ts index 93f4c33f2..370834a0c 100644 --- a/react-ui/src/components/view_model/login.viewmodel.ts +++ b/react-ui/src/components/view_model/login.viewmodel.ts @@ -9,7 +9,6 @@ export interface PageLoginState { export default function useLoginViewModel() { const {login, loginProperties} = useAuth(); const {isLoading: loginLoading, error: loginError, reset: resetLogin} = loginProperties!; - const [localFormState, updateLocalFormState] = useState({ submitted: false, diff --git a/react-ui/src/index.tsx b/react-ui/src/index.tsx index 50f9c3509..3eb8a604f 100644 --- a/react-ui/src/index.tsx +++ b/react-ui/src/index.tsx @@ -1,48 +1,27 @@ import React from 'react' import ReactDOM, { Container } from 'react-dom/client' import { - Route, - RouterProvider, - createBrowserRouter, - createRoutesFromElements + RouterProvider } from 'react-router-dom' -import Landingpage from './components/view/landingpage/landingpage' import './index.scss' -import { BasicLayout } from '@layout/auth.layout' -import { LoginLayout } from '@layout/login.layout' -import { ProtectedLayout } from '@layout/protected.layout/protected.layout' import i18next from 'i18next' import { I18nextProvider } from 'react-i18next' import { Provider } from 'react-redux' import { ToastContainer } from 'react-toastify' import { PersistGate } from 'redux-persist/integration/react' import './i18n/config' +import { router } from './routes' import { persistor, store } from './stores' import './utils/icons/icons' - -const root = ReactDOM.createRoot(document.getElementById('root') as Container) - -// create a proper routing -const router = createBrowserRouter( - createRoutesFromElements( - <Route element={<BasicLayout />}> - <Route path="/login" element={<LoginLayout />} /> - <Route element={<ProtectedLayout />}> - <Route path="/" element={<Landingpage />} /> - </Route> - </Route> - ) -) - const installToastify = () => { return ( <ToastContainer /> ) } -root.render( +ReactDOM.createRoot(document.getElementById("root")).render( <React.StrictMode> <Provider store={store}> <PersistGate loading={null} persistor={persistor}> @@ -53,4 +32,4 @@ root.render( </PersistGate> </Provider> </React.StrictMode> -) +); \ No newline at end of file diff --git a/react-ui/src/routes.tsx b/react-ui/src/routes.tsx new file mode 100644 index 000000000..28a06cbfb --- /dev/null +++ b/react-ui/src/routes.tsx @@ -0,0 +1,16 @@ +import { BasicLayout } from "@layout/basic.layout" +import { LoginLayout } from "@layout/login.layout" +import { ProtectedLayout } from "@layout/protected.layout/protected.layout" +import DeviceView from "@view/device/device.view" +import { createBrowserRouter, createRoutesFromElements, Route } from "react-router-dom" + +export const router = createBrowserRouter( + createRoutesFromElements( + <Route element={<BasicLayout />}> + <Route path="/login" element={<LoginLayout />} /> + <Route element={<ProtectedLayout />}> + <Route path="/" element={<DeviceView />} /> + </Route> + </Route> + ) +) \ No newline at end of file diff --git a/react-ui/src/stores/api.store.ts b/react-ui/src/stores/api.store.ts index bd36b8227..fd894e97b 100644 --- a/react-ui/src/stores/api.store.ts +++ b/react-ui/src/stores/api.store.ts @@ -1,11 +1,11 @@ -import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react' -import { RootState } from '.' +import { getCookieValue } from '@helper/coookie'; +import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'; // initialize an empty api service that we'll inject endpoints into later as needed export const emptySplitApi = createApi({ baseQuery: fetchBaseQuery({ - baseUrl: '/api', prepareHeaders: (headers, { getState }) => { - const token = (getState() as RootState).user.token + baseUrl: '/api', prepareHeaders: (headers) => { + const token = getCookieValue('token'); if (token) { headers.set('authorize', `${token}`) diff --git a/react-ui/src/stores/persist.store.ts b/react-ui/src/stores/persist.store.ts index 806b6e2e1..feb5241a8 100644 --- a/react-ui/src/stores/persist.store.ts +++ b/react-ui/src/stores/persist.store.ts @@ -4,6 +4,7 @@ import { persistReducer } from "redux-persist"; import storage from "redux-persist/es/storage"; import { emptySplitApi } from "./api.store"; import scheduleReducer from "@reducer/schedule.reducer"; +import deviceReducer from "@reducer/device.reducer"; /** local storage config */ @@ -16,6 +17,7 @@ const rootPersistConfig = { const rootReducer = combineReducers({ user: userReducer, + device: deviceReducer, scheduler: scheduleReducer, [emptySplitApi.reducerPath]: emptySplitApi.reducer, }) diff --git a/react-ui/src/stores/reducer/device.reducer.ts b/react-ui/src/stores/reducer/device.reducer.ts new file mode 100644 index 000000000..ec47ff1f8 --- /dev/null +++ b/react-ui/src/stores/reducer/device.reducer.ts @@ -0,0 +1,74 @@ +import { api, NetworkelementFlattenedManagedNetworkElement, NetworkElementServiceGetAllFlattenedApiArg } from '@api/api'; +import { createSlice, PayloadAction } from '@reduxjs/toolkit'; +import { QueryActionCreatorResult } from '@reduxjs/toolkit/query'; +import { RootState } from '..'; +import { startListening } from '../middleware/listener.middleware'; +import { setUser } from './user.reducer'; + +type Device = NetworkelementFlattenedManagedNetworkElement; + + +export interface DeviceSliceState { + devices: Device[], +} + +const initialState: DeviceSliceState = { + devices: [], +} + + + + +const deviceSlice = createSlice({ + name: 'device', + initialState, + reducers: { + setDevices: (state, action: PayloadAction<Device[]>) => { state.devices = action.payload }, + }, +}) + +export const { setDevices } = deviceSlice.actions + +export default deviceSlice.reducer +export const deviceReducerPath = deviceSlice.reducerPath; + + + +let fetchSubscription: QueryActionCreatorResult<any>[] = []; +export const abortFetching = () => { + fetchSubscription.forEach((subscription) => { + subscription.unsubscribe(); + }); + fetchSubscription = []; +} + +// continously fetch devices +const FETCH_DEVICES_INTERVAL = 5000; // in ms +startListening({ + actionCreator: setUser, + effect: async (_, listenerApi) => { + const { user } = listenerApi.getState() as RootState; + + const payload: NetworkElementServiceGetAllFlattenedApiArg = { + pid: Object.keys(user?.user.roles)[0], + timestamp: new Date().getTime().toString(), + } + + const subscription = listenerApi.dispatch(api.endpoints.networkElementServiceGetAllFlattened.initiate(payload, { + subscriptionOptions: { + pollingInterval: FETCH_DEVICES_INTERVAL, + skipPollingIfUnfocused: true, + } + })); + + fetchSubscription = [...fetchSubscription, subscription]; + }, +}) + +// save fetched devices +startListening({ + predicate: (action) => api.endpoints.networkElementServiceGetAllFlattened.matchFulfilled(action), + effect: async (action, listenerApi) => { + listenerApi.dispatch(setDevices(action.payload.mne)); + }, +}) \ No newline at end of file diff --git a/react-ui/src/stores/reducer/schedule.reducer.ts b/react-ui/src/stores/reducer/schedule.reducer.ts index 345b26bf2..2e2bbe36b 100644 --- a/react-ui/src/stores/reducer/schedule.reducer.ts +++ b/react-ui/src/stores/reducer/schedule.reducer.ts @@ -3,18 +3,21 @@ import { startListening } from '../middleware/listener.middleware'; export enum ScheduleState { INIT, RUNNING, STOPPED } -type Task<T> = (options: T) => void; - -interface Schedule<T> { - f: Task<T>, +export type Task = { + job: (options: object) => void, interval: number, + type: string +}; + +interface Schedule { + task: Task, id: number, state: ScheduleState, intervalId: NodeJS.Timeout | undefined } export interface ScheduleReducerState { - schedules: Schedule<any>[] + schedules: Schedule[] } const initialState: ScheduleReducerState = { @@ -25,10 +28,9 @@ const ScheduleSlice = createSlice({ name: 'schedule', initialState, reducers: { - addSchedule: (state, action: PayloadAction<{ task: Task<any>, interval: number }>) => { + registerTask: (state, action: PayloadAction<Task>) => { const newSchedule = { - f: action.payload.task, - interval: action.payload.interval, + task: action.payload.task, id: state.schedules.length, state: ScheduleState.INIT, intervalId: undefined @@ -36,9 +38,9 @@ const ScheduleSlice = createSlice({ state.schedules = [...state.schedules, newSchedule] }, - startSchedule: (state, action: PayloadAction<Schedule<any>>) => { + startSchedule: (state, action: PayloadAction<Schedule>) => { const schedule = action.payload; - schedule.intervalId = setInterval(schedule.f, schedule.interval); + schedule.intervalId = setInterval(schedule.task.job, schedule.task.interval); schedule.state = ScheduleState.RUNNING; state.schedules[schedule.id] = schedule; @@ -46,25 +48,25 @@ const ScheduleSlice = createSlice({ }, }) -export const { addSchedule } = ScheduleSlice.actions +export const { registerTask } = ScheduleSlice.actions export const { startSchedule } = ScheduleSlice.actions export default ScheduleSlice.reducer -startListening({ - actionCreator: addSchedule, +// startListening({ +// actionCreator: addSchedule, - effect: (action, listenerApi) => { - const newState = listenerApi.getState() as ScheduleReducerState; - const originalState = listenerApi.getOriginalState() as ScheduleReducerState; +// effect: (action, listenerApi) => { +// const newState = listenerApi.getState() as ScheduleReducerState; +// const originalState = listenerApi.getOriginalState() as ScheduleReducerState; - // get the added schedule - const schedule = newState.schedules.filter(s => !originalState.schedules.includes(s)).at(0); - if (!schedule) { - throw new Error("Added schedule not found in store"); - } +// // get the added schedule +// const schedule = newState.schedules.filter(s => !originalState.schedules.includes(s)).at(0); +// if (!schedule) { +// throw new Error("Added schedule not found in store"); +// } - listenerApi.dispatch(startSchedule(schedule)) - }, -}) \ No newline at end of file +// listenerApi.dispatch(startSchedule(schedule)) +// }, +// }) \ No newline at end of file diff --git a/react-ui/src/stores/reducer/user.reducer.ts b/react-ui/src/stores/reducer/user.reducer.ts index b1d5f99cd..797626db0 100644 --- a/react-ui/src/stores/reducer/user.reducer.ts +++ b/react-ui/src/stores/reducer/user.reducer.ts @@ -1,10 +1,10 @@ import { api, RbacUser, UserServiceGetUsersApiArg } from '@api/api'; -import { createSlice, PayloadAction } from '@reduxjs/toolkit'; -import { startListening } from '../middleware/listener.middleware'; +import { createAsyncThunk, createSlice, PayloadAction } from '@reduxjs/toolkit'; import { RootState } from '..'; +import { startListening } from '../middleware/listener.middleware'; +import { getCookieValue, setCookieValue } from '@helper/coookie'; export interface UserSliceState { - token: string, // defined by the frontend user input. This value is getting compared with the backend response username: string, user: RbacUser | null, @@ -12,7 +12,6 @@ export interface UserSliceState { const initialState: UserSliceState = { - token: '', username: '', user: null, } @@ -22,8 +21,10 @@ const userSlice = createSlice({ initialState, reducers: { setToken: (state, action: PayloadAction<{ token: string, username: string }>) => { - state.token = action.payload.token; - state.username = action.payload.username + const token = action.payload?.token || ''; + setCookieValue('token', token); + + state.username = action.payload?.username || '' }, setUser: (state, action: PayloadAction<RbacUser>) => { state.user = action.payload }, }, @@ -36,33 +37,27 @@ export default userSlice.reducer export const userReducerPath = userSlice.reducerPath; +export const fetchUser = createAsyncThunk( + 'user/fetchUser', + (_, thunkAPI) => { + const payload: UserServiceGetUsersApiArg = {}; -startListening({ - predicate: (action, { user }: any) => { - return setToken.match(action) && !!user.token; - }, - - effect: async (_, listenerApi) => { - const payload: UserServiceGetUsersApiArg = {}; + thunkAPI.dispatch(api.endpoints.userServiceGetUsers.initiate(payload)).then((response) => { + if (response.error || !response.data?.user?.length) { + // TODO proper error handling + throw new Error('Fetching the pnd list after successful login failed'); + } - listenerApi.dispatch(api.endpoints.userServiceGetUsers.initiate(payload)).then((response) => { - if (response.error || !response.data?.user?.length) { - // TODO proper error handling - throw new Error('Fetching the pnd list after successful login failed'); - } + const { user } = thunkAPI.getState() as RootState; - const {user} = listenerApi.getState() as RootState; + // TODO ask if this is the correct approach + const matchedUser = response.data.user.find((_user) => _user.name === user.username); - // TODO ask if this is the correct approach - const matchedUser = response.data.user.find((_user) => _user.name === user.username); + if (!matchedUser) { + // TODO proper error handling + throw new Error('No user found with the provided username'); + } - if (!matchedUser) { - // TODO proper error handling - throw new Error('No user found with the provided username'); - } - - - listenerApi.dispatch(setUser(matchedUser)); - }); - }, -}) \ No newline at end of file + thunkAPI.dispatch(setUser(matchedUser)); + }); +}); \ No newline at end of file diff --git a/react-ui/src/utils/helper/coookie.ts b/react-ui/src/utils/helper/coookie.ts new file mode 100644 index 000000000..75cba2dc8 --- /dev/null +++ b/react-ui/src/utils/helper/coookie.ts @@ -0,0 +1,14 @@ +export const getCookieValue = (name: string): string => { + const regex = new RegExp(`(^| )${name}=([^;]+)`) + const match = document.cookie.match(regex) + + if (match) { + return match[2]; + } + + return ''; +} + +export const setCookieValue = (key: string, value: string): void => { + document.cookie = `${key}=${value}; Secure; SameSite=Lax`; +} \ No newline at end of file diff --git a/react-ui/src/utils/layouts/auth.layout.tsx b/react-ui/src/utils/layouts/basic.layout.tsx similarity index 52% rename from react-ui/src/utils/layouts/auth.layout.tsx rename to react-ui/src/utils/layouts/basic.layout.tsx index a38da03a9..7b7f2b11c 100644 --- a/react-ui/src/utils/layouts/auth.layout.tsx +++ b/react-ui/src/utils/layouts/basic.layout.tsx @@ -1,19 +1,14 @@ import { AuthProvider } from "@provider/auth.provider"; -import { useEffect } from "react"; import { useOutlet } from "react-router-dom"; -import { ScheduleProvider } from "../scheduler"; export const BasicLayout = () => { const outlet = useOutlet(); - const { initSchedules } = ScheduleProvider(); - useEffect(() => { - initSchedules(); - }, []) + return ( <AuthProvider> - {outlet} + {outlet} </AuthProvider> ) } \ No newline at end of file diff --git a/react-ui/src/utils/layouts/login.layout.tsx b/react-ui/src/utils/layouts/login.layout.tsx index 08a875f75..43e40aa30 100644 --- a/react-ui/src/utils/layouts/login.layout.tsx +++ b/react-ui/src/utils/layouts/login.layout.tsx @@ -1,5 +1,5 @@ import { useAuth } from "@provider/auth.provider"; -import LoginPage from "@view/login/login.view"; +import LoginView from "@view/login/login.view"; import { useEffect } from "react"; import { useNavigate, useOutlet } from "react-router-dom"; @@ -18,6 +18,6 @@ export const LoginLayout = ({ children }) => { }, []); return ( - <LoginPage>{outlet}</LoginPage> + <LoginView>{outlet}</LoginView> ) } \ No newline at end of file diff --git a/react-ui/src/utils/layouts/protected.layout/protected.layout.scss b/react-ui/src/utils/layouts/protected.layout/protected.layout.scss index eb713fa7a..47561615d 100644 --- a/react-ui/src/utils/layouts/protected.layout/protected.layout.scss +++ b/react-ui/src/utils/layouts/protected.layout/protected.layout.scss @@ -1,5 +1,7 @@ @import "/src/style/colors.scss"; +$sidebar-width: 4.5em; + .head-links { text-decoration: none; color: map-get($theme-colors, dark); @@ -18,6 +20,10 @@ } .sidebar { - width: 4.5em; + width: $sidebar-width; height: 100vh; } + +.main-content { + margin-left: $sidebar-width; +} \ No newline at end of file diff --git a/react-ui/src/utils/layouts/protected.layout/protected.layout.tsx b/react-ui/src/utils/layouts/protected.layout/protected.layout.tsx index 3993a0383..834b2ac5f 100644 --- a/react-ui/src/utils/layouts/protected.layout/protected.layout.tsx +++ b/react-ui/src/utils/layouts/protected.layout/protected.layout.tsx @@ -7,7 +7,8 @@ import { Dropdown } from "react-bootstrap"; import { useTranslation } from "react-i18next"; import { Link, Outlet, useNavigate } from "react-router-dom"; import "./protected.layout.scss"; -import { useAppSelector } from '@hooks'; +import { useAppDispatch, useAppSelector } from '@hooks'; +import { fetchUser } from '@reducer/user.reducer'; export const ProtectedLayout = () => { @@ -15,11 +16,15 @@ export const ProtectedLayout = () => { const { user } = useAppSelector(state => state.user); const navigate = useNavigate(); const { t } = useTranslation('common') + const dispatch = useAppDispatch(); useEffect(() => { if (!isAuthenticated()) { navigate('/login') + return; } + + dispatch(fetchUser()); }, []); /** @@ -27,9 +32,6 @@ export const ProtectedLayout = () => { */ const handleActiveLink = (targetPath: string): string => { const href = window.location.href; - console.log(href); - - return href.includes(targetPath) ? ' active' : ''; } @@ -101,7 +103,9 @@ export const ProtectedLayout = () => { <div> {HorizontalNavbar()} {VerticalSidebar()} - <Outlet /> + <div className='main-content'> + <Outlet /> + </div> </div> ) }; \ No newline at end of file diff --git a/react-ui/src/utils/provider/auth.provider.tsx b/react-ui/src/utils/provider/auth.provider.tsx index d583d4116..638d9b408 100644 --- a/react-ui/src/utils/provider/auth.provider.tsx +++ b/react-ui/src/utils/provider/auth.provider.tsx @@ -1,5 +1,7 @@ import { AuthServiceLoginApiArg, AuthServiceLoginApiResponse, useAuthServiceLoginMutation } from "@api/api"; +import { getCookieValue } from "@helper/coookie"; import { useAppDispatch, useAppSelector } from "@hooks"; +import { abortFetching } from "@reducer/device.reducer"; import { setToken } from "@reducer/user.reducer"; import { jwtDecode } from "jwt-decode"; import { createContext, useContext, useEffect, useMemo } from "react"; @@ -30,17 +32,17 @@ const AuthContext = createContext<AuthProviderType>({ export const AuthProvider = ({ children }) => { const dispatch = useAppDispatch();; const navigate = useNavigate(); - const { token } = useAppSelector(state => state.user); + const { username } = useAppSelector(state => state.user); useEffect(() => { - console.log('auth provider init'); - - if (isAuthenticated()) { + const token = getCookieValue('token'); + + if (token) { navigate('/') } else { navigate('/login') } - }, []); + }, [username]); const [ sendLogin, @@ -80,6 +82,7 @@ export const AuthProvider = ({ children }) => { } const isAuthenticated = () => { + const token = getCookieValue('token'); if (!token) { return false; } @@ -96,7 +99,8 @@ export const AuthProvider = ({ children }) => { } const logout = () => { - dispatch(setToken("")); + abortFetching(); + dispatch(setToken(null)); // TODO: purge other information } diff --git a/react-ui/src/utils/provider/fetch.provider.tsx b/react-ui/src/utils/provider/fetch.provider.tsx deleted file mode 100644 index 9db4e121c..000000000 --- a/react-ui/src/utils/provider/fetch.provider.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import { useAppSelector } from "@hooks"; -import { useAuth } from "./auth.provider" - - -// seconds before token expire to trigger a refetch -const REFETCH_TOKEN_TIME = 43200; - -const FetchProvider = () => { - const { isAuthenticated: isAuthed } = useAuth(); - const { token } = useAppSelector((root) => root.user); - - - const checkFetch = (): boolean => { - const { exp } = jwtDecode(token); - const currentTime = new Date().getTime() / 1000; - - return exp - } - - return -} \ No newline at end of file diff --git a/react-ui/src/utils/scheduler.tsx b/react-ui/src/utils/scheduler.tsx deleted file mode 100644 index 51bf94de3..000000000 --- a/react-ui/src/utils/scheduler.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import { useAppSelector } from "@hooks"; -import { startSchedule } from "@reducer/schedule.reducer"; -import { useDispatch } from "react-redux"; -import { AppDispatch } from "src/stores"; - -export const ScheduleProvider = () => { - const dispatch = useDispatch<AppDispatch>(); - const { schedules } = useAppSelector(state => state.scheduler); - - - const initSchedules = () => { - schedules.forEach(schedule => { - dispatch(startSchedule(schedule)); - }) - } - - return { - initSchedules - } -} diff --git a/react-ui/tsconfig.json b/react-ui/tsconfig.json index b8c8c0625..cc5d30818 100644 --- a/react-ui/tsconfig.json +++ b/react-ui/tsconfig.json @@ -15,7 +15,7 @@ "jsx": "react-jsx", /* Linting */ - "strict": true, + "strict": false, "noUnusedLocals": true, "noUnusedParameters": true, "noFallthroughCasesInSwitch": true, @@ -30,6 +30,8 @@ "@provider/*": ["src/utils/provider/*"], "@layout/*": ["src/utils/layouts/*"], "@hooks": ["src/hooks"], + "@task/*": ["src/utils/tasks/*"], + "@helper/*": ["src/utils/helper/*"], } }, "include": [ diff --git a/react-ui/tsconfig.node.json b/react-ui/tsconfig.node.json index 9357c44f3..3a29b0309 100644 --- a/react-ui/tsconfig.node.json +++ b/react-ui/tsconfig.node.json @@ -5,7 +5,7 @@ "module": "ES2020", "moduleResolution": "bundler", "allowSyntheticDefaultImports": true, - "strict": true + "strict": false }, "include": [ "vite.config.ts" diff --git a/react-ui/vite.config.mjs b/react-ui/vite.config.mjs index c52ee47ca..dee436d2a 100644 --- a/react-ui/vite.config.mjs +++ b/react-ui/vite.config.mjs @@ -36,6 +36,8 @@ export default defineConfig({ "@provider": "/src/utils/provider", "@layout": "/src/utils/layouts", "@hooks": "/src/hooks.ts", + "@task": "/src/utils/tasks", + "@helper": "/src/utils/helper", }, }, -- GitLab