diff --git a/gulpfile.js b/gulpfile.js index f0cf92b580e..ad7d93ed225 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -95,6 +95,7 @@ gulp.task('watch', function() { gulp.watch(['src/*.jade'], ['build:pages']); gulp.watch('images/{*,**/*}', ['copy:images']); gulp.watch('fonts/*', ['copy:fonts']); + gulp.watch('src/help/entries/*.md', ['copy:text']); gulp.watch(['src/electron/{*,**/*}'], ['copy:js']); gulp.watch('package.json', function() { gutil.log('package.json changed!'); @@ -232,8 +233,12 @@ gulp.task('copy:package.json', function() { }); gulp.task('copy:text', function() { - return gulp.src(['README.md']) - .pipe(gulp.dest('build/')); + return merge( + gulp.src('README.md') + .pipe(gulp.dest('build/')), + gulp.src('src/help/entries/*.md') + .pipe(gulp.dest('build/src/help/entries')) + ); }); // Copy non-UI js into the build. diff --git a/images/help/dev/view_hierarchy.png b/images/help/dev/view_hierarchy.png new file mode 100644 index 00000000000..761805501ee Binary files /dev/null and b/images/help/dev/view_hierarchy.png differ diff --git a/images/help/schema/sampling-results-full.png b/images/help/schema/sampling-results-full.png new file mode 100644 index 00000000000..e6f3a8ba8f1 Binary files /dev/null and b/images/help/schema/sampling-results-full.png differ diff --git a/images/help/schema/sampling-results-sample.png b/images/help/schema/sampling-results-sample.png new file mode 100644 index 00000000000..8c752a0862d Binary files /dev/null and b/images/help/schema/sampling-results-sample.png differ diff --git a/package.json b/package.json index ac48684dcf1..eef7cc77174 100644 --- a/package.json +++ b/package.json @@ -77,7 +77,9 @@ "debug": "^2.2.0", "electron-squirrel-startup": "^0.1.4", "keytar": "mongodb-js/node-keytar", + "highlight.js": "^8.9.1", "localforage": "^1.3.0", + "marky-mark": "^1.2.1", "mongodb-collection-model": "^0.1.1", "mongodb-connection-model": "^3.0.7", "mongodb-instance-model": "^1.0.2", diff --git a/src/app.js b/src/app.js index 41b120343e4..0a4c403c645 100644 --- a/src/app.js +++ b/src/app.js @@ -28,6 +28,7 @@ var MongoDBInstance = require('./models/mongodb-instance'); var User = require('./models/user'); var Router = require('./router'); var Statusbar = require('./statusbar'); +var $ = require('jquery'); var debug = require('debug')('scout:app'); @@ -119,7 +120,12 @@ var Application = View.extend({ user: User }, events: { - 'click a': 'onLinkClick' + 'click a': 'onLinkClick', + 'click i.help': 'onHelpClicked' + }, + onHelpClicked: function(evt) { + var id = $(evt.target).attr('data-hook'); + app.sendMessage('show help window', id); }, onClientReady: function() { debug('Client ready! Took %dms to become readable', @@ -284,12 +290,12 @@ app.extend({ setFeature: function(id, bool) { FEATURES[id] = bool; }, - sendMessage: function(msg, arg1) { - ipc.send('message', msg, arg1); + sendMessage: function(msg, arg) { + ipc.send('message', msg, arg); }, - onMessageReceived: function(msg) { - debug('message received from main process:', msg); - this.trigger(msg); + onMessageReceived: function(msg, arg) { + debug('message received from main process:', msg, arg); + this.trigger(msg, arg); }, metrics: metrics, init: function() { diff --git a/src/connect/authentication.js b/src/connect/authentication.js index 5f24fa12cf0..29515d0b764 100644 --- a/src/connect/authentication.js +++ b/src/connect/authentication.js @@ -38,8 +38,9 @@ var MONGODB = { new InputView({ template: inputTemplate, name: 'mongodb_database_name', - label: 'Database Name', + label: 'Authentication Database', placeholder: 'admin', + helpEntry: 'connect-userpass-auth-db', required: false }) ] @@ -57,6 +58,7 @@ var KERBEROS = { name: 'kerberos_principal', label: 'Principal', placeholder: '', + helpEntry: 'connect-kerberos-principal', required: true, tests: [ function(value) { diff --git a/src/connect/connect-form-view.js b/src/connect/connect-form-view.js index 026d0f087c5..6eb8494dc16 100644 --- a/src/connect/connect-form-view.js +++ b/src/connect/connect-form-view.js @@ -187,6 +187,7 @@ var ConnectFormView = FormView.extend({ name: 'name', label: 'Favorite Name', placeholder: 'e.g. Shared Dev, QA Box, PRODUCTION', + helpEntry: 'connect-favorite-name', required: false }) ]; diff --git a/src/connect/filereader-default.jade b/src/connect/filereader-default.jade index 5899d26547a..4bfc3311954 100644 --- a/src/connect/filereader-default.jade +++ b/src/connect/filereader-default.jade @@ -1,5 +1,8 @@ .form-item - label(data-hook='label')= label + label + span(data-hook='label')= label + if helpEntry + i.help(data-hook='#{helpEntry}') .message.message-below.message-error(data-hook='message-container') p(data-hook='message-text') .form-item-file diff --git a/src/connect/filereader-view.js b/src/connect/filereader-view.js index f1cbbe5dc63..d2b17362d45 100644 --- a/src/connect/filereader-view.js +++ b/src/connect/filereader-view.js @@ -30,6 +30,10 @@ module.exports = InputView.extend({ type: 'boolean', required: true, default: false + }, + helpEntry: { + type: 'string', + default: null } }, derived: { @@ -91,6 +95,9 @@ module.exports = InputView.extend({ _.defaults(spec, { value: [] }); + if (spec.helpEntry) { + this.helpEntry = spec.helpEntry; + } this.invalidClass = 'has-error'; this.validityClassSelector = '.form-item-file'; InputView.prototype.initialize.call(this, spec); diff --git a/src/connect/input-default.jade b/src/connect/input-default.jade index 97d39b97898..2c3f9fbda1b 100644 --- a/src/connect/input-default.jade +++ b/src/connect/input-default.jade @@ -1,5 +1,8 @@ .form-item - label(data-hook='label')= label + label + span(data-hook='label')= label + if helpEntry + i.help(data-hook='#{helpEntry}') .message.message-above.message-error(data-hook='message-container') p(data-hook='message-text') input.form-control(placeholder='#{placeholder}') diff --git a/src/connect/input-view.js b/src/connect/input-view.js index 76a82d049cc..b48f5aca2fd 100644 --- a/src/connect/input-view.js +++ b/src/connect/input-view.js @@ -6,7 +6,17 @@ var InputView = require('ampersand-input-view'); * so that label gets correctly rendered on subsequent render() calls. */ module.exports = InputView.extend({ - initialize: function() { + props: { + helpEntry: { + type: 'string', + default: null + } + }, + initialize: function(spec) { + spec = spec || {}; + if (spec.helpEntry) { + this.helpEntry = spec.helpEntry; + } this.invalidClass = 'has-error'; this.validityClassSelector = '.form-item'; InputView.prototype.initialize.apply(this, arguments); diff --git a/src/connect/select-default.jade b/src/connect/select-default.jade index 0af3b291c89..90c222689d4 100644 --- a/src/connect/select-default.jade +++ b/src/connect/select-default.jade @@ -1,5 +1,8 @@ .form-item - label(data-hook='label')= label + label + span(data-hook='label')= label + if helpEntry + i.help(data-hook='#{helpEntry}') .message.message-below.message-error(data-hook='message-container') p(data-hook='message-text') select.form-control(type='select') diff --git a/src/connect/ssl.js b/src/connect/ssl.js index a6912abbd42..2ca1f0018bd 100644 --- a/src/connect/ssl.js +++ b/src/connect/ssl.js @@ -41,6 +41,7 @@ var SERVER = { name: 'ssl_ca', multi: true, label: 'Certificate Authority', + helpEntry: 'connect-ssl-certificate-authority', required: true }) ] @@ -59,22 +60,26 @@ var ALL = { name: 'ssl_ca', multi: true, label: 'Certificate Authority', + helpEntry: 'connect-ssl-certificate-authority', required: true }), new FileReaderView({ name: 'ssl_private_key', label: 'Client Private Key', + helpEntry: 'connect-ssl-client-certificate-key', required: true }), new FileReaderView({ name: 'ssl_certificate', label: 'Client Certificate', + helpEntry: 'connect-ssl-client-certificate', required: true }), new InputView({ template: inputTemplate, name: 'ssl_private_key_password', - label: 'Private Key Password', + label: 'Client Key Password', + helpEntry: 'connect-ssl-private-key-password', required: false }) ] diff --git a/src/electron/help.js b/src/electron/help.js new file mode 100644 index 00000000000..6491b4a2f34 --- /dev/null +++ b/src/electron/help.js @@ -0,0 +1,39 @@ +var path = require('path'); +var ipc = require('ipc'); +var mm = require('marky-mark'); +var _ = require('lodash'); +var highlight = require('highlight.js'); + +var debug = require('debug')('scout:electron:help'); + +debug('adding ipc listener for `/help/entries`...'); +ipc.on('/help/entries', function(evt) { + var dir = path.join(__dirname, '..', 'help', 'entries'); + debug('parsing entries with marky-mark from `%s`', dir); + + // add syntax highlighting options, pass through to `marked` module + var options = { + marked: { + highlight: function(code) { + var result = highlight.highlightAuto(code).value; + return result; + } + } + }; + + mm.parseDirectory(dir, options, function(err, posts) { + if (err) { + debug('error parsing entries', err); + evt.sender.send('/help/entries/error', err); + return; + } + debug('successfully parsed!', posts); + // in production don't return the dev-only entries + if (process.env.NODE_ENV === 'production') { + posts = _.filter(posts, function(post) { + return !post.meta.devOnly; + }); + } + evt.sender.send('/help/entries/success', posts); + }); +}); diff --git a/src/electron/index.js b/src/electron/index.js index 57ca581544f..ceb8e747b25 100644 --- a/src/electron/index.js +++ b/src/electron/index.js @@ -40,4 +40,8 @@ if (!require('electron-squirrel-startup')) { var AppMenu = require('./menu'); AppMenu.init(); require('./window-manager'); + + app.on('ready', function() { + require('./help'); + }); } diff --git a/src/electron/menu.js b/src/electron/menu.js index e7cc67cbdd1..b6c53cd9552 100644 --- a/src/electron/menu.js +++ b/src/electron/menu.js @@ -1,13 +1,14 @@ // based off of https://github.com/atom/atom/blob/master/src/browser/application-menu.coffee // use js2.coffee to convert it to JS -var _ = require('lodash'); -var app = require('app'); var BrowserWindow = require('browser-window'); -var debug = require('debug')('electron:menu'); var Menu = require('menu'); var State = require('ampersand-state'); +var _ = require('lodash'); +var app = require('app'); +var debug = require('debug')('electron:menu'); + // submenu related function separator() { return { @@ -15,7 +16,7 @@ function separator() { }; } -function quitSubMenu(label) { +function quitSubMenuItem(label) { return { label: label, accelerator: 'CmdOrCtrl+Q', @@ -25,7 +26,7 @@ function quitSubMenu(label) { }; } -function compassOverviewSubMenu() { +function compassOverviewSubMenuItem() { return { label: 'Compass Overview', click: function() { @@ -58,7 +59,7 @@ function darwinCompassSubMenu() { selector: 'unhideAllApplications:' }, separator(), - quitSubMenu('Quit') + quitSubMenuItem('Quit') ] }; } @@ -78,7 +79,7 @@ function connectSubMenu(nonDarwin) { if (nonDarwin) { subMenu.push(separator()); - subMenu.push(quitSubMenu('Exit')); + subMenu.push(quitSubMenuItem('Exit')); } return { @@ -126,7 +127,7 @@ function editSubMenu() { }; } -function nonDarwinAboutSubMenu() { +function nonDarwinAboutSubMenuItem() { return { label: 'About Compass', click: function() { @@ -135,18 +136,27 @@ function nonDarwinAboutSubMenu() { }; } +function helpWindowSubMenuItem() { + return { + label: 'Show Compass Help', + accelerator: 'F1', + click: function() { + app.emit('show help window'); + } + }; +} + function helpSubMenu(showCompassOverview) { var subMenu = []; - if (showCompassOverview) { - subMenu.push(compassOverviewSubMenu()); + subMenu.push(helpWindowSubMenuItem()); - if (process.platform !== 'darwin') { - subMenu.push(separator()); - } + if (showCompassOverview) { + subMenu.push(compassOverviewSubMenuItem()); } if (process.platform !== 'darwin') { - subMenu.push(nonDarwinAboutSubMenu()); + subMenu.push(separator()); + subMenu.push(nonDarwinAboutSubMenuItem()); } return { @@ -155,6 +165,15 @@ function helpSubMenu(showCompassOverview) { }; } +function nonDarwinCompassSubMenuItem() { + return { + label: 'MongoDB Compass', + submenu: [ + quitSubMenuItem('Exit') + ] + }; +} + function shareSubMenu() { return { label: 'Share', @@ -230,20 +249,19 @@ function darwinMenu(menuState) { } menu.push(windowSubMenu()); - - if (menuState.showCompassOverview) { - menu.push(helpSubMenu(menuState.showCompassOverview)); - } + menu.push(helpSubMenu(menuState.showCompassOverview)); return menu; } function nonDarwinMenu(menuState) { var menu = [ - connectSubMenu(true), - viewSubMenu() + nonDarwinCompassSubMenuItem() ]; + menu.push(connectSubMenu()); + menu.push(viewSubMenu()); + if (menuState.showShare) { menu.push(shareSubMenu()); } @@ -297,10 +315,6 @@ var AppMenu = (function() { getTemplate: function(winID) { var menuState = this.windowTemplates.get(winID); - debug('WINDOW\'s ' + winID + ' Menu State'); - debug('showCompassOverview: ' + menuState.showCompassOverview); - debug('showShare: ' + menuState.showShare); - if (process.platform === 'darwin') { return darwinMenu(menuState); } @@ -339,7 +353,6 @@ var AppMenu = (function() { Menu.setApplicationMenu(menu); }, - // share/hide submenu fns hideShare: function() { this.updateMenu('showShare', false); }, diff --git a/src/electron/window-manager.js b/src/electron/window-manager.js index fcdcc7c7eab..3a318705bb4 100644 --- a/src/electron/window-manager.js +++ b/src/electron/window-manager.js @@ -3,36 +3,37 @@ * [BrowserWindow](https://github.com/atom/electron/blob/master/docs/api/browser-window.md) * class */ - var AppMenu = require('./menu'); var BrowserWindow = require('browser-window'); var Notifier = require('node-notifier'); -var Path = require('path'); var _ = require('lodash'); var app = require('app'); var config = require('./config'); var debug = require('debug')('scout-electron:window-manager'); var dialog = require('dialog'); +var path = require('path'); /** * When running in electron, we're in `RESOURCES/src/electron`. */ -var RESOURCES = Path.resolve(__dirname, '../../'); +var RESOURCES = path.resolve(__dirname, '../../'); var SCOUT_ICON_PATH = RESOURCES + '/images/scout.png'; /** * The app's HTML shell which is the output of `./src/index.jade` * created by the `build:pages` gulp task. */ -var DEFAULT_URL = 'file://' + Path.join(RESOURCES, 'index.html#connect'); +var DEFAULT_URL = 'file://' + path.join(RESOURCES, 'index.html#connect'); +var HELP_URL = 'file://' + path.join(RESOURCES, 'index.html#help'); /** - * We want the Connect dialog window to be special - * and for there to ever only be one instance of it - * so we'll use scope to essentially make it a Singleton. + * We want the Connect and Help window to be special + * and for there to ever only be one instance of each of them + * so we'll use scope to essentially make each of them a Singleton. */ var connectWindow; +var helpWindow; // @todo (imlucas): Removed in setup branch as we dont need to do this anymore // as a `all-windows-closed` event has been added to the `app` event api @@ -136,11 +137,6 @@ function createWindow(opts, url) { return module.exports.create(opts); } -app.on('close connect', function() { - connectWindow.close(); - connectWindow = null; -}); - app.on('show about dialog', function() { dialog.showMessageBox({ type: 'info', @@ -156,9 +152,29 @@ app.on('show connect dialog', function() { } connectWindow = createWindow({}, DEFAULT_URL); - connectWindow.on('focus', function() { - debug('connect window focused.'); - connectWindow.webContents.send('message', 'connect-window-focused'); + connectWindow.on('closed', function() { + debug('connect window closed.'); + connectWindow = null; + }); +}); + +app.on('show help window', function(id) { + if (helpWindow) { + helpWindow.focus(); + if (_.isString(id)) { + helpWindow.webContents.send('message', 'show-help-entry', id); + } + return; + } + + var url = HELP_URL; + if (_.isString(id)) { + url += '/' + id; + } + + helpWindow = createWindow({}, url); + helpWindow.on('closed', function() { + helpWindow = null; }); }); @@ -203,7 +219,7 @@ app.on('ready', function() { }); var ipc = require('ipc'); -ipc.on('message', function(event, msg, arg1) { - debug('message received in main process', msg); - app.emit(msg, arg1); +ipc.on('message', function(event, msg, arg) { + debug('message received in main process', msg, arg); + app.emit(msg, arg, event); }); diff --git a/src/help/entries/connect-favorite-name.md b/src/help/entries/connect-favorite-name.md new file mode 100644 index 00000000000..e4024cbc48b --- /dev/null +++ b/src/help/entries/connect-favorite-name.md @@ -0,0 +1,12 @@ +--- +title: Favorite Name +tags: + - authentication + - connect + - favorite +--- + +Favorite name for a saved connection. + +Connections can be saved as favorites. Provide a name here and click +"Create Favorite" to save a connection. diff --git a/src/help/entries/connect-kerberos-principal.md b/src/help/entries/connect-kerberos-principal.md new file mode 100644 index 00000000000..53299018fbc --- /dev/null +++ b/src/help/entries/connect-kerberos-principal.md @@ -0,0 +1,21 @@ +--- +title: Kerberos Principal +tags: + - authentication + - connect + - kerberos + - needs review +related: + - connect-kerberos-service-name +--- + + +A Kerberos principal is a unique identity to which Kerberos can assign tickets. + + +Principals can have an arbitrary number of components. Each component is +separated by a component separator, generally `/`. The last component is the +realm, separated from the rest of the principal by the realm separator, +generally `@`. + +An example for a Kerberos Principal is: `primary/instance@REALM`. diff --git a/src/help/entries/connect-kerberos-service-name.md b/src/help/entries/connect-kerberos-service-name.md new file mode 100644 index 00000000000..7ad27925190 --- /dev/null +++ b/src/help/entries/connect-kerberos-service-name.md @@ -0,0 +1,17 @@ +--- +title: Kerberos Service Name +tags: + - authentication + - connect + - kerberos + - needs review +--- + + +A Kerberos Service Name is the name by which a client uniquely identifies an +instance of a service. + + +Typically, the service name for a MongoDB instance is named `mongodb`. Provide +another name here if your MongoDB instance was set up with a different service +name. diff --git a/src/help/entries/connect-ssl-certificate-authority.md b/src/help/entries/connect-ssl-certificate-authority.md new file mode 100644 index 00000000000..764d29bf2e4 --- /dev/null +++ b/src/help/entries/connect-ssl-certificate-authority.md @@ -0,0 +1,28 @@ +--- +title: Certificate Authority File +tags: + - authentication + - connect + - ssl + - needs review +related: + - connect-ssl-client-certificate-key + - connect-ssl-client-certificate + - connect-ssl-private-key-password +--- + + +Provide certificate(s) of one or more Certificate Authorities that you trust +to validate the server's identity. + + +For production use, your MongoDB deployment should use valid certificates +generated and signed by a single certificate authority. You or your organization +can generate and maintain an independent certificate authority, or use +certificates generated by a third-party SSL vendor. + +To verify the identity of the MongoDB deployment you connect to, provide +one or more certificates of trusted Certificate Authorities. If this field +is left blank, the server's identity will not be checked which is +**not recommended** because it leaves you vulnerable to man-in-the-middle +attacks. diff --git a/src/help/entries/connect-ssl-client-certificate-key.md b/src/help/entries/connect-ssl-client-certificate-key.md new file mode 100644 index 00000000000..4e5f64d92b0 --- /dev/null +++ b/src/help/entries/connect-ssl-client-certificate-key.md @@ -0,0 +1,15 @@ +--- +title: Client Certificate Key +tags: + - authentication + - connect + - ssl + - todo +related: + - connect-ssl-certificate-authority + - connect-ssl-client-certificate + - connect-ssl-private-key-password +--- +Describes what the `Client Certificate Key` field in the connect dialog does. + +I am *markdown*. diff --git a/src/help/entries/connect-ssl-client-certificate.md b/src/help/entries/connect-ssl-client-certificate.md new file mode 100644 index 00000000000..f00bd088d96 --- /dev/null +++ b/src/help/entries/connect-ssl-client-certificate.md @@ -0,0 +1,15 @@ +--- +title: Client Certificate +tags: + - authentication + - connect + - ssl + - todo +related: + - connect-ssl-certificate-authority + - connect-ssl-client-certificate-key + - connect-ssl-private-key-password +--- +Describes what the `Client Certificate` field in the connect dialog does. + +I am *markdown*. diff --git a/src/help/entries/connect-ssl-private-key-password.md b/src/help/entries/connect-ssl-private-key-password.md new file mode 100644 index 00000000000..aa56a7117ba --- /dev/null +++ b/src/help/entries/connect-ssl-private-key-password.md @@ -0,0 +1,13 @@ +--- +title: Private Key Password +tags: + - authentication + - connect + - ssl + - todo +related: + - connect-ssl-certificate-authority + - connect-ssl-client-certificate-key + - connect-ssl-client-certificate +--- +Describes what the `Private Key Password` field in the connect dialog does. It's optional. diff --git a/src/help/entries/connect-userpass-auth-db.md b/src/help/entries/connect-userpass-auth-db.md new file mode 100644 index 00000000000..ed235c8d87e --- /dev/null +++ b/src/help/entries/connect-userpass-auth-db.md @@ -0,0 +1,18 @@ +--- +title: Authentication Database +tags: + - authentication + - connect +--- +This field represents the _Authentication Database_ (also called +_Authentication Source_) for User/Password authentication. + +In MongoDB's authentication model, a username is scoped to a database, called +_Authentication Database_. The user’s privileges are not necessarily limited to +this database. The user can have privileges in additional databases. + +If the field is left blank, the default value `admin` is used. + +For authentication methods other than User/Password authentication (like +_Kerberos_, _LDAP_, _X.509_) the _Authentication Database_ is always set +to `$external` and no user input is required. diff --git a/src/help/entries/dev-ampersand-tips.md b/src/help/entries/dev-ampersand-tips.md new file mode 100644 index 00000000000..4dca856cac1 --- /dev/null +++ b/src/help/entries/dev-ampersand-tips.md @@ -0,0 +1,99 @@ +--- +title: ampersand.js Tips +tags: + - help + - ampersand + - view + - model +devOnly: true +--- + +Ampersand.js is the Model and View layer used by Compass. + +### View Hierarchy + +This diagram shows the view hierarchy of Compass and where the `.js` files and `.jade` templates for each view are. + + + +### Subviews vs. `renderCollection()` vs. `renderSubview()` + +[Ampersand views](http://ampersandjs.com/docs#ampersand-view) offer a number of ways of rendering subviews inside of them, for modular view composition. Each have their own advantages for certain use cases. + +#### Subviews + +Subviews can be defined inside a `subviews` property when extending an AmpersandView, see [ampersand-view subviews](http://ampersandjs.com/docs#ampersand-view-subviews). An example would look like this: + +```js +var AmpersandView = require('ampersand-view'); +var ControlPanelView = require('./controlpanel'); + +module.exports = AmpersandView.extend({ + template: require('./my-view.jade'), + subviews: { + controlview: { // accessible via `self.controlview` + container: '[data-hook=controlpanel-subview]', // use *-subiew hook + waitFor: 'model.controls', // waits until model.controls becomes true-thy + prepareView: function (el) { + return new ControlPanelView({ // return new view instance + el: el, // always pass in the el element + model: this.model.controls // optionally pass in a model or collection + }); + } + } +}); +``` + +Subviews defined this way in the `subviews` property **replace** the DOM element container. Keep this in mind when writing your .html or .jade templates. It makes no sense to add a class or id or any other attributes to the container element, as it will be removed and replaced with the root of the subview. + +**Pro Tip:** A good convention is to always use a hook ending in `-subview`, as opposed to `-container` (see below), to make it easy to see which elements are replaced and which ones stay in the DOM. + +#### renderCollection() + +AmpersandView also has a [`.renderCollection()`](http://ampersandjs.com/docs#ampersand-view-rendercollection) method. It takes an AmpersandCollection, a view class, and a container selector and renders an instance of the view for each model in the collection inside the container. It also listens to changes in the collection and automatically removes or adds new views. + +A typical example often uses `.renderCollection()` inside the `render()` method and could look like this: + +```js +var AmpersandView = require('ampersand-view'); +var MySingleItemView = require('./single-item'); + +module.exports = AmpersandView.extend({ + template: require('./my-view.jade'), + render: function () { + this.renderWithTemplate(this); + this.renderCollection(this.collection, MySingleItemView, this.queryByHook('items-container')); + } +} +``` + +**Pro Tip:** As a convention, here we use a hook ending in `-container`, because the container remains in the DOM. + + +#### renderSubview() + +[`.renderSubview()`](http://ampersandjs.com/docs#ampersand-view-rendersubview) is like a manual version of the `subviews` property mentioned above. It is most often used if the subview is added dynamically at some later point, if you require the container to remain in the DOM, or if the `subviews` mechanism doesn't allow for your specific use case, e.g. the conditions when the subview needs to be rendered. (Notice though that often this can be solved with a _derived property_, that the subview can `waitFor`). + +Typical code could look like this: + +```js +var AmpersandView = require('ampersand-view'); +var MySpecialView = require('./my-special'); + +module.exports = AmpersandView.extend({ + template: require('./my-view.jade'), + events: { + 'click .specialButton': 'onSpecialClicked' + }, + onSpecialClicked: function (evt) { + if (this.rendered) { + var specialModel = ...; // compute special model here + this.renderSubview(specialModel, MySpecialView, this.queryByHook('special-container')); + } + } +} +``` + +This is similar to the `.renderCollection()` example, except it applies to a single view instance instead of multiple views. + +**Pro Tip:** As a convention, again we use a hook ending in `-container`, because the container remains in the DOM. diff --git a/src/help/entries/dev-how-help-works.md b/src/help/entries/dev-how-help-works.md new file mode 100644 index 00000000000..1a849698e1b --- /dev/null +++ b/src/help/entries/dev-how-help-works.md @@ -0,0 +1,107 @@ +--- +title: Compass Help System +tags: + - help +devOnly: true +--- + +This entry explains the help system in Compass. + +The Help system in Compass runs in a separate singleton window, like the +Connect Window. The sidebar shows a list of all available entries, sorted +alphabetically, the main view shows the title, tags, the actual help text, +and related entries. + +### How to add a help button in Compass + +To add a help button (little round info circle) anywhere in the app, go to +the `.jade` file and add a font-awesome symbol like so: + +``` +i.help(data-hook='my-help-entry') +``` + +The data-hook needs to point to a valid help entry key, in this case +`my-help-entry`. + +No additional click handlers are required in any of the view classes. This +is already handled in `./src/app.js`, see the `event` object and the +`onHelpClicked` method. + + +### How to add a help entry + +To add a help entry, add a file to the `src/help/entries` folder. +The filename must be the help entry that was used in the `i.help` +element, and the extension is `.md` for markdown, e.g. `my-help-entry.md`. + +As a convention, the first part of the entry key should be one of: + +- `connect-` for help entries related to the Connect Window +- `schema-` for help entries related to the Schema Window +- `dev-` for developer-only help entries like this one + + +##### Front Matter + +The entry content itself needs to start with a header called "front matter", +which is in YAML format. Here is an example: + +``` +--- +title: Kerberos Principal +tags: + - authentication + - connect + - kerberos +related: + - connect-kerberos-service-name +devOnly: false +--- +``` + +The only required field in the front matter is the `title` field. All other +fields are optional. + +##### Tags + +`tags` are added to the top of the article below the title, but currently +don't have any other function. We might add search by tags in a later version. + +##### Related Links + +When you add one or more related entry keys under the `related` field, a +"Related" section is automatically added at the bottom of the article, linking +to the related entries. + +##### Entries for Developers + +If the `devOnly` field is present and set to `true`, the entry receives +a special red `development` tag, indicating that this entry is meant for +internal development. All such entries are filtered out in the production +version and will not be visible to the end user. + +##### Content + +The actual content for a help entry is written in Markdown. For instructions +see Google. + +##### Images + +Images can be included in help entries as well. To add an image, copy it +to the `./images/help/` folder at the top level, and insert the image in +a help entry like so: + +``` + +``` + +### Known Issues + +- The help system currently doesn't support external links yet. +- The help window is not yet a singleton +- The help window is too big +- The help window needs styling +- The sidebar should have some hierarchy to find articles faster +- The sidebar should have a filter at the top to find articles faster +- The help system should be accessible from the Help menu diff --git a/src/help/entries/dev-taxonomy.md b/src/help/entries/dev-taxonomy.md new file mode 100644 index 00000000000..2e1a1d93e53 --- /dev/null +++ b/src/help/entries/dev-taxonomy.md @@ -0,0 +1,121 @@ +--- +title: Compass Taxonomy +tags: + - taxonomy + - needs work +devOnly: true +--- + +The [docs style guide][docs-style] has a beautifully detailed and thorough +taxonomy which covers most of the tech taxonomy and it is strongly recommended +you spend some time reviewing it. + + +## deployment + +``` +i.fa.fa-fw.fa-leaf +``` + +1 or more instances that make up a descrete unit of infrastructure. +There are 3 types of deployments: + +### standalone + +``` +i.fa.fa-fw.fa-store +``` + +Only instance is a store instance. + +### replicaset + +``` +i.fa.fa-fw.fa-replicaset +``` + +3 or more store instances and 0 or more arbiter instances. + +### cluster + +``` +i.fa.fa-fw.fa-cluster +``` + +A deployment with sharding enabled. Made up of 1 or more router instances, and general 2 or more replica sets. + +## instance + +A MongoDB process. + +### router + +``` +i.fa.fa-fw.fa-router +``` + +A mongos process. + +### config + +``` +i.fa.fa-fw.fa-config +``` + +A mongod process running as a config server for routers. + +### store + +``` +i.fa.fa-fw.fa-store +``` + +A vanilla mongod process running as part of a standlone or replica set. + +## dataset + +### database + +### collection + +### index + +### document + +## System (wip. whats it called in the kernel?) + +### Replication + +The active distributed-system providing fault-tolerance. + +#### oplog + +The element of replication that stores all operations and powers the oplog synchronization subsystem. + +#### oplog synchronization + +The subsystem by which members communicate. + +#### election + +#### member + +##### primary + +##### secondary + +##### arbiter + +### Sharding + +#### routing + +#### shard + +#### balancing + +#### chunk + +#### chunk migration + +[docs-style]: http://docs.mongodb.org/manual/meta/style-guide/#jargon-and-common-terms diff --git a/src/help/entries/schema-how-sampling-works.md b/src/help/entries/schema-how-sampling-works.md new file mode 100644 index 00000000000..316f3ab3ac8 --- /dev/null +++ b/src/help/entries/schema-how-sampling-works.md @@ -0,0 +1,7 @@ +--- +title: How Sampling Works +tags: + - schema + - sampling + - todo +--- diff --git a/src/help/entries/schema-query-bar.md b/src/help/entries/schema-query-bar.md new file mode 100644 index 00000000000..e9cb6427ee4 --- /dev/null +++ b/src/help/entries/schema-query-bar.md @@ -0,0 +1,8 @@ +--- +title: Query Bar +tags: + - schema + - sampling + - query + - todo +--- diff --git a/src/help/entries/schema-sampling-results.md b/src/help/entries/schema-sampling-results.md new file mode 100644 index 00000000000..f4fc5059692 --- /dev/null +++ b/src/help/entries/schema-sampling-results.md @@ -0,0 +1,37 @@ +--- +title: Sampling Results +tags: + - schema + - sampling + - results +related: + - schema-how-sampling-works + - schema-query-bar +--- + +The message below the Query Bar provides information about the +number of documents that matched the query, and the number of documents +used for the schema report. + + +There are two possible outcomes when you use the +[Query Bar](#schema-query-bar) to refine the results. + +1. The query you specified matches _more than_ the sampling limit (currently + 100 documents). + + In this case, Compass samples 100 documents randomly from + the matching documents, and builds a schema report based on that sample. + You will see a message that provides both the number of matched documents + and the size of the sample set. Example: + +  + +2. The query you specified matches _less than or equal to_ the sampling limit + (currently 100 documents). + + In this case, Compass uses all matched documents + to build a schema report. You will see a message that provides the number + of matched documents. Example: + +  diff --git a/src/help/index.jade b/src/help/index.jade new file mode 100644 index 00000000000..1c343878a27 --- /dev/null +++ b/src/help/index.jade @@ -0,0 +1,5 @@ +.page.help + div(data-hook='sidebar-subview') + .content.with-sidebar + h1(data-hook='help-entry-title') + .help-entry(data-hook='help-entry-content') diff --git a/src/help/index.js b/src/help/index.js new file mode 100644 index 00000000000..be975838a1b --- /dev/null +++ b/src/help/index.js @@ -0,0 +1,150 @@ +var View = require('ampersand-view'); +var format = require('util').format; +var debug = require('debug')('scout:help'); +var relatedTemplate = require('./related.jade'); +var tagTemplate = require('./tags.jade'); +var HelpEntryCollection = require('../models/help-entry-collection'); +var HelpEntry = require('../models/help-entry'); +var SidebarView = require('./sidebar'); +var ViewSwitcher = require('ampersand-view-switcher'); +var app = require('ampersand-app'); +var _ = require('lodash'); + +var entries = new HelpEntryCollection(); + +var HelpPage = View.extend({ + session: { + entryId: 'string' + }, + children: { + entry: HelpEntry + }, + derived: { + title: { + deps: ['entry.title'], + fn: function() { + var t = 'MongoDB Compass Help'; + if (this.entry.title) { + t += ': ' + this.entry.title; + } + return t; + } + } + }, + events: { + 'click a': 'onLinkClicked' + }, + bindings: { + 'entry.title': { + hook: 'help-entry-title' + }, + title: { + type: function(el, newVal) { + document.title = newVal; + } + } + }, + onLinkClicked: function(evt) { + evt.preventDefault(); + evt.stopPropagation(); + // @todo handle external links + var entryId = evt.delegateTarget.hash.slice(1); + if (entryId) { + this.show(entryId); + } + }, + template: require('./index.jade'), + initialize: function(spec) { + spec = spec || {}; + entries.fetch(); + + if (spec.entryId) { + this.entryId = spec.entryId; + debug('initialized with entryId `%s`', this.entryId); + } + this.listenTo(this, 'change:rendered', function() { + this.viewSwitcher = new ViewSwitcher(this.queryByHook('help-entry-content')); + + if (this.entryId) { + this.show(this.entryId); + } + }); + + this.listenTo(app, 'show-help-entry', this.show.bind(this)); + }, + show: function(entryId) { + debug('show `%s`', entryId); + if (entries.length === 0) { + entries.once('sync', this.show.bind(this, entryId)); + debug('entries not synced yet. queuing...'); + return; + } + + var entry = entries.get(entryId); + + if (!entry) { + debug('Unknown help entry', entryId); + this.viewSwitcher.clear(); + app.statusbar.showMessage('Help entry not found.'); + return; + } + + app.statusbar.hide(); + + if (!entries.select(entry)) { + debug('already selected'); + return; + } + + // get related entries + var relatedEntries = _(entry.related) + .map(function(relEntry) { + return entries.get(relEntry); + }) + .filter(function(relEntry) { + return relEntry; + }) + .value(); + + var view = new View({ + /** + * constructing the final help window template here, which consists of + * - surrounding
+ * - "tags" template, @see ./tags.jade + * - the content, @see ./entries/*.md files + * - "related" template, @see ./related.jade + */ + template: + '