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(); 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,8 +376,7 @@ public static class SessionMethods
&& sessionUser.HasAccessTo(originalUser) && sessionUser.HasAccessTo(originalUser)
&& editedUser && editedUser
.WithParentOf(originalUser) // prevent moving .WithParentOf(originalUser) // prevent moving
.WithNameOf(originalUser) .WithPasswordOf(originalUser)
.WithPasswordOf(originalUser)
.Apply(Db.Update); .Apply(Db.Update);
} }
@ -473,24 +472,36 @@ 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;
return sessionUser is not null if (sessionUser is null || installation is null || user is null)
&& installation is not null return false;
&& user is not null
&& user.IsDescendantOf(sessionUser) if (!(user.IsDescendantOf(sessionUser) || sessionUser.UserType == 2))
&& sessionUser.HasAccessTo(installation) return false;
&& user.HasAccessTo(installation)
&& Db.InstallationAccess.Delete(a => a.UserId == user.Id && a.InstallationId == installation.Id) > 0; 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) 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) && (user.IsDescendantOf(sessionUser) || sessionUser.UserType == 2)
&& 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,10 +40,9 @@ 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;
// these columns must not be modified! // ParentId must not be modified via this method
user.ParentId = originalUser.ParentId; user.ParentId = originalUser.ParentId;
user.Name = originalUser.Name;
return Update(obj: user); return Update(obj: user);
} }

View File

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

View File

@ -15,9 +15,11 @@ 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';
@ -26,6 +28,7 @@ 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';
@ -52,22 +55,24 @@ 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, setUser } = context; const { currentUser } = 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< const [selectedInstallationNames, setSelectedInstallationNames] = useState<string[]>([]);
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,
@ -75,130 +80,118 @@ function UserAccess(props: UserAccessProps) {
updatedmessage, updatedmessage,
errormessage, errormessage,
setErrorMessage, setErrorMessage,
setUpdatedMessage, setUpdatedMessage
RevokeAccessFromResource
} = accessContext; } = 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 return axiosConfig
.get('/GetAllFolders') .get('/GetAllFolders')
.then((res) => { .then((res) => setAvailableFolders(res.data))
setFolders(res.data);
})
.catch((err) => { .catch((err) => {
if (err.response && err.response.status == 401) { if (err.response && err.response.status == 401) removeToken();
removeToken();
}
}); });
}, [setFolders]); }, []);
const fetchInstallations = useCallback(async () => { const fetchAvailableInstallations = useCallback(async () => {
try { try {
// fetch product 0 const [res0, res1, res2, res3] = await Promise.all([
const res0 = await axiosConfig.get( axiosConfig.get(`/GetAllInstallationsFromProduct?product=0`),
`/GetAllInstallationsFromProduct?product=0` axiosConfig.get(`/GetAllInstallationsFromProduct?product=1`),
); axiosConfig.get(`/GetAllInstallationsFromProduct?product=2`),
const installations0 = res0.data; axiosConfig.get(`/GetAllInstallationsFromProduct?product=3`)
]);
// fetch product 1 setAvailableInstallations([...res0.data, ...res1.data, ...res2.data, ...res3.data]);
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) { if (err.response && err.response.status === 401) removeToken();
removeToken();
}
} finally {
} }
}, [setInstallations]); }, []);
useEffect(() => { useEffect(() => {
fetchInstallationsForUser(props.current_user.id); fetchDirectGrants();
}, [props.current_user]); }, [props.current_user]);
const handleGrantAccess = () => { const handleGrantAccess = () => {
fetchFolders(); fetchAvailableFolders();
fetchInstallations(); fetchAvailableInstallations();
setOpenModal(true); setOpenModal(true);
setSelectedFolderNames([]); setSelectedFolderNames([]);
setSelectedInstallationNames([]); setSelectedInstallationNames([]);
}; };
const handleFolderChange = (event) => { const handleRevokeFolder = async (folderId: number, folderName: string) => {
setSelectedFolderNames(event.target.value); 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) => { const handleRevokeInstallation = async (installationId: number) => {
setSelectedInstallationNames(event.target.value); axiosConfig
}; .post(`/RevokeUserAccessToInstallation?UserId=${props.current_user.id}&InstallationId=${installationId}`)
const handleOpenFolder = () => { .then(() => {
setOpenFolder(true); setUpdatedMessage(intl.formatMessage({ id: 'revokedAccessFromUser' }) + ' ' + props.current_user.name);
}; setUpdated(true);
setTimeout(() => setUpdated(false), 3000);
const handleCloseFolder = () => { fetchDirectGrants();
setOpenFolder(false); })
}; .catch(() => {
const handleCancel = () => { setErrorMessage(intl.formatMessage({ id: 'unableToRevokeAccess' }));
setOpenModal(false); setError(true);
}; });
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 = folders.find((folder) => folder.name === folderName); const folder = availableFolders.find((f) => f.name === folderName);
await axiosConfig await axiosConfig
.post( .post(`/GrantUserAccessToFolder?UserId=${props.current_user.id}&FolderId=${folder.id}`)
`/GrantUserAccessToFolder?UserId=${props.current_user.id}&FolderId=${folder.id}` .then(() => {
) setUpdatedMessage(intl.formatMessage({ id: 'grantedAccessToUser' }, { name: props.current_user.name }));
.then((response) => { setUpdated(true);
if (response) {
setUpdatedMessage(intl.formatMessage({ id: 'grantedAccessToUser' }, { name: props.current_user.name }));
setUpdated(true);
}
}) })
.catch((err) => { .catch(() => {
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 = installations.find( const installation = availableInstallations.find((i) => i.name === installationName);
(installation) => installation.name === installationName
);
await axiosConfig await axiosConfig
.post( .post(`/GrantUserAccessToInstallation?UserId=${props.current_user.id}&InstallationId=${installation.id}`)
`/GrantUserAccessToInstallation?UserId=${props.current_user.id}&InstallationId=${installation.id}` .then(() => {
) setUpdatedMessage(intl.formatMessage({ id: 'grantedAccessToUser' }, { name: props.current_user.name }));
.then((response) => { setUpdated(true);
if (response) {
setUpdatedMessage(intl.formatMessage({ id: 'grantedAccessToUser' }, { name: props.current_user.name }));
setUpdated(true);
}
}) })
.catch((err) => { .catch(() => {
setErrorMessage(intl.formatMessage({ id: 'errorOccured' })); setErrorMessage(intl.formatMessage({ id: 'errorOccured' }));
setError(true); setError(true);
}); });
} }
setOpenModal(false); setOpenModal(false);
fetchInstallationsForUser(props.current_user.id); fetchDirectGrants();
}; };
return ( return (
@ -206,51 +199,25 @@ function UserAccess(props: UserAccessProps) {
<Grid container> <Grid container>
<Grid item xs={12} md={12}> <Grid item xs={12} md={12}>
{updated && ( {updated && (
<Alert <Alert severity="success" sx={{ mt: 1 }}>
severity="success"
sx={{
mt: 1
}}
>
{updatedmessage} {updatedmessage}
<IconButton <IconButton color="inherit" size="small" onClick={() => setUpdated(false)}>
color="inherit"
size="small"
onClick={() => setUpdated(false)}
>
<CloseIcon fontSize="small" /> <CloseIcon fontSize="small" />
</IconButton> </IconButton>
</Alert> </Alert>
)} )}
{error && ( {error && (
<Alert <Alert severity="error" sx={{ marginTop: '20px', marginBottom: '20px' }}>
severity="error"
sx={{
marginTop: '20px',
marginBottom: '20px'
}}
>
{errormessage} {errormessage}
<IconButton <IconButton color="inherit" size="small" onClick={() => setError(false)} sx={{ marginLeft: '10px' }}>
color="inherit"
size="small"
onClick={() => setError(false)}
sx={{
marginLeft: '10px'
}}
>
<CloseIcon fontSize="small" /> <CloseIcon fontSize="small" />
</IconButton> </IconButton>
</Alert> </Alert>
)} )}
<Modal {/* Grant Access Modal */}
open={openModal} <Modal open={openModal} onClose={() => {}} aria-labelledby="grant-modal">
onClose={() => {}}
aria-labelledby="error-modal"
aria-describedby="error-modal-description"
>
<Box <Box
sx={{ sx={{
position: 'absolute', position: 'absolute',
@ -264,59 +231,29 @@ function UserAccess(props: UserAccessProps) {
p: 4 p: 4
}} }}
> >
<Box <Box component="form" sx={{ textAlign: 'center' }} noValidate autoComplete="off">
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 <InputLabel sx={{ fontSize: 14, backgroundColor: 'white' }}>
sx={{ <FormattedMessage id="grantAccessToFolders" defaultMessage="Grant access to folders" />
fontSize: 14,
backgroundColor: 'white'
}}
>
<FormattedMessage
id="grantAccessToFolders"
defaultMessage="Grant access to folders"
/>
</InputLabel> </InputLabel>
<Select <Select
multiple multiple
value={selectedFolderNames} value={selectedFolderNames}
onChange={handleFolderChange} onChange={(e) => setSelectedFolderNames(e.target.value as string[])}
open={openFolder} open={openFolder}
onClose={handleCloseFolder} onClose={() => setOpenFolder(false)}
onOpen={handleOpenFolder} onOpen={() => setOpenFolder(true)}
renderValue={(selected) => ( renderValue={(selected) => (
<div> <div>{selected.map((f) => <span key={f}>{f}, </span>)}</div>
{selected.map((folder) => (
<span key={folder}>{folder}, </span>
))}
</div>
)} )}
> >
{folders.map((folder) => ( {availableFolders.map((folder) => (
<MenuItem key={folder.id} value={folder.name}> <MenuItem key={folder.id} value={folder.name}>{folder.name}</MenuItem>
{folder.name}
</MenuItem>
))} ))}
<Button <Button
sx={{ sx={{ marginLeft: '150px', marginTop: '10px', backgroundColor: theme.colors.primary.main, color: 'white', '&:hover': { backgroundColor: theme.colors.primary.dark }, padding: '6px 8px' }}
marginLeft: '150px', onClick={() => setOpenFolder(false)}
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>
@ -326,52 +263,26 @@ function UserAccess(props: UserAccessProps) {
<div> <div>
<FormControl fullWidth sx={{ marginTop: 2, width: 390 }}> <FormControl fullWidth sx={{ marginTop: 2, width: 390 }}>
<InputLabel <InputLabel sx={{ fontSize: 14, backgroundColor: 'white' }}>
sx={{ <FormattedMessage id="grantAccessToInstallations" defaultMessage="Grant access to installations" />
fontSize: 14,
backgroundColor: 'white'
}}
>
<FormattedMessage
id="grantAccessToInstallations"
defaultMessage="Grant access to installations"
/>
</InputLabel> </InputLabel>
<Select <Select
multiple multiple
value={selectedInstallationNames} value={selectedInstallationNames}
onChange={handleInstallationChange} onChange={(e) => setSelectedInstallationNames(e.target.value as string[])}
open={openInstallation} open={openInstallation}
onClose={handleCloseInstallation} onClose={() => setOpenInstallation(false)}
onOpen={handleOpenInstallation} onOpen={() => setOpenInstallation(true)}
renderValue={(selected) => ( renderValue={(selected) => (
<div> <div>{selected.map((i) => <span key={i}>{i}, </span>)}</div>
{selected.map((installation) => (
<span key={installation}>{installation}, </span>
))}
</div>
)} )}
> >
{installations.map((installation) => ( {availableInstallations.map((installation) => (
<MenuItem <MenuItem key={installation.id} value={installation.name}>{installation.name}</MenuItem>
key={installation.id}
value={installation.name}
>
{installation.name}
</MenuItem>
))} ))}
<Button <Button
sx={{ sx={{ marginLeft: '150px', marginTop: '10px', backgroundColor: theme.colors.primary.main, color: 'white', '&:hover': { backgroundColor: theme.colors.primary.dark }, padding: '6px 8px' }}
marginLeft: '150px', onClick={() => setOpenInstallation(false)}
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>
@ -380,32 +291,15 @@ function UserAccess(props: UserAccessProps) {
</div> </div>
<Button <Button
sx={{ sx={{ marginTop: '20px', backgroundColor: theme.colors.primary.main, color: 'white', '&:hover': { backgroundColor: theme.colors.primary.dark }, padding: '6px 8px' }}
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={handleCancel} onClick={() => setOpenModal(false)}
sx={{ sx={{ marginTop: '20px', marginLeft: '10px', backgroundColor: theme.colors.primary.main, color: 'white', '&:hover': { backgroundColor: theme.colors.primary.dark }, padding: '6px 8px' }}
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>
@ -416,43 +310,63 @@ function UserAccess(props: UserAccessProps) {
<Button <Button
variant="contained" variant="contained"
onClick={handleGrantAccess} onClick={handleGrantAccess}
sx={{ sx={{ marginTop: '20px', marginBottom: '20px', backgroundColor: '#ffc04d', color: '#000000', '&:hover': { bgcolor: '#f7b34d' } }}
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>
<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 ( return (
<Fragment key={installation.name}> <Fragment key={folder.id}>
<ListItem <ListItem
sx={{ sx={{ mb: isLast ? 1 : 0 }}
mb: isLast ? 4 : 0 // Apply margin-bottom to the last item only
}}
secondaryAction={ secondaryAction={
currentUser.userType === UserType.admin && ( currentUser.userType === UserType.admin && (
<IconButton <IconButton onClick={() => handleRevokeFolder(folder.id, folder.name)} edge="end">
onClick={() => { <PersonRemoveIcon />
RevokeAccessFromResource( </IconButton>
'ToInstallation', )
props.current_user.id, }
'InstallationId', >
installation.id, <ListItemAvatar>
props.current_user.name <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); {/* Direct Installation Access Section */}
}} <Grid item xs={12} md={12}>
edge="end" <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 /> <PersonRemoveIcon />
</IconButton> </IconButton>
) )
@ -469,22 +383,9 @@ function UserAccess(props: UserAccessProps) {
</Fragment> </Fragment>
); );
})} })}
{directInstallations.length === 0 && (
{accessibleInstallationsForUser.length == 0 && ( <Alert severity="info" sx={{ mb: 4 }}>
<Alert <FormattedMessage id="noDirectInstallationAccess" defaultMessage="No direct installation access grants" />
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,16 +474,18 @@ function SodioHomeInstallation(props: singleInstallationProps) {
} }
/> />
<Route {currentUser.userType !== UserType.client && (
path={routes.log} <Route
element={ path={routes.log}
<Log element={
errorLoadingS3Data={errorLoadingS3Data} <Log
id={props.current_installation.id} errorLoadingS3Data={errorLoadingS3Data}
status={props.current_installation.status} id={props.current_installation.id}
></Log> status={props.current_installation.status}
} ></Log>
/> }
/>
)}
<Route <Route
path={routes.live} path={routes.live}
@ -497,18 +499,20 @@ function SodioHomeInstallation(props: singleInstallationProps) {
} }
/> />
<Route {currentUser.userType !== UserType.client && (
path={routes.batteryview + '/*'} <Route
element={ path={routes.batteryview + '/*'}
<BatteryViewSodioHome element={
values={values} <BatteryViewSodioHome
s3Credentials={s3Credentials} values={values}
installationId={props.current_installation.id} s3Credentials={s3Credentials}
installation={props.current_installation} installationId={props.current_installation.id}
connected={connected} installation={props.current_installation}
></BatteryViewSodioHome> connected={connected}
} ></BatteryViewSodioHome>
></Route> }
/>
)}
{currentUser.userType == UserType.admin && ( {currentUser.userType == UserType.admin && (
<Route <Route
@ -559,16 +563,14 @@ function SodioHomeInstallation(props: singleInstallationProps) {
} }
/> />
{currentUser.userType == UserType.admin && ( <Route
<Route path={routes.report}
path={routes.report} element={
element={ <WeeklyReport
<WeeklyReport installationId={props.current_installation.id}
installationId={props.current_installation.id} />
/> }
} />
/>
)}
<Route <Route
path={'*'} path={'*'}

View File

@ -118,7 +118,6 @@ 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: (
@ -128,14 +127,12 @@ 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: (
@ -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', value: 'live',
@ -178,14 +214,24 @@ 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 tabs = const inInstallationView =
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',
@ -216,7 +262,6 @@ 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: (
@ -226,7 +271,6 @@ function SodioHomeInstallationTabs(props: SodioHomeInstallationTabsProps) {
/> />
) )
}, },
{ {
value: 'information', value: 'information',
label: ( label: (
@ -261,10 +305,7 @@ function SodioHomeInstallationTabs(props: SodioHomeInstallationTabsProps) {
) )
} }
] ]
: currentTab != 'list' && : inInstallationView && currentUser.userType == UserType.partner
currentTab != 'tree' &&
!location.pathname.includes('folder') &&
currentUser.userType == UserType.client
? [ ? [
{ {
value: 'list', value: 'list',
@ -282,12 +323,67 @@ 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 => {