Compare commits

..

No commits in common. "7476c939c3b363d4033288f57e7e8dacaec2793b" and "d464c9cd716444a25f004d90061057c05382ea93" have entirely different histories.

8 changed files with 453 additions and 517 deletions

View File

@ -363,38 +363,6 @@ public class Controller : ControllerBase
return user.AccessibleInstallations().ToList(); 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))] [HttpGet(nameof(GetUsersWithInheritedAccessToInstallation))]
public ActionResult<IEnumerable<Object>> GetUsersWithInheritedAccessToInstallation(Int64 id, Token authToken) public ActionResult<IEnumerable<Object>> GetUsersWithInheritedAccessToInstallation(Int64 id, Token authToken)
{ {

View File

@ -376,7 +376,8 @@ public static class SessionMethods
&& sessionUser.HasAccessTo(originalUser) && sessionUser.HasAccessTo(originalUser)
&& editedUser && editedUser
.WithParentOf(originalUser) // prevent moving .WithParentOf(originalUser) // prevent moving
.WithPasswordOf(originalUser) .WithNameOf(originalUser)
.WithPasswordOf(originalUser)
.Apply(Db.Update); .Apply(Db.Update);
} }
@ -472,36 +473,24 @@ public static class SessionMethods
public static Boolean RevokeUserAccessTo(this Session? session, User? user, Installation? installation) public static Boolean RevokeUserAccessTo(this Session? session, User? user, Installation? installation)
{ {
var sessionUser = session?.User; var sessionUser = session?.User;
if (sessionUser is null || installation is null || user is null) return sessionUser is not null
return false; && installation is not null
&& user is not null
if (!(user.IsDescendantOf(sessionUser) || sessionUser.UserType == 2)) && user.IsDescendantOf(sessionUser)
return false; && sessionUser.HasAccessTo(installation)
&& user.HasAccessTo(installation)
if (!sessionUser.HasAccessTo(installation) || !user.HasAccessTo(installation)) && Db.InstallationAccess.Delete(a => a.UserId == user.Id && a.InstallationId == installation.Id) > 0;
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) public static Boolean RevokeUserAccessTo(this Session? session, User? user, Folder? folder)
{ {
var sessionUser = session?.User; var sessionUser = session?.User;
return sessionUser is not null return sessionUser is not null
&& folder is not null && folder is not null
&& user is not null && user is not null
&& (user.IsDescendantOf(sessionUser) || sessionUser.UserType == 2) && user.IsDescendantOf(sessionUser)
&& sessionUser.HasAccessTo(folder) && sessionUser.HasAccessTo(folder)
&& user.HasAccessTo(folder) && user.HasAccessTo(folder)
&& Db.FolderAccess.Delete(a => a.UserId == user.Id && a.FolderId == folder.Id) > 0; && Db.FolderAccess.Delete(a => a.UserId == user.Id && a.FolderId == folder.Id) > 0;

View File

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

View File

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

View File

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

View File

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

View File

@ -118,6 +118,7 @@ function SodioHomeInstallationTabs(props: SodioHomeInstallationTabsProps) {
value: 'log', value: 'log',
label: <FormattedMessage id="log" defaultMessage="Log" /> label: <FormattedMessage id="log" defaultMessage="Log" />
}, },
{ {
value: 'manage', value: 'manage',
label: ( label: (
@ -127,12 +128,14 @@ function SodioHomeInstallationTabs(props: SodioHomeInstallationTabsProps) {
/> />
) )
}, },
{ {
value: 'information', value: 'information',
label: ( label: (
<FormattedMessage id="information" defaultMessage="Information" /> <FormattedMessage id="information" defaultMessage="Information" />
) )
}, },
{ {
value: 'configuration', value: 'configuration',
label: ( label: (
@ -161,45 +164,6 @@ 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', value: 'live',
@ -214,24 +178,14 @@ function SodioHomeInstallationTabs(props: SodioHomeInstallationTabsProps) {
label: ( label: (
<FormattedMessage id="information" defaultMessage="Information" /> <FormattedMessage id="information" defaultMessage="Information" />
) )
},
{
value: 'report',
label: (
<FormattedMessage
id="report"
defaultMessage="Report"
/>
)
} }
]; ];
const inInstallationView = const tabs =
currentTab != 'list' && currentTab != 'list' &&
currentTab != 'tree' && currentTab != 'tree' &&
!location.pathname.includes('folder'); !location.pathname.includes('folder') &&
currentUser.userType == UserType.admin
const tabs = inInstallationView && currentUser.userType == UserType.admin
? [ ? [
{ {
value: 'list', value: 'list',
@ -262,6 +216,7 @@ function SodioHomeInstallationTabs(props: SodioHomeInstallationTabsProps) {
value: 'log', value: 'log',
label: <FormattedMessage id="log" defaultMessage="Log" /> label: <FormattedMessage id="log" defaultMessage="Log" />
}, },
{ {
value: 'manage', value: 'manage',
label: ( label: (
@ -271,6 +226,7 @@ function SodioHomeInstallationTabs(props: SodioHomeInstallationTabsProps) {
/> />
) )
}, },
{ {
value: 'information', value: 'information',
label: ( label: (
@ -305,7 +261,10 @@ function SodioHomeInstallationTabs(props: SodioHomeInstallationTabsProps) {
) )
} }
] ]
: inInstallationView && currentUser.userType == UserType.partner : currentTab != 'list' &&
currentTab != 'tree' &&
!location.pathname.includes('folder') &&
currentUser.userType == UserType.client
? [ ? [
{ {
value: 'list', value: 'list',
@ -323,67 +282,12 @@ function SodioHomeInstallationTabs(props: SodioHomeInstallationTabsProps) {
value: 'overview', value: 'overview',
label: <FormattedMessage id="overview" defaultMessage="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', value: 'information',
label: ( label: (
<FormattedMessage id="information" defaultMessage="Information" /> <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', value: 'information',
label: <FormattedMessage id="information" defaultMessage="Information" /> label: <FormattedMessage id="information" defaultMessage="Information" />
}, },
...(currentUser.userType === UserType.admin ? [{ {
value: 'manage', value: 'manage',
label: ( label: (
<FormattedMessage id="manageAccess" defaultMessage="Manage Access" /> <FormattedMessage id="manageAccess" defaultMessage="Manage Access" />
) )
}] : []) }
]; ];
const handleTabsChange = (_event: ChangeEvent<{}>, value: string): void => { const handleTabsChange = (_event: ChangeEvent<{}>, value: string): void => {