diff --git a/scout-ui/package.json b/scout-ui/package.json index e5b0c953146..dcdff8fda6c 100644 --- a/scout-ui/package.json +++ b/scout-ui/package.json @@ -64,6 +64,7 @@ "jquery": "^2.1.1", "lodash": "^3.8.0", "moment": "^2.8.2", + "mongodb-extended-json": "^1.3.1", "mongodb-schema": "^2.1.0", "numeral": "^1.5.3", "octicons": "https://github.com/github/octicons/archive/v2.2.0.tar.gz", diff --git a/scout-ui/src/home/collection.jade b/scout-ui/src/home/collection.jade index f897afb9e5b..adbead08b20 100644 --- a/scout-ui/src/home/collection.jade +++ b/scout-ui/src/home/collection.jade @@ -1,7 +1,13 @@ .collection-view .header - h3(data-hook='name') - div(data-hook='stats-subview') + .container-fluid + h3(data-hook='name') + .row + .col-md-12 + div(data-hook='stats-subview') + .row + .col-md-12 + div(data-hook='refine-bar') .column-container .column.main div(data-hook='fields-subview') @@ -9,5 +15,3 @@ .splitter i.fa.fa-lg.fa-rotate-90.fa-sort div(data-hook='documents-subview') - - diff --git a/scout-ui/src/home/collection.js b/scout-ui/src/home/collection.js index 6a82bd3437e..d318e222910 100644 --- a/scout-ui/src/home/collection.js +++ b/scout-ui/src/home/collection.js @@ -4,6 +4,8 @@ var AmpersandView = require('ampersand-view'); var CollectionStatsView = require('../collection-stats'); var FieldListView = require('../field-list'); var DocumentListView = require('../document-view'); +var RefineBarView = require('../refine-view'); + var debug = require('debug')('scout-ui:home:collection'); var $ = require('jquery'); @@ -46,8 +48,14 @@ module.exports = AmpersandView.extend({ this.schema.ns = this.model._id; this.listenTo(this.schema, 'error', this.onError); + this.schema.fetch(); this.model.fetch(); + + this.listenTo(app.queryOptions, 'change', this.onQueryChanged); + }, + onQueryChanged: function() { + this.schema.refine(app.queryOptions.serialize()); }, onSplitterClick: function() { this.toggle('open'); @@ -61,10 +69,10 @@ module.exports = AmpersandView.extend({ hook: 'stats-subview', prepareView: function(el) { return new CollectionStatsView({ - el: el, - parent: this, - model: this.model - }); + el: el, + parent: this, + model: this.model + }); } }, fields: { @@ -72,10 +80,20 @@ module.exports = AmpersandView.extend({ hook: 'fields-subview', prepareView: function(el) { return new FieldListView({ - el: el, - parent: this, - collection: this.schema.fields - }); + el: el, + parent: this, + collection: this.schema.fields + }); + } + }, + refinebar: { + hook: 'refine-bar', + prepareView: function(el) { + return new RefineBarView({ + el: el, + parent: this, + model: app.queryOptions + }); } }, documents: { @@ -83,10 +101,10 @@ module.exports = AmpersandView.extend({ hook: 'documents-subview', prepareView: function(el) { return new DocumentListView({ - el: el, - parent: this, - collection: this.model.documents - }); + el: el, + parent: this, + collection: this.model.documents + }); } } } diff --git a/scout-ui/src/home/index.js b/scout-ui/src/home/index.js index a635d7a9495..03333ef9a90 100644 --- a/scout-ui/src/home/index.js +++ b/scout-ui/src/home/index.js @@ -5,6 +5,7 @@ var debug = require('debug')('scout-ui:home'); var app = require('ampersand-app'); var format = require('util').format; var SidebarView = require('../sidebar'); +var FieldListView = require('../field-list'); var CollectionView = require('./collection'); require('bootstrap/js/dropdown'); @@ -38,7 +39,7 @@ module.exports = AmpersandView.extend({ this.showCollection(current); }); - this.listenTo(this, 'change:rendered', this.onRendered); + this.once('change:rendered', this.onRendered); this.model.fetch(); }, onRendered: function() { diff --git a/scout-ui/src/home/index.less b/scout-ui/src/home/index.less index d5cc1cf4c47..287c4d4db95 100644 --- a/scout-ui/src/home/index.less +++ b/scout-ui/src/home/index.less @@ -54,12 +54,14 @@ .collection-view { + @header-height: 120px; + .header { padding-left: 20px; display: flex; position: relative; z-index: 10; - height: 100px; + height: @header-height; } .column-container { @@ -67,8 +69,8 @@ display: flex; overflow: hidden; height: 100vh; - margin-top: -100px; - padding-top: 100px; + margin-top: -@header-height; + padding-top: @header-height; position: relative; width: 100%; } diff --git a/scout-ui/src/index.js b/scout-ui/src/index.js index df86be14527..70f28c79fe1 100644 --- a/scout-ui/src/index.js +++ b/scout-ui/src/index.js @@ -10,6 +10,7 @@ var domReady = require('domready'); var ViewSwitcher = require('ampersand-view-switcher'); var qs = require('qs'); var Router = require('./router'); +var QueryOptions = require('./models/query-options'); var PageContainer = AmpersandView.extend({ template: '
', @@ -48,12 +49,14 @@ var PageContainer = AmpersandView.extend({ }); var StatusbarView = require('./statusbar'); + app.extend({ /** * init URL handlers and the history tracker. */ router: new Router(), statusbar: new StatusbarView(), + queryOptions: new QueryOptions(), currentPage: null, init: function() { domReady(function() { @@ -99,4 +102,7 @@ app.extend({ } }); +// for debugging purposes +window.app = app; + module.exports = app.init(); diff --git a/scout-ui/src/index.less b/scout-ui/src/index.less index f9229d489c3..9473f83cfff 100644 --- a/scout-ui/src/index.less +++ b/scout-ui/src/index.less @@ -4,5 +4,6 @@ // Components @import "./src/home/index.less"; @import "./src/minicharts/index.less"; +@import "./src/refine-view/index.less"; @import "./src/field-list/index.less"; @import "./src/object-tree/index.less"; diff --git a/scout-ui/src/models/index.js b/scout-ui/src/models/index.js index 24c5f7e761c..74daf03228e 100644 --- a/scout-ui/src/models/index.js +++ b/scout-ui/src/models/index.js @@ -12,14 +12,11 @@ var types = brain.types; var _ = require('underscore'); var es = require('event-stream'); var Schema = require('mongodb-schema').Schema; +var QueryOptions = require('./query-options'); // Yay! Use the API from the devtools console. window.scout = client; -// Handy debugging! Just type `data` in the devtools console to see the array -// of documents currently in the schema. -window.data = []; - // The currently active schema. window.schema = null; @@ -34,6 +31,52 @@ client.on('error', function(err) { }); var SampledSchema = Schema.extend({ + /** + * Clear any data accumulated from sampling. + */ + reset: function(options) { + this.fields.reset(); + if (this.parent && this.parent.model && this.parent.model.documents) { + this.parent.model.documents.reset(); + } + }, + /** + * After you fetch an initial sample, next you'll want to drill-down to a + * smaller slice or drill back up to look at a larger slice. + * + * @example + * schema.fetch({}); + * schema.refine({a: 1}); + * schema.refine({a: 1, b: 1}); + * schema.refine({a: 2}); + */ + refine: function(options) { + this.reset(); + this.fetch(options); + }, + /** + * Take another sample on top of what you currently have. + * + * @example + * schema.fetch({limit: 100}); + * // schema.documents.length is now 100 + * schema.more({limit: 100}); + * // schema.documents.length is now 200 + * schema.more({limit: 10}); + * // schema.documents.length is now 210 + */ + more: function(options) { + this.fetch(options); + }, + /** + * Get a sample of documents for a collection from the server. + * Really this should only be called directly from the `initialize` function + * + * @param {Object} [options] + * @option {Number} [size=100] Number of documents the sample should contain. + * @option {Object} [query={}] + * @option {Object} [fields=null] + */ fetch: function(options) { options = _.defaults((options || {}), { size: 100, @@ -44,37 +87,36 @@ var SampledSchema = Schema.extend({ wrapError(this, options); var model = this; - var collection; + window.schema = this; + + /** + * Collection of sampled documents someone else wants to keep track of. + * + * {@see scout-ui/src/home/collection.js#model} + * @todo (imlucas): Yes this is a crappy hack. + */ + var documents; if (this.parent && this.parent.model && this.parent.model.documents) { - collection = this.parent.model.documents; - collection.reset(); + documents = this.parent.model.documents; } - - window.schema = this; - window.data = []; var parser = this.stream() .on('error', function(err) { options.error(err, 'error', err.message); }) .on('data', function(doc) { - window.data.push(doc); - if (collection) { - collection.add(doc); + if (documents) { + documents.add(doc); } }) .on('end', function() { - process.nextTick(function() { - model.trigger('sync', model, model.serialize(), options); - }); + model.trigger('sync', model, model.serialize(), options); }); model.trigger('request', model, {}, options); - process.nextTick(function() { - client.sample(model.ns, options) - .on('error', parser.emit.bind(parser, 'error')) - .pipe(parser); - }); + client.sample(model.ns, options) + .on('error', parser.emit.bind(parser, 'error')) + .pipe(parser); } }); @@ -163,5 +205,6 @@ module.exports = { } }, WithScout), SampledDocumentCollection: SampledDocumentCollection, - SampledSchema: SampledSchema + SampledSchema: SampledSchema, + QueryOptions: QueryOptions }; diff --git a/scout-ui/src/models/query-options.js b/scout-ui/src/models/query-options.js new file mode 100644 index 00000000000..0bc992648a5 --- /dev/null +++ b/scout-ui/src/models/query-options.js @@ -0,0 +1,38 @@ +var AmpersandState = require('ampersand-state'); +var app = require('ampersand-app'); + +module.exports = AmpersandState.extend({ + props: { + query: { + type: 'object', + default: function() { + return {}; + } + }, + sort: { + type: 'object', + default: function() { + return { + '_id': -1 + }; + } + }, + limit: { + type: 'number', + default: 10000, + }, + skip: { + type: 'number', + default: 0 + } + }, + derived: { + queryString: { + deps: ['query'], + fn: function() { + return JSON.stringify(this.query); + } + } + } +}); + diff --git a/scout-ui/src/refine-view/index.jade b/scout-ui/src/refine-view/index.jade new file mode 100644 index 00000000000..37e0ae66403 --- /dev/null +++ b/scout-ui/src/refine-view/index.jade @@ -0,0 +1,6 @@ +.refine-view-container + form + .input-group(data-hook='refine-input-group') + input#refineInput.form-control(type='text', data-hook='refine-input') + span.input-group-btn + button.btn.btn-default(type='button', data-hook='refine-button') Refine diff --git a/scout-ui/src/refine-view/index.js b/scout-ui/src/refine-view/index.js new file mode 100644 index 00000000000..72d6411c069 --- /dev/null +++ b/scout-ui/src/refine-view/index.js @@ -0,0 +1,64 @@ +var AmpersandView = require('ampersand-view'); +var debug = require('debug')('scout-ui:refine-view:index'); +var $ = require('jquery'); +var EJSON = require('mongodb-extended-json'); + +module.exports = AmpersandView.extend({ + template: require('./index.jade'), + props: { + valid: { + type: 'boolean', + default: true + } + }, + bindings: { + 'model.queryString': { + type: 'value', + hook: 'refine-input' + }, + 'valid': [ + // red input border while query is invalid + { + type: 'booleanClass', + hook: 'refine-input-group', + yes: '', + no: 'has-error' + }, + // disable button while query is invalid + { + type: 'booleanAttribute', + hook: 'refine-button', + no: 'disabled', + yes: null + } + ] + }, + events: { + 'click [data-hook=refine-button]': 'buttonClicked', + 'input [data-hook=refine-input]': 'inputChanged', + 'submit form': 'submit', + }, + inputChanged: function(evt) { + // validate user input on the fly + var queryStr = $(this.queryByHook('refine-input')).val(); + try { + EJSON.parse(queryStr); + } catch (e) { + this.valid = false; + return; + } + this.valid = true; + }, + buttonClicked: function(evt) { + var queryStr = $(this.queryByHook('refine-input')).val(); + var queryObj = EJSON.parse(queryStr); + this.model.query = queryObj; + + // Modifying the query will reset field-list#schema and because we're using + // good ampersand, outgoing views will be removed for us automatically. + }, + submit: function (evt) { + evt.preventDefault(); + this.buttonClicked(); + } +}); diff --git a/scout-ui/src/refine-view/index.less b/scout-ui/src/refine-view/index.less new file mode 100644 index 00000000000..34939ce14c7 --- /dev/null +++ b/scout-ui/src/refine-view/index.less @@ -0,0 +1,5 @@ +.refine-view-container { + input { + font-family: "Source Code Pro", sans-serif; + } +}