Skip to content

Commit

Permalink
Create User Groups list details drawer (#1692)
Browse files Browse the repository at this point in the history
* feat: create user details view

* feat: create group details drawer

* fix: use events context for user groups drawer
  • Loading branch information
CodyWMitchell authored Nov 8, 2024
1 parent 49a1b89 commit 0cd326e
Show file tree
Hide file tree
Showing 7 changed files with 307 additions and 27 deletions.
4 changes: 2 additions & 2 deletions src/helpers/group/group-helper.js
Original file line number Diff line number Diff line change
Expand Up @@ -124,8 +124,8 @@ export async function fetchRolesForGroup(groupId, excluded, { limit, offset, nam
description || undefined,
undefined,
undefined,
limit,
offset,
limit || undefined,
offset || undefined,
'display_name',
options
);
Expand Down
117 changes: 117 additions & 0 deletions src/smart-components/access-management/GroupDetailsDrawer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import {
Drawer,
DrawerActions,
DrawerCloseButton,
DrawerContent,
DrawerContentBody,
DrawerHead,
DrawerPanelContent,
Icon,
Popover,
Tab,
TabTitleText,
Tabs,
Title,
} from '@patternfly/react-core';
import React, { useEffect } from 'react';
import { OutlinedQuestionCircleIcon } from '@patternfly/react-icons';
import { useIntl } from 'react-intl';
import messages from '../../Messages';
import { Group } from '../../redux/reducers/group-reducer';
import GroupDetailsRolesView from './GroupDetailsRolesView';
import GroupDetailsServiceAccountsView from './GroupDetailsServiceAccountsView';
import GroupDetailsUsersView from './GroupDetailsUsersView';
import { EventTypes, useDataViewEventsContext } from '@patternfly/react-data-view';

interface GroupDetailsProps {
focusedGroup?: Group;
drawerRef: React.RefObject<HTMLDivElement>;
onClose: () => void;
ouiaId: string;
}

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

return (
<DrawerPanelContent>
<DrawerHead>
<Title headingLevel="h2">
<span tabIndex={focusedGroup ? 0 : -1} ref={drawerRef}>{`${focusedGroup?.name}`}</span>
</Title>
<DrawerActions>
<DrawerCloseButton onClick={onClose} />
</DrawerActions>
</DrawerHead>
<Tabs isFilled activeKey={activeTabKey} onSelect={(_, tabIndex) => setActiveTabKey(tabIndex)}>
<Tab eventKey={0} title={intl.formatMessage(messages.users)}>
{focusedGroup && <GroupDetailsUsersView groupId={focusedGroup.uuid} ouiaId={`${ouiaId}-users-view`} />}
</Tab>
<Tab eventKey={1} title={intl.formatMessage(messages.serviceAccounts)}>
{focusedGroup && <GroupDetailsServiceAccountsView groupId={focusedGroup.uuid} ouiaId={`${ouiaId}-service-accounts-view`} />}
</Tab>
<Tab
eventKey={2}
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>
}
>
{focusedGroup && <GroupDetailsRolesView groupId={focusedGroup.uuid} ouiaId={`${ouiaId}-assigned-roles-view`} />}
</Tab>
</Tabs>
</DrawerPanelContent>
);
};

interface DetailDrawerProps {
setFocusedGroup: (group: Group | undefined) => void;
focusedGroup?: Group;
children: React.ReactNode;
ouiaId: string;
}

