Blob Blame History Raw
/* eslint-disable jsx-a11y/label-has-associated-control */
/* eslint-disable react/no-did-update-set-state */

import React from "react";
import PropTypes from "prop-types";
import { connect } from "react-redux";
import { FormattedMessage, defineMessages, injectIntl, intlShape } from "react-intl";
import { Modal, Alert, Spinner } from "patternfly-react";
import SourcesListItem from "../ListView/SourcesListItem";
import EmptyState from "../EmptyState/EmptyState";
import {
  addModalManageSourcesEntry,
  removeModalManageSourcesEntry,
  modalManageSourcesFailure,
} from "../../core/actions/modals";

const messages = defineMessages({
  infotip: {
    defaultMessage:
      "Sources are used for resolving blueprint dependencies and for composing images. " +
      "When adding custom sources you must make sure that the packages in the source do not conflict with any other package sources, " +
      "otherwise resolving dependencies and composing images will fail.",
  },
  errorStateTitle: {
    defaultMessage: "An error occurred",
  },
  errorStateMessage: {
    defaultMessage: "An error occurred while trying to get sources.",
  },
  closeButtonLabel: {
    defaultMessage: "Close",
  },
  sourcePath: {
    defaultMessage: "Source path",
    description: "The path or url to the source repository",
  },
  name: {
    defaultMessage: "Name",
    description: "Name of source",
  },
  type: {
    defaultMessage: "Type",
    description: "Type of source",
  },
  security: {
    defaultMessage: "Security",
  },
  check_ssl: {
    defaultMessage: "Check SSL certificate",
  },
  check_gpg: {
    defaultMessage: "Check GPG key",
  },
  selectOne: {
    defaultMessage: "Select one",
  },
  typeRepo: {
    defaultMessage: "yum repository",
  },
  typeMirrorlist: {
    defaultMessage: "mirrorlist",
  },
  typeMetalink: {
    defaultMessage: "metalink",
  },
  add: {
    defaultMessage: "Add source",
  },
  save: {
    defaultMessage: "Add source",
  },
  update: {
    defaultMessage: "Update source",
  },
  cancel: {
    defaultMessage: "Cancel",
  },
});

class ManageSources extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      showModal: false,
    };
    this.open = this.open.bind(this);
    this.close = this.close.bind(this);
  }

  open() {
    this.setState({ showModal: true });
  }

  close() {
    this.setState({ showModal: false });
  }

  render() {
    return (
      <>
        <a href="#" onClick={!this.props.disabled ? this.open : undefined}>
          <FormattedMessage
            defaultMessage="Manage sources"
            description="User action for displaying the list of source repositories"
          />
        </a>
        {this.state.showModal && (
          <ManageSourcesModal
            manageSources={this.props.manageSources}
            addSource={this.props.addModalManageSourcesEntry}
            removeSource={this.props.removeModalManageSourcesEntry}
            clearError={this.props.modalManageSourcesFailure}
            close={this.close}
            intl={this.props.intl}
          />
        )}
      </>
    );
  }
}

