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:
Julia Aranovich 2016-01-26 18:19:48 +03:00
parent cc501d6013
commit 05a62fe06a
16 changed files with 431 additions and 102 deletions

View File

@ -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}}

View File

@ -159,6 +159,13 @@ NODE_LIST_FILTERS = Enum(
'group_id'
)
NODE_ROLE_GROUPS = Enum(
'base',
'compute',
'storage',
'other'
)
NETWORK_INTERFACE_TYPES = Enum(
'ether',
'bond'

View File

@ -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),
)

View File

@ -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:

View File

@ -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': {

View File

@ -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)
)

View File

@ -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,

View File

@ -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);
}
}
};

View File

@ -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

View File

@ -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);

View File

@ -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;
}
}

View File

@ -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);

View File

@ -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']);

View File

@ -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

View File

@ -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')

View File

@ -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>
);
}