Compare commits

...

3 Commits

8 changed files with 517 additions and 453 deletions

View File

@ -363,6 +363,38 @@ public class Controller : ControllerBase
return user.AccessibleInstallations().ToList();
}
[HttpGet(nameof(GetDirectInstallationAccessForUser))]
public ActionResult<IEnumerable<Object>> GetDirectInstallationAccessForUser(Int64 userId, Token authToken)
{
var sessionUser = Db.GetSession(authToken)?.User;
if (sessionUser == null)
return Unauthorized();
var user = Db.GetUserById(userId);
if (user == null)
return Unauthorized();
return user.DirectlyAccessibleInstallations()
.Select(i => new { i.Id, i.Name })
.ToList<Object>();
}
[HttpGet(nameof(GetDirectFolderAccessForUser))]
public ActionResult<IEnumerable<Object>> GetDirectFolderAccessForUser(Int64 userId, Token authToken)
{
var sessionUser = Db.GetSession(authToken)?.User;
if (sessionUser == null)
return Unauthorized();
var user = Db.GetUserById(userId);
if (user == null)
return Unauthorized();
return user.DirectlyAccessibleFolders()
.Select(f => new { f.Id, f.Name })
.ToList<Object>();
}
[HttpGet(nameof(GetUsersWithInheritedAccessToInstallation))]
public ActionResult<IEnumerable<Object>> GetUsersWithInheritedAccessToInstallation(Int64 id, Token authToken)
{

View File

@ -376,8 +376,7 @@ public static class SessionMethods
&& sessionUser.HasAccessTo(originalUser)
&& editedUser
.WithParentOf(originalUser) // prevent moving
.WithNameOf(originalUser)
.WithPasswordOf(originalUser)
.WithPasswordOf(originalUser)
.Apply(Db.Update);
}
@ -473,24 +472,36 @@ public static class SessionMethods
public static Boolean RevokeUserAccessTo(this Session? session, User? user, Installation? installation)
{
var sessionUser = session?.User;
return sessionUser is not null
&& installation is not null
&& user is not null
&& user.IsDescendantOf(sessionUser)
&& sessionUser.HasAccessTo(installation)
&& user.HasAccessTo(installation)
&& Db.InstallationAccess.Delete(a => a.UserId == user.Id && a.InstallationId == installation.Id) > 0;
if (sessionUser is null || installation is null || user is null)
return false;
if (!(user.IsDescendantOf(sessionUser) || sessionUser.UserType == 2))
return false;
if (!sessionUser.HasAccessTo(installation) || !user.HasAccessTo(installation))
return false;
// Try direct InstallationAccess record first
if (Db.InstallationAccess.Delete(a => a.UserId == user.Id && a.InstallationId == installation.Id) > 0)
return true;
// No direct record — access is inherited via a folder; revoke that folder access
var accessFolder = installation.Ancestors()
.FirstOrDefault(f => user.HasDirectAccessTo(f));
return accessFolder is not null
&& Db.FolderAccess.Delete(a => a.UserId == user.Id && a.FolderId == accessFolder.Id) > 0;
}
public static Boolean RevokeUserAccessTo(this Session? session, User? user, Folder? folder)
{
var sessionUser = session?.User;
return sessionUser is not null
&& folder is not null
&& user is not null
&& user.IsDescendantOf(sessionUser)
&& (user.IsDescendantOf(sessionUser) || sessionUser.UserType == 2)
&& sessionUser.HasAccessTo(folder)
&& user.HasAccessTo(folder)
&& Db.FolderAccess.Delete(a => a.UserId == user.Id && a.FolderId == folder.Id) > 0;

View File

@ -40,10 +40,9 @@ public static partial class Db
var originalUser = GetUserById(user.Id);
if (originalUser is null) return false;
// these columns must not be modified!
user.ParentId = originalUser.ParentId;
user.Name = originalUser.Name;
// ParentId must not be modified via this method
user.ParentId = originalUser.ParentId;
return Update(obj: user);
}

View File

