Merge remote-tracking branch 'origin/main'
This commit is contained in:
commit
35b3d91f05
|
@ -281,11 +281,24 @@ public class Controller : ControllerBase
|
|||
return Unauthorized();
|
||||
|
||||
return user
|
||||
.AccessibleInstallations()
|
||||
.AccessibleInstallations(product:0)
|
||||
.Select(i => i.FillOrderNumbers().HideParentIfUserHasNoAccessToParent(user).HideWriteKeyIfUserIsNotAdmin(user.UserType))
|
||||
.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))]
|
||||
|
@ -309,7 +322,7 @@ public class Controller : ControllerBase
|
|||
return Unauthorized();
|
||||
|
||||
var foldersAndInstallations = user
|
||||
.AccessibleFoldersAndInstallations()
|
||||
.AccessibleFoldersAndInstallations(product:0)
|
||||
.Do(o => o.FillOrderNumbers())
|
||||
.Select(o => o.HideParentIfUserHasNoAccessToParent(user))
|
||||
.OfType<Object>(); // Important! JSON serializer must see Objects otherwise
|
||||
|
@ -342,6 +355,7 @@ public class Controller : ControllerBase
|
|||
[HttpPost(nameof(CreateInstallation))]
|
||||
public async Task<ActionResult<Installation>> CreateInstallation([FromBody] Installation installation, Token authToken)
|
||||
{
|
||||
|
||||
var session = Db.GetSession(authToken);
|
||||
|
||||
if (! await session.Create(installation))
|
||||
|
@ -453,9 +467,14 @@ public class Controller : ControllerBase
|
|||
if (!session.Update(installation))
|
||||
return Unauthorized();
|
||||
|
||||
if (installation.Product == 0)
|
||||
{
|
||||
return installation.FillOrderNumbers().HideParentIfUserHasNoAccessToParent(session!.User).HideWriteKeyIfUserIsNotAdmin(session.User.UserType);
|
||||
}
|
||||
|
||||
return installation.HideParentIfUserHasNoAccessToParent(session!.User);
|
||||
}
|
||||
|
||||
[HttpPost(nameof(AcknowledgeError))]
|
||||
public ActionResult AcknowledgeError(Int64 id, Token authToken)
|
||||
{
|
||||
|
|
|
@ -7,16 +7,8 @@ public class Installation : TreeNode
|
|||
public String Location { get; set; } = "";
|
||||
public String Region { get; set; } = "";
|
||||
public String Country { get; set; } = "";
|
||||
public String InstallationName { get; set; } = "";
|
||||
public String VpnIp { get; set; } = "";
|
||||
|
||||
// TODO: make relation
|
||||
//public IReadOnlyList<String> OrderNumbers { get; set; } = Array.Empty<String>();
|
||||
// public String? OrderNumbers { get; set; } = "";
|
||||
|
||||
public Double Lat { get; set; }
|
||||
public Double Long { get; set; }
|
||||
|
||||
public String S3Region { get; set; } = "sos-ch-dk-2";
|
||||
public String S3Provider { get; set; } = "exo.io";
|
||||
public String S3WriteKey { get; set; } = "";
|
||||
|
@ -26,9 +18,10 @@ public class Installation : TreeNode
|
|||
public String ReadRoleId { get; set; } = "";
|
||||
public String WriteRoleId { get; set; } = "";
|
||||
|
||||
|
||||
public int Product { get; set; } = 0;
|
||||
[Ignore]
|
||||
public String OrderNumbers { get; set; }
|
||||
public String VrmLink { get; set; } = "";
|
||||
|
||||
|
||||
}
|
|
@ -81,6 +81,7 @@ public static class ExoCmd
|
|||
{
|
||||
var readRoleId = installation.ReadRoleId;
|
||||
|
||||
|
||||
if (String.IsNullOrEmpty(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 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 authheader = "credential=" + S3Credentials.Key + ",expires=" + unixtime + ",signature=" +
|
||||
BuildSignature("POST", method, contentString, unixtime);
|
||||
|
||||
|
@ -165,7 +165,6 @@ public static class ExoCmd
|
|||
""";
|
||||
|
||||
var unixtime = DateTimeOffset.UtcNow.ToUnixTimeSeconds()+60;
|
||||
|
||||
var authheader = "credential="+S3Credentials.Key+",signed-query-args="+",expires="+unixtime+",signature="+BuildSignature("POST", method, contentString, unixtime);
|
||||
|
||||
var client = new HttpClient();
|
||||
|
@ -294,6 +293,7 @@ public static class ExoCmd
|
|||
return await s3Region.PutBucket(installation.BucketName()) != null;
|
||||
}
|
||||
|
||||
|
||||
public static async Task<Boolean> SendConfig(this Installation installation, Configuration config)
|
||||
{
|
||||
|
||||
|
@ -316,7 +316,7 @@ public static class ExoCmd
|
|||
|
||||
// return result.ExitCode == 200;
|
||||
|
||||
var maxRetransmissions = 2;
|
||||
var maxRetransmissions = 4;
|
||||
UdpClient udpClient = new UdpClient();
|
||||
udpClient.Client.ReceiveTimeout = 2000;
|
||||
int port = 9000;
|
||||
|
|
|
@ -53,6 +53,7 @@ public static class FolderMethods
|
|||
.Where(f => f.ParentId == parent.Id);
|
||||
}
|
||||
|
||||
|
||||
public static IEnumerable<Folder> DescendantFolders(this Folder 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.Relations;
|
||||
using InnovEnergy.Lib.S3Utils;
|
||||
using InnovEnergy.Lib.Utils;
|
||||
|
||||
namespace InnovEnergy.App.Backend.DataTypes.Methods;
|
||||
|
@ -12,20 +8,29 @@ namespace InnovEnergy.App.Backend.DataTypes.Methods;
|
|||
public static class InstallationMethods
|
||||
{
|
||||
private static readonly String BucketNameSalt =
|
||||
Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") == ""
|
||||
? "stage"
|
||||
:"3e5b3069-214a-43ee-8d85-57d72000c19d";
|
||||
// Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") == ""
|
||||
// ? "stage" :"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)
|
||||
{
|
||||
if (installation.Product == 0)
|
||||
{
|
||||
return $"{installation.Id}-{BucketNameSalt}";
|
||||
}
|
||||
|
||||
return $"{installation.Id}-{SalidomoBucketNameSalt}";
|
||||
|
||||
}
|
||||
|
||||
public static async Task<Boolean> RenewS3Credentials(this Installation installation)
|
||||
{
|
||||
if(!installation.S3Key.IsNullOrEmpty())
|
||||
await installation.RevokeReadKey();
|
||||
|
||||
|
||||
var (key,secret) = await installation.CreateReadKey();
|
||||
|
||||
installation.S3Key = key;
|
||||
|
|
|
@ -118,25 +118,41 @@ public static class SessionMethods
|
|||
{
|
||||
var user = session?.User;
|
||||
|
||||
//Salimax installation
|
||||
if (installation.Product==0)
|
||||
{
|
||||
return user is not null
|
||||
&& installation is not null
|
||||
&& user.UserType !=0
|
||||
&& user.UserType != 0
|
||||
&& user.HasAccessToParentOf(installation)
|
||||
&& Db.Create(installation) // TODO: these two in a transaction
|
||||
&& installation.SetOrderNumbers()
|
||||
&& Db.Create(new InstallationAccess { UserId = user.Id, InstallationId = installation.Id })
|
||||
&& await installation.CreateBucket()
|
||||
&& await installation.RenewS3Credentials(); // generation of access _after_ generation of
|
||||
// bucket to prevent "zombie" access-rights.
|
||||
// This might ** us over if the creation of access rights fails,
|
||||
// as bucket-names are unique and bound to the installation id... -K
|
||||
&& await installation.RenewS3Credentials();
|
||||
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
var user = session?.User;
|
||||
|
||||
var original = Db.GetInstallationById(installation?.Id);
|
||||
//Salimax installation
|
||||
if (installation.Product==0)
|
||||
{
|
||||
|
||||
return user is not null
|
||||
&& installation is not null
|
||||
|
@ -149,16 +165,33 @@ public static class SessionMethods
|
|||
.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)
|
||||
{
|
||||
var user = session?.User;
|
||||
|
||||
|
||||
return user is not null
|
||||
&& installation is not null
|
||||
&& user.UserType !=0
|
||||
&& user.UserType != 0
|
||||
&& user.HasAccessTo(installation)
|
||||
&& Db.Delete(installation)
|
||||
&& await installation.DeleteBucket();
|
||||
|
||||
|
||||
}
|
||||
|
||||
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 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
|
||||
.AccessibleFolders()
|
||||
.SelectMany(u => u.ChildInstallations()).ToList();
|
||||
.SelectMany(u => u.ChildInstallations()).ToList().Where(f=>f.Product==product);
|
||||
|
||||
return direct
|
||||
.Concat(fromFolders)
|
||||
.Distinct();
|
||||
}
|
||||
|
||||
|
||||
public static IEnumerable<Folder> AccessibleFolders(this User user)
|
||||
{
|
||||
return user
|
||||
|
@ -32,12 +33,12 @@ public static class UserMethods
|
|||
.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>;
|
||||
|
||||
user.AccessibleInstallations().ForEach(i => i.FillOrderNumbers());
|
||||
var installations = user.AccessibleInstallations();
|
||||
user.AccessibleInstallations(product).ForEach(i => i.FillOrderNumbers());
|
||||
var installations = user.AccessibleInstallations(product);
|
||||
|
||||
return folders.Concat(installations);
|
||||
}
|
||||
|
@ -158,6 +159,7 @@ public static class UserMethods
|
|||
.Any(user.HasDirectAccessTo);
|
||||
}
|
||||
|
||||
|
||||
public static Boolean HasAccessTo(this User user, User? other)
|
||||
{
|
||||
if (other is null)
|
||||
|
@ -169,19 +171,10 @@ public static class UserMethods
|
|||
.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)
|
||||
{
|
||||
|
||||
return other?.Type switch
|
||||
{
|
||||
"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.Methods;
|
||||
using InnovEnergy.App.Backend.Relations;
|
||||
|
@ -143,11 +141,15 @@ public static partial class Db
|
|||
.Distinct()
|
||||
.ToList();
|
||||
|
||||
|
||||
|
||||
var validWriteKeys = Installations
|
||||
.Select(i => i.S3WriteKey)
|
||||
.Distinct()
|
||||
.ToList();
|
||||
|
||||
|
||||
|
||||
const String provider = "exo.io";
|
||||
var S3keys = await ExoCmd.GetAccessKeys();
|
||||
|
||||
|
@ -243,6 +245,7 @@ public static partial class Db
|
|||
{
|
||||
await installation.RenewS3Credentials();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -72,10 +72,16 @@ public static partial class Db
|
|||
|
||||
|
||||
Boolean DeleteInstallationAndItsDependencies()
|
||||
{
|
||||
if (installation.Product == 0)
|
||||
{
|
||||
InstallationAccess.Delete(i => i.InstallationId == installation.Id);
|
||||
OrderNumber2Installation.Delete(i => i.InstallationId == installation.Id);
|
||||
|
||||
}
|
||||
|
||||
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
|
||||
Salimax0007 ie-entwicklung@10.2.4.154 LerchenhofHerr Twannberg
|
||||
Salimax0008 ie-entwicklung@10.2.4.113 Wittmann Kottingbrunn
|
||||
Salimax0010 ie-entwicklung@10.2.4.211 Mahotech 1
|
||||
SalidomoServer ig@134.209.238.170
|
|
@ -20,6 +20,7 @@ import { axiosConfigWithoutToken } from './Resources/axiosConfig';
|
|||
import InstallationsContextProvider from './contexts/InstallationsContextProvider';
|
||||
import AccessContextProvider from './contexts/AccessContextProvider';
|
||||
import WebSocketContextProvider from './contexts/WebSocketContextProvider';
|
||||
import SalidomoInstallationTabs from './content/dashboards/SalidomoInstallations';
|
||||
|
||||
function App() {
|
||||
const context = useContext(UserContext);
|
||||
|
@ -146,6 +147,18 @@ function App() {
|
|||
</AccessContextProvider>
|
||||
}
|
||||
/>
|
||||
|
||||
<Route
|
||||
path={routes.salidomo_installations + '*'}
|
||||
element={
|
||||
<AccessContextProvider>
|
||||
<InstallationsContextProvider>
|
||||
<SalidomoInstallationTabs />
|
||||
</InstallationsContextProvider>
|
||||
</AccessContextProvider>
|
||||
}
|
||||
/>
|
||||
|
||||
<Route path={routes.users + '*'} element={<Users />} />
|
||||
<Route
|
||||
path={'*'}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
{
|
||||
"users": "/users/",
|
||||
"installations": "/installations/",
|
||||
"salidomo_installations": "/salidomo_installations/",
|
||||
"installation": "installation/",
|
||||
"login": "/login/",
|
||||
"forgotPassword": "/forgotPassword/",
|
||||
|
|
|
@ -116,7 +116,7 @@ function Configuration(props: ConfigurationProps) {
|
|||
setErrorDateModalOpen(true);
|
||||
return;
|
||||
} else if (
|
||||
formValues.CalibrationChargeState != 2 &&
|
||||
formValues.CalibrationChargeState === 1 &&
|
||||
dayjs(formValues.calibrationChargeDate).isBefore(dayjs())
|
||||
) {
|
||||
//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 =
|
||||
props.current_installation.id.toString() +
|
||||
'-3e5b3069-214a-43ee-8d85-57d72000c19d';
|
||||
props.current_installation.product === 0
|
||||
? props.current_installation.id.toString() +
|
||||
'-3e5b3069-214a-43ee-8d85-57d72000c19d'
|
||||
: props.current_installation.id.toString() +
|
||||
'-c0436b6a-d276-4cd8-9c44-1eae86cf5d0e';
|
||||
|
||||
const s3Credentials = { s3Bucket, ...S3data };
|
||||
|
||||
|
|
|
@ -30,7 +30,7 @@ function InstallationTabs() {
|
|||
];
|
||||
|
||||
const [currentTab, setCurrentTab] = useState<string>(undefined);
|
||||
const { installations, fetchAllInstallations } =
|
||||
const { salimaxInstallations, fetchAllInstallations } =
|
||||
useContext(InstallationsContext);
|
||||
|
||||
const webSocketsContext = useContext(WebSocketContext);
|
||||
|
@ -50,16 +50,16 @@ function InstallationTabs() {
|
|||
}, [location]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!socket && installations.length > 0) {
|
||||
openSocket(installations);
|
||||
if (!socket && salimaxInstallations.length > 0) {
|
||||
openSocket(salimaxInstallations);
|
||||
}
|
||||
}, [installations]);
|
||||
}, [salimaxInstallations]);
|
||||
|
||||
useEffect(() => {
|
||||
if (installations.length === 0) {
|
||||
if (salimaxInstallations.length === 0) {
|
||||
fetchAllInstallations();
|
||||
}
|
||||
}, [installations]);
|
||||
}, [salimaxInstallations]);
|
||||
|
||||
const handleTabsChange = (_event: ChangeEvent<{}>, value: string): void => {
|
||||
setCurrentTab(value);
|
||||
|
@ -270,7 +270,7 @@ function InstallationTabs() {
|
|||
}
|
||||
];
|
||||
|
||||
return installations.length > 1 ? (
|
||||
return salimaxInstallations.length > 1 ? (
|
||||
<>
|
||||
<Container maxWidth="xl" sx={{ marginTop: '20px' }} className="mainframe">
|
||||
<TabsContainerWrapper>
|
||||
|
@ -312,7 +312,9 @@ function InstallationTabs() {
|
|||
element={
|
||||
<Grid item xs={12}>
|
||||
<Box p={4}>
|
||||
<InstallationSearch installations={installations} />
|
||||
<InstallationSearch
|
||||
installations={salimaxInstallations}
|
||||
/>
|
||||
</Box>
|
||||
</Grid>
|
||||
}
|
||||
|
@ -330,7 +332,7 @@ function InstallationTabs() {
|
|||
</Container>
|
||||
<Footer />
|
||||
</>
|
||||
) : installations.length === 1 ? (
|
||||
) : salimaxInstallations.length === 1 ? (
|
||||
<>
|
||||
<Container maxWidth="xl" sx={{ marginTop: '20px' }} className="mainframe">
|
||||
<TabsContainerWrapper>
|
||||
|
@ -368,7 +370,7 @@ function InstallationTabs() {
|
|||
<Grid item xs={12}>
|
||||
<Box p={4}>
|
||||
<Installation
|
||||
current_installation={installations[0]}
|
||||
current_installation={salimaxInstallations[0]}
|
||||
type="installation"
|
||||
></Installation>
|
||||
</Box>
|
||||
|
|
|
@ -51,6 +51,7 @@ function installationForm(props: installationFormProps) {
|
|||
const handleSubmit = async (e) => {
|
||||
setLoading(true);
|
||||
formValues.parentId = props.parentid;
|
||||
formValues.product = 0;
|
||||
const responseData = await createInstallation(formValues);
|
||||
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';
|
||||
|
||||
interface I_InstallationContextProviderProps {
|
||||
installations: I_Installation[];
|
||||
salimaxInstallations: I_Installation[];
|
||||
salidomoInstallations: I_Installation[];
|
||||
foldersAndInstallations: I_Installation[];
|
||||
fetchAllInstallations: () => Promise<void>;
|
||||
fetchAllSalidomoInstallations: () => Promise<void>;
|
||||
fetchAllFoldersAndInstallations: () => Promise<void>;
|
||||
createInstallation: (value: Partial<I_Installation>) => Promise<void>;
|
||||
updateInstallation: (value: I_Installation, view: string) => Promise<void>;
|
||||
|
@ -33,9 +35,11 @@ interface I_InstallationContextProviderProps {
|
|||
|
||||
export const InstallationsContext =
|
||||
createContext<I_InstallationContextProviderProps>({
|
||||
installations: [],
|
||||
salimaxInstallations: [],
|
||||
salidomoInstallations: [],
|
||||
foldersAndInstallations: [],
|
||||
fetchAllInstallations: () => Promise.resolve(),
|
||||
fetchAllSalidomoInstallations: () => Promise.resolve(),
|
||||
fetchAllFoldersAndInstallations: () => Promise.resolve(),
|
||||
createInstallation: () => Promise.resolve(),
|
||||
updateInstallation: () => Promise.resolve(),
|
||||
|
@ -56,7 +60,12 @@ const InstallationsContextProvider = ({
|
|||
}: {
|
||||
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 [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState(false);
|
||||
|
@ -70,7 +79,22 @@ const InstallationsContextProvider = ({
|
|||
axiosConfig
|
||||
.get('/GetAllInstallations', {})
|
||||
.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) => {
|
||||
if (err.response && err.response.status == 401) {
|
||||
|
@ -100,7 +124,11 @@ const InstallationsContextProvider = ({
|
|||
.post('/CreateInstallation', formValues)
|
||||
.then((res) => {
|
||||
setLoading(false);
|
||||
if (formValues.product == 0) {
|
||||
fetchAllFoldersAndInstallations();
|
||||
} else {
|
||||
fetchAllSalidomoInstallations();
|
||||
}
|
||||
})
|
||||
|
||||
.catch((error) => {
|
||||
|
@ -123,11 +151,15 @@ const InstallationsContextProvider = ({
|
|||
if (response) {
|
||||
setLoading(false);
|
||||
setUpdated(true);
|
||||
if (formValues.product == 0) {
|
||||
if (view == 'installation') {
|
||||
fetchAllInstallations();
|
||||
} else {
|
||||
fetchAllFoldersAndInstallations();
|
||||
}
|
||||
} else {
|
||||
fetchAllSalidomoInstallations();
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
setUpdated(false);
|
||||
|
@ -154,11 +186,15 @@ const InstallationsContextProvider = ({
|
|||
if (response) {
|
||||
setLoading(false);
|
||||
setUpdated(true);
|
||||
if (formValues.product == 0) {
|
||||
if (view == 'installation') {
|
||||
fetchAllInstallations();
|
||||
} else {
|
||||
fetchAllFoldersAndInstallations();
|
||||
}
|
||||
} else {
|
||||
fetchAllSalidomoInstallations();
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
setUpdated(false);
|
||||
|
@ -246,9 +282,11 @@ const InstallationsContextProvider = ({
|
|||
return (
|
||||
<InstallationsContext.Provider
|
||||
value={{
|
||||
installations,
|
||||
salimaxInstallations,
|
||||
salidomoInstallations,
|
||||
foldersAndInstallations,
|
||||
fetchAllInstallations,
|
||||
fetchAllSalidomoInstallations,
|
||||
fetchAllFoldersAndInstallations,
|
||||
createInstallation,
|
||||
updateInstallation,
|
||||
|
|
|
@ -2,24 +2,21 @@ import { I_S3Credentials } from 'src/interfaces/S3Types';
|
|||
|
||||
export interface I_Installation extends I_S3Credentials {
|
||||
type: string;
|
||||
title?: string;
|
||||
status?: number;
|
||||
detail?: string;
|
||||
instance?: string;
|
||||
vrmLink?: string;
|
||||
vrmIdentifier?: string;
|
||||
location: string;
|
||||
region: string;
|
||||
country: string;
|
||||
installationName: string;
|
||||
vpnIp: string;
|
||||
orderNumbers: string[] | string;
|
||||
lat: number;
|
||||
long: number;
|
||||
id: number;
|
||||
name: string;
|
||||
information: string;
|
||||
parentId: number;
|
||||
s3WriteKey: string;
|
||||
s3WriteSecret: string;
|
||||
product: number;
|
||||
}
|
||||
|
||||
export interface I_Folder {
|
||||
|
|
|
@ -170,7 +170,9 @@ function SidebarMenu() {
|
|||
<List
|
||||
component="div"
|
||||
subheader={
|
||||
<ListSubheader component="div" disableSticky></ListSubheader>
|
||||
<ListSubheader component="div" disableSticky>
|
||||
Products
|
||||
</ListSubheader>
|
||||
}
|
||||
>
|
||||
<SubMenuWrapper>
|
||||
|
@ -183,13 +185,33 @@ function SidebarMenu() {
|
|||
to="/installations"
|
||||
startIcon={<BrightnessLowTwoToneIcon />}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="installations"
|
||||
defaultMessage="Installations"
|
||||
/>
|
||||
<Box sx={{ marginTop: '3px' }}>
|
||||
<FormattedMessage id="salimax" defaultMessage="Salimax" />
|
||||
</Box>
|
||||
</Button>
|
||||
</ListItem>
|
||||
</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>
|
||||
</List>
|
||||
|
||||
|
|
Loading…
Reference in New Issue