Skip to content

Commit

Permalink
feat: create user details view
Browse files Browse the repository at this point in the history
  • Loading branch information
CodyWMitchell committed Nov 5, 2024
1 parent 2de8e1d commit 53ccf7a
Show file tree
Hide file tree
Showing 8 changed files with 263 additions and 14 deletions.
8 changes: 8 additions & 0 deletions cypress/e2e/users-and-user-groups.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
});
});
16 changes: 16 additions & 0 deletions src/Messages.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -2173,6 +2178,17 @@ export default defineMessages({
defaultMessage:
'Select a user group to add <b>{numUsers} {plural}</b> 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',
Expand Down
3 changes: 1 addition & 2 deletions src/redux/reducers/user-reducer.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -25,7 +24,7 @@ export interface UserStore {
meta: PaginationDefaultI;
filters: UserFilters;
pagination: PaginationDefaultI & { redirected?: boolean };
data?: UserProps[];
data?: User[];
};
}

Expand Down
118 changes: 118 additions & 0 deletions src/smart-components/access-management/UserDetailsDrawer.tsx
Original file line number Diff line number Diff line change
@@ -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<HTMLDivElement>;
onClose: () => void;
ouiaId: string;
}

const UserDetailsDrawerContent: React.FunctionComponent<UserDetailsProps> = ({ focusedUser, drawerRef, onClose, ouiaId }) => {
const [activeTabKey, setActiveTabKey] = React.useState<string | number>(0);
const intl = useIntl();

return (
<DrawerPanelContent>
<DrawerHead>
<Title headingLevel="h2">
<span tabIndex={focusedUser ? 0 : -1} ref={drawerRef}>{`${focusedUser?.first_name} ${focusedUser?.last_name}`}</span>
</Title>
<TextContent>
<Text>{focusedUser?.email}</Text>
</TextContent>
<DrawerActions>
<DrawerCloseButton onClick={onClose} />
</DrawerActions>
</DrawerHead>
<Tabs isFilled activeKey={activeTabKey} onSelect={(_, tabIndex) => setActiveTabKey(tabIndex)}>
<Tab eventKey={0} title={intl.formatMessage(messages.userGroups)}>
{focusedUser && <UserDetailsGroupsView ouiaId={`${ouiaId}-user-groups-view`} userId={focusedUser.username} />}
</Tab>
<Tab
eventKey={1}
title={
<TabTitleText>
{intl.formatMessage(messages.assignedRoles)}
<Popover
triggerAction="hover"
position="top-end"
headerContent={intl.formatMessage(messages.assignedRoles)}
bodyContent={intl.formatMessage(messages.assignedRolesDescription)}
>
<Icon className="pf-v5-u-pl-sm" isInline>
<OutlinedQuestionCircleIcon />
</Icon>
</Popover>
</TabTitleText>
}
>
{focusedUser && <UserDetailsRolesView userId={focusedUser.username} ouiaId={`${ouiaId}-assigned-users-view`} />}
</Tab>
</Tabs>
</DrawerPanelContent>
);
};

interface DetailDrawerProps {
focusedUser?: User;
setFocusedUser: (user: User | undefined) => void;
children: React.ReactNode;
ouiaId: string;
}

const UserDetailsDrawer: React.FunctionComponent<DetailDrawerProps> = ({ focusedUser, setFocusedUser, children, ouiaId }) => {
const drawerRef = React.useRef<HTMLDivElement>(null);
const context = useDataViewEventsContext();

useEffect(() => {
const unsubscribe = context.subscribe(EventTypes.rowClick, (user: User | undefined) => {
setFocusedUser(user);
drawerRef.current?.focus();
});

return () => unsubscribe();
}, [drawerRef]);

return (
<Drawer isExpanded={Boolean(focusedUser)} data-ouia-component-id={ouiaId}>
<DrawerContent
panelContent={
<UserDetailsDrawerContent
ouiaId={`${ouiaId}-panel-content`}
drawerRef={drawerRef}
focusedUser={focusedUser}
onClose={() => setFocusedUser(undefined)}
/>
}
>
<DrawerContentBody hasPadding>{children}</DrawerContentBody>
</DrawerContent>
</Drawer>
);
};

