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 Oct 30, 2024
1 parent 0cbadd7 commit cc9aae7
Show file tree
Hide file tree
Showing 6 changed files with 233 additions and 13 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
187 changes: 187 additions & 0 deletions src/smart-components/access-management/UserDetailsDrawer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
import {
Drawer,
DrawerActions,
DrawerCloseButton,
DrawerContent,
DrawerContentBody,
DrawerHead,
DrawerPanelContent,
Icon,
Popover,
Tab,
TabTitleText,
Tabs,
Text,
TextContent,
Title,
} from '@patternfly/react-core';
import React, { useCallback, useEffect } from 'react';
import { User } from '../../redux/reducers/user-reducer';
import { OutlinedQuestionCircleIcon } from '@patternfly/react-icons';
import { DataView, DataViewTable } from '@patternfly/react-data-view';
import { useDispatch, useSelector } from 'react-redux';
import { RBACStore } from '../../redux/store';
import { fetchGroups } from '../../redux/actions/group-actions';
import { mappedProps } from '../../helpers/shared/helpers';
import { fetchRoles } from '../../redux/actions/role-actions';
import { useIntl } from 'react-intl';
import messages from '../../Messages';

interface UserGroupsViewProps {
userId: string;
ouiaId: string;
}

const UserGroupsView: 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) => ({
groups: state.groupReducer?.groups?.data || [],
}));

const fetchData = useCallback(
(apiProps: { username: string }) => {
const { username } = apiProps;
dispatch(fetchGroups({ ...mappedProps({ username }), usesMetaInURL: true, system: false }));
},
[dispatch]
);

useEffect(() => {
fetchData({
username: userId,
});
}, [fetchData, userId]);

const rows = groups.map((group: any) => ({
row: [group.name, group.principalCount || '?'], // TODO: principalCount is not available from API?
}));

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>
);
};

interface UserRolesViewProps {
userId: string;
ouiaId: string;
}

const UserRolesView: 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) => ({
roles: state.roleReducer?.roles?.data || [],
}));

const fetchData = useCallback(
(apiProps: { username: string }) => {
const { username } = apiProps;
dispatch(fetchRoles({ ...mappedProps({ username }), usesMetaInURL: true, system: false }));
},
[dispatch]
);

useEffect(() => {
fetchData({
username: userId,
});
}, [fetchData, userId]);

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>
);
};

interface UserDetailsProps {
focusedUser?: User;
onClose: () => void;
ouiaId: string;
}

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

return (
<DrawerPanelContent>
<DrawerHead>
<Title headingLevel="h2">{`${focusedUser?.first_name} ${focusedUser?.last_name}`}</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 && <UserGroupsView 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 && <UserRolesView userId={focusedUser.username} ouiaId={`${ouiaId}-assigned-users-view`} />}
</Tab>
</Tabs>
</DrawerPanelContent>
);
};

interface DetailDrawerProps {
isOpen: boolean;
focusedUser?: User;
onClose: () => void;
children: React.ReactNode;
ouiaId: string;
}

export const UserDetailsDrawer: React.FunctionComponent<DetailDrawerProps> = ({ isOpen, focusedUser, onClose, children, ouiaId }) => {
const drawerRef = React.useRef<HTMLDivElement>(null);

return (
<Drawer isExpanded={isOpen} onExpand={() => drawerRef.current?.focus()} data-ouia-component-id={ouiaId}>
<DrawerContent
panelContent={<UserDetailsDrawerContent ouiaId={`${ouiaId}-panel-content`} focusedUser={focusedUser} onClose={onClose} />}
ref={drawerRef}
>
<DrawerContentBody hasPadding>{children}</DrawerContentBody>
</DrawerContent>
</Drawer>
);
};
11 changes: 7 additions & 4 deletions src/smart-components/access-management/UsersTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ 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';

const COLUMNS: string[] = ['Username', 'Email', 'First name', 'Last name', 'Status', 'Org admin'];

Expand All @@ -30,10 +29,11 @@ const PER_PAGE_OPTIONS = [
const OUIA_ID = 'iam-users-table';

interface UsersTableProps {
onAddUserClick: (selected: any[]) => void;
onAddUserClick: (selected: User[]) => void;
onFocusUser?: (user: User) => void;
}

const UsersTable: React.FunctionComponent<UsersTableProps> = ({ onAddUserClick }) => {
const UsersTable: React.FunctionComponent<UsersTableProps> = ({ onAddUserClick, onFocusUser }) => {
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
const [currentUser, setCurrentUser] = useState<User | undefined>();
const dispatch = useDispatch();
Expand Down Expand Up @@ -84,7 +84,7 @@ const UsersTable: React.FunctionComponent<UsersTableProps> = ({ onAddUserClick }
};

const rows = useMemo(() => {
return users.map((user: UserProps) => ({
return users.map((user: User) => ({
id: user.username,
is_active: user.is_active,
row: [
Expand Down Expand Up @@ -113,6 +113,9 @@ const UsersTable: React.FunctionComponent<UsersTableProps> = ({ onAddUserClick }
props: { isActionCell: true },
},
],
props: {
onClick: () => onFocusUser && onFocusUser(user),
},
}));
}, [users, intl, onAddUserClick, handleModalToggle]);

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,17 @@ 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';

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 +36,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 +75,16 @@ 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>
<UserDetailsDrawer ouiaId="user-details-drawer" isOpen={!!focusedUser} focusedUser={focusedUser} onClose={() => setFocusedUser(undefined)}>
<TabContent eventKey={0} id="usersTab" ref={usersRef} aria-label="Users tab">
<UsersTable
onAddUserClick={handleOpenAddUserModal}
onFocusUser={(user) => (user?.is_active ? setFocusedUser(user) : setFocusedUser(undefined))}
/>
</TabContent>
</UserDetailsDrawer>
)}
{activeTabKey === 1 && (
<TabContent eventKey={1} id="groupsTab" ref={groupsRef} aria-label="Groups tab">
Expand Down

0 comments on commit cc9aae7

Please sign in to comment.