Katello/katello

View on GitHub
webpack/components/extensions/HostDetails/Cards/SystemPurposeCard/SystemPurposeEditModal.js

Summary

Maintainability
F
3 days
Test Coverage
import React, { useState, useEffect } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { isEqual } from 'lodash';
import PropTypes from 'prop-types';
import { STATUS } from 'foremanReact/constants';
import { propsToCamelCase } from 'foremanReact/common/helpers';
import {
  Modal,
  Button,
  Form,
  FormGroup,
  FormSelect,
  FormSelectOption,
  Select,
  SelectOption,
  SelectVariant,
} from '@patternfly/react-core';
import { FormattedMessage } from 'react-intl';
import { translate as __ } from 'foremanReact/common/I18n';
import {
  selectOrganizationStatus,
  selectOrganization,
  selectAvailableReleaseVersions,
  selectAvailableReleaseVersionsStatus,
} from './SystemPurposeSelectors';
import { getHostAvailableReleaseVersions, getAKAvailableReleaseVersions, getOrganization, updateHostSysPurposeAttributes, updateAKSysPurposeAttributes } from './SystemPurposeActions';
import HOST_DETAILS_KEY from '../../HostDetailsConstants';
import { defaultUsages, defaultRoles, defaultServiceLevels } from './SystemPurposeConstants';
import { getActivationKey } from '../../../../../scenes/ActivationKeys/Details/ActivationKeyActions';

