829 lines
28 KiB
JavaScript
829 lines
28 KiB
JavaScript
/*
|
|
* 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 (
|
|
<div>
|
|
{
|
|
_.map(this.props.components, (component) => {
|
|
var icon = AVAILABILITY_STATUS_ICONS[component.get('availability')];
|
|
return (
|
|
<Input
|
|
key={component.id}
|
|
type='checkbox'
|
|
name={component.id}
|
|
label={component.get('label')}
|
|
description={component.get('description')}
|
|
value={component.id}
|
|
checked={component.get('enabled')}
|
|
disabled={component.get('disabled')}
|
|
tooltipIcon={icon}
|
|
tooltipText={component.get('warnings')}
|
|
tooltipPlacement='top'
|
|
onChange={this.props.onChange}
|
|
/>
|
|
);
|
|
})
|
|
}
|
|
</div>
|
|
);
|
|
}
|
|
});
|
|
|
|
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 (
|
|
<div>
|
|
{
|
|
_.map(this.props.components, (component) => {
|
|
var icon = AVAILABILITY_STATUS_ICONS[component.get('availability')];
|
|
return (
|
|
<Input
|
|
key={component.id}
|
|
type='radio'
|
|
name={this.props.groupName}
|
|
label={component.get('label')}
|
|
description={component.get('description')}
|
|
value={component.id}
|
|
checked={this.state.value == component.id}
|
|
disabled={component.get('disabled')}
|
|
tooltipPlacement='top'
|
|
tooltipIcon={icon}
|
|
tooltipText={component.get('warnings')}
|
|
onChange={this.onChange}
|
|
/>
|
|
);
|
|
})
|
|
}
|
|
</div>
|
|
);
|
|
}
|
|
});
|
|
|
|
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'),
|
|
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'),
|
|
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,
|
|
name = this.props.wizard.get('name'),
|
|
nameError = this.props.wizard.get('name_error'),
|
|
release = this.props.wizard.get('release');
|
|
|
|
if (this.props.loading) {
|
|
return null;
|
|
}
|
|
var os = release.get('operating_system'),
|
|
connectivityAlert = i18n('dialog.create_cluster_wizard.name_release.' + os + '_connectivity_alert');
|
|
return (
|
|
<div className='create-cluster-form name-and-release'>
|
|
<Input
|
|
type='text'
|
|
name='name'
|
|
autoComplete='off'
|
|
label={i18n('dialog.create_cluster_wizard.name_release.name')}
|
|
value={name}
|
|
error={nameError}
|
|
onChange={this.props.onChange}
|
|
/>
|
|
<Input
|
|
type='select'
|
|
name='release'
|
|
label={i18n('dialog.create_cluster_wizard.name_release.release_label')}
|
|
value={release.id}
|
|
onChange={this.props.onChange}
|
|
>
|
|
{
|
|
releases.map((release) => {
|
|
if (!release.get('is_deployable')) {
|
|
return null;
|
|
}
|
|
return <option key={release.id} value={release.id}>{release.get('name')}</option>;
|
|
})
|
|
}
|
|
</Input>
|
|
<div className='help-block'>
|
|
{connectivityAlert &&
|
|
<div className='alert alert-warning'>{connectivityAlert}</div>
|
|
}
|
|
<div className='release-description'>{release.get('description')}</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
});
|
|
|
|
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'),
|
|
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) => 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 (
|
|
<div className='wizard-compute-pane'>
|
|
<ComponentCheckboxGroup
|
|
groupName='hypervisor'
|
|
components={this.components}
|
|
onChange={this.props.onChange}
|
|
/>
|
|
{this.constructor.hasErrors(this.props.wizard) &&
|
|
<div className='alert alert-warning'>{i18n('dialog.create_cluster_wizard.compute.empty_choice')}</div>
|
|
}
|
|
</div>
|
|
);
|
|
}
|
|
});
|
|
|
|
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'),
|
|
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) => 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 (
|
|
<ComponentRadioGroup
|
|
groupName='network'
|
|
components={monolithic}
|
|
onChange={this.onChange}
|
|
/>
|
|
);
|
|
},
|
|
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 (
|
|
<ComponentCheckboxGroup
|
|
groupName='ml2'
|
|
components={ml2}
|
|
onChange={this.props.onChange}
|
|
/>
|
|
);
|
|
},
|
|
render() {
|
|
return (
|
|
<div className='wizard-network-pane'>
|
|
{this.renderMonolithicDriverControls()}
|
|
<div className='ml2'>
|
|
{this.renderML2DriverControls()}
|
|
</div>
|
|
{this.constructor.hasErrors(this.props.wizard) &&
|
|
<div className='alert alert-warning'>{i18n('dialog.create_cluster_wizard.network.ml2_empty_choice')}</div>
|
|
}
|
|
</div>
|
|
);
|
|
}
|
|
});
|
|
|
|
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 (
|
|
<div className='wizard-storage-pane'>
|
|
<div className='row'>
|
|
<div className='col-xs-6'>
|
|
<h4>{i18n('dialog.create_cluster_wizard.storage.block')}</h4>
|
|
{this.renderSection(this.components, 'block', this.props.onChange)}
|
|
</div>
|
|
<div className='col-xs-6'>
|
|
<h4>{i18n('dialog.create_cluster_wizard.storage.object')}</h4>
|
|
{this.renderSection(this.components, 'object', this.props.onChange)}
|
|
</div>
|
|
</div>
|
|
<div className='row'>
|
|
<div className='col-xs-6'>
|
|
<h4>{i18n('dialog.create_cluster_wizard.storage.image')}</h4>
|
|
{this.renderSection(this.components, 'image', this.props.onChange)}
|
|
</div>
|
|
<div className='col-xs-6'>
|
|
<h4>{i18n('dialog.create_cluster_wizard.storage.ephemeral')}</h4>
|
|
{this.renderSection(this.components, 'ephemeral', this.props.onChange)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
});
|
|
|
|
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 (
|
|
<div className='wizard-compute-pane'>
|
|
<ComponentCheckboxGroup
|
|
groupName='additionalComponents'
|
|
components={this.components}
|
|
onChange={this.props.onChange}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|
|
});
|
|
|
|
var Finish = React.createClass({
|
|
statics: {
|
|
paneName: 'Finish',
|
|
title: i18n('dialog.create_cluster_wizard.ready.title')
|
|
},
|
|
render() {
|
|
return (
|
|
<p>
|
|
<span>{i18n('dialog.create_cluster_wizard.ready.env_select_deploy')} </span>
|
|
<b>{i18n('dialog.create_cluster_wizard.ready.deploy')} </b>
|
|
<span>{i18n('dialog.create_cluster_wizard.ready.or_make_config_choice')} </span>
|
|
<b>{i18n('dialog.create_cluster_wizard.ready.env')} </b>
|
|
<span>{i18n('dialog.create_cluster_wizard.ready.console')}</span>
|
|
</p>
|
|
);
|
|
}
|
|
});
|
|
|
|
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 (
|
|
<div className='wizard-body'>
|
|
<div className='wizard-steps-box'>
|
|
<div className='wizard-steps-nav col-xs-3'>
|
|
<ul className='wizard-step-nav-item nav nav-pills nav-stacked'>
|
|
{
|
|
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 (
|
|
<li key={pane.title} role='wizard-step'
|
|
className={classes}>
|
|
<a onClick={_.partial(this.goToPane, index)}>{pane.title}</a>
|
|
</li>
|
|
);
|
|
})
|
|
}
|
|
</ul>
|
|
</div>
|
|
{!this.components &&
|
|
<div className='pane-content col-xs-9 pane-progress-bar'>
|
|
<ProgressBar/>
|
|
</div>
|
|
}
|
|
{this.components &&
|
|
<div className='pane-content col-xs-9 forms-box access'>
|
|
<Pane
|
|
ref='pane'
|
|
actionInProgress={this.state.actionInProgress}
|
|
loading={this.state.loading}
|
|
onChange={this.onChange}
|
|
releases={this.releases}
|
|
wizard={this.wizard}
|
|
allComponents={this.components}
|
|
/>
|
|
</div>
|
|
}
|
|
<div className='clearfix'></div>
|
|
</div>
|
|
</div>
|
|
);
|
|
},
|
|
renderFooter() {
|
|
var actionInProgress = this.state.actionInProgress;
|
|
return (
|
|
<div className='wizard-footer'>
|
|
<button className={utils.classNames('btn btn-default pull-left', {disabled: actionInProgress})} data-dismiss='modal'>
|
|
{i18n('common.cancel_button')}
|
|
</button>
|
|
<button
|
|
className={utils.classNames('btn btn-default prev-pane-btn', {disabled: !this.state.previousEnabled || actionInProgress})}
|
|
onClick={this.prevPane}
|
|
>
|
|
<i className='glyphicon glyphicon-arrow-left' aria-hidden='true'></i>
|
|
|
|
<span>{i18n('dialog.create_cluster_wizard.prev')}</span>
|
|
</button>
|
|
{this.state.nextVisible &&
|
|
<button
|
|
className={utils.classNames('btn btn-default btn-success next-pane-btn', {disabled: !this.state.nextEnabled || actionInProgress})}
|
|
onClick={this.nextPane}
|
|
>
|
|
<span>{i18n('dialog.create_cluster_wizard.next')}</span>
|
|
|
|
<i className='glyphicon glyphicon-arrow-right' aria-hidden='true'></i>
|
|
</button>
|
|
}
|
|
{this.state.createVisible &&
|
|
<button
|
|
className={utils.classNames('btn btn-default btn-success finish-btn', {disabled: actionInProgress})}
|
|
onClick={this.saveCluster}
|
|
autoFocus
|
|
>
|
|
{i18n('dialog.create_cluster_wizard.create')}
|
|
</button>
|
|
}
|
|
</div>
|
|
);
|
|
}
|
|
});
|
|
|
|
export default CreateClusterWizard;
|