From 2c53601802111f060c22ba2dc0a347defc45b51f Mon Sep 17 00:00:00 2001 From: Thomas Rueckstiess Date: Tue, 10 Nov 2015 13:29:46 +1100 Subject: [PATCH 1/2] INT-780 save ssl cert paths, removing debug msgs --- src/connect/behavior.js | 11 +++--- src/connect/connect-form-view.js | 3 +- src/connect/filereader-view.js | 64 ++++++++++++++++++++++++++++++++ src/connect/index.js | 11 +----- src/connect/input-view.js | 39 +------------------ src/connect/ssl.js | 14 +++---- 6 files changed, 79 insertions(+), 63 deletions(-) create mode 100644 src/connect/filereader-view.js diff --git a/src/connect/behavior.js b/src/connect/behavior.js index eecd5ff69d6..caa7148bec3 100644 --- a/src/connect/behavior.js +++ b/src/connect/behavior.js @@ -1,9 +1,10 @@ var State = require('ampersand-state'); -var debug = require('debug')('scout:connect:behavior'); var assert = require('assert'); var Connection = require('../models/connection'); var _ = require('lodash'); +// var debug = require('debug')('scout:connect:behavior'); + module.exports = State.extend({ props: { view: 'any', @@ -79,9 +80,9 @@ module.exports = State.extend({ }, dispatch: function(action) { var newState = this.reduce(this.state, action); - if (newState !== this.state) { - debug('transition: (%s, %s) ==> %s', this.state, action, newState); - } + // if (newState !== this.state) { + // debug('transition: (%s, %s) ==> %s', this.state, action, newState); + // } this.state = newState; return this.state; }, @@ -100,7 +101,7 @@ module.exports = State.extend({ // check if the current state allows the given action if (this.validTransitions[state].indexOf(action) === -1) { - debug('ignoring action `%s` in state `%s`', action, state); + // debug('ignoring action `%s` in state `%s`', action, state); return state; } // general actions, independent of state diff --git a/src/connect/connect-form-view.js b/src/connect/connect-form-view.js index 77cb96a67e3..cabaecae751 100644 --- a/src/connect/connect-form-view.js +++ b/src/connect/connect-form-view.js @@ -4,7 +4,7 @@ var SelectView = require('ampersand-select-view'); var authOptions = require('./authentication'); var sslOptions = require('./ssl'); var FilteredCollection = require('ampersand-filtered-subcollection'); -var debug = require('debug')('scout:connect:connect-form-view'); +// var debug = require('debug')('scout:connect:connect-form-view'); require('bootstrap/js/popover'); @@ -66,7 +66,6 @@ var ConnectFormView = FormView.extend({ return; } var name = obj.hostname + ':' + obj.port; - debug('obj', obj); if (obj.authentication === 'MONGODB') { if (obj.mongodb_username) { name = obj.mongodb_username + '@' + name; diff --git a/src/connect/filereader-view.js b/src/connect/filereader-view.js new file mode 100644 index 00000000000..e881c173aba --- /dev/null +++ b/src/connect/filereader-view.js @@ -0,0 +1,64 @@ +var InputView = require('./input-view'); +var _ = require('lodash'); +// var path = require('path'); +var fileReaderTemplate = require('./filereader-default.jade'); + +// var debug = require('debug')('scout:connect:filereader-view'); + +module.exports = InputView.extend({ + template: fileReaderTemplate, + clean: function() { + var value; + value = _.chain(this.input.files) + .map(function(file) { + return _.get(file, 'path', null); + }) + .filter() + .value(); + if (value.length === 0) { + value = ''; + } + return value; + }, + setValue: function(value, skipValidation) { + if (!this.input) { + this.inputValue = value; + return; + } + this.input.value = ''; + /** + * Cannot set input value for file types. @see INT-780 + */ + // if (value || value === 0) { + // if (!_.isArray(value)) { + // value = [value]; + // } + // if (value.length <= 1) { + // this.input.value = path.basename(value); + // } else { + // this.input.value = 'multiple files'; + // } + // this.input.files = _.map(value, function(f) { + // return { + // name: path.basename(f), + // path: f + // }; + // }); + // } + this.inputValue = this.clean(); + if (!skipValidation && !this.getErrorMessage()) { + this.shouldValidate = true; + } else if (skipValidation) { + this.shouldValidate = false; + } + }, + handleChange: function() { + if (this.inputValue && this.changed) { + this.shouldValidate = true; + } + // for `file` type input fields, this is the only event and we need + // to set this.inputValue here again. + this.inputValue = this.clean(); + this.runTests(); + } +}); diff --git a/src/connect/index.js b/src/connect/index.js index c9da201bb80..ec17cab3ca0 100644 --- a/src/connect/index.js +++ b/src/connect/index.js @@ -94,6 +94,7 @@ var ConnectView = View.extend({ 'input input[name=name]': 'onNameInputChanged', 'change input[name=name]': 'onNameInputChanged', 'input input': 'onAnyInputChanged', + 'change input': 'onAnyInputChanged', 'change select': 'onAnyInputChanged' }, @@ -292,14 +293,8 @@ var ConnectView = View.extend({ */ updateConnection: function() { if (this.connection) { - debug('updating existing connection from form data'); - // set previous auth fields - var authFields = Connection.getFieldNames(this.previousAuthMethod); - debug('authFields', authFields); this.connection.set(this.form.data); - debug('after', this.connection.serialize()); } else { - debug('creating new connection from form data'); this.connection = new Connection(this.form.data); } this.connection.is_favorite = true; @@ -339,10 +334,6 @@ var ConnectView = View.extend({ return; } app.statusbar.show(); - debug('trying to connect with URL %s and options %j', - connection.driver_url, - connection.driver_options - ); connection.test(function(err) { app.statusbar.hide(); diff --git a/src/connect/input-view.js b/src/connect/input-view.js index 5f557b37db5..76a82d049cc 100644 --- a/src/connect/input-view.js +++ b/src/connect/input-view.js @@ -1,11 +1,9 @@ var InputView = require('ampersand-input-view'); -var _ = require('lodash'); - // var debug = require('debug')('scout:connect:input-view'); /** - * Need to overwrite render() method and pass in `this` for renderWithTemplate(), so that - * label gets correctly rendered on subsequent render() calls. + * Need to overwrite render() method and pass in `this` for renderWithTemplate() + * so that label gets correctly rendered on subsequent render() calls. */ module.exports = InputView.extend({ initialize: function() { @@ -23,38 +21,5 @@ module.exports = InputView.extend({ // if the field is not required this.setValue(this.inputValue, !this.required); return this; - }, - clean: function(val) { - if (this.type === 'number') { - return Number(val); - } else if (this.type === 'string') { - return val.trim(); - } - return val; - }, - /** - * overwriting InputView#beforeSubmit to handle `file` type correctly. - * For `file` type, the input view returns the actual path (provided by - * electron) rather than the browser fake path. - * @see https://github.com/atom/electron/blob/master/docs/api/file-object.md - */ - beforeSubmit: function() { - var value; - if (this.type === 'file') { - value = _.chain(this.input.files) - .map(function(file) { - return _.get(file, 'path', null); - }) - .filter() - .value(); - if (value.length === 0) { - value = ''; - } - } else { - value = this.input.value; - } - this.inputValue = this.clean(value); - this.shouldValidate = true; - this.runTests(); } }); diff --git a/src/connect/ssl.js b/src/connect/ssl.js index d4af8efc050..a1198c90456 100644 --- a/src/connect/ssl.js +++ b/src/connect/ssl.js @@ -6,8 +6,8 @@ var SSLOptionCollection = require('./models/ssl-option-collection'); var InputView = require('./input-view'); +var FileReaderView = require('./filereader-view'); var inputTemplate = require('./input-default.jade'); -var fileReaderTemplate = require('./filereader-default.jade'); // var debug = require('debug')('scout:connect:ssl'); @@ -37,8 +37,7 @@ var SERVER = { // enabled: app.isFeatureEnabled('Connect with SSL SERVER'), enabled: true, fields: [ - new InputView({ - template: fileReaderTemplate, + new FileReaderView({ name: 'ssl_ca', type: 'file', label: 'Certificate Authority', @@ -57,24 +56,21 @@ var ALL = { // enabled: app.isFeatureEnabled('Connect with SSL ALL'), enabled: true, fields: [ - new InputView({ - template: fileReaderTemplate, + new FileReaderView({ name: 'ssl_ca', type: 'file', label: 'Certificate Authority', placeholder: '', required: true }), - new InputView({ - template: fileReaderTemplate, + new FileReaderView({ name: 'ssl_private_key', type: 'file', label: 'Certificate Key', placeholder: '', required: true }), - new InputView({ - template: fileReaderTemplate, + new FileReaderView({ name: 'ssl_certificate', type: 'file', label: 'Certificate', From 176941e07187ad2e4a628df18c6b9bff389d35f2 Mon Sep 17 00:00:00 2001 From: Thomas Rueckstiess Date: Wed, 11 Nov 2015 15:42:33 +1100 Subject: [PATCH 2/2] INT-780 custom file picker using electron dialog HTML file picker cannot set the value programmatically for security reasons. But in electron land we can use their `showOpenDialog`. --- package.json | 1 + src/connect/connect-form-view.js | 2 +- src/connect/filereader-default.jade | 5 +- src/connect/filereader-view.js | 204 ++++++++++++++++++++++------ src/connect/index.jade | 4 +- src/connect/index.js | 3 +- src/connect/index.less | 4 +- src/connect/ssl.js | 10 -- 8 files changed, 174 insertions(+), 59 deletions(-) diff --git a/package.json b/package.json index 76e75781594..fa95fe6c071 100644 --- a/package.json +++ b/package.json @@ -88,6 +88,7 @@ "ampersand-collection-filterable": "^0.2.1", "ampersand-collection-lodash-mixin": "^2.0.1", "ampersand-collection-rest-mixin": "^5.0.0", + "ampersand-dom-bindings": "^3.7.0", "ampersand-filtered-subcollection": "^2.0.4", "ampersand-form-view": "^5.1.1", "ampersand-input-view": "^5.0.0", diff --git a/src/connect/connect-form-view.js b/src/connect/connect-form-view.js index cabaecae751..8c224bdc47e 100644 --- a/src/connect/connect-form-view.js +++ b/src/connect/connect-form-view.js @@ -188,7 +188,7 @@ var ConnectFormView = FormView.extend({ el: this.parent.queryByHook('saveas-subview'), name: 'name', label: 'Name', - placeholder: 'e.g. Shared Dev, Stats Box, PRODUCTION', + placeholder: 'e.g. Shared Dev, QA Box, PRODUCTION', required: false }) ]; diff --git a/src/connect/filereader-default.jade b/src/connect/filereader-default.jade index f3f3924f505..5899d26547a 100644 --- a/src/connect/filereader-default.jade +++ b/src/connect/filereader-default.jade @@ -3,4 +3,7 @@ .message.message-below.message-error(data-hook='message-container') p(data-hook='message-text') .form-item-file - input(type="file", name="uploadKeyFile", multiple) + .btn.btn-default(data-hook='load-file-button', style='text-transform: none;') + i.fa.fa-file + |   + span(data-hook='button-label')= buttonTitle diff --git a/src/connect/filereader-view.js b/src/connect/filereader-view.js index e881c173aba..c5738efba61 100644 --- a/src/connect/filereader-view.js +++ b/src/connect/filereader-view.js @@ -1,64 +1,182 @@ var InputView = require('./input-view'); var _ = require('lodash'); -// var path = require('path'); +var path = require('path'); +var remote = window.require('remote'); +var dialog = remote.require('dialog'); +var format = require('util').format; +var bindings = require('ampersand-dom-bindings'); var fileReaderTemplate = require('./filereader-default.jade'); // var debug = require('debug')('scout:connect:filereader-view'); module.exports = InputView.extend({ template: fileReaderTemplate, - clean: function() { - var value; - value = _.chain(this.input.files) - .map(function(file) { - return _.get(file, 'path', null); - }) - .filter() - .value(); - if (value.length === 0) { - value = ''; + props: { + inputValue: { + type: 'array', + required: true, + default: function() { + return []; + } + }, + removed: { + type: 'boolean', + required: true, + default: false } - return value; }, - setValue: function(value, skipValidation) { - if (!this.input) { - this.inputValue = value; - return; + derived: { + buttonTitle: { + deps: ['inputValue'], + fn: function() { + if (this.inputValue.length === 0) { + return 'Choose certificate(s)'; + } else if (this.inputValue.length === 1) { + return path.basename(this.inputValue[0]); + } + return format('%d files selected', this.inputValue.length); + } + }, + numSelectedFiles: { + deps: ['inputValue'], + fn: function() { + return this.inputValue.length; + } + } + }, + events: { + 'click [data-hook=load-file-button]': 'loadFileButtonClicked' + }, + bindings: { + buttonTitle: { + type: 'text', + hook: 'button-label' + }, + 'label': [ + { + hook: 'label' + }, + { + type: 'toggle', + hook: 'label' + } + ], + 'message': { + type: 'text', + hook: 'message-text' + }, + 'showMessage': { + type: 'toggle', + hook: 'message-container' } - this.input.value = ''; - /** - * Cannot set input value for file types. @see INT-780 - */ - // if (value || value === 0) { - // if (!_.isArray(value)) { - // value = [value]; - // } - // if (value.length <= 1) { - // this.input.value = path.basename(value); - // } else { - // this.input.value = 'multiple files'; - // } - // this.input.files = _.map(value, function(f) { - // return { - // name: path.basename(f), - // path: f - // }; - // }); - // } - this.inputValue = this.clean(); + }, + /** + * Set value to empty array in spec, instead of '', set appropriate + * invalidClass and validityClassSelector and always validate. + * @param {Object} spec the spec to set up this input view + */ + initialize: function(spec) { + spec = spec || {}; + _.defaults(spec, {value: []}); + this.invalidClass = 'has-error'; + this.validityClassSelector = '.form-item-file'; + InputView.prototype.initialize.call(this, spec); + }, + /** + * @todo (thomasr) + * Because ampersand-input-view still uses ampersand-view@8.x where + * the render/remove/render cycle doesn't correctly set up the bindings + * again, we need to re-initialize the bindings manually here. Once they + * upgrade to ampersand-view@9.x we can probably remove this entire + * render method. + * + * @return {Object} this + */ + render: function() { + this.renderWithTemplate(this); + this.input = this.queryByHook('load-file-button'); + if (this.removed) { + this._parsedBindings = bindings(this.bindings, this); + this._initializeBindings(); + this.removed = false; + } + this.setValue(this.inputValue, !this.required); + return this; + }, + /** + * Turn into no-op, as we don't work on input elements + * @see ampersand-input-view.js#handleTypeChange + */ + handleTypeChange: function() { + }, + /** + * Turn into identity, as we don't need to trim the value + * @param {Array} val the value to pass through + * @return {Array} return the unchanged value + */ + clean: function(val) { + return val; + }, + /** + * Turn into no-op, as we don't work on input elements + * @see ampersand-input-view.js#initInputBindings + */ + // initInputBindings: function() { + // }, + /** + * Only call ampersand-view's remove, we don't need to remove event listeners + * @see ampersand-input-view.js#remove + */ + remove: function() { + this.removed = true; + InputView.prototype.remove.apply(this, arguments); + }, + /** + * Not setting this.input.value here because our this.input is a div + * @param {Array} value the value to assign to this.inputValue + * @param {Boolean} skipValidation whether it should be validated or not + * @see ampersand-input-view.js#setValue + */ + setValue: function(value, skipValidation) { + this.inputValue = value; if (!skipValidation && !this.getErrorMessage()) { this.shouldValidate = true; } else if (skipValidation) { this.shouldValidate = false; } }, - handleChange: function() { - if (this.inputValue && this.changed) { - this.shouldValidate = true; + /** + * Need to change the value empty check to empty arrays instead + * @return {String} error message + * @see ampersand-input-view.js#getErrorMessage + */ + getErrorMessage: function() { + var message = ''; + if (this.required && this.value.length === 0) { + return this.requiredMessage; } - // for `file` type input fields, this is the only event and we need - // to set this.inputValue here again. - this.inputValue = this.clean(); + (this.tests || []).some(function(test) { + message = test.call(this, this.value) || ''; + return message; + }, this); + return message; + }, + /** + * Don't access this.input.value as we don't work on input elements + * @see ampersand-input-view.js#beforeSubmit + */ + beforeSubmit: function() { + // at the point where we've tried to submit, we want to validate + // everything from now on. + this.shouldValidate = true; this.runTests(); + }, + loadFileButtonClicked: function() { + dialog.showOpenDialog({ + properties: ['openFile', 'multiSelections'] + }, function(filenames) { + this.inputValue = filenames || []; + this.handleChange(); + }.bind(this)); } }); diff --git a/src/connect/index.jade b/src/connect/index.jade index 98161035939..fa7efcb1c6c 100644 --- a/src/connect/index.jade +++ b/src/connect/index.jade @@ -21,7 +21,9 @@ - if (!method.enabled) classNames.push('hidden') - if (i === 0) classNames.push('active') div(class=classNames, id='auth-#{method._id}') - + + hr + div(data-hook='sslselect-subview') - for method, i in sslMethods diff --git a/src/connect/index.js b/src/connect/index.js index ec17cab3ca0..f2a81c8dca5 100644 --- a/src/connect/index.js +++ b/src/connect/index.js @@ -95,7 +95,8 @@ var ConnectView = View.extend({ 'change input[name=name]': 'onNameInputChanged', 'input input': 'onAnyInputChanged', 'change input': 'onAnyInputChanged', - 'change select': 'onAnyInputChanged' + 'change select': 'onAnyInputChanged', + 'click div.btn': 'onAnyInputChanged' }, /** diff --git a/src/connect/index.less b/src/connect/index.less index 4759d671bbc..4c3e311f74b 100644 --- a/src/connect/index.less +++ b/src/connect/index.less @@ -139,12 +139,12 @@ overflow: auto; } label { - width: 32%; + width: 35%; text-align: right; padding: 5px 15px 0 0; } input, select, .form-item-file { - width: 68%; + width: 65%; } .form-item-file { input { diff --git a/src/connect/ssl.js b/src/connect/ssl.js index a1198c90456..b90de9ea9e8 100644 --- a/src/connect/ssl.js +++ b/src/connect/ssl.js @@ -39,9 +39,7 @@ var SERVER = { fields: [ new FileReaderView({ name: 'ssl_ca', - type: 'file', label: 'Certificate Authority', - placeholder: '', required: true }) ] @@ -58,31 +56,23 @@ var ALL = { fields: [ new FileReaderView({ name: 'ssl_ca', - type: 'file', label: 'Certificate Authority', - placeholder: '', required: true }), new FileReaderView({ name: 'ssl_private_key', - type: 'file', label: 'Certificate Key', - placeholder: '', required: true }), new FileReaderView({ name: 'ssl_certificate', - type: 'file', label: 'Certificate', - placeholder: '', required: true }), new InputView({ template: inputTemplate, name: 'ssl_private_key_password', - type: 'password', label: 'Private Key Password', - placeholder: '', required: false }) ]