Skip to content
Snippets Groups Projects
Commit d4e6b5c8 authored by Matthias Feyll's avatar Matthias Feyll :cookie:
Browse files

(fix) fix race conidtion on login | refetching persist in store now

parent b5755528
No related branches found
No related tags found
No related merge requests found
Pipeline #222032 failed
Showing
with 277 additions and 158 deletions
#!/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
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
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
import './landingpage.scss'
function Landingpage() {
return (
<div className="App">
</div>
)
}
export default Landingpage
...@@ -6,7 +6,7 @@ import logo from '@assets/logo.svg' ...@@ -6,7 +6,7 @@ import logo from '@assets/logo.svg'
import useLoginViewModel from '@viewmodel/login.viewmodel' import useLoginViewModel from '@viewmodel/login.viewmodel'
import React, { useRef } from 'react' import React, { useRef } from 'react'
const LoginPage = ({ children }) => { const LoginView = ({ children }) => {
const { t } = useTranslation('common') const { t } = useTranslation('common')
const { login, handleErrorMessageRendering, displayFormFieldChecks, loginLoading } = useLoginViewModel(); const { login, handleErrorMessageRendering, displayFormFieldChecks, loginLoading } = useLoginViewModel();
...@@ -82,4 +82,4 @@ const LoginPage = ({ children }) => { ...@@ -82,4 +82,4 @@ const LoginPage = ({ children }) => {
) )
} }
export default LoginPage export default LoginView
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
...@@ -10,7 +10,6 @@ export default function useLoginViewModel() { ...@@ -10,7 +10,6 @@ export default function useLoginViewModel() {
const {login, loginProperties} = useAuth(); const {login, loginProperties} = useAuth();
const {isLoading: loginLoading, error: loginError, reset: resetLogin} = loginProperties!; const {isLoading: loginLoading, error: loginError, reset: resetLogin} = loginProperties!;
const [localFormState, updateLocalFormState] = useState({ const [localFormState, updateLocalFormState] = useState({
submitted: false, submitted: false,
valid: false, valid: false,
......
import React from 'react' import React from 'react'
import ReactDOM, { Container } from 'react-dom/client' import ReactDOM, { Container } from 'react-dom/client'
import { import {
Route, RouterProvider
RouterProvider,
createBrowserRouter,
createRoutesFromElements
} from 'react-router-dom' } from 'react-router-dom'
import Landingpage from './components/view/landingpage/landingpage'
import './index.scss' 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 i18next from 'i18next'
import { I18nextProvider } from 'react-i18next' import { I18nextProvider } from 'react-i18next'
import { Provider } from 'react-redux' import { Provider } from 'react-redux'
import { ToastContainer } from 'react-toastify' import { ToastContainer } from 'react-toastify'
import { PersistGate } from 'redux-persist/integration/react' import { PersistGate } from 'redux-persist/integration/react'
import './i18n/config' import './i18n/config'
import { router } from './routes'
import { persistor, store } from './stores' import { persistor, store } from './stores'
import './utils/icons/icons' 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 = () => { const installToastify = () => {
return ( return (
<ToastContainer /> <ToastContainer />
) )
} }
root.render( ReactDOM.createRoot(document.getElementById("root")).render(
<React.StrictMode> <React.StrictMode>
<Provider store={store}> <Provider store={store}>
<PersistGate loading={null} persistor={persistor}> <PersistGate loading={null} persistor={persistor}>
...@@ -53,4 +32,4 @@ root.render( ...@@ -53,4 +32,4 @@ root.render(
</PersistGate> </PersistGate>
</Provider> </Provider>
</React.StrictMode> </React.StrictMode>
) );
\ No newline at end of file
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
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react' import { getCookieValue } from '@helper/coookie';
import { RootState } from '.' import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
// initialize an empty api service that we'll inject endpoints into later as needed // initialize an empty api service that we'll inject endpoints into later as needed
export const emptySplitApi = createApi({ export const emptySplitApi = createApi({
baseQuery: fetchBaseQuery({ baseQuery: fetchBaseQuery({
baseUrl: '/api', prepareHeaders: (headers, { getState }) => { baseUrl: '/api', prepareHeaders: (headers) => {
const token = (getState() as RootState).user.token const token = getCookieValue('token');
if (token) { if (token) {
headers.set('authorize', `${token}`) headers.set('authorize', `${token}`)
......
...@@ -4,6 +4,7 @@ import { persistReducer } from "redux-persist"; ...@@ -4,6 +4,7 @@ import { persistReducer } from "redux-persist";
import storage from "redux-persist/es/storage"; import storage from "redux-persist/es/storage";
import { emptySplitApi } from "./api.store"; import { emptySplitApi } from "./api.store";
import scheduleReducer from "@reducer/schedule.reducer"; import scheduleReducer from "@reducer/schedule.reducer";
import deviceReducer from "@reducer/device.reducer";
/** local storage config */ /** local storage config */
...@@ -16,6 +17,7 @@ const rootPersistConfig = { ...@@ -16,6 +17,7 @@ const rootPersistConfig = {
const rootReducer = combineReducers({ const rootReducer = combineReducers({
user: userReducer, user: userReducer,
device: deviceReducer,
scheduler: scheduleReducer, scheduler: scheduleReducer,
[emptySplitApi.reducerPath]: emptySplitApi.reducer, [emptySplitApi.reducerPath]: emptySplitApi.reducer,
}) })
......
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
...@@ -3,18 +3,21 @@ import { startListening } from '../middleware/listener.middleware'; ...@@ -3,18 +3,21 @@ import { startListening } from '../middleware/listener.middleware';
export enum ScheduleState { INIT, RUNNING, STOPPED } export enum ScheduleState { INIT, RUNNING, STOPPED }
type Task<T> = (options: T) => void; export type Task = {
job: (options: object) => void,
interface Schedule<T> {
f: Task<T>,
interval: number, interval: number,
type: string
};
interface Schedule {
task: Task,
id: number, id: number,
state: ScheduleState, state: ScheduleState,
intervalId: NodeJS.Timeout | undefined intervalId: NodeJS.Timeout | undefined
} }
export interface ScheduleReducerState { export interface ScheduleReducerState {
schedules: Schedule<any>[] schedules: Schedule[]
} }
const initialState: ScheduleReducerState = { const initialState: ScheduleReducerState = {
...@@ -25,10 +28,9 @@ const ScheduleSlice = createSlice({ ...@@ -25,10 +28,9 @@ const ScheduleSlice = createSlice({
name: 'schedule', name: 'schedule',
initialState, initialState,
reducers: { reducers: {
addSchedule: (state, action: PayloadAction<{ task: Task<any>, interval: number }>) => { registerTask: (state, action: PayloadAction<Task>) => {
const newSchedule = { const newSchedule = {
f: action.payload.task, task: action.payload.task,
interval: action.payload.interval,
id: state.schedules.length, id: state.schedules.length,
state: ScheduleState.INIT, state: ScheduleState.INIT,
intervalId: undefined intervalId: undefined
...@@ -36,9 +38,9 @@ const ScheduleSlice = createSlice({ ...@@ -36,9 +38,9 @@ const ScheduleSlice = createSlice({
state.schedules = [...state.schedules, newSchedule] state.schedules = [...state.schedules, newSchedule]
}, },
startSchedule: (state, action: PayloadAction<Schedule<any>>) => { startSchedule: (state, action: PayloadAction<Schedule>) => {
const schedule = action.payload; const schedule = action.payload;
schedule.intervalId = setInterval(schedule.f, schedule.interval); schedule.intervalId = setInterval(schedule.task.job, schedule.task.interval);
schedule.state = ScheduleState.RUNNING; schedule.state = ScheduleState.RUNNING;
state.schedules[schedule.id] = schedule; state.schedules[schedule.id] = schedule;
...@@ -46,25 +48,25 @@ const ScheduleSlice = createSlice({ ...@@ -46,25 +48,25 @@ const ScheduleSlice = createSlice({
}, },
}) })
export const { addSchedule } = ScheduleSlice.actions export const { registerTask } = ScheduleSlice.actions
export const { startSchedule } = ScheduleSlice.actions export const { startSchedule } = ScheduleSlice.actions
export default ScheduleSlice.reducer export default ScheduleSlice.reducer
startListening({ // startListening({
actionCreator: addSchedule, // actionCreator: addSchedule,
effect: (action, listenerApi) => { // effect: (action, listenerApi) => {
const newState = listenerApi.getState() as ScheduleReducerState; // const newState = listenerApi.getState() as ScheduleReducerState;
const originalState = listenerApi.getOriginalState() as ScheduleReducerState; // const originalState = listenerApi.getOriginalState() as ScheduleReducerState;
// get the added schedule // // get the added schedule
const schedule = newState.schedules.filter(s => !originalState.schedules.includes(s)).at(0); // const schedule = newState.schedules.filter(s => !originalState.schedules.includes(s)).at(0);
if (!schedule) { // if (!schedule) {
throw new Error("Added schedule not found in store"); // throw new Error("Added schedule not found in store");
} // }
listenerApi.dispatch(startSchedule(schedule)) // listenerApi.dispatch(startSchedule(schedule))
}, // },
}) // })
\ No newline at end of file \ No newline at end of file
import { api, RbacUser, UserServiceGetUsersApiArg } from '@api/api'; import { api, RbacUser, UserServiceGetUsersApiArg } from '@api/api';
import { createSlice, PayloadAction } from '@reduxjs/toolkit'; import { createAsyncThunk, createSlice, PayloadAction } from '@reduxjs/toolkit';
import { startListening } from '../middleware/listener.middleware';
import { RootState } from '..'; import { RootState } from '..';
import { startListening } from '../middleware/listener.middleware';
import { getCookieValue, setCookieValue } from '@helper/coookie';
export interface UserSliceState { export interface UserSliceState {
token: string,
// defined by the frontend user input. This value is getting compared with the backend response // defined by the frontend user input. This value is getting compared with the backend response
username: string, username: string,
user: RbacUser | null, user: RbacUser | null,
...@@ -12,7 +12,6 @@ export interface UserSliceState { ...@@ -12,7 +12,6 @@ export interface UserSliceState {
const initialState: UserSliceState = { const initialState: UserSliceState = {
token: '',
username: '', username: '',
user: null, user: null,
} }
...@@ -22,8 +21,10 @@ const userSlice = createSlice({ ...@@ -22,8 +21,10 @@ const userSlice = createSlice({
initialState, initialState,
reducers: { reducers: {
setToken: (state, action: PayloadAction<{ token: string, username: string }>) => { setToken: (state, action: PayloadAction<{ token: string, username: string }>) => {
state.token = action.payload.token; const token = action.payload?.token || '';
state.username = action.payload.username setCookieValue('token', token);
state.username = action.payload?.username || ''
}, },
setUser: (state, action: PayloadAction<RbacUser>) => { state.user = action.payload }, setUser: (state, action: PayloadAction<RbacUser>) => { state.user = action.payload },
}, },
...@@ -36,22 +37,18 @@ export default userSlice.reducer ...@@ -36,22 +37,18 @@ export default userSlice.reducer
export const userReducerPath = userSlice.reducerPath; export const userReducerPath = userSlice.reducerPath;
export const fetchUser = createAsyncThunk(
startListening({ 'user/fetchUser',
predicate: (action, { user }: any) => { (_, thunkAPI) => {
return setToken.match(action) && !!user.token;
},
effect: async (_, listenerApi) => {
const payload: UserServiceGetUsersApiArg = {}; const payload: UserServiceGetUsersApiArg = {};
listenerApi.dispatch(api.endpoints.userServiceGetUsers.initiate(payload)).then((response) => { thunkAPI.dispatch(api.endpoints.userServiceGetUsers.initiate(payload)).then((response) => {
if (response.error || !response.data?.user?.length) { if (response.error || !response.data?.user?.length) {
// TODO proper error handling // TODO proper error handling
throw new Error('Fetching the pnd list after successful login failed'); throw new Error('Fetching the pnd list after successful login failed');
} }
const {user} = listenerApi.getState() as RootState; const { user } = thunkAPI.getState() as RootState;
// TODO ask if this is the correct approach // TODO ask if this is the correct approach
const matchedUser = response.data.user.find((_user) => _user.name === user.username); const matchedUser = response.data.user.find((_user) => _user.name === user.username);
...@@ -61,8 +58,6 @@ startListening({ ...@@ -61,8 +58,6 @@ startListening({
throw new Error('No user found with the provided username'); throw new Error('No user found with the provided username');
} }
thunkAPI.dispatch(setUser(matchedUser));
listenerApi.dispatch(setUser(matchedUser)); });
}); });
\ No newline at end of file
},
})
\ No newline at end of file
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
import { AuthProvider } from "@provider/auth.provider"; import { AuthProvider } from "@provider/auth.provider";
import { useEffect } from "react";
import { useOutlet } from "react-router-dom"; import { useOutlet } from "react-router-dom";
import { ScheduleProvider } from "../scheduler";
export const BasicLayout = () => { export const BasicLayout = () => {
const outlet = useOutlet(); const outlet = useOutlet();
const { initSchedules } = ScheduleProvider();
useEffect(() => {
initSchedules();
}, [])
return ( return (
<AuthProvider> <AuthProvider>
......
import { useAuth } from "@provider/auth.provider"; 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 { useEffect } from "react";
import { useNavigate, useOutlet } from "react-router-dom"; import { useNavigate, useOutlet } from "react-router-dom";
...@@ -18,6 +18,6 @@ export const LoginLayout = ({ children }) => { ...@@ -18,6 +18,6 @@ export const LoginLayout = ({ children }) => {
}, []); }, []);
return ( return (
<LoginPage>{outlet}</LoginPage> <LoginView>{outlet}</LoginView>
) )
} }
\ No newline at end of file
@import "/src/style/colors.scss"; @import "/src/style/colors.scss";
$sidebar-width: 4.5em;
.head-links { .head-links {
text-decoration: none; text-decoration: none;
color: map-get($theme-colors, dark); color: map-get($theme-colors, dark);
...@@ -18,6 +20,10 @@ ...@@ -18,6 +20,10 @@
} }
.sidebar { .sidebar {
width: 4.5em; width: $sidebar-width;
height: 100vh; height: 100vh;
} }
.main-content {
margin-left: $sidebar-width;
}
\ No newline at end of file
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please to comment