diff --git a/jams-react-client/public/locales/en/translation.json b/jams-react-client/public/locales/en/translation.json index 970cc1a4d44c0b061daf7389e91c8bcefbe77698..9ef108cebc783872bec8c454c19a12691bb0af21 100644 --- a/jams-react-client/public/locales/en/translation.json +++ b/jams-react-client/public/locales/en/translation.json @@ -234,5 +234,6 @@ "device_id": "Device Id", "add_user_to_group": "Add user to group ...", "add_user_to_a_group": "Add user to a group", - "remove_from_group": "Remove from group" + "remove_from_group": "Remove from group", + "myprofile": "My profile" } diff --git a/jams-react-client/public/locales/fr/translation.json b/jams-react-client/public/locales/fr/translation.json index ac1a71087be1b8dff3dd5c8c0f6d5952a09699ca..b20eb23c1446fc39cbe2e0335e2df58d35f1720d 100644 --- a/jams-react-client/public/locales/fr/translation.json +++ b/jams-react-client/public/locales/fr/translation.json @@ -234,5 +234,6 @@ "device_id": "Identifiant de l'appareil", "add_user_to_group": "Ajouter un utilisateur au groupe...", "add_user_to_a_group": "Ajouter l'utilisateur à un groupe", - "remove_from_group": "Retirer du groupe" + "remove_from_group": "Retirer du groupe", + "myprofile": "My profile" } diff --git a/jams-react-client/src/assets/jss/material-dashboard-react/components/cardFooterStyle.js b/jams-react-client/src/assets/jss/material-dashboard-react/components/cardFooterStyle.js index b3ca458b8f6f2c68e2d781807b0f1a144cf265a8..94bbf6508582c0a90ef7ecb8c8d8fc296d86ac71 100644 --- a/jams-react-client/src/assets/jss/material-dashboard-react/components/cardFooterStyle.js +++ b/jams-react-client/src/assets/jss/material-dashboard-react/components/cardFooterStyle.js @@ -7,7 +7,7 @@ const cardFooterStyle = theme => ({ margin: "0 15px 10px", borderRadius: "0", justifyContent: "space-between", - alignItems: "center", + // alignItems: "center", display: "flex", backgroundColor: "transparent", border: "0", diff --git a/jams-react-client/src/assets/jss/material-dashboard-react/layouts/adminStyle.js b/jams-react-client/src/assets/jss/material-dashboard-react/layouts/adminStyle.js index fa699b3298046f84b44635e8da82396b14107bde..9d5893e1597b993a3422f3773042c79fc5c085cf 100644 --- a/jams-react-client/src/assets/jss/material-dashboard-react/layouts/adminStyle.js +++ b/jams-react-client/src/assets/jss/material-dashboard-react/layouts/adminStyle.js @@ -32,7 +32,7 @@ const appStyle = theme => ({ }, container, map: { - marginTop: "70px" + marginTop: "0px" } }); diff --git a/jams-react-client/src/components/Navbars/AdminNavbarLinks.js b/jams-react-client/src/components/Navbars/AdminNavbarLinks.js index 1b9ff7d3e25fc245da78b76d3c1034419cddeabc..ef329b0af18143a85c12c550083452b3a26b4d30 100644 --- a/jams-react-client/src/components/Navbars/AdminNavbarLinks.js +++ b/jams-react-client/src/components/Navbars/AdminNavbarLinks.js @@ -38,112 +38,6 @@ export default function AdminNavbarLinks(props) { return ( <div> - {/* <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> - <Button - color={window.innerWidth > 959 ? "transparent" : "white"} - justIcon={window.innerWidth > 959} - simple={!(window.innerWidth > 959)} - aria-label="Dashboard" - className={classes.buttonLink} - > - <Dashboard className={classes.icons} /> - <Hidden mdUp implementation="css"> - <p className={classes.linkText}>Dashboard</p> - </Hidden> - </Button> - <div className={classes.manager}> - <Button - color={window.innerWidth > 959 ? "transparent" : "white"} - justIcon={window.innerWidth > 959} - simple={!(window.innerWidth > 959)} - aria-owns={openNotification ? "notification-menu-list-grow" : null} - aria-haspopup="true" - onClick={handleClickNotification} - className={classes.buttonLink} - > - <Notifications className={classes.icons} /> - <span className={classes.notifications}>5</span> - <Hidden mdUp implementation="css"> - <p onClick={handleCloseNotification} className={classes.linkText}> - Notification - </p> - </Hidden> - </Button> - <Poppers - open={Boolean(openNotification)} - anchorEl={openNotification} - transition - disablePortal - className={ - classNames({ [classes.popperClose]: !openNotification }) + - " " + - classes.popperNav - } - > - {({ TransitionProps, placement }) => ( - <Grow - {...TransitionProps} - id="notification-menu-list-grow" - style={{ - transformOrigin: - placement === "bottom" ? "center top" : "center bottom" - }} - > - <Paper> - <ClickAwayListener onClickAway={handleCloseNotification}> - <MenuList role="menu"> - <MenuItem - onClick={handleCloseNotification} - className={classes.dropdownItem} - > - Mike John responded to your email - </MenuItem> - <MenuItem - onClick={handleCloseNotification} - className={classes.dropdownItem} - > - You have 5 new tasks - </MenuItem> - <MenuItem - onClick={handleCloseNotification} - className={classes.dropdownItem} - > - You{"'"}re now friend with Andrew - </MenuItem> - <MenuItem - onClick={handleCloseNotification} - className={classes.dropdownItem} - > - Another Notification - </MenuItem> - <MenuItem - onClick={handleCloseNotification} - className={classes.dropdownItem} - > - Another One - </MenuItem> - </MenuList> - </ClickAwayListener> - </Paper> - </Grow> - )} - </Poppers> - </div> */} <div className={classes.manager}> <Button color={window.innerWidth > 959 ? "transparent" : "white"} diff --git a/jams-react-client/src/components/Navbars/Navbar.js b/jams-react-client/src/components/Navbars/Navbar.js index 370ac08b64487ece259e14aa513d556658096934..fd31db219fdfa9bf1f2bd6fc5e56a375d478858e 100755 --- a/jams-react-client/src/components/Navbars/Navbar.js +++ b/jams-react-client/src/components/Navbars/Navbar.js @@ -26,14 +26,9 @@ export default function Header(props) { <AppBar className={classes.appBar + appBarClasses}> <Toolbar className={classes.container}> <div className={classes.flex}> - {/* Here we create navbar brand, based on route name */} <Button color="transparent" href="#" className={classes.title}> - {/* {makeBrand()} */} </Button> </div> - {/* <Hidden smDown implementation="css"> - {props.rtlActive ? <RTLNavbarLinks /> : <AdminNavbarLinks />} - </Hidden> */} <Hidden mdUp implementation="css"> <IconButton color="info" diff --git a/jams-react-client/src/components/ServerParameters/ServerParameters.js b/jams-react-client/src/components/ServerParameters/ServerParameters.js index 3144a3c909fe89560bba7af06b71d27471d338b2..16efbb7f5bd2cd091b7ff2c7dbbf6d111484c6a9 100644 --- a/jams-react-client/src/components/ServerParameters/ServerParameters.js +++ b/jams-react-client/src/components/ServerParameters/ServerParameters.js @@ -109,7 +109,7 @@ export default function ServerParameters(props) { auth.admin = true; window.localStorage.setItem('scope', 'true'); auth.checkDirectoryType(() => { - history.push("/admin/users"); + history.push("/users"); }); } diff --git a/jams-react-client/src/components/Sidebar/Sidebar.js b/jams-react-client/src/components/Sidebar/Sidebar.js index 5f4105a88f122b76154d55165d303df98ca91b7e..7f130062fad1ad0fcdea93c3581043d0e5db2d62 100755 --- a/jams-react-client/src/components/Sidebar/Sidebar.js +++ b/jams-react-client/src/components/Sidebar/Sidebar.js @@ -1,6 +1,6 @@ /*eslint-disable*/ import React from "react"; -import { useHistory } from "react-router-dom"; +import { Link, useHistory } from "react-router-dom"; import classNames from "classnames"; import PropTypes from "prop-types"; import { NavLink } from "react-router-dom"; @@ -14,8 +14,6 @@ import ListItemText from "@material-ui/core/ListItemText"; import Icon from "@material-ui/core/Icon"; // core components -import AdminNavbarLinks from "components/Navbars/AdminNavbarLinks.js"; -import RTLNavbarLinks from "components/Navbars/RTLNavbarLinks.js"; import ExitToAppIcon from "@material-ui/icons/ExitToApp"; import UpdateIcon from "@material-ui/icons/Update"; @@ -40,11 +38,6 @@ export default function Sidebar(props) { }); }; - const handleListItemClick = (e, layout, path) => { - e.preventDefault(); - history.push(layout + path.substring(0, 2)); - }; - React.useEffect(() => { if (auth.isUpdateAvailable() && auth.hasAdminScope()) setOpen(true); }, [mainPanel]); @@ -57,6 +50,8 @@ export default function Sidebar(props) { var links = ( <List className={classes.list}> {routes.map((prop, key) => { + if( prop.admin && !auth.hasAdminScope()) return; + if( !prop.admin && auth.hasAdminScope() && prop.path === "/user/admin") return; var activePro = " "; var listItemClasses; if (prop.path === "/upgrade-to-pro") { @@ -89,34 +84,35 @@ export default function Sidebar(props) { activeClassName="active" key={key} > - <ListItem - button - className={classes.itemLink + listItemClasses} - onClick={(e) => handleListItemClick(e, prop.layout, prop.path)} - > - {typeof prop.icon === "string" ? ( - <Icon - className={classNames(classes.itemIcon, whiteFontClasses, { - [classes.itemIconRTL]: props.rtlActive, - })} - > - {prop.icon} - </Icon> - ) : ( - <prop.icon - className={classNames(classes.itemIcon, whiteFontClasses, { - [classes.itemIconRTL]: props.rtlActive, + <Link to={`${prop.path}`}> + <ListItem + button + className={classes.itemLink + listItemClasses} + > + {typeof prop.icon === "string" ? ( + <Icon + className={classNames(classes.itemIcon, whiteFontClasses, { + [classes.itemIconRTL]: props.rtlActive, + })} + > + {prop.icon} + </Icon> + ) : ( + <prop.icon + className={classNames(classes.itemIcon, whiteFontClasses, { + [classes.itemIconRTL]: props.rtlActive, + })} + /> + )} + <ListItemText + primary={props.rtlActive ? prop.rtlName : prop.name} + className={classNames(classes.itemText, whiteFontClasses, { + [classes.itemTextRTL]: props.rtlActive, })} + disableTypography={true} /> - )} - <ListItemText - primary={props.rtlActive ? prop.rtlName : prop.name} - className={classNames(classes.itemText, whiteFontClasses, { - [classes.itemTextRTL]: props.rtlActive, - })} - disableTypography={true} - /> - </ListItem> + </ListItem> + </Link> </NavLink> </div> ); @@ -188,7 +184,6 @@ export default function Sidebar(props) { <div className={classes.logoImage}> <img src={logo} alt="logo" className={classes.img} /> </div> - {/* {logoText} */} </a> </div> ); @@ -211,7 +206,6 @@ export default function Sidebar(props) { > {brand} <div className={classes.sidebarWrapper}> - {props.rtlActive ? <RTLNavbarLinks /> : <AdminNavbarLinks />} {links} {bottomLinks} </div> diff --git a/jams-react-client/src/index.js b/jams-react-client/src/index.js index c9561a1e18f4b0d49080dc5df211dcdfc674fa16..587dbcdf0964e6616ab20a4cb89194c75e92cac9 100644 --- a/jams-react-client/src/index.js +++ b/jams-react-client/src/index.js @@ -24,8 +24,15 @@ import { ConfiguredRoute } from "configured.route"; import auth from "./auth"; // core components -import Admin from "layouts/Admin.js"; -import RTL from "layouts/RTL.js"; +import UsersRoute from "routes/UsersRoute.js"; +import UserRoute from "routes/UserRoute.js"; +import CreateUserRoute from "routes/CreateUserRoute.js"; +import GroupsRoute from "routes/GroupsRoute.js"; +import GroupRoute from "routes/GroupRoute.js"; +import BlueprintsRoute from "routes/BlueprintsRoute.js"; +import BlueprintRoute from "routes/BlueprintRoute.js"; +import SettingsRoute from "routes/SettingsRoute.js"; + import SignIn from "layouts/SignIn.js"; import "assets/css/material-dashboard-react.css?v=1.9.0"; @@ -41,8 +48,14 @@ auth.isServerInstalled(() => { <Router history={hist}> <Switch> <ConfiguredRoute path="/signin" component={SignIn} /> - <ProtectedRoute path="/admin" component={Admin} /> - {/*<Route path="/rtl" component={RTL} />*/} + <ProtectedRoute path="/users" component={UsersRoute} /> + <ProtectedRoute path="/user/:username" component={UserRoute} /> + <ProtectedRoute path="/createuser" component={CreateUserRoute} /> + <ProtectedRoute path="/groups" component={GroupsRoute} /> + {/* <ProtectedRoute path="/group/:groupname" component={GroupRoute} /> */} + <ProtectedRoute path="/blueprints" component={BlueprintsRoute} /> + <ProtectedRoute path="/blueprint/:blueprintname" component={BlueprintRoute} /> + <ProtectedRoute path="/settings" component={SettingsRoute} /> <Redirect from="/" to="/signin" /> </Switch> </Router> diff --git a/jams-react-client/src/layouts/Admin.js b/jams-react-client/src/layouts/BaseLayout.js similarity index 79% rename from jams-react-client/src/layouts/Admin.js rename to jams-react-client/src/layouts/BaseLayout.js index df779bcc1d0a08229a4b5503ac17507309c1d845..90f51fcae771cd8d091f1dcf94a5ad7ce550c4f5 100644 --- a/jams-react-client/src/layouts/Admin.js +++ b/jams-react-client/src/layouts/BaseLayout.js @@ -49,7 +49,7 @@ let ps; const useStyles = makeStyles(styles); -export default function Admin({ ...rest }) { +export default function Admin(props) { // styles const classes = useStyles(); // ref to help us initialize PerfectScrollbar on windows devices @@ -69,11 +69,17 @@ export default function Admin({ ...rest }) { const [query, setQuery] = React.useState(false); const [snackbarMessage, setSnackbarMessage] = React.useState(""); - const routes = [ + const Routes = [ + { + path: `/user/${auth.getUsername()}`, + name: i18next.t("myprofile", "My profile"), + icon: Person, + component: Users, + layout: "/admin", + }, { path: "/users", name: i18next.t("users", "Users"), - rtlName: "المستخدمون", icon: Person, component: Users, layout: "/admin", @@ -81,57 +87,32 @@ export default function Admin({ ...rest }) { { path: "/groups", name: i18next.t("groups", "Groups"), - rtlName: "مجموعات", icon: Group, component: Groups, layout: "/admin", + admin: true }, { path: "/blueprints", name: i18next.t("blueprints", "Blueprints"), - rtlName: "المخططات", icon: AllInbox, component: Blueprints, layout: "/admin", + admin: true }, { path: "/settings", name: i18next.t("settings", "Settings"), - rtlName: "Øضانة بيض", icon: SettingsIcon, component: Settings, layout: "/admin", + admin: true }, ]; - const switchRoutes = ( - <Switch> - {routes.map((prop, key) => { - if (prop.layout === "/admin") { - return ( - <Route - path={prop.layout + prop.path} - component={prop.component} - key={key} - /> - ); - } - return null; - })} - <Redirect from="/users" to="/admin/users" /> - <Redirect from="/admin/u" to="/admin/users" /> - <Redirect from="/admin/g" to="/admin/groups" /> - <Redirect from="/admin/b" to="/admin/blueprints" /> - <Redirect from="/admin/s" to="/admin/settings" /> - </Switch> - ); - const handleDrawerToggle = () => { setMobileOpen(!mobileOpen); }; - const getRoute = () => { - return window.location.pathname !== "/admin/maps"; - }; const resizeFunction = () => { if (window.innerWidth >= 960) { setMobileOpen(false); @@ -237,7 +218,7 @@ export default function Admin({ ...rest }) { </DialogActions> </Dialog> <Sidebar - routes={routes} + routes={Routes} logoText={"Jams"} logo={logo} image={image} @@ -248,32 +229,15 @@ export default function Admin({ ...rest }) { updating={updating} snackbarMessage={snackbarMessage} setOpenUpdate={setOpenUpdate} - {...rest} /> <div className={classes.mainPanel} ref={mainPanel}> - <Navbar - routes={routes} - handleDrawerToggle={handleDrawerToggle} - {...rest} - /> - {/* On the /maps route we want the map to be on full screen - this is not possible if the content and conatiner classes are present because they have some paddings which would make the map smaller */} - {getRoute() ? ( - <div className={classes.content}> - <div className={classes.container}>{switchRoutes}</div> - </div> - ) : ( - <div className={classes.map}>{switchRoutes}</div> - )} - {getRoute() ? <Footer /> : null} - {/* <FixedPlugin - handleImageClick={handleImageClick} - handleColorClick={handleColorClick} - bgColor={color} - bgImage={image} - handleFixedClick={handleFixedClick} - fixedClasses={fixedClasses} - /> */} + <Navbar + routes={Routes} + handleDrawerToggle={handleDrawerToggle} + /> + <div className={classes.map}>{props.component}</div> + <Footer /> </div> </div> ); diff --git a/jams-react-client/src/layouts/ListLayout.js b/jams-react-client/src/layouts/ListLayout.js new file mode 100644 index 0000000000000000000000000000000000000000..f802786b616a5bfc77c12a9c99aea216dffd8568 --- /dev/null +++ b/jams-react-client/src/layouts/ListLayout.js @@ -0,0 +1,249 @@ +import React from "react"; +import { Switch, Route, Redirect } from "react-router-dom"; +// creates a beautiful scrollbar +import PerfectScrollbar from "perfect-scrollbar"; +import "perfect-scrollbar/css/perfect-scrollbar.css"; +// @material-ui/core components +import { makeStyles } from "@material-ui/core/styles"; +// core components +import Navbar from "components/Navbars/Navbar.js"; +import Footer from "components/Footer/Footer.js"; +import Sidebar from "components/Sidebar/Sidebar.js"; + +// @material-ui/icons +import AccountCircleIcon from '@material-ui/icons/AccountCircle'; +import Person from "@material-ui/icons/Person"; +import Group from "@material-ui/icons/Group"; +import AllInbox from "@material-ui/icons/AllInbox"; +import SettingsIcon from "@material-ui/icons/Settings"; +// core components/views for Admin layout +import Users from "views/Users/Users.js"; +import Groups from "views/Groups/Groups.js"; +import Blueprints from "views/Blueprints/Blueprints.js"; +import Settings from "views/Settings/Settings.js"; +// core components/views for RTL layout + +import styles from "assets/jss/material-dashboard-react/layouts/adminStyle.js"; + +import bgImage from "assets/img/sidebar-2.jpg"; +import logo from "assets/img/logo-jams.svg"; + +import auth from "auth"; +import configApiCall from "api.js"; +import { + api_path_get_start_update +} from "globalUrls"; + +import axios from "axios"; + + +import Dialog from "@material-ui/core/Dialog/Dialog"; +import DialogTitle from "@material-ui/core/DialogTitle/DialogTitle"; +import DialogContent from "@material-ui/core/DialogContent/DialogContent"; +import DialogActions from "@material-ui/core/DialogActions/DialogActions"; +import DialogContentText from "@material-ui/core/DialogContentText/DialogContentText"; +import Button from "@material-ui/core/Button"; + +import i18next from "i18next"; + +let ps; + +const useStyles = makeStyles(styles); + +export default function Admin(props) { + // styles + const classes = useStyles(); + // ref to help us initialize PerfectScrollbar on windows devices + const mainPanel = React.createRef(); + // states and functions + const [image, setImage] = React.useState(bgImage); + const [color, setColor] = React.useState("blue"); + const [fixedClasses, setFixedClasses] = React.useState("dropdown"); + const [mobileOpen, setMobileOpen] = React.useState(false); + const [open, setOpen] = React.useState(false); + const [message, setMessage] = React.useState(false); + const [openUpdate, setOpenUpdate] = React.useState(false); + const [dialogMessage, setDialogMessage] = React.useState(""); + const [messageYes, setMessageYes] = React.useState(""); + const [messageNo, setMessageNo] = React.useState(""); + const [updating, setUpdating] = React.useState(false); + const [query, setQuery] = React.useState(false); + const [snackbarMessage, setSnackbarMessage] = React.useState(""); + + const Routes = [ + { + path: `/user/${auth.getUsername()}`, + name: i18next.t("myprofile", "My profile"), + icon: AccountCircleIcon, + component: Users, + layout: "/admin", + admin: false + }, + { + path: "/users", + name: i18next.t("users", "Users"), + icon: Person, + component: Users, + layout: "/admin", + admin: false + }, + { + path: "/groups", + name: i18next.t("groups", "Groups"), + icon: Group, + component: Groups, + layout: "/admin", + admin: true + }, + { + path: "/blueprints", + name: i18next.t("blueprints", "Blueprints"), + icon: AllInbox, + component: Blueprints, + layout: "/admin", + admin: true + }, + { + path: "/settings", + name: i18next.t("settings", "Settings"), + icon: SettingsIcon, + component: Settings, + layout: "/admin", + admin: true + }, + ]; + + const handleDrawerToggle = () => { + setMobileOpen(!mobileOpen); + }; + const resizeFunction = () => { + if (window.innerWidth >= 960) { + setMobileOpen(false); + } + }; + + const handleQuery = () => { + setQuery(true); + if (auth.hasAdminScope()) { + if (auth.isActivated()) { + setDialogMessage( + i18next.t("new_version_jams_available", "A new version of JAMS is available. Would you like to update now?") + ); + setMessageYes("Update Now"); + setMessageNo("Update Later"); + } else { + setDialogMessage( + i18next.t("running_community_version_jams", "You are currently running the community version of JAMS. Would you like to purchase a JAMS subscription?") + ); + setMessageYes("Yes, go to Jami Store"); + setMessageNo("No thanks"); + } + } else { + setDialogMessage( + i18next.t("you_are_not_allowed_to_access_this_section", "You are not allowed to access this section. Please contact your administrator to get administrator privileges.") + ); + } + }; + + // initialize and destroy the PerfectScrollbar plugin + React.useEffect(() => { + auth.checkForUpdates(() => { + auth.getUpdates(() => { + if (auth.isUpdateAvailable()) { + setOpen(true); + setMessage(i18next.t("an_update_is_available_for_jams", "An update is available for JAMS.")); + } + }); + }); + + if (navigator.platform.indexOf("Win") > -1) { + ps = new PerfectScrollbar(mainPanel.current, { + suppressScrollX: true, + suppressScrollY: false, + }); + document.body.style.overflow = "hidden"; + } + window.addEventListener("resize", resizeFunction); + // Specify how to clean up after this effect: + return function cleanup() { + if (navigator.platform.indexOf("Win") > -1) { + ps.destroy(); + } + window.removeEventListener("resize", resizeFunction); + }; + }, [openUpdate, mainPanel]); + + const handleCancel = () => { + setOpenUpdate(false); + }; + + const handleUpdate = () => { + setQuery(false); + if (auth.isActivated()) { + setSnackbarMessage(i18next.t("updating_jams_shutting_down_shortly", "Updating JAMS, shutting down shortly...")); + axios(configApiCall(api_path_get_start_update, "POST", null, null)) + .then(() => { + handleCancel(); + setUpdating(true); + }) + .catch((error) => { + setSnackbarMessage(i18next.t("error_while_attempting_update_jams", "Error occurred while attempting to update JAMS:") + error); + }); + } + }; + + return ( + <div className={classes.wrapper}> + <Dialog + open={openUpdate} + onClose={handleCancel} + aria-labelledby="form-dialog-title" + > + <DialogTitle id="form-revoke-dialog-title">Jams Update</DialogTitle> + <DialogContent> + <DialogContentText id="alert-dialog-description"> + {dialogMessage} + </DialogContentText> + </DialogContent> + <DialogActions> + <Button + onClick={(e) => { + if (auth.isActivated()) handleUpdate(); + else window.location.href = "https://staging.jami.biz/"; + }} + color="primary" + > + {messageYes} + </Button> + <Button onClick={handleCancel} color="primary"> + {messageNo} + </Button> + </DialogActions> + </Dialog> + <Sidebar + routes={Routes} + logoText={"Jams"} + logo={logo} + image={image} + handleDrawerToggle={handleDrawerToggle} + open={mobileOpen} + color={color} + handleQuery={handleQuery} + updating={updating} + snackbarMessage={snackbarMessage} + setOpenUpdate={setOpenUpdate} + /> + + <div className={classes.mainPanel} ref={mainPanel}> + <Navbar + routes={Routes} + handleDrawerToggle={handleDrawerToggle} + /> + <div className={classes.content}> + <div className={classes.container}>{props.component}</div> + </div> + <Footer /> + </div> + </div> + ); +} diff --git a/jams-react-client/src/layouts/SignIn.js b/jams-react-client/src/layouts/SignIn.js index 094d9a655cdf9faa300a1a21f1959b41cbbfaa06..820d57e5adc10765d682934ac5feabedfa5a1272 100644 --- a/jams-react-client/src/layouts/SignIn.js +++ b/jams-react-client/src/layouts/SignIn.js @@ -74,7 +74,11 @@ export default function SignIn(props) { React.useEffect(() => { if (auth.isAuthenticated() && auth.isInstalled()) { - history.push("/admin/users"); + if(auth.hasAdminScope()) + history.push("/users"); + else { + history.push(`/user/${auth.getUsername()}`); + } } }); @@ -88,7 +92,11 @@ export default function SignIn(props) { auth.checkLastKnownStep(() => { auth.checkDirectoryType(() => { if (auth.isInstalled) { - history.push("/admin/users"); + if(auth.hasAdminScope()) + history.push("/users"); + else { + history.push(`/user/${auth.getUsername()}`); + } } else { history.push("/"); } diff --git a/jams-react-client/src/routes/BlueprintRoute.js b/jams-react-client/src/routes/BlueprintRoute.js new file mode 100644 index 0000000000000000000000000000000000000000..182d9901069503604ace5eafce6670e79f5ccabd --- /dev/null +++ b/jams-react-client/src/routes/BlueprintRoute.js @@ -0,0 +1,10 @@ +import React from "react"; + +import BaseLayout from "layouts/BaseLayout.js"; +import Blueprint from "views/Blueprint/Blueprint"; + +export default function UsersRoute (props) { + return ( + <BaseLayout component={<Blueprint blueprintName={props.match.params.blueprintname} />} /> + ) +} \ No newline at end of file diff --git a/jams-react-client/src/routes/BlueprintsRoute.js b/jams-react-client/src/routes/BlueprintsRoute.js new file mode 100644 index 0000000000000000000000000000000000000000..cd7657009979b8207e33ba8d545c288379ceec4f --- /dev/null +++ b/jams-react-client/src/routes/BlueprintsRoute.js @@ -0,0 +1,11 @@ +import React from "react"; + +import Blueprints from "views/Blueprints/Blueprints.js"; + +import ListLayout from "layouts/ListLayout.js"; + +export default function UsersRoute () { + return ( + <ListLayout component={<Blueprints />} /> + ) +} \ No newline at end of file diff --git a/jams-react-client/src/routes/CreateUserRoute.js b/jams-react-client/src/routes/CreateUserRoute.js new file mode 100644 index 0000000000000000000000000000000000000000..031f871e74efc7b8d963ecab2a1b26c53007d4da --- /dev/null +++ b/jams-react-client/src/routes/CreateUserRoute.js @@ -0,0 +1,19 @@ +import React from "react"; + +import EditCreateUserProfile from "views/UserProfile/EditCreateUserProfile.js"; + +import ListLayout from "layouts/ListLayout.js"; + +export default function CreateUserRoute () { + return ( + <ListLayout component={<EditCreateUserProfile createUser={true} />} /> + ) +} + +// <EditCreateUserProfile + // createUser={createUser} + // setCreateUser={setCreateUser} + // setError={props.setError} + // setErrorMessage={props.setErrorMessage} + // setSeverity={props.setSeverity} + // /> \ No newline at end of file diff --git a/jams-react-client/src/routes/GroupRoute.js b/jams-react-client/src/routes/GroupRoute.js new file mode 100644 index 0000000000000000000000000000000000000000..4fe4eb49fb407c4d3539f3e32472a5194f16c35f --- /dev/null +++ b/jams-react-client/src/routes/GroupRoute.js @@ -0,0 +1,11 @@ +import React from "react"; + +import EditGroup from "views/Groups/EditGroup.js"; + +import BaseLayout from "layouts/BaseLayout.js"; + +export default function UsersRoute (props) { + return ( + <BaseLayout component={<EditGroup groupName={props.match.params.groupname} />} /> + ) +} \ No newline at end of file diff --git a/jams-react-client/src/routes/GroupsRoute.js b/jams-react-client/src/routes/GroupsRoute.js new file mode 100644 index 0000000000000000000000000000000000000000..ea731f5c42aaa0397bddaa2e8f25ea65afa2aab1 --- /dev/null +++ b/jams-react-client/src/routes/GroupsRoute.js @@ -0,0 +1,11 @@ +import React from "react"; + +import Groups from "views/Groups/Groups.js"; + +import ListLayout from "layouts/ListLayout.js"; + +export default function UsersRoute () { + return ( + <ListLayout component={<Groups />} /> + ) +} \ No newline at end of file diff --git a/jams-react-client/src/routes/SettingsRoute.js b/jams-react-client/src/routes/SettingsRoute.js new file mode 100644 index 0000000000000000000000000000000000000000..60fd2a61988b26b930a2375aa81c4593b4d899fa --- /dev/null +++ b/jams-react-client/src/routes/SettingsRoute.js @@ -0,0 +1,11 @@ +import React from "react"; + +import Settings from "views/Settings/Settings.js"; + +import BaseLayout from "layouts/BaseLayout.js"; + +export default function UsersRoute (props) { + return ( + <BaseLayout component={<Settings />} /> + ) +} \ No newline at end of file diff --git a/jams-react-client/src/routes/UserRoute.js b/jams-react-client/src/routes/UserRoute.js new file mode 100644 index 0000000000000000000000000000000000000000..5a765ab7d4bcc5e66d0e549d6f587edbedb40de8 --- /dev/null +++ b/jams-react-client/src/routes/UserRoute.js @@ -0,0 +1,11 @@ +import React from "react"; + +import UserProfile from "views/UserProfile/UserProfile.js"; + +import BaseLayout from "layouts/BaseLayout.js"; + +export default function UsersRoute (props) { + return ( + <BaseLayout component={<UserProfile username={props.match.params.username} />} /> + ) +} \ No newline at end of file diff --git a/jams-react-client/src/routes/UsersRoute.js b/jams-react-client/src/routes/UsersRoute.js new file mode 100644 index 0000000000000000000000000000000000000000..668f097a4055947aac069c0615474edbecfd9fa4 --- /dev/null +++ b/jams-react-client/src/routes/UsersRoute.js @@ -0,0 +1,11 @@ +import React from "react"; + +import Users from "views/Users/Users.js"; + +import ListLayout from "layouts/ListLayout.js"; + +export default function UsersRoute () { + return ( + <ListLayout component={<Users />} /> + ) +} \ No newline at end of file diff --git a/jams-react-client/src/routes.js b/jams-react-client/src/routes/routes.js similarity index 51% rename from jams-react-client/src/routes.js rename to jams-react-client/src/routes/routes.js index 525ca3c058d133af1630355ea9adc9d2d0d2a118..03eafaedd60d2c74a167390f5a0a26d8394d47c9 100644 --- a/jams-react-client/src/routes.js +++ b/jams-react-client/src/routes/routes.js @@ -29,58 +29,44 @@ import Blueprints from "views/Blueprints/Blueprints.js"; import Settings from "views/Settings/Settings.js"; // core components/views for RTL layout -import "./i18n"; import i18next from "i18next"; -const dashboardRoutes = [ - { - path: "/users", - name: ( - <Suspense fallback={<div>Loading...</div>}> - {i18next.t("users", "Users")} - </Suspense> - ), - rtlName: "المستخدمون", - icon: Person, - component: Users, - layout: "/admin", - }, - { - path: "/groups", - name: ( - <Suspense fallback={<div>Loading...</div>}> - {i18next.t("groups", "Groups")} - </Suspense> - ), - rtlName: "مجموعات", - icon: Group, - component: Groups, - layout: "/admin", - }, - { - path: "/blueprints", - name: ( - <Suspense fallback={<div>Loading...</div>}> - {i18next.t("blueprints", "Blueprints")} - </Suspense> - ), - rtlName: "المخططات", - icon: AllInbox, - component: Blueprints, - layout: "/admin", - }, - { - path: "/settings", - name: ( - <Suspense fallback={<div>Loading...</div>}> - {i18next.t("settings", "Settings")} - </Suspense> - ), - rtlName: "Øضانة بيض", - icon: SettingsIcon, - component: Settings, - layout: "/admin", - }, -]; +export default function Routes() { + const dashboardRoutes = [ + { + path: "/users", + name: i18next.t("users", "Users"), + rtlName: "المستخدمون", + icon: Person, + component: Users, + layout: "/admin", + }, + { + path: "/groups", + name: i18next.t("groups", "Groups"), + rtlName: "مجموعات", + icon: Group, + component: Groups, + layout: "/admin", + }, + { + path: "/blueprints", + name: i18next.t("blueprints", "Blueprints"), + rtlName: "المخططات", + icon: AllInbox, + component: Blueprints, + layout: "/admin", + }, + { + path: "/settings", + name: i18next.t("settings", "Settings"), + rtlName: "Øضانة بيض", + icon: SettingsIcon, + component: Settings, + layout: "/admin", + }, + ]; -export default dashboardRoutes; + return dashboardRoutes; + +} \ No newline at end of file diff --git a/jams-react-client/src/views/Blueprint/Blueprint.js b/jams-react-client/src/views/Blueprint/Blueprint.js index 7ea53ff7757ec114216af8a98a01e9ff2c39baaf..0805526cc953cbd9c88b77d76d4ea34909fdf3f7 100644 --- a/jams-react-client/src/views/Blueprint/Blueprint.js +++ b/jams-react-client/src/views/Blueprint/Blueprint.js @@ -78,7 +78,7 @@ export default function Blueprint(props) { return ( <div> <AppBar position="static" style={{ background: infoColor[0] }}> - <Tabs value={value} onChange={handleChange} aria-label="simple tabs example"> + <Tabs value={value} onChange={handleChange} aria-label="simple tabs example" style={{ margin: "0.7rem"}}> <Tab label={i18next.t("permissions", "Permissions")} {...a11yProps(0)} /> <Tab label={i18next.t("configuration", "Configuration")} {...a11yProps(1)} /> </Tabs> diff --git a/jams-react-client/src/views/Blueprints/Blueprints.js b/jams-react-client/src/views/Blueprints/Blueprints.js index b7333a9f2f4e66e25607e7ef1ebf4ae8d086b912..78dd21e152dda47d19641d6bd3f9a1d230a48da0 100644 --- a/jams-react-client/src/views/Blueprints/Blueprints.js +++ b/jams-react-client/src/views/Blueprints/Blueprints.js @@ -1,5 +1,5 @@ import React, { useCallback, useEffect } from "react"; -import { useHistory } from "react-router-dom"; +import { Link, useHistory } from "react-router-dom"; // @material-ui/core components import { makeStyles } from "@material-ui/core/styles"; import InputLabel from "@material-ui/core/InputLabel"; @@ -207,7 +207,7 @@ export default function Blueprints() { setOpen(false); console.log("Could not create " + blueprintName + " " + error); }); - history.push("/admin/blueprints"); + history.push(`/blueprint/${blueprintName}`); }; const handleRemoveBlueprint = (blueprintRemovedName) => { @@ -232,199 +232,179 @@ export default function Blueprints() { console.log("Could not create " + removedBlueprint + " " + error); setOpenRemoveDialog(false); }); - history.push("/admin/blueprints"); + history.push("/blueprints"); }; - if (!auth.hasAdminScope()) { - return ( - <div> - <h4> - {i18next.t("you_are_not_allowed_to_access_this_section", "You are not allowed to access this section. Please contact your administrator to get administrator privileges.")} - </h4> - </div> - ); - } else { - if (selectedBlueprint) { - return ( - <div> - <Blueprint blueprintName={selectedBlueprintName} /> - </div> - ); - } else { - return ( - <div> - <Dialog - open={open} - onClose={handleClose} - aria-labelledby="alert-dialog-title" - aria-describedby="alert-dialog-description" + + return ( + <div> + <Dialog + open={open} + onClose={handleClose} + aria-labelledby="alert-dialog-title" + aria-describedby="alert-dialog-description" + > + <DialogTitle id="alert-dialog-title"> + {i18next.t("create_blueprint", "Create blueprint")} + </DialogTitle> + <DialogContent> + <DialogContentText id="alert-dialog-description"> + <FormControl + className={classes.margin} + size="large" + error={blueprintNameExits} + > + <InputLabel htmlFor="blueprintName"> + {i18next.t("blueprint_name", "Blueprint name")} + </InputLabel> + <Input + id="blueprintName" + placeholder={i18next.t("blueprint_name", "Blueprint name")} + startAdornment={ + <InputAdornment position="start"> + <AllInbox /> + </InputAdornment> + } + onChange={(e) => { + setBlueprintName(e.target.value); + initCheckBlueprintNameExists(e.target.value); + }} + /> + </FormControl> + {disableCreate && blueprintName.length > 0 && ( + <p>{i18next.t("blueprint_name_already_exists", "Blueprint name already exists!")}</p> + )} + {disableCreate && blueprintName.length === 0 && ( + <p>{i18next.t("blueprint_name_is_empty", "Blueprint name is empty")}</p> + )} + </DialogContentText> + </DialogContent> + <DialogActions> + <Button onClick={handleClose} color="info"> + Cancel + </Button> + <Button + onClick={handleCreateBlueprint} + color="primary" + disabled={disableCreate} + autoFocus > - <DialogTitle id="alert-dialog-title"> - {i18next.t("create_blueprint", "Create blueprint")} - </DialogTitle> - <DialogContent> - <DialogContentText id="alert-dialog-description"> - <FormControl - className={classes.margin} - size="large" - error={blueprintNameExits} - > - <InputLabel htmlFor="blueprintName"> - {i18next.t("blueprint_name", "Blueprint name")} - </InputLabel> - <Input - id="blueprintName" - placeholder={i18next.t("blueprint_name", "Blueprint name")} - startAdornment={ - <InputAdornment position="start"> - <AllInbox /> - </InputAdornment> - } - onChange={(e) => { - setBlueprintName(e.target.value); - initCheckBlueprintNameExists(e.target.value); - }} - /> - </FormControl> - {disableCreate && blueprintName.length > 0 && ( - <p>{i18next.t("blueprint_name_already_exists", "Blueprint name already exists!")}</p> - )} - {disableCreate && blueprintName.length === 0 && ( - <p>{i18next.t("blueprint_name_is_empty", "Blueprint name is empty")}</p> - )} - </DialogContentText> - </DialogContent> - <DialogActions> - <Button onClick={handleClose} color="info"> - Cancel - </Button> - <Button - onClick={handleCreateBlueprint} - color="primary" - disabled={disableCreate} - autoFocus - > - Create - </Button> - </DialogActions> - </Dialog> - <Dialog - open={openRemoveDialog} - onClose={() => setOpenRemoveDialog(false)} - aria-labelledby="alert-dialog-title" - aria-describedby="alert-dialog-description" + Create + </Button> + </DialogActions> + </Dialog> + <Dialog + open={openRemoveDialog} + onClose={() => setOpenRemoveDialog(false)} + aria-labelledby="alert-dialog-title" + aria-describedby="alert-dialog-description" + > + <DialogTitle id="alert-dialog-title"> + {i18next.t("remove_blueprint", "Remove blueprint")} + </DialogTitle> + <DialogContent> + <DialogContentText id="alert-dialog-description"> + {i18next.t("are_you_sure_you_want_to_delete", "Are you sure you want to delete")}{" "} + <strong>{removedBlueprint}</strong> ? + </DialogContentText> + </DialogContent> + <DialogActions> + <Button + onClick={() => setOpenRemoveDialog(false)} + color="primary" > - <DialogTitle id="alert-dialog-title"> - {i18next.t("remove_blueprint", "Remove blueprint")} - </DialogTitle> - <DialogContent> - <DialogContentText id="alert-dialog-description"> - {i18next.t("are_you_sure_you_want_to_delete", "Are you sure you want to delete")}{" "} - <strong>{removedBlueprint}</strong> ? - </DialogContentText> - </DialogContent> - <DialogActions> - <Button - onClick={() => setOpenRemoveDialog(false)} - color="primary" - > - {i18next.t("cancel", "Cancel")} - </Button> - <Button onClick={removeBlueprint} color="info" autoFocus> - {i18next.t("remove", "Remove")} - </Button> - </DialogActions> - </Dialog> - <GridContainer> - <GridItem xs={12} sm={12} md={12}> - <Button - variant="contained" - color="primary" - href="#contained-buttons" - onClick={(e) => setOpen(true)} - > - <AddCircleOutlineIcon /> {i18next.t("create_blueprint", "Create blueprint")} - </Button> - <div className={classes.searchWrapper}> - {!zeroBlueprint && ( - <CustomInput - formControlProps={{ - className: classes.margin + " " + classes.search, - }} - inputProps={{ - placeholder: i18next.t("search_blueprints_placeholder", "Search blueprints…"), - inputProps: { - "aria-label": i18next.t("search_blueprints", "Search blueprints"), - }, - onKeyUp: (e) => setSearchValue(e.target.value), + {i18next.t("cancel", "Cancel")} + </Button> + <Button onClick={removeBlueprint} color="info" autoFocus> + {i18next.t("remove", "Remove")} + </Button> + </DialogActions> + </Dialog> + <GridContainer> + <GridItem xs={12} sm={12} md={12}> + <Button + variant="contained" + color="primary" + href="#contained-buttons" + onClick={(e) => setOpen(true)} + > + <AddCircleOutlineIcon /> {i18next.t("create_blueprint", "Create blueprint")} + </Button> + <div className={classes.searchWrapper}> + {!zeroBlueprint && ( + <CustomInput + formControlProps={{ + className: classes.margin + " " + classes.search, + }} + inputProps={{ + placeholder: i18next.t("search_blueprints_placeholder", "Search blueprints…"), + inputProps: { + "aria-label": i18next.t("search_blueprints", "Search blueprints"), + }, + onKeyUp: (e) => setSearchValue(e.target.value), + }} + /> + )} + {!zeroBlueprint && <Search />} + <div className={classes.loading}> + {loading && ( + <LinearProgress variant="determinate" value={progress} /> + )} + </div> + </div> + </GridItem> + {zeroBlueprint ? ( + <div className={classes.blueprintsNotFound}> + <InfoIcon /> + <p style={{ marginLeft: "10px" }}>{i18next.t("no_blueprints_found", "No blueprints found")}</p> + </div> + ) : ( + blueprints.map((blueprint) => ( + <GridItem + xs={12} + sm={6} + md={3} + lg={2} + xl={2} + key={blueprint.name}> + <Card profile> + <Link to={`/blueprint/${blueprint.name}`}> + <CardBody profile> + <h3 className={classes.cardTitle}> + {blueprint.name + ? blueprint.name + : i18next.t("no_blueprint_name", "No blueprint name")} + </h3> + <strong>Description:</strong> + <p>{i18next.t("no_description", "No description")}</p> + <ul> + <li> + <GroupIcon + fontSize="small" + style={{ marginRight: "10px" }} + />{" "} + {blueprint.usersCount + ? blueprint.usersCount + " user(s)" + : i18next.t("no_users_count", "No users count")} + </li> + </ul> + </CardBody> + </Link> + <CardFooter> + <IconButton + color="secondary" + onClick={() => { + handleRemoveBlueprint(blueprint.name); }} - /> - )} - {!zeroBlueprint && <Search />} - <div className={classes.loading}> - {loading && ( - <LinearProgress variant="determinate" value={progress} /> - )} - </div> - </div> + > + <DeleteOutlineIcon /> + </IconButton> + </CardFooter> + </Card> </GridItem> - {zeroBlueprint ? ( - <div className={classes.blueprintsNotFound}> - <InfoIcon /> - <p style={{ marginLeft: "10px" }}>{i18next.t("no_blueprints_found", "No blueprints found")}</p> - </div> - ) : ( - blueprints.map((blueprint) => ( - <GridItem - xs={12} - sm={6} - md={3} - lg={2} - xl={2} - key={blueprint.name}> - <Card profile> - <a - href="#" - onClick={(e) => redirectToBlueprint(e, blueprint.name)} - > - <CardBody profile> - <h3 className={classes.cardTitle}> - {blueprint.name - ? blueprint.name - : i18next.t("no_blueprint_name", "No blueprint name")} - </h3> - <strong>Description:</strong> - <p>{i18next.t("no_description", "No description")}</p> - <ul> - <li> - <GroupIcon - fontSize="small" - style={{ marginRight: "10px" }} - />{" "} - {blueprint.usersCount - ? blueprint.usersCount + " user(s)" - : i18next.t("no_users_count", "No users count")} - </li> - </ul> - </CardBody> - </a> - <CardFooter> - <IconButton - color="secondary" - onClick={() => { - handleRemoveBlueprint(blueprint.name); - }} - > - <DeleteOutlineIcon /> - </IconButton> - </CardFooter> - </Card> - </GridItem> - )) - )} - </GridContainer> - </div> - ); - } - } + )) + )} + </GridContainer> + </div> + ); } diff --git a/jams-react-client/src/views/Contacts/Contacts.js b/jams-react-client/src/views/Contacts/Contacts.js index cbce942d6e246d42050cf93ad8a9c386f3a0bccd..d101d23f3e919700d57e4740d2f607ee39f06baa 100644 --- a/jams-react-client/src/views/Contacts/Contacts.js +++ b/jams-react-client/src/views/Contacts/Contacts.js @@ -1,5 +1,5 @@ import React, { useEffect } from "react"; -import { useHistory } from "react-router-dom"; +import { Link, useHistory } from "react-router-dom"; // @material-ui/core components import { makeStyles } from "@material-ui/core/styles"; // core components @@ -99,7 +99,18 @@ export default function Users(props) { ) ) .then((response) => { - setUsers(response.data.profiles); + let profiles = []; + const profilesResults = response.data.profiles; + profilesResults.forEach((profile) =>{ + if(profile.username !== props.username) { + let existingContact = false; + contacts.forEach((contact)=>{ + if(profile.username === contact.username) existingContact = true; + }) + if(!existingContact) profiles.push(profile); + } + }) + setUsers(profiles); }) .catch((error) => { console.log(error); @@ -261,8 +272,8 @@ export default function Users(props) { return Math.min(oldProgress + diff, 100); }); }, 500); - searchContacts(); getAllContacts(); + searchContacts(); return () => { clearInterval(timer); }; @@ -354,7 +365,7 @@ export default function Users(props) { </Dialog> <GridContainer> <GridItem xs={12} sm={12} md={12}> - <Button + {auth.hasAdminScope() && <Button variant="contained" color="primary" href="#contained-buttons" @@ -363,7 +374,7 @@ export default function Users(props) { }} > <AddCircleOutlineIcon /> {i18next.t("add_a_contact", "Add contact")} - </Button> + </Button>} <div className={classes.searchWrapper}> <CustomInput formControlProps={{ @@ -387,12 +398,15 @@ export default function Users(props) { </GridItem> {contacts .filter((data) => { - if (searchValue == null) return data; - else if ( - data.uri != null && - data.uri.toLowerCase().includes(searchValue.toLowerCase()) - ) { - return data; + if (searchValue === null) return data; + else { + if(typeof data.name !== "undefined" && typeof data.firstName !== "undefined" && typeof data.lastName !== "undefined" && typeof data.uri !== "undefined"){ + return data.name.toLowerCase().includes(searchValue.toLowerCase()) || data.firstName.toLowerCase().includes(searchValue.toLowerCase()) || data.lastName.toLowerCase().includes(searchValue.toLowerCase()) || data.uri.toLowerCase().includes(searchValue.toLowerCase()); + } + else { + return data; + } + } }) .map((contact) => ( @@ -404,54 +418,53 @@ export default function Users(props) { key={contact.uri} style={{ display: contact.display }} > - <Card profile> - <CardBody profile> - <CardAvatar profile> - <img - src={ - contact.profilePicture - ? "data:image/png;base64, " + contact.profilePicture - : noProfilePicture - } - alt="..." - /> - </CardAvatar> - <h4 className={classes.cardTitle}> - {contact.firstName !== "" - ? contact.firstName - : "No first name"}{" "} - {contact.lastName !== "" ? contact.lastName : "No last name"} - </h4> - <ul> - <li> + {contact.name && <Card profile> + <a href={`/user/${contact.name}`}> + <CardBody profile> + <CardAvatar profile> <img - src={jami} - width="20" - alt="Jami" - style={{ marginRight: "10px" }} - />{" "} - {contact.name ? contact.name : "No name"} - </li> - <li> - <BusinessOutlinedIcon - fontSize="small" - style={{ marginRight: "10px" }} - />{" "} - {contact.organization} - </li> - </ul> - </CardBody> - <CardFooter> - <IconButton - color="secondary" - onClick={() => { - handleRemoveContact(contact.uri, contact.name); - }} - > - <DeleteOutlineIcon /> - </IconButton> - </CardFooter> - </Card> + src={ + contact.profilePicture + ? "data:image/png;base64, " + contact.profilePicture + : noProfilePicture + } + alt="..." + /> + </CardAvatar> + <h4 className={classes.cardTitle}> + {(contact.firstName || contact.lastName) &&`${contact.firstName} ${contact.lastName}`} + </h4> + <ul> + <li> + {contact.name && <img + src={jami} + width="20" + alt="Jami" + style={{ marginRight: "10px" }} + />} + {contact.name && ` ${contact.name}`} + </li> + <li> + {contact.organization && <BusinessOutlinedIcon + fontSize="small" + style={{ marginRight: "10px" }} + />} + {contact.organization && ` ${contact.organization}`} + </li> + </ul> + </CardBody> + </a> + <CardFooter> + <IconButton + color="secondary" + onClick={() => { + handleRemoveContact(contact.uri, contact.name); + }} + > + <DeleteOutlineIcon /> + </IconButton> + </CardFooter> + </Card>} </GridItem> ))} {contacts === [] && props.username + i18next.t("has_no_contacts", " has no contacts")} diff --git a/jams-react-client/src/views/Groups/EditGroup.js b/jams-react-client/src/views/Groups/EditGroup.js index 22a1d5d2dc61666d6879fa42221b106a76d36cbb..e84c6899964295f229156cfc04ea17aa361b06b4 100644 --- a/jams-react-client/src/views/Groups/EditGroup.js +++ b/jams-react-client/src/views/Groups/EditGroup.js @@ -1,5 +1,5 @@ import React from "react"; -import { useHistory } from "react-router-dom"; +import { Link, useHistory } from "react-router-dom"; import classnames from "classnames"; // @material-ui/core components @@ -224,7 +224,16 @@ export default function EditGroup(props) { ) ) .then((response) => { - setUsers(response.data.profiles); + let profiles = []; + const profilesResults = response.data.profiles; + profilesResults.forEach((profile) =>{ + let existingUser = false; + users.forEach((user)=>{ + if(profile.username === user.username) existingUser = true; + }) + if(!existingUser) profiles.push(profile); + }) + setUsers(profiles); }) .catch((error) => { console.log(error); @@ -348,24 +357,26 @@ export default function EditGroup(props) { {groupMembers.map(user => <TableRow key={user.username} className={classes.tableRow}> <TableCell className={tableCellClasses}> - <Avatar - style={{ marginRight: "10px" }} - alt={user.username} - src={ - user.profilePicture - ? "data:image/png;base64, " + user.profilePicture - : noProfilePicture - } - /> + <Link to={`/user/${user.username}`}> + <Avatar + style={{ marginRight: "10px" }} + alt={user.username} + src={ + user.profilePicture + ? "data:image/png;base64, " + user.profilePicture + : noProfilePicture + } + /> + </Link> </TableCell> <TableCell className={tableCellClasses}> - {user.username} + <Link to={`/user/${user.username}`}>{user.username}</Link> </TableCell> <TableCell className={tableCellClasses}> - {user.firstName} + <Link to={`/user/${user.username}`}>{user.firstName}</Link> </TableCell> <TableCell className={tableCellClasses}> - {user.lastName} + <Link to={`/user/${user.username}`}>{user.lastName}</Link> </TableCell> <TableCell align="right" className={classes.tableActions}> <Button color="primary" onClick={() => updateUserInGroup(user)}>{i18next.t("remove_user", "Remove user")}</Button> @@ -378,4 +389,4 @@ export default function EditGroup(props) { </GridContainer> </div> ); -} \ No newline at end of file +} \ No newline at end of file diff --git a/jams-react-client/src/views/Groups/Groups.js b/jams-react-client/src/views/Groups/Groups.js index be38fe723255d5a79d59f8e485f56e588cb67f43..7ce03df255567144f51be09e7bef5bc3b858671c 100644 --- a/jams-react-client/src/views/Groups/Groups.js +++ b/jams-react-client/src/views/Groups/Groups.js @@ -132,7 +132,7 @@ export default function Groups() { setOpenRemoveDialog(false); }); - history.push("/admin/groups"); + history.push("/groups"); }; const getBlueprintsOptions = () => { @@ -267,7 +267,7 @@ export default function Groups() { console.log("Error creating group: " + error); setOpenCreate(false); }); - history.push("/admin/groups"); + history.push(`/group/${groupName}`); }; const blueprintsOptionsItems = tool.buildSelectMenuItems( diff --git a/jams-react-client/src/views/Settings/Settings.js b/jams-react-client/src/views/Settings/Settings.js index e8dde97209a969438c7d8c9997e5815253c8059f..ec3c61fd7454f79bde451f021bc7adf27ec15593 100644 --- a/jams-react-client/src/views/Settings/Settings.js +++ b/jams-react-client/src/views/Settings/Settings.js @@ -99,6 +99,7 @@ export default function Settings(props) { value={value} onChange={handleChange} aria-label="simple tabs example" + style={{ margin: "0.7rem"}} > <Tab label={i18next.t("general", "General")} {...a11yProps(0)} /> <Tab label={i18next.t("subscription", "Subscription")} {...a11yProps(1)} /> diff --git a/jams-react-client/src/views/UserProfile/DisplayUserProfile.js b/jams-react-client/src/views/UserProfile/DisplayUserProfile.js index 06f9b416785086f917937fd9f7521e3012a402c8..69d7a547ea77dfb9d28978224dcc87e73cc9f1f3 100644 --- a/jams-react-client/src/views/UserProfile/DisplayUserProfile.js +++ b/jams-react-client/src/views/UserProfile/DisplayUserProfile.js @@ -126,16 +126,41 @@ const styles = (theme) => ({ minWidth: "80vh", maxWidth: "80vh", }, + profileFooter: { + [theme.breakpoints.down("md")]: { + display: "flex", + flexDirection: "column", + } + }, footerActionButtons:{ display: "flex", flexDirection: "row", + alignItems: "end", "& button": { marginRight: "1rem" }, + [theme.breakpoints.down("md")]: { + flexDirection: "column", + alignItems: "stretch", + "& button": { + marginRight: "0", + width: "100%" + }, + } }, footerActionButtonsRight:{ display: "flex", justifyContent: "flex-end", + alignItems: "end", + [theme.breakpoints.down("md")]: { + flexDirection: "column", + justifyContent: "center", + alignItems: "stretch", + "& button": { + marginRight: "0", + width: "100%" + }, + } }, userProfileHeader: { display: "flex", @@ -155,8 +180,8 @@ const styles = (theme) => ({ }, }, cardAvatarMobile: { - [theme.breakpoints.down("sm")]: { - margin: "-50px 50px 0", + [theme.breakpoints.down("md")]: { + textAlign: "center" }, }, loading: { @@ -191,7 +216,19 @@ export default function DisplayUserProfile(props) { ) ) .then((response) => { - setGroups(response.data); + let availableGroups = []; + const groupResults = response.data; + groupResults.forEach((possibleGroup) =>{ + let existingGroup = false; + groups.forEach((actualGroup)=>{ + if(actualGroup === possibleGroup.name) { + existingGroup = true; + return; + } + }) + if(!existingGroup) availableGroups.push(possibleGroup); + }) + setGroups(availableGroups); }) .catch((error) => { if (error.response) { @@ -404,6 +441,16 @@ export default function DisplayUserProfile(props) { const tableCellClasses = classnames(classes.tableCell); + const canEdit = () => { + if(auth.isLocalDirectory()) { + if(!auth.hasAdminScope() && auth.getUsername() !== user.username) { + return false + } + return true; + } + else return false; + } + return ( <div> <Dialog @@ -563,10 +610,10 @@ export default function DisplayUserProfile(props) { </Grid> </div> </CardBody> - <CardFooter> + <CardFooter className={classes.profileFooter}> <Grid container className={classes.footerActionButtons}> <Grid item > - {auth.isLocalDirectory() && ( + {canEdit() && ( <Button color="info" onClick={() => props.setDisplayUser(false)} diff --git a/jams-react-client/src/views/UserProfile/EditCreateUserProfile.js b/jams-react-client/src/views/UserProfile/EditCreateUserProfile.js index 3830d690b05f735b19bd2ab662d5d9aff3b1ff4e..47984eacbc522fbf0263d38471352f2dbe36f7e5 100644 --- a/jams-react-client/src/views/UserProfile/EditCreateUserProfile.js +++ b/jams-react-client/src/views/UserProfile/EditCreateUserProfile.js @@ -360,7 +360,7 @@ export default function EditCreateUserProfile(props) { const handleUserProfileCreation = (data) => { axios(configApiCall(api_path_post_create_user_profile, "POST", data, null)) .then(() => { - history.push("/"); + history.push(`/user/${data.username}`); }) .catch((error) => { console.log("Error creating user profile: " + error); diff --git a/jams-react-client/src/views/UserProfile/UserProfile.js b/jams-react-client/src/views/UserProfile/UserProfile.js index cf6ee46ac854eaf4c4044d25f37611d158738220..3bd1f7b7f03971bbaeb7d866099b6f3b62dfcd87 100755 --- a/jams-react-client/src/views/UserProfile/UserProfile.js +++ b/jams-react-client/src/views/UserProfile/UserProfile.js @@ -69,6 +69,7 @@ export default function UserProfile(props) { value={value} onChange={handleChange} aria-label="simple tabs example" + style={{ margin: "0.7rem"}} > <Tab label={i18next.t("profile", "Profile")} {...a11yProps(0)} /> <Tab label={i18next.t("devices", "Devices")} {...a11yProps(1)} /> @@ -93,7 +94,7 @@ export default function UserProfile(props) { <Devices username={props.username} /> </TabPanel> <TabPanel value={value} index={2}> - <Contacts username={props.username} /> + <Contacts username={props.username} setValue={setValue} /> </TabPanel> </div> ); diff --git a/jams-react-client/src/views/Users/Users.js b/jams-react-client/src/views/Users/Users.js index 715c9867c2e8104a43cfa454dd220d7c6aefab56..52527d72043952d699565f936611bba5f0adc439 100644 --- a/jams-react-client/src/views/Users/Users.js +++ b/jams-react-client/src/views/Users/Users.js @@ -1,4 +1,5 @@ import React, { useState, useEffect, useCallback } from "react"; +import { Link } from 'react-router-dom' import { useHistory } from "react-router-dom"; // @material-ui/core components import { makeStyles } from "@material-ui/core/styles"; @@ -125,12 +126,7 @@ export default function Users(props) { clearInterval(timer); }; }, []); - const [selectedProfile, setSelectedProfile] = useState(false); - const redirectToUserProfile = (e, username) => { - e.preventDefault(); - setSelectedProfile(true); - setSelectedUsername(username); - }; + const searchUsers = (value, page = "1") => { setSelectedPage(page); setLoading(true); @@ -176,175 +172,148 @@ export default function Users(props) { const handleChangePage = (e, page) => { searchUsers(searchValue, page); }; - if (!auth.hasAdminScope() && auth.getUsername() !== "") { - return ( - <div> - <UserProfile username={auth.getUsername()} /> - </div> - ); - } else if (selectedProfile && auth.hasAdminScope() && selectedUsername !== "") { - return ( - <div> - <UserProfile username={selectedUsername} /> - </div> - ); - } else if (createUser) { - return ( - <EditCreateUserProfile - createUser={createUser} - setCreateUser={setCreateUser} - setError={props.setError} - setErrorMessage={props.setErrorMessage} - setSeverity={props.setSeverity} - /> - ); - } else { - return ( - <div> - <GridContainer> - <GridItem xs={12} sm={12} md={12}> - {auth.isLocalDirectory() && ( + return ( + <div> + <GridContainer> + <GridItem xs={12} sm={12} md={12}> + {auth.isLocalDirectory() && auth.hasAdminScope() && ( + <Link to={"/createuser"}> <Button variant="contained" color="primary" - href="#contained-buttons" - onClick={(e) => setCreateUser(true)} > <AddCircleOutlineIcon />{" "} {i18next.t("create_user", "Create user")} </Button> - )} - <GridContainer> - <GridItem xs={12} sm={12} md={6}> - {!noUsersFound && ( - <CustomInput - formControlProps={{ - className: classes.margin + " " + classes.search, - }} - inputProps={{ - placeholder: - i18next.t("search_users_using", "Search users using (username, first name, last name)"), - inputProps: { - "aria-label": i18next.t("search_users", "Search users"), - }, - onKeyUp: handleSearchUsers, - }} - /> - )} - {!noUsersFound && <Search />} - </GridItem> - <GridItem xs={12} sm={12} md={6}> - {!noUsersFound && ( - <Pagination - count={numberPages} - page={selectedPage} - onChange={handleChangePage} - /> - )} - </GridItem> - </GridContainer> - - <div className={classes.loading}> - {loading && ( - <LinearProgress variant="determinate" value={progress} /> + </Link> + )} + <GridContainer> + <GridItem xs={12} sm={12} md={6}> + {!noUsersFound && ( + <CustomInput + formControlProps={{ + className: classes.margin + " " + classes.search, + }} + inputProps={{ + placeholder: + i18next.t("search_users_using", "Search users using (username, first name, last name)"), + inputProps: { + "aria-label": i18next.t("search_users", "Search users"), + }, + onKeyUp: handleSearchUsers, + }} + /> )} - </div> - - {noUsersFound && ( - <div className={classes.usersNotFound}> - <InfoIcon /> + {!noUsersFound && <Search />} + </GridItem> + <GridItem xs={12} sm={12} md={6}> + {!noUsersFound && ( + <Pagination + count={numberPages} + page={selectedPage} + onChange={handleChangePage} + /> + )} + </GridItem> + </GridContainer> - <p style={{ marginLeft: "10px" }}>{i18next.t("no_users_found", "No users found")}</p> - </div> + <div className={classes.loading}> + {loading && ( + <LinearProgress variant="determinate" value={progress} /> )} - </GridItem> - {(!noUsersFound || !noMatchFound) && - 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={6} - md={3} - lg={2} - xl={2} - - key={user.username} - > - <Card profile> - <a - href="#" - onClick={(e) => redirectToUserProfile(e, user.username)} - > - <CardBody profile> - <CardAvatar profile> - <img - src={ - user.profilePicture - ? "data:image/png;base64, " + - user.profilePicture - : noProfilePicture - } - alt="..." - /> - </CardAvatar> - - <GridContainer - container - direction="column" - justify="flex-end" - alignItems="flex-start" - > - <GridItem style={{ minHeight:"50px"}}> - <h4 className={classes.cardTitle}> - {`${user.firstName} ${user.lastName}`} - </h4> - </GridItem> - <GridItem> - <ul className={classes.cardBodyContent}> - <li> - <img - src={jami} - width="20px" - alt="Jami" - style={{ minWidth:"20px", marginRight: "10px" }} - />{" "} - {user.username} - </li> - <li> - {user.organization && <BusinessOutlinedIcon - fontSize="small" - style={{ marginRight: "10px" }} - />}{" "} - {user.organization} - </li> - </ul> - </GridItem> - </GridContainer> - </CardBody> - </a> - </Card> - </GridItem> - ))} - {noMatchFound && ( + </div> + + {noUsersFound && ( <div className={classes.usersNotFound}> <InfoIcon /> - <p style={{ marginLeft: "10px" }}> - {i18next.t("no_users_found_matching", "No users found matching search value!")} - </p> + <p style={{ marginLeft: "10px" }}>{i18next.t("no_users_found", "No users found")}</p> </div> )} - </GridContainer> - </div> - ); - } + </GridItem> + {(!noUsersFound || !noMatchFound) && + 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={6} + md={3} + lg={2} + xl={2} + + key={user.username} + > + <Card profile > + <Link to={`/user/${user.username}`}> + <CardBody profile> + <CardAvatar profile> + <img + src={ + user.profilePicture + ? "data:image/png;base64, " + + user.profilePicture + : noProfilePicture + } + alt="..." + /> + </CardAvatar> + + <GridContainer + container + direction="column" + justify="flex-end" + alignItems="flex-start" + > + <GridItem style={{ minHeight:"50px"}}> + <h4 className={classes.cardTitle}> + {`${user.firstName} ${user.lastName}`} + </h4> + </GridItem> + <GridItem> + <ul className={classes.cardBodyContent}> + <li> + <img + src={jami} + width="20px" + alt="Jami" + style={{ minWidth:"20px", marginRight: "10px" }} + />{" "} + {user.username} + </li> + <li> + {user.organization && <BusinessOutlinedIcon + fontSize="small" + style={{ marginRight: "10px" }} + />}{" "} + {user.organization} + </li> + </ul> + </GridItem> + </GridContainer> + </CardBody> + </Link> + </Card> + </GridItem> + ))} + {noMatchFound && ( + <div className={classes.usersNotFound}> + <InfoIcon /> + + <p style={{ marginLeft: "10px" }}> + {i18next.t("no_users_found_matching", "No users found matching search value!")} + </p> + </div> + )} + </GridContainer> + </div> + ); } diff --git a/jams-server/src/main/java/net/jami/jams/server/core/TomcatLauncher.java b/jams-server/src/main/java/net/jami/jams/server/core/TomcatLauncher.java index b91bbb1e879ddce760f58944d11f62ab36992442..8c848ca6633b2ce8449b63578631850753ddd62d 100644 --- a/jams-server/src/main/java/net/jami/jams/server/core/TomcatLauncher.java +++ b/jams-server/src/main/java/net/jami/jams/server/core/TomcatLauncher.java @@ -55,22 +55,23 @@ public class TomcatLauncher { this.startServer(); } - //Swap connectors fix. - public void swapConnectors(){ - if(getTomcat().getConnector().findSslHostConfigs() != null && getTomcat().getConnector().findSslHostConfigs().length > 0){ - getTomcat().getConnector().findSslHostConfigs()[0].setTruststoreFile(System.getProperty("user.dir") + File.separator + "keystore.jks"); + // Swap connectors fix. + public void swapConnectors() { + if (getTomcat().getConnector().findSslHostConfigs() != null + && getTomcat().getConnector().findSslHostConfigs().length > 0) { + getTomcat().getConnector().findSslHostConfigs()[0] + .setTruststoreFile(System.getProperty("user.dir") + File.separator + "keystore.jks"); getTomcat().getConnector().findSslHostConfigs()[0].setTruststorePassword("changeit"); getTomcat().getConnector().findSslHostConfigs()[0].setCertificateVerification("optional"); ((Http11NioProtocol) getTomcat().getConnector().getProtocolHandler()).reloadSslHostConfigs(); - } - else{ + } else { log.error("Could not reload SSL configuration because the server is not running over SSL!"); } } public TomcatLauncher(int port, String certificateFile, String keyFile) { - if (!Files.exists(Paths.get(System.getProperty("user.dir") + File.separator + certificateFile)) || - !Files.exists(Paths.get(System.getProperty("user.dir") + File.separator + keyFile))) { + if (!Files.exists(Paths.get(System.getProperty("user.dir") + File.separator + certificateFile)) + || !Files.exists(Paths.get(System.getProperty("user.dir") + File.separator + keyFile))) { log.info("Could not find certificate or keyfile, starting in plain HTTP connector as fallback!"); tomcat.getService().addConnector(TomcatConnectorFactory.getNoSSLConnector(port)); this.startServer(); @@ -78,9 +79,11 @@ public class TomcatLauncher { } if (Files.exists(Paths.get(System.getProperty("user.dir") + File.separator + "keystore.jks"))) { log.info("Found a valid trust store, injecting into tomcat!"); - tomcat.getService().addConnector(TomcatConnectorFactory.getSSLConnectorWithTrustStore(certificateFile, keyFile, port)); + tomcat.getService() + .addConnector(TomcatConnectorFactory.getSSLConnectorWithTrustStore(certificateFile, keyFile, port)); } else { - Connector connector = TomcatConnectorFactory.getSSLConnectorWithoutTrustStore(certificateFile, keyFile, port); + Connector connector = TomcatConnectorFactory.getSSLConnectorWithoutTrustStore(certificateFile, keyFile, + port); tomcat.getService().addConnector(connector); } this.startServer(); @@ -94,7 +97,8 @@ public class TomcatLauncher { log.info("Serving application from: " + new File(System.getProperty("user.dir")).getAbsolutePath()); WebResourceRoot resources = new StandardRoot(context); if (jarName.contains(".jar")) { - resources.addPreResources(new JarResourceSet(resources, "/WEB-INF/classes", jarName, "/net/jami/jams/server/servlets")); + resources.addPreResources( + new JarResourceSet(resources, "/WEB-INF/classes", jarName, "/net/jami/jams/server/servlets")); resources.addPreResources(new JarResourceSet(resources, "/", jarName, "/webapp")); } else { log.info("WARNING: You are running from your local filesystem, this makes sense only for developers!"); @@ -104,16 +108,18 @@ public class TomcatLauncher { basePath.append("/").append(paths[i]); } basePath.append("/jams-server"); - resources.addPreResources(new DirResourceSet(resources, "/WEB-INF/classes", basePath.toString() + "/target/classes/net/jami/jams/server/servlets", "/")); - resources.addPreResources(new DirResourceSet(resources, "/", basePath.toString() + "/target/classes", "/webapp")); + resources.addPreResources(new DirResourceSet(resources, "/WEB-INF/classes", + basePath.toString() + "/target/classes/net/jami/jams/server/servlets", "/")); + resources.addPreResources( + new DirResourceSet(resources, "/", basePath.toString() + "/target/classes", "/webapp")); } context.setResources(resources); - //We always go to login by default. + // We always go to login by default. context.addWelcomeFile("index"); - //Register error pages. + // Register error pages. ErrorPage notFound = new ErrorPage(); notFound.setErrorCode(404); - notFound.setLocation("/index.html"); + notFound.setLocation("/"); ErrorPage genericError = new ErrorPage(); genericError.setErrorCode(500); genericError.setLocation("/500"); diff --git a/jams-server/src/main/java/net/jami/jams/server/servlets/general/IndexServlet.java b/jams-server/src/main/java/net/jami/jams/server/servlets/general/IndexServlet.java deleted file mode 100644 index 6dd355b46c470f67d081985a19dcb76c58daf366..0000000000000000000000000000000000000000 --- a/jams-server/src/main/java/net/jami/jams/server/servlets/general/IndexServlet.java +++ /dev/null @@ -1,41 +0,0 @@ -/* -* Copyright (C) 2020 by Savoir-faire Linux -* Authors: William Enright <william.enright@savoirfairelinux.com> -* Ndeye Anna Ndiaye <anna.ndiaye@savoirfairelinux.com> -* Johnny Flores <johnny.flores@savoirfairelinux.com> -* Mohammed Raza <mohammed.raza@savoirfairelinux.com> -* Felix Sidokhine <felix.sidokhine@savoirfairelinux.com> -* -* -* This program 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. -* -* This program 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 this program. If not, see <https://www.gnu.org/licenses/>. -*/ -package net.jami.jams.server.servlets.general; - -import jakarta.servlet.ServletException; -import jakarta.servlet.annotation.WebServlet; -import jakarta.servlet.http.HttpServlet; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; - -import java.io.IOException; - -@WebServlet("/index") -public class IndexServlet extends HttpServlet { - - //This is to by-pass the path issue. - @Override - protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { - resp.sendRedirect("index.html"); - } -}