Add VMware UI settings tab to Cluster page

Add VMware related functions to FuelWeb UI
by adding a separate VMware tab to Cluster page

Change-Id: Idf29b85b2793cde0fd491d26e3cb5ee99da06ad9
Implements: blueprint vmware-ui-settings
This commit is contained in:
Anton Zemlyanov 2015-01-28 12:52:56 +04:00
parent 2df71c76d6
commit b9f332bd4d
8 changed files with 761 additions and 4 deletions

View File

@ -60,7 +60,7 @@ define([
}
};
var restrictionMixin = {
var restrictionMixin = models.restrictionMixin = {
expandRestrictions: function(restrictions, path) {
path = path || 'restrictions';
this.expandedRestrictions = this.expandedRestrictions || {};

View File

@ -30,9 +30,10 @@ define(
'jsx!views/cluster_page_tabs/settings_tab',
'jsx!views/cluster_page_tabs/logs_tab',
'jsx!views/cluster_page_tabs/actions_tab',
'jsx!views/cluster_page_tabs/healthcheck_tab'
'jsx!views/cluster_page_tabs/healthcheck_tab',
'plugins/vmware/vmware'
],
function($, _, i18n, Backbone, React, utils, models, dispatcher, componentMixins, dialogs, NodesTab, NetworkTab, SettingsTab, LogsTab, ActionsTab, HealthCheckTab) {
function($, _, i18n, Backbone, React, utils, models, dispatcher, componentMixins, dialogs, NodesTab, NetworkTab, SettingsTab, LogsTab, ActionsTab, HealthCheckTab, VmWareTab) {
'use strict';
var ClusterPage, ClusterInfo, DeploymentResult, DeploymentControl,
@ -172,7 +173,7 @@ function($, _, i18n, Backbone, React, utils, models, dispatcher, componentMixins
if (this.hasChanges()) return i18n('dialog.dismiss_settings.default_message');
},
getAvailableTabs: function() {
return [
var tabs = [
{url: 'nodes', tab: NodesTab},
{url: 'network', tab: NetworkTab},
{url: 'settings', tab: SettingsTab},
@ -180,6 +181,13 @@ function($, _, i18n, Backbone, React, utils, models, dispatcher, componentMixins
{url: 'healthcheck', tab: HealthCheckTab},
{url: 'actions', tab: ActionsTab}
];
var settings = this.props.cluster.get('settings'),
useVCenter = settings.get('common.use_vcenter').value,
index = _.findIndex(tabs, {url: 'settings'});
if (useVCenter) {
tabs.splice(index + 1, 0, {url: 'vmware', tab: VmWareTab});
}
return tabs;
},
checkTab: function(props) {
props = props || this.props;

Binary file not shown.

After

Width:  |  Height:  |  Size: 829 B

View File

@ -0,0 +1,57 @@
@legend-fg: #222222;
@legend-bg: #ececec;
// box-sizing
.box-sizing-mixin() {
box-sizing: border-box;
-moz-box-sizing: border-box;
-webkit-box-sizing: border-box;
}
.tab-vmware-normal {
// TODO replace the icon with a real one
background: url('/static/plugins/vmware/icons.png') no-repeat 0px 0px;
}
.vmware {
.parameter-description {
padding: 10px 5px !important;
}
.label-wrapper {
margin-top: 8px;
width: 185px;
}
legend.vmware {
display: block;
margin-bottom: 5px;
background-color: @legend-bg;
border-radius: 4px;
padding: 12px;
height: 42px;
margin-top: 25px;
font-size: 18px;
color: @legend-fg;
line-height: 18px;
.box-sizing-mixin;
}
.page-control-box {
margin: 10px 20px;
}
.idented {
margin-left: 40px;
}
.password input {
width: 176px;
}
.nova-compute .btn {
margin: 0px;
padding: 0px;
width: 20px;
}
}

View File

@ -0,0 +1,23 @@
{
"en-US": {
"translation": {
"cluster_page": {
"tabs": {
"vmware": "VMware"
}
},
"vmware": {
"title": "VMware vCenter Settings",
"availability_zones": "vCenter",
"network": "Network",
"glance": "Glance",
"cinder": "Cinder",
"reset_to_defaults": "Load Defaults",
"cancel": "Cancel Changes",
"apply": "Save Changes",
"nova_computes": "Nova Computes",
"nova_compute": "Nova Compute Instance"
}
}
}
}

View File

@ -0,0 +1,31 @@
/*
* 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.
**/
define(
[
'jsx!./vmware_tab',
'json!./translations.json',
'less!./styles',
'jquery',
'underscore'
],
function(VmWareTab, translations, styles, $, _) {
'use strict';
_.merge($.i18n.options.resStore, translations);
return VmWareTab;
});

View File

@ -0,0 +1,286 @@
/*
* 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.
**/
define(
[
'jquery',
'underscore',
'i18n',
'backbone',
'models'
],
function($, _, i18n, Backbone, models) {
'use strict';
function isRegularField(field) {
return _.contains(['text', 'password', 'checkbox'], field.type);
}
// models for testing restrictions
var restrictionModels = {};
// Test regex using regex cache
var regexCache = {};
function testRegex(regexText, value) {
if (!regexCache[regexText]) {
regexCache[regexText] = new RegExp(regexText);
}
return regexCache[regexText].test(value);
}
var BaseModel = Backbone.Model.extend(models.restrictionMixin).extend({
constructorName: 'BaseModel',
toJSON: function() {
return _.omit(this.attributes, 'metadata');
},
validate: function() {
this.expandedRestrictions = this.expandedRestrictions || {};
var result = {};
_.each(this.attributes.metadata, function(field) {
if (!isRegularField(field) || field.type == 'checkbox') {
return;
}
var isDisabled = this.checkRestrictions(restrictionModels, undefined, field.name);
if (isDisabled.result) {
return;
}
var value = this.get(field.name);
if (field.regex) {
if (!testRegex(field.regex.source, value)) {
result[field.name] = field.regex.error;
}
}
}, this);
return _.isEmpty(result) ? null : result;
},
parseRestrictions: function() {
var metadata = this.get('metadata');
_.each(metadata, function(field) {
var key = field.name,
restrictions = field.restrictions || [],
childModel = this.get(key);
this.expandRestrictions(restrictions, key);
if (_.isFunction(childModel.parseRestrictions)) {
childModel.parseRestrictions();
}
}, this);
},
testRestrictions: function() {
var results = {
hide: {},
disable: {}
};
var metadata = this.get('metadata');
_.each(metadata, function(field) {
var key = field.name;
var disableResult = this.checkRestrictions(restrictionModels, undefined, key);
results.disable[key] = disableResult.result;
var hideResult = this.checkRestrictions(restrictionModels, 'hide', key);
results.hide[key] = hideResult.result;
}, this);
return results;
}
});
var BaseCollection = Backbone.Collection.extend({
constructorName: 'BaseCollection',
model: BaseModel,
isValid: function() {
this.validationError = this.validate();
return this.validationError;
},
validate: function() {
var errors = _.compact(this.models.map(function(model) {
model.isValid();
return model.validationError;
}));
return _.isEmpty(errors) ? null : errors;
},
parseRestrictions: function() {
_.invoke(this.models, 'parseRestrictions');
},
testRestrictions: function() {
_.invoke(this.models, 'testRestrictions', restrictionModels);
}
});
var Cinder = BaseModel.extend({constructorName: 'Cinder'});
var NovaCompute = BaseModel.extend({constructorName: 'NovaCompute'});
var NovaComputes = BaseCollection.extend({
constructorName: 'NovaComputes',
model: NovaCompute
});
var AvailabilityZone = BaseModel.extend({
constructorName: 'AvailabilityZone',
constructor: function(data) {
Backbone.Model.apply(this, arguments);
if (data) {
this.set(this.parse(data));
}
},
parse: function(response) {
var result = {};
var metadata = response.metadata;
result.metadata = metadata;
// regular fields
_.each(metadata, function(field) {
if (isRegularField(field)) {
result[field.name] = response[field.name];
}
}, this);
// nova_computes
var novaMetadata = _.find(metadata, {name: 'nova_computes'});
var novaValues = _.clone(response.nova_computes);
novaValues = _.map(novaValues, function(value) {
value.metadata = novaMetadata.fields;
return new NovaCompute(value);
});
result.nova_computes = new NovaComputes(novaValues);
// cinder
var cinderMetadata = _.find(metadata, {name: 'cinder'});
var cinderValue = _.extend(_.clone(response.cinder), {metadata: cinderMetadata.fields});
result.cinder = new Cinder(cinderValue);
return result;
},
toJSON: function() {
var result = _.omit(this.attributes, 'metadata', 'nova_computes', 'cinder');
result.nova_computes = this.get('nova_computes').toJSON();
result.cinder = this.get('cinder').toJSON();
return result;
},
validate: function() {
var errors = _.merge({}, BaseModel.prototype.validate.call(this));
var novaComputes = this.get('nova_computes');
novaComputes.isValid();
if (novaComputes.validationError) {
errors.nova_computes = novaComputes.validationError;
}
var cinder = this.get('cinder');
cinder.isValid();
if (cinder.validationError) {
errors.cinder = cinder.validationError;
}
return _.isEmpty(errors) ? null : errors;
}
});
var AvailabilityZones = BaseCollection.extend({
constructorName: 'AvailabilityZones',
model: AvailabilityZone
});
var Network = BaseModel.extend({constructorName: 'Network'});
var Glance = BaseModel.extend({constructorName: 'Glance'});
var VCenter = BaseModel.extend({
constructorName: 'VCenter',
url: function() {
return '/api/v1/clusters/' + this.id + '/vmware_attributes' + (this.loadDefaults ? '/defaults' : '');
},
parse: function(response) {
if (!response.editable || !response.editable.metadata || !response.editable.value) {
return;
}
var metadata = response.editable.metadata || [],
value = response.editable.value || {};
// Availability Zone(s)
var azMetadata = _.find(metadata, {name: 'availability_zones'});
var azValues = _.clone(value.availability_zones);
azValues = _.map(azValues, function(value) {
value.metadata = azMetadata.fields;
return value;
});
// Network
var networkMetadata = _.find(metadata, {name: 'network'});
var networkValue = _.extend(_.clone(value.network), {metadata: networkMetadata.fields});
// Glance
var glanceMetadata = _.find(metadata, {name: 'glance'});
var glanceValue = _.extend(_.clone(value.glance), {metadata: glanceMetadata.fields});
return {
metadata: metadata,
availability_zones: new AvailabilityZones(azValues),
network: new Network(networkValue),
glance: new Glance(glanceValue)
};
},
isFilled: function() {
var result = this.get('availability_zones') && this.get('network') && this.get('glance');
return !!result;
},
toJSON: function() {
if (!this.isFilled()) {
return {};
}
return {
editable: {
value: {
availability_zones: this.get('availability_zones').toJSON(),
network: this.get('network').toJSON(),
glance: this.get('glance').toJSON()
}
}
};
},
validate: function() {
if (!this.isFilled()) {
return null;
}
var errors = {};
_.each(this.get('metadata'), function(field) {
var key = field.name;
var model = this.get(key);
// do not validate disabled restrictions
var isDisabled = this.checkRestrictions(restrictionModels, undefined, key);
if (isDisabled.result) {
return;
}
model.isValid();
if (model.validationError) {
errors[key] = model.validationError;
}
}, this);
return _.isEmpty(errors) ? null : errors;
},
setModels: function(models) {
restrictionModels = models;
}
});
return {
VCenter: VCenter,
AvailabilityZone: AvailabilityZone,
Network: Network,
Glance: Glance,
NovaCompute: NovaCompute,
isRegularField: isRegularField
};
});

View File

@ -0,0 +1,352 @@
/*
* 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.
**/
define(
[
'react',
'jquery',
'i18n',
'underscore',
'dispatcher',
'jsx!views/controls',
'jsx!component_mixins',
'plugins/vmware/vmware_models'
], function(React, $, i18n, _, dispatcher, controls, componentMixins, vmwareModels) {
'use strict';
var cx = React.addons.classSet;
var Field = React.createClass({
onChange: function(name, value) {
this.props.model.set(name, value);
this.setState({model: this.props.model});
_.defer(function() {dispatcher.trigger('vcenter_model_update'); });
},
render: function() {
var metadata = this.props.metadata,
options = this.props.options || [],
errors = this.props.model.validationError,
errorText = errors ? errors[metadata.name] : null;
var classes = cx({
'settings-group table-wrapper parameter-box': true,
password: metadata.type == 'password',
'has-error': !!errorText
});
return (
<div key={metadata.name} className={classes}>
<controls.Input
key={metadata.name}
{... _.pick(metadata, 'name', 'type', 'label')}
value={this.props.model.get(metadata.name)}
checked={this.props.model.get(metadata.name)}
description={errorText || metadata.description}
descriptionClassName={cx({'validation-error': errorText})}
toggleable={metadata.type == 'password'}
wrapperClassName='tablerow-wrapper'
onChange={this.onChange}
disabled={this.props.disabled}
>
{options.map(function(index) {
return <option key={index.label} value={index.value}>{index.label}</option>;
}, this)}
</controls.Input>
</div>
);
}
});
var FieldGroup = React.createClass({
render: function() {
var metadata = _.filter(this.props.model.get('metadata'), vmwareModels.isRegularField);
var fields = metadata.map(function(meta) {
return (
<Field {... _.pick(this.props, 'model', 'disabled')} key={meta.name} metadata={meta}/>
);
}, this);
return (
<div>
{fields}
</div>
);
}
});
var Cinder = React.createClass({
render: function() {
var model = this.props.model;
if (!model) {
return null;
}
return (
<div className='cinder'>
<legend className='vmware'>{i18n('vmware.cinder')}</legend>
<FieldGroup model={model} disabled={this.props.disabled}/>
</div>
);
}
});
var NovaCompute = React.createClass({
render: function() {
var model = this.props.model;
if (!model) {
return null;
}
var removeButtonClasses = cx({'btn btn-link': true, hide: this.props.isRemovable});
return (
<div className='nova-compute'>
<h4>
<button className='btn btn-link'
disabled={this.props.disabled}
onClick={_.bind(function() {this.props.onAdd(model)}, this)}>
<i className='icon-plus-circle'></i>
</button>
<button className={removeButtonClasses}
disabled={this.props.disabled}
onClick={_.bind(function() {this.props.onRemove(model)}, this)}>
<i className='icon-minus-circle'></i>
</button>
&thinsp;
{i18n('vmware.nova_compute')}
</h4>
<FieldGroup model={model} disabled={this.props.disabled}/>
</div>
);
}
});
var AvailabilityZone = React.createClass({
addNovaCompute: function(current) {
var collection = this.props.model.get('nova_computes'),
index = collection.indexOf(current);
collection.add(current.clone(), {at: index + 1});
collection.parseRestrictions();
this.setState({model: this.props.model});
_.defer(function() {dispatcher.trigger('vcenter_model_update'); });
},
removeNovaCompute: function(current) {
var collection = this.props.model.get('nova_computes');
collection.remove(current);
this.setState({model: this.props.model});
_.defer(function() { dispatcher.trigger('vcenter_model_update'); });
},
renderFields: function() {
var model = this.props.model,
meta = model.get('metadata');
meta = _.filter(meta, vmwareModels.isRegularField);
return (
<FieldGroup model={model} disabled={this.props.disabled}/>
);
},
renderComputes: function(actions) {
var novaComputes = this.props.model.get('nova_computes'),
isSingleInstance = (novaComputes.length == 1),
disabled = actions.disable.cinder;
return (
<div className='idented'>
<legend className='vmware'>{i18n('vmware.nova_computes')}</legend>
{
novaComputes.map(function(value) {
return (
<NovaCompute
key={value.cid}
model={value}
onAdd={this.addNovaCompute}
onRemove={this.removeNovaCompute}
isRemovable={isSingleInstance}
disabled={disabled || this.props.disabled}
/>
);
}, this)
}
</div>
);
},
renderCinder: function(actions) {
var disabled = actions.disable.cinder;
return (
<Cinder model={this.props.model.get('cinder')} disabled={disabled || this.props.disabled}/>
);
},
render: function() {
var restrictActions = this.props.model.testRestrictions();
return (
<div>
{this.renderFields(restrictActions)}
{this.renderComputes(restrictActions)}
{this.renderCinder(restrictActions)}
</div>
);
}
});
var AvailabilityZones = React.createClass({
render: function() {
if (!this.props.collection) {
return null;
}
return (
<div className='availability-zones'>
<legend className='vmware'>{i18n('vmware.availability_zones')}</legend>
{
this.props.collection.map(function(model) {
return <AvailabilityZone key={model.cid} model={model} disabled={this.props.disabled}/>;
}, this)
}
</div>
);
}
});
var Network = React.createClass({
render: function() {
var model = this.props.model;
if (!model) {
return null;
}
return (
<div className='network'>
<legend className='vmware'>{i18n('vmware.network')}</legend>
<FieldGroup model={model} disabled={this.props.disabled}/>
</div>
);
}
});
var Glance = React.createClass({
render: function() {
var model = this.props.model;
if (!model) {
return null;
}
return (
<div className='glance'>
<legend className='vmware'>{i18n('vmware.glance')}</legend>
<FieldGroup meta={model.get('metadata')} model={model} disabled={this.props.disabled}/>
</div>
);
}
});
var VCenter = React.createClass({
componentDidMount: function() {
this.clusterId = this.props.cluster.id;
this.model = new vmwareModels.VCenter({id: this.clusterId});
this.model.on('sync', _.bind(function() {
this.model.parseRestrictions();
this.actions = this.model.testRestrictions();
if (!this.model.loadDefaults) {
this.json = JSON.stringify(this.model.toJSON());
}
this.model.loadDefaults = false;
this.setState({model: this.model});
}, this));
this.defaultModel = new vmwareModels.VCenter({id: this.clusterId});
this.defaultModel.on('sync', _.bind(function() {
this.defaultModel.parseRestrictions();
this.defaultsJson = JSON.stringify(this.defaultModel.toJSON());
this.setState({defaultModel: this.defaultModel});
}, this));
this.setState({model: this.model, defaultModel: this.defaultModel});
this.model.setModels({
cluster: this.props.cluster,
settings: this.props.cluster.get('settings')
});
this.readDefaultsData();
this.readData();
dispatcher.on('vcenter_model_update', _.bind(function() {
this.forceUpdate();
}, this));
},
componentWillUnmount: function() {
$(document).off('vcenter_model_update');
},
getInitialState: function() {
return {model: null};
},
readData: function() {
this.model.fetch();
},
readDefaultsData: function() {
this.defaultModel.loadDefaults = true;
this.defaultModel.fetch();
},
saveData: function() {
this.model.save();
},
onLoadDefaults: function() {
this.model.loadDefaults = true;
this.model.fetch().done(_.bind(function() {
this.model.loadDefaults = false;
}, this));
},
onCancel: function() {
this.readData();
},
onSave: function() {
this.saveData();
},
revertChanges: function() {
this.readData();
},
render: function() {
if (!this.state.model || !this.actions) {
return null;
}
var model = this.state.model,
currentJson = JSON.stringify(this.model.toJSON()),
editable = this.props.cluster.isAvailableForSettingsChanges(),
// TODO hide = this.actions.hide || {},
disable = this.actions.disable || {};
model.isValid();
this.hasChanges = (this.json != currentJson);
this.hasDefaultsChanges = (this.defaultsJson != currentJson);
var saveDisabled = !editable || !this.hasChanges || !!model.validationError,
defaultsDisabled = !editable || !this.hasDefaultsChanges;
return (
<div className='vmware'>
<div className='wrapper'>
<h3>{i18n('vmware.title')}</h3>
<AvailabilityZones collection={model.get('availability_zones')} disabled={!editable || disable.availability_zones}/>
<Network model={model.get('network')} disabled={!editable || disable.network}/>
<Glance model={model.get('glance')} disabled={!editable || disable.glance}/>
</div>
<div className='page-control-box'>
<div className='page-control-button-placeholder'>
<button className='btn btn-load-defaults' onClick={this.onLoadDefaults} disabled={defaultsDisabled}>
{i18n('vmware.reset_to_defaults')}
</button>
<button className='btn btn-revert-changes' onClick={this.onCancel} disabled={!this.hasChanges}>
{i18n('vmware.cancel')}
</button>
<button className='btn btn-success btn-apply-changes' onClick={this.onSave} disabled={saveDisabled}>
{i18n('vmware.apply')}
</button>
</div>
</div>
</div>
);
}
});
return VCenter;
});