diff --git a/src/App.js b/src/App.js index 4eaf5114ae5462673c6f232501444993714787cf..47adf6b0e408d234fdb88d0b259acc58ecf7ac7f 100644 --- a/src/App.js +++ b/src/App.js @@ -27,6 +27,7 @@ import { RecordListView } from "./views/records/RecordListView"; import { RecordDetailsView } from "./views/records/RecordDetailsView"; import { GlobalAppBarContext } from "@ess-ics/ce-ui-common"; import { applicationTitle } from "./components/common/Helper"; +import { UserPageView } from "./views/UserPage/UserPageView"; // setting up the application (TAB)title function App() { @@ -112,6 +113,11 @@ function App() { element={<LoginView />} exact /> + <Route + path="/user/:userName" + element={<UserPageView />} + exact + /> <Route path="*" element={<NotFound />} diff --git a/src/api/SwaggerApi.js b/src/api/SwaggerApi.js index cf1c3b10640bc82749b894fb02974e2a8f58d5f7..6cfee3e15dea9372d5705114f2900e0eb2059d3a 100644 --- a/src/api/SwaggerApi.js +++ b/src/api/SwaggerApi.js @@ -563,8 +563,10 @@ export function useUser() { console.warn("useUser error: ", message); }, []); - const [userInfo, getUserInfo, resetUserInfo, userInfoLoading] = - useUserInfo(onError); + const [userInfo, getUserInfo, resetUserInfo, userInfoLoading] = useUserInfo( + null, + onError + ); const [userRoles, getUserRoles, resetUserRoles, userRolesLoading] = useUserRoles(onError); const [user, setUser] = useState(); @@ -598,10 +600,13 @@ export function useUser() { return [user, getUser, resetUser, loading]; } -export function useUserInfo(onError) { +export function useUserInfo(userName, onError) { const api = useContext(apiContext); const method = useCallAndUnpack(api.apis.Git.infoFromUserName, unpackUser); - return useAsync({ fcn: method, call: false, onError: onError }); + const boundMethod = useCallback(method.bind(null, { user_name: userName }), [ + userName + ]); + return useAsync({ fcn: boundMethod, call: false, onError: onError }); } export function unpackUserRoles(roles) { diff --git a/src/components/common/User/UserIOCList.js b/src/components/common/User/UserIOCList.js new file mode 100644 index 0000000000000000000000000000000000000000..3c67ddb2da784b6cfae4687548c6708e14abef1e --- /dev/null +++ b/src/components/common/User/UserIOCList.js @@ -0,0 +1,52 @@ +import React from "react"; +import { Card, CardHeader } from "@mui/material"; +import { useState, useEffect } from "react"; +import { useIOCSearch } from "../../../api/SwaggerApi"; +import { IOCAsyncList } from "../../IOC/IOCAsyncList"; +import { initRequestParams } from "../Helper"; + +export function UserIocList({ userName }) { + const [iocs, getIocs, , loading] = useIOCSearch(); + const [iocList, setIocList] = useState([]); + const rowsPerPage = [20, 50]; + + const [lazyParams, setLazyParams] = useState({ + first: 0, + rows: 20, + page: 0 + }); + + useEffect(() => { + setIocList(iocs.iocList); + }, [iocs, setIocList]); + + useEffect(() => { + let requestParams = initRequestParams(lazyParams); + + requestParams.owner = userName; + + getIocs(requestParams); + }, [getIocs, lazyParams, userName]); + + return ( + <Card> + <CardHeader + title={`IOCs registered by ${userName}`} + titleTypographyProps={{ + variant: "h2", + component: "h2" + }} + /> + <IOCAsyncList + iocs={iocList} + setIocs={setIocList} + loading={loading} + rowType="explore" + lazyParams={lazyParams} + setLazyParams={setLazyParams} + totalCount={iocs.totalCount} + rowsPerPage={rowsPerPage} + /> + </Card> + ); +} diff --git a/src/components/common/User/UserOperationList.js b/src/components/common/User/UserOperationList.js new file mode 100644 index 0000000000000000000000000000000000000000..d98be2cb5df1b0fc946b28a32e5f0d5113499da1 --- /dev/null +++ b/src/components/common/User/UserOperationList.js @@ -0,0 +1,53 @@ +import React from "react"; +import { Card, CardHeader } from "@mui/material"; +import { initRequestParams } from "../Helper"; +import { useEffect, useState } from "react"; +import { useOperationsSearch } from "../../../api/SwaggerApi"; +import { JobAsyncList } from "../../Job/JobAsyncList"; + +export function UserOperationList({ userName }) { + const [operations, getOperations, , loading] = useOperationsSearch(); + + const [lazyParams, setLazyParams] = useState({ + first: 0, + rows: 20, + page: 0 + }); + + const rowsPerPage = [20, 50]; + + const [jobList, setJobList] = useState([]); + + useEffect(() => { + let requestParams = initRequestParams(lazyParams); + + requestParams.user = userName; + + getOperations(requestParams); + }, [getOperations, lazyParams, userName]); + + useEffect(() => { + setJobList(operations.operationsList); + }, [operations, setJobList]); + + return ( + <Card> + <CardHeader + title={`Operations by ${userName}`} + titleTypographyProps={{ + variant: "h2", + component: "h2" + }} + /> + <JobAsyncList + jobs={jobList} + setJobs={setJobList} + loading={loading} + totalCount={operations.totalCount} + lazyParams={lazyParams} + setLazyParams={setLazyParams} + rowsPerPage={rowsPerPage} + /> + </Card> + ); +} diff --git a/src/components/common/User/UserProfile.js b/src/components/common/User/UserProfile.js new file mode 100644 index 0000000000000000000000000000000000000000..25b0610fc12859fb934d05de9686648d0f6ec132 --- /dev/null +++ b/src/components/common/User/UserProfile.js @@ -0,0 +1,35 @@ +import React from "react"; +import { Avatar, Card, CardHeader, Typography } from "@mui/material"; +import { KeyValueTable } from "@ess-ics/ce-ui-common/dist/components/common/KeyValueTable"; + +export function UserProfile({ userInfo, errorMessage }) { + const userData = { + username: userInfo?.loginName, + avatar: ( + <Avatar + src={userInfo?.avatar} + variant="rounded" + /> + ) + }; + + return ( + <Card> + <CardHeader + title="User profile page" + titleTypographyProps={{ + variant: "h2", + component: "h2" + }} + /> + {errorMessage ? ( + <Typography>{errorMessage}</Typography> + ) : ( + <KeyValueTable + obj={userData} + variant="overline" + /> + )} + </Card> + ); +} diff --git a/src/components/common/User/index.js b/src/components/common/User/index.js new file mode 100644 index 0000000000000000000000000000000000000000..44989ea76755d9b0c09beb68eba9323e55729505 --- /dev/null +++ b/src/components/common/User/index.js @@ -0,0 +1,5 @@ +import { UserIocList } from "./UserIOCList"; +import { UserOperationList } from "./UserOperationList"; +import { UserProfile } from "./UserProfile"; + +export { UserIocList, UserOperationList, UserProfile }; diff --git a/src/components/navigation/NavigationMenu/LoginControls.js b/src/components/navigation/NavigationMenu/LoginControls.js index cfdd5dbcad03735f06efe2196898a70cab6148e5..f5d8dc72914a71de0e948786ca1b8fff93b20861 100644 --- a/src/components/navigation/NavigationMenu/LoginControls.js +++ b/src/components/navigation/NavigationMenu/LoginControls.js @@ -8,6 +8,7 @@ import React, { import { string } from "prop-types"; import { Button, Avatar, Chip } from "@mui/material"; import LockOpenIcon from "@mui/icons-material/LockOpen"; +import PersonIcon from "@mui/icons-material/Person"; import { userContext, LoginDialog, @@ -21,6 +22,7 @@ const propTypes = { export function ProfileMenu({ user }) { const { userRoles, logout } = useContext(userContext); + const navigate = useNavigate(); const objUserRoles = userRoles ?.filter((role) => role.includes("DeploymentTool")) @@ -33,6 +35,10 @@ export function ProfileMenu({ user }) { ) })); + function userProfile() { + navigate(`user/${user.loginName}`); + } + const profileMenuProps = { /* If editing this id, be sure to edit the check for this in @@ -52,6 +58,11 @@ export function ProfileMenu({ user }) { ), menuItems: [ ...objUserRoles, + { + text: "User profile page", + action: () => userProfile(), + icon: <PersonIcon fontSize="small" /> + }, { text: "Logout", action: () => logout(), diff --git a/src/views/UserPage/UserPageView.js b/src/views/UserPage/UserPageView.js new file mode 100644 index 0000000000000000000000000000000000000000..e0f8b0938b101bfc6169c86f0bd3ff9a5c259d77 --- /dev/null +++ b/src/views/UserPage/UserPageView.js @@ -0,0 +1,113 @@ +import React, { useContext, useState } from "react"; +import { Grid } from "@mui/material"; +import { styled } from "@mui/material/styles"; +import { RootContainer } from "../../components/common/Container/RootContainer"; +import { useParams } from "react-router-dom"; +import { useUserInfo } from "../../api/SwaggerApi"; +import { userContext } from "@ess-ics/ce-ui-common/dist/contexts/User"; +import { UserProfile } from "../../components/common/User/UserProfile"; +import { useEffect } from "react"; +import { UserIocList } from "../../components/common/User/UserIOCList"; +import { UserOperationList } from "../../components/common/User/UserOperationList"; + +const PREFIX = "UserPage"; + +const classes = { + paper: `${PREFIX}-paper` +}; + +const StyledRootContainer = styled(RootContainer)(({ theme }) => ({ + [`& .${classes.paper}`]: { + maxWidth: "80%", + padding: theme.spacing(4) + } +})); + +export function UserPageView() { + const { userName } = useParams(); + const { user } = useContext(userContext); + const [errorMessage, setErrorMessage] = useState(); + + const [userInfo, getUserInfo, ,] = useUserInfo(userName, (m, s) => { + // log error message for userInfo + console.warn(m); + // user not found + if (s === 404) { + setErrorMessage("User not found!"); + // do not show snackbar error + return false; + } + + // user doesn't have permission to fetch userInfo + if (s === 401) { + setErrorMessage("No activity"); + // do not show snackbar error + return false; + } + // show any other errors with snackbar + }); + + useEffect(() => { + // user logs in => clear error message, and try to re-request userInfo + if (user) { + setErrorMessage(null); + } else { + // user is not logged in => show a message + setErrorMessage("No activity!"); + } + }, [user]); + + useEffect(() => { + // request userInfo only if user is logged in + if (user) { + getUserInfo(); + } + }, [getUserInfo, userName, user]); + + // content for userActivity, e.g. operations that the user done, or IOCs that owns + const userActivityContent = ( + <> + <Grid + item + xs={12} + > + <UserOperationList userName={userName} /> + </Grid> + + <Grid + item + xs={12} + > + <UserIocList userName={userName} /> + </Grid> + </> + ); + + // show userPage for everyone, but hide userActivity for non-logged in users, and for not existing user + const content = ( + <> + <Grid + item + xs={12} + > + <UserProfile + userInfo={userInfo} + errorMessage={errorMessage} + /> + </Grid> + + {user && !errorMessage && userActivityContent} + </> + ); + + return ( + <StyledRootContainer> + <Grid + container + spacing={1} + > + {content} + </Grid> + </StyledRootContainer> + ); +} diff --git a/src/views/UserPage/index.js b/src/views/UserPage/index.js new file mode 100644 index 0000000000000000000000000000000000000000..232a503f885e084f47d64b422b4d761f9d8497f5 --- /dev/null +++ b/src/views/UserPage/index.js @@ -0,0 +1,4 @@ +import { UserPageView } from "./UserPageView"; + +export { UserPageView }; +export default UserPageView;