Merge "Node list sorting and filtering"
This commit is contained in:
commit
e4e19e41a5
@ -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"},
|
||||
|
@ -72,12 +72,6 @@ NOVA_NET_MANAGERS = Enum(
|
||||
'VlanManager'
|
||||
)
|
||||
|
||||
CLUSTER_GROUPING = Enum(
|
||||
'roles',
|
||||
'hardware',
|
||||
'both'
|
||||
)
|
||||
|
||||
CLUSTER_NET_PROVIDERS = Enum(
|
||||
'nova_network',
|
||||
'neutron'
|
||||
|
@ -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')
|
||||
|
@ -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)
|
||||
|
@ -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"},
|
||||
|
@ -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):
|
||||
|
@ -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;
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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": "오류",
|
||||
|
@ -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) {
|
||||
|
@ -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']}
|
||||
/>;
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -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']}
|
||||
/>;
|
||||
}
|
||||
});
|
||||
|
@ -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;
|
||||
});
|
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user