class ManageSourcesModal extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      addEntry: false,
      url: "",
      name: "",
      type: "",
      check_ssl: false,
      check_gpg: false,
      warningDuplicateName: false,
      warningDuplicateUrl: false,
      editName: "",
    };
    this.handleShowForm = this.handleShowForm.bind(this);
    this.handleChange = this.handleChange.bind(this);
    this.handleValidateName = this.handleValidateName.bind(this);
    this.handleValidateUrl = this.handleValidateUrl.bind(this);
    this.handleEditSource = this.handleEditSource.bind(this);
    this.handleSubmitSource = this.handleSubmitSource.bind(this);
  }

  componentDidUpdate(prevProps) {
    // if no errors are returned on add/edit, then reset form state after fetching sources completes
    if (
      Object.keys(this.props.manageSources.error).length === 0 &&
      !this.props.manageSources.fetchingSources &&
      prevProps.manageSources.fetchingSources
    ) {
      this.setState({
        addEntry: false,
        url: "",
        name: "",
        type: "",
        check_ssl: false,
        check_gpg: false,
      });
    }
  }

  handleShowForm(e, showForm) {
    this.setState({ addEntry: showForm });
    if (showForm) {
      this.setState({
        editName: "",
      });
    } else {
      this.props.clearError({});
      this.setState({
        url: "",
        name: "",
        type: "",
        check_ssl: false,
        check_gpg: false,
        warningDuplicateName: false,
        warningDuplicateUrl: false,
      });
    }
  }

  handleChange(e, input) {
    let value;
    if (input === "check_ssl" || input === "check_gpg") {
      value = e.target.checked;
    } else {
      value = e.target.value.trim();
    }
    if (input === "name") {
      this.handleValidateName(value);
    }
    if (input === "url") {
      this.handleValidateUrl(value);
    }
    this.setState({ [input]: value });
  }

  handleValidateName(name) {
    const duplicateName = this.props.manageSources.hasOwnProperty(name);
    this.setState({ warningDuplicateName: duplicateName });
  }

  handleValidateUrl(url) {
    const sourceUrls = Object.values(this.props.manageSources.sources).map((source) => source.url);
    this.setState({ warningDuplicateUrl: !sourceUrls.every((sourceUrl) => sourceUrl !== url) });
  }

  handleEditSource(name) {
    this.setState({
      addEntry: true,
      editName: name,
      name,
      type: this.props.manageSources.sources[name].type,
      url: this.props.manageSources.sources[name].url,
      check_ssl: this.props.manageSources.sources[name].check_ssl,
      check_gpg: this.props.manageSources.sources[name].check_gpg,
    });
  }

  handleSubmitSource() {
    this.props.clearError({});
    const source = {
      name: this.state.name,
      url: this.state.url,
      type: this.state.type,
      check_ssl: this.state.check_ssl,
      check_gpg: this.state.check_gpg,
    };
    this.props.addSource(source);
  }

  render() {
    const { formatMessage } = this.props.intl;
    const { manageSources } = this.props;
    const systemSources = Object.values(manageSources.sources).filter((source) => source.system === true);
    const customSources = Object.values(manageSources.sources).filter((source) => source.system !== true);
    const disabledSubmit =
      this.state.name === "" ||
      this.state.url === "" ||
      this.state.type === "" ||
      this.state.warningDuplicateName ||
      this.state.warningDuplicateUrl ||
      manageSources.fetchingSources;
    const manageSourcesForm = (
      <>
        {Object.keys(manageSources.error).length !== 0 && (
          <Alert>
            <FormattedMessage defaultMessage="An error occurred when saving the source. Check that the path is valid and try again." />
          </Alert>
        )}
        <form id="cmpsr-form-add-source" className="form-horizontal form-horizontal-pf-align-left">
          <p className="fields-status-pf">
            <FormattedMessage
              defaultMessage="The fields marked with {val} are required."
              values={{
                val: <span className="required-pf">*</span>,
              }}
            />
          </p>
          <div className={`form-group ${this.state.warningDuplicateName ? "has-error" : ""}`}>
            <label className="col-sm-2 control-label required-pf" htmlFor="textInput1-modal-source">
              {formatMessage(messages.name)}
            </label>
            <div className="col-sm-10">
              <input
                autoFocus
                type="text"
                id="textInput1-modal-source"
                className="form-control"
                aria-describedby="textInput1-modal-source-help"
                aria-required="true"
                aria-invalid={this.state.warningDuplicateName}
                readOnly={this.state.editName !== ""}
                value={this.state.name}
                onChange={(e) => this.handleChange(e, "name")}
              />
              {this.state.warningDuplicateName && (
                <span className="help-block" id="textInput1-modal-source-help">
                  <FormattedMessage defaultMessage="This source name already exists." />
                </span>
              )}
            </div>
          </div>
          <div className={`form-group ${this.state.warningDuplicateUrl ? "has-error" : ""}`}>
            <label className="col-sm-2 control-label required-pf" htmlFor="textInput2-modal-source">
              {formatMessage(messages.sourcePath)}
            </label>
            <div className="col-sm-10">
              <input
                type="text"
                id="textInput2-modal-source"
                className="form-control"
                aria-describedby="textInput2-modal-source-help"
                aria-required="true"
                aria-invalid={this.state.warningDuplicateUrl}
                value={this.state.url}
                onChange={(e) => this.handleChange(e, "url")}
              />
              {this.state.warningDuplicateUrl && (
                <span className="help-block" id="textInput2-modal-source-help">
                  <FormattedMessage defaultMessage="This source path already exists." />
                </span>
              )}
            </div>
          </div>
          <div className="form-group">
            <label className="col-sm-2 control-label required-pf" htmlFor="textInput3-modal-source">
              {formatMessage(messages.type)}
            </label>
            <div className="col-sm-10">
              <select
                id="textInput3-modal-source"
                className="form-control"
                value={this.state.type}
                aria-required="true"
                onChange={(e) => this.handleChange(e, "type")}
              >
                <option value="" disabled hidden>
                  {formatMessage(messages.selectOne)}
                </option>
                <option value="yum-baseurl">{formatMessage(messages.typeRepo)}</option>
                <option value="yum-mirrorlist">{formatMessage(messages.typeMirrorlist)}</option>
                <option value="yum-metalink">{formatMessage(messages.typeMetalink)}</option>
              </select>
            </div>
          </div>
          <div className="form-group">
            <label className="col-sm-2 control-label" id="checkboxGroup-modal-source">
              {formatMessage(messages.security)}
            </label>
            <fieldset className="col-sm-10 checkbox" aria-labelledby="checkboxGroup-modal-source">
              <div>
                <label htmlFor="checkboxInput4-modal-source">
                  <input
                    type="checkbox"
                    id="checkboxInput4-modal-source"
                    checked={this.state.check_ssl}
                    onChange={(e) => this.handleChange(e, "check_ssl")}
                  />
                  {formatMessage(messages.check_ssl)}
                </label>
              </div>
              <div>
                <label htmlFor="checkboxInput5-modal-source">
                  <input
                    type="checkbox"
                    id="checkboxInput5-modal-source"
                    checked={this.state.check_gpg}
                    onChange={(e) => this.handleChange(e, "check_gpg")}
                  />
                  {formatMessage(messages.check_gpg)}
                </label>
              </div>
            </fieldset>
          </div>
        </form>
      </>
    );
    return (
      <Modal
        show
        id="cmpsr-modal-manage-sources"
        onHide={this.props.close}
        bsSize="large"
        aria-labelledby="title-manage-sources"
      >
        <Modal.Header>
          <Modal.CloseButton onClick={this.props.close} />
          <Modal.Title id="title-manage-sources">
            {(!this.state.addEntry && (
              <FormattedMessage
                defaultMessage="Sources"
                description="Sources provide the contents from which components are selected"
              />
            )) ||
              (this.state.editName === "" && <FormattedMessage defaultMessage="Add source" />) || (
                <FormattedMessage defaultMessage="Edit source" />
              )}
          </Modal.Title>
        </Modal.Header>
        <Modal.Body>
          {(Object.keys(manageSources.sources).length === 0 && (
            <EmptyState
              title={formatMessage(messages.errorStateTitle)}
              message={formatMessage(messages.errorStateMessage)}
            />
          )) || (
            <>
              {(!this.state.addEntry && (
                <>
                  <div className="cmpsr-header cmpsr-header--modal">
                    <div className="cmpsr-header__actions">
                      <input
                        type="button"
                        autoFocus={this.state.editName === ""}
                        className="btn btn-primary pull-right"
                        onClick={(e) => this.handleShowForm(e, true)}
                        value={formatMessage(messages.add)}
                      />
                    </div>
                  </div>
                  <div className="list-pf cmpsr-list-pf list-pf-stacked cmpsr-list-sources">
                    {systemSources.map((source) => (
                      <SourcesListItem source={source} key={source.name} />
                    ))}
                    {customSources.length > 0 &&
                      customSources.map((source) => (
                        <SourcesListItem
                          source={source}
                          key={source.name}
                          edited={this.state.editName}
                          fetching={manageSources.fetchingSources}
                          edit={this.handleEditSource}
                          remove={this.props.removeSource}
                        />
                      ))}
                  </div>
                </>
              )) ||
                manageSourcesForm}
            </>
          )}
        </Modal.Body>
        <Modal.Footer>
          {(!this.state.addEntry && (
            <button type="button" className="btn btn-default" onClick={this.props.close}>
              <FormattedMessage defaultMessage="Close" />
            </button>
          )) || (
            <>
              {manageSources.fetchingSources && (
                <div className="pull-left">
                  <Spinner loading size="xs" inline />
                  <FormattedMessage defaultMessage="Saving source" />
                </div>
              )}
              <button type="button" className="btn btn-default" onClick={(e) => this.handleShowForm(e, false)}>
                {formatMessage(messages.cancel)}
              </button>

              <button
                type="submit"
                className="btn btn-primary"
                form="cmpsr-form-add-source"
                disabled={disabledSubmit}
                onClick={() => this.handleSubmitSource()}
              >
                {(this.state.editName === "" && formatMessage(messages.save)) || formatMessage(messages.update)}
              </button>
            </>
          )}
        </Modal.Footer>
      </Modal>
    );
  }
}

