diff --git a/package.json b/package.json index 7eb590db86a..f0dcab06ec1 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", @@ -67,10 +68,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-connection-model": "0.0.2", - "scout-server": "http://bin.mongodb.org/js/scout-server/v0.2.1/scout-server-0.2.1.tar.gz" + "mongodb-collection-model": "^0.1.0", + "mongodb-connection-model": "^0.0.3", + "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": { "ampersand-app": "^1.0.4", @@ -114,7 +119,8 @@ "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-js-fmt": "^0.0.3", "mongodb-language-model": "^0.2.1", "mongodb-schema": "^3.3.0", "mousetrap": "^1.5.3", @@ -130,8 +136,7 @@ "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.2/scout-client-0.1.2.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", diff --git a/src/app.js b/src/app.js index a2eaaefd681..c41ef237a5d 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,62 @@ 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) { + debug('clearing client stall timeout...'); + clearTimeout(this.clientStalledTimeout); + + console.error('Fatal Error!: ', id, err); + 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 + // 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 +166,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,47 +212,68 @@ 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(); - 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(); + domReady(function() { + 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.startRouter(); + return; + } + + app.statusbar.show('Retrieving connection details...'); - 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()); + 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.queryOptions = new QueryOptions(); - state.volatileQueryOptions = new QueryOptions(); - state.instance = new MongoDBInstance(); - start(); + 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.')); } }); - } else { - start(); - } + }); // 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 diff --git a/src/connect/auth-fields.js b/src/connect/auth-fields.js index 95a8ef647f8..c313e2a199c 100644 --- a/src/connect/auth-fields.js +++ b/src/connect/auth-fields.js @@ -67,13 +67,6 @@ module.exports = { password, database_name ], - - 'MONGODB-CR': [ - username, - password, - database_name - ], - GSSAPI: [ username, service_name diff --git a/src/connect/connect-form-view.js b/src/connect/connect-form-view.js index 77467fc6818..cc98bfdbe6c 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. @@ -73,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 [ @@ -110,7 +86,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/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/index.jade b/src/connect/index.jade index 738485ee84c..330489b5912 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,42 @@ 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(style='padding-left: 4px;', 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, id=method._id) + + #connect-ssl-settings(class=getFeatureClass('Connect with SSL')) + hr + .form-group + label.control-label + a(href="javascript:void(0);", data-hook='openSSL') + i + span(style='padding-left: 4px;', 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..4e47ebeb211 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: [ @@ -131,8 +140,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 +157,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 +184,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'); @@ -186,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()); @@ -203,75 +215,213 @@ 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. + * Use a connection to view schemas, such as after + * submitting a form or when double-clicking on + * a list item like in `./sidebar`. * - * @param {Object} connection The new connection to check - * @return {String|null} Name of the connection that is otherwise identical to obj + * @param {Connection} model + * @param {Object} [options] + * @option {Boolean} close - Close the connect dialog on success [Default: `false`]. + * @api public */ - 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; - }, + connect: function(model, options) { + options = _.defaults(options || {}, { + close: false + }); + + app.statusbar.show(); + debug('testing credentials are usable...'); + model.test(function(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; + } + + // 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)); + }, /** - * checks if the connection name already exists but with different details. Returns true - * if the name already exists, or false otherwise. + * 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 {Object} connection The new connection to check - * @return {Boolean} Whether or not the connection name already exists + * @param {Connection} model + * @param {Object} [options] + * @api private */ - checkExistingName: function(connection) { - var existingConnection = this.connections.get(connection.name, 'name'); - - if (connection.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; - }, - /* (err, model) */ - onConnectionError: function() { - this.message = 'Could not connect to MongoDB. ' - + 'Please double check your info.'; - }, - onConnectionAccepted: function(model) { - // save connection if a name was provided - if (model.name !== '') { - model.save(); - this.connections.add(model); - } + onConnectionSuccessful: function(model, options) { + options = _.defaults(options, { + close: true + }); + /** + * 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.last_used = new Date(); + 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 + * }); + */ + this.connections.add(model, { + merge: true + }); - // 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 (_.trim(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); + } + + 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 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); + }, 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 +440,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/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') 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 diff --git a/src/connect/sidebar.js b/src/connect/sidebar.js index d0091e5a5e4..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'), @@ -52,7 +66,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 +78,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.onConnectionSelected(model); }, onItemDoubleClick: function(event, model) { this.onItemClick(event, model); - this.parent.form.onSubmit(event); + this.parent.connect(model); } }); - module.exports = SidebarView; diff --git a/src/electron/window-manager.js b/src/electron/window-manager.js index 5ceec3a5e4c..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. @@ -115,6 +115,15 @@ module.exports.create = function(opts) { app.quit(); } }); + + // @todo (imlucas) + // When in dev mode, automaticaly open devtools + // detached for ease of debugging. + // if (process.env.NODE_ENV === 'development') { + // _window.openDevTools({ + // detach: true + // }); + // } return _window; }; 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/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/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 }); 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. */ 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() { 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; 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; } });