From 5f54c950d7a63ef3141953f58262dd1a96b78fd1 Mon Sep 17 00:00:00 2001 From: Julia Aranovich Date: Tue, 15 Nov 2016 10:58:37 +0300 Subject: [PATCH] Unassigned networks on interfaces configuration screen Closes-Bug: #1606177 Change-Id: I211fa81474d5f511584924e3ab2dae956930f1f1 (cherry picked from commit cff512b7ad215293d67f7568975b7ff781572ddf) --- static/styles/main.less | 64 ++++++-- static/tests/functional/pages/interfaces.js | 18 +++ .../tests/functional/test_node_interfaces.js | 16 ++ static/translations/core.json | 2 + static/views/cluster_page_tabs/network_tab.js | 6 +- .../edit_node_interfaces_screen.js | 140 +++++++++++++++--- 6 files changed, 210 insertions(+), 36 deletions(-) diff --git a/static/styles/main.less b/static/styles/main.less index fd6c56cf9..6356a0ecc 100644 --- a/static/styles/main.less +++ b/static/styles/main.less @@ -3912,7 +3912,7 @@ input[type=range] { } } -.ifc-list { +.ifc-list, .unassigned-networks { .ifc-container { @base-ifc-indent: 5px; margin-bottom: @base-ifc-indent * 2; @@ -4115,15 +4115,15 @@ input[type=range] { .physnet { label {width: 250px;} } - .toggle-configuration-control { - .glyphicon { - margin-top: 9px; - left: 50px; - cursor: pointer; - transition: transform .35s ease; - &.rotate { - transform: rotate(-180deg); - } + } + .toggle-configuration-control { + .glyphicon { + margin-top: 9px; + left: 50px; + cursor: pointer; + transition: transform .35s ease; + &.rotate { + transform: rotate(-180deg); } } } @@ -4176,16 +4176,16 @@ input[type=range] { } .networks-block { - padding: @base-ifc-indent * 2; + padding: 0; > div { - padding: 0; + padding: @base-ifc-indent * 2; } .ifc-networks { @network-block-width: 120px; @network-block-height: @network-block-width / 2; text-align: center; font-weight: 100; - min-height: @network-block-height + @base-ifc-indent; + min-height: @network-block-height; margin-right: 5px; .network-block { @@ -4195,7 +4195,6 @@ input[type=range] { border-box: sizing; background-color: @ifc-container-dark-color; margin-left: @base-ifc-indent; - margin-bottom: @base-ifc-indent; padding: @base-ifc-indent * 2 @base-ifc-indent @base-ifc-indent; cursor: move; .network-name { @@ -4282,6 +4281,43 @@ input[type=range] { } } +.unassigned-networks { + .ifc-container .ifc-inner-container { + background: #f5f5f5; + border-color: #e3e3e3; + .ifc-header { + width: 100%; + .common-ifc-name.no-checkbox { + padding: 0 5px; + } + .toggle-configuration-control { + .glyphicon { + margin-top: 0; + } + } + } + .networks-block .ifc-networks { + min-height: 80px; + .no-networks { + padding-left: 5px; + } + } + &.compact { + .networks-block .ifc-networks { + min-height: 70px; + } + } + &.collapsed { + .ifc-header { + border-bottom: none; + .common-ifc-name.no-checkbox { + padding-top: 0px; + } + } + } + } +} + // Healthcheck tab .healthcheck-tab { @padding-top-controls: 10px; diff --git a/static/tests/functional/pages/interfaces.js b/static/tests/functional/pages/interfaces.js index 9c2b6c92e..9431d51a2 100644 --- a/static/tests/functional/pages/interfaces.js +++ b/static/tests/functional/pages/interfaces.js @@ -86,6 +86,24 @@ class InterfacesPage { .then((ifcElement) => this.remote.dragTo(ifcElement)); } + removeNetwork(networkName) { + return this.remote + .findAllByCssSelector('div.network-block') + .then( + (networkElements) => networkElements.reduce( + (result, networkElement) => networkElement + .getVisibleText() + .then((currentNetworkName) => { + return currentNetworkName === networkName ? networkElement : result; + }), + null + ) + ) + .then((networkElement) => this.remote.dragFrom(networkElement)) + .then(() => this.remote.findByCssSelector('.unassigned-networks .ifc-inner-container')) + .then((ifcElement) => this.remote.dragTo(ifcElement)); + } + selectInterface(ifcName) { return this.remote .then(() => this.findInterfaceElement(ifcName)) diff --git a/static/tests/functional/test_node_interfaces.js b/static/tests/functional/test_node_interfaces.js index 4c33cf551..b9fa9862c 100644 --- a/static/tests/functional/test_node_interfaces.js +++ b/static/tests/functional/test_node_interfaces.js @@ -83,6 +83,22 @@ registerSuite(() => { 'MTU control is hidden after clicking MTU link again' ); }, + 'Unassigned networks'() { + return this.remote + .assertElementExists('.unassigned-networks .collapsed', 'Unassigned networks block exists') + .assertElementNotExists( + '.unassigned-networks .networks-block.collapse.in', + 'Unassigned networks block is collapsed by default' + ) + .clickByCssSelector('.unassigned-networks .toggle-configuration-control .glyphicon') + .then(() => interfacesPage.removeNetwork('Public')) + .assertElementTextEquals( + '.unassigned-networks .network-block .network-name', + 'Public', + 'Public network was successfully removed' + ) + .assertElementEnabled('.btn-apply', 'Network removal can be saved'); + }, 'Untagged networks error'() { return this.remote .then(() => interfacesPage.assignNetworkToInterface('Public', 'eth0')) diff --git a/static/translations/core.json b/static/translations/core.json index ffc815d96..035db1ea8 100644 --- a/static/translations/core.json +++ b/static/translations/core.json @@ -507,6 +507,8 @@ "fast": "Fast" }, "bonding_policy": "Xmit Hash Policy", + "unassigned_networks": "Unassigned Networks (__count__)", + "no_unassigned_networks": "There are no unassigned networks", "drag_and_drop_description": "You can drag and drop logical networks between the interfaces", "configuration_error": { "title": "Interfaces Configuration Error", diff --git a/static/views/cluster_page_tabs/network_tab.js b/static/views/cluster_page_tabs/network_tab.js index d045edc99..cddfb269a 100644 --- a/static/views/cluster_page_tabs/network_tab.js +++ b/static/views/cluster_page_tabs/network_tab.js @@ -1271,10 +1271,12 @@ var Network = React.createClass({ return (