const GroupDetailsDrawer: React.FunctionComponent<DetailDrawerProps> = ({ focusedGroup, setFocusedGroup, children, ouiaId }) => {
const drawerRef = React.useRef<HTMLDivElement>(null);
const context = useDataViewEventsContext();

useEffect(() => {
const unsubscribe = context.subscribe(EventTypes.rowClick, (group: Group | undefined) => {
setFocusedGroup(group);
drawerRef.current?.focus();
});

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

return (
<Drawer isExpanded={Boolean(focusedGroup)} onExpand={() => drawerRef.current?.focus()} data-ouia-component-id={ouiaId}>
<DrawerContent
panelContent={
<GroupDetailsDrawerContent
ouiaId={`${ouiaId}-panel-content`}
focusedGroup={focusedGroup}
drawerRef={drawerRef}
onClose={() => setFocusedGroup(undefined)}
/>
}
>
<DrawerContentBody hasPadding>{children}</DrawerContentBody>
</DrawerContent>
</Drawer>
);
};

export default GroupDetailsDrawer;
42 changes: 42 additions & 0 deletions src/smart-components/access-management/GroupDetailsRolesView.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
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 { fetchRolesForGroup } from '../../redux/actions/group-actions';

interface GroupRolesViewProps {
groupId: string;
ouiaId: string;
}

const GroupDetailsRolesView: React.FunctionComponent<GroupRolesViewProps> = ({ groupId, ouiaId }) => {
const dispatch = useDispatch();
const intl = useIntl();
const GROUP_ROLES_COLUMNS: string[] = [intl.formatMessage(messages.roles), intl.formatMessage(messages.workspace)];

const roles = useSelector((state: RBACStore) => state.groupReducer?.selectedGroup?.roles?.data || []);

const fetchData = useCallback(() => {
dispatch(fetchRolesForGroup(groupId, { limit: 1000 }));
}, [dispatch, groupId]);

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

const rows = roles.map((role: any) => ({
row: [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="GroupRolesView" ouiaId={`${ouiaId}-table`} columns={GROUP_ROLES_COLUMNS} rows={rows} />
</DataView>
</div>
);
};

export default GroupDetailsRolesView;
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
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 { fetchServiceAccountsForGroup } from '../../redux/actions/group-actions';

interface GroupDetailsServiceAccountsViewProps {
groupId: string;
ouiaId: string;
}

const GroupDetailsServiceAccountsView: React.FunctionComponent<GroupDetailsServiceAccountsViewProps> = ({ groupId, ouiaId }) => {
const dispatch = useDispatch();
const intl = useIntl();
const GROUP_SERVICE_ACCOUNTS_COLUMNS: string[] = [
intl.formatMessage(messages.name),
intl.formatMessage(messages.clientId),
intl.formatMessage(messages.owner),
];

const serviceAccounts = useSelector((state: RBACStore) => state.groupReducer?.selectedGroup?.serviceAccounts?.data || []);

const fetchData = useCallback(() => {
dispatch(fetchServiceAccountsForGroup(groupId, { limit: 1000 }));
}, [dispatch, groupId]);

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

const rows = serviceAccounts.map((serviceAccount: any) => ({
row: [serviceAccount.name, serviceAccount.clientId, serviceAccount.owner],
}));

return (
<div className="pf-v5-u-pt-md">
<DataView ouiaId={ouiaId}>
<DataViewTable
variant="compact"
aria-label="GroupServiceAccountsView"
ouiaId={`${ouiaId}-table`}
columns={GROUP_SERVICE_ACCOUNTS_COLUMNS}
rows={rows}
/>
</DataView>
</div>
);
};

export default GroupDetailsServiceAccountsView;
46 changes: 46 additions & 0 deletions src/smart-components/access-management/GroupDetailsUsersView.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
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 { fetchMembersForGroup } from '../../redux/actions/group-actions';

interface GroupDetailsUsersViewProps {
groupId: string;
ouiaId: string;
}

const GroupDetailsUsersView: React.FunctionComponent<GroupDetailsUsersViewProps> = ({ groupId, ouiaId }) => {
const dispatch = useDispatch();
const intl = useIntl();
const GROUP_USERS_COLUMNS: string[] = [
intl.formatMessage(messages.username),
intl.formatMessage(messages.firstName),
intl.formatMessage(messages.lastName),
];

const serviceAccounts = useSelector((state: RBACStore) => state.groupReducer?.selectedGroup?.members?.data || []);

const fetchData = useCallback(() => {
dispatch(fetchMembersForGroup(groupId, { limit: 1000 }));
}, [dispatch, groupId]);

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

const rows = serviceAccounts.map((user: any) => ({
row: [user.username, user.first_name, user.last_name], // TODO: Last name is not showing (fix this)
}));

return (
<div className="pf-v5-u-pt-md">
<DataView ouiaId={ouiaId}>
<DataViewTable variant="compact" aria-label="GroupDetailsUsersView" ouiaId={`${ouiaId}-table`} columns={GROUP_USERS_COLUMNS} rows={rows} />
</DataView>
</div>
);
};

export default GroupDetailsUsersView;
60 changes: 38 additions & 22 deletions src/smart-components/access-management/UserGroupsTable.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useEffect, useCallback } from 'react';
import React, { useEffect, useCallback, useMemo } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { useDataViewSelection, useDataViewPagination } from '@patternfly/react-data-view/dist/dynamic/Hooks';
import { BulkSelect, BulkSelectValue } from '@patternfly/react-component-groups/dist/dynamic/BulkSelect';
Expand All @@ -14,6 +14,8 @@ import { fetchGroups } from '../../redux/actions/group-actions';
import { formatDistanceToNow } from 'date-fns';
import { useIntl } from 'react-intl';
import messages from '../../Messages';
import { Group } from '../../redux/reducers/group-reducer';
import { EventTypes, useDataViewEventsContext } from '@patternfly/react-data-view';

const COLUMNS: string[] = ['User group name', 'Description', 'Users', 'Service accounts', 'Roles', 'Workspaces', 'Last modified'];

Expand All @@ -31,6 +33,7 @@ interface UserGroupsTableProps {
enableActions?: boolean;
ouiaId?: string;
onChange?: (selectedGroups: any[]) => void;
focusedGroup?: Group;
}

const UserGroupsTable: React.FunctionComponent<UserGroupsTableProps> = ({
Expand All @@ -39,9 +42,11 @@ const UserGroupsTable: React.FunctionComponent<UserGroupsTableProps> = ({
enableActions = true,
ouiaId = 'iam-user-groups-table',
onChange,
focusedGroup,
}) => {
const dispatch = useDispatch();
const intl = useIntl();
const { trigger } = useDataViewEventsContext();

const rowActions = [
{ title: intl.formatMessage(messages['usersAndUserGroupsEditUserGroup']), onClick: () => console.log('EDIT USER GROUP') },
Expand Down Expand Up @@ -108,28 +113,39 @@ const UserGroupsTable: React.FunctionComponent<UserGroupsTableProps> = ({
}
};

const rows = groups.map((group: any) => ({
id: group.uuid,
row: [
group.name,
group.description ? (
<Tooltip isContentLeftAligned content={group.description}>
<span>{group.description.length > 23 ? group.description.slice(0, 20) + '...' : group.description}</span>
</Tooltip>
) : (
<div className="pf-v5-u-color-400">{intl.formatMessage(messages['usersAndUserGroupsNoDescription'])}</div>
),
group.principalCount,
group.serviceAccounts || '?', // not currently in API
group.roleCount,
group.workspaces || '?', // not currently in API
formatDistanceToNow(new Date(group.modified), { addSuffix: true }),
enableActions && {
cell: <ActionsColumn items={rowActions} />,
props: { isActionCell: true },
const rows = useMemo(() => {
const handleRowClick = (event: any, group: Group | undefined) => {
(event.target.matches('td') || event.target.matches('tr')) && trigger(EventTypes.rowClick, group);
};

return groups.map((group: any) => ({
id: group.uuid,
row: [
group.name,
group.description ? (
<Tooltip isContentLeftAligned content={group.description}>
<span>{group.description.length > 23 ? group.description.slice(0, 20) + '...' : group.description}</span>
</Tooltip>
) : (
<div className="pf-v5-u-color-400">{intl.formatMessage(messages['usersAndUserGroupsNoDescription'])}</div>
),
group.principalCount,
group.serviceAccounts || '?', // not currently in API
group.roleCount,
group.workspaces || '?', // not currently in API
formatDistanceToNow(new Date(group.modified), { addSuffix: true }),
enableActions && {
cell: <ActionsColumn items={rowActions} />,
props: { isActionCell: true },
},
],
props: {
isClickable: true,
onRowClick: (event: any) => handleRowClick(event, focusedGroup?.uuid === group.uuid ? undefined : group),
isRowSelected: focusedGroup?.uuid === group.uuid,
},
],
}));
}));
}, [groups, focusedGroup]);

const pageSelected = rows.length > 0 && rows.every(isSelected);
const pagePartiallySelected = !pageSelected && rows.some(isSelected);
Expand Down
Loading

0 comments on commit 0cd326e

Please sign in to comment.