Update backend and frontend with middleware functionality

This commit is contained in:
Noe 2023-11-13 17:51:36 +01:00
parent 71b4a1d2bd
commit 1155e1bc4d
25 changed files with 318 additions and 553 deletions

View File

@ -61,6 +61,7 @@ public class Controller : ControllerBase
if (session is null)
{
Console.WriteLine("------------------------------------Unauthorized user----------------------------------------------");
HttpContext.Response.StatusCode = (int)HttpStatusCode.Unauthorized;
HttpContext.Abort();
return;
@ -68,6 +69,7 @@ public class Controller : ControllerBase
if (!HttpContext.WebSockets.IsWebSocketRequest)
{
Console.WriteLine("------------------------------------Not a websocket request ----------------------------------------------");
HttpContext.Response.StatusCode = (int)HttpStatusCode.Unauthorized;
HttpContext.Abort();
return;

View File

@ -8,7 +8,8 @@ public class Installation : TreeNode
public String Region { get; set; } = "";
public String Country { get; set; } = "";
public String InstallationName { get; set; } = "";
public String VpnIp { get; set; } = "";
// TODO: make relation
//public IReadOnlyList<String> OrderNumbers { get; set; } = Array.Empty<String>();
// public String? OrderNumbers { get; set; } = "";
@ -25,6 +26,7 @@ public class Installation : TreeNode
public String ReadRoleId { get; set; } = "";
public String WriteRoleId { get; set; } = "";
[Ignore]
public String OrderNumbers { get; set; }

View File

@ -6,9 +6,7 @@ using System.Net;
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json.Nodes;
using Amazon.S3.Model;
using InnovEnergy.App.Backend.Database;
using InnovEnergy.Lib.Utils;
namespace InnovEnergy.App.Backend.DataTypes.Methods;
@ -244,17 +242,11 @@ public static class ExoCmd
return id;
}
public static async Task<Boolean> CreateBucket(this Installation installation)
{
var cors = new CORSConfiguration();
cors.Rules.Add(new CORSRule());
cors.Rules[0].AllowedHeaders = new List<string> { "*" };
cors.Rules[0].AllowedOrigins = new List<string> { "*" };
cors.Rules[0].AllowedMethods = new List<string> { "Get", "Head" };
var s3Region = new S3Region($"https://{installation.S3Region}.{installation.S3Provider}", S3Creds!);
var a = await s3Region.PutBucket(installation.BucketName());
return a != null && await a.PutCors(cors);
return await s3Region.PutBucket(installation.BucketName()) != null;
}
public static async Task<Boolean> SendConfig(this Installation installation, String config)

View File

@ -99,11 +99,10 @@ public static class SessionMethods
&& user.HasWriteAccess
&& user.HasAccessToParentOf(installation)
&& Db.Create(installation) // TODO: these two in a transaction
&& installation.SetOrderNumbers()
&& installation.SetOrderNumbers()
&& Db.Create(new InstallationAccess { UserId = user.Id, InstallationId = installation.Id })
&& await installation.CreateBucket()
&& await installation.RenewS3Credentials();
// generation of access _after_ generation of
&& await installation.CreateBucket()
&& await installation.RenewS3Credentials(); // generation of access _after_ generation of
// bucket to prevent "zombie" access-rights.
// This might ** us over if the creation of access rights fails,
// as bucket-names are unique and bound to the installation id... -K

View File

@ -1,3 +1,7 @@
using System.Net.WebSockets;
using System.Text;
using System.Text.Json;
using System.Threading.Channels;
using Hellang.Middleware.ProblemDetails;
using InnovEnergy.App.Backend.Database;
using InnovEnergy.App.Backend.Websockets;
@ -5,6 +9,7 @@ using Microsoft.AspNetCore.HttpOverrides;
using Microsoft.AspNetCore.Mvc;
using Microsoft.OpenApi.Models;
using InnovEnergy.Lib.Utils;
using RabbitMQ.Client;
namespace InnovEnergy.App.Backend;
@ -16,6 +21,16 @@ public static class Program
Db.Init();
var builder = WebApplication.CreateBuilder(args);
string vpnServerIp = "194.182.190.208";
//string vpnServerIp = "127.0.0.1";
WebsocketManager.Factory = new ConnectionFactory { HostName = vpnServerIp};
WebsocketManager.Connection = WebsocketManager.Factory.CreateConnection();
WebsocketManager.Channel = WebsocketManager.Connection.CreateModel();
Console.WriteLine("Middleware subscribed to RabbitMQ queue, ready for receiving messages");
WebsocketManager.Channel.QueueDeclare(queue: "statusQueue", durable: true, exclusive: false, autoDelete: false, arguments: null);
WebsocketManager.StartRabbitMqConsumer();
Console.WriteLine("Queue declared");
WebsocketManager.InformInstallationsToSubscribeToRabbitMq();
builder.Services.AddControllers();

View File

@ -4,6 +4,7 @@ using System.Net.Sockets;
using System.Net.WebSockets;
using System.Text;
using System.Text.Json;
using InnovEnergy.App.Backend.Database;
using RabbitMQ.Client;
using RabbitMQ.Client.Events;
@ -11,27 +12,30 @@ namespace InnovEnergy.App.Backend.Websockets;
public static class WebsocketManager
{
public static readonly Dictionary<int, InstallationInfo> InstallationConnections = new Dictionary<int, InstallationInfo>();
private static ConnectionFactory _factory = null!;
private static IConnection _connection = null!;
private static IModel _channel = null!;
public static Dictionary<int, InstallationInfo> InstallationConnections = new Dictionary<int, InstallationInfo>();
public static ConnectionFactory Factory = null!;
public static IConnection Connection = null!;
public static IModel Channel = null!;
public static void InformInstallationsToSubscribeToRabbitMq()
{
var installationsIds = new List<int> { 1 };
var installationIps = new List<string> { "10.2.3.115" };
//var installationIps = new List<string> { "10.2.3.115" };
var installationIps = Db.Installations.Select(inst => inst.VpnIp).ToList();
Console.WriteLine("Count is "+installationIps.Count);
var maxRetransmissions = 2;
StartRabbitMqConsumer();
UdpClient udpClient = new UdpClient();
udpClient.Client.ReceiveTimeout = 2000;
int port = 9000;
//Send a message to each installation and tell it to subscribe to the queue
for (int i = 0; i < installationsIds.Count; i++)
using (udpClient)
{
using (udpClient)
for (int i = 0; i < installationIps.Count; i++)
{
if(installationIps[i]==""){continue;}
Console.WriteLine("-----------------------------------------------------------");
Console.WriteLine("Trying to reach installation with IP: " + installationIps[i]);
//Try at most MAX_RETRANSMISSIONS times to reach an installation.
for (int j = 0; j < maxRetransmissions; j++)
{
@ -46,7 +50,7 @@ public static class WebsocketManager
{
byte[] replyData = udpClient.Receive(ref remoteEndPoint);
string replyMessage = Encoding.UTF8.GetString(replyData);
Console.WriteLine("Received "+replyMessage +" from installation " + installationsIds[i]);
Console.WriteLine("Received " + replyMessage + " from installation " + installationIps[i]);
break;
}
catch (SocketException ex)
@ -63,83 +67,79 @@ public static class WebsocketManager
}
}
}
Console.WriteLine("Start RabbitMQ Consumer");
}
public static void StartRabbitMqConsumer()
public static async Task StartRabbitMqConsumer()
{
string vpnServerIp = "194.182.190.208";
//string vpnServerIp = "127.0.0.1";
_factory = new ConnectionFactory { HostName = vpnServerIp};
_connection = _factory.CreateConnection();
_channel = _connection.CreateModel();
Console.WriteLine("Middleware subscribed to RabbitMQ queue, ready for receiving messages");
_channel.QueueDeclare(queue: "statusQueue", durable: false, exclusive: false, autoDelete: false, arguments: null);
var consumer = new EventingBasicConsumer(_channel);
consumer.Received += (_, ea) => CallbackReceiveMessageFromQueue(ea);
_channel.BasicConsume(queue: "statusQueue", autoAck: true, consumer: consumer);
}
[UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "<Pending>")]
private static void CallbackReceiveMessageFromQueue(BasicDeliverEventArgs ea)
{
var body = ea.Body.ToArray();
var message = Encoding.UTF8.GetString(body);
StatusMessage? receivedStatusMessage = JsonSerializer.Deserialize<StatusMessage>(message);
lock (InstallationConnections)
var consumer = new EventingBasicConsumer(Channel);
consumer.Received += (_, ea) =>
{
// Process the received message
if (receivedStatusMessage != null)
var body = ea.Body.ToArray();
var message = Encoding.UTF8.GetString(body);
StatusMessage? receivedStatusMessage = JsonSerializer.Deserialize<StatusMessage>(message);
lock (InstallationConnections)
{
Console.WriteLine("Received a message from installation: " + receivedStatusMessage.InstallationId + " and status is: " + receivedStatusMessage.Status);
Console.WriteLine("----------------------------------------------");
Console.WriteLine("Update installation connection table");
var installationId = receivedStatusMessage.InstallationId;
if (!InstallationConnections.ContainsKey(installationId))
// Process the received message
if (receivedStatusMessage != null)
{
Console.WriteLine("Create new empty list for installation: " + installationId);
InstallationConnections[installationId] = new InstallationInfo
Console.WriteLine("Received a message from installation: " + receivedStatusMessage.InstallationId + " and status is: " + receivedStatusMessage.Status);
Console.WriteLine("----------------------------------------------");
Console.WriteLine("Update installation connection table");
var installationId = receivedStatusMessage.InstallationId;
if (!InstallationConnections.ContainsKey(installationId))
{
Status = receivedStatusMessage.Status
};
}
Console.WriteLine("----------------------------------------------");
foreach (var installationConnection in InstallationConnections)
{
if (installationConnection.Key == installationId && installationConnection.Value.Connections.Count > 0)
{
Console.WriteLine("Update all the connected websockets for installation " + installationId);
installationConnection.Value.Status = receivedStatusMessage.Status;
var jsonObject = new
Console.WriteLine("Create new empty list for installation: " + installationId);
InstallationConnections[installationId] = new InstallationInfo
{
id = installationId,
status = receivedStatusMessage.Status
Status = receivedStatusMessage.Status
};
}
else
{
InstallationConnections[installationId].Status = receivedStatusMessage.Status;
}
string jsonString = JsonSerializer.Serialize(jsonObject);
byte[] dataToSend = Encoding.UTF8.GetBytes(jsonString);
Console.WriteLine("----------------------------------------------");
foreach (var connection in installationConnection.Value.Connections)
foreach (var installationConnection in InstallationConnections)
{
if (installationConnection.Key == installationId && installationConnection.Value.Connections.Count > 0)
{
connection.SendAsync(
new ArraySegment<byte>(dataToSend, 0, dataToSend.Length),
WebSocketMessageType.Text,
true, // Indicates that this is the end of the message
CancellationToken.None
);
Console.WriteLine("Update all the connected websockets for installation " + installationId);
installationConnection.Value.Status = receivedStatusMessage.Status;
var jsonObject = new
{
id = installationId,
status = receivedStatusMessage.Status
};
string jsonString = JsonSerializer.Serialize(jsonObject);
byte[] dataToSend = Encoding.UTF8.GetBytes(jsonString);
foreach (var connection in installationConnection.Value.Connections)
{
connection.SendAsync(
new ArraySegment<byte>(dataToSend, 0, dataToSend.Length),
WebSocketMessageType.Text,
true, // Indicates that this is the end of the message
CancellationToken.None
);
}
}
}
}
}
}
};
Channel.BasicConsume(queue: "statusQueue", autoAck: true, consumer: consumer);
}
[UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "<Pending>")]
public static async Task HandleWebSocketConnection(WebSocket currentWebSocket)
{
var buffer = new byte[4096];
@ -149,13 +149,34 @@ public static class WebsocketManager
{
//Listen for incoming messages on this WebSocket
var result = await currentWebSocket.ReceiveAsync(buffer, CancellationToken.None);
Console.WriteLine("Received a new message from websocket");
if (result.MessageType != WebSocketMessageType.Text)
continue;
var message = Encoding.UTF8.GetString(buffer, 0, result.Count);
var installationIds = JsonSerializer.Deserialize<int[]>(message);
//This is a ping message to keep the connection alive, reply with a pong
if (installationIds[0] == -1)
{
var jsonObject = new
{
id = -1,
status = -1
};
var jsonString = JsonSerializer.Serialize(jsonObject);
var dataToSend = Encoding.UTF8.GetBytes(jsonString);
currentWebSocket.SendAsync(dataToSend,
WebSocketMessageType.Text,
true,
CancellationToken.None
);
continue;
}
Console.WriteLine("Received a new message from websocket");
lock (InstallationConnections)
{
//Each front-end will send the list of the installations it wants to access

View File

@ -1,11 +1,11 @@
Prototype ie-entwicklung@10.2.3.115
Salimax0001 ie-entwicklung@10.2.3.104
Salimax0002 ie-entwicklung@10.2.4.29
Salimax0003 ie-entwicklung@10.2.4.33
Salimax0004 ie-entwicklung@10.2.4.32
Prototype ie-entwicklung@10.2.3.115 Prototype
Salimax0001 ie-entwicklung@10.2.3.104 Marti Technik (Bern)
Salimax0002 ie-entwicklung@10.2.4.29 Weidmann Oberwil (ZG)
Salimax0003 ie-entwicklung@10.2.4.33 Elektrotechnik Stefan GmbH
Salimax0004 ie-entwicklung@10.2.4.32 Biohof Gubelmann (Walde)
Salimax0004A ie-entwicklung@10.2.4.153
Salimax0005 ie-entwicklung@10.2.4.36
Salimax0006 ie-entwicklung@10.2.4.35
Salimax0007 ie-entwicklung@10.2.4.154
Salimax0008 ie-entwicklung@10.2.4.113
Salimax0005 ie-entwicklung@10.2.4.36 Schreinerei Schönthal (Thun)
Salimax0006 ie-entwicklung@10.2.4.35 Steakhouse Mettmenstetten
Salimax0007 ie-entwicklung@10.2.4.154 LerchenhofHerr Twannberg
Salimax0008 ie-entwicklung@10.2.4.113 Wittmann Kottingbrunn

View File

@ -1,110 +0,0 @@
"MinSoc": Number, 0 - 100 this is the minimum State of Charge that the batteries must not go below,
"ForceCalibrationCharge": Boolean (true or false), A flag to force a calibration charge,
"DisplayIndividualBatteries": Boolean (true or false), To display the indvidual batteries
"PConstant": Number 0 - 1, P value of our controller.
"GridSetPoint": Number in Watts, The set point of our controller.
"BatterySelfDischargePower": Number, 200, this a physical measurement of the self discharging power.
"HoldSocZone": Number, 1, This is magic number for the soft landing factor.
"IslandMode": { // Dc Link Voltage in Island mode
"AcDc": {
"MaxDcLinkVoltage": Number, 810, Max Dc Link Voltage,
"MinDcLinkVoltage": Number, 690, Min Dc Link Voltage,
"ReferenceDcLinkVoltage": Number, 750, Reference Dc Link
},
"DcDc": {
"LowerDcLinkVoltage": Number, 50, Lower Dc Link Window ,
"ReferenceDcLinkVoltage": 750, reference Dc Link
"UpperDcLinkVoltage": Number, 50, Upper Dc Link Window ,
}
},
"GridTie": {// Dc Link Voltage in GrieTie mode
"AcDc": {
"MaxDcLinkVoltage":Number, 780, Max Dc Link Voltage,
"MinDcLinkVoltage": Number, 690, Min Dc Link Voltage,
"ReferenceDcLinkVoltage": Number, 750, Reference Dc Link
},
"DcDc": {
"LowerDcLinkVoltage": Number, 20, Lower Dc Link Window ,
"ReferenceDcLinkVoltage": 750, reference Dc Link
"UpperDcLinkVoltage": Number, 20, Upper Dc Link Window ,
}
},
"MaxBatteryChargingCurrent":Number, 0 - 210, Max Charging current by DcDc
"MaxBatteryDischargingCurrent":Number, 0 - 210, Max Discharging current by DcDc
"MaxDcPower": Number, 0 - 10000, Max Power exported/imported by DcDc (10000 is the maximum)
"MaxChargeBatteryVoltage": Number, 57, Max Charging battery Voltage
"MinDischargeBatteryVoltage": Number, 0, Min Charging Battery Voltage
"Devices": { This is All Salimax devices (including offline ones)
"RelaysIp": {
"DeviceState": 1, // 0: is not present, 1: Present and Can be mesured, 2: Present but must be computed/calculted
"Host": "10.0.1.1", // Ip @ of the device in the local network
"Port": 502 // port
},
"GridMeterIp": {
"DeviceState": 1,
"Host": "10.0.4.1",
"Port": 502
},
"PvOnAcGrid": {
"DeviceState": 0, // If a device is not present
"Host": "false", // this is not important
"Port": 0 // this is not important
},
"LoadOnAcGrid": {
"DeviceState": 2, // this is a computed device
"Host": "true",
"Port": 0
},
"PvOnAcIsland": {
"DeviceState": 0,
"Host": "false",
"Port": 0
},
"IslandBusLoadMeterIp": {
"DeviceState": 1,
"Host": "10.0.4.2",
"Port": 502
},
"TruConvertAcIp": {
"DeviceState": 1,
"Host": "10.0.2.1",
"Port": 502
},
"PvOnDc": {
"DeviceState": 1,
"Host": "10.0.5.1",
"Port": 502
},
"LoadOnDc": {
"DeviceState": 0,
"Host": "false",
"Port": 0
},
"TruConvertDcIp": {
"DeviceState": 1,
"Host": "10.0.3.1",
"Port": 502
},
"BatteryIp": {
"DeviceState": 1,
"Host": "localhost",
"Port": 6855
},
"BatteryNodes": [ // this is a list of battery nodes
2,
3,
4,
5,
6
]
},
"S3": { // this is parameters of S3 Buckets and co
"Bucket": "8-3e5b3069-214a-43ee-8d85-57d72000c19d",
"Region": "sos-ch-dk-2",
"Provider": "exo.io",
"Key": "EXO502627299197f83e8b090f63",
"Secret": "jUNYJL6B23WjndJnJlgJj4rc1i7uh981u5Aba5xdA5s",
"ContentType": "text/plain; charset=utf-8",
"Host": "8-3e5b3069-214a-43ee-8d85-57d72000c19d.sos-ch-dk-2.exo.io",
"Url": "https://8-3e5b3069-214a-43ee-8d85-57d72000c19d.sos-ch-dk-2.exo.io"
}

View File

@ -26,7 +26,8 @@ public record InstallationToHtmlInterface(
[Controller]
public class Controller : ControllerBase
{
//Todo change me for updates
//Todo automatically grab newest version?
private const String FirmwareVersion = "AF09";

View File

@ -17,8 +17,9 @@ import routes from 'src/Resources/routes.json';
import './App.css';
import ForgotPassword from './components/ForgotPassword';
import { axiosConfigWithoutToken } from './Resources/axiosConfig';
import UsersContextProvider from './contexts/UsersContextProvider';
import InstallationsContextProvider from './contexts/InstallationsContextProvider';
import AccessContextProvider from './contexts/AccessContextProvider';
import WebSocketContextProvider from './contexts/WebSocketContextProvider';
function App() {
const context = useContext(UserContext);
@ -148,20 +149,22 @@ function App() {
<Route
path="/"
element={
<SidebarLayout
language={language}
onSelectLanguage={setLanguage}
/>
<WebSocketContextProvider>
<SidebarLayout
language={language}
onSelectLanguage={setLanguage}
/>
</WebSocketContextProvider>
}
>
<Route
path={routes.installations + '*'}
element={
<UsersContextProvider>
<AccessContextProvider>
<InstallationsContextProvider>
<InstallationTabs />
</InstallationsContextProvider>
</UsersContextProvider>
</AccessContextProvider>
}
/>

View File

@ -1,13 +1,13 @@
import axios from 'axios';
export const axiosConfigWithoutToken = axios.create({
//baseURL: 'https://monitor.innov.energy/api'
baseURL: 'https://stage.innov.energy/api'
baseURL: 'https://monitor.innov.energy/api'
// baseURL: 'http://127.0.0.1:7087/api'
});
const axiosConfig = axios.create({
//baseURL: 'https://monitor.innov.energy/api'
baseURL: 'https://stage.innov.energy/api'
baseURL: 'https://monitor.innov.energy/api'
//baseURL: 'http://127.0.0.1:7087/api'
});
axiosConfig.defaults.params = {};

View File

@ -15,7 +15,7 @@ import {
import { I_Installation } from 'src/interfaces/InstallationTypes';
import Installation from './Installation';
import CancelIcon from '@mui/icons-material/Cancel';
import { LogContext } from 'src/contexts/LogContextProvider';
import { WebSocketContext } from 'src/contexts/WebSocketContextProvider';
import { FormattedMessage } from 'react-intl';
import { useNavigate } from 'react-router-dom';
@ -25,8 +25,8 @@ interface FlatInstallationViewProps {
const FlatInstallationView = (props: FlatInstallationViewProps) => {
const [isRowHovered, setHoveredRow] = useState(-1);
const logContext = useContext(LogContext);
const { getStatus } = logContext;
const webSocketContext = useContext(WebSocketContext);
const { getStatus } = webSocketContext;
const navigate = useNavigate();
const searchParams = new URLSearchParams(location.search);
const installationId = parseInt(searchParams.get('installation'));
@ -95,7 +95,6 @@ const FlatInstallationView = (props: FlatInstallationViewProps) => {
installation.id === selectedInstallation;
const status = getStatus(installation.id);
const rowStyles =
isRowHovered === installation.id
? {

View File

@ -29,12 +29,13 @@ import {
TopologyValues
} from 'src/content/dashboards/Log/graph.util';
import { Notification } from 'src/interfaces/S3Types';
import { LogContext } from 'src/contexts/LogContextProvider';
import { WebSocketContext } from 'src/contexts/WebSocketContextProvider';
import Topology from '../Topology/Topology';
import { FormattedMessage } from 'react-intl';
import Overview from '../Overview/overview';
import Configuration from '../Configuration/Configuration';
import { fetchData } from 'src/content/dashboards/Installations/fetchData';
import CancelIcon from '@mui/icons-material/Cancel';
interface singleInstallationProps {
current_installation?: I_Installation;
@ -64,16 +65,17 @@ function Installation(props: singleInstallationProps) {
const [warnings, setWarnings] = useState<Notification[]>([]);
const [errors, setErrors] = useState<Notification[]>([]);
const [errorLoadingS3Data, setErrorLoadingS3Data] = useState(false);
const logContext = useContext(LogContext);
const { installationStatus, handleLogWarningOrError, getStatus } = logContext;
const webSocketsContext = useContext(WebSocketContext);
const { getStatus } = webSocketsContext;
const searchParams = new URLSearchParams(location.search);
const installationId = parseInt(searchParams.get('installation'));
const currentTab = searchParams.get('tab');
const [values, setValues] = useState<TopologyValues | null>(null);
const [openModalDeleteInstallation, setOpenModalDeleteInstallation] =
useState(false);
const status = getStatus(props.current_installation.id);
if (formValues == undefined) {
return null;
}
@ -131,126 +133,67 @@ function Installation(props: singleInstallationProps) {
const s3Credentials = { s3Bucket, ...S3data };
useEffect(() => {
let isMounted = true;
setFormValues(props.current_installation);
setErrorLoadingS3Data(false);
let disconnectedStatusResult = [];
if (installationId == props.current_installation.id) {
let isMounted = true;
setFormValues(props.current_installation);
setErrorLoadingS3Data(false);
let disconnectedStatusResult = [];
const fetchDataPeriodically = async () => {
const now = UnixTime.now().earlier(TimeSpan.fromSeconds(20));
const date = now.toDate();
const fetchDataPeriodically = async () => {
const now = UnixTime.now().earlier(TimeSpan.fromSeconds(20));
const date = now.toDate();
try {
const res = await fetchData(now, s3Credentials);
try {
const res = await fetchData(now, s3Credentials);
if (!isMounted) {
return;
}
const newWarnings: Notification[] = [];
const newErrors: Notification[] = [];
if (res === FetchResult.notAvailable || res === FetchResult.tryLater) {
handleLogWarningOrError(props.current_installation.id, -1);
disconnectedStatusResult.unshift(-1);
disconnectedStatusResult = disconnectedStatusResult.slice(0, 5);
let i = 0;
//If at least one status value shows an error, then show error
for (i; i < disconnectedStatusResult.length; i++) {
if (disconnectedStatusResult[i] != -1) {
break;
}
if (!isMounted) {
return;
}
if (i === disconnectedStatusResult.length) {
setErrorLoadingS3Data(true);
}
} else {
setErrorLoadingS3Data(false);
setValues(
extractValues({
time: now,
value: res
})
);
const newWarnings: Notification[] = [];
const newErrors: Notification[] = [];
for (const key in res) {
if (
(res.hasOwnProperty(key) &&
key.includes('/Alarms') &&
res[key].value !== '') ||
(key.includes('/Warnings') && res[key].value !== '')
) {
if (key.includes('/Warnings')) {
newWarnings.push({
device: key.substring(1, key.lastIndexOf('/')),
description: res[key].value.toString(),
date:
date.getFullYear() +
'-' +
date.getMonth() +
'-' +
date.getDay(),
time:
date.getHours() +
':' +
date.getMinutes() +
':' +
date.getSeconds()
});
} else if (key.includes('/Alarms')) {
newErrors.push({
device: key.substring(1, key.lastIndexOf('/')),
description: res[key].value.toString(),
date:
date.getFullYear() +
'-' +
date.getMonth() +
'-' +
date.getDay(),
time:
date.getHours() +
':' +
date.getMinutes() +
':' +
date.getSeconds()
});
if (
res === FetchResult.notAvailable ||
res === FetchResult.tryLater
) {
disconnectedStatusResult.unshift(-1);
disconnectedStatusResult = disconnectedStatusResult.slice(0, 5);
let i = 0;
//If at least one status value shows an error, then show error
for (i; i < disconnectedStatusResult.length; i++) {
if (disconnectedStatusResult[i] != -1) {
break;
}
}
}
setWarnings(newWarnings);
setErrors(newErrors);
if (newErrors.length > 0) {
handleLogWarningOrError(props.current_installation.id, 2);
disconnectedStatusResult.unshift(2);
disconnectedStatusResult = disconnectedStatusResult.slice(0, 5);
} else if (newWarnings.length > 0) {
disconnectedStatusResult.unshift(1);
disconnectedStatusResult = disconnectedStatusResult.slice(0, 5);
handleLogWarningOrError(props.current_installation.id, 1);
if (i === disconnectedStatusResult.length) {
setErrorLoadingS3Data(true);
}
} else {
disconnectedStatusResult.unshift(0);
disconnectedStatusResult = disconnectedStatusResult.slice(0, 5);
handleLogWarningOrError(props.current_installation.id, 0);
setErrorLoadingS3Data(false);
setValues(
extractValues({
time: now,
value: res
})
);
}
} catch (err) {
setErrorLoadingS3Data(true);
}
} catch (err) {
setErrorLoadingS3Data(true);
}
};
};
const interval = setInterval(fetchDataPeriodically, 2000);
const interval = setInterval(fetchDataPeriodically, 2000);
// Cleanup function to cancel interval and update isMounted when unmounted
return () => {
isMounted = false;
clearInterval(interval);
};
}, []);
// Cleanup function to cancel interval and update isMounted when unmounted
return () => {
isMounted = false;
clearInterval(interval);
};
}
}, [installationId]);
if (installationId == props.current_installation.id) {
return (
@ -354,6 +297,69 @@ function Installation(props: singleInstallationProps) {
{props.current_installation.name}
</Typography>
</div>
<div style={{ display: 'flex', alignItems: 'center' }}>
<Typography
fontWeight="bold"
color="text.primary"
noWrap
sx={{
marginTop: '0px',
marginBottom: '10px',
fontSize: '14px'
}}
>
Status:
</Typography>
<div
style={{
display: 'flex',
alignItems: 'center',
marginLeft: '80px',
marginTop: '-10px'
}}
>
{status === -1 ? (
<CancelIcon
style={{
width: '23px',
height: '23px',
color: 'red',
borderRadius: '50%'
}}
/>
) : (
''
)}
{status === -2 ? (
<CircularProgress
size={20}
sx={{
color: '#f7b34d'
}}
/>
) : (
''
)}
<div
style={{
width: '20px',
height: '20px',
marginLeft: '2px',
borderRadius: '50%',
backgroundColor:
status === 2
? 'red'
: status === 1
? 'orange'
: status === -1 || status === -2
? 'transparent'
: 'green'
}}
/>
</div>
</div>
<Card variant="outlined">
<Grid
@ -482,6 +488,16 @@ function Installation(props: singleInstallationProps) {
{currentUser.hasWriteAccess && (
<>
<div>
<TextField
label="Vpn IP"
name="VpnIp"
value={formValues.vpnIp}
variant="outlined"
fullWidth
/>
</div>
<div>
<TextField
label="S3 Write Key"

View File

@ -8,7 +8,6 @@ import {
} from '@mui/material';
import SearchTwoToneIcon from '@mui/icons-material/SearchTwoTone';
import FlatInstallationView from 'src/content/dashboards/Installations/FlatInstallationView';
import LogContextProvider from '../../../contexts/LogContextProvider';
import { I_Installation } from '../../../interfaces/InstallationTypes';
interface installationSearchProps {
@ -59,9 +58,8 @@ function InstallationSearch(props: installationSearchProps) {
</FormControl>
</Grid>
</Grid>
<LogContextProvider>
<FlatInstallationView installations={filteredData} />
</LogContextProvider>
<FlatInstallationView installations={filteredData} />
</>
);
}

View File

@ -17,8 +17,8 @@ import InstallationSearch from './InstallationSearch';
import { FormattedMessage } from 'react-intl';
import { UserContext } from '../../../contexts/userContext';
import { InstallationsContext } from '../../../contexts/InstallationsContextProvider';
import LogContextProvider from '../../../contexts/LogContextProvider';
import Installation from './Installation';
import { WebSocketContext } from '../../../contexts/WebSocketContextProvider';
function InstallationTabs() {
const theme = useTheme();
@ -34,6 +34,15 @@ function InstallationTabs() {
const { installations, fetchAllInstallations } =
useContext(InstallationsContext);
const webSocketsContext = useContext(WebSocketContext);
const { socket, openSocket } = webSocketsContext;
useEffect(() => {
if (!socket && installations.length > 0) {
openSocket(installations);
}
}, [installations]);
useEffect(() => {
if (installations.length === 0) {
fetchAllInstallations();
@ -315,12 +324,10 @@ function InstallationTabs() {
element={
<Grid item xs={12}>
<Box p={4}>
<LogContextProvider>
<Installation
current_installation={installations[0]}
type="installation"
></Installation>
</LogContextProvider>
<Installation
current_installation={installations[0]}
type="installation"
></Installation>
</Box>
</Grid>
}

View File

@ -25,7 +25,6 @@ import PersonRemoveIcon from '@mui/icons-material/PersonRemove';
import PersonIcon from '@mui/icons-material/Person';
import Button from '@mui/material/Button';
import { Close as CloseIcon } from '@mui/icons-material';
import { UsersContext } from 'src/contexts/UsersContextProvider';
import { AccessContext } from 'src/contexts/AccessContextProvider';
import { FormattedMessage } from 'react-intl';
@ -43,7 +42,6 @@ function Access(props: AccessProps) {
const [isRowHovered, setHoveredRow] = useState(-1);
const [directButtonPressed, setDirectButtonPressed] = useState(false);
const [inheritedButtonPressed, setInheritedButtonPressed] = useState(false);
const { availableUsers, fetchAvailableUsers } = useContext(UsersContext);
const [selectedUsers, setSelectedUsers] = useState<string[]>([]);
const [openFolder, setOpenFolder] = useState(false);
const [openModal, setOpenModal] = useState(false);
@ -55,6 +53,8 @@ function Access(props: AccessProps) {
const accessContext = useContext(AccessContext);
const {
availableUsers,
fetchAvailableUsers,
usersWithDirectAccess,
fetchUsersWithDirectAccessForResource,
usersWithInheritedAccess,

View File

@ -125,7 +125,7 @@ function Overview(props: OverviewProps) {
const timestamp = item.time.ticks * 1000;
const adjustedTimestamp = new Date(timestamp);
adjustedTimestamp.setHours(adjustedTimestamp.getHours() + 2);
adjustedTimestamp.setHours(adjustedTimestamp.getHours() + 1);
if (csvContent[path]) {
const value = csvContent[path];

View File

@ -7,7 +7,7 @@ import InsertDriveFileIcon from '@mui/icons-material/InsertDriveFile';
import Typography from '@mui/material/Typography';
import { makeStyles } from '@mui/styles';
import CancelIcon from '@mui/icons-material/Cancel';
import { LogContext } from 'src/contexts/LogContextProvider';
import { WebSocketContext } from 'src/contexts/WebSocketContextProvider';
import routes from 'src/Resources/routes.json';
import { useNavigate } from 'react-router-dom';
@ -38,8 +38,8 @@ const useTreeItemStyles = makeStyles((theme) => ({
function CustomTreeItem(props: CustomTreeItemProps) {
const theme = useTheme();
const classes = useTreeItemStyles();
const logContext = useContext(LogContext);
const { getStatus } = logContext;
const webSocketContext = useContext(WebSocketContext);
const { getStatus } = webSocketContext;
const status = getStatus(props.node.id);
const navigate = useNavigate();
const [selected, setSelected] = useState(false);

View File

@ -8,7 +8,7 @@ import {
} from '@mui/material';
import SearchTwoToneIcon from '@mui/icons-material/SearchTwoTone';
import FlatUsersView from './FlatUsersView';
import { UsersContext } from '../../../contexts/UsersContextProvider';
import { AccessContext } from '../../../contexts/AccessContextProvider';
import Button from '@mui/material/Button';
import UserForm from './userForm';
import { UserContext } from '../../../contexts/userContext';
@ -17,7 +17,7 @@ import { FormattedMessage } from 'react-intl';
function UsersSearch() {
const theme = useTheme();
const [searchTerm, setSearchTerm] = useState('');
const { availableUsers, fetchAvailableUsers } = useContext(UsersContext);
const { availableUsers, fetchAvailableUsers } = useContext(AccessContext);
const [filteredData, setFilteredData] = useState(availableUsers);
const [openModal, setOpenModal] = useState(false);
const context = useContext(UserContext);

View File

@ -1,15 +1,15 @@
import Footer from 'src/components/Footer';
import { Box, Container, Grid, useTheme } from '@mui/material';
import UsersSearch from './UsersSearch';
import UsersContextProvider from 'src/contexts/UsersContextProvider';
import React from 'react';
import AccessContextProvider from '../../../contexts/AccessContextProvider';
function Users() {
const theme = useTheme();
return (
<>
<UsersContextProvider>
<AccessContextProvider>
<Container maxWidth="xl" sx={{ marginTop: '20px' }}>
<Grid item xs={12}>
<Box p={4}>
@ -18,7 +18,7 @@ function Users() {
</Grid>
</Container>
<Footer />
</UsersContextProvider>
</AccessContextProvider>
</>
);
}

View File

@ -14,6 +14,8 @@ import {
import { FormattedMessage } from 'react-intl';
interface AccessContextProviderProps {
availableUsers: InnovEnergyUser[];
fetchAvailableUsers: () => Promise<void>;
usersWithDirectAccess: InnovEnergyUser[];
fetchUsersWithDirectAccessForResource: (
tempresourceType: string,
@ -42,6 +44,10 @@ interface AccessContextProviderProps {
}
export const AccessContext = createContext<AccessContextProviderProps>({
availableUsers: [],
fetchAvailableUsers: () => {
return Promise.resolve();
},
usersWithDirectAccess: [],
fetchUsersWithDirectAccessForResource: () => Promise.resolve(),
usersWithInheritedAccess: [],
@ -67,6 +73,7 @@ const AccessContextProvider = ({ children }: { children: ReactNode }) => {
const [usersWithDirectAccess, setUsersWithDirectAccess] = useState<
InnovEnergyUser[]
>([]);
const [availableUsers, setAvailableUsers] = useState<InnovEnergyUser[]>([]);
const [usersWithInheritedAccess, setUsersWithInheritedAccess] = useState<
I_UserWithInheritedAccess[]
@ -120,6 +127,12 @@ const AccessContextProvider = ({ children }: { children: ReactNode }) => {
[]
);
const fetchAvailableUsers = async (): Promise<void> => {
return axiosConfig.get('/GetAllChildUsers').then((res) => {
setAvailableUsers(res.data);
});
};
const RevokeAccessFromResource = useCallback(
async (
resourceType: string,
@ -179,6 +192,8 @@ const AccessContextProvider = ({ children }: { children: ReactNode }) => {
return (
<AccessContext.Provider
value={{
availableUsers,
fetchAvailableUsers,
usersWithDirectAccess,
fetchUsersWithDirectAccessForResource,
usersWithInheritedAccess,

View File

@ -1,86 +0,0 @@
import { createContext, ReactNode, useState } from 'react';
interface LogContextProviderProps {
installationStatus: Record<number, number[]>;
handleLogWarningOrError: (installation_id: number, value: number) => void;
getStatus: (installationId: number) => number;
}
export const LogContext = createContext<LogContextProviderProps | undefined>(
undefined
);
export const LogContextProvider = ({ children }: { children: ReactNode }) => {
const [installationStatus, setInstallationStatus] = useState<
Record<number, number[]>
>({});
const BUFFER_LENGTH = 5;
const handleLogWarningOrError = (installation_id: number, value: number) => {
setInstallationStatus((prevStatus) => {
// Create a new object by spreading the previous state
const newStatus = { ...prevStatus };
if (!newStatus.hasOwnProperty(installation_id)) {
newStatus[installation_id] = [];
}
newStatus[installation_id].unshift(value);
newStatus[installation_id] = newStatus[installation_id].slice(
0,
BUFFER_LENGTH
);
return newStatus;
});
};
const getStatus = (installationId: number) => {
let status;
if (!installationStatus.hasOwnProperty(installationId)) {
status = -2;
} else {
let i = 0;
//If at least one status value shows an error, then show error
for (i; i < installationStatus[installationId].length; i++) {
if (installationStatus[installationId][i] === 2) {
status = 2;
return status;
}
if (installationStatus[installationId][i] === 1) {
status = 1;
return status;
}
}
if (installationStatus[installationId][0] == -1) {
let i = 0;
for (i; i < installationStatus[installationId].length; i++) {
if (installationStatus[installationId][i] != -1) {
break;
}
}
if (i === installationStatus[installationId].length) {
status = -1;
}
} else {
status = installationStatus[installationId][0];
}
}
return status;
};
return (
<LogContext.Provider
value={{
installationStatus,
handleLogWarningOrError,
getStatus
}}
>
{children}
</LogContext.Provider>
);
};
export default LogContextProvider;

View File

@ -1,114 +0,0 @@
import { createContext, ReactNode, useCallback, useState } from 'react';
import { useParams } from 'react-router-dom';
import axiosConfig from 'src/Resources/axiosConfig';
import {
I_UserWithInheritedAccess,
InnovEnergyUser
} from 'src/interfaces/UserTypes';
interface I_UsersContextProviderProps {
directAccessUsers: InnovEnergyUser[];
setDirectAccessUsers: (value: InnovEnergyUser[]) => void;
inheritedAccessUsers: I_UserWithInheritedAccess[];
setInheritedAccessUsers: (value: I_UserWithInheritedAccess[]) => void;
availableUsers: InnovEnergyUser[];
setAvailableUsers: (value: InnovEnergyUser[]) => void;
getAvailableUsersForResource: () => InnovEnergyUser[];
fetchUsersWithInheritedAccessForResource: () => Promise<void>;
fetchUsersWithDirectAccessForResource: () => Promise<void>;
fetchAvailableUsers: () => Promise<void>;
}
export const UsersContext = createContext<I_UsersContextProviderProps>({
directAccessUsers: [],
setDirectAccessUsers: () => {},
inheritedAccessUsers: [],
setInheritedAccessUsers: () => {},
availableUsers: [],
setAvailableUsers: () => {},
getAvailableUsersForResource: () => {
return [];
},
fetchUsersWithInheritedAccessForResource: () => {
return Promise.resolve();
},
fetchUsersWithDirectAccessForResource: () => {
return Promise.resolve();
},
fetchAvailableUsers: () => {
return Promise.resolve();
}
});
const UsersContextProvider = ({ children }: { children: ReactNode }) => {
const [directAccessUsers, setDirectAccessUsers] = useState<InnovEnergyUser[]>(
[]
);
const [inheritedAccessUsers, setInheritedAccessUsers] = useState<
I_UserWithInheritedAccess[]
>([]);
const [availableUsers, setAvailableUsers] = useState<InnovEnergyUser[]>([]);
const currentType = 13;
const { id } = useParams();
const fetchUsersWithAccess = useCallback(
async (route: string, setState: (value: any) => void) => {
axiosConfig.get(route + currentType, { params: { id } }).then((res) => {
setState(res.data);
});
},
[currentType, id]
);
const fetchUsersWithInheritedAccessForResource = useCallback(async () => {
fetchUsersWithAccess(
'/GetUsersWithInheritedAccessTo',
setInheritedAccessUsers
);
}, [fetchUsersWithAccess]);
const fetchUsersWithDirectAccessForResource = useCallback(async () => {
fetchUsersWithAccess('/GetUsersWithDirectAccessTo', setDirectAccessUsers);
}, [fetchUsersWithAccess]);
const getAvailableUsersForResource = () => {
const inheritedUsers = inheritedAccessUsers.map(
(inheritedAccessUser) => inheritedAccessUser.user
);
const allUsersWithAccess = [...inheritedUsers, ...directAccessUsers];
return availableUsers.filter(
(availableUser) =>
!allUsersWithAccess.find(
(userWithAccess) => availableUser.id === userWithAccess.id
)
);
};
const fetchAvailableUsers = async (): Promise<void> => {
return axiosConfig.get('/GetAllChildUsers').then((res) => {
setAvailableUsers(res.data);
});
};
return (
<UsersContext.Provider
value={{
directAccessUsers,
setDirectAccessUsers,
inheritedAccessUsers,
setInheritedAccessUsers,
availableUsers,
setAvailableUsers,
getAvailableUsersForResource,
fetchUsersWithInheritedAccessForResource,
fetchUsersWithDirectAccessForResource,
fetchAvailableUsers
}}
>
{children}
</UsersContext.Provider>
);
};
export default UsersContextProvider;

View File

@ -10,6 +10,7 @@ export interface I_Installation extends I_S3Credentials {
region: string;
country: string;
installationName: string;
vpnIp: string;
orderNumbers: string[] | string;
lat: number;
long: number;

View File

@ -18,6 +18,7 @@ import { FormattedMessage } from 'react-intl';
import { TokenContext } from 'src/contexts/tokenContext';
import { useNavigate } from 'react-router-dom';
import routes from 'src/Resources/routes.json';
import { WebSocketContext } from '../../../../contexts/WebSocketContextProvider';
const UserBoxButton = styled(Button)(
({ theme }) => `
@ -63,8 +64,11 @@ function HeaderUserbox() {
const tokencontext = useContext(TokenContext);
const { token, setNewToken, removeToken } = tokencontext;
const navigate = useNavigate();
const webSocketsContext = useContext(WebSocketContext);
const { closeSocket } = webSocketsContext;
const handleSubmit = () => {
closeSocket();
axiosConfig.post('/Logout').then(() => {
removeToken();
removeUser();