export default UserDetailsDrawer;
43 changes: 43 additions & 0 deletions src/smart-components/access-management/UserDetailsGroupsView.tsx
Original file line number Diff line number Diff line change
@@ -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<UserGroupsViewProps> = ({ 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 (
<div className="pf-v5-u-pt-md">
<DataView ouiaId={ouiaId}>
<DataViewTable variant="compact" aria-label="UserGroupsView" ouiaId={`${ouiaId}-table`} columns={columns} rows={rows} />
</DataView>
</div>
);
};

export default UserDetailsGroupsView;
47 changes: 47 additions & 0 deletions src/smart-components/access-management/UserDetailsRolesView.tsx
Original file line number Diff line number Diff line change
@@ -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<UserRolesViewProps> = ({ 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 (
<div className="pf-v5-u-pt-md">
<DataView ouiaId={ouiaId}>
<DataViewTable variant="compact" aria-label="UserRolesView" ouiaId={`${ouiaId}-table`} columns={USER_ROLES_COLUMNS} rows={rows} />
</DataView>
</div>
);
};

export default UserDetailsRolesView;
21 changes: 16 additions & 5 deletions src/smart-components/access-management/UsersTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'];

Expand All @@ -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<UsersTableProps> = ({ onAddUserClick }) => {
const UsersTable: React.FunctionComponent<UsersTableProps> = ({ onAddUserClick, focusedUser }) => {
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
const [currentUser, setCurrentUser] = useState<User | undefined>();
const dispatch = useDispatch();
const intl = useIntl();
const { trigger } = useDataViewEventsContext();

const handleModalToggle = (_event: KeyboardEvent | React.MouseEvent, user: User) => {
setCurrentUser(user);
Expand Down Expand Up @@ -84,7 +86,11 @@ const UsersTable: React.FunctionComponent<UsersTableProps> = ({ 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: [
Expand Down Expand Up @@ -113,8 +119,13 @@ const UsersTable: React.FunctionComponent<UsersTableProps> = ({ 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);
Expand Down
21 changes: 14 additions & 7 deletions src/smart-components/access-management/users-and-user-groups.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,18 @@ 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'];

const UsersAndUserGroups: React.FunctionComponent = () => {
const intl = useIntl();
const [activeTabKey, setActiveTabKey] = React.useState<number>(0);
const [isAddUserGroupModalOpen, setIsAddUserGroupModalOpen] = React.useState<boolean>(false);
const [selectedUsers, setSelectedUsers] = React.useState<any[]>([]);

const [selectedUsers, setSelectedUsers] = React.useState<User[]>([]);
const [focusedUser, setFocusedUser] = React.useState<User | undefined>(undefined);
const usersRef = React.createRef<HTMLElement>();
const groupsRef = React.createRef<HTMLElement>();

Expand All @@ -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);
Expand Down Expand Up @@ -73,11 +76,15 @@ const UsersAndUserGroups: React.FunctionComponent = () => {
/>
</Tabs>
</PageSection>
<PageSection>
<PageSection padding={{ default: 'noPadding' }}>
{activeTabKey === 0 && (
<TabContent eventKey={0} id="usersTab" ref={usersRef} aria-label="Users tab">
<UsersTable onAddUserClick={handleOpenAddUserModal} />
</TabContent>
<DataViewEventsProvider>
<UserDetailsDrawer ouiaId="user-details-drawer" setFocusedUser={setFocusedUser} focusedUser={focusedUser}>
<TabContent eventKey={0} id="usersTab" ref={usersRef} aria-label="Users tab">
<UsersTable onAddUserClick={handleOpenAddUserModal} focusedUser={focusedUser} />
</TabContent>
</UserDetailsDrawer>
</DataViewEventsProvider>
)}
{activeTabKey === 1 && (
<TabContent eventKey={1} id="groupsTab" ref={groupsRef} aria-label="Groups tab">
Expand Down

0 comments on commit 53ccf7a

Please sign in to comment.