File contents control for settings tab

Related to blueprint ssl-endpoints

Change-Id: I82840a1fb0b43e693e37df53985ec0717ec21586
This commit is contained in:
Nick Bogdanov 2015-06-16 13:38:58 +03:00
parent 48d09eb09f
commit 889209c47a
8 changed files with 187 additions and 7 deletions

View File

@ -44,7 +44,17 @@ structure includes the following attributes::
* *label* is a setting title that is displayed on UI
* *weight* defines the order in which this setting is displayed in its group.
This attribute is desirable
* *type* defines the type of UI control to use for the setting
* *type* defines the type of UI control to use for the setting. The following types are supported:
* *text* - single line input
* *password* - password input
* *textarea* - multiline input
* *checkbox* - multiple-options selector
* *radio* - single-option selector
* *select* - drop-down list
* *hidden* - invisible input
* *file* - file contents input
* *regex* section is applicable for settings of "text" type. "regex.source"
is used when validating with a regular expression. "regex.error" contains
a warning displayed near invalid field

View File

@ -22,7 +22,8 @@
"open-sans-fontface": "1.4.0"
},
"devDependencies": {
"es5-shim": "4.1.0"
"es5-shim": "4.1.0",
"sinon": "1.15.3"
},
"overrides": {
"react": {
@ -56,6 +57,13 @@
"dist/css/bootstrap.css.map",
"dist/fonts/*"
]
},
"sinon": {
"main": [
"lib/sinon.js",
"lib/sinon/*",
"lib/sinon/util/*"
]
}
},
"ignore": [

View File

@ -77,6 +77,7 @@ attribute_schema = {
'select',
'text',
'textarea',
'file',
]
},
#'value': None, # custom validation depending on type

View File

