diff --git a/static/styles/main.less b/static/styles/main.less index fa98d4ec3..cd4201692 100644 --- a/static/styles/main.less +++ b/static/styles/main.less @@ -452,26 +452,27 @@ button, .btn:not(.btn-link) {.font-semibold;} padding: 0; line-height: 1; } - -.btn-success { - .button-mixin(@btn-success); -} -.btn-primary { - .button-mixin(@btn-primary); -} -.btn-warning { - .button-mixin(@btn-warning); -} -.btn-danger { - .button-mixin(@btn-danger); -} -.btn-info { - .button-mixin(@btn-info); -} -.btn-link { - color: @link-color; - &:hover { - color: @link-color-hover; +.btn, .open > .dropdown-toggle { + &.btn-success { + .button-mixin(@btn-success); + } + &.btn-primary { + .button-mixin(@btn-primary); + } + &.btn-warning { + .button-mixin(@btn-warning); + } + &.btn-danger { + .button-mixin(@btn-danger); + } + &.btn-info { + .button-mixin(@btn-info); + } + &.btn-link { + color: @link-color; + &:hover { + color: @link-color-hover; + } } } @@ -1945,7 +1946,6 @@ input[type=range] { .control-buttons-box { margin-bottom: @management-panel-indent * 3; - overflow: hidden; padding-left: 0; .btn-group { vertical-align: baseline; @@ -1954,6 +1954,9 @@ input[type=range] { .glyphicon { margin-right: @management-panel-indent; } + &.dropdown-toggle { + padding: 6px 8px; + } } &:not(:first-child) button:first-child { margin-left: @management-panel-indent; diff --git a/static/tests/functional/test_equipment_page.js b/static/tests/functional/test_equipment_page.js index 726ca1057..ecbe1b09c 100644 --- a/static/tests/functional/test_equipment_page.js +++ b/static/tests/functional/test_equipment_page.js @@ -60,7 +60,13 @@ registerSuite(() => { 'Removing of offline nodes is available on the page') .clickByCssSelector('.node.pending_addition > label') .assertElementNotExists('.control-buttons-box .btn', - 'No management buttons for selected node') + 'No management buttons for selected online node') + .clickByCssSelector('.node.pending_addition > label') + .clickByCssSelector('.node.offline > label') + .assertElementExists('.control-buttons-box .btn', + 'Management buttons for selected offline node') + .assertElementExists('.control-buttons-box .btn-remove-nodes', + 'Only removing of offline nodes is available on the page') .assertElementExists('.node-list-management-buttons .btn-labels:not(:disabled)', 'Nodes can be labelled on the page') .assertElementsExist('.node.pending_addition .btn-view-logs', 4, diff --git a/static/translations/core.json b/static/translations/core.json index 3c49c2953..75b366b93 100644 --- a/static/translations/core.json +++ b/static/translations/core.json @@ -590,6 +590,7 @@ "search_placeholder": "Node name, MAC or IP address", "edit_roles_button": "Edit Roles", "add_nodes_button": "Add Nodes", + "remove_offline_nodes": "Remove offline nodes", "label_value_not_specified" : "Value is not specified", "label_not_assigned": "Label is not assigned", "labels": { @@ -1254,9 +1255,9 @@ "cant_discard_instruction_start": "Use the ", "cant_discard_instruction_end": " tab to adjust the configuration manually." }, - "remove_node": { - "default_message": "Fuel will remove this node from its database. This operation will not erase MBR or any data on this node. If you power on the node, it will boot bootstrap (discovery) system, and the node will appear in Fuel as a new unallocated server.", - "title": "Remove Node" + "remove_nodes": { + "default_message": "Fuel will remove the offline node(s) from its database. This operation will not erase MBR or any data on the node(s). If you power on a node, it will boot bootstrap (discovery) system, and a node will appear in Fuel as a new unallocated server.", + "title": "Remove Offline Nodes" }, "remove_cluster": { "title": "Delete Environment", @@ -2334,10 +2335,6 @@ "discard_deletion": "你确定要放弃删除节点吗?", "cant_discard": "不能放弃变更。" }, - "remove_node": { - "default_message": "Fuel 会从数据库中删除这个节点。这个操作不会删除节点的 MBR 及任何数据。如果你开启这个节点,它会进入(发现)系统,这个节点会重新成为 Fuel 未分配节点。", - "title": "删除节点" - }, "remove_cluster": { "title": "删除环境", "incomplete_actions_text": "这个环境有操作未完成,删除环境可能失败,并导致状态不一致。", @@ -3023,10 +3020,6 @@ "alert_text": "本当に変更を破棄してもよろしいですか?", "cant_discard": "ノードの変更を破棄することができません" }, - "remove_node": { - "default_message": "Fuelはデータベースからこのノードを削除します。この操作ではノードのMBRやそのほかのデータは削除されません。ノードの起動した場合、ブートストラップ(ディスカバリー)システムが起動され、新規または未割り当てのサーバとしてFuelに再表示されます。", - "title": "ノードの削除" - }, "remove_cluster": { "title": "環境の削除", "incomplete_actions_text": "完了していないアクションがあります。 環境を削除すると、失敗し、一貫性のない状態につながる可能性があります。", diff --git a/static/views/cluster_page_tabs/nodes_tab_screens/node.js b/static/views/cluster_page_tabs/nodes_tab_screens/node.js index 906bf0e00..8dbf2a943 100644 --- a/static/views/cluster_page_tabs/nodes_tab_screens/node.js +++ b/static/views/cluster_page_tabs/nodes_tab_screens/node.js @@ -15,13 +15,11 @@ **/ import _ from 'underscore'; import i18n from 'i18n'; -import Backbone from 'backbone'; import React from 'react'; import utils from 'utils'; import models from 'models'; -import dispatcher from 'dispatcher'; import {Input, Popover, Tooltip, Link} from 'views/controls'; -import {DeleteNodesDialog, RemoveOfflineNodeDialog, ShowNodeInfoDialog} from 'views/dialogs'; +import {DeleteNodesDialog, RemoveOfflineNodesDialog, ShowNodeInfoDialog} from 'views/dialogs'; import {renamingMixin} from 'component_mixins'; var Node = React.createClass({ @@ -116,37 +114,9 @@ var Node = React.createClass({ }, removeNode(e) { e.preventDefault(); - if (this.props.viewMode === 'compact') this.toggleExtendedNodePanel(); - RemoveOfflineNodeDialog - .show() - .then(() => { - // 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 - return Backbone - .sync('delete', this.props.node) - .then( - (task) => { - dispatcher.trigger('networkConfigurationUpdated updateNodeStats ' + - 'updateNotifications labelsConfigurationUpdated'); - 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; - } - if (this.props.cluster) { - this.props.cluster.get('tasks').add(new models.Task(task), {parse: true}); - } - this.props.node.set('status', 'removing'); - }, - (response) => { - utils.showErrorDialog({response: response}); - } - ); - }); + var {node, viewMode, cluster} = this.props; + if (viewMode === 'compact') this.toggleExtendedNodePanel(); + RemoveOfflineNodesDialog.show({nodes: new models.Nodes(node), cluster}); }, showNodeDetails(e) { e.preventDefault(); diff --git a/static/views/cluster_page_tabs/nodes_tab_screens/node_list_screen.js b/static/views/cluster_page_tabs/nodes_tab_screens/node_list_screen.js index 5811a28e0..b6684559b 100644 --- a/static/views/cluster_page_tabs/nodes_tab_screens/node_list_screen.js +++ b/static/views/cluster_page_tabs/nodes_tab_screens/node_list_screen.js @@ -24,7 +24,7 @@ import utils from 'utils'; import models from 'models'; import dispatcher from 'dispatcher'; import {Input, Popover, Tooltip, ProgressButton, MultiSelectControl} from 'views/controls'; -import {DeleteNodesDialog} from 'views/dialogs'; +import {DeleteNodesDialog, RemoveOfflineNodesDialog} from 'views/dialogs'; import {backboneMixin, pollingMixin, dispatcherMixin, unsavedChangesMixin} from 'component_mixins'; import Node from 'views/cluster_page_tabs/nodes_tab_screens/node'; import {Sorter, Filter} from 'views/cluster_page_tabs/nodes_tab_screens/sorter_and_filter'; @@ -681,6 +681,12 @@ ManagementPanel = React.createClass({ .show({nodes, cluster}) .then(_.partial(selectNodes, _.map(nodes.filter({status: 'ready'}), 'id'), null, true)); }, + showRemoveNodesDialog() { + var {cluster, nodes, selectNodes} = this.props; + RemoveOfflineNodesDialog + .show({nodes, cluster}) + .then(_.partial(selectNodes, _.map(nodes.filter({status: 'removing'}), 'id'), null, false)); + }, hasChanges() { return this.props.hasChanges; }, @@ -871,7 +877,7 @@ ManagementPanel = React.createClass({ }, render() { var { - nodes, screenNodes, filteredNodes, mode, locked, showBatchActionButtons, + cluster, nodes, screenNodes, filteredNodes, mode, locked, showBatchActionButtons, viewMode, changeViewMode, showViewModeButtons, search, activeSorters, availableSorters, labelSorters, defaultSorting, changeSortingOrder, addSorting, @@ -1031,84 +1037,121 @@ ManagementPanel = React.createClass({
{showBatchActionButtons && ( - mode !== 'list' ? -
- - - {i18n('common.apply_changes_button')} - -
+ cluster ? ( + mode !== 'list' ? +
+ + + {i18n('common.apply_changes_button')} + +
+ : + [ + !!nodes.length && +
+ + +
, + !locked && !!nodes.length && +
+ +
, + !locked && !!nodes.length && nodes.some({pending_deletion: false}) && +
+ {nodes.every({online: false}) ? +
+ + +
    +
  • + +
  • +
+
+ : + + } +
, + !locked && +
+ +
+ ] + ) : - [ - !!nodes.length && -
- - -
, -
- {!locked && !!nodes.length && nodes.some({pending_deletion: false}) && - - } - {!locked && !!nodes.length && - - } -
, - !locked && -
- -
- ] + !!nodes.length && nodes.every({online: false}) && + )}
{mode !== 'edit' && !!screenNodes.length && [ diff --git a/static/views/dialogs.js b/static/views/dialogs.js index 5b2073323..fdd37a70c 100644 --- a/static/views/dialogs.js +++ b/static/views/dialogs.js @@ -2051,14 +2051,45 @@ export var DiscardSettingsChangesDialog = React.createClass({ } }); -export var RemoveOfflineNodeDialog = React.createClass({ +export var RemoveOfflineNodesDialog = React.createClass({ mixins: [dialogMixin], getDefaultProps() { return { - title: i18n('dialog.remove_node.title'), - defaultMessage: i18n('dialog.remove_node.default_message') + title: i18n('dialog.remove_nodes.title'), + defaultMessage: i18n('dialog.remove_nodes.default_message') }; }, + removeNodes() { + this.setState({actionInProgress: true}); + var {cluster, nodes} = this.props; + var offlineNodes = nodes.filter({online: false}); + // sync('delete') is used instead of node.destroy() because we want + // to keep showing the 'Removing' status until the nodes is truly removed + // Otherwise this nodes would disappear and might reappear again upon + // cluster nodes refetch with status 'Removing' which would look ugly + // to the end user + Backbone.sync('delete', nodes, { + url: _.result(nodes, 'url') + '/?' + $.param({ids: _.map(offlineNodes, 'id').join(',')}) + }) + .then( + (task) => { + dispatcher.trigger('networkConfigurationUpdated updateNodeStats ' + + 'updateNotifications labelsConfigurationUpdated'); + if (task.status === 'ready') { + // Do not send the 'DELETE' request again, just get rid of the nodes. + _.each(offlineNodes, (node) => node.trigger('destroy', node)); + } else { + if (cluster) { + cluster.get('tasks').add(new models.Task(task), {parse: true}); + } + _.each(offlineNodes, (node) => node.set({status: 'removing'})); + } + this.resolveResult(); + this.close(); + }, + (response) => this.showError(response) + ); + }, renderBody() { return (
@@ -2075,10 +2106,10 @@ export var RemoveOfflineNodeDialog = React.createClass({ - {i18n('cluster_page.nodes_tab.node.remove')} + {i18n('common.remove_button')} ]; } diff --git a/static/views/equipment_page.js b/static/views/equipment_page.js index f73a93ab4..e84cb7a03 100644 --- a/static/views/equipment_page.js +++ b/static/views/equipment_page.js @@ -118,7 +118,6 @@ EquipmentPage = React.createClass({ )} {... _.pick(this.state, 'selectedNodeIds')} {... _.pick(this, 'selectNodes', 'updateUISettings')} - showBatchActionButtons={false} />