@ -175,6 +175,9 @@ function InformationSodistorehome(props: InformationSodistorehomeProps) {
return true;
};
const canEdit = currentUser.userType === UserType.admin;
const isPartner = currentUser.userType === UserType.partner;
return (
<>
{openModalDeleteInstallation && (
@ -276,6 +279,7 @@ function InformationSodistorehome(props: InformationSodistorehomeProps) {
onChange={handleChange}
variant="outlined"
fullWidth
inputProps={{ readOnly: !canEdit }}
/>
</div>
<div>
@ -288,8 +292,9 @@ function InformationSodistorehome(props: InformationSodistorehomeProps) {
onChange={handleChange}
variant="outlined"
fullWidth
required
error={formValues.region === ''}
required={canEdit}
error={canEdit && formValues.region === ''}
inputProps={{ readOnly: !canEdit }}
/>
</div>
<div>
@ -305,8 +310,9 @@ function InformationSodistorehome(props: InformationSodistorehomeProps) {
onChange={handleChange}
variant="outlined"
fullWidth
required
error={formValues.location === ''}
required={canEdit}
error={canEdit && formValues.location === ''}
inputProps={{ readOnly: !canEdit }}
/>
</div>
<div>
@ -319,23 +325,26 @@ function InformationSodistorehome(props: InformationSodistorehomeProps) {
onChange={handleChange}
variant="outlined"
fullWidth
required
error={formValues.country === ''}
required={canEdit}
error={canEdit && formValues.country === ''}
inputProps={{ readOnly: !canEdit }}
/>
</div>
<div>
<TextField
label={
<FormattedMessage id="vpnip" defaultMessage="VPN IP" />
}
name="vpnIp"
value={formValues.vpnIp}
onChange={handleChange}
variant="outlined"
fullWidth
/>
</div>
{canEdit && (
<div>
<TextField
label={
<FormattedMessage id="vpnip" defaultMessage="VPN IP" />
}
name="vpnIp"
value={formValues.vpnIp}
onChange={handleChange}
variant="outlined"
fullWidth
/>
</div>
)}
<div>
<FormControl
@ -362,6 +371,7 @@ function InformationSodistorehome(props: InformationSodistorehomeProps) {
name="device"
value={formValues.device}
onChange={handleChange}
inputProps={{ readOnly: !canEdit }}
>
{DeviceTypes.map((device) => (
<MenuItem key={device.id} value={device.id}>
@ -372,106 +382,116 @@ function InformationSodistorehome(props: InformationSodistorehomeProps) {
</FormControl>
</div>
<div>
<TextField
label={
<FormattedMessage
id="installationSerialNumber"
defaultMessage="Installation Serial Number"
/>
}
name="serialNumber"
value={formValues.serialNumber}
onChange={handleChange}
variant="outlined"
fullWidth
/>
</div>
<div>
<TextField
label={
<FormattedMessage
id="inverterSN"
defaultMessage="Inverter Serial Number"
/>
}
name="inverterSN"
value={formValues.inverterSN}
onChange={handleChange}
variant="outlined"
fullWidth
/>
</div>
<div>
<TextField
label={
<FormattedMessage
id="dataloggerSN"
defaultMessage="Datalogger Serial Number"
/>
}
name="dataloggerSN"
value={formValues.dataloggerSN}
onChange={handleChange}
variant="outlined"
fullWidth
/>
</div>
<div>
<TextField
label={
<FormattedMessage
id="batteryClusterNumber"
defaultMessage="Battery Cluster Number"
/>
}
name="batteryClusterNumber"
value={formValues.batteryClusterNumber}
onChange={handleChange}
variant="outlined"
fullWidth
/>
</div>
<div>
<TextField
label={
<FormattedMessage
id="batteryNumber"
defaultMessage="Battery Number"
/>
}
name="batteryNumber"
type="text"
value={batteryNumber === 0 ? '' : batteryNumber}
onChange={handleBatteryNumberChange}
variant="outlined"
fullWidth
placeholder="Enter number of batteries"
/>
</div>
{batteryNumber > 0 &&
batterySerialNumbers.map((serialNumber, index) => (
<div key={index}>
{(canEdit || isPartner) && (
<>
<div>
<TextField
label={`Battery Pack ${index + 1}`}
name={`batterySN${index + 1}`}
value={serialNumber}
onChange={(e) =>
handleBatterySerialNumberChange(index, e.target.value)
label={
<FormattedMessage
id="installationSerialNumber"
defaultMessage="Installation Serial Number"
/>
}
onKeyDown={(e) => handleBatterySnKeyDown(e, index)}
inputRef={(el) => (batterySnRefs.current[index] = el)}
name="serialNumber"
value={formValues.serialNumber}
onChange={handleChange}
variant="outlined"
fullWidth
placeholder="Scan or enter serial number"
inputProps={{ readOnly: !canEdit }}
/>
</div>
))}
<div>
<TextField
label={
<FormattedMessage
id="inverterSN"
defaultMessage="Inverter Serial Number"
/>
}
name="inverterSN"
value={formValues.inverterSN}
onChange={handleChange}
variant="outlined"
fullWidth
inputProps={{ readOnly: !canEdit }}
/>
</div>
<div>
<TextField
label={
<FormattedMessage
id="dataloggerSN"
defaultMessage="Datalogger Serial Number"
/>
}
name="dataloggerSN"
value={formValues.dataloggerSN}
onChange={handleChange}
variant="outlined"
fullWidth
inputProps={{ readOnly: !canEdit }}
/>
</div>
<div>
<TextField
label={
<FormattedMessage
id="batteryClusterNumber"
defaultMessage="Battery Cluster Number"
/>
}
name="batteryClusterNumber"
value={formValues.batteryClusterNumber}
onChange={handleChange}
variant="outlined"
fullWidth
inputProps={{ readOnly: !canEdit }}
/>
</div>
<div>
<TextField
label={
<FormattedMessage
id="batteryNumber"
defaultMessage="Battery Number"
/>
}
name="batteryNumber"
type="text"
value={batteryNumber === 0 ? '' : batteryNumber}
onChange={handleBatteryNumberChange}
variant="outlined"
fullWidth
placeholder={canEdit ? 'Enter number of batteries' : ''}
inputProps={{ readOnly: !canEdit }}
/>
</div>
{batteryNumber > 0 &&
batterySerialNumbers.map((serialNumber, index) => (
<div key={index}>
<TextField
label={`Battery Pack ${index + 1}`}
name={`batterySN${index + 1}`}
value={serialNumber}
onChange={(e) =>
handleBatterySerialNumberChange(index, e.target.value)
}
onKeyDown={(e) => handleBatterySnKeyDown(e, index)}
inputRef={(el) => (batterySnRefs.current[index] = el)}
variant="outlined"
fullWidth
placeholder={canEdit ? 'Scan or enter serial number' : ''}
inputProps={{ readOnly: !canEdit }}
/>
</div>
))}
</>
)}
<div>
<TextField
@ -486,10 +506,11 @@ function InformationSodistorehome(props: InformationSodistorehomeProps) {
onChange={handleChange}
variant="outlined"
fullWidth
inputProps={{ readOnly: !canEdit }}
/>
</div>
{currentUser.userType == UserType.admin && (
{canEdit && (
<>
<div>
<TextField
@ -533,21 +554,23 @@ function InformationSodistorehome(props: InformationSodistorehomeProps) {
marginTop: 10
}}
>
<Button
variant="contained"
onClick={handleSubmit}
sx={{
marginLeft: '10px'
}}
disabled={!areRequiredFieldsFilled()}
>
<FormattedMessage
id="applyChanges"
defaultMessage="Apply Changes"
/>
</Button>
{canEdit && (
<Button
variant="contained"
onClick={handleSubmit}
sx={{
marginLeft: '10px'
}}
disabled={!areRequiredFieldsFilled()}
>
<FormattedMessage
id="applyChanges"
defaultMessage="Apply Changes"
/>
</Button>
)}
{currentUser.userType == UserType.admin && (
{canEdit && (
<Button
variant="contained"
onClick={handleDelete}

View File

@ -15,9 +15,11 @@ import {
IconButton,
InputLabel,
ListItem,
ListSubheader,
MenuItem,
Modal,
Select,
Typography,
useTheme
} from '@mui/material';
import { TokenContext } from 'src/contexts/tokenContext';
@ -26,6 +28,7 @@ import ListItemAvatar from '@mui/material/ListItemAvatar';
import Avatar from '@mui/material/Avatar';
import ListItemText from '@mui/material/ListItemText';
import PersonIcon from '@mui/icons-material/Person';
import FolderIcon from '@mui/icons-material/Folder';
import Button from '@mui/material/Button';
import { Close as CloseIcon } from '@mui/icons-material';
import { AccessContext } from 'src/contexts/AccessContextProvider';
@ -52,22 +55,24 @@ function UserAccess(props: UserAccessProps) {
const tokencontext = useContext(TokenContext);
const { removeToken } = tokencontext;
const context = useContext(UserContext);
const { currentUser, setUser } = context;
const { currentUser } = context;
const [openFolder, setOpenFolder] = useState(false);
const [openInstallation, setOpenInstallation] = useState(false);
const [openModal, setOpenModal] = useState(false);
const [selectedFolderNames, setSelectedFolderNames] = useState<string[]>([]);
const [selectedInstallationNames, setSelectedInstallationNames] = useState<
string[]
>([]);
const [selectedInstallationNames, setSelectedInstallationNames] = useState<string[]>([]);
// Available choices for grant modal
const [availableFolders, setAvailableFolders] = useState<I_Folder[]>([]);
const [availableInstallations, setAvailableInstallations] = useState<I_Installation[]>([]);
// Direct grants for this user
const [directFolders, setDirectFolders] = useState<{ id: number; name: string }[]>([]);
const [directInstallations, setDirectInstallations] = useState<{ id: number; name: string }[]>([]);
const [folders, setFolders] = useState<I_Folder[]>([]);
const [installations, setInstallations] = useState<I_Installation[]>([]);
const accessContext = useContext(AccessContext);
const {
fetchInstallationsForUser,
accessibleInstallationsForUser,
error,
setError,
updated,
@ -75,130 +80,118 @@ function UserAccess(props: UserAccessProps) {
updatedmessage,
errormessage,
setErrorMessage,
setUpdatedMessage,
RevokeAccessFromResource
setUpdatedMessage
} = accessContext;
const fetchFolders = useCallback(async () => {
const fetchDirectGrants = useCallback(async () => {
try {
const [foldersRes, installationsRes] = await Promise.all([
axiosConfig.get(`/GetDirectFolderAccessForUser?userId=${props.current_user.id}`),
axiosConfig.get(`/GetDirectInstallationAccessForUser?userId=${props.current_user.id}`)
]);
setDirectFolders(foldersRes.data);
setDirectInstallations(installationsRes.data);
} catch (err) {
if (err.response && err.response.status === 401) removeToken();
}
}, [props.current_user.id]);
const fetchAvailableFolders = useCallback(async () => {
return axiosConfig
.get('/GetAllFolders')
.then((res) => {
setFolders(res.data);
})
.then((res) => setAvailableFolders(res.data))
.catch((err) => {
if (err.response && err.response.status == 401) {
removeToken();
}
if (err.response && err.response.status == 401) removeToken();
});
}, [setFolders]);
}, []);
const fetchInstallations = useCallback(async () => {
const fetchAvailableInstallations = useCallback(async () => {
try {
// fetch product 0
const res0 = await axiosConfig.get(
`/GetAllInstallationsFromProduct?product=0`
);
const installations0 = res0.data;
// fetch product 1
const res1 = await axiosConfig.get(
`/GetAllInstallationsFromProduct?product=3`
);
const installations1 = res1.data;
// aggregate
const combined = [...installations0, ...installations1];
// update
setInstallations(combined);
const [res0, res1, res2, res3] = await Promise.all([
axiosConfig.get(`/GetAllInstallationsFromProduct?product=0`),
axiosConfig.get(`/GetAllInstallationsFromProduct?product=1`),
axiosConfig.get(`/GetAllInstallationsFromProduct?product=2`),
axiosConfig.get(`/GetAllInstallationsFromProduct?product=3`)
]);
setAvailableInstallations([...res0.data, ...res1.data, ...res2.data, ...res3.data]);
} catch (err) {
if (err.response && err.response.status === 401) {
removeToken();
}
} finally {
if (err.response && err.response.status === 401) removeToken();
}
}, [setInstallations]);
}, []);
useEffect(() => {
fetchInstallationsForUser(props.current_user.id);
fetchDirectGrants();
}, [props.current_user]);
const handleGrantAccess = () => {
fetchFolders();
fetchInstallations();
fetchAvailableFolders();
fetchAvailableInstallations();
setOpenModal(true);
setSelectedFolderNames([]);
setSelectedInstallationNames([]);
};
const handleFolderChange = (event) => {
setSelectedFolderNames(event.target.value);
const handleRevokeFolder = async (folderId: number, folderName: string) => {
axiosConfig
.post(`/RevokeUserAccessToFolder?UserId=${props.current_user.id}&FolderId=${folderId}`)
.then(() => {
setUpdatedMessage(intl.formatMessage({ id: 'revokedAccessFromUser' }) + ' ' + props.current_user.name);
setUpdated(true);
setTimeout(() => setUpdated(false), 3000);
fetchDirectGrants();
})
.catch(() => {
setErrorMessage(intl.formatMessage({ id: 'unableToRevokeAccess' }));
setError(true);
});
};
const handleInstallationChange = (event) => {
setSelectedInstallationNames(event.target.value);
};
const handleOpenFolder = () => {
setOpenFolder(true);
};
const handleCloseFolder = () => {
setOpenFolder(false);
};
const handleCancel = () => {
setOpenModal(false);
};
const handleOpenInstallation = () => {
setOpenInstallation(true);
};
const handleCloseInstallation = () => {
setOpenInstallation(false);
const handleRevokeInstallation = async (installationId: number) => {
axiosConfig
.post(`/RevokeUserAccessToInstallation?UserId=${props.current_user.id}&InstallationId=${installationId}`)
.then(() => {
setUpdatedMessage(intl.formatMessage({ id: 'revokedAccessFromUser' }) + ' ' + props.current_user.name);
setUpdated(true);
setTimeout(() => setUpdated(false), 3000);
fetchDirectGrants();
})
.catch(() => {
setErrorMessage(intl.formatMessage({ id: 'unableToRevokeAccess' }));
setError(true);
});
};
const handleSubmit = async () => {
for (const folderName of selectedFolderNames) {
const folder = folders.find((folder) => folder.name === folderName);
const folder = availableFolders.find((f) => f.name === folderName);
await axiosConfig
.post(
`/GrantUserAccessToFolder?UserId=${props.current_user.id}&FolderId=${folder.id}`
)
.then((response) => {
if (response) {
setUpdatedMessage(intl.formatMessage({ id: 'grantedAccessToUser' }, { name: props.current_user.name }));
setUpdated(true);
}
.post(`/GrantUserAccessToFolder?UserId=${props.current_user.id}&FolderId=${folder.id}`)
.then(() => {
setUpdatedMessage(intl.formatMessage({ id: 'grantedAccessToUser' }, { name: props.current_user.name }));
setUpdated(true);
})
.catch((err) => {
.catch(() => {
setErrorMessage(intl.formatMessage({ id: 'errorOccured' }));
setError(true);
});
}
for (const installationName of selectedInstallationNames) {
const installation = installations.find(
(installation) => installation.name === installationName
);
const installation = availableInstallations.find((i) => i.name === installationName);
await axiosConfig
.post(
`/GrantUserAccessToInstallation?UserId=${props.current_user.id}&InstallationId=${installation.id}`
)
.then((response) => {
if (response) {
setUpdatedMessage(intl.formatMessage({ id: 'grantedAccessToUser' }, { name: props.current_user.name }));
setUpdated(true);
}
.post(`/GrantUserAccessToInstallation?UserId=${props.current_user.id}&InstallationId=${installation.id}`)
.then(() => {
setUpdatedMessage(intl.formatMessage({ id: 'grantedAccessToUser' }, { name: props.current_user.name }));
setUpdated(true);
})
.catch((err) => {
.catch(() => {
setErrorMessage(intl.formatMessage({ id: 'errorOccured' }));
setError(true);
});
}
setOpenModal(false);
fetchInstallationsForUser(props.current_user.id);
fetchDirectGrants();
};
return (
@ -206,51 +199,25 @@ function UserAccess(props: UserAccessProps) {
<Grid container>
<Grid item xs={12} md={12}>
{updated && (
<Alert
severity="success"
sx={{
mt: 1
}}
>
<Alert severity="success" sx={{ mt: 1 }}>
{updatedmessage}
<IconButton
color="inherit"
size="small"
onClick={() => setUpdated(false)}
>
<IconButton color="inherit" size="small" onClick={() => setUpdated(false)}>
<CloseIcon fontSize="small" />
</IconButton>
</Alert>
)}
{error && (
<Alert
severity="error"
sx={{
marginTop: '20px',
marginBottom: '20px'
}}
>
<Alert severity="error" sx={{ marginTop: '20px', marginBottom: '20px' }}>
{errormessage}
<IconButton
color="inherit"
size="small"
onClick={() => setError(false)}
sx={{
marginLeft: '10px'
}}
>
<IconButton color="inherit" size="small" onClick={() => setError(false)} sx={{ marginLeft: '10px' }}>
<CloseIcon fontSize="small" />
</IconButton>
</Alert>
)}
<Modal
open={openModal}
onClose={() => {}}
aria-labelledby="error-modal"
aria-describedby="error-modal-description"
>
{/* Grant Access Modal */}
<Modal open={openModal} onClose={() => {}} aria-labelledby="grant-modal">
<Box
sx={{
position: 'absolute',
@ -264,59 +231,29 @@ function UserAccess(props: UserAccessProps) {
p: 4
}}
>
<Box
component="form"
sx={{
textAlign: 'center'
}}
noValidate
autoComplete="off"
>
<Box component="form" sx={{ textAlign: 'center' }} noValidate autoComplete="off">
<div>
<FormControl fullWidth sx={{ marginTop: 1, width: 390 }}>
<InputLabel
sx={{
fontSize: 14,
backgroundColor: 'white'
}}
>
<FormattedMessage
id="grantAccessToFolders"
defaultMessage="Grant access to folders"
/>
<InputLabel sx={{ fontSize: 14, backgroundColor: 'white' }}>
<FormattedMessage id="grantAccessToFolders" defaultMessage="Grant access to folders" />
</InputLabel>
<Select
multiple
value={selectedFolderNames}
onChange={handleFolderChange}
onChange={(e) => setSelectedFolderNames(e.target.value as string[])}
open={openFolder}
onClose={handleCloseFolder}
onOpen={handleOpenFolder}
onClose={() => setOpenFolder(false)}
onOpen={() => setOpenFolder(true)}
renderValue={(selected) => (
<div>
{selected.map((folder) => (
<span key={folder}>{folder}, </span>
))}
</div>
<div>{selected.map((f) => <span key={f}>{f}, </span>)}</div>
)}
>
{folders.map((folder) => (
<MenuItem key={folder.id} value={folder.name}>
{folder.name}
</MenuItem>
{availableFolders.map((folder) => (
<MenuItem key={folder.id} value={folder.name}>{folder.name}</MenuItem>
))}
<Button
sx={{
marginLeft: '150px',
marginTop: '10px',
backgroundColor: theme.colors.primary.main,
color: 'white',
'&:hover': {
backgroundColor: theme.colors.primary.dark
},
padding: '6px 8px'
}}
onClick={handleCloseFolder}
sx={{ marginLeft: '150px', marginTop: '10px', backgroundColor: theme.colors.primary.main, color: 'white', '&:hover': { backgroundColor: theme.colors.primary.dark }, padding: '6px 8px' }}
onClick={() => setOpenFolder(false)}
>
<FormattedMessage id="submit" defaultMessage="Submit" />
</Button>
@ -326,52 +263,26 @@ function UserAccess(props: UserAccessProps) {
<div>
<FormControl fullWidth sx={{ marginTop: 2, width: 390 }}>
<InputLabel
sx={{
fontSize: 14,
backgroundColor: 'white'
}}
>
<FormattedMessage
id="grantAccessToInstallations"
defaultMessage="Grant access to installations"
/>
<InputLabel sx={{ fontSize: 14, backgroundColor: 'white' }}>
<FormattedMessage id="grantAccessToInstallations" defaultMessage="Grant access to installations" />
</InputLabel>
<Select
multiple
value={selectedInstallationNames}
onChange={handleInstallationChange}
onChange={(e) => setSelectedInstallationNames(e.target.value as string[])}
open={openInstallation}
onClose={handleCloseInstallation}
onOpen={handleOpenInstallation}
onClose={() => setOpenInstallation(false)}
onOpen={() => setOpenInstallation(true)}
renderValue={(selected) => (
<div>
{selected.map((installation) => (
<span key={installation}>{installation}, </span>
))}
</div>
<div>{selected.map((i) => <span key={i}>{i}, </span>)}</div>
)}
>
{installations.map((installation) => (
<MenuItem
key={installation.id}
value={installation.name}
>
{installation.name}
</MenuItem>
{availableInstallations.map((installation) => (
<MenuItem key={installation.id} value={installation.name}>{installation.name}</MenuItem>
))}
<Button
sx={{
marginLeft: '150px',
marginTop: '10px',
backgroundColor: theme.colors.primary.main,
color: 'white',
'&:hover': {
backgroundColor: theme.colors.primary.dark
},
padding: '6px 8px'
}}
onClick={handleCloseInstallation}
sx={{ marginLeft: '150px', marginTop: '10px', backgroundColor: theme.colors.primary.main, color: 'white', '&:hover': { backgroundColor: theme.colors.primary.dark }, padding: '6px 8px' }}
onClick={() => setOpenInstallation(false)}
>
<FormattedMessage id="submit" defaultMessage="Submit" />
</Button>
@ -380,32 +291,15 @@ function UserAccess(props: UserAccessProps) {
</div>
<Button
sx={{
marginTop: '20px',
backgroundColor: theme.colors.primary.main,
color: 'white',
'&:hover': {
backgroundColor: theme.colors.primary.dark
},
padding: '6px 8px'
}}
sx={{ marginTop: '20px', backgroundColor: theme.colors.primary.main, color: 'white', '&:hover': { backgroundColor: theme.colors.primary.dark }, padding: '6px 8px' }}
onClick={handleSubmit}
>
<FormattedMessage id="submit" defaultMessage="Submit" />
</Button>
<Button
variant="contained"
onClick={handleCancel}
sx={{
marginTop: '20px',
marginLeft: '10px',
backgroundColor: theme.colors.primary.main,
color: 'white',
'&:hover': {
backgroundColor: theme.colors.primary.dark
},
padding: '6px 8px'
}}
onClick={() => setOpenModal(false)}
sx={{ marginTop: '20px', marginLeft: '10px', backgroundColor: theme.colors.primary.main, color: 'white', '&:hover': { backgroundColor: theme.colors.primary.dark }, padding: '6px 8px' }}
>
<FormattedMessage id="cancel" defaultMessage="Cancel" />
</Button>
@ -416,43 +310,63 @@ function UserAccess(props: UserAccessProps) {
<Button
variant="contained"
onClick={handleGrantAccess}
sx={{
marginTop: '20px',
marginBottom: '20px',
backgroundColor: '#ffc04d',
color: '#000000',
'&:hover': { bgcolor: '#f7b34d' }
}}
sx={{ marginTop: '20px', marginBottom: '20px', backgroundColor: '#ffc04d', color: '#000000', '&:hover': { bgcolor: '#f7b34d' } }}
>
<FormattedMessage id="grantAccess" defaultMessage="Grant Access" />
</Button>
</Grid>
<Grid item xs={12} md={12}>
{accessibleInstallationsForUser.map((installation, index) => {
const isLast = index === accessibleInstallationsForUser.length - 1;
{/* Folder Access Section */}
<Grid item xs={12} md={12}>
<Typography variant="subtitle2" sx={{ mt: 1, mb: 0.5, color: 'text.secondary', fontWeight: 600 }}>
<FormattedMessage id="folderAccess" defaultMessage="Folder Access" />
</Typography>
{directFolders.map((folder, index) => {
const isLast = index === directFolders.length - 1;
return (
<Fragment key={installation.name}>
<Fragment key={folder.id}>
<ListItem
sx={{
mb: isLast ? 4 : 0 // Apply margin-bottom to the last item only
}}
sx={{ mb: isLast ? 1 : 0 }}
secondaryAction={
currentUser.userType === UserType.admin && (
<IconButton
onClick={() => {
RevokeAccessFromResource(
'ToInstallation',
props.current_user.id,
'InstallationId',
installation.id,
props.current_user.name
);
<IconButton onClick={() => handleRevokeFolder(folder.id, folder.name)} edge="end">
<PersonRemoveIcon />
</IconButton>
)
}
>
<ListItemAvatar>
<Avatar>
<FolderIcon />
</Avatar>
</ListItemAvatar>
<ListItemText primary={folder.name} />
</ListItem>
<Divider />
</Fragment>
);
})}
{directFolders.length === 0 && (
<Alert severity="info" sx={{ mb: 1 }}>
<FormattedMessage id="noDirectFolderAccess" defaultMessage="No folder access grants" />
</Alert>
)}
</Grid>
fetchInstallationsForUser(props.current_user.id);
}}
edge="end"
>
{/* Direct Installation Access Section */}
<Grid item xs={12} md={12}>
<Typography variant="subtitle2" sx={{ mt: 2, mb: 0.5, color: 'text.secondary', fontWeight: 600 }}>
<FormattedMessage id="directInstallationAccess" defaultMessage="Direct Installation Access" />
</Typography>
{directInstallations.map((installation, index) => {
const isLast = index === directInstallations.length - 1;
return (
<Fragment key={installation.id}>
<ListItem
sx={{ mb: isLast ? 4 : 0 }}
secondaryAction={
currentUser.userType === UserType.admin && (
<IconButton onClick={() => handleRevokeInstallation(installation.id)} edge="end">
<PersonRemoveIcon />
</IconButton>
)
@ -469,22 +383,9 @@ function UserAccess(props: UserAccessProps) {
</Fragment>
);
})}
{accessibleInstallationsForUser.length == 0 && (
<Alert
severity="error"
sx={{
display: 'flex',
alignItems: 'center',
marginTop: '20px',
marginBottom: '20px'
}}
>
<FormattedMessage
id="theUserDoesNOtHaveAccessToAnyInstallation"
defaultMessage="The user does not have access to any installation "
/>
<IconButton color="inherit" size="small"></IconButton>
{directInstallations.length === 0 && (
<Alert severity="info" sx={{ mb: 4 }}>
<FormattedMessage id="noDirectInstallationAccess" defaultMessage="No direct installation access grants" />
</Alert>
)}
</Grid>

View File

@ -474,16 +474,18 @@ function SodioHomeInstallation(props: singleInstallationProps) {
}
/>
<Route
path={routes.log}
element={
<Log
errorLoadingS3Data={errorLoadingS3Data}
id={props.current_installation.id}
status={props.current_installation.status}
></Log>
}
/>
{currentUser.userType !== UserType.client && (
<Route
path={routes.log}
element={
<Log
errorLoadingS3Data={errorLoadingS3Data}
id={props.current_installation.id}
status={props.current_installation.status}
></Log>
}
/>
)}
<Route
path={routes.live}
@ -497,18 +499,20 @@ function SodioHomeInstallation(props: singleInstallationProps) {
}
/>
<Route
path={routes.batteryview + '/*'}
element={
<BatteryViewSodioHome
values={values}
s3Credentials={s3Credentials}
installationId={props.current_installation.id}
installation={props.current_installation}
connected={connected}
></BatteryViewSodioHome>
}
></Route>
{currentUser.userType !== UserType.client && (
<Route
path={routes.batteryview + '/*'}
element={
<BatteryViewSodioHome
values={values}
s3Credentials={s3Credentials}
installationId={props.current_installation.id}
installation={props.current_installation}
connected={connected}
></BatteryViewSodioHome>
}
/>
)}
{currentUser.userType == UserType.admin && (
<Route
@ -559,16 +563,14 @@ function SodioHomeInstallation(props: singleInstallationProps) {
}
/>
{currentUser.userType == UserType.admin && (
<Route
path={routes.report}
element={
<WeeklyReport
installationId={props.current_installation.id}
/>
}
/>
)}
<Route
path={routes.report}
element={
<WeeklyReport
installationId={props.current_installation.id}
/>
}
/>
<Route
path={'*'}

View File

@ -118,7 +118,6 @@ function SodioHomeInstallationTabs(props: SodioHomeInstallationTabsProps) {
value: 'log',
label: <FormattedMessage id="log" defaultMessage="Log" />
},
{
value: 'manage',
label: (
@ -128,14 +127,12 @@ function SodioHomeInstallationTabs(props: SodioHomeInstallationTabsProps) {
/>
)
},
{
value: 'information',
label: (
<FormattedMessage id="information" defaultMessage="Information" />
)
},
{
value: 'configuration',
label: (
@ -164,6 +161,45 @@ function SodioHomeInstallationTabs(props: SodioHomeInstallationTabsProps) {
)
}
]
: currentUser.userType == UserType.partner
? [
{
value: 'live',
label: <FormattedMessage id="live" defaultMessage="Live" />
},
{
value: 'overview',
label: <FormattedMessage id="overview" defaultMessage="Overview" />
},
{
value: 'batteryview',
label: (
<FormattedMessage
id="batteryview"
defaultMessage="Battery View"
/>
)
},
{
value: 'log',
label: <FormattedMessage id="log" defaultMessage="Log" />
},
{
value: 'information',
label: (
<FormattedMessage id="information" defaultMessage="Information" />
)
},
{
value: 'report',
label: (
<FormattedMessage
id="report"
defaultMessage="Report"
/>
)
}
]
: [
{
value: 'live',
@ -178,14 +214,24 @@ function SodioHomeInstallationTabs(props: SodioHomeInstallationTabsProps) {
label: (
<FormattedMessage id="information" defaultMessage="Information" />
)
},
{
value: 'report',
label: (
<FormattedMessage
id="report"
defaultMessage="Report"
/>
)
}
];
const tabs =
const inInstallationView =
currentTab != 'list' &&
currentTab != 'tree' &&
!location.pathname.includes('folder') &&
currentUser.userType == UserType.admin
!location.pathname.includes('folder');
const tabs = inInstallationView && currentUser.userType == UserType.admin
? [
{
value: 'list',
@ -216,7 +262,6 @@ function SodioHomeInstallationTabs(props: SodioHomeInstallationTabsProps) {
value: 'log',
label: <FormattedMessage id="log" defaultMessage="Log" />
},
{
value: 'manage',
label: (
@ -226,7 +271,6 @@ function SodioHomeInstallationTabs(props: SodioHomeInstallationTabsProps) {
/>
)
},
{
value: 'information',
label: (
@ -261,10 +305,7 @@ function SodioHomeInstallationTabs(props: SodioHomeInstallationTabsProps) {
)
}
]
: currentTab != 'list' &&
currentTab != 'tree' &&
!location.pathname.includes('folder') &&
currentUser.userType == UserType.client
: inInstallationView && currentUser.userType == UserType.partner
? [
{
value: 'list',
@ -282,12 +323,67 @@ function SodioHomeInstallationTabs(props: SodioHomeInstallationTabsProps) {
value: 'overview',
label: <FormattedMessage id="overview" defaultMessage="Overview" />
},
{
value: 'batteryview',
label: (
<FormattedMessage
id="batteryview"
defaultMessage="Battery View"
/>
)
},
{
value: 'log',
label: <FormattedMessage id="log" defaultMessage="Log" />
},
{
value: 'information',
label: (
<FormattedMessage id="information" defaultMessage="Information" />
)
},
{
value: 'report',
label: (
<FormattedMessage
id="report"
defaultMessage="Report"
/>
)
}
]
: inInstallationView && currentUser.userType == UserType.client
? [
{
value: 'list',
icon: <ListIcon id="mode-toggle-button-list-icon" />
},
{
value: 'tree',
icon: <AccountTreeIcon id="mode-toggle-button-tree-icon" />
},
{
value: 'live',
label: <FormattedMessage id="live" defaultMessage="Live" />
},
{
value: 'overview',
label: <FormattedMessage id="overview" defaultMessage="Overview" />
},
{
value: 'information',
label: (
<FormattedMessage id="information" defaultMessage="Information" />
)
},
{
value: 'report',
label: (
<FormattedMessage
id="report"
defaultMessage="Report"
/>
)
}
]
: [

View File

@ -44,12 +44,12 @@ function Folder(props: singleFolderProps) {
value: 'information',
label: <FormattedMessage id="information" defaultMessage="Information" />
},
{
...(currentUser.userType === UserType.admin ? [{
value: 'manage',
label: (
<FormattedMessage id="manageAccess" defaultMessage="Manage Access" />
)
}
}] : [])
];
const handleTabsChange = (_event: ChangeEvent<{}>, value: string): void => {