diff --git a/cypress/e2e/users-and-user-groups.cy.ts b/cypress/e2e/users-and-user-groups.cy.ts
index 5d646534d..91eabe5cb 100644
--- a/cypress/e2e/users-and-user-groups.cy.ts
+++ b/cypress/e2e/users-and-user-groups.cy.ts
@@ -120,4 +120,12 @@ describe('Users and User Groups page', () => {
cy.get('[data-ouia-component-id="iam-users-table-add-user-button"]').click();
cy.get('[data-ouia-component-id="add-user-group-modal"]').should('be.visible');
});
+
+ it('can view user details when a user is clicked', () => {
+ cy.get('[data-ouia-component-id="iam-users-table-table-tr-0"]').click();
+ cy.get('[data-ouia-component-id="user-details-drawer"]').should('be.visible');
+ cy.get('[data-ouia-component-id="user-details-drawer"]').contains(mockUsers.data[0].first_name).should('exist');
+ cy.get('[data-ouia-component-id="user-details-drawer"]').contains(mockUsers.data[0].last_name).should('exist');
+ cy.get('[data-ouia-component-id="user-details-drawer"]').contains(mockUsers.data[0].email).should('exist');
+ });
});
diff --git a/src/Messages.js b/src/Messages.js
index c23c9f5b3..229171b79 100644
--- a/src/Messages.js
+++ b/src/Messages.js
@@ -947,6 +947,11 @@ export default defineMessages({
description: 'Overview Hero third list item',
defaultMessage: `Assign users to these groups, allowing them to inherit the permissions associated with their group's roles`,
},
+ workspace: {
+ id: 'workspace',
+ description: 'Workspace singular label',
+ defaultMessage: 'Workspace',
+ },
workspaces: {
id: 'workspaces',
description: 'Workspaces heading',
@@ -2173,6 +2178,17 @@ export default defineMessages({
defaultMessage:
'Select a user group to add {numUsers} {plural} to. These are all the user groups in your account. To manage user groups, go to user groups.',
},
+ assignedRoles: {
+ id: 'assignedRoles',
+ description: 'User details assigned roles label',
+ defaultMessage: 'Assigned roles',
+ },
+ assignedRolesDescription: {
+ id: 'assignedRolesDescription',
+ description: 'User details roles info popover description',
+ defaultMessage:
+ 'User groups are granted roles that contain a set of permissions. Roles are limited to the workspace in which they were assigned.',
+ },
assignedUserGroupsTooltipHeader: {
id: 'assignedUserGroupsTooltipHeader',
description: 'header for assigned user groups tooltip',
diff --git a/src/redux/reducers/user-reducer.ts b/src/redux/reducers/user-reducer.ts
index df99aa0ff..c75cc84ff 100644
--- a/src/redux/reducers/user-reducer.ts
+++ b/src/redux/reducers/user-reducer.ts
@@ -1,6 +1,5 @@
import { FETCH_USERS, UPDATE_USERS_FILTERS } from '../action-types';
import { defaultSettings, PaginationDefaultI } from '../../helpers/shared/pagination';
-import { UserProps } from '../../smart-components/user/user-table-helpers';
export interface User {
email: string;
@@ -25,7 +24,7 @@ export interface UserStore {
meta: PaginationDefaultI;
filters: UserFilters;
pagination: PaginationDefaultI & { redirected?: boolean };
- data?: UserProps[];
+ data?: User[];
};
}
diff --git a/src/smart-components/access-management/UserDetailsDrawer.tsx b/src/smart-components/access-management/UserDetailsDrawer.tsx
new file mode 100644
index 000000000..419a84136
--- /dev/null
+++ b/src/smart-components/access-management/UserDetailsDrawer.tsx
@@ -0,0 +1,118 @@
+import {
+ Drawer,
+ DrawerActions,
+ DrawerCloseButton,
+ DrawerContent,
+ DrawerContentBody,
+ DrawerHead,
+ DrawerPanelContent,
+ Icon,
+ Popover,
+ Tab,
+ TabTitleText,
+ Tabs,
+ Text,
+ TextContent,
+ Title,
+} from '@patternfly/react-core';
+import React, { useEffect } from 'react';
+import { User } from '../../redux/reducers/user-reducer';
+import { OutlinedQuestionCircleIcon } from '@patternfly/react-icons';
+import { useIntl } from 'react-intl';
+import messages from '../../Messages';
+import UserDetailsGroupsView from './UserDetailsGroupsView';
+import UserDetailsRolesView from './UserDetailsRolesView';
+import { EventTypes, useDataViewEventsContext } from '@patternfly/react-data-view';
+
+interface UserDetailsProps {
+ focusedUser?: User;
+ drawerRef: React.RefObject;
+ onClose: () => void;
+ ouiaId: string;
+}
+
+const UserDetailsDrawerContent: React.FunctionComponent = ({ focusedUser, drawerRef, onClose, ouiaId }) => {
+ const [activeTabKey, setActiveTabKey] = React.useState(0);
+ const intl = useIntl();
+
+ return (
+
+
+
+ {`${focusedUser?.first_name} ${focusedUser?.last_name}`}
+
+
+ {focusedUser?.email}
+
+
+
+
+
+ setActiveTabKey(tabIndex)}>
+
+ {focusedUser && }
+
+
+ {intl.formatMessage(messages.assignedRoles)}
+
+
+
+
+
+
+ }
+ >
+ {focusedUser && }
+
+
+
+ );
+};
+
+interface DetailDrawerProps {
+ focusedUser?: User;
+ setFocusedUser: (user: User | undefined) => void;
+ children: React.ReactNode;
+ ouiaId: string;
+}
+
+const UserDetailsDrawer: React.FunctionComponent = ({ focusedUser, setFocusedUser, children, ouiaId }) => {
+ const drawerRef = React.useRef(null);
+ const context = useDataViewEventsContext();
+
+ useEffect(() => {
+ const unsubscribe = context.subscribe(EventTypes.rowClick, (user: User | undefined) => {
+ setFocusedUser(user);
+ drawerRef.current?.focus();
+ });
+
+ return () => unsubscribe();
+ }, [drawerRef]);
+
+ return (
+
+ setFocusedUser(undefined)}
+ />
+ }
+ >
+ {children}
+
+
+ );
+};
+
+export default UserDetailsDrawer;
diff --git a/src/smart-components/access-management/UserDetailsGroupsView.tsx b/src/smart-components/access-management/UserDetailsGroupsView.tsx
new file mode 100644
index 000000000..5051ad2e6
--- /dev/null
+++ b/src/smart-components/access-management/UserDetailsGroupsView.tsx
@@ -0,0 +1,43 @@
+import { DataView, DataViewTable } from '@patternfly/react-data-view';
+import React, { useCallback, useEffect } from 'react';
+import { useIntl } from 'react-intl';
+import { useDispatch, useSelector } from 'react-redux';
+import { mappedProps } from '../../helpers/shared/helpers';
+import { fetchGroups } from '../../redux/actions/group-actions';
+import { RBACStore } from '../../redux/store';
+import messages from '../../Messages';
+
+interface UserGroupsViewProps {
+ userId: string;
+ ouiaId: string;
+}
+
+const UserDetailsGroupsView: React.FunctionComponent = ({ userId, ouiaId }) => {
+ const dispatch = useDispatch();
+ const intl = useIntl();
+ const columns: string[] = [intl.formatMessage(messages.userGroup), intl.formatMessage(messages.users)];
+
+ const groups = useSelector((state: RBACStore) => state.groupReducer?.groups?.data || []);
+
+ const fetchData = useCallback(() => {
+ dispatch(fetchGroups({ ...mappedProps({ username: userId }), usesMetaInURL: true, system: false }));
+ }, [dispatch, userId]);
+
+ useEffect(() => {
+ fetchData();
+ }, [fetchData]);
+
+ const rows = groups.map((group: any) => ({
+ row: [group.name, group.principalCount || '?'], // TODO: update once API provides principalCount [RHCLOUD-35963]
+ }));
+
+ return (
+
+
+
+
+
+ );
+};
+
+export default UserDetailsGroupsView;
diff --git a/src/smart-components/access-management/UserDetailsRolesView.tsx b/src/smart-components/access-management/UserDetailsRolesView.tsx
new file mode 100644
index 000000000..6ca781cd4
--- /dev/null
+++ b/src/smart-components/access-management/UserDetailsRolesView.tsx
@@ -0,0 +1,47 @@
+import { DataView, DataViewTable } from '@patternfly/react-data-view';
+import React, { useCallback, useEffect } from 'react';
+import { useDispatch, useSelector } from 'react-redux';
+import { RBACStore } from '../../redux/store';
+import messages from '../../Messages';
+import { useIntl } from 'react-intl';
+import { fetchRoles } from '../../redux/actions/role-actions';
+import { mappedProps } from '../../helpers/shared/helpers';
+
+interface UserRolesViewProps {
+ userId: string;
+ ouiaId: string;
+}
+
+const UserDetailsRolesView: React.FunctionComponent = ({ userId, ouiaId }) => {
+ const dispatch = useDispatch();
+ const intl = useIntl();
+ const USER_ROLES_COLUMNS: string[] = [
+ intl.formatMessage(messages.roles),
+ intl.formatMessage(messages.userGroup),
+ intl.formatMessage(messages.workspace),
+ ];
+
+ const roles = useSelector((state: RBACStore) => state.roleReducer?.roles?.data || []);
+
+ const fetchData = useCallback(() => {
+ dispatch(fetchRoles({ ...mappedProps({ username: userId }), usesMetaInURL: true, system: false }));
+ }, [dispatch, userId]);
+
+ useEffect(() => {
+ fetchData();
+ }, [fetchData]);
+
+ const rows = roles.map((role: any) => ({
+ row: [role.name, role.display_name, '?'], // TODO: Update once API provides workspace data
+ }));
+
+ return (
+
+
+
+
+
+ );
+};
+
+export default UserDetailsRolesView;
diff --git a/src/smart-components/access-management/UsersTable.tsx b/src/smart-components/access-management/UsersTable.tsx
index 413ed74eb..6fd7c80ca 100644
--- a/src/smart-components/access-management/UsersTable.tsx
+++ b/src/smart-components/access-management/UsersTable.tsx
@@ -15,7 +15,7 @@ import { useIntl } from 'react-intl';
import messages from '../../Messages';
import { useSearchParams } from 'react-router-dom';
import { WarningModal } from '@patternfly/react-component-groups';
-import { UserProps } from '../user/user-table-helpers';
+import { EventTypes, useDataViewEventsContext } from '@patternfly/react-data-view';
const COLUMNS: string[] = ['Username', 'Email', 'First name', 'Last name', 'Status', 'Org admin'];
@@ -30,14 +30,16 @@ const PER_PAGE_OPTIONS = [
const OUIA_ID = 'iam-users-table';
interface UsersTableProps {
- onAddUserClick: (selected: any[]) => void;
+ onAddUserClick: (selected: User[]) => void;
+ focusedUser?: User;
}
-const UsersTable: React.FunctionComponent = ({ onAddUserClick }) => {
+const UsersTable: React.FunctionComponent = ({ onAddUserClick, focusedUser }) => {
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
const [currentUser, setCurrentUser] = useState();
const dispatch = useDispatch();
const intl = useIntl();
+ const { trigger } = useDataViewEventsContext();
const handleModalToggle = (_event: KeyboardEvent | React.MouseEvent, user: User) => {
setCurrentUser(user);
@@ -84,7 +86,11 @@ const UsersTable: React.FunctionComponent = ({ onAddUserClick }
};
const rows = useMemo(() => {
- return users.map((user: UserProps) => ({
+ const handleRowClick = (event: any, user: User | undefined) => {
+ (event.target.matches('td') || event.target.matches('tr')) && trigger(EventTypes.rowClick, user);
+ };
+
+ return users.map((user: User) => ({
id: user.username,
is_active: user.is_active,
row: [
@@ -113,8 +119,13 @@ const UsersTable: React.FunctionComponent = ({ onAddUserClick }
props: { isActionCell: true },
},
],
+ props: {
+ isClickable: Boolean(user.is_active),
+ onRowClick: (event: any) => user.is_active && handleRowClick(event, focusedUser?.username === user.username ? undefined : user),
+ isRowSelected: focusedUser?.username === user.username,
+ },
}));
- }, [users, intl, onAddUserClick, handleModalToggle]);
+ }, [users, intl, onAddUserClick, handleModalToggle, trigger, focusedUser?.username]);
const pageSelected = rows.length > 0 && rows.every(isSelected);
const pagePartiallySelected = !pageSelected && rows.some(isSelected);
diff --git a/src/smart-components/access-management/users-and-user-groups.tsx b/src/smart-components/access-management/users-and-user-groups.tsx
index 663cd978c..34b8ad8b1 100644
--- a/src/smart-components/access-management/users-and-user-groups.tsx
+++ b/src/smart-components/access-management/users-and-user-groups.tsx
@@ -7,6 +7,9 @@ import UsersTable from './UsersTable';
import UserGroupsTable from './UserGroupsTable';
import { useLocation, useNavigate } from 'react-router-dom';
import AddUserGroupModal from './AddUserGroupModal';
+import { User } from '../../redux/reducers/user-reducer';
+import UserDetailsDrawer from './UserDetailsDrawer';
+import { DataViewEventsProvider } from '@patternfly/react-data-view';
const TAB_NAMES = ['users', 'user-groups'];
@@ -14,8 +17,8 @@ const UsersAndUserGroups: React.FunctionComponent = () => {
const intl = useIntl();
const [activeTabKey, setActiveTabKey] = React.useState(0);
const [isAddUserGroupModalOpen, setIsAddUserGroupModalOpen] = React.useState(false);
- const [selectedUsers, setSelectedUsers] = React.useState([]);
-
+ const [selectedUsers, setSelectedUsers] = React.useState([]);
+ const [focusedUser, setFocusedUser] = React.useState(undefined);
const usersRef = React.createRef();
const groupsRef = React.createRef();
@@ -34,7 +37,7 @@ const UsersAndUserGroups: React.FunctionComponent = () => {
updateURL(TAB_NAMES[activeTab]);
};
- const handleOpenAddUserModal = (selected: any[]) => {
+ const handleOpenAddUserModal = (selected: User[]) => {
if (selected.length > 0) {
setSelectedUsers(selected);
setIsAddUserGroupModalOpen(true);
@@ -73,11 +76,15 @@ const UsersAndUserGroups: React.FunctionComponent = () => {
/>
-
+
{activeTabKey === 0 && (
-
-
-
+
+
+
+
+
+
+
)}
{activeTabKey === 1 && (