RedHatInsights/insights-rbac-ui

View on GitHub
src/smart-components/role/add-role/inventory-groups-role.js

Summary

Maintainability
F
3 days
Test Coverage
import React, { useEffect, useReducer } from 'react';
import {
  Button,
  Grid,
  GridItem,
  Text,
  TextVariants,
  FormGroup,
  Tooltip,
  Divider,
  MenuToggle,
  TextInputGroup,
  TextInputGroupMain,
  TextInputGroupUtilities,
  SelectList,
  SelectOption,
  Select,
  Spinner,
  Badge,
  Chip,
  ChipGroup,
} from '@patternfly/react-core';
import { TimesIcon } from '@patternfly/react-icons';
import { shallowEqual, useSelector, useDispatch } from 'react-redux';
import useFieldApi from '@data-driven-forms/react-form-renderer/use-field-api';
import useFormApi from '@data-driven-forms/react-form-renderer/use-form-api';
import { FormattedMessage, useIntl } from 'react-intl';
import { fetchInventoryGroups } from '../../../redux/actions/inventory-actions';
import { debouncedFetch } from '../../../helpers/shared/helpers';
import messages from '../../../Messages';
import './cost-resources.scss';
import { useFlag } from '@unleash/proxy-client-react';

const selector = ({ inventoryReducer: { resourceTypes, total, isLoading } }) => ({
  resourceTypes,
  totalCount: total,
  isLoading,
});

const reducer = (state, action) => {
  const prevState = state[action.key];
  switch (action.type) {
    case 'toggle':
      return {
        ...state,
        [action.key]: {
          ...prevState,
          isOpen: action.isOpen,
          filterValue: action.filterValue ?? prevState.filterValue,
          page: action.page ?? prevState.page,
        },
      };
    case 'select':
      if (prevState.selected.some((item) => item?.id === action.processedSelection?.id)) {
        return {
          ...state,
          [action.key]: {
            ...prevState,
            selected: prevState.selected.filter((item) => item.id !== action.processedSelection?.id),
          },
        };
      } else if (action.processedSelection?.id !== undefined && action.processedSelection?.name !== undefined) {
        return {
          ...state,
          [action.key]: {
            ...prevState,
            selected: [...prevState.selected, action.processedSelection],
          },
        };
      } else {
        return state;
      }
    case 'copyToAll': {
      const firstPermissionSelection = state[action.permissions[0]].selected;
      return {
        ...state,
        ...action.permissions.reduce((acc, permission) => {
          acc[permission] = {
            ...state[permission],
            selected: !permission.includes('inventory:hosts') ? firstPermissionSelection.filter(({ id }) => id !== null) : firstPermissionSelection,
          };
          return acc;
        }, {}),
      };
    }
    case 'selectAll':
      return {
        ...state,
        [action.key]: {
          ...prevState,
          selected: action.selectionArray,
        },
      };
    case 'setFilter':
      return {
        ...state,
        [action.key]: {
          ...prevState,
          filterValue: action.filterValue,
          page: 1,
        },
      };
    case 'setPage':
      return {
        ...state,
        [action.key]: {
          ...prevState,
          page: action.page,
        },
      };
    case 'clear':
      return {
        ...state,
        [action.key]: {
          ...prevState,
          selected: [],
        },
      };
    default:
      return state;
  }
};

const InventoryGroupsRole = (props) => {
  const intl = useIntl();
  const dispatch = useDispatch();
  const { input } = useFieldApi(props);
  const formOptions = useFormApi();
  const isHosts = (permissionID) => permissionID.includes('hosts:');
  const enableWorkspacesNameChange = useFlag('platform.rbac.groups-to-workspaces-rename');

  const { resourceTypes, totalCount, isLoading } = useSelector(selector, shallowEqual);
  const permissions =
    formOptions
      .getState()
      .values['add-permissions-table'].filter(({ uuid }) => uuid.split(':')[0].includes('inventory'))
      .map(({ uuid }) => uuid) || [];

  const fetchData = (permissions, apiProps) => dispatch(fetchInventoryGroups(permissions, apiProps));

  const onSelect = (_event, selection, selectAll, key) => {
    const ungroupedSystems = { id: null, name: 'null' };
    return (
      (selectAll &&
        dispatchLocally({
          type: 'selectAll',
          selectionArray: isHosts(key) ? [ungroupedSystems, ...Object.values(resourceTypes[key])] : Object.values(resourceTypes[key]),
          key,
        })) ||
      dispatchLocally({ type: 'select', processedSelection: selection === 'null' ? ungroupedSystems : resourceTypes[key][selection], key })
    );
  };
  const clearSelection = (key) => dispatchLocally({ type: 'clear', key });

  const [state, dispatchLocally] = useReducer(
    reducer,
    permissions.reduce(
      (acc, permission) => ({
        ...acc,
        [permission]: {
          page: 1,
          selected: [],
          filterValue: '',
          isOpen: false,
        },
      }),
      {}
    )
  );

  useEffect(() => {
    fetchData(permissions, {});
    formOptions.change('inventory-group-permissions', []);
  }, []);

  useEffect(() => {
    const groupsPermissionsDefinition = Object.entries(state).map(([permission, { selected }]) => ({ permission, groups: selected }));
    input.onChange(groupsPermissionsDefinition);
    formOptions.change('inventory-group-permissions', groupsPermissionsDefinition);
  }, [state]);

  const onToggleClick = (permissionID) => dispatchLocally({ type: 'toggle', key: permissionID, isOpen: !state[permissionID].isOpen });

  const onTextInputChange = (_event, value, permissionID) => {
    dispatchLocally({ type: 'setFilter', key: permissionID, filterValue: value });
    debouncedFetch(() => fetchData([permissionID], { name: value }), 2000);
  };

  const toggle = (toggleRef, permissionID) => (
    <Tooltip content={<div>{intl.formatMessage(enableWorkspacesNameChange ? messages.workspacesTooltip : messages.inventoryGroupsTooltip)}</div>}>
      <MenuToggle
        variant="typeahead"
        aria-label={intl.formatMessage(enableWorkspacesNameChange ? messages.workspacesTypeAheadLabel : messages.inventoryGroupsTypeAheadLabel)}
        onClick={() => onToggleClick(permissionID)}
        innerRef={toggleRef}
        isExpanded={state[permissionID].isOpen}
        isFullWidth
      >
        <TextInputGroup isPlain>
          <TextInputGroupMain
            value={state[permissionID].filterValue}
            onClick={() => state[permissionID].isOpen || onToggleClick(permissionID)}
            onChange={(e, value) => onTextInputChange(e, value, permissionID)}
            autoComplete="off"
            placeholder={intl.formatMessage(enableWorkspacesNameChange ? messages.selectWorkspaces : messages.selectGroups)}
            role="combobox"
            isExpanded={state[permissionID].isOpen}
          >
            {state[permissionID].selected.length > 0 ? (
              <ChipGroup aria-label="Current selections">
                <Chip
                  closeBtnAriaLabel="Clear all"
                  badge={<Badge isRead>{state[permissionID].selected.length}</Badge>}
                  onClick={(ev) => {
                    ev.stopPropagation();
                    clearSelection(permissionID);
                  }}
                >
                  selected
                </Chip>
              </ChipGroup>
            ) : null}
          </TextInputGroupMain>

          <TextInputGroupUtilities>
            {state[permissionID].filterValue.length > 0 && (
              <Button
                variant="plain"
                aria-label="Clear input value"
                onClick={(e) => {
                  e.stopPropagation();
                  e.preventDefault();
                  onTextInputChange(e, '', permissionID);
                }}
              >
                <TimesIcon aria-hidden />
              </Button>
            )}
          </TextInputGroupUtilities>
        </TextInputGroup>
      </MenuToggle>
    </Tooltip>
  );

  const makeRow = (permissionID, index) => {
    const options = Object.values(resourceTypes?.[permissionID] ?? {});

    return (
      <React.Fragment key={permissionID}>
        <Grid>
          <GridItem md={3}>
            <FormGroup label={permissionID?.replace('inventory:', '')} isRequired />
          </GridItem>
          <GridItem md={7}>
            <Select
              role="menu"
              aria-labelledby={permissionID}
              className="rbac-c-resource-type-select"
              isOpen={state[permissionID].isOpen}
              selected={state[permissionID].selected}
              onSelect={(event, selection) => onSelect(event, selection, selection === 'select-all', permissionID)}
              onOpenChange={(isOpen) => dispatchLocally({ type: 'toggle', key: permissionID, isOpen })}
              toggle={(toggleRef) => toggle(toggleRef, permissionID)}
            >
              <SelectList>
                {options?.length > 0 ? (
                  <SelectOption className="pf-v5-u-link-color" key={`${permissionID}-all`} value="select-all">
                    <FormattedMessage
                      {...messages.selectAll}
                      values={{
                        length: options?.length + Number(isHosts(permissionID)),
                      }}
                    />
                  </SelectOption>
                ) : null}
                {isHosts(permissionID) ? (
                  <>
                    <SelectOption
                      key={`${permissionID}-ungrouped`}
                      value="null"
                      hasCheckbox
                      isSelected={state[permissionID].selected.some((item) => item.name === 'null')}
                    >
                      <FormattedMessage {...messages.ungroupedSystems} />
                    </SelectOption>
                    {options.length > 0 ? <Divider component="li" key={`${permissionID}-divider`} /> : null}
                  </>
                ) : null}
                {options.map((option) => (
                  <SelectOption
                    hasCheckbox
                    key={option.id}
                    isSelected={state[permissionID].selected.some((item) => item.name === option.name)}
                    className={option.className}
                    value={option.name}
                  >
                    {option.name}
                  </SelectOption>
                ))}
                {isLoading || (resourceTypes[permissionID] && Object.values(resourceTypes[permissionID]).length < totalCount) ? (
                  <SelectOption
                    {...(!isLoading && { isLoadButton: true })}
                    {...(isLoading && { isLoading: true })}
                    onClick={() => {
                      fetchData([permissionID], { page: state[permissionID].page + 1, name: state[permissionID].filterValue });
                      dispatchLocally({ type: 'setPage', key: permissionID, page: state[permissionID].page + 1 });
                    }}
                    value="loader"
                  >
                    {isLoading ? <Spinner size="lg" /> : intl.formatMessage(messages.seeMore)}
                  </SelectOption>
                ) : null}
              </SelectList>
            </Select>
          </GridItem>
          <GridItem md={2}>
            {index <= 0 && permissions.length > 1 && (
              <Button key={`${permissionID}-copy`} variant="link" isInLink onClick={() => dispatchLocally({ type: 'copyToAll', permissions })}>
                {intl.formatMessage(messages.copyToAll)}
              </Button>
            )}
          </GridItem>
        </Grid>
      </React.Fragment>
    );
  };

  return (
    <Grid hasGutter>
      <GridItem md={3} className="rbac-m-hide-on-sm">
        <Text component={TextVariants.h4} className="rbac-bold-text pf-v5-u-mt-sm">
          {intl.formatMessage(messages.permissions)}
        </Text>
      </GridItem>
      <GridItem lg={9} md={6} className="rbac-m-hide-on-sm">
        <Text component={TextVariants.h4} className="rbac-bold-text pf-v5-u-mt-sm">
          {intl.formatMessage(enableWorkspacesNameChange ? messages.workspacesDefinition : messages.groupDefinition)}
        </Text>
      </GridItem>
      {permissions.map(makeRow)}
    </Grid>
  );
};

export default InventoryGroupsRole;