Merge "Improvements for the ClusterActionsPanel component"

This commit is contained in:
Jenkins 2016-03-11 12:04:16 +00:00 committed by Gerrit Code Review
commit 385fea48f1
5 changed files with 206 additions and 214 deletions

View File

@ -4478,6 +4478,7 @@ input[type=range] {
margin-bottom: @dashboard-offset;
}
.task-alerts {
padding: 0;
.invalid {
.font-semibold;
font-size: @base-font-size - 1;
@ -4513,15 +4514,31 @@ input[type=range] {
}
}
&.actions-panel {
.no-nodes {
color: @gray;
margin-left: 0;
margin-right: 0;
.action-description {
padding-right: 0;
p:last-child {
padding-bottom: @dashboard-offset;
}
}
.nav {
.dropdown {
margin-right: 0;
li.deployment-modes-label {
text-align: right;
.deployment-modes-label {
color: @gray;
padding: 7px 0 0;
cursor: default;
margin-right: @dashboard-offset;
}
.dropdown-toggle {
padding: 0;
position: relative;
top: -1px;
}
.dropdown-menu {
right: 0;
left: auto;
min-width: auto;
}
}
}

View File

@ -121,8 +121,8 @@ define([
1000,
'Provision VMs action appears on the Dashboard'
)
.clickByCssSelector('.actions-panel .nav button.dropdown-toggle')
.clickByCssSelector('.actions-panel .nav .dropdown-menu li.deploy button')
.clickByCssSelector('.actions-panel .dropdown button.dropdown-toggle')
.clickByCssSelector('.actions-panel .dropdown .dropdown-menu li.deploy button')
.then(function() {
return dashboardPage.discardChanges();
});
@ -262,8 +262,8 @@ define([
total,
'The number of Pending Addition nodes in statistics is correct'
)
.clickByCssSelector('.actions-panel .nav button.dropdown-toggle')
.clickByCssSelector('.actions-panel .nav .dropdown-menu li.deploy button')
.clickByCssSelector('.actions-panel .dropdown button.dropdown-toggle')
.clickByCssSelector('.actions-panel .dropdown .dropdown-menu li.deploy button')
.then(function() {
return dashboardPage.discardChanges();
});

View File

@ -71,8 +71,8 @@ define([
'Provision nodes': function() {
this.timeout = 100000;
return this.remote
.clickByCssSelector('.actions-panel .nav button.dropdown-toggle')
.clickByCssSelector('.actions-panel .nav .dropdown-menu li.provision button')
.clickByCssSelector('.actions-panel .dropdown button.dropdown-toggle')
.clickByCssSelector('.actions-panel .dropdown .dropdown-menu li.provision button')
.assertElementContainsText(
'.btn-provision',
'Provision 1 Node',
@ -123,8 +123,8 @@ define([
.then(function() {
return clusterPage.goToTab('Dashboard');
})
.clickByCssSelector('.actions-panel .nav button.dropdown-toggle')
.clickByCssSelector('.actions-panel .nav .dropdown-menu li.provision button')
.clickByCssSelector('.actions-panel .dropdown button.dropdown-toggle')
.clickByCssSelector('.actions-panel .dropdown .dropdown-menu li.provision button')
.clickByCssSelector('.changes-list .dropdown-toggle')
.clickByCssSelector('.changes-list .btn-select-nodes')
.then(function() {
@ -205,11 +205,11 @@ define([
'Deploy nodes': function() {
this.timeout = 100000;
return this.remote
.clickByCssSelector('.actions-panel .nav button.dropdown-toggle')
.clickByCssSelector('.actions-panel .nav .dropdown-menu li.deployment button')
.clickByCssSelector('.actions-panel .dropdown button.dropdown-toggle')
.clickByCssSelector('.actions-panel .dropdown .dropdown-menu li.deployment button')
.assertElementDisabled('.btn-deploy-nodes', 'There are no provisioned nodes to deploy')
.clickByCssSelector('.actions-panel .nav button.dropdown-toggle')
.clickByCssSelector('.actions-panel .nav .dropdown-menu li.provision button')
.clickByCssSelector('.actions-panel .dropdown button.dropdown-toggle')
.clickByCssSelector('.actions-panel .dropdown .dropdown-menu li.provision button')
.clickByCssSelector('.btn-provision')
.then(function() {
return modal.waitToOpen();
@ -222,8 +222,8 @@ define([
})
.assertElementAppears('div.deploy-process div.progress', 2000, 'Provisioning started')
.assertElementDisappears('div.deploy-process div.progress', 5000, 'Provisioning finished')
.clickByCssSelector('.actions-panel .nav button.dropdown-toggle')
.clickByCssSelector('.actions-panel .nav .dropdown-menu li.deployment button')
.clickByCssSelector('.actions-panel .dropdown button.dropdown-toggle')
.clickByCssSelector('.actions-panel .dropdown .dropdown-menu li.deployment button')
.assertElementContainsText('.btn-deploy-nodes', 'Deploy 1 Node', '1 node to be deployed')
.clickByCssSelector('.btn-deploy-nodes')
.then(function() {

View File

@ -627,8 +627,6 @@
"new_environment_welcome": "Welcome to the New OpenStack Environment!",
"add_nodes": "Add nodes to the OpenStack Environment.",
"deployment_of_environment_cannot_be_started": "Deployment cannot be started due to invalid environment configuration. Please review and address the warnings below before proceeding and see the",
"provisioning_cannot_be_started": "Provisioning of nodes cannot be started due to invalid environment configuration. Please review and address the warnings below before proceeding",
"deployment_of_nodes_cannot_be_started": "Deployment of nodes cannot be started due to invalid environment configuration. Please review and address the warnings below before proceeding",
"stop": "Stop",
"healthcheck": "To view the OpenStack health check status go to ",
"healthcheck_tab": "Healthcheck tab",
@ -666,22 +664,22 @@
"tls_not_enabled": "TLS is not enabled. It is highly recommended to enable and configure TLS.",
"tls_for_horizon_not_enabled": "TLS is not enabled for Horizon public endpoint. It is highly recommended to enable and configure TLS for Horizon.",
"tls_for_services_not_enabled": "TLS is not enabled for OpenStack public endpoints. It is highly recommended to enable and configure TLS.",
"offline_nodes": "__count__ node is offline.",
"offline_nodes_plural": "__count__ nodes are offline.",
"offline_nodes": "__count__ node is offline",
"offline_nodes_plural": "__count__ nodes are offline",
"unprovisioned_virt_nodes": "1 __role__ node is not provisioned.",
"unprovisioned_virt_nodes_plural": "Some __role__ nodes are not provisioned.",
"deployment_mode": "Deployment Mode",
"actions": {
"deploy": {
"title": "Regular deployment",
"title": "Provisioning + Deployment",
"button_title_all_nodes": "Deploy Changes"
},
"provision": {
"title": "Advanced provisioning",
"description": "Provisioning installs an operating system on nodes.",
"title": "Provisioning only",
"description": "\"Provisioning only\" installs the previously selected operating system __os__ on the nodes, but does not deploy OpenStack services.\nTo deploy OpenStack services after provisioning has been completed, select \"Deployment only\" from the Deployment Mode dropdown. To complete provisioning and deployment at the same time, select \"Provisioning + Deployment\" from the Deployment Mode dropdown.",
"nodes_to_provision": "__count__ node is discovered",
"nodes_to_provision_plural": "__count__ nodes are discovered",
"no_nodes_to_provision": "No online discovered nodes to provision.",
"no_nodes": "Nodes must be assigned a role in order to be provisioned. Please use the \"Add Nodes\" button to add roles to available discovered nodes before proceeding.\nPlease select \"Deployment only\" or \"Provisioning + Deployment\" from the Deployment Mode dropdown to continue with already provisioned nodes.",
"button_title_all_nodes": "Provision __count__ Node",
"button_title_all_nodes_plural": "Provision __count__ Nodes",
"button_title_some_nodes": "Provision __selected__ of __count__ Nodes",
@ -689,11 +687,11 @@
"choose_nodes": "Choose nodes for provisioning"
},
"deployment": {
"title": "Advanced deployment",
"description": "Deploys OpenStack on nodes.",
"title": "Deployment only",
"description": "\"Advanced deployment\" deploys OpenStack services on nodes which have the operating system already provisioned.\nTo complete provisioning and deployment at the same time, select \"Provisioning + Deployment\" from the Deployment Mode dropdown.",
"nodes_to_deploy": "__count__ node is provisioned",
"nodes_to_deploy_plural": "__count__ nodes are provisioned",
"no_nodes_to_deploy": "No online provisioned nodes to deploy.",
"no_nodes": "Nodes can only be deployed using the \"Deployment only\" feature if they have been provisioned with an operating system.\nPlease select \"Provisioning only\" or \"Provisioning + Deployment\" from the Deployment Mode dropdown to continue.",
"button_title_all_nodes": "Deploy __count__ Node",
"button_title_all_nodes_plural": "Deploy __count__ Nodes",
"button_title_some_nodes": "Deploy __selected__ of __count__ Nodes",

View File

@ -348,12 +348,6 @@ var ClusterActionsPanel = React.createClass({
);
},
validations(action) {
var checkForOfflineNodes = function(nodes) {
var offlineNodes = _.filter(nodes, (node) => !node.get('online'));
if (offlineNodes.length) {
return i18n(ns + 'offline_nodes', {count: offlineNodes.length});
}
};
switch (action) {
case 'deploy':
return [
@ -373,10 +367,14 @@ var ClusterActionsPanel = React.createClass({
};
}
},
// check for offline nodes
function(cluster) {
return {
blocker: [checkForOfflineNodes(cluster.get('nodes').models)]
};
var offlineNodes = cluster.get('nodes').where({online: false});
if (offlineNodes.length) {
return {
blocker: [i18n(ns + 'offline_nodes', {count: offlineNodes.length})]
};
}
},
// check if TLS settings are not configured
function(cluster) {
@ -494,46 +492,16 @@ var ClusterActionsPanel = React.createClass({
}
}
];
case 'provision':
return [
function(cluster) {
return {
error: [checkForOfflineNodes(
cluster.get('nodes').filter((node) => node.isProvisioningPossible())
)]
};
}
];
case 'deployment':
return [
function(cluster) {
return {
error: [checkForOfflineNodes(
cluster.get('nodes').filter((node) => node.isDeploymentPossible())
)]
};
}
];
case 'spawn_vms':
return [
function(cluster) {
return {
blocker: [checkForOfflineNodes(
cluster.get('nodes').filter(
(node) => node.hasRole('virt') && node.isProvisioningPossible()
)
)]
};
}
];
default:
return [];
}
},
renderNodesAmount(nodes, dictKey) {
renderNodesNumber(nodes, dictKey, showDeleteButton = false) {
if (!nodes.length) return null;
return (
<li className='changes-item'>
{i18n(ns + dictKey, {count: nodes.length})}
{_.all(nodes, (node) => node.get('pending_addition') || node.get('pending_deletion')) &&
{showDeleteButton &&
<button
className='btn btn-link btn-discard-changes'
onClick={() => DiscardNodeChangesDialog.show({cluster: this.props.cluster, nodes})}
@ -573,22 +541,25 @@ var ClusterActionsPanel = React.createClass({
var action = this.state.currentAction;
var actionNs = ns + 'actions.' + action + '.';
var nodes = this.props.cluster.get('nodes');
var {cluster} = this.props;
var nodes = {
provision: new models.Nodes(
cluster.get('nodes').filter((node) => node.isProvisioningPossible())
),
deployment: new models.Nodes(
cluster.get('nodes').filter((node) => node.isDeploymentPossible())
),
spawn_vms: new models.Nodes(
cluster.get('nodes').filter(
(node) => node.hasRole('virt') && node.get('status') === 'discover'
)
),
deploy: cluster.get('nodes')
}[action];
var offlineNodes = nodes.where({online: false});
var alerts = this.validate(action);
var blockerDescriptions = {
provision: <InstructionElement
description='provisioning_cannot_be_started'
isAlert
/>,
deployment: <InstructionElement
description='deployment_of_nodes_cannot_be_started'
isAlert
/>,
spawn_vms: <InstructionElement
description='provisioning_cannot_be_started'
isAlert
/>,
deploy: <InstructionElement
description='deployment_of_environment_cannot_be_started'
isAlert
@ -601,135 +572,144 @@ var ClusterActionsPanel = React.createClass({
};
var actionButtonProps = {
cluster: this.props.cluster,
ns: actionNs,
disabled: !this.isActionAvailable(action)
disabled: !this.isActionAvailable(action),
nodes,
cluster
};
var actionControls;
switch (action) {
case 'deploy':
actionControls = (
<div className='col-xs-3 changes-list' key={action}>
{nodes.hasChanges() &&
<ul>
{this.renderNodesAmount(nodes.where({pending_addition: true}), 'added_node')}
{this.renderNodesAmount(
nodes.where({status: 'provisioned', pending_deletion: false}),
'provisioned_node'
)}
{this.renderNodesAmount(nodes.where({pending_deletion: true}), 'deleted_node')}
</ul>
actionControls = [
nodes.hasChanges() &&
<ul key='node-changes'>
{this.renderNodesNumber(nodes.where({pending_addition: true}), 'added_node', true)}
{this.renderNodesNumber(
nodes.where({pending_deletion: false, status: 'provisioned'}),
'provisioned_node'
)}
{this.renderNodesNumber(nodes.where({pending_deletion: true}), 'deleted_node', true)}
</ul>,
<ClusterActionButton
{...actionButtonProps}
key='action-button'
nodes={nodes}
className='deploy-btn'
iconClassName='deploy-icon'
warning={
_.isEmpty(alerts.blocker) &&
(!_.isEmpty(alerts.error) || !_.isEmpty(alerts.warning))
}
<ClusterActionButton
{...actionButtonProps}
nodes={nodes.models}
className='deploy-btn'
iconClassName='deploy-icon'
warning={
_.isEmpty(alerts.blocker) &&
(!_.isEmpty(alerts.error) || !_.isEmpty(alerts.warning))
}
dialog={DeployClusterDialog}
/>
</div>
);
dialog={DeployClusterDialog}
/>
];
break;
case 'provision':
var nodesToProvision = nodes.filter((node) => node.isProvisioningPossible());
actionControls = [
!!nodesToProvision.length &&
<div className='action-description' key='action-description'>
{i18n(actionNs + 'description')}
</div>,
<div className='col-xs-3 changes-list' key={action}>
<ul>
!!nodes.length &&
<ul key='node-changes'>
<li>
{i18n(
actionNs +
(nodesToProvision.length ? 'nodes_to_provision' : 'no_nodes_to_provision'),
{count: nodesToProvision.length}
)}
{i18n(actionNs + 'nodes_to_provision', {count: nodes.length})}
</li>
</ul>
<ClusterActionButton
{...actionButtonProps}
nodes={nodesToProvision}
className='btn-provision'
dialog={ProvisionNodesDialog}
canSelectNodes
/>
</div>
{!!offlineNodes.length &&
<li>
{i18n(ns + 'offline_nodes', {count: offlineNodes.length})}
</li>
}
</ul>,
<ClusterActionButton
{...actionButtonProps}
key='action-button'
className='btn-provision'
dialog={ProvisionNodesDialog}
canSelectNodes
/>
];
break;
case 'deployment':
var nodesToDeploy = nodes.filter((node) => node.isDeploymentPossible());
actionControls = [
!!nodesToDeploy.length &&
<div className='action-description' key='action-description'>
{i18n(actionNs + 'description')}
</div>,
<div className='col-xs-3 changes-list' key={action}>
<ul>
!!nodes.length &&
<ul key='node-changes'>
<li>
{i18n(
actionNs + (nodesToDeploy.length ? 'nodes_to_deploy' : 'no_nodes_to_deploy'),
{count: nodesToDeploy.length}
)}
{i18n(actionNs + 'nodes_to_deploy', {count: nodes.length})}
</li>
</ul>
<ClusterActionButton
{...actionButtonProps}
nodes={nodesToDeploy}
className='btn-deploy-nodes'
dialog={DeployNodesDialog}
canSelectNodes
/>
</div>
{!!offlineNodes.length &&
<li>
{i18n(ns + 'offline_nodes', {count: offlineNodes.length})}
</li>
}
</ul>,
<ClusterActionButton
{...actionButtonProps}
key='action-button'
className='btn-deploy-nodes'
dialog={DeployNodesDialog}
canSelectNodes
/>
];
break;
case 'spawn_vms':
var vmsToProvision = nodes.filter(
(node) => node.hasRole('virt') && node.get('status') === 'discover'
);
actionControls = (
<div className='col-xs-3 changes-list' key={action}>
<ul>
actionControls = [
<ul key='node-changes'>
<li>
{i18n(
actionNs + 'nodes_to_provision',
{
count: nodes.length,
role: cluster.get('roles').find({name: 'virt'}).get('label')
}
)}
</li>
{!!offlineNodes.length &&
<li>
{i18n(
actionNs + 'nodes_to_provision',
{
count: vmsToProvision.length,
role: this.props.cluster.get('roles').find({name: 'virt'}).get('label')
}
)}
{i18n(ns + 'offline_nodes', {count: offlineNodes.length})}
</li>
</ul>
<ClusterActionButton
{...actionButtonProps}
nodes={vmsToProvision}
className='btn-provision-vms'
dialog={ProvisionVMsDialog}
/>
</div>
);
}
</ul>,
<ClusterActionButton
{...actionButtonProps}
key='action-button'
className='btn-provision-vms'
dialog={ProvisionVMsDialog}
/>
];
break;
default:
actionControls = null;
}
return (
<div className='dashboard-block actions-panel clearfix'>
{this.renderActionsDropdown()}
{actionControls}
<div className='col-xs-9 task-alerts'>
{_.map(['blocker', 'error', 'warning'],
(severity) => <WarningsBlock
key={severity}
severity={severity}
blockersDescription={blockerDescriptions[action]}
alerts={alerts[severity]}
/>
)}
<div className='dashboard-block actions-panel row'>
<div className='col-xs-8' key={action}>
<div className='row'>
<div className='col-xs-12 action-description'>
{utils.renderMultilineText(i18n(
actionNs + (nodes.length ? 'description' : 'no_nodes'),
{
defaultValue: '',
os: cluster.get('release').get('operating_system')
}
))}
</div>
</div>
<div className='row'>
<div className='col-xs-4 changes-list'>
{actionControls}
</div>
<div className='col-xs-8 task-alerts'>
{_.map(['blocker', 'error', 'warning'],
(severity) => <WarningsBlock
key={severity}
severity={severity}
blockersDescription={blockerDescriptions[action]}
alerts={alerts[severity]}
/>
)}
</div>
</div>
</div>
<div className='col-xs-4 action-dropdown'>
{this.renderActionsDropdown()}
</div>
</div>
);
@ -739,30 +719,28 @@ var ClusterActionsPanel = React.createClass({
if (!this.isActionAvailable('spawn_vms')) actions = _.without(actions, 'spawn_vms');
return (
<ul className='nav navbar-nav navbar-right'>
<li className='deployment-modes-label'>
<div className='dropdown'>
<span className='deployment-modes-label'>
{i18n(ns + 'deployment_mode')}:
</li>
<li className='dropdown'>
<button className='btn btn-link dropdown-toggle' data-toggle='dropdown'>
{i18n(
ns + 'actions.' + this.state.currentAction + '.title'
)} <span className='caret'></span>
</button>
<ul className='dropdown-menu'>
{_.map(actions,
(action) => <li key={action} className={action}>
<button
className='btn btn-link'
onClick={() => this.toggleAction(action)}
>
{i18n(ns + 'actions.' + action + '.title')}
</button>
</li>
)}
</ul>
</li>
</ul>
</span>
<button className='btn btn-link dropdown-toggle' data-toggle='dropdown'>
{i18n(
ns + 'actions.' + this.state.currentAction + '.title'
)} <span className='caret'></span>
</button>
<ul className='dropdown-menu'>
{_.map(actions,
(action) => <li key={action} className={action}>
<button
className='btn btn-link'
onClick={() => this.toggleAction(action)}
>
{i18n(ns + 'actions.' + action + '.title')}
</button>
</li>
)}
</ul>
</div>
);
},
render() {
@ -790,7 +768,7 @@ var ClusterActionsPanel = React.createClass({
</div>
);
}
return <div className='row'>{this.renderActions()}</div>;
return <div>{this.renderActions()}</div>;
}
});
@ -798,7 +776,7 @@ var ClusterActionButton = React.createClass({
getInitialState() {
return {
// offline nodes should not be selected for the task
selectedNodeIds: _.pluck(_.filter(this.props.nodes, (node) => node.get('online')), 'id')
selectedNodeIds: _.pluck(this.props.nodes.where({online: true}), 'id')
};
},
getDefaultProps() {
@ -809,8 +787,7 @@ var ClusterActionButton = React.createClass({
};
},
showSelectNodesDialog() {
var {cluster} = this.props;
var nodes = new models.Nodes(this.props.nodes);
var {nodes, cluster} = this.props;
nodes.fetch = function(options) {
return this.constructor.__super__.fetch.call(this,
_.extend({data: {cluster_id: cluster.id}}, options));