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;
+ }
+}