/* * Copyright 2015 Mirantis, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); you may * not use this file except in compliance with the License. You may obtain * a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations * under the License. **/ import $ from 'jquery'; import _ from 'underscore'; import i18n from 'i18n'; import React from 'react'; import ReactDOM from 'react-dom'; import Backbone from 'backbone'; import models from 'models'; import utils from 'utils'; import {dialogMixin} from 'views/dialogs'; import {Input, ProgressBar} from 'views/controls'; var AVAILABILITY_STATUS_ICONS = { compatible: 'glyphicon-ok-sign', available: 'glyphicon-info-sign', incompatible: 'glyphicon-warning-sign' }; var ComponentCheckboxGroup = React.createClass({ hasEnabledComponents() { return _.any(this.props.components, (component) => component.get('enabled')); }, render() { return (
{ _.map(this.props.components, (component) => { var icon = AVAILABILITY_STATUS_ICONS[component.get('availability')]; return ( ); }) }
); } }); var ComponentRadioGroup = React.createClass({ getInitialState() { var activeComponent = _.find(this.props.components, (component) => component.get('enabled')); return { value: activeComponent && activeComponent.id }; }, hasEnabledComponents() { return _.any(this.props.components, (component) => component.get('enabled')); }, onChange(name, value) { _.each(this.props.components, (component) => { this.props.onChange(component.id, component.id == value); }); this.setState({value: value}); }, render() { return (
{ _.map(this.props.components, (component) => { var icon = AVAILABILITY_STATUS_ICONS[component.get('availability')]; return ( ); }) }
); } }); var ClusterWizardPanesMixin = { componentWillMount() { if (this.props.allComponents) { this.components = this.props.allComponents.getComponentsByType( this.constructor.componentType, {sorted: true} ); this.processRestrictions(this.components); } }, componentDidMount() { $(ReactDOM.findDOMNode(this)).find('input:enabled').first().focus(); }, areComponentsMutuallyExclusive(components) { if (components.length <= 1) { return false; } var allComponentsExclusive = _.all(components, (component) => { var peerIds = _.pluck(_.reject(components, {id: component.id}), 'id'); var incompatibleIds = _.pluck(_.pluck(component.get('incompatible'), 'component'), 'id'); // peerIds should be subset of incompatibleIds to have exclusiveness property return peerIds.length == _.intersection(peerIds, incompatibleIds).length; }); return allComponentsExclusive; }, processRestrictions(paneComponents, types, stopList = []) { this.processIncompatible(paneComponents, types, stopList); this.processRequires(paneComponents, types); }, processCompatible(allComponents, paneComponents, types, stopList = []) { // all previously enabled components // should be compatible with the current component _.each(paneComponents, (component) => { // skip already disabled if (component.get('disabled')) { return; } // index of compatible elements var compatibleComponents = {}; _.each(component.get('compatible'), (compatible) => { compatibleComponents[compatible.component.id] = compatible; }); // scan all components to find enabled // and not present in the index var isCompatible = true; var warnings = []; allComponents.each((testedComponent) => { var type = testedComponent.get('type'); var isInStopList = _.find(stopList, (component) => component.id == testedComponent.id); if (component.id == testedComponent.id || !_.contains(types, type) || isInStopList) { // ignore self or forward compatibilities return; } if (testedComponent.get('enabled') && !compatibleComponents[testedComponent.id]) { warnings.push(testedComponent.get('label')); isCompatible = false; } }); component.set({ isCompatible: isCompatible, warnings: isCompatible ? i18n('dialog.create_cluster_wizard.compatible') : i18n('dialog.create_cluster_wizard.incompatible_list') + warnings.join(', '), availability: (isCompatible ? 'compatible' : 'available') }); }); }, processIncompatible(paneComponents, types, stopList) { // disable components that have // incompatible components already enabled _.each(paneComponents, (component) => { var incompatibles = component.get('incompatible') || []; var isDisabled = false; var warnings = []; _.each(incompatibles, (incompatible) => { var type = incompatible.component.get('type'); var isInStopList = _.find( stopList, (component) => component.id == incompatible.component.id ); if (!_.contains(types, type) || isInStopList) { // ignore forward incompatibilities return; } if (incompatible.component.get('enabled')) { isDisabled = true; warnings.push(incompatible.message); } }); component.set({ disabled: isDisabled, warnings: warnings.join(' '), enabled: isDisabled ? false : component.get('enabled'), availability: 'incompatible' }); }); }, processRequires(paneComponents, types) { // if component has requires, // it is disabled until all requires are already enabled _.each(paneComponents, (component) => { // skip already disabled components if (component.get('disabled')) { return; } var requires = component.get('requires') || []; if (requires.length == 0) { // no requires component.set({isRequired: false}); return; } var isDisabled = false; var warnings = []; _.each(requires, (require) => { var type = require.component.get('type'); if (!_.contains(types, type)) { // ignore forward requires return; } if (!require.component.get('enabled')) { isDisabled = true; warnings.push(require.message); } }); component.set({ disabled: isDisabled, isRequired: true, warnings: isDisabled ? warnings.join(' ') : null, enabled: isDisabled ? false : component.get('enabled'), availability: 'incompatible' }); }); }, selectActiveComponent(components) { var active = _.find(components, (component) => component.get('enabled')); if (active && !active.get('disabled')) { return; } var newActive = _.find(components, (component) => !component.get('disabled')); if (newActive) { newActive.set({enabled: true}); } if (active) { active.set({enabled: false}); } } }; var NameAndRelease = React.createClass({ mixins: [ClusterWizardPanesMixin], statics: { paneName: 'NameAndRelease', title: i18n('dialog.create_cluster_wizard.name_release.title'), hasErrors(wizard) { return !!wizard.get('name_error'); } }, isValid() { var wizard = this.props.wizard; var [name, cluster, clusters] = [ wizard.get('name'), wizard.get('cluster'), wizard.get('clusters') ]; // test cluster name is already taken if (clusters.findWhere({name: name})) { var error = i18n('dialog.create_cluster_wizard.name_release.existing_environment', {name: name}); wizard.set({name_error: error}); return false; } // validate cluster fields cluster.isValid(); if (cluster.validationError && cluster.validationError.name) { wizard.set({name_error: cluster.validationError.name}); return false; } return true; }, render() { var releases = this.props.releases; var name = this.props.wizard.get('name'); var nameError = this.props.wizard.get('name_error'); var release = this.props.wizard.get('release'); if (this.props.loading) { return null; } var os = release.get('operating_system'); var connectivityAlert = i18n( 'dialog.create_cluster_wizard.name_release.' + os + '_connectivity_alert' ); return (
{ releases.map((release) => { if (!release.get('is_deployable')) { return null; } return ; }) }
{connectivityAlert &&
{connectivityAlert}
}
{release.get('description')}
); } }); var Compute = React.createClass({ mixins: [ClusterWizardPanesMixin], statics: { paneName: 'Compute', componentType: 'hypervisor', title: i18n('dialog.create_cluster_wizard.compute.title'), vCenterPath: 'hypervisor:vmware', vCenterNetworkBackends: ['network:neutron:ml2:nsx', 'network:neutron:ml2:dvs'], hasErrors(wizard) { var allComponents = wizard.get('components'); var components = allComponents.getComponentsByType(this.componentType, {sorted: true}); return !_.any(components, (component) => component.get('enabled')); } }, checkVCenter(allComponents) { // TODO remove this hack in 9.0 var hasCompatibleBackends = _.any(allComponents.models, (component) => { return _.contains(this.constructor.vCenterNetworkBackends, component.id); }); if (!hasCompatibleBackends) { var vCenter = _.find(allComponents.models, (component) => { return component.id == this.constructor.vCenterPath; }); vCenter.set({ disabled: true, warnings: i18n('dialog.create_cluster_wizard.compute.vcenter_requires_network_backend') }); } }, render() { this.processRestrictions(this.components, ['hypervisor']); this.checkVCenter(this.props.allComponents); return (
{this.constructor.hasErrors(this.props.wizard) &&
{i18n('dialog.create_cluster_wizard.compute.empty_choice')}
}
); } }); var Network = React.createClass({ mixins: [ClusterWizardPanesMixin], statics: { paneName: 'Network', panesForRestrictions: ['hypervisor', 'network'], componentType: 'network', title: i18n('dialog.create_cluster_wizard.network.title'), ml2CorePath: 'network:neutron:core:ml2', hasErrors(wizard) { var allComponents = wizard.get('components'); var components = allComponents.getComponentsByType(this.componentType, {sorted: true}); var ml2core = _.find(components, (component) => component.id == this.ml2CorePath); if (ml2core && ml2core.get('enabled')) { var ml2 = _.filter(components, (component) => component.isML2Driver()); return !_.any(ml2, (ml2driver) => ml2driver.get('enabled')); } return false; } }, onChange(name, value) { this.props.onChange(name, value); // reset all ml2 drivers if ml2 core unselected var component = _.find(this.components, (component) => component.id == name); if (!component.isML2Driver() && component.id != this.constructor.ml2CorePath) { _.each(this.components, (component) => { if (component.isML2Driver()) { component.set({enabled: false}); } }); } }, renderMonolithicDriverControls() { var monolithic = _.filter(this.components, (component) => !component.isML2Driver()); var hasMl2 = _.any(this.components, (component) => component.isML2Driver()); if (!hasMl2) { monolithic = _.filter(monolithic, (component) => { return component.id != this.constructor.ml2CorePath; }); } this.processRestrictions(monolithic, this.constructor.panesForRestrictions); this.processCompatible( this.props.allComponents, monolithic, this.constructor.panesForRestrictions, monolithic ); this.selectActiveComponent(monolithic); return ( ); }, renderML2DriverControls() { var ml2 = _.filter(this.components, (component) => component.isML2Driver()); this.processRestrictions(ml2, this.constructor.panesForRestrictions); this.processCompatible(this.props.allComponents, ml2, this.constructor.panesForRestrictions); return ( ); }, render() { return (
{this.renderMonolithicDriverControls()}
{this.renderML2DriverControls()}
{this.constructor.hasErrors(this.props.wizard) &&
{i18n('dialog.create_cluster_wizard.network.ml2_empty_choice')}
}
); } }); var Storage = React.createClass({ mixins: [ClusterWizardPanesMixin], statics: { paneName: 'Storage', panesForRestrictions: ['hypervisor', 'network', 'storage'], componentType: 'storage', title: i18n('dialog.create_cluster_wizard.storage.title') }, renderSection(components, type) { var sectionComponents = _.filter(components, (component) => component.get('subtype') == type); var isRadio = this.areComponentsMutuallyExclusive(sectionComponents); this.processRestrictions( sectionComponents, this.constructor.panesForRestrictions, (isRadio ? sectionComponents : []) ); this.processCompatible( this.props.allComponents, sectionComponents, this.constructor.panesForRestrictions, isRadio ? sectionComponents : [] ); return ( React.createElement((isRadio ? ComponentRadioGroup : ComponentCheckboxGroup), { groupName: type, components: sectionComponents, onChange: this.props.onChange }) ); }, render() { this.processRestrictions(this.components, this.constructor.panesForRestrictions); this.processCompatible( this.props.allComponents, this.components, this.constructor.panesForRestrictions ); return (

{i18n('dialog.create_cluster_wizard.storage.block')}

{this.renderSection(this.components, 'block', this.props.onChange)}

{i18n('dialog.create_cluster_wizard.storage.object')}

{this.renderSection(this.components, 'object', this.props.onChange)}

{i18n('dialog.create_cluster_wizard.storage.image')}

{this.renderSection(this.components, 'image', this.props.onChange)}

{i18n('dialog.create_cluster_wizard.storage.ephemeral')}

{this.renderSection(this.components, 'ephemeral', this.props.onChange)}
); } }); var AdditionalServices = React.createClass({ mixins: [ClusterWizardPanesMixin], statics: { paneName: 'AdditionalServices', panesForRestrictions: ['hypervisor', 'network', 'storage', 'additional_service'], componentType: 'additional_service', title: i18n('dialog.create_cluster_wizard.additional.title') }, render() { this.processRestrictions(this.components, this.constructor.panesForRestrictions); this.processCompatible( this.props.allComponents, this.components, this.constructor.panesForRestrictions ); return (
); } }); var Finish = React.createClass({ statics: { paneName: 'Finish', title: i18n('dialog.create_cluster_wizard.ready.title') }, render() { return (

{i18n('dialog.create_cluster_wizard.ready.env_select_deploy')} {i18n('dialog.create_cluster_wizard.ready.deploy')} {i18n('dialog.create_cluster_wizard.ready.or_make_config_choice')} {i18n('dialog.create_cluster_wizard.ready.env')} {i18n('dialog.create_cluster_wizard.ready.console')}

); } }); var clusterWizardPanes = [ NameAndRelease, Compute, Network, Storage, AdditionalServices, Finish ]; var CreateClusterWizard = React.createClass({ mixins: [dialogMixin], getInitialState() { return { title: i18n('dialog.create_cluster_wizard.title'), loading: true, activePaneIndex: 0, maxAvailablePaneIndex: 0, panes: clusterWizardPanes, paneHasErrors: false, previousAvailable: true, nextAvailable: true, createEnabled: false }; }, componentWillMount() { this.stopHandlingKeys = false; this.wizard = new Backbone.DeepModel(); this.settings = new models.Settings(); this.releases = new models.Releases(); this.cluster = new models.Cluster(); this.wizard.set({cluster: this.cluster, clusters: this.props.clusters}); }, componentDidMount() { this.releases.fetch().done(() => { var defaultRelease = this.releases.findWhere({is_deployable: true}); this.wizard.set('release', defaultRelease.id); this.selectRelease(defaultRelease.id); this.setState({loading: false}); }); this.updateState({activePaneIndex: 0}); }, getListOfTypesToRestore(currentIndex, maxIndex) { var panesTypes = []; _.each(clusterWizardPanes, (pane, paneIndex) => { if ((paneIndex <= maxIndex) && (paneIndex > currentIndex) && pane.componentType) { panesTypes.push(pane.componentType); } }, this); return panesTypes; }, updateState(nextState) { var numberOfPanes = this.getEnabledPanes().length; var nextActivePaneIndex = _.isNumber(nextState.activePaneIndex) ? nextState.activePaneIndex : this.state.activePaneIndex; var pane = clusterWizardPanes[nextActivePaneIndex]; var paneHasErrors = _.isFunction(pane.hasErrors) ? pane.hasErrors(this.wizard) : false; var newState = _.merge(nextState, { activePaneIndex: nextActivePaneIndex, previousEnabled: nextActivePaneIndex > 0, nextEnabled: !paneHasErrors, nextVisible: (nextActivePaneIndex < numberOfPanes - 1), createVisible: nextActivePaneIndex == numberOfPanes - 1, paneHasErrors: paneHasErrors }); this.setState(newState); }, getEnabledPanes() { return _.reject(this.state.panes, 'hidden'); }, getActivePane() { var panes = this.getEnabledPanes(); return panes[this.state.activePaneIndex]; }, isCurrentPaneValid() { var pane = this.refs.pane; if (pane && _.isFunction(pane.isValid) && !pane.isValid()) { this.updateState({paneHasErrors: true}); return false; } return true; }, prevPane() { // check for pane's validation errors if (!this.isCurrentPaneValid()) { return; } this.updateState({activePaneIndex: this.state.activePaneIndex - 1}); }, nextPane() { // check for pane's validation errors if (!this.isCurrentPaneValid()) { return; } var nextIndex = this.state.activePaneIndex + 1; this.updateState({ activePaneIndex: nextIndex, maxAvailablePaneIndex: _.max([nextIndex, this.state.maxAvailablePaneIndex]) }); }, goToPane(index) { if (index > this.state.maxAvailablePaneIndex) { return; } // check for pane's validation errors if (!this.isCurrentPaneValid()) { return; } this.updateState({activePaneIndex: index}); }, saveCluster() { if (this.stopHandlingKeys) { return; } this.stopHandlingKeys = true; this.setState({actionInProgress: true}); var cluster = this.cluster; cluster.set({components: this.components}); var deferred = cluster.save(); if (deferred) { this.updateState({disabled: true}); deferred .done(() => { this.props.clusters.add(cluster); this.close(); app.nodeNetworkGroups.fetch(); app.navigate('#cluster/' + this.cluster.id, {trigger: true}); }) .fail((response) => { this.stopHandlingKeys = false; this.setState({actionInProgress: false}); if (response.status == 409) { this.updateState({disabled: false, activePaneIndex: 0}); cluster.trigger('invalid', cluster, {name: utils.getResponseText(response)}); } else { this.close(); utils.showErrorDialog({ response: response, title: i18n('dialog.create_cluster_wizard.create_cluster_error.title') }); } }); } }, selectRelease(releaseId) { var release = this.releases.findWhere({id: releaseId}); this.wizard.set({release: release}); this.cluster.set({release: releaseId}); // fetch components based on releaseId this.setState({loading: true}); this.components = new models.ComponentsCollection([], {releaseId: releaseId}); this.wizard.set({components: this.components}); this.components.fetch().done(() => { this.components.invoke('expandWildcards', this.components); this.components.invoke('restoreDefaultValue', this.components); this.setState({loading: false}); }); }, onChange(name, value) { var maxAvailablePaneIndex = this.state.maxAvailablePaneIndex; switch (name) { case 'name': this.wizard.set('name', value); this.cluster.set('name', value); this.wizard.unset('name_error'); break; case 'release': this.selectRelease(parseInt(value, 10)); break; default: maxAvailablePaneIndex = this.state.activePaneIndex; var panesToRestore = this.getListOfTypesToRestore( this.state.activePaneIndex, this.state.maxAvailablePaneIndex ); if (panesToRestore.length > 0) { this.components.restoreDefaultValues(panesToRestore); } var component = this.components.findWhere({id: name}); component.set({enabled: value}); break; } this.updateState({maxAvailablePaneIndex: maxAvailablePaneIndex}); }, onKeyDown(e) { if (this.state.actionInProgress) { return; } if (e.key == 'Enter') { e.preventDefault(); if (this.getActivePane().paneName == 'Finish') { this.saveCluster(); } else { this.nextPane(); } } }, renderBody() { var activeIndex = this.state.activePaneIndex; var Pane = this.getActivePane(); return (
    { this.state.panes.map((pane, index) => { var classes = utils.classNames('wizard-step', { disabled: index > this.state.maxAvailablePaneIndex, available: index <= this.state.maxAvailablePaneIndex && index != activeIndex, active: index == activeIndex }); return (
  • {pane.title}
  • ); }) }
{!this.components &&
} {this.components &&
}
); }, renderFooter() { var actionInProgress = this.state.actionInProgress; return (
{this.state.nextVisible && } {this.state.createVisible && }
); } }); export default CreateClusterWizard;