RedHatInsights/insights-rbac-ui

View on GitHub
src/smart-components/user/user.js

Summary

Maintainability
F
4 days
Test Coverage
import React, { useEffect, useState, useContext, Fragment, Suspense } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { Outlet, useNavigationType, useParams } from 'react-router-dom';
import { useIntl } from 'react-intl';
import { Button, Label, Stack, StackItem, Text, TextContent, TextVariants } from '@patternfly/react-core';
import { TableVariant, compoundExpand } from '@patternfly/react-table';
import { Table, TableHeader, TableBody } from '@patternfly/react-table/deprecated';
import { CheckIcon, CloseIcon } from '@patternfly/react-icons';
import { useChrome } from '@redhat-cloud-services/frontend-components/useChrome';
import SkeletonTable from '@patternfly/react-component-groups/dist/dynamic/SkeletonTable';
import debounce from 'lodash/debounce';
import Section from '@redhat-cloud-services/frontend-components/Section';
import DateFormat from '@redhat-cloud-services/frontend-components/DateFormat';
import Skeleton, { SkeletonSize } from '@redhat-cloud-services/frontend-components/Skeleton';
import useAppNavigate from '../../hooks/useAppNavigate';
import AppLink, { mergeToBasename } from '../../presentational-components/shared/AppLink';
import Breadcrumbs from '../../presentational-components/shared/breadcrumbs';
import EmptyWithAction from '../../presentational-components/shared/empty-state';
import PermissionsContext from '../../utilities/permissions-context';
import messages from '../../Messages';
import pathnames from '../../utilities/pathnames';
import { TableToolbarView } from '../../presentational-components/shared/table-toolbar-view';
import { TopToolbar, TopToolbarTitle } from '../../presentational-components/shared/top-toolbar';
import { fetchRoles, fetchRoleForUser } from '../../redux/actions/role-actions';
import { fetchUsers } from '../../redux/actions/user-actions';
import { BAD_UUID, getDateFormat } from '../../helpers/shared/helpers';
import { addRolesToGroup, fetchAdminGroup } from '../../redux/actions/group-actions';
import { defaultSettings } from '../../helpers/shared/pagination';
import './user.scss';

let debouncedFetch;

