From eb2b0ef60ab9e10ee8bef4a266db3cb0ba4337b4 Mon Sep 17 00:00:00 2001
From: Andrii Podriez <andrey5577990@gmail.com>
Date: Thu, 25 Jul 2024 13:54:16 +0200
Subject: [PATCH] updated redux storage and added RTK Query

Signed-off-by: Andrii Podriez <andrey5577990@gmail.com>
---
 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")
     }
 )