From 70456486227c78a942d946625a0e8331b615cd7b Mon Sep 17 00:00:00 2001 From: Noe Date: Tue, 14 Jan 2025 13:56:12 +0100 Subject: [PATCH 1/7] updated frontend-backend --- csharp/App/Backend/Controller.cs | 13 +++-- csharp/App/Backend/DataTypes/Installation.cs | 16 ++++++- .../App/Backend/DataTypes/Methods/ExoCmd.cs | 4 +- .../Backend/DataTypes/Methods/Installation.cs | 2 +- .../App/Backend/DataTypes/Methods/Session.cs | 12 ++--- csharp/App/Backend/Database/Delete.cs | 2 +- csharp/App/Backend/Program.cs | 5 +- csharp/App/Backend/Relations/Session.cs | 4 +- .../App/Backend/Websockets/RabbitMQManager.cs | 28 +++-------- .../Backend/Websockets/WebsockerManager.cs | 47 +++++++++---------- csharp/App/Backend/deploy.sh | 4 ++ .../Cerbo_Release/CerboReleaseFiles/rc.local | 1 - .../dbus-fzsonick-48tl/config.py | 6 +-- .../content/dashboards/History/History.tsx | 2 +- .../FlatInstallationView.tsx | 9 ++-- 15 files changed, 79 insertions(+), 76 deletions(-) diff --git a/csharp/App/Backend/Controller.cs b/csharp/App/Backend/Controller.cs index 7e3df592c..406223442 100644 --- a/csharp/App/Backend/Controller.cs +++ b/csharp/App/Backend/Controller.cs @@ -67,6 +67,7 @@ public class Controller : ControllerBase [HttpGet(nameof(CreateWebSocket))] public async Task CreateWebSocket(Token authToken) { + //Everytime a user logs in, this function is called var session = Db.GetSession(authToken)?.User; if (session is null) @@ -85,6 +86,8 @@ public class Controller : ControllerBase return; } + //Create a websocket and pass its descriptor to the HandleWebSocketConnection method. + //This descriptor is returned to the frontend on the background var webSocketContext = await HttpContext.WebSockets.AcceptWebSocketAsync(); var webSocket = webSocketContext; @@ -179,7 +182,7 @@ public class Controller : ControllerBase Int64 startTimestamp = Int64.Parse(start.ToString().Substring(0,5)); Int64 endTimestamp = Int64.Parse(end.ToString().Substring(0,5)); - if (installation.Product == 1) + if (installation.Product == (int)ProductType.Salidomo) { start = Int32.Parse(start.ToString().Substring(0, start.ToString().Length - 2)); @@ -190,7 +193,7 @@ public class Controller : ControllerBase while (startTimestamp <= endTimestamp) { - string bucketPath = installation.Product==0? "s3://"+installation.S3BucketId + "-3e5b3069-214a-43ee-8d85-57d72000c19d/"+startTimestamp : + string bucketPath = installation.Product==(int)ProductType.Salimax? "s3://"+installation.S3BucketId + "-3e5b3069-214a-43ee-8d85-57d72000c19d/"+startTimestamp : "s3://"+installation.S3BucketId + "-c0436b6a-d276-4cd8-9c44-1eae86cf5d0e/"+startTimestamp; Console.WriteLine("Fetching data for "+startTimestamp); @@ -447,7 +450,7 @@ public class Controller : ControllerBase return Unauthorized(); return user - .AccessibleInstallations(product:0) + .AccessibleInstallations(product:(int)ProductType.Salimax) .Select(i => i.FillOrderNumbers().HideParentIfUserHasNoAccessToParent(user).HideWriteKeyIfUserIsNotAdmin(user.UserType)) .ToList(); } @@ -461,7 +464,7 @@ public class Controller : ControllerBase return Unauthorized(); return user - .AccessibleInstallations(product:1) + .AccessibleInstallations(product:(int)ProductType.Salidomo) .ToList(); } @@ -634,7 +637,7 @@ public class Controller : ControllerBase if (!session.Update(installation)) return Unauthorized(); - if (installation.Product == 0) + if (installation.Product == (int)ProductType.Salimax) { return installation.FillOrderNumbers().HideParentIfUserHasNoAccessToParent(session!.User).HideWriteKeyIfUserIsNotAdmin(session.User.UserType); } diff --git a/csharp/App/Backend/DataTypes/Installation.cs b/csharp/App/Backend/DataTypes/Installation.cs index 1ca209c6e..f67c0c921 100644 --- a/csharp/App/Backend/DataTypes/Installation.cs +++ b/csharp/App/Backend/DataTypes/Installation.cs @@ -2,6 +2,20 @@ using SQLite; namespace InnovEnergy.App.Backend.DataTypes; +public enum ProductType +{ + Salimax = 0, + Salidomo = 1 +} + +public enum StatusType +{ + Offline = -1, + Green = 0, + Warning = 1, + Alarm = 2 +} + public class Installation : TreeNode { //Each installation has 2 roles, a read role and a write role. @@ -24,7 +38,7 @@ public class Installation : TreeNode public String WriteRoleId { get; set; } = ""; public Boolean TestingMode { get; set; } = false; public int Status { get; set; } = -1; - public int Product { get; set; } = 0; + public int Product { get; set; } = (int)ProductType.Salimax; public int Device { get; set; } = 0; [Ignore] diff --git a/csharp/App/Backend/DataTypes/Methods/ExoCmd.cs b/csharp/App/Backend/DataTypes/Methods/ExoCmd.cs index 86f22ed03..56d3e991e 100644 --- a/csharp/App/Backend/DataTypes/Methods/ExoCmd.cs +++ b/csharp/App/Backend/DataTypes/Methods/ExoCmd.cs @@ -143,7 +143,7 @@ public static class ExoCmd { const String url = "https://api-ch-dk-2.exoscale.com/v2/iam-role"; const String method = "iam-role"; - String rolename = installation.Product==0?Db.Installations.Count(f => f.Product == 0) + installation.Name:Db.Installations.Count(f => f.Product == 1) + installation.Name; + String rolename = installation.Product==(int)ProductType.Salimax?Db.Installations.Count(f => f.Product == (int)ProductType.Salimax) + installation.Name:Db.Installations.Count(f => f.Product == (int)ProductType.Salidomo) + installation.Name; var contentString = $$""" @@ -316,7 +316,7 @@ public static class ExoCmd { const String url = "https://api-ch-dk-2.exoscale.com/v2/iam-role"; const String method = "iam-role"; - String rolename = installation.Product==0?Db.Installations.Count(f => f.Product == 0) + installation.Name:Db.Installations.Count(f => f.Product == 1) + installation.Name; + String rolename = installation.Product==(int)ProductType.Salimax?Db.Installations.Count(f => f.Product == (int)ProductType.Salimax) + installation.Name:Db.Installations.Count(f => f.Product == (int)ProductType.Salidomo) + installation.Name; var contentString = $$""" { diff --git a/csharp/App/Backend/DataTypes/Methods/Installation.cs b/csharp/App/Backend/DataTypes/Methods/Installation.cs index be2e714ac..cc5ebda69 100644 --- a/csharp/App/Backend/DataTypes/Methods/Installation.cs +++ b/csharp/App/Backend/DataTypes/Methods/Installation.cs @@ -16,7 +16,7 @@ public static class InstallationMethods public static String BucketName(this Installation installation) { - if (installation.Product == 0) + if (installation.Product == (int)ProductType.Salimax) { return $"{installation.S3BucketId}-{BucketNameSalt}"; } diff --git a/csharp/App/Backend/DataTypes/Methods/Session.cs b/csharp/App/Backend/DataTypes/Methods/Session.cs index 37e239d92..3d044b728 100644 --- a/csharp/App/Backend/DataTypes/Methods/Session.cs +++ b/csharp/App/Backend/DataTypes/Methods/Session.cs @@ -72,7 +72,7 @@ public static class SessionMethods public static async Task RunScriptInBackground(this Session? session, String vpnIp, Int64 batteryNode,String version,Int64 product) { Console.WriteLine("-----------------------------------Start updating firmware-----------------------------------"); - string scriptPath = (product == 0) + string scriptPath = (product == (int)ProductType.Salimax) ? "/home/ubuntu/backend/uploadBatteryFw/update_firmware_Salimax.sh" : "/home/ubuntu/backend/uploadBatteryFw/update_firmware_Salidomo.sh"; @@ -95,7 +95,7 @@ public static class SessionMethods public static async Task RunDownloadLogScript(this Session? session, String vpnIp, Int64 batteryNode,Int64 product) { Console.WriteLine("-----------------------------------Start downloading battery log-----------------------------------"); - string scriptPath = (product == 0) + string scriptPath = (product == (int)ProductType.Salimax) ? "/home/ubuntu/backend/downloadBatteryLog/download_bms_log_Salimax.sh" : "/home/ubuntu/backend/downloadBatteryLog/download_bms_log_Salidomo.sh"; @@ -210,7 +210,7 @@ public static class SessionMethods //Salimax installation - if (installation.Product == 0) + if (installation.Product == (int)ProductType.Salimax) { return user is not null && user.UserType != 0 @@ -223,7 +223,7 @@ public static class SessionMethods } - if (installation.Product == 1) + if (installation.Product == (int)ProductType.Salidomo) { return user is not null && user.UserType != 0 @@ -242,7 +242,7 @@ public static class SessionMethods var original = Db.GetInstallationById(installation?.Id); //Salimax installation - if (installation.Product == 0) + if (installation.Product == (int)ProductType.Salimax) { return user is not null @@ -256,7 +256,7 @@ public static class SessionMethods .Apply(Db.Update); } - if (installation.Product==1) + if (installation.Product==(int)ProductType.Salidomo) { return user is not null && installation is not null diff --git a/csharp/App/Backend/Database/Delete.cs b/csharp/App/Backend/Database/Delete.cs index 6c0497ed8..a4dd5a748 100644 --- a/csharp/App/Backend/Database/Delete.cs +++ b/csharp/App/Backend/Database/Delete.cs @@ -100,7 +100,7 @@ public static partial class Db Boolean DeleteInstallationAndItsDependencies() { InstallationAccess.Delete(i => i.InstallationId == installation.Id); - if (installation.Product == 0) + if (installation.Product == (int)ProductType.Salimax) { //For Salimax, delete the OrderNumber2Installation entries associated with this installation id. OrderNumber2Installation.Delete(i => i.InstallationId == installation.Id); diff --git a/csharp/App/Backend/Program.cs b/csharp/App/Backend/Program.cs index 797e61cfd..1d6b618dd 100644 --- a/csharp/App/Backend/Program.cs +++ b/csharp/App/Backend/Program.cs @@ -25,11 +25,10 @@ public static class Program Db.Init(); var builder = WebApplication.CreateBuilder(args); - RabbitMqManager.InitializeEnvironment(); - RabbitMqManager.StartRabbitMqConsumer().SupressAwaitWarning(); + //RabbitMqManager.InitializeEnvironment(); + //RabbitMqManager.StartRabbitMqConsumer().SupressAwaitWarning(); WebsocketManager.MonitorSalimaxInstallationTable().SupressAwaitWarning(); WebsocketManager.MonitorSalidomoInstallationTable().SupressAwaitWarning(); - builder.Services.AddControllers(); builder.Services.AddProblemDetails(setup => diff --git a/csharp/App/Backend/Relations/Session.cs b/csharp/App/Backend/Relations/Session.cs index d4535d5d5..9f7b037ea 100644 --- a/csharp/App/Backend/Relations/Session.cs +++ b/csharp/App/Backend/Relations/Session.cs @@ -43,8 +43,8 @@ public class Session : Relation Token = CreateToken(); UserId = user.Id; LastSeen = DateTime.Now; - AccessToSalimax = user.AccessibleInstallations(product: 0).ToList().Count > 0; - AccessToSalidomo = user.AccessibleInstallations(product: 1).ToList().Count > 0; + AccessToSalimax = user.AccessibleInstallations(product: (int)ProductType.Salimax).ToList().Count > 0; + AccessToSalidomo = user.AccessibleInstallations(product: (int)ProductType.Salidomo).ToList().Count > 0; } private static String CreateToken() diff --git a/csharp/App/Backend/Websockets/RabbitMQManager.cs b/csharp/App/Backend/Websockets/RabbitMQManager.cs index f31726c93..7dd73116c 100644 --- a/csharp/App/Backend/Websockets/RabbitMQManager.cs +++ b/csharp/App/Backend/Websockets/RabbitMQManager.cs @@ -10,16 +10,13 @@ namespace InnovEnergy.App.Backend.Websockets; public static class RabbitMqManager { - public static ConnectionFactory Factory = null!; public static IConnection Connection = null!; public static IModel Channel = null!; - + //This function will be called from the Backend/Program.cs public static void InitializeEnvironment() { - - //string vpnServerIp = "194.182.190.208"; string vpnServerIp = "10.2.0.11"; //Subscribe to RabbitMq queue as a consumer @@ -57,24 +54,19 @@ public static class RabbitMqManager { Installation installation = Db.Installations.FirstOrDefault(f => f.Product == receivedStatusMessage.Product && f.S3BucketId == receivedStatusMessage.InstallationId); int installationId = (int)installation.Id; - //Console.WriteLine("received a message from rabbitmq\n"); //This is a heartbit message, just update the timestamp for this installation. //There is no need to notify the corresponding front-ends. - //Every 15 iterations(30 seconds), the installation sends a heartbit message to the queue + //Every 15 iterations(30 seconds), the installation sends a heartbit message to the queue. if (receivedStatusMessage.Type == MessageType.Heartbit) { - // if (installation.Product == 1 && installation.Device == 2) - // { - // Console.WriteLine("This is a heartbit message from installation: " + installationId + " Name of the file is " + receivedStatusMessage.Timestamp); - // } + //Do not do anything here, just for debugging purposes. } else { //Traverse the Warnings list, and store each of them to the database if (receivedStatusMessage.Warnings != null) { - foreach (var warning in receivedStatusMessage.Warnings) { Warning newWarning = new Warning @@ -98,7 +90,7 @@ public static class RabbitMqManager { string monitorLink; - if (installation.Product == 0) + if (installation.Product == (int)ProductType.Salimax) { monitorLink = $"https://monitor.innov.energy/installations/list/installation/{installation.S3BucketId}/batteryview"; @@ -140,7 +132,7 @@ public static class RabbitMqManager $"Error created date and time: {alarm.Date} {alarm.Time}\n"+ $"\n"+ $"Thank you for your great support:)"; - // Disable this function now + //Disable this function now //Mailer.Send("InnovEnergy Support Team", recipient, subject, text); } //Create a new error and add it to the database @@ -149,9 +141,9 @@ public static class RabbitMqManager } } - var prevStatus = 0; + Int32 prevStatus; - //This installation id does not exist in our data structure, add it. + //This installation id does not exist in our in-memory data structure, add it. if (!WebsocketManager.InstallationConnections.ContainsKey(installationId)) { prevStatus = -2; @@ -168,11 +160,6 @@ public static class RabbitMqManager prevStatus = WebsocketManager.InstallationConnections[installationId].Status; WebsocketManager.InstallationConnections[installationId].Status = receivedStatusMessage.Status; WebsocketManager.InstallationConnections[installationId].Timestamp = DateTime.Now; - // if (installationId == 130) - // { - // Console.WriteLine("prevStatus " + prevStatus + " , new status is: " + receivedStatusMessage.Status + " and status is: " + receivedStatusMessage.Status); - // } - } installation.Status = receivedStatusMessage.Status; @@ -189,5 +176,4 @@ public static class RabbitMqManager }; Channel.BasicConsume(queue: "statusQueue", autoAck: true, consumer: consumer); } - } \ No newline at end of file diff --git a/csharp/App/Backend/Websockets/WebsockerManager.cs b/csharp/App/Backend/Websockets/WebsockerManager.cs index 9a16d1c11..4034a8696 100644 --- a/csharp/App/Backend/Websockets/WebsockerManager.cs +++ b/csharp/App/Backend/Websockets/WebsockerManager.cs @@ -13,8 +13,8 @@ public static class WebsocketManager { public static Dictionary InstallationConnections = new Dictionary(); - //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. + //Every 1 minute, 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 Salimax installation unavailable. public static async Task MonitorSalimaxInstallationTable() { while (true){ @@ -22,15 +22,15 @@ public static class WebsocketManager Console.WriteLine("MONITOR SALIMAX INSTALLATIONS\n"); foreach (var installationConnection in InstallationConnections){ - if (installationConnection.Value.Product==0 && (DateTime.Now - installationConnection.Value.Timestamp) > TimeSpan.FromMinutes(2)){ + if (installationConnection.Value.Product==(int)ProductType.Salimax && (DateTime.Now - installationConnection.Value.Timestamp) > TimeSpan.FromMinutes(2)){ - Console.WriteLine("Installation ID is "+installationConnection.Key); - Console.WriteLine("installationConnection.Value.Timestamp is "+installationConnection.Value.Timestamp); - Console.WriteLine("diff is "+(DateTime.Now-installationConnection.Value.Timestamp)); + // Console.WriteLine("Installation ID is "+installationConnection.Key); + // Console.WriteLine("installationConnection.Value.Timestamp is "+installationConnection.Value.Timestamp); + // Console.WriteLine("diff is "+(DateTime.Now-installationConnection.Value.Timestamp)); - installationConnection.Value.Status = -1; - Installation installation = Db.Installations.FirstOrDefault(f => f.Product == 0 && f.Id == installationConnection.Key); - installation.Status = -1; + installationConnection.Value.Status = (int)StatusType.Offline; + Installation installation = Db.Installations.FirstOrDefault(f => f.Product == (int)ProductType.Salimax && f.Id == installationConnection.Key); + installation.Status = (int)StatusType.Offline; installation.Apply(Db.Update); if (installationConnection.Value.Connections.Count > 0){InformWebsocketsForInstallation(installationConnection.Key);} } @@ -42,6 +42,8 @@ public static class WebsocketManager } } + //Every 1 minute, check the timestamp of the latest received message for every installation. + //If the difference between the two timestamps is more than 1 hour, we consider this Salidomo installation unavailable. public static async Task MonitorSalidomoInstallationTable() { while (true){ @@ -50,26 +52,17 @@ public static class WebsocketManager Console.WriteLine("MONITOR SALIDOMO INSTALLATIONS\n"); foreach (var installationConnection in InstallationConnections){ Console.WriteLine("Installation ID is "+installationConnection.Key); - - - // if (installationConnection.Key == 104) - // { - // Console.WriteLine("installationConnection.Value.Timestamp is "+installationConnection.Value.Timestamp); - // Console.WriteLine("diff is "+(DateTime.Now-installationConnection.Value.Timestamp)); - // - // - // } - if (installationConnection.Value.Product==1 && (DateTime.Now - installationConnection.Value.Timestamp) > TimeSpan.FromMinutes(30)) + if (installationConnection.Value.Product==(int)ProductType.Salidomo && (DateTime.Now - installationConnection.Value.Timestamp) > TimeSpan.FromMinutes(60)) { // Console.WriteLine("Installation ID is "+installationConnection.Key); // Console.WriteLine("installationConnection.Value.Timestamp is "+installationConnection.Value.Timestamp); // Console.WriteLine("diff is "+(DateTime.Now-installationConnection.Value.Timestamp)); - Installation installation = Db.Installations.FirstOrDefault(f => f.Product == 1 && f.Id == installationConnection.Key); - installation.Status = -1; + Installation installation = Db.Installations.FirstOrDefault(f => f.Product == (int)ProductType.Salidomo && f.Id == installationConnection.Key); + installation.Status = (int)StatusType.Offline; installation.Apply(Db.Update); - installationConnection.Value.Status = -1; + installationConnection.Value.Status = (int)StatusType.Offline; if (installationConnection.Value.Connections.Count > 0){InformWebsocketsForInstallation(installationConnection.Key);} } } @@ -143,7 +136,8 @@ public static class WebsocketManager continue; } - //Console.WriteLine("Received a new message from websocket"); + //Received a new message from this websocket. + //We have a HandleWebSocketConnection per connected frontend lock (InstallationConnections) { List dataToSend = new List(); @@ -153,14 +147,17 @@ public static class WebsocketManager //Then, report the status of each requested installation to the front-end that created the websocket connection foreach (var installationId in installationIds) { - //Console.WriteLine("New id is "+installationId); var installation = Db.GetInstallationById(installationId); if (!InstallationConnections.ContainsKey(installationId)) { - //Console.WriteLine("Create new empty list for installation id " + installationId); + //Since we keep all the changes to the database, in case that the backend reboots, we need to update the in-memory data structure. + //Thus, if the status is -1, we put an old timestamp, otherwise, we put the most recent timestamp. + //We store everything to the database, because when the backend reboots, we do not want to wait until all the installations send the heartbit messages. + //We want the in memory data structure to be up to date immediately. InstallationConnections[installationId] = new InstallationInfo { Status = installation.Status, + Timestamp = installation.Status==(int)StatusType.Offline ? DateTime.Now.AddDays(-1) : DateTime.Now, Product = installation.Product }; } diff --git a/csharp/App/Backend/deploy.sh b/csharp/App/Backend/deploy.sh index 8d32235e7..ff3675a7d 100755 --- a/csharp/App/Backend/deploy.sh +++ b/csharp/App/Backend/deploy.sh @@ -1 +1,5 @@ +#To deploy to the monitor server, uncomment the following line dotnet publish Backend.csproj -c Release -r linux-x64 --self-contained true -p:PublishTrimmed=false && rsync -av bin/Release/net6.0/linux-x64/publish/ ubuntu@194.182.190.208:~/backend && ssh ubuntu@194.182.190.208 'sudo systemctl restart backend' + +#To deploy to the stage server, uncomment the following line +#dotnet publish Backend.csproj -c Release -r linux-x64 --self-contained true -p:PublishTrimmed=false && rsync -av bin/Release/net6.0/linux-x64/publish/ ubuntu@91.92.154.141:~/backend && ssh ubuntu@91.92.154.141 'sudo systemctl restart backend' diff --git a/firmware/Cerbo_Release/CerboReleaseFiles/rc.local b/firmware/Cerbo_Release/CerboReleaseFiles/rc.local index 6e70344de..374afcaf8 100755 --- a/firmware/Cerbo_Release/CerboReleaseFiles/rc.local +++ b/firmware/Cerbo_Release/CerboReleaseFiles/rc.local @@ -66,5 +66,4 @@ ln -s "$vpn_service_path" "$vpn_symlink_path" # EmuMeter_symlink_path="/service/EmuMeter" # ln -s "$EmuMeter_service_path" "$EmuMeter_symlink_path" - exit 0 diff --git a/firmware/Venus_Release/VenusReleaseFiles/dbus-fzsonick-48tl/config.py b/firmware/Venus_Release/VenusReleaseFiles/dbus-fzsonick-48tl/config.py index a9666d22f..595ed1bcb 100755 --- a/firmware/Venus_Release/VenusReleaseFiles/dbus-fzsonick-48tl/config.py +++ b/firmware/Venus_Release/VenusReleaseFiles/dbus-fzsonick-48tl/config.py @@ -54,6 +54,6 @@ INNOVENERGY_PROTOCOL_VERSION = '48TL200V3' # S3 Credentials -S3BUCKET = "627-c0436b6a-d276-4cd8-9c44-1eae86cf5d0e" -S3KEY = "EXOb7bcf7d1e53f2d46923144de" -S3SECRET = "-uUmMuAfx40LpTKTZgdbXswTw09o_qmE4gzkmQS8PTk" +S3BUCKET = "140-c0436b6a-d276-4cd8-9c44-1eae86cf5d0e" +S3KEY = "EXOa947c7fc5990a7a6f6c40860" +S3SECRET = "J1yOTLbYEO6cMxQ2wgIwe__ru9-_RH5BBtKzx_2JJHk" diff --git a/typescript/frontend-marios2/src/content/dashboards/History/History.tsx b/typescript/frontend-marios2/src/content/dashboards/History/History.tsx index 40b4108ea..e2a953e92 100644 --- a/typescript/frontend-marios2/src/content/dashboards/History/History.tsx +++ b/typescript/frontend-marios2/src/content/dashboards/History/History.tsx @@ -533,7 +533,7 @@ function HistoryOfActions(props: HistoryProps) { HandleDelete(action)} - disabled={action.userName != currentUser.name} + // disabled={action.userName != currentUser.name} > diff --git a/typescript/frontend-marios2/src/content/dashboards/SalidomoInstallations/FlatInstallationView.tsx b/typescript/frontend-marios2/src/content/dashboards/SalidomoInstallations/FlatInstallationView.tsx index e8dfcc1f3..84ac01509 100644 --- a/typescript/frontend-marios2/src/content/dashboards/SalidomoInstallations/FlatInstallationView.tsx +++ b/typescript/frontend-marios2/src/content/dashboards/SalidomoInstallations/FlatInstallationView.tsx @@ -130,10 +130,11 @@ const FlatInstallationView = (props: FlatInstallationViewProps) => { {sortedInstallations - .filter( - (installation) => - installation.status === -1 && installation.device === 1 - ) + // .filter( + // (installation) => + // installation.status === -1 && + // installation.testingMode == false + // ) .map((installation) => { const isInstallationSelected = installation.s3BucketId === selectedInstallation; From eaf33646936563d7dc043197fc38ce9d0aa20efb Mon Sep 17 00:00:00 2001 From: Yinyin Liu Date: Fri, 17 Jan 2025 14:59:47 +0100 Subject: [PATCH 2/7] added Sodiohome under the side bar of monitor --- typescript/frontend-marios2/src/App.tsx | 17 ++++- .../src/Resources/routes.json | 1 + .../frontend-marios2/src/components/login.tsx | 7 +- .../contexts/InstallationsContextProvider.tsx | 68 +++++++++++++++++-- .../src/contexts/ProductIdContextProvider.tsx | 29 +++++++- .../Sidebar/SidebarMenu/index.tsx | 20 +++++- 6 files changed, 127 insertions(+), 15 deletions(-) diff --git a/typescript/frontend-marios2/src/App.tsx b/typescript/frontend-marios2/src/App.tsx index 2233a4448..5a8d30096 100644 --- a/typescript/frontend-marios2/src/App.tsx +++ b/typescript/frontend-marios2/src/App.tsx @@ -19,6 +19,7 @@ import { axiosConfigWithoutToken } from './Resources/axiosConfig'; import InstallationsContextProvider from './contexts/InstallationsContextProvider'; import AccessContextProvider from './contexts/AccessContextProvider'; import SalidomoInstallationTabs from './content/dashboards/SalidomoInstallations'; +import SodiohomeInstallationTabs from './content/dashboards/Installations/index'; import { ProductIdContext } from './contexts/ProductIdContextProvider'; function App() { @@ -29,7 +30,7 @@ function App() { const navigate = useNavigate(); const searchParams = new URLSearchParams(location.search); const username = searchParams.get('username'); - const { setAccessToSalimax, setAccessToSalidomo } = + const { setAccessToSalimax, setAccessToSalidomo,setAccessToSodiohome } = useContext(ProductIdContext); const [language, setLanguage] = useState('en'); @@ -71,10 +72,13 @@ function App() { setUser(response.data.user); setAccessToSalimax(response.data.accessToSalimax); setAccessToSalidomo(response.data.accessToSalidomo); + setAccessToSodiohome(response.data.accessToSodiohome); if (response.data.accessToSalimax) { navigate(routes.installations); - } else { + } else if(response.data.accessToSalidomo){ navigate(routes.salidomo_installations); + } else{ + navigate(routes.sodiohome_installations); } } }) @@ -162,6 +166,15 @@ function App() { } /> + + + + } + /> + } /> ([]); + const [sodiohomeInstallations, setSodiohomeInstallations] = useState< + I_Installation[] + >([]); const [foldersAndInstallations, setFoldersAndInstallations] = useState([]); const [loading, setLoading] = useState(false); const [error, setError] = useState(false); @@ -75,12 +78,24 @@ const InstallationsContextProvider = ({ : installation; }); + const updatedSodiohome = sodiohomeInstallations.map((installation) => { + const update = pendingUpdates.current[installation.id]; + return update + ? { + ...installation, + status: update.status, + testingMode: update.testingMode + } + : installation; + }); + setSalidomoInstallations(updatedSalidomo); setSalimaxInstallations(updatedSalimax); + setSodiohomeInstallations(updatedSodiohome); // Clear the pending updates after applying pendingUpdates.current = {}; - }, [salidomoInstallations, salimaxInstallations]); + }, [salidomoInstallations, salimaxInstallations,sodiohomeInstallations]); useEffect(() => { const timer = setInterval(() => { @@ -98,17 +113,33 @@ const InstallationsContextProvider = ({ const socket = new WebSocket(urlWithToken); - // Connection opened socket.addEventListener('open', () => { + let installationsToSend = []; + + if (product === 0) { + installationsToSend = salimaxInstallations; + } else if (product === 1) { + installationsToSend = salidomoInstallations; + } else if (product === 2) { + installationsToSend = sodiohomeInstallations; + } + + // Send the corresponding installation IDs socket.send( - JSON.stringify( - product === 1 - ? salidomoInstallations.map((installation) => installation.id) - : salimaxInstallations.map((installation) => installation.id) - ) + JSON.stringify(installationsToSend.map((installation) => installation.id)) ); }); + // socket.addEventListener('open', () => { + // socket.send( + // JSON.stringify( + // product === 1 + // ? salidomoInstallations.map((installation) => installation.id) + // : salimaxInstallations.map((installation) => installation.id) + // ) + // ); + // }); + // Periodically send ping messages to keep the connection alive const pingInterval = setInterval(() => { if (socket.readyState === WebSocket.OPEN) { @@ -166,6 +197,20 @@ const InstallationsContextProvider = ({ }); }, [navigate, removeToken]); + const fetchAllSodiohomeInstallations = useCallback(async () => { + axiosConfig + .get('/GetAllSodiohomeInstallations') + .then((res: AxiosResponse) => + setSodiohomeInstallations(res.data) + ) + .catch((err: AxiosError) => { + if (err.response?.status === 401) { + removeToken(); + navigate(routes.login); + } + }); + }, [navigate, removeToken]); + const fetchAllFoldersAndInstallations = useCallback( async (product: number) => { axiosConfig @@ -207,6 +252,8 @@ const InstallationsContextProvider = ({ fetchAllInstallations(); else if (formValues.product === 1 && view === 'installation') fetchAllSalidomoInstallations(); + else if (formValues.product === 2 && view === 'installation') + fetchAllSodiohomeInstallations(); else fetchAllFoldersAndInstallations(formValues.product); setTimeout(() => setUpdated(false), 3000); }) @@ -222,6 +269,7 @@ const InstallationsContextProvider = ({ fetchAllFoldersAndInstallations, fetchAllInstallations, fetchAllSalidomoInstallations, + fetchAllSodiohomeInstallations, navigate, removeToken ] @@ -237,6 +285,8 @@ const InstallationsContextProvider = ({ fetchAllInstallations(); else if (formValues.product === 1 && view === 'installation') fetchAllSalidomoInstallations(); + else if (formValues.product === 2 && view === 'installation') + fetchAllSodiohomeInstallations(); else fetchAllFoldersAndInstallations(formValues.product); setTimeout(() => setUpdated(false), 3000); }) @@ -252,6 +302,7 @@ const InstallationsContextProvider = ({ fetchAllFoldersAndInstallations, fetchAllInstallations, fetchAllSalidomoInstallations, + fetchAllSodiohomeInstallations, navigate, removeToken ] @@ -317,9 +368,11 @@ const InstallationsContextProvider = ({ () => ({ salimaxInstallations, salidomoInstallations, + sodiohomeInstallations, foldersAndInstallations, fetchAllInstallations, fetchAllSalidomoInstallations, + fetchAllSodiohomeInstallations, fetchAllFoldersAndInstallations, createInstallation, updateInstallation, @@ -341,6 +394,7 @@ const InstallationsContextProvider = ({ [ salimaxInstallations, salidomoInstallations, + sodiohomeInstallations, foldersAndInstallations, loading, error, diff --git a/typescript/frontend-marios2/src/contexts/ProductIdContextProvider.tsx b/typescript/frontend-marios2/src/contexts/ProductIdContextProvider.tsx index be16df74a..69727358e 100644 --- a/typescript/frontend-marios2/src/contexts/ProductIdContextProvider.tsx +++ b/typescript/frontend-marios2/src/contexts/ProductIdContextProvider.tsx @@ -7,8 +7,10 @@ interface ProductIdContextType { setProduct: (new_product: number) => void; accessToSalimax: boolean; accessToSalidomo: boolean; + accessToSodiohome: boolean; setAccessToSalimax: (access: boolean) => void; setAccessToSalidomo: (access: boolean) => void; + setAccessToSodiohome: (access: boolean) => void; } // Create the context. @@ -16,6 +18,14 @@ export const ProductIdContext = createContext( undefined ); +// Define the product mapping for dynamic assignment +const productMapping: { [key: string]: number } = { + salimax: 0, + salidomo: 1, + sodiohome: 2, + // Additional mappings can be added here +}; + // Create a UserContextProvider component export const ProductIdContextProvider = ({ children @@ -31,8 +41,14 @@ export const ProductIdContextProvider = ({ const storedValue = localStorage.getItem('accessToSalidomo'); return storedValue === 'true'; }); - const [product, setProduct] = useState(location.includes('salidomo') ? 1 : 0); - + const [accessToSodiohome, setAccessToSodiohome] = useState(() => { + const storedValue = localStorage.getItem('accessToSodiohome'); + return storedValue === 'true'; + }); + // const [product, setProduct] = useState(location.includes('salidomo') ? 1 : 0); + const [product, setProduct] = useState( + productMapping[Object.keys(productMapping).find((key) => location.includes(key)) || ''] || -1 + ); const changeProductId = (new_product: number) => { setProduct(new_product); }; @@ -47,6 +63,11 @@ export const ProductIdContextProvider = ({ localStorage.setItem('accessToSalidomo', JSON.stringify(access)); }; + const changeAccessSodiohome = (access: boolean) => { + setAccessToSodiohome(access); + localStorage.setItem('accessToSodiohome', JSON.stringify(access)); + }; + return ( {children} diff --git a/typescript/frontend-marios2/src/layouts/SidebarLayout/Sidebar/SidebarMenu/index.tsx b/typescript/frontend-marios2/src/layouts/SidebarLayout/Sidebar/SidebarMenu/index.tsx index 9b9ce4207..80501219d 100644 --- a/typescript/frontend-marios2/src/layouts/SidebarLayout/Sidebar/SidebarMenu/index.tsx +++ b/typescript/frontend-marios2/src/layouts/SidebarLayout/Sidebar/SidebarMenu/index.tsx @@ -164,7 +164,7 @@ function SidebarMenu() { const { closeSidebar } = useContext(SidebarContext); const context = useContext(UserContext); const { currentUser, setUser } = context; - const { accessToSalimax, accessToSalidomo } = useContext(ProductIdContext); + const { accessToSalimax, accessToSalidomo,accessToSodiohome } = useContext(ProductIdContext); return ( <> @@ -216,6 +216,24 @@ function SidebarMenu() { )} + + {accessToSodiohome && ( + + + + + + )} From 4116d4bd17558edc2c21fc43a717f34cc0881f88 Mon Sep 17 00:00:00 2001 From: Yinyin Liu Date: Fri, 17 Jan 2025 16:01:53 +0100 Subject: [PATCH 3/7] corrected SodiohomeInstallationTabs directory path --- typescript/frontend-marios2/src/App.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/typescript/frontend-marios2/src/App.tsx b/typescript/frontend-marios2/src/App.tsx index 5a8d30096..1b51d2995 100644 --- a/typescript/frontend-marios2/src/App.tsx +++ b/typescript/frontend-marios2/src/App.tsx @@ -19,7 +19,7 @@ import { axiosConfigWithoutToken } from './Resources/axiosConfig'; import InstallationsContextProvider from './contexts/InstallationsContextProvider'; import AccessContextProvider from './contexts/AccessContextProvider'; import SalidomoInstallationTabs from './content/dashboards/SalidomoInstallations'; -import SodiohomeInstallationTabs from './content/dashboards/Installations/index'; +// import SodiohomeInstallationTabs from './content/dashboards/SodiohomeInstallations'; import { ProductIdContext } from './contexts/ProductIdContextProvider'; function App() { @@ -170,7 +170,7 @@ function App() { path={routes.sodiohome_installations + '*'} element={ - + {/**/} } /> From 63f60bdb3e69b20f64c52e3aa0fb62b63bef54c0 Mon Sep 17 00:00:00 2001 From: Yinyin Liu Date: Fri, 17 Jan 2025 16:03:37 +0100 Subject: [PATCH 4/7] implemented Add new Installation page for Sodiohome --- .../SodiohomeInstallationForm.tsx | 253 ++++++++++++++++++ .../content/dashboards/Tree/Information.tsx | 9 + .../src/contexts/ProductIdContextProvider.tsx | 28 +- .../src/interfaces/InstallationTypes.tsx | 1 + 4 files changed, 282 insertions(+), 9 deletions(-) create mode 100644 typescript/frontend-marios2/src/content/dashboards/SodiohomeInstallations/SodiohomeInstallationForm.tsx diff --git a/typescript/frontend-marios2/src/content/dashboards/SodiohomeInstallations/SodiohomeInstallationForm.tsx b/typescript/frontend-marios2/src/content/dashboards/SodiohomeInstallations/SodiohomeInstallationForm.tsx new file mode 100644 index 000000000..5b9c55f88 --- /dev/null +++ b/typescript/frontend-marios2/src/content/dashboards/SodiohomeInstallations/SodiohomeInstallationForm.tsx @@ -0,0 +1,253 @@ +import React, { useContext, useState } from 'react'; +import { + Alert, + Box, + CircularProgress, + FormControl, + IconButton, + InputLabel, + MenuItem, + Modal, + Select, + TextField, + useTheme +} from '@mui/material'; +import Button from '@mui/material/Button'; +import { Close as CloseIcon } from '@mui/icons-material'; +import { I_Installation } from 'src/interfaces/InstallationTypes'; +import { InstallationsContext } from 'src/contexts/InstallationsContextProvider'; +import { FormattedMessage } from 'react-intl'; + +interface SodiohomeInstallationFormProps { + cancel: () => void; + submit: () => void; + parentid: number; +} + +function SodiohomeInstallationForm(props: SodiohomeInstallationFormProps) { + const theme = useTheme(); + const [open, setOpen] = useState(true); + const [formValues, setFormValues] = useState>({ + name: '', + region: '', + location: '', + country: '', + serialNumber: '' + }); + const requiredFields = ['name', 'location', 'country', 'serialNumber']; + + const installationContext = useContext(InstallationsContext); + const { createInstallation, loading, setLoading, error, setError } = + installationContext; + + const handleChange = (e) => { + const { name, value } = e.target; + + setFormValues({ + ...formValues, + [name]: value + }); + }; + const handleSubmit = async (e) => { + setLoading(true); + formValues.parentId = props.parentid; + formValues.product = 2; + const responseData = await createInstallation(formValues); + props.submit(); + }; + const handleCancelSubmit = (e) => { + props.cancel(); + }; + + const areRequiredFieldsFilled = () => { + for (const field of requiredFields) { + if (!formValues[field]) { + return false; + } + } + return true; + }; + + const isMobile = window.innerWidth <= 1490; + + return ( + <> + {}} + aria-labelledby="error-modal" + aria-describedby="error-modal-description" + > + + +
+ + } + name="name" + value={formValues.name} + onChange={handleChange} + required + error={formValues.name === ''} + /> +
+
+ } + name="region" + value={formValues.region} + onChange={handleChange} + required + error={formValues.region === ''} + /> +
+
+ + } + name="location" + value={formValues.location} + onChange={handleChange} + required + error={formValues.location === ''} + /> +
+ +
+ + } + name="country" + value={formValues.country} + onChange={handleChange} + required + error={formValues.country === ''} + /> +
+ +
+ + } + name="serialNumber" + value={formValues.serialNumber} + onChange={handleChange} + required + error={formValues.serialNumber === ''} + /> +
+ +
+ + } + name="information" + value={formValues.information} + onChange={handleChange} + /> +
+
+
+ + + + + {loading && ( + + )} + + {error && ( + + + setError(false)} + sx={{ marginLeft: '4px' }} + > + + + + )} +
+
+
+ + ); +} + +export default SodiohomeInstallationForm; diff --git a/typescript/frontend-marios2/src/content/dashboards/Tree/Information.tsx b/typescript/frontend-marios2/src/content/dashboards/Tree/Information.tsx index 4855a049f..0f698d296 100644 --- a/typescript/frontend-marios2/src/content/dashboards/Tree/Information.tsx +++ b/typescript/frontend-marios2/src/content/dashboards/Tree/Information.tsx @@ -23,6 +23,7 @@ import { InstallationsContext } from '../../../contexts/InstallationsContextProv import { UserType } from '../../../interfaces/UserTypes'; import SalidomoInstallationForm from '../SalidomoInstallations/SalidomoInstallationForm'; import { ProductIdContext } from '../../../contexts/ProductIdContextProvider'; +import SodiohomeInstallationForm from '../SodiohomeInstallations/SodiohomeInstallationForm'; interface TreeInformationProps { folder: I_Folder; @@ -213,6 +214,14 @@ function TreeInformation(props: TreeInformationProps) { parentid={props.folder.id} /> )} + {openModalInstallation && product == 2 && ( + + )} + ( ); // Define the product mapping for dynamic assignment -const productMapping: { [key: string]: number } = { - salimax: 0, - salidomo: 1, - sodiohome: 2, - // Additional mappings can be added here -}; +// const productMapping: { [key: string]: number } = { +// salimax: 0, +// salidomo: 1, +// sodiohome: 2, +// // Additional mappings can be added here +// }; // Create a UserContextProvider component export const ProductIdContextProvider = ({ @@ -46,9 +46,19 @@ export const ProductIdContextProvider = ({ return storedValue === 'true'; }); // const [product, setProduct] = useState(location.includes('salidomo') ? 1 : 0); - const [product, setProduct] = useState( - productMapping[Object.keys(productMapping).find((key) => location.includes(key)) || ''] || -1 - ); + // const [product, setProduct] = useState( + // productMapping[Object.keys(productMapping).find((key) => location.includes(key)) || ''] || -1 + // ); + const [product, setProduct] = useState(() => { + if (location.includes('salidomo')) { + return 1; + } else if (location.includes('sodiohome')) { + return 2; + } else { + return 0; + } + }); + const changeProductId = (new_product: number) => { setProduct(new_product); }; diff --git a/typescript/frontend-marios2/src/interfaces/InstallationTypes.tsx b/typescript/frontend-marios2/src/interfaces/InstallationTypes.tsx index 21518e004..967751843 100644 --- a/typescript/frontend-marios2/src/interfaces/InstallationTypes.tsx +++ b/typescript/frontend-marios2/src/interfaces/InstallationTypes.tsx @@ -20,6 +20,7 @@ export interface I_Installation extends I_S3Credentials { device: number; testingMode?: boolean; status?: number; + serialNumber?: string; } export interface I_Folder { From 4e28d56346059f65238aa29c0931f980734e5b37 Mon Sep 17 00:00:00 2001 From: Noe Date: Mon, 20 Jan 2025 08:33:24 +0100 Subject: [PATCH 5/7] Fixed create installation tab (do not depend on product id) --- csharp/App/Backend/DataTypes/Installation.cs | 3 +- csharp/App/Backend/Relations/Session.cs | 2 + csharp/App/Backend/deploy.sh | 4 +- .../dbus-fzsonick-48tl/config.py | 6 +- .../src/Resources/axiosConfig.tsx | 4 +- .../FlatInstallationView.tsx | 10 +- .../content/dashboards/Tree/Information.tsx | 133 +++++++++++++++++- 7 files changed, 143 insertions(+), 19 deletions(-) diff --git a/csharp/App/Backend/DataTypes/Installation.cs b/csharp/App/Backend/DataTypes/Installation.cs index f67c0c921..a5af9b9e0 100644 --- a/csharp/App/Backend/DataTypes/Installation.cs +++ b/csharp/App/Backend/DataTypes/Installation.cs @@ -5,7 +5,8 @@ namespace InnovEnergy.App.Backend.DataTypes; public enum ProductType { Salimax = 0, - Salidomo = 1 + Salidomo = 1, + SodioHome =2 } public enum StatusType diff --git a/csharp/App/Backend/Relations/Session.cs b/csharp/App/Backend/Relations/Session.cs index 9f7b037ea..e87b762e4 100644 --- a/csharp/App/Backend/Relations/Session.cs +++ b/csharp/App/Backend/Relations/Session.cs @@ -14,6 +14,7 @@ public class Session : Relation [Indexed] public DateTime LastSeen { get; set; } public Boolean AccessToSalimax { get; set; } = false; public Boolean AccessToSalidomo { get; set; } = false; + public Boolean AccessToSodioHome { get; set; } = false; [Ignore] public Boolean Valid => DateTime.Now - LastSeen <=MaxAge ; // Private backing field @@ -45,6 +46,7 @@ public class Session : Relation LastSeen = DateTime.Now; AccessToSalimax = user.AccessibleInstallations(product: (int)ProductType.Salimax).ToList().Count > 0; AccessToSalidomo = user.AccessibleInstallations(product: (int)ProductType.Salidomo).ToList().Count > 0; + AccessToSodioHome = user.AccessibleInstallations(product: (int)ProductType.SodioHome).ToList().Count > 0; } private static String CreateToken() diff --git a/csharp/App/Backend/deploy.sh b/csharp/App/Backend/deploy.sh index ff3675a7d..52b6913db 100755 --- a/csharp/App/Backend/deploy.sh +++ b/csharp/App/Backend/deploy.sh @@ -1,5 +1,5 @@ #To deploy to the monitor server, uncomment the following line -dotnet publish Backend.csproj -c Release -r linux-x64 --self-contained true -p:PublishTrimmed=false && rsync -av bin/Release/net6.0/linux-x64/publish/ ubuntu@194.182.190.208:~/backend && ssh ubuntu@194.182.190.208 'sudo systemctl restart backend' +#dotnet publish Backend.csproj -c Release -r linux-x64 --self-contained true -p:PublishTrimmed=false && rsync -av bin/Release/net6.0/linux-x64/publish/ ubuntu@194.182.190.208:~/backend && ssh ubuntu@194.182.190.208 'sudo systemctl restart backend' #To deploy to the stage server, uncomment the following line -#dotnet publish Backend.csproj -c Release -r linux-x64 --self-contained true -p:PublishTrimmed=false && rsync -av bin/Release/net6.0/linux-x64/publish/ ubuntu@91.92.154.141:~/backend && ssh ubuntu@91.92.154.141 'sudo systemctl restart backend' +dotnet publish Backend.csproj -c Release -r linux-x64 --self-contained true -p:PublishTrimmed=false && rsync -av bin/Release/net6.0/linux-x64/publish/ ubuntu@91.92.154.141:~/backend && ssh ubuntu@91.92.154.141 'sudo systemctl restart backend' diff --git a/firmware/Venus_Release/VenusReleaseFiles/dbus-fzsonick-48tl/config.py b/firmware/Venus_Release/VenusReleaseFiles/dbus-fzsonick-48tl/config.py index 595ed1bcb..02e178bdf 100755 --- a/firmware/Venus_Release/VenusReleaseFiles/dbus-fzsonick-48tl/config.py +++ b/firmware/Venus_Release/VenusReleaseFiles/dbus-fzsonick-48tl/config.py @@ -54,6 +54,6 @@ INNOVENERGY_PROTOCOL_VERSION = '48TL200V3' # S3 Credentials -S3BUCKET = "140-c0436b6a-d276-4cd8-9c44-1eae86cf5d0e" -S3KEY = "EXOa947c7fc5990a7a6f6c40860" -S3SECRET = "J1yOTLbYEO6cMxQ2wgIwe__ru9-_RH5BBtKzx_2JJHk" +S3BUCKET = "158-c0436b6a-d276-4cd8-9c44-1eae86cf5d0e" +S3KEY = "EXOf4d6d68a9ce062f25541fe4a" +S3SECRET = "4zTQBvwIWnFYajRhoZW0F7k_6rdhnPiSqdvw9cMAZw8" diff --git a/typescript/frontend-marios2/src/Resources/axiosConfig.tsx b/typescript/frontend-marios2/src/Resources/axiosConfig.tsx index 0d4606588..9f230ab85 100644 --- a/typescript/frontend-marios2/src/Resources/axiosConfig.tsx +++ b/typescript/frontend-marios2/src/Resources/axiosConfig.tsx @@ -1,12 +1,12 @@ import axios from 'axios'; export const axiosConfigWithoutToken = axios.create({ - baseURL: 'https://monitor.innov.energy/api' + baseURL: 'https://stage.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: 'http://127.0.0.1:7087/api' }); diff --git a/typescript/frontend-marios2/src/content/dashboards/SalidomoInstallations/FlatInstallationView.tsx b/typescript/frontend-marios2/src/content/dashboards/SalidomoInstallations/FlatInstallationView.tsx index 84ac01509..cf43e6912 100644 --- a/typescript/frontend-marios2/src/content/dashboards/SalidomoInstallations/FlatInstallationView.tsx +++ b/typescript/frontend-marios2/src/content/dashboards/SalidomoInstallations/FlatInstallationView.tsx @@ -130,11 +130,11 @@ const FlatInstallationView = (props: FlatInstallationViewProps) => { {sortedInstallations - // .filter( - // (installation) => - // installation.status === -1 && - // installation.testingMode == false - // ) + .filter( + (installation) => + installation.status === -1 && + installation.testingMode == false + ) .map((installation) => { const isInstallationSelected = installation.s3BucketId === selectedInstallation; diff --git a/typescript/frontend-marios2/src/content/dashboards/Tree/Information.tsx b/typescript/frontend-marios2/src/content/dashboards/Tree/Information.tsx index 0f698d296..c33c456b2 100644 --- a/typescript/frontend-marios2/src/content/dashboards/Tree/Information.tsx +++ b/typescript/frontend-marios2/src/content/dashboards/Tree/Information.tsx @@ -4,9 +4,13 @@ import { CardContent, CircularProgress, Container, + FormControl, Grid, IconButton, + InputLabel, + MenuItem, Modal, + Select, TextField, Typography, useTheme @@ -18,11 +22,10 @@ import React, { useContext, useState } from 'react'; import { I_Folder } from '../../../interfaces/InstallationTypes'; import { UserContext } from '../../../contexts/userContext'; import FolderForm from './folderForm'; -import InstallationForm from '../Installations/installationForm'; import { InstallationsContext } from '../../../contexts/InstallationsContextProvider'; import { UserType } from '../../../interfaces/UserTypes'; +import InstallationForm from '../Installations/installationForm'; import SalidomoInstallationForm from '../SalidomoInstallations/SalidomoInstallationForm'; -import { ProductIdContext } from '../../../contexts/ProductIdContextProvider'; import SodiohomeInstallationForm from '../SodiohomeInstallations/SodiohomeInstallationForm'; interface TreeInformationProps { @@ -38,6 +41,8 @@ function TreeInformation(props: TreeInformationProps) { const { currentUser } = context; const [formValues, setFormValues] = useState(props.folder); const [openModalFolder, setOpenModalFolder] = useState(false); + const [openModalInstallationChoice, setOpenModalInstallationChoice] = + useState(false); const [openModalInstallation, setOpenModalInstallation] = useState(false); const requiredFields = ['name']; const [openModalDeleteFolder, setOpenModalDeleteFolder] = useState(false); @@ -53,7 +58,17 @@ function TreeInformation(props: TreeInformationProps) { deleteFolder } = installationContext; - const { product, setProduct } = useContext(ProductIdContext); + //const { product, setProduct } = useContext(ProductIdContext); + const [product, setProduct] = useState('Salimax'); + + const handleChangeInstallationChoice = (e) => { + setProduct(e.target.value); // Directly update the product state + // console.log('Selected Product:', e.target.value); + }; + + const ProductTypes = ['Salimax', 'Salidomo', 'Sodiohome']; + + const isMobile = window.innerWidth <= 1490; const handleChange = (e) => { const { name, value } = e.target; @@ -70,6 +85,16 @@ function TreeInformation(props: TreeInformationProps) { }; const handleNewInstallationInsertion = () => { + setOpenModalInstallationChoice(true); + //setOpenModalInstallation(true); + }; + + const handleCancelSubmitInstallationChoice = () => { + setOpenModalInstallationChoice(false); + }; + + const handleSubmitInstallationChoice = () => { + setOpenModalInstallationChoice(false); setOpenModalInstallation(true); }; @@ -200,21 +225,117 @@ function TreeInformation(props: TreeInformationProps) { parentid={props.folder.id} /> )} - {openModalInstallation && product == 0 && ( + {openModalInstallationChoice && ( + {}} + aria-labelledby="error-modal" + aria-describedby="error-modal-description" + > + + +
+ + + + + + +
+
+
+ + + +
+
+
+ )} + {openModalInstallation && product == 'Salimax' && ( )} - {openModalInstallation && product == 1 && ( + {openModalInstallation && product == 'Salidomo' && ( )} - {openModalInstallation && product == 2 && ( + {openModalInstallation && product == 'Sodiohome' && ( Date: Mon, 20 Jan 2025 10:54:33 +0100 Subject: [PATCH 6/7] frontend ready to implement index.tsx for Sodiohome --- csharp/App/Backend/DataTypes/Installation.cs | 1 + .../FlatInstallationView.tsx | 10 ++++---- .../content/dashboards/Tree/Information.tsx | 2 ++ .../contexts/InstallationsContextProvider.tsx | 25 ++++++++++++------- 4 files changed, 24 insertions(+), 14 deletions(-) diff --git a/csharp/App/Backend/DataTypes/Installation.cs b/csharp/App/Backend/DataTypes/Installation.cs index a5af9b9e0..19e174222 100644 --- a/csharp/App/Backend/DataTypes/Installation.cs +++ b/csharp/App/Backend/DataTypes/Installation.cs @@ -41,6 +41,7 @@ public class Installation : TreeNode public int Status { get; set; } = -1; public int Product { get; set; } = (int)ProductType.Salimax; public int Device { get; set; } = 0; + public string SerialNumber { get; set; } = ""; [Ignore] public String OrderNumbers { get; set; } diff --git a/typescript/frontend-marios2/src/content/dashboards/SalidomoInstallations/FlatInstallationView.tsx b/typescript/frontend-marios2/src/content/dashboards/SalidomoInstallations/FlatInstallationView.tsx index cf43e6912..84ac01509 100644 --- a/typescript/frontend-marios2/src/content/dashboards/SalidomoInstallations/FlatInstallationView.tsx +++ b/typescript/frontend-marios2/src/content/dashboards/SalidomoInstallations/FlatInstallationView.tsx @@ -130,11 +130,11 @@ const FlatInstallationView = (props: FlatInstallationViewProps) => { {sortedInstallations - .filter( - (installation) => - installation.status === -1 && - installation.testingMode == false - ) + // .filter( + // (installation) => + // installation.status === -1 && + // installation.testingMode == false + // ) .map((installation) => { const isInstallationSelected = installation.s3BucketId === selectedInstallation; diff --git a/typescript/frontend-marios2/src/content/dashboards/Tree/Information.tsx b/typescript/frontend-marios2/src/content/dashboards/Tree/Information.tsx index c33c456b2..5252c147e 100644 --- a/typescript/frontend-marios2/src/content/dashboards/Tree/Information.tsx +++ b/typescript/frontend-marios2/src/content/dashboards/Tree/Information.tsx @@ -122,11 +122,13 @@ function TreeInformation(props: TreeInformationProps) { const handleFolderFormSubmit = () => { setOpenModalFolder(false); setOpenModalInstallation(false); + setLoading(false); }; const handleInstallationFormSubmit = () => { setOpenModalFolder(false); setOpenModalInstallation(false); + setLoading(false); }; const handleFormCancel = () => { diff --git a/typescript/frontend-marios2/src/contexts/InstallationsContextProvider.tsx b/typescript/frontend-marios2/src/contexts/InstallationsContextProvider.tsx index 331454664..e52bae21d 100644 --- a/typescript/frontend-marios2/src/contexts/InstallationsContextProvider.tsx +++ b/typescript/frontend-marios2/src/contexts/InstallationsContextProvider.tsx @@ -82,10 +82,10 @@ const InstallationsContextProvider = ({ const update = pendingUpdates.current[installation.id]; return update ? { - ...installation, - status: update.status, - testingMode: update.testingMode - } + ...installation, + status: update.status, + testingMode: update.testingMode + } : installation; }); @@ -95,7 +95,7 @@ const InstallationsContextProvider = ({ // Clear the pending updates after applying pendingUpdates.current = {}; - }, [salidomoInstallations, salimaxInstallations,sodiohomeInstallations]); + }, [salidomoInstallations, salimaxInstallations, sodiohomeInstallations]); useEffect(() => { const timer = setInterval(() => { @@ -109,7 +109,7 @@ const InstallationsContextProvider = ({ setcurrentProduct(product); const tokenString = localStorage.getItem('token'); const token = tokenString !== null ? tokenString : ''; - const urlWithToken = `wss://monitor.innov.energy/api/CreateWebSocket?authToken=${token}`; + const urlWithToken = `wss://stage.innov.energy/api/CreateWebSocket?authToken=${token}`; const socket = new WebSocket(urlWithToken); @@ -126,7 +126,9 @@ const InstallationsContextProvider = ({ // Send the corresponding installation IDs socket.send( - JSON.stringify(installationsToSend.map((installation) => installation.id)) + JSON.stringify( + installationsToSend.map((installation) => installation.id) + ) ); }); @@ -199,7 +201,7 @@ const InstallationsContextProvider = ({ const fetchAllSodiohomeInstallations = useCallback(async () => { axiosConfig - .get('/GetAllSodiohomeInstallations') + .get('/GetAllSodioHomeInstallations') .then((res: AxiosResponse) => setSodiohomeInstallations(res.data) ) @@ -230,7 +232,10 @@ const InstallationsContextProvider = ({ async (formValues: Partial) => { axiosConfig .post('/CreateInstallation', formValues) - .then(() => fetchAllFoldersAndInstallations(formValues.product)) + .then(() => { + setLoading(false); + fetchAllFoldersAndInstallations(formValues.product); + }) .catch((error) => { setError(true); if (error.response?.status === 401) { @@ -330,8 +335,10 @@ const InstallationsContextProvider = ({ .put('/UpdateFolder', formValues) .then(() => { setUpdated(true); + fetchAllFoldersAndInstallations(product); setTimeout(() => setUpdated(false), 3000); + setLoading(false); }) .catch((error) => { setError(true); From 25310a4250bb6f8659de55584ae070431c86a64d Mon Sep 17 00:00:00 2001 From: Noe Date: Tue, 21 Jan 2025 09:24:35 +0100 Subject: [PATCH 7/7] frontend ready to implement index.tsx for Sodiohome --- csharp/App/Backend/Controller.cs | 13 + .../App/Backend/DataTypes/Methods/Session.cs | 11 + .../SodiohomeInstallations/Installation.tsx | 390 +++++++++++++++++ .../InstallationSearch.tsx | 100 +++++ .../SodiohomeInstallations/index.tsx | 396 ++++++++++++++++++ 5 files changed, 910 insertions(+) create mode 100644 typescript/frontend-marios2/src/content/dashboards/SodiohomeInstallations/Installation.tsx create mode 100644 typescript/frontend-marios2/src/content/dashboards/SodiohomeInstallations/InstallationSearch.tsx create mode 100644 typescript/frontend-marios2/src/content/dashboards/SodiohomeInstallations/index.tsx diff --git a/csharp/App/Backend/Controller.cs b/csharp/App/Backend/Controller.cs index 406223442..bca9d21b5 100644 --- a/csharp/App/Backend/Controller.cs +++ b/csharp/App/Backend/Controller.cs @@ -468,6 +468,19 @@ public class Controller : ControllerBase .ToList(); } + [HttpGet(nameof(GetAllSodioHomeInstallations))] + public ActionResult> GetAllSodioHomeInstallations(Token authToken) + { + var user = Db.GetSession(authToken)?.User; + + if (user is null) + return Unauthorized(); + + return user + .AccessibleInstallations(product:(int)ProductType.SodioHome) + .ToList(); + } + [HttpGet(nameof(GetAllFolders))] diff --git a/csharp/App/Backend/DataTypes/Methods/Session.cs b/csharp/App/Backend/DataTypes/Methods/Session.cs index 3d044b728..ff6fa8090 100644 --- a/csharp/App/Backend/DataTypes/Methods/Session.cs +++ b/csharp/App/Backend/DataTypes/Methods/Session.cs @@ -232,6 +232,17 @@ public static class SessionMethods && await installation.CreateBucket() && await installation.RenewS3Credentials(); } + + if (installation.Product == (int)ProductType.SodioHome) + { + return user is not null + && user.UserType != 0 + && user.HasAccessToParentOf(installation) + && Db.Create(installation); + } + + + return false; } diff --git a/typescript/frontend-marios2/src/content/dashboards/SodiohomeInstallations/Installation.tsx b/typescript/frontend-marios2/src/content/dashboards/SodiohomeInstallations/Installation.tsx new file mode 100644 index 000000000..98a003a5a --- /dev/null +++ b/typescript/frontend-marios2/src/content/dashboards/SodiohomeInstallations/Installation.tsx @@ -0,0 +1,390 @@ +import { I_Installation } from 'src/interfaces/InstallationTypes'; +import React from 'react'; +import { Grid } from '@mui/material'; + +interface singleInstallationProps { + current_installation?: I_Installation; + type?: string; +} + +function SodioHomeInstallation(props: singleInstallationProps) { + // const context = useContext(UserContext); + // const { currentUser } = context; + // const location = useLocation().pathname; + // const [errorLoadingS3Data, setErrorLoadingS3Data] = useState(false); + // const [currentTab, setCurrentTab] = useState(undefined); + // const [values, setValues] = useState(null); + // const status = props.current_installation.status; + // const [ + // failedToCommunicateWithInstallation, + // setFailedToCommunicateWithInstallation + // ] = useState(0); + // const [connected, setConnected] = useState(true); + // const [loading, setLoading] = useState(true); + // + // if (props.current_installation == undefined) { + // return null; + // } + // + // const S3data = { + // s3Region: props.current_installation.s3Region, + // s3Provider: props.current_installation.s3Provider, + // s3Key: props.current_installation.s3Key, + // s3Secret: props.current_installation.s3Secret, + // s3BucketId: props.current_installation.s3BucketId + // }; + // + // const s3Bucket = + // props.current_installation.s3BucketId.toString() + + // '-' + + // 'c0436b6a-d276-4cd8-9c44-1eae86cf5d0e'; + // + // const [fetchFunctionCalled, setFetchFunctionCalled] = useState(false); + // const s3Credentials = { s3Bucket, ...S3data }; + // + // function timeout(delay: number) { + // return new Promise((res) => setTimeout(res, delay)); + // } + // + // const continueFetching = useRef(false); + // + // const fetchDataPeriodically = async () => { + // var timeperiodToSearch = 30; + // let res; + // let timestampToFetch; + // + // for (var i = 0; i < timeperiodToSearch; i += 1) { + // if (!continueFetching.current) { + // return false; + // } + // timestampToFetch = UnixTime.now().earlier(TimeSpan.fromMinutes(i)); + // console.log('timestamp to fetch is ' + timestampToFetch); + // + // try { + // res = await fetchData(timestampToFetch, s3Credentials, true); + // if (res !== FetchResult.notAvailable && res !== FetchResult.tryLater) { + // break; + // } + // } catch (err) { + // console.error('Error fetching data:', err); + // return false; + // } + // } + // + // if (i >= timeperiodToSearch) { + // setConnected(false); + // setLoading(false); + // return false; + // } + // setConnected(true); + // setLoading(false); + // console.log('NUMBER OF FILES=' + Object.keys(res).length); + // + // while (continueFetching.current) { + // for (const timestamp of Object.keys(res)) { + // if (!continueFetching.current) { + // setFetchFunctionCalled(false); + // return false; + // } + // console.log(`Timestamp: ${timestamp}`); + // console.log(res[timestamp]); + // + // // Set values asynchronously with delay + // setValues( + // extractValues({ + // time: UnixTime.fromTicks(parseInt(timestamp, 10)), + // value: res[timestamp] + // }) + // ); + // // Wait for 2 seconds before processing next timestamp + // await timeout(2000); + // } + // + // timestampToFetch = timestampToFetch.later(TimeSpan.fromMinutes(20)); + // console.log('NEW TIMESTAMP TO FETCH IS ' + timestampToFetch); + // + // for (i = 0; i < 10; i++) { + // if (!continueFetching.current) { + // return false; + // } + // + // try { + // console.log('Trying to fetch timestamp ' + timestampToFetch); + // res = await fetchData(timestampToFetch, s3Credentials, true); + // if ( + // res !== FetchResult.notAvailable && + // res !== FetchResult.tryLater + // ) { + // break; + // } + // } catch (err) { + // console.error('Error fetching data:', err); + // return false; + // } + // timestampToFetch = timestampToFetch.later(TimeSpan.fromMinutes(1)); + // } + // } + // }; + // useEffect(() => { + // let path = location.split('/'); + // setCurrentTab(path[path.length - 1]); + // }, [location]); + // + // useEffect(() => { + // if (location.includes('batteryview')) { + // if (location.includes('batteryview') && !location.includes('mainstats')) { + // if (!continueFetching.current) { + // continueFetching.current = true; + // if (!fetchFunctionCalled) { + // setFetchFunctionCalled(true); + // fetchDataPeriodically(); + // } + // } + // } + // + // return () => { + // continueFetching.current = false; + // }; + // } else { + // continueFetching.current = false; + // } + // }, [currentTab, location]); + // + // useEffect(() => { + // if (status === null) { + // setConnected(false); + // } + // }, [status]); + // + + return ; + + // return ( + // <> + // + //
+ // + // + // + // + // {props.current_installation.name} + // + //
+ //
+ // + // Status: + // + //
+ // {status === -1 ? ( + // + // ) : ( + // '' + // )} + // + // {status === -2 ? ( + // + // ) : ( + // '' + // )} + // + //
+ // + // {props.current_installation.testingMode && ( + // + // )} + //
+ //
+ // {loading && + // currentTab != 'information' && + // currentTab != 'overview' && + // currentTab != 'manage' && + // currentTab != 'history' && + // currentTab != 'log' && ( + // + // + // + // Connecting to the device... + // + // + // )} + // + // + // + // + // + // } + // /> + // + // + // } + // /> + // + // + // } + // /> + // + // + // } + // > + // + // {currentUser.userType == UserType.admin && ( + // + // } + // /> + // )} + // + // {currentUser.userType == UserType.admin && ( + // + // + // + // } + // /> + // )} + // + // } + // /> + // + // + // + // + // + // ); +} + +export default SodioHomeInstallation; diff --git a/typescript/frontend-marios2/src/content/dashboards/SodiohomeInstallations/InstallationSearch.tsx b/typescript/frontend-marios2/src/content/dashboards/SodiohomeInstallations/InstallationSearch.tsx new file mode 100644 index 000000000..62ca20dc5 --- /dev/null +++ b/typescript/frontend-marios2/src/content/dashboards/SodiohomeInstallations/InstallationSearch.tsx @@ -0,0 +1,100 @@ +import React, { useMemo, useState } from 'react'; +import { FormControl, Grid, InputAdornment, TextField } from '@mui/material'; +import SearchTwoToneIcon from '@mui/icons-material/SearchTwoTone'; +import { I_Installation } from '../../../interfaces/InstallationTypes'; +import { useLocation } from 'react-router-dom'; +import routes from '../../../Resources/routes.json'; + +interface installationSearchProps { + installations: I_Installation[]; +} + +function InstallationSearch(props: installationSearchProps) { + const [searchTerm, setSearchTerm] = useState(''); + const currentLocation = useLocation(); + // const [filteredData, setFilteredData] = useState(props.installations); + + const indexedData = useMemo(() => { + return props.installations.map((item) => ({ + ...item, + nameLower: item.name.toLowerCase(), + locationLower: item.location.toLowerCase(), + regionLower: item.region.toLowerCase() + })); + }, [props.installations]); + + const filteredData = useMemo(() => { + return indexedData.filter( + (item) => + item.nameLower.includes(searchTerm.toLowerCase()) || + item.locationLower.includes(searchTerm.toLowerCase()) || + item.regionLower.includes(searchTerm.toLowerCase()) + ); + }, [searchTerm, indexedData]); + + return ( + <> + + +
+ + setSearchTerm(e.target.value)} + fullWidth + InputProps={{ + startAdornment: ( + + + + ) + }} + /> + +
+
+
+ + {/**/} + {/**/} + {/* {filteredData.map((installation) => {*/} + {/* return (*/} + {/* */} + {/* }*/} + {/* />*/} + {/* );*/} + {/* })}*/} + {/**/} + + ); +} + +export default InstallationSearch; diff --git a/typescript/frontend-marios2/src/content/dashboards/SodiohomeInstallations/index.tsx b/typescript/frontend-marios2/src/content/dashboards/SodiohomeInstallations/index.tsx new file mode 100644 index 000000000..c9f4ac837 --- /dev/null +++ b/typescript/frontend-marios2/src/content/dashboards/SodiohomeInstallations/index.tsx @@ -0,0 +1,396 @@ +import React, { ChangeEvent, useContext, useEffect, useState } from 'react'; +import { Box, Card, Container, Grid, Tab, Tabs } from '@mui/material'; +import { TabsContainerWrapper } from 'src/layouts/TabsContainerWrapper'; +import { Link, Navigate, Route, Routes, useLocation } from 'react-router-dom'; +import routes from 'src/Resources/routes.json'; +import InstallationSearch from './InstallationSearch'; +import { FormattedMessage } from 'react-intl'; +import { UserContext } from '../../../contexts/userContext'; +import { InstallationsContext } from '../../../contexts/InstallationsContextProvider'; +import ListIcon from '@mui/icons-material/List'; +import AccountTreeIcon from '@mui/icons-material/AccountTree'; +import TreeView from '../Tree/treeView'; +import { ProductIdContext } from '../../../contexts/ProductIdContextProvider'; +import { UserType } from '../../../interfaces/UserTypes'; +import SodioHomeInstallation from './Installation'; + +function SodioHomeInstallationTabs() { + const location = useLocation(); + const context = useContext(UserContext); + const { currentUser } = context; + const tabList = [ + 'batteryview', + 'information', + 'manage', + 'overview', + 'log', + 'history' + ]; + + const [currentTab, setCurrentTab] = useState(undefined); + const [fetchedInstallations, setFetchedInstallations] = + useState(false); + const { + sodiohomeInstallations, + fetchAllSodiohomeInstallations, + currentProduct, + socket, + openSocket, + closeSocket + } = useContext(InstallationsContext); + const { product, setProduct } = useContext(ProductIdContext); + + // const webSocketsContext = useContext(WebSocketContext); + // const { socket, openSocket, closeSocket } = webSocketsContext; + + useEffect(() => { + let path = location.pathname.split('/'); + + if (path[path.length - 2] === 'list') { + setCurrentTab('list'); + } else if (path[path.length - 2] === 'tree') { + setCurrentTab('tree'); + } else { + //Even if we are located at path: /batteryview/mainstats, we want the BatteryView tab to be bold + setCurrentTab(path.find((pathElement) => tabList.includes(pathElement))); + } + }, [location]); + + useEffect(() => { + if (sodiohomeInstallations.length === 0 && fetchedInstallations === false) { + fetchAllSodiohomeInstallations(); + setFetchedInstallations(true); + } + }, [sodiohomeInstallations]); + + useEffect(() => { + if (sodiohomeInstallations && sodiohomeInstallations.length > 0) { + if (!socket) { + openSocket(1); + } else if (currentProduct == 0) { + closeSocket(); + openSocket(1); + } + } + }, [sodiohomeInstallations]); + + useEffect(() => { + setProduct(1); + }, []); + + const handleTabsChange = (_event: ChangeEvent<{}>, value: string): void => { + setCurrentTab(value); + }; + + const navigateToTabPath = (pathname: string, tab_value: string): string => { + let pathlist = pathname.split('/'); + let ret_path = ''; + for (let i = 1; i < pathlist.length; i++) { + if (Number.isNaN(Number(pathlist[i]))) { + ret_path += '/'; + ret_path += pathlist[i]; + } else { + ret_path += '/'; + ret_path += pathlist[i]; + ret_path += '/'; + break; + } + } + + ret_path += tab_value; + return ret_path; + }; + + const singleInstallationTabs = + currentUser.userType == UserType.admin + ? [ + { + value: 'batteryview', + label: ( + + ) + }, + { + value: 'overview', + label: + }, + { + value: 'log', + label: + }, + + { + value: 'manage', + label: ( + + ) + }, + + { + value: 'information', + label: ( + + ) + }, + { + value: 'history', + label: ( + + ) + } + ] + : [ + { + value: 'batteryview', + label: ( + + ) + }, + { + value: 'overview', + label: + }, + + { + value: 'information', + label: ( + + ) + } + ]; + + const tabs = + currentTab != 'list' && + currentTab != 'tree' && + !location.pathname.includes('folder') && + currentUser.userType == UserType.admin + ? [ + { + value: 'list', + icon: + }, + { + value: 'tree', + icon: + }, + { + value: 'batteryview', + label: ( + + ) + }, + { + value: 'overview', + label: + }, + { + value: 'log', + label: + }, + + { + value: 'manage', + label: ( + + ) + }, + + { + value: 'information', + label: ( + + ) + }, + { + value: 'history', + label: ( + + ) + } + ] + : currentTab != 'list' && + currentTab != 'tree' && + !location.pathname.includes('folder') && + currentUser.userType == UserType.client + ? [ + { + value: 'list', + icon: + }, + { + value: 'tree', + icon: + }, + { + value: 'batteryview', + label: ( + + ) + }, + { + value: 'overview', + label: + }, + + { + value: 'information', + label: ( + + ) + } + ] + : [ + { + value: 'list', + icon: + }, + { + value: 'tree', + icon: + } + ]; + + return sodiohomeInstallations.length > 1 ? ( + <> + + + + {tabs.map((tab) => ( + + ))} + + + + + + + + + + + } + /> + + } /> + + + } + > + + + + + + ) : sodiohomeInstallations.length === 1 ? ( + <> + {' '} + + + + {singleInstallationTabs.map((tab) => ( + + ))} + + + + + + + + + + + } + /> + + + + + + ) : null; +} + +export default SodioHomeInstallationTabs;