ManageSources.propTypes = {
  manageSources: PropTypes.shape({
    fetchingSources: PropTypes.bool,
    sources: PropTypes.objectOf(PropTypes.object),
    error: PropTypes.object,
  }),
  disabled: PropTypes.bool,
  removeModalManageSourcesEntry: PropTypes.func,
  addModalManageSourcesEntry: PropTypes.func,
  modalManageSourcesFailure: PropTypes.func,
  intl: intlShape.isRequired,
};

ManageSources.defaultProps = {
  manageSources: {},
  disabled: false,
  removeModalManageSourcesEntry() {},
  addModalManageSourcesEntry() {},
  modalManageSourcesFailure() {},
};

ManageSourcesModal.propTypes = {
  manageSources: PropTypes.shape({
    fetchingSources: PropTypes.bool,
    sources: PropTypes.objectOf(PropTypes.object),
    error: PropTypes.object,
  }),
  removeSource: PropTypes.func,
  addSource: PropTypes.func,
  clearError: PropTypes.func,
  close: PropTypes.func,
  intl: intlShape.isRequired,
};

ManageSourcesModal.defaultProps = {
  manageSources: {},
  removeSource() {},
  addSource() {},
  clearError() {},
  close() {},
};

const mapDispatchToProps = (dispatch) => ({
  addModalManageSourcesEntry: (source) => {
    dispatch(addModalManageSourcesEntry(source));
  },
  removeModalManageSourcesEntry: (sourceName) => {
    dispatch(removeModalManageSourcesEntry(sourceName));
  },
  modalManageSourcesFailure: (error) => {
    dispatch(modalManageSourcesFailure(error));
  },
});

export default connect(null, mapDispatchToProps)(injectIntl(ManageSources));