[React] Edit node disks screen
Related to blueprint backbone-to-react Change-Id: I5b19884a3f1a2ed253b67bcf3583e99739c716e1
This commit is contained in:
parent
2d18ec2cfc
commit
4244eb13e3
@ -82,6 +82,30 @@ define(['jquery', 'underscore', 'backbone', 'dispatcher', 'react', 'react.backbo
|
||||
$('html').off(this.state.clickEventName);
|
||||
Backbone.history.off('route', null, this);
|
||||
}
|
||||
},
|
||||
nodeConfigurationScreenMixin: {
|
||||
goToNodeList: function(cluster) {
|
||||
if (!cluster) cluster = this.props.cluster;
|
||||
app.navigate('#cluster/' + cluster.id + '/nodes', {trigger: true, replace: true});
|
||||
},
|
||||
isLockedScreen: function() {
|
||||
var nodesAvailableForChanges = this.props.nodes.any(function(node) {
|
||||
return node.get('pending_addition') || node.get('status') == 'error';
|
||||
});
|
||||
return !nodesAvailableForChanges ||
|
||||
this.props.cluster && !!this.props.cluster.tasks({group: 'deployment', status: 'running'}).length;
|
||||
},
|
||||
showDiscardChangesDialog: function() {
|
||||
var dialogs = require('jsx!views/dialogs');
|
||||
dialogs.DiscardSettingsChangesDialog.show({cb: this.goToNodeList});
|
||||
},
|
||||
returnToNodeList: function() {
|
||||
if (this.hasChanges()) {
|
||||
this.showDiscardChangesDialog();
|
||||
} else {
|
||||
this.goToNodeList();
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
|
@ -613,7 +613,7 @@ define([
|
||||
var error;
|
||||
var unallocatedSpace = this.getUnallocatedSpace({volumes: attrs.volumes});
|
||||
if (unallocatedSpace < 0) {
|
||||
error = 'Volume groups total size exceeds available space of ' + utils.formatNumber(unallocatedSpace * -1) + ' MB';
|
||||
error = i18n('cluster_page.nodes_tab.configure_disks.validation_error', {size: utils.formatNumber(unallocatedSpace * -1)});
|
||||
}
|
||||
return error;
|
||||
}
|
||||
@ -634,15 +634,10 @@ define([
|
||||
var groupAllocatedSpace = currentDisk.collection.reduce(function(sum, disk) {return disk.id == currentDisk.id ? sum : sum + disk.get('volumes').findWhere({name: this.get('name')}).get('size');}, 0, this);
|
||||
return minimum - groupAllocatedSpace;
|
||||
},
|
||||
validate: function(attrs, options) {
|
||||
var error;
|
||||
var min = this.getMinimalSize(options.minimum);
|
||||
if (_.isNaN(attrs.size)) {
|
||||
error = 'Invalid size';
|
||||
} else if (attrs.size < min) {
|
||||
error = 'The value is too low. You must allocate at least ' + utils.formatNumber(min) + ' MB';
|
||||
}
|
||||
return error;
|
||||
getMaxSize: function() {
|
||||
var volumes = this.collection.disk.get('volumes'),
|
||||
diskAllocatedSpace = volumes.reduce(function(total, volume) {return this.get('name') == volume.get('name') ? total : total + volume.get('size');}, 0, this);
|
||||
return this.collection.disk.get('size') - diskAllocatedSpace ;
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -1987,9 +1987,18 @@ button, .btn:not(.btn-link) {.font-semibold;}
|
||||
|
||||
// Disks styles
|
||||
.disk-box {
|
||||
label {
|
||||
.font-semibold;
|
||||
@colors: green, blue, orange, gray, pure-red, black, red;
|
||||
.generate-colors(length(@colors));
|
||||
.generate-colors(@n, @i: 1) when (@i =< @n) {
|
||||
.volume-type-@{i} {
|
||||
@color: extract(@colors, @i);
|
||||
background: @@color;
|
||||
}
|
||||
.generate-colors(@n, (@i + 1));
|
||||
}
|
||||
|
||||
label {.font-semibold;}
|
||||
|
||||
.disk-visual {
|
||||
@disk-visual-block-height: 50px;
|
||||
@disk-visual-block-padding: 4px;
|
||||
@ -2010,6 +2019,7 @@ button, .btn:not(.btn-link) {.font-semibold;}
|
||||
}
|
||||
.close-btn {
|
||||
position: absolute;
|
||||
z-index: 1000;
|
||||
top: 0;
|
||||
right: @disk-visual-block-padding;
|
||||
cursor: pointer;
|
||||
@ -2027,45 +2037,59 @@ button, .btn:not(.btn-link) {.font-semibold;}
|
||||
.disk-info-box {
|
||||
.form-group {
|
||||
margin-bottom: 0;
|
||||
label {
|
||||
text-transform: capitalize;
|
||||
}
|
||||
p {
|
||||
padding: 0;
|
||||
}
|
||||
label {text-transform: capitalize;}
|
||||
p {padding: 0;}
|
||||
}
|
||||
}
|
||||
.disk-utility-box {
|
||||
@disk-form-margin: 5px;
|
||||
.form-group {
|
||||
margin-bottom: @disk-form-margin;
|
||||
margin: 0;
|
||||
.volume-group-flag {
|
||||
display: inline-block;
|
||||
width: 10px;
|
||||
}
|
||||
.volume-group-use-all-allowed-btn button {
|
||||
font-size: @base-font-size - 4px;
|
||||
padding: 0;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.volume-group-size-label {
|
||||
padding: 0;
|
||||
}
|
||||
.volume-group-input {
|
||||
padding-right: @disk-form-margin;
|
||||
}
|
||||
label, .volume-group-use-all-allowed-btn, .volume-group-size-label {
|
||||
padding-top: 6px;
|
||||
.volume-group-range, .volume-group-input {
|
||||
label, .help-block {display: none;}
|
||||
}
|
||||
.volume-group-label {
|
||||
margin: 0;
|
||||
padding-top: @disk-form-margin;
|
||||
}
|
||||
.volume-group-size-label {
|
||||
padding: @disk-form-margin;
|
||||
text-align: right;
|
||||
}
|
||||
input[type=range] {
|
||||
/*fix for FF unable to apply focus style bug */
|
||||
border: 1px solid transparent;
|
||||
margin-top: @disk-form-margin;
|
||||
&:focus {outline: none;}
|
||||
&:focus::-webkit-slider-runnable-track {background: transparent;}
|
||||
&:disabled {opacity: 0.4;}
|
||||
}
|
||||
input[type=number] {
|
||||
position: relative;
|
||||
top: -2px;
|
||||
}
|
||||
}
|
||||
.volume-group-error {
|
||||
.volume-group {margin-bottom: @disk-form-margin;}
|
||||
.volume-group-notice {
|
||||
font-size: @base-font-size - 2px;
|
||||
margin: 0 @disk-form-margin * 2 @disk-form-margin * 2;
|
||||
color: @blue;
|
||||
margin: 0 @disk-form-margin @disk-form-margin * 2 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.node-disks + .page-buttons {
|
||||
margin-top: 30px;
|
||||
}
|
||||
|
||||
// Interfaces styles
|
||||
|
||||
.ifc-list {
|
||||
|
@ -1,23 +0,0 @@
|
||||
<div class="row">
|
||||
<div class="title">
|
||||
<%- i18n('cluster_page.nodes_tab.configure_disks.title', {count: nodes.length, name: nodes.length && nodes.at(0).get('name')}) %>
|
||||
</div>
|
||||
|
||||
<div class="col-xs-12 node-disks">
|
||||
<div class="progress">
|
||||
<div class="progress-bar progress-bar-striped active" style="width: 100%;"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-xs-12 page-buttons">
|
||||
<div class="well clearfix">
|
||||
<div class="btn-group">
|
||||
<button class="btn btn-default btn-return" data-i18n="cluster_page.nodes_tab.back_to_nodes_button"></button>
|
||||
</div>
|
||||
<div class="btn-group pull-right">
|
||||
<button data-i18n="common.load_defaults_button" class="btn btn-default btn-defaults"></button>
|
||||
<button data-i18n="common.cancel_changes_button" class="btn btn-default btn-revert-changes"></button>
|
||||
<button data-i18n="common.apply_button" class="btn btn-success btn-apply"></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
@ -1,89 +0,0 @@
|
||||
<% var diskName = disk.get('name') %>
|
||||
|
||||
<div class="col-xs-12 disk-box" data-disk="<%- disk.id %>">
|
||||
|
||||
<div class="row">
|
||||
<h4 class="col-xs-6">
|
||||
<%- diskName %> (<%- disk.id %>)
|
||||
</h4>
|
||||
<h4 class="col-xs-6 text-right">
|
||||
<%- i18n('cluster_page.nodes_tab.configure_disks.total_space') %>: <%= showDiskSize(disk.get('size'), 2) %>
|
||||
</h4>
|
||||
</div>
|
||||
|
||||
<div class="row disk-visual clearfix" data-toggle="collapse" data-target="#<%- diskName %>">
|
||||
<% volumes.each(function(volume) { %>
|
||||
<div class="volume-group pull-left" data-volume="<%- volume.get('name') %>" style="width:0;">
|
||||
<div class="text-center">
|
||||
<div><%- volume.get('label') %></div>
|
||||
<div class="volume-group-size"><%= showDiskSize(0) %></div>
|
||||
</div>
|
||||
<div class="close-btn hide">×</div>
|
||||
</div>
|
||||
<% }) %>
|
||||
<div class="volume-group pull-left" data-volume="unallocated" style="width: 100%">
|
||||
<div class="text-center">
|
||||
<div data-i18n="cluster_page.nodes_tab.configure_disks.unallocated"></div>
|
||||
<div class="volume-group-size"><%= showDiskSize(disk.get('size'), 2) %></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row collapse disk-details" id="<%- diskName %>">
|
||||
|
||||
<div class="col-xs-6">
|
||||
<% if (diskMetaData) { %>
|
||||
<h5 data-i18n="cluster_page.nodes_tab.configure_disks.disk_information"></h5>
|
||||
<div class="form-horizontal disk-info-box">
|
||||
<% _.each(sortEntryProperties(diskMetaData, ['name', 'model', 'size']), function(propertyName) { %>
|
||||
<div class="form-group">
|
||||
<label class="col-xs-2">
|
||||
<%- propertyName.replace(/_/g, ' ') %>
|
||||
</label>
|
||||
<div class="col-xs-10">
|
||||
<p class="form-control-static">
|
||||
<%- propertyName == 'size' ? showDiskSize(diskMetaData[propertyName]) : diskMetaData[propertyName] %>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<% }) %>
|
||||
</div>
|
||||
<% } %>
|
||||
</div>
|
||||
|
||||
<div class="col-xs-6">
|
||||
<h5 data-i18n="cluster_page.nodes_tab.configure_disks.volume_groups"></h5>
|
||||
<div class="form-horizontal disk-utility-box">
|
||||
<% volumes.each(function(volume) { %>
|
||||
<% var volumeName = volume.get('name') %>
|
||||
<div class="form-group volume-group" data-volume="<%- volumeName %>">
|
||||
<label class="col-xs-4">
|
||||
<span class="volume-group-flag <%- volumeName %>"> </span>
|
||||
<%- volume.get('label') %>
|
||||
</label>
|
||||
<div class="col-xs-3 volume-group-use-all-allowed-btn">
|
||||
<% if (!locked) { %>
|
||||
<button class="btn btn-link" data-i18n="cluster_page.nodes_tab.configure_disks.use_all_allowed_space"></button>
|
||||
<% } %>
|
||||
</div>
|
||||
<div class="col-xs-4 volume-group-input">
|
||||
<input
|
||||
id="<%- disk.id + '-' + volumeName %>"
|
||||
type="text"
|
||||
class="form-control"
|
||||
<%= locked ? 'disabled' : '' %>
|
||||
name="<%- volumeName %>"
|
||||
value="<%- disk.get('volumes').findWhere({name: volumeName}).get('size') || 0 %>"
|
||||
/>
|
||||
</div>
|
||||
<div class="col-xs-1 volume-group-size-label" data-i18n="common.size.mb"></div>
|
||||
</div>
|
||||
<div class="volume-group-error text-danger text-right"></div>
|
||||
<% }) %>
|
||||
<div class="volume-group-error common text-danger text-right"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
@ -1,92 +0,0 @@
|
||||
<div class="clearfix"></div>
|
||||
|
||||
<% var slaveInterfaces = ifc.getSlaveInterfaces() %>
|
||||
<div class="physical-network-box" data-name="<%- ifc.get('name') %>">
|
||||
<div class="network-box-item">
|
||||
<% if (ifc.isBond()) { %>
|
||||
<div class="network-box-name">
|
||||
<% if (bondingAvailable) { %>
|
||||
<label>
|
||||
<div class="pull-left">
|
||||
<div class="custom-tumbler network-bond-name-checkbox">
|
||||
<input type="checkbox">
|
||||
<!-- [if !IE |(gte IE 9)]> --><span> </span><!-- <![endif] -->
|
||||
</div>
|
||||
</div>
|
||||
<div class="network-bond-name pull-left"><%- ifc.get('name') %></div>
|
||||
</label>
|
||||
<% } else { %>
|
||||
<div class="network-bond-name pull-left disabled"><%- ifc.get('name') %></div>
|
||||
<% } %>
|
||||
<div class="network-bond-mode pull-right">
|
||||
<b><%- i18n('cluster_page.nodes_tab.configure_interfaces.bonding_mode') %>:</b>
|
||||
<span>
|
||||
<select name="mode" <%= bondingAvailable ? '' : 'disabled' %>></select>
|
||||
</span>
|
||||
</div>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
|
||||
<div class="physical-network-checkbox"></div>
|
||||
<% } else { %>
|
||||
<div class="physical-network-checkbox">
|
||||
<% var bondable = bondingAvailable && !ifc.get('assigned_networks').find(function(interfaceNetwork) {return interfaceNetwork.getFullNetwork().get('meta').unmovable}) %>
|
||||
<% if (bondable) { %>
|
||||
<label>
|
||||
<div class="custom-tumbler">
|
||||
<input type="checkbox">
|
||||
<!-- [if !IE |(gte IE 9)]> --><span> </span><!-- <![endif] -->
|
||||
</div>
|
||||
</label>
|
||||
<% } %>
|
||||
</div>
|
||||
<% } %>
|
||||
|
||||
<div class="network-connections-block">
|
||||
<% _.each(slaveInterfaces, function(slaveInterface) { %>
|
||||
<div class="network-interfaces-status">
|
||||
<div class="interface-<%= slaveInterface.get('state') != 'down' ? 'online' : 'offline'%>"></div>
|
||||
<div class="network-interfaces-name"><%- slaveInterface.get('name') %></div>
|
||||
</div>
|
||||
<% }) %>
|
||||
</div>
|
||||
|
||||
<div class="network-connections-info-block">
|
||||
<% _.each(slaveInterfaces, function(slaveInterface) { %>
|
||||
<div class="network-connections-info-block-item">
|
||||
<div class="network-connections-info-position"></div>
|
||||
<div class="network-connections-info-description">
|
||||
<div>MAC: <%- slaveInterface.get('mac') %></div>
|
||||
<div><%- i18n('cluster_page.nodes_tab.configure_interfaces.speed') %>: <%- showBandwidth(slaveInterface.get('current_speed')) %></div>
|
||||
<% if (bondingAvailable && slaveInterfaces.length >= 3) { %>
|
||||
<button class="btn btn-link btn-remove-interface" type="button" data-interface-id="<%= slaveInterface.id %>">Remove</button>
|
||||
<% } %>
|
||||
</div>
|
||||
</div>
|
||||
<% }) %>
|
||||
</div>
|
||||
|
||||
<div class="logical-network-box">
|
||||
<div class="logical-network-group">
|
||||
<% ifc.get('assigned_networks').each(function(interfaceNetwork) { %>
|
||||
<% var networkName = interfaceNetwork.get('name') %>
|
||||
<% var network = interfaceNetwork.getFullNetwork() %>
|
||||
<% if (networkName != 'floating') { %>
|
||||
</div><div class="logical-network-group <%= locked || network.get('meta').unmovable ? 'disabled' : '' %>">
|
||||
<% } %>
|
||||
<div class="logical-network-item" data-name="<%- networkName %>">
|
||||
<div class="name"><%- i18n('network.' + networkName, {defaultValue: networkName}) %></div>
|
||||
<% var vlanRange = network.getVlanRange() %>
|
||||
<% if (!_.isNull(vlanRange)) { %>
|
||||
<div class="id">
|
||||
<%- i18n('cluster_page.nodes_tab.configure_interfaces.vlan_id', {count: _.uniq(vlanRange).length}) + ': ' + _.uniq(vlanRange).join('-') %>
|
||||
</div>
|
||||
<% } %>
|
||||
</div>
|
||||
<% }) %>
|
||||
</div>
|
||||
<div class="network-help-message hide" data-i18n="cluster_page.nodes_tab.configure_interfaces.drag_and_drop_description"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="network-box-error-message common enable-selection"> </div>
|
@ -1,8 +0,0 @@
|
||||
background: <%= startColor %>;
|
||||
background: -moz-linear-gradient(top, <%= startColor %> 0%, <%= endColor %> 100%);
|
||||
background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,<%= startColor %>), color-stop(100%,<%= endColor %>));
|
||||
background: -webkit-linear-gradient(top, <%= startColor %> 0%, <%= endColor %> 100%);
|
||||
background: -o-linear-gradient(top, <%= startColor %> 0%, <%= endColor %> 100%);
|
||||
background: -ms-linear-gradient(top, <%= startColor %> 0%, <%= endColor %> 100%);
|
||||
background: linear-gradient(to bottom, <%= startColor %> 0%, <%= endColor %> 100%);
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr="<%= startColor %>", endColorstr="<%= endColor %>", GradientType=0);
|
@ -262,6 +262,10 @@
|
||||
"disks": "Configure Disks",
|
||||
"interfaces": "Configure Interfaces"
|
||||
},
|
||||
"node_loading_error": {
|
||||
"title": "Error loading nodes",
|
||||
"load_error": "Some nodes failed to load"
|
||||
},
|
||||
"configure_interfaces": {
|
||||
"title": "Configure interfaces on __name__",
|
||||
"title_plural": "Configure interfaces on __count__ nodes",
|
||||
@ -303,10 +307,6 @@
|
||||
"saving_warning": "Unable to apply changes",
|
||||
"load_defaults_warning": "Unable to load default configuration"
|
||||
},
|
||||
"node_loading_error": {
|
||||
"title": "Error loading nodes",
|
||||
"load_error": "Some nodes failed to load"
|
||||
},
|
||||
"validation": {
|
||||
"too_many_untagged_networks": "Untagged networks can not be assigned to the same interface",
|
||||
"invalid_mtu": "Invalid MTU value"
|
||||
@ -322,6 +322,8 @@
|
||||
"total_space": "Total Space",
|
||||
"unallocated": "Unallocated",
|
||||
"use_all_allowed_space": "Use All Allowed Space",
|
||||
"validation_error": "Volume groups total size exceeds available space of __size__ MB",
|
||||
"minimum_reached": "Minimum allowed size reached",
|
||||
"configuration_error": {
|
||||
"title": "Disks Configuration Error",
|
||||
"saving_warning": "Unable to apply changes",
|
||||
|
@ -18,14 +18,13 @@ define(
|
||||
'jquery',
|
||||
'underscore',
|
||||
'react',
|
||||
'jsx!backbone_view_wrapper',
|
||||
'jsx!views/cluster_page_tabs/nodes_tab_screens/cluster_nodes_screen',
|
||||
'jsx!views/cluster_page_tabs/nodes_tab_screens/add_nodes_screen',
|
||||
'jsx!views/cluster_page_tabs/nodes_tab_screens/edit_nodes_screen',
|
||||
'views/cluster_page_tabs/nodes_tab_screens/edit_node_disks_screen',
|
||||
'jsx!views/cluster_page_tabs/nodes_tab_screens/edit_node_disks_screen',
|
||||
'jsx!views/cluster_page_tabs/nodes_tab_screens/edit_node_interfaces_screen'
|
||||
],
|
||||
function($, _, React, BackboneViewWrapper, ClusterNodesScreen, AddNodesScreen, EditNodesScreen, EditNodeDisksScreen, EditNodeInterfacesScreen) {
|
||||
function($, _, React, ClusterNodesScreen, AddNodesScreen, EditNodesScreen, EditNodeDisksScreen, EditNodeInterfacesScreen) {
|
||||
'use strict';
|
||||
|
||||
var ReactTransitionGroup = React.addons.TransitionGroup;
|
||||
@ -48,7 +47,7 @@ function($, _, React, BackboneViewWrapper, ClusterNodesScreen, AddNodesScreen, E
|
||||
list: ClusterNodesScreen,
|
||||
add: AddNodesScreen,
|
||||
edit: EditNodesScreen,
|
||||
disks: BackboneViewWrapper(EditNodeDisksScreen),
|
||||
disks: EditNodeDisksScreen,
|
||||
interfaces: EditNodeInterfacesScreen
|
||||
};
|
||||
},
|
||||
|
@ -1,314 +0,0 @@
|
||||
/*
|
||||
* Copyright 2013 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',
|
||||
'utils',
|
||||
'models',
|
||||
'views/cluster_page_tabs/nodes_tab_screens/edit_node_screen',
|
||||
'text!templates/cluster/edit_node_disks.html',
|
||||
'text!templates/cluster/node_disk.html',
|
||||
'text!templates/cluster/volume_style.html',
|
||||
'jquery-autoNumeric'
|
||||
],
|
||||
function($, _, i18n, Backbone, utils, models, EditNodeScreen, editNodeDisksScreenTemplate, nodeDisksTemplate, volumeStylesTemplate) {
|
||||
'use strict';
|
||||
var EditNodeDisksScreen, NodeDisk;
|
||||
|
||||
EditNodeDisksScreen = EditNodeScreen.extend({
|
||||
className: 'edit-node-disks-screen',
|
||||
constructorName: 'EditNodeDisksScreen',
|
||||
template: _.template(editNodeDisksScreenTemplate),
|
||||
events: {
|
||||
'click .btn-defaults': 'loadDefaults',
|
||||
'click .btn-revert-changes': 'revertChanges',
|
||||
'click .btn-apply:not(:disabled)': 'applyChanges',
|
||||
'click .btn-return:not(:disabled)': 'returnToNodeList'
|
||||
},
|
||||
hasChanges: function() {
|
||||
var volumes = _.pluck(this.disks.toJSON(), 'volumes');
|
||||
return !this.nodes.reduce(function(result, node) {
|
||||
return result && _.isEqual(volumes, _.pluck(node.disks.toJSON(), 'volumes'));
|
||||
}, true);
|
||||
},
|
||||
hasValidationErrors: function() {
|
||||
var result = false;
|
||||
this.disks.each(function(disk) {result = result || disk.validationError || _.some(disk.get('volumes').models, 'validationError');}, this);
|
||||
return result;
|
||||
},
|
||||
isLocked: function() {
|
||||
var nodesAvailableForChanges = this.nodes.filter(function(node) {
|
||||
return node.get('pending_addition') || (node.get('status') == 'error' && node.get('error_type') == 'provision');
|
||||
});
|
||||
return !nodesAvailableForChanges.length || this.constructor.__super__.isLocked.apply(this);
|
||||
},
|
||||
checkForChanges: function() {
|
||||
this.updateButtonsState(this.isLocked());
|
||||
this.applyChangesButton.set('disabled', this.isLocked() || !this.hasChanges() || this.hasValidationErrors());
|
||||
this.cancelChangesButton.set('disabled', this.isLocked() || (!this.hasChanges() && !this.hasValidationErrors()));
|
||||
},
|
||||
loadDefaults: function() {
|
||||
this.disableControls(true);
|
||||
this.disks.fetch({url: _.result(this.nodes.at(0), 'url') + '/disks/defaults/'})
|
||||
.fail(_.bind(function(response) {
|
||||
utils.showErrorDialog({
|
||||
title: i18n('cluster_page.nodes_tab.configure_disks.configuration_error.title'),
|
||||
message: i18n('cluster_page.nodes_tab.configure_disks.configuration_error.load_defaults_warning'),
|
||||
response: response
|
||||
});
|
||||
}, this));
|
||||
},
|
||||
revertChanges: function() {
|
||||
this.disks.reset(_.cloneDeep(this.nodes.at(0).disks.toJSON()), {parse: true});
|
||||
},
|
||||
applyChanges: function() {
|
||||
if (this.hasValidationErrors()) {
|
||||
return (new $.Deferred()).reject();
|
||||
}
|
||||
this.disableControls(true);
|
||||
return $.when.apply($, this.nodes.map(function(node) {
|
||||
node.disks.each(function(disk, index) {
|
||||
disk.set({volumes: new models.Volumes(this.disks.at(index).get('volumes').toJSON())});
|
||||
}, this);
|
||||
return Backbone.sync('update', node.disks, {url: _.result(node, 'url') + '/disks'});
|
||||
}, this))
|
||||
.done(_.bind(function() {
|
||||
this.cluster.fetch();
|
||||
this.render();
|
||||
}, this))
|
||||
.fail(_.bind(function(response) {
|
||||
this.checkForChanges();
|
||||
utils.showErrorDialog({
|
||||
title: i18n('cluster_page.nodes_tab.configure_disks.configuration_error.title'),
|
||||
message: utils.getResponseText(response) || i18n('cluster_page.nodes_tab.configure_disks.configuration_error.saving_warning')
|
||||
});
|
||||
}, this));
|
||||
},
|
||||
mapVolumesColors: function() {
|
||||
this.volumesColors = {};
|
||||
var colors = [
|
||||
['#23a85e', '#1d8a4d'],
|
||||
['#3582ce', '#2b6ba9'],
|
||||
['#eea616', '#c38812'],
|
||||
['#1cbbb4', '#189f99'],
|
||||
['#9e0b0f', '#870a0d'],
|
||||
['#8f50ca', '#7a44ac'],
|
||||
['#1fa0e3', '#1b88c1'],
|
||||
['#85c329', '#71a623'],
|
||||
['#7d4900', '#6b3e00']
|
||||
];
|
||||
this.volumes.each(function(volume, index) {
|
||||
this.volumesColors[volume.get('name')] = colors[index];
|
||||
}, this);
|
||||
},
|
||||
initialize: function() {
|
||||
this.constructor.__super__.initialize.apply(this, arguments);
|
||||
if (this.nodes.length) {
|
||||
this.cluster.on('change:status', this.revertChanges, this);
|
||||
this.volumes = new models.Volumes();
|
||||
this.volumes.url = _.result(this.nodes.at(0), 'url') + '/volumes';
|
||||
this.loading = $.when.apply($, this.nodes.map(function(node) {
|
||||
node.disks = new models.Disks();
|
||||
return node.disks.fetch({url: _.result(node, 'url') + '/disks'});
|
||||
}, this).concat(this.volumes.fetch()))
|
||||
.done(_.bind(function() {
|
||||
this.disks = new models.Disks(_.cloneDeep(this.nodes.at(0).disks.toJSON()), {parse: true});
|
||||
this.disks.on('sync', this.render, this);
|
||||
this.disks.on('reset', this.render, this);
|
||||
this.disks.on('error', this.checkForChanges, this);
|
||||
this.mapVolumesColors();
|
||||
this.render();
|
||||
}, this))
|
||||
.fail(_.bind(this.goToNodeList, this));
|
||||
} else {
|
||||
this.goToNodeList();
|
||||
}
|
||||
this.initButtons();
|
||||
},
|
||||
getDiskMetaData: function(disk) {
|
||||
var result;
|
||||
var disksMetaData = this.nodes.at(0).get('meta').disks;
|
||||
// try to find disk metadata by matching "extra" field
|
||||
// if at least one entry presents both in disk and metadata entry,
|
||||
// this metadata entry is for our disk
|
||||
var extra = disk.get('extra') || [];
|
||||
result = _.find(disksMetaData, function(diskMetaData) {
|
||||
if (_.isArray(diskMetaData.extra)) {
|
||||
return _.intersection(diskMetaData.extra, extra).length;
|
||||
}
|
||||
return false;
|
||||
}, this);
|
||||
|
||||
// if matching "extra" fields doesn't work, try to search by disk id
|
||||
if (!result) {
|
||||
result = _.find(disksMetaData, {disk: disk.id});
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
renderDisks: function() {
|
||||
this.tearDownRegisteredSubViews();
|
||||
this.$('.node-disks').html('');
|
||||
this.disks.each(function(disk) {
|
||||
var nodeDisk = new NodeDisk({
|
||||
disk: disk,
|
||||
diskMetaData: this.getDiskMetaData(disk),
|
||||
screen: this
|
||||
});
|
||||
this.registerSubView(nodeDisk);
|
||||
this.$('.node-disks').append(nodeDisk.render().el);
|
||||
}, this);
|
||||
},
|
||||
render: function() {
|
||||
this.$el.html(this.template({
|
||||
nodes: this.nodes,
|
||||
locked: this.isLocked()
|
||||
})).i18n();
|
||||
if (this.loading && this.loading.state() != 'pending') {
|
||||
this.renderDisks();
|
||||
this.checkForChanges();
|
||||
}
|
||||
this.setupButtonsBindings();
|
||||
return this;
|
||||
}
|
||||
});
|
||||
|
||||
NodeDisk = Backbone.View.extend({
|
||||
template: _.template(nodeDisksTemplate),
|
||||
volumeStylesTemplate: _.template(volumeStylesTemplate),
|
||||
templateHelpers: {
|
||||
sortEntryProperties: utils.sortEntryProperties,
|
||||
showDiskSize: utils.showDiskSize
|
||||
},
|
||||
events: {
|
||||
'click .disk-visual': 'toggleDiskFormVisibleAttribute',
|
||||
'click .close-btn': 'deleteVolume',
|
||||
'click .volume-group-use-all-allowed-btn button': 'useAllAllowedSpace'
|
||||
},
|
||||
toggleDiskFormVisibleAttribute: function() {
|
||||
this.diskForm.set({visible: !this.diskForm.get('visible')});
|
||||
},
|
||||
getVolumeMinimum: function(name) {
|
||||
return this.screen.volumes.findWhere({name: name}).get('min_size');
|
||||
},
|
||||
checkForGroupsDeletionAvailability: function() {
|
||||
this.disk.get('volumes').each(function(volume) {
|
||||
var name = volume.get('name'),
|
||||
showRemoveButton = !this.screen.isLocked() && this.diskForm.get('visible') && volume.getMinimalSize(this.getVolumeMinimum(name)) <= 0;
|
||||
this.$('.disk-visual [data-volume=' + name + '] .close-btn').toggleClass('hide', !showRemoveButton);
|
||||
}, this);
|
||||
},
|
||||
updateDisk: function() {
|
||||
this.$('.disk-utility-box .form-group').removeClass('has-error').next('.volume-group-error').text('');
|
||||
this.$('.volume-group-error.common').text('');
|
||||
this.disk.get('volumes').each(function(volume) {
|
||||
volume.set({size: volume.get('size')}, {validate: true, minimum: this.getVolumeMinimum(volume.get('name'))});
|
||||
}, this); // volumes validation (minimum)
|
||||
this.disk.set({volumes: this.disk.get('volumes')}, {validate: true}); // disk validation (maximum)
|
||||
this.renderVisualGraph();
|
||||
},
|
||||
updateDisks: function() {
|
||||
this.updateDisk();
|
||||
_.invoke(_.omit(this.screen.subViews, this.cid), 'updateDisk', this);
|
||||
this.screen.checkForChanges();
|
||||
},
|
||||
deleteVolume: function(e) {
|
||||
e.stopPropagation();
|
||||
var volumeName = this.$(e.currentTarget).parents('.volume-group').data('volume');
|
||||
var volume = this.disk.get('volumes').findWhere({name: volumeName});
|
||||
volume.set({size: 0});
|
||||
},
|
||||
useAllAllowedSpace: function(e) {
|
||||
var volumeName = this.$(e.currentTarget).parents('.volume-group').data('volume');
|
||||
var volume = this.disk.get('volumes').findWhere({name: volumeName});
|
||||
volume.set({size: _.max([0, this.disk.getUnallocatedSpace({skip: volumeName})])});
|
||||
},
|
||||
initialize: function(options) {
|
||||
_.defaults(this, options);
|
||||
this.diskForm = new Backbone.Model({visible: false});
|
||||
this.diskForm.on('change:visible', this.checkForGroupsDeletionAvailability, this);
|
||||
this.disk.on('invalid', function(cluster, error) {
|
||||
this.$('.disk-utility-box .form-group').addClass('has-error');
|
||||
this.$('.volume-group-error.common').text(error);
|
||||
}, this);
|
||||
this.disk.get('volumes').each(function(volume) {
|
||||
volume.on('change:size', this.updateDisks, this);
|
||||
volume.on('change:size', function() {_.invoke(this.screen.subViews, 'checkForGroupsDeletionAvailability', this);}, this);
|
||||
volume.on('invalid', function(cluster, error) {
|
||||
this.$('.form-group[data-volume=' + volume.get('name') + ']').addClass('has-error').next('.volume-group-error').text(error);
|
||||
}, this);
|
||||
}, this);
|
||||
},
|
||||
renderVolume: function(name, width, size) {
|
||||
this.$('.disk-visual [data-volume=' + name + ']')
|
||||
.css('width', width + '%')
|
||||
.find('.volume-group-size').text(utils.showDiskSize(size, 2));
|
||||
},
|
||||
renderVisualGraph: function() {
|
||||
if (!this.disk.get('volumes').some('validationError') && this.disk.isValid()) {
|
||||
var unallocatedWidth = 100;
|
||||
this.disk.get('volumes').each(function(volume) {
|
||||
var width = this.disk.get('size') ? utils.floor(volume.get('size') / this.disk.get('size') * 100, 2) : 0;
|
||||
unallocatedWidth -= width;
|
||||
this.renderVolume(volume.get('name'), width, volume.get('size'));
|
||||
}, this);
|
||||
this.renderVolume('unallocated', unallocatedWidth, this.disk.getUnallocatedSpace());
|
||||
}
|
||||
},
|
||||
applyColors: function() {
|
||||
this.disk.get('volumes').each(function(volume) {
|
||||
var name = volume.get('name');
|
||||
var colors = this.screen.volumesColors[name];
|
||||
this.$('.disk-visual [data-volume=' + name + '], .volume-group-flag.' + name)
|
||||
.attr('style', this.volumeStylesTemplate({startColor: _.first(colors), endColor: _.last(colors)}));
|
||||
}, this);
|
||||
},
|
||||
setupVolumesBindings: function() {
|
||||
this.disk.get('volumes').each(function(volume) {
|
||||
var bindings = {};
|
||||
bindings['input[name=' + volume.get('name') + ']'] = {
|
||||
events: ['keyup'],
|
||||
observe: 'size',
|
||||
getVal: function($el) {
|
||||
return Number($el.autoNumeric('get'));
|
||||
},
|
||||
update: function($el, value) {
|
||||
$el.autoNumeric('set', value);
|
||||
}
|
||||
};
|
||||
this.stickit(volume, bindings);
|
||||
}, this);
|
||||
},
|
||||
render: function() {
|
||||
this.$el.html(this.template(_.extend({
|
||||
diskMetaData: this.diskMetaData,
|
||||
disk: this.disk,
|
||||
volumes: this.screen.volumes,
|
||||
locked: this.screen.isLocked()
|
||||
}, this.templateHelpers))).i18n();
|
||||
this.applyColors();
|
||||
this.renderVisualGraph();
|
||||
this.$('input').autoNumeric('init', {mDec: 0});
|
||||
this.setupVolumesBindings();
|
||||
return this;
|
||||
}
|
||||
});
|
||||
|
||||
return EditNodeDisksScreen;
|
||||
});
|
@ -0,0 +1,331 @@
|
||||
/*
|
||||
* 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',
|
||||
'react',
|
||||
'utils',
|
||||
'models',
|
||||
'jsx!component_mixins',
|
||||
'jsx!views/controls'
|
||||
],
|
||||
function($, _, i18n, Backbone, React, utils, models, ComponentMixins, controls) {
|
||||
'use strict';
|
||||
|
||||
var EditNodeDisksScreen = React.createClass({
|
||||
mixins: [
|
||||
ComponentMixins.nodeConfigurationScreenMixin,
|
||||
ComponentMixins.backboneMixin('cluster', 'change:status change:nodes sync'),
|
||||
ComponentMixins.backboneMixin('nodes', 'change sync'),
|
||||
ComponentMixins.backboneMixin('disks', 'reset change')
|
||||
],
|
||||
statics: {
|
||||
fetchData: function(options) {
|
||||
var cluster = options.cluster,
|
||||
nodeIds = utils.deserializeTabOptions(options.screenOptions[0]).nodes.split(',').map(function(id) {return parseInt(id, 10);}),
|
||||
nodes = new models.Nodes(cluster.get('nodes').getByIds(nodeIds));
|
||||
if (nodes.length != nodeIds.length) {
|
||||
utils.showErrorDialog({
|
||||
title: i18n('cluster_page.nodes_tab.node_loading_error.title'),
|
||||
message: i18n('cluster_page.nodes_tab.node_loading_error.load_error')
|
||||
});
|
||||
ComponentMixins.nodeConfigurationScreenMixin.goToNodeList(cluster);
|
||||
return;
|
||||
}
|
||||
|
||||
var volumes = new models.Volumes();
|
||||
volumes.url = _.result(nodes.at(0), 'url') + '/volumes';
|
||||
return $.when.apply($, nodes.map(function(node) {
|
||||
node.disks = new models.Disks();
|
||||
return node.disks.fetch({url: _.result(node, 'url') + '/disks'});
|
||||
}, this).concat(volumes.fetch()))
|
||||
.then(function() {
|
||||
var disks = new models.Disks(_.cloneDeep(nodes.at(0).disks.toJSON()), {parse: true});
|
||||
return {
|
||||
disks: disks,
|
||||
nodes: nodes,
|
||||
volumes: volumes
|
||||
};
|
||||
});
|
||||
}
|
||||
},
|
||||
getInitialState: function() {
|
||||
return {actionInProgress: false};
|
||||
},
|
||||
hasChanges: function() {
|
||||
var volumes = _.pluck(this.props.disks.toJSON(), 'volumes');
|
||||
return this.props.nodes.any(function(node) {
|
||||
return !_.isEqual(volumes, _.pluck(node.disks.toJSON(), 'volumes'));
|
||||
});
|
||||
},
|
||||
loadDefaults: function() {
|
||||
this.setState({actionInProgress: true});
|
||||
this.props.disks.fetch({url: _.result(this.props.nodes.at(0), 'url') + '/disks/defaults/'})
|
||||
.fail(_.bind(function(response) {
|
||||
var ns = 'cluster_page.nodes_tab.configure_disks.configuration_error.';
|
||||
utils.showErrorDialog({
|
||||
title: i18n(ns + 'title'),
|
||||
message: utils.getResponseText(response) || i18n(ns + 'load_defaults_warning')
|
||||
});
|
||||
}, this))
|
||||
.always(_.bind(function() {
|
||||
this.setState({actionInProgress: false});
|
||||
}, this));
|
||||
},
|
||||
revertChanges: function() {
|
||||
this.props.disks.reset(_.cloneDeep(this.props.nodes.at(0).disks.toJSON()), {parse: true});
|
||||
},
|
||||
applyChanges: function() {
|
||||
this.setState({actionInProgress: true});
|
||||
return $.when.apply($, this.props.nodes.map(function(node) {
|
||||
node.disks.each(function(disk, index) {
|
||||
disk.set({volumes: new models.Volumes(this.props.disks.at(index).get('volumes').toJSON())});
|
||||
}, this);
|
||||
return Backbone.sync('update', node.disks, {url: _.result(node, 'url') + '/disks'});
|
||||
}, this))
|
||||
.fail(_.bind(function(response) {
|
||||
var ns = 'cluster_page.nodes_tab.configure_disks.configuration_error.';
|
||||
utils.showErrorDialog({
|
||||
title: i18n(ns + 'title'),
|
||||
message: utils.getResponseText(response) || i18n(ns + 'saving_warning')
|
||||
});
|
||||
}, this))
|
||||
.always(_.bind(function() {
|
||||
this.setState({actionInProgress: false});
|
||||
}, this));
|
||||
},
|
||||
getDiskMetaData: function(disk) {
|
||||
var result,
|
||||
disksMetaData = this.props.nodes.at(0).get('meta').disks;
|
||||
// try to find disk metadata by matching "extra" field
|
||||
// if at least one entry presents both in disk and metadata entry,
|
||||
// this metadata entry is for our disk
|
||||
var extra = disk.get('extra') || [];
|
||||
result = _.find(disksMetaData, function(diskMetaData) {
|
||||
return _.isArray(diskMetaData.extra) && _.intersection(diskMetaData.extra, extra).length;
|
||||
}, this);
|
||||
// if matching "extra" fields doesn't work, try to search by disk id
|
||||
if (!result) {
|
||||
result = _.find(disksMetaData, {disk: disk.id});
|
||||
}
|
||||
return result;
|
||||
},
|
||||
getVolumesInfo: function(disk) {
|
||||
var volumes = {},
|
||||
unallocatedWidth = 100;
|
||||
disk.get('volumes').each(function(volume) {
|
||||
var size = volume.get('size') || 0,
|
||||
width = this.getVolumeWidth(disk, size),
|
||||
name = volume.get('name');
|
||||
unallocatedWidth -= width;
|
||||
volumes[name] = {
|
||||
size: size,
|
||||
width: width,
|
||||
max: volume.getMaxSize(),
|
||||
min: volume.getMinimalSize(this.props.volumes.findWhere({name: name}).get('min_size'))
|
||||
};
|
||||
}, this);
|
||||
volumes.unallocated = {
|
||||
size: disk.getUnallocatedSpace(),
|
||||
width: unallocatedWidth
|
||||
};
|
||||
return volumes;
|
||||
},
|
||||
getVolumeWidth: function(disk, size) {
|
||||
return disk.get('size') ? utils.floor(size / disk.get('size') * 100, 2) : 0;
|
||||
},
|
||||
render: function() {
|
||||
var hasChanges = this.hasChanges(),
|
||||
loadDefaultsDisabled = !!this.state.actionInProgress || this.isLockedScreen(),
|
||||
revertChangesDisabled = !!this.state.actionInProgress || !hasChanges,
|
||||
applyDisabled = !!this.state.actionInProgress || !hasChanges;
|
||||
return (
|
||||
<div className='edit-node-disks-screen'>
|
||||
<div className='row'>
|
||||
<div className='title'>
|
||||
{i18n('cluster_page.nodes_tab.configure_disks.title', {count: this.props.nodes.length, name: this.props.nodes.length && this.props.nodes.at(0).get('name')})}
|
||||
</div>
|
||||
<div className='col-xs-12 node-disks'>
|
||||
{this.props.disks.map(function(disk, index) {
|
||||
return (<NodeDisk
|
||||
disk={disk}
|
||||
key={index}
|
||||
disabled={this.state.actionInProgress}
|
||||
volumes={this.props.volumes}
|
||||
volumesInfo={this.getVolumesInfo(disk)}
|
||||
diskMetaData={this.getDiskMetaData(disk)}
|
||||
/>);
|
||||
}, this)}
|
||||
</div>
|
||||
<div className='col-xs-12 page-buttons'>
|
||||
<div className='well clearfix'>
|
||||
<div className='btn-group'>
|
||||
<button onClick={this.returnToNodeList} className='btn btn-default btn-return'>{i18n('cluster_page.nodes_tab.back_to_nodes_button')}</button>
|
||||
</div>
|
||||
<div className='btn-group pull-right'>
|
||||
<button className='btn btn-default btn-defaults' onClick={this.loadDefaults} disabled={loadDefaultsDisabled}>{i18n('common.load_defaults_button')}</button>
|
||||
<button className='btn btn-default btn-revert-changes' onClick={this.revertChanges} disabled={revertChangesDisabled}>{i18n('common.cancel_changes_button')}</button>
|
||||
<button className='btn btn-success btn-apply' onClick={this.applyChanges} disabled={applyDisabled}>{i18n('common.apply_button')}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
var NodeDisk = React.createClass({
|
||||
getInitialState: function() {
|
||||
return {key: _.now()};
|
||||
},
|
||||
componentDidMount: function() {
|
||||
$('.disk-details', this.getDOMNode())
|
||||
.on('show.bs.collapse', this.setState.bind(this, {collapsed: true}, null))
|
||||
.on('hide.bs.collapse', this.setState.bind(this, {collapsed: false}, null));
|
||||
},
|
||||
updateDisk: function(name, value, force) {
|
||||
var size = parseInt(value) || 0,
|
||||
volumeInfo = this.props.volumesInfo[name];
|
||||
if (size > volumeInfo.max) {
|
||||
size = volumeInfo.max;
|
||||
} else if (size < volumeInfo.min) {
|
||||
size = volumeInfo.min;
|
||||
}
|
||||
this.props.disk.get('volumes').findWhere({name: name}).set({size: size});
|
||||
this.props.disk.trigger('change', this.props.disk);
|
||||
if (force) {
|
||||
this.setState({key: _.now()});
|
||||
}
|
||||
},
|
||||
render: function() {
|
||||
var disk = this.props.disk,
|
||||
volumesInfo = this.props.volumesInfo,
|
||||
diskMetaData = this.props.diskMetaData,
|
||||
sortOrder = ['name', 'model', 'size'],
|
||||
ns = 'cluster_page.nodes_tab.configure_disks.';
|
||||
return (
|
||||
<div className='col-xs-12 disk-box' data-disk={disk.id} key={this.props.key}>
|
||||
<div className='row'>
|
||||
<h4 className='col-xs-6'>
|
||||
{disk.get('name')} ({disk.id})
|
||||
</h4>
|
||||
<h4 className='col-xs-6 text-right'>
|
||||
{i18n(ns + 'total_space')} : {utils.showDiskSize(disk.get('size'), 2)}
|
||||
</h4>
|
||||
</div>
|
||||
<div className='row disk-visual clearfix'>
|
||||
{this.props.volumes.map(function(volume, index) {
|
||||
var volumeName = volume.get('name');
|
||||
return (
|
||||
<div className={'volume-group pull-left volume-type-' + (index + 1)} data-volume={volumeName} key={'volume_' + volumeName} style={{width: volumesInfo[volumeName].width + '%'}}>
|
||||
<div className='text-center toggle' data-toggle='collapse' data-target={'#' + disk.get('name')}>
|
||||
<div>{volume.get('label')}</div>
|
||||
<div className='volume-group-size'>{utils.showDiskSize(volumesInfo[volumeName].size, 2)}</div>
|
||||
</div>
|
||||
{volumesInfo[volumeName].min <= 0 && this.state.collapsed &&
|
||||
<div className='close-btn' onClick={_.partial(this.updateDisk, volumeName, 0, true)}>×</div>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}, this)}
|
||||
<div className={'volume-group pull-left'} data-volume='unallocated' style={{width: volumesInfo.unallocated.width + '%'}}>
|
||||
<div className='text-center toggle' data-toggle='collapse' data-target={'#' + disk.get('name')}>
|
||||
<div className='volume-group-name'>{i18n(ns + 'unallocated')}</div>
|
||||
<div className='volume-group-size'>{utils.showDiskSize(volumesInfo.unallocated.size, 2)}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='row collapse disk-details' id={disk.get('name')} key='diskDetails' ref={disk.get('name')}>
|
||||
<div className='col-xs-5'>
|
||||
{diskMetaData &&
|
||||
<div>
|
||||
<h5>{i18n(ns + 'disk_information')}</h5>
|
||||
<div className='form-horizontal disk-info-box'>
|
||||
{_.map(utils.sortEntryProperties(diskMetaData, sortOrder), function(propertyName) {
|
||||
return (
|
||||
<div className='form-group' key={'property_' + propertyName}>
|
||||
<label className='col-xs-2'>{propertyName.replace(/_/g, ' ')}</label>
|
||||
<div className='col-xs-10'>
|
||||
<p className='form-control-static'>
|
||||
{propertyName == 'size' ? utils.showDiskSize(diskMetaData[propertyName]) : diskMetaData[propertyName]}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<div className='col-xs-7'>
|
||||
<h5>{i18n(ns + 'volume_groups')}</h5>
|
||||
<div className='form-horizontal disk-utility-box'>
|
||||
{this.props.volumes.map(function(volume, index) {
|
||||
var volumeName = volume.get('name'),
|
||||
value = volumesInfo[volumeName].size,
|
||||
currentMaxSize = volumesInfo[volumeName].max,
|
||||
currentMinSize = _.max([volumesInfo[volumeName].min, 0]),
|
||||
disabled = this.props.disabled || currentMaxSize == currentMinSize;
|
||||
|
||||
var props = {
|
||||
name: volumeName,
|
||||
min: currentMinSize,
|
||||
max: currentMaxSize,
|
||||
disabled: disabled
|
||||
};
|
||||
return (
|
||||
<div key={'edit_' + volumeName}>
|
||||
<div className='form-group volume-group row' data-volume={volumeName}>
|
||||
<label className='col-xs-4 volume-group-label'>
|
||||
<span ref={'volume-group-flag ' + volumeName} className={'volume-type-' + (index + 1)}> </span>
|
||||
{volume.get('label')}
|
||||
</label>
|
||||
<div className='col-xs-4 volume-group-range'>
|
||||
<controls.Input {...props}
|
||||
key={currentMinSize + currentMaxSize + this.state.key}
|
||||
type='range'
|
||||
onInput={this.updateDisk}
|
||||
defaultValue={value}
|
||||
/>
|
||||
</div>
|
||||
<controls.Input {...props}
|
||||
type='number'
|
||||
wrapperClassName='col-xs-3 volume-group-input'
|
||||
onChange={_.partialRight(this.updateDisk, true)}
|
||||
value={value}
|
||||
/>
|
||||
<div className='col-xs-1 volume-group-size-label'>{i18n('common.size.mb')}</div>
|
||||
</div>
|
||||
{!!value && value == currentMinSize &&
|
||||
<div className='volume-group-notice text-right'>{i18n(ns + 'minimum_reached')}</div>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}, this)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
return EditNodeDisksScreen;
|
||||
});
|
@ -23,39 +23,18 @@ define(
|
||||
'utils',
|
||||
'models',
|
||||
'dispatcher',
|
||||
'jsx!views/dialogs',
|
||||
'jsx!views/controls',
|
||||
'jsx!component_mixins',
|
||||
'jquery-ui/sortable'
|
||||
],
|
||||
function($, _, Backbone, React, i18n, utils, models, dispatcher, dialogs, controls, ComponentMixins) {
|
||||
function($, _, Backbone, React, i18n, utils, models, dispatcher, controls, ComponentMixins) {
|
||||
'use strict';
|
||||
|
||||
var ScreenMixin, EditNodeInterfacesScreen, NodeInterface;
|
||||
|
||||
ScreenMixin = {
|
||||
goToNodeList: function() {
|
||||
app.navigate('#cluster/' + this.props.cluster.get('id') + '/nodes', {trigger: true});
|
||||
},
|
||||
isLockedScreen: function() {
|
||||
var nodesAvailableForChanges = this.props.nodes.filter(function(node) {
|
||||
return node.get('pending_addition') || node.get('status') == 'error';
|
||||
});
|
||||
return !nodesAvailableForChanges.length ||
|
||||
this.props.cluster && !!this.props.cluster.tasks({group: 'deployment', status: 'running'}).length;
|
||||
},
|
||||
returnToNodeList: function() {
|
||||
if (this.hasChanges()) {
|
||||
dialogs.DiscardSettingsChangesDialog.show({cb: _.bind(this.goToNodeList, this)});
|
||||
} else {
|
||||
this.goToNodeList();
|
||||
}
|
||||
}
|
||||
};
|
||||
var EditNodeInterfacesScreen, NodeInterface;
|
||||
|
||||
EditNodeInterfacesScreen = React.createClass({
|
||||
mixins: [
|
||||
ScreenMixin,
|
||||
ComponentMixins.nodeConfigurationScreenMixin,
|
||||
ComponentMixins.backboneMixin('interfaces', 'change:checked change:slaves change:bond_properties change:interface_properties reset sync'),
|
||||
ComponentMixins.backboneMixin('cluster', 'change:status change:networkConfiguration change:nodes sync'),
|
||||
ComponentMixins.backboneMixin('nodes', 'change sync')
|
||||
@ -64,7 +43,7 @@ function($, _, Backbone, React, i18n, utils, models, dispatcher, dialogs, contro
|
||||
fetchData: function(options) {
|
||||
var cluster = options.cluster,
|
||||
nodeIds = utils.deserializeTabOptions(options.screenOptions[0]).nodes.split(',').map(function(id) {return parseInt(id, 10);}),
|
||||
nodeLoadingErrorNS = 'cluster_page.nodes_tab.configure_interfaces.node_loading_error.',
|
||||
nodeLoadingErrorNS = 'cluster_page.nodes_tab.node_loading_error.',
|
||||
nodes,
|
||||
networkConfiguration,
|
||||
networksMetadata = new models.ReleaseNetworkProperties();
|
||||
@ -76,7 +55,7 @@ function($, _, Backbone, React, i18n, utils, models, dispatcher, dialogs, contro
|
||||
title: i18n(nodeLoadingErrorNS + 'title'),
|
||||
message: i18n(nodeLoadingErrorNS + 'load_error')
|
||||
});
|
||||
ScreenMixin.goToNodeList();
|
||||
ComponentMixins.nodeConfigurationScreenMixin.goToNodeList(cluster);
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -1,49 +0,0 @@
|
||||
/*
|
||||
* Copyright 2013 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(
|
||||
[
|
||||
'underscore',
|
||||
'utils',
|
||||
'models',
|
||||
'jsx!views/dialogs',
|
||||
'views/cluster_page_tabs/nodes_tab_screens/screen'
|
||||
],
|
||||
function(_, utils, models, dialogs, Screen) {
|
||||
'use strict';
|
||||
var EditNodeScreen;
|
||||
|
||||
EditNodeScreen = Screen.extend({
|
||||
constructorName: 'EditNodeScreen',
|
||||
keepScrollPosition: true,
|
||||
disableControls: function(disable) {
|
||||
this.updateButtonsState(disable || this.isLocked());
|
||||
},
|
||||
returnToNodeList: function() {
|
||||
if (this.hasChanges()) {
|
||||
dialogs.DiscardSettingsChangesDialog.show({cb: _.bind(this.goToNodeList, this)});
|
||||
} else {
|
||||
this.goToNodeList();
|
||||
}
|
||||
},
|
||||
initialize: function(options) {
|
||||
_.defaults(this, options);
|
||||
var nodeIds = utils.deserializeTabOptions(this.screenOptions[0]).nodes.split(',').map(function(id) {return parseInt(id, 10);});
|
||||
this.nodes = new models.Nodes(this.cluster.get('nodes').getByIds(nodeIds));
|
||||
}
|
||||
});
|
||||
|
||||
return EditNodeScreen;
|
||||
});
|
@ -1,49 +0,0 @@
|
||||
/*
|
||||
* Copyright 2013 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(['backbone'],
|
||||
function(Backbone) {
|
||||
'use strict';
|
||||
var Screen;
|
||||
|
||||
Screen = Backbone.View.extend({
|
||||
constructorName: 'Screen',
|
||||
keepScrollPosition: false,
|
||||
goToNodeList: function() {
|
||||
app.navigate('#cluster/' + this.cluster.id + '/nodes', {trigger: true});
|
||||
},
|
||||
isLocked: function() {
|
||||
return !!this.cluster.tasks({group: 'deployment', status: 'running'}).length;
|
||||
},
|
||||
initButtons: function() {
|
||||
this.loadDefaultsButton = new Backbone.Model({disabled: false});
|
||||
this.cancelChangesButton = new Backbone.Model({disabled: true});
|
||||
this.applyChangesButton = new Backbone.Model({disabled: true});
|
||||
},
|
||||
setupButtonsBindings: function() {
|
||||
var bindings = {attributes: [{name: 'disabled', observe: 'disabled'}]};
|
||||
this.stickit(this.loadDefaultsButton, {'.btn-defaults': bindings});
|
||||
this.stickit(this.cancelChangesButton, {'.btn-revert-changes': bindings});
|
||||
this.stickit(this.applyChangesButton, {'.btn-apply': bindings});
|
||||
},
|
||||
updateButtonsState: function(state) {
|
||||
this.applyChangesButton.set('disabled', state);
|
||||
this.cancelChangesButton.set('disabled', state);
|
||||
this.loadDefaultsButton.set('disabled', state);
|
||||
}
|
||||
});
|
||||
|
||||
return Screen;
|
||||
});
|
@ -52,7 +52,7 @@ define(['jquery', 'underscore', 'react', 'utils', 'jsx!component_mixins'], funct
|
||||
controls.Input = React.createClass({
|
||||
mixins: [tooltipMixin],
|
||||
propTypes: {
|
||||
type: React.PropTypes.oneOf(['text', 'password', 'textarea', 'checkbox', 'radio', 'select', 'hidden', 'number']).isRequired,
|
||||
type: React.PropTypes.oneOf(['text', 'password', 'textarea', 'checkbox', 'radio', 'select', 'hidden', 'number', 'range']).isRequired,
|
||||
name: React.PropTypes.node,
|
||||
label: React.PropTypes.node,
|
||||
description: React.PropTypes.node,
|
||||
@ -62,6 +62,7 @@ define(['jquery', 'underscore', 'react', 'utils', 'jsx!component_mixins'], funct
|
||||
tooltipText: React.PropTypes.node,
|
||||
toggleable: React.PropTypes.bool,
|
||||
onChange: React.PropTypes.func,
|
||||
onInput: React.PropTypes.func,
|
||||
extraContent: React.PropTypes.node
|
||||
},
|
||||
getInitialState: function() {
|
||||
@ -79,24 +80,37 @@ define(['jquery', 'underscore', 'react', 'utils', 'jsx!component_mixins'], funct
|
||||
debouncedChange: _.debounce(function() {
|
||||
return this.onChange();
|
||||
}, 200, {leading: true}),
|
||||
debouncedInput: _.debounce(function() {
|
||||
return this.onInput();
|
||||
}, 10, {leading: true}),
|
||||
onChange: function() {
|
||||
if (this.props.onChange) {
|
||||
var input = this.getInputDOMNode();
|
||||
return this.props.onChange(this.props.name, this.props.type == 'checkbox' ? input.checked : input.value);
|
||||
}
|
||||
},
|
||||
onInput: function() {
|
||||
if (this.props.onInput) {
|
||||
var input = this.getInputDOMNode();
|
||||
return this.props.onInput(this.props.name, input.value);
|
||||
}
|
||||
},
|
||||
renderInput: function() {
|
||||
var classes = {'form-control': true};
|
||||
var classes = {'form-control': this.props.type != 'range'};
|
||||
classes[this.props.inputClassName] = this.props.inputClassName;
|
||||
var props = {
|
||||
ref: 'input',
|
||||
key: 'input',
|
||||
type: (this.props.toggleable && this.state.visible) ? 'text' : this.props.type,
|
||||
className: utils.classNames(classes),
|
||||
// debounced onChange callback is supported for uncontrolled inputs
|
||||
onChange: this.props.value ? this.onChange : this.debouncedChange
|
||||
},
|
||||
Tag = _.contains(['select', 'textarea'], this.props.type) ? this.props.type : 'input',
|
||||
className: utils.classNames(classes)
|
||||
};
|
||||
if (this.props.type == 'range') {
|
||||
props.onInput = this.debouncedInput;
|
||||
} else {
|
||||
// debounced onChange callback is supported for uncontrolled inputs
|
||||
props.onChange = this.props.value ? this.onChange : this.debouncedChange;
|
||||
}
|
||||
var Tag = _.contains(['select', 'textarea'], this.props.type) ? this.props.type : 'input',
|
||||
input = <Tag {...this.props} {...props}>{this.props.children}</Tag>,
|
||||
isCheckboxOrRadio = this.isCheckboxOrRadio(),
|
||||
inputWrapperClasses = {
|
||||
|
@ -57,13 +57,13 @@ casper.then(function() {
|
||||
|
||||
this.then(function() {
|
||||
this.test.comment('Testing nodes disk block');
|
||||
this.click(sdaDisk + ' .disk-visual');
|
||||
this.click(sdaDisk + ' .disk-visual [data-volume=os] .toggle');
|
||||
vmSDA = this.getElementAttribute(sdaDiskVM + ' input', 'value');
|
||||
osSDA = this.getElementAttribute(sdaDiskOS + ' input', 'value');
|
||||
this.test.assertExists(sdaDiskOS, 'Base system group form is presented');
|
||||
this.test.assertExists(sdaDiskVM, 'Virtual Storage group form is presented');
|
||||
this.test.assertExists(sdaDisk + ' .disk-visual [data-volume=os] .close-btn.hide', 'Button Close for Base system group is not presented');
|
||||
this.test.assertDoesntExist(sdaDisk + ' .disk-visual [data-volume=vm] .close-btn:visible', 'Button Close for Virtual Storage group is presented');
|
||||
this.test.assertDoesntExist(sdaDisk + ' .disk-visual [data-volume=os] .close-btn', 'Button Close for Base system group is not presented');
|
||||
this.test.assertExists(sdaDisk + ' .disk-visual [data-volume=vm] .close-btn', 'Button Close for Virtual Storage group is presented');
|
||||
});
|
||||
|
||||
this.then(function() {
|
||||
@ -73,10 +73,6 @@ casper.then(function() {
|
||||
this.test.assertExists('.btn-defaults:not(:disabled)', 'Load Defaults button is enabled');
|
||||
this.test.assertExists('.btn-revert-changes:not(:disabled)', 'Cancel button is enabled');
|
||||
this.test.assertExists('.btn-apply:not(:disabled)', 'Apply button is enabled');
|
||||
this.click(sdaDiskVM + ' .volume-group-use-all-allowed-btn button');
|
||||
this.test.assertExists('.btn-defaults:not(:disabled)', 'Load Defaults button is enabled');
|
||||
this.test.assertExists('.btn-revert-changes:disabled', 'Cancel button is disabled');
|
||||
this.test.assertExists('.btn-apply:disabled', 'Apply button is disabled');
|
||||
});
|
||||
|
||||
this.then(function() {
|
||||
@ -85,8 +81,8 @@ casper.then(function() {
|
||||
this.click('.btn-defaults');
|
||||
this.test.assertSelectorAppears('.btn-defaults:not(:disabled)', 'Defaults were loaded');
|
||||
this.then(function() {
|
||||
this.test.assertEvalEquals(function(sdaDiskVM) {return $(sdaDiskVM + ' input').attr('value')}, vmSDA, 'Volume group input control VM contains default value', {sdaDiskVM:sdaDiskVM});
|
||||
this.test.assertEvalEquals(function(sdaDiskOS) {return $(sdaDiskOS + ' input').attr('value')}, osSDA, 'Volume group input control OS contains default value', {sdaDiskOS:sdaDiskOS});
|
||||
this.test.assertEvalEquals(function(sdaDiskVM) {return $(sdaDiskVM + ' input[type=number]').attr('value')}, vmSDA, 'Volume group input control VM contains default value', {sdaDiskVM:sdaDiskVM});
|
||||
this.test.assertEvalEquals(function(sdaDiskOS) {return $(sdaDiskOS + ' input[type=number]').attr('value')}, osSDA, 'Volume group input control OS contains default value', {sdaDiskOS:sdaDiskOS});
|
||||
});
|
||||
});
|
||||
|
||||
@ -95,42 +91,10 @@ casper.then(function() {
|
||||
this.click(sdaDisk + ' .disk-visual [data-volume=vm] .close-btn');
|
||||
this.test.assertEquals(this.getElementBounds(sdaDisk + ' .disk-visual [data-volume=vm]').width, 0, 'VM group was removed successfully');
|
||||
this.click('.btn-revert-changes');
|
||||
this.test.assertEvalEquals(function(sdaDiskVM) {return $(sdaDiskVM + ' input').attr('value')}, vmSDA, 'Volume group input control VM contains default value', {sdaDiskVM:sdaDiskVM});
|
||||
this.test.assertEvalEquals(function(sdaDiskVM) {return $(sdaDiskVM + ' input[type=number]').attr('value')}, vmSDA, 'Volume group input control VM contains default value', {sdaDiskVM:sdaDiskVM});
|
||||
this.click(sdaDisk + ' .disk-visual [data-volume=vm] .close-btn');
|
||||
this.test.assertEval(function(sdaDisk) {return $(sdaDisk + ' .disk-visual [data-volume=unallocated]').width() > 0}, 'There is unallocated space after Virtual Storage VG removal',{sdaDisk:sdaDisk});
|
||||
this.test.assertEvalEquals(function(sdaDiskVM) {return $(sdaDiskVM + ' input').val()}, '0', 'Volume group input control contains correct value',{sdaDiskVM:sdaDiskVM});
|
||||
this.click(sdaDiskVM + ' .volume-group-use-all-allowed-btn button');
|
||||
this.test.assertEquals(this.getElementBounds(sdaDisk + ' .disk-visual [data-volume=unallocated]').width, 0, 'Use all unallocated area for VM');
|
||||
this.fill(sdaDiskVM, {'vm': '0'});
|
||||
this.evaluate(function(sdaDiskVM) {$(sdaDiskVM + ' input').keyup();},{sdaDiskVM: sdaDiskVM});
|
||||
this.test.assertEquals(this.getElementBounds(sdaDisk + ' .disk-visual [data-volume=vm]').width, 0, 'VM group was removed successfully');
|
||||
this.test.assertEval(function(sdaDisk) {return $(sdaDisk + ' .disk-visual [data-volume=unallocated]').width() > 0}, 'There is unallocated space after Virtual Storage VG removal', {sdaDisk:sdaDisk});
|
||||
this.test.assertEvalEquals(function(sdaDiskVM) {return $(sdaDiskVM + ' input').val()},'0', 'Volume group input control contains correct value',{sdaDiskVM:sdaDiskVM});
|
||||
});
|
||||
|
||||
|
||||
this.then(function() {
|
||||
this.test.comment('Testing use all allowed link');
|
||||
this.click(sdaDiskOS + ' .volume-group-use-all-allowed-btn button');
|
||||
this.test.assertEquals(this.getElementBounds(sdaDisk + ' .disk-visual [data-volume=unallocated]').width, 0, 'Use all allowed link works correctly');
|
||||
});
|
||||
|
||||
this.then(function() {
|
||||
this.test.comment('Testing validation of VG size');
|
||||
this.fill(sdaDiskOS, {'os': '0'});
|
||||
this.evaluate(function(sdaDiskOS) {$(sdaDiskOS + ' input').keyup();}, {sdaDiskOS: sdaDiskOS});
|
||||
this.test.assertExists(sdaDiskOS + '.has-error', 'Field validation has worked');
|
||||
this.test.assertEval(function(sdaDisk) {return $(sdaDisk + ' .disk-visual [data-volume=os]').width() > 0}, 'VG size was not changed',{sdaDisk:sdaDisk});
|
||||
this.click(vdaDisk + ' .disk-visual');
|
||||
this.test.assertExists(vdaDiskVM, 'Virtual Storage group form is presented');
|
||||
this.fill(vdaDiskVM, {'vm': '10000'});
|
||||
this.evaluate(function(vdaDiskVM) {$(vdaDiskVM + ' input').keyup();}, {vdaDiskVM: vdaDiskVM});
|
||||
this.fill(vdaDiskOS, {'os': '50000'});
|
||||
this.evaluate(function(vdaDiskOS) {$(vdaDiskOS + ' input').keyup();}, {vdaDiskOS: vdaDiskOS});
|
||||
this.test.assertDoesntExist(vdaDiskOS + '.has-error', 'Field validation has worked');
|
||||
this.fill(vdaDiskVM, {'vm': '200000'});
|
||||
this.evaluate(function(vdaDiskVM) {$(vdaDiskVM + ' input').keyup();}, {vdaDiskVM: vdaDiskVM});
|
||||
this.test.assertExists(vdaDiskOS + '.has-error', 'Field validation has worked in case of number that bigger than available space on disk');
|
||||
this.test.assertEvalEquals(function(sdaDiskVM) {return $(sdaDiskVM + ' input[type=number]').val()}, '0', 'Volume group input control contains correct value',{sdaDiskVM:sdaDiskVM});
|
||||
});
|
||||
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user