diff --git a/nailgun/static/models.js b/nailgun/static/models.js index 63c5628360..db83483484 100644 --- a/nailgun/static/models.js +++ b/nailgun/static/models.js @@ -363,10 +363,9 @@ models.Cluster = BaseModel.extend({ return !this.get('is_locked'); }, isDeploymentPossible() { - var nodes = this.get('nodes'); - return this.get('release').get('state') !== 'unavailable' && !!nodes.length && - (nodes.hasChanges() || this.needsRedeployment()) && - !this.task({group: 'deployment', active: true}); + return this.get('release').get('state') !== 'unavailable' && + !this.task({group: 'deployment', active: true}) && + (this.get('status') !== 'operational' || this.get('nodes').hasChanges()); }, getCapacity() { var result = { @@ -458,10 +457,14 @@ models.Node = BaseModel.extend({ if (!onlyDeployedRoles) nodeRoles = nodeRoles.concat(this.get('pending_roles')); return !!_.intersection(nodeRoles, roles).length; }, + isProvisioningPossible() { + var status = this.get('status'); + return status === 'discover' || status === 'error' && this.get('error_type') === 'provisioning'; + }, hasChanges() { return this.get('pending_addition') || this.get('pending_deletion') || - this.get('cluster') && !!this.get('pending_roles').length; + !!this.get('cluster') && !!this.get('pending_roles').length; }, areDisksConfigurable() { var status = this.get('status'); @@ -576,7 +579,9 @@ models.Task = BaseModel.extend({ }, groups: { network: ['verify_networks', 'check_networks'], - deployment: ['update', 'stop_deployment', 'deploy', 'reset_environment', 'spawn_vms'] + deployment: [ + 'update', 'stop_deployment', 'deploy', 'provision', 'reset_environment', 'spawn_vms' + ] }, extendGroups(filters) { var names = utils.composeList(filters.name); @@ -628,11 +633,13 @@ models.Tasks = BaseCollection.extend({ }, comparator: 'id', filterTasks(filters) { - return _.flatten(_.map(this.model.prototype.extendGroups(filters), (name) => { - return this.filter((task) => { - return task.match(_.extend(_.omit(filters, 'group'), {name: name})); - }); - })); + return _.chain(this.model.prototype.extendGroups(filters)) + .map((name) => { + return this.filter((task) => task.match(_.extend(_.omit(filters, 'group'), {name: name}))); + }) + .flatten() + .compact() + .value(); }, findTask(filters) { return this.filterTasks(filters)[0]; diff --git a/nailgun/static/styles/main.less b/nailgun/static/styles/main.less index d1a4485cfe..0b2f3773d1 100644 --- a/nailgun/static/styles/main.less +++ b/nailgun/static/styles/main.less @@ -3218,7 +3218,7 @@ input[type=range] { } } -.display-changes-dialog { +.display-changes-dialog, .provision-nodes-dialog { hr { margin: 8px 0 12px 0; } @@ -4372,7 +4372,7 @@ input[type=range] { margin-bottom: 6px; .capacity-items { margin-top: -9px; - .capacity-item { + > div { cursor: default; background-color: @white; padding: 4px 10px; @@ -4388,6 +4388,7 @@ input[type=range] { } .capacity-value { .font-semibold; + float: right; } } } @@ -4411,9 +4412,11 @@ input[type=range] { } } + @dashboard-border-color: #d3d3d3; + .dashboard-block { padding: @dashboard-offset; - border: 1px solid #d3d3d3; + border: 1px solid @dashboard-border-color; border-radius: 4px; margin: 0 15px @dashboard-offset * 2; > div { @@ -4463,7 +4466,7 @@ input[type=range] { .instruction { margin-bottom: @dashboard-offset; } - .environment-alerts { + .task-alerts { .invalid { .font-semibold; font-size: @base-font-size - 1; @@ -4498,7 +4501,21 @@ input[type=range] { width: 95%; } } + &.actions-panel { + .no-nodes { + color: @gray; + } + .nav { + margin-right: 0; + li.deployment-modes-label { + color: @gray; + padding: 7px 0 0; + cursor: default; + } + } + } } + .links-block { .row:not(:last-child) { margin-bottom: @dashboard-offset * 2; diff --git a/nailgun/static/tests/functional/pages/dashboard.js b/nailgun/static/tests/functional/pages/dashboard.js index 4308efe552..477897e6ea 100644 --- a/nailgun/static/tests/functional/pages/dashboard.js +++ b/nailgun/static/tests/functional/pages/dashboard.js @@ -22,7 +22,7 @@ define([ function DashboardPage(remote) { this.remote = remote; this.modal = new ModalWindow(remote); - this.deployButtonSelector = 'button.deploy-btn'; + this.deployButtonSelector = '.actions-panel .deploy-btn'; } DashboardPage.prototype = { diff --git a/nailgun/static/tests/functional/test_cluster_dashboard.js b/nailgun/static/tests/functional/test_cluster_dashboard.js index d8323975cf..69837f44ff 100644 --- a/nailgun/static/tests/functional/test_cluster_dashboard.js +++ b/nailgun/static/tests/functional/test_cluster_dashboard.js @@ -68,8 +68,11 @@ define([ .type('\uE00C') .end() .assertElementNotExists(renameInputSelector, 'Rename control disappears') - .assertElementTextEquals(nameSelector, initialName, - 'Switching rename control does not change cluster name') + .assertElementTextEquals( + nameSelector, + initialName, + 'Switching rename control does not change cluster name' + ) .then(function() { return dashboardPage.setClusterName(newName); }) @@ -86,9 +89,13 @@ define([ .then(function() { return dashboardPage.setClusterName(initialName); }) - .assertElementAppears('.rename-block.has-error', 1000, - 'Error style for duplicate name is applied') - .assertElementTextEquals('.rename-block .text-danger', + .assertElementAppears( + '.rename-block.has-error', + 1000, + 'Error style for duplicate name is applied' + ) + .assertElementTextEquals( + '.rename-block .text-danger', 'Environment with this name already exists', 'Duplicate name error text appears' ) @@ -101,7 +108,7 @@ define([ return clustersPage.goToEnvironment(initialName); }); }, - 'Provision button availability': function() { + 'Provision VMs button availability': function() { return this.remote .then(function() { return common.addNodesToCluster(1, ['Virtual']); @@ -109,8 +116,13 @@ define([ .then(function() { return clusterPage.goToTab('Dashboard'); }) - .assertElementTextEquals(dashboardPage.deployButtonSelector, 'Provision VMs', - 'After adding Virtual node deploy button has appropriate text') + .assertElementAppears( + '.actions-panel .btn-provision-vms', + 1000, + 'Provision VMs action appears on the Dashboard' + ) + .clickByCssSelector('.actions-panel .nav button.dropdown-toggle') + .clickByCssSelector('.actions-panel .nav .dropdown-menu li.deploy button') .then(function() { return dashboardPage.discardChanges(); }); @@ -124,14 +136,19 @@ define([ return clusterPage.goToTab('Networks'); }) .clickByCssSelector('.subtab-link-network_verification') - .assertElementContainsText('.alert-warning', 'At least two online nodes are required', - 'Network verification warning appears if only one node added') + .assertElementContainsText( + '.alert-warning', + 'At least two online nodes are required', + 'Network verification warning appears if only one node added' + ) .then(function() { return clusterPage.goToTab('Dashboard'); }) - .assertElementContainsText('.warnings-block', + .assertElementContainsText( + '.actions-panel .warnings-block', 'Please verify your network settings before deployment', - 'Network verification warning is shown') + 'Network verification warning is shown' + ) .then(function() { return dashboardPage.discardChanges(); }); @@ -139,18 +156,21 @@ define([ 'No controller warning': function() { return this.remote .then(function() { - // Adding single compute return common.addNodesToCluster(1, ['Compute']); }) .then(function() { return clusterPage.goToTab('Dashboard'); }) - .assertElementDisabled(dashboardPage.deployButtonSelector, - 'No deployment should be possible without controller nodes added') + .assertElementDisabled( + dashboardPage.deployButtonSelector, + 'No deployment should be possible without controller nodes added' + ) .assertElementExists('div.instruction.invalid', 'Invalid configuration message is shown') - .assertElementContainsText('.environment-alerts ul.text-danger li', - 'At least 1 Controller nodes are required (0 selected currently).', - 'No controllers added warning should be shown') + .assertElementContainsText( + '.task-alerts ul.text-danger li', + 'At least 1 Controller nodes are required (0 selected currently).', + 'No controllers added warning should be shown' + ) .then(function() { return dashboardPage.discardChanges(); }); @@ -202,25 +222,48 @@ define([ .then(function() { return clusterPage.goToTab('Dashboard'); }) - .assertElementTextEquals(valueSelector + '.total', total, - 'The number of Total nodes in statistics is updated according to added nodes') - .assertElementTextEquals(valueSelector + '.controller', controllerNodes, - 'The number of controllerNodes nodes in statistics is updated according to ' + - 'added nodes') - .assertElementTextEquals(valueSelector + '.compute', computeNodes, - 'The number of Compute nodes in statistics is updated according to added nodes') - .assertElementTextEquals(valueSelector + '.base-os', operatingSystemNodes, - 'The number of Operating Systems nodes in statistics is updated according to ' + - 'added nodes') - .assertElementTextEquals(valueSelector + '.virt', virtualNodes, - 'The number of Virtual nodes in statistics is updated according to added nodes') - .assertElementTextEquals(valueSelector + '.offline', 1, - 'The number of Offline nodes in statistics is updated according to added nodes') - .assertElementTextEquals(valueSelector + '.error', 1, - 'The number of Error nodes in statistics is updated according to added nodes') - .assertElementTextEquals(valueSelector + '.pending_addition', total, - 'The number of Pending Addition nodes in statistics is updated according to ' + - 'added nodes') + .assertElementTextEquals( + valueSelector + '.total', + total, + 'The number of Total nodes in statistics is correct' + ) + .assertElementTextEquals( + valueSelector + '.controller', + controllerNodes, + 'The number of controllerNodes nodes in statistics is correct' + ) + .assertElementTextEquals( + valueSelector + '.compute', + computeNodes, + 'The number of Compute nodes in statistics is correct' + ) + .assertElementTextEquals( + valueSelector + '.base-os', + operatingSystemNodes, + 'The number of Operating Systems nodes in statistics is correct' + ) + .assertElementTextEquals( + valueSelector + '.virt', + virtualNodes, + 'The number of Virtual nodes in statistics is corrects' + ) + .assertElementTextEquals( + valueSelector + '.offline', + 1, + 'The number of Offline nodes in statistics is correct' + ) + .assertElementTextEquals( + valueSelector + '.error', + 1, + 'The number of Error nodes in statistics is correct' + ) + .assertElementTextEquals( + valueSelector + '.pending_addition', + 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') .then(function() { return dashboardPage.discardChanges(); }); diff --git a/nailgun/static/tests/functional/test_cluster_deployment.js b/nailgun/static/tests/functional/test_cluster_deployment.js index acf6e487bc..4e53e882d4 100644 --- a/nailgun/static/tests/functional/test_cluster_deployment.js +++ b/nailgun/static/tests/functional/test_cluster_deployment.js @@ -56,62 +56,105 @@ define([ return clusterPage.goToTab('Dashboard'); }); }, - 'No deployment button when there are no nodes added': function() { - return this.remote - .assertElementNotExists(dashboardPage.deployButtonSelector, - 'No deployment should be possible without nodes added'); - }, - 'Discard changes': function() { + 'Provision nodes': function() { + this.timeout = 100000; return this.remote + .assertElementNotExists( + dashboardPage.deployButtonSelector, + 'No deployment should be possible without nodes added' + ) .then(function() { - // Adding three controllers return common.addNodesToCluster(1, ['Controller']); }) .then(function() { return clusterPage.goToTab('Dashboard'); }) - .clickByCssSelector('.btn-discard-changes') + .clickByCssSelector('.actions-panel .nav button.dropdown-toggle') + .clickByCssSelector('.actions-panel .nav .dropdown-menu li.provision button') + .assertElementContainsText( + '.actions-panel .changes-list ul li', + '1 node to be provisioned.', + '1 node to be provisioned' + ) + .clickByCssSelector('.btn-provision') .then(function() { return modal.waitToOpen(); }) - .assertElementContainsText('h4.modal-title', 'Discard Changes', - 'Discard Changes confirmation modal expected') .then(function() { - return modal.clickFooterButton('Discard'); + return modal.checkTitle('Provision Nodes'); + }) + .then(function() { + return modal.clickFooterButton('Start Provisioning'); }) .then(function() { return modal.waitToClose(); }) - .assertElementAppears('.dashboard-block a.btn-add-nodes', 2000, - 'All changes discarded, add nodes button gets visible in deploy readiness block'); + .assertElementAppears('div.deploy-process div.progress', 2000, 'Provisioning started') + .assertElementDisappears('div.deploy-process div.progress', 5000, 'Provisioning finished') + .assertElementContainsText( + 'div.alert-success strong', + 'Success', + 'Provisioning successfully finished' + ) + .then(function() { + return clusterPage.isTabLocked('Networks'); + }) + .then(function(isLocked) { + assert.isFalse(isLocked, 'Networks tab is not locked after nodes were provisioned'); + }) + .then(function() { + return clusterPage.isTabLocked('Settings'); + }) + .then(function(isLocked) { + assert.isFalse(isLocked, 'Settings tab is not locked after nodes were provisioned'); + }) + .then(function() { + return clusterPage.goToTab('Dashboard'); + }) + .assertElementEnabled( + dashboardPage.deployButtonSelector, + 'Provisioned nodes can be deployed' + ) + .then(function() { + return clusterPage.resetEnvironment(clusterName); + }); }, 'Start/stop deployment': function() { this.timeout = 100000; return this.remote .then(function() { - return common.addNodesToCluster(3, ['Controller']); + return common.addNodesToCluster(2, ['Controller']); }) .then(function() { return clusterPage.goToTab('Dashboard'); }) - .assertElementAppears('.dashboard-tab', 200, 'Dashboard tab opened') .then(function() { return dashboardPage.startDeployment(); }) .assertElementAppears('div.deploy-process div.progress', 2000, 'Deployment started') - .assertElementAppears('button.stop-deployment-btn:not(:disabled)', 5000, - 'Stop button appears') + .assertElementAppears( + 'button.stop-deployment-btn:not(:disabled)', + 5000, + 'Stop button appears' + ) .then(function() { return dashboardPage.stopDeployment(); }) .assertElementDisappears('div.deploy-process div.progress', 20000, 'Deployment stopped') - .assertElementAppears(dashboardPage.deployButtonSelector, 1000, - 'Deployment button available') - .assertElementContainsText('div.alert-warning strong', 'Success', - 'Deployment successfully stopped alert is expected') - .assertElementNotExists('.go-to-healthcheck', - 'Healthcheck link is not visible after stopped deploy') - // Reset environment button is available + .assertElementAppears( + dashboardPage.deployButtonSelector, + 3000, + 'Deployment button available' + ) + .assertElementContainsText( + 'div.alert-warning strong', + 'Success', + 'Deployment successfully stopped alert is expected' + ) + .assertElementNotExists( + '.go-to-healthcheck', + 'Healthcheck link is not visible after stopped deploy' + ) .then(function() { return clusterPage.resetEnvironment(clusterName); }); @@ -120,7 +163,6 @@ define([ this.timeout = 100000; return this.remote .then(function() { - // Adding single controller (enough for deployment) return common.addNodesToCluster(1, ['Controller']); }) .then(function() { @@ -141,8 +183,11 @@ define([ .then(function() { return dashboardPage.startDeployment(); }) - .assertElementDisappears('.dashboard-block .progress', 60000, - 'Progress bar disappears after deployment') + .assertElementDisappears( + '.dashboard-block .progress', + 60000, + 'Progress bar disappears after deployment' + ) .assertElementAppears('.links-block', 5000, 'Deployment completed') .assertElementExists('.go-to-healthcheck', 'Healthcheck link is visible after deploy') .findByLinkText('Horizon') @@ -162,8 +207,10 @@ define([ .then(function(isLocked) { assert.isTrue(isLocked, 'Networks tab should turn locked after deployment'); }) - .assertElementEnabled('.add-nodegroup-btn', - 'Add Node network group button is enabled after cluster deploy') + .assertElementEnabled( + '.add-nodegroup-btn', + 'Add Node network group button is enabled after cluster deploy' + ) .then(function() { return clusterPage.isTabLocked('Settings'); }) diff --git a/nailgun/static/translations/core.json b/nailgun/static/translations/core.json index 32f0acb10d..efba441780 100644 --- a/nailgun/static/translations/core.json +++ b/nailgun/static/translations/core.json @@ -624,6 +624,7 @@ "add_nodes": "Add nodes to the OpenStack Environment.", "not_deployed_instruction": "Before you proceed, review the environment configuration, including the node disk partitioning and network settings. You will not be able to change these settings after Fuel deploys the OpenStack environment.", "deployment_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", "stop": "Stop", "healthcheck": "To view the OpenStack health check status go to ", "healthcheck_tab": "Healthcheck tab", @@ -644,7 +645,46 @@ "horizon_description": "The OpenStack dashboard Horizon is now available.", "go_to_horizon": "Go to Horizon", "no_storage_enabled": "No storage option enabled", - "http_plugin_link": "(HTTP)" + "http_plugin_link": "(HTTP)", + "added_node": "Added __count__ node", + "added_node_plural": "Added __count__ nodes", + "deleted_node": "Deleted __count__ node", + "deleted_node_plural": "Deleted __count__ nodes", + "provisioned_node": "Provisioned __count__ node", + "provisioned_node_plural": "Provisioned __count__ nodes", + "verification_not_performed": "Please verify your network settings before deployment.", + "verification_in_progress": "Network verification is in progress.", + "verification_failed": "Networks verification failed with an error.", + "get_more_info": "For more information please visit the", + "networks_link": "Networks tab", + "invalid_settings": "Environment settings are invalid.", + "settings_link": "Settings tab", + "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": "Some nodes are offline.", + "unprovisioned_virt_nodes": "The __role__ node is not provisioned.", + "unprovisioned_virt_nodes_plural": "Some __role__ nodes are not provisioned.", + "deployment_mode": "Deployment Mode", + "actions": { + "deploy": { + "title": "Regular deployment", + "button_title": "Deploy Changes" + }, + "provision": { + "title": "Advanced provisioning", + "nodes_to_provision": "__count__ node to be provisioned.", + "nodes_to_provision_plural": "__count__ nodes to be provisioned.", + "no_nodes_to_provision": "No online discovered nodes to provision.", + "button_title": "Provision Nodes" + }, + "spawn_vms": { + "title": "VMs provisioning", + "nodes_to_provision": "__count__ node to be provisioned.", + "nodes_to_provision_plural": "__count__ nodes to be provisioned.", + "button_title": "Provision VMs" + } + } }, "network_tab": { "title": "Network Settings", @@ -798,13 +838,11 @@ "tenant_label": "Tenant", "tenant_description": "Tenant (project) name for Administrator" }, - "deploy_changes": "Deploy Changes", - "provision_vms": "Provision VMs", "deploy": "Deploying", + "provision": "Provisioning of nodes", "reset_environment": "Resetting", "stop_deployment": "Stopping", "stop_deployment_button": "Stop deployment", - "deployment_mode": "Deployment Mode", "show_details_button": "Show additional information", "hide_details_button": "Hide additional information", "openstack_release": "OpenStack Release", @@ -944,38 +982,18 @@ "saving_failed_message": "Failed to save changes. Please fix the errors and try again.", "no_discard_message": "You have unsaved changes on this page. Press Cancel to stay on the page or Save and Proceed to save data and proceed." }, - "display_changes": { + "deploy_cluster": { "title": "Deploy Changes", - "added_node": "Added __count__ node", - "added_node_plural": "Added __count__ nodes", - "deleted_node": "Deleted __count__ node", - "deleted_node_plural": "Deleted __count__ nodes", - "provisioned_node": "Provisioned __count__ node", - "provisioned_node_plural": "Provisioned __count__ nodes", - "settings_changes": { - "attributes": "Changed OpenStack settings.", - "networks": "Changed network settings.", - "disks": "Changed disk configuration of the following nodes:", - "interfaces": "Changed interface configuration of the following nodes:" - }, - "warnings": { - "no_deployment": "Environment configuration is invalid. Cannot start the deployment. Please check the warnings below.", - "connectivity_alert": "Packages and updates are fetched from the repositories defined in the Settings tab. If your environment uses the default repositories, verify that the Fuel Master node can access the Internet. If the Fuel Master node is not connected to the Internet, you must set up a local mirror and configure Fuel to upload packages from the local mirror. Fuel must have network access to the local mirror." - }, - "deploy": "Deploy", - "redeployment_needed": "Deployment completed with errors. Please redeploy your environment.", - "verification_not_performed": "Please verify your network settings before deployment.", - "verification_in_progress": "Network verification is in progress.", - "verification_failed": "Networks verification failed with an error.", - "get_more_info": "For more information please visit the", - "networks_link": "Networks tab", - "invalid_settings": "Environment settings are invalid.", - "settings_link": "Settings tab", - "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.", + "no_deployment": "Environment configuration is invalid. Cannot start the deployment. Please check the warnings below.", + "connectivity_alert": "Packages and updates are fetched from the repositories defined in the Settings tab. If your environment uses the default repositories, verify that the Fuel Master node can access the Internet. If the Fuel Master node is not connected to the Internet, you must set up a local mirror and configure Fuel to upload packages from the local mirror. Fuel must have network access to the local mirror.", "are_you_sure_deploy": "Click Deploy to start the deployment or Cancel to make changes.", - "offline_nodes": "Some nodes are offline." + "deploy": "Deploy" + }, + "provision_nodes": { + "title": "Provision Nodes", + "locked_node_settings_alert": "Before you proceed, review the environment nodes configuration, including disk partitioning and interfaces configuration. You will not be able to change these settings after Fuel provisions the nodes.", + "are_you_sure_provision": "Click Start Provisioning to provision the operating system onto the environment nodes, or Cancel to make changes.", + "start_provisioning": "Start Provisioning" }, "provision_vms": { "title": "Provision VMs", @@ -1008,10 +1026,10 @@ "title": "Delete Nodes", "common_message": "Are you sure you want to delete the selected node from the environment?", "common_message_plural": "Are you sure you want to delete the selected nodes from the environment?", - "not_deployed_nodes_message": "The not deployed node will return to the pool of unallocated nodes.", - "not_deployed_nodes_message_plural": "Not deployed nodes will return to the pool of unallocated nodes.", - "deployed_nodes_message": "The deployed node will be marked as pending deletion and will be removed from the environment after re-deployment.", - "deployed_nodes_message_plural": "Deployed nodes will be marked as pending deletion and will be removed from the environment after redeployment." + "added_nodes_message": "The node will return to the pool of unallocated nodes.", + "added_nodes_message_plural": "The nodes will return to the pool of unallocated nodes.", + "deployed_nodes_message": "The node will be marked as pending deletion and will be removed from the environment after deployment.", + "deployed_nodes_message_plural": "The nodes will be marked as pending deletion and will be removed from the environment after deployment." }, "discard_changes": { "title": "Discard Changes", @@ -1566,7 +1584,13 @@ }, "dashboard_tab": { "delete_environment": "删除环境", - "alert_delete": "清除每个节点并放回未分配节点池" + "alert_delete": "清除每个节点并放回未分配节点池", + "added_node": "增加 __count__ 个节点。", + "added_node_plural": "增加 __count__ 个节点。", + "deleted_node": "删除 __count__ 个节点。", + "deleted_node_plural": "删除 __count__ 个节点。", + "provisioned_node": "配置了 __count__ 个节点", + "provisioned_node_plural": "配置了 __count__ 个节点" }, "network_tab": { "title": "网络设置", @@ -1760,38 +1784,11 @@ "saving_failed_message": "保存失败。请修复错误后再保存。", "no_discard_message": "你有尚未保存的变更。选择“取消”留在本页,或者“保存并更改”使配置生效。" }, - "display_changes": { + "deploy_cluster": { "title": "部署变更", - "added_node": "增加 __count__ 个节点。", - "added_node_plural": "增加 __count__ 个节点。", - "deleted_node": "删除 __count__ 个节点。", - "deleted_node_plural": "删除 __count__ 个节点。", - "provisioned_node": "配置了 __count__ 个节点", - "provisioned_node_plural": "配置了 __count__ 个节点", - "settings_changes": { - "attributes": "OpenStack 设置已改变。", - "networks": "网络设置已改变。", - "disks": "以下节点的磁盘配置已改变:", - "interfaces": "以下节点的接口配置已改变:" - }, - "warnings": { - "no_deployment": "环境配置有误,无法开始部署。请检查下面的错误。", - "connectivity_alert": "软件包和更新所用的镜像源定义在设置标签页中。如果你的环境使用默认的镜像源,请检查 Fuel 的主节点能访问外网。如果 Fuel 的主节点没有连接外网,你必须搭建一个本地的镜像源,并配置 Fuel 从本地镜像源获取软件包。Fuel 必须能通过网络访问本地镜像源。" - }, "deploy": "部署", - "redeployment_needed": "某些节点配置后状态错误,需要重新部署。", - "verification_not_performed": "请在部署前检测网络。", - "verification_in_progress": "正在进行网络检测。", - "verification_failed": "网络检测遇到问题。", - "get_more_info": "更多信息请访问", - "networks_link": "网络标签页", - "invalid_settings": "环境设置有误。", - "settings_link": "设置标签页", - "tls_not_enabled": "没有启用 TLS。强烈建议启用并设置 TLS。", - "tls_for_horizon_not_enabled": "Horizon 公共接入点没有启用 TLS。强烈建议为 Horizon 启用和配置 TLS。", - "tls_for_services_not_enabled": "OpenStack 公共接入点没有启用 TLS。强烈建议启用和配置 TLS。", - "are_you_sure_deploy": "点击部署开始部署,或者点击取消。", - "offline_nodes": "有些节点离线。" + "no_deployment": "环境配置有误,无法开始部署。请检查下面的错误。", + "connectivity_alert": "软件包和更新所用的镜像源定义在设置标签页中。如果你的环境使用默认的镜像源,请检查 Fuel 的主节点能访问外网。如果 Fuel 的主节点没有连接外网,你必须搭建一个本地的镜像源,并配置 Fuel 从本地镜像源获取软件包。Fuel 必须能通过网络访问本地镜像源。" }, "provision_vms": { "title": "配置 VM", @@ -2262,7 +2259,19 @@ "reset_disabled_for_deploying_cluster": "環境のリセットはデプロイ中の環境には使用できません。", "rename_error": { "title": "環境の名前変更ができませんでした" - } + }, + "added_node": "__count__ノードを追加。", + "added_node_plural": "__count__ノードを追加。", + "deleted_node": "__count__ノードを削除。", + "deleted_node_plural": "__count__ノードを削除。", + "locked_settings_alert": "環境設定とノードの設定はデプロイ後に変更できなくなりますので注意してください。", + "verification_not_performed": "デプロイ前にネットワーク設定を確認することをおすすめします。", + "verification_in_progress": "ネットワークの検証が進行中です。", + "verification_failed": "ネットワークの検証がエラーのために失敗しました。", + "get_more_info": "詳しい情報は次を参照してください:", + "networks_link": "ネットワークタブ", + "invalid_settings": "環境設定が不正です。", + "settings_link": "設定タブ" }, "network_tab": { "title": "ネットワーク設定", @@ -2489,32 +2498,11 @@ "leave_button": "ページから離れ、変更を破棄", "default_message": "このページでの変更が保存されていません。変更を破棄してこのページから離れてもよろしいですか?" }, - "display_changes": { + "deploy_cluster": { "title": "変更をデプロイ", - "added_node": "__count__ノードを追加。", - "added_node_plural": "__count__ノードを追加。", - "deleted_node": "__count__ノードを削除。", - "deleted_node_plural": "__count__ノードを削除。", - "settings_changes": { - "attributes": "OpenStack設定を変更。", - "networks": "ネットワーク設定を変更。", - "disks": "以下のノードのディスク構成を変更:", - "interfaces": "以下のノードのインタフェイス構成を変更:" - }, - "warnings": { - "no_deployment": "環境の設定が無効です。デプロイは開始できません。次の警告を確認してください。", - "connectivity_alert": "デフォルトではパッケージやパッケージの更新は、リポジトリから取得されます。 Fuelノードがインタネットにアクセスできることを確認してください。\n別のリポジトリを指定またはローカルミラーを作成するには、デプロイ前に設定タブを確認してください。" - }, "deploy": "デプロイ", - "redeployment_needed": "いくつかのノードはデプロイ後にステータスがエラーになっています。再デプロイが必要です。", - "locked_settings_alert": "環境設定とノードの設定はデプロイ後に変更できなくなりますので注意してください。", - "verification_not_performed": "デプロイ前にネットワーク設定を確認することをおすすめします。", - "verification_in_progress": "ネットワークの検証が進行中です。", - "verification_failed": "ネットワークの検証がエラーのために失敗しました。", - "get_more_info": "詳しい情報は次を参照してください:", - "networks_link": "ネットワークタブ", - "invalid_settings": "環境設定が不正です。", - "settings_link": "設定タブ" + "no_deployment": "環境の設定が無効です。デプロイは開始できません。次の警告を確認してください。", + "connectivity_alert": "デフォルトではパッケージやパッケージの更新は、リポジトリから取得されます。 Fuelノードがインタネットにアクセスできることを確認してください。\n別のリポジトリを指定またはローカルミラーを作成するには、デプロイ前に設定タブを確認してください。" }, "show_node": { "manufacturer_label": "製造元", @@ -2813,7 +2801,23 @@ "reset_environment_warning": "이 작업은 기존의 모든 노드를 배치전 상태로 바꾸고 기존의 환경을 삭제합니다.", "reset_disabled_for_new_cluster": "아직 배치되지 않은 환경에서 환경재설정은 할수없습니다.", "repeated_reset_disabled": "현재의 환경은 이미 재설정되고 있읍니다.", - "reset_disabled_for_deploying_cluster": "배치되고있는 환경에서 환경재설정은 할수 없습니다.배치 프로세스를 중지하려면 [배치중지]를 사용합니다." + "reset_disabled_for_deploying_cluster": "배치되고있는 환경에서 환경재설정은 할수 없습니다.배치 프로세스를 중지하려면 [배치중지]를 사용합니다.", + "added_node": "__count__ 노드를 추가", + "added_node_plural": "__count__ 노드들을 추가", + "deleted_node": "__count__ 노드를 삭제", + "deleted_node_plural": "__count__ 노드들을 삭제", + "verification_not_performed": "请在部署前检测网络。", + "verification_in_progress": "正在进行网络检测。", + "verification_failed": "网络检测遇到问题。", + "get_more_info": "更多信息请访问", + "networks_link": "网络标签页", + "invalid_settings": "环境设置有误。", + "settings_link": "设置标签页", + "tls_not_enabled": "没有启用 TLS。强烈建议启用并设置 TLS。", + "tls_for_horizon_not_enabled": "Horizon 公共接入点没有启用 TLS。强烈建议为 Horizon 启用和配置 TLS。", + "tls_for_services_not_enabled": "OpenStack 公共接入点没有启用 TLS。强烈建议启用和配置 TLS。", + "are_you_sure_deploy": "点击部署开始部署,或者点击取消。", + "offline_nodes": "有些节点离线。" }, "network_tab": { "title": "네트워크 설정", @@ -2985,21 +2989,9 @@ "leave_button": "변경사항을 취소하고 페이지를 떠나가기", "default_message": "설정이 수정되었지만 저장되지 않았습니다.변경 사항을 취소하고 페이지를 떠나겠습니까?" }, - "display_changes": { + "deploy_cluster": { "title": "변경내용 배치", - "added_node": "__count__ 노드를 추가", - "added_node_plural": "__count__ 노드들을 추가", - "deleted_node": "__count__ 노드를 삭제", - "deleted_node_plural": "__count__ 노드들을 삭제", - "settings_changes": { - "attributes": "OpenStack 설정", - "networks": "네트워크 설정", - "disks": "다음 노드들의 디스크 설정:" - }, - "warnings": { - }, - "deploy": "배치되었읍니다", - "redeployment_needed": "일부 노드는 배치후 오류상태가 있습니다. 재배치가 필요합니다." + "deploy": "배치되었읍니다" }, "show_node": { "manufacturer_label": "제조업자", diff --git a/nailgun/static/views/cluster_page_tabs/dashboard_tab.js b/nailgun/static/views/cluster_page_tabs/dashboard_tab.js index bf4fa8e957..3eed0dbeab 100644 --- a/nailgun/static/views/cluster_page_tabs/dashboard_tab.js +++ b/nailgun/static/views/cluster_page_tabs/dashboard_tab.js @@ -22,12 +22,12 @@ import utils from 'utils'; import dispatcher from 'dispatcher'; import {Input, ProgressBar, Tooltip} from 'views/controls'; import { - DiscardNodeChangesDialog, DeployChangesDialog, ProvisionVMsDialog, + DiscardNodeChangesDialog, DeployClusterDialog, ProvisionVMsDialog, ProvisionNodesDialog, RemoveClusterDialog, ResetEnvironmentDialog, StopDeploymentDialog } from 'views/dialogs'; import {backboneMixin, pollingMixin, renamingMixin} from 'component_mixins'; -var namespace = 'cluster_page.dashboard_tab.'; +var ns = 'cluster_page.dashboard_tab.'; var DashboardTab = React.createClass({ mixins: [ @@ -63,15 +63,16 @@ var DashboardTab = React.createClass({ }, render() { var cluster = this.props.cluster; - var nodes = cluster.get('nodes'); var release = cluster.get('release'); var runningDeploymentTask = cluster.task({group: 'deployment', active: true}); - + var finishedDeploymentTask = cluster.task({group: 'deployment', active: false}); var dashboardLinks = [{ url: '/', - title: i18n(namespace + 'horizon'), - description: i18n(namespace + 'horizon_description') - }].concat(cluster.get('pluginLinks').invoke('pick', 'url', 'title', 'description')); + title: i18n(ns + 'horizon'), + description: i18n(ns + 'horizon_description') + }].concat( + cluster.get('pluginLinks').invoke('pick', 'url', 'title', 'description') + ); return (
@@ -86,31 +87,28 @@ var DashboardTab = React.createClass({
} {runningDeploymentTask ? - + : [ - cluster.task({group: 'deployment', active: false}) && - , + finishedDeploymentTask && + , cluster.get('status') === 'operational' && - , - (nodes.hasChanges() || cluster.needsRedeployment()) && - , - !nodes.length && ( -
-
-
-

{i18n(namespace + 'new_environment_welcome')}

- - -
-
-
- ) + , + ] } @@ -122,11 +120,12 @@ var DashboardTab = React.createClass({ var DashboardLinks = React.createClass({ renderLink(link) { + var {links, cluster} = this.props; return ( 1 ? 'col-xs-6' : 'col-xs-12'} - cluster={this.props.cluster} + className={links.length > 1 ? 'col-xs-6' : 'col-xs-12'} + cluster={cluster} /> ); }, @@ -169,34 +168,29 @@ var DashboardLink = React.createClass({ return 'http://' + this.props.cluster.get('networkConfiguration').get('public_vip') + url; }, render() { - var isSSLEnabled = this.props.cluster.get('settings').get('public_ssl.horizon.value'); - var isURLRelative = !(/^(?:https?:)?\/\//.test(this.props.url)); - var url = isURLRelative ? this.processRelativeURL(this.props.url) : this.props.url; + var {url, title, description, className, cluster} = this.props; + var isSSLEnabled = cluster.get('settings').get('public_ssl.horizon.value'); + var isURLRelative = !(/^(?:https?:)?\/\//.test(url)); + var link = isURLRelative ? this.processRelativeURL(url) : url; return ( -
+
-
{this.props.description}
+
{description}
); } }); var DeploymentInProgressControl = React.createClass({ - showDialog(Dialog) { - Dialog.show({cluster: this.props.cluster}); - }, render() { var task = this.props.task; - var taskName = task.get('name'); - var isInfiniteTask = task.isInfinite(); - var taskProgress = task.get('progress'); var showStopButton = task.match({name: 'deploy'}); return (
@@ -204,24 +198,24 @@ var DeploymentInProgressControl = React.createClass({

- {i18n(namespace + 'current_task') + ' '} + {i18n(ns + 'current_task') + ' '} - {i18n('cluster_page.' + taskName) + '...'} + {i18n('cluster_page.' + task.get('name')) + '...'}

- + {showStopButton && } @@ -238,8 +232,14 @@ var DeploymentResult = React.createClass({ return {collapsed: false}; }, dismissTaskResult() { - var task = this.props.cluster.task({group: 'deployment'}); - if (task) task.destroy(); + var {task, cluster} = this.props; + if (task.match({name: 'deploy'})) { + // deletion of 'deploy' task invokes all deployment tasks deletion in backend + task.destroy({silent: true}) + .done(() => cluster.get('tasks').fetch()); + } else { + task.destroy(); + } }, componentDidMount() { $('.result-details', ReactDOM.findDOMNode(this)) @@ -247,8 +247,7 @@ var DeploymentResult = React.createClass({ .on('hide.bs.collapse', this.setState.bind(this, {collapsed: false}, null)); }, render() { - var task = this.props.cluster.task({group: 'deployment', active: false}); - if (!task) return null; + var {task} = this.props; var error = task.match({status: 'error'}); var delimited = task.escape('message').split('\n\n'); var summary = delimited.shift(); @@ -288,7 +287,7 @@ var DocumentationLinks = React.createClass({ - {i18n(namespace + labelKey)} + {i18n(ns + labelKey)}
@@ -297,9 +296,9 @@ var DocumentationLinks = React.createClass({ render() { return (
-
{i18n(namespace + 'documentation')}
+
{i18n(ns + 'documentation')}
-

{i18n(namespace + 'documentation_description')}

+

{i18n(ns + 'documentation_description')}

{this.renderDocumentationLinks('http://docs.openstack.org/', 'openstack_documentation')} @@ -313,25 +312,21 @@ var DocumentationLinks = React.createClass({ } }); -// @FIXME (morale): this component is written in a bad pattern of 'monolith' component -// it should be refactored to provide proper logics separation and decoupling -var DeployReadinessBlock = React.createClass({ - mixins: [ - // this is needed to somehow handle the case when verification - // is in progress and user pressed Deploy - backboneMixin({ - modelOrCollection(props) { - return props.cluster.get('tasks'); - }, - renderOn: 'update change' - }), - backboneMixin('cluster', 'change') - ], - ns: 'dialog.display_changes.', +var ClusterActionsPanel = React.createClass({ + getDefaultProps() { + return { + actions: ['spawn_vms', 'deploy', 'provision'] + }; + }, + getInitialState() { + return { + currentAction: this.isActionAvailable('spawn_vms') ? 'spawn_vms' : 'deploy' + }; + }, getConfigModels() { var {cluster} = this.props; return { - cluster: cluster, + cluster, settings: cluster.get('settings'), version: app.version, release: cluster.get('release'), @@ -339,239 +334,424 @@ var DeployReadinessBlock = React.createClass({ networking_parameters: cluster.get('networkConfiguration').get('networking_parameters') }; }, - validate(cluster) { + validate(action) { return _.reduce( - this.validations, - (accumulator, validator) => _.merge(accumulator, validator.call(this, cluster), (a, b) => - a.concat(_.compact(b))), + this.validations(action), + (accumulator, validator) => _.merge( + accumulator, + validator.call(this, this.props.cluster), + (a, b) => a.concat(_.compact(b)) + ), {blocker: [], error: [], warning: []} ); }, - validations: [ - // check if some cluster nodes are offline - function(cluster) { - if (cluster.get('nodes').any({online: false})) { - return {blocker: [i18n(this.ns + 'offline_nodes')]}; - } - }, - // check if TLS settings are not configured - function(cluster) { - var sslSettings = cluster.get('settings').get('public_ssl'); - if (!sslSettings.horizon.value && !sslSettings.services.value) { - return {warning: [i18n(this.ns + 'tls_not_enabled')]}; - } - if (!sslSettings.horizon.value) { - return {warning: [i18n(this.ns + 'tls_for_horizon_not_enabled')]}; - } - if (!sslSettings.services.value) { - return {warning: [i18n(this.ns + 'tls_for_services_not_enabled')]}; - } - }, - // check if deployment failed - function(cluster) { - return cluster.needsRedeployment() && { - error: [ - - ] - }; - }, - // check VCenter settings - function(cluster) { - if (cluster.get('settings').get('common.use_vcenter.value')) { - var vcenter = cluster.get('vcenter'); - vcenter.setModels(this.getConfigModels()); - return !vcenter.isValid() && { - blocker: [ - {i18n('vmware.has_errors') + ' '} - - {i18n('vmware.tab_name')} - - - ] - }; - } - }, - // check cluster settings - function(cluster) { - var configModels = this.getConfigModels(); - var areSettingsInvalid = !cluster.get('settings').isValid({models: configModels}); - return areSettingsInvalid && - {blocker: [ - - {i18n(this.ns + 'invalid_settings')} - {' ' + i18n(this.ns + 'get_more_info') + ' '} - - {i18n(this.ns + 'settings_link')} - . - + validations(action) { + var checkForUnpovisionedVirtNodes = function(cluster) { + var unprovisionedVirtNodes = cluster.get('nodes').filter( + (node) => node.hasRole('virt') && node.get('status') === 'discover' + ); + if (unprovisionedVirtNodes.length) { + return {blocker: [ + i18n(ns + 'unprovisioned_virt_nodes', { + role: cluster.get('roles').find({name: 'virt'}).get('label'), + count: unprovisionedVirtNodes.length + }) ]}; - }, - // check node amount restrictions according to their roles - function(cluster) { - var configModels = this.getConfigModels(); - var roleModels = cluster.get('roles'); - var validRoleModels = roleModels.filter((role) => { - return !role.checkRestrictions(configModels).result; - }); - var limitValidations = _.zipObject(validRoleModels.map((role) => { - return [role.get('name'), role.checkLimits(configModels, cluster.get('nodes'))]; - })); - var limitRecommendations = _.zipObject(validRoleModels.map((role) => { - return [role.get('name'), role.checkLimits(configModels, cluster.get('nodes'), true, - ['recommended'])]; - })); - return { - blocker: roleModels.map((role) => { - var name = role.get('name'); - var limits = limitValidations[name]; - return limits && !limits.valid && limits.message; - }), - warning: roleModels.map((role) => { - var name = role.get('name'); - var recommendation = limitRecommendations[name]; - return recommendation && !recommendation.valid && recommendation.message; - }) - }; - }, - // check cluster network configuration - function(cluster) { - if (this.props.cluster.get('nodeNetworkGroups').length > 1) return null; - var networkVerificationTask = cluster.task('verify_networks'); - var makeComponent = (text, isError) => { - var span = ( - - {text} - {' ' + i18n(this.ns + 'get_more_info') + ' '} - - {i18n(this.ns + 'networks_link')} - . - - ); - return isError ? {error: [span]} : {warning: [span]}; - }; - if (_.isUndefined(networkVerificationTask)) { - return makeComponent(i18n(this.ns + 'verification_not_performed')); - } else if (networkVerificationTask.match({status: 'error'})) { - return makeComponent(i18n(this.ns + 'verification_failed'), true); - } else if (networkVerificationTask.match({active: true})) { - return makeComponent(i18n(this.ns + 'verification_in_progress')); } + }; + switch (action) { + case 'deploy': + return [ + checkForUnpovisionedVirtNodes, + // check if some cluster nodes are offline + function(cluster) { + if (cluster.get('nodes').any({online: false})) { + return {blocker: [i18n(ns + 'offline_nodes')]}; + } + }, + // check if TLS settings are not configured + function(cluster) { + var sslSettings = cluster.get('settings').get('public_ssl'); + if (!sslSettings.horizon.value && !sslSettings.services.value) { + return {warning: [i18n(ns + 'tls_not_enabled')]}; + } + if (!sslSettings.horizon.value) { + return {warning: [i18n(ns + 'tls_for_horizon_not_enabled')]}; + } + if (!sslSettings.services.value) { + return {warning: [i18n(ns + 'tls_for_services_not_enabled')]}; + } + }, + // check if deployment failed + function(cluster) { + return cluster.needsRedeployment() && { + error: [ + + ] + }; + }, + // check VCenter settings + function(cluster) { + if (cluster.get('settings').get('common.use_vcenter.value')) { + var vcenter = cluster.get('vcenter'); + vcenter.setModels(this.getConfigModels()); + return !vcenter.isValid() && { + blocker: [ + {i18n('vmware.has_errors') + ' '} + + {i18n('vmware.tab_name')} + + + ] + }; + } + }, + // check cluster settings + function(cluster) { + var configModels = this.getConfigModels(); + var areSettingsInvalid = !cluster.get('settings').isValid({models: configModels}); + return areSettingsInvalid && + {blocker: [ + + {i18n(ns + 'invalid_settings')} + {' ' + i18n(ns + 'get_more_info') + ' '} + + {i18n(ns + 'settings_link')} + . + + ]}; + }, + // check node amount restrictions according to their roles + function(cluster) { + var configModels = this.getConfigModels(); + var roleModels = cluster.get('roles'); + var validRoleModels = roleModels.filter( + (role) => !role.checkRestrictions(configModels).result + ); + var limitValidations = _.zipObject(validRoleModels.map( + (role) => [role.get('name'), role.checkLimits(configModels, cluster.get('nodes'))] + )); + var limitRecommendations = _.zipObject(validRoleModels.map( + (role) => [ + role.get('name'), + role.checkLimits(configModels, cluster.get('nodes'), true, ['recommended']) + ] + )); + return { + blocker: roleModels.map((role) => { + var limits = limitValidations[role.get('name')]; + return limits && !limits.valid && limits.message; + }), + warning: roleModels.map((role) => { + var recommendation = limitRecommendations[role.get('name')]; + return recommendation && !recommendation.valid && recommendation.message; + }) + }; + }, + // check cluster network configuration + function(cluster) { + // network verification is not supported in multi-rack environment + if (cluster.get('nodeNetworkGroups').length > 1) return null; + + var task = cluster.task('verify_networks'); + var makeComponent = (text, isError) => { + var span = ( + + {text} + {' ' + i18n(ns + 'get_more_info') + ' '} + + {i18n(ns + 'networks_link')} + . + + ); + return isError ? {error: [span]} : {warning: [span]}; + }; + + if (_.isUndefined(task)) { + return makeComponent(i18n(ns + 'verification_not_performed')); + } + if (task.match({status: 'error'})) { + return makeComponent(i18n(ns + 'verification_failed'), true); + } + if (task.match({active: true})) { + return makeComponent(i18n(ns + 'verification_in_progress')); + } + } + ]; + case 'provision': + return [ + checkForUnpovisionedVirtNodes, + // check if some discovered nodes are offline + function(cluster) { + if (cluster.get('nodes').any( + (node) => node.isProvisioningPossible() && !node.get('online') + )) { + return {blocker: [i18n(ns + 'offline_nodes')]}; + } + } + ]; + case 'spawn_vms': + return [ + // check if some virt nodes are offline + function(cluster) { + if (cluster.get('nodes').any( + (node) => node.isProvisioningPossible() && + node.hasRole('virt') && + !node.get('online') + )) { + return {blocker: [i18n(ns + 'offline_nodes')]}; + } + } + ]; } - ], + }, showDialog(Dialog, options) { Dialog.show(_.extend({cluster: this.props.cluster}, options)); }, - renderChangedNodesAmount(nodes, dictKey) { + renderNodesAmount(nodes, dictKey) { if (!nodes.length) return null; return ( -
  • - {i18n('dialog.display_changes.' + dictKey, {count: nodes.length})} - +
  • + {i18n(ns + dictKey, {count: nodes.length})} + {_.all(nodes, (node) => node.get('pending_addition') || node.get('pending_deletion')) && + + }
  • ); }, - render() { - var cluster = this.props.cluster; - var nodes = cluster.get('nodes'); - var alerts = this.validate(cluster); - var isDeploymentPossible = cluster.isDeploymentPossible() && !alerts.blocker.length; - var isVMsProvisioningAvailable = nodes.any((node) => { - return node.get('pending_addition') && node.hasRole('virt'); - }); + isActionAvailable(action) { + var {cluster} = this.props; + switch (action) { + case 'deploy': + return !this.validate(action).blocker.length && cluster.isDeploymentPossible(); + case 'provision': + return !this.validate(action).blocker.length && cluster.get('nodes').any( + (node) => node.isProvisioningPossible() + ); + case 'spawn_vms': + return cluster.get('nodes').any( + (node) => node.hasRole('virt') && node.get('status') === 'discover' + ); + default: + return true; + } + }, + toggleAction(action) { + this.setState({currentAction: action}); + }, + renderActionControls() { + var action = this.state.currentAction; + var actionNs = ns + 'actions.' + action + '.'; + var isActionAvailable = this.isActionAvailable(action); + + var nodes = this.props.cluster.get('nodes'); + + var alerts = this.validate(action); + var blockerDescriptions = { + provision: , + spawn_vms: , + deploy: + }; + var alertList = ( +
    + {_.map(['blocker', 'error', 'warning'], + (severity) => + )} +
    + ); + + var getButtonProps = function(className) { + return { + className: utils.classNames({ + 'btn btn-primary': true, + 'btn-warning': _.isEmpty(alerts.blocker) && + (!_.isEmpty(alerts.error) || !_.isEmpty(alerts.warning)), + [className]: true + }), + disabled: !isActionAvailable + }; + }; + + switch (action) { + case 'deploy': + return [ +
    + {nodes.hasChanges() && +
      + {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')} +
    + } + +
    , + alertList + ]; + case 'provision': + var nodesToProvision = nodes.filter((node) => node.isProvisioningPossible()); + return [ + !nodesToProvision.length && +
    + {i18n(actionNs + 'no_nodes_to_provision')} +
    , +
    + {!!nodesToProvision.length && +
      +
    • + {i18n(actionNs + 'nodes_to_provision', {count: nodesToProvision.length})} +
    • +
    + } + +
    , + alertList + ]; + case 'spawn_vms': + return [ +
    +
      +
    • + {i18n(actionNs + 'nodes_to_provision', { + count: nodes.filter( + (node) => node.hasRole('virt') && node.get('status') === 'discover' + ).length + })} +
    • +
    + +
    , + alertList + ]; + default: + return null; + } + }, + renderActionsDropdown() { + var actions = _.without(this.props.actions, this.state.currentAction); + if (!this.isActionAvailable('spawn_vms')) actions = _.without(actions, 'spawn_vms'); return ( -
    -
    -
    - {nodes.hasChanges() && -
    -

    {i18n(namespace + 'changes_header')}

    -
      - {this.renderChangedNodesAmount( - nodes.where({pending_addition: true}), - 'added_node' - )} - {this.renderChangedNodesAmount( - nodes.where({status: 'provisioned'}), - 'provisioned_node' - )} - {this.renderChangedNodesAmount( - nodes.where({pending_deletion: true}), - 'deleted_node' - )} -
    -
    - } - {isVMsProvisioningAvailable ? - - : - - } -
    -
    - {_.map(['blocker', 'error', 'warning'], - (severity) => +
      +
    • + {i18n(ns + 'deployment_mode')}: +
    • +
    • + +
        + {_.map(actions, + (action) =>
      • + +
      • )} +
      +
    • +
    + ); + }, + render() { + var {cluster} = this.props; + var nodes = cluster.get('nodes'); + if (nodes.length && !nodes.hasChanges() && !cluster.needsRedeployment()) return null; + + if (!nodes.length) { + return ( +
    +
    +
    +

    {i18n(ns + 'new_environment_welcome')}

    + + +
    + ); + } + return ( +
    +
    + {this.renderActionsDropdown()} + {this.renderActionControls()} +
    ); } }); var WarningsBlock = React.createClass({ - ns: 'dialog.display_changes.', render() { - if (_.isEmpty(this.props.alerts)) return null; - var className = this.props.severity === 'warning' ? 'warning' : 'danger'; + var {alerts, severity, blockersDescription} = this.props; + if (_.isEmpty(alerts)) return null; return (
    - {this.props.severity === 'blocker' && - - } -
      - {_.map(this.props.alerts, (alert, index) => { - return
    • {alert}
    • ; - }, this)} + {severity === 'blocker' && blockersDescription} +
        + {_.map(alerts, (alert, index) =>
      • {alert}
      • )}
    ); @@ -592,18 +772,18 @@ var ClusterInfo = React.createClass({ var libvirtSettings = settings.get('common').libvirt_type; var computeLabel = _.find(libvirtSettings.values, {data: libvirtSettings.value}).label; if (settings.get('common').use_vcenter.value) { - return computeLabel + ' ' + i18n(namespace + 'and_vcenter'); + return computeLabel + ' ' + i18n(ns + 'and_vcenter'); } return computeLabel; case 'network': var networkingParameters = cluster.get('networkConfiguration').get('networking_parameters'); if (cluster.get('net_provider') === 'nova_network') { - return i18n(namespace + 'nova_with') + ' ' + networkingParameters.get('net_manager'); + return i18n(ns + 'nova_with') + ' ' + networkingParameters.get('net_manager'); } return (i18n('common.network.neutron_' + networkingParameters.get('segmentation_type'))); case 'storage_backends': return _.map(_.where(settings.get('storage'), {value: true}), 'label') || - i18n(namespace + 'no_storage_enabled'); + i18n(ns + 'no_storage_enabled'); default: return cluster.get(fieldName); } @@ -616,7 +796,7 @@ var ClusterInfo = React.createClass({
    - {i18n(namespace + 'cluster_info_fields.' + field)} + {i18n(ns + 'cluster_info_fields.' + field)}
    @@ -634,29 +814,24 @@ var ClusterInfo = React.createClass({ ); }, renderClusterCapacity() { - var ns = namespace + 'cluster_info_fields.'; + var capacityNs = ns + 'cluster_info_fields.'; var capacity = this.props.cluster.getCapacity(); + return (
    -
    {i18n(ns + 'capacity')}
    +
    {i18n(capacityNs + 'capacity')}
    -
    - {i18n(ns + 'cpu_cores')} - - {capacity.cores} ({capacity.ht_cores}) - +
    + {i18n(capacityNs + 'cpu_cores')} + {capacity.cores} ({capacity.ht_cores})
    -
    - {i18n(ns + 'ram')} - - {utils.showDiskSize(capacity.ram)} - +
    + {i18n(capacityNs + 'ram')} + {utils.showDiskSize(capacity.ram)}
    -
    - {i18n(ns + 'hdd')} - - {utils.showDiskSize(capacity.hdd)} - +
    + {i18n(capacityNs + 'hdd')} + {utils.showDiskSize(capacity.hdd)}
    @@ -664,23 +839,17 @@ var ClusterInfo = React.createClass({ }, getNumberOfNodesWithRole(field) { var nodes = this.props.cluster.get('nodes'); - if (!nodes.length) return 0; if (field === 'total') return nodes.length; return _.filter(nodes.invoke('hasRole', field)).length; }, getNumberOfNodesWithStatus(field) { var nodes = this.props.cluster.get('nodes'); - if (!nodes.length) return 0; switch (field) { case 'offline': return nodes.where({online: false}).length; - case 'error': - return nodes.where({status: 'error'}).length; case 'pending_addition': case 'pending_deletion': - var searchObject = {}; - searchObject[field] = true; - return nodes.where(searchObject).length; + return nodes.where({[field]: true}).length; default: return nodes.where({status: field}).length; } @@ -690,14 +859,14 @@ var ClusterInfo = React.createClass({ var numberOfNodes = isRole ? this.getNumberOfNodesWithRole(field) : this.getNumberOfNodesWithStatus(field); return numberOfNodes ? -
    +
    {isRole && field !== 'total' ? this.props.cluster.get('roles').find({name: field}).get('label') : field === 'total' ? - i18n(namespace + 'cluster_info_fields.total') + i18n(ns + 'cluster_info_fields.total') : i18n('cluster_page.nodes_tab.node.status.' + field, {os: this.props.cluster.get('release').get('operating_system') || 'OS'}) @@ -717,36 +886,30 @@ var ClusterInfo = React.createClass({ return result; }, renderStatistics() { - var hasNodes = !!this.props.cluster.get('nodes').length; - var fieldRoles = _.union(['total'], this.props.cluster.get('roles').pluck('name')); - var fieldStatuses = [ + var {cluster} = this.props; + var roles = _.union(['total'], cluster.get('roles').pluck('name')); + var statuses = [ 'offline', 'error', 'pending_addition', 'pending_deletion', 'ready', 'provisioned', 'provisioning', 'deploying', 'removing' ]; return (
    -
    {i18n(namespace + 'cluster_info_fields.statistics')}
    - {hasNodes ? +
    {i18n(ns + 'cluster_info_fields.statistics')}
    + {cluster.get('nodes').length ? [
    -
    - {this.renderLegend(fieldRoles, true)} -
    - + {this.renderLegend(roles, true)} + {!cluster.task({group: 'deployment', active: true}) && + + }
    ,
    -
    - {this.renderLegend(fieldStatuses)} -
    + {this.renderLegend(statuses)}
    ] :
    -

    - {i18n(namespace + 'no_nodes_warning_add_them')} -

    +

    {i18n(ns + 'no_nodes_warning_add_them')}

    }
    @@ -759,10 +922,10 @@ var ClusterInfo = React.createClass({
    -
    {i18n(namespace + 'summary')}
    +
    {i18n(ns + 'summary')}
    - {i18n(namespace + 'cluster_info_fields.name')} + {i18n(ns + 'cluster_info_fields.name')}
    @@ -770,8 +933,7 @@ var ClusterInfo = React.createClass({ :
    @@ -785,9 +947,9 @@ var ClusterInfo = React.createClass({ {this.renderClusterInfoFields()} {(cluster.get('status') === 'operational') &&
    - {i18n(namespace + 'healthcheck')} + {i18n(ns + 'healthcheck')} - {i18n(namespace + 'healthcheck_tab')} + {i18n(ns + 'healthcheck_tab')}
    } @@ -812,15 +974,13 @@ var ClusterInfo = React.createClass({ var AddNodesButton = React.createClass({ render() { - var disabled = !!this.props.cluster.task({group: 'deployment', active: true}); return ( - {i18n(namespace + 'go_to_nodes')} + {i18n(ns + 'go_to_nodes')} ); } @@ -829,7 +989,7 @@ var AddNodesButton = React.createClass({ var RenameEnvironmentAction = React.createClass({ applyAction(e) { e.preventDefault(); - var cluster = this.props.cluster; + var {cluster, endRenaming} = this.props; var name = this.state.name; if (name !== cluster.get('name')) { var deferred = cluster.save({name: name}, {patch: true, wait: true}); @@ -841,7 +1001,7 @@ var RenameEnvironmentAction = React.createClass({ this.setState({error: utils.getResponseText(response)}); } else { utils.showErrorDialog({ - title: i18n(namespace + 'rename_error.title'), + title: i18n(ns + 'rename_error.title'), response: response }); } @@ -851,13 +1011,13 @@ var RenameEnvironmentAction = React.createClass({ }) .always(() => { this.setState({disabled: false}); - if (!(this.state && this.state.error)) this.props.endRenaming(); + if (!this.state.error) endRenaming(); }); } else if (cluster.validationError) { this.setState({error: cluster.validationError.name}); } } else { - this.props.endRenaming(); + endRenaming(); } }, getInitialState() { @@ -902,9 +1062,7 @@ var RenameEnvironmentAction = React.createClass({ autoFocus /> {this.state.error && -
    - {this.state.error} -
    +
    {this.state.error}
    }
    @@ -918,33 +1076,33 @@ var ResetEnvironmentAction = React.createClass({ backboneMixin('task') ], getDescriptionKey() { - if (this.props.task) { - if (this.props.task.match({name: 'reset_environment'})) return 'repeated_reset_disabled'; + var {cluster, task} = this.props; + if (task) { + if (task.match({name: 'reset_environment'})) return 'repeated_reset_disabled'; return 'reset_disabled_for_deploying_cluster'; } - if (this.props.cluster.get('status') === 'new') return 'no_changes_to_reset'; + if (cluster.get('nodes').all({status: 'discover'})) return 'no_changes_to_reset'; return 'reset_environment_description'; }, - applyAction(e) { - e.preventDefault(); - ResetEnvironmentDialog.show({cluster: this.props.cluster}); - }, render() { - var isLocked = this.props.cluster.get('status') === 'new' || !!this.props.task; + var {cluster, task} = this.props; + var isLocked = cluster.get('status') === 'new' && + cluster.get('nodes').all({status: 'discover'}) || + !!task; return (
    @@ -954,23 +1112,19 @@ var ResetEnvironmentAction = React.createClass({ }); var DeleteEnvironmentAction = React.createClass({ - applyAction(e) { - e.preventDefault(); - RemoveClusterDialog.show({cluster: this.props.cluster}); - }, render() { return (
    @@ -980,17 +1134,27 @@ var DeleteEnvironmentAction = React.createClass({ }); var InstructionElement = React.createClass({ - render() { - var link = utils.composeDocumentationLink(this.props.link); - var classes = { - instruction: true + propTypes: { + description: React.PropTypes.string.isRequired, + isAlert: React.PropTypes.bool, + link: React.PropTypes.shape({ + url: React.PropTypes.string, + title: React.PropTypes.string + }), + explanation: React.PropTypes.string + }, + getDefaultProps() { + return { + isAlert: false }; - classes[this.props.wrapperClass] = !!this.props.wrapperClass; + }, + render() { + var {description, isAlert, link, explanation} = this.props; return ( -
    - {i18n(namespace + this.props.description) + ' '} - {i18n(namespace + this.props.linkTitle)} - {this.props.explanation ? ' ' + i18n(namespace + this.props.explanation) : '.'} +
    + {i18n(ns + description) + (link ? ' ' : '')} + {link && {i18n(ns + link.title)}} + {explanation ? ' ' + i18n(ns + explanation) : '.'}
    ); } diff --git a/nailgun/static/views/cluster_page_tabs/nodes_tab_screens/node.js b/nailgun/static/views/cluster_page_tabs/nodes_tab_screens/node.js index f532725b08..3ed2493b3f 100644 --- a/nailgun/static/views/cluster_page_tabs/nodes_tab_screens/node.js +++ b/nailgun/static/views/cluster_page_tabs/nodes_tab_screens/node.js @@ -79,9 +79,8 @@ var Node = React.createClass({ e.preventDefault(); if (this.state.actionInProgress) return; this.setState({actionInProgress: true}); - var node = new models.Node(this.props.node.attributes); - var data = {pending_deletion: false}; - node.save(data, {patch: true}) + new models.Node(this.props.node.attributes) + .save({pending_deletion: false}, {patch: true}) .done(() => { this.props.cluster.fetchRelated('nodes').done(() => { this.setState({actionInProgress: false}); @@ -302,7 +301,7 @@ var Node = React.createClass({ ); }, renderCompactNode(options) { - var node = this.props.node; + var {node, checked, locked, onNodeSelection, renderActionButtons} = this.props; var {ns, status, roles, nodePanelClasses, logoClasses, statusClasses, isSelectable} = options; return (
    @@ -310,12 +309,10 @@ var Node = React.createClass({