ManageIQ/manageiq-ui-classic

View on GitHub
app/javascript/components/provider-form/index.jsx

Summary

Maintainability
A
3 hrs
Test Coverage
import React, { useState, useEffect } from 'react';
import PropTypes from 'prop-types';
import { componentTypes, validatorTypes } from '@@ddf';
import { pick, keyBy } from 'lodash';

import { API } from '../../http_api';
import MiqFormRenderer from '../../forms/data-driven-form';
import miqRedirectBack from '../../helpers/miq-redirect-back';
import mapper from '../../forms/mappers/componentMapper';
import EditingContext from './editing-context';
import ProtocolSelector from './protocol-selector';
import ProviderCredentials from './provider-credentials';
import ValidateProviderCredentials from './validate-provider-credentials';
import DetectButton from './detect-button';
import validateName from '../../helpers/storage_manager/validate-names';
import { trimFieldValue } from './helper';

const findSkipSubmits = (schema, items) => {
  const found = schema.skipSubmit && items.includes(schema.name) ? [schema.name] : [];
  const children = Array.isArray(schema.fields) ? schema.fields.flatMap((field) => findSkipSubmits(field, items)) : [];
  return [...found, ...children];
};

const ProviderForm = ({
  providerId, kind, title, redirect,
}) => {
  const edit = !!providerId;
  const [{ fields, initialValues }, setState] = useState({});

  const submitLabel = edit ? __('Save') : __('Add');

  const commonFields = [
    {
      component: componentTypes.TEXT_FIELD,
      id: 'name',
      name: 'name',
      label: __('Name'),
      isRequired: true,
      validate: [
        {
          type: validatorTypes.REQUIRED,
        },
        async(value) => validateName('providers', value, edit),
      ],
    },
    {
      component: componentTypes.SELECT,
      id: 'zone_id',
      name: 'zone_id',
      label: __('Zone'),
      includeEmpty: true,
      loadOptions: () =>
        API.get('/api/zones?expand=resources&attributes=id,name,visible&filter[]=visible!=false&sort_by=name')
          .then(({ resources }) => resources.map(({ id: value, name: label }) => ({ value, label }))),
      isRequired: true,
      validate: [{
        type: validatorTypes.REQUIRED,
      }],
    },
  ];

  const loadProviderFields = (type) => API.options(`/api/providers?type=${type}`).then(
    ({ data: { provider_form_schema } }) => ([ // eslint-disable-line camelcase
      ...commonFields,
      {
        component: componentTypes.SUB_FORM,
        id: type,
        name: type,
        ...provider_form_schema, // eslint-disable-line camelcase
      },
    ]),
  );

  const typeSelectField = (edit, filter, setState, providers) => ({
    component: componentTypes.SELECT,
    id: 'type',
    name: 'type',
    label: __('Type'),
    kind: filter,
    isDisabled: edit,
    isRequired: true,
    options: providers,
    onChange: (value) => {
      if (value !== '-1') {
        loadProviderFields(value).then((fields) => setState(({ fields: [firstField] }) => ({
          fields: [firstField, ...fields],
        })));
      } else {
        setState(({ fields: [firstField] }) => ({
          fields: [firstField,
            {
              id: 'networkWarning',
              component: componentTypes.PLAIN_TEXT,
              name: 'networkWarning',
              label: __('Please select a type.'),
            }],
        }));
      }
    },
  });

  useEffect(() => {
    API.options('/api/providers').then(({ data: { supported_providers } }) => { // eslint-disable-line camelcase
      const filter = kind;
      const providers = supported_providers.filter(({ kind }) => kind === filter).map(({ title, type }) => ({ value: type, label: title }));
      providers.unshift({ label: `<${__('Choose')}>`, value: '-1' });

      if (providerId) {
        miqSparkleOn();
        API.get(`/api/providers/${providerId}?attributes=endpoints,authentications`).then(({
          type,
          endpoints: _endpoints,
          authentications: _authentications,
          ...provider
        }) => {
        // DDF can handle arrays with FieldArray, but only with a heterogenous schema, which isn't enough.
        // As a solution, we're converting the arrays to objects indexed by role/authtype and converting
        // it back to an array of objects before submitting the form. Validation, however, should not be
        // converted back as the schema is being used in the password sanitization process.
          const endpoints = keyBy(_endpoints, 'role');
          const authentications = keyBy(_authentications, 'authtype');
          loadProviderFields(type).then((fields) => {
            setState({
              fields: [typeSelectField(true, kind, setState, providers), ...fields],
              initialValues: {
                ...provider,
                type,
                endpoints,
                authentications,
              },
            });
          }).then(miqSparkleOff);
        });
      } else {
      // As the typeSelectField relies on the setState() function, it's necessary to set the initial state
      // here and not above in the useState() function.
        setState({ fields: [typeSelectField(false, kind, setState, providers)] });
      }
    });
  }, [providerId]);

  const onCancel = () => {
    const message = sprintf(
      providerId
        ? __('Edit of %s "%s" was cancelled by the user')
        : __('Add of %s was cancelled by the user'),
      title,
      initialValues && initialValues.name,
    );
    miqRedirectBack(message, 'success', redirect);
  };

  const onSubmit = ({ type, ..._data }, { getState }) => {
    if (type !== '-1') {
      miqSparkleOn();

      const message = sprintf(__('%s %s was saved'), title, _data.name || initialValues.name);

      // Retrieve the modified fields from the schema
      const modified = Object.keys(getState().modified);
      // Imit the fields that have `skipSubmit` set to `true`
      const toDelete = findSkipSubmits({ fields }, modified);
      // Construct a list of fields to be submitted
      const toSubmit = modified.filter((field) => !toDelete.includes(field));

      // Build up the form data using the list and pull out endpoints and authentications
      const { endpoints: _endpoints = { default: {} }, authentications: _authentications = {}, ...rest } = pick(_data, toSubmit);
      // Convert endpoints and authentications back to an array
      const endpoints = Object.keys(_endpoints).map((key) => ({ role: key, ..._endpoints[key] }));
      let authentications = Object.keys(_authentications).map((key) => ({ authtype: key, ..._authentications[key] }));

      if (authentications.length === 0) {
        authentications = Object.keys(initialValues.authentications).map((key) => ({ authtype: key }));
      }

      // Construct the full form data with all the necessary items
      const data = trimFieldValue({
        ...rest,
        endpoints,
        authentications,
        ...(edit ? undefined : { type }),
        ddf: true,
      });

      const request = providerId ? API.patch(`/api/providers/${providerId}`, data) : API.post('/api/providers', data);
      request.then(() => miqRedirectBack(message, 'success', redirect)).catch(miqSparkleOff);
    }
  };

  const componentMapper = {
    ...mapper,
    'protocol-selector': ProtocolSelector,
    'provider-credentials': ProviderCredentials,
    'validate-provider-credentials': ValidateProviderCredentials,
    'detect-button': DetectButton,
  };

  return (
    <div>
      { fields && (
        <EditingContext.Provider value={{ providerId, setState }}>
          <MiqFormRenderer
            componentMapper={componentMapper}
            schema={{ fields }}
            onSubmit={onSubmit}
            onCancel={onCancel}
            initialValues={initialValues}
            clearedValue={null}
            buttonsLabels={{ submitLabel }}
            canReset={edit}
            clearOnUnmount
            keepDirtyOnReinitilize
          />
        </EditingContext.Provider>
      ) }
    </div>
  );
};

ProviderForm.propTypes = {
  providerId: PropTypes.string,
  kind: PropTypes.string,
  title: PropTypes.string,
  redirect: PropTypes.string,
};

ProviderForm.defaultProps = {
  providerId: undefined,
  kind: undefined,
  title: undefined,
  redirect: undefined,
};

export default ProviderForm;