Redesign of node roles panel
* Redesigned a role list view on Add Nodes/Edit Roles screens in Fuel UI to take up less space on the screen * Also the role list is grouped by a new 'group' role attribute Implements: blueprint redesign-of-node-roles-panel Change-Id: Ie99e2b911439ae050a5212febe0fac7502550ea9
This commit is contained in:
parent
cc501d6013
commit
05a62fe06a
|
@ -13,6 +13,8 @@
|
|||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from nailgun import consts
|
||||
|
||||
from nailgun.api.v1.validators.json_schema import base_types
|
||||
|
||||
VOLUME_ALLOCATION = {
|
||||
|
@ -95,6 +97,11 @@ ROLE_META_INFO = {
|
|||
"type": "array",
|
||||
"description": ("Specified roles will be updated if current role"
|
||||
" added to cluster first time.")},
|
||||
"group": {
|
||||
"type": "string",
|
||||
"enum": list(consts.NODE_ROLE_GROUPS),
|
||||
"description": ("Name of a role group which reflects the role"
|
||||
" purpose in cloud or deployment process")},
|
||||
"limits": LIMITS,
|
||||
"restrictions": base_types.RESTRICTIONS}}
|
||||
|
||||
|
|
|
@ -159,6 +159,13 @@ NODE_LIST_FILTERS = Enum(
|
|||
'group_id'
|
||||
)
|
||||
|
||||
NODE_ROLE_GROUPS = Enum(
|
||||
'base',
|
||||
'compute',
|
||||
'storage',
|
||||
'other'
|
||||
)
|
||||
|
||||
NETWORK_INTERFACE_TYPES = Enum(
|
||||
'ether',
|
||||
'bond'
|
||||
|
|
|
@ -21,11 +21,14 @@ Create Date: 2015-12-15 17:20:49.519542
|
|||
"""
|
||||
|
||||
from alembic import op
|
||||
import six
|
||||
import sqlalchemy as sa
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
from oslo_serialization import jsonutils
|
||||
|
||||
from nailgun import consts
|
||||
|
||||
revision = '11a9adc6d36a'
|
||||
down_revision = '43b2cb64dae6'
|
||||
|
||||
|
@ -34,9 +37,11 @@ def upgrade():
|
|||
add_foreign_key_ondelete()
|
||||
upgrade_ip_address()
|
||||
update_vips_from_network_roles()
|
||||
upgrade_node_roles_metadata()
|
||||
|
||||
|
||||
def downgrade():
|
||||
downgrade_node_roles_metadata()
|
||||
remove_foreign_key_ondelete()
|
||||
downgrade_ip_address()
|
||||
|
||||
|
@ -535,3 +540,51 @@ def downgrade_ip_address():
|
|||
)
|
||||
op.drop_column('ip_addrs', 'is_user_defined')
|
||||
op.drop_column('ip_addrs', 'vip_namespace')
|
||||
|
||||
|
||||
def upgrade_node_roles_metadata():
|
||||
connection = op.get_bind()
|
||||
select_query = sa.sql.text("SELECT id, roles_metadata FROM releases")
|
||||
update_query = sa.sql.text(
|
||||
"UPDATE releases SET roles_metadata = :roles_metadata WHERE id = :id")
|
||||
|
||||
for id, roles_metadata in connection.execute(select_query):
|
||||
roles_metadata = jsonutils.loads(roles_metadata)
|
||||
|
||||
role_groups = {
|
||||
'controller': 'base',
|
||||
'compute': 'compute',
|
||||
'virt': 'compute',
|
||||
'compute-vmware': 'compute',
|
||||
'ironic': 'compute',
|
||||
'cinder': 'storage',
|
||||
'cinder-block-device': 'storage',
|
||||
'cinder-vmware': 'storage',
|
||||
'ceph-osd': 'storage'
|
||||
}
|
||||
for role_name, role_metadata in six.iteritems(roles_metadata):
|
||||
role_metadata['group'] = role_groups\
|
||||
.get(role_name, consts.NODE_ROLE_GROUPS.other)
|
||||
|
||||
connection.execute(
|
||||
update_query,
|
||||
id=id,
|
||||
roles_metadata=jsonutils.dumps(roles_metadata),
|
||||
)
|
||||
|
||||
|
||||
def downgrade_node_roles_metadata():
|
||||
connection = op.get_bind()
|
||||
select_query = sa.sql.text("SELECT id, roles_metadata FROM releases")
|
||||
update_query = sa.sql.text(
|
||||
"UPDATE releases SET roles_metadata = :roles_metadata WHERE id = :id")
|
||||
|
||||
for id, roles_metadata in connection.execute(select_query):
|
||||
roles_metadata = jsonutils.loads(roles_metadata)
|
||||
for role_name, role_metadata in six.iteritems(roles_metadata):
|
||||
del role_metadata['group']
|
||||
connection.execute(
|
||||
update_query,
|
||||
id=id,
|
||||
roles_metadata=jsonutils.dumps(roles_metadata),
|
||||
)
|
||||
|
|
|
@ -19,6 +19,7 @@
|
|||
has_primary: true
|
||||
public_ip_required: true
|
||||
public_for_dvr_required: true
|
||||
group: "base"
|
||||
limits:
|
||||
min: 1
|
||||
recommended: 3
|
||||
|
@ -31,14 +32,16 @@
|
|||
description: "A Compute node creates, manages, and terminates virtual machine instances."
|
||||
weight: 20
|
||||
public_for_dvr_required: true
|
||||
group: "compute"
|
||||
limits:
|
||||
recommended: 1
|
||||
fault_tolerance: "2%"
|
||||
cinder:
|
||||
# NOTE: naming, see https://bugs.launchpad.net/fuel/+bug/1383224
|
||||
name: "Storage - Cinder"
|
||||
name: "Cinder"
|
||||
description: "Cinder provides scheduling of block storage resources, typically delivered over iSCSI and other compatible backend storage systems. Block storage can be used for database storage, expandable file systems, or to provide a server with access to raw block level devices."
|
||||
weight: 30
|
||||
group: "storage"
|
||||
limits:
|
||||
recommended: 1
|
||||
restrictions:
|
||||
|
@ -48,11 +51,12 @@
|
|||
- condition: "settings:storage.volumes_ceph.value == true"
|
||||
message: "Ceph RBD cannot be used with Cinder"
|
||||
cinder-block-device:
|
||||
name: 'Storage - Cinder Block Device'
|
||||
name: 'Cinder Block Device'
|
||||
description: 'Host node for Cinder Block Devices'
|
||||
has_primary: false
|
||||
public_ip_required: false
|
||||
weight: 35
|
||||
group: "storage"
|
||||
conflicts:
|
||||
- controller
|
||||
- cinder
|
||||
|
@ -64,18 +68,20 @@
|
|||
- condition: "settings:storage.volumes_ceph.value == true"
|
||||
message: "Ceph RBD cannot be used with Cinder Block Device"
|
||||
cinder-vmware:
|
||||
name: "Storage - Cinder Proxy to VMware Datastore"
|
||||
name: "Cinder Proxy to VMware Datastore"
|
||||
description: "Cinder-VMware provides scheduling of block storage resources delivered over VMware vCenter. Block storage can be used for database storage, expandable file systems, or providing a server with access to raw block level devices."
|
||||
weight: 40
|
||||
group: "storage"
|
||||
limits:
|
||||
recommended: 1
|
||||
restrictions:
|
||||
- condition: "settings:common.use_vcenter.value == false"
|
||||
action: "hide"
|
||||
ceph-osd:
|
||||
name: "Storage - Ceph OSD"
|
||||
name: "Ceph OSD"
|
||||
description: "Ceph storage can be configured to provide storage for block volumes (Cinder), images (Glance) and ephemeral instance storage (Nova). It can also provide object storage through the S3 and Swift API (See settings to enable each)."
|
||||
weight: 50
|
||||
group: "storage"
|
||||
limits:
|
||||
min: "settings:storage.osd_pool_size.value"
|
||||
restrictions:
|
||||
|
@ -87,6 +93,7 @@
|
|||
name: "Telemetry - MongoDB"
|
||||
description: "A feature-complete and recommended database for storage of metering data from OpenStack Telemetry (Ceilometer)."
|
||||
weight: 60
|
||||
group: "other"
|
||||
conflicts:
|
||||
- compute
|
||||
- ceph-osd
|
||||
|
@ -109,10 +116,12 @@
|
|||
name: "Operating System"
|
||||
description: "Install base Operating System without additional packages and configuration."
|
||||
weight: 70
|
||||
group: "other"
|
||||
virt:
|
||||
name: "Virtual"
|
||||
description: "ADVANCED: Make available possibilities to spawn vms on this node that can be assign as a normal nodes."
|
||||
weight: 80
|
||||
group: "compute"
|
||||
public_ip_required: true
|
||||
conflicts:
|
||||
- controller
|
||||
|
@ -124,6 +133,7 @@
|
|||
name: "Compute VMware"
|
||||
description: "A node that runs nova-compute with VCDriver, that manages ESXi computing resources via VMware vCenter."
|
||||
weight: 90
|
||||
group: "compute"
|
||||
conflicts:
|
||||
- controller
|
||||
- compute
|
||||
|
@ -141,6 +151,7 @@
|
|||
name: "Ironic"
|
||||
description: "Ironic conductor"
|
||||
weight: 100
|
||||
group: "compute"
|
||||
limits:
|
||||
min: 1
|
||||
recommended: 3
|
||||
|
@ -1193,7 +1204,7 @@
|
|||
volumes_lvm:
|
||||
value: true
|
||||
label: "Cinder LVM over iSCSI for volumes"
|
||||
description: "It is recommended to have at least one Storage - Cinder LVM node."
|
||||
description: "It is recommended to have at least one Cinder node."
|
||||
weight: 10
|
||||
type: "checkbox"
|
||||
restrictions:
|
||||
|
@ -1201,7 +1212,7 @@
|
|||
volumes_block_device:
|
||||
value: false
|
||||
label: "Cinder Block device driver"
|
||||
description: "High performance block device storage. It is recommended to have at least one Storage - Cinder Block Device"
|
||||
description: "High performance block device storage. It is recommended to have at least one Cinder Block Device"
|
||||
weight: 15
|
||||
type: "checkbox"
|
||||
restrictions:
|
||||
|
@ -1248,7 +1259,7 @@
|
|||
osd_pool_size:
|
||||
value: "3"
|
||||
label: "Ceph object replication factor"
|
||||
description: "Configures the default number of object replicas in Ceph. This number must be equal to or lower than the number of deployed 'Storage - Ceph OSD' nodes."
|
||||
description: "Configures the default number of object replicas in Ceph. This number must be equal to or lower than the number of deployed 'Ceph OSD' nodes."
|
||||
weight: 85
|
||||
type: "text"
|
||||
regex:
|
||||
|
|
|
@ -280,7 +280,7 @@ class TestDataMigration(BaseTestCase):
|
|||
'storage': {
|
||||
'volumes_lvm': {
|
||||
'description': ('It is recommended to have at least '
|
||||
'one Storage - Cinder LVM node.')
|
||||
'one Cinder node.')
|
||||
}
|
||||
},
|
||||
'common': {
|
||||
|
|
|
@ -16,6 +16,7 @@ import alembic
|
|||
from oslo_serialization import jsonutils
|
||||
import sqlalchemy as sa
|
||||
|
||||
from nailgun import consts
|
||||
from nailgun.db import db
|
||||
from nailgun.db import dropdb
|
||||
from nailgun.db.migration import ALEMBIC_CONFIG
|
||||
|
@ -42,6 +43,54 @@ def prepare():
|
|||
'version': '2015.1-8.0',
|
||||
'operating_system': 'ubuntu',
|
||||
'state': 'available',
|
||||
'roles': jsonutils.dumps([
|
||||
'controller',
|
||||
'compute',
|
||||
'virt',
|
||||
'compute-vmware',
|
||||
'ironic',
|
||||
'cinder',
|
||||
'cinder-block-device',
|
||||
'cinder-vmware',
|
||||
'ceph-osd',
|
||||
'mongo',
|
||||
'base-os',
|
||||
]),
|
||||
'roles_metadata': jsonutils.dumps({
|
||||
'controller': {
|
||||
'name': 'Controller',
|
||||
},
|
||||
'compute': {
|
||||
'name': 'Compute',
|
||||
},
|
||||
'virt': {
|
||||
'name': 'Virtual',
|
||||
},
|
||||
'compute-vmware': {
|
||||
'name': 'Compute VMware',
|
||||
},
|
||||
'ironic': {
|
||||
'name': 'Ironic',
|
||||
},
|
||||
'cinder': {
|
||||
'name': 'Cinder',
|
||||
},
|
||||
'cinder-block-device': {
|
||||
'name': 'Cinder Block Device',
|
||||
},
|
||||
'cinder-vmware': {
|
||||
'name': 'Cinder Proxy to VMware Datastore',
|
||||
},
|
||||
'ceph-osd': {
|
||||
'name': 'Ceph OSD',
|
||||
},
|
||||
'mongo': {
|
||||
'name': 'Telemetry - MongoDB',
|
||||
},
|
||||
'base-os': {
|
||||
'name': 'Operating System',
|
||||
}
|
||||
}),
|
||||
'networks_metadata': jsonutils.dumps({
|
||||
'neutron': {
|
||||
'networks': [
|
||||
|
@ -299,3 +348,32 @@ class TestVipMigration(base.BaseAlembicMigrationTest):
|
|||
),
|
||||
result
|
||||
)
|
||||
|
||||
|
||||
class TestNodeRolesMigration(base.BaseAlembicMigrationTest):
|
||||
def test_category_is_injected_to_roles_meta(self):
|
||||
result = db.execute(
|
||||
sa.select([self.meta.tables['releases'].c.roles_metadata])
|
||||
)
|
||||
rel_row = result.fetchone()
|
||||
|
||||
roles_metadata = jsonutils.loads(rel_row[0])
|
||||
|
||||
role_groups = {
|
||||
'controller': 'base',
|
||||
'compute': 'compute',
|
||||
'virt': 'compute',
|
||||
'compute-vmware': 'compute',
|
||||
'ironic': 'compute',
|
||||
'cinder': 'storage',
|
||||
'cinder-block-device': 'storage',
|
||||
'cinder-vmware': 'storage',
|
||||
'ceph-osd': 'storage'
|
||||
}
|
||||
|
||||
for role_name in roles_metadata:
|
||||
role_group = roles_metadata[role_name].get('group')
|
||||
self.assertEquals(
|
||||
role_group,
|
||||
role_groups.get(role_name, consts.NODE_ROLE_GROUPS.other)
|
||||
)
|
||||
|
|
|
@ -375,7 +375,7 @@ def move_orchestrator_data_to_attributes(connection):
|
|||
|
||||
def upgrade_attributes_metadata_6_0_to_6_1(attributes_meta):
|
||||
attributes_meta['editable']['storage']['volumes_lvm']['description'] = \
|
||||
'It is recommended to have at least one Storage - Cinder LVM node.'
|
||||
'It is recommended to have at least one Cinder node.'
|
||||
attributes_meta['editable']['common']['use_vcenter'] = {
|
||||
"value": False,
|
||||
"weight": 30,
|
||||
|
|
|
@ -106,7 +106,7 @@ export function pollingMixin(updateInterval, delayedStart) {
|
|||
|
||||
export var outerClickMixin = {
|
||||
propTypes: {
|
||||
toggle: React.PropTypes.func.isRequired
|
||||
toggle: React.PropTypes.func
|
||||
},
|
||||
getInitialState() {
|
||||
return {
|
||||
|
@ -119,12 +119,16 @@ export var outerClickMixin = {
|
|||
}
|
||||
},
|
||||
componentDidMount() {
|
||||
$('html').on(this.state.clickEventName, this.handleBodyClick);
|
||||
Backbone.history.on('route', _.partial(this.props.toggle, false), this);
|
||||
if (this.props.toggle) {
|
||||
$('html').on(this.state.clickEventName, this.handleBodyClick);
|
||||
Backbone.history.on('route', _.partial(this.props.toggle, false), this);
|
||||
}
|
||||
},
|
||||
componentWillUnmount() {
|
||||
$('html').off(this.state.clickEventName);
|
||||
Backbone.history.off('route', null, this);
|
||||
if (this.props.toggle) {
|
||||
$('html').off(this.state.clickEventName);
|
||||
Backbone.history.off('route', null, this);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
<!-- Generator: Adobe Illustrator 15.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
width="80px" height="840px" viewBox="0 0 80 840" enable-background="new 0 0 80 840" xml:space="preserve">
|
||||
width="80px" height="920px" viewBox="0 0 80 920" enable-background="new 0 0 80 920" xml:space="preserve">
|
||||
<g>
|
||||
<circle fill="#48A565" cx="20.5" cy="819.5" r="7.5"/>
|
||||
</g>
|
||||
|
@ -892,4 +892,10 @@
|
|||
"/>
|
||||
<path fill="#FFFFFF" d="M72.502,764h-24c-2.2,0-4,1.8-4,4v24c0,2.2,1.8,4,4,4h24c2.2,0,4-1.8,4-4v-24
|
||||
C76.502,765.8,74.702,764,72.502,764z M64.061,784l-3.559-4.447L56.943,784h-3.842l7.4-9.25l7.4,9.25H64.061z"/>
|
||||
<polygon fill-rule="evenodd" clip-rule="evenodd" fill="#FFFFFF" points="15.243,858.039 17.906,860.646 25.757,853 28.5,855.671
|
||||
17.92,866 12.5,860.71 "/>
|
||||
<polygon fill-rule="evenodd" clip-rule="evenodd" fill="#48A565" points="55.242,858.039 57.906,860.646 65.758,853 68.5,855.671
|
||||
57.92,866 52.5,860.71 "/>
|
||||
<rect x="14" y="898" fill-rule="evenodd" clip-rule="evenodd" fill="#FFFFFF" width="13" height="4"/>
|
||||
<rect x="54" y="898" fill-rule="evenodd" clip-rule="evenodd" fill="#48A565" width="13" height="4"/>
|
||||
</svg>
|
||||
|
|
Before Width: | Height: | Size: 51 KiB After Width: | Height: | Size: 51 KiB |
|
@ -274,6 +274,7 @@ models.Roles = BaseCollection.extend(restrictionMixin).extend({
|
|||
constructorName: 'Roles',
|
||||
comparator: 'weight',
|
||||
model: models.Role,
|
||||
groups: ['base', 'compute', 'storage', 'other'],
|
||||
initialize() {
|
||||
this.processConflictsAndRestrictions();
|
||||
this.on('update', this.processConflictsAndRestrictions, this);
|
||||
|
|
|
@ -270,6 +270,17 @@
|
|||
.icon-default-styles(-52px, -800px);
|
||||
}
|
||||
|
||||
.glyphicon-selected-role {
|
||||
.icon-default-styles(-12px, -852px);
|
||||
width: 17px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.glyphicon-indeterminated-role {
|
||||
.icon-default-styles(-13px, -892px);
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.discard-changes-icon {
|
||||
.icon-default-styles(-13px, -572px);
|
||||
&:hover {
|
||||
|
@ -3049,35 +3060,98 @@ input[type=range] {
|
|||
|
||||
|
||||
.role-panel {
|
||||
.checkbox-group {
|
||||
label {
|
||||
h4 {
|
||||
margin: 0px 0px 20px 0px;
|
||||
}
|
||||
h6 {
|
||||
text-transform: capitalize;
|
||||
margin-top: 17px;
|
||||
.font-normal;
|
||||
}
|
||||
.role-block {
|
||||
@role-height: 48px;
|
||||
position: relative;
|
||||
float: left;
|
||||
width: 24%;
|
||||
height: 50px;
|
||||
background-color: @white;
|
||||
border: 1px solid @gray + 77%;
|
||||
border-radius: 23px;
|
||||
margin-bottom: 12px;
|
||||
cursor: pointer;
|
||||
.font-semibold;
|
||||
&:not(:last-child) {
|
||||
margin-right: 1%;
|
||||
}
|
||||
.role {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
margin-bottom: 2px;
|
||||
padding-top: 2px;
|
||||
color: @base-text-color;
|
||||
cursor: pointer;
|
||||
.custom-tumbler {
|
||||
top: -1px;
|
||||
height: @role-height;
|
||||
display: table-cell;
|
||||
vertical-align: middle;
|
||||
padding: 0px 16px;
|
||||
line-height: 16px;
|
||||
.break-word;
|
||||
i {
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 16px;
|
||||
& + span {
|
||||
display: inline-block;
|
||||
margin-left: 24px;
|
||||
}
|
||||
}
|
||||
.glyphicon {
|
||||
.text-warning;
|
||||
position: relative;
|
||||
top: -5px;
|
||||
left: 5px;
|
||||
}
|
||||
.popover {
|
||||
animation-name: bounceIn;
|
||||
animation-duration: 0.8s;
|
||||
animation-fill-mode: both;
|
||||
animation-timing-function: step-end;
|
||||
position: absolute;
|
||||
bottom: 60px;
|
||||
top: auto;
|
||||
display: block;
|
||||
color: @base-text-color;
|
||||
font-size: @base-font-size - 1;
|
||||
width: 100%;
|
||||
hr {
|
||||
margin: 10px 0;
|
||||
}
|
||||
}
|
||||
&:not(.disabled):hover {
|
||||
background-color: @gray + 77%;
|
||||
border: 1px solid @gray + 77%;
|
||||
}
|
||||
&:not(.disabled):not(.selected):not(.indeterminated) {
|
||||
i {
|
||||
display: none;
|
||||
& + span {
|
||||
margin-left: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
&.selected, &.indeterminated {
|
||||
background-color: @green;
|
||||
border: 1px solid @green;
|
||||
color: @white;
|
||||
&:hover {
|
||||
background-color: @green - 10%;
|
||||
border: 1px solid @green - 10%;
|
||||
}
|
||||
}
|
||||
&.disabled {
|
||||
label {
|
||||
cursor: default;
|
||||
color: @gray;
|
||||
cursor: default;
|
||||
color: @orange;
|
||||
border-color: lighten(@orange, 20%);
|
||||
&.indeterminated {
|
||||
background-color: lighten(@green, 15%);
|
||||
border: 1px solid lighten(@green, 15%);
|
||||
color: @white;
|
||||
}
|
||||
}
|
||||
.help-block {
|
||||
color: @gray;
|
||||
font-size: @base-font-size - 2;
|
||||
margin-left: 28px;
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
.row:last-child .role-block {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -74,14 +74,14 @@ define([
|
|||
},
|
||||
checkNodeRoles: function(assignRoles) {
|
||||
return this.remote
|
||||
.findAllByCssSelector('div.role-panel label')
|
||||
.findAllByCssSelector('.role-panel .role-block')
|
||||
.then(function(roles) {
|
||||
return roles.reduce(
|
||||
function(result, role) {
|
||||
return role
|
||||
.getVisibleText()
|
||||
.then(function(label) {
|
||||
var index = assignRoles.indexOf(label.substr(1));
|
||||
var index = assignRoles.indexOf(label);
|
||||
if (index >= 0) {
|
||||
role.click();
|
||||
assignRoles.splice(index, 1);
|
||||
|
|
|
@ -159,7 +159,7 @@ define([
|
|||
'Capacity table tests': function() {
|
||||
return this.remote
|
||||
.then(function() {
|
||||
return common.addNodesToCluster(1, ['Controller', 'Storage - Cinder']);
|
||||
return common.addNodesToCluster(1, ['Controller', 'Cinder']);
|
||||
})
|
||||
.then(function() {
|
||||
return common.addNodesToCluster(2, ['Compute']);
|
||||
|
@ -189,7 +189,7 @@ define([
|
|||
return common.addNodesToCluster(controllerNodes, ['Controller']);
|
||||
})
|
||||
.then(function() {
|
||||
return common.addNodesToCluster(storageCinderNodes, ['Storage - Cinder']);
|
||||
return common.addNodesToCluster(storageCinderNodes, ['Cinder']);
|
||||
})
|
||||
.then(function() {
|
||||
return common.addNodesToCluster(computeNodes, ['Compute']);
|
||||
|
|
|
@ -48,59 +48,94 @@ define([
|
|||
});
|
||||
},
|
||||
'Add Cluster Nodes': function() {
|
||||
var self = this;
|
||||
return this.remote
|
||||
.assertElementExists('.node-list .alert-warning',
|
||||
'Node list shows warning if there are no nodes in environment')
|
||||
.assertElementExists(
|
||||
'.node-list .alert-warning',
|
||||
'Node list shows warning if there are no nodes in environment'
|
||||
)
|
||||
.clickByCssSelector('.btn-add-nodes')
|
||||
.assertElementsAppear('.node', 2000, 'Unallocated nodes loaded')
|
||||
.assertElementDisabled(applyButtonSelector,
|
||||
'Apply button is disabled until both roles and nodes chosen')
|
||||
.assertElementDisabled('.role-panel [type=checkbox][name=mongo]',
|
||||
'Unavailable role has locked checkbox')
|
||||
.assertElementExists('.role-panel .mongo i.tooltip-icon',
|
||||
'Unavailable role has warning tooltip')
|
||||
.assertElementsExist('.role-panel .row', 4, 'Roles are splitted in groups')
|
||||
.assertElementExists('.role-block.mongo.disabled', 'Unavailable role is locked')
|
||||
.assertElementExists(
|
||||
'.role-block.mongo i.glyphicon-warning-sign',
|
||||
'Unavailable role has warning icon'
|
||||
)
|
||||
.findByCssSelector('.role-block.mongo')
|
||||
.then(function(element) {
|
||||
return self.remote.moveMouseTo(element);
|
||||
})
|
||||
.end()
|
||||
// the following timeout as we have 0.5s transition time for role popover
|
||||
.sleep(600)
|
||||
.assertElementExists(
|
||||
'.role-block.mongo .popover .text-warning',
|
||||
'Role popover is opened and the role warning is shown in the popover'
|
||||
)
|
||||
.then(function() {
|
||||
return clusterPage.checkNodeRoles(['Controller', 'Storage - Cinder']);
|
||||
return clusterPage.checkNodeRoles(['Controller', 'Cinder']);
|
||||
})
|
||||
.assertElementDisabled('.role-panel [type=checkbox][name=compute]',
|
||||
'Compute role can not be added together with selected roles')
|
||||
.assertElementDisabled(applyButtonSelector,
|
||||
'Apply button is disabled until both roles and nodes chosen')
|
||||
.assertElementExists(
|
||||
'.role-block.controller i.glyphicon-selected-role',
|
||||
'Selected role has checkbox icon'
|
||||
)
|
||||
.assertElementExists(
|
||||
'.role-block.compute.disabled',
|
||||
'Compute role can not be added together with selected roles'
|
||||
)
|
||||
.assertElementDisabled(
|
||||
applyButtonSelector,
|
||||
'Apply button is disabled until both roles and nodes chosen'
|
||||
)
|
||||
.then(function() {
|
||||
return clusterPage.checkNodes(nodesAmount);
|
||||
})
|
||||
.clickByCssSelector(applyButtonSelector)
|
||||
.waitForElementDeletion(applyButtonSelector, 2000)
|
||||
.assertElementAppears('.nodes-group', 2000, 'Cluster node list loaded')
|
||||
.assertElementsExist('.node-list .node', nodesAmount, nodesAmount +
|
||||
' nodes were successfully added to the cluster')
|
||||
.assertElementsExist(
|
||||
'.node-list .node',
|
||||
nodesAmount,
|
||||
nodesAmount + ' nodes were successfully added to the cluster'
|
||||
)
|
||||
.assertElementExists('.nodes-group', 'One node group is present');
|
||||
},
|
||||
'Edit cluster node roles': function() {
|
||||
return this.remote
|
||||
.then(function() {
|
||||
return common.addNodesToCluster(1, ['Storage - Cinder']);
|
||||
return common.addNodesToCluster(1, ['Cinder']);
|
||||
})
|
||||
.assertElementsExist('.nodes-group', 2, 'Two node groups are present')
|
||||
// select all nodes
|
||||
.clickByCssSelector('.select-all label')
|
||||
.clickByCssSelector('.btn-edit-roles')
|
||||
.assertElementDisappears('.btn-edit-roles', 2000,
|
||||
'Cluster nodes screen unmounted')
|
||||
.assertElementNotExists('.node-box [type=checkbox]:not(:disabled)',
|
||||
'Node selection is locked on Edit Roles screen')
|
||||
.assertElementNotExists('[name=select-all]:not(:disabled)',
|
||||
'Select All checkboxes are locked on Edit Roles screen')
|
||||
.assertElementExists('.role-panel [type=checkbox][name=controller]:indeterminate',
|
||||
'Controller role checkbox has indeterminate state')
|
||||
.assertElementDisappears('.btn-edit-roles', 2000, 'Cluster nodes screen unmounted')
|
||||
.assertElementNotExists(
|
||||
'.node-box [type=checkbox]:not(:disabled)',
|
||||
'Node selection is locked on Edit Roles screen'
|
||||
)
|
||||
.assertElementNotExists(
|
||||
'[name=select-all]:not(:disabled)',
|
||||
'Select All checkboxes are locked on Edit Roles screen'
|
||||
)
|
||||
.assertElementExists(
|
||||
'.role-block.controller i.glyphicon-indeterminated-role',
|
||||
'Controller role has indeterminate state'
|
||||
)
|
||||
// uncheck Cinder role
|
||||
.then(function() {
|
||||
// uncheck Cinder role
|
||||
return clusterPage.checkNodeRoles(['Storage - Cinder', 'Storage - Cinder']);
|
||||
return clusterPage.checkNodeRoles(['Cinder', 'Cinder']);
|
||||
})
|
||||
.clickByCssSelector(applyButtonSelector)
|
||||
.assertElementDisappears('.btn-apply', 2000, 'Role editing screen unmounted')
|
||||
.assertElementsExist('.node-list .node', nodesAmount,
|
||||
'One node was removed from cluster after editing roles');
|
||||
.assertElementDisappears('.btn-apply', 3000, 'Role editing screen unmounted')
|
||||
.assertElementsExist(
|
||||
'.node-list .node',
|
||||
nodesAmount,
|
||||
'One node was removed from cluster after editing roles'
|
||||
);
|
||||
},
|
||||
'Remove Cluster': function() {
|
||||
return this.remote
|
||||
|
|
|
@ -58,7 +58,7 @@ define([
|
|||
return common.createCluster(clusterName);
|
||||
})
|
||||
.then(function() {
|
||||
return common.addNodesToCluster(1, ['Storage - Cinder']);
|
||||
return common.addNodesToCluster(1, ['Cinder']);
|
||||
})
|
||||
// Just in case - reset and hide badge notification counter by clicking on it
|
||||
.clickByCssSelector('.notifications-icon')
|
||||
|
|
|
@ -28,7 +28,7 @@ import {backboneMixin, pollingMixin, dispatcherMixin, unsavedChangesMixin} from
|
|||
import Node from 'views/cluster_page_tabs/nodes_tab_screens/node';
|
||||
|
||||
var NodeListScreen, MultiSelectControl, NumberRangeControl, ManagementPanel,
|
||||
NodeLabelsPanel, RolePanel, SelectAllMixin, NodeList, NodeGroup;
|
||||
NodeLabelsPanel, RolePanel, Role, SelectAllMixin, NodeList, NodeGroup;
|
||||
|
||||
class Sorter {
|
||||
constructor(name, order, isLabel = false) {
|
||||
|
@ -1681,18 +1681,9 @@ NodeLabelsPanel = React.createClass({
|
|||
});
|
||||
|
||||
RolePanel = React.createClass({
|
||||
componentDidMount() {
|
||||
this.updateIndeterminateRolesState();
|
||||
},
|
||||
componentDidUpdate() {
|
||||
this.updateIndeterminateRolesState();
|
||||
this.assignRoles();
|
||||
},
|
||||
updateIndeterminateRolesState() {
|
||||
_.each(this.refs, (roleView, role) => {
|
||||
roleView.getInputDOMNode().indeterminate = _.contains(this.props.indeterminateRoles, role);
|
||||
});
|
||||
},
|
||||
assignRoles() {
|
||||
var roles = this.props.cluster.get('roles');
|
||||
this.props.nodes.each((node) => {
|
||||
|
@ -1712,9 +1703,9 @@ RolePanel = React.createClass({
|
|||
}
|
||||
});
|
||||
},
|
||||
processRestrictions(role, models) {
|
||||
processRestrictions(role) {
|
||||
var name = role.get('name');
|
||||
var restrictionsCheck = role.checkRestrictions(models, 'disable');
|
||||
var restrictionsCheck = role.checkRestrictions(this.props.configModels, 'disable');
|
||||
var roleLimitsCheckResults = this.props.processedRoleLimits[name];
|
||||
var roles = this.props.cluster.get('roles');
|
||||
var conflicts = _.chain(this.props.selectedRoles)
|
||||
|
@ -1742,31 +1733,93 @@ RolePanel = React.createClass({
|
|||
};
|
||||
},
|
||||
render() {
|
||||
var groups = models.Roles.prototype.groups;
|
||||
var groupedRoles = this.props.cluster.get('roles').groupBy(
|
||||
(role) => _.contains(groups, role.get('group')) ? role.get('group') : 'other'
|
||||
);
|
||||
return (
|
||||
<div className='well role-panel'>
|
||||
<h4>{i18n('cluster_page.nodes_tab.assign_roles')}</h4>
|
||||
{this.props.cluster.get('roles').map((role) => {
|
||||
if (!role.checkRestrictions(this.props.configModels, 'hide').result) {
|
||||
var name = role.get('name');
|
||||
var processedRestrictions = this.props.nodes.length ?
|
||||
this.processRestrictions(role, this.props.configModels) : {};
|
||||
return (
|
||||
<Input
|
||||
key={name}
|
||||
ref={name}
|
||||
type='checkbox'
|
||||
name={name}
|
||||
label={role.get('label')}
|
||||
description={role.get('description')}
|
||||
checked={_.contains(this.props.selectedRoles, name)}
|
||||
disabled={!this.props.nodes.length || processedRestrictions.result}
|
||||
tooltipText={!!this.props.nodes.length && processedRestrictions.message}
|
||||
onChange={this.props.selectRoles}
|
||||
wrapperClassName={name}
|
||||
/>
|
||||
);
|
||||
}
|
||||
{_.map(groups, (group) =>
|
||||
<div key={group} className={group + ' row'}>
|
||||
<div className='col-xs-1'>
|
||||
<h6>{group}</h6>
|
||||
</div>
|
||||
<div className='col-xs-11'>
|
||||
{_.map(groupedRoles[group], (role) => {
|
||||
if (role.checkRestrictions(this.props.configModels, 'hide').result) return null;
|
||||
var roleName = role.get('name');
|
||||
var selected = _.contains(this.props.selectedRoles, roleName);
|
||||
return (
|
||||
<Role
|
||||
key={roleName}
|
||||
ref={roleName}
|
||||
role={role}
|
||||
selected={selected}
|
||||
indeterminated={_.contains(this.props.indeterminateRoles, roleName)}
|
||||
restrictions={this.processRestrictions(role)}
|
||||
isRolePanelDisabled={!this.props.nodes.length}
|
||||
onClick={() => this.props.selectRoles(roleName, !selected)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
Role = React.createClass({
|
||||
getInitialState() {
|
||||
return {
|
||||
isPopoverVisible: false
|
||||
};
|
||||
},
|
||||
togglePopover() {
|
||||
this.setState({isPopoverVisible: !this.state.isPopoverVisible});
|
||||
},
|
||||
render() {
|
||||
var {role, selected, indeterminated, restrictions, isRolePanelDisabled, onClick} = this.props;
|
||||
var disabled = isRolePanelDisabled || restrictions.result;
|
||||
return (
|
||||
<div
|
||||
className={utils.classNames({
|
||||
'role-block': true,
|
||||
[role.get('name')]: true,
|
||||
selected,
|
||||
indeterminated,
|
||||
disabled
|
||||
})}
|
||||
onClick={!disabled && onClick}
|
||||
onMouseEnter={this.togglePopover}
|
||||
onMouseLeave={this.togglePopover}
|
||||
>
|
||||
<div className='role'>
|
||||
<i
|
||||
className={utils.classNames({
|
||||
glyphicon: true,
|
||||
'glyphicon-selected-role': selected,
|
||||
'glyphicon-indeterminated-role': indeterminated && !restrictions.message,
|
||||
'glyphicon-warning-sign': !!restrictions.message
|
||||
})}
|
||||
/>
|
||||
{role.get('label')}
|
||||
</div>
|
||||
{this.state.isPopoverVisible &&
|
||||
<Popover placement='top'>
|
||||
<div>
|
||||
{!!restrictions.message &&
|
||||
<div>
|
||||
<div className='text-warning'>{restrictions.message}</div>
|
||||
<hr />
|
||||
</div>
|
||||
}
|
||||
<div>{role.get('description')}</div>
|
||||
</div>
|
||||
</Popover>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue