diff --git a/csharp/App/Backend/Backend.csproj b/csharp/App/Backend/Backend.csproj index bbbac1191..0e70bb5dd 100644 --- a/csharp/App/Backend/Backend.csproj +++ b/csharp/App/Backend/Backend.csproj @@ -23,6 +23,7 @@ + @@ -51,4 +52,11 @@ + + + + ..\..\..\..\..\..\.nuget\packages\rabbitmq.client\6.6.0\lib\netstandard2.0\RabbitMQ.Client.dll + + + diff --git a/csharp/App/Backend/Controller.cs b/csharp/App/Backend/Controller.cs index 524cad118..e76f2d71d 100644 --- a/csharp/App/Backend/Controller.cs +++ b/csharp/App/Backend/Controller.cs @@ -1,7 +1,12 @@ +using System.Net; +using System.Net.WebSockets; +using System.Text; +using System.Text.Json; using InnovEnergy.App.Backend.Database; using InnovEnergy.App.Backend.DataTypes; using InnovEnergy.App.Backend.DataTypes.Methods; using InnovEnergy.App.Backend.Relations; +using InnovEnergy.App.Backend.Websockets; using InnovEnergy.Lib.Utils; using Microsoft.AspNetCore.Mvc; @@ -9,10 +14,12 @@ namespace InnovEnergy.App.Backend; using Token = String; + [Controller] [Route("api/")] public class Controller : ControllerBase { + [HttpPost(nameof(Login))] public ActionResult Login(String username, String? password) { @@ -47,6 +54,125 @@ public class Controller : ControllerBase : Unauthorized(); } + [HttpGet(nameof(CreateWebSocket))] + public async Task CreateWebSocket(Token authToken) + { + var session = Db.GetSession(authToken)?.User; + + if (session is null) + { + HttpContext.Response.StatusCode = (int)HttpStatusCode.Unauthorized; + HttpContext.Abort(); + return; + } + + if (!HttpContext.WebSockets.IsWebSocketRequest) + { + HttpContext.Response.StatusCode = (int)HttpStatusCode.Unauthorized; + HttpContext.Abort(); + return; + } + + var webSocketContext = await HttpContext.WebSockets.AcceptWebSocketAsync(); + var webSocket = webSocketContext; + + //Handle the WebSocket connection + //WebsocketManager.HandleWebSocketConnection(webSocket) + + var buffer = new byte[4096]; + try + { + while (webSocket.State == WebSocketState.Open) + { + //Listen for incoming messages on this WebSocket + var result = await webSocket.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(message); + + lock (WebsocketManager.InstallationConnections) + { + //Each front-end will send the list of the installations it wants to access + //If this is a new key (installation id), initialize the list for this key and then add the websocket object for this client + //Then, report the status of each requested installation to the front-end that created the websocket connection + if (installationIds != null) + foreach (var installationId in installationIds) + { + if (!WebsocketManager.InstallationConnections.ContainsKey(installationId)) + { + Console.WriteLine("Create new empty list for installation id " + installationId); + WebsocketManager.InstallationConnections[installationId] = new InstallationInfo + { + Status = -2 + }; + } + + WebsocketManager.InstallationConnections[installationId].Connections.Add(webSocket); + + var jsonObject = new + { + id = installationId, + status = WebsocketManager.InstallationConnections[installationId].Status + }; + + var jsonString = JsonSerializer.Serialize(jsonObject); + var dataToSend = Encoding.UTF8.GetBytes(jsonString); + + + webSocket.SendAsync(dataToSend, + WebSocketMessageType.Text, + true, // Indicates that this is the end of the message + CancellationToken.None + ); + } + + Console.WriteLine("Printing installation connection list"); + Console.WriteLine("----------------------------------------------"); + foreach (var installationConnection in WebsocketManager.InstallationConnections) + { + Console.WriteLine("Installation ID: " + installationConnection.Key + " Number of Connections: " + installationConnection.Value.Connections.Count); + } + Console.WriteLine("----------------------------------------------"); + } + } + + lock (WebsocketManager.InstallationConnections) + { + //When the front-end terminates the connection, the following code will be executed + Console.WriteLine("The connection has been terminated"); + foreach (var installationConnection in WebsocketManager.InstallationConnections) + { + if (installationConnection.Value.Connections.Contains(webSocket)) + { + installationConnection.Value.Connections.Remove(webSocket); + } + } + } + + await webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Connection closed by server", CancellationToken.None); + lock (WebsocketManager.InstallationConnections) + { + //Print the installationConnections dictionary after deleting a websocket + Console.WriteLine("Print the installation connections list after deleting a websocket"); + Console.WriteLine("----------------------------------------------"); + foreach (var installationConnection in WebsocketManager.InstallationConnections) + { + Console.WriteLine("Installation ID: " + installationConnection.Key + " Number of Connections: " + installationConnection.Value.Connections.Count); + } + + Console.WriteLine("----------------------------------------------"); + } + } + catch (Exception ex) + { + Console.WriteLine("WebSocket error: " + ex.Message); + } + + + } [HttpGet(nameof(GetUserById))] public ActionResult GetUserById(Int64 id, Token authToken) diff --git a/csharp/App/Backend/DataTypes/Methods/ExoCmd.cs b/csharp/App/Backend/DataTypes/Methods/ExoCmd.cs index b28f64142..8f2836cd5 100644 --- a/csharp/App/Backend/DataTypes/Methods/ExoCmd.cs +++ b/csharp/App/Backend/DataTypes/Methods/ExoCmd.cs @@ -44,7 +44,7 @@ public static class ExoCmd messageToSign += time; - Console.WriteLine("Message to sign:\n" + messageToSign); + //Console.WriteLine("Message to sign:\n" + messageToSign); var hmac = HmacSha256Digest(messageToSign, Secret); @@ -108,7 +108,7 @@ public static class ExoCmd if (response.StatusCode != HttpStatusCode.OK){ Console.WriteLine("Fuck"); } - Console.WriteLine($"Created Key for {installation.Name}"); + //Console.WriteLine($"Created Key for {installation.Name}"); var responseString = await response.Content.ReadAsStringAsync(); var responseJson = JsonNode.Parse(responseString) ; @@ -155,7 +155,7 @@ public static class ExoCmd var response = await client.PostAsync(url, content); var responseString = await response.Content.ReadAsStringAsync(); - Console.WriteLine(responseString); + //Console.WriteLine(responseString); //Put Role ID into database var id = JsonNode.Parse(responseString)!["reference"]!["id"]!.GetValue(); @@ -232,7 +232,7 @@ public static class ExoCmd var response = await client.PostAsync(url, content); var responseString = await response.Content.ReadAsStringAsync(); - Console.WriteLine(responseString); + //Console.WriteLine(responseString); //Put Role ID into database var id = JsonNode.Parse(responseString)!["reference"]!["id"]!.GetValue(); diff --git a/csharp/App/Backend/Program.cs b/csharp/App/Backend/Program.cs index 455a9ba8b..09ebc3c0a 100644 --- a/csharp/App/Backend/Program.cs +++ b/csharp/App/Backend/Program.cs @@ -1,5 +1,6 @@ using Hellang.Middleware.ProblemDetails; using InnovEnergy.App.Backend.Database; +using InnovEnergy.App.Backend.Websockets; using Microsoft.AspNetCore.HttpOverrides; using Microsoft.AspNetCore.Mvc; using Microsoft.OpenApi.Models; @@ -14,6 +15,8 @@ public static class Program //Db.CreateFakeRelations(); Db.Init(); var builder = WebApplication.CreateBuilder(args); + + WebsocketManager.InformInstallationsToSubscribeToRabbitMq(); builder.Services.AddControllers(); builder.Services.AddProblemDetails(setup => @@ -46,8 +49,9 @@ public static class Program await next(context); }); + - + app.UseWebSockets(); app.UseForwardedHeaders(new ForwardedHeadersOptions { ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto diff --git a/csharp/App/Backend/Websockets/InstallationInfo.cs b/csharp/App/Backend/Websockets/InstallationInfo.cs new file mode 100644 index 000000000..770a6698c --- /dev/null +++ b/csharp/App/Backend/Websockets/InstallationInfo.cs @@ -0,0 +1,7 @@ +using System.Net.WebSockets; + +public class InstallationInfo +{ + public int Status { get; set; } + public List Connections { get; } = new List(); +} \ No newline at end of file diff --git a/csharp/App/Backend/Websockets/StatusMessage.cs b/csharp/App/Backend/Websockets/StatusMessage.cs new file mode 100644 index 000000000..074ac71b0 --- /dev/null +++ b/csharp/App/Backend/Websockets/StatusMessage.cs @@ -0,0 +1,6 @@ + +public class StatusMessage +{ + public required int InstallationId { get; init; } + public required int Status { get; init; } +} \ No newline at end of file diff --git a/csharp/App/Backend/Websockets/WebsockerManager.cs b/csharp/App/Backend/Websockets/WebsockerManager.cs new file mode 100644 index 000000000..37e5d4e85 --- /dev/null +++ b/csharp/App/Backend/Websockets/WebsockerManager.cs @@ -0,0 +1,238 @@ +using System.Diagnostics.CodeAnalysis; +using System.Net; +using System.Net.Sockets; +using System.Net.WebSockets; +using System.Text; +using System.Text.Json; +using RabbitMQ.Client; +using RabbitMQ.Client.Events; + +namespace InnovEnergy.App.Backend.Websockets; + +public static class WebsocketManager +{ + public static readonly Dictionary InstallationConnections = new Dictionary(); + private static ConnectionFactory _factory = null!; + private static IConnection _connection = null!; + private static IModel _channel = null!; + + public static void InformInstallationsToSubscribeToRabbitMq() + { + var installationsIds = new List { 1 }; + var installationIps = new List { "10.2.3.115" }; + 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) + { + //Try at most MAX_RETRANSMISSIONS times to reach an installation. + for (int j = 0; j < maxRetransmissions; j++) + { + string message = "This is a message from RabbitMQ server, you can subscribe to the RabbitMQ queue"; + byte[] data = Encoding.UTF8.GetBytes(message); + udpClient.Send(data, data.Length, installationIps[i], port); + + Console.WriteLine($"Sent UDP message to {installationIps[i]}:{port}: {message}"); + IPEndPoint remoteEndPoint = new IPEndPoint(IPAddress.Parse(installationIps[i]), port); + + try + { + byte[] replyData = udpClient.Receive(ref remoteEndPoint); + string replyMessage = Encoding.UTF8.GetString(replyData); + Console.WriteLine("Received "+replyMessage +" from installation " + installationsIds[i]); + break; + } + catch (SocketException ex) + { + if (ex.SocketErrorCode == SocketError.TimedOut) + { + Console.WriteLine("Timed out waiting for a response. Retry..."); + } + else + { + Console.WriteLine("Error: " + ex.Message); + } + } + } + } + } + } + + public static void 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 = "")] + private static void CallbackReceiveMessageFromQueue(BasicDeliverEventArgs ea) + { + var body = ea.Body.ToArray(); + var message = Encoding.UTF8.GetString(body); + StatusMessage? receivedStatusMessage = JsonSerializer.Deserialize(message); + + lock (InstallationConnections) + { + // Process the received message + if (receivedStatusMessage != null) + { + 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)) + { + Console.WriteLine("Create new empty list for installation: " + installationId); + InstallationConnections[installationId] = new InstallationInfo + { + 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 + { + 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(dataToSend, 0, dataToSend.Length), + WebSocketMessageType.Text, + true, // Indicates that this is the end of the message + CancellationToken.None + ); + } + } + } + } + } + } + + [UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "")] + public static async Task HandleWebSocketConnection(WebSocket currentWebSocket) + { + var buffer = new byte[4096]; + try + { + while (currentWebSocket.State == WebSocketState.Open) + { + //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(message); + + lock (InstallationConnections) + { + //Each front-end will send the list of the installations it wants to access + //If this is a new key (installation id), initialize the list for this key and then add the websocket object for this client + //Then, report the status of each requested installation to the front-end that created the websocket connection + if (installationIds != null) + foreach (var installationId in installationIds) + { + if (!InstallationConnections.ContainsKey(installationId)) + { + Console.WriteLine("Create new empty list for installation id " + installationId); + InstallationConnections[installationId] = new InstallationInfo + { + Status = -2 + }; + } + + InstallationConnections[installationId].Connections.Add(currentWebSocket); + + var jsonObject = new + { + id = installationId, + status = InstallationConnections[installationId].Status + }; + + var jsonString = JsonSerializer.Serialize(jsonObject); + var dataToSend = Encoding.UTF8.GetBytes(jsonString); + + + currentWebSocket.SendAsync(dataToSend, + WebSocketMessageType.Text, + true, // Indicates that this is the end of the message + CancellationToken.None + ); + } + + Console.WriteLine("Printing installation connection list"); + Console.WriteLine("----------------------------------------------"); + foreach (var installationConnection in InstallationConnections) + { + Console.WriteLine("Installation ID: " + installationConnection.Key + " Number of Connections: " + installationConnection.Value.Connections.Count); + } + Console.WriteLine("----------------------------------------------"); + } + } + + lock (InstallationConnections) + { + //When the front-end terminates the connection, the following code will be executed + Console.WriteLine("The connection has been terminated"); + foreach (var installationConnection in InstallationConnections) + { + if (installationConnection.Value.Connections.Contains(currentWebSocket)) + { + installationConnection.Value.Connections.Remove(currentWebSocket); + } + } + } + + await currentWebSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Connection closed by server", CancellationToken.None); + lock (InstallationConnections) + { + //Print the installationConnections dictionary after deleting a websocket + Console.WriteLine("Print the installation connections list after deleting a websocket"); + Console.WriteLine("----------------------------------------------"); + foreach (var installationConnection in InstallationConnections) + { + Console.WriteLine("Installation ID: " + installationConnection.Key + " Number of Connections: " + installationConnection.Value.Connections.Count); + } + + Console.WriteLine("----------------------------------------------"); + } + } + catch (Exception ex) + { + Console.WriteLine("WebSocket error: " + ex.Message); + } + } + +} \ No newline at end of file diff --git a/csharp/App/BmsTunnel/Program.cs b/csharp/App/BmsTunnel/Program.cs index eb23e57d8..b9968d63e 100644 --- a/csharp/App/BmsTunnel/Program.cs +++ b/csharp/App/BmsTunnel/Program.cs @@ -25,11 +25,11 @@ public static class Program ExplainNode(); ExplainExit(); - Console.WriteLine(""); + //Console.WriteLine(""); while (true) { - Console.WriteLine(""); + //Console.WriteLine(""); Console.Write($"node{tunnel.Node}> "); var cmd = Console.ReadLine()?.ToUpper().Trim(); diff --git a/csharp/App/Middleware/Program.cs b/csharp/App/Middleware/Program.cs index 65bf0efb9..7a5bd0ed4 100644 --- a/csharp/App/Middleware/Program.cs +++ b/csharp/App/Middleware/Program.cs @@ -4,6 +4,7 @@ using System.Net; using System.Net.Sockets; using System.Net.WebSockets; using System.Text; +using InnovEnergy.Lib.Utils; internal class Program { @@ -16,6 +17,7 @@ internal class Program var installationsIds = new List {1}; var installationIps = new List {"10.2.3.115"}; var MAX_RETRANSMISSIONS = 2; + RabbitMqConsumer.StartRabbitMqConsumer(installationConnections,SharedDataLock); UdpClient udpClient = new UdpClient(); @@ -58,8 +60,6 @@ internal class Program } } - Console.WriteLine("WebSocket server is running. Press Enter to exit."); - Console.WriteLine("WebSocket server is running. Press Enter to exit."); await WebSocketListener.StartServerAsync(installationConnections,SharedDataLock); } diff --git a/csharp/App/Middleware/RabbitMQConsumer.cs b/csharp/App/Middleware/RabbitMQConsumer.cs index 90967731e..688f202b9 100644 --- a/csharp/App/Middleware/RabbitMQConsumer.cs +++ b/csharp/App/Middleware/RabbitMQConsumer.cs @@ -15,72 +15,74 @@ public static class RabbitMqConsumer public static void StartRabbitMqConsumer(Dictionary installationConnections, Object sharedDataLock) { string vpnServerIp = "194.182.190.208"; - _factory = new ConnectionFactory { HostName = vpnServerIp }; + _factory = new ConnectionFactory { HostName = "localhost" }; _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 += (model, ea) => - { - var body = ea.Body.ToArray(); - var message = Encoding.UTF8.GetString(body); - StatusMessage? receivedStatusMessage = JsonSerializer.Deserialize(message); - - lock (sharedDataLock) - { - // Process the received message - if (receivedStatusMessage != null) - { - 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)) - { - Console.WriteLine("Create new empty list for installation: " + installationId); - installationConnections[installationId] = new InstallationInfo - { - 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 - { - 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(dataToSend, 0, dataToSend.Length), - WebSocketMessageType.Text, - true, // Indicates that this is the end of the message - CancellationToken.None - ); - } - } - } - } - } - }; + consumer.Received += (_, ea) => Callback(installationConnections, sharedDataLock, ea); _channel.BasicConsume(queue: "statusQueue", autoAck: true, consumer: consumer); } + + private static void Callback(Dictionary installationConnections, Object sharedDataLock, BasicDeliverEventArgs ea) + { + var body = ea.Body.ToArray(); + var message = Encoding.UTF8.GetString(body); + StatusMessage? receivedStatusMessage = JsonSerializer.Deserialize(message); + + lock (sharedDataLock) + { + // Process the received message + if (receivedStatusMessage != null) + { + 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)) + { + Console.WriteLine("Create new empty list for installation: " + installationId); + installationConnections[installationId] = new InstallationInfo + { + 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 + { + 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(dataToSend, 0, dataToSend.Length), + WebSocketMessageType.Text, + true, // Indicates that this is the end of the message + CancellationToken.None + ); + } + } + } + } + } + } } \ No newline at end of file diff --git a/typescript/frontend-marios2/src/Resources/axiosConfig.tsx b/typescript/frontend-marios2/src/Resources/axiosConfig.tsx index 84da2b64f..6069ad19f 100644 --- a/typescript/frontend-marios2/src/Resources/axiosConfig.tsx +++ b/typescript/frontend-marios2/src/Resources/axiosConfig.tsx @@ -1,11 +1,13 @@ import axios from 'axios'; export const axiosConfigWithoutToken = axios.create({ - baseURL: 'https://monitor.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://monitor.innov.energy/api' + baseURL: 'http://127.0.0.1:7087/api' }); axiosConfig.defaults.params = {}; diff --git a/typescript/frontend-marios2/src/content/dashboards/Tree/treeView.tsx b/typescript/frontend-marios2/src/content/dashboards/Tree/treeView.tsx index 1626d2530..82e8c737d 100644 --- a/typescript/frontend-marios2/src/content/dashboards/Tree/treeView.tsx +++ b/typescript/frontend-marios2/src/content/dashboards/Tree/treeView.tsx @@ -1,7 +1,6 @@ import { Box, Grid, useTheme } from '@mui/material'; import InstallationTree from './InstallationTree'; import InstallationsContextProvider from 'src/contexts/InstallationsContextProvider'; -import LogContextProvider from '../../../contexts/LogContextProvider'; function TreeView() { const theme = useTheme(); @@ -10,9 +9,7 @@ function TreeView() { - - - +