From 8faab6613e3c82097f13eafc8e09489149595110 Mon Sep 17 00:00:00 2001 From: Andrii Podriez Date: Thu, 25 Jul 2024 13:54:16 +0200 Subject: [PATCH] updated redux storage and added RTK Query Signed-off-by: Andrii Podriez --- src/store/apiSlice.js | 306 +++++++++++++++++++++++++++++++++++++++++ src/store/icSlice.js | 185 +++++++++++++++++++++---- src/store/index.js | 11 +- src/store/userSlice.js | 6 - 4 files changed, 474 insertions(+), 34 deletions(-) create mode 100644 src/store/apiSlice.js diff --git a/src/store/apiSlice.js b/src/store/apiSlice.js new file mode 100644 index 0000000..4361ab6 --- /dev/null +++ b/src/store/apiSlice.js @@ -0,0 +1,306 @@ +import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'; +import { sessionToken } from '../localStorage'; + +export const apiSlice = createApi({ + reducerPath: 'scenarios', + baseQuery: fetchBaseQuery({ + baseUrl: '/api/v2', + prepareHeaders: (headers) => { + const token = sessionToken; + if (token) { + headers.set('Authorization', `Bearer ${token}`); + } + return headers; + }, + }), + endpoints: (builder) => ({ + getScenarios: builder.query({ + query: () => 'scenarios', + }), + getScenarioById: builder.query({ + query: (id) => `scenarios/${id}`, + }), + addScenario: builder.mutation({ + query: (scenario) => ({ + url: 'scenarios', + method: 'POST', + body: scenario, + }), + }), + deleteScenario: builder.mutation({ + query: (id) => ({ + url: `scenarios/${id}`, + method: 'DELETE', + }), + }), + updateScenario: builder.mutation({ + query: ({ id, ...updatedScenario }) => ({ + url: `scenarios/${id}`, + method: 'PUT', + body: updatedScenario, + }), + }), + getConfigs: builder.query({ + query: (scenarioID) => `configs?scenarioID=${scenarioID}`, + }), + getUsersOfScenario: builder.query({ + query: (scenarioID) => `scenarios/${scenarioID}/users/`, + }), + getDashboards: builder.query({ + query: (scenarioID) => `dashboards?scenarioID=${scenarioID}`, + }), + getICS: builder.query({ + query: () => 'ic', + }), + addUserToScenario: builder.mutation({ + query: ({ scenarioID, username }) => { + return ({ + url: `scenarios/${scenarioID}/user?username=${username}`, + method: 'PUT', + })}, + }), + removeUserFromScenario: builder.mutation({ + query: ({ scenarioID, username }) => ({ + url: `scenarios/${scenarioID}/user/?username=${username}`, + method: 'DELETE', + }), + }), + addComponentConfig: builder.mutation({ + query: (config) => ({ + url: 'configs', + method: 'POST', + body: config, + }), + }), + deleteComponentConfig: builder.mutation({ + query: (configID) => ({ + url: `configs/${configID}`, + method: 'DELETE', + }), + }), + addDashboard: builder.mutation({ + query: (dashboard) => ({ + url: 'dashboards', + method: 'POST', + body: dashboard, + }), + }), + deleteDashboard: builder.mutation({ + query: (dashboardID) => ({ + url: `dashboards/${dashboardID}`, + method: 'DELETE', + }), + }), + updateDashboard: builder.mutation({ + query: ({ dashboardID, dashboard }) => ({ + url: `dashboards/${dashboardID}`, + method: 'PUT', + body: {dashboard}, + }), + }), + getSignals: builder.query({ + query: ({ direction, configID }) => ({ + url: 'signals', + params: { direction, configID }, + }), + }), + addSignal: builder.mutation({ + query: (signal) => ({ + url: 'signals', + method: 'POST', + body: { signal }, + }), + }), + deleteSignal: builder.mutation({ + query: (signalID) => ({ + url: `signals/${signalID}`, + method: 'DELETE', + }), + }), + //users + getUsers: builder.query({ + query: () => 'users', + }), + getUserById: builder.query({ + query: (id) => `users/${id}`, + }), + addUser: builder.mutation({ + query: (user) => ({ + url: 'users', + method: 'POST', + body: user, + }), + }), + updateUser: builder.mutation({ + query: (user) => { + return { + url: `users/${user.id}`, + method: 'PUT', + body: {user: user}, + }}, + }), + deleteUser: builder.mutation({ + query: (id) => ({ + url: `users/${id}`, + method: 'DELETE', + }), + }), + //results + getResults: builder.query({ + query: (scenarioID) => ({ + url: 'results', + params: { scenarioID }, + }), + }), + addResult: builder.mutation({ + query: (result) => ({ + url: 'results', + method: 'POST', + body: result, + }), + }), + deleteResult: builder.mutation({ + query: (resultID) => ({ + url: `results/${resultID}`, + method: 'DELETE', + }), + }), + //files + getFiles: builder.query({ + query: (scenarioID) => ({ + url: 'files', + params: { scenarioID }, + }), + }), + addFile: builder.mutation({ + query: ({ scenarioID, file }) => { + const formData = new FormData(); + formData.append('inputFile', file); + return { + url: `files?scenarioID=${scenarioID}`, + method: 'POST', + body: formData, + }; + }, + }), + downloadFile: builder.query({ + query: (fileID) => ({ + url: `files/${fileID}`, + responseHandler: 'blob', + responseType: 'blob', + }), + }), + updateFile: builder.mutation({ + query: ({ fileID, file }) => { + const formData = new FormData(); + formData.append('inputFile', file); + return { + url: `files/${fileID}`, + method: 'PUT', + body: formData, + }; + }, + }), + deleteFile: builder.mutation({ + query: (fileID) => ({ + url: `files/${fileID}`, + method: 'DELETE', + }), + }), + sendAction: builder.mutation({ + query: (params) => ({ + url: `/ic/${params.icid}/action`, + method: 'POST', + body: [params], + }), + }), + + getDashboard: builder.query({ + query: (dashboardID) => `/dashboards/${dashboardID}`, + }), + + getWidgets: builder.query({ + query: (dashboardID) => ({ + url: 'widgets', + params: { dashboardID }, + }), + }), + addWidget: builder.mutation({ + query: (widget) => ({ + url: 'widgets', + method: 'POST', + body: { widget }, + }), + }), + getWidget: builder.query({ + query: (widgetID) => `/widgets/${widgetID}`, + }), + updateWidget: builder.mutation({ + query: ({ widgetID, updatedWidget }) => ({ + url: `/widgets/${widgetID}`, + method: 'PUT', + body: updatedWidget, + }), + }), + deleteWidget: builder.mutation({ + query: (widgetID) => ({ + url: `/widgets/${widgetID}`, + method: 'DELETE', + }), + }), + + getConfig: builder.query({ + query: () => '/config', + }) + }), +}); + +export const { + useGetScenariosQuery, + useGetScenarioByIdQuery, + useGetConfigsQuery, + useLazyGetConfigsQuery, + useGetDashboardsQuery, + useGetICSQuery, + useAddScenarioMutation, + useDeleteScenarioMutation, + useUpdateScenarioMutation, + useGetUsersOfScenarioQuery, + useAddUserToScenarioMutation, + useRemoveUserFromScenarioMutation, + useAddComponentConfigMutation, + useDeleteComponentConfigMutation, + useAddDashboardMutation, + useDeleteDashboardMutation, + useLazyGetSignalsQuery, + useGetSignalsQuery, + useAddSignalMutation, + useDeleteSignalMutation, + + useGetResultsQuery, + useAddResultMutation, + useDeleteResultMutation, + + useGetUsersQuery, + useGetUserByIdQuery, + useAddUserMutation, + useUpdateUserMutation, + useDeleteUserMutation, + + useGetFilesQuery, + useAddFileMutation, + useLazyDownloadFileQuery, + useUpdateFileMutation, + useDeleteFileMutation, + + useGetDashboardQuery, + useUpdateDashboardMutation, + + useSendActionMutation, + + useAddWidgetMutation, + useLazyGetWidgetsQuery, + useUpdateWidgetMutation, + useDeleteWidgetMutation, + useGetConfigQuery, +} = apiSlice; diff --git a/src/store/icSlice.js b/src/store/icSlice.js index c8cc96d..aa4b95b 100644 --- a/src/store/icSlice.js +++ b/src/store/icSlice.js @@ -17,25 +17,67 @@ import {createSlice, createAsyncThunk} from '@reduxjs/toolkit' import RestAPI from '../common/api/rest-api'; - import { sessionToken } from '../localStorage'; +import NotificationsDataManager from '../common/data-managers/notifications-data-manager'; +import NotificationsFactory from '../common/data-managers/notifications-factory'; + const icSlice = createSlice({ name: 'infrastructure', initialState: { ICsArray: [], - checkedICsArray: [], + + checkedICsIds: [], isLoading: false, currentIC: {}, - isCurrentICLoading: false + isCurrentICLoading: false, + //IC used for Edit and Delete Modals + editModalIC: null, + deleteModalIC: null, + isDeleteModalOpened: false, + isEditModalOpened: false }, reducers: { - checkICsByCategory: (state, args) => { - const category = args.payload; + updateCheckedICs: (state, args) => { + // each table has an object that maps IDs of all its ICs to boolean values + // which indicates wether or note user picked it in checbox column + const checkboxValues = args.payload; + let checkedICsIds = [...state.checkedICsIds]; - for(const ic in state.ICsArray){ - if (ic.category == category) state.checkedICsArray.push(ic) + for(const id in checkboxValues){ + if(checkedICsIds.includes(id)){ + if(!checkboxValues[id]){ + checkedICsIds = checkedICsIds.filter((checkedId) => checkedId != id); + } + } else { + if(checkboxValues[id]){ + checkedICsIds.push(id); + } + } } + + state.checkedICsIds = checkedICsIds; + }, + clearCheckedICs: (state, args) => { + state.checkedICsIds = []; + }, + openEditModal: (state, args) => { + state.isEditModalOpened = true; + state.editModalIC = args.payload; + console.log(state.editModalIC) + }, + closeEditModal: (state, args) => { + state.isEditModalOpened = false; + state.editModalIC = null; + }, + openDeleteModal: (state, args) => { + state.deleteModalIC = args.payload; + state.isDeleteModalOpened = true; + }, + closeDeleteModal: (state, args) => { + state.deleteModalIC = null; + state.isDeleteModalOpened = false; + } }, extraReducers: builder => { @@ -50,20 +92,33 @@ const icSlice = createSlice({ .addCase(loadICbyId.pending, (state, action) => { state.isCurrentICLoading = true }) - .addCase(loadICbyId.fulfilled, (state, action) => { - state.isCurrentICLoading = false - state.currentIC = action.payload; - console.log("fetched IC", state.currentIC.name) - }) - //TODO - // .addCase(restartIC.fullfilled, (state, action) => { - // console.log("restart fullfilled") - // //loadAllICs({token: sessionToken}) - // }) - // .addCase(shutdownIC.fullfilled, (state, action) => { - // console.log("shutdown fullfilled") - // //loadAllICs({token: sessionToken}) - // }) + + .addCase(loadICbyId.fulfilled, (state, action) => { + state.isCurrentICLoading = false + state.currentIC = action.payload; + console.log("fetched IC", state.currentIC.name) + }) + .addCase(addIC.rejected, (state, action) => { + NotificationsDataManager.addNotification(NotificationsFactory.ADD_ERROR("Error while adding infrastructural component: " + action.error.message)); + }) + .addCase(sendActionToIC.rejected, (state, action) => { + NotificationsDataManager.addNotification(NotificationsFactory.ADD_ERROR("Error while sending action to infrastructural component: " + action.error.message)); + }) + .addCase(editIC.rejected, (state, action) => { + NotificationsDataManager.addNotification(NotificationsFactory.ADD_ERROR("Error while trying to update an infrastructural component: " + action.error.message)); + }) + .addCase(deleteIC.rejected, (state, action) => { + NotificationsDataManager.addNotification(NotificationsFactory.ADD_ERROR("Error while trying to delete an infrastructural component: " + action.error.message)); + }) + //TODO + // .addCase(restartIC.fullfilled, (state, action) => { + // console.log("restart fullfilled") + // //loadAllICs({token: sessionToken}) + // }) + // .addCase(shutdownIC.fullfilled, (state, action) => { + // console.log("shutdown fullfilled") + // //loadAllICs({token: sessionToken}) + // }) } }); @@ -93,6 +148,85 @@ export const loadICbyId = createAsyncThunk( } ) +//adds a new Infrastructural component. Data object must contain token and ic fields +export const addIC = createAsyncThunk( + 'infrastructure/addIC', + async (data, {rejectWithValue}) => { + try { + //post request body: ic object that is to be added + const ic = {ic: data.ic}; + const res = await RestAPI.post('/api/v2/ic/', ic, data.token); + return res; + } catch (error) { + console.log("Error adding IC: ", error); + return rejectWithValue(error.response.data); + } + } +) + +//sends an action to IC. Data object must contain a token, IC's id and actions string +export const sendActionToIC = createAsyncThunk( + 'infrastructure/sendActionToIC', + async (data, {rejectWithValue}) => { + try { + const token = data.token; + const id = data.id; + let actions = data.actions; + + console.log("actions: ", actions) + + if (!Array.isArray(actions)) + actions = [ actions ] + + for (let action of actions) { + if (action.when) { + // Send timestamp as Unix Timestamp + action.when = Math.round(new Date(action.when).getTime() / 1000); + } + } + + const res = await RestAPI.post('/api/v2/ic/'+id+'/action', actions, token); + NotificationsDataManager.addNotification(NotificationsFactory.ACTION_INFO()); + return res; + } catch (error) { + console.log("Error sending an action to IC: ", error); + return rejectWithValue(error.response.data); + } + } +) + +//send a request to update IC's data. Data object must contain token, and updated ic object +export const editIC = createAsyncThunk( + 'infrastructure/editIC', + async (data, {rejectWithValue}) => { + try { + //post request body: ic object that is to be added + const {token, ic} = data; + const res = await RestAPI.put('/api/v2/ic/'+ic.id, {ic: ic}, token); + return res; + } catch (error) { + return rejectWithValue(error.response.data); + } + } +) + +//send a request to delete IC. Data object must contain token, and id of the IC that is to be deleted +export const deleteIC = createAsyncThunk( + 'infrastructure/deleteIC', + async (data, {rejectWithValue}) => { + try { + //post request body: ic object that is to be added + const {token, id} = data; + const res = await RestAPI.delete('/api/v2/ic/'+id, token); + return res; + } catch (error) { + console.log("Error updating IC: ", error); + return rejectWithValue(error.response.data); + } + } +) + + //TODO //restarts ICs @@ -110,7 +244,8 @@ export const restartIC = createAsyncThunk( } ) -//restarts ICs + +//shut ICs down export const shutdownIC = createAsyncThunk( 'infrastructure/shutdownIC', async (data) => { @@ -125,6 +260,8 @@ export const shutdownIC = createAsyncThunk( } ) -export const {checkICsByCategory} = icSlice.actions; -export default icSlice.reducer; \ No newline at end of file +export const {updateCheckedICs, clearCheckedICs, openEditModal, openDeleteModal, closeDeleteModal, closeEditModal} = icSlice.actions; + +export default icSlice.reducer; + diff --git a/src/store/index.js b/src/store/index.js index 9867d04..c4f6072 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -16,16 +16,19 @@ ******************************************************************************/ import { configureStore } from "@reduxjs/toolkit"; - import userReducer from './userSlice'; import icReducer from './icSlice'; import configReducer from './configSlice' +import { apiSlice } from "./apiSlice"; export const store = configureStore({ reducer: { user: userReducer, infrastructure: icReducer, - config: configReducer + config: configReducer, + [apiSlice.reducerPath]: apiSlice.reducer, }, - devTools: true -}) \ No newline at end of file + middleware: (getDefaultMiddleware) => + getDefaultMiddleware().concat(apiSlice.middleware), + devTools: true, +}) diff --git a/src/store/userSlice.js b/src/store/userSlice.js index 18f6117..7293e4b 100644 --- a/src/store/userSlice.js +++ b/src/store/userSlice.js @@ -16,9 +16,7 @@ ******************************************************************************/ import {createSlice, createAsyncThunk} from '@reduxjs/toolkit' - import RestAPI from '../common/api/rest-api'; -import ICDataDataManager from '../ic/ic-data-data-manager'; const userSlice = createSlice({ name: 'user', @@ -91,12 +89,8 @@ export const loginExternal = createAsyncThunk( export const logout = createAsyncThunk( 'user/logout', async () => { - // disconnect from all infrastructure components - ICDataDataManager.closeAll(); //remove token and current user from local storage localStorage.clear(); - - console.log("logged out") } )