RedHatInsights/insights-rbac-ui

View on GitHub
src/smart-components/group/add-group/users-list-itless.js

Summary

Maintainability
F
1 wk
Test Coverage
import React, { useEffect, Fragment, useState, useContext, useRef, useCallback, Suspense } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import truncate from 'lodash/truncate';
import { useIntl } from 'react-intl';
import PropTypes from 'prop-types';
import { useLocation, useNavigate, Outlet } from 'react-router-dom';
import { TableToolbarView } from '../../../presentational-components/shared/table-toolbar-view';
import AppLink, { mergeToBasename } from '../../../presentational-components/shared/AppLink';
import { fetchUsers, updateUsersFilters, updateUsers, updateUserIsOrgAdminStatus } from '../../../redux/actions/user-actions';
import { Button, Switch as PF4Switch, Label, Modal, ModalVariant, List, ListItem, Checkbox, Stack, StackItem } from '@patternfly/react-core';
import { Dropdown, DropdownItem, DropdownToggle } from '@patternfly/react-core/deprecated';
import { sortable, nowrap } from '@patternfly/react-table';
import { CheckIcon, CloseIcon } from '@patternfly/react-icons';
import { mappedProps, isExternalIdp } from '../../../helpers/shared/helpers';
import UsersRow from '../../../presentational-components/shared/UsersRow';
import {
  defaultSettings,
  defaultAdminSettings,
  syncDefaultPaginationWithUrl,
  applyPaginationToUrl,
  isPaginationPresentInUrl,
} from '../../../helpers/shared/pagination';
import { syncDefaultFiltersWithUrl, applyFiltersToUrl, areFiltersPresentInUrl } from '../../../helpers/shared/filters';
import messages from '../../../Messages';
import PermissionsContext from '../../../utilities/permissions-context';
import { useScreenSize, isSmallScreen } from '@redhat-cloud-services/frontend-components/useScreenSize';
import paths from '../../../utilities/pathnames';
import useChrome from '@redhat-cloud-services/frontend-components/useChrome';

const IsAdminCellTextContent = ({ isOrgAdmin }) => {
  const intl = useIntl();

  return isOrgAdmin ? (
    <Fragment>
      <CheckIcon key="yes-icon" className="pf-v5-u-mr-sm" />
      <span key="yes">{intl.formatMessage(messages.yes)}</span>
    </Fragment>
  ) : (
    <Fragment>
      <CloseIcon key="no-icon" className="pf-v5-u-mr-sm" />
      <span key="no">{intl.formatMessage(messages.no)}</span>
    </Fragment>
  );
};

IsAdminCellTextContent.propTypes = {
  isOrgAdmin: PropTypes.bool,
};

const IsAdminCellDropdownContent = ({ isOrgAdmin, userId, isDisabled, toggleUserIsOrgAdminStatus }) => {
  const [isAdminDropdownOpen, setIsAdminDropdownOpen] = useState(false);
  const intl = useIntl();

  const onIsAdminDropdownToggle = (isOpen) => {
    setIsAdminDropdownOpen(isOpen);
  };

  const onIsAdminDropdownSelect = (_event) => {
    const isAdminStatusMap = { yes: true, no: false };

    toggleUserIsOrgAdminStatus(isAdminStatusMap[_event?.target?.id], null, { userId });
    setIsAdminDropdownOpen(false);
  };

  const dropdownItems = [
    <DropdownItem key={`is-admin-dropdown-item-${userId}`} componentID="yes">
      {intl.formatMessage(messages.yes)}
    </DropdownItem>,
    <DropdownItem key={`is-not-admin-dropdown-item-${userId}`} componentID="no">
      {intl.formatMessage(messages.no)}
    </DropdownItem>,
  ];
  return (
    <Dropdown
      id={`is-admin-dropdown-${userId}`}
      key={`is-admin-dropdown-${userId}`}
      onSelect={onIsAdminDropdownSelect}
      toggle={
        <DropdownToggle
          id={`is-admin-dropdown-toggle-${userId}`}
          key={`is-admin-dropdown-toggle-${userId}`}
          isDisabled={isDisabled}
          onToggle={onIsAdminDropdownToggle}
        >
          {isOrgAdmin ? intl.formatMessage(messages.yes) : intl.formatMessage(messages.no)}
        </DropdownToggle>
      }
      isOpen={isAdminDropdownOpen}
      dropdownItems={dropdownItems}
    />
  );
};

