Blob Blame History Raw
import React from "react";
import { FormattedMessage, defineMessages, injectIntl, intlShape } from "react-intl";
import PropTypes from "prop-types";
import { Tabs, Tab } from "patternfly-react";
import { Tooltip, TooltipPosition } from "@patternfly/react-core";
import { connect } from "react-redux";
import ComponentTypeIcons from "./ComponentTypeIcons";
import DependencyListView from "./DependencyListView";
import LabelWithBadge from "./LabelWithBadge";
import Loading from "../Loading/Loading";
import EmptyState from "../EmptyState/EmptyState";
import { fetchingInputDetails, fetchingInputDeps } from "../../core/actions/inputs";

const messages = defineMessages({
  details: {
    defaultMessage: "Details",
  },
  dependencies: {
    defaultMessage: "Dependencies",
  },
  hideDetails: {
    defaultMessage: "Hide Details",
  },
  removeFromBlueprint: {
    defaultMessage: "Remove from Blueprint",
  },
  noDepsTitle: {
    defaultMessage: "No Dependencies",
  },
  noDepsMessage: {
    defaultMessage: "This component has no dependencies.",
  },
});

class ComponentDetailsView extends React.Component {
  constructor() {
    super();
    this.state = {
      selectedBuildIndex: undefined,
      savedVersion: undefined,
    };
    this.setBuildIndex = this.setBuildIndex.bind(this);
    this.handleVersionSelect = this.handleVersionSelect.bind(this);
    this.handleChildComponent = this.handleChildComponent.bind(this);
    this.handleParentComponent = this.handleParentComponent.bind(this);
    this.handleCloseDetails = this.handleCloseDetails.bind(this);
    this.handleSelectedBuildDeps = this.handleSelectedBuildDeps.bind(this);
  }

  componentDidMount() {
    this.props.fetchingInputDetails(this.props.component);
    if (this.props.handleUpdateComponent) {
      this.setBuildIndex();
    } else if (this.props.dependencies === undefined) {
      this.props.fetchingInputDeps(this.props.component);
    }
  }

  componentDidUpdate(prevProps) {
    if (this.props.component.name !== prevProps.component.name) {
      this.props.fetchingInputDetails(this.props.component);
      this.setState({ selectedBuildIndex: undefined }); // eslint-disable-line react/no-did-update-set-state
    }
    if (this.props.handleUpdateComponent) {
      this.setBuildIndex();
    } else if (this.props.dependencies === undefined) {
      this.props.fetchingInputDeps(this.props.component);
    }
  }

  handleVersionSelect(event) {
    this.setState({ selectedBuildIndex: event.target.value });
    this.handleSelectedBuildDeps(event.target.value);
  }

  handleSelectedBuildDeps(index) {
    const { component } = this.props;
    // update dependencies for selected component build
    const selectedComponentBuild = {
      ...component,
      release: component.builds[index].release,
      version: component.builds[index].depsolveVersion,
    };
    this.props.fetchingInputDeps(selectedComponentBuild);
  }

  handleParentComponent(event, component, index) {
    // user clicks a node in the breadcrumb
    this.props.setSelectedInput(component);
    const updatedParents = this.props.componentParent.slice(0, index);
    this.props.setSelectedInputParent(updatedParents);
    event.preventDefault();
    event.stopPropagation();
  }

  handleChildComponent(event, component) {
    // user clicks a list item in the dependencies tab
    this.props.setSelectedInput(component);
    const updatedParents = this.props.componentParent.concat(this.props.component);
    this.props.setSelectedInputParent(updatedParents);
    event.preventDefault();
    event.stopPropagation();
  }

  handleCloseDetails(event) {
    this.props.clearSelectedInput();
    event.preventDefault();
    event.stopPropagation();
  }

  setBuildIndex() {
    // if component is not in the blueprint, then default to the first option ("*")
    // if component is in the blueprint, then set selectedBuildIndex to the object with the current selected version
    const { component, selectedComponents } = this.props;
    let index = this.state.selectedBuildIndex;
    if (component.builds !== undefined && index === undefined) {
      if (component.userSelected === true) {
        const selectedVersion = selectedComponents.find((selected) => selected.name === component.name).version;
        const selectedBuild = component.builds.filter((obj) => obj.version === selectedVersion)[0];
        index = component.builds.indexOf(selectedBuild);
        if (index === -1) {
          index = 0;
        }
        this.setState({ selectedBuildIndex: index });
        this.setState({ savedVersion: selectedVersion });
        this.handleSelectedBuildDeps(index);
      } else {
        this.setState({ selectedBuildIndex: 0 });
        this.handleSelectedBuildDeps(0);
      }
    }
  }

  render() {
    const {
      component,
      dependencies,
      componentParent,
      blueprint,
      handleAddComponent,
      handleUpdateComponent,
      handleRemoveComponent,
      handleDepListItem,
    } = this.props;
    const { selectedBuildIndex } = this.state;
    const { formatMessage } = this.props.intl;

    return (
      <div className="cmpsr-panel__body cmpsr-panel__body--main">
        <div className="cmpsr-header">
          {(componentParent.length > 0 && (
            <ol className="breadcrumb">
              <li>
                <a href="#" onClick={(e) => this.handleCloseDetails(e)}>
                  <FormattedMessage
                    defaultMessage="Back to {blueprint}"
                    values={{
                      blueprint,
                    }}
                  />
                </a>
              </li>
              {componentParent.map((parent, i) => (
                <li key={parent.name}>
                  <a href="#" onClick={(e) => this.handleParentComponent(e, parent, i)}>
                    {parent.name}
                  </a>
                </li>
              ))}
              <li />
            </ol>
          )) || (
            <ol className="breadcrumb">
              <li>
                <a href="#" onClick={(e) => this.handleCloseDetails(e)}>
                  <FormattedMessage
                    defaultMessage="Back to {blueprint}"
                    values={{
                      blueprint,
                    }}
                  />
                </a>
              </li>
            </ol>
          )}
          <div className="cmpsr-header__actions">
            <ul className="list-inline">
              {handleAddComponent !== undefined &&
                ((component.inBlueprint && !component.userSelected) || !component.inBlueprint) && (
                  <li>
                    <button
                      className="btn btn-primary add"
                      type="button"
                      onClick={(e) => handleAddComponent(e, component, component.builds[selectedBuildIndex].version)}
                    >
                      <FormattedMessage defaultMessage="Add" />
                    </button>
                  </li>
                )}
              {handleUpdateComponent !== undefined &&
                component.inBlueprint &&
                component.userSelected &&
                component.builds !== undefined &&
                selectedBuildIndex !== undefined &&
                component.builds[selectedBuildIndex].version !== this.state.savedVersion && (
                  <li>
                    <button
                      className="btn btn-primary"
                      type="button"
                      onClick={(e) => handleUpdateComponent(e, component, component.builds[selectedBuildIndex].version)}
                    >
                      <FormattedMessage defaultMessage="Apply Change" />
                    </button>
                  </li>
                )}
              {handleRemoveComponent !== undefined && component.inBlueprint && component.userSelected && (
                <li>
                  <Tooltip position={TooltipPosition.bottom} content={formatMessage(messages.removeFromBlueprint)}>
                    <button
                      className="btn btn-default"
                      type="button"
                      onClick={(e) => handleRemoveComponent(e, component.name)}
                    >
                      <FormattedMessage defaultMessage="Remove" />
                    </button>
                  </Tooltip>
                </li>
              )}
              <li>
                <Tooltip position={TooltipPosition.bottom} content={formatMessage(messages.hideDetails)}>
                  <button type="button" className="close" onClick={(e) => this.handleCloseDetails(e)}>
                    <span className="pficon pficon-close" />
                  </button>
                </Tooltip>
              </li>
            </ul>
          </div>
          <h3 className="cmpsr-title">
            <span>
              <ComponentTypeIcons
                componentType={component.ui_type}
                compDetails
                componentInBlueprint={component.inBlueprint}
                isSelected={component.userSelected}
              />{" "}
              {component.name}
            </span>
          </h3>
        </div>
        {handleUpdateComponent !== undefined && component.builds !== undefined && component.builds.length > 1 && (
          <div className="cmpsr-component-details__form">
            <h4>
              <FormattedMessage defaultMessage="Component Options" />
            </h4>
            <form className="form-horizontal">
              <div className="form-group">
                <label className="col-sm-3 col-md-2 control-label" htmlFor="cmpsr-compon__version-select">
                  <FormattedMessage defaultMessage="Version" />
                </label>
                <div className="col-sm-8 col-md-9">
                  <select
                    id="cmpsr-compon__version-select"
                    className="form-control"
                    value={selectedBuildIndex}
                    onChange={this.handleVersionSelect}
                  >
                    {component.builds.map((build, i) => (
                      <option key={build.version} value={i}>
                        {build.version} {build.version === "*" && "(latest version)"}
                      </option>
                    ))}
                  </select>
                </div>
              </div>
            </form>
          </div>
        )}
        <div>
          <Tabs id="blueprint-tabs">
            <Tab eventKey="details" title={formatMessage(messages.details)}>
              <h4 className="cmpsr-title">{component.summary}</h4>
              <p>{component.description}</p>
              <dl className="dl-horizontal">
                <dt>
                  <FormattedMessage defaultMessage="Type" />
                </dt>
                <dd>{component.ui_type}</dd>
                <dt>
                  <FormattedMessage defaultMessage="Version" />
                </dt>
                {((component.builds === undefined || selectedBuildIndex === undefined) && (
                  <dd>{component.version}</dd>
                )) || <dd>{component.builds[selectedBuildIndex].depsolveVersion}</dd>}
                <dt>
                  <FormattedMessage defaultMessage="Release" />
                </dt>
                {((component.builds === undefined || selectedBuildIndex === undefined) && (
                  <dd>{component.release}</dd>
                )) || <dd>{component.builds[selectedBuildIndex].release}</dd>}
                <dt>
                  <FormattedMessage defaultMessage="URL" />
                </dt>
                {(component.homepage !== null && (
                  <dd>
                    <a target="_blank" rel="noopener noreferrer" href={component.homepage}>
                      {component.homepage}
                    </a>
                  </dd>
                )) || <dd>&nbsp;</dd>}
              </dl>
            </Tab>
            {(dependencies === undefined && (
              <Tab eventKey="dependencies" title={formatMessage(messages.dependencies)}>
                <Loading />
              </Tab>
            )) || (
              <Tab
                eventKey="dependencies"
                title={<LabelWithBadge title={formatMessage(messages.dependencies)} badge={dependencies.length} />}
              >
                {(dependencies.length === 0 && (
                  <EmptyState
                    title={formatMessage(messages.noDepsTitle)}
                    message={formatMessage(messages.noDepsMessage)}
                  />
                )) || (
                  <DependencyListView
                    id="cmpsr-component-dependencies"
                    listItems={dependencies}
                    noEditComponent
                    handleComponentDetails={this.handleChildComponent}
                    componentDetailsParent={component}
                    fetchDetails={handleDepListItem}
                  />
                )}
              </Tab>
            )}
          </Tabs>
        </div>
      </div>
    );
  }
}

ComponentDetailsView.propTypes = {
  component: PropTypes.shape({
    arch: PropTypes.string,
    epoch: PropTypes.number,
    homepage: PropTypes.string,
    inBlueprint: PropTypes.bool,
    name: PropTypes.string,
    release: PropTypes.string,
    summary: PropTypes.string,
    description: PropTypes.string,
    ui_type: PropTypes.string,
    userSelected: PropTypes.bool,
    version: PropTypes.string,
    builds: PropTypes.arrayOf(PropTypes.object),
  }),
  selectedComponents: PropTypes.arrayOf(PropTypes.object),
  blueprint: PropTypes.string,
  componentParent: PropTypes.arrayOf(PropTypes.object),
  handleRemoveComponent: PropTypes.func,
  handleAddComponent: PropTypes.func,
  handleUpdateComponent: PropTypes.func,
  handleDepListItem: PropTypes.func,
  fetchingInputDeps: PropTypes.func,
  fetchingInputDetails: PropTypes.func,
  dependencies: PropTypes.arrayOf(PropTypes.object),
  setSelectedInput: PropTypes.func,
  setSelectedInputParent: PropTypes.func,
  clearSelectedInput: PropTypes.func,
  intl: intlShape.isRequired,
};

ComponentDetailsView.defaultProps = {
  component: {},
  selectedComponents: [],
  blueprint: "",
  componentParent: [],
  handleRemoveComponent: undefined,
  handleAddComponent: undefined,
  handleUpdateComponent: undefined,
  handleDepListItem() {},
  fetchingInputDeps() {},
  fetchingInputDetails() {},
  dependencies: undefined,
  setSelectedInput() {},
  setSelectedInputParent() {},
  clearSelectedInput() {},
};

const mapDispatchToProps = (dispatch) => ({
  fetchingInputDetails: (component) => {
    dispatch(fetchingInputDetails(component));
  },
  fetchingInputDeps: (component) => {
    dispatch(fetchingInputDeps(component));
  },
});

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