From 728fee1a07bfa6ab6c7d85865ef7fe42d404df93 Mon Sep 17 00:00:00 2001 From: Lucas Hrabovsky Date: Thu, 24 Sep 2015 15:33:46 -0400 Subject: [PATCH 01/22] Adding feature flags for connect --- src/app.js | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/src/app.js b/src/app.js index a2eaaefd681..c2a8fe522c2 100644 --- a/src/app.js +++ b/src/app.js @@ -163,14 +163,30 @@ function start() { state.router = new Router(); domReady(state._onDOMReady.bind(state)); } +// @todo (imlucas): Feature flags can be overrideen +// via `window.localStorage`. +var FEATURES = { + querybuilder: true, + 'Connect with SSL': false, + 'Connect with Kerberos': true, + 'Connect with LDAP': false, + 'Connect with X.509': false +}; app.extend({ client: null, + // @note (imlucas): Backwards compat for querybuilder + features: FEATURES, + /** + * Check whether a feature flag is currently enabled. + * + * @param {String} id - A key in `FEATURES`. + * @return {Boolean} + */ + isFeatureEnabled: function(id) { + return FEATURES[id] === true; + }, init: function() { - // feature flags - this.features = { - querybuilder: true - }; state.statusbar = new Statusbar(); if (connection_id) { From 8ae50c6fbdd22c060582cf94c92b717248abd9e1 Mon Sep 17 00:00:00 2001 From: Lucas Hrabovsky Date: Thu, 24 Sep 2015 15:34:40 -0400 Subject: [PATCH 02/22] :construction: refactoring connect --- src/connect/connect-form-view.js | 28 +---- src/connect/index.jade | 83 ++++++------- src/connect/index.js | 201 +++++++++++++++++++++++++++---- src/connect/sidebar.js | 9 +- 4 files changed, 215 insertions(+), 106 deletions(-) diff --git a/src/connect/connect-form-view.js b/src/connect/connect-form-view.js index 77467fc6818..c5f70b1be77 100644 --- a/src/connect/connect-form-view.js +++ b/src/connect/connect-form-view.js @@ -18,33 +18,7 @@ var ConnectFormView = FormView.extend({ */ submitCallback: function(obj) { debug('form submitted', obj); - - var connection = new Connection(obj); - - var existingName = this.parent.checkExistingConnection(connection); - if (existingName) { - this.valid = false; - this.setValue('name', existingName); - return; - } - - existingName = this.parent.checkExistingName(connection); - if (existingName) { - this.valid = false; - return; - } - - app.statusbar.show(); - - debug('testing credentials are usable...'); - connection.test(function(err) { - app.statusbar.hide(); - if (err) { - this.parent.onConnectionError(err, connection); - return; - } - this.parent.onConnectionAccepted(connection); - }.bind(this)); + this.parent.onFormSubmitted(new Connection(obj)); }, clean: function(obj) { // clean up the form values here, e.g. conversion to numbers etc. diff --git a/src/connect/index.jade b/src/connect/index.jade index 738485ee84c..26ac37c7df2 100644 --- a/src/connect/index.jade +++ b/src/connect/index.jade @@ -2,9 +2,7 @@ .content.with-sidebar .container-fluid form.form-horizontal(data-hook='connect-form', style='margin-top: 20px;') - .message.alert(data-hook='message') - div(data-hook='hostname-subview') div(data-hook='port-subview') @@ -12,52 +10,43 @@ div(data-hook='saveas-subview') - hr - - .form-group - label.control-label - a(href="javascript:void(0);", data-hook='openAuth') - i - span(data-hook='open-auth-label') - - .form-group - div(data-hook='auth-container') - ul.nav.nav-tabs(role='tablist') - li.active(role='presentation') - a(href='#SCRAM-SHA-1', aria-controls='scram-sha-1', role='tab', data-toggle='tab', data-method='SCRAM-SHA-1') User/Password - //- li(role='presentation') - //- a(href='#MONGODB-CR', aria-controls='mongodb-cr', role='tab', data-toggle='tab', data-method='MONGODB-CR') MONGODB-CR - li(role='presentation') - a(href='#GSSAPI', aria-controls='kerberos', role='tab', data-toggle='tab', data-method='GSSAPI') Kerberos - //- li(role='presentation') - //- a(href='#MONGODB-X509', aria-controls='x509', role='tab', data-toggle='tab', data-method='MONGODB-X509') X.509 - //- li(role='presentation') - //- a(href='#PLAIN', aria-controls='ldap', role='tab', data-toggle='tab', data-method='PLAIN') LDAP - - div.tab-content - div.tab-pane.active(role='tabpanel', id='SCRAM-SHA-1') - //- div.tab-pane(role='tabpanel', id='MONGODB-CR') - //- h4 MONGODB-CR Authentication - div.tab-pane(role='tabpanel', id='GSSAPI') - //- div.tab-pane(role='tabpanel', id='MONGODB-X509') - //- h4 X.509 Authentication - //- div.tab-pane(role='tabpanel', id='PLAIN') - //- h4 LDAP Authentication - - hr - - .form-group - label.control-label - a(href="javascript:void(0);", data-hook='openSSL') - i - span(data-hook='open-ssl-label') - - div(data-hook='ssl-container') - h4 SSL Settings - + #connect-authentication-settings + hr + .form-group + label.control-label + a(href="javascript:void(0);", data-hook='openAuth') + i + span(data-hook='open-auth-label') + + .form-group + div(data-hook='auth-container') + ul.nav.nav-tabs + - for method, i in authMethods + - classNames = [] + - if (!method.enabled) classNames.push('hidden') + - if (i === 0) classNames.push('active') + li(class=classNames) + a(href="##{method._id}", data-toggle='tab', data-method=method._id)= method.title - hr - + .tab-content + - for method, i in authMethods + - classNames = [] + - if (!method.enabled) classNames.push('hidden') + - if (i === 0) classNames.push('active') + .tab-pane(class=classNames) + + #connect-ssl-settings(class=getFeatureClass('Connect with SSL')) + hr + .form-group + label.control-label + a(href="javascript:void(0);", data-hook='openSSL') + i + |   + span(data-hook='open-ssl-label') + + div(data-hook='ssl-container') + h4 SSL Settings + hr .form-group button.submit.btn.btn-primary(type='submit', data-hook='submit-button') Connect diff --git a/src/connect/index.js b/src/connect/index.js index 6742becb527..a0b533b7580 100644 --- a/src/connect/index.js +++ b/src/connect/index.js @@ -131,8 +131,10 @@ var ConnectView = View.extend({ this.connections.fetch(); }, /** - * Triggers when the user expands/collapses the auth section - * @param {Object} evt the click event + * Triggers when the user clicks the disclosure icon to expand/collapse + * the auth section. + * + * @param {MouseEvent} evt */ onOpenAuthClicked: function(evt) { evt.stopPropagation(); @@ -146,7 +148,8 @@ var ConnectView = View.extend({ }, /** * Triggers when the user expands/collapses the SSL section - * @param {Object} evt the click event + * + * @param {MouseEvent} evt - The click event */ onOpenSSLClicked: function(evt) { evt.stopPropagation(); @@ -172,7 +175,7 @@ var ConnectView = View.extend({ /** * Triggers when the user switches between auth tabs - * @param {Object} evt the click event + * @param {MouseEvent} evt - the click event */ onAuthTabClicked: function(evt) { this.authMethod = $(evt.target).data('method'); @@ -205,11 +208,11 @@ var ConnectView = View.extend({ }, /** - * checks if the connection already exists under a different name. Returns null if the + * Checks if the connection already exists under a different name. Returns null if the * connection doesn't exist yet, or the name of the connection, if it does. * - * @param {Object} connection The new connection to check - * @return {String|null} Name of the connection that is otherwise identical to obj + * @param {Object} connection - The new connection to check + * @return {String|null} - Name of the connection that is otherwise identical to obj */ checkExistingConnection: function(connection) { var existingConnection = this.connections.get(connection.uri); @@ -228,16 +231,15 @@ var ConnectView = View.extend({ }, /** - * checks if the connection name already exists but with different details. Returns true - * if the name already exists, or false otherwise. + * Checks if the connection name already exists but with different details. * - * @param {Object} connection The new connection to check - * @return {Boolean} Whether or not the connection name already exists + * @param {Connection} model - A connection to check. + * @return {Boolean} - Whether or not the connection name already exists. */ - checkExistingName: function(connection) { - var existingConnection = this.connections.get(connection.name, 'name'); + checkExistingName: function(model) { + var existingConnection = this.connections.get(model.name, 'name'); - if (connection.name !== '' + if (model.name !== '' && existingConnection && existingConnection.uri !== connection.uri) { app.statusbar.hide(); @@ -249,29 +251,167 @@ var ConnectView = View.extend({ } return false; }, - /* (err, model) */ - onConnectionError: function() { - this.message = 'Could not connect to MongoDB. ' - + 'Please double check your info.'; + /** + * Use a connection to view schemas, such as after + * submitting a form or when double-clicking on + * a list item like in `./sidebar`. + * + * @param {Connection} model + * @param {Object} [options] + * @option {Boolean} close - Close the connect dialog on success [Default: `false`]. + * @api public + */ + connect: function(model, options) { + app.statusbar.show(); + + debug('testing credentials are usable...'); + model.test(function(err) { + app.statusbar.hide(); + if (err) { + debug('failed to connect', err); + this.onError(new Error('Could not connect to MongoDB.'), model); + return; + } + this.onConnectionSuccessful(model, options); + }.bind(this)); }, - onConnectionAccepted: function(model) { - // save connection if a name was provided - if (model.name !== '') { - model.save(); + /** + * If the connection is useable, save/update it in the + * store and open a new window that will show the schema + * view using it. + * + * @param {Connection} model + * @param {Object} [options] + * @api private + */ + onConnectionSuccessful: function(model, options) { + /** + * The save method will handle calling the correct method + * of the sync being used by the model, whether that's + * `create` or `update`. + * + * @see http://ampersandjs.com/docs#ampersand-model-save + */ + model.save(); + /** + * @todo (imlucas): So we can see what auth mechanisms + * and accoutrement people are actually using IRL. + * + * metrics.trackEvent('connect success', { + * auth_mechanism: model.auth_mechanism, + * ssl: model.ssl + * }); + */ + if (model.isNew()) { + debug('yay! its a new successful connection!'); this.connections.add(model); } - // connect - debug('all good, connecting:', model.serialize()); + debug('opening schema view for', model.serialize()); window.open(format('%s?connection_id=%s#schema', window.location.origin, model.getId())); setTimeout(this.set.bind(this, { message: '' }), 500); - setTimeout(window.close, 1000); + + if (options.close) { + setTimeout(window.close, 1000); + } + }, + /** + * If there is a validation or connection error show a nice message. + * + * @param {Error} err + * @param {Connection} model + * @api private + */ + onError: function(err, model) { + // @todo (imlucas): `metrics.trackEvent('connect error', auth_mechanism + ssl boolean)` + debug('showing error message', { + err: err, + model: model + }); + this.message = err.message; + this.has_error = true; }, + /** + * When the form is submitted, validate the resulting model + * and then connect using it. + * + * @param {Connection} model + * @api private + */ + onFormSubmitted: function(model) { + this.reset(); + if (!model.isValid()) { + this.onError(model.validationError); + } + // @todo: Check if already exists. + this.connect(model); + }, + /** + * Update the form's state based on an existing + * connection, e.g. clicking on a list item + * like in `./sidebar.js`. + * + * @param {Connection} model + * @api public + */ + onConnectionSelected: function(model) { + // If the new model has auth, expand the auth options container + // and select the correct tab. + this.authMethod = model.auth_mechanism; + + if (model.auth_mechanism !== null) { + this.authOpen = true; + } else { + this.authOpen = false; + } + + // Changing `this.authMethod` dynamically updates the + // fields in the form because it's a top-level constraint + // so we need to get a list of what keys are currently + // available to set. + var fields = _.flatten('name', 'port', 'hostname', + authFields[this.authMethod] || []); + debug('Populating form fields', fields); + + // Populates the form from values in the model. + this.form.setValues(_.pick(model, fields)); + }, render: function() { - this.renderWithTemplate(); + // @todo (imlucas): Consolidate w/ `./auth-fields.js`. + var authMethods = [ + { + _id: 'SCRAM-SHA-1', + title: 'User/Password', + enabled: true + }, + { + _id: 'GSSAPI', + title: 'Kerberos', + enabled: app.isFeatureEnabled('Connect with Kerberos') + }, + { + _id: 'PLAIN', + title: 'LDAP', + enabled: app.isFeatureEnabled('Connect with LDAP') + }, + { + _id: 'MONGODB-X509', + title: 'X.509', + enabled: app.isFeatureEnabled('Connect with X.509') + } + ]; + this.renderWithTemplate({ + authMethods: authMethods, + getFeatureClass: function getFeatureClass(feature_id) { + if (!app.isFeatureEnabled(feature_id)) { + return ['hidden']; + } + } + }); + this.form = new ConnectFormView({ parent: this, el: this.queryByHook('connect-form'), @@ -290,6 +430,15 @@ var ConnectView = View.extend({ trigger: 'hover' }); }, + /** + * Return to a clean state between form submissions. + * + * @api private + */ + reset: function() { + this.message = ''; + this.has_error = false; + }, subviews: { sidebar: { waitFor: 'connections', diff --git a/src/connect/sidebar.js b/src/connect/sidebar.js index d0091e5a5e4..e337f8b6c5c 100644 --- a/src/connect/sidebar.js +++ b/src/connect/sidebar.js @@ -52,7 +52,7 @@ var SidebarItemView = View.extend({ /** - * Renders all existing connections in the sidebar. + * Renders all existing connections as list in the sidebar. */ var SidebarView = View.extend({ namespace: 'SidebarView', @@ -64,15 +64,12 @@ var SidebarView = View.extend({ onItemClick: function(event, model) { event.stopPropagation(); event.preventDefault(); - - // fill in the form with the clicked connection details - this.parent.form.setValues(model.serialize()); + this.parent.onConnection(model); }, onItemDoubleClick: function(event, model) { this.onItemClick(event, model); - this.parent.form.onSubmit(event); + this.parent.connect(model); } }); - module.exports = SidebarView; From f67c05b217e947c975adc4c3da8b07e654022c8f Mon Sep 17 00:00:00 2001 From: Lucas Hrabovsky Date: Thu, 24 Sep 2015 16:19:29 -0400 Subject: [PATCH 03/22] make things work --- src/connect/index.jade | 2 +- src/connect/index.js | 5 +++-- src/connect/sidebar.js | 2 +- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/connect/index.jade b/src/connect/index.jade index 26ac37c7df2..0ffbb791b63 100644 --- a/src/connect/index.jade +++ b/src/connect/index.jade @@ -33,7 +33,7 @@ - classNames = [] - if (!method.enabled) classNames.push('hidden') - if (i === 0) classNames.push('active') - .tab-pane(class=classNames) + .tab-pane(class=classNames, id=method._id) #connect-ssl-settings(class=getFeatureClass('Connect with SSL')) hr diff --git a/src/connect/index.js b/src/connect/index.js index a0b533b7580..f51e4a3c391 100644 --- a/src/connect/index.js +++ b/src/connect/index.js @@ -372,12 +372,13 @@ var ConnectView = View.extend({ // fields in the form because it's a top-level constraint // so we need to get a list of what keys are currently // available to set. - var fields = _.flatten('name', 'port', 'hostname', + var fields = _.flatten(['name', 'port', 'hostname'], authFields[this.authMethod] || []); debug('Populating form fields', fields); + var values = _.pick(model, fields); // Populates the form from values in the model. - this.form.setValues(_.pick(model, fields)); + this.form.setValues(values); }, render: function() { // @todo (imlucas): Consolidate w/ `./auth-fields.js`. diff --git a/src/connect/sidebar.js b/src/connect/sidebar.js index e337f8b6c5c..c76ef1b0575 100644 --- a/src/connect/sidebar.js +++ b/src/connect/sidebar.js @@ -64,7 +64,7 @@ var SidebarView = View.extend({ onItemClick: function(event, model) { event.stopPropagation(); event.preventDefault(); - this.parent.onConnection(model); + this.parent.onConnectionSelected(model); }, onItemDoubleClick: function(event, model) { this.onItemClick(event, model); From 9fff452fcc610f429cd341b2bb1cbbf1aa019677 Mon Sep 17 00:00:00 2001 From: Lucas Hrabovsky Date: Thu, 24 Sep 2015 16:49:47 -0400 Subject: [PATCH 04/22] implement untitled --- src/app.js | 42 ++++++++-------- src/connect/index.js | 76 ++++++++++------------------- src/models/connection-collection.js | 1 - 3 files changed, 48 insertions(+), 71 deletions(-) diff --git a/src/app.js b/src/app.js index c2a8fe522c2..8748564e76b 100644 --- a/src/app.js +++ b/src/app.js @@ -161,7 +161,7 @@ var Statusbar = require('./statusbar'); function start() { state.router = new Router(); - domReady(state._onDOMReady.bind(state)); + state._onDOMReady(); } // @todo (imlucas): Feature flags can be overrideen // via `window.localStorage`. @@ -187,29 +187,31 @@ app.extend({ return FEATURES[id] === true; }, init: function() { - state.statusbar = new Statusbar(); + domReady(function() { + state.statusbar = new Statusbar(); - if (connection_id) { - state.connection = new Connection({ - _id: connection_id - }); + if (connection_id) { + state.connection = new Connection({ + _id: connection_id + }); - debug('looking up connection `%s`...', connection_id); - state.connection.fetch({ - success: function() { - debug('got connection `%j`...', state.connection.serialize()); - app.client = getOrCreateClient(app.endpoint, state.connection.serialize()); + debug('looking up connection `%s`...', connection_id); + state.connection.fetch({ + success: function() { + debug('got connection `%j`...', state.connection.serialize()); + app.client = getOrCreateClient(app.endpoint, state.connection.serialize()); - state.queryOptions = new QueryOptions(); - state.volatileQueryOptions = new QueryOptions(); - state.instance = new MongoDBInstance(); - start(); - } - }); - } else { - start(); - } + state.queryOptions = new QueryOptions(); + state.volatileQueryOptions = new QueryOptions(); + state.instance = new MongoDBInstance(); + start(); + } + }); + } else { + start(); + } + }); // set up ipc ipc.on('message', state.onMessageReceived.bind(this)); }, diff --git a/src/connect/index.js b/src/connect/index.js index f51e4a3c391..e4bd19c2fe6 100644 --- a/src/connect/index.js +++ b/src/connect/index.js @@ -206,51 +206,6 @@ var ConnectView = View.extend({ this.previousAuthMethod = this.authMethod; debug('form data now has the following fields', Object.keys(this.form.data)); }, - - /** - * Checks if the connection already exists under a different name. Returns null if the - * connection doesn't exist yet, or the name of the connection, if it does. - * - * @param {Object} connection - The new connection to check - * @return {String|null} - Name of the connection that is otherwise identical to obj - */ - checkExistingConnection: function(connection) { - var existingConnection = this.connections.get(connection.uri); - - if (connection.name !== '' - && existingConnection - && connection.name !== existingConnection.name) { - app.statusbar.hide(); - this.has_error = true; - this.message = format('This connection already exists under the name "%s". ' - + 'Click "Connect" again to use that connection.', - existingConnection.name); - return existingConnection.name; - } - return null; - }, - - /** - * Checks if the connection name already exists but with different details. - * - * @param {Connection} model - A connection to check. - * @return {Boolean} - Whether or not the connection name already exists. - */ - checkExistingName: function(model) { - var existingConnection = this.connections.get(model.name, 'name'); - - if (model.name !== '' - && existingConnection - && existingConnection.uri !== connection.uri) { - app.statusbar.hide(); - this.has_error = true; - this.message = format('Another connection with the name "%s" already exists. Please ' - + 'delete the existing connection first or choose a different name.', - existingConnection.name); - return true; - } - return false; - }, /** * Use a connection to view schemas, such as after * submitting a form or when double-clicking on @@ -262,6 +217,10 @@ var ConnectView = View.extend({ * @api public */ connect: function(model, options) { + options = _.defaults(options, { + close: false + }); + app.statusbar.show(); debug('testing credentials are usable...'); @@ -285,6 +244,9 @@ var ConnectView = View.extend({ * @api private */ onConnectionSuccessful: function(model, options) { + options = _.defaults(options, { + close: false + }); /** * The save method will handle calling the correct method * of the sync being used by the model, whether that's @@ -302,10 +264,7 @@ var ConnectView = View.extend({ * ssl: model.ssl * }); */ - if (model.isNew()) { - debug('yay! its a new successful connection!'); - this.connections.add(model); - } + this.connections.add(model); debug('opening schema view for', model.serialize()); window.open(format('%s?connection_id=%s#schema', window.location.origin, model.getId())); @@ -342,11 +301,28 @@ var ConnectView = View.extend({ */ onFormSubmitted: function(model) { this.reset(); + + if (!model.name) { + // If no name specified, the connection name + // will be `Untitled (1)`. If there are existing + // `Untitled (\d)` connections, increment a counter + // on them like every MS Office does. + var untitleds = _.chain(this.connections.models) + .filter(function(model) { + return _.startsWith(model.name, 'Untitled ('); + }) + .sort('name') + .value(); + + model.name = format('Untitled (%d)', untitleds.length + 1); + } + + // @todo (imlucas): Dont allow duplicate names? + if (!model.isValid()) { this.onError(model.validationError); } - // @todo: Check if already exists. this.connect(model); }, /** diff --git a/src/models/connection-collection.js b/src/models/connection-collection.js index 0ca84609a37..4af0c2c2709 100644 --- a/src/models/connection-collection.js +++ b/src/models/connection-collection.js @@ -9,6 +9,5 @@ module.exports = Collection.extend(lodashMixin, restMixin, { model: Connection, comparator: 'last_used', mainIndex: '_id', - indexes: ['name'], sync: connectionSync }); From 42099c16e9f8bfe286284e2e8f39a7eebbeaaf47 Mon Sep 17 00:00:00 2001 From: Lucas Hrabovsky Date: Fri, 25 Sep 2015 09:34:37 -0400 Subject: [PATCH 05/22] open devtools detached when in dev mode for all new windows --- src/electron/window-manager.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/electron/window-manager.js b/src/electron/window-manager.js index 5ceec3a5e4c..618aeeb06c3 100644 --- a/src/electron/window-manager.js +++ b/src/electron/window-manager.js @@ -115,6 +115,14 @@ module.exports.create = function(opts) { app.quit(); } }); + + // When in dev mode, automaticaly open devtools + // detached for ease of debugging. + if (process.env.NODE_ENV === 'development') { + _window.openDevTools({ + detach: true + }); + } return _window; }; From eb09506fbbd3afc45d9eb5995fe086be1a10eae6 Mon Sep 17 00:00:00 2001 From: Lucas Hrabovsky Date: Fri, 25 Sep 2015 09:35:27 -0400 Subject: [PATCH 06/22] kernel 2.6.x doesn't support SCRAM so try again w/ CR --- src/connect/index.js | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/src/connect/index.js b/src/connect/index.js index e4bd19c2fe6..460ad25f710 100644 --- a/src/connect/index.js +++ b/src/connect/index.js @@ -217,7 +217,7 @@ var ConnectView = View.extend({ * @api public */ connect: function(model, options) { - options = _.defaults(options, { + options = _.defaults(options || {}, { close: false }); @@ -226,12 +226,30 @@ var ConnectView = View.extend({ debug('testing credentials are usable...'); model.test(function(err) { app.statusbar.hide(); - if (err) { + if (!err) { + this.onConnectionSuccessful(model, options); + return; + } + + if (model.auth_mechanism !== 'SCRAM-SHA-1') { debug('failed to connect', err); this.onError(new Error('Could not connect to MongoDB.'), model); return; } - this.onConnectionSuccessful(model, options); + + // For Kernel 2.6.x + model.auth_mechanism = 'MONGODB-CR'; + debug('trying again w/ MONGODB-CR...'); + app.statusbar.show(); + + model.test(function(err) { + if (err) { + debug('failed to connect again... bailing', err); + this.onError(new Error('Could not connect to MongoDB.'), model); + return; + } + this.onConnectionSuccessful(model, options); + }.bind(this)); }.bind(this)); }, /** From defdbf9a83d3f457072031a98a21c5d84342910a Mon Sep 17 00:00:00 2001 From: Lucas Hrabovsky Date: Fri, 25 Sep 2015 10:21:34 -0400 Subject: [PATCH 07/22] INT-635: resize connect window with auth settings visibility toggled --- src/connect/index.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/connect/index.js b/src/connect/index.js index 460ad25f710..f9659af208b 100644 --- a/src/connect/index.js +++ b/src/connect/index.js @@ -87,6 +87,15 @@ var ConnectView = View.extend({ selector: '[data-hook=openAuth] > i', yes: 'caret', no: 'caret-right' + }, + { + type: function(el, authOpen) { + if (authOpen) { + window.resizeTo(window.outerWidth, 630); + } else { + window.resizeTo(window.outerWidth, 410); + } + } } ], sslOpen: [ From c967fd203a25d7565f9583420f5df3f5650290c3 Mon Sep 17 00:00:00 2001 From: Lucas Hrabovsky Date: Fri, 25 Sep 2015 10:22:18 -0400 Subject: [PATCH 08/22] update connect copy + icon spacing --- src/connect/connect-form-view.js | 2 +- src/connect/index.jade | 5 ++--- src/connect/input-saveas.jade | 5 ++++- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/connect/connect-form-view.js b/src/connect/connect-form-view.js index c5f70b1be77..086386eb8bb 100644 --- a/src/connect/connect-form-view.js +++ b/src/connect/connect-form-view.js @@ -84,7 +84,7 @@ var ConnectFormView = FormView.extend({ template: require('./input-saveas.jade'), el: this.parent.queryByHook('saveas-subview'), name: 'name', - placeholder: 'Connection Name', + placeholder: 'e.g. Shared Dev, Stats Box, PRODUCTION', required: false }) ]; diff --git a/src/connect/index.jade b/src/connect/index.jade index 0ffbb791b63..330489b5912 100644 --- a/src/connect/index.jade +++ b/src/connect/index.jade @@ -16,7 +16,7 @@ label.control-label a(href="javascript:void(0);", data-hook='openAuth') i - span(data-hook='open-auth-label') + span(style='padding-left: 4px;', data-hook='open-auth-label') .form-group div(data-hook='auth-container') @@ -41,8 +41,7 @@ label.control-label a(href="javascript:void(0);", data-hook='openSSL') i - |   - span(data-hook='open-ssl-label') + span(style='padding-left: 4px;', data-hook='open-ssl-label') div(data-hook='ssl-container') h4 SSL Settings diff --git a/src/connect/input-saveas.jade b/src/connect/input-saveas.jade index 74e7443b40f..503837ac016 100644 --- a/src/connect/input-saveas.jade +++ b/src/connect/input-saveas.jade @@ -1,7 +1,10 @@ .form-group label.control-label span Save As... (optional)    - i.fa.fa-info-circle(tabindex="0", role="button", data-toggle="popover", data-trigger="focus", data-content="You can choose a friendly connection name for this connection, for example \"Development Server\". If you enter a name here, the connection will be saved and appears on the left side.") + i.fa.fa-info-circle( + tabindex="0", role="button", + data-toggle="popover", data-trigger="focus", + data-content="You can choose a friendly connection name for this connection to make it easier to keep track of what you\'ve connected to recently and stay organized.") input.form-control div.message.message-below.message-error(data-hook='message-container') p(data-hook='message-text') From e6f744b4ee804deed556170ae977e10febe69c1e Mon Sep 17 00:00:00 2001 From: Lucas Hrabovsky Date: Fri, 25 Sep 2015 10:22:44 -0400 Subject: [PATCH 09/22] fix duplicate connections being added to sidebar --- src/connect/index.js | 9 ++++++--- src/electron/window-manager.js | 13 +++++++------ src/models/connection.js | 4 +--- 3 files changed, 14 insertions(+), 12 deletions(-) diff --git a/src/connect/index.js b/src/connect/index.js index f9659af208b..35f1e171fdb 100644 --- a/src/connect/index.js +++ b/src/connect/index.js @@ -272,7 +272,7 @@ var ConnectView = View.extend({ */ onConnectionSuccessful: function(model, options) { options = _.defaults(options, { - close: false + close: true }); /** * The save method will handle calling the correct method @@ -281,6 +281,7 @@ var ConnectView = View.extend({ * * @see http://ampersandjs.com/docs#ampersand-model-save */ + model.last_used = new Date(); model.save(); /** * @todo (imlucas): So we can see what auth mechanisms @@ -291,7 +292,9 @@ var ConnectView = View.extend({ * ssl: model.ssl * }); */ - this.connections.add(model); + this.connections.add(model, { + merge: true + }); debug('opening schema view for', model.serialize()); window.open(format('%s?connection_id=%s#schema', window.location.origin, model.getId())); @@ -329,7 +332,7 @@ var ConnectView = View.extend({ onFormSubmitted: function(model) { this.reset(); - if (!model.name) { + if (_.trim(model.name) === '') { // If no name specified, the connection name // will be `Untitled (1)`. If there are existing // `Untitled (\d)` connections, increment a counter diff --git a/src/electron/window-manager.js b/src/electron/window-manager.js index 618aeeb06c3..27f6dd8b541 100644 --- a/src/electron/window-manager.js +++ b/src/electron/window-manager.js @@ -32,7 +32,7 @@ var DEFAULT_HEIGHT = 700; * windows like the connection and setup dialogs. */ var DEFAULT_WIDTH_DIALOG = 640; -var DEFAULT_HEIGHT_DIALOG = 600; +var DEFAULT_HEIGHT_DIALOG = 470; /** * Adjust the heights to account for platforms * that use a single menu bar at the top of the screen. @@ -116,13 +116,14 @@ module.exports.create = function(opts) { } }); + // @todo (imlucas) // When in dev mode, automaticaly open devtools // detached for ease of debugging. - if (process.env.NODE_ENV === 'development') { - _window.openDevTools({ - detach: true - }); - } + // if (process.env.NODE_ENV === 'development') { + // _window.openDevTools({ + // detach: true + // }); + // } return _window; }; diff --git a/src/models/connection.js b/src/models/connection.js index 013bf8f436a..ec5217fefb9 100644 --- a/src/models/connection.js +++ b/src/models/connection.js @@ -15,9 +15,7 @@ module.exports = Connection.extend({ default: function() { return uuid.v4(); } - } - }, - session: { + }, /** * Updated on each successful connection to the Deployment. */ From 68295bddf854139305a31f7749ce44404d288ac8 Mon Sep 17 00:00:00 2001 From: Lucas Hrabovsky Date: Fri, 25 Sep 2015 10:42:46 -0400 Subject: [PATCH 10/22] :bug: Populate auth fields when using a connection --- src/connect/index.js | 15 +++++++++------ src/connect/sidebar.jade | 1 - 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/src/connect/index.js b/src/connect/index.js index 35f1e171fdb..056a42b6b98 100644 --- a/src/connect/index.js +++ b/src/connect/index.js @@ -198,14 +198,14 @@ var ConnectView = View.extend({ // remove and unregister old fields var oldFields = authFields[this.previousAuthMethod]; - // debug('removing fields:', _.pluck(oldFields, 'name')); + debug('removing fields:', _.pluck(oldFields, 'name')); _.each(oldFields, function(field) { this.form.removeField(field.name); }.bind(this)); // register new with form, render, append to DOM var newFields = authFields[this.authMethod]; - // debug('adding fields:', _.pluck(newFields, 'name')); + debug('adding fields:', _.pluck(newFields, 'name')); _.each(newFields, function(field) { this.form.addField(field.render()); @@ -378,10 +378,13 @@ var ConnectView = View.extend({ // fields in the form because it's a top-level constraint // so we need to get a list of what keys are currently // available to set. - var fields = _.flatten(['name', 'port', 'hostname'], - authFields[this.authMethod] || []); - debug('Populating form fields', fields); - var values = _.pick(model, fields); + var keys = ['name', 'port', 'hostname']; + if (model.auth_mechanism) { + keys.push.apply(keys, _.pluck(authFields[this.authMethod], 'name')); + } + + debug('Populating form fields with keys', keys); + var values = _.pick(model, keys); // Populates the form from values in the model. this.form.setValues(values); diff --git a/src/connect/sidebar.jade b/src/connect/sidebar.jade index e6d01e59238..c6998ea7941 100644 --- a/src/connect/sidebar.jade +++ b/src/connect/sidebar.jade @@ -3,5 +3,4 @@ div .panel-heading(style='padding: 10px;') .panel-title Saved Connections ul.list-group(data-hook='connection-list', style='top: 32px;') - .sidebar-bg From 00fd9d44e367b0712ce418fd10f5a41e8d05516a Mon Sep 17 00:00:00 2001 From: Lucas Hrabovsky Date: Fri, 25 Sep 2015 12:36:52 -0400 Subject: [PATCH 11/22] :arrow_up: mongodb-connection-model@0.0.3 + use DEFAULT `auth_mechanism` @see https://github.com/mongodb/node-mongodb-native/pull/1299 --- package.json | 2 +- src/connect/auth-fields.js | 8 +------- src/connect/index.js | 26 ++++---------------------- 3 files changed, 6 insertions(+), 30 deletions(-) diff --git a/package.json b/package.json index 7eb590db86a..14d13a0e065 100644 --- a/package.json +++ b/package.json @@ -69,7 +69,7 @@ "ampersand-input-view": "^5.0.0", "debug": "^2.2.0", "electron-squirrel-startup": "^0.1.2", - "mongodb-connection-model": "0.0.2", + "mongodb-connection-model": "^0.0.3", "scout-server": "http://bin.mongodb.org/js/scout-server/v0.2.1/scout-server-0.2.1.tar.gz" }, "devDependencies": { diff --git a/src/connect/auth-fields.js b/src/connect/auth-fields.js index 95a8ef647f8..477dfff95e4 100644 --- a/src/connect/auth-fields.js +++ b/src/connect/auth-fields.js @@ -62,13 +62,7 @@ var service_name = new InputView({ * Define fields for each auth method here */ module.exports = { - 'SCRAM-SHA-1': [ - username, - password, - database_name - ], - - 'MONGODB-CR': [ + DEFAULT: [ username, password, database_name diff --git a/src/connect/index.js b/src/connect/index.js index 056a42b6b98..ca69dd82397 100644 --- a/src/connect/index.js +++ b/src/connect/index.js @@ -150,7 +150,7 @@ var ConnectView = View.extend({ evt.preventDefault(); this.toggle('authOpen'); if (this.authOpen) { - this.authMethod = this.previousAuthMethod || 'SCRAM-SHA-1'; + this.authMethod = this.previousAuthMethod || 'DEFAULT'; } else { this.authMethod = null; } @@ -235,30 +235,12 @@ var ConnectView = View.extend({ debug('testing credentials are usable...'); model.test(function(err) { app.statusbar.hide(); - if (!err) { - this.onConnectionSuccessful(model, options); - return; - } - - if (model.auth_mechanism !== 'SCRAM-SHA-1') { + if (err) { debug('failed to connect', err); this.onError(new Error('Could not connect to MongoDB.'), model); return; } - - // For Kernel 2.6.x - model.auth_mechanism = 'MONGODB-CR'; - debug('trying again w/ MONGODB-CR...'); - app.statusbar.show(); - - model.test(function(err) { - if (err) { - debug('failed to connect again... bailing', err); - this.onError(new Error('Could not connect to MongoDB.'), model); - return; - } - this.onConnectionSuccessful(model, options); - }.bind(this)); + this.onConnectionSuccessful(model, options); }.bind(this)); }, /** @@ -393,7 +375,7 @@ var ConnectView = View.extend({ // @todo (imlucas): Consolidate w/ `./auth-fields.js`. var authMethods = [ { - _id: 'SCRAM-SHA-1', + _id: 'DEFAULT', title: 'User/Password', enabled: true }, From 91f6c7344984f45885706a1ea58587c1c2665de8 Mon Sep 17 00:00:00 2001 From: Lucas Hrabovsky Date: Fri, 25 Sep 2015 12:57:07 -0400 Subject: [PATCH 12/22] :arrow_up: scout-server@0.2.2 scout-client@0.1.2 --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 14d13a0e065..70dde593384 100644 --- a/package.json +++ b/package.json @@ -70,7 +70,7 @@ "debug": "^2.2.0", "electron-squirrel-startup": "^0.1.2", "mongodb-connection-model": "^0.0.3", - "scout-server": "http://bin.mongodb.org/js/scout-server/v0.2.1/scout-server-0.2.1.tar.gz" + "scout-server": "http://bin.mongodb.org/js/scout-server/v0.2.2/scout-server-0.2.2.tar.gz" }, "devDependencies": { "ampersand-app": "^1.0.4", @@ -131,7 +131,7 @@ "run-sequence": "^1.1.2", "run-series": "^1.1.2", "scout-brain": "http://bin.mongodb.org/js/scout-brain/v0.0.2/scout-brain-0.0.2.tar.gz", - "scout-client": "http://bin.mongodb.org/js/scout-client/v0.1.2/scout-client-0.1.2.tar.gz", + "scout-client": "http://bin.mongodb.org/js/scout-client/v0.1.3/scout-client-0.1.3.tar.gz", "stream-combiner2": "^1.0.2", "uuid": "^2.0.1", "vinyl-buffer": "^1.0.0", From 5aa592f1ff79fab0084bf35245679866414f37ec Mon Sep 17 00:00:00 2001 From: Lucas Hrabovsky Date: Fri, 25 Sep 2015 16:21:10 -0400 Subject: [PATCH 13/22] :fire: replace scout-brain with new mongodb-*-*models --- package.json | 6 +++++- src/home/collection.js | 2 +- src/home/index.js | 3 ++- src/models/mongodb-collection.js | 18 ++++++++++++------ src/models/mongodb-instance.js | 14 +++++++------- src/models/sampled-document-collection.js | 18 ++++++++++++++++-- src/models/types.js | 7 ------- 7 files changed, 43 insertions(+), 25 deletions(-) delete mode 100644 src/models/types.js diff --git a/package.json b/package.json index 70dde593384..997d1871ba1 100644 --- a/package.json +++ b/package.json @@ -67,9 +67,14 @@ "dependencies": { "ampersand-form-view": "^5.1.1", "ampersand-input-view": "^5.0.0", + "ampersand-rest-collection": "^5.0.0", "debug": "^2.2.0", "electron-squirrel-startup": "^0.1.2", + "mongodb-collection-model": "^0.1.0", "mongodb-connection-model": "^0.0.3", + "mongodb-database-model": "^0.1.0", + "mongodb-instance-model": "^0.2.0", + "mongodb-ns": "^1.0.0", "scout-server": "http://bin.mongodb.org/js/scout-server/v0.2.2/scout-server-0.2.2.tar.gz" }, "devDependencies": { @@ -130,7 +135,6 @@ "raf": "^3.0.0", "run-sequence": "^1.1.2", "run-series": "^1.1.2", - "scout-brain": "http://bin.mongodb.org/js/scout-brain/v0.0.2/scout-brain-0.0.2.tar.gz", "scout-client": "http://bin.mongodb.org/js/scout-client/v0.1.3/scout-client-0.1.3.tar.gz", "stream-combiner2": "^1.0.2", "uuid": "^2.0.1", diff --git a/src/home/collection.js b/src/home/collection.js index 5f6fb83e7b9..69cdac8bab6 100644 --- a/src/home/collection.js +++ b/src/home/collection.js @@ -92,7 +92,7 @@ var MongoDBCollectionView = View.extend({ var ns = this.parent.ns; if (!ns) { this.visible = false; - debug('not updating because parent has no collection namespace.'); + debug('No active collection namespace so no schema has been requested yet.'); return; } this.visible = true; diff --git a/src/home/index.js b/src/home/index.js index 66882d383de..4e00d7a9363 100644 --- a/src/home/index.js +++ b/src/home/index.js @@ -1,9 +1,9 @@ var View = require('ampersand-view'); var format = require('util').format; var SidebarView = require('../sidebar'); -var debug = require('debug')('scout-ui:home'); var CollectionView = require('./collection'); var app = require('ampersand-app'); +var debug = require('debug')('scout:home'); var HomeView = View.extend({ props: { @@ -32,6 +32,7 @@ var HomeView = View.extend({ this.listenTo(app.instance, 'sync', this.onInstanceFetched); this.listenTo(app.connection, 'change:name', this.updateTitle); this.once('change:rendered', this.onRendered); + debug('fetching instance model...'); app.instance.fetch(); }, onInstanceFetched: function() { diff --git a/src/models/mongodb-collection.js b/src/models/mongodb-collection.js index 9f7075e885b..2789396c9b1 100644 --- a/src/models/mongodb-collection.js +++ b/src/models/mongodb-collection.js @@ -1,13 +1,13 @@ -var MongoDBCollection = require('scout-brain').models.Collection; -var types = require('./types'); +var MongoDBCollection = require('mongodb-collection-model'); +var toNS = require('mongodb-ns'); var scoutClientMixin = require('./scout-client-mixin'); var format = require('util').format; /** * Metadata for a MongoDB Collection. - * @see https://github.com/10gen/scout/blob/dev/scout-brain/lib/models/collection.js + * @see http://npm.im/mongodb-collection-model */ -module.exports = MongoDBCollection.extend(scoutClientMixin, { +var CollectionModel = MongoDBCollection.extend(scoutClientMixin, { namespace: 'MongoDBCollection', session: { selected: { @@ -19,14 +19,14 @@ module.exports = MongoDBCollection.extend(scoutClientMixin, { name: { deps: ['_id'], fn: function() { - return types.ns(this._id).collection; + return toNS(this._id).collection; } }, specialish: { name: { deps: ['_id'], fn: function() { - return types.ns(this._id).specialish; + return toNS(this._id).specialish; } } }, @@ -43,3 +43,9 @@ module.exports = MongoDBCollection.extend(scoutClientMixin, { }, true); } }); + +module.exports = CollectionModel; + +module.exports.Collection = MongoDBCollection.Collection.extend({ + model: CollectionModel +}); diff --git a/src/models/mongodb-instance.js b/src/models/mongodb-instance.js index 2a4717f2cd3..b7bb45f313f 100644 --- a/src/models/mongodb-instance.js +++ b/src/models/mongodb-instance.js @@ -1,20 +1,19 @@ -var MongoDBInstance = require('scout-brain').models.Instance; -var MongoDBCollectionCollection = require('scout-brain').models.CollectionCollection; +var MongoDBInstance = require('mongodb-instance-model'); var MongoDBCollection = require('./mongodb-collection'); var scoutClientMixin = require('./scout-client-mixin'); var selectableMixin = require('./selectable-collection-mixin'); -var types = require('./types'); +var toNS = require('mongodb-ns'); /** * A user selectable collection of `MongoDBCollection`'s with `specialish` * collections filtered out. */ -var MongoDBCollectionOnInstanceCollection = MongoDBCollectionCollection.extend(selectableMixin, { +var MongoDBCollectionOnInstanceCollection = MongoDBCollection.Collection.extend(selectableMixin, { namespace: 'MongoDBCollectionOnInstanceCollection', model: MongoDBCollection, parse: function(res) { return res.filter(function(d) { - return !types.ns(d._id).specialish; + return !toNS(d._id).specialish; }); } }); @@ -22,11 +21,12 @@ var MongoDBCollectionOnInstanceCollection = MongoDBCollectionCollection.extend(s /** * Metadata for a MongoDB Instance, such as a `db.hostInfo()`, `db.listDatabases()`, * `db.buildInfo()`, and more. - * @see https://github.com/10gen/scout/blob/dev/scout-brain/lib/models/instance.js + * + * @see http://npm.im/mongodb-instance-model */ module.exports = MongoDBInstance.extend(scoutClientMixin, { namespace: 'MongoDBInstance', - children: { + collections: { collections: MongoDBCollectionOnInstanceCollection }, url: '/instance' diff --git a/src/models/sampled-document-collection.js b/src/models/sampled-document-collection.js index 2a99f9976b3..f7e3010f921 100644 --- a/src/models/sampled-document-collection.js +++ b/src/models/sampled-document-collection.js @@ -1,9 +1,23 @@ -var DocumentCollection = require('scout-brain').models.DocumentCollection; +var State = require('ampersand-state'); +var Collection = require('ampersand-rest-collection'); -module.exports = DocumentCollection.extend({ +var DocumentModel = State.extend({ + idAttribute: '_id', + extraProperties: 'allow' +}); + +var DocumentCollection = Collection.extend({ + model: DocumentModel, + comparator: '_id' +}); + + +var SampledDocumentCollection = DocumentCollection.extend({ /** * Don't do client-side sorting as the cursor on the server-side handles sorting. */ comparator: false, namespace: 'SampledDocumentCollection' }); + +module.exports = SampledDocumentCollection; diff --git a/src/models/types.js b/src/models/types.js deleted file mode 100644 index ae7799f53ee..00000000000 --- a/src/models/types.js +++ /dev/null @@ -1,7 +0,0 @@ -var types = require('scout-brain').types; - -/** - * Friendly wrapper around MongoDB and types used in Compass. - * @see https://github.com/10gen/scout/blob/dev/scout-brain/lib/types.js - */ -module.exports = types; From 18489a22765b78f7407f9da44879a7c1dd0462b3 Mon Sep 17 00:00:00 2001 From: Lucas Hrabovsky Date: Fri, 25 Sep 2015 16:27:28 -0400 Subject: [PATCH 14/22] :arrow_up: mongodb-js-precommit@0.2.2 No read-only. Also remove the unused `mongodb-database-model` --- package.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/package.json b/package.json index 997d1871ba1..c19c0e9e432 100644 --- a/package.json +++ b/package.json @@ -72,7 +72,6 @@ "electron-squirrel-startup": "^0.1.2", "mongodb-collection-model": "^0.1.0", "mongodb-connection-model": "^0.0.3", - "mongodb-database-model": "^0.1.0", "mongodb-instance-model": "^0.2.0", "mongodb-ns": "^1.0.0", "scout-server": "http://bin.mongodb.org/js/scout-server/v0.2.2/scout-server-0.2.2.tar.gz" @@ -119,7 +118,7 @@ "mocha": "^2.2.5", "moment": "^2.10.3", "mongodb-extended-json": "^1.3.1", - "mongodb-js-precommit": "^0.1.2", + "mongodb-js-precommit": "^0.2.2", "mongodb-language-model": "^0.2.1", "mongodb-schema": "^3.3.0", "mousetrap": "^1.5.3", From 93454544efe90617fb787d52a4edb1bcdffc2344 Mon Sep 17 00:00:00 2001 From: Lucas Hrabovsky Date: Fri, 25 Sep 2015 16:28:09 -0400 Subject: [PATCH 15/22] :arrow_right: mongodb-js-fmt@0.0.3 Use `npm run fmt` to format your code. --- package.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index c19c0e9e432..919cd37cbc9 100644 --- a/package.json +++ b/package.json @@ -54,7 +54,8 @@ "start": "gulp dev", "release": "gulp release", "test": "mocha", - "check": "mongodb-js-precommit" + "check": "mongodb-js-precommit", + "fmt": "mongodb-js-fmt src/{**/*.js,*.js}" }, "pre-commit": [ "check", @@ -119,6 +120,7 @@ "moment": "^2.10.3", "mongodb-extended-json": "^1.3.1", "mongodb-js-precommit": "^0.2.2", + "mongodb-js-fmt": "^0.0.3", "mongodb-language-model": "^0.2.1", "mongodb-schema": "^3.3.0", "mousetrap": "^1.5.3", From ee6dd1f5db56d944a64f9c07cff277e66ce9ea22 Mon Sep 17 00:00:00 2001 From: Lucas Hrabovsky Date: Fri, 25 Sep 2015 16:28:52 -0400 Subject: [PATCH 16/22] Show a :lock: next to connections which use auth --- src/connect/connection.jade | 7 ++++++- src/connect/sidebar.js | 14 ++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/src/connect/connection.jade b/src/connect/connection.jade index 620cb2e7db2..d6c085b3cba 100644 --- a/src/connect/connection.jade +++ b/src/connect/connection.jade @@ -1,4 +1,9 @@ li.list-group-item .close-icon(data-hook='close') i.fa.fa-close - a(data-hook='name') + a() + span(data-hook='name') + span(style='padding-left: 4px;', + data-hook='has-auth', + title='This connection has authentication enabled.') + i.fa.fa-lock diff --git a/src/connect/sidebar.js b/src/connect/sidebar.js index c76ef1b0575..867212907cd 100644 --- a/src/connect/sidebar.js +++ b/src/connect/sidebar.js @@ -28,6 +28,20 @@ var SidebarItemView = View.extend({ hover: { type: 'toggle', hook: 'close' + }, + has_auth: { + type: 'booleanClass', + hook: 'has-auth', + yes: 'visible', + no: 'hidden' + } + }, + derived: { + has_auth: { + deps: ['model.auth_mechanism'], + fn: function() { + return this.model.auth_mechanism !== null; + } } }, template: require('./connection.jade'), From 28d1019849d5d6b6e319d1a9aead8753bfce81de Mon Sep 17 00:00:00 2001 From: Lucas Hrabovsky Date: Fri, 25 Sep 2015 16:33:04 -0400 Subject: [PATCH 17/22] :art: overhaul app initialization MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Provide user visible feedback as to what’s happening during initialization - Add the notion of a fatal error state on the app view: something didn’t happen and we can’t proceed - When client encounters an error, we’re fatal - If the client doesnt become readable within 5 seconds we’re fatal (instead of folks staring at a white screen for 20 seconds going "uummm”) - report fatals back to bugging nicely --- src/app.js | 143 ++++++++++++++++++++++++++++++++++--------------- src/bugsnag.js | 2 + 2 files changed, 103 insertions(+), 42 deletions(-) diff --git a/src/app.js b/src/app.js index 8748564e76b..c5fd5042d47 100644 --- a/src/app.js +++ b/src/app.js @@ -1,3 +1,5 @@ +/* eslint no-console:0 */ + var pkg = require('../package.json'); var app = require('ampersand-app'); app.extend({ @@ -8,7 +10,9 @@ app.extend({ 'App Version': pkg.version } }); -require('./bugsnag').listen(app); + +var bugsnag = require('./bugsnag'); +bugsnag.listen(app); var _ = require('lodash'); var domReady = require('domready'); @@ -17,6 +21,13 @@ var getOrCreateClient = require('scout-client'); var ViewSwitcher = require('ampersand-view-switcher'); var View = require('ampersand-view'); var localLinks = require('local-links'); + +var QueryOptions = require('./models/query-options'); +var Connection = require('./models/connection'); +var MongoDBInstance = require('./models/mongodb-instance'); +var Router = require('./router'); +var Statusbar = require('./statusbar'); + var debug = require('debug')('scout:app'); // Inter-process communication with main process (Electron window) @@ -78,24 +89,59 @@ var Application = View.extend({ /** * Enable/Disable features with one global switch */ - features: 'object' + features: 'object', + clientStartedAt: 'date', + clientStalledTimeout: 'number' }, events: { 'click a': 'onLinkClick' }, - /** - * We have what we need, we can now start our router and show the appropriate page! - */ - _onDOMReady: function() { - this.el = document.querySelector('#application'); - this.render(); + onClientReady: function() { + debug('Client ready! Took %dms to become readable', + new Date() - this.clientStartedAt); + + debug('clearing client stall timeout...'); + clearTimeout(this.clientStalledTimeout); + debug('initializing singleton models... '); + this.queryOptions = new QueryOptions(); + this.volatileQueryOptions = new QueryOptions(); + this.instance = new MongoDBInstance(); + + this.startRouter(); + }, + startRouter: function() { + this.router = new Router(); + debug('Listening for page changes from the router...'); this.listenTo(this.router, 'page', this.onPageChange); + debug('Starting router...'); this.router.history.start({ pushState: false, root: '/' }); + app.statusbar.hide(); + }, + onFatalError: function(id, err) { + console.error('Fatal Error!: ', id, err); + bugsnag.notifyException(err, 'fatal!' + id); + window.alert('Fatal Error: ' + id + ': ' + err.message); + }, + // ms we'll wait for a `scout-client` instance + // to become readable before giving up and showing + // a fatal error message. + CLIENT_STALLED_REDLINE: 5 * 1000, + startClientStalledTimer: function() { + this.clientStartedAt = new Date(); + + debug('Starting client stalled timer to bail in %dms...', + this.CLIENT_STALLED_REDLINE); + + this.clientStalledTimeout = setTimeout(function() { + this.onFatalError('client stalled', + new Error('Error connecting to MongoDB. ' + + 'Please reload the page.')); + }.bind(this), this.CLIENT_STALLED_REDLINE); }, /** * When you want to go to a different page in the app or just save @@ -117,15 +163,25 @@ var Application = View.extend({ trigger: !options.silent }); }, + /** + * Called a soon as the DOM is ready so we can + * start showing status indicators as + * quickly as possible. + */ render: function() { + debug('Rendering app container...'); + + this.el = document.querySelector('#application'); this.renderWithTemplate(this); this.pageSwitcher = new ViewSwitcher(this.queryByHook('layout-container'), { show: function() { document.scrollTop = 0; } }); - - this.statusbar.el = this.queryByHook('statusbar'); + debug('rendering statusbar...'); + this.statusbar = new Statusbar({ + el: this.queryByHook('statusbar') + }); this.statusbar.render(); }, onPageChange: function(view) { @@ -153,16 +209,6 @@ var state = new Application({ connection_id: connection_id }); -var QueryOptions = require('./models/query-options'); -var Connection = require('./models/connection'); -var MongoDBInstance = require('./models/mongodb-instance'); -var Router = require('./router'); -var Statusbar = require('./statusbar'); - -function start() { - state.router = new Router(); - state._onDOMReady(); -} // @todo (imlucas): Feature flags can be overrideen // via `window.localStorage`. var FEATURES = { @@ -188,29 +234,42 @@ app.extend({ }, init: function() { domReady(function() { - state.statusbar = new Statusbar(); - - if (connection_id) { - state.connection = new Connection({ - _id: connection_id - }); - - - debug('looking up connection `%s`...', connection_id); - state.connection.fetch({ - success: function() { - debug('got connection `%j`...', state.connection.serialize()); - app.client = getOrCreateClient(app.endpoint, state.connection.serialize()); - - state.queryOptions = new QueryOptions(); - state.volatileQueryOptions = new QueryOptions(); - state.instance = new MongoDBInstance(); - start(); - } - }); - } else { - start(); + state.render(); + + if (!connection_id) { + // Not serving a part of the app which uses the client, + // so we can just start everything up now. + state.start(); + return; } + + app.statusbar.show('Retrieving connection details...'); + + state.connection = new Connection({ + _id: connection_id + }); + + debug('looking up connection `%s`...', connection_id); + state.connection.fetch({ + success: function() { + app.statusbar.show('Connection details loaded! Initializing client...'); + + var endpoint = app.endpoint; + var connection = state.connection.serialize(); + + app.client = getOrCreateClient(endpoint, connection) + .on('readable', state.onClientReady.bind(state)) + .on('error', state.onFatalError.bind(state, 'create client')); + + state.startClientStalledTimer(); + }, + error: function() { + // @todo (imlucas) `ampersand-sync-localforage` currently drops + // the real error so for now just use a generic. + state.onFatalError(state, 'fetch connection', + new Error('Error retrieving connection. Please reload the page.')); + } + }); }); // set up ipc ipc.on('message', state.onMessageReceived.bind(this)); diff --git a/src/bugsnag.js b/src/bugsnag.js index 6ace11b0521..fde1a2085c1 100644 --- a/src/bugsnag.js +++ b/src/bugsnag.js @@ -28,6 +28,8 @@ function beforeNotify(d) { debug('redacted bugsnag report\n', JSON.stringify(d, null, 2)); } +module.exports = bugsnag; + /** * Configure bugsnag's api client which attaches a handler to * `window.onerror` so any uncaught exceptions are trapped and logged From 104653f4bc229e0c51d3d1db48c5572b015d23f0 Mon Sep 17 00:00:00 2001 From: Lucas Hrabovsky Date: Fri, 25 Sep 2015 16:36:21 -0400 Subject: [PATCH 18/22] :tshirt: found by latest fmt updates --- src/connect/connect-form-view.js | 2 ++ src/minicharts/querybuilder.js | 2 +- src/models/editable-query.js | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/connect/connect-form-view.js b/src/connect/connect-form-view.js index 086386eb8bb..cc98bfdbe6c 100644 --- a/src/connect/connect-form-view.js +++ b/src/connect/connect-form-view.js @@ -47,6 +47,8 @@ var ConnectFormView = FormView.extend({ * These are the default form fields that are always present in the connect dialog. Auth and * SSL fields are added/removed dynamically, depending on whether the options are expanded or * collapsed. + * + * @return {Array} */ fields: function() { return [ diff --git a/src/minicharts/querybuilder.js b/src/minicharts/querybuilder.js index 5f0d067d981..3423a2dc867 100644 --- a/src/minicharts/querybuilder.js +++ b/src/minicharts/querybuilder.js @@ -342,7 +342,7 @@ module.exports = { mustSelect = mustSelect && d.value < message.selected[1]; halfSelect = halfSelect || d.value + message.dx > message.selected[1]; } - // mustSelect = lowerSelect && upperSelect; + // mustSelect = lowerSelect && upperSelect; } if (mustSelect) { el.classList.add('selected'); diff --git a/src/models/editable-query.js b/src/models/editable-query.js index 37876266c37..7f5e3b9a3e7 100644 --- a/src/models/editable-query.js +++ b/src/models/editable-query.js @@ -42,7 +42,7 @@ module.exports = Model.extend({ // // return the string without key quotes for display in RefineBarView // } // }, - /*eslint no-new: 0*/ + /* eslint no-new: 0 */ valid: { deps: ['cleanString'], fn: function() { From 0d078689fff48308758343db37eb2fe42dd908db Mon Sep 17 00:00:00 2001 From: Lucas Hrabovsky Date: Fri, 25 Sep 2015 16:38:19 -0400 Subject: [PATCH 19/22] :bug: missed a method rename --- src/app.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app.js b/src/app.js index c5fd5042d47..5e487709f84 100644 --- a/src/app.js +++ b/src/app.js @@ -239,7 +239,7 @@ app.extend({ if (!connection_id) { // Not serving a part of the app which uses the client, // so we can just start everything up now. - state.start(); + state.startRouter(); return; } From 2eaa856f2334023b25ea0c07a0b7cf7412713c7f Mon Sep 17 00:00:00 2001 From: Lucas Hrabovsky Date: Fri, 25 Sep 2015 17:02:37 -0400 Subject: [PATCH 20/22] :arrow_up: scout-client@0.1.4 So error messages from the server are actually visible. --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 919cd37cbc9..f0dcab06ec1 100644 --- a/package.json +++ b/package.json @@ -136,7 +136,7 @@ "raf": "^3.0.0", "run-sequence": "^1.1.2", "run-series": "^1.1.2", - "scout-client": "http://bin.mongodb.org/js/scout-client/v0.1.3/scout-client-0.1.3.tar.gz", + "scout-client": "http://bin.mongodb.org/js/scout-client/v0.1.4/scout-client-0.1.4.tar.gz", "stream-combiner2": "^1.0.2", "uuid": "^2.0.1", "vinyl-buffer": "^1.0.0", From eb735cb3730e0ba93d670c920a649a8c4f9053bb Mon Sep 17 00:00:00 2001 From: Lucas Hrabovsky Date: Fri, 25 Sep 2015 17:03:32 -0400 Subject: [PATCH 21/22] :art: Make status bar have an api for fatal's https://cldup.com/PGKQawQtnZ.png --- src/app.js | 7 +++++-- src/statusbar/index.jade | 2 +- src/statusbar/index.js | 17 +++++++++++++++++ 3 files changed, 23 insertions(+), 3 deletions(-) diff --git a/src/app.js b/src/app.js index 5e487709f84..c41ef237a5d 100644 --- a/src/app.js +++ b/src/app.js @@ -123,9 +123,12 @@ var Application = View.extend({ app.statusbar.hide(); }, onFatalError: function(id, err) { + debug('clearing client stall timeout...'); + clearTimeout(this.clientStalledTimeout); + console.error('Fatal Error!: ', id, err); - bugsnag.notifyException(err, 'fatal!' + id); - window.alert('Fatal Error: ' + id + ': ' + err.message); + bugsnag.notifyException(err, 'Fatal Error: ' + id); + app.statusbar.fatal(err); }, // ms we'll wait for a `scout-client` instance // to become readable before giving up and showing diff --git a/src/statusbar/index.jade b/src/statusbar/index.jade index 700520e6d9e..629fe09857b 100644 --- a/src/statusbar/index.jade +++ b/src/statusbar/index.jade @@ -3,6 +3,6 @@ .progress-bar.progress-bar-striped.active(data-hook='inner-bar') ul.message-background.with-sidebar.centered(data-hook='message-container'): li p(data-hook='message') - .spinner-circles + .spinner-circles(data-hook='loading') .circle-1 .circle-2 diff --git a/src/statusbar/index.js b/src/statusbar/index.js index a0b639a2e24..34297f0a4aa 100644 --- a/src/statusbar/index.js +++ b/src/statusbar/index.js @@ -7,10 +7,20 @@ var StatusbarView = View.extend({ }, message: { type: 'string' + }, + loading: { + type: 'boolean', + default: true } }, template: require('./index.jade'), bindings: { + loading: { + hook: 'loading', + type: 'booleanClass', + yes: 'visible', + no: 'hidden' + }, message: [ { hook: 'message' @@ -68,13 +78,20 @@ var StatusbarView = View.extend({ onComplete: function() { this.hide(); }, + fatal: function(err) { + this.loading = false; + this.message = 'Fatal Error: ' + err.message; + this.width = 100; + }, show: function(message) { this.message = message || ''; this.width = 100; + this.loading = true; }, hide: function() { this.message = ''; this.width = 0; + this.loading = false; } }); From 9aff3459d10a7ebb8360d92c8c47fd7fa1b3af68 Mon Sep 17 00:00:00 2001 From: Lucas Hrabovsky Date: Fri, 25 Sep 2015 17:08:18 -0400 Subject: [PATCH 22/22] :rage: switch back to retry hack as the driver throws an error if DEFAULT is used --- src/connect/auth-fields.js | 3 +-- src/connect/index.js | 28 +++++++++++++++++++++++----- 2 files changed, 24 insertions(+), 7 deletions(-) diff --git a/src/connect/auth-fields.js b/src/connect/auth-fields.js index 477dfff95e4..c313e2a199c 100644 --- a/src/connect/auth-fields.js +++ b/src/connect/auth-fields.js @@ -62,12 +62,11 @@ var service_name = new InputView({ * Define fields for each auth method here */ module.exports = { - DEFAULT: [ + 'SCRAM-SHA-1': [ username, password, database_name ], - GSSAPI: [ username, service_name diff --git a/src/connect/index.js b/src/connect/index.js index ca69dd82397..4e47ebeb211 100644 --- a/src/connect/index.js +++ b/src/connect/index.js @@ -150,7 +150,7 @@ var ConnectView = View.extend({ evt.preventDefault(); this.toggle('authOpen'); if (this.authOpen) { - this.authMethod = this.previousAuthMethod || 'DEFAULT'; + this.authMethod = this.previousAuthMethod || 'SCRAM-SHA-1'; } else { this.authMethod = null; } @@ -234,13 +234,31 @@ var ConnectView = View.extend({ debug('testing credentials are usable...'); model.test(function(err) { - app.statusbar.hide(); - if (err) { + if (!err) { + this.onConnectionSuccessful(model, options); + return; + } + + if (model.auth_mechanism !== 'SCRAM-SHA-1') { debug('failed to connect', err); this.onError(new Error('Could not connect to MongoDB.'), model); return; } - this.onConnectionSuccessful(model, options); + + // For Kernel 2.6.x + model.auth_mechanism = 'MONGODB-CR'; + debug('trying again w/ MONGODB-CR...'); + app.statusbar.show(); + + model.test(function(err) { + if (err) { + debug('failed to connect again... bailing', err); + this.onError(new Error('Could not connect to MongoDB.'), model); + return; + } + this.onConnectionSuccessful(model, options); + }.bind(this)); + }.bind(this)); }, /** @@ -375,7 +393,7 @@ var ConnectView = View.extend({ // @todo (imlucas): Consolidate w/ `./auth-fields.js`. var authMethods = [ { - _id: 'DEFAULT', + _id: 'SCRAM-SHA-1', title: 'User/Password', enabled: true },