@ -41,7 +41,8 @@ define(function() {
i18next: 'vendor/bower/i18next/release/i18next-1.7.1',
deepModel: 'vendor/custom/deep-model',
lessLibrary: 'vendor/bower/less/dist/less',
'require-css': 'vendor/bower/require-css'
'require-css': 'vendor/bower/require-css',
sinon: 'vendor/bower/sinon/lib/sinon'
},
shim: {
'expression/parser': {

View File

@ -1913,6 +1913,14 @@ button, .btn:not(.btn-link) {.font-semibold;}
input, textarea {
float: left;
}
input[type=file] {
opacity: 0;
position: absolute;
}
&:not(.disabled) input.file-name[readonly] {
background-color: white;
cursor: pointer;
}
input, textarea, select {
max-width: @default-input-width;
width: @default-input-width;

View File

@ -0,0 +1,92 @@
define([
'intern!object',
'intern/chai!assert',
'underscore',
'sinon'
], function(registerSuite, assert, _, sinon) {
'use strict';
var input;
registerSuite({
name: 'File Control',
beforeEach: function() {
var controls = require('views/controls');
input = new controls.Input({
type: 'file',
name: 'some_file',
label: 'Please select some file',
description: 'File should be selected from the local disk',
disabled: false,
onChange: sinon.spy(),
defaultValue: {
name: 'certificate.crt',
content: 'CERTIFICATE'
}
});
},
Initialization: function() {
var initialState = input.getInitialState();
assert.equal(input.props.type, 'file', 'Input type should be equal to file');
assert.equal(initialState.fileName, 'certificate.crt', 'Default file name must correspond to provided one');
assert.equal(initialState.content, 'CERTIFICATE', 'Content should be equal to the default');
},
'File selection': function() {
var clickSpy = sinon.spy();
sinon.stub(input, 'getInputDOMNode').returns({
click: clickSpy
});
input.pickFile();
assert.ok(clickSpy.calledOnce, 'When icon clicked input control should be clicked too to open select file dialog');
},
'File fetching': function() {
var readMethod = sinon.mock(),
readerObject = {
readAsBinaryString: readMethod,
result: 'File contents'
},
saveMethod = sinon.spy(input, 'saveFile');
window.FileReader = function() {return readerObject};
sinon.stub(input, 'getInputDOMNode').returns({
value: '/dummy/path/to/somefile.ext',
files: ['file1']
});
input.readFile();
assert.ok(readMethod.calledOnce, 'File reading as binary expected to be executed once');
sinon.assert.calledWith(readMethod, 'file1');
readerObject.onload();
assert.ok(saveMethod.calledOnce, 'saveFile handler called once');
sinon.assert.calledWith(saveMethod, 'somefile.ext', 'File contents');
},
'File saving': function() {
var setState = sinon.spy(input, 'setState'),
dummyName = 'dummy.ext',
dummyContent = 'Lorem ipsum dolores';
input.saveFile(dummyName, dummyContent);
assert.deepEqual(setState.args[0][0], {
fileName: dummyName,
content: dummyContent
}, 'Save file must update control state with data supplied');
assert.deepEqual(input.props.onChange.args[0][1], {
name: dummyName,
content: dummyContent
}, 'Control sends updated data upon changes');
}
});
});

View File

@ -47,6 +47,11 @@
"recommended": "It is recommended to have at least __limitValue__ of __roleName__ nodes (__count__ selected currently)."
}
},
"controls": {
"file": {
"placeholder": "No file selected"
}
},
"node_details": {
"cpu": "CPU",
"hdd": "HDD",

View File

@ -20,7 +20,8 @@
* Based on https://github.com/react-bootstrap/react-bootstrap/blob/master/src/Input.jsx
**/
define(['jquery', 'underscore', 'react', 'utils', 'jsx!component_mixins'], function($, _, React, utils, componentMixins) {
define(['i18n', 'jquery', 'underscore', 'react', 'utils', 'jsx!component_mixins'],
function(i18n, $, _, React, utils, componentMixins) {
'use strict';
var controls = {};
@ -52,7 +53,7 @@ define(['jquery', 'underscore', 'react', 'utils', 'jsx!component_mixins'], funct
controls.Input = React.createClass({
mixins: [tooltipMixin],
propTypes: {
type: React.PropTypes.oneOf(['text', 'password', 'textarea', 'checkbox', 'radio', 'select', 'hidden', 'number', 'range']).isRequired,
type: React.PropTypes.oneOf(['text', 'password', 'textarea', 'checkbox', 'radio', 'select', 'hidden', 'number', 'range', 'file']).isRequired,
name: React.PropTypes.node,
label: React.PropTypes.node,
description: React.PropTypes.node,
@ -66,7 +67,11 @@ define(['jquery', 'underscore', 'react', 'utils', 'jsx!component_mixins'], funct
extraContent: React.PropTypes.node
},
getInitialState: function() {
return {visible: false};
return {
visible: false,
fileName: this.props.defaultValue && this.props.defaultValue.name || null,
content: this.props.defaultValue && this.props.defaultValue.content || null
};
},
togglePassword: function() {
this.setState({visible: !this.state.visible});
@ -83,10 +88,41 @@ define(['jquery', 'underscore', 'react', 'utils', 'jsx!component_mixins'], funct
debouncedInput: _.debounce(function() {
return this.onInput();
}, 10, {leading: true}),
pickFile: function() {
this.getInputDOMNode().click();
},
saveFile: function(fileName, content) {
this.setState({
fileName: fileName,
content: content
});
return this.props.onChange(
this.props.name,
{name: fileName, content: content}
);
},
removeFile: function() {
this.refs.form.getDOMNode().reset();
this.saveFile(null, null);
},
readFile: function() {
var reader = new FileReader(),
input = this.getInputDOMNode();
if (input.files.length) {
reader.onload = (function() {
return this.saveFile(input.value.replace(/^.*[\\\/]/g, ''), reader.result);
}).bind(this);
reader.readAsBinaryString(input.files[0]);
}
},
onChange: function() {
if (this.props.onChange) {
var input = this.getInputDOMNode();
return this.props.onChange(this.props.name, this.props.type == 'checkbox' ? input.checked : input.value);
return this.props.onChange(
this.props.name,
this.props.type == 'checkbox' ? input.checked : input.value
);
}
},
onInput: function() {
@ -106,6 +142,8 @@ define(['jquery', 'underscore', 'react', 'utils', 'jsx!component_mixins'], funct
};
if (this.props.type == 'range') {
props.onInput = this.debouncedInput;
} else if (this.props.type == 'file') {
props.onChange = this.readFile;
} else {
// debounced onChange callback is supported for uncontrolled inputs
props.onChange = (_.isUndefined(this.props.value) && _.isUndefined(this.props.checked)) ? this.debouncedChange : this.onChange;
@ -118,9 +156,26 @@ define(['jquery', 'underscore', 'react', 'utils', 'jsx!component_mixins'], funct
'custom-tumbler': isCheckboxOrRadio,
textarea: this.props.type == 'textarea'
};
if (this.props.type == 'file') {
input = <form ref='form'>{input}</form>;
}
return (
<div key='input-group' className={utils.classNames(inputWrapperClasses)}>
{input}
{this.props.type == 'file' &&
<div className='input-group'>
<input
className='form-control file-name'
type='text'
placeholder={i18n('controls.file.placeholder')}
value={this.state.fileName && '[' + utils.showSize(this.state.content.length) + '] ' + this.state.fileName}
onClick={this.pickFile}
readOnly />
<div className='input-group-addon' onClick={this.state.fileName ? this.removeFile : this.pickFile}>
<i className={this.state.fileName ? 'glyphicon glyphicon-remove' : 'glyphicon glyphicon-file'} />
</div>
</div>
}
{this.props.toggleable &&
<div className='input-group-addon' onClick={this.togglePassword}>
<i className={this.state.visible ? 'glyphicon glyphicon-eye-close' : 'glyphicon glyphicon-eye-open'} />