- {i18n('network.' + networkName)} + {i18n('network.' + networkName, {defaultValue: networkName})}

-
{i18n('network.descriptions.' + networkName)}
+
+ {i18n('network.descriptions.' + networkName, {defaultValue: networkName})} +
}
]} + interfaces.some( + (ifc) => ifc.get('assigned_networks').some({name: network.get('name')}) + ) + ) + } + networkingParameters={cluster.get('networkConfiguration').get('networking_parameters')} + {...{viewMode, locked, interfaces}} + />
{interfaces.map((ifc, index) => { var ifcName = ifc.get('name'); @@ -859,6 +870,98 @@ var EditNodeInterfacesScreen = React.createClass({ } }); +var UnassignedNetworks = React.createClass({ + statics: { + target: { + drop({interfaces}, monitor) { + var {networkName, interfaceName} = monitor.getItem(); + var sourceInterface = interfaces.find({name: interfaceName}); + var network = sourceInterface.get('assigned_networks').find({name: networkName}); + sourceInterface.get('assigned_networks').remove(network); + // trigger 'change' event to update screen buttons state + sourceInterface.trigger('change', sourceInterface); + }, + canDrop(props, monitor) { + return !!monitor.getItem().interfaceName; + } + }, + collect(connect, monitor) { + return { + connectDropTarget: connect.dropTarget(), + isOver: monitor.isOver(), + canDrop: monitor.canDrop() + }; + } + }, + getInitialState() { + return {collapsed: true}; + }, + toggle() { + $(ReactDOM.findDOMNode(this.refs['networks-panel'])).collapse('toggle'); + }, + assignNetworksPanelEvents() { + $(ReactDOM.findDOMNode(this.refs['networks-panel'])) + .on('show.bs.collapse', () => this.setState({collapsed: false})) + .on('hide.bs.collapse', () => this.setState({collapsed: true})); + }, + componentDidMount() { + this.assignNetworksPanelEvents(); + }, + render() { + var {networks, viewMode, connectDropTarget} = this.props; + var {collapsed} = this.state; + return connectDropTarget( +
+
+
+
+
+ {i18n(ns + 'unassigned_networks', {count: networks.length})} +
+
+ +
+
+
+
+ {networks.map((network) => + + )} + {!networks.length && +
{i18n(ns + 'no_unassigned_networks')}
+ } +
+
+
+
+
+ ); + } +}); + +var UnassignedNetworksDropTarget = DropTarget( + 'network', + UnassignedNetworks.target, + UnassignedNetworks.collect +)(UnassignedNetworks); + var ErrorScreen = React.createClass({ render() { var {nodes, cluster, nodesByNetworksMap} = this.props; @@ -915,12 +1018,15 @@ var ErrorScreen = React.createClass({ var NodeInterface = React.createClass({ statics: { target: { - drop(props, monitor) { - var targetInterface = props.interface; - var sourceInterface = props.interfaces.find({name: monitor.getItem().interfaceName}); - var network = sourceInterface.get('assigned_networks') - .find({name: monitor.getItem().networkName}); - sourceInterface.get('assigned_networks').remove(network); + drop({interface: targetInterface, interfaces, cluster}, monitor) { + var {networkName, interfaceName} = monitor.getItem(); + var sourceInterface = interfaces.find({name: interfaceName}); + var network = sourceInterface ? + sourceInterface.get('assigned_networks').find({name: networkName}) + : + cluster.get('networkConfiguration').get('networks') + .find({name: networkName}).pick('id', 'name'); + if (sourceInterface) sourceInterface.get('assigned_networks').remove(network); targetInterface.get('assigned_networks').add(network); // trigger 'change' event to update screen buttons state targetInterface.trigger('change', targetInterface); @@ -1065,7 +1171,6 @@ var NodeInterface = React.createClass({
); @@ -1234,8 +1339,8 @@ var NodeInterface = React.createClass({ isMassConfiguration={!!this.props.nodes.length} bondingModeChanged={this.bondingModeChanged} /> - : -
+ : +
}
@@ -1255,7 +1360,7 @@ var Network = React.createClass({ beginDrag(props) { return { networkName: props.network.get('name'), - interfaceName: props.interface.get('name') + interfaceName: props.interface && props.interface.get('name') }; }, canDrag(props) { @@ -1270,9 +1375,7 @@ var Network = React.createClass({ } }, render() { - var network = this.props.network; - var interfaceNetwork = this.props.interfaceNetwork; - var networkingParameters = this.props.networkingParameters; + var {network, label, networkingParameters} = this.props; var classes = { 'network-block pull-left': true, disabled: !this.constructor.source.canDrag(this.props), @@ -1283,10 +1386,7 @@ var Network = React.createClass({ return this.props.connectDragSource(
- {i18n( - 'network.' + interfaceNetwork.get('name'), - {defaultValue: interfaceNetwork.get('name')} - )} + {i18n('network.' + label, {defaultValue: label})}
{vlanRange &&
@@ -1304,8 +1404,8 @@ var DraggableNetwork = DragSource('network', Network.source, Network.collect)(Ne var NodeInterfaceAttributes = React.createClass({ assignConfigurationPanelEvents() { $(ReactDOM.findDOMNode(this.refs['configuration-panel'])) - .on('show.bs.collapse', () => this.setState({pendingToggle: false, collapsed: false})) - .on('hide.bs.collapse', () => this.setState({pendingToggle: false, collapsed: true})); + .on('show.bs.collapse', () => this.setState({pendingToggle: false, collapsed: false})) + .on('hide.bs.collapse', () => this.setState({pendingToggle: false, collapsed: true})); }, componentDidMount() { this.assignConfigurationPanelEvents();