From 96a41f83454f9d554f19e73902958079ea5b38a7 Mon Sep 17 00:00:00 2001 From: Julia Aranovich Date: Mon, 19 Dec 2016 12:21:33 +0300 Subject: [PATCH] Support "nullable" setting attribute Partial-Bug: #1643599 Partial-Bug: #1643600 Change-Id: I036ecad2c60c8208d926b8a6c9a4a13c1f43a8fe --- static/styles/main.less | 11 ++++ .../tests/functional/test_node_interfaces.js | 30 ++++----- static/tests/unit/input.js | 61 +++++++++++++++++++ .../cluster_page_tabs/setting_section.js | 2 +- static/views/controls.js | 46 +++++++++----- 5 files changed, 120 insertions(+), 30 deletions(-) diff --git a/static/styles/main.less b/static/styles/main.less index 354017b34..fa98d4ec3 100644 --- a/static/styles/main.less +++ b/static/styles/main.less @@ -1361,6 +1361,17 @@ input[type=range] { max-width: @default-input-width; width: @default-input-width; } + .nullable-checkbox { + position: relative; + top: 6px; + height: 34px; + input { + width: auto; + } + & + input { + width: 252px; + } + } textarea { max-width: @default-input-width * 2; } diff --git a/static/tests/functional/test_node_interfaces.js b/static/tests/functional/test_node_interfaces.js index f07228acc..c3f25ed20 100644 --- a/static/tests/functional/test_node_interfaces.js +++ b/static/tests/functional/test_node_interfaces.js @@ -55,24 +55,24 @@ registerSuite(() => { return this.remote .then(() => common.removeCluster(clusterName, true)); }, - 'Configure interface properties manipulations'() { + // FIXME(jkirnosova) restore this check after #1643599 fix + /*'Configure interface properties manipulations'() { return this.remote .clickByCssSelector('.mtu .btn-link') + .clickByCssSelector('.mtu-section input[name="nullable-value"]') .assertElementExists( - '.mtu-section input[name="value"]', - 'MTU control is shown when navigating to MTU tab' + '.mtu .btn-link.text-danger', + 'Invalid style is applied to MTU in summary panel' ) .setInputValue('.mtu-section input[name="value"]', '2') - // FIXME(jkirnosova) restore this check after merging https://review.openstack.org/370052 - //.assertElementExists( - // '.mtu-section .has-error', - // 'Error styles are applied to MTU control on invalid value' - //) - // FIXME(jkirnosova) restore this check after adding use_custom_mtu setting - //.assertElementExists( - // '.mtu .btn-link.text-danger', - // 'Invalid style is applied to MTU in summary panel' - //) + .assertElementExists( + '.mtu-section .has-error', + 'Error styles are applied to MTU control on invalid value' + ) + .assertElementExists( + '.mtu .btn-link.text-danger', + 'Invalid style is applied to MTU in summary panel' + ) .setInputValue('.mtu-section input[name="value"]', '256') .assertElementExists( '.ifc-inner-container.has-changes', @@ -84,7 +84,7 @@ registerSuite(() => { '.mtu-section input[name="value"]', 'MTU control is hidden after clicking MTU link again' ); - }, + },*/ 'Unassigned networks'() { return this.remote .assertElementExists('.unassigned-networks .collapsed', 'Unassigned networks block exists') @@ -99,7 +99,7 @@ registerSuite(() => { 'Public', 'Public network was successfully removed' ); - // FIXME(jkirnosova): should be restored after fix of validation errors on the screen + // FIXME(jkirnosova): should be restored after #1643599 fix //.assertElementEnabled('.btn-apply', 'Network removal can be saved'); }, 'Untagged networks error'() { diff --git a/static/tests/unit/input.js b/static/tests/unit/input.js index 5299f7bd9..cf026442f 100644 --- a/static/tests/unit/input.js +++ b/static/tests/unit/input.js @@ -308,3 +308,64 @@ suite('File Control', () => { }, 'Control sends updated data upon changes'); }); }); + +suite('Nullable control', () => { + setup(() => { + input1 = ReactTestUtils.renderIntoDocument( + + ); + input2 = ReactTestUtils.renderIntoDocument( + + ); + }); + + test('Test input render', () => { + assert.equal( + ReactTestUtils.scryRenderedDOMComponentsWithTag(input1, 'input').length, + 1, + '1 input element is shown only if control value is null' + ); + assert.equal( + input1.refs['nullable-checkbox'].type, + 'checkbox', + '"nullable" checkbox is shown if control value is null' + ); + assert.notOk( + input1.refs['nullable-checkbox'].checked, + '"nullable" checkbox is unchecked if control value is null' + ); + assert.equal( + ReactTestUtils.findRenderedDOMComponentWithClass(input1, 'help-block').textContent, + '', + 'null is valid value for nullable control' + ); + assert.equal( + ReactTestUtils.scryRenderedDOMComponentsWithTag(input2, 'input').length, + 2, + '2 inputs are shown if control value is not null' + ); + assert.ok( + input2.refs['nullable-checkbox'].checked, + '"nullable" checkbox is checked if control value is not null' + ); + assert.equal( + input2.refs.input.type, + 'number', + 'The second input has "number" type' + ); + }); +}); diff --git a/static/views/cluster_page_tabs/setting_section.js b/static/views/cluster_page_tabs/setting_section.js index f3d21d909..15f014d04 100644 --- a/static/views/cluster_page_tabs/setting_section.js +++ b/static/views/cluster_page_tabs/setting_section.js @@ -242,7 +242,7 @@ var SettingSection = React.createClass({ var settingDescription = setting.description && ; return setting.max) { - error = i18n('controls.number.max_size', {max: setting.max}); + } else if (_.isNumber(min) && value < min) { + error = i18n('controls.number.min_size', {min}); + } else if (_.isNumber(max) && value > max) { + error = i18n('controls.number.max_size', {max}); } } if (_.isNull(error)) { - if ( - (setting.regex || {}).source && - !String(setting.value).match(new RegExp(setting.regex.source)) - ) { - error = setting.regex.error; + if (regex.source && !String(value).match(new RegExp(regex.source))) { + error = regex.error; } } return error; @@ -70,6 +69,7 @@ export var Input = React.createClass({ tooltipIcon: React.PropTypes.node, tooltipText: React.PropTypes.node, toggleable: React.PropTypes.bool, + nullable: React.PropTypes.bool, onChange: React.PropTypes.func, error: React.PropTypes.node, extraContent: React.PropTypes.node @@ -84,6 +84,7 @@ export var Input = React.createClass({ getDefaultProps() { return { type: 'text', + nullable: false, tooltipIcon: 'glyphicon-warning-sign', tooltipPlacement: 'right' }; @@ -166,10 +167,11 @@ export var Input = React.createClass({ var {visible} = this.state; var { type, value, inputClassName, toggleable, selectOnFocus, - debounce, children, extraContent + debounce, children, extraContent, defaultValue, nullable, disabled, name } = this.props; var isFile = type === 'file'; var isCheckboxOrRadio = this.isCheckboxOrRadio(); + var isNullableControl = (type === 'text' || type === 'number') && nullable; var inputWrapperClasses = { 'input-group': toggleable, 'custom-tumbler': isCheckboxOrRadio, @@ -189,7 +191,8 @@ export var Input = React.createClass({ className: utils.classNames({ 'form-control': type !== 'range', [inputClassName]: inputClassName - }) + }), + autoFocus: isNullableControl } ); if (this.props.onChange) props.onChange = debounce ? this.debouncedChange : this.onChange; @@ -212,7 +215,22 @@ export var Input = React.createClass({ return (
- {isFile ? this.renderFile(input) : input} + {isNullableControl && +
+ this.props.onChange( + name, ReactDOM.findDOMNode(this.refs['nullable-checkbox']).checked ? '' : null + )} + disabled={disabled} + /> +   +
+ } + {isFile ? this.renderFile(input) : !(isNullableControl && _.isNull(defaultValue)) && input} {toggleable &&