const SystemPurposeEditModal = ({
  closeModal, name, purposeRole, purposeUsage, purposeAddons,
  serviceLevel, releaseVersion, isOpen, orgId, id, type,
}) => {
  const initialPurposeRole = purposeRole ?? '';
  const initialServiceLevel = serviceLevel ?? '';
  const initialPurposeUsage = purposeUsage ?? '';
  const initialReleaseVersion = releaseVersion ?? '';
  const initialPurposeAddons = purposeAddons ?? [];
  const [selectedRole, setSelectedRole] = useState(initialPurposeRole);
  const [selectedServiceLevel, setSelectedServiceLevel] = useState(initialServiceLevel);
  const [selectedUsage, setSelectedUsage] = useState(initialPurposeUsage);
  const [addonSelectOpen, setAddonSelectOpen] = useState(false);
  const [selectedAddons, setSelectedAddons] = useState(initialPurposeAddons);
  const [selectedReleaseVersion, setSelectedReleaseVersion] = useState(initialReleaseVersion);

  const unmodified = (
    selectedRole === initialPurposeRole &&
    selectedServiceLevel === initialServiceLevel &&
    selectedUsage === initialPurposeUsage &&
    selectedReleaseVersion === initialReleaseVersion &&
    isEqual(selectedAddons, initialPurposeAddons)
  );
  const dispatch = useDispatch();

  const orgStatus = useSelector(state => selectOrganizationStatus(state, orgId));
  const organizationDetails = useSelector(state => selectOrganization(state, orgId));
  const orgDetails = propsToCamelCase(organizationDetails ?? { systemPurposes: {} });
  const availableSyspurposeAttributes = orgDetails?.systemPurposes ?? {};
  const availableServiceLevels = orgDetails?.serviceLevels ?? [];
  const { addons: availableAddons, roles: availableRoles, usage: availableUsages }
    = availableSyspurposeAttributes;
  const selectorAPIKey = type === 'host' ? 'AVAILABLE_RELEASE_VERSIONS' : 'RELEASES';

  const availableReleaseVersionsStatus
    = useSelector(state => selectAvailableReleaseVersionsStatus(state, id, selectorAPIKey));
  const availableReleaseVersions = useSelector(state =>
    selectAvailableReleaseVersions(state, id, selectorAPIKey))?.results ?? [];
  useEffect(() => {
    if (orgId && orgStatus !== STATUS.RESOLVED) {
      dispatch(getOrganization({ orgId }));
    }
  }, [orgId, orgStatus, dispatch]);

  const actionToDispatch = type === 'host' ? getHostAvailableReleaseVersions : getAKAvailableReleaseVersions;
  useEffect(() => {
    if (id && availableReleaseVersionsStatus !== STATUS.RESOLVED) {
      dispatch(actionToDispatch({ id }));
    }
  }, [type, id, availableReleaseVersionsStatus, actionToDispatch, dispatch]);

  const toggleAddonSelect = isOpenState => setAddonSelectOpen(isOpenState);

  const onAddonSelect = (_event, selected) => {
    const newSelectedAddons = new Set(selectedAddons);
    if (!selected) return;
    if (newSelectedAddons.has(selected)) {
      newSelectedAddons.delete(selected);
    } else {
      newSelectedAddons.add(selected);
    }
    setSelectedAddons([...newSelectedAddons]);
  };

  const refreshHostDetails = () => dispatch({
    type: 'API_GET',
    payload: {
      key: HOST_DETAILS_KEY,
      url: `/api/hosts/${name}`,
    },
  });

  // Building the dropdown options is a bit complex because they come from several sources:
  // 1. The hard-coded set of default values (defaultOptions)
  // 2. The set of available values from the API (additionalOptions)
  // 3. The value actually set on the host (initialOption - this need not be a value from 1 or 2)
  // We then need to combine these values into a single set of options, and ensure that
  // (a) (unset) appears first;
  // (b) there are no duplicate options;
  // (c) that the currently selected option always appears (currentSelected); and
  // (d) that the order of the items doesn't change unexpectedly.
  const buildOptions = (defaultOptions, additionalOptions, currentSelected, initialOption) => {
    const optionToObject = option => ({ label: option || __('(unset)'), value: option });
    const uniqOptions = new Set(['', ...defaultOptions ?? [], ...additionalOptions ?? [], currentSelected, initialOption]);
    uniqOptions.delete(null);
    uniqOptions.delete(undefined);
    return [...[...uniqOptions]?.map(optionToObject)];
  };

  const roleOptions =
    buildOptions(defaultRoles, availableRoles, selectedRole, purposeRole);
  const usageOptions =
    buildOptions(defaultUsages, availableUsages, selectedUsage, purposeUsage);

  // addons may be present on the host but not available from subscriptions,
  // so we combine the options here
  const addonToObject = addon => ({ label: addon, value: addon });
  const addonsOptions =
    [...new Set([ // don't repeat addons if they are already selected
      ...purposeAddons.map(addonToObject), ...availableAddons?.map(addonToObject) ?? [],
    ])];

  const serviceLevelOptions =
    buildOptions(defaultServiceLevels, availableServiceLevels, selectedServiceLevel, serviceLevel);

  const releaseVersionOptions =
    buildOptions([], availableReleaseVersions, selectedReleaseVersion, releaseVersion);

  const handleSave = (event) => {
    event.preventDefault();
    closeModal();
    const optionsToValue = (options, stateValue) =>
      options.find(option => option.value === stateValue)?.value;
    if (type === 'host') {
      dispatch(updateHostSysPurposeAttributes({
        id,
        attributes: {
          autoheal: true,
          purpose_role: optionsToValue(roleOptions, selectedRole),
          purpose_usage: optionsToValue(usageOptions, selectedUsage),
          purpose_addons: selectedAddons,
          release_version: optionsToValue(releaseVersionOptions, selectedReleaseVersion),
          service_level: optionsToValue(serviceLevelOptions, selectedServiceLevel),
        },
        refreshHostDetails,
      }));
    } else {
      dispatch(updateAKSysPurposeAttributes({
        id,
        attributes: {
          autoheal: true,
          purpose_role: optionsToValue(roleOptions, selectedRole),
          purpose_usage: optionsToValue(usageOptions, selectedUsage),
          purpose_addons: selectedAddons,
          release_version: optionsToValue(releaseVersionOptions, selectedReleaseVersion),
          service_level: optionsToValue(serviceLevelOptions, selectedServiceLevel),
        },
        refreshAKDetails: () => dispatch(getActivationKey(id)),
      }));
    }
  };

  const handleCancel = () => {
    setSelectedRole(initialPurposeRole);
    setSelectedServiceLevel(initialServiceLevel);
    setSelectedUsage(initialPurposeUsage);
    setSelectedAddons(initialPurposeAddons);
    setSelectedReleaseVersion(initialReleaseVersion);
    closeModal();
  };

  const modalActions = ([
    <Button ouiaId="save-syspurpose" key="save-syspurpose" variant="primary" onClick={handleSave} isDisabled={unmodified}>
      {__('Save')}
    </Button>,
    <Button ouiaId="cancel-syspurpose" key="cancel" variant="link" onClick={handleCancel}>
      {__('Cancel')}
    </Button>,
  ]);


  return (
    <Modal
      isOpen={isOpen}
      onClose={handleCancel}
      title={__('Edit system purpose attributes')}
      width="40%"
      position="top"
      actions={modalActions}
      id="syspurpose-edit-modal"
      ouiaId="syspurpose-edit-modal"
    >
      <FormattedMessage
        className="syspurpose-edit-modal-blurb"
        id="syspurpose-edit-modal-blurb"
        defaultMessage={type === 'host' ? __('Select system purpose attributes for host {name}.') : __('Select system purpose attributes for activation key {name}.')}
        values={{
          name: <strong>{name}</strong>,
        }}
      />
      <Form isHorizontal style={{ marginTop: '1.3rem' }}>
        <FormGroup label={__('Role')} fieldId="role">
          <FormSelect
            id="role"
            name="role"
            ouiaId="role-select"
            value={selectedRole}
            onChange={setSelectedRole}
          >
            {roleOptions.map(option => (
              <FormSelectOption
                key={option.value}
                value={option.value}
                label={option.label}
              />
            ))}
          </FormSelect>
        </FormGroup>
        <FormGroup label={__('SLA')} fieldId="serviceLevel">
          <FormSelect
            id="serviceLevel"
            name="serviceLevel"
            ouiaId="service-level-select"
            value={selectedServiceLevel}
            onChange={setSelectedServiceLevel}
          >
            {serviceLevelOptions.map(option => (
              <FormSelectOption
                key={option.value}
                value={option.value}
                label={option.label}
              />
            ))}
          </FormSelect>
        </FormGroup>
        <FormGroup label={__('Usage')} fieldId="usage">
          <FormSelect
            id="usage"
            name="usage"
            ouiaId="usage-select"
            value={selectedUsage}
            onChange={setSelectedUsage}
          >
            {usageOptions.map(option => (
              <FormSelectOption
                key={option.value}
                value={option.value}
                label={option.label}
              />
            ))}
          </FormSelect>
        </FormGroup>
        <FormGroup label={__('Release version')} fieldId="releaseVersion">
          <FormSelect
            id="releaseVersion"
            name="release_version"
            ouiaId="release-version-select"
            value={selectedReleaseVersion}
            onChange={setSelectedReleaseVersion}
          >
            {releaseVersionOptions.map(option => (
              <FormSelectOption
                key={option.value}
                value={option.value}
                label={option.label}
              />
            ))}
          </FormSelect>
        </FormGroup>
        <FormGroup label={__('Add-ons')} fieldId="addons">
          <span id="syspurpose-addons-title" hidden>
            Checkbox Title
          </span>
          <Select
            variant={SelectVariant.typeaheadMulti}
            aria-label="syspurpose-addons"
            ouiaId="syspurpose-addons-select"
            onToggle={toggleAddonSelect}
            onSelect={onAddonSelect}
            selections={selectedAddons}
            isOpen={addonSelectOpen}
            placeholderText={__('Select add-ons')}
            aria-labelledby="syspurpose-addons-title"
            menuAppendTo="parent"
          >
            {addonsOptions.map(option => (
              <SelectOption
                key={option.value}
                value={option.value}
                label={option.label}
              />
            ))}
          </Select>
        </FormGroup>
      </Form>
    </Modal>
  );
};

export default SystemPurposeEditModal;

SystemPurposeEditModal.propTypes = {
  closeModal: PropTypes.func.isRequired,
  name: PropTypes.string,
  purposeRole: PropTypes.string.isRequired,
  purposeUsage: PropTypes.string.isRequired,
  purposeAddons: PropTypes.arrayOf(PropTypes.string).isRequired,
  serviceLevel: PropTypes.string.isRequired,
  releaseVersion: PropTypes.string,
  isOpen: PropTypes.bool.isRequired,
  orgId: PropTypes.number,
  id: PropTypes.number,
  type: PropTypes.string.isRequired,
};

SystemPurposeEditModal.defaultProps = {
  name: '',
  orgId: null,
  id: null,
  releaseVersion: '',
};