Blob Blame History Raw
import React from "react";
import { FormattedMessage, defineMessages, injectIntl, intlShape } from "react-intl";
import PropTypes from "prop-types";
import { connect } from "react-redux";
import { Pagination, PaginationVariant, TextInput } from "@patternfly/react-core";
import Link from "../../components/Link/Link";
import Layout from "../../components/Layout/Layout";
import BlueprintContents from "../../components/ListView/BlueprintContents";
import ComponentInputs from "../../components/ListView/ComponentInputs";
import ComponentDetailsView from "../../components/ListView/ComponentDetailsView";
import CreateImageUpload from "../../components/Wizard/CreateImageUpload";
import ExportBlueprint from "../../components/Modal/ExportBlueprint";
import PendingChanges from "../../components/Modal/PendingChanges";
import EmptyState from "../../components/EmptyState/EmptyState";
import Loading from "../../components/Loading/Loading";
import BlueprintToolbar from "../../components/Toolbar/BlueprintToolbar";
import BlueprintApi from "../../data/BlueprintApi";
import NotificationsApi from "../../data/NotificationsApi";
import {
  fetchingBlueprintContents,
  setBlueprint,
  updateBlueprintComponents,
  undo,
  redo,
  deleteHistory,
  fetchingCompDeps,
} from "../../core/actions/blueprints";
import {
  fetchingInputs,
  setSelectedInputPage,
  clearSelectedInput,
  setSelectedInput,
  setSelectedInputDeps,
  setSelectedInputParent,
  deleteFilter,
  fetchingDepDetails,
} from "../../core/actions/inputs";
import { setModalActive } from "../../core/actions/modals";
import {
  componentsSortSetKey,
  componentsSortSetValue,
  dependenciesSortSetKey,
  dependenciesSortSetValue,
} from "../../core/actions/sort";
import {
  componentsFilterAddValue,
  componentsFilterRemoveValue,
  componentsFilterClearValues,
} from "../../core/actions/filter";
import {
  makeGetBlueprintById,
  makeGetSortedSelectedComponents,
  makeGetSortedDependencies,
  makeGetFutureLength,
  makeGetPastLength,
  makeGetFilteredComponents,
  makeGetSelectedInputs,
  makeGetSelectedDeps,
} from "../../core/selectors";
import "./index.css";

const messages = defineMessages({
  listTitleAvailableComps: {
    defaultMessage: "Available Components",
  },
  addComponentTitle: {
    defaultMessage: "Add Blueprint Components",
  },
  addComponentMessageOne: {
    defaultMessage:
      "Browse or search for components, then add them to the blueprint. Or leave the blueprint empty to create a minimal image.",
  },
  addComponentMessageTwo: {
    defaultMessage:
      "The packages needed to support the selected image type are automatically included when creating an image.",
  },
  blueprintTitle: {
    defaultMessage: "Blueprint",
  },
  filterByLabel: {
    defaultMessage: "Filter Available Components by Name",
  },
  filterByPlaceholder: {
    defaultMessage: "Filter By Name...",
  },
  emptyStateNoResultsMessage: {
    defaultMessage: "Modify your filter criteria to get results.",
  },
  emptyStateNoResultsTitle: {
    defaultMessage: "No Results Match the Filter Criteria",
  },
  paginationPerPage: {
    defaultMessage: "per page",
  },
});

class EditBlueprintPage extends React.Component {
  constructor() {
    super();
    this.state = {
      page: 1,
      pageSize: 50,
    };
    this.setNotifications = this.setNotifications.bind(this);
    this.handleCommit = this.handleCommit.bind(this);
    this.handleAddComponent = this.handleAddComponent.bind(this);
    this.handleUpdateComponent = this.handleUpdateComponent.bind(this);
    this.handleRemoveComponent = this.handleRemoveComponent.bind(this);
    this.handleComponentDetails = this.handleComponentDetails.bind(this);
    this.handleComponentListItem = this.handleComponentListItem.bind(this);
    this.handleDepListItem = this.handleDepListItem.bind(this);
    this.handleHideModal = this.handleHideModal.bind(this);
    this.handleShowModal = this.handleShowModal.bind(this);
    this.handleDiscardChanges = this.handleDiscardChanges.bind(this);
    this.handleUndo = this.handleUndo.bind(this);
    this.handleSetPage = this.handleSetPage.bind(this);
    this.handlePageSizeSelect = this.handlePageSizeSelect.bind(this);
  }

