1
0
Fork 0
mirror of https://git.rwth-aachen.de/acs/public/villas/web/ synced 2025-03-09 00:00:01 +01:00

implemented users and mappings management for usergroups

Signed-off-by: Andrii Podriez <andrey5577990@gmail.com>
This commit is contained in:
Andrii Podriez 2024-09-26 02:33:04 +02:00 committed by Youssef Nakti
parent 54130a56de
commit 9ad6467c80
10 changed files with 453 additions and 45 deletions

View file

@ -27,6 +27,7 @@ import {Button} from "react-bootstrap";
import NotificationsFactory from "../../../common/data-managers/notifications-factory";
import notificationsDataManager from "../../../common/data-managers/notifications-data-manager";
import FileSaver from "file-saver";
import moment from "moment";
import {
useGetResultsQuery,
useAddResultMutation,
@ -142,6 +143,11 @@ const ResultsTable = (props) => {
setIsDeleteModalOpened(false);
setResultToDelete({});
}
const stateUpdateModifier = (dateString) => {
const date = moment(dateString);
return `${date.fromNow()}`;
};
return (
<div>
@ -175,11 +181,13 @@ const ResultsTable = (props) => {
<DataColumn
title='Created at'
dataKey='createdAt'
modifier={(createdAt) => stateUpdateModifier(createdAt)}
width={200}
/>
<DataColumn
title='Last update'
dataKey='updatedAt'
modifier={(updatedAt) => stateUpdateModifier(updatedAt)}
width={200}
/>
<LinkbuttonColumn

View file

@ -0,0 +1,97 @@
/**
* This file is part of VILLASweb.
*
* VILLASweb is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* VILLASweb is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with VILLASweb. If not, see <http://www.gnu.org/licenses/>.
******************************************************************************/
import React from 'react';
import { useState } from 'react';
import { Form, Col, Button} from 'react-bootstrap';
import Dialog from '../../../common/dialogs/dialog';
import { useGetScenariosQuery } from '../../../store/apiSlice';
const AddScenarioMappingDialog = ({isDialogOpened, onClose, mappings}) => {
const [name, setName] = useState('');
const [isValid, setIsValid] = useState(false);
const [selectedOption, setSelectedOption] = useState('addUsersToScenario');
const [selectedScenarioID, setSelectedScenarioID] = useState('');
const {data: {scenarios} = {}, isLoading: isLoadingScenarios} = useGetScenariosQuery();
const handleRadioChange = (e) => {
setSelectedOption(e.target.value);
}
const handleClose = (canceled) => {
if(canceled) {
onClose(null);
} else {
onClose({scenarioID: Number(selectedScenarioID), duplicate: selectedOption === 'duplicateScenarioForUsers'});
}
}
const handleSelectChange = (e) => {
setSelectedScenarioID(e.target.value);
setIsValid(e.target.value !== '');
};
return (<Dialog
show={isDialogOpened}
title="New User Group"
buttonTitle="Add"
onClose={handleClose}
onReset={() => {}}
valid={isValid}>
<Form>
<Form.Group as={Col} controlId="radioGroup" style={{ marginBottom: '15px' }}>
<div>
<Form.Check
type="radio"
id="addUsersToScenario"
name="options"
label="Add users to scenario"
value="addUsersToScenario"
checked={selectedOption === 'addUsersToScenario'}
onChange={handleRadioChange}
/>
<Form.Check
type="radio"
id="duplicateScenarioForUsers"
name="options"
label="Duplicate scenario for each user"
value="duplicateScenarioForUsers"
checked={selectedOption === 'duplicateScenarioForUsers'}
onChange={handleRadioChange}
/>
</div>
</Form.Group>
<Form.Group controlId="scenario">
<Form.Label>Select Option</Form.Label>
{isLoadingScenarios ? <div>Loading...</div> : (
<Form.Control as="select" value={selectedScenarioID} onChange={handleSelectChange}>
<option value="">-- Select scenario --</option>
{scenarios.map(scenario => {
//check if existing mappings are already added to the usergroup
if(!mappings.some(mapping => mapping.scenarioID === scenario.id)) return <option key={scenario.id} value={scenario.id}>{scenario.name}</option>;
})}
</Form.Control>
)}
</Form.Group>
</Form>
</Dialog>);
}
export default AddScenarioMappingDialog;

View file

@ -0,0 +1,81 @@
/**
* This file is part of VILLASweb.
*
* VILLASweb is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* VILLASweb is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with VILLASweb. If not, see <http://www.gnu.org/licenses/>.
******************************************************************************/
import React from 'react';
import { useState, useEffect } from 'react';
import { Form, Col, Dropdown, Badge } from 'react-bootstrap';
import Dialog from '../../../common/dialogs/dialog';
import { useGetUsersQuery } from '../../../store/apiSlice';
const AddUserToUsergroupDialog = ({isModalOpened, onClose, currentUsers}) => {
const [selectedUsers, setSelectedusers] = useState([]);
const [isValid, setIsValid] = useState(false);
const {data: {users} = {}, isLoading: isLoadingUsers} = useGetUsersQuery();
const toggleUser = (event, option) => {
event.preventDefault();
if (selectedUsers.includes(option)) {
setSelectedusers(prevState => ([...prevState.filter((item) => item !== option)]));
} else {
setSelectedusers(prevState => ([...prevState, option]));
}
}
useEffect(() => {
setIsValid(selectedUsers.length > 0);
}, [selectedUsers]);
const handleClose = (canceled) => {
if(!canceled){
onClose(selectedUsers);
} else {
onClose([]);
}
}
const handleReset = () => {
setSelectedusers([]);
}
return (
<Dialog show={isModalOpened} title="Add Users" buttonTitle="Add" onClose={handleClose} onReset={handleReset} valid={isValid}>
<Dropdown autoClose="outside">
<Dropdown.Toggle variant="success" id="dropdown-basic">Select Options</Dropdown.Toggle>
<Dropdown.Menu>
{isLoadingUsers ? <div>Loading...</div> : [...users].filter(user => !currentUsers.some(currentUser => currentUser.id == user.id)).map(user =>
<Dropdown.Item
key={user.id}
onClick={(event) => toggleUser(event, user)}
style={{
backgroundColor: selectedUsers.includes(user) ? '#d3d3d3' : ''
}}
>
{user.username}
</Dropdown.Item>)}
</Dropdown.Menu>
</Dropdown>
<div className='mt-4'>
{selectedUsers.length > 0 ? selectedUsers.map(user =>
<Badge className="fs-6 me-2 mb-2" key={user.id} bg="primary">{user.username}</Badge>) : <div className="fst-italic">No users selected</div>}
</div>
</Dialog>
);
}
export default AddUserToUsergroupDialog;

View file

@ -0,0 +1,69 @@
/**
* This file is part of VILLASweb.
*
* VILLASweb is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* VILLASweb is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with VILLASweb. If not, see <http://www.gnu.org/licenses/>.
******************************************************************************/
import React from 'react';
import { useState, useEffect } from 'react';
import { Form, Col, Button} from 'react-bootstrap';
import Dialog from '../../../common/dialogs/dialog';
const RenameUsergroupDialog = ({isModalOpened, onClose, oldName}) => {
const [name, setName] = useState(oldName);
const [isValid, setIsValid] = useState(false);
useEffect(() => {
if (isModalOpened && oldName) {
setName(oldName);
}
}, [isModalOpened, oldName]);
const handleNameChange = (e) => {
const newName = e.target.value;
setName(newName);
setIsValid(newName.length >= 3 && !(/^\s/.test(newName)));
}
const handleClose = (canceled) => {
if(canceled) {
onClose(null);
} else {
onClose(name);
}
}
const handleReset = () => {
setName('');
}
return (<Dialog
show={isModalOpened}
title="Rename User Group"
buttonTitle="Update"
onClose={handleClose}
onReset={handleReset}
valid={isValid}>
<Form>
<Form.Group as={Col} controlId="name" style={{marginBottom: '15px'}}>
<Form.Label>Name</Form.Label>
<Form.Control type="text" placeholder="Enter new name" value={name} onChange={handleNameChange} />
<Form.Control.Feedback />
</Form.Group>
</Form>
</Dialog>);
}
export default RenameUsergroupDialog;

View file

@ -15,16 +15,60 @@
* along with VILLASweb. If not, see <http://www.gnu.org/licenses/>.
******************************************************************************/
import { useGetUsergroupByIdQuery } from "../../../store/apiSlice";
import { useState } from "react";
import { useGetScenariosQuery, useGetUsergroupByIdQuery } from "../../../store/apiSlice";
import { Table, DataColumn, LinkColumn, ButtonColumn } from "../../../common/table";
import { iconStyle, buttonStyle } from "../styles";
import IconButton from "../../../common/buttons/icon-button";
import AddScenarioMappingDialog from "../dialogs/addScenarioMappingDialog";
import { useUpdateUsergroupMutation } from "../../../store/apiSlice";
import DeleteDialog from "../../../common/dialogs/delete-dialog";
const UsergroupScenariosTable = ({usergroupID}) => {
const {data: {usergroup} = {}, isLoading} = useGetUsergroupByIdQuery(usergroupID);
const {data: {usergroup} = {}, isLoading, refetch} = useGetUsergroupByIdQuery(usergroupID);
const {data: {scenarios} = {}, isLoading: isScenariosLoading } = useGetScenariosQuery();
const [isAddScenarioDialogOpen, setIsAddScenarioDialogOpen] = useState(false);
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
const [mappingToDelete, setMappingToDelete] = useState({scenarioID: null});
const [updateUsergroup] = useUpdateUsergroupMutation();
const handleAddScenarioMapping = () => {
const handleAddScenarioMapping = async (newMapping) => {
if(newMapping){
try{
//add new mapping while saving name and existing mappings if there are any
const oldMappings = usergroup.scenarioMappings.length > 0 ? [...usergroup.scenarioMappings] : [];
await updateUsergroup({usergroupID: usergroupID, usergroup: {name: usergroup.name, scenarioMappings: [...oldMappings, newMapping]}}).unwrap();
refetch();
} catch(error) {
console.log("Error updating mappings", error);
}
}
setIsAddScenarioDialogOpen(false);
}
const handleRemoveScenarioMapping = async (isConfirmed) => {
if(isConfirmed){
try {
//update usergroup with new mappings without the target
const newMappings = [...usergroup.scenarioMappings].filter(mapping => mapping.id !== mappingToDelete.id);
await updateUsergroup({usergroupID: usergroupID, usergroup: {name: usergroup.name, scenarioMappings: newMappings}}).unwrap();
refetch();
} catch (error) {
console.log("Error removing mapping", error);
}
}
setIsDeleteDialogOpen(false);
setMappingToDelete({scenarioID: null});
}
const getScenarioName = (scenarioID) => {
if(isScenariosLoading){
return <div>Loading...</div>;
}
const scenario = scenarios.find((scenario) => scenario.id === scenarioID);
return scenario ? <div>{scenario.name}</div> : <div>unknown</div>;
}
const getDuplicateLabel = (duplicate) => {
@ -40,7 +84,7 @@ const UsergroupScenariosTable = ({usergroupID}) => {
<IconButton
childKey={0}
tooltip="Add Scenario Mapping"
onClick={() => handleAddScenarioMapping()}
onClick={() => setIsAddScenarioDialogOpen(true)}
icon="plus"
buttonStyle={buttonStyle}
iconStyle={iconStyle}
@ -48,14 +92,27 @@ const UsergroupScenariosTable = ({usergroupID}) => {
</span>
</h2>
<Table data={usergroup.scenarioMappings}>
<DataColumn title='ID' dataKey='id' width={70}/>
<LinkColumn title="Scenario ID" dataKey="scenarioID" link="/scenarios/" linkKey="id" />
<DataColumn title='Duplicate' dataKey='duplicate' modifier={(duplicate) => getDuplicateLabel(duplicate)}/>
{/* <ButtonColumn
width="200"
align="right"
/> */}
<LinkColumn title="Scenario ID" dataKey="scenarioID" link="/scenarios/" linkKey="scenarioID" width={120} />
<DataColumn title='Name' dataKey='scenarioID' modifier={(scenarioID) => getScenarioName(scenarioID)}/>
<DataColumn title='Duplicated' dataKey='duplicate' modifier={(duplicate) => getDuplicateLabel(duplicate)}/>
<ButtonColumn
width="200"
align="right"
deleteButton
onDelete={(index) => {
setMappingToDelete(usergroup.scenarioMappings[index]);
setIsDeleteDialogOpen(true);
}}
/>
</Table>
<AddScenarioMappingDialog isDialogOpened={isAddScenarioDialogOpen} mappings={usergroup.scenarioMappings} onClose={(newMapping) => handleAddScenarioMapping(newMapping)} />
<DeleteDialog
title="scenario mapping for scenario"
name={mappingToDelete.scenarioID}
show={isDeleteDialogOpen}
onClose={(isConfirmed) => handleRemoveScenarioMapping(isConfirmed)}
/>
</div>);
}

View file

@ -15,16 +15,40 @@
* along with VILLASweb. If not, see <http://www.gnu.org/licenses/>.
******************************************************************************/
import { useGetUsersByUsergroupIdQuery } from "../../../store/apiSlice";
import { useState } from "react";
import { useGetUsersByUsergroupIdQuery, useAddUserToUsergroupMutation, useDeleteUserFromUsergroupMutation } from "../../../store/apiSlice";
import { Table, DataColumn, LinkColumn, ButtonColumn } from "../../../common/table";
import { iconStyle, buttonStyle } from "../styles";
import IconButton from "../../../common/buttons/icon-button";
import AddUserToUsergroupDialog from "../dialogs/addUserToUsergroupDialog";
import NotificationsFactory from "../../../common/data-managers/notifications-factory";
import notificationsDataManager from "../../../common/data-managers/notifications-data-manager";
const UsergroupUsersTable = ({usergroupID}) => {
const {data: {users}=[], isLoading} = useGetUsersByUsergroupIdQuery(usergroupID);
const {data: {users}=[], isLoading, refetch} = useGetUsersByUsergroupIdQuery(usergroupID);
const [isAddUserDialogOpen, setIsAddUserDialogOpen] = useState(false);
const [addUserToUsergroup] = useAddUserToUsergroupMutation();
const [removeUserFromGroup] = useDeleteUserFromUsergroupMutation();
const handleAddUser = () => {
const handleAddUser = async (selectedUsers) => {
if(selectedUsers.length > 0){
try {
await Promise.all(selectedUsers.map(user => addUserToUsergroup({usergroupID: usergroupID, username: user.username}).unwrap()));
refetch();
} catch (error) {
console.log('Error adding users', error);
}
}
setIsAddUserDialogOpen(false);
}
const handleRemoveUser = async (user) => {
try{
await removeUserFromGroup({usergroupID: usergroupID, username: user.username}).unwrap();
refetch();
} catch(error) {
console.log('Error removing users', error);
}
}
if(isLoading) return <div>Loading...</div>;
@ -36,7 +60,7 @@ const UsergroupUsersTable = ({usergroupID}) => {
<IconButton
childKey={0}
tooltip="Add Users"
onClick={() => handleAddUser()}
onClick={() => setIsAddUserDialogOpen(true)}
icon="plus"
buttonStyle={buttonStyle}
iconStyle={iconStyle}
@ -44,21 +68,20 @@ const UsergroupUsersTable = ({usergroupID}) => {
</span>
</h2>
<Table data={users}>
<DataColumn
title='ID'
dataKey='id'
width={70}
<DataColumn title='ID' dataKey='id' width={70} />
<DataColumn title="Username" dataKey="username" width={150} />
<ButtonColumn
width="200"
align="right"
deleteButton
onDelete={(index) => handleRemoveUser(users[index])}
/>
<DataColumn
title="Name"
dataKey="name"
width={70}
/>
{/* <ButtonColumn
width="200"
align="right"
/> */}
</Table>
<AddUserToUsergroupDialog
isModalOpened={isAddUserDialogOpen}
onClose={(selectedUsers) => handleAddUser(selectedUsers)}
currentUsers={users}
/>
</div>);
}

View file

@ -15,23 +15,55 @@
* along with VILLASweb. If not, see <http://www.gnu.org/licenses/>.
******************************************************************************/
import { useState } from "react";
import { useParams } from "react-router-dom/cjs/react-router-dom.min";
import { Table, DataColumn, LinkColumn } from "../../common/table";
import { Row, Col } from "react-bootstrap";
import UsergroupScenariosTable from "./tables/usergroup-scenarios-table";
import UsergroupUsersTable from "./tables/usergroup-users-table";
import { useGetUsergroupByIdQuery } from "../../store/apiSlice";
import IconButton from "../../common/buttons/icon-button";
import { buttonStyle, iconStyle } from "./styles";
import { useGetUsergroupByIdQuery, useUpdateUsergroupMutation } from "../../store/apiSlice";
import RenameUsergroupDialog from "./dialogs/renameGroupDialog";
const Usergroup = (props) => {
const Usergroup = () => {
const params = useParams();
const usergroupID = params.usergroup;
const {data: {usergroup} = {}, isLoading} = useGetUsergroupByIdQuery(usergroupID);
const {data: {usergroup} = {}, isLoading, refetch} = useGetUsergroupByIdQuery(usergroupID);
const [isRenameModalOpened, setIsRenameModalOpened] = useState(false);
const [updateUsergroup] = useUpdateUsergroupMutation();
const handleRename = async (newName) => {
if(newName){
try {
//update only the name
await updateUsergroup({usergroupID: usergroup.id, usergroup: {name: newName, ScenarioMappings: usergroup.ScenarioMappings}}).unwrap();
refetch();
} catch (error) {
console.log('Error updating group', error);
}
}
setIsRenameModalOpened(false);
}
if(isLoading) return <div className='loading'>Loading...</div>;
return (
<div className='section'>
<h1>{usergroup.name}</h1>
<h1>
{usergroup.name}
<span className="icon-button">
<IconButton
childKey={1}
tooltip='Change name'
onClick={() => setIsRenameModalOpened(true)}
icon='edit'
buttonStyle={buttonStyle}
iconStyle={iconStyle}
/>
</span>
</h1>
<Row className="mt-4">
<Col>
<UsergroupUsersTable usergroupID={usergroupID} />
@ -40,6 +72,12 @@ const Usergroup = (props) => {
<UsergroupScenariosTable usergroupID={usergroupID} />
</Col>
</Row>
<RenameUsergroupDialog
isModalOpened={isRenameModalOpened}
onClose={handleRename}
oldName={usergroup.name}
/>
</div>
);
}

View file

@ -22,6 +22,7 @@ import IconButton from "../../common/buttons/icon-button";
import { buttonStyle, iconStyle } from "./styles";
import AddUsergroupDialog from "./dialogs/addUsergroupDialog";
import DeleteDialog from "../../common/dialogs/delete-dialog";
import moment from "moment";
const Usergroups = (props) => {
const {data: {usergroups} = {}, refetch: refetchUsergroups, isLoading} = useGetUsergroupsQuery();
@ -60,6 +61,11 @@ const Usergroups = (props) => {
setDialogUsergroup({});
setIsDeleteDialogOpen(false);
}
const stateUpdateModifier = (dateString) => {
const date = moment(dateString);
return `${date.fromNow()}`;
};
if(isLoading) return <div>Loading</div>;
@ -69,12 +75,12 @@ const Usergroups = (props) => {
User Groups
<span className="icon-button">
<IconButton
childKey={0}
tooltip="Add Usergroup"
onClick={() => setIsAddDialogOpen(true)}
icon="plus"
buttonStyle={buttonStyle}
iconStyle={iconStyle}
childKey={0}
tooltip="Add Usergroup"
onClick={() => setIsAddDialogOpen(true)}
icon="plus"
buttonStyle={buttonStyle}
iconStyle={iconStyle}
/>
</span>
</h1>
@ -90,6 +96,11 @@ const Usergroups = (props) => {
link="/usergroup/"
linkKey="id"
/>
<DataColumn
title='Last Update'
dataKey='updatedAt'
modifier={(updatedAt) => stateUpdateModifier(updatedAt)}
/>
<ButtonColumn
width="200"
align="right"
@ -103,11 +114,11 @@ const Usergroups = (props) => {
<AddUsergroupDialog isModalOpened={isAddDialogOpen} onClose={handleAddNewGroup} />
<DeleteDialog
title="scenario"
name={dialogUsegroup.name}
show={isDeleteDialogOpen}
onClose={(isConfirmed) => handleDeleteUsergroup(isConfirmed)}
/>
title="scenario"
name={dialogUsegroup.name}
show={isDeleteDialogOpen}
onClose={(isConfirmed) => handleDeleteUsergroup(isConfirmed)}
/>
</div>);
}
}

View file

@ -110,5 +110,8 @@ export const {
useAddUsergroupMutation,
useDeleteUsergroupMutation,
useGetUsergroupByIdQuery,
useGetUsersByUsergroupIdQuery
useGetUsersByUsergroupIdQuery,
useAddUserToUsergroupMutation,
useDeleteUserFromUsergroupMutation,
useUpdateUsergroupMutation
} = apiSlice;

View file

@ -31,6 +31,27 @@ export const usergroupEndpoints = (builder) => ({
},
}),
}),
updateUsergroup: builder.mutation({
query: ({ usergroupID, usergroup }) => ({
url: `/usergroups/${usergroupID}`,
method: 'PUT',
body: { usergroup },
}),
}),
addUserToUsergroup: builder.mutation({
query: ({ usergroupID, username }) => ({
url: `/usergroups/${usergroupID}/user`,
method: 'PUT',
params: { username },
}),
}),
deleteUserFromUsergroup: builder.mutation({
query: ({ usergroupID, username }) => ({
url: `/usergroups/${usergroupID}/user`,
method: 'DELETE',
params: { username },
}),
}),
getUsersByUsergroupId: builder.query({
query: (usergroupID) => `/usergroups/${usergroupID}/users`,
}),