Merge "Node list sorting and filtering"

This commit is contained in:
Jenkins 2015-07-14 15:18:19 +00:00 committed by Gerrit Code Review
commit e4e19e41a5
14 changed files with 1632 additions and 696 deletions

View File

@ -19,20 +19,31 @@ from nailgun import consts
from nailgun.api.v1.validators.json_schema import base_types
from nailgun.api.v1.validators.json_schema import role
CLUSTER_UI_SETTING = {
CLUSTER_UI_SETTINGS = {
"type": "object",
"required": ["view_mode", "grouping"],
"required": ["view_mode", "filter", "sort", "search"],
"properties": {
"view_mode": {
"type": "string",
"description": "View mode of cluster nodes",
"enum": list(consts.NODE_VIEW_MODES)
"enum": list(consts.NODE_VIEW_MODES),
},
"grouping": {
"filter": {
"type": "object",
"description": "Filters applied to node list",
},
"sort": {
"type": "array",
"description": "Sorters applied to node list",
'minItems': 1,
'items': [
{'type': 'object'},
],
},
"search": {
"type": "string",
"description": "Grouping mode of cluster nodes",
"enum": list(consts.CLUSTER_GROUPING)
}
"description": "Search value applied to node list",
},
}
}
@ -57,7 +68,7 @@ single_schema = {
"type": "string",
"enum": list(consts.CLUSTER_NET_PROVIDERS)
},
"ui_settings": CLUSTER_UI_SETTING,
"ui_settings": CLUSTER_UI_SETTINGS,
"release_id": {"type": "number"},
"pending_release_id": base_types.NULLABLE_ID,
"replaced_deployment_info": {"type": "object"},

View File

@ -72,12 +72,6 @@ NOVA_NET_MANAGERS = Enum(
'VlanManager'
)
CLUSTER_GROUPING = Enum(
'roles',
'hardware',
'both'
)
CLUSTER_NET_PROVIDERS = Enum(
'nova_network',
'neutron'

View File

@ -509,7 +509,12 @@ def upgrade_cluster_ui_settings():
sa.Column(
'ui_settings',
fields.JSON(),
server_default='{"view_mode": "standard", "grouping": "roles"}',
server_default=jsonutils.dumps({
"view_mode": "standard",
"filter": {},
"sort": [{"roles": "asc"}],
"search": ""
}),
nullable=False
)
)
@ -524,7 +529,7 @@ def downgrade_cluster_ui_settings():
sa.Enum(
'roles', 'hardware', 'both', name='cluster_grouping'),
nullable=False,
default=consts.CLUSTER_GROUPING.roles
default='roles'
)
)
op.drop_column('clusters', 'ui_settings')

View File

@ -25,6 +25,8 @@ from sqlalchemy import Unicode
from sqlalchemy.orm import backref
from sqlalchemy.orm import relationship
from oslo.serialization import jsonutils
from nailgun import consts
from nailgun.db import db
@ -69,7 +71,12 @@ class Cluster(Base):
ui_settings = Column(
JSON,
nullable=False,
server_default='{"view_mode": "standard", "grouping": "roles"}'
server_default=jsonutils.dumps({
"view_mode": "standard",
"filter": {},
"sort": [{"roles": "asc"}],
"search": ""
}),
)
name = Column(Unicode(50), unique=True, nullable=False)
release_id = Column(Integer, ForeignKey('releases.id'), nullable=False)

View File

@ -52,20 +52,31 @@ from nailgun.utils import dict_merge
from nailgun.utils import traverse
CLUSTER_UI_SETTING = {
CLUSTER_UI_SETTINGS = {
"type": "object",
"required": ["view_mode", "grouping"],
"required": ["view_mode", "filter", "sort", "search"],
"properties": {
"view_mode": {
"type": "string",
"description": "View mode of cluster nodes",
"enum": list(consts.NODE_VIEW_MODES)
"enum": list(consts.NODE_VIEW_MODES),
},
"grouping": {
"filter": {
"type": "object",
"description": "Filters applied to node list",
},
"sort": {
"type": "array",
"description": "Sorters applied to node list",
'minItems': 1,
'items': [
{'type': 'object'},
],
},
"search": {
"type": "string",
"description": "Grouping mode of cluster nodes",
"enum": list(consts.CLUSTER_GROUPING)
}
"description": "Search value applied to node list",
},
}
}
@ -171,7 +182,7 @@ class Cluster(NailgunObject):
"type": "string",
"enum": list(consts.CLUSTER_NET_PROVIDERS)
},
"ui_settings": CLUSTER_UI_SETTING,
"ui_settings": CLUSTER_UI_SETTINGS,
"release_id": {"type": "number"},
"pending_release_id": {"type": "number"},
"replaced_deployment_info": {"type": "object"},

View File

@ -643,9 +643,9 @@ class TestClusterUISettingsMigration(base.BaseAlembicMigrationTest):
self.assertItemsEqual(
ui_settings['view_mode'],
consts.NODE_VIEW_MODES.standard)
self.assertItemsEqual(
ui_settings['grouping'],
consts.CLUSTER_GROUPING.roles)
self.assertItemsEqual(ui_settings['filter'], {})
self.assertItemsEqual(ui_settings['sort'], [{'roles': 'asc'}])
self.assertItemsEqual(ui_settings['search'], '')
class TestClusterBondMetaMigration(base.BaseAlembicMigrationTest):

View File

@ -326,13 +326,6 @@ define([
}
return _.isEmpty(errors) ? null : errors;
},
groupings: function() {
return {
roles: i18n('cluster_page.nodes_tab.roles'),
hardware: i18n('cluster_page.nodes_tab.hardware_info'),
both: i18n('cluster_page.nodes_tab.roles_and_hardware_info')
};
},
viewModes: function() {
return ['standard', 'compact'];
},
@ -378,14 +371,13 @@ define([
resource = this.get('meta').memory.total;
} else if (resourceName == 'disks') {
resource = _.pluck(this.get('meta').disks, 'size').sort(function(a, b) {return a - b;});
} else if (resourceName == 'disks_amount') {
resource = this.get('meta').disks.length;
} else if (resourceName == 'interfaces') {
resource = this.get('meta').interfaces.length;
}
} catch (ignore) {}
if (_.isNaN(resource)) {
resource = 0;
}
return resource;
return _.isNaN(resource) ? 0 : resource;
},
sortedRoles: function(preferredOrder) {
return _.union(this.get('roles'), this.get('pending_roles')).sort(function(a, b) {
@ -407,8 +399,15 @@ define([
return releaseRoles.findWhere({name: role}).get('label');
}).join(', ');
},
getHardwareSummary: function() {
return i18n('node_details.hdd') + ': ' + utils.showDiskSize(this.resource('hdd')) + ' \u00A0 ' + i18n('node_details.ram') + ': ' + utils.showMemorySize(this.resource('ram'));
getStatusSummary: function() {
// 'offline' status has higher priority
if (!this.get('online')) return 'offline';
var status = this.get('status');
// 'removing' end 'error' statuses have higher priority
if (_.contains(['removing', 'error'], status)) return status;
if (this.get('pending_addition')) return 'pending_addition';
if (this.get('pending_deletion')) return 'pending_deletion';
return status;
}
});

View File

@ -1242,51 +1242,196 @@ button, .btn:not(.btn-link) {.font-semibold;}
}
.node-management-panel {
height: 70px;
label {
font-size: @base-font-size - 2;
select, input {
.font-normal;
}
}
.control-buttons-box {
overflow: hidden;
.btn-group {
margin-left: 10px;
.glyphicon {
margin-right: 5px;
}
}
}
.label-wrapper {
margin-bottom: 5px;
}
.filter-group {
input[name=filter] {
padding-right: 22px;
}
.btn-clear-filter {
position: absolute;
bottom: 21px;
right: 24px;
}
}
@management-panel-indent: 5px;
min-height: 70px;
.view-mode-switcher {
padding-right: 0;
margin-right: @management-panel-indent * 3;
float: left;
.btn-group label {
width: auto;
font-size: @base-font-size;
padding: 7px 10px 3px;
}
}
.node-list-management-buttons {
button {
margin-right: @management-panel-indent;
}
.search {
min-width: 350px;
position: relative;
.form-group {
margin: 0;
}
.btn-clear-search {
position: absolute;
right: 0;
top: 6px;
}
}
}
.well {
margin: @management-panel-indent * 2 0;
> div {
margin: 0 0 @management-panel-indent @management-panel-indent;
}
.well-heading {
.font-semibold;
.btn-link {
padding: 0;
}
}
.multiselect {
&:last-child .btn-link {
padding-left: @management-panel-indent;
}
}
.control-buttons {
float: none;
margin: 0;
}
}
.help-block {
display: none;
}
.active-sorters-filters {
border-radius: @management-panel-indent;
border: 1px solid @gray + 80%;
padding: @management-panel-indent * 2;
margin: @management-panel-indent * 2 0 @management-panel-indent * 4;
cursor: pointer;
.active-sorters, .active-filters {
position: relative;
> div {
padding-right: 40px;
> div {
margin-right: @management-panel-indent;
display: inline;
}
}
> strong {
padding-right: 0;
}
button {
padding: 0;
position: absolute;
top: 0;
right: @management-panel-indent * 2;
}
&.active-filters {
margin-bottom: @management-panel-indent * 2;
}
}
}
.form-group:not(.checkbox-group) label {
width: 60px;
margin: 8px 5px 0 0;
float: left;
&:empty {
display: none;
}
}
.control-buttons-box {
overflow: hidden;
.btn-group {
margin-left: @management-panel-indent;
.glyphicon {
margin-right: @management-panel-indent;
}
}
}
.filter-group {
margin-bottom: @management-panel-indent;
&:first-child {
margin-top: @management-panel-indent * 2;
}
&:last-child {
margin-bottom: @management-panel-indent * 3;
position: relative;
}
.form-group {
margin-right: @management-panel-indent;
}
.glyphicon {
margin-right: @management-panel-indent;
&.glyphicon-plus-sign {
position: relative;
top: 7px;
}
}
}
.sorters, .filters {
.glyphicon {
padding-left: @management-panel-indent;
}
}
.sorters {
.form-control {
width: auto;
float: left;
}
}
}
.multiselect, .number-range {
.popover {
display: block;
width: 210px;
top: 25px;
.arrow {
display: none;
}
}
&.multiselect {
.popover-content {
> div {
padding: 0 5px;
.custom-tumbler + span {
position: relative;
top: 3px;
}
label {
.font-normal;
font-size: @base-font-size - 2;
}
&.divider {
border-bottom: 1px solid @gray + 80%;
margin: 8px 0 10px;
}
}
}
}
&.number-range {
.popover {
span {
padding-top: 6px;
}
.form-control {
width: 72px;
margin: 0 5px;
}
}
}
}
// NODE LIST
.node-list {
.select-all {
.label-wrapper {
label {
display: inline-block;
position: relative;
top: 1px;
@ -1294,7 +1439,7 @@ button, .btn:not(.btn-link) {.font-semibold;}
}
}
.node-list-header .select-all {
margin: 0 21px 8px 0;
margin: 0 21px 8px 7px;
}
.nodes-group {
@ -3072,3 +3217,10 @@ button, .btn:not(.btn-link) {.font-semibold;}
margin-top: 10px;
}
}
.btn-link.glyphicon {
padding-left: 0;
&:hover {
text-decoration: none;
}
}

View File

@ -30,8 +30,8 @@
"gb": "GB",
"tb": "TB"
},
"node":"__count__ node",
"node_plural":"__count__ nodes",
"node": "__count__ node",
"node_plural": "__count__ nodes",
"important": "Important",
"success": "Success",
"error": "Error",
@ -59,7 +59,17 @@
"system": "System",
"memory": "Memory",
"disks": "Disks",
"interfaces": "Interfaces"
"interfaces": "Interfaces",
"interfaces_amount": "__count__ interface",
"interfaces_amount_plural": "__count__ interfaces",
"disks_amount": "__count__ disk (__size__)",
"disks_amount_plural": "__count__ disks (__size__)",
"cpu_details": "__real__ real (__total__ total) CPU",
"cores": "__count__ real CPU",
"ht_cores": "__count__ total CPU",
"total_hdd": "__total__ HDD",
"total_ram": "__total__ RAM",
"offline": "Offline"
},
"cluster": {
"status": {
@ -279,6 +289,42 @@
"roles": "Roles",
"hardware_info": "Hardware Info",
"roles_and_hardware_info": "Roles and hardware info",
"sorters": {
"roles": "Roles",
"status": "Status",
"offline": "Offline status",
"name": "Name",
"mac": "MAC address",
"ip": "IP address",
"manufacturer": "Manufacturer",
"cores": "CPU (real)",
"ht_cores": "CPU (total)",
"hdd": "HDD total size",
"disks": "Disks amount and sizes",
"ram": "RAM total size",
"interfaces": "Interfaces amount"
},
"filters": {
"roles": "Roles",
"hdd": "HDD total size (Gb)",
"status": "Status",
"manufacturer": "Manufacturer",
"cores": "CPU (real)",
"ht_cores": "CPU (total)",
"disks_amount": "Disks amount",
"ram": "RAM total size (Gb)",
"interfaces": "Interfaces amount",
"prefixes": {
"cores": "CPU (real)",
"ht_cores": "CPU (total)",
"hdd": "Gb HDD",
"disks_amount": "disks",
"ram": "Gb RAM",
"interfaces": "interfaces"
}
},
"filter_results_amount": "Shown __count__ result for ",
"filter_results_amount_plural": "Shown __count__ results for ",
"assign_roles": "Assign Roles",
"breadcrumbs": {
"add": "Add Nodes",
@ -356,10 +402,18 @@
},
"node_management_panel": {
"group_by": "Group By",
"sort_by": "Sort By",
"clear_all": "Clear All",
"more": "More",
"filter_by": "Filter By",
"filter_placeholder": "Node name/mac",
"search_placeholder": "Search by node name, MAC or IP address",
"edit_roles_button": "Edit Roles",
"add_nodes_button": "Add Nodes",
"more_than": "More than ",
"less_than": "Less than ",
"selected_options": "__count__ selected",
"select_all": "Select All",
"node_management_error": {
"title": "Node Management Error",
"saving_warning": "Unable to apply changes",
@ -398,7 +452,7 @@
"title": "Node Deletion Error",
"node_deletion_warning": "Unable to delete nodes"
},
"no_filtered_nodes_warning": "There are no nodes found matching the filter value.",
"no_filtered_nodes_warning": "No nodes found matching the selected filters.",
"filtered_nodes_count":"__filteredNodesCount__ of __count__ node",
"filtered_nodes_count_plural": "__filteredNodesCount__ of __count__ nodes"
},
@ -2144,8 +2198,8 @@
"gb": "GB",
"tb": "TB"
},
"node":"__count__ 노드",
"node_plural":"__count__ 노드",
"node": "__count__ 노드",
"node_plural": "__count__ 노드",
"important": "중요",
"success": "성공",
"error": "오류",

View File

@ -59,15 +59,16 @@ function($, _, i18n, Backbone, React, utils, models, dispatcher, componentMixins
breadcrumbsPath: function(pageOptions) {
var cluster = pageOptions.cluster,
tabOptions = pageOptions.tabOptions[0],
screenRegexp = /^\w+$/,
isScreen = tabOptions && tabOptions.match(screenRegexp),
addScreenBreadcrumb = tabOptions && tabOptions.match(/^(?!list$)\w+$/),
breadcrumbs = [
['home', '#'],
['environments', '#clusters'],
[cluster.get('name'), '#cluster/' + cluster.get('id') + '/nodes'],
[i18n('cluster_page.tabs.' + pageOptions.activeTab), '#cluster/' + cluster.get('id') + '/' + pageOptions.activeTab, !isScreen]
[i18n('cluster_page.tabs.' + pageOptions.activeTab), '#cluster/' + cluster.get('id') + '/' + pageOptions.activeTab, !addScreenBreadcrumb]
];
if (isScreen) breadcrumbs.push([i18n('cluster_page.nodes_tab.breadcrumbs.' + tabOptions), null, true]);
if (addScreenBreadcrumb) {
breadcrumbs.push([i18n('cluster_page.nodes_tab.breadcrumbs.' + tabOptions), null, true]);
}
return breadcrumbs;
},
title: function(pageOptions) {

View File

@ -43,7 +43,40 @@ function($, _, React, models, NodeListScreen) {
return this.refs.screen.revertChanges();
},
render: function() {
return <NodeListScreen {... _.omit(this.props, 'screenOptions')} ref='screen' mode='add' />;
return <NodeListScreen {... _.omit(this.props, 'screenOptions')}
ref='screen'
mode='add'
sorters={[
'status',
'name',
'mac',
'ip',
'manufacturer',
'cores',
'ht_cores',
'hdd',
'disks',
'ram',
'interfaces'
]}
defaultSorting={[{status: 'asc'}]}
filters={[
'status',
'manufacturer',
'cores',
'ht_cores',
'hdd',
'disks_amount',
'ram',
'interfaces'
]}
statusesToFilter={[
'discover',
'error',
'offline'
]}
defaultFilters={['status']}
/>;
}
});

View File

@ -29,6 +29,44 @@ function(React, NodeListScreen) {
mode='list'
cluster={this.props.cluster}
nodes={this.props.cluster.get('nodes')}
sorters={[
'roles',
'status',
'name',
'mac',
'ip',
'manufacturer',
'cores',
'ht_cores',
'hdd',
'disks',
'ram',
'interfaces'
]}
defaultSorting={[{roles: 'asc'}]}
filters={[
'roles',
'status',
'manufacturer',
'cores',
'ht_cores',
'hdd',
'disks_amount',
'ram',
'interfaces'
]}
statusesToFilter={[
'ready',
'pending_addition',
'pending_deletion',
'provisioned',
'provisioning',
'deploying',
'removing',
'error',
'offline'
]}
defaultFilters={['roles', 'status']}
/>;
}
});

View File

@ -0,0 +1,424 @@
/*
* 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',
'dispatcher',
'jsx!views/controls',
'jsx!views/dialogs'
],
function($, _, i18n, Backbone, React, utils, models, dispatcher, controls, dialogs) {
'use strict';
var Node = React.createClass({
getInitialState: function() {
return {
renaming: false,
actionInProgress: false,
eventNamespace: 'click.editnodename' + this.props.node.id,
extendedView: false
};
},
componentWillUnmount: function() {
$('html').off(this.state.eventNamespace);
},
componentDidUpdate: function() {
if (!this.props.node.get('cluster') && !this.props.checked) this.props.node.set({pending_roles: []}, {assign: true});
},
startNodeRenaming: function(e) {
e.preventDefault();
$('html').on(this.state.eventNamespace, _.bind(function(e) {
if ($(e.target).hasClass('node-name-input')) {
e.preventDefault();
} else {
this.endNodeRenaming();
}
}, this));
this.setState({renaming: true});
},
endNodeRenaming: function() {
$('html').off(this.state.eventNamespace);
this.setState({
renaming: false,
actionInProgress: false
});
},
getNodeLogsLink: function() {
var status = this.props.node.get('status'),
error = this.props.node.get('error_type'),
options = {type: 'remote', node: this.props.node.id};
if (status == 'discover') {
options.source = 'bootstrap/messages';
} else if (status == 'provisioning' || status == 'provisioned' || (status == 'error' && error == 'provision')) {
options.source = 'install/anaconda';
} else if (status == 'deploying' || status == 'ready' || (status == 'error' && error == 'deploy')) {
options.source = 'install/puppet';
}
return '#cluster/' + this.props.cluster.id + '/logs/' + utils.serializeTabOptions(options);
},
applyNewNodeName: function(newName) {
if (newName && newName != this.props.node.get('name')) {
this.setState({actionInProgress: true});
this.props.node.save({name: newName}, {patch: true, wait: true}).always(this.endNodeRenaming);
} else {
this.endNodeRenaming();
}
},
onNodeNameInputKeydown: function(e) {
if (e.key == 'Enter') {
this.applyNewNodeName(this.refs.name.getInputDOMNode().value);
} else if (e.key == 'Escape') {
this.endNodeRenaming();
}
},
discardNodeChanges: function() {
if (this.state.actionInProgress) return;
this.setState({actionInProgress: true});
var node = new models.Node(this.props.node.attributes),
nodeWillBeRemoved = node.get('pending_addition'),
data = nodeWillBeRemoved ? {cluster_id: null, pending_addition: false, pending_roles: []} : {pending_deletion: false};
node.save(data, {patch: true})
.done(_.bind(function() {
this.props.cluster.fetchRelated('nodes').done(_.bind(function() {
if (!nodeWillBeRemoved) this.setState({actionInProgress: false});
}, this));
dispatcher.trigger('updateNodeStats networkConfigurationUpdated');
}, this))
.fail(function(response) {
utils.showErrorDialog({
title: i18n('dialog.discard_changes.cant_discard'),
response: response
});
});
},
removeNode: function(e) {
e.preventDefault();
if (this.props.viewMode == 'compact') this.toggleExtendedNodePanel();
dialogs.RemoveNodeConfirmDialog.show({
cb: this.removeNodeConfirmed
});
},
removeNodeConfirmed: function() {
// sync('delete') is used instead of node.destroy() because we want
// to keep showing the 'Removing' status until the node is truly removed
// Otherwise this node would disappear and might reappear again upon
// cluster nodes refetch with status 'Removing' which would look ugly
// to the end user
Backbone.sync('delete', this.props.node).then(_.bind(function(task) {
dispatcher.trigger('networkConfigurationUpdated updateNodeStats updateNotifications');
if (task.status == 'ready') {
// Do not send the 'DELETE' request again, just get rid
// of this node.
this.props.node.trigger('destroy', this.props.node);
return;
}
this.props.cluster.get('tasks').add(new models.Task(task), {parse: true});
this.props.node.set('status', 'removing');
}, this)
);
},
showNodeDetails: function(e) {
e.preventDefault();
if (this.state.extendedView) this.toggleExtendedNodePanel();
dialogs.ShowNodeInfoDialog.show({node: this.props.node});
},
sortRoles: function(roles) {
var preferredOrder = this.props.cluster.get('release').get('roles');
return roles.sort(function(a, b) {
return _.indexOf(preferredOrder, a) - _.indexOf(preferredOrder, b);
});
},
toggleExtendedNodePanel: function() {
var states = this.state.extendedView ? {extendedView: false, renaming: false} : {extendedView: true};
this.setState(states);
},
renderNameControl: function() {
if (this.state.renaming) return (
<controls.Input
ref='name'
type='text'
name='node-name'
defaultValue={this.props.node.get('name')}
inputClassName='form-control node-name-input'
disabled={this.state.actionInProgress}
onKeyDown={this.onNodeNameInputKeydown}
autoFocus
/>
);
return (
<p
title={i18n('cluster_page.nodes_tab.node.edit_name')}
onClick={!this.state.actionInProgress && this.startNodeRenaming}
>
{this.props.node.get('name') || this.props.node.get('mac')}
</p>
);
},
renderStatusLabel: function(status) {
return (
<span>
{i18n('cluster_page.nodes_tab.node.status.' + status, {
os: this.props.cluster.get('release').get('operating_system') || 'OS'
})}
</span>
);
},
renderNodeProgress: function(showPercentage) {
var nodeProgress = this.props.node.get('progress');
return (
<div className='progress'>
<div className='progress-bar' role='progressbar' style={{width: _.max([nodeProgress, 3]) + '%'}}>
{showPercentage && (nodeProgress + '%')}
</div>
</div>
);
},
renderNodeHardwareSummary: function() {
var node = this.props.node;
return (
<div className='node-hardware'>
<span>{i18n('node_details.cpu')}: {node.resource('cores') || '0'} ({node.resource('ht_cores') || '?'})</span>
<span>{i18n('node_details.hdd')}: {node.resource('hdd') ? utils.showDiskSize(node.resource('hdd')) : '?' + i18n('common.size.gb')}</span>
<span>{i18n('node_details.ram')}: {node.resource('ram') ? utils.showMemorySize(node.resource('ram')) : '?' + i18n('common.size.gb')}</span>
</div>
);
},
renderLogsLink: function(iconRepresentation) {
return (
<a className={iconRepresentation ? 'icon icon-logs' : 'btn'} href={this.getNodeLogsLink()}>
{!iconRepresentation && i18n('cluster_page.nodes_tab.node.view_logs')}
</a>
);
},
renderNodeCheckbox: function() {
return (
<controls.Input
type='checkbox'
name={this.props.node.id}
checked={this.props.checked}
disabled={!this.props.node.isSelectable()}
onChange={this.props.mode != 'edit' && this.props.onNodeSelection}
wrapperClassName='pull-left'
/>
);
},
renderRemoveButton: function() {
return (
<button onClick={this.removeNode} className='btn node-remove-button'>
{i18n('cluster_page.nodes_tab.node.remove')}
</button>
);
},
renderRoleList: function(roles) {
return (
<ul className='clearfix'>
{_.map(roles, function(role) {
return (
<li
key={this.props.node.id + role}
className={utils.classNames({'text-success': !this.props.node.get('roles').length})}
>
{role}
</li>
);
}, this)}
</ul>
);
},
showDeleteNodesDialog: function() {
if (this.props.viewMode == 'compact') this.toggleExtendedNodePanel();
dialogs.DeleteNodesDialog.show({nodes: [this.props.node], cluster: this.props.cluster});
},
render: function() {
var ns = 'cluster_page.nodes_tab.node.',
node = this.props.node,
isSelectable = node.isSelectable() && this.props.mode != 'edit',
status = node.getStatusSummary(),
roles = this.sortRoles(node.get('roles').length ? node.get('roles') : node.get('pending_roles'));
// compose classes
var nodePanelClasses = {
node: true,
selected: this.props.checked,
'col-xs-12': this.props.viewMode != 'compact',
unavailable: !isSelectable
};
nodePanelClasses[status] = status;
var manufacturer = node.get('manufacturer'),
logoClasses = {
'manufacturer-logo': true
};
logoClasses[manufacturer.toLowerCase()] = manufacturer;
var statusClasses = {
'node-status': true
},
statusClass = {
pending_addition: 'text-success',
pending_deletion: 'text-warning',
error: 'text-danger',
ready: 'text-info',
provisioning: 'text-info',
deploying: 'text-success',
provisioned: 'text-info'
}[status];
statusClasses[statusClass] = true;
if (this.props.viewMode == 'compact') return (
<div className='compact-node'>
<div className={utils.classNames(nodePanelClasses)}>
<label className='node-box'>
<div
className='node-box-inner clearfix'
onClick={isSelectable && _.partial(this.props.onNodeSelection, null, !this.props.checked)}
>
<div className='node-buttons'>
{this.props.checked && <i className='glyphicon glyphicon-ok' />}
</div>
<div className='node-name'>
<p>{node.get('name') || node.get('mac')}</p>
</div>
<div className={utils.classNames(statusClasses)}>
{_.contains(['provisioning', 'deploying'], status) ?
this.renderNodeProgress()
:
this.renderStatusLabel(status)
}
</div>
</div>
<div className='node-hardware'>
<p>
<span>
{node.resource('cores') || '0'} ({node.resource('ht_cores') || '?'})
</span> / <span>
{node.resource('hdd') ? utils.showDiskSize(node.resource('hdd')) : '?' + i18n('common.size.gb')}
</span> / <span>
{node.resource('ram') ? utils.showMemorySize(node.resource('ram')) : '?' + i18n('common.size.gb')}
</span>
</p>
<p className='btn btn-link' onClick={this.toggleExtendedNodePanel}>
{i18n(ns + 'more_info')}
</p>
</div>
</label>
</div>
{this.state.extendedView &&
<controls.Popover className='node-popover' toggle={this.toggleExtendedNodePanel}>
<div>
<div className='node-name clearfix'>
{this.renderNodeCheckbox()}
<div className='name pull-left'>
{this.renderNameControl()}
</div>
</div>
<div className='node-stats'>
{!!roles.length &&
<div className='role-list'>
<i className='glyphicon glyphicon-pushpin' />
{this.renderRoleList(roles)}
</div>
}
<div className={utils.classNames(statusClasses)}>
<i className='glyphicon glyphicon-time' />
{_.contains(['provisioning', 'deploying'], status) ?
<div>
{this.renderStatusLabel(status)}
{this.renderLogsLink()}
{this.renderNodeProgress(true)}
</div>
:
<div>
{this.renderStatusLabel(status)}
{status == 'offline' && this.renderRemoveButton()}
{!!node.get('cluster') &&
(node.hasChanges() ?
<button className='btn btn-discard' onClick={node.get('pending_addition') ? this.showDeleteNodesDialog : this.discardNodeChanges}>
{i18n(ns + (node.get('pending_addition') ? 'discard_addition' : 'discard_deletion'))}
</button>
:
this.renderLogsLink()
)
}
</div>
}
</div>
</div>
<div className='hardware-info clearfix'>
<div className={utils.classNames(logoClasses)} />
{this.renderNodeHardwareSummary()}
</div>
<div className='node-popover-buttons'>
<button className='btn btn-default node-details' onClick={this.showNodeDetails}>Details</button>
</div>
</div>
</controls.Popover>
}
</div>
);
return (
<div className={utils.classNames(nodePanelClasses)}>
<label className='node-box'>
{this.renderNodeCheckbox()}
<div className={utils.classNames(logoClasses)} />
<div className='node-name'>
<div className='name'>
{this.renderNameControl()}
</div>
<div className='role-list'>
{this.renderRoleList(roles)}
</div>
</div>
<div className='node-action'>
{!!node.get('cluster') &&
((this.props.locked || !node.hasChanges()) ?
this.renderLogsLink(true)
:
<div
className='icon'
title={i18n(ns + (node.get('pending_addition') ? 'discard_addition' : 'discard_deletion'))}
onClick={node.get('pending_addition') ? this.showDeleteNodesDialog : this.discardNodeChanges}
/>
)
}
</div>
<div className={utils.classNames(statusClasses)}>
{_.contains(['provisioning', 'deploying'], status) ?
this.renderNodeProgress(true)
:
<div>
{this.renderStatusLabel(status)}
{status == 'offline' && this.renderRemoveButton()}
</div>
}
</div>
{this.renderNodeHardwareSummary()}
<div className='node-settings' onClick={this.showNodeDetails} />
</label>
</div>
);
}
});
return Node;
});