diff --git a/ui/src/components/NotificationsArea.js b/ui/src/components/NotificationsArea.js index afb38c0..c6e1832 100644 --- a/ui/src/components/NotificationsArea.js +++ b/ui/src/components/NotificationsArea.js @@ -1,19 +1,23 @@ import React from 'react'; +import { connect } from 'react-redux'; import { Alert } from 'react-bootstrap'; -import { useNotificationsSystem } from '../contexts/NotificationsContext'; +import { NotificationsSystemActions } from '../store/slices/Notifications'; -function NotificationsArea() { - const NotificationsSystem = useNotificationsSystem(); - const { currentNotifications } = NotificationsSystem; - if(!currentNotifications) return null; - - const hideNotifications = () => { NotificationsSystem.clear(); }; +function NotificationsArea({ hasNotifications, message, clearNotifications }) { + if(!hasNotifications) return null; return ( - - {currentNotifications.message} + + {message} ); } -export default NotificationsArea; +export default connect( + (state) => ({ + hasNotifications: state.notifications.currentNotifications != null, + message: state.notifications.currentNotifications?.message + }), { + clearNotifications: NotificationsSystemActions.clear + } +)(NotificationsArea); \ No newline at end of file diff --git a/ui/src/components/SystemControls.js b/ui/src/components/SystemControls.js index 6076831..16d1a8a 100644 --- a/ui/src/components/SystemControls.js +++ b/ui/src/components/SystemControls.js @@ -3,29 +3,18 @@ import { connect } from 'react-redux'; import { Button } from 'react-bootstrap'; import { useWaterPumpAPI } from '../contexts/WaterPumpAPIContext'; -import { useNotificationsSystem } from '../contexts/NotificationsContext.js'; import { startPump, stopPump } from '../store/slices/SystemStatus.js'; export function SystemControlsComponent({ pouringTime, systemStatus, startPump, stopPump }) { const api = useWaterPumpAPI().API; - const NotificationsSystem = useNotificationsSystem(); - const handleStart = async () => { - try { - await startPump({ api , pouringTime }); - } catch (error) { - NotificationsSystem.alert('Error starting water pump: ' + error.message); - } + await startPump({ api , pouringTime }); }; const handleStop = async () => { - try { - await stopPump({ api }); - } catch (error) { - NotificationsSystem.alert('Error stopping water pump: ' + error.message); - } + await stopPump({ api }); }; const isRunning = systemStatus.pump.running; diff --git a/ui/src/components/WaterPumpStatusProvider.js b/ui/src/components/WaterPumpStatusProvider.js index 3cadb5b..035cfed 100644 --- a/ui/src/components/WaterPumpStatusProvider.js +++ b/ui/src/components/WaterPumpStatusProvider.js @@ -2,14 +2,12 @@ import React from 'react'; import { connect } from 'react-redux'; import { updateSystemStatus } from '../store/slices/SystemStatus'; import { useWaterPumpAPI } from '../contexts/WaterPumpAPIContext'; -import { useNotificationsSystem } from '../contexts/NotificationsContext'; const FETCH_INTERVAL = 5000; const CHECK_INTERVAL = Math.round(FETCH_INTERVAL / 10); function WaterPumpStatusProviderComoponent({ children, updateStatus, systemStatus }) { const { API } = useWaterPumpAPI(); - const NotificationsSystem = useNotificationsSystem(); const nextFetchTime = React.useRef(0); // Function to fetch water pump status @@ -19,16 +17,10 @@ function WaterPumpStatusProviderComoponent({ children, updateStatus, systemStatu if(null == API) return; nextFetchTime.current = Number.MAX_SAFE_INTEGER; // prevent concurrent fetches - try { - const status = await API.status(); - updateStatus(status); - } catch (error) { - NotificationsSystem.alert('Error fetching system status: ' + error.message); - updateStatus(null); - } + await updateStatus(API); nextFetchTime.current = Date.now() + FETCH_INTERVAL; }, - [API, NotificationsSystem, updateStatus, nextFetchTime] + [API, updateStatus, nextFetchTime] ); // Effect to start fetching periodically and when API changes @@ -58,6 +50,5 @@ export default connect( systemStatus: state.systemStatus }), { updateStatus: updateSystemStatus - } )(WaterPumpStatusProviderComoponent); \ No newline at end of file diff --git a/ui/src/contexts/NotificationsContext.js b/ui/src/contexts/NotificationsContext.js deleted file mode 100644 index 6685a68..0000000 --- a/ui/src/contexts/NotificationsContext.js +++ /dev/null @@ -1,23 +0,0 @@ -import React from 'react'; - -const NotificationsContext = React.createContext(); - -export function useNotificationsSystem() { - return React.useContext(NotificationsContext); -} - -export function NotificationsProvider({ children }) { - const [notifications, setNotifications] = React.useState(null); - - const value = { - alert: (message) => { setNotifications({ message }); }, - clear: () => { setNotifications(null); }, - currentNotifications: notifications, - }; - - return ( - - {children} - - ); -} \ No newline at end of file diff --git a/ui/src/index.js b/ui/src/index.js index 6777eb1..aa24bae 100644 --- a/ui/src/index.js +++ b/ui/src/index.js @@ -2,7 +2,6 @@ import React from 'react'; import App from './App.js'; import 'bootstrap/dist/css/bootstrap.min.css'; // Importing Bootstrap CSS -import { NotificationsProvider } from './contexts/NotificationsContext.js'; import { WaterPumpAPIProvider } from './contexts/WaterPumpAPIContext.js'; // Redux store import { AppStore } from './store'; @@ -12,11 +11,9 @@ const root = createRoot(document.getElementById('root')); root.render( - - - - - + + + ); diff --git a/ui/src/store/slices/Notifications.js b/ui/src/store/slices/Notifications.js new file mode 100644 index 0000000..9fbc377 --- /dev/null +++ b/ui/src/store/slices/Notifications.js @@ -0,0 +1,18 @@ +import { createSlice } from '@reduxjs/toolkit'; + +export const NotificationsSlice = createSlice({ + name: 'notifications', + initialState: { + currentNotifications: null + }, + reducers: { + alert: (state, action) => { + state.currentNotifications = action.payload; + }, + clear: state => { + state.currentNotifications = null; + } + } +}); + +export const NotificationsSystemActions = NotificationsSlice.actions; \ No newline at end of file diff --git a/ui/src/store/slices/SystemStatus.js b/ui/src/store/slices/SystemStatus.js index 745cf4f..8e80d2f 100644 --- a/ui/src/store/slices/SystemStatus.js +++ b/ui/src/store/slices/SystemStatus.js @@ -1,20 +1,49 @@ import { createSlice, createAsyncThunk } from '@reduxjs/toolkit'; +import { NotificationsSystemActions } from './Notifications'; + +function withNotification(action, message) { + return async (params, { dispatch }) => { + try { + return await action(params); + } catch(error) { + dispatch(NotificationsSystemActions.alert({ + type: 'error', + message: `${message} (${error.message})` + })); + throw error; + } + }; +} // Async thunks export const startPump = createAsyncThunk( 'systemStatus/startPump', - async ({ api, pouringTime }, { dispatch }) => { - const response = await api.start(pouringTime); - return response; - } + withNotification( + async ({ api, pouringTime }) => { + return await api.start(pouringTime); + }, + 'Failed to start pump' + ) ); export const stopPump = createAsyncThunk( 'systemStatus/stopPump', - async ({ api }, { dispatch }) => { - const response = await api.stop(); - return response; - } + withNotification( + async ({ api }) => { + return await api.stop(); + }, + 'Failed to stop pump' + ) +); + +export const updateSystemStatus = createAsyncThunk( + 'systemStatus/update', + withNotification( + async ( api ) => { + return await api.status(); + }, + 'Failed to update system status' + ) ); // slice for system status @@ -25,18 +54,17 @@ const bindStatus = (state, action) => { export const SystemStatusSlice = createSlice({ name: 'systemStatus', initialState: null, - reducers: { - updateSystemStatus: bindStatus, - }, + reducers: {}, extraReducers: (builder) => { // update system status on start/stop pump builder.addCase(startPump.fulfilled, bindStatus); builder.addCase(stopPump.fulfilled, bindStatus); + builder.addCase(updateSystemStatus.fulfilled, bindStatus); // on error, do not update system status builder.addCase(startPump.rejected, (state, action) => state); builder.addCase(stopPump.rejected, (state, action) => state); + builder.addCase(updateSystemStatus.rejected, (state, action) => state); } }); -export const actions = SystemStatusSlice.actions; -export const { updateSystemStatus } = actions; \ No newline at end of file +export const actions = SystemStatusSlice.actions; \ No newline at end of file diff --git a/ui/src/store/slices/index.js b/ui/src/store/slices/index.js index 6f7cd36..9031b1d 100644 --- a/ui/src/store/slices/index.js +++ b/ui/src/store/slices/index.js @@ -1,7 +1,8 @@ import { SystemStatusSlice } from "./SystemStatus"; import { UISlice } from "./UI"; +import { NotificationsSlice } from "./Notifications"; -const slices = [ SystemStatusSlice, UISlice ]; +const slices = [ SystemStatusSlice, UISlice, NotificationsSlice ]; // export all slices as an object { [sliceName]: slice } export const ALL_APP_SLICES = slices.reduce((acc, slice) => { acc[slice.name] = slice;