IsAdminCellDropdownContent.propTypes = {
  isOrgAdmin: PropTypes.bool,
  userId: PropTypes.string,
  isDisabled: PropTypes.bool,
  toggleUserIsOrgAdminStatus: PropTypes.func,
};

const UsersListItless = ({ selectedUsers, setSelectedUsers, userLinks, usesMetaInURL, displayNarrow, props }) => {
  const intl = useIntl();
  const navigate = useNavigate();
  const location = useLocation();
  const dispatch = useDispatch();
  const [selectedRows, setSelectedRows] = useState([]);
  const [isDeactivateConfirmationModalOpen, setIsDeactivateConfirmationModalOpen] = useState(false);
  const [isDeactivateConfirmationChecked, setIsDeactivateConfirmationChecked] = useState(false);
  const [isToolbarDropdownOpen, setIsToolbarDropdownOpen] = useState(false);
  const { orgAdmin } = useContext(PermissionsContext);
  const screenSize = useScreenSize();
  // use for text filter to focus
  const innerRef = useRef(null);
  const isAdmin = orgAdmin;
  const chrome = useChrome();
  const [currentUser, setCurrentUser] = useState({});
  const [userToken, setUserToken] = useState('');

  // for usesMetaInURL (Users page) store pagination settings in Redux, otherwise use results from meta
  let pagination = useSelector(({ userReducer: { users } }) => ({
    limit: (usesMetaInURL ? users.pagination.limit : users.meta.limit) ?? (orgAdmin ? defaultAdminSettings : defaultSettings).limit,
    offset: (usesMetaInURL ? users.pagination.offset : users.meta.offset) ?? (orgAdmin ? defaultAdminSettings : defaultSettings).offset,
    count: usesMetaInURL ? users.pagination.count : users.meta.count,
    redirected: usesMetaInURL && users.pagination.redirected,
  }));

  const { users, isLoading, stateFilters } = useSelector(
    ({
      userReducer: {
        users: { data, filters = {} },
        isUserDataLoading,
      },
    }) => ({
      users: data?.map?.((data) => ({ ...data, uuid: data.external_source_id })),
      isLoading: isUserDataLoading,
      stateFilters: location.search.length > 0 || Object.keys(filters).length > 0 ? filters : { status: ['Active'] },
    })
  );

  const fetchData = useCallback((apiProps) => dispatch(fetchUsers(apiProps)), [dispatch]);

  const confirmDeactivateUsers = () => {
    toggleUserActivationStatus(false, null, selectedRows);
    setIsDeactivateConfirmationModalOpen(false);
    setIsDeactivateConfirmationChecked(false);
  };

  const toggleUserIsOrgAdminStatus = (isOrgAdmin, _event, user = {}) => {
    const { limit, offset } = syncDefaultPaginationWithUrl(location, navigate, pagination);
    const newFilters = usesMetaInURL
      ? syncDefaultFiltersWithUrl(location, navigate, ['username', 'email', 'status'], filters)
      : { status: filters.status };
    const newUserObj = { id: user.userId, is_org_admin: isOrgAdmin };
    dispatch(updateUserIsOrgAdminStatus(newUserObj))
      .then(() => {
        setFilters(newFilters);
        if (setSelectedUsers) {
          setSelectedUsers([]);
        } else {
          setSelectedRows([]);
        }
        fetchData({ ...mappedProps({ limit, offset, filters: newFilters }), usesMetaInURL });
      })
      .catch((err) => {
        console.error(err);
      });
  };

  const toolbarDropdowns = () => {
    const onToggle = (isOpen) => {
      setIsToolbarDropdownOpen(isOpen);
    };
    const onToolbarDropdownSelect = async (_event) => {
      const userActivationStatusMap = { activate: true, deactivate: false };

      if (_event?.target?.id === 'deactivate') {
        setIsDeactivateConfirmationModalOpen(true);
      } else {
        toggleUserActivationStatus(userActivationStatusMap[_event?.target?.id], null, selectedRows);
      }
      setIsToolbarDropdownOpen(false);
    };
    const dropdownItems = [
      <DropdownItem key="activate-users-dropdown-item" componentID="activate">
        {intl.formatMessage(messages.activateUsersButton)}
      </DropdownItem>,
      <DropdownItem key="deactivate-users-dropdown-item" componentID="deactivate">
        {intl.formatMessage(messages.deactivateUsersButton)}
      </DropdownItem>,
    ];
    return (
      <Dropdown
        onSelect={onToolbarDropdownSelect}
        toggle={
          <DropdownToggle id="toolbar-dropdown-toggle" isDisabled={selectedRows.length === 0} onToggle={onToggle}>
            {intl.formatMessage(messages.activateUsersButton)}
          </DropdownToggle>
        }
        isOpen={isToolbarDropdownOpen}
        dropdownItems={dropdownItems}
      />
    );
  };
  const toolbarButtons = () => [
    <AppLink to={paths['invite-users'].link} key="invite-users" className="rbac-m-hide-on-sm">
      <Button ouiaId="invite-users-button" variant="primary" aria-label="Invite users">
        {intl.formatMessage(messages.inviteUsers)}
      </Button>
    </AppLink>,
    ...(isSmallScreen(screenSize)
      ? [
          {
            label: intl.formatMessage(messages.inviteUsers),
            onClick: () => {
              navigate(mergeToBasename(paths['invite-users'].link));
            },
          },
        ]
      : []),
  ];
  const toggleUserActivationStatus = (isActivated, _event, users = []) => {
    const { limit, offset } = syncDefaultPaginationWithUrl(location, navigate, pagination);
    const newFilters = usesMetaInURL
      ? syncDefaultFiltersWithUrl(location, navigate, ['username', 'email', 'status'], filters)
      : { status: filters.status };
    const newUserList = users.map((user) => {
      return { id: user?.uuid || user?.external_source_id, is_active: isActivated };
    });
    dispatch(updateUsers(newUserList))
      .then(() => {
        setFilters(newFilters);
        if (setSelectedUsers) {
          setSelectedUsers([]);
        } else {
          setSelectedRows([]);
        }
        fetchData({ ...mappedProps({ limit, offset, filters: newFilters }), usesMetaInURL });
      })
      .catch((err) => {
        console.error(err);
      });
  };

  useEffect(() => {
    chrome.auth.getUser().then((user) => setCurrentUser(user));
    chrome.auth.getToken().then((token) => setUserToken(token));
  }, []);

  const isUserSelectable = (external_source_id) => external_source_id != currentUser?.identity?.internal?.account_id;

  const createITLessRows = (userLinks, data, checkedRows = []) => {
    const maxLength = 25;
    return data
      ? data.reduce(
          (
            acc,
            { external_source_id, username, is_active: is_active, email, first_name: firstName, last_name: lastName, is_org_admin: isOrgAdmin }
          ) => [
            ...acc,
            {
              uuid: external_source_id,
              cells: [
                {
                  title:
                    isAdmin && !displayNarrow ? (
                      <IsAdminCellDropdownContent
                        isOrgAdmin={isOrgAdmin}
                        userId={external_source_id}
                        isDisabled={!isAdmin || currentUser?.identity?.internal?.account_id == external_source_id}
                        toggleUserIsOrgAdminStatus={toggleUserIsOrgAdminStatus}
                      />
                    ) : (
                      <IsAdminCellTextContent isOrgAdmin={isOrgAdmin} />
                    ),
                  props: {
                    'data-is-active': isOrgAdmin,
                  },
                },
                {
                  title: userLinks ? (
                    <AppLink to={paths['user-detail'].link.replace(':username', username)}>{username.toString()}</AppLink>
                  ) : displayNarrow ? (
                    <span title={username}>{truncate(username, { length: maxLength })}</span>
                  ) : (
                    username
                  ),
                },
                {
                  title: displayNarrow ? <span title={email}>{truncate(email, { length: maxLength })}</span> : email,
                },
                firstName,
                lastName,
                {
                  title:
                    isAdmin && !displayNarrow ? (
                      <PF4Switch
                        key="status"
                        isDisabled={!isAdmin || currentUser?.identity?.internal?.account_id == external_source_id}
                        label={intl.formatMessage(messages.active)}
                        labelOff={intl.formatMessage(messages.inactive)}
                        isChecked={is_active}
                        onChange={(checked, _event) => {
                          toggleUserActivationStatus(checked, _event, [
                            {
                              external_source_id,
                              is_active: is_active,
                            },
                          ]);
                        }}
                      />
                    ) : (
                      <Label key="status" color={is_active ? 'green' : 'grey'}>
                        {intl.formatMessage(is_active ? messages.active : messages.inactive)}
                      </Label>
                    ),
                  props: {
                    'data-is-active': is_active,
                  },
                },
              ],
              selected: Boolean(checkedRows?.find?.(({ uuid }) => uuid === external_source_id)),
              disableSelection: displayNarrow ? undefined : !isUserSelectable(external_source_id),
            },
          ],
          []
        )
      : [];
  };

  const updateStateFilters = useCallback((filters) => dispatch(updateUsersFilters(filters)), [dispatch]);
  const columns = [
    { title: intl.formatMessage(displayNarrow ? messages.orgAdmin : messages.orgAdministrator), key: 'org-admin', transforms: [nowrap] },
    { title: intl.formatMessage(messages.username), key: 'username', transforms: [sortable] },
    { title: intl.formatMessage(messages.email) },
    { title: intl.formatMessage(messages.firstName), transforms: [nowrap] },
    { title: intl.formatMessage(messages.lastName), transforms: [nowrap] },
    { title: intl.formatMessage(messages.status), transforms: [nowrap] },
  ];
  const [sortByState, setSortByState] = useState({ index: 1, direction: 'asc' });

  const [filters, setFilters] = useState(
    usesMetaInURL
      ? stateFilters
      : {
          username: '',
          email: '',
          status: [intl.formatMessage(messages.active)],
        }
  );

  useEffect(() => {
    usesMetaInURL && applyPaginationToUrl(location, navigate, pagination.limit, pagination.offset);
  }, [pagination.offset, pagination.limit, pagination.count, pagination.redirected]);

  useEffect(() => {
    const { limit, offset } = usesMetaInURL ? syncDefaultPaginationWithUrl(location, navigate, pagination) : pagination;
    const newFilters = usesMetaInURL
      ? syncDefaultFiltersWithUrl(location, navigate, ['username', 'email', 'status'], filters)
      : { status: filters.status };
    setFilters(newFilters);
    fetchData({ ...mappedProps({ limit, offset, filters: newFilters }), usesMetaInURL });
  }, []);

  useEffect(() => {
    if (usesMetaInURL) {
      isPaginationPresentInUrl(location) || applyPaginationToUrl(location, navigate, pagination.limit, pagination.offset);
      Object.values(filters).some((filter) => filter?.length > 0) &&
        !areFiltersPresentInUrl(location, Object.keys(filters)) &&
        syncDefaultFiltersWithUrl(location, navigate, Object.keys(filters), filters);
    }
  });

  const setCheckedItems = (newSelection) => {
    if (setSelectedUsers) {
      setSelectedUsers((users) => {
        return newSelection(users)
          .filter((user) => (displayNarrow ? user : user?.uuid != currentUser?.identity?.internal?.account_id))
          .map(({ uuid, username }) => ({ uuid, label: username || uuid }));
      });
    } else {
      setSelectedRows((users) => {
        return newSelection(users)
          .filter((user) => (displayNarrow ? user : user?.uuid != currentUser?.identity?.internal?.account_id))
          .map(({ uuid, username }) => ({ uuid, label: username || uuid }));
      });
    }
  };

  const updateFilters = (payload) => {
    usesMetaInURL && updateStateFilters(payload);
    setFilters({ username: '', ...payload });
  };
  return (
    <>
      <Modal
        title={intl.formatMessage(messages.deactivateUsersConfirmationModalTitle)}
        titleIconVariant="warning"
        description={intl.formatMessage(messages.deactivateUsersConfirmationModalDescription)}
        variant={ModalVariant.medium}
        isOpen={isDeactivateConfirmationModalOpen}
        footer={
          <Stack hasGutter>
            <StackItem>
              <Checkbox
                label={intl.formatMessage(messages.deactivateUsersConfirmationModalCheckboxText)}
                isChecked={isDeactivateConfirmationChecked}
                onChange={(checked) => {
                  setIsDeactivateConfirmationChecked(checked);
                }}
                id="deactivateUsersConfirmationCheckbox"
                name="deactivate-users-confirmation-checkbox"
              />
            </StackItem>
            <StackItem>
              <Button
                key="confirm-deactivate-users"
                ouiaId="danger-confirm-deactivate-users-button"
                isDisabled={selectedRows.length === 0 || !isDeactivateConfirmationChecked}
                variant="danger"
                onClick={() => {
                  confirmDeactivateUsers();
                }}
              >
                {intl.formatMessage(messages.deactivateUsersConfirmationButton)}
              </Button>
              <Button
                id="deactivate-users-confirmation-cancel"
                ouiaId="secondary-cancel-button"
                key="cancel"
                variant="link"
                onClick={() => {
                  setIsDeactivateConfirmationModalOpen(false);
                }}
              >
                {intl.formatMessage(messages.cancel)}
              </Button>
            </StackItem>
          </Stack>
        }
        onClose={() => {
          setIsDeactivateConfirmationModalOpen(false);
        }}
      >
        <List isPlain isBordered>
          {selectedRows.map((user) => (
            <ListItem key={user.uuid}>{user.label}</ListItem>
          ))}
        </List>
      </Modal>
      <TableToolbarView
        toolbarChildren={isAdmin && !displayNarrow ? toolbarDropdowns : () => null}
        toolbarButtons={isAdmin && !displayNarrow && !isExternalIdp(userToken) ? toolbarButtons : () => []}
        isCompact
        isSelectable
        borders={false}
        columns={columns}
        rows={createITLessRows(userLinks, users, selectedUsers ? selectedUsers : selectedRows)}
        sortBy={sortByState}
        onSort={(e, index, direction) => {
          const orderBy = `${direction === 'desc' ? '-' : ''}${columns[index - 1].key}`;
          setSortByState({ index, direction });
          fetchData({ ...pagination, filters, usesMetaInURL, orderBy });
        }}
        data={users}
        ouiaId="users-table"
        fetchData={(config) => {
          const status = Object.prototype.hasOwnProperty.call(config, 'status') ? config.status : filters.status;
          const { username, email, count, limit, offset, orderBy } = config;

          fetchData({ ...mappedProps({ count, limit, offset, orderBy, filters: { username, email, status } }), usesMetaInURL }).then(() => {
            innerRef?.current?.focus();
          });
          usesMetaInURL && applyFiltersToUrl(location, navigate, { username, email, status });
        }}
        emptyFilters={{ username: '', email: '', status: '' }}
        setFilterValue={({ username, email, status }) => {
          updateFilters({
            username: typeof username === 'undefined' ? filters.username : username,
            email: typeof email === 'undefined' ? filters.email : email,
            status: typeof status === 'undefined' || status === filters.status ? filters.status : status,
          });
        }}
        isLoading={isLoading}
        pagination={pagination}
        checkedRows={selectedUsers ? selectedUsers : selectedRows}
        setCheckedItems={setCheckedItems}
        rowWrapper={UsersRow}
        titlePlural={intl.formatMessage(messages.users).toLowerCase()}
        titleSingular={intl.formatMessage(messages.user)}
        filters={[
          {
            key: 'username',
            value: filters.username,
            placeholder: intl.formatMessage(messages.filterByKey, { key: intl.formatMessage(messages.username).toLowerCase() }),
            innerRef,
          },
          {
            key: 'email',
            value: filters.email,
            placeholder: intl.formatMessage(messages.filterByKey, { key: intl.formatMessage(messages.email).toLowerCase() }),
            innerRef,
          },
          {
            key: 'status',
            value: filters.status,
            label: intl.formatMessage(messages.status),
            type: 'checkbox',
            items: [
              { label: intl.formatMessage(messages.active), value: 'Active' },
              { label: intl.formatMessage(messages.inactive), value: 'Inactive' },
            ],
          },
        ]}
        tableId="users-list"
        {...props}
      />
      <Suspense>
        <Outlet
          context={{
            [paths['invite-users'].path]: {
              fetchData: () => fetchData({ ...pagination, filters, usesMetaInURL }),
            },
          }}
        />
      </Suspense>
    </>
  );
};

UsersListItless.propTypes = {
  displayNarrow: PropTypes.bool,
  users: PropTypes.array,
  searchFilter: PropTypes.string,
  setSelectedUsers: PropTypes.func,
  selectedUsers: PropTypes.array,
  userLinks: PropTypes.bool,
  props: PropTypes.object,
  usesMetaInURL: PropTypes.bool,
};

UsersListItless.defaultProps = {
  displayNarrow: false,
  users: [],
  userLinks: false,
  usesMetaInURL: false,
};

export default UsersListItless;