diff --git a/.gitignore b/.gitignore index 2bc73f5049968a4c2fb67f069c08588d0a41ba2b..2060fbffb24f9ea81a9e736f30d04becde30e3bb 100644 --- a/.gitignore +++ b/.gitignore @@ -99,3 +99,7 @@ jams-react-client/node_modules/ jams-react-client/build/ jams-react-client/package-lock.json jams-server/src/main/resources/webapp/ + +# VScode +.vscode/ +*.factorypath diff --git a/jams-react-client/package.json b/jams-react-client/package.json index c8f13356ea0c6d844742353792a895c93f02dd2f..753c7cbfb72e1401875bd47ad75b4fa65e96d1fa 100644 --- a/jams-react-client/package.json +++ b/jams-react-client/package.json @@ -14,6 +14,7 @@ "fuctbase64": "^1.4.0", "history": "4.10.1", "image-to-base64": "^2.1.1", + "lodash": "^4.17.19", "material-ui-popup-state": "^1.6.1", "package.json": "^2.0.1", "perfect-scrollbar": "1.5.0", diff --git a/jams-react-client/src/components/Drawer/Drawer.js b/jams-react-client/src/components/Drawer/Drawer.js new file mode 100644 index 0000000000000000000000000000000000000000..c9cef1cc481ac10588a5697d9c7be33a116e2d8a --- /dev/null +++ b/jams-react-client/src/components/Drawer/Drawer.js @@ -0,0 +1,140 @@ +import React, { useEffect, useCallback } from 'react'; +import clsx from 'clsx'; +import { makeStyles } from '@material-ui/core/styles'; +import CustomInput from "components/CustomInput/CustomInput.js"; +import Drawer from '@material-ui/core/Drawer'; +import Search from "@material-ui/icons/Search"; +import Button from '@material-ui/core/Button'; +import List from '@material-ui/core/List'; +import Divider from '@material-ui/core/Divider'; +import ListItem from '@material-ui/core/ListItem'; +import ListItemIcon from '@material-ui/core/ListItemIcon'; +import ListItemText from '@material-ui/core/ListItemText'; +import InboxIcon from '@material-ui/icons/MoveToInbox'; +import MailIcon from '@material-ui/icons/Mail'; +import AddCircleOutlineIcon from '@material-ui/icons/AddCircleOutline'; +import LinearProgress from '@material-ui/core/LinearProgress'; + +import {api_path_get_user_directory_search, api_path_get_auth_contacts} from 'globalUrls' +import { useHistory } from 'react-router-dom'; +import axios from "axios"; +import configApiCall from "api.js"; +import auth from 'auth.js' + +import { debounce } from "lodash"; + +const useStyles = makeStyles({ + list: { + width: 500, + }, + fullList: { + width: 'auto', + }, + search : { + width: "100%" + }, + margin: { + marginLeft: "5px", + marginRight: "5px" + } +}); + +export default function TemporaryDrawer(props) { + const classes = useStyles(); + const history = useHistory(); + const [direction, setDirection] = React.useState(props.direction); + const [contacts, setContacts] = React.useState([]); + + useEffect(() => { + /** + * Get contact list to pass to the drawer to add contacts to user + */ + axios(configApiCall(api_path_get_user_directory_search, 'GET', {"queryString":"*"}, null)).then((response)=>{ + setContacts(response.data) + }).catch((error) =>{ + console.log(error); + if(error.response.status == 401){ + auth.authenticated = false + history.push('/') + } + }); + }, []) + + const addContact = (jamiId) => { + axios(configApiCall(api_path_get_auth_contacts, 'PUT', {"uri": "jami://" + jamiId}, null)).then((response)=>{ + props.setOpenDrawer(false) + props.getAllContacts() + }).catch((error) =>{ + console.log("Error adding user: " + error) + props.setOpenDrawer(false) + }); + } + + const searchContacts = (value) => { + axios(configApiCall(api_path_get_user_directory_search, 'GET', {"queryString": value ? value : "*"}, null)).then((response)=>{ + setContacts(response.data) + }).catch((error) =>{ + console.log(error); + setContacts([]) + if(error.response.status == 401){ + auth.authenticated = false + history.push('/') + } + }); + + } + + const listContacts = () => ( + + <List> + {contacts.map((contact) => ( + <ListItem button key={contact.username} onClick={() => {addContact(contact.jamiId)}} > + <AddCircleOutlineIcon style={{ marginRight: "10px"}} /><ListItemText primary={contact.username} /> + </ListItem> + ))} + </List> + ); + + const initSearchContacts = useCallback(debounce((searchValue) => searchContacts(searchValue), 500), []) + + const handleSearchContacts = (e) => { + const searchValue = e.target.value; + initSearchContacts(searchValue) + } + + + return ( + <div> + <React.Fragment key={props.direction}> + <Drawer + anchor="right" + open={props.openDrawer} + onClose={() => {props.setOpenDrawer(false)}}> + <div + className={clsx(classes.list, { + [classes.fullList]: direction === 'top' || direction === 'bottom', + })} + role="presentation" + > + <div className={classes.searchWrapper}> + <CustomInput + formControlProps={{ + className: classes.margin + " " + classes.search + }} + inputProps={{ + placeholder: "Add a contact ...", + inputProps: { + "aria-label": "Add a contact" + }, + onKeyUp: handleSearchContacts, + }} + /> + </div> + <Divider /> + {listContacts()} + </div> + </Drawer> + </React.Fragment> + </div> + ); +} diff --git a/jams-react-client/src/globalUrls.js b/jams-react-client/src/globalUrls.js index 918bb413be9ce833cb1b0850524725c5b13aec6d..0e712677a16d4955f716e7e6cdfe7cb010a5cefe 100644 --- a/jams-react-client/src/globalUrls.js +++ b/jams-react-client/src/globalUrls.js @@ -35,6 +35,8 @@ const api_path_get_user_directory_search ='/api/auth/directory/search'; const api_path_post_create_user_profile = '/api/admin/directory/entry'; const api_path_put_update_user_profile = '/api/admin/directory/entry'; const api_path_get_user_search = '/api/admin/users'; +const api_path_get_auth_contacts = '/api/auth/contacts'; +const api_path_delete_auth_contacts = '/api/auth/contacts'; module.exports = { uri, @@ -74,4 +76,6 @@ module.exports = { api_path_post_create_user_profile, api_path_put_update_user_profile, api_path_get_user_search, + api_path_get_auth_contacts, + api_path_delete_auth_contacts } \ No newline at end of file diff --git a/jams-react-client/src/views/Contacts/Contacts.js b/jams-react-client/src/views/Contacts/Contacts.js new file mode 100644 index 0000000000000000000000000000000000000000..a77c82db84829eef24fb70c10b73787b6f23f1e7 --- /dev/null +++ b/jams-react-client/src/views/Contacts/Contacts.js @@ -0,0 +1,230 @@ +import React, { useState, useEffect } from "react"; +import { useHistory } from 'react-router-dom'; +import { Switch, Route, Redirect } from "react-router-dom"; +// @material-ui/core components +import { makeStyles } from "@material-ui/core/styles"; +import InputLabel from "@material-ui/core/InputLabel"; +// core components +import GridItem from "components/Grid/GridItem.js"; +import GridContainer from "components/Grid/GridContainer.js"; +import CustomInput from "components/CustomInput/CustomInput.js"; +import Button from "components/CustomButtons/Button.js"; +import Card from "components/Card/Card.js"; +import CardHeader from "components/Card/CardHeader.js"; +import CardAvatar from "components/Card/CardAvatar.js"; +import CardBody from "components/Card/CardBody.js"; +import CardFooter from "components/Card/CardFooter.js"; +import UserProfile from "views/UserProfile/UserProfile.js" +import Divider from '@material-ui/core/Divider'; + +import PersonIcon from '@material-ui/icons/Person'; +import PermIdentityIcon from '@material-ui/icons/PermIdentity'; +import PhoneOutlinedIcon from '@material-ui/icons/PhoneOutlined'; +import BusinessOutlinedIcon from '@material-ui/icons/BusinessOutlined'; +import Search from "@material-ui/icons/Search"; +import IconButton from '@material-ui/core/IconButton'; + +import MailOutlineIcon from '@material-ui/icons/MailOutline'; +import axios from "axios"; +import configApiCall from "api.js"; +import auth from 'auth.js' +import { api_path_get_auth_contacts, api_path_delete_auth_contacts } from "globalUrls"; + +import AddCircleOutlineIcon from '@material-ui/icons/AddCircleOutline'; +import KeyboardReturnIcon from '@material-ui/icons/KeyboardReturn'; +import DeleteOutlineIcon from '@material-ui/icons/DeleteOutline'; +import jami from "assets/img/faces/jami.png"; +import noProfilePicture from "assets/img/faces/no-profile-picture.png"; +import EditCreateUserProfile from "views/UserProfile/EditCreateUserProfile"; + +import LinearProgress from '@material-ui/core/LinearProgress'; + +import headerLinksStyle from "assets/jss/material-dashboard-react/components/headerLinksStyle.js"; + +import TemporaryDrawer from "components/Drawer/Drawer" + +import Dialog from '@material-ui/core/Dialog'; +import DialogActions from '@material-ui/core/DialogActions'; +import DialogContent from '@material-ui/core/DialogContent'; +import DialogContentText from '@material-ui/core/DialogContentText'; +import DialogTitle from '@material-ui/core/DialogTitle'; + +const styles = { + ...headerLinksStyle, + cardCategoryWhite: { + color: "rgba(255,255,255,.62)", + margin: "0", + fontSize: "14px", + marginTop: "0", + marginBottom: "0" + }, + cardTitleWhite: { + color: "#FFFFFF", + marginTop: "0px", + minHeight: "auto", + fontWeight: "300", + fontFamily: "'Roboto', 'Helvetica', 'Arial', sans-serif", + marginBottom: "3px", + textDecoration: "none" + }, + deleteIcon : { + float: "right" + }, + search : { + width: "90%" + }, + loading: { + width: '100%', + } +}; + +const useStyles = makeStyles(styles); + +export default function Users(props) { + + const classes = useStyles(); + const history = useHistory(); + const [contacts, setContacts] = React.useState([]) + const [searchValue, setSearchValue] = React.useState(null) + const [loading, setLoading] = React.useState(false) + const [progress, setProgress] = React.useState(0); + const [openDrawer, setOpenDrawer] = React.useState(false); + const [removedContact, setRemovedContact] = React.useState(); + const [open, setOpen] = React.useState(false) + + const getAllContacts = () => { + axios(configApiCall(api_path_get_auth_contacts, 'GET', null, null)).then((response)=>{ + /* + TODO: Include the username of the user of witch we want to display contacts + at the moment the admin sees his contacts in each user profile he visits + */ + let orginalUsers = response.data + orginalUsers.map((user) => { + user.display = "" + }) + setContacts(orginalUsers) + setLoading(false) + }).catch((error) =>{ + console.log(error); + if(error.response.status == 401){ + auth.authenticated = false + history.push('/') + } + }); + } + + useEffect(() => { + + setLoading(true) + const timer = setInterval(() => { + setProgress((oldProgress) => { + if (oldProgress === 100) { + return 0; + } + const diff = Math.random() * 10; + return Math.min(oldProgress + diff, 100); + }); + }, 500); + getAllContacts() + return () => { + clearInterval(timer); + }; + }, []); + + const removeContact = () => { + axios(configApiCall(api_path_delete_auth_contacts, 'DELETE', {"uri": removedContact.replace("jami://", "")}, null)).then((response)=>{ + getAllContacts() + }).catch((error) =>{ + alert("Uri: " + removedContact + " was not removed " + error) + }); + } + + const handleRemoveContact = (uri) => { + setRemovedContact(uri) + setOpen(true) + } + + return( + <div> + <TemporaryDrawer openDrawer={openDrawer} setOpenDrawer={setOpenDrawer} getAllContacts={getAllContacts} direction="right"/> + <Dialog + open={open} + onClose={() => setOpen(false)} + aria-labelledby="alert-dialog-title" + aria-describedby="alert-dialog-description" + > + <DialogTitle id="alert-dialog-title">{"Remove contact"}</DialogTitle> + <DialogContent> + <DialogContentText id="alert-dialog-description"> + Are you sure you want to delete <strong>{removedContact}</strong> ? + </DialogContentText> + </DialogContent> + <DialogActions> + <Button onClick={() => setOpen(false)} color="primary"> + Cancel + </Button> + <Button onClick={removeContact} color="danger" autoFocus> + Remove + </Button> + </DialogActions> + </Dialog> + <GridContainer> + <GridItem xs={12} sm={12} md={12}> + <Button variant="contained" color="primary" href="#contained-buttons" onClick={() => {setOpenDrawer(true)}}> + <AddCircleOutlineIcon /> Add a contact + </Button> + <div className={classes.searchWrapper}> + <CustomInput + formControlProps={{ + className: classes.margin + " " + classes.search + }} + inputProps={{ + placeholder: "Search contacts using (uri, firstname, lastname)", + inputProps: { + "aria-label": "Search contacts" + }, + onKeyUp: (e) => setSearchValue(e.target.value), + }} + /> + <Search /> + <div className={classes.loading}> + {loading && <LinearProgress variant="determinate" value={progress} />} + </div> + </div> + </GridItem> + { + contacts.filter((data)=>{ + if(searchValue == null) + return data + else if((data.uri != null ) && data.uri.toLowerCase().includes(searchValue.toLowerCase())){ + return data + } + }).map(user => + <GridItem xs={12} sm={12} md={2} key={user.username} style={{display: user.display}}> + <Card profile> + <CardBody profile> + <CardAvatar profile> + <img src={user.profilePicture ? ('data:image/png;base64, ' + user.profilePicture) : noProfilePicture} alt="..." /> + </CardAvatar> + + <h4 className={classes.cardTitle}>{user.firstName} {user.lastName}</h4> + <ul> + <li><img src={jami} width="20" alt="Jami" style={{marginRight: "10px"}} /> {user.username ? user.username : 'No username'}</li> + <li><BusinessOutlinedIcon fontSize='small' style={{marginRight: "10px"}} /> {user.organization ? user.organization : 'No organization'}</li> + {/* <li><img src={jami} width="20" alt="Jami" style={{marginRight: "10px"}} /> {user.uri}</li> */} + </ul> + </CardBody> + <CardFooter> + <IconButton color="secondary" onClick={ () => {handleRemoveContact(user.uri)}}><DeleteOutlineIcon /></IconButton> + </CardFooter> + </Card> + </GridItem> + ) + } + { + contacts == [] && props.username + " has no contacts" + } + </GridContainer> + </div> + ) +} \ No newline at end of file diff --git a/jams-react-client/src/views/UserProfile/UserProfile.js b/jams-react-client/src/views/UserProfile/UserProfile.js index a4d24300cb593c4e0204413b6a8460e02393e544..fd3debd9ec0e7411ef778cede7f72af4f3b2d323 100755 --- a/jams-react-client/src/views/UserProfile/UserProfile.js +++ b/jams-react-client/src/views/UserProfile/UserProfile.js @@ -26,6 +26,8 @@ import noProfilePicture from "assets/img/faces/no-profile-picture.png"; import EditCreateUserProfile from "./EditCreateUserProfile" +import Contacts from "views/Contacts/Contacts" + import { infoColor } from "assets/jss/material-dashboard-react.js"; @@ -103,82 +105,18 @@ export default function UserProfile(props) { <Tab label="Profile" {...a11yProps(0)} /> <Tab label="Devices" {...a11yProps(1)} /> <Tab label="Contacts" {...a11yProps(2)} /> - <Tab label="Permissions" {...a11yProps(3)} /> - <Tab label="Configuration" {...a11yProps(4)} /> </Tabs> </AppBar> <TabPanel value={value} index={0}> - {displayUser? <DisplayUserProfile username={username} setDisplayUser={setDisplayUser}/>: <EditCreateUserProfile username={username} createUser={false} setDisplayUser={setDisplayUser}/> + { + displayUser ? <DisplayUserProfile username={username} setDisplayUser={setDisplayUser}/>: <EditCreateUserProfile username={username} createUser={false} setDisplayUser={setDisplayUser}/> } - </TabPanel> <TabPanel value={value} index={1}> <Devices username={props.username}/> </TabPanel> <TabPanel value={value} index={2}> - <div className={classes.searchWrapper}> - <CustomInput - formControlProps={{ - className: classes.margin + " " + classes.search - }} - inputProps={{ - placeholder: "Search", - inputProps: { - "aria-label": "Search" - } - }} - /> - <Button color="white" aria-label="edit" justIcon round> - <Search /> - </Button> - </div> - <br></br> - <GridContainer> - <GridItem xs={12} sm={12} md={4}> - <Card profile> - <CardAvatar profile> - <a href="#pablo" onClick={e => e.preventDefault()}> - <img src={noProfilePicture} alt="..." /> - </a> - </CardAvatar> - <CardBody profile> - <h6 className={classes.cardCategory}>CEO</h6> - <h4 className={classes.cardTitle}>Larbi Gharib</h4> - <p className={classes.description}> - 13 devices - </p> - <Button color="success" round> - Add - </Button> - </CardBody> - </Card> - </GridItem> - <GridItem xs={12} sm={12} md={4}> - <Card profile> - <CardAvatar profile> - <a href="#pablo" onClick={e => e.preventDefault()}> - <img src={noProfilePicture} alt="..." /> - </a> - </CardAvatar> - <CardBody profile> - <h6 className={classes.cardCategory}>CEO</h6> - <h4 className={classes.cardTitle}>Christophe Villemer</h4> - <p className={classes.description}> - 10 devices - </p> - <Button color="danger" round> - Remove - </Button> - </CardBody> - </Card> - </GridItem> - </GridContainer> - </TabPanel> - <TabPanel value={value} index={3}> - Item Two - </TabPanel> - <TabPanel value={value} index={4}> - Item Three + <Contacts username={props.username} /> </TabPanel> </div> ); diff --git a/jams-react-client/src/views/Users/Users.js b/jams-react-client/src/views/Users/Users.js index d5d3453835ddc2d4bfc51cc40bfc4e1f5bdba2cf..9ee6b08353d6bb6e51ad5ecd8b0428fcde793f91 100644 --- a/jams-react-client/src/views/Users/Users.js +++ b/jams-react-client/src/views/Users/Users.js @@ -1,4 +1,4 @@ -import React, { useState, useEffect } from "react"; +import React, { useState, useEffect, useCallback } from "react"; import { useHistory } from 'react-router-dom'; import { Switch, Route, Redirect } from "react-router-dom"; // @material-ui/core components @@ -40,6 +40,8 @@ import LinearProgress from '@material-ui/core/LinearProgress'; import headerLinksStyle from "assets/jss/material-dashboard-react/components/headerLinksStyle.js"; +import { debounce } from "lodash"; + const styles = { ...headerLinksStyle, cardCategoryWhite: { @@ -77,7 +79,6 @@ export default function Users() { const [users, setUsers] = React.useState([]) const [selectedUsername, setSelectedUsername] = React.useState("") const [createUser, setCreateUser] = React.useState(false) - const [searchValue, setSearchValue] = React.useState(null) const [loading, setLoading] = React.useState(false) const [progress, setProgress] = React.useState(0); @@ -93,11 +94,7 @@ export default function Users() { }); }, 500); axios(configApiCall(api_path_get_user_directory_search, 'GET', {"queryString":"*"}, null)).then((response)=>{ - let orginalUsers = response.data - orginalUsers.map((user) => { - user.display = "" - }) - setUsers(orginalUsers) + setUsers(response.data) setLoading(false) }).catch((error) =>{ console.log(error); @@ -113,17 +110,38 @@ export default function Users() { const [selectedProfile, setSelectedProfile] = useState(false); - function redirectToUserProfile(e, username) { + const redirectToUserProfile = (e, username) => { e.preventDefault() setSelectedProfile(true); setSelectedUsername(username) } - function redirectToUsers(e) { + const redirectToUsers = (e) => { e.preventDefault() setSelectedProfile(false); history.push('/admin') } + const searchUsers = (value) => { + axios(configApiCall(api_path_get_user_directory_search, 'GET', {"queryString": value ? value : "*"}, null)).then((response)=>{ + setUsers(response.data) + }).catch((error) =>{ + console.log(error); + setUsers([]) + if(error.response.status == 401){ + auth.authenticated = false + history.push('/') + } + }); + + } + + const initSearchUsers = useCallback(debounce((searchValue) => searchUsers(searchValue), 500), []) + + const handleSearchUsers = (e) => { + const searchValue = e.target.value; + initSearchUsers(searchValue) + } + if (!auth.hasAdminScope()) { return ( <div> @@ -164,7 +182,7 @@ export default function Users() { inputProps: { "aria-label": "Search users" }, - onKeyUp: (e) => setSearchValue(e.target.value), + onKeyUp: handleSearchUsers, }} /> <Search /> @@ -174,23 +192,12 @@ export default function Users() { </div> </GridItem> { - users.filter((data)=>{ - if(searchValue == null) - return data - else if((data.username != null ) && data.username.toLowerCase().includes(searchValue.toLowerCase()) || - (data.firstName != null ) && data.firstName.toLowerCase().includes(searchValue.toLowerCase()) || - (data.lastName != null ) && data.lastName.toLowerCase().includes(searchValue.toLowerCase()) || - (data.organization != null ) && data.organization.toLowerCase().includes(searchValue.toLowerCase()) || - (data.email != null ) && data.email.toLowerCase().includes(searchValue.toLowerCase()) || - (data.phoneNumber != null ) && data.phoneNumber.toLowerCase().includes(searchValue.toLowerCase()) || - (data.phoneNumberExtension != null ) && data.phoneNumberExtension.toLowerCase().includes(searchValue.toLowerCase()) || - (data.mobileNumber != null ) && data.mobileNumber.toLowerCase().includes(searchValue.toLowerCase()) || - (data.faxNumber != null ) && data.faxNumber.toLowerCase().includes(searchValue.toLowerCase()) - ){ - return data - } + users.sort(function(a, b){ + if(a.username < b.username) { return -1; } + if(a.username > b.username) { return 1; } + return 0; }).map(user => - <GridItem xs={12} sm={12} md={2} key={user.username} style={{display: user.display}}> + <GridItem xs={12} sm={12} md={2} key={user.username}> <Card profile> <CardBody profile>