const User = () => {
  const intl = useIntl();
  const navigate = useAppNavigate();
  const navigationType = useNavigationType();
  const dispatch = useDispatch();
  const { username } = useParams();
  const [filter, setFilter] = useState('');
  const [expanded, setExpanded] = useState({});
  const [loadingRolesTemp, setLoadingRolesTemp] = useState(false);
  const [selectedAddRoles, setSelectedAddRoles] = useState([]);
  const chrome = useChrome();

  const selector = ({
    roleReducer: { error, roles, isLoading: isLoadingRoles, rolesWithAccess },
    userReducer: {
      users: { data },
      isUserDataLoading: isLoadingUsers,
    },
    groupReducer: { adminGroup },
  }) => ({
    adminGroup,
    roles,
    isLoadingRoles,
    rolesWithAccess,
    user: data && data.filter((user) => user.username === username)[0],
    isLoadingUsers,
    userExists: error !== BAD_UUID,
  });

  const { roles, isLoadingRoles, rolesWithAccess, user, isLoadingUsers, userExists, adminGroup } = useSelector(selector);
  const { orgAdmin, userAccessAdministrator } = useContext(PermissionsContext);
  const isAdmin = orgAdmin || userAccessAdministrator;

  const fetchRolesData = (apiProps) => dispatch(fetchRoles(apiProps));

  useEffect(() => {
    chrome.appObjectId(username);
    dispatch(fetchAdminGroup({ chrome }));
    dispatch(fetchUsers({ ...defaultSettings, limit: 0, filters: { username } }));
    fetchRolesData({ limit: 20, offset: 0, username });
    setLoadingRolesTemp(true);
    fetchRolesData({ limit: 20, offset: 0, addFields: ['groups_in'], username }).then(() => setLoadingRolesTemp(false));
    debouncedFetch = debounce(
      (limit, offset, name, addFields, username) => fetchRolesData({ limit, offset, displayName: name, addFields, username }),
      500
    );
    return () => chrome.appObjectId(undefined);
  }, []);

  const columns = [
    {
      title: intl.formatMessage(messages.roles),
    },
    {
      title: intl.formatMessage(messages.groups),
      cellTransforms: [compoundExpand],
    },
    {
      title: intl.formatMessage(messages.permissions),
      cellTransforms: [compoundExpand],
    },
    {
      title: intl.formatMessage(messages.lastModified),
    },
  ];

  const nestedPermissionsCells = [
    intl.formatMessage(messages.application),
    intl.formatMessage(messages.resourceType),
    intl.formatMessage(messages.operation),
  ];

  const createRows = (data, username, adminGroup) =>
    data
      ? data.reduce(
          (acc, { uuid, display_name, groups_in = [], modified, accessCount }, i) => [
            ...acc,
            {
              uuid,
              cells: [
                { title: display_name, props: { component: 'th', isOpen: false } },
                { title: loadingRolesTemp ? <Skeleton size={SkeletonSize.xs} /> : groups_in.length, props: { isOpen: expanded[uuid] === 1 } },
                { title: accessCount, props: { isOpen: expanded[uuid] === 2 } },
                { title: <DateFormat type={getDateFormat(modified)} date={modified} /> },
              ],
            },
            {
              uuid: `${uuid}-groups`,
              parent: 3 * i,
              compoundParent: 1,
              cells: [
                {
                  props: { colSpan: 4, className: 'pf-m-no-padding' },
                  title:
                    groups_in?.length > 0 ? (
                      <Table
                        ouiaId="groups-in-role-nested-table"
                        aria-label="Simple Table"
                        variant={TableVariant.compact}
                        cells={[intl.formatMessage(messages.name), intl.formatMessage(messages.description), ' ']}
                        rows={groups_in.map((group) => ({
                          cells: [
                            { title: <AppLink to={pathnames['group-detail'].link.replace(':groupId', group.uuid)}>{group.name}</AppLink> },
                            group.description,
                            {
                              title:
                                adminGroup.uuid === group.uuid ? null : (
                                  <AppLink
                                    to={pathnames['user-add-group-roles'].link.replace(':username', username).replace(':groupId', group.uuid)}
                                    state={{ name: group.name }}
                                  >
                                    {intl.formatMessage(messages.addRoleToThisGroup)}
                                  </AppLink>
                                ),
                              props: { className: 'pf-v5-u-text-align-right' },
                            },
                          ],
                        }))}
                      >
                        <TableHeader />
                        <TableBody />
                      </Table>
                    ) : (
                      <Text className="pf-v5-u-mx-lg pf-v5-u-my-sm">
                        {loadingRolesTemp ? intl.formatMessage(messages.loading) : intl.formatMessage(messages.noGroups)}
                      </Text>
                    ),
                },
              ],
            },
            {
              uuid: `${uuid}-permissions`,
              parent: 3 * i,
              compoundParent: 2,
              cells: [
                {
                  props: { colSpan: 4, className: 'pf-m-no-padding' },
                  title:
                    rolesWithAccess && rolesWithAccess[uuid] ? (
                      rolesWithAccess[uuid].access?.length > 0 ? (
                        <Table
                          aria-label="Simple Table"
                          ouiaId="permissions-in-role-nested-table"
                          variant={TableVariant.compact}
                          cells={nestedPermissionsCells}
                          rows={rolesWithAccess[uuid].access.map((access) => ({ cells: access.permission.split(':') }))}
                        >
                          <TableHeader />
                          <TableBody />
                        </Table>
                      ) : (
                        <Text className="pf-v5-u-mx-lg pf-v5-u-my-sm">{intl.formatMessage(messages.noPermissions)}</Text>
                      )
                    ) : (
                      <SkeletonTable rows={accessCount} columns={nestedPermissionsCells} variant={TableVariant.compact} />
                    ),
                },
              ],
            },
          ],
          []
        )
      : [];

  const onExpand = (_event, _rowIndex, colIndex, isOpen, rowData) => {
    if (!isOpen) {
      setExpanded({ ...expanded, [rowData.uuid]: colIndex });
      // Permissions
      if (colIndex === 2) {
        dispatch(fetchRoleForUser(rowData.uuid));
      }
    } else {
      setExpanded({ ...expanded, [rowData.uuid]: -1 });
    }
  };

  const breadcrumbsList = [
    { title: intl.formatMessage(messages.users), to: mergeToBasename(pathnames.users.link) },
    { title: userExists ? username : intl.formatMessage(messages.invalidUser), isActive: true },
  ];

  const toolbarButtons = () => [
    ...(isAdmin
      ? [
          <AppLink to={pathnames['add-user-to-group'].link.replace(':username', username)} key="add-user-to-group" className="rbac-m-hide-on-sm">
            <Button ouiaId="add-user-to-group-button" variant="primary" aria-label="Add user to a group">
              {intl.formatMessage(messages.addUserToGroup)}
            </Button>
          </AppLink>,
          {
            label: intl.formatMessage(messages.addUserToGroup),
            onClick: () => {
              navigate(pathnames['add-user-to-group'].link.replace(':username', username));
            },
          },
        ]
      : []),
  ];

  return (
    <Fragment>
      {userExists ? (
        <Stack>
          <StackItem>
            <TopToolbar breadcrumbs={breadcrumbsList}>
              <TopToolbarTitle
                title={username}
                renderTitleTag={() =>
                  isLoadingUsers ? (
                    <Skeleton size="xs" className="rbac-c-user__label-skeleton"></Skeleton>
                  ) : (
                    <Label color={user?.is_active && 'green'}>{intl.formatMessage(user?.is_active ? messages.active : messages.inactive)}</Label>
                  )
                }
              >
                {!isLoadingUsers && user ? (
                  <Fragment>
                    <TextContent>
                      {`${intl.formatMessage(messages.orgAdministrator)}: `}
                      {user?.is_org_admin ? (
                        <CheckIcon key="yes-icon" className="pf-v5-u-mx-sm" />
                      ) : (
                        <CloseIcon key="no-icon" className="pf-v5-u-mx-sm" />
                      )}
                      {intl.formatMessage(user?.is_org_admin ? messages.yes : messages.no)}
                    </TextContent>
                    {user?.email && <Text component={TextVariants.p}>{`${intl.formatMessage(messages.email)}: ${user.email}`}</Text>}
                    {user?.username && (
                      <TextContent>
                        <Text component={TextVariants.p}>{`${intl.formatMessage(messages.username)}: ${user.username}`}</Text>
                      </TextContent>
                    )}
                  </Fragment>
                ) : null}
              </TopToolbarTitle>
            </TopToolbar>
          </StackItem>
          <StackItem>
            <Section type="content" className="rbac-c-user-roles">
              <TableToolbarView
                columns={columns}
                isExpandable
                onExpand={onExpand}
                rows={createRows(roles.data, username, adminGroup)}
                data={roles.data}
                filterValue={filter}
                ouiaId="user-details-table"
                fetchData={({ limit, offset, name }) => {
                  debouncedFetch(limit, offset, name, ['groups_in'], username);
                }}
                setFilterValue={({ name }) => setFilter(name)}
                isLoading={isLoadingRoles}
                toolbarButtons={toolbarButtons}
                pagination={roles.meta}
                filterPlaceholder={intl.formatMessage(messages.roleName).toLowerCase()}
                titlePlural={intl.formatMessage(messages.roles).toLowerCase()}
                titleSingular={intl.formatMessage(messages.role).toLowerCase()}
                tableId="user"
              />
              <Suspense>
                <Outlet
                  context={{
                    // add user to group:
                    username,
                    // add group roles:
                    selectedRoles: selectedAddRoles,
                    setSelectedRoles: setSelectedAddRoles,
                    closeUrl: pathnames['user-detail'].link.replace(':username', username),
                    addRolesToGroup: (groupId, roles) => dispatch(addRolesToGroup(groupId, roles)),
                  }}
                />
              </Suspense>
            </Section>
          </StackItem>
        </Stack>
      ) : (
        <Fragment>
          <section className="pf-v5-c-page__main-breadcrumb pf-v5-u-pb-md">
            <Breadcrumbs {...breadcrumbsList} />
          </section>
          <EmptyWithAction
            title={intl.formatMessage(messages.userNotFound)}
            description={[intl.formatMessage(messages.userNotFoundDescription, { username })]}
            actions={[
              <Button
                key="back-button"
                className="pf-v5-u-mt-xl"
                ouiaId="back-button"
                variant="primary"
                aria-label="Back to previous page"
                onClick={() => navigate(navigationType !== 'POP' ? -1 : pathnames.users.link)}
              >
                {intl.formatMessage(messages.backToPreviousPage)}
              </Button>,
            ]}
          />
        </Fragment>
      )}
    </Fragment>
  );
};

export default User;