  componentDidMount() {
    const { formatMessage } = this.props.intl;
    document.title = formatMessage(messages.blueprintTitle);
    // get blueprint, get inputs; then update inputs
    if (this.props.blueprint.components === undefined) {
      this.props.fetchingBlueprintContents(this.props.route.params.blueprint.replace(/\s/g, "-"));
    }
    this.props.fetchingInputs(this.props.inputs.inputFilters, this.state.page, this.state.pageSize);
  }

  componentWillUnmount() {
    this.props.deleteFilter();
    this.props.clearSelectedInput();
  }

  handleClearFilters(event) {
    const filter = {
      field: "name",
      value: "",
    };
    this.props.deleteFilter();
    this.props.fetchingInputs(filter, 1, this.state.pageSize);
    $("#cmpsr-blueprint-input-filter").val("");
    event.preventDefault();
    event.stopPropagation();
  }

  handleSetPage(_event, page) {
    this.setState({
      page,
    });
    this.props.setSelectedInputPage(page);
    const filter = this.props.inputs.inputFilters;
    this.props.fetchingInputs(filter, page, this.state.pageSize);
  }

  handlePageSizeSelect(_event, pageSize) {
    this.setState({
      pageSize,
    });
    const filter = this.props.inputs.inputFilters;
    this.props.fetchingInputs(filter, this.state.page, pageSize);
  }

  handleCommit() {
    // clear existing notifications
    NotificationsApi.closeNotification(undefined, "committed");
    NotificationsApi.closeNotification(undefined, "committing");
    // display the committing notification
    NotificationsApi.displayNotification(this.props.blueprint.name, "committing");
    this.setNotifications();
    // post blueprint (includes 'committed' notification)
    Promise.all([BlueprintApi.handleCommitBlueprint(this.props.blueprint)])
      .then(() => {
        // then after blueprint is posted, reload blueprint details
        // to get details that were updated during commit (i.e. version)
        Promise.all([BlueprintApi.reloadBlueprintDetails(this.props.blueprint)])
          .then((data) => {
            const blueprintToSet = { ...this.props.blueprint, version: data[0].version };
            this.props.setBlueprint(blueprintToSet);
          })
          .catch((e) => console.log(`Error in reload blueprint details: ${e}`));
      })
      .catch((e) => console.log(`Error in blueprint commit: ${e}`));
  }

  handleAddComponent(event, component, version) {
    this.props.clearSelectedInput();
    const addedPackage = {
      name: component.name,
      version,
    };
    const pendingChange = {
      componentOld: null,
      componentNew: `${component.name}-${version}`,
    };
    let pendingChanges = this.props.blueprint.localPendingChanges;
    const prevChange = pendingChanges.find((change) => change.componentOld === pendingChange.componentNew);
    // removing then adding a component of the same version results in no change listed
    // if a different version of this component was removed, that change and this change will
    // still be listed as separate changes
    // but if a previous change exists where the same component version was removed...
    if (prevChange !== undefined) {
      // then filter that previous change, and don't add this change
      pendingChanges = pendingChanges.filter((component) => component !== prevChange);
    } else {
      pendingChanges = [pendingChange].concat(pendingChanges);
    }
    const packages = this.props.blueprint.packages.concat(addedPackage);
    const { modules } = this.props.blueprint;
    const addedComponent = { ...component, version, userSelected: true, inBlueprint: true };
    // for now, just adding the component to the state
    // component info will load after committing the change to the workspace and reloading the blueprint
    const components = this.props.blueprint.components.concat(addedComponent);
    this.props.updateBlueprintComponents(this.props.blueprint.id, components, packages, modules, pendingChanges);
    event.preventDefault();
    event.stopPropagation();
  }

