Merge remote-tracking branch 'origin/main'
This commit is contained in:
commit
35b3d91f05
|
@ -281,11 +281,24 @@ public class Controller : ControllerBase
|
||||||
return Unauthorized();
|
return Unauthorized();
|
||||||
|
|
||||||
return user
|
return user
|
||||||
.AccessibleInstallations()
|
.AccessibleInstallations(product:0)
|
||||||
.Select(i => i.FillOrderNumbers().HideParentIfUserHasNoAccessToParent(user).HideWriteKeyIfUserIsNotAdmin(user.UserType))
|
.Select(i => i.FillOrderNumbers().HideParentIfUserHasNoAccessToParent(user).HideWriteKeyIfUserIsNotAdmin(user.UserType))
|
||||||
.ToList();
|
.ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpGet(nameof(GetAllSalidomoInstallations))]
|
||||||
|
public ActionResult<IEnumerable<Installation>> GetAllSalidomoInstallations(Token authToken)
|
||||||
|
{
|
||||||
|
var user = Db.GetSession(authToken)?.User;
|
||||||
|
|
||||||
|
if (user is null)
|
||||||
|
return Unauthorized();
|
||||||
|
|
||||||
|
return user
|
||||||
|
.AccessibleInstallations(product:1)
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
[HttpGet(nameof(GetAllFolders))]
|
[HttpGet(nameof(GetAllFolders))]
|
||||||
|
@ -309,7 +322,7 @@ public class Controller : ControllerBase
|
||||||
return Unauthorized();
|
return Unauthorized();
|
||||||
|
|
||||||
var foldersAndInstallations = user
|
var foldersAndInstallations = user
|
||||||
.AccessibleFoldersAndInstallations()
|
.AccessibleFoldersAndInstallations(product:0)
|
||||||
.Do(o => o.FillOrderNumbers())
|
.Do(o => o.FillOrderNumbers())
|
||||||
.Select(o => o.HideParentIfUserHasNoAccessToParent(user))
|
.Select(o => o.HideParentIfUserHasNoAccessToParent(user))
|
||||||
.OfType<Object>(); // Important! JSON serializer must see Objects otherwise
|
.OfType<Object>(); // Important! JSON serializer must see Objects otherwise
|
||||||
|
@ -342,6 +355,7 @@ public class Controller : ControllerBase
|
||||||
[HttpPost(nameof(CreateInstallation))]
|
[HttpPost(nameof(CreateInstallation))]
|
||||||
public async Task<ActionResult<Installation>> CreateInstallation([FromBody] Installation installation, Token authToken)
|
public async Task<ActionResult<Installation>> CreateInstallation([FromBody] Installation installation, Token authToken)
|
||||||
{
|
{
|
||||||
|
|
||||||
var session = Db.GetSession(authToken);
|
var session = Db.GetSession(authToken);
|
||||||
|
|
||||||
if (! await session.Create(installation))
|
if (! await session.Create(installation))
|
||||||
|
@ -453,9 +467,14 @@ public class Controller : ControllerBase
|
||||||
if (!session.Update(installation))
|
if (!session.Update(installation))
|
||||||
return Unauthorized();
|
return Unauthorized();
|
||||||
|
|
||||||
|
if (installation.Product == 0)
|
||||||
|
{
|
||||||
return installation.FillOrderNumbers().HideParentIfUserHasNoAccessToParent(session!.User).HideWriteKeyIfUserIsNotAdmin(session.User.UserType);
|
return installation.FillOrderNumbers().HideParentIfUserHasNoAccessToParent(session!.User).HideWriteKeyIfUserIsNotAdmin(session.User.UserType);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return installation.HideParentIfUserHasNoAccessToParent(session!.User);
|
||||||
|
}
|
||||||
|
|
||||||
[HttpPost(nameof(AcknowledgeError))]
|
[HttpPost(nameof(AcknowledgeError))]
|
||||||
public ActionResult AcknowledgeError(Int64 id, Token authToken)
|
public ActionResult AcknowledgeError(Int64 id, Token authToken)
|
||||||
{
|
{
|
||||||
|
|
|
@ -7,16 +7,8 @@ public class Installation : TreeNode
|
||||||
public String Location { get; set; } = "";
|
public String Location { get; set; } = "";
|
||||||
public String Region { get; set; } = "";
|
public String Region { get; set; } = "";
|
||||||
public String Country { get; set; } = "";
|
public String Country { get; set; } = "";
|
||||||
public String InstallationName { get; set; } = "";
|
|
||||||
public String VpnIp { get; set; } = "";
|
public String VpnIp { get; set; } = "";
|
||||||
|
|
||||||
// TODO: make relation
|
|
||||||
//public IReadOnlyList<String> OrderNumbers { get; set; } = Array.Empty<String>();
|
|
||||||
// public String? OrderNumbers { get; set; } = "";
|
|
||||||
|
|
||||||
public Double Lat { get; set; }
|
|
||||||
public Double Long { get; set; }
|
|
||||||
|
|
||||||
public String S3Region { get; set; } = "sos-ch-dk-2";
|
public String S3Region { get; set; } = "sos-ch-dk-2";
|
||||||
public String S3Provider { get; set; } = "exo.io";
|
public String S3Provider { get; set; } = "exo.io";
|
||||||
public String S3WriteKey { get; set; } = "";
|
public String S3WriteKey { get; set; } = "";
|
||||||
|
@ -26,9 +18,10 @@ public class Installation : TreeNode
|
||||||
public String ReadRoleId { get; set; } = "";
|
public String ReadRoleId { get; set; } = "";
|
||||||
public String WriteRoleId { get; set; } = "";
|
public String WriteRoleId { get; set; } = "";
|
||||||
|
|
||||||
|
public int Product { get; set; } = 0;
|
||||||
[Ignore]
|
[Ignore]
|
||||||
public String OrderNumbers { get; set; }
|
public String OrderNumbers { get; set; }
|
||||||
|
public String VrmLink { get; set; } = "";
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
|
@ -81,6 +81,7 @@ public static class ExoCmd
|
||||||
{
|
{
|
||||||
var readRoleId = installation.ReadRoleId;
|
var readRoleId = installation.ReadRoleId;
|
||||||
|
|
||||||
|
|
||||||
if (String.IsNullOrEmpty(readRoleId)
|
if (String.IsNullOrEmpty(readRoleId)
|
||||||
||! await CheckRoleExists(readRoleId))
|
||! await CheckRoleExists(readRoleId))
|
||||||
{
|
{
|
||||||
|
@ -113,11 +114,10 @@ public static class ExoCmd
|
||||||
{
|
{
|
||||||
var url = "https://api-ch-dk-2.exoscale.com/v2/api-key";
|
var url = "https://api-ch-dk-2.exoscale.com/v2/api-key";
|
||||||
var method = "api-key";
|
var method = "api-key";
|
||||||
var contentString = $$"""{"role-id": "{{roleName}}", "name":"{{installation.BucketName()}}"}""";
|
var contentString = $$"""{"role-id": "{{roleName}}", "name":"{{ installation.BucketName()}}"}""";
|
||||||
var unixtime = DateTimeOffset.UtcNow.ToUnixTimeSeconds() + 60;
|
var unixtime = DateTimeOffset.UtcNow.ToUnixTimeSeconds() + 60;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
var authheader = "credential=" + S3Credentials.Key + ",expires=" + unixtime + ",signature=" +
|
var authheader = "credential=" + S3Credentials.Key + ",expires=" + unixtime + ",signature=" +
|
||||||
BuildSignature("POST", method, contentString, unixtime);
|
BuildSignature("POST", method, contentString, unixtime);
|
||||||
|
|
||||||
|
@ -165,7 +165,6 @@ public static class ExoCmd
|
||||||
""";
|
""";
|
||||||
|
|
||||||
var unixtime = DateTimeOffset.UtcNow.ToUnixTimeSeconds()+60;
|
var unixtime = DateTimeOffset.UtcNow.ToUnixTimeSeconds()+60;
|
||||||
|
|
||||||
var authheader = "credential="+S3Credentials.Key+",signed-query-args="+",expires="+unixtime+",signature="+BuildSignature("POST", method, contentString, unixtime);
|
var authheader = "credential="+S3Credentials.Key+",signed-query-args="+",expires="+unixtime+",signature="+BuildSignature("POST", method, contentString, unixtime);
|
||||||
|
|
||||||
var client = new HttpClient();
|
var client = new HttpClient();
|
||||||
|
@ -294,6 +293,7 @@ public static class ExoCmd
|
||||||
return await s3Region.PutBucket(installation.BucketName()) != null;
|
return await s3Region.PutBucket(installation.BucketName()) != null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public static async Task<Boolean> SendConfig(this Installation installation, Configuration config)
|
public static async Task<Boolean> SendConfig(this Installation installation, Configuration config)
|
||||||
{
|
{
|
||||||
|
|
||||||
|
@ -316,7 +316,7 @@ public static class ExoCmd
|
||||||
|
|
||||||
// return result.ExitCode == 200;
|
// return result.ExitCode == 200;
|
||||||
|
|
||||||
var maxRetransmissions = 2;
|
var maxRetransmissions = 4;
|
||||||
UdpClient udpClient = new UdpClient();
|
UdpClient udpClient = new UdpClient();
|
||||||
udpClient.Client.ReceiveTimeout = 2000;
|
udpClient.Client.ReceiveTimeout = 2000;
|
||||||
int port = 9000;
|
int port = 9000;
|
||||||
|
|
|
@ -53,6 +53,7 @@ public static class FolderMethods
|
||||||
.Where(f => f.ParentId == parent.Id);
|
.Where(f => f.ParentId == parent.Id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public static IEnumerable<Folder> DescendantFolders(this Folder parent)
|
public static IEnumerable<Folder> DescendantFolders(this Folder parent)
|
||||||
{
|
{
|
||||||
return parent
|
return parent
|
||||||
|
|
|
@ -1,9 +1,5 @@
|
||||||
using System.Net;
|
|
||||||
using System.Text.Json.Nodes;
|
|
||||||
using CliWrap;
|
|
||||||
using InnovEnergy.App.Backend.Database;
|
using InnovEnergy.App.Backend.Database;
|
||||||
using InnovEnergy.App.Backend.Relations;
|
using InnovEnergy.App.Backend.Relations;
|
||||||
using InnovEnergy.Lib.S3Utils;
|
|
||||||
using InnovEnergy.Lib.Utils;
|
using InnovEnergy.Lib.Utils;
|
||||||
|
|
||||||
namespace InnovEnergy.App.Backend.DataTypes.Methods;
|
namespace InnovEnergy.App.Backend.DataTypes.Methods;
|
||||||
|
@ -12,20 +8,29 @@ namespace InnovEnergy.App.Backend.DataTypes.Methods;
|
||||||
public static class InstallationMethods
|
public static class InstallationMethods
|
||||||
{
|
{
|
||||||
private static readonly String BucketNameSalt =
|
private static readonly String BucketNameSalt =
|
||||||
Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") == ""
|
// Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") == ""
|
||||||
? "stage"
|
// ? "stage" :"3e5b3069-214a-43ee-8d85-57d72000c19d";
|
||||||
:"3e5b3069-214a-43ee-8d85-57d72000c19d";
|
"3e5b3069-214a-43ee-8d85-57d72000c19d";
|
||||||
|
|
||||||
|
private static readonly String SalidomoBucketNameSalt = "c0436b6a-d276-4cd8-9c44-1eae86cf5d0e";
|
||||||
|
|
||||||
public static String BucketName(this Installation installation)
|
public static String BucketName(this Installation installation)
|
||||||
|
{
|
||||||
|
if (installation.Product == 0)
|
||||||
{
|
{
|
||||||
return $"{installation.Id}-{BucketNameSalt}";
|
return $"{installation.Id}-{BucketNameSalt}";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return $"{installation.Id}-{SalidomoBucketNameSalt}";
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
public static async Task<Boolean> RenewS3Credentials(this Installation installation)
|
public static async Task<Boolean> RenewS3Credentials(this Installation installation)
|
||||||
{
|
{
|
||||||
if(!installation.S3Key.IsNullOrEmpty())
|
if(!installation.S3Key.IsNullOrEmpty())
|
||||||
await installation.RevokeReadKey();
|
await installation.RevokeReadKey();
|
||||||
|
|
||||||
|
|
||||||
var (key,secret) = await installation.CreateReadKey();
|
var (key,secret) = await installation.CreateReadKey();
|
||||||
|
|
||||||
installation.S3Key = key;
|
installation.S3Key = key;
|
||||||
|
|
|
@ -118,25 +118,41 @@ public static class SessionMethods
|
||||||
{
|
{
|
||||||
var user = session?.User;
|
var user = session?.User;
|
||||||
|
|
||||||
|
//Salimax installation
|
||||||
|
if (installation.Product==0)
|
||||||
|
{
|
||||||
return user is not null
|
return user is not null
|
||||||
&& installation is not null
|
&& user.UserType != 0
|
||||||
&& user.UserType !=0
|
|
||||||
&& user.HasAccessToParentOf(installation)
|
&& user.HasAccessToParentOf(installation)
|
||||||
&& Db.Create(installation) // TODO: these two in a transaction
|
&& Db.Create(installation) // TODO: these two in a transaction
|
||||||
&& installation.SetOrderNumbers()
|
&& installation.SetOrderNumbers()
|
||||||
&& Db.Create(new InstallationAccess { UserId = user.Id, InstallationId = installation.Id })
|
&& Db.Create(new InstallationAccess { UserId = user.Id, InstallationId = installation.Id })
|
||||||
&& await installation.CreateBucket()
|
&& await installation.CreateBucket()
|
||||||
&& await installation.RenewS3Credentials(); // generation of access _after_ generation of
|
&& await installation.RenewS3Credentials();
|
||||||
// bucket to prevent "zombie" access-rights.
|
|
||||||
// This might ** us over if the creation of access rights fails,
|
|
||||||
// as bucket-names are unique and bound to the installation id... -K
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (installation.Product==1)
|
||||||
|
{
|
||||||
|
return user is not null
|
||||||
|
&& user.UserType != 0
|
||||||
|
&& user.HasAccessToParentOf(installation)
|
||||||
|
&& Db.Create(installation)
|
||||||
|
&& await installation.CreateBucket()
|
||||||
|
&& await installation.RenewS3Credentials();
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
public static Boolean Update(this Session? session, Installation? installation)
|
public static Boolean Update(this Session? session, Installation? installation)
|
||||||
{
|
{
|
||||||
var user = session?.User;
|
var user = session?.User;
|
||||||
|
|
||||||
var original = Db.GetInstallationById(installation?.Id);
|
var original = Db.GetInstallationById(installation?.Id);
|
||||||
|
//Salimax installation
|
||||||
|
if (installation.Product==0)
|
||||||
|
{
|
||||||
|
|
||||||
return user is not null
|
return user is not null
|
||||||
&& installation is not null
|
&& installation is not null
|
||||||
|
@ -149,16 +165,33 @@ public static class SessionMethods
|
||||||
.Apply(Db.Update);
|
.Apply(Db.Update);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (installation.Product==1)
|
||||||
|
{
|
||||||
|
return user is not null
|
||||||
|
&& installation is not null
|
||||||
|
&& original is not null
|
||||||
|
&& user.UserType !=0
|
||||||
|
&& user.HasAccessToParentOf(installation)
|
||||||
|
&& installation
|
||||||
|
.Apply(Db.Update);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
public static async Task<Boolean> Delete(this Session? session, Installation? installation)
|
public static async Task<Boolean> Delete(this Session? session, Installation? installation)
|
||||||
{
|
{
|
||||||
var user = session?.User;
|
var user = session?.User;
|
||||||
|
|
||||||
|
|
||||||
return user is not null
|
return user is not null
|
||||||
&& installation is not null
|
&& installation is not null
|
||||||
&& user.UserType !=0
|
&& user.UserType != 0
|
||||||
&& user.HasAccessTo(installation)
|
&& user.HasAccessTo(installation)
|
||||||
&& Db.Delete(installation)
|
&& Db.Delete(installation)
|
||||||
&& await installation.DeleteBucket();
|
&& await installation.DeleteBucket();
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Boolean Create(this Session? session, User newUser)
|
public static Boolean Create(this Session? session, User newUser)
|
||||||
|
|
|
@ -12,18 +12,19 @@ namespace InnovEnergy.App.Backend.DataTypes.Methods;
|
||||||
|
|
||||||
public static class UserMethods
|
public static class UserMethods
|
||||||
{
|
{
|
||||||
public static IEnumerable<Installation> AccessibleInstallations(this User user)
|
public static IEnumerable<Installation> AccessibleInstallations(this User user,int product)
|
||||||
{
|
{
|
||||||
var direct = user.DirectlyAccessibleInstallations().ToList();
|
var direct = user.DirectlyAccessibleInstallations().ToList().Where(f=>f.Product==product);
|
||||||
var fromFolders = user
|
var fromFolders = user
|
||||||
.AccessibleFolders()
|
.AccessibleFolders()
|
||||||
.SelectMany(u => u.ChildInstallations()).ToList();
|
.SelectMany(u => u.ChildInstallations()).ToList().Where(f=>f.Product==product);
|
||||||
|
|
||||||
return direct
|
return direct
|
||||||
.Concat(fromFolders)
|
.Concat(fromFolders)
|
||||||
.Distinct();
|
.Distinct();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public static IEnumerable<Folder> AccessibleFolders(this User user)
|
public static IEnumerable<Folder> AccessibleFolders(this User user)
|
||||||
{
|
{
|
||||||
return user
|
return user
|
||||||
|
@ -32,12 +33,12 @@ public static class UserMethods
|
||||||
.Distinct();
|
.Distinct();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static IEnumerable<TreeNode> AccessibleFoldersAndInstallations(this User user)
|
public static IEnumerable<TreeNode> AccessibleFoldersAndInstallations(this User user,int product)
|
||||||
{
|
{
|
||||||
var folders = user.AccessibleFolders() as IEnumerable<TreeNode>;
|
var folders = user.AccessibleFolders() as IEnumerable<TreeNode>;
|
||||||
|
|
||||||
user.AccessibleInstallations().ForEach(i => i.FillOrderNumbers());
|
user.AccessibleInstallations(product).ForEach(i => i.FillOrderNumbers());
|
||||||
var installations = user.AccessibleInstallations();
|
var installations = user.AccessibleInstallations(product);
|
||||||
|
|
||||||
return folders.Concat(installations);
|
return folders.Concat(installations);
|
||||||
}
|
}
|
||||||
|
@ -158,6 +159,7 @@ public static class UserMethods
|
||||||
.Any(user.HasDirectAccessTo);
|
.Any(user.HasDirectAccessTo);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public static Boolean HasAccessTo(this User user, User? other)
|
public static Boolean HasAccessTo(this User user, User? other)
|
||||||
{
|
{
|
||||||
if (other is null)
|
if (other is null)
|
||||||
|
@ -169,19 +171,10 @@ public static class UserMethods
|
||||||
.Contains(user);
|
.Contains(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Boolean HasAccessTo(this User user, TreeNode? other)
|
|
||||||
{
|
|
||||||
return other?.Type switch
|
|
||||||
{
|
|
||||||
"installation" => user.HasAccessTo((Installation)other),
|
|
||||||
"user" => user.HasAccessTo((User)other),
|
|
||||||
"folder" => user.HasAccessTo((Folder)other),
|
|
||||||
_ => false
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Boolean HasAccessToParentOf(this User user, TreeNode? other)
|
public static Boolean HasAccessToParentOf(this User user, TreeNode? other)
|
||||||
{
|
{
|
||||||
|
|
||||||
return other?.Type switch
|
return other?.Type switch
|
||||||
{
|
{
|
||||||
"Installation" => user.HasAccessTo(Db.GetFolderById(other.ParentId)),
|
"Installation" => user.HasAccessTo(Db.GetFolderById(other.ParentId)),
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
using System.Reactive.Concurrency;
|
|
||||||
using System.Reactive.Linq;
|
|
||||||
using InnovEnergy.App.Backend.DataTypes;
|
using InnovEnergy.App.Backend.DataTypes;
|
||||||
using InnovEnergy.App.Backend.DataTypes.Methods;
|
using InnovEnergy.App.Backend.DataTypes.Methods;
|
||||||
using InnovEnergy.App.Backend.Relations;
|
using InnovEnergy.App.Backend.Relations;
|
||||||
|
@ -143,11 +141,15 @@ public static partial class Db
|
||||||
.Distinct()
|
.Distinct()
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
var validWriteKeys = Installations
|
var validWriteKeys = Installations
|
||||||
.Select(i => i.S3WriteKey)
|
.Select(i => i.S3WriteKey)
|
||||||
.Distinct()
|
.Distinct()
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const String provider = "exo.io";
|
const String provider = "exo.io";
|
||||||
var S3keys = await ExoCmd.GetAccessKeys();
|
var S3keys = await ExoCmd.GetAccessKeys();
|
||||||
|
|
||||||
|
@ -243,6 +245,7 @@ public static partial class Db
|
||||||
{
|
{
|
||||||
await installation.RenewS3Credentials();
|
await installation.RenewS3Credentials();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -72,10 +72,16 @@ public static partial class Db
|
||||||
|
|
||||||
|
|
||||||
Boolean DeleteInstallationAndItsDependencies()
|
Boolean DeleteInstallationAndItsDependencies()
|
||||||
|
{
|
||||||
|
if (installation.Product == 0)
|
||||||
{
|
{
|
||||||
InstallationAccess.Delete(i => i.InstallationId == installation.Id);
|
InstallationAccess.Delete(i => i.InstallationId == installation.Id);
|
||||||
OrderNumber2Installation.Delete(i => i.InstallationId == installation.Id);
|
OrderNumber2Installation.Delete(i => i.InstallationId == installation.Id);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
return Installations.Delete(i => i.Id == installation.Id) > 0;
|
return Installations.Delete(i => i.Id == installation.Id) > 0;
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -9,4 +9,5 @@ Salimax0005 ie-entwicklung@10.2.4.36 Schreinerei Schönthal (Thu
|
||||||
Salimax0006 ie-entwicklung@10.2.4.35 Steakhouse Mettmenstetten
|
Salimax0006 ie-entwicklung@10.2.4.35 Steakhouse Mettmenstetten
|
||||||
Salimax0007 ie-entwicklung@10.2.4.154 LerchenhofHerr Twannberg
|
Salimax0007 ie-entwicklung@10.2.4.154 LerchenhofHerr Twannberg
|
||||||
Salimax0008 ie-entwicklung@10.2.4.113 Wittmann Kottingbrunn
|
Salimax0008 ie-entwicklung@10.2.4.113 Wittmann Kottingbrunn
|
||||||
|
Salimax0010 ie-entwicklung@10.2.4.211 Mahotech 1
|
||||||
SalidomoServer ig@134.209.238.170
|
SalidomoServer ig@134.209.238.170
|
|
@ -20,6 +20,7 @@ import { axiosConfigWithoutToken } from './Resources/axiosConfig';
|
||||||
import InstallationsContextProvider from './contexts/InstallationsContextProvider';
|
import InstallationsContextProvider from './contexts/InstallationsContextProvider';
|
||||||
import AccessContextProvider from './contexts/AccessContextProvider';
|
import AccessContextProvider from './contexts/AccessContextProvider';
|
||||||
import WebSocketContextProvider from './contexts/WebSocketContextProvider';
|
import WebSocketContextProvider from './contexts/WebSocketContextProvider';
|
||||||
|
import SalidomoInstallationTabs from './content/dashboards/SalidomoInstallations';
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const context = useContext(UserContext);
|
const context = useContext(UserContext);
|
||||||
|
@ -146,6 +147,18 @@ function App() {
|
||||||
</AccessContextProvider>
|
</AccessContextProvider>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<Route
|
||||||
|
path={routes.salidomo_installations + '*'}
|
||||||
|
element={
|
||||||
|
<AccessContextProvider>
|
||||||
|
<InstallationsContextProvider>
|
||||||
|
<SalidomoInstallationTabs />
|
||||||
|
</InstallationsContextProvider>
|
||||||
|
</AccessContextProvider>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
<Route path={routes.users + '*'} element={<Users />} />
|
<Route path={routes.users + '*'} element={<Users />} />
|
||||||
<Route
|
<Route
|
||||||
path={'*'}
|
path={'*'}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
{
|
{
|
||||||
"users": "/users/",
|
"users": "/users/",
|
||||||
"installations": "/installations/",
|
"installations": "/installations/",
|
||||||
|
"salidomo_installations": "/salidomo_installations/",
|
||||||
"installation": "installation/",
|
"installation": "installation/",
|
||||||
"login": "/login/",
|
"login": "/login/",
|
||||||
"forgotPassword": "/forgotPassword/",
|
"forgotPassword": "/forgotPassword/",
|
||||||
|
|
|
@ -116,7 +116,7 @@ function Configuration(props: ConfigurationProps) {
|
||||||
setErrorDateModalOpen(true);
|
setErrorDateModalOpen(true);
|
||||||
return;
|
return;
|
||||||
} else if (
|
} else if (
|
||||||
formValues.CalibrationChargeState != 2 &&
|
formValues.CalibrationChargeState === 1 &&
|
||||||
dayjs(formValues.calibrationChargeDate).isBefore(dayjs())
|
dayjs(formValues.calibrationChargeDate).isBefore(dayjs())
|
||||||
) {
|
) {
|
||||||
//console.log('asked for', dayjs(formValues.calibrationChargeDate));
|
//console.log('asked for', dayjs(formValues.calibrationChargeDate));
|
||||||
|
|
|
@ -0,0 +1,389 @@
|
||||||
|
import {
|
||||||
|
Alert,
|
||||||
|
Box,
|
||||||
|
CardContent,
|
||||||
|
CircularProgress,
|
||||||
|
Container,
|
||||||
|
Grid,
|
||||||
|
IconButton,
|
||||||
|
Modal,
|
||||||
|
TextField,
|
||||||
|
Typography,
|
||||||
|
useTheme
|
||||||
|
} from '@mui/material';
|
||||||
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
import Button from '@mui/material/Button';
|
||||||
|
import { Close as CloseIcon } from '@mui/icons-material';
|
||||||
|
import React, { useContext, useState } from 'react';
|
||||||
|
import { I_S3Credentials } from '../../../interfaces/S3Types';
|
||||||
|
import { I_Installation } from '../../../interfaces/InstallationTypes';
|
||||||
|
import { InstallationsContext } from '../../../contexts/InstallationsContextProvider';
|
||||||
|
import { UserContext } from '../../../contexts/userContext';
|
||||||
|
import routes from '../../../Resources/routes.json';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
|
interface InformationSalidomoProps {
|
||||||
|
values: I_Installation;
|
||||||
|
s3Credentials: I_S3Credentials;
|
||||||
|
type?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function InformationSalidomo(props: InformationSalidomoProps) {
|
||||||
|
if (props.values === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const context = useContext(UserContext);
|
||||||
|
const { currentUser } = context;
|
||||||
|
const theme = useTheme();
|
||||||
|
const [formValues, setFormValues] = useState(props.values);
|
||||||
|
const requiredFields = ['name', 'region', 'location', 'country'];
|
||||||
|
const [openModalDeleteInstallation, setOpenModalDeleteInstallation] =
|
||||||
|
useState(false);
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const installationContext = useContext(InstallationsContext);
|
||||||
|
const {
|
||||||
|
updateInstallation,
|
||||||
|
deleteInstallation,
|
||||||
|
loading,
|
||||||
|
setLoading,
|
||||||
|
error,
|
||||||
|
setError,
|
||||||
|
updated,
|
||||||
|
setUpdated
|
||||||
|
} = installationContext;
|
||||||
|
|
||||||
|
const handleChange = (e) => {
|
||||||
|
const { name, value } = e.target;
|
||||||
|
setFormValues({
|
||||||
|
...formValues,
|
||||||
|
[name]: value
|
||||||
|
});
|
||||||
|
};
|
||||||
|
const handleSubmit = () => {
|
||||||
|
setLoading(true);
|
||||||
|
setError(false);
|
||||||
|
updateInstallation(formValues, props.type);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDelete = () => {
|
||||||
|
setLoading(true);
|
||||||
|
setError(false);
|
||||||
|
setOpenModalDeleteInstallation(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteInstallationModalHandle = () => {
|
||||||
|
setOpenModalDeleteInstallation(false);
|
||||||
|
deleteInstallation(formValues, props.type);
|
||||||
|
setLoading(false);
|
||||||
|
|
||||||
|
navigate(routes.salidomo_installations + routes.list, {
|
||||||
|
replace: true
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteInstallationModalHandleCancel = () => {
|
||||||
|
setOpenModalDeleteInstallation(false);
|
||||||
|
setLoading(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const areRequiredFieldsFilled = () => {
|
||||||
|
for (const field of requiredFields) {
|
||||||
|
if (!formValues[field]) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{openModalDeleteInstallation && (
|
||||||
|
<Modal
|
||||||
|
open={openModalDeleteInstallation}
|
||||||
|
onClose={deleteInstallationModalHandleCancel}
|
||||||
|
aria-labelledby="error-modal"
|
||||||
|
aria-describedby="error-modal-description"
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
position: 'absolute',
|
||||||
|
top: '50%',
|
||||||
|
left: '50%',
|
||||||
|
transform: 'translate(-50%, -50%)',
|
||||||
|
width: 350,
|
||||||
|
bgcolor: 'background.paper',
|
||||||
|
borderRadius: 4,
|
||||||
|
boxShadow: 24,
|
||||||
|
p: 4,
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
alignItems: 'center'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography
|
||||||
|
variant="body1"
|
||||||
|
gutterBottom
|
||||||
|
sx={{ fontWeight: 'bold' }}
|
||||||
|
>
|
||||||
|
Do you want to delete this installation?
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
marginTop: 10
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
sx={{
|
||||||
|
marginTop: 2,
|
||||||
|
textTransform: 'none',
|
||||||
|
bgcolor: '#ffc04d',
|
||||||
|
color: '#111111',
|
||||||
|
'&:hover': { bgcolor: '#f7b34d' }
|
||||||
|
}}
|
||||||
|
onClick={deleteInstallationModalHandle}
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
sx={{
|
||||||
|
marginTop: 2,
|
||||||
|
marginLeft: 2,
|
||||||
|
textTransform: 'none',
|
||||||
|
bgcolor: '#ffc04d',
|
||||||
|
color: '#111111',
|
||||||
|
'&:hover': { bgcolor: '#f7b34d' }
|
||||||
|
}}
|
||||||
|
onClick={deleteInstallationModalHandleCancel}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Box>
|
||||||
|
</Modal>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Container maxWidth="xl">
|
||||||
|
<Grid
|
||||||
|
container
|
||||||
|
direction="row"
|
||||||
|
justifyContent="center"
|
||||||
|
alignItems="stretch"
|
||||||
|
spacing={3}
|
||||||
|
>
|
||||||
|
<Grid item xs={12} md={12}>
|
||||||
|
<CardContent>
|
||||||
|
<Box
|
||||||
|
component="form"
|
||||||
|
sx={{
|
||||||
|
'& .MuiTextField-root': { m: 1, width: '50ch' }
|
||||||
|
}}
|
||||||
|
noValidate
|
||||||
|
autoComplete="off"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<TextField
|
||||||
|
label={
|
||||||
|
<FormattedMessage
|
||||||
|
id="installation_name"
|
||||||
|
defaultMessage="Installation Name"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
name="installationName"
|
||||||
|
value={formValues.name}
|
||||||
|
onChange={handleChange}
|
||||||
|
variant="outlined"
|
||||||
|
fullWidth
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<TextField
|
||||||
|
label={
|
||||||
|
<FormattedMessage id="region" defaultMessage="Region" />
|
||||||
|
}
|
||||||
|
name="region"
|
||||||
|
value={formValues.region}
|
||||||
|
onChange={handleChange}
|
||||||
|
variant="outlined"
|
||||||
|
fullWidth
|
||||||
|
required
|
||||||
|
error={formValues.region === ''}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<TextField
|
||||||
|
label={
|
||||||
|
<FormattedMessage
|
||||||
|
id="location"
|
||||||
|
defaultMessage="Location"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
name="location"
|
||||||
|
value={formValues.location}
|
||||||
|
onChange={handleChange}
|
||||||
|
variant="outlined"
|
||||||
|
fullWidth
|
||||||
|
required
|
||||||
|
error={formValues.location === ''}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<TextField
|
||||||
|
label={
|
||||||
|
<FormattedMessage id="country" defaultMessage="Country" />
|
||||||
|
}
|
||||||
|
name="country"
|
||||||
|
value={formValues.country}
|
||||||
|
onChange={handleChange}
|
||||||
|
variant="outlined"
|
||||||
|
fullWidth
|
||||||
|
required
|
||||||
|
error={formValues.country === ''}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<TextField
|
||||||
|
label={
|
||||||
|
<FormattedMessage id="vpnip" defaultMessage="VPN IP" />
|
||||||
|
}
|
||||||
|
name="vpnIp"
|
||||||
|
value={formValues.vpnIp}
|
||||||
|
onChange={handleChange}
|
||||||
|
variant="outlined"
|
||||||
|
fullWidth
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<TextField
|
||||||
|
label={
|
||||||
|
<FormattedMessage
|
||||||
|
id="information"
|
||||||
|
defaultMessage="Information"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
name="information"
|
||||||
|
value={formValues.information}
|
||||||
|
onChange={handleChange}
|
||||||
|
variant="outlined"
|
||||||
|
fullWidth
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<TextField
|
||||||
|
label="S3 Bucket Name"
|
||||||
|
name="s3writesecretkey"
|
||||||
|
value={
|
||||||
|
formValues.id + '-c0436b6a-d276-4cd8-9c44-1eae86cf5d0e'
|
||||||
|
}
|
||||||
|
variant="outlined"
|
||||||
|
fullWidth
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
marginTop: 10
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
onClick={handleSubmit}
|
||||||
|
sx={{
|
||||||
|
marginLeft: '10px'
|
||||||
|
}}
|
||||||
|
disabled={!areRequiredFieldsFilled()}
|
||||||
|
>
|
||||||
|
<FormattedMessage
|
||||||
|
id="applyChanges"
|
||||||
|
defaultMessage="Apply Changes"
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
onClick={handleDelete}
|
||||||
|
sx={{
|
||||||
|
marginLeft: '10px'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FormattedMessage
|
||||||
|
id="deleteInstallation"
|
||||||
|
defaultMessage="Delete Installation"
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{loading && (
|
||||||
|
<CircularProgress
|
||||||
|
sx={{
|
||||||
|
color: theme.palette.primary.main,
|
||||||
|
marginLeft: '20px'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{error && (
|
||||||
|
<Alert
|
||||||
|
severity="error"
|
||||||
|
sx={{
|
||||||
|
ml: 1,
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FormattedMessage
|
||||||
|
id="errorOccured"
|
||||||
|
defaultMessage="An error has occurred"
|
||||||
|
/>
|
||||||
|
<IconButton
|
||||||
|
color="inherit"
|
||||||
|
size="small"
|
||||||
|
onClick={() => setError(false)}
|
||||||
|
sx={{ marginLeft: '4px' }}
|
||||||
|
>
|
||||||
|
<CloseIcon fontSize="small" />
|
||||||
|
</IconButton>
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
{updated && (
|
||||||
|
<Alert
|
||||||
|
severity="success"
|
||||||
|
sx={{
|
||||||
|
ml: 1,
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FormattedMessage
|
||||||
|
id="successfullyUpdated"
|
||||||
|
defaultMessage="Successfully updated"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<IconButton
|
||||||
|
color="inherit"
|
||||||
|
size="small"
|
||||||
|
onClick={() => setUpdated(false)} // Set error state to false on click
|
||||||
|
sx={{ marginLeft: '4px' }}
|
||||||
|
>
|
||||||
|
<CloseIcon fontSize="small" />
|
||||||
|
</IconButton>
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Box>
|
||||||
|
</CardContent>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</Container>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default InformationSalidomo;
|
|
@ -52,8 +52,11 @@ function Installation(props: singleInstallationProps) {
|
||||||
};
|
};
|
||||||
|
|
||||||
const s3Bucket =
|
const s3Bucket =
|
||||||
props.current_installation.id.toString() +
|
props.current_installation.product === 0
|
||||||
'-3e5b3069-214a-43ee-8d85-57d72000c19d';
|
? props.current_installation.id.toString() +
|
||||||
|
'-3e5b3069-214a-43ee-8d85-57d72000c19d'
|
||||||
|
: props.current_installation.id.toString() +
|
||||||
|
'-c0436b6a-d276-4cd8-9c44-1eae86cf5d0e';
|
||||||
|
|
||||||
const s3Credentials = { s3Bucket, ...S3data };
|
const s3Credentials = { s3Bucket, ...S3data };
|
||||||
|
|
||||||
|
|
|
@ -30,7 +30,7 @@ function InstallationTabs() {
|
||||||
];
|
];
|
||||||
|
|
||||||
const [currentTab, setCurrentTab] = useState<string>(undefined);
|
const [currentTab, setCurrentTab] = useState<string>(undefined);
|
||||||
const { installations, fetchAllInstallations } =
|
const { salimaxInstallations, fetchAllInstallations } =
|
||||||
useContext(InstallationsContext);
|
useContext(InstallationsContext);
|
||||||
|
|
||||||
const webSocketsContext = useContext(WebSocketContext);
|
const webSocketsContext = useContext(WebSocketContext);
|
||||||
|
@ -50,16 +50,16 @@ function InstallationTabs() {
|
||||||
}, [location]);
|
}, [location]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!socket && installations.length > 0) {
|
if (!socket && salimaxInstallations.length > 0) {
|
||||||
openSocket(installations);
|
openSocket(salimaxInstallations);
|
||||||
}
|
}
|
||||||
}, [installations]);
|
}, [salimaxInstallations]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (installations.length === 0) {
|
if (salimaxInstallations.length === 0) {
|
||||||
fetchAllInstallations();
|
fetchAllInstallations();
|
||||||
}
|
}
|
||||||
}, [installations]);
|
}, [salimaxInstallations]);
|
||||||
|
|
||||||
const handleTabsChange = (_event: ChangeEvent<{}>, value: string): void => {
|
const handleTabsChange = (_event: ChangeEvent<{}>, value: string): void => {
|
||||||
setCurrentTab(value);
|
setCurrentTab(value);
|
||||||
|
@ -270,7 +270,7 @@ function InstallationTabs() {
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
return installations.length > 1 ? (
|
return salimaxInstallations.length > 1 ? (
|
||||||
<>
|
<>
|
||||||
<Container maxWidth="xl" sx={{ marginTop: '20px' }} className="mainframe">
|
<Container maxWidth="xl" sx={{ marginTop: '20px' }} className="mainframe">
|
||||||
<TabsContainerWrapper>
|
<TabsContainerWrapper>
|
||||||
|
@ -312,7 +312,9 @@ function InstallationTabs() {
|
||||||
element={
|
element={
|
||||||
<Grid item xs={12}>
|
<Grid item xs={12}>
|
||||||
<Box p={4}>
|
<Box p={4}>
|
||||||
<InstallationSearch installations={installations} />
|
<InstallationSearch
|
||||||
|
installations={salimaxInstallations}
|
||||||
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
</Grid>
|
</Grid>
|
||||||
}
|
}
|
||||||
|
@ -330,7 +332,7 @@ function InstallationTabs() {
|
||||||
</Container>
|
</Container>
|
||||||
<Footer />
|
<Footer />
|
||||||
</>
|
</>
|
||||||
) : installations.length === 1 ? (
|
) : salimaxInstallations.length === 1 ? (
|
||||||
<>
|
<>
|
||||||
<Container maxWidth="xl" sx={{ marginTop: '20px' }} className="mainframe">
|
<Container maxWidth="xl" sx={{ marginTop: '20px' }} className="mainframe">
|
||||||
<TabsContainerWrapper>
|
<TabsContainerWrapper>
|
||||||
|
@ -368,7 +370,7 @@ function InstallationTabs() {
|
||||||
<Grid item xs={12}>
|
<Grid item xs={12}>
|
||||||
<Box p={4}>
|
<Box p={4}>
|
||||||
<Installation
|
<Installation
|
||||||
current_installation={installations[0]}
|
current_installation={salimaxInstallations[0]}
|
||||||
type="installation"
|
type="installation"
|
||||||
></Installation>
|
></Installation>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
|
@ -51,6 +51,7 @@ function installationForm(props: installationFormProps) {
|
||||||
const handleSubmit = async (e) => {
|
const handleSubmit = async (e) => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
formValues.parentId = props.parentid;
|
formValues.parentId = props.parentid;
|
||||||
|
formValues.product = 0;
|
||||||
const responseData = await createInstallation(formValues);
|
const responseData = await createInstallation(formValues);
|
||||||
props.submit();
|
props.submit();
|
||||||
};
|
};
|
||||||
|
|
|
@ -0,0 +1,208 @@
|
||||||
|
import React, { useContext, useState } from 'react';
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
Grid,
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableContainer,
|
||||||
|
TableHead,
|
||||||
|
TableRow,
|
||||||
|
Typography,
|
||||||
|
useTheme
|
||||||
|
} from '@mui/material';
|
||||||
|
import { I_Installation } from 'src/interfaces/InstallationTypes';
|
||||||
|
import { WebSocketContext } from 'src/contexts/WebSocketContextProvider';
|
||||||
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
import { useLocation, useNavigate } from 'react-router-dom';
|
||||||
|
import routes from '../../../Resources/routes.json';
|
||||||
|
|
||||||
|
interface FlatInstallationViewProps {
|
||||||
|
installations: I_Installation[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const FlatInstallationView = (props: FlatInstallationViewProps) => {
|
||||||
|
const [isRowHovered, setHoveredRow] = useState(-1);
|
||||||
|
const webSocketContext = useContext(WebSocketContext);
|
||||||
|
const { getStatus } = webSocketContext;
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const [selectedInstallation, setSelectedInstallation] = useState<number>(-1);
|
||||||
|
const currentLocation = useLocation();
|
||||||
|
|
||||||
|
const handleSelectOneInstallation = (installationID: number): void => {
|
||||||
|
if (selectedInstallation != installationID) {
|
||||||
|
setSelectedInstallation(installationID);
|
||||||
|
setSelectedInstallation(-1);
|
||||||
|
|
||||||
|
navigate(
|
||||||
|
routes.salidomo_installations +
|
||||||
|
routes.list +
|
||||||
|
routes.installation +
|
||||||
|
`${installationID}` +
|
||||||
|
'/' +
|
||||||
|
routes.batteryview,
|
||||||
|
{
|
||||||
|
replace: true
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
setSelectedInstallation(-1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const theme = useTheme();
|
||||||
|
|
||||||
|
const handleRowMouseEnter = (id: number) => {
|
||||||
|
setHoveredRow(id);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRowMouseLeave = () => {
|
||||||
|
setHoveredRow(-1);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Grid container spacing={1} sx={{ marginTop: 0.1 }}>
|
||||||
|
<Grid
|
||||||
|
item
|
||||||
|
sx={{
|
||||||
|
display:
|
||||||
|
currentLocation.pathname ===
|
||||||
|
routes.salidomo_installations + 'list' ||
|
||||||
|
currentLocation.pathname ===
|
||||||
|
routes.salidomo_installations + routes.list
|
||||||
|
? 'block'
|
||||||
|
: 'none'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Card>
|
||||||
|
<TableContainer>
|
||||||
|
<Table>
|
||||||
|
<TableHead>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell>
|
||||||
|
<FormattedMessage id="name" defaultMessage="Name" />
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<FormattedMessage id="location" defaultMessage="Location" />
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<FormattedMessage id="region" defaultMessage="Region" />
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<FormattedMessage id="country" defaultMessage="Country" />
|
||||||
|
</TableCell>
|
||||||
|
|
||||||
|
<TableCell>
|
||||||
|
<FormattedMessage id="VRM Link" defaultMessage="VRM Link" />
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
</TableHead>
|
||||||
|
<TableBody>
|
||||||
|
{props.installations.map((installation) => {
|
||||||
|
const isInstallationSelected =
|
||||||
|
installation.id === selectedInstallation;
|
||||||
|
|
||||||
|
const status = getStatus(installation.id);
|
||||||
|
const rowStyles =
|
||||||
|
isRowHovered === installation.id
|
||||||
|
? {
|
||||||
|
cursor: 'pointer',
|
||||||
|
backgroundColor: theme.colors.primary.lighter
|
||||||
|
}
|
||||||
|
: {};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TableRow
|
||||||
|
hover
|
||||||
|
key={installation.id}
|
||||||
|
selected={isInstallationSelected}
|
||||||
|
style={rowStyles}
|
||||||
|
onClick={() =>
|
||||||
|
handleSelectOneInstallation(installation.id)
|
||||||
|
}
|
||||||
|
onMouseEnter={() => handleRowMouseEnter(installation.id)}
|
||||||
|
onMouseLeave={() => handleRowMouseLeave()}
|
||||||
|
>
|
||||||
|
<TableCell>
|
||||||
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
fontWeight="bold"
|
||||||
|
color="text.primary"
|
||||||
|
gutterBottom
|
||||||
|
noWrap
|
||||||
|
sx={{ marginTop: '10px', fontSize: 'small' }}
|
||||||
|
>
|
||||||
|
{installation.name}
|
||||||
|
</Typography>
|
||||||
|
</TableCell>
|
||||||
|
|
||||||
|
<TableCell>
|
||||||
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
fontWeight="bold"
|
||||||
|
color="text.primary"
|
||||||
|
gutterBottom
|
||||||
|
noWrap
|
||||||
|
sx={{ marginTop: '10px', fontSize: 'small' }}
|
||||||
|
>
|
||||||
|
{installation.location}
|
||||||
|
</Typography>
|
||||||
|
</TableCell>
|
||||||
|
|
||||||
|
<TableCell>
|
||||||
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
fontWeight="bold"
|
||||||
|
color="text.primary"
|
||||||
|
gutterBottom
|
||||||
|
noWrap
|
||||||
|
sx={{ marginTop: '10px', fontSize: 'small' }}
|
||||||
|
>
|
||||||
|
{installation.region}
|
||||||
|
</Typography>
|
||||||
|
</TableCell>
|
||||||
|
|
||||||
|
<TableCell>
|
||||||
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
fontWeight="bold"
|
||||||
|
color="text.primary"
|
||||||
|
gutterBottom
|
||||||
|
noWrap
|
||||||
|
sx={{ marginTop: '10px', fontSize: 'small' }}
|
||||||
|
>
|
||||||
|
{installation.country}
|
||||||
|
</Typography>
|
||||||
|
</TableCell>
|
||||||
|
|
||||||
|
<TableCell>
|
||||||
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
fontWeight="bold"
|
||||||
|
color="text.primary"
|
||||||
|
gutterBottom
|
||||||
|
noWrap
|
||||||
|
sx={{ marginTop: '10px', fontSize: 'small' }}
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
href={installation.vrmLink}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
VRM link
|
||||||
|
</a>
|
||||||
|
</Typography>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</TableContainer>
|
||||||
|
</Card>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FlatInstallationView;
|
|
@ -0,0 +1,205 @@
|
||||||
|
import React, { useContext, useEffect, useState } from 'react';
|
||||||
|
import { Card, Grid, Typography } from '@mui/material';
|
||||||
|
import { I_Installation } from 'src/interfaces/InstallationTypes';
|
||||||
|
import { UserContext } from 'src/contexts/userContext';
|
||||||
|
import { TimeSpan, UnixTime } from 'src/dataCache/time';
|
||||||
|
import { FetchResult } from 'src/dataCache/dataCache';
|
||||||
|
import {
|
||||||
|
extractValues,
|
||||||
|
TopologyValues
|
||||||
|
} from 'src/content/dashboards/Log/graph.util';
|
||||||
|
import { WebSocketContext } from 'src/contexts/WebSocketContextProvider';
|
||||||
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
import Overview from '../Overview/overview';
|
||||||
|
import { fetchData } from 'src/content/dashboards/Installations/fetchData';
|
||||||
|
import { Navigate, Route, Routes, useLocation } from 'react-router-dom';
|
||||||
|
import routes from '../../../Resources/routes.json';
|
||||||
|
import BatteryView from '../BatteryView/BatteryView';
|
||||||
|
import InformationSalidomo from '../Information/InformationSalidomo';
|
||||||
|
|
||||||
|
interface singleInstallationProps {
|
||||||
|
current_installation?: I_Installation;
|
||||||
|
type?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function Installation(props: singleInstallationProps) {
|
||||||
|
const context = useContext(UserContext);
|
||||||
|
const { currentUser } = context;
|
||||||
|
const location = useLocation().pathname;
|
||||||
|
const [errorLoadingS3Data, setErrorLoadingS3Data] = useState(false);
|
||||||
|
const webSocketsContext = useContext(WebSocketContext);
|
||||||
|
const { getStatus } = webSocketsContext;
|
||||||
|
const [currentTab, setCurrentTab] = useState<string>(undefined);
|
||||||
|
const [values, setValues] = useState<TopologyValues | null>(null);
|
||||||
|
const status = getStatus(props.current_installation.id);
|
||||||
|
|
||||||
|
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
|
||||||
|
};
|
||||||
|
|
||||||
|
const s3Bucket =
|
||||||
|
props.current_installation.id.toString() +
|
||||||
|
'-' +
|
||||||
|
'c0436b6a-d276-4cd8-9c44-1eae86cf5d0e';
|
||||||
|
|
||||||
|
const s3Credentials = { s3Bucket, ...S3data };
|
||||||
|
|
||||||
|
const fetchDataPeriodically = async () => {
|
||||||
|
const now = UnixTime.now().earlier(TimeSpan.fromSeconds(20));
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetchData(now, s3Credentials);
|
||||||
|
|
||||||
|
if (res != FetchResult.notAvailable && res != FetchResult.tryLater) {
|
||||||
|
setValues(
|
||||||
|
extractValues({
|
||||||
|
time: now,
|
||||||
|
value: res
|
||||||
|
})
|
||||||
|
);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchDataOnlyOneTime = async () => {
|
||||||
|
let success = false;
|
||||||
|
const max_retransmissions = 3;
|
||||||
|
|
||||||
|
for (let i = 0; i < max_retransmissions; i++) {
|
||||||
|
success = await fetchDataPeriodically();
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||||
|
if (success) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let path = location.split('/');
|
||||||
|
|
||||||
|
setCurrentTab(path[path.length - 1]);
|
||||||
|
}, [location]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (
|
||||||
|
currentTab == 'live' ||
|
||||||
|
currentTab == 'configuration' ||
|
||||||
|
location.includes('batteryview')
|
||||||
|
) {
|
||||||
|
let interval;
|
||||||
|
|
||||||
|
if (
|
||||||
|
currentTab == 'live' ||
|
||||||
|
(location.includes('batteryview') && !location.includes('mainstats'))
|
||||||
|
) {
|
||||||
|
fetchDataPeriodically();
|
||||||
|
interval = setInterval(fetchDataPeriodically, 2000);
|
||||||
|
}
|
||||||
|
if (currentTab == 'configuration' || location.includes('mainstats')) {
|
||||||
|
fetchDataOnlyOneTime();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cleanup function to cancel interval and update isMounted when unmounted
|
||||||
|
return () => {
|
||||||
|
if (
|
||||||
|
currentTab == 'live' ||
|
||||||
|
(location.includes('batteryview') && !location.includes('mainstats'))
|
||||||
|
) {
|
||||||
|
clearInterval(interval);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}, [currentTab, location]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Grid item xs={12} md={12}>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center' }}>
|
||||||
|
<Typography
|
||||||
|
fontWeight="bold"
|
||||||
|
color="text.primary"
|
||||||
|
noWrap
|
||||||
|
sx={{
|
||||||
|
marginTop: '-20px',
|
||||||
|
marginBottom: '10px',
|
||||||
|
fontSize: '14px'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FormattedMessage
|
||||||
|
id="installation_name_simple"
|
||||||
|
defaultMessage="Installation Name:"
|
||||||
|
/>
|
||||||
|
</Typography>
|
||||||
|
<Typography
|
||||||
|
fontWeight="bold"
|
||||||
|
color="orange"
|
||||||
|
noWrap
|
||||||
|
sx={{
|
||||||
|
marginTop: '-20px',
|
||||||
|
marginBottom: '10px',
|
||||||
|
marginLeft: '5px',
|
||||||
|
fontSize: '14px'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{props.current_installation.name}
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Card variant="outlined">
|
||||||
|
<Grid
|
||||||
|
container
|
||||||
|
direction="row"
|
||||||
|
justifyContent="center"
|
||||||
|
alignItems="stretch"
|
||||||
|
spacing={0}
|
||||||
|
>
|
||||||
|
<Routes>
|
||||||
|
<Route
|
||||||
|
path={routes.information}
|
||||||
|
element={
|
||||||
|
<InformationSalidomo
|
||||||
|
values={props.current_installation}
|
||||||
|
s3Credentials={s3Credentials}
|
||||||
|
type={props.type}
|
||||||
|
></InformationSalidomo>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Route
|
||||||
|
path={routes.batteryview + '*'}
|
||||||
|
element={
|
||||||
|
<BatteryView
|
||||||
|
values={values}
|
||||||
|
s3Credentials={s3Credentials}
|
||||||
|
installationId={props.current_installation.id}
|
||||||
|
></BatteryView>
|
||||||
|
}
|
||||||
|
></Route>
|
||||||
|
<Route
|
||||||
|
path={routes.overview}
|
||||||
|
element={<Overview s3Credentials={s3Credentials}></Overview>}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Route
|
||||||
|
path={'*'}
|
||||||
|
element={<Navigate to={routes.information}></Navigate>}
|
||||||
|
/>
|
||||||
|
</Routes>
|
||||||
|
</Grid>
|
||||||
|
</Card>
|
||||||
|
</Grid>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Installation;
|
|
@ -0,0 +1,127 @@
|
||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { FormControl, Grid, InputAdornment, TextField } from '@mui/material';
|
||||||
|
import SearchTwoToneIcon from '@mui/icons-material/SearchTwoTone';
|
||||||
|
import FlatInstallationView from './FlatInstallationView';
|
||||||
|
import { I_Installation } from '../../../interfaces/InstallationTypes';
|
||||||
|
import { Route, Routes, useLocation } from 'react-router-dom';
|
||||||
|
import routes from '../../../Resources/routes.json';
|
||||||
|
import Installation from './Installation';
|
||||||
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
import Button from '@mui/material/Button';
|
||||||
|
import SalidomonstallationForm from './SalidomoInstallationForm';
|
||||||
|
|
||||||
|
interface installationSearchProps {
|
||||||
|
installations: I_Installation[];
|
||||||
|
}
|
||||||
|
|
||||||
|
function InstallationSearch(props: installationSearchProps) {
|
||||||
|
const [searchTerm, setSearchTerm] = useState('');
|
||||||
|
const currentLocation = useLocation();
|
||||||
|
const [filteredData, setFilteredData] = useState(props.installations);
|
||||||
|
const [openModalInstallation, setOpenModalInstallation] = useState(false);
|
||||||
|
|
||||||
|
const handleNewInstallationInsertion = () => {
|
||||||
|
setOpenModalInstallation(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleInstallationFormSubmit = () => {
|
||||||
|
setOpenModalInstallation(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFormCancel = () => {
|
||||||
|
setOpenModalInstallation(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const filtered = props.installations.filter(
|
||||||
|
(item) =>
|
||||||
|
item.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||||
|
item.location.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||||
|
item.region.toLowerCase().includes(searchTerm.toLowerCase())
|
||||||
|
);
|
||||||
|
setFilteredData(filtered);
|
||||||
|
}, [searchTerm, props.installations]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{openModalInstallation && (
|
||||||
|
<SalidomonstallationForm
|
||||||
|
cancel={handleFormCancel}
|
||||||
|
submit={handleInstallationFormSubmit}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<Grid container>
|
||||||
|
<Grid
|
||||||
|
item
|
||||||
|
xs={12}
|
||||||
|
md={6}
|
||||||
|
sx={{
|
||||||
|
display:
|
||||||
|
currentLocation.pathname ===
|
||||||
|
routes.salidomo_installations + 'list' ||
|
||||||
|
currentLocation.pathname ===
|
||||||
|
routes.salidomo_installations + routes.list
|
||||||
|
? 'block'
|
||||||
|
: 'none'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
alignItems: 'flex-start'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
onClick={handleNewInstallationInsertion}
|
||||||
|
sx={{ marginBottom: '8px' }}
|
||||||
|
>
|
||||||
|
<FormattedMessage
|
||||||
|
id="addNewInstallation"
|
||||||
|
defaultMessage="Add new installation"
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<FormControl variant="outlined">
|
||||||
|
<TextField
|
||||||
|
placeholder="Search"
|
||||||
|
value={searchTerm}
|
||||||
|
onChange={(e) => setSearchTerm(e.target.value)}
|
||||||
|
fullWidth
|
||||||
|
InputProps={{
|
||||||
|
startAdornment: (
|
||||||
|
<InputAdornment position="start">
|
||||||
|
<SearchTwoToneIcon />
|
||||||
|
</InputAdornment>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</div>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<FlatInstallationView installations={filteredData} />
|
||||||
|
<Routes>
|
||||||
|
{filteredData.map((installation) => {
|
||||||
|
return (
|
||||||
|
<Route
|
||||||
|
key={installation.id}
|
||||||
|
path={routes.installation + installation.id + '*'}
|
||||||
|
element={
|
||||||
|
<Installation
|
||||||
|
key={installation.id}
|
||||||
|
current_installation={installation}
|
||||||
|
type="installation"
|
||||||
|
></Installation>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Routes>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default InstallationSearch;
|
|
@ -0,0 +1,258 @@
|
||||||
|
import React, { useContext, useState } from 'react';
|
||||||
|
import {
|
||||||
|
Alert,
|
||||||
|
Box,
|
||||||
|
CircularProgress,
|
||||||
|
IconButton,
|
||||||
|
Modal,
|
||||||
|
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 SalidomoInstallationFormProps {
|
||||||
|
cancel: () => void;
|
||||||
|
submit: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function SalidomonstallationForm(props: SalidomoInstallationFormProps) {
|
||||||
|
const theme = useTheme();
|
||||||
|
const [open, setOpen] = useState(true);
|
||||||
|
const [formValues, setFormValues] = useState<Partial<I_Installation>>({
|
||||||
|
name: '',
|
||||||
|
region: '',
|
||||||
|
location: '',
|
||||||
|
country: '',
|
||||||
|
vpnIp: '',
|
||||||
|
vrmLink: ''
|
||||||
|
});
|
||||||
|
const requiredFields = ['name', 'location', 'country', 'vpnIp', 'vrmLink'];
|
||||||
|
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 = 1;
|
||||||
|
formValues.product = 1;
|
||||||
|
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 (
|
||||||
|
<>
|
||||||
|
<Modal
|
||||||
|
open={open}
|
||||||
|
onClose={() => {}}
|
||||||
|
aria-labelledby="error-modal"
|
||||||
|
aria-describedby="error-modal-description"
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
position: 'absolute',
|
||||||
|
top: isMobile ? '50%' : '40%',
|
||||||
|
left: '50%',
|
||||||
|
transform: 'translate(-50%, -50%)',
|
||||||
|
width: 500,
|
||||||
|
bgcolor: 'background.paper',
|
||||||
|
borderRadius: 4,
|
||||||
|
boxShadow: 24,
|
||||||
|
p: 4
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
component="form"
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
alignItems: 'center', // Center items horizontally
|
||||||
|
'& .MuiTextField-root': {
|
||||||
|
m: 1,
|
||||||
|
width: 390
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
noValidate
|
||||||
|
autoComplete="off"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<TextField
|
||||||
|
label={
|
||||||
|
<FormattedMessage
|
||||||
|
id="installationName"
|
||||||
|
defaultMessage="Installation Name"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
name="name"
|
||||||
|
value={formValues.name}
|
||||||
|
onChange={handleChange}
|
||||||
|
required
|
||||||
|
error={formValues.name === ''}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<TextField
|
||||||
|
label={<FormattedMessage id="region" defaultMessage="Region" />}
|
||||||
|
name="region"
|
||||||
|
value={formValues.region}
|
||||||
|
onChange={handleChange}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<TextField
|
||||||
|
label={
|
||||||
|
<FormattedMessage id="location" defaultMessage="Location" />
|
||||||
|
}
|
||||||
|
name="location"
|
||||||
|
value={formValues.location}
|
||||||
|
onChange={handleChange}
|
||||||
|
required
|
||||||
|
error={formValues.location === ''}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<TextField
|
||||||
|
label={
|
||||||
|
<FormattedMessage id="country" defaultMessage="Country" />
|
||||||
|
}
|
||||||
|
name="country"
|
||||||
|
value={formValues.country}
|
||||||
|
onChange={handleChange}
|
||||||
|
required
|
||||||
|
error={formValues.country === ''}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<TextField
|
||||||
|
label={<FormattedMessage id="VpnIp" defaultMessage="VpnIp" />}
|
||||||
|
name="vpnIp"
|
||||||
|
value={formValues.vpnIp}
|
||||||
|
onChange={handleChange}
|
||||||
|
required
|
||||||
|
error={formValues.vpnIp === ''}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<TextField
|
||||||
|
label={
|
||||||
|
<FormattedMessage id="VRM Link" defaultMessage="VRM Link" />
|
||||||
|
}
|
||||||
|
name="vrmLink"
|
||||||
|
value={formValues.vrmLink}
|
||||||
|
onChange={handleChange}
|
||||||
|
required
|
||||||
|
error={formValues.vrmLink === ''}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<TextField
|
||||||
|
label={
|
||||||
|
<FormattedMessage
|
||||||
|
id="Information"
|
||||||
|
defaultMessage="Information"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
name="information"
|
||||||
|
value={formValues.information}
|
||||||
|
onChange={handleChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Box>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
marginTop: 10
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
onClick={handleSubmit}
|
||||||
|
sx={{
|
||||||
|
marginLeft: '20px'
|
||||||
|
}}
|
||||||
|
disabled={!areRequiredFieldsFilled()}
|
||||||
|
>
|
||||||
|
<FormattedMessage id="submit" defaultMessage="Submit" />
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
onClick={handleCancelSubmit}
|
||||||
|
sx={{
|
||||||
|
marginLeft: '10px'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FormattedMessage id="cancel" defaultMessage="Cancel" />
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{loading && (
|
||||||
|
<CircularProgress
|
||||||
|
sx={{
|
||||||
|
color: theme.palette.primary.main,
|
||||||
|
marginLeft: '20px'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<Alert
|
||||||
|
severity="error"
|
||||||
|
sx={{
|
||||||
|
ml: 1,
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FormattedMessage
|
||||||
|
id="errorOccured"
|
||||||
|
defaultMessage="An error has occured"
|
||||||
|
/>
|
||||||
|
<IconButton
|
||||||
|
color="inherit"
|
||||||
|
size="small"
|
||||||
|
onClick={() => setError(false)}
|
||||||
|
sx={{ marginLeft: '4px' }}
|
||||||
|
>
|
||||||
|
<CloseIcon fontSize="small" />
|
||||||
|
</IconButton>
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Box>
|
||||||
|
</Modal>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SalidomonstallationForm;
|
|
@ -0,0 +1,163 @@
|
||||||
|
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 { WebSocketContext } from '../../../contexts/WebSocketContextProvider';
|
||||||
|
import ListIcon from '@mui/icons-material/List';
|
||||||
|
|
||||||
|
function SalidomoInstallationTabs() {
|
||||||
|
const location = useLocation();
|
||||||
|
const context = useContext(UserContext);
|
||||||
|
const { currentUser } = context;
|
||||||
|
const tabList = ['batteryview', 'information'];
|
||||||
|
|
||||||
|
const [currentTab, setCurrentTab] = useState<string>(undefined);
|
||||||
|
const [fetchedInstallations, setFetchedInstallations] =
|
||||||
|
useState<boolean>(false);
|
||||||
|
const { salidomoInstallations, fetchAllSalidomoInstallations } =
|
||||||
|
useContext(InstallationsContext);
|
||||||
|
|
||||||
|
const webSocketsContext = useContext(WebSocketContext);
|
||||||
|
const { socket, openSocket } = webSocketsContext;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let path = location.pathname.split('/');
|
||||||
|
|
||||||
|
if (path[path.length - 2] === 'list') {
|
||||||
|
setCurrentTab('list');
|
||||||
|
} 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 (salidomoInstallations.length === 0 && fetchedInstallations === false) {
|
||||||
|
fetchAllSalidomoInstallations();
|
||||||
|
setFetchedInstallations(true);
|
||||||
|
}
|
||||||
|
}, [salidomoInstallations]);
|
||||||
|
|
||||||
|
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 tabs =
|
||||||
|
currentTab != 'list' && !location.pathname.includes('folder')
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
value: 'list',
|
||||||
|
icon: <ListIcon id="mode-toggle-button-list-icon" />
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
value: 'batteryview',
|
||||||
|
label: (
|
||||||
|
<FormattedMessage
|
||||||
|
id="batteryview"
|
||||||
|
defaultMessage="Battery View"
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
value: 'information',
|
||||||
|
label: (
|
||||||
|
<FormattedMessage id="information" defaultMessage="Information" />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
]
|
||||||
|
: [
|
||||||
|
{
|
||||||
|
value: 'list',
|
||||||
|
icon: <ListIcon id="mode-toggle-button-list-icon" />
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container maxWidth="xl" sx={{ marginTop: '20px' }} className="mainframe">
|
||||||
|
<TabsContainerWrapper>
|
||||||
|
<Tabs
|
||||||
|
onChange={handleTabsChange}
|
||||||
|
value={currentTab}
|
||||||
|
variant="scrollable"
|
||||||
|
scrollButtons="auto"
|
||||||
|
textColor="primary"
|
||||||
|
indicatorColor="primary"
|
||||||
|
>
|
||||||
|
{tabs.map((tab) => (
|
||||||
|
<Tab
|
||||||
|
key={tab.value}
|
||||||
|
value={tab.value}
|
||||||
|
icon={tab.icon}
|
||||||
|
component={Link}
|
||||||
|
label={tab.label}
|
||||||
|
to={
|
||||||
|
tab.value === 'list'
|
||||||
|
? routes[tab.value]
|
||||||
|
: navigateToTabPath(location.pathname, routes[tab.value])
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Tabs>
|
||||||
|
</TabsContainerWrapper>
|
||||||
|
<Card variant="outlined">
|
||||||
|
<Grid
|
||||||
|
container
|
||||||
|
direction="row"
|
||||||
|
justifyContent="center"
|
||||||
|
alignItems="stretch"
|
||||||
|
spacing={0}
|
||||||
|
>
|
||||||
|
<Routes>
|
||||||
|
<Route
|
||||||
|
path={routes.list + '*'}
|
||||||
|
element={
|
||||||
|
<Grid item xs={12}>
|
||||||
|
<Box p={4}>
|
||||||
|
<InstallationSearch installations={salidomoInstallations} />
|
||||||
|
</Box>
|
||||||
|
</Grid>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Route
|
||||||
|
path={'*'}
|
||||||
|
element={
|
||||||
|
<Navigate
|
||||||
|
to={routes.salidomo_installations + routes.list}
|
||||||
|
></Navigate>
|
||||||
|
}
|
||||||
|
></Route>
|
||||||
|
</Routes>
|
||||||
|
</Grid>
|
||||||
|
</Card>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SalidomoInstallationTabs;
|
|
@ -13,9 +13,11 @@ import routes from '../Resources/routes.json';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
interface I_InstallationContextProviderProps {
|
interface I_InstallationContextProviderProps {
|
||||||
installations: I_Installation[];
|
salimaxInstallations: I_Installation[];
|
||||||
|
salidomoInstallations: I_Installation[];
|
||||||
foldersAndInstallations: I_Installation[];
|
foldersAndInstallations: I_Installation[];
|
||||||
fetchAllInstallations: () => Promise<void>;
|
fetchAllInstallations: () => Promise<void>;
|
||||||
|
fetchAllSalidomoInstallations: () => Promise<void>;
|
||||||
fetchAllFoldersAndInstallations: () => Promise<void>;
|
fetchAllFoldersAndInstallations: () => Promise<void>;
|
||||||
createInstallation: (value: Partial<I_Installation>) => Promise<void>;
|
createInstallation: (value: Partial<I_Installation>) => Promise<void>;
|
||||||
updateInstallation: (value: I_Installation, view: string) => Promise<void>;
|
updateInstallation: (value: I_Installation, view: string) => Promise<void>;
|
||||||
|
@ -33,9 +35,11 @@ interface I_InstallationContextProviderProps {
|
||||||
|
|
||||||
export const InstallationsContext =
|
export const InstallationsContext =
|
||||||
createContext<I_InstallationContextProviderProps>({
|
createContext<I_InstallationContextProviderProps>({
|
||||||
installations: [],
|
salimaxInstallations: [],
|
||||||
|
salidomoInstallations: [],
|
||||||
foldersAndInstallations: [],
|
foldersAndInstallations: [],
|
||||||
fetchAllInstallations: () => Promise.resolve(),
|
fetchAllInstallations: () => Promise.resolve(),
|
||||||
|
fetchAllSalidomoInstallations: () => Promise.resolve(),
|
||||||
fetchAllFoldersAndInstallations: () => Promise.resolve(),
|
fetchAllFoldersAndInstallations: () => Promise.resolve(),
|
||||||
createInstallation: () => Promise.resolve(),
|
createInstallation: () => Promise.resolve(),
|
||||||
updateInstallation: () => Promise.resolve(),
|
updateInstallation: () => Promise.resolve(),
|
||||||
|
@ -56,7 +60,12 @@ const InstallationsContextProvider = ({
|
||||||
}: {
|
}: {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
}) => {
|
}) => {
|
||||||
const [installations, setInstallations] = useState<I_Installation[]>([]);
|
const [salimaxInstallations, setSalimaxInstallations] = useState<
|
||||||
|
I_Installation[]
|
||||||
|
>([]);
|
||||||
|
const [salidomoInstallations, setSalidomoInstallations] = useState<
|
||||||
|
I_Installation[]
|
||||||
|
>([]);
|
||||||
const [foldersAndInstallations, setFoldersAndInstallations] = useState([]);
|
const [foldersAndInstallations, setFoldersAndInstallations] = useState([]);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [error, setError] = useState(false);
|
const [error, setError] = useState(false);
|
||||||
|
@ -70,7 +79,22 @@ const InstallationsContextProvider = ({
|
||||||
axiosConfig
|
axiosConfig
|
||||||
.get('/GetAllInstallations', {})
|
.get('/GetAllInstallations', {})
|
||||||
.then((res: AxiosResponse<I_Installation[]>) => {
|
.then((res: AxiosResponse<I_Installation[]>) => {
|
||||||
setInstallations(res.data);
|
setSalimaxInstallations(res.data);
|
||||||
|
})
|
||||||
|
.catch((err: AxiosError) => {
|
||||||
|
if (err.response && err.response.status == 401) {
|
||||||
|
removeToken();
|
||||||
|
navigate(routes.login);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const fetchAllSalidomoInstallations = useCallback(async () => {
|
||||||
|
let isMounted = true;
|
||||||
|
axiosConfig
|
||||||
|
.get('/GetAllSalidomoInstallations', {})
|
||||||
|
.then((res: AxiosResponse<I_Installation[]>) => {
|
||||||
|
setSalidomoInstallations(res.data);
|
||||||
})
|
})
|
||||||
.catch((err: AxiosError) => {
|
.catch((err: AxiosError) => {
|
||||||
if (err.response && err.response.status == 401) {
|
if (err.response && err.response.status == 401) {
|
||||||
|
@ -100,7 +124,11 @@ const InstallationsContextProvider = ({
|
||||||
.post('/CreateInstallation', formValues)
|
.post('/CreateInstallation', formValues)
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
|
if (formValues.product == 0) {
|
||||||
fetchAllFoldersAndInstallations();
|
fetchAllFoldersAndInstallations();
|
||||||
|
} else {
|
||||||
|
fetchAllSalidomoInstallations();
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
|
@ -123,11 +151,15 @@ const InstallationsContextProvider = ({
|
||||||
if (response) {
|
if (response) {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
setUpdated(true);
|
setUpdated(true);
|
||||||
|
if (formValues.product == 0) {
|
||||||
if (view == 'installation') {
|
if (view == 'installation') {
|
||||||
fetchAllInstallations();
|
fetchAllInstallations();
|
||||||
} else {
|
} else {
|
||||||
fetchAllFoldersAndInstallations();
|
fetchAllFoldersAndInstallations();
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
fetchAllSalidomoInstallations();
|
||||||
|
}
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
setUpdated(false);
|
setUpdated(false);
|
||||||
|
@ -154,11 +186,15 @@ const InstallationsContextProvider = ({
|
||||||
if (response) {
|
if (response) {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
setUpdated(true);
|
setUpdated(true);
|
||||||
|
if (formValues.product == 0) {
|
||||||
if (view == 'installation') {
|
if (view == 'installation') {
|
||||||
fetchAllInstallations();
|
fetchAllInstallations();
|
||||||
} else {
|
} else {
|
||||||
fetchAllFoldersAndInstallations();
|
fetchAllFoldersAndInstallations();
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
fetchAllSalidomoInstallations();
|
||||||
|
}
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
setUpdated(false);
|
setUpdated(false);
|
||||||
|
@ -246,9 +282,11 @@ const InstallationsContextProvider = ({
|
||||||
return (
|
return (
|
||||||
<InstallationsContext.Provider
|
<InstallationsContext.Provider
|
||||||
value={{
|
value={{
|
||||||
installations,
|
salimaxInstallations,
|
||||||
|
salidomoInstallations,
|
||||||
foldersAndInstallations,
|
foldersAndInstallations,
|
||||||
fetchAllInstallations,
|
fetchAllInstallations,
|
||||||
|
fetchAllSalidomoInstallations,
|
||||||
fetchAllFoldersAndInstallations,
|
fetchAllFoldersAndInstallations,
|
||||||
createInstallation,
|
createInstallation,
|
||||||
updateInstallation,
|
updateInstallation,
|
||||||
|
|
|
@ -2,24 +2,21 @@ import { I_S3Credentials } from 'src/interfaces/S3Types';
|
||||||
|
|
||||||
export interface I_Installation extends I_S3Credentials {
|
export interface I_Installation extends I_S3Credentials {
|
||||||
type: string;
|
type: string;
|
||||||
title?: string;
|
vrmLink?: string;
|
||||||
status?: number;
|
vrmIdentifier?: string;
|
||||||
detail?: string;
|
|
||||||
instance?: string;
|
|
||||||
location: string;
|
location: string;
|
||||||
region: string;
|
region: string;
|
||||||
country: string;
|
country: string;
|
||||||
installationName: string;
|
installationName: string;
|
||||||
vpnIp: string;
|
vpnIp: string;
|
||||||
orderNumbers: string[] | string;
|
orderNumbers: string[] | string;
|
||||||
lat: number;
|
|
||||||
long: number;
|
|
||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
information: string;
|
information: string;
|
||||||
parentId: number;
|
parentId: number;
|
||||||
s3WriteKey: string;
|
s3WriteKey: string;
|
||||||
s3WriteSecret: string;
|
s3WriteSecret: string;
|
||||||
|
product: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface I_Folder {
|
export interface I_Folder {
|
||||||
|
|
|
@ -170,7 +170,9 @@ function SidebarMenu() {
|
||||||
<List
|
<List
|
||||||
component="div"
|
component="div"
|
||||||
subheader={
|
subheader={
|
||||||
<ListSubheader component="div" disableSticky></ListSubheader>
|
<ListSubheader component="div" disableSticky>
|
||||||
|
Products
|
||||||
|
</ListSubheader>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<SubMenuWrapper>
|
<SubMenuWrapper>
|
||||||
|
@ -183,13 +185,33 @@ function SidebarMenu() {
|
||||||
to="/installations"
|
to="/installations"
|
||||||
startIcon={<BrightnessLowTwoToneIcon />}
|
startIcon={<BrightnessLowTwoToneIcon />}
|
||||||
>
|
>
|
||||||
<FormattedMessage
|
<Box sx={{ marginTop: '3px' }}>
|
||||||
id="installations"
|
<FormattedMessage id="salimax" defaultMessage="Salimax" />
|
||||||
defaultMessage="Installations"
|
</Box>
|
||||||
/>
|
|
||||||
</Button>
|
</Button>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
</List>
|
</List>
|
||||||
|
|
||||||
|
{currentUser.userType == UserType.admin && (
|
||||||
|
<List component="div">
|
||||||
|
<ListItem component="div">
|
||||||
|
<Button
|
||||||
|
disableRipple
|
||||||
|
component={RouterLink}
|
||||||
|
onClick={closeSidebar}
|
||||||
|
to="/salidomo_installations"
|
||||||
|
startIcon={<BrightnessLowTwoToneIcon />}
|
||||||
|
>
|
||||||
|
<Box sx={{ marginTop: '3px' }}>
|
||||||
|
<FormattedMessage
|
||||||
|
id="salidomo"
|
||||||
|
defaultMessage="Salidomo"
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</Button>
|
||||||
|
</ListItem>
|
||||||
|
</List>
|
||||||
|
)}
|
||||||
</SubMenuWrapper>
|
</SubMenuWrapper>
|
||||||
</List>
|
</List>
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue