History of actions testing mode is enabled in the backend.

All the other connections are updated through the websocket manager
Fixed bug with status value in the front-end
This commit is contained in:
Noe 2024-07-23 14:52:10 +02:00
parent 1e6c10b96e
commit 95798ba904
14 changed files with 95 additions and 104 deletions

View File

@ -793,7 +793,7 @@ public class Controller : ControllerBase
public async Task<ActionResult<IEnumerable<Object>>> InsertNewAction([FromBody] UserAction action, Token authToken) public async Task<ActionResult<IEnumerable<Object>>> InsertNewAction([FromBody] UserAction action, Token authToken)
{ {
var session = Db.GetSession(authToken); var session = Db.GetSession(authToken);
var actionSuccess = await session.RecordUserAction(action); var actionSuccess = await session.InsertUserAction(action);
return actionSuccess ? Ok() : Unauthorized(); return actionSuccess ? Ok() : Unauthorized();
} }
@ -835,7 +835,7 @@ public class Controller : ControllerBase
Description = config.GetConfigurationString() Description = config.GetConfigurationString()
}; };
var actionSuccess = await session.RecordUserAction(action); var actionSuccess = await session.InsertUserAction(action);
return actionSuccess?Ok():Unauthorized(); return actionSuccess?Ok():Unauthorized();
} }

View File

@ -19,6 +19,7 @@ public class Installation : TreeNode
public int S3BucketId { get; set; } = 0; public int S3BucketId { get; set; } = 0;
public String ReadRoleId { get; set; } = ""; public String ReadRoleId { get; set; } = "";
public String WriteRoleId { get; set; } = ""; public String WriteRoleId { get; set; } = "";
public Boolean TestingMode { get; set; } = false;
public int Product { get; set; } = 0; public int Product { get; set; } = 0;
public int Device { get; set; } = 0; public int Device { get; set; } = 0;

View File

@ -1,6 +1,7 @@
using System.Diagnostics; using System.Diagnostics;
using InnovEnergy.App.Backend.Database; using InnovEnergy.App.Backend.Database;
using InnovEnergy.App.Backend.Relations; using InnovEnergy.App.Backend.Relations;
using InnovEnergy.App.Backend.Websockets;
using InnovEnergy.Lib.Utils; using InnovEnergy.Lib.Utils;
using Org.BouncyCastle.Asn1.X509; using Org.BouncyCastle.Asn1.X509;
@ -131,14 +132,17 @@ public static class SessionMethods
&& await installation.SendConfig(configuration); && await installation.SendConfig(configuration);
} }
public static async Task<Boolean> RecordUserAction(this Session? session, UserAction action) public static async Task<Boolean> InsertUserAction(this Session? session, UserAction action)
{ {
var user = session?.User; var user = session?.User;
if (user is null || user.UserType == 0) if (user is null || user.UserType == 0)
return false; return false;
action.UserName = user.Name; var installation = Db.GetInstallationById(action.InstallationId);
installation.TestingMode = action.TestingMode;
installation.Apply(Db.Update);
WebsocketManager.InformWebsocketsForInstallation(action.InstallationId);
// Save the configuration change to the database // Save the configuration change to the database
Db.HandleAction(action); Db.HandleAction(action);
@ -152,6 +156,14 @@ public static class SessionMethods
if (user is null || user.UserType == 0) if (user is null || user.UserType == 0)
return false; return false;
var installation = Db.GetInstallationById(action.InstallationId);
if (installation.TestingMode != action.TestingMode)
{
installation.TestingMode = action.TestingMode;
installation.Apply(Db.Update);
WebsocketManager.InformWebsocketsForInstallation(action.InstallationId);
}
Db.UpdateAction(action); Db.UpdateAction(action);
return true; return true;
} }

View File

@ -12,6 +12,8 @@ public class UserAction
public Int64 InstallationId { get; set; } // Installation ID where the configuration change is made public Int64 InstallationId { get; set; } // Installation ID where the configuration change is made
public Boolean TestingMode { get; set; }
public DateTime Timestamp { get; set; } // Timestamp of the configuration change public DateTime Timestamp { get; set; } // Timestamp of the configuration change
public String Description { get; set; } = null!;// Serialized string representing the new configuration public String Description { get; set; } = null!;// Serialized string representing the new configuration

View File

@ -111,11 +111,14 @@ public static partial class Db
public static void UpdateAction(UserAction updatedAction) public static void UpdateAction(UserAction updatedAction)
{ {
var existingAction = UserActions.FirstOrDefault(action => action.Id == updatedAction.Id); var existingAction = UserActions.FirstOrDefault(action => action.Id == updatedAction.Id);
if (existingAction != null) if (existingAction != null)
{ {
existingAction.Description = updatedAction.Description; //existingAction.Description = updatedAction.Description;
existingAction.Timestamp = updatedAction.Timestamp; //existingAction.Timestamp = updatedAction.Timestamp;
Update(existingAction); //existingAction.TestingMode = updatedAction.TestingMode;
Update(updatedAction);
Console.WriteLine("---------------Updated the Action in the database-----------------"); Console.WriteLine("---------------Updated the Action in the database-----------------");
} }
} }

View File

@ -9,7 +9,7 @@ namespace InnovEnergy.App.Backend.Websockets;
public static class WebsocketManager public static class WebsocketManager
{ {
public static Dictionary<int, InstallationInfo> InstallationConnections = new Dictionary<int, InstallationInfo>(); public static Dictionary<Int64, InstallationInfo> InstallationConnections = new Dictionary<Int64, InstallationInfo>();
//Every 2 minutes, check the timestamp of the latest received message for every installation. //Every 2 minutes, check the timestamp of the latest received message for every installation.
//If the difference between the two timestamps is more than two minutes, we consider this installation unavailable. //If the difference between the two timestamps is more than two minutes, we consider this installation unavailable.
@ -30,15 +30,18 @@ public static class WebsocketManager
} }
//Inform all the connected websockets regarding installation "installationId" //Inform all the connected websockets regarding installation "installationId"
public static void InformWebsocketsForInstallation(int installationId) public static void InformWebsocketsForInstallation(Int64 installationId)
{ {
var installation = Db.GetInstallationById(installationId);
var installationConnection = InstallationConnections[installationId]; var installationConnection = InstallationConnections[installationId];
Console.WriteLine("Update all the connected websockets for installation " + installationId); Console.WriteLine("Update all the connected websockets for installation " + installationId);
var jsonObject = new var jsonObject = new
{ {
id = installationId, id = installationId,
status = installationConnection.Status status = installationConnection.Status,
testingMode = installation.TestingMode
}; };
string jsonString = JsonSerializer.Serialize(jsonObject); string jsonString = JsonSerializer.Serialize(jsonObject);
@ -99,9 +102,10 @@ public static class WebsocketManager
//Then, report the status of each requested installation to the front-end that created the websocket connection //Then, report the status of each requested installation to the front-end that created the websocket connection
foreach (var installationId in installationIds) foreach (var installationId in installationIds)
{ {
var installation = Db.GetInstallationById(installationId);
if (!InstallationConnections.ContainsKey(installationId)) if (!InstallationConnections.ContainsKey(installationId))
{ {
Console.WriteLine("Create new empty list for installation id " + installationId); //Console.WriteLine("Create new empty list for installation id " + installationId);
InstallationConnections[installationId] = new InstallationInfo InstallationConnections[installationId] = new InstallationInfo
{ {
Status = -1 Status = -1
@ -113,7 +117,8 @@ public static class WebsocketManager
var jsonObject = new var jsonObject = new
{ {
id = installationId, id = installationId,
status = InstallationConnections[installationId].Status status = InstallationConnections[installationId].Status,
testingMode = installation.TestingMode
}; };
var jsonString = JsonSerializer.Serialize(jsonObject); var jsonString = JsonSerializer.Serialize(jsonObject);

View File

@ -11,8 +11,6 @@ import fr from './lang/fr.json';
import SuspenseLoader from './components/SuspenseLoader'; import SuspenseLoader from './components/SuspenseLoader';
import SidebarLayout from './layouts/SidebarLayout'; import SidebarLayout from './layouts/SidebarLayout';
import { TokenContext } from './contexts/tokenContext'; import { TokenContext } from './contexts/tokenContext';
import { TestModeProvider } from './contexts/TestModeContext';
import ResetPassword from './components/ResetPassword';
import InstallationTabs from './content/dashboards/Installations/index'; import InstallationTabs from './content/dashboards/Installations/index';
import routes from 'src/Resources/routes.json'; import routes from 'src/Resources/routes.json';
import './App.css'; import './App.css';
@ -143,9 +141,7 @@ function App() {
element={ element={
<AccessContextProvider> <AccessContextProvider>
<InstallationsContextProvider> <InstallationsContextProvider>
<TestModeProvider>
<InstallationTabs /> <InstallationTabs />
</TestModeProvider>
</InstallationsContextProvider> </InstallationsContextProvider>
</AccessContextProvider> </AccessContextProvider>
} }

View File

@ -20,7 +20,6 @@ import { AxiosError, AxiosResponse } from 'axios/index';
import routes from '../../../Resources/routes.json'; import routes from '../../../Resources/routes.json';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { TokenContext } from '../../../contexts/tokenContext'; import { TokenContext } from '../../../contexts/tokenContext';
import { useTestMode } from '../../../contexts/TestModeContext';
import { Action } from '../../../interfaces/S3Types'; import { Action } from '../../../interfaces/S3Types';
import Button from '@mui/material/Button'; import Button from '@mui/material/Button';
import { DateTimePicker, LocalizationProvider } from '@mui/x-date-pickers'; import { DateTimePicker, LocalizationProvider } from '@mui/x-date-pickers';
@ -49,19 +48,14 @@ function HistoryOfActions(props: HistoryProps) {
const [newAction, setNewAction] = useState<Partial<Action>>({ const [newAction, setNewAction] = useState<Partial<Action>>({
installationId: props.id, installationId: props.id,
timestamp: actionDate.toDate(), timestamp: actionDate.toDate(),
description: '' description: '',
testingMode: false
}); });
const { testModeMap, setTestMode } = useTestMode();
const isTestMode = testModeMap[props.id] || false;
const context = useContext(UserContext); const context = useContext(UserContext);
const { currentUser, setUser } = context; const { currentUser, setUser } = context;
const [isRowHovered, setHoveredRow] = useState(-1); const [isRowHovered, setHoveredRow] = useState(-1);
const [selectedAction, setSelectedAction] = useState<number>(-1); const [selectedAction, setSelectedAction] = useState<number>(-1);
const [editMode, setEditMode] = useState(false); const [editMode, setEditMode] = useState(false);
const handleTestModeToggle = () => {
setTestMode(props.id, !isTestMode);
};
const handleDateChange = (newdate) => { const handleDateChange = (newdate) => {
setActionDate(newdate); setActionDate(newdate);
setNewAction({ setNewAction({
@ -71,10 +65,10 @@ function HistoryOfActions(props: HistoryProps) {
}; };
const handleChange = (e) => { const handleChange = (e) => {
const { name, value } = e.target; const { name, type, checked, value } = e.target;
setNewAction({ setNewAction({
...newAction, ...newAction,
[name]: value [name]: type === 'checkbox' ? checked : value
}); });
}; };
@ -83,7 +77,8 @@ function HistoryOfActions(props: HistoryProps) {
setActionDate(dayjs()); setActionDate(dayjs());
setNewAction({ setNewAction({
...newAction, ...newAction,
['description']: '' ['description']: '',
['timestamp']: dayjs().toDate()
}); });
setEditMode(false); setEditMode(false);
}; };
@ -233,8 +228,9 @@ function HistoryOfActions(props: HistoryProps) {
<FormControlLabel <FormControlLabel
control={ control={
<Switch <Switch
checked={isTestMode} name="testingMode"
onChange={handleTestModeToggle} checked={newAction.testingMode}
onChange={handleChange}
/> />
} }
label="Test Mode" label="Test Mode"

View File

@ -16,7 +16,6 @@ import { I_Installation } from 'src/interfaces/InstallationTypes';
import CancelIcon from '@mui/icons-material/Cancel'; import CancelIcon from '@mui/icons-material/Cancel';
import BuildIcon from '@mui/icons-material/Build'; import BuildIcon from '@mui/icons-material/Build';
import { WebSocketContext } from 'src/contexts/WebSocketContextProvider'; import { WebSocketContext } from 'src/contexts/WebSocketContextProvider';
import { useTestMode } from 'src/contexts/TestModeContext';
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
import { useLocation, useNavigate } from 'react-router-dom'; import { useLocation, useNavigate } from 'react-router-dom';
import routes from '../../../Resources/routes.json'; import routes from '../../../Resources/routes.json';
@ -28,11 +27,10 @@ interface FlatInstallationViewProps {
const FlatInstallationView = (props: FlatInstallationViewProps) => { const FlatInstallationView = (props: FlatInstallationViewProps) => {
const [isRowHovered, setHoveredRow] = useState(-1); const [isRowHovered, setHoveredRow] = useState(-1);
const webSocketContext = useContext(WebSocketContext); const webSocketContext = useContext(WebSocketContext);
const { getStatus } = webSocketContext; const { getStatus, getTestingMode } = webSocketContext;
const navigate = useNavigate(); const navigate = useNavigate();
const [selectedInstallation, setSelectedInstallation] = useState<number>(-1); const [selectedInstallation, setSelectedInstallation] = useState<number>(-1);
const currentLocation = useLocation(); const currentLocation = useLocation();
const { testModeMap } = useTestMode();
const sortedInstallations = [...props.installations].sort((a, b) => { const sortedInstallations = [...props.installations].sort((a, b) => {
// Compare the status field of each installation and sort them based on the status. // Compare the status field of each installation and sort them based on the status.
@ -246,7 +244,7 @@ const FlatInstallationView = (props: FlatInstallationViewProps) => {
}} }}
/> />
{testModeMap[installation.id] && ( {getTestingMode(installation.id) && (
<BuildIcon <BuildIcon
style={{ style={{
width: '23px', width: '23px',
@ -255,7 +253,7 @@ const FlatInstallationView = (props: FlatInstallationViewProps) => {
borderRadius: '50%', borderRadius: '50%',
position: 'relative', position: 'relative',
zIndex: 1, zIndex: 1,
marginLeft: status === -1 || status === -2 ? '-23px' : '2px', marginLeft: '15px'
}} }}
/> />
)} )}

View File

@ -32,7 +32,6 @@ import BatteryView from '../BatteryView/BatteryView';
import { UserType } from '../../../interfaces/UserTypes'; import { UserType } from '../../../interfaces/UserTypes';
import HistoryOfActions from '../History/History'; import HistoryOfActions from '../History/History';
import PvView from '../PvView/PvView'; import PvView from '../PvView/PvView';
import { useTestMode } from '../../../contexts/TestModeContext';
interface singleInstallationProps { interface singleInstallationProps {
current_installation?: I_Installation; current_installation?: I_Installation;
@ -46,13 +45,12 @@ function Installation(props: singleInstallationProps) {
const [fetchFunctionCalled, setFetchFunctionCalled] = useState(false); const [fetchFunctionCalled, setFetchFunctionCalled] = useState(false);
const [errorLoadingS3Data, setErrorLoadingS3Data] = useState(false); const [errorLoadingS3Data, setErrorLoadingS3Data] = useState(false);
const webSocketsContext = useContext(WebSocketContext); const webSocketsContext = useContext(WebSocketContext);
const { getStatus } = webSocketsContext; const { getStatus, getTestingMode } = webSocketsContext;
const [currentTab, setCurrentTab] = useState<string>(undefined); const [currentTab, setCurrentTab] = useState<string>(undefined);
const [values, setValues] = useState<TopologyValues | null>(null); const [values, setValues] = useState<TopologyValues | null>(null);
const status = getStatus(props.current_installation.id); const status = getStatus(props.current_installation.id);
const [connected, setConnected] = useState(true); const [connected, setConnected] = useState(true);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const { testModeMap } = useTestMode();
if (props.current_installation == undefined) { if (props.current_installation == undefined) {
return null; return null;
@ -362,7 +360,7 @@ function Installation(props: singleInstallationProps) {
}} }}
/> />
{testModeMap[props.current_installation.id] && ( {getTestingMode(props.current_installation.id) && (
<BuildIcon <BuildIcon
style={{ style={{
width: '23px', width: '23px',
@ -371,34 +369,37 @@ function Installation(props: singleInstallationProps) {
borderRadius: '50%', borderRadius: '50%',
position: 'relative', position: 'relative',
zIndex: 1, zIndex: 1,
marginLeft: status === -1 || status === -2 ? '-23px' : '2px' marginLeft: '15px'
}} }}
/> />
)} )}
</div> </div>
</div> </div>
{loading && currentTab != 'information' && currentTab != 'history' && ( {loading &&
<Container currentTab != 'information' &&
maxWidth="xl" currentTab != 'history' &&
sx={{ currentTab != 'log' && (
display: 'flex', <Container
flexDirection: 'column', maxWidth="xl"
justifyContent: 'center', sx={{
alignItems: 'center', display: 'flex',
height: '70vh' flexDirection: 'column',
}} justifyContent: 'center',
> alignItems: 'center',
<CircularProgress size={60} style={{ color: '#ffc04d' }} /> height: '70vh'
<Typography }}
variant="body2"
style={{ color: 'black', fontWeight: 'bold' }}
mt={2}
> >
Connecting to the device... <CircularProgress size={60} style={{ color: '#ffc04d' }} />
</Typography> <Typography
</Container> variant="body2"
)} style={{ color: 'black', fontWeight: 'bold' }}
mt={2}
>
Connecting to the device...
</Typography>
</Container>
)}
<Card variant="outlined"> <Card variant="outlined">
<Grid <Grid

View File

@ -1,30 +0,0 @@
import React, { createContext, useContext, useState, ReactNode } from 'react';
interface TestModeContextProps {
testModeMap: { [key: number]: boolean };
setTestMode: (id: number, mode: boolean) => void;
}
const TestModeContext = createContext<TestModeContextProps | undefined>(undefined);
export const useTestMode = () => {
const context = useContext(TestModeContext);
if (!context) {
throw new Error('useTestMode must be used within a TestModeProvider');
}
return context;
};
export const TestModeProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
const [testModeMap, setTestModeMap] = useState<{ [key: number]: boolean }>({});
const setTestMode = (id: number, mode: boolean) => {
setTestModeMap(prev => ({ ...prev, [id]: mode }));
};
return (
<TestModeContext.Provider value={{ testModeMap, setTestMode }}>
{children}
</TestModeContext.Provider>
);
};

View File

@ -7,6 +7,7 @@ interface WebSocketContextProviderProps {
openSocket: (installations: I_Installation[]) => void; openSocket: (installations: I_Installation[]) => void;
closeSocket: () => void; closeSocket: () => void;
getStatus: (installationId: number) => number; getStatus: (installationId: number) => number;
getTestingMode: (installationId: number) => boolean;
} }
// Create the context. // Create the context.
@ -18,7 +19,10 @@ const WebSocketContextProvider = ({ children }: { children: ReactNode }) => {
const [socket, setSocket] = useState<WebSocket>(null); const [socket, setSocket] = useState<WebSocket>(null);
const [installations, setInstallations] = useState<I_Installation[]>(null); const [installations, setInstallations] = useState<I_Installation[]>(null);
const [installationStatus, setInstallationStatus] = useState< const [installationStatus, setInstallationStatus] = useState<
Record<number, number[]> Record<number, number>
>({});
const [installationMode, setInstallatioMode] = useState<
Record<number, boolean>
>({}); >({});
const BUFFER_LENGTH = 5; const BUFFER_LENGTH = 5;
@ -52,23 +56,19 @@ const WebSocketContextProvider = ({ children }: { children: ReactNode }) => {
if (message.id != -1) { if (message.id != -1) {
const installation_id = message.id; const installation_id = message.id;
const status = message.status;
//console.log('Message from server ', installation_id, status); //console.log('Message from server ', installation_id, status);
setInstallatioMode((prevMode) => {
// Create a new object by spreading the previous state
const newMode = { ...prevMode };
newMode[installation_id] = message.testingMode;
return newMode;
});
setInstallationStatus((prevStatus) => { setInstallationStatus((prevStatus) => {
// Create a new object by spreading the previous state // Create a new object by spreading the previous state
const newStatus = { ...prevStatus }; const newStatus = { ...prevStatus };
newStatus[installation_id] = message.status;
if (!newStatus.hasOwnProperty(installation_id)) {
newStatus[installation_id] = [];
}
newStatus[installation_id].unshift(status);
newStatus[installation_id] = newStatus[installation_id].slice(
0,
BUFFER_LENGTH
);
return newStatus; return newStatus;
}); });
} }
@ -90,18 +90,23 @@ const WebSocketContextProvider = ({ children }: { children: ReactNode }) => {
if (!installationStatus.hasOwnProperty(installationId)) { if (!installationStatus.hasOwnProperty(installationId)) {
status = -2; status = -2;
} else { } else {
status = installationStatus[installationId][0]; status = installationStatus[installationId];
} }
return status; return status;
}; };
const getTestingMode = (installationId: number) => {
return installationMode[installationId];
};
return ( return (
<WebSocketContext.Provider <WebSocketContext.Provider
value={{ value={{
socket: socket, socket: socket,
openSocket: openSocket, openSocket: openSocket,
closeSocket: closeSocket, closeSocket: closeSocket,
getStatus: getStatus getStatus: getStatus,
getTestingMode: getTestingMode
}} }}
> >
{children} {children}

View File

@ -18,6 +18,7 @@ export interface I_Installation extends I_S3Credentials {
s3WriteSecret: string; s3WriteSecret: string;
product: number; product: number;
device: number; device: number;
testingMode: boolean;
} }
export interface I_Folder { export interface I_Folder {

View File

@ -29,4 +29,5 @@ export interface Action {
installationId: number; installationId: number;
timestamp: Date; timestamp: Date;
description: string; description: string;
testingMode: boolean;
} }