  handleUpdateComponent(event, component, version) {
    const name = component.name;
    this.props.clearSelectedInput();
    const selectedComponents = this.props.blueprint.packages.concat(this.props.blueprint.modules);
    const oldVersion = selectedComponents.find((selectedComp) => selectedComp.name === name).version;
    const updatedComponent = {
      name,
      version,
    };
    // let??
    const pendingChange = {
      componentOld: `${name}-${oldVersion}`,
      componentNew: `${name}-${version}`,
    };
    let pendingChanges = this.props.blueprint.localPendingChanges;
    const prevChange = pendingChanges.find((change) => change.componentNew === pendingChange.componentOld);
    // if this component was added or updated in this session...
    if (prevChange !== undefined) {
      // then only list this component once in the list of changes,
      // where the change shows the old version of the previous change
      pendingChange.componentOld = prevChange.componentOld;
      pendingChanges = pendingChanges.filter((change) => change !== prevChange);
    }
    if (prevChange === undefined || pendingChange.componentOld !== pendingChange.componentNew) {
      pendingChanges = [pendingChange].concat(pendingChanges);
    }
    let { packages } = this.props.blueprint;
    let { modules } = this.props.blueprint;
    if (modules.some((module) => module.name === name)) {
      modules = modules.filter((item) => item.name !== updatedComponent.name).concat(updatedComponent);
    } else {
      packages = packages.filter((item) => item.name !== updatedComponent.name).concat(updatedComponent);
    }
    const components = this.props.blueprint.components.map((blueprintComp) => {
      if (blueprintComp.name === name) {
        const componentData = { ...blueprintComp, name, version, userSelected: true, inBlueprint: true };
        return componentData;
      }
      return blueprintComp;
    });
    this.props.updateBlueprintComponents(this.props.blueprint.id, components, packages, modules, pendingChanges);
    event.preventDefault();
    event.stopPropagation();
  }

  handleRemoveComponent(event, name) {
    this.props.clearSelectedInput();
    const selectedComponents = this.props.blueprint.packages.concat(this.props.blueprint.modules);
    const { version } = selectedComponents.find((component) => component.name === name);

    const pendingChange = {
      componentOld: `${name}-${version}`,
      componentNew: null,
    };
    let pendingChanges = this.props.blueprint.localPendingChanges;
    const prevChange = pendingChanges.find((change) => change.componentNew === pendingChange.componentOld);
    // if this component was updated in this session...
    if (prevChange !== undefined) {
      // then only list this component once in the list of changes,
      // where the change shows the old version of the previous change
      pendingChange.componentOld = prevChange.componentOld;
      pendingChanges = pendingChanges.filter((component) => component !== prevChange);
      // but if this component was added in this session (i.e. componentOld === null)
      // then neither change should be listed
    }
    if (prevChange === undefined || prevChange.componentOld !== null) {
      pendingChanges = [pendingChange].concat(pendingChanges);
    }
    const packages = this.props.blueprint.packages.filter((pack) => pack.name !== name);
    const modules = this.props.blueprint.modules.filter((module) => module.name !== name);
    const components = this.props.blueprint.components.filter((component) => component.name !== name);
    this.props.updateBlueprintComponents(this.props.blueprint.id, components, packages, modules, pendingChanges);
    event.preventDefault();
    event.stopPropagation();
  }

  handleComponentDetails(event, component) {
    // the user selected a component to view more details on the right
    if (component.name !== this.props.selectedInput.component.name) {
      // if the user did not click on the current selected component:
      this.props.setSelectedInput(component);
      this.props.setSelectedInputParent([]);
    } else {
      // if the user clicked on the current selected component:
      this.props.clearSelectedInput();
    }
    event.preventDefault();
    event.stopPropagation();
  }

  handleComponentListItem(component) {
    this.props.fetchingCompDeps(component, this.props.blueprint.id);
  }

  handleDepListItem(component) {
    this.props.fetchingDepDetails(component, this.props.blueprint.id);
  }

  // handle show/hide of modal dialogs
  handleHideModal() {
    this.props.setModalActive(null);
  }

  handleShowModal(e, modalType) {
    switch (modalType) {
      case "modalPendingChanges":
        // this.getComponentUpdates();
        this.props.setModalActive("modalPendingChanges");
        break;
      default:
        this.props.setModalActive(null);
        break;
    }
    e.preventDefault();
    e.stopPropagation();
  }

  handleDiscardChanges() {
    const workspaceChanges = this.props.blueprint.workspacePendingChanges.length;
    const reload = workspaceChanges > 0;
    this.props.deleteHistory(this.props.blueprint.id, reload);
    // only fetch blueprint contents if workspace changes existed
    // when this blueprint originally loaded
  }

  handleUndo() {
    const workspaceChanges = this.props.blueprint.workspacePendingChanges.length;
    if (this.props.pastLength === 1) {
      const reload = workspaceChanges > 0;
      this.props.deleteHistory(this.props.blueprint.id, reload);
    } else {
      this.props.undo(this.props.blueprint.id, false);
    }
  }

  setNotifications() {
    this.layout.setNotifications();
  }

  getFilteredInputs(event) {
    if (event.which === 13 || event.keyCode === 13) {
      const filter = {
        field: "name",
        value: event.target.value,
      };
      this.props.fetchingInputs(filter, 1, this.state.pageSize);
      this.props.setSelectedInputPage(0);
      $("#cmpsr-blueprint-input-filter").blur();
      event.preventDefault();
    }
  }

  render() {
    if (this.props.blueprint.id === undefined) {
      return <Loading />;
    }
    const blueprintDisplayName = this.props.route.params.blueprint;
    const {
      blueprint,
      selectedComponents,
      dependencies,
      inputComponents,
      inputs,
      modalActive,
      componentsSortKey,
      componentsSortValue,
      componentsFilters,
      pastLength,
      futureLength,
      selectedInputDeps,
      setSelectedInput,
      setSelectedInputParent,
      clearSelectedInput,
    } = this.props;
    const numPendingChanges = blueprint.localPendingChanges.length + blueprint.workspacePendingChanges.length;
    const { formatMessage } = this.props.intl;

    return (
      <Layout
        className="cmpsr-grid__wrapper"
        ref={(c) => {
          this.layout = c;
        }}
      >
        <header className="cmpsr-header">
          <ol className="breadcrumb">
            <li>
              <Link to="/blueprints">
                <FormattedMessage defaultMessage="Back to Blueprints" />
              </Link>
            </li>
            <li>
              <Link to={`/blueprint/${blueprintDisplayName}`}>{blueprintDisplayName}</Link>
            </li>
            <li className="active">
              <strong>
                <FormattedMessage defaultMessage="Edit Packages" />
              </strong>
            </li>
          </ol>
          <div className="cmpsr-header__actions">
            <ul className="list-inline">
              {numPendingChanges > 0 && (
                <li>
                  <a href="#" onClick={(e) => this.handleShowModal(e, "modalPendingChanges")}>
                    <FormattedMessage
                      defaultMessage="{pendingChanges, plural,
                        one {# Pending Change}
                        other {# Pending Changes}
                        }"
                      values={{
                        pendingChanges: numPendingChanges,
                      }}
                    />
                  </a>
                </li>
              )}
              {(numPendingChanges > 0 && (
                <li>
                  <button
                    type="button"
                    className="btn btn-primary"
                    onClick={(e) => this.handleShowModal(e, "modalPendingChanges")}
                  >
                    <FormattedMessage defaultMessage="Commit" />
                  </button>
                </li>
              )) || (
                <li>
                  <button className="btn btn-primary disabled" type="button">
                    <FormattedMessage defaultMessage="Commit" />
                  </button>
                </li>
              )}
              {(numPendingChanges > 0 && (
                <li>
                  <button className="btn btn-default" type="button" onClick={this.handleDiscardChanges}>
                    <FormattedMessage defaultMessage="Discard Changes" />
                  </button>
                </li>
              )) || (
                <li>
                  <button className="btn btn-default disabled" type="button">
                    <FormattedMessage defaultMessage="Discard Changes" />
                  </button>
                </li>
              )}
              <li className="list__subgroup-item--first">
                <CreateImageUpload blueprint={blueprint} layout={this.layout} />
              </li>
              <li>
                <div className="dropdown dropdown-kebab-pf">
                  <button
                    className="btn btn-link dropdown-toggle"
                    type="button"
                    id="dropdownKebab"
                    data-toggle="dropdown"
                    aria-haspopup="true"
                    aria-expanded="false"
                  >
                    <span className="fa fa-ellipsis-v" />
                  </button>
                  <ul className="dropdown-menu dropdown-menu-right" aria-labelledby="dropdownKebab">
                    <li>
                      <ExportBlueprint blueprint={blueprint} />
                    </li>
                  </ul>
                </div>
              </li>
            </ul>
          </div>
          <div className="cmpsr-title">
            <h1 className="cmpsr-title__item">{blueprintDisplayName}</h1>
          </div>
        </header>
        {(inputs.selectedInput.set === false && (
          <h3 className="cmpsr-panel__title cmpsr-panel__title--main">
            <FormattedMessage defaultMessage="Blueprint Components" />
          </h3>
        )) || (
          <h3 className="cmpsr-panel__title cmpsr-panel__title--main">
            <FormattedMessage defaultMessage="Component Details" />
          </h3>
        )}
        {(inputs.selectedInput.set === false && (
          <div className="cmpsr-panel__body cmpsr-panel__body--main">
            {componentsSortKey !== undefined && componentsSortValue !== undefined && (
              <BlueprintToolbar
                emptyState={
                  (selectedComponents === undefined || selectedComponents.length === 0) &&
                  componentsFilters.filterValues.length === 0
                }
                blueprintId={blueprint.id}
                filters={componentsFilters}
                filterRemoveValue={this.props.componentsFilterRemoveValue}
                filterClearValues={this.props.componentsFilterClearValues}
                filterAddValue={this.props.componentsFilterAddValue}
                componentsSortKey={componentsSortKey}
                componentsSortValue={componentsSortValue}
                componentsSortSetValue={this.props.componentsSortSetValue}
                dependenciesSortSetValue={this.props.dependenciesSortSetValue}
                undo={this.handleUndo}
                redo={this.props.redo}
                pastLength={pastLength}
                futureLength={futureLength}
                showUndoRedo
              />
            )}
            <BlueprintContents
              components={selectedComponents}
              dependencies={dependencies}
              handleRemoveComponent={this.handleRemoveComponent}
              handleComponentDetails={this.handleComponentDetails}
              filterClearValues={this.props.componentsFilterClearValues}
              filterValues={componentsFilters.filterValues}
              errorState={this.props.blueprintContentsError}
              fetchingState={this.props.blueprintContentsFetching}
              fetchDetails={this.handleComponentListItem}
              undo={this.handleUndo}
              pastLength={pastLength}
            >
              <EmptyState
                title={formatMessage(messages.addComponentTitle)}
                message={`${formatMessage(messages.addComponentMessageOne)} ${formatMessage(
                  messages.addComponentMessageTwo
                )}`}
              />
            </BlueprintContents>
          </div>
        )) ||
          (inputs.selectedInput.set === true && (
            <ComponentDetailsView
              blueprint={blueprintDisplayName}
              selectedComponents={blueprint.packages.concat(blueprint.modules)}
              component={inputs.selectedInput.component}
              dependencies={selectedInputDeps}
              componentParent={inputs.selectedInput.parent}
              setSelectedInput={setSelectedInput}
              setSelectedInputParent={setSelectedInputParent}
              clearSelectedInput={clearSelectedInput}
              handleComponentDetails={this.handleComponentDetails}
              handleDepListItem={this.handleDepListItem}
              handleAddComponent={this.handleAddComponent}
              handleUpdateComponent={this.handleUpdateComponent}
              handleRemoveComponent={this.handleRemoveComponent}
            />
          ))}
        <h3 className="cmpsr-panel__title cmpsr-panel__title--sidebar">
          {formatMessage(messages.listTitleAvailableComps)}
        </h3>
        {(inputComponents !== undefined && blueprint.components !== undefined && blueprint.packages !== undefined && (
          <div className="cmpsr-panel__body cmpsr-panel__body--sidebar">
            <div className="toolbar-pf">
              <form className="toolbar-pf-actions pf-l-flex pf-m-space-items-none">
                <TextInput
                  className="pf-m-flex-1 pf-u-mb-xs"
                  type="text"
                  id="cmpsr-blueprint-input-filter"
                  aria-label={formatMessage(messages.filterByLabel)}
                  placeholder={formatMessage(messages.filterByPlaceholder)}
                  onKeyPress={(e) => this.getFilteredInputs(e)}
                />
                <Pagination
                  className="pf-m-flex-1 pf-u-flex-nowrap-on-lg pf-u-ml-md-on-md"
                  itemCount={inputs.totalInputs}
                  perPage={this.state.pageSize}
                  page={this.state.page}
                  onSetPage={this.handleSetPage}
                  onPerPageSelect={this.handlePageSizeSelect}
                  isCompact
                  variant={PaginationVariant.bottom}
                  titles={{
                    perPageSuffix: formatMessage(messages.paginationPerPage),
                  }}
                />
              </form>
              {inputs.inputFilters !== undefined && inputs.inputFilters.value.length > 0 && (
                <div className="toolbar-pf-results">
                  <ul className="list-inline">
                    <li>
                      <span className="label label-info">
                        <FormattedMessage
                          defaultMessage="Name: {name}"
                          values={{
                            name: inputs.inputFilters.value,
                          }}
                        />
                        <a href="#" onClick={(e) => this.handleClearFilters(e)}>
                          <span className="pficon pficon-close" />
                        </a>
                      </span>
                    </li>
                    <li>
                      <a href="#" onClick={(e) => this.handleClearFilters(e)}>
                        <FormattedMessage defaultMessage="Clear All Filters" />
                      </a>
                    </li>
                  </ul>
                </div>
              )}
            </div>
            {blueprint.components.length === 0 &&
              Object.keys(this.props.blueprintContentsError).length === 0 &&
              componentsFilters.filterValues.length === 0 && (
                <div className="alert alert-info alert-dismissable">
                  <button
                    type="button"
                    className="close"
                    data-dismiss="alert"
                    aria-hidden="true"
                    aria-label="Dismiss Message"
                  >
                    <span className="pficon pficon-close" />
                  </button>
                  <span className="pficon pficon-info" />
                  <FormattedMessage
                    defaultMessage="{selectComponents} in this list to add to the blueprint."
                    values={{
                      selectComponents: (
                        <strong>
                          <FormattedMessage defaultMessage="Select components" />
                        </strong>
                      ),
                    }}
                  />
                </div>
              )}
            {(inputs.inputFilters !== undefined &&
              inputs.inputFilters.value.length > 0 &&
              inputComponents.length === 0 && (
                <EmptyState
                  title={formatMessage(messages.emptyStateNoResultsTitle)}
                  message={formatMessage(messages.emptyStateNoResultsMessage)}
                >
                  <button className="btn btn-link" type="button" onClick={(e) => this.handleClearFilters(e)}>
                    <FormattedMessage defaultMessage="Clear All Filters" />
                  </button>
                </EmptyState>
              )) ||
              (inputs.loading && <Loading />) || (
                <ComponentInputs
                  label={formatMessage(messages.listTitleAvailableComps)}
                  components={inputComponents}
                  handleComponentDetails={this.handleComponentDetails}
                  handleAddComponent={this.handleAddComponent}
                  handleRemoveComponent={this.handleRemoveComponent}
                />
              )}
          </div>
        )) || (
          <div className="cmpsr-panel__body cmpsr-panel__body--sidebar">
            <div className="toolbar-pf">
              <form className="toolbar-pf-actions">
                <input
                  type="text"
                  className="form-control"
                  id="cmpsr-blueprint-input-filter"
                  aria-label={formatMessage(messages.filterByLabel)}
                  placeholder={formatMessage(messages.filterByPlaceholder)}
                  disabled="disabled"
                />
              </form>
            </div>
            <Loading />
          </div>
        )}
        {modalActive === "modalPendingChanges" ? (
          <PendingChanges
            handleCommit={this.handleCommit}
            blueprint={blueprint}
            contents={dependencies}
            handleHideModal={this.handleHideModal}
          />
        ) : null}
      </Layout>
    );
  }
}

EditBlueprintPage.propTypes = {
  route: PropTypes.shape({
    keys: PropTypes.arrayOf(PropTypes.object),
    load: PropTypes.func,
    page: PropTypes.string,
    params: PropTypes.object,
    path: PropTypes.string,
    pattern: PropTypes.object,
  }),
  blueprint: PropTypes.shape({
    components: PropTypes.arrayOf(PropTypes.object),
    description: PropTypes.string,
    groups: PropTypes.array,
    id: PropTypes.string,
    localPendingChanges: PropTypes.arrayOf(PropTypes.object),
    modules: PropTypes.array,
    name: PropTypes.string,
    packages: PropTypes.arrayOf(PropTypes.object),
    version: PropTypes.string,
    workspacePendingChanges: PropTypes.arrayOf(PropTypes.object),
  }),
  inputs: PropTypes.shape({
    inputComponents: PropTypes.arrayOf(PropTypes.object),
    inputFilters: PropTypes.object,
    loading: PropTypes.bool,
    pageSize: PropTypes.number,
    selectedInput: PropTypes.object,
    selectedInputPage: PropTypes.number,
    totalInputs: PropTypes.number,
  }),
  inputComponents: PropTypes.arrayOf(PropTypes.object),
  modalActive: PropTypes.string,
  selectedInput: PropTypes.shape({
    component: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
    parent: PropTypes.arrayOf(PropTypes.object),
  }),
  selectedInputDeps: PropTypes.arrayOf(PropTypes.object),
  fetchingBlueprintContents: PropTypes.func,
  fetchingCompDeps: PropTypes.func,
  setBlueprint: PropTypes.func,
  updateBlueprintComponents: PropTypes.func,
  fetchingInputs: PropTypes.func,
  fetchingDepDetails: PropTypes.func,
  setSelectedInputPage: PropTypes.func,
  setSelectedInput: PropTypes.func,
  clearSelectedInput: PropTypes.func,
  setSelectedInputParent: PropTypes.func,
  deleteFilter: PropTypes.func,
  setModalActive: PropTypes.func,
  dependenciesSortSetValue: PropTypes.func,
  componentsSortSetValue: PropTypes.func,
  selectedComponents: PropTypes.arrayOf(PropTypes.object),
  dependencies: PropTypes.arrayOf(PropTypes.object),
  componentsSortKey: PropTypes.string,
  componentsSortValue: PropTypes.string,
  componentsFilters: PropTypes.shape({
    defaultFilterType: PropTypes.string,
    filterTypes: PropTypes.arrayOf(PropTypes.object),
    filterValues: PropTypes.arrayOf(PropTypes.object),
  }),
  componentsFilterAddValue: PropTypes.func,
  componentsFilterRemoveValue: PropTypes.func,
  componentsFilterClearValues: PropTypes.func,
  pastLength: PropTypes.number,
  futureLength: PropTypes.number,
  undo: PropTypes.func,
  redo: PropTypes.func,
  deleteHistory: PropTypes.func,
  blueprintContentsError: PropTypes.shape({
    message: PropTypes.string,
    options: PropTypes.object,
    problem: PropTypes.string,
    url: PropTypes.string,
  }),
  blueprintContentsFetching: PropTypes.bool,
  intl: intlShape.isRequired,
};

EditBlueprintPage.defaultProps = {
  route: {},
  blueprint: {},
  inputs: {},
  inputComponents: undefined,
  modalActive: "",
  selectedInput: {},
  fetchingBlueprintContents() {},
  setBlueprint() {},
  fetchingCompDeps() {},
  updateBlueprintComponents() {},
  fetchingInputs() {},
  fetchingDepDetails() {},
  setSelectedInputPage() {},
  setSelectedInput() {},
  clearSelectedInput() {},
  setSelectedInputParent() {},
  selectedInputDeps: undefined,
  deleteFilter() {},
  setModalActive() {},
  dependenciesSortSetValue() {},
  componentsSortSetValue() {},
  selectedComponents: [],
  dependencies: [],
  componentsSortKey: "",
  componentsSortValue: "",
  componentsFilters: {},
  componentsFilterAddValue() {},
  componentsFilterRemoveValue() {},
  componentsFilterClearValues() {},
  pastLength: 0,
  futureLength: 0,
  undo() {},
  redo() {},
  deleteHistory() {},
  blueprintContentsError: {},
  blueprintContentsFetching: true,
};

const makeMapStateToProps = () => {
  const getBlueprintById = makeGetBlueprintById();
  const getSortedSelectedComponents = makeGetSortedSelectedComponents();
  const getSortedDependencies = makeGetSortedDependencies();
  const getFilteredComponents = makeGetFilteredComponents();
  const getPastLength = makeGetPastLength();
  const getFutureLength = makeGetFutureLength();
  const getSelectedInputs = makeGetSelectedInputs();
  const getSelectedDeps = makeGetSelectedDeps();
  const mapStateToProps = (state, props) => {
    if (getBlueprintById(state, props.route.params.blueprint.replace(/\s/g, "-")) !== undefined) {
      const fetchedBlueprint = getBlueprintById(state, props.route.params.blueprint.replace(/\s/g, "-"));
      return {
        blueprint: fetchedBlueprint.present,
        selectedComponents: getFilteredComponents(state, getSortedSelectedComponents(state, fetchedBlueprint.present)),
        dependencies: getFilteredComponents(state, getSortedDependencies(state, fetchedBlueprint.present)),
        componentsSortKey: state.sort.components.key,
        componentsSortValue: state.sort.components.value,
        componentsFilters: state.filter.components,
        inputs: state.inputs,
        inputComponents: getSelectedInputs(state, fetchedBlueprint.present.components),
        selectedInput: state.inputs.selectedInput,
        selectedInputDeps: getSelectedDeps(
          state,
          state.inputs.selectedInput.component.dependencies,
          fetchedBlueprint.present.components
        ),
        modalActive: state.modals.modalActive,
        pastLength: getPastLength(fetchedBlueprint),
        futureLength: getFutureLength(fetchedBlueprint),
        blueprintContentsError: fetchedBlueprint.present.errorState,
        blueprintContentsFetching: !!(
          fetchedBlueprint.present.components === undefined && fetchedBlueprint.present.errorState === undefined
        ),
      };
    }
    return {
      blueprint: {},
      selectedComponents: [],
      dependencies: [],
      componentsSortKey: state.sort.components.key,
      componentsSortValue: state.sort.components.value,
      componentsFilters: state.filter.components,
      inputs: state.inputs,
      inputComponents: state.inputs.inputComponents,
      selectedInput: state.inputs.selectedInput,
      modalActive: state.modals.modalActive,
      pastLength: 0,
      futureLength: 0,
      blueprintContentsError: {},
    };
  };
  return mapStateToProps;
};

const mapDispatchToProps = (dispatch) => ({
  fetchingBlueprintContents: (blueprintId) => {
    dispatch(fetchingBlueprintContents(blueprintId));
  },
  fetchingInputs: (filter, selectedInputPage, pageSize) => {
    dispatch(fetchingInputs(filter, selectedInputPage, pageSize));
  },
  setSelectedInputPage: (selectedInputPage) => {
    dispatch(setSelectedInputPage(selectedInputPage));
  },
  setBlueprint: (blueprint) => {
    dispatch(setBlueprint(blueprint));
  },
  updateBlueprintComponents: (blueprintId, components, packages, modules, pendingChange) => {
    dispatch(updateBlueprintComponents(blueprintId, components, packages, modules, pendingChange));
  },
  setSelectedInput: (selectedInput) => {
    dispatch(setSelectedInput(selectedInput));
  },
  setSelectedInputDeps: (dependencies) => {
    dispatch(setSelectedInputDeps(dependencies));
  },
  setSelectedInputParent: (selectedInputParent) => {
    dispatch(setSelectedInputParent(selectedInputParent));
  },
  clearSelectedInput: () => {
    dispatch(clearSelectedInput());
  },
  deleteFilter: () => {
    dispatch(deleteFilter());
  },
  setModalActive: (modalActive) => {
    dispatch(setModalActive(modalActive));
  },
  componentsSortSetKey: (key) => {
    dispatch(componentsSortSetKey(key));
  },
  componentsSortSetValue: (value) => {
    dispatch(componentsSortSetValue(value));
  },
  dependenciesSortSetKey: (key) => {
    dispatch(dependenciesSortSetKey(key));
  },
  dependenciesSortSetValue: (value) => {
    dispatch(dependenciesSortSetValue(value));
  },
  componentsFilterAddValue: (value) => {
    dispatch(componentsFilterAddValue(value));
  },
  componentsFilterRemoveValue: (value) => {
    dispatch(componentsFilterRemoveValue(value));
  },
  componentsFilterClearValues: (value) => {
    dispatch(componentsFilterClearValues(value));
  },
  undo: (blueprintId, reload) => {
    dispatch(undo(blueprintId, reload));
  },
  redo: (blueprintId, reload) => {
    dispatch(redo(blueprintId, reload));
  },
  deleteHistory: (blueprintId, reload) => {
    dispatch(deleteHistory(blueprintId, reload));
  },
  fetchingCompDeps: (component, blueprintId) => {
    dispatch(fetchingCompDeps(component, blueprintId));
  },
  fetchingDepDetails: (component, blueprintId) => {
    dispatch(fetchingDepDetails(component, blueprintId));
  },
});

export default connect(makeMapStateToProps, mapDispatchToProps)(injectIntl(EditBlueprintPage));