From 081b3ca93530a970e8db453b9cc364468678dab7 Mon Sep 17 00:00:00 2001 From: Thomas Rueckstiess Date: Sat, 4 Jun 2016 18:09:25 +0800 Subject: [PATCH 01/25] replace schema view with component MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit adding field and type components. nested array fields working. use native-parser stream directly use schema stream ampersand model temporarily. towards react minicharts… progress on d3 component and d3 minichart fns porting d3 functions to work with react. store only runs one sampling process at a time fix bug that prevented initial rendering of d3 components sort undefined types last. make subtypes work array subtypes fully working now. detect geo coordinates (use new module) maps work, responsive minicharts. Tooltips on types working. use golden ratio for map dimensions. QueryStore, set/clear/add/remove, unique mc query builder more verbose comments for field.jsx distinct queries with few and many. correct range/single click queries on few/many. performance could be better. also try react-d3-library. move helpers to util use throttled store methods to improve performance fixed “many”. fully working for distinct queries. fixed few. re-enable brush background on “many” charts. adding setRangeValue, setOrUnsetValue bump react@15.2.0, hadron-compile-cache@0.2.0 make unsetIfSet an option for query actions implemented query building on ranges (numeric) fix resizing bug in distinct many charts was re-creating many() charts on every redraw… querybuilder for date/objectid and geo fix svg unique bug, full path name for query building adding internal status bar package status component and early integration progress bar for sampling. needs some work. status / progress bar fixed for schema view. remove old statusbar folder, schema model added QueryBar component, moved query store + utils query validation, query bar backwards pass. apply and reset actions hooked up with status and schema query bar: enter also applies valid queries. make all query builder actions sync, better performance clean up schema component. status subview, schema actions, maxtimems --- package.json | 8 +- src/app/connect/index.js | 8 +- src/app/home/collection.js | 80 +-- src/app/home/index.js | 2 + src/app/index.js | 41 +- src/app/index.less | 2 +- src/app/minicharts/d3fns/few.js | 9 +- src/app/minicharts/index.less | 8 +- src/app/refine-view/index.js | 22 +- src/app/schema/field-list.jade | 1 - src/app/schema/field-list.js | 158 ------ src/app/schema/field.jade | 11 - src/app/schema/index.js | 325 ++++++------ src/app/schema/index.less | 2 - src/app/schema/type-list-item.jade | 9 - src/app/schema/type-list.jade | 1 - src/app/schema/type-list.js | 180 ------- src/help/index.js | 5 +- src/internal-packages/.eslintrc | 15 + src/internal-packages/query/index.js | 19 + .../query/lib/action/index.jsx | 77 +++ .../query/lib/component/index.jsx | 35 ++ .../query/lib/component/input-form.jsx | 83 +++ .../query/lib/store/index.jsx | 364 +++++++++++++ src/internal-packages/query/lib/util/index.js | 162 ++++++ src/internal-packages/query/package.json | 9 + src/internal-packages/query/styles/index.less | 1 + .../query/test/index.test.js | 9 + .../query/test/ranges.test.js | 197 +++++++ src/internal-packages/schema/index.js | 21 + .../schema/lib/action/index.jsx | 22 + .../schema/lib/component/d3component.jsx | 97 ++++ .../schema/lib/component/field.jsx | 191 +++++++ .../schema/lib/component/minichart.jsx | 128 +++++ .../schema/lib/component/schema.jsx | 92 ++++ .../status-subview/buttons-error.jsx | 77 +++ .../status-subview/buttons-waiting.jsx | 53 ++ .../lib/component/status-subview/index.jsx | 42 ++ .../lib/component/status-subview/steps.jsx | 91 ++++ .../schema/lib/component/type.jsx | 141 +++++ .../schema/lib/component/unique.jsx | 120 +++++ .../schema/lib/d3/boolean.js | 88 ++++ .../schema/lib/d3/coordinates.js | 488 ++++++++++++++++++ src/internal-packages/schema/lib/d3/d3-tip.js | 349 +++++++++++++ src/internal-packages/schema/lib/d3/date.js | 427 +++++++++++++++ src/internal-packages/schema/lib/d3/few.js | 253 +++++++++ src/internal-packages/schema/lib/d3/index.js | 8 + src/internal-packages/schema/lib/d3/many.js | 482 +++++++++++++++++ .../schema/lib/d3/mapstyle.js | 89 ++++ src/internal-packages/schema/lib/d3/number.js | 136 +++++ src/internal-packages/schema/lib/d3/shared.js | 46 ++ src/internal-packages/schema/lib/d3/string.js | 91 ++++ .../schema/lib/d3/tooltip.jade | 3 + .../schema/lib/store/schema-store.jsx | 201 ++++++++ src/internal-packages/schema/package.json | 9 + src/internal-packages/schema/util | 0 src/internal-packages/status/index.js | 21 + .../status/lib/actions/index.js | 112 ++++ .../status/lib/components/status.jsx | 82 +++ .../status/lib/stores/status-store.jsx | 203 ++++++++ src/internal-packages/status/package.json | 9 + .../status/styles}/index.less | 0 62 files changed, 5436 insertions(+), 579 deletions(-) delete mode 100644 src/app/schema/field-list.jade delete mode 100644 src/app/schema/field-list.js delete mode 100644 src/app/schema/field.jade delete mode 100644 src/app/schema/type-list-item.jade delete mode 100644 src/app/schema/type-list.jade delete mode 100644 src/app/schema/type-list.js create mode 100644 src/internal-packages/.eslintrc create mode 100644 src/internal-packages/query/index.js create mode 100644 src/internal-packages/query/lib/action/index.jsx create mode 100644 src/internal-packages/query/lib/component/index.jsx create mode 100644 src/internal-packages/query/lib/component/input-form.jsx create mode 100644 src/internal-packages/query/lib/store/index.jsx create mode 100644 src/internal-packages/query/lib/util/index.js create mode 100644 src/internal-packages/query/package.json create mode 100644 src/internal-packages/query/styles/index.less create mode 100644 src/internal-packages/query/test/index.test.js create mode 100644 src/internal-packages/query/test/ranges.test.js create mode 100644 src/internal-packages/schema/index.js create mode 100644 src/internal-packages/schema/lib/action/index.jsx create mode 100644 src/internal-packages/schema/lib/component/d3component.jsx create mode 100644 src/internal-packages/schema/lib/component/field.jsx create mode 100644 src/internal-packages/schema/lib/component/minichart.jsx create mode 100644 src/internal-packages/schema/lib/component/schema.jsx create mode 100644 src/internal-packages/schema/lib/component/status-subview/buttons-error.jsx create mode 100644 src/internal-packages/schema/lib/component/status-subview/buttons-waiting.jsx create mode 100644 src/internal-packages/schema/lib/component/status-subview/index.jsx create mode 100644 src/internal-packages/schema/lib/component/status-subview/steps.jsx create mode 100644 src/internal-packages/schema/lib/component/type.jsx create mode 100644 src/internal-packages/schema/lib/component/unique.jsx create mode 100644 src/internal-packages/schema/lib/d3/boolean.js create mode 100644 src/internal-packages/schema/lib/d3/coordinates.js create mode 100644 src/internal-packages/schema/lib/d3/d3-tip.js create mode 100644 src/internal-packages/schema/lib/d3/date.js create mode 100644 src/internal-packages/schema/lib/d3/few.js create mode 100644 src/internal-packages/schema/lib/d3/index.js create mode 100644 src/internal-packages/schema/lib/d3/many.js create mode 100644 src/internal-packages/schema/lib/d3/mapstyle.js create mode 100644 src/internal-packages/schema/lib/d3/number.js create mode 100644 src/internal-packages/schema/lib/d3/shared.js create mode 100644 src/internal-packages/schema/lib/d3/string.js create mode 100644 src/internal-packages/schema/lib/d3/tooltip.jade create mode 100644 src/internal-packages/schema/lib/store/schema-store.jsx create mode 100644 src/internal-packages/schema/package.json create mode 100644 src/internal-packages/schema/util create mode 100644 src/internal-packages/status/index.js create mode 100644 src/internal-packages/status/lib/actions/index.js create mode 100644 src/internal-packages/status/lib/components/status.jsx create mode 100644 src/internal-packages/status/lib/stores/status-store.jsx create mode 100644 src/internal-packages/status/package.json rename src/{app/statusbar => internal-packages/status/styles}/index.less (100%) diff --git a/package.json b/package.json index 4d65efc54ad..08d8867fa50 100644 --- a/package.json +++ b/package.json @@ -98,6 +98,7 @@ "debug": "mongodb-js/debug#v2.2.3", "debug-menu": "^0.3.0", "electron-squirrel-startup": "^1.0.0", + "detect-coordinates": "^0.1.0", "font-awesome": "https://github.com/FortAwesome/Font-Awesome/archive/v4.4.0.tar.gz", "get-object-path": "azer/get-object-path#74eb42de0cfd02c14ffdd18552f295aba723d394", "hadron-action": "^0.1.0", @@ -124,7 +125,7 @@ "mongodb-data-service": "^1.1.1", "mongodb-database-model": "^0.1.2", "mongodb-explain-plan-model": "^0.2.0", - "mongodb-extended-json": "^1.6.0", + "mongodb-extended-json": "^1.7.0", "mongodb-js-metrics": "^1.2.0", "mongodb-language-model": "^0.3.3", "mongodb-ns": "^1.0.3", @@ -138,6 +139,10 @@ "react": "^15.2.1", "react-bootstrap": "0.29.5", "react-dom": "^15.2.1", + "react-native-listener": "^1.0.1", + "react-tooltip": "^2.0.3", + "reflux": "0.4.1", + "reflux-state-mixin": "^0.7.0", "semver": "^5.1.0", "storage-mixin": "^0.8.0", "tunnel-ssh": "^3.2.1-beta", @@ -147,6 +152,7 @@ "uuid": "^2.0.1" }, "devDependencies": { + "babel-eslint": "^6.0.4", "chai": "^3.4.1", "chai-as-promised": "^5.1.0", "electron-prebuilt": "1.2.8", diff --git a/src/app/connect/index.js b/src/app/connect/index.js index 9ac5cb17568..bb25328ac58 100644 --- a/src/app/connect/index.js +++ b/src/app/connect/index.js @@ -3,7 +3,7 @@ var ConnectFormView = require('./connect-form-view'); var Connection = require('../models/connection'); var ConnectionCollection = require('../models/connection-collection'); var MongoDBConnection = require('mongodb-connection-model'); - +var StatusActions = require('../../internal-packages/status/lib/actions'); var SidebarWrapperView = require('./sidebar'); var View = require('ampersand-view'); @@ -428,7 +428,7 @@ var ConnectView = View.extend({ this.dispatch('error received'); return; } - app.statusbar.show(); + StatusActions.showIndeterminateProgressBar(); var onSave = function() { this.connections.add(this.connection, { merge: true }); @@ -437,8 +437,8 @@ var ConnectView = View.extend({ }; connection.test(function(err) { - app.statusbar.hide(); if (!err) { + StatusActions.hide(); // now save connection this.connection = connection; this.connection.save({ last_used: new Date() }, { success: onSave.bind(this) }); @@ -459,7 +459,7 @@ var ConnectView = View.extend({ */ useConnection: function(connection) { connection = connection || this.connection; - app.statusbar.hide(); + StatusActions.hide(); metrics.track('Connection', 'used', { authentication: connection.authentication, ssl: connection.ssl, diff --git a/src/app/home/collection.js b/src/app/home/collection.js index 27a9ed458e2..ffdac97230e 100644 --- a/src/app/home/collection.js +++ b/src/app/home/collection.js @@ -3,9 +3,10 @@ var CollectionStatsView = require('../collection-stats'); var DocumentView = require('../documents'); var SchemaView = require('../schema'); var IndexView = require('../indexes'); -var RefineBarView = require('../refine-view'); var ExplainView = require('../explain-plan'); var MongoDBCollection = require('../models/mongodb-collection'); +var React = require('react'); +var ReactDOM = require('react-dom'); var NamespaceStore = require('hadron-reflux-store').NamespaceStore; var _ = require('lodash'); @@ -15,6 +16,14 @@ var debug = require('debug')('mongodb-compass:home:collection'); var collectionTemplate = require('./collection.jade'); +// map tab label to correct view and switch views +var tabToViewMap = { + 'DOCUMENTS': 'documentView', + 'SCHEMA': 'schemaView', + 'EXPLAIN PLAN': 'explainView', + 'INDEXES': 'indexView' +}; + var MongoDBCollectionView = View.extend({ // modelType: 'Collection', template: collectionTemplate, @@ -68,6 +77,7 @@ var MongoDBCollectionView = View.extend({ }, documentView: { hook: 'document-subview', + waitFor: 'ns', prepareView: function(el) { return new DocumentView({ el: el, @@ -78,6 +88,7 @@ var MongoDBCollectionView = View.extend({ }, schemaView: { hook: 'schema-subview', + waitFor: 'ns', prepareView: function(el) { return new SchemaView({ el: el, @@ -88,6 +99,7 @@ var MongoDBCollectionView = View.extend({ }, indexView: { hook: 'index-subview', + waitFor: 'ns', prepareView: function(el) { return new IndexView({ el: el, @@ -105,52 +117,54 @@ var MongoDBCollectionView = View.extend({ model: this.model }); } - }, - refineBarView: { - hook: 'refine-bar-subview', - prepareView: function(el) { - var view = new RefineBarView({ - el: el, - parent: this, - queryOptions: app.queryOptions, - volatileQueryOptions: app.volatileQueryOptions - }); - view.on('submit', function() { - this.trigger('submit:query'); - }.bind(this)); - return view; - } } + // refineBarView: { + // hook: 'refine-bar-subview', + // prepareView: function(el) { + // var view = new RefineBarView({ + // el: el, + // parent: this, + // queryOptions: app.queryOptions, + // volatileQueryOptions: app.volatileQueryOptions + // }); + // view.on('submit', function() { + // this.trigger('submit:query'); + // }.bind(this)); + // return view; + // } + // } }, initialize: function() { this.model = new MongoDBCollection(); - this.listenToAndRun(this.parent, 'change:ns', this.onCollectionChanged.bind(this)); + NamespaceStore.listen( this.onCollectionChanged.bind(this) ); + // this.listenToAndRun(this.parent, 'change:ns', this.onCollectionChanged.bind(this)); + }, + render: function() { + this.renderWithTemplate(this); + // render query bar here for now + var queryBarComponent = app.componentRegistry.findByRole('App:QueryBar')[0]; + ReactDOM.render(React.createElement(queryBarComponent), this.queryByHook('refine-bar-subview')); }, onTabClicked: function(e) { e.preventDefault(); e.stopPropagation(); - - // map tab label to correct view and switch views - var tabToViewMap = { - 'DOCUMENTS': 'documentView', - 'SCHEMA': 'schemaView', - 'EXPLAIN PLAN': 'explainView', - 'INDEXES': 'indexView' - }; this.switchView(tabToViewMap[e.target.innerText]); }, switchView: function(viewStr) { + debug('switching to', viewStr); // disable all views but the active one - _.each(this._subviews, function(subview) { - subview.visible = false; - }); - if (this[viewStr]) { - this[viewStr].visible = true; - } this.activeView = viewStr; + _.each(_.values(tabToViewMap), function(subview) { + if (!this[subview]) return; + if (subview === viewStr) { + this[viewStr].el.classList.remove('hidden'); + } else { + this[subview].el.classList.add('hidden'); + } + }.bind(this)); }, onCollectionChanged: function() { - this.ns = this.parent.ns; + this.ns = NamespaceStore.ns; if (!this.ns) { this.visible = false; debug('No active collection namespace so no schema has been requested yet.'); @@ -158,8 +172,6 @@ var MongoDBCollectionView = View.extend({ } this.visible = true; this.model._id = this.ns; - // Need to keep the global state in sync. - NamespaceStore.ns = this.ns; this.model.once('sync', this.onCollectionFetched.bind(this)); this.model.fetch(); }, diff --git a/src/app/home/index.js b/src/app/home/index.js index 3f5cf908a92..983688d00e6 100644 --- a/src/app/home/index.js +++ b/src/app/home/index.js @@ -5,6 +5,7 @@ var IdentifyView = require('../identify'); var CollectionView = require('./collection'); var InstancePropertyView = require('./instance-properties'); var CollectionListItemView = require('./collection-list-item'); +var NamespaceStore = require('hadron-reflux-store').NamespaceStore; var TourView = require('../tour'); var NetworkOptInView = require('../network-optin'); var app = require('ampersand-app'); @@ -141,6 +142,7 @@ var HomeView = View.extend({ } this.ns = model.getId(); + NamespaceStore.ns = ns.ns; this.updateTitle(model); this.showNoCollectionsZeroState = false; this.showDefaultZeroState = false; diff --git a/src/app/index.js b/src/app/index.js index 5bce8a457f8..e2c9c793d77 100644 --- a/src/app/index.js +++ b/src/app/index.js @@ -50,11 +50,15 @@ var Preferences = require('./models/preferences'); var ApplicationStore = require('hadron-reflux-store').ApplicationStore; var User = require('./models/user'); var Router = require('./router'); -var Statusbar = require('./statusbar'); +// var Statusbar = require('./statusbar'); var migrateApp = require('./migrations'); var metricsSetup = require('./metrics'); var metrics = require('mongodb-js-metrics')(); +var StatusActions = require('../internal-packages/status/lib/actions'); + +var React = require('react'); +var ReactDOM = require('react-dom'); var AutoUpdate = require('../auto-update'); var addInspectElementMenu = require('debug-menu').install; @@ -254,7 +258,7 @@ var Application = View.extend({ pushState: false, root: '/' }); - app.statusbar.hide(); + StatusActions.hide(); }, onFatalError: function(id, err) { debug('clearing client stall timeout...'); @@ -262,7 +266,7 @@ var Application = View.extend({ console.error('Fatal Error!: ', id, err); metrics.error(err); - app.statusbar.fatal(err); + StatusActions.setMessage(err); }, // ms we'll wait for a `mongodb-scope-client` instance // to become readable before giving up and showing @@ -317,10 +321,13 @@ var Application = View.extend({ }); debug('rendering statusbar...'); - this.statusbar = new Statusbar({ - el: this.queryByHook('statusbar') - }); - this.statusbar.render(); + // this.statusbar = new Statusbar({ + // el: this.queryByHook('statusbar') + // }); + // this.statusbar.render(); + + this.statusComponent = app.componentRegistry.findByRole('App:Status')[0]; + ReactDOM.render(React.createElement(this.statusComponent), this.queryByHook('statusbar')); this.autoUpdate = new AutoUpdate({ el: this.queryByHook('auto-update') @@ -377,9 +384,11 @@ app.extend({ return; } - app.statusbar.show({ + StatusActions.configure({ message: 'Retrieving connection details...', - staticSidebar: true + progressbar: true, + progress: 100, + sidebar: true }); state.connection = new Connection({ @@ -392,9 +401,7 @@ app.extend({ state.onFatalError('fetch connection', err); return; } - app.statusbar.show({ - message: 'Connecting to MongoDB...' - }); + StatusActions.setMessage('Connecting to MongoDB...'); var DataService = require('mongodb-data-service'); app.dataService = new DataService(state.connection) @@ -434,11 +441,11 @@ app.extend({ } }); -Object.defineProperty(app, 'statusbar', { - get: function() { - return state.statusbar; - } -}); +// Object.defineProperty(app, 'statusbar', { +// get: function() { +// return state.statusbar; +// } +// }); Object.defineProperty(app, 'autoUpdate', { get: function() { diff --git a/src/app/index.less b/src/app/index.less index 7329ff02206..f0e3672c472 100644 --- a/src/app/index.less +++ b/src/app/index.less @@ -12,7 +12,6 @@ @import "minicharts/index.less"; @import "refine-view/index.less"; @import "sampling-message/index.less"; -@import "statusbar/index.less"; @import "tour/index.less"; @import "sidebar/index.less"; @import "network-optin/index.less"; @@ -26,3 +25,4 @@ @import "./styles/mapbox-gl.css"; @import "../internal-packages/crud/styles/crud.less"; +@import "../internal-packages/status/styles/index.less"; diff --git a/src/app/minicharts/d3fns/few.js b/src/app/minicharts/d3fns/few.js index c4d8efa6949..5fbe03b2869 100644 --- a/src/app/minicharts/d3fns/few.js +++ b/src/app/minicharts/d3fns/few.js @@ -145,7 +145,14 @@ var minicharts_d3fns_few = function() { function chart(selection) { selection.each(function(data) { - var values = _.pluck(data, 'count'); + var values = _.map(data, 'count'); + _.each(data, (d, i) => { + data[i].xpos = _.sum(_(data) + .slice(0, i) + .map('count') + .value() + ); + }); var sumValues = d3.sum(values); var maxValue = d3.max(values); var percentFormat = shared.friendlyPercentFormat(maxValue / sumValues * 100); diff --git a/src/app/minicharts/index.less b/src/app/minicharts/index.less index f424fc4a60c..9e201684a46 100644 --- a/src/app/minicharts/index.less +++ b/src/app/minicharts/index.less @@ -144,9 +144,11 @@ svg.minichart { &.selected { fill: @mc-fg-selected; - &.half { - mask: url(#mask-stripe); - } + } + + &.half-selected { + fill: @mc-fg-selected; + mask: url(#mask-stripe); } &.unselected { diff --git a/src/app/refine-view/index.js b/src/app/refine-view/index.js index af5c5f057db..e13b4924261 100644 --- a/src/app/refine-view/index.js +++ b/src/app/refine-view/index.js @@ -3,13 +3,14 @@ var _ = require('lodash'); var AmpersandView = require('ampersand-view'); var EditableQuery = require('../models/editable-query'); var EJSON = require('mongodb-extended-json'); +var QueryStore = require('../../internal-packages/schema/lib/store'); var Query = require('mongodb-language-model').Query; var QueryOptions = require('../models/query-options'); var app = require('ampersand-app'); var metrics = require('mongodb-js-metrics')(); // var metrics = require('mongodb-js-metrics')(); -// var debug = require('debug')('scout:refine-view:index'); +var debug = require('debug')('scout:refine-view:index'); var indexTemplate = require('./index.jade'); @@ -92,17 +93,20 @@ module.exports = AmpersandView.extend({ 'submit form': 'submit' }, initialize: function() { - this.volatileQuery = this.volatileQueryOptions.query; - this.listenToAndRun(this.volatileQueryOptions, 'change:query', this.updateQueryListener); + // this.volatileQuery = this.volatileQueryOptions.query; + // this.listenToAndRun(this.volatileQueryOptions, 'change:query', this.updateQueryListener); + QueryStore.listen(this.onQueryBufferChanged.bind(this)); }, updateQueryListener: function() { - this.stopListening(this.volatileQuery, 'change:buffer', this.onQueryBufferChanged); - this.volatileQuery = this.volatileQueryOptions.query; - this.listenTo(this.volatileQuery, 'change:buffer', this.onQueryBufferChanged); - this.editableQuery.rawString = this.volatileQueryOptions.queryString; + // this.stopListening(this.volatileQuery, 'change:buffer', this.onQueryBufferChanged); + // this.volatileQuery = this.volatileQueryOptions.query; + // this.listenTo(this.volatileQuery, 'change:buffer', this.onQueryBufferChanged); + // this.editableQuery.rawString = this.volatileQueryOptions.queryString; }, - onQueryBufferChanged: function() { - this.editableQuery.rawString = EJSON.stringify(this.volatileQuery.serialize()); + onQueryBufferChanged: function(store) { + debug('store', store); + this.editableQuery.rawString = EJSON.stringify(store.query); + // this.editableQuery.rawString = EJSON.stringify(this.volatileQuery.serialize()); }, /** * when user changes the text in the input field, copy the value into editableQuery. If the diff --git a/src/app/schema/field-list.jade b/src/app/schema/field-list.jade deleted file mode 100644 index fd44592d995..00000000000 --- a/src/app/schema/field-list.jade +++ /dev/null @@ -1 +0,0 @@ -.schema-field-list(data-hook='fields') diff --git a/src/app/schema/field-list.js b/src/app/schema/field-list.js deleted file mode 100644 index 7cd5ce67a73..00000000000 --- a/src/app/schema/field-list.js +++ /dev/null @@ -1,158 +0,0 @@ -var View = require('ampersand-view'); -var TypeListView = require('./type-list'); -var MinichartView = require('../minicharts'); -var ViewSwitcher = require('ampersand-view-switcher'); -var $ = require('jquery'); -var _ = require('lodash'); -var raf = require('raf'); -var SampledSchema = require('../models/sampled-schema'); - -var fieldTemplate = require('./field.jade'); -var fieldListTemplate = require('./field-list.jade'); -// var debug = require('debug')('mongodb-compass:schema:field-list'); - -function handleCaret(el) { - var $el = $(el); - // only apply to own caret, not children carets - if ($el.next().text() !== this.model.name) { - return; - } - if (this.model.fields || this.model.arrayFields) { - $el.addClass('caret'); - $el.next().css('cursor', 'pointer'); - } else { - $el.removeClass('caret'); - } -} - -var FieldListView; - -var FieldView = View.extend({ - modelType: 'FieldView', - session: { - expanded: { - type: 'boolean', - default: false - }, - type_model: 'state', - visible: { - type: 'boolean', - default: false - }, - minichartView: 'any' - }, - bindings: { - 'model.name': { - hook: 'name' - }, - 'model.fields': { - type: handleCaret, - hook: 'caret' - }, - 'model.arrayFields': { - type: handleCaret, - hook: 'caret' - }, - expanded: { - type: 'booleanClass', - yes: 'expanded', - no: 'collapsed' - }, - visible: { - type: 'booleanClass', - no: 'hidden' - } - }, - events: { - 'click .schema-field-name': 'click' - }, - template: fieldTemplate, - subviews: { - types: { - hook: 'types-subview', - waitFor: 'visible', - prepareView: function(el) { - return new TypeListView({ - el: el, - parent: this, - collection: this.model.types - }); - } - }, - fieldListView: { - hook: 'fields-subview', - waitFor: 'model.fields', - prepareView: function(el) { - return new FieldListView({ - el: el, - parent: this, - collection: this.model.fields - }); - } - }, - arrayFieldListView: { - hook: 'arrayfields-subview', - waitFor: 'model.arrayFields', - prepareView: function(el) { - return new FieldListView({ - el: el, - parent: this, - collection: this.model.arrayFields - }); - } - } - }, - initialize: function() { - this.listenTo(this, 'change:visible', this.renderMinicharts); - }, - render: function() { - this.renderWithTemplate(this); - this.viewSwitcher = new ViewSwitcher(this.queryByHook('minichart-container')); - }, - renderMinicharts: function() { - if (!this.type_model) { - this.type_model = this.model.types.at(0); - } - this.minichartView = new MinichartView({ - model: this.type_model, - parent: this - }); - this.viewSwitcher.set(this.minichartView); - }, - click: function(evt) { - this.toggle('expanded'); - evt.preventDefault(); - evt.stopPropagation(); - } -}); - -FieldListView = View.extend({ - modelType: 'FieldListView', - session: { - fieldCollectionView: 'object' - }, - template: fieldListTemplate, - initialize: function() { - if (this.collection.parent instanceof SampledSchema) { - this.listenTo(this.collection.parent, 'sync', this.makeFieldVisible); - } else { - // lazy rendering of nested subviews - this.listenTo(this.parent, 'change:expanded', this.makeFieldVisible); - } - }, - makeFieldVisible: function() { - var views = this.fieldCollectionView.views; - _.each(_.filter(views, 'visible', false), function(fieldView) { - raf(function() { - fieldView.visible = true; - }); - }); - }, - render: function() { - this.renderWithTemplate(); - this.fieldCollectionView = this.renderCollection(this.collection, - FieldView, this.queryByHook('fields')); - } -}); - -module.exports = FieldListView; diff --git a/src/app/schema/field.jade b/src/app/schema/field.jade deleted file mode 100644 index 7e2c9c95d7e..00000000000 --- a/src/app/schema/field.jade +++ /dev/null @@ -1,11 +0,0 @@ -.schema-field.schema-field-basic - .row - .col-sm-4 - .schema-field-name - span(data-hook='caret') - span(data-hook='name') - div(data-hook='types-subview') - .col-sm-7.col-sm-offset-1 - div(data-hook='minichart-container') - div(data-hook='fields-subview') - div(data-hook='arrayfields-subview') \ No newline at end of file diff --git a/src/app/schema/index.js b/src/app/schema/index.js index 1cfee526180..3909965e70f 100644 --- a/src/app/schema/index.js +++ b/src/app/schema/index.js @@ -1,166 +1,201 @@ var View = require('ampersand-view'); -var FieldListView = require('./field-list.js'); -var SampledSchema = require('../models/sampled-schema'); -var SamplingMessageView = require('../sampling-message'); var app = require('ampersand-app'); -var electron = require('electron'); -var remote = electron.remote; -var dialog = remote.dialog; -var BrowserWindow = remote.BrowserWindow; -var clipboard = remote.clipboard; -var format = require('util').format; -var metrics = require('mongodb-js-metrics')(); -var ipc = require('hadron-ipc'); -var eJSON = require('mongodb-extended-json'); -var debug = require('debug')('mongodb-compass:schema:index'); +var React = require('react'); +var ReactDOM = require('react-dom'); -var indexTemplate = require('./index.jade'); +// var debug = require('debug')('mongodb-compass:schema'); var SchemaView = View.extend({ - // modelType: 'Collection', - template: indexTemplate, + template: require('./index.jade'), props: { - visible: { + loading: { type: 'boolean', default: false - }, - sampling: { - type: 'boolean', - default: false - }, - hasRefineBar: ['boolean', true, true] - }, - derived: { - is_empty: { - deps: ['schema.sample_size', 'schema.is_fetching'], - fn: function() { - return this.schema.sample_size === 0 && !this.schema.is_fetching; - } } }, - events: { - 'click .splitter': 'onSplitterClick' - }, bindings: { - visible: { - type: 'booleanClass', - no: 'hidden' - }, - is_empty: [ - { - hook: 'empty', - type: 'booleanClass', - no: 'hidden' - }, - { - hook: 'column-container', - type: 'booleanClass', - yes: 'hidden' - } - ] - }, - children: { - schema: SampledSchema + loading: { + type: 'toggle', + hook: 'loading' + } }, initialize: function() { - this.listenTo(this.schema, 'sync error', this.schemaIsSynced.bind(this)); - this.listenTo(this.schema, 'request', this.schemaIsRequested.bind(this)); - this.listenTo(this.model, 'sync', this.onCollectionFetched.bind(this)); - this.listenTo(this.parent, 'submit:query', this.onQueryChanged.bind(this)); - this.on('change:visible', this.onVisibleChanged.bind(this)); - - ipc.on('window:menu-share-schema-json', this.onShareSchema.bind(this)); + this.schemaView = app.componentRegistry.findByRole('Collection:Schema')[0]; }, render: function() { - this.renderWithTemplate(this); + this.renderWithTemplate(); + ReactDOM.render(React.createElement(this.schemaView), this.queryByHook('fields-subview')); }, - schemaIsSynced: function() { - // only listen to share menu events if we have a sync'ed schema - this.sampling = false; - ipc.call('window:show-share-submenu'); - }, - schemaIsRequested: function() { - ipc.call('window:hide-share-submenu'); - this.sampling = true; - }, - onVisibleChanged: function() { - if (this.visible) { - this.parent.refineBarView.visible = this.hasRefineBar; - } - if (this.visible && this.sampling) { - app.statusbar.visible = true; - } else { - app.statusbar.visible = false; - } - }, - onShareSchema: function() { - clipboard.writeText(eJSON.stringify(this.schema.serialize(), null, ' ')); - - var detail = format('The schema definition of %s has been copied to your ' - + 'clipboard in JSON format.', this.model._id); - - dialog.showMessageBox(BrowserWindow.getFocusedWindow(), { - type: 'info', - message: 'Share Schema', - detail: detail, - buttons: ['OK'] - }); - - metrics.track('Share Schema', 'used'); - }, - onCollectionFetched: function() { - debug('collection fetched in schema'); - // track collection information - - // @todo thomasr fix this, maybe this.model._id ? - // if (!ns) { - // this.visible = false; - // debug('No active collection namespace so no schema has been requested yet.'); - // return; - // } - - // this.visible = true; - app.queryOptions.reset(); - app.volatileQueryOptions.reset(); - - this.schema.ns = this.model._id; - this.schema.reset(); - var options = app.volatileQueryOptions.serialize(); - if (this.visible) { - app.statusbar.visible = true; - } - this.schema.fetch(options); - }, - onQueryChanged: function() { - var options = app.queryOptions.serialize(); - if (this.visible) { - app.statusbar.visible = true; - } - this.schema.refine(options); - }, - subviews: { - sampling_message: { - hook: 'sampling-message-subview', - prepareView: function(el) { - return new SamplingMessageView({ - el: el, - parent: this, - model: this.schema - }); - } - }, - fields: { - hook: 'fields-subview', - prepareView: function(el) { - return new FieldListView({ - el: el, - parent: this, - collection: this.schema.fields - }); - } - } + remove: function() { + View.prototype.remove.call(this); } }); module.exports = SchemaView; + +// var View = require('ampersand-view'); +// var FieldListView = require('./field-list.js'); +// var SampledSchema = require('../models/sampled-schema'); +// var SamplingMessageView = require('../sampling-message'); +// var app = require('ampersand-app'); +// var electron = require('electron'); +// var remote = electron.remote; +// var dialog = remote.dialog; +// var BrowserWindow = remote.BrowserWindow; +// var clipboard = remote.clipboard; +// var format = require('util').format; +// var metrics = require('mongodb-js-metrics')(); +// var ipc = require('hadron-ipc'); +// +// var debug = require('debug')('mongodb-compass:schema:index'); +// +// var indexTemplate = require('./index.jade'); +// +// var SchemaView = View.extend({ +// // modelType: 'Collection', +// template: indexTemplate, +// props: { +// visible: { +// type: 'boolean', +// default: false +// }, +// sampling: { +// type: 'boolean', +// default: false +// }, +// hasRefineBar: ['boolean', true, true] +// }, +// derived: { +// is_empty: { +// deps: ['schema.sample_size', 'schema.is_fetching'], +// fn: function() { +// return this.schema.sample_size === 0 && !this.schema.is_fetching; +// } +// } +// }, +// events: { +// 'click .splitter': 'onSplitterClick' +// }, +// bindings: { +// visible: { +// type: 'booleanClass', +// no: 'hidden' +// }, +// is_empty: [ +// { +// hook: 'empty', +// type: 'booleanClass', +// no: 'hidden' +// }, +// { +// hook: 'column-container', +// type: 'booleanClass', +// yes: 'hidden' +// } +// ] +// }, +// children: { +// schema: SampledSchema +// }, +// initialize: function() { +// this.listenTo(this.schema, 'sync error', this.schemaIsSynced.bind(this)); +// this.listenTo(this.schema, 'request', this.schemaIsRequested.bind(this)); +// this.listenTo(this.model, 'sync', this.onCollectionFetched.bind(this)); +// this.listenTo(this.parent, 'submit:query', this.onQueryChanged.bind(this)); +// this.on('change:visible', this.onVisibleChanged.bind(this)); +// +// ipc.on('window:menu-share-schema-json', this.onShareSchema.bind(this)); +// }, +// render: function() { +// this.renderWithTemplate(this); +// }, +// schemaIsSynced: function() { +// // only listen to share menu events if we have a sync'ed schema +// this.sampling = false; +// ipc.call('window:show-share-submenu'); +// }, +// schemaIsRequested: function() { +// ipc.call('window:hide-share-submenu'); +// this.sampling = true; +// }, +// onVisibleChanged: function() { +// if (this.visible) { +// this.parent.refineBarView.visible = this.hasRefineBar; +// } +// if (this.visible && this.sampling) { +// app.statusbar.visible = true; +// } else { +// app.statusbar.visible = false; +// } +// }, +// onShareSchema: function() { +// clipboard.writeText(JSON.stringify(this.schema.serialize(), null, ' ')); +// +// var detail = format('The schema definition of %s has been copied to your ' +// + 'clipboard in JSON format.', this.model._id); +// +// dialog.showMessageBox(BrowserWindow.getFocusedWindow(), { +// type: 'info', +// message: 'Share Schema', +// detail: detail, +// buttons: ['OK'] +// }); +// +// metrics.track('Share Schema', 'used'); +// }, +// onCollectionFetched: function() { +// debug('collection fetched in schema'); +// // track collection information +// +// // @todo thomasr fix this, maybe this.model._id ? +// // if (!ns) { +// // this.visible = false; +// // debug('No active collection namespace so no schema has been requested yet.'); +// // return; +// // } +// +// // this.visible = true; +// app.queryOptions.reset(); +// app.volatileQueryOptions.reset(); +// +// this.schema.ns = this.model._id; +// this.schema.reset(); +// var options = app.volatileQueryOptions.serialize(); +// if (this.visible) { +// app.statusbar.visible = true; +// } +// this.schema.fetch(options); +// }, +// onQueryChanged: function() { +// var options = app.queryOptions.serialize(); +// if (this.visible) { +// app.statusbar.visible = true; +// } +// this.schema.refine(options); +// }, +// subviews: { +// sampling_message: { +// hook: 'sampling-message-subview', +// prepareView: function(el) { +// return new SamplingMessageView({ +// el: el, +// parent: this, +// model: this.schema +// }); +// } +// }, +// fields: { +// hook: 'fields-subview', +// prepareView: function(el) { +// return new FieldListView({ +// el: el, +// parent: this, +// collection: this.schema.fields +// }); +// } +// } +// } +// }); +// +// module.exports = SchemaView; diff --git a/src/app/schema/index.less b/src/app/schema/index.less index f3453ccfc9d..b4f2d2466be 100644 --- a/src/app/schema/index.less +++ b/src/app/schema/index.less @@ -26,8 +26,6 @@ } } - - .schema-field { // second level border-bottom: 3px solid @gray8; diff --git a/src/app/schema/type-list-item.jade b/src/app/schema/type-list-item.jade deleted file mode 100644 index ab90325b556..00000000000 --- a/src/app/schema/type-list-item.jade +++ /dev/null @@ -1,9 +0,0 @@ -.schema-field-wrapper(data-hook='bar') - if isSubtype - .schema-field-type(data-toggle='tooltip') - span.schema-field-type-label - = model && model.getId() - if !isSubtype - .schema-field-type(data-toggle='tooltip') - .array-subtypes - div(data-hook='array-subtype-subview') diff --git a/src/app/schema/type-list.jade b/src/app/schema/type-list.jade deleted file mode 100644 index 0edc21a0f3d..00000000000 --- a/src/app/schema/type-list.jade +++ /dev/null @@ -1 +0,0 @@ -.schema-field-type-list(data-hook='types') diff --git a/src/app/schema/type-list.js b/src/app/schema/type-list.js deleted file mode 100644 index 290e00575c8..00000000000 --- a/src/app/schema/type-list.js +++ /dev/null @@ -1,180 +0,0 @@ -var View = require('ampersand-view'); -var format = require('util').format; -var numeral = require('numeral'); -var tooltipMixin = require('../tooltip-mixin'); -var _ = require('lodash'); - -var typeListTemplate = require('./type-list.jade'); -var typeListItemTemplate = require('./type-list-item.jade'); -// var debug = require('debug')('mongodb-compass:field-list:type-list'); - -var TypeListView; - -var TypeListItem = View.extend(tooltipMixin, { - template: typeListItemTemplate, - modelType: 'TypeListItem', - bindings: { - active: { - type: 'booleanClass', - name: 'active' - }, - selected: { - type: 'booleanClass', - name: 'selected' - }, - 'model.name': [ - { - hook: 'name' - }, - { - hook: 'bar', - type: function(el) { - el.classList.add('schema-field-type-' + this.model.getId().toLowerCase()); - } - } - ], - probability_percentage: { - hook: 'bar', - type: function(el) { - el.style.width = this.probability_percentage; - } - }, - tooltip_message: { - type: function() { - // need to set `title` and `data-original-title` due to bug in bootstrap's tooltip - // @see https://github.com/twbs/bootstrap/issues/14769 - this.tooltip({ - title: this.tooltip_message, - placement: this.isSubtype ? 'bottom' : 'top', - container: 'body' - }).attr('data-original-title', this.tooltip_message); - } - } - }, - session: { - parent: 'state', - active: { - type: 'boolean', - default: false - }, - selected: { - type: 'boolean', - default: false - } - }, - derived: { - probability_percentage: { - deps: ['model.probability'], - fn: function() { - // no rounding, use exact proportions for relative widths - return this.model.probability * 100 + '%'; - } - }, - tooltip_message: { - deps: ['model.probability'], - fn: function() { - return format('%s (%s)', this.model.getId(), numeral(this.model.probability).format('%')); - } - }, - isSubtype: { - deps: ['parent'], - fn: function() { - return this.parent.hasSubtypes; - } - } - }, - events: { - 'click .schema-field-type-document': 'documentTypeClicked', - 'click .schema-field-wrapper': 'typeClicked' - }, - subviews: { - subtypeView: { - hook: 'array-subtype-subview', - waitFor: 'model.types', - prepareView: function(el) { - return new TypeListView({ - el: el, - parent: this, - hasSubtypes: true, - collection: this.model.types - }); - } - } - }, - documentTypeClicked: function(evt) { - // expands the nested subdocument fields by triggering click in FieldView - var fieldView = this.parent.parent; - if (fieldView.modelType === 'FieldView') { - fieldView.click(evt); - } - }, - typeClicked: function(evt) { - evt.stopPropagation(); - - if (this.active) { - // @todo rueckstiess: already active, query building mode - // this.toggle('selected'); - } else { - // no clicks on Undefined allowed - if (this.model.getId() === 'Undefined') { - return; - } - - // find the field view, at most 2 levels up - var fieldView = this.parent.parent; - if (fieldView.getType() !== 'FieldView') { - fieldView = fieldView.parent.parent; - } - // if type model has changed, render its minichart - if (fieldView.type_model !== this.model) { - fieldView.types.deactivateAll(); - this.active = true; - this.selected = false; - fieldView.type_model = this.model; - fieldView.renderMinicharts(); - } - } - } -}); - - -TypeListView = module.exports = View.extend({ - modelType: 'TypeListView', - session: { - collectionView: 'object', - hasSubtypes: { - type: 'boolean', - default: false - }, - parent: 'state' - }, - template: typeListTemplate, - deactivateAll: function() { - if (!this.collectionView) { - return; - } - _.each(this.collectionView.views, function(typeView) { - typeView.active = false; - typeView.selected = false; - }); - // also deactivate the array subtypes - if (!_.get(this, 'parent.isSubtype')) { - var arrayView = _.find(this.collectionView.views, function(view) { - return view.model.name === 'Array'; - }); - if (arrayView) { - arrayView.subtypeView.deactivateAll(); - } - } - }, - render: function() { - if (!_.get(this, 'parent.isSubtype')) { - this.renderWithTemplate(this); - this.collectionView = this.renderCollection(this.collection, TypeListItem, - this.queryByHook('types')); - } - if (!this.hasSubtypes) { - this.collectionView.views[0].active = true; - } - } -}); diff --git a/src/help/index.js b/src/help/index.js index 0b83bbf3e86..a30c7167b8d 100644 --- a/src/help/index.js +++ b/src/help/index.js @@ -6,6 +6,7 @@ var HelpEntryCollection = require('./help-entry-collection'); var HelpEntry = require('./help-entry'); var SidebarView = require('../app/sidebar'); var ViewSwitcher = require('ampersand-view-switcher'); +var StatusActions = require('../../internal-packages/status/lib/actions'); var app = require('ampersand-app'); var metrics = require('mongodb-js-metrics')(); var _ = require('lodash'); @@ -128,11 +129,11 @@ var HelpPage = View.extend({ if (!entry) { debug('Unknown help entry', entryId); this.viewSwitcher.clear(); - app.statusbar.showMessage('Help entry not found.'); + StatusActions.setMessage('Help entry not found.'); return; } - app.statusbar.hide(); + StatusActions.hide(); if (!entries.select(entry)) { debug('already selected'); diff --git a/src/internal-packages/.eslintrc b/src/internal-packages/.eslintrc new file mode 100644 index 00000000000..4fb1f9df647 --- /dev/null +++ b/src/internal-packages/.eslintrc @@ -0,0 +1,15 @@ +{ + "env": { + "mocha": true, + "node": true, + }, + "rules": { + "camelcase": 1 + }, + "extends": [ + "mongodb-js/node", + "mongodb-js/browser", + "mongodb-js/react" + ], + "plugins": ["react"] +} diff --git a/src/internal-packages/query/index.js b/src/internal-packages/query/index.js new file mode 100644 index 00000000000..ae3dcb33905 --- /dev/null +++ b/src/internal-packages/query/index.js @@ -0,0 +1,19 @@ +const app = require('ampersand-app'); +const QueryBar = require('./lib/component'); + +/** + * Activate all the components in the Query Bar package. + */ +function activate() { + app.componentRegistry.register(QueryBar, { role: 'App:QueryBar' }); +} + +/** + * Deactivate all the components in the Query Bar package. + */ +function deactivate() { + app.componentRegistry.deregister(QueryBar); +} + +module.exports.activate = activate; +module.exports.deactivate = deactivate; diff --git a/src/internal-packages/query/lib/action/index.jsx b/src/internal-packages/query/lib/action/index.jsx new file mode 100644 index 00000000000..d4636eb59b2 --- /dev/null +++ b/src/internal-packages/query/lib/action/index.jsx @@ -0,0 +1,77 @@ +const Reflux = require('reflux'); + +const QueryBuilderActions = Reflux.createActions({ + /* Generic actions */ + + /** + * Sets the entire query, overwriting all fields. + */ + 'setQuery': {sync: true}, + /** + * Sets the entire query as string, overwriting all fields. + * Useful for text input. + */ + 'setQueryString': {sync: true}, + /** + * Sets the value for a specific field, overwriting all previous values. + */ + 'setValue': {sync: true}, + /** + * Clears the value of a specific field. + */ + 'clearValue': {sync: true}, + + /* Distinct actions: + * support single values (equality clause) and multiple values ($in clause). + */ + + /** + * Adds a value to a distinct set of values for a field. + */ + 'addDistinctValue': {sync: true}, + /** + * Removes a value from a distinct set of values for a field. + */ + 'removeDistinctValue': {sync: true}, + /** + * Toggles a value in a distinct set of values for a field. + */ + 'toggleDistinctValue': {sync: true}, + /** + * Sets all distinct values of a field at once. If a single value is specified + * and the value is already set, remove the value. + */ + 'setDistinctValues': {sync: true}, + + + /* Range actions: + * support single values (equality clause) and ranges ($gt(e) / $lt(e) clauses). + */ + + /** + * Sets a range of values, specifying min and max values and inclusive/exclusive + * upper and lower bounds. + */ + 'setRangeValues': {sync: true}, + + /* Geo actions */ + + /** + * sets a $geoWithin query with center and distance. + */ + 'setGeoWithinValue': {sync: true}, + + /* Execution */ + + /** + * apply the current query, only possible if the query is valid. Also stores + * the current query as `lastExecutedQuery`. + */ + 'apply': {sync: true}, + /** + * return to the last executed query, dismissing all changes. + */ + 'reset': {sync: true} +}); + +module.exports = QueryBuilderActions; diff --git a/src/internal-packages/query/lib/component/index.jsx b/src/internal-packages/query/lib/component/index.jsx new file mode 100644 index 00000000000..a69f2774e99 --- /dev/null +++ b/src/internal-packages/query/lib/component/index.jsx @@ -0,0 +1,35 @@ +const React = require('react'); +const QueryStore = require('../store'); +const QueryInputForm = require('./input-form'); +const StateMixin = require('reflux-state-mixin'); + +// const debug = require('debug')('mongodb-compass:query-bar'); + +const QueryBar = React.createClass({ + + /** + * automatically subscribe/unsubscribe to changes from the store. + */ + mixins: [ StateMixin.connect(QueryStore) ], + + /** + * Render Query Bar. + * + * @returns {React.Component} The Query Bar view. + */ + render() { + return ( +
+
+
+
+ +
+
+
+
+ ); + } +}); + +module.exports = QueryBar; diff --git a/src/internal-packages/query/lib/component/input-form.jsx b/src/internal-packages/query/lib/component/input-form.jsx new file mode 100644 index 00000000000..082cc7fd0f1 --- /dev/null +++ b/src/internal-packages/query/lib/component/input-form.jsx @@ -0,0 +1,83 @@ +const React = require('react'); +const QueryActions = require('../action'); +const EJSON = require('mongodb-extended-json'); + +// const debug = require('debug')('mongodb-compass:query-bar'); + +const DEFAULT_QUERY_STRING = '{}'; + +const QueryInputGroup = React.createClass({ + + propTypes: { + query: React.PropTypes.object.isRequired, + lastExecutedQuery: React.PropTypes.object, + valid: React.PropTypes.bool.isRequired, + queryString: React.PropTypes.string.isRequired + }, + + onChange(evt) { + QueryActions.setQueryString(evt.target.value); + }, + + onApplyButtonClicked(evt) { + evt.preventDefault(); + evt.stopPropagation(); + + if (this.props.valid) { + QueryActions.apply(); + } + }, + + onResetButtonClicked() { + QueryActions.reset(); + }, + + /** + * Render Query Bar input form (just the input field and buttons). + * + * @returns {React.Component} The Query Bar view. + */ + render() { + const query = this.props.queryString; + const inputGroupClass = this.props.valid ? + 'input-group' : 'input-group has-error'; + const notEmpty = this.props.queryString !== DEFAULT_QUERY_STRING && + this.props.queryString !== ''; + const resetButtonStyle = { + display: notEmpty ? 'inline-block' : 'none' + }; + + const hasChanges = this.props.queryString !== EJSON.stringify(this.props.lastExecutedQuery); + const applyDisabled = !(this.props.valid && hasChanges); + + return ( +
+
+ + + + + +
+
+ ); + } +}); + +module.exports = QueryInputGroup; diff --git a/src/internal-packages/query/lib/store/index.jsx b/src/internal-packages/query/lib/store/index.jsx new file mode 100644 index 00000000000..3724bc28488 --- /dev/null +++ b/src/internal-packages/query/lib/store/index.jsx @@ -0,0 +1,364 @@ +const Reflux = require('reflux'); +const NamespaceStore = require('hadron-reflux-store').NamespaceStore; +const StateMixin = require('reflux-state-mixin'); +const QueryActions = require('../action'); +const SchemaActions = require('../../../schema/lib/action'); +const EJSON = require('mongodb-extended-json'); +const Query = require('mongodb-language-model').Query; +const _ = require('lodash'); +const hasDistinctValue = require('../util').hasDistinctValue; +const bsonEqual = require('../util').bsonEqual; + +const debug = require('debug')('mongodb-compass:stores:query'); +// const metrics = require('mongodb-js-metrics')(); + +/** + * The reflux store for the schema. + */ +const QueryStore = Reflux.createStore({ + mixins: [StateMixin.store], + listenables: QueryActions, + + /** + * listen to Namespace store and reset if ns changes. + */ + init: function() { + NamespaceStore.listen(() => { + // reset the store + this.setState(this.getInitialState()); + }); + }, + + /** + * Initialize the document list store. + * + * @return {Object} the initial store state. + */ + getInitialState() { + return { + query: {}, + queryString: '{}', + valid: true, + lastExecutedQuery: null + }; + }, + + /** + * Sets `queryString` and `valid`, and if it is a valid query, also set `query`. + * If it is not a valid query, set `valid` to `false` and don't set the query. + * + * @param {Object} queryString the query string (i.e. manual user input) + */ + setQueryString(queryString) { + const query = this._validateQueryString(queryString); + const state = { + queryString: queryString, + valid: Boolean(query) + }; + if (query) { + state.query = query; + } + this.setState(state); + }, + + _cleanQueryString(queryString) { + let output = queryString; + // accept whitespace-only input as empty query + if (_.trim(output) === '') { + output = '{}'; + } + // wrap field names in double quotes. I appologize for the next line of code. + // @see http://stackoverflow.com/questions/6462578/alternative-to-regex-match-all-instances-not-inside-quotes + // @see https://regex101.com/r/xM7iH6/1 + output = output.replace(/([{,])\s*([^,{\s\'"]+)\s*:(?=([^"\\]*(\\.|"([^"\\]*\\.)*[^"\\]*"))*[^"]*$)/g, '$1"$2":'); + // replace multiple whitespace with single whitespace + output = output.replace(/\s+/g, ' '); + return output; + }, + + /** + * validates whether a string is a valid query. + * + * @param {Object} queryString a string to validate + * @return {Object|Boolean} false if invalid, otherwise the query + */ + _validateQueryString(queryString) { + let parsed; + try { + // is it valid eJSON? + const cleaned = this._cleanQueryString(queryString); + parsed = EJSON.parse(cleaned); + // is it a valid parsable Query according to the language? + /* eslint no-unused-vars: 0 */ + const query = new Query(parsed, { + parse: true + }); + } catch (e) { + return false; + } + return parsed; + }, + + /** + * sets the query and the query string, and computes `valid`. + * + * @param {Object} query a valid query. + */ + setQuery(query) { + const queryString = EJSON.stringify(query); + const valid = this._validateQueryString(queryString); + this.setState({ + query: query, + queryString: queryString, + valid: Boolean(valid) + }); + }, + + /** + * Sets the value for the given field. + * + * @param {Object} args arguments must include `field` and `value`, and + * can optionally include `unsetIfSet`: + * field the field of the query to set the value on. + * value the value to set. + * unsetIfSet (optional) boolean, unsets the value if an identical + * value is already set. This is useful for the toggle + * behavior we use on minichart bars. + */ + setValue(args) { + const query = _.clone(this.state.query); + if (args.unsetIfSet && _.isEqual(query[args.field], args.value, bsonEqual)) { + delete query[args.field]; + } else { + query[args.field] = args.value; + } + this.setQuery(query); + }, + + /** + * takes either a single value or an array of values, and sets the value + * correctly as equality or $in depending on the number of values. + * + * @param {Object} args arguments must include `field` and `value`: + * field the field of the query to set the value on. + * value the value(s) to set. Can be a single value or an + * array of values, in which case `$in` is used. + */ + setDistinctValues(args) { + const query = _.clone(this.state.query); + if (_.isArray(args.value)) { + if (args.value.length > 1) { + query[args.field] = {$in: args.value}; + } else if (args.value.length === 1) { + query[args.field] = args.value[0]; + } else { + this.clearValue(args); + } + this.setQuery(query); + return; + } + query[args.field] = args.value; + this.setQuery(query); + }, + + clearValue(args) { + const query = _.clone(this.state.query); + delete query[args.field]; + this.setQuery(query); + }, + + /** + * adds a discrete value to a field, converting primitive values to $in lists + * as required. + * + * @param {Object} args object with a `field` and `value` key. + */ + addDistinctValue(args) { + const query = _.clone(this.state.query); + const field = _.get(query, args.field, undefined); + + // field not present in query yet, add primitive value + if (field === undefined) { + query[args.field] = args.value; + this.setQuery(query); + return; + } + // field is object, could be a $in clause or a primitive value + if (_.isPlainObject(field)) { + if (_.has(field, '$in')) { + // add value to $in array if it is not present yet + const inArray = query[args.field].$in; + if (!_.contains(inArray, args.value)) { + query[args.field].$in.push(args.value); + this.setQuery(query); + } + return; + } + // it is not a $in operator, replace the value + query[args.field] = args.value; + this.setQuery(query); + return; + } + // in all other cases, we want to turn a primitive value into a $in list + query[args.field] = {$in: [field, args.value]}; + this.setQuery(query); + }, + + removeDistinctValue(args) { + const query = _.clone(this.state.query); + const field = _.get(query, args.field, undefined); + + if (field === undefined) { + return; + } + + if (_.isPlainObject(field)) { + if (_.has(field, '$in')) { + // add value to $in array if it is not present yet + const inArray = query[args.field].$in; + const newArray = _.pull(inArray, args.value); + // if $in array was reduced to single value, replace with primitive + if (newArray.length > 1) { + query[args.field].$in = newArray; + } else if (newArray.length === 1) { + query[args.field] = newArray[0]; + } else { + delete query[args.field]; + } + this.setQuery(query); + return; + } + } + // if value to remove is the same as the primitive value, unset field + if (_.isEqual(field, args.value, bsonEqual)) { + delete query[args.field]; + this.setQuery(query); + return; + } + // else do nothing + return; + }, + + /** + * adds distinct value (equality or $in) if not yet present, otherwise + * removes it. + * + * @param {Object} args object with a `field` and `value` key. + */ + toggleDistinctValue(args) { + const field = _.get(this.state.query, args.field, undefined); + const actionFn = hasDistinctValue(field, args.value) ? + this.removeDistinctValue : this.addDistinctValue; + actionFn(args); + }, + + /** + * Sets a range with minimum and/or maximum, and determines inclusive/exclusive + * upper and lower bounds. If neither `min` nor `max` are set, clears the field. + * + * @param {Object} args arguments must include `field`, and can optionally + * include `min`, `max`, `minInclusive`, `maxInclusive` + * and `unsetIfSet`: + * field the field of the query to set the value on. + * min (optional) the minimum value (lower bound) + * minInclusive (optional) boolean, true uses $gte, false uses $gt + * default is true. + * max (optional) the maximum value (upper bound) + * maxInclusive (optional) boolean, true uses $lte, false uses $lt + * default is false. + * unsetIfSet (optional) boolean, unsets the value if an identical + * value is already set. This is useful for the toggle + * behavior we use on minichart bars. + */ + setRangeValues(args) { + const query = _.clone(this.state.query); + const value = {}; + let op; + // without min and max, clear the field + const minValue = _.get(args, 'min', undefined); + const maxValue = _.get(args, 'max', undefined); + if (minValue === undefined && maxValue === undefined) { + this.clearValue({field: args.field}); + return; + } + + if (minValue !== undefined) { + op = _.get(args, 'minInclusive', true) ? '$gte' : '$gt'; + value[op] = minValue; + } + + if (maxValue !== undefined) { + op = _.get(args, 'maxInclusive', false) ? '$lte' : '$lt'; + value[op] = maxValue; + } + + // if `args.unsetIfSet` is true, then unset the value if it's already set + if (args.unsetIfSet && _.isEqual(query[args.field], value, bsonEqual)) { + delete query[args.field]; + } else { + query[args.field] = value; + } + this.setQuery(query); + }, + + /** + * takes a center coordinate [lng, lat] and a radius in miles and constructs + * a circular geoWithin query. + * + * @param {Object} args arguments must include `field` and `value`: + * field the field of the query to set the value on. + * center array of two numeric values: longitude and latitude + * radius radius in miles of the circle + * + * @see https://docs.mongodb.com/manual/tutorial/calculate-distances-using-spherical-geometry-with-2d-geospatial-indexes/ + */ + setGeoWithinValue(args) { + const query = _.clone(this.state.query); + const value = {}; + const radius = _.get(args, 'radius', 0); + const center = _.get(args, 'center', null); + + if (radius && center) { + value.$geoWithin = { + $centerSphere: [[center[0], center[1]], radius] + }; + query[args.field] = value; + this.setQuery(query); + return; + } + // else if center or radius are not set, or radius is 0, clear field + this.clearValue({field: args.field}); + }, + + /** + * apply the current (valid) query, and store it in `lastExecutedQuery`. + */ + apply() { + if (this.state.valid) { + this.setState({ + lastExecutedQuery: _.clone(this.state.query) + }); + // start queries for all tabs: schema, documents, explain, indexes + SchemaActions.startSampling(); + } + }, + + /** + * dismiss current changes to the query and restore `{}` as the query. + */ + reset() { + if (!_.isEqual(this.state.query, {})) { + this.setQuery({}); + if (!_.isEqual(this.state.lastExecutedQuery, {})) { + QueryActions.apply(); + } + } + }, + + storeDidUpdate(prevState) { + debug('query store changed from %j to %j', prevState, this.state); + } + +}); + +module.exports = QueryStore; diff --git a/src/internal-packages/query/lib/util/index.js b/src/internal-packages/query/lib/util/index.js new file mode 100644 index 00000000000..e9afc5bfa8f --- /dev/null +++ b/src/internal-packages/query/lib/util/index.js @@ -0,0 +1,162 @@ +const _ = require('lodash'); +// const debug = require('debug')('mongodb-compass:schema:test'); + +function bsonEqual(value, other) { + const bsontype = _.get(value, '_bsontype', undefined); + if (bsontype === 'ObjectID') { + return value.equals(other); + } + // for all others, use native comparisons + return undefined; +} + +/** + * determines if a field in the query has a distinct value (equality or $in). + * + * @param {Any} field the right-hand side of a document field + * @param {Any} value the value to check + * @return {Boolean} whether or not value is included in field + */ +function hasDistinctValue(field, value) { + // field not present, add primitive value + if (field === undefined) { + return false; + } + // field is object, could be a $in clause or a primitive value + if (_.isPlainObject(field)) { + if (_.has(field, '$in')) { + // check if $in array contains the value + const inArray = field.$in; + return (_.contains(inArray, value)); + } + } + // it is not a $in operator, check value directly + return (_.isEqual(field, value, bsonEqual)); +} + +/** + * returns an array of all distinct values in a field (equality or $in) + * + * @param {Any} field the right-hand side of a document field + * @return {Boolean} array of values for this field + */ +function getDistinctValues(field) { + // field not present, return empty array + if (field === undefined) { + return []; + } + // field is object, could be a $in clause or a primitive value + if (_.isPlainObject(field)) { + if (_.has(field, '$in')) { + return field.$in; + } + } + // it is not a $in operator, return single value as array + return [field]; +} + +/** + * returns whether a value is fully or partially covered by a range, + * specified with $gt(e)/$lt(e). Ranges can be open ended on either side, + * and can also be single equality queries, e.g. {"field": 16}. + * + * @examples + * inValueRange(15, {value: 15, dx: 0}) => 'yes' + * inValueRange({$gte: 15, $lt: 30}, {value: 20, dx: 5}) => 'yes' + * inValueRange({$gte: 15, $lt: 30}, {value: 15, dx: 5}) => 'yes' + * inValueRange(15, {value: 15, dx: 1}) => 'partial' + * inValueRange({$gt: 15, $lt: 30}, {value: 15, dx: 5}) => 'partial' + * inValueRange({$gte: 15, $lt: 30}, {value: 20, dx: 20}) => 'partial' + * inValueRange({$gte: 15, $lt: 30}, {value: 10, dx: 10}) => 'partial' + * inValueRange({$gte: 15, $lt: 30}, {value: 10, dx: 5}) => 'partial' + * inValueRange({$gt: 15, $lt: 30}, {value: 10, dx: 5}) => 'no' + * inValueRange({$gte: 15, $lt: 30}, {value: 10, dx: 4}) => 'no' + * + * @param {Object|number} field the field value (range or number) + * @param {Object} d object with a `value` and `dx` field if + * the value represents a binned range itself + * @return {String} 'yes', 'partial', 'no' + */ +function inValueRange(field, d) { + const compOperators = { + $gte: function(a, b) { + return a >= b; + }, + $gt: function(a, b) { + return a > b; + }, + $lte: function(a, b) { + return a <= b; + }, + $lt: function(a, b) { + return a < b; + } + }; + + const conditions = []; + const edgeCase = []; + + if (!_.isPlainObject(field)) { + // add an equality condition + conditions.push(function(a) { + return _.isEqual(a, field, bsonEqual); + }); + edgeCase.push(field); + } else { + _.forOwn(field, function(value, key) { + // add comparison condition(s), right-curried with the value of the query + conditions.push(_.curryRight(compOperators[key])(value)); + edgeCase.push(value); + }); + } + const dx = _.get(d, 'dx', null); + + // extract bound(s) + const bounds = dx === null ? [d.value] : _.uniq([d.value, d.value + dx]); + + /* + * Logic to determine if the query covers the value (or value range) + * + * if all bounds pass all conditions, the value is fully covered in the range (yes) + * if one of two bounds passes all conditions, the value is partially covered (partial) + * if none of the bounds pass all conditions, the value is not covered (no) + * + * Since the upper bound of a bar represents the exclusive bound + * (i.e. lower <= x < upper) we need to use a little hack to adjust for + * the math. This means that if someone adjusts the query bound manually by + * less than 1 millionth of the value, one of the bars may appear half + * selected instead of not/fully selected. The error is purely visual. + */ + const results = _.map(bounds, function(bound, i) { + // adjust the upper bound slightly as it represents an exclusive bound + // getting this right would require a lot more code to check for all 4 + // edge cases. + if (i > 0) { + bound *= 0.999999; + } + return _.every(_.map(conditions, function(cond) { + return cond(bound); + })); + }); + + if (_.every(results)) { + return 'yes'; + } + if (_.some(results)) { + return 'partial'; + } + // check for edge case where range wraps around query on both ends + if (_.every(edgeCase, function(val) { + return val > bounds[0] && val < bounds[bounds.length - 1]; + })) { + return 'partial'; + } + return 'no'; +} + +module.exports = { + hasDistinctValue: hasDistinctValue, + getDistinctValues: getDistinctValues, + inValueRange: inValueRange, + bsonEqual: bsonEqual +}; diff --git a/src/internal-packages/query/package.json b/src/internal-packages/query/package.json new file mode 100644 index 00000000000..10bf6ab4495 --- /dev/null +++ b/src/internal-packages/query/package.json @@ -0,0 +1,9 @@ +{ + "name": "query-bar", + "productName": "Compass Query Bar", + "description": "Compass Query Bar component with buttons.", + "version": "0.0.1", + "authors": "MongoDB Inc.", + "private": true, + "main": "./index.js" +} diff --git a/src/internal-packages/query/styles/index.less b/src/internal-packages/query/styles/index.less new file mode 100644 index 00000000000..2a7280ef085 --- /dev/null +++ b/src/internal-packages/query/styles/index.less @@ -0,0 +1 @@ +// styles for Query Bar package diff --git a/src/internal-packages/query/test/index.test.js b/src/internal-packages/query/test/index.test.js new file mode 100644 index 00000000000..8e581382891 --- /dev/null +++ b/src/internal-packages/query/test/index.test.js @@ -0,0 +1,9 @@ +/* eslint no-var: 0 */ +var QueryBar = require('../lib/component'); +var assert = require('assert'); + +describe('QueryBar', function() { + it('should work', function() { + assert.ok(QueryBar); + }); +}); diff --git a/src/internal-packages/query/test/ranges.test.js b/src/internal-packages/query/test/ranges.test.js new file mode 100644 index 00000000000..d1b222c63a9 --- /dev/null +++ b/src/internal-packages/query/test/ranges.test.js @@ -0,0 +1,197 @@ +/* eslint no-var: 0 */ +var inValueRange = require('../lib/util').inValueRange; +var assert = require('assert'); +var bson = require('bson'); + +describe('inValueRange', function() { + describe('equality queries', function() { + var query; + beforeEach(function() { + query = 15; + }); + it('should detect a match', function() { + assert.equal(inValueRange(query, {value: 15, dx: 0}), 'yes'); + }); + it('should detect a partial match', function() { + assert.equal(inValueRange(query, {value: 14, dx: 2}), 'partial'); + }); + it('should detect a miss', function() { + assert.equal(inValueRange(query, {value: 14.99, dx: 0}), 'no'); + assert.equal(inValueRange(query, {value: 15.01, dx: 0}), 'no'); + }); + }); + describe('closed ranges with $gte and $lt', function() { + var query; + beforeEach(function() { + query = {$gte: 15, $lt: 30}; + }); + it('should detect a match', function() { + assert.equal(inValueRange(query, {value: 20, dx: 5}), 'yes'); + }); + it('should detect a match at the lower bound', function() { + assert.equal(inValueRange(query, {value: 15, dx: 5}), 'yes'); + }); + it('should detect a partial match across the upper bound', function() { + assert.equal(inValueRange(query, {value: 20, dx: 20}), 'partial'); + }); + it('should detect a partial match across the lower bound', function() { + assert.equal(inValueRange(query, {value: 10, dx: 10}), 'partial'); + }); + it('should detect a miss exactly at the lower bound', function() { + assert.equal(inValueRange(query, {value: 10, dx: 5}), 'no'); + }); + it('should detect a miss exactly at the upper bound', function() { + assert.equal(inValueRange(query, {value: 30, dx: 5}), 'no'); + }); + it('should detect a miss just below the lower bound', function() { + assert.equal(inValueRange(query, {value: 10, dx: 4.99}), 'no'); + }); + it('should detect edge case where range wraps around both bounds', function() { + assert.equal(inValueRange(query, {value: 0, dx: 100}), 'partial'); + }); + }); + + describe('open ranges with $gte', function() { + var query; + beforeEach(function() { + query = {$gte: 15}; + }); + it('should detect a match for a range', function() { + assert.equal(inValueRange(query, {value: 20, dx: 5}), 'yes'); + }); + it('should detect a match for single value', function() { + assert.equal(inValueRange(query, {value: 20, dx: 0}), 'yes'); + }); + it('should detect a match for a range, starting at the bound', function() { + assert.equal(inValueRange(query, {value: 15, dx: 5}), 'yes'); + }); + it('should detect a miss for a range, ending at the bound', function() { + assert.equal(inValueRange(query, {value: 10, dx: 5}), 'no'); + }); + it('should detect a match for single value at the bound', function() { + assert.equal(inValueRange(query, {value: 15, dx: 0}), 'yes'); + }); + it('should detect a partial match for a range across the bound', function() { + assert.equal(inValueRange(query, {value: 12, dx: 5}), 'partial'); + }); + it('should detect a miss for a range below the bound', function() { + assert.equal(inValueRange(query, {value: -20, dx: 5}), 'no'); + }); + it('should detect a miss for a single value below the bound', function() { + assert.equal(inValueRange(query, {value: -20, dx: 0}), 'no'); + }); + }); + describe('open ranges with $gt', function() { + var query; + beforeEach(function() { + query = {$gt: 15}; + }); + it('should detect a match for a range', function() { + assert.equal(inValueRange(query, {value: 20, dx: 5}), 'yes'); + }); + it('should detect a match for single value', function() { + assert.equal(inValueRange(query, {value: 20, dx: 0}), 'yes'); + }); + it('should detect a partial match for a range, starting at the bound', function() { + assert.equal(inValueRange(query, {value: 15, dx: 5}), 'partial'); + }); + it('should detect a miss for single value at the bound', function() { + assert.equal(inValueRange(query, {value: 15, dx: 0}), 'no'); + }); + it('should detect a partial match for a range across the bound', function() { + assert.equal(inValueRange(query, {value: 12, dx: 5}), 'partial'); + }); + it('should detect a miss for a range ending at the bound', function() { + assert.equal(inValueRange(query, {value: 10, dx: 5}), 'no'); + }); + it('should detect a miss for a range below the bound', function() { + assert.equal(inValueRange(query, {value: -20, dx: 5}), 'no'); + }); + it('should detect a miss for a single value below the bound', function() { + assert.equal(inValueRange(query, {value: -20, dx: 0}), 'no'); + }); + }); + describe('open ranges with $lte', function() { + var query; + beforeEach(function() { + query = {$lte: 15}; + }); + it('should detect a match for a range', function() { + assert.equal(inValueRange(query, {value: 5, dx: 5}), 'yes'); + }); + it('should detect a match for single value', function() { + assert.equal(inValueRange(query, {value: 10, dx: 0}), 'yes'); + }); + it('should detect a match for single value at the bound', function() { + assert.equal(inValueRange(query, {value: 15, dx: 0}), 'yes'); + }); + it('should detect a partial match for a range across the bound', function() { + assert.equal(inValueRange(query, {value: 12, dx: 5}), 'partial'); + }); + it('should detect a match for a range ending at the bound', function() { + assert.equal(inValueRange(query, {value: 10, dx: 5}), 'yes'); + }); + it('should detect a partial match for a range starting at the bound', function() { + assert.equal(inValueRange(query, {value: 15, dx: 5}), 'partial'); + }); + it('should detect a miss for a range above the bound', function() { + assert.equal(inValueRange(query, {value: 20, dx: 5}), 'no'); + }); + it('should detect a miss for a single value above the bound', function() { + assert.equal(inValueRange(query, {value: 20, dx: 0}), 'no'); + }); + }); + describe('open ranges with $lt', function() { + var query; + beforeEach(function() { + query = {$lt: 15}; + }); + it('should detect a match for a range', function() { + assert.equal(inValueRange(query, {value: 5, dx: 5}), 'yes'); + }); + it('should detect a match for single value', function() { + assert.equal(inValueRange(query, {value: 10, dx: 0}), 'yes'); + }); + it('should detect a miss for a range starting at the bound', function() { + assert.equal(inValueRange(query, {value: 15, dx: 5}), 'no'); + }); + it('should detect a miss for single value at the bound', function() { + assert.equal(inValueRange(query, {value: 15, dx: 0}), 'no'); + }); + it('should detect a partial match for a range across the bound', function() { + assert.equal(inValueRange(query, {value: 12, dx: 5}), 'partial'); + }); + it('should detect a match for a range ending at the bound', function() { + assert.equal(inValueRange(query, {value: 10, dx: 5}), 'yes'); + }); + it('should detect a miss for a range above the bound', function() { + assert.equal(inValueRange(query, {value: 20, dx: 5}), 'no'); + }); + it('should detect a miss for a single value above the bound', function() { + assert.equal(inValueRange(query, {value: 20, dx: 0}), 'no'); + }); + }); + + describe('non-numeric types', function() { + it('should work for dates', function() { + var query = {$gte: new Date('2011-01-01'), $lte: new Date('2013-01-01')}; + assert.equal(inValueRange(query, {value: new Date('2012-01-01')}), 'yes'); + assert.equal(inValueRange(query, {value: new Date('2015-01-01')}), 'no'); + }); + it('should work for objectids', function() { + var query = { + $gte: new bson.ObjectId('578cfb38d5021e616087f53f'), + $lte: new bson.ObjectId('578cfb42d5021e616087f541') + }; + assert.equal(inValueRange(query, {value: new bson.ObjectId('578cfb3ad5021e616087f540')}), 'yes'); + assert.equal(inValueRange(query, {value: new bson.ObjectId('578cfb6fd5021e616087f542')}), 'no'); + }); + }); + + describe('special edge cases', function() { + it('should detect a miss exactly at the lower bound for very large numbers', function() { + var query = {$gte: 10000000000, $lt: 10100000000}; + assert.equal(inValueRange(query, {value: 9900000000, dx: 100000000}), 'no'); + }); + }); +}); diff --git a/src/internal-packages/schema/index.js b/src/internal-packages/schema/index.js new file mode 100644 index 00000000000..336b0c12e83 --- /dev/null +++ b/src/internal-packages/schema/index.js @@ -0,0 +1,21 @@ +'use strict'; + +const app = require('ampersand-app'); +const Schema = require('./lib/component/schema'); + +/** + * Activate all the components in the CRUD package. + */ +function activate() { + app.componentRegistry.register(Schema, { role: 'Collection:Schema' }); +} + +/** + * Deactivate all the components in the CRUD package. + */ +function deactivate() { + app.componentRegistry.deregister(Schema); +} + +module.exports.activate = activate; +module.exports.deactivate = deactivate; diff --git a/src/internal-packages/schema/lib/action/index.jsx b/src/internal-packages/schema/lib/action/index.jsx new file mode 100644 index 00000000000..0f664422929 --- /dev/null +++ b/src/internal-packages/schema/lib/action/index.jsx @@ -0,0 +1,22 @@ +const Reflux = require('reflux'); + +const SchemaActions = Reflux.createActions({ + /** + * starts schema sampling with the current query + */ + startSampling: {sync: true}, + /** + * stops schema sampling + */ + stopSampling: {sync: true}, + /** + * set new maxTimeMS value + */ + setMaxTimeMS: {sync: true}, + /** + * reset maxTimeMS value to default + */ + resetMaxTimeMS: {sync: true} +}); + +module.exports = SchemaActions; diff --git a/src/internal-packages/schema/lib/component/d3component.jsx b/src/internal-packages/schema/lib/component/d3component.jsx new file mode 100644 index 00000000000..820c8de7672 --- /dev/null +++ b/src/internal-packages/schema/lib/component/d3component.jsx @@ -0,0 +1,97 @@ +const React = require('react'); +const ReactDOM = require('react-dom'); +const d3 = require('d3'); +const _ = require('lodash'); + +// const debug = require('debug')('mongodb-compass:schema:d3component'); + +const D3Component = React.createClass({ + + propTypes: { + fieldName: React.PropTypes.string.isRequired, + type: React.PropTypes.object.isRequired, + renderMode: React.PropTypes.oneOf(['svg', 'div']), + width: React.PropTypes.number, + height: React.PropTypes.number, + fn: React.PropTypes.func.isRequired, + query: React.PropTypes.any + }, + + getInitialState() { + return { + chart: null + }; + }, + + componentWillMount() { + this.setState({ + chart: this.props.fn() + }); + }, + + componentDidMount: function() { + this._redraw(); + }, + + componentDidUpdate() { + this._redraw(); + }, + + _getContainer() { + let options = { + className: 'minichart', + ref: 'container' + }; + const sizeOptions = { + width: this.props.width, + height: this.props.height + }; + if (this.props.renderMode === 'svg') { + options = _.assign(options, sizeOptions); + return ( + + + + + + + + + + + ); + } + options = _.assign(options, { + style: sizeOptions + }); + return
; + }, + + _redraw() { + const el = ReactDOM.findDOMNode(this.refs.container); + this.state.chart + .width(this.props.width) + .height(this.props.height); + + this.state.chart.options({ + fieldName: this.props.fieldName, + unique: this.props.type.unique, + query: this.props.query + }); + + d3.select(el) + .datum(this.props.type.values) + .call(this.state.chart); + }, + + render() { + const container = this._getContainer(); + return ( +
+ {container} +
+ ); + } +}); + +module.exports = D3Component; diff --git a/src/internal-packages/schema/lib/component/field.jsx b/src/internal-packages/schema/lib/component/field.jsx new file mode 100644 index 00000000000..e70c560c9fa --- /dev/null +++ b/src/internal-packages/schema/lib/component/field.jsx @@ -0,0 +1,191 @@ +const React = require('react'); +const Type = require('./type'); +const Minichart = require('./minichart'); +const detectCoordinates = require('detect-coordinates'); +const _ = require('lodash'); + +// const debug = require('debug')('mongodb-compass:schema:field'); + +/** + * The full schema component class. + */ +const FIELD_CLASS = 'schema-field'; + +/** + * Component for the entire document list. + */ +const Field = React.createClass({ + propTypes: { + // non-dotted name of the field, e.g. `street` + name: React.PropTypes.string, + // full dotted name of the field, e.g. `address.street` + path: React.PropTypes.string, + // array of type objects present in this field + types: React.PropTypes.array, + // array of subfields in a nested documents + fields: React.PropTypes.array + }, + + getInitialState() { + return { + // whether the nested fields are collapsed (true) or expanded (false) + collapsed: true, + // a reference to the active type object (only null initially) + activeType: null + }; + }, + + componentWillMount() { + // sets the active type to the first type in the props.types array + this.setState({ + activeType: this.props.types.length > 0 ? this.props.types[0] : null + }); + }, + + /** + * returns the field list (an array of components) for nested + * subdocuments. + * + * @return {component} Field list or empty div + */ + getChildren() { + const fields = _.get(this.getNestedDocType(), 'fields', []); + let fieldList; + + if (this.state.collapsed) { + // return empty div if field is collapsed + fieldList = []; + } else { + fieldList = fields.map((field) => { + return ; + }); + } + return ( +
+ {fieldList} +
+ ); + }, + + /** + * returns Document type object of a nested document, either directly nested + * or sub-documents inside an array. + * + * @return {Object} object representation of `Document` type. + * + * @example + * {foo: {bar: 1}} ==> {bar: 1} is a direct descendant + * {foo: [{baz: 2}]} ==> {baz: 2} is a nested document inside an array + * + * @see mongodb-js/mongodb-schema + */ + getNestedDocType() { + // check for directly nested document first + const docType = _.find(this.props.types, 'name', 'Document'); + if (docType) { + return docType; + } + // otherwise check for nested documents inside an array + const arrType = _.find(this.props.types, 'name', 'Array'); + if (arrType) { + return _.find(arrType.types, 'name', 'Document'); + } + return null; + }, + + /** + * tests type for semantic interpretations, like geo coordinates, and + * replaces type information like name and values if there's a match. + * + * @param {Object} type The original type + * @return {Object} The possibly modified type + */ + getSemanticType(type) { + // check if the type represents geo coordinates + const coords = detectCoordinates(type); + if (coords) { + type.name = 'Coordinates'; + type.values = coords; + } + return type; + }, + + /** + * onclick handler to toggle collapsed/expanded state. This will hide/show + * the nested fields and turn the disclosure triangle sideways. + */ + titleClicked() { + this.setState({collapsed: !this.state.collapsed}); + }, + + /** + * callback passed down to each type to be called when the type is + * clicked. Will change the state of the Field component to track the + * active type. + * + * @param {Object} type object of the clicked type + */ + renderType(type) { + this.setState({activeType: type}); + }, + + /** + * Render a single field; + * + * @returns {React.Component} A react component for a single field + */ + render() { + // top-level class of this component + const cls = FIELD_CLASS + ' ' + (this.state.collapsed ? 'collapsed' : 'expanded'); + + // types represented as horizontal bars with labels + const types = _.sortBy(this.props.types, (type) => { + if (type.name === 'Undefined') { + return -Infinity; + } + return type.probability; + }).reverse(); + const typeList = types.map((type) => { + // allow for semantic types and convert the type, e.g. geo coordinates + type = this.getSemanticType(type); + return ( + + ); + }); + + const activeType = this.state.activeType; + + // children fields in case of nested array / document + return ( +
+
+
+
+ + {this.props.name} +
+
+ {typeList} +
+
+
+ +
+
+ {this.getChildren()} +
+ ); + } +}); + +module.exports = Field; diff --git a/src/internal-packages/schema/lib/component/minichart.jsx b/src/internal-packages/schema/lib/component/minichart.jsx new file mode 100644 index 00000000000..2000a8d5aa9 --- /dev/null +++ b/src/internal-packages/schema/lib/component/minichart.jsx @@ -0,0 +1,128 @@ +const React = require('react'); +const UniqueMinichart = require('./unique'); +const _ = require('lodash'); +// const DocumentMinichart = require('./document'); +// const ArrayMinichart = require('./array'); +const QueryStore = require('../../../query/lib/store'); + +const D3Component = require('./d3component'); +const vizFns = require('../d3'); + +const debug = require('debug')('mongodb-compass:schema:minichart'); + +const Minichart = React.createClass({ + + propTypes: { + fieldName: React.PropTypes.string.isRequired, + type: React.PropTypes.object.isRequired + }, + + getInitialState() { + return { + containerWidth: null, + query: {} + }; + }, + + componentDidMount() { + const rect = this.refs.minichart.getBoundingClientRect(); + + /* eslint react/no-did-mount-set-state: 0 */ + + // yes, this is not ideal, we are rendering the empty container first to + // measure the size, then render the component with content a second time, + // but it is not noticable to the user. + this.setState({ + containerWidth: rect.width + }); + window.addEventListener('resize', this.handleResize); + + this.unsubscribeQueryStore = QueryStore.listen((store) => { + this.setState({ + query: store.query + }); + }); + }, + + componentWillUnmount() { + window.removeEventListener('resize', this.handleResize); + this.unsubscribeQueryStore(); + }, + + handleResize() { + const rect = this.refs.minichart.getBoundingClientRect(); + this.setState({ + containerWidth: rect.width + }); + }, + + minichartFactory() { + /* eslint camelcase: 0 */ + const typeName = this.props.type.name; + const fieldName = this.props.fieldName; + const queryClause = this.state.query[fieldName]; + const has_duplicates = this.props.type.has_duplicates; + const fn = vizFns[typeName.toLowerCase()]; + const width = this.state.containerWidth; + + if (_.includes(['String', 'Number'], typeName) && !has_duplicates) { + return ( + + ); + } + if (typeName === 'Coordinates') { + const height = width / 1.618; // = golden ratio + return ( + + ); + } + if (typeName === 'Document') { + return
Document Placeholder Minichart
; + } + if (typeName === 'Array') { + return
Array Placeholder Minichart
; + } + if (typeName === 'Undefined') { + return
Undefined
; + } + if (!fn) { + return
Unknown Type
; + } + return ( + + ); + }, + + render() { + const minichart = this.state.containerWidth ? this.minichartFactory() : null; + return ( +
+ {minichart} +
+ ); + } + +}); + +module.exports = Minichart; diff --git a/src/internal-packages/schema/lib/component/schema.jsx b/src/internal-packages/schema/lib/component/schema.jsx new file mode 100644 index 00000000000..6a87ffa9aa7 --- /dev/null +++ b/src/internal-packages/schema/lib/component/schema.jsx @@ -0,0 +1,92 @@ +const React = require('react'); +const SchemaStore = require('../store/schema-store'); +const StateMixin = require('reflux-state-mixin'); +const Field = require('./field'); +const StatusSubview = require('../component/status-subview'); +const StatusStore = require('../../../status/lib/stores/status-store'); +const StatusActions = require('../../../status/lib/actions'); + +const _ = require('lodash'); + +// const debug = require('debug')('mongodb-compass:schema'); + +/** + * Component for the entire schema view component. + */ +const Schema = React.createClass({ + + mixins: [ + StateMixin.connect(SchemaStore) + ], + + shouldComponentUpdate() { + // @todo optimize this + return true; + }, + + /** + * updates the progress bar according to progress of schema sampling. + * The count is indeterminate (trickling), and sampling/analyzing is + * increased in 5% steps. + */ + _updateProgressBar() { + if (this.state.samplingState === 'error') { + StatusActions.configure({ + progressbar: false, + animation: false + }); + } + const progress = this.state.samplingProgress; + // initial schema phase, cannot measure progress, enable trickling + if (this.state.samplingProgress === -1) { + this.trickleStop = null; + StatusActions.configure({ + visible: true, + progressbar: true, + progress: 0, + animation: true, + trickle: true, + subview: StatusSubview + }); + } else if (progress >= 0 && progress < 100 && progress % 5 === 1) { + if (this.trickleStop === null) { + // remember where trickling stopped to calculate remaining progress + this.trickleStop = StatusStore.state.progress; + } + const newProgress = Math.ceil(this.trickleStop + (100 - this.trickleStop) / 100 * progress); + StatusActions.configure({ + visible: true, + trickle: false, + animation: true, + progressbar: true, + subview: StatusSubview, + progress: newProgress + }); + } else if (progress === 100) { + StatusActions.done(); + } + }, + + + /** + * Render the schema + * + * @returns {React.Component} The schema view. + */ + render() { + this._updateProgressBar(); + + const fieldList = _.get(this.state.schema, 'fields', []).map((field) => { + return ; + }); + return ( +
+
+ {fieldList} +
+
+ ); + } +}); + +module.exports = Schema; diff --git a/src/internal-packages/schema/lib/component/status-subview/buttons-error.jsx b/src/internal-packages/schema/lib/component/status-subview/buttons-error.jsx new file mode 100644 index 00000000000..20eef389ea7 --- /dev/null +++ b/src/internal-packages/schema/lib/component/status-subview/buttons-error.jsx @@ -0,0 +1,77 @@ +const React = require('react'); +const ms = require('ms'); +const StatusActions = require('../../../../status/lib/actions'); +const SchemaActions = require('../../action'); + +// const debug = require('debug')('mongodb-compass:schema:status-subview:buttons-error'); + +const RETRY_INC_MAXTIMEMS_VALUE = 60000; +/** + * Component for the entire document list. + */ +const ButtonsError = React.createClass({ + propTypes: { + maxTimeMS: React.PropTypes.number.isRequired, + samplingState: React.PropTypes.string.isRequired + }, + + onTryAgainButtonClick() { + // increase maxTimeMS and sample again + SchemaActions.setMaxTimeMS(RETRY_INC_MAXTIMEMS_VALUE); + SchemaActions.startSampling(); + }, + + onNewQueryButtonClick() { + // dismiss status view + StatusActions.hide(); + }, + + /** + * only show the retry button if the maxTimeMS value hasn't been increased + * yet (first time). + * + * @return {React.Component|null} Retry button or null. + */ + _getTryAgainButton() { + if (this.props.maxTimeMS < RETRY_INC_MAXTIMEMS_VALUE) { + return ( +
+ Try for 1 minute +
+ ); + } + return null; + }, + + render() { + // if sampling state is not `error`, don't show this component + if (this.props.samplingState !== 'error') { + return null; + } + + const sampleTime = ms(this.props.maxTimeMS, {long: true}); + const tryAgainButton = this._getTryAgainButton(); + + return ( +
+
+
+ The query took longer than {sampleTime} on the database. + As a safety measure, Compass aborts long-running queries.   + + Learn More + + +
+
+ {tryAgainButton} +
+ Create New Query +
+
+
+ ); + } +}); + +module.exports = ButtonsError; diff --git a/src/internal-packages/schema/lib/component/status-subview/buttons-waiting.jsx b/src/internal-packages/schema/lib/component/status-subview/buttons-waiting.jsx new file mode 100644 index 00000000000..0b6b9b29fab --- /dev/null +++ b/src/internal-packages/schema/lib/component/status-subview/buttons-waiting.jsx @@ -0,0 +1,53 @@ +const React = require('react'); +const SchemaActions = require('../../action'); + +// const debug = require('debug')('mongodb-compass:schema:status-subview:buttons-waiting'); + +const SHOW_WAITING_BUTTONS_TIME_MS = 15000; + +/** + * Component for the entire document list. + */ +const ButtonsWaiting = React.createClass({ + propTypes: { + samplingTimeMS: React.PropTypes.number.isRequired, + samplingState: React.PropTypes.string.isRequired + }, + + onStopPartialButton() { + SchemaActions.stopSampling(); + }, + + render() { + // if in error state, don't show this component + if (this.props.samplingState === 'error') { + return null; + } + + // if below 15 second threshold, hide this component + const buttonStyle = { + visibility: (this.props.samplingTimeMS < SHOW_WAITING_BUTTONS_TIME_MS) ? + 'hidden' : 'visible' + }; + + return ( +
+
+
+ Document analysis is taking longer than expected.   + + Learn More + + +
+
+
+ Stop and show partial results +
+
+
+ ); + } +}); + +module.exports = ButtonsWaiting; diff --git a/src/internal-packages/schema/lib/component/status-subview/index.jsx b/src/internal-packages/schema/lib/component/status-subview/index.jsx new file mode 100644 index 00000000000..439f9b7d528 --- /dev/null +++ b/src/internal-packages/schema/lib/component/status-subview/index.jsx @@ -0,0 +1,42 @@ +const React = require('react'); +const ButtonsWaiting = require('./buttons-waiting'); + +const StateMixin = require('reflux-state-mixin'); +const ButtonsError = require('./buttons-error'); + +const SchemaStore = require('../../store/schema-store'); +const SchemaSteps = require('./steps'); + +// const debug = require('debug')('mongodb-compass:schema:status-subview'); + +/** + * Component for the entire document list. + */ +const SchemaStatusSubview = React.createClass({ + + mixins: [ + StateMixin.connect(SchemaStore) + ], + + render() { + return ( +
+ + + +
+ ); + } + +}); + +module.exports = SchemaStatusSubview; diff --git a/src/internal-packages/schema/lib/component/status-subview/steps.jsx b/src/internal-packages/schema/lib/component/status-subview/steps.jsx new file mode 100644 index 00000000000..cc5b8d9b40c --- /dev/null +++ b/src/internal-packages/schema/lib/component/status-subview/steps.jsx @@ -0,0 +1,91 @@ +const React = require('react'); +const _ = require('lodash'); + +// const debug = require('debug')('mongodb-compass:schema:status-subview:steps'); + +const SHOW_STEPS_TIME_MS = 3000; + +/** + * Component for the entire document list. + */ +const SchemaSteps = React.createClass({ + + propTypes: { + samplingTimeMS: React.PropTypes.number.isRequired, + samplingState: React.PropTypes.string.isRequired + }, + + getInitialState() { + return { + errorState: null + }; + }, + + /** + * remember the last known non-error state internally. + * + * @param {Object} nextProps next props of this component + */ + componentWillReceiveProps(nextProps) { + if (this.props.samplingState !== 'error' && nextProps.samplingState === 'error') { + this.setState({ + errorState: this.props.samplingState + }); + } + }, + + _getSamplingIndicator() { + if (_.contains(['counting', 'sampling'], this.props.samplingState)) { + return 'fa fa-fw fa-spin fa-circle-o-notch'; + } + if (this.props.samplingState === 'analyzing' || + (this.props.samplingState === 'error' && this.state.errorState === 'analyzing')) { + return 'mms-icon-check'; + } + if (this.props.samplingState === 'error' && this.state.errorState === 'sampling') { + return 'fa fa-fw fa-warning'; + } + return 'fa fa-fw'; + }, + + _getAnalyzingIndicator() { + if (this.props.samplingState === 'analyzing') { + return 'fa fa-fw fa-spin fa-circle-o-notch'; + } + if (this.props.samplingState === 'complete') { + return 'mms-icon-check'; + } + if (this.props.samplingState === 'error' && this.state.errorState === 'analyzing') { + return 'fa fa-fw fa-warning'; + } + return 'fa fa-fw'; + }, + + render() { + // if below 3 second threshold, don't show this component + const style = { + visibility: (this.props.samplingTimeMS < SHOW_STEPS_TIME_MS) ? + 'hidden' : 'visible' + }; + + const samplingIndicator = this._getSamplingIndicator(); + const analyzingIndicator = this._getAnalyzingIndicator(); + + return ( +
+
    +
  • + + Sampling Collection +
  • +
  • + + Analyzing Documents +
  • +
+
+ ); + } +}); + +module.exports = SchemaSteps; diff --git a/src/internal-packages/schema/lib/component/type.jsx b/src/internal-packages/schema/lib/component/type.jsx new file mode 100644 index 00000000000..9c952058a83 --- /dev/null +++ b/src/internal-packages/schema/lib/component/type.jsx @@ -0,0 +1,141 @@ +const React = require('react'); +const _ = require('lodash'); +const ReactTooltip = require('react-tooltip'); +const numeral = require('numeral'); + +// const debug = require('debug')('mongodb-compass:schema:type'); + +/** + * The full schema component class. + */ +const TYPE_CLASS = 'schema-field-wrapper'; + +/** + * Component for the entire document list. + */ +const Type = React.createClass({ + propTypes: { + name: React.PropTypes.string.isRequired, // type name, e.g. `Number` + types: React.PropTypes.array, // array of types (for subtypes) + activeType: React.PropTypes.any, // currently active type overall + self: React.PropTypes.object, // a reference to this type + probability: React.PropTypes.number.isRequired, // length of bar + renderType: React.PropTypes.func.isRequired, // callback function + showSubTypes: React.PropTypes.bool.isRequired // should subtypes be rendered? + }, + + /** + * The type bar corresponding to this Type was clicked. Execute the + * callback passed in from the parent (either or component + * in case of subtypes). + * + * @param {Object} e click event (need to stop propagation) + */ + typeClicked(e) { + e.stopPropagation(); + this.props.renderType(this.props.self); + }, + + /** + * A subtype was clicked (in case of an Array type). Pass up to the Field + * so the entire type bar can be re-rendered. + * + * @param {Object} subtype The subtype object + */ + subTypeClicked(subtype) { + this.props.renderType(subtype); + }, + + /** + * returns a list of subtype components for Array types. + * + * @return {ReactFragment} array of components for subtype bar + */ + _getArraySubTypes() { + // only worry about subtypes if the type is Array + if (this.props.name !== 'Array') { + return null; + } + // only show one level of subtypes, further Arrays inside Arrays don't + // render their subtypes. + if (!this.props.showSubTypes) { + return null; + } + // sort the subtypes same as types (by probability, undefined last) + const subtypes = _.sortBy(this.props.types, (type) => { + if (type.name === 'Undefined') { + return -Infinity; + } + return type.probability; + }).reverse(); + // is one of the subtypes active? + const activeSubType = _.find(subtypes, this.props.activeType); + // generate the react fragment of subtypes, pass in showSubTypes=false + // to stop the recursion after one step. + const typeList = subtypes.map((subtype) => { + return ( + + ); + }); + return ( +
+
+ {typeList} +
+
+ ); + }, + + /** + * Render a single type + * + * @returns {React.Component} A react component for a single type, + * possibly with subtypes included for Array type. + */ + render() { + const type = this.props.name.toLowerCase(); + let cls = `${TYPE_CLASS} schema-field-type-${type}`; + if (this.props.activeType === this.props.self) { + cls += ' active'; + } + const handleClick = type === 'undefined' ? null : this.typeClicked; + const percentage = (this.props.probability * 100) + '%'; + const style = { + width: percentage + }; + const subtypes = this._getArraySubTypes(); + const label = {this.props.name}; + const tooltipText = `${this.props.name} (${numeral(this.props.probability).format('0%')})`; + const tooltipOptions = { + 'data-tip': tooltipText, + 'data-effect': 'solid', + 'data-border': true, + 'data-place': this.props.showSubTypes ? 'top' : 'bottom' + }; + tooltipOptions['data-offset'] = this.props.showSubTypes ? + '{"top": -15, "left": 0}' : '{"top": 10, "left": 0}'; + return ( +
+ + {this.props.showSubTypes ? label : null} +
+ {subtypes} + {this.props.showSubTypes ? null : label} +
+ ); + } +}); + +module.exports = Type; diff --git a/src/internal-packages/schema/lib/component/unique.jsx b/src/internal-packages/schema/lib/component/unique.jsx new file mode 100644 index 00000000000..f68d17ede1b --- /dev/null +++ b/src/internal-packages/schema/lib/component/unique.jsx @@ -0,0 +1,120 @@ +const React = require('react'); +const _ = require('lodash'); +const NativeListener = require('react-native-listener'); +const QueryBuilderAction = require('../../../query/lib/action'); +const hasDistinctValue = require('../../../query/lib/util').hasDistinctValue; + +// const debug = require('debug')('mongodb-compass:minichart:unique'); + +const ValueBubble = React.createClass({ + propTypes: { + fieldName: React.PropTypes.string.isRequired, + value: React.PropTypes.any.isRequired, + query: React.PropTypes.any + }, + + // getInitialState() { + // return { + // selected: false + // }; + // }, + + onBubbleClicked(e) { + // const newState = !this.state.selected; + // this.setState({ + // selected: newState + // }); + const action = e.shiftKey ? + QueryBuilderAction.toggleDistinctValue : QueryBuilderAction.setValue; + action({ + field: this.props.fieldName, + value: this.props.value, + unsetIfSet: true + }); + }, + + render() { + const value = this.props.value; + const selectedClass = hasDistinctValue(this.props.query, value) ? + 'selected' : 'unselected'; + return ( +
  • + + {value.toString()} + +
  • + ); + } +}); + +/* eslint react/no-multi-comp: 0 */ +const UniqueMinichart = React.createClass({ + propTypes: { + fieldName: React.PropTypes.string.isRequired, + type: React.PropTypes.object.isRequired, + width: React.PropTypes.number, + query: React.PropTypes.any + }, + + getInitialState() { + return { + sample: _.sample(this.props.type.values, 20) + }; + }, + + onRefresh(e) { + e.stopPropagation(); + e.preventDefault(); + this.setState({ + sample: _.sample(this.props.type.values, 20) + }); + }, + + /** + * Render a single field; + * + * @returns {React.Component} A react component for a single field + */ + render() { + if (!this.props.type.values) { + return
    ; + } + const sample = this.state.sample || []; + const fieldName = this.props.fieldName.toLowerCase(); + const typeName = this.props.type.name.toLowerCase(); + const randomValueList = sample.map((value) => { + return ( + + ); + }); + const style = { + width: this.props.width + }; + + return ( +
    +
    +
    + + + + + +
    +
    +
      + {randomValueList} +
    +
    +
    +
    + ); + } +}); + +module.exports = UniqueMinichart; diff --git a/src/internal-packages/schema/lib/d3/boolean.js b/src/internal-packages/schema/lib/d3/boolean.js new file mode 100644 index 00000000000..18cc218d427 --- /dev/null +++ b/src/internal-packages/schema/lib/d3/boolean.js @@ -0,0 +1,88 @@ +/* eslint camelcase: 0 */ +const d3 = require('d3'); +const _ = require('lodash'); +const few = require('./few'); +const shared = require('./shared'); +// const debug = require('debug')('mongodb-compass:minicharts:boolean'); + + +const minicharts_d3fns_boolean = function() { + // --- beginning chart setup --- + let width = 400; + let height = 100; + const options = { + view: null + }; + const fewChart = few(); + const margin = shared.margin; + // --- end chart setup --- + + function chart(selection) { + selection.each(function(data) { + const el = d3.select(this); + const innerWidth = width - margin.left - margin.right; + const innerHeight = height - margin.top - margin.bottom; + + // group by true/false + const grouped = _(data) + .groupBy(function(d) { + return d; + }) + .defaults({ + false: [], + true: [] + }) + .map(function(v, k) { + return { + label: k, + value: k === 'true', + count: v.length + }; + }) + .sortByOrder('label', [false]) // order: false, true + .value(); + + fewChart + .width(innerWidth) + .height(innerHeight) + .options(options); + + const g = el.selectAll('g').data([grouped]); + + // append g element if it doesn't exist yet + g.enter() + .append('g') + .attr('transform', 'translate(' + margin.left + ',' + margin.top + ')'); + + g.call(fewChart); + }); + } + + chart.width = function(value) { + if (!arguments.length) { + return width; + } + width = value; + return chart; + }; + + chart.height = function(value) { + if (!arguments.length) { + return height; + } + height = value; + return chart; + }; + + chart.options = function(value) { + if (!arguments.length) { + return options; + } + _.assign(options, value); + return chart; + }; + + return chart; +}; + +module.exports = minicharts_d3fns_boolean; diff --git a/src/internal-packages/schema/lib/d3/coordinates.js b/src/internal-packages/schema/lib/d3/coordinates.js new file mode 100644 index 00000000000..8595d6780b2 --- /dev/null +++ b/src/internal-packages/schema/lib/d3/coordinates.js @@ -0,0 +1,488 @@ +/* eslint camelcase: 0 */ +const d3 = require('d3'); +const _ = require('lodash'); +const shared = require('./shared'); +const app = require('ampersand-app'); +const QueryBuilderAction = require('../../../query/lib/action'); +const turfDistance = require('turf-distance'); +const turfPoint = require('turf-point'); +const turfDestination = require('turf-destination'); + +// const metrics = require('mongodb-js-metrics')(); +// const debug = require('debug')('mongodb-compass:minicharts:geo'); + +const SELECTED_COLOR = '#F68A1E'; +const UNSELECTED_COLOR = '#43B1E5'; +const CONTROL_COLOR = '#ed271c'; +const TOKEN = 'pk.eyJ1IjoibW9uZ29kYi1jb21wYXNzIiwiYSI6ImNpbWUxZjNudjAwZTZ0emtrczByanZ4MzIifQ.6Mha4zoflraopcZKOLSpYQ'; + +const minicharts_d3fns_geo = function() { + // --- beginning chart setup --- + let width = 400; + let height = 100; + let map = null; + let circleControl; + let mapboxgl; + + const options = { + view: null + }; + + let circleCenter; + let circleOuter; // control points + let mileDistance; + let circleSelected = false; // have we completed the circle? + let svg; + let render; + let dots; + + const margin = shared.margin; + + function CircleSelector(container) { + const dragging = false; // track whether we are dragging + + // we expose events on our component + const dispatch = d3.dispatch('update', 'clear'); + + // this will likely be overriden by leaflet projection + let project; + let unproject; + + // const project = d3.geo.mercator(); + // const unproject = d3.geo.mercator().invert; + + let update; + + function querybuilder() { + if (circleCenter && circleOuter) { + mileDistance = turfDistance( + turfPoint([circleCenter.lng, circleCenter.lat]), + turfPoint([circleOuter.lng, circleOuter.lat]), + 'miles' + ); + QueryBuilderAction.setGeoWithinValue({ + field: options.fieldName, + center: [circleCenter.lng, circleCenter.lat], + radius: mileDistance / 3963.2 + }); + } else { + QueryBuilderAction.clearValue({ + field: options.fieldName + }); + } + } + + function distance(ll0, ll1) { + const p0 = project(ll0); + const p1 = project(ll1); + const dist = Math.sqrt((p1.x - p0.x) * (p1.x - p0.x) + (p1.y - p0.y) * (p1.y - p0.y)); + return dist; + } + + const drag = d3.behavior.drag() + .on('drag', function(d, i) { + if (circleSelected) { + dragging = true; + const p = d3.mouse(container.node()); + const ll = unproject([p[0], p[1]]); + if (i) { + circleOuter = ll; + } else { + const dlat = circleCenter.lat - ll.lat; + const dlng = circleCenter.lng - ll.lng; + circleCenter = ll; + circleOuter.lat -= dlat; + circleOuter.lng -= dlng; + } + update(); + querybuilder(); + } else { + return; + } + }) + .on('dragend', function() { + // kind of a dirty hack... + setTimeout(function() { + dragging = false; + querybuilder(); + }, 100); + }); + + function clear(dontUpdate) { + circleCenter = null; + circleOuter = null; + circleSelected = false; + container.selectAll('circle.lasso').remove(); + container.selectAll('circle.control').remove(); + container.selectAll('line.lasso').remove(); + dispatch.clear(); + if (!dontUpdate) { + querybuilder(); + } + return; + } + + this.clear = clear; + + update = function(g) { + if (g) { + container = g; + } + if (!circleCenter || !circleOuter) return; + const dist = distance(circleCenter, circleOuter); + const circleLasso = container.selectAll('circle.lasso').data([dist]); + circleLasso.enter().append('circle') + .classed('lasso', true) + .style({ + stroke: SELECTED_COLOR, + 'stroke-width': 2, + fill: SELECTED_COLOR, + 'fill-opacity': 0.1 + }); + + circleLasso + .attr({ + cx: project(circleCenter).x, + cy: project(circleCenter).y, + r: dist + }); + + const line = container.selectAll('line.lasso').data([circleOuter]); + line.enter().append('line') + .classed('lasso', true) + .style({ + stroke: CONTROL_COLOR, + 'stroke-dasharray': '2 2' + }); + + line.attr({ + x1: project(circleCenter).x, + y1: project(circleCenter).y, + x2: project(circleOuter).x, + y2: project(circleOuter).y + }); + + const controls = container.selectAll('circle.control') + .data([circleCenter, circleOuter]); + controls.enter().append('circle') + .classed('control', true) + .style({ + 'cursor': 'move' + }); + + controls.attr({ + cx: function(d) { return project(d).x; }, + cy: function(d) { return project(d).y; }, + r: 5, + stroke: CONTROL_COLOR, + fill: CONTROL_COLOR, + 'fill-opacity': 0.7 + }) + .call(drag) + .on('mousedown', function() { + map.dragPan.disable(); + }) + .on('mouseup', function() { + map.dragPan.enable(); + }); + + dispatch.update(); + }; // end update() + this.update = update; + + function setCircle(centerLL, radiusMiles) { + const pCenter = turfPoint([centerLL[0], centerLL[1]]); + const pOuter = turfDestination(pCenter, radiusMiles, 45, 'miles'); + circleCenter = mapboxgl.LngLat.convert(pCenter.geometry.coordinates); + circleOuter = mapboxgl.LngLat.convert(pOuter.geometry.coordinates); + circleSelected = true; + update(); + } + this.setCircle = setCircle; + + container.on('mousedown.circle', function() { + if (!d3.event.shiftKey) return; + if (dragging && circleSelected) return; + if (!dragging && circleSelected) { + // reset and remove circle + clear(); + return; + } + + map.dragPan.disable(); + const p = d3.mouse(this); + const ll = unproject([p[0], p[1]]); + + if (!circleCenter) { + // We set the center to the initial click + circleCenter = ll; + circleOuter = ll; + } + update(); + }); + + container.on('mousemove.circle', function() { + if (circleSelected || !circleCenter) return; + // we draw a guideline for where the next point would go. + const p = d3.mouse(this); + const ll = unproject([p[0], p[1]]); + circleOuter = ll; + update(); + querybuilder(); + }); + + container.on('mouseup.circle', function() { + if (dragging && circleSelected) return; + + map.dragPan.enable(); + + const p = d3.mouse(this); + const ll = unproject([p[0], p[1]]); + + if (circleCenter) { + if (!circleSelected) { + circleOuter = ll; + circleSelected = true; + querybuilder(); + } + } + }); + + this.projection = function(val) { + if (!val) return project; + project = val; + return this; + }; + + this.inverseProjection = function(val) { + if (!val) return unproject; + unproject = val; + return this; + }; + + this.distance = function(ll) { + if (!ll) ll = circleOuter; + return distance(circleCenter, ll); + }; + + d3.rebind(this, dispatch, 'on'); + return this; + } + + function disableMapsFeature() { + // disable in preferences and persist + app.preferences.save('googleMaps', false); + delete window.google; + // options.view.parent.render(); + } + + function loadMapBoxScript(done) { + const script = document.createElement('script'); + script.setAttribute('type', 'text/javascript'); + script.src = 'https://api.tiles.mapbox.com/mapbox-gl-js/v0.15.0/mapbox-gl.js'; + script.onerror = function() { + done('Error ocurred while loading MapBox script.'); + }; + script.onload = function() { + done(null, window.mapboxgl); + }; + document.getElementsByTagName('head')[0].appendChild(script); + } + + + function selectFromQuery() { + if (options.query === undefined) { + circleControl.clear(true); + } else { + const center = options.query.$geoWithin.$centerSphere[0]; + const radius = options.query.$geoWithin.$centerSphere[1] * 3963.2; + // only redraw if the center/radius is different to the existing circle + if (radius !== mileDistance || !_.isEqual(center, [circleCenter.lng, circleCenter.lat])) { + circleControl.setCircle(center, radius); + } + } + } + // --- end chart setup --- + + function chart(selection) { + // load mapbox script + if (!window.mapboxgl) { + loadMapBoxScript(function(err) { + if (err) { + disableMapsFeature(); + } else { + chart.call(this, selection); + } + }); + return; + } + mapboxgl = window.mapboxgl; + + selection.each(function(data) { + function getLL(d) { + if (d instanceof mapboxgl.LngLat) return d; + return new mapboxgl.LngLat(+d[0], +d[1]); + } + function project(d) { + return map.project(getLL(d)); + } + + const el = d3.select(this); + const innerWidth = width - margin.left - margin.right; + const innerHeight = height - margin.top - margin.bottom; + + // append inner div once + const innerDiv = el.selectAll('div.map').data([null]); + innerDiv.enter().append('div') + .attr('class', 'map'); + + innerDiv + .style({ + width: innerWidth + 'px', + height: innerHeight + 'px', + padding: margin.top + 'px ' + margin.right + 'px ' + margin.bottom + + 'px ' + margin.left + 'px;' + }); + + // append info sprinkle + el.selectAll('i.help').data([null]).enter().append('i') + .classed('help', true) + .attr('data-hook', 'schema-geo-query-builder'); + + // compute bounds from data + const bounds = new mapboxgl.LngLatBounds(); + _.each(data, function(d) { + bounds.extend(getLL(d)); + }); + + // create the map once + if (!map) { + mapboxgl.accessToken = TOKEN; + map = new mapboxgl.Map({ + container: innerDiv[0][0], + // not allowed to whitelabel the map without enterprise license + // attributionControl: false, + style: 'mapbox://styles/mapbox/light-v8', + center: bounds.getCenter() + }); + map.dragPan.enable(); + map.scrollZoom.enable(); + map.boxZoom.disable(); + + // Add zoom and rotation controls to the map + map.addControl(new mapboxgl.Navigation({position: 'top-left'})); + + // Setup our svg layer that we can manipulate with d3 + const container = map.getCanvasContainer(); + svg = d3.select(container).append('svg'); + + circleControl = new CircleSelector(svg) + .projection(project) + .inverseProjection(function(a) { + return map.unproject({x: a[0], y: a[1]}); + }); + + // when lasso changes, update point selections + circleControl.on('update', function() { + svg.selectAll('circle.dot').style({ + fill: function(d) { + const thisDist = circleControl.distance(d); + const circleDist = circleControl.distance(); + if (thisDist < circleDist) { + return SELECTED_COLOR; + } + return UNSELECTED_COLOR; + } + }); + }); + circleControl.on('clear', function() { + svg.selectAll('circle.dot').style('fill', UNSELECTED_COLOR); + }); + + /* eslint no-inner-declarations: 0 */ + render = function() { + // update points + dots.attr({ + cx: function(d) { + const x = project(d).x; + return x; + }, + cy: function(d) { + const y = project(d).y; + return y; + } + }); + // update circle + circleControl.update(svg); + }; + + // re-render our visualization whenever the view changes + map.on('viewreset', function() { + render(); + }); + map.on('move', function() { + render(); + }); + + _.defer(function() { + map.resize(); + map.fitBounds(bounds, { + linear: true, + padding: 20 + }); + }); + } // end if (!map) ... + + // draw data points + dots = svg.selectAll('circle.dot') + .data(data); + dots.enter().append('circle').classed('dot', true) + .attr('r', 4) + .style({ + fill: UNSELECTED_COLOR, + stroke: 'white', + 'stroke-opacity': 0.6, + 'stroke-width': 1 + }); + + selectFromQuery(); + render(); + }); // end selection.each() + } + + chart.width = function(value) { + if (!arguments.length) { + return width; + } + width = value; + return chart; + }; + + chart.height = function(value) { + if (!arguments.length) { + return height; + } + height = value; + return chart; + }; + + chart.options = function(value) { + if (!arguments.length) { + return options; + } + _.assign(options, value); + return chart; + }; + + chart.geoSelection = function(value) { + if (!value) { + circleControl.clear(); + return; + } + circleControl.setCircle(value[0], value[1]); + }; + + return chart; +}; + +module.exports = minicharts_d3fns_geo; diff --git a/src/internal-packages/schema/lib/d3/d3-tip.js b/src/internal-packages/schema/lib/d3/d3-tip.js new file mode 100644 index 00000000000..d84c2514f9d --- /dev/null +++ b/src/internal-packages/schema/lib/d3/d3-tip.js @@ -0,0 +1,349 @@ +/* eslint no-use-before-define: 0, one-var: 0, no-else-return: 0, no-unused-vars: 0, eqeqeq: 0, no-shadow: 0, yoda: 0, consistent-return: 0, one-var: 0, camelcase: 0 */ +// d3.tip +// Copyright (c) 2013 Justin Palmer +// +// Tooltips for d3.js SVG visualizations + +(function(root, factory) { + if (typeof module === 'object' && module.exports) { + // CommonJS + module.exports = function(d3) { + d3.tip = factory(d3); + return d3.tip; + }; + } else { + // Browser global. + root.d3.tip = factory(root.d3); + } +}(this, function(d3) { + // Public - contructs a new tooltip + // + // Returns a tip + return function() { + var direction = d3_tip_direction; + var offset = d3_tip_offset; + var html = d3_tip_html; + var node = initNode(); + var svg = null; + var point = null; + var target = null; + + function tip(vis) { + svg = getSVGNode(vis); + if (!svg) { + return; + } + point = svg.createSVGPoint(); + document.body.appendChild(node); + } + + // Public - show the tooltip on the screen + // + // Returns a tip + tip.show = function() { + var args = Array.prototype.slice.call(arguments); + if (args[args.length - 1] instanceof SVGElement) { + target = args.pop(); + } + + var content = html.apply(this, args), + poffset = offset.apply(this, args), + dir = direction.apply(this, args), + nodel = getNodeEl(), + i = directions.length, + coords, + scrollTop = document.documentElement.scrollTop || document.body.scrollTop, + scrollLeft = document.documentElement.scrollLeft || document.body.scrollLeft; + + nodel.html(content) + .style({ + opacity: 1, + 'pointer-events': 'all' + }); + + while (i--) { + nodel.classed(directions[i], false); + } + coords = direction_callbacks.get(dir).apply(this); + nodel.classed(dir, true).style({ + top: (coords.top + poffset[0] + scrollTop).toString() + 'px', + left: (coords.left + poffset[1] + scrollLeft).toString() + 'px' + }); + + return tip; + }; + + // Public - hide the tooltip + // + // Returns a tip + tip.hide = function() { + var nodel = getNodeEl(); + nodel.style({ + opacity: 0, + 'pointer-events': 'none' + }); + return tip; + }; + + // Public: Proxy attr calls to the d3 tip container. Sets or gets attribute value. + // + // n - name of the attribute + // v - value of the attribute + // + // Returns tip or attribute value + tip.attr = function(n, v) { + if (arguments.length < 2 && typeof n === 'string') { + return getNodeEl().attr(n); + } else { + var args = Array.prototype.slice.call(arguments); + d3.selection.prototype.attr.apply(getNodeEl(), args); + } + + return tip; + }; + + // Public: Proxy style calls to the d3 tip container. Sets or gets a style value. + // + // n - name of the property + // v - value of the property + // + // Returns tip or style property value + tip.style = function(n, v) { + if (arguments.length < 2 && typeof n === 'string') { + return getNodeEl().style(n); + } else { + var args = Array.prototype.slice.call(arguments); + d3.selection.prototype.style.apply(getNodeEl(), args); + } + + return tip; + }; + + // Public: Set or get the direction of the tooltip + // + // v - One of n(north), s(south), e(east), or w(west), nw(northwest), + // sw(southwest), ne(northeast) or se(southeast) + // + // Returns tip or direction + tip.direction = function(v) { + if (!arguments.length) { + return direction; + } + direction = v == null ? v : d3.functor(v); + + return tip; + }; + + // Public: Sets or gets the offset of the tip + // + // v - Array of [x, y] offset + // + // Returns offset or + tip.offset = function(v) { + if (!arguments.length) { + return offset; + } + offset = v == null ? v : d3.functor(v); + + return tip; + }; + + // Public: sets or gets the html value of the tooltip + // + // v - String value of the tip + // + // Returns html value or tip + tip.html = function(v) { + if (!arguments.length) { + return html; + } + html = v == null ? v : d3.functor(v); + + return tip; + }; + + // Public: destroys the tooltip and removes it from the DOM + // + // Returns a tip + tip.destroy = function() { + if (node) { + getNodeEl().remove(); + node = null; + } + return tip; + }; + + function d3_tip_direction() { + return 'n'; + } + function d3_tip_offset() { + return [0, 0]; + } + function d3_tip_html() { + return ' '; + } + + var direction_callbacks = d3.map({ + n: direction_n, + s: direction_s, + e: direction_e, + w: direction_w, + nw: direction_nw, + ne: direction_ne, + sw: direction_sw, + se: direction_se + }), + + directions = direction_callbacks.keys(); + + function direction_n() { + var bbox = getScreenBBox(); + return { + top: bbox.n.y - node.offsetHeight, + left: bbox.n.x - node.offsetWidth / 2 + }; + } + + function direction_s() { + var bbox = getScreenBBox(); + return { + top: bbox.s.y, + left: bbox.s.x - node.offsetWidth / 2 + }; + } + + function direction_e() { + var bbox = getScreenBBox(); + return { + top: bbox.e.y - node.offsetHeight / 2, + left: bbox.e.x + }; + } + + function direction_w() { + var bbox = getScreenBBox(); + return { + top: bbox.w.y - node.offsetHeight / 2, + left: bbox.w.x - node.offsetWidth + }; + } + + function direction_nw() { + var bbox = getScreenBBox(); + return { + top: bbox.nw.y - node.offsetHeight, + left: bbox.nw.x - node.offsetWidth + }; + } + + function direction_ne() { + var bbox = getScreenBBox(); + return { + top: bbox.ne.y - node.offsetHeight, + left: bbox.ne.x + }; + } + + function direction_sw() { + var bbox = getScreenBBox(); + return { + top: bbox.sw.y, + left: bbox.sw.x - node.offsetWidth + }; + } + + function direction_se() { + var bbox = getScreenBBox(); + return { + top: bbox.se.y, + left: bbox.e.x + }; + } + + function initNode() { + var node = d3.select(document.createElement('div')); + node.style({ + position: 'absolute', + top: 0, + opacity: 0, + 'pointer-events': 'none', + 'box-sizing': 'border-box' + }); + + return node.node(); + } + + function getSVGNode(el) { + el = el.node(); + if (!el) { + return; + } + if (el.tagName.toLowerCase() === 'svg') { + return el; + } + + return el.ownerSVGElement; + } + + function getNodeEl() { + if (node === null) { + node = initNode(); + // re-add node to DOM + document.body.appendChild(node); + } + return d3.select(node); + } + + // Private - gets the screen coordinates of a shape + // + // Given a shape on the screen, will return an SVGPoint for the directions + // n(north), s(south), e(east), w(west), ne(northeast), se(southeast), nw(northwest), + // sw(southwest). + // + // +-+-+ + // | | + // + + + // | | + // +-+-+ + // + // Returns an Object {n, s, e, w, nw, sw, ne, se} + function getScreenBBox() { + var targetel = target || d3.event.target; + + while ('undefined' === typeof targetel.getScreenCTM && 'undefined' === targetel.parentNode) { + targetel = targetel.parentNode; + } + + var bbox = {}, + matrix = targetel.getScreenCTM(), + tbbox = targetel.getBBox(), + width = tbbox.width, + height = tbbox.height, + x = tbbox.x, + y = tbbox.y; + + point.x = x; + point.y = y; + bbox.nw = point.matrixTransform(matrix); + point.x += width; + bbox.ne = point.matrixTransform(matrix); + point.y += height; + bbox.se = point.matrixTransform(matrix); + point.x -= width; + bbox.sw = point.matrixTransform(matrix); + point.y -= height / 2; + bbox.w = point.matrixTransform(matrix); + point.x += width; + bbox.e = point.matrixTransform(matrix); + point.x -= width / 2; + point.y -= height / 2; + bbox.n = point.matrixTransform(matrix); + point.y += height; + bbox.s = point.matrixTransform(matrix); + + return bbox; + } + + return tip; + }; +})); diff --git a/src/internal-packages/schema/lib/d3/date.js b/src/internal-packages/schema/lib/d3/date.js new file mode 100644 index 00000000000..cb686011c8a --- /dev/null +++ b/src/internal-packages/schema/lib/d3/date.js @@ -0,0 +1,427 @@ +/* eslint no-use-before-define: 0, camelcase:0 */ +const d3 = require('d3'); +const _ = require('lodash'); +const $ = require('jquery'); +const moment = require('moment'); +const shared = require('./shared'); +const many = require('./many'); +const QueryBuilderAction = require('../../../query/lib/action'); +const inValueRange = require('../../../query/lib/util').inValueRange; + +// const debug = require('debug')('mongodb-compass:minicharts:date'); + +require('./d3-tip')(d3); + +function generateDefaults(n) { + const doc = {}; + _.each(_.range(n), function(d) { + doc[d] = []; + }); + return doc; +} + +function extractTimestamp(d) { + return d._bsontype === 'ObjectID' ? d.getTimestamp() : d; +} + +const minicharts_d3fns_date = function() { + // --- beginning chart setup --- + let width = 400; + let height = 100; + let el; + let lastNonShiftRangeValue = null; + + const upperRatio = 2.5; + const upperMargin = 20; + const options = {}; + + const weekdayLabels = moment.weekdays(); + + // A formatter for dates + const format = d3.time.format('%Y-%m-%d %H:%M:%S'); + + const margin = shared.margin; + const barcodeX = d3.time.scale(); + + // set up tooltips + const tip = d3.tip() + .attr('class', 'd3-tip') + .html(function(d) { + return d.label; + }) + .direction('n') + .offset([-9, 0]); + + const brush = d3.svg.brush() + .x(barcodeX) + // .on('brushstart', brushstart) + .on('brush', brushed) + .on('brushend', brushend); + + // function brushstart(clickedLine) { + // // remove selections and half selections + // const lines = d3.selectAll(options.view.queryAll('.selectable')); + // lines.classed('selected', function() { + // return this === clickedLine; + // }); + // lines.classed('unselected', function() { + // return this !== clickedLine; + // }); + // } + + function handleDrag() { + const lines = el.selectAll('line.selectable'); + const numSelected = el.selectAll('line.selectable.selected').length; + const s = brush.extent(); + + // add `unselected` class to all elements + lines.classed('unselected', true); + lines.classed('selected', false); + + // get elements within the brush + const selected = lines.filter(function(d) { + return s[0] <= d.ts && d.ts <= s[1]; + }); + + // add `selected` class and remove `unselected` class + selected.classed('selected', true); + selected.classed('unselected', false); + + if (numSelected !== selected[0].length) { + // number of selected items has changed, trigger querybuilder event + if (selected[0].length === 0) { + // clear value + QueryBuilderAction.clearValue({ + field: options.fieldName + }); + return; + } + } + + const minValue = _.min(selected.data(), function(d) { + return d.ts; + }); + const maxValue = _.max(selected.data(), function(d) { + return d.ts; + }); + + if (_.isEqual(minValue.ts, maxValue.ts)) { + // if values are the same, single equality query + QueryBuilderAction.setValue({ + field: options.fieldName, + value: minValue.value + }); + return; + } + // binned values, build range query with $gte and $lte + QueryBuilderAction.setRangeValues({ + field: options.fieldName, + min: minValue.value, + max: maxValue.value, + maxInclusive: true + }); + } + + function brushed() { + handleDrag(); + } + + function brushend() { + d3.select(this).call(brush.clear()); + } + + + function handleMouseDown(d) { + if (d3.event.shiftKey && lastNonShiftRangeValue) { + const minVal = d.ts < lastNonShiftRangeValue.ts ? d.value : lastNonShiftRangeValue.value; + const maxVal = d.ts > lastNonShiftRangeValue.ts ? d.value : lastNonShiftRangeValue.value; + QueryBuilderAction.setRangeValues({ + field: options.fieldName, + min: minVal, + max: maxVal, + maxInclusive: true + }); + } else { + // remember non-shift value so that range can be extended with shift + lastNonShiftRangeValue = d; + QueryBuilderAction.setValue({ + field: options.fieldName, + value: d.value, + unsetIfSet: true + }); + } + + const parent = $(this).closest('.minichart'); + const background = parent.find('g.brush > rect.background')[0]; + const brushNode = parent.find('g.brush')[0]; + const start = barcodeX.invert(d3.mouse(background)[0]); + + const w = d3.select(window) + .on('mousemove', mousemove) + .on('mouseup', mouseup); + + d3.event.preventDefault(); // disable text dragging + + function mousemove() { + const extent = [start, barcodeX.invert(d3.mouse(background)[0])]; + d3.select(brushNode).call(brush.extent(_.sortBy(extent))); + brushed.call(brushNode); + } + + function mouseup() { + // bar.classed('selected', true); + w.on('mousemove', null).on('mouseup', null); + brushend.call(brushNode); + } + } + + function selectFromQuery(lines) { + if (options.query === undefined) { + lines.classed('unselected', false); + lines.classed('selected', false); + lines.classed('half', false); + return; + } + lines.each(function(d) { + d.inRange = inValueRange(options.query, d); + }); + + lines.classed('selected', function(d) { + return d.inRange === 'yes'; + }); + lines.classed('unselected', function(d) { + return d.inRange === 'no'; + }); + } + + + function chart(selection) { + selection.each(function(data) { + const values = data.map(function(d) { + const ts = extractTimestamp(d); + return { + label: format(ts), + ts: ts, + value: d, + count: 1 + }; + }); + + // without `-1` the tooltip won't always trigger on the rightmost value + const innerWidth = width - margin.left - margin.right - 1; + const innerHeight = height - margin.top - margin.bottom; + el = d3.select(this); + + const barcodeTop = Math.floor(innerHeight / 2 + 15); + const barcodeBottom = Math.floor(innerHeight - 10); + + const upperBarBottom = innerHeight / 2 - 20; + + barcodeX + .domain(d3.extent(values, function(d) { + return d.ts; + })) + .range([0, innerWidth]); + + // group by weekdays + const weekdays = _(values) + .groupBy(function(d) { + return moment(d.ts).weekday(); + }) + .defaults(generateDefaults(7)) + .map(function(d, i) { + return { + label: weekdayLabels[i], + count: d.length + }; + }) + .value(); + + // group by hours + const hourLabels = d3.range(24); + const hours = _(values) + .groupBy(function(d) { + return d.ts.getHours(); + }) + .defaults(generateDefaults(24)) + .map(function(d, i) { + return { + label: hourLabels[i] + ':00', + count: d.length + }; + }) + .value(); + el.call(tip); + + const g = el.selectAll('g').data([data]); + + // append g element if it doesn't exist yet + const gEnter = g.enter() + .append('g') + .attr('transform', 'translate(' + margin.left + ',' + margin.top + ')'); + + gEnter.append('g') + .attr('class', 'weekday') + .append('text') + .attr('class', 'date-icon fa-fw') + .attr('x', 0) + .attr('dx', '-0.6em') + .attr('y', 0) + .attr('dy', '1em') + .attr('text-anchor', 'end') + .attr('font-family', 'FontAwesome') + .text('\uf133'); + + gEnter.append('g') + .attr('class', 'hour') + .append('text') + .attr('class', 'date-icon fa-fw') + .attr('x', 0) + .attr('dx', '-0.6em') + .attr('y', 0) + .attr('dy', '1em') + .attr('text-anchor', 'end') + .attr('font-family', 'FontAwesome') + .text('\uf017'); + + el.select('.hour') + .attr('transform', 'translate(' + (innerWidth / (upperRatio + 1) + upperMargin) + ', 0)'); + + const gBrush = g.selectAll('.brush').data([0]); + gBrush.enter().append('g') + .attr('class', 'brush') + .call(brush) + .selectAll('rect') + .attr('y', barcodeTop) + .attr('height', barcodeBottom - barcodeTop); + + gEnter.append('g') + .attr('class', 'line-container'); + + const lines = g.select('.line-container').selectAll('.selectable').data(values, function(d) { + return d.ts; + }); + + lines.enter().append('line') + .attr('class', 'line selectable') + .style('opacity', function() { + return lines.size() > 200 ? 0.3 : 1.0; + }) + .on('mouseover', tip.show) + .on('mouseout', tip.hide) + .on('mousedown', handleMouseDown); + + // disabling direct onClick handler in favor of click-drag + // .on('click', handleClick); + + lines + .attr('y1', barcodeTop) + .attr('y2', barcodeBottom) + .attr('x2', function(d) { + return barcodeX(d.ts); + }) + .attr('x1', function(d) { + return barcodeX(d.ts); + }); + + lines.exit().remove(); + + // unset the non-shift clicked bar marker if the query is empty + if (options.query === undefined) { + lastNonShiftRangeValue = null; + } + + // paint remaining lines in correct color + el.selectAll('line.selectable').call(selectFromQuery); + + const text = g.selectAll('.text') + .data(barcodeX.domain()); + + text.enter().append('text') + .attr('class', 'text') + .attr('dy', '0.75em') + .attr('y', barcodeBottom + 5); + + text + .attr('x', function(d, i) { + return i * innerWidth; + }) + .attr('text-anchor', function(d, i) { + return i ? 'end' : 'start'; + }) + .text(function(d, i) { + if (format(barcodeX.domain()[0]) === format(barcodeX.domain()[1])) { + if (i === 0) { + return 'inserted: ' + format(d); + } + } else { + return (i ? 'last: ' : 'first: ') + format(d); + } + }); + + text.exit().remove(); + + let chartWidth = innerWidth / (upperRatio + 1) - upperMargin; + const weekdayContainer = g.select('g.weekday').data([weekdays]); + const manyDayChart = many() + .width(chartWidth) + .height(upperBarBottom) + .options({ + selectable: false, + bgbars: true, + labels: { + 'text-anchor': 'middle', + text: function(d) { + return d.label[0]; + } + } + }); + weekdayContainer.call(manyDayChart); + + chartWidth = innerWidth / (upperRatio + 1) * upperRatio - upperMargin; + const hourContainer = g.select('g.hour').data([hours]); + const manyHourChart = many() + .width(chartWidth) + .height(upperBarBottom) + .options({ + selectable: false, + bgbars: true, + labels: { + text: function(d, i) { + return i % 6 === 0 || i === 23 ? d.label : ''; + } + } + }); + hourContainer.call(manyHourChart); + }); + } + + chart.width = function(value) { + if (!arguments.length) { + return width; + } + width = value; + return chart; + }; + + chart.height = function(value) { + if (!arguments.length) { + return height; + } + height = value; + return chart; + }; + + chart.options = function(value) { + if (!arguments.length) { + return options; + } + _.assign(options, value); + return chart; + }; + + return chart; +}; + + +module.exports = minicharts_d3fns_date; diff --git a/src/internal-packages/schema/lib/d3/few.js b/src/internal-packages/schema/lib/d3/few.js new file mode 100644 index 00000000000..8e44e154133 --- /dev/null +++ b/src/internal-packages/schema/lib/d3/few.js @@ -0,0 +1,253 @@ +/* eslint no-use-before-define: 0, camelcase: 0 */ +const d3 = require('d3'); +const $ = require('jquery'); +const _ = require('lodash'); +const shared = require('./shared'); +const QueryBuilderAction = require('../../../query/lib/action'); +const tooltipTemplate = require('./tooltip.jade'); +const hasDistinctValue = require('../../../query/lib/util').hasDistinctValue; + +// const debug = require('debug')('mongodb-compass:minicharts:few'); + +require('./d3-tip')(d3); + +const minicharts_d3fns_few = function() { + // --- beginning chart setup --- + let width = 400; // default width + let height = 100; // default height + let el; + + const barHeight = 25; + const brushHeight = 80; + const options = {}; + + const xScale = d3.scale.linear(); + + // set up tooltips + const tip = d3.tip() + .attr('class', 'd3-tip') + .direction('n') + .offset([-9, 0]); + const brush = d3.svg.brush() + .x(xScale) + .on('brush', brushed) + .on('brushend', brushend); + // --- end chart setup --- + + function handleDrag() { + // ignore this event when shift is pressed, only works for single clicks + if (d3.event.shiftKey) { + return; + } + const bars = el.selectAll('rect.selectable'); + const numSelected = el.selectAll('rect.selectable.selected')[0].length; + const s = brush.extent(); + // add `unselected` class to all elements + bars.classed('unselected', true); + // get elements within the brush + const selected = bars.filter(function(d) { + const left = d.xpos; + const right = left + d.count; + return s[0] <= right && left <= s[1]; + }); + // add `selected` class and remove `unselected` class + selected.classed('selected', true); + selected.classed('unselected', false); + + // if selection has changed, trigger query builder event + if (numSelected !== selected[0].length) { + const values = _.map(selected.data(), 'value'); + QueryBuilderAction.setDistinctValues({ + field: options.fieldName, + value: values + }); + } + } + + function brushed() { + handleDrag(); + } + + function brushend() { + d3.select(this).call(brush.clear()); + } + + function handleMouseDown(d) { + const parent = $(this).closest('.minichart'); + const background = parent.find('g.brush > rect.background')[0]; + const brushNode = parent.find('g.brush')[0]; + const start = xScale.invert(d3.mouse(background)[0]); + + + const qbAction = d3.event.shiftKey ? + QueryBuilderAction.toggleDistinctValue : QueryBuilderAction.setValue; + qbAction({ + field: options.fieldName, + value: d.value, + unsetIfSet: true + }); + + const w = d3.select(window) + .on('mousemove', mousemove) + .on('mouseup', mouseup); + + d3.event.preventDefault(); // disable text dragging + + function mousemove() { + const extent = [start, xScale.invert(d3.mouse(background)[0])]; + d3.select(brushNode).call(brush.extent(_.sortBy(extent))); + brushed.call(brushNode); + } + + function mouseup() { + w.on('mousemove', null).on('mouseup', null); + brushend.call(brushNode); + } + } + + function selectFromQuery(bars) { + // handle distinct selections + if (options.query === undefined) { + bars.classed('unselected', false); + bars.classed('selected', false); + bars.classed('half', false); + return; + } + bars.classed('selected', function(d) { + return hasDistinctValue(options.query, d.value); + }); + bars.classed('unselected', function(d) { + return !hasDistinctValue(options.query, d.value); + }); + } + + function chart(selection) { + selection.each(function(data) { + _.each(data, (d, i) => { + data[i].xpos = _.sum(_(data) + .slice(0, i) + .map('count') + .value() + ); + }); + const values = _.map(data, 'count'); + const sumValues = d3.sum(values); + const maxValue = d3.max(values); + const percentFormat = shared.friendlyPercentFormat(maxValue / sumValues * 100); + el = d3.select(this); + + xScale + .domain([0, sumValues]) + .range([0, width]); + + // setup tool tips + tip.html(function(d, i) { + if (typeof d.tooltip === 'function') { + return d.tooltip(d, i); + } + return d.tooltip || tooltipTemplate({ + label: shared.truncateTooltip(d.label), + count: percentFormat(d.count / sumValues * 100, false) + }); + }); + el.call(tip); + + const gBrush = el.selectAll('.brush').data([0]); + gBrush.enter().append('g') + .attr('class', 'brush') + .call(brush) + .selectAll('rect') + .attr('y', (height - brushHeight) / 2) + .attr('height', brushHeight); + + // select all g.bar elements + const bar = el.selectAll('g.bar') + .data(data, function(d) { + return d.label; // identify data by its label + }); + + bar + .attr('transform', function(d) { + return 'translate(' + xScale(d.xpos) + ', ' + (height - barHeight) / 2 + ')'; + }); + + const barEnter = bar.enter().append('g') + .attr('class', 'bar few') + .attr('transform', function(d) { // repeat transform attr here but without transition + return 'translate(' + xScale(d.xpos) + ', ' + (height - barHeight) / 2 + ')'; + }) + .on('mousedown', handleMouseDown); + + barEnter.append('rect') + .attr('class', function(d, i) { + return 'selectable fg fg-' + i; + }) + .attr('y', 0) + .attr('x', 0) + .attr('height', barHeight); + + barEnter.append('text') + .attr('y', barHeight / 2) + .attr('dy', '0.3em') + .attr('dx', 10) + .attr('text-anchor', 'start') + .attr('fill', 'white'); + + barEnter.append('rect') + .attr('class', 'glass') + .attr('y', 0) + .attr('x', 0) + .attr('height', barHeight) + .on('mouseover', tip.show) + .on('mouseout', tip.hide); + + bar.select('rect.selectable') + .attr('width', function(d) { + return xScale(d.count); + }); + + bar.select('rect.glass') + .attr('width', function(d) { + return xScale(d.count); + }); + + bar.select('text') + .text(function(d) { + return d.label; + }); + + bar.exit().remove(); + + // paint remaining bars in correct color + el.selectAll('rect.selectable').call(selectFromQuery); + }); + } + + chart.width = function(value) { + if (!arguments.length) { + return width; + } + width = value; + return chart; + }; + + chart.height = function(value) { + if (!arguments.length) { + return height; + } + height = value; + return chart; + }; + + chart.options = function(value) { + if (!arguments.length) { + return options; + } + _.assign(options, value); + return chart; + }; + + return chart; +}; + +module.exports = minicharts_d3fns_few; diff --git a/src/internal-packages/schema/lib/d3/index.js b/src/internal-packages/schema/lib/d3/index.js new file mode 100644 index 00000000000..9666b377e4b --- /dev/null +++ b/src/internal-packages/schema/lib/d3/index.js @@ -0,0 +1,8 @@ +module.exports = { + number: require('./number'), + boolean: require('./boolean'), + date: require('./date'), + string: require('./string'), + objectid: require('./date'), + coordinates: require('./coordinates') +}; diff --git a/src/internal-packages/schema/lib/d3/many.js b/src/internal-packages/schema/lib/d3/many.js new file mode 100644 index 00000000000..5a926253d51 --- /dev/null +++ b/src/internal-packages/schema/lib/d3/many.js @@ -0,0 +1,482 @@ +/* eslint no-use-before-define: 0, camelcase: 0 */ +const d3 = require('d3'); +const $ = require('jquery'); +const _ = require('lodash'); +const shared = require('./shared'); +const QueryBuilderAction = require('../../../query/lib/action'); +const tooltipTemplate = require('./tooltip.jade'); +const hasDistinctValue = require('../../../query/lib/util').hasDistinctValue; +const inValueRange = require('../../../query/lib/util').inValueRange; + +// const debug = require('debug')('mongodb-compass:minicharts:many'); + +require('./d3-tip')(d3); + +const minicharts_d3fns_many = function() { + // --- beginning chart setup --- + let width = 400; // default width + let height = 100; // default height + let el; + let lastNonShiftRangeValue = null; + + const options = { + bgbars: false, + scale: false, + labels: false, // label defaults will be set further below + selectable: true, // setting to false disables query builder for this chart + selectionType: 'distinct' // can be `distinct` or `range` + }; + + const xScale = d3.scale.ordinal(); + const yScale = d3.scale.linear(); + const labelScale = d3.scale.ordinal(); + + // set up tooltips + const tip = d3.tip() + .attr('class', 'd3-tip') + .direction('n') + .offset([-9, 0]); + const brush = d3.svg.brush() + .on('brush', brushed) + .on('brushend', brushend); + // --- end chart setup --- + + function handleDrag() { + // ignore this event when shift is pressed for distinct selections. + // multiple unconnected $in ranges are not supported yet + if (d3.event.shiftKey) { + return; + } + const bars = el.selectAll('rect.selectable'); + const numSelected = el.selectAll('rect.selectable.selected')[0].length; + const s = brush.extent(); + // add `unselected` class to all elements + bars.classed('unselected', true); + // get elements within the brush + const selected = bars.filter(function(d) { + const left = xScale(d.label); + const right = left + xScale.rangeBand(); + return s[0] <= right && left <= s[1]; + }); + // add `selected` class and remove `unselected` class + selected.classed('selected', true); + selected.classed('unselected', false); + + // if selection has changed, trigger query builder event + if (numSelected !== selected[0].length) { + if (selected[0].length === 0) { + // clear value + QueryBuilderAction.clearValue({ + field: options.fieldName + }); + return; + } + // distinct values (strings) + if (options.selectionType === 'distinct') { + const values = _.map(selected.data(), 'value'); + QueryBuilderAction.setDistinctValues({ + field: options.fieldName, + value: values + }); + return; + } + // numeric types + const minValue = _.min(selected.data(), function(d) { + return d.value; + }); + const maxValue = _.max(selected.data(), function(d) { + return d.value; + }); + + if (minValue.value === maxValue.value + maxValue.dx) { + // if not binned and values are the same, single equality query + QueryBuilderAction.setValue({ + field: options.fieldName, + value: minValue.value + }); + return; + } + // binned values, build range query with $gte and $lt (if binned) + // or $gte and $lte (if not binned) + QueryBuilderAction.setRangeValues({ + field: options.fieldName, + min: minValue.value, + max: maxValue.value + maxValue.dx, + maxInclusive: maxValue.dx === 0 + }); + } + } + + function brushed() { + handleDrag(); + } + + function brushend() { + d3.select(this).call(brush.clear()); + } + + + /** + * Handles event of single mousedown (either as click, or beginning of a + * brush drag event). + * + * For distinct (non-numeric values), the behavior is this: + * - If shift is pressed: toggle the value (selected if it was unselected, + * and vice versa) + * - If shift is not pressed: set the value to selected one, unless already + * selected, in which case unselect all values. + * + * For ranges (numeric values), the behavior is this: + * - If the bar represents a single value (not binned), create a single value + * equality query, e.g. {"field": 16}. + * - If the bar represents a range (binned), create a $gte / $lt range query, + * e.g. {"field": {"$gte": 20, "$lt": 25}} for a bin size of 5. + * + * @param {Document} d the data associated with the clicked bar + */ + function handleMouseDown(d) { + if (!options.selectable) { + return; + } + + if (options.selectionType === 'distinct') { + // distinct values, behavior dependent on shift key + const qbAction = d3.event.shiftKey ? + QueryBuilderAction.toggleDistinctValue : QueryBuilderAction.setValue; + qbAction({ + field: options.fieldName, + value: d.value, + unsetIfSet: true + }); + } else if (d3.event.shiftKey && lastNonShiftRangeValue) { + QueryBuilderAction.setRangeValues({ + field: options.fieldName, + min: Math.min(d.value, lastNonShiftRangeValue.value), + max: Math.max(d.value + d.dx, lastNonShiftRangeValue.value + lastNonShiftRangeValue.dx), + maxInclusive: d.dx === 0 + }); + } else { + // remember non-shift value so that range can be extended with shift + lastNonShiftRangeValue = d; + if (d.dx > 0) { + // binned bars, turn single value into range + QueryBuilderAction.setRangeValues({ + field: options.fieldName, + min: d.value, + max: d.value + d.dx, + unsetIfSet: true + }); + } else { + // bars don't represent bins, build single value query + QueryBuilderAction.setValue({ + field: options.fieldName, + value: d.value, + unsetIfSet: true + }); + } + } + + const parent = $(this).closest('.minichart'); + const background = parent.find('g.brush > rect.background')[0]; + const brushNode = parent.find('g.brush')[0]; + const start = d3.mouse(background)[0]; + + const w = d3.select(window) + .on('mousemove', mousemove) + .on('mouseup', mouseup); + + d3.event.preventDefault(); // disable text dragging + + function mousemove() { + const extent = [start, d3.mouse(background)[0]]; + d3.select(brushNode).call(brush.extent(_.sortBy(extent))); + brushed.call(brushNode); + } + + function mouseup() { + w.on('mousemove', null).on('mouseup', null); + brushend.call(brushNode); + } + } + + function selectFromQuery(bars) { + if (options.query === undefined) { + bars.classed('unselected', false); + bars.classed('selected', false); + bars.classed('half', false); + return; + } + // handle distinct selections + if (options.selectionType === 'distinct') { + bars.each(function(d) { + d.hasDistinct = hasDistinctValue(options.query, d.value); + }); + bars.classed('selected', function(d) { + return d.hasDistinct; + }); + bars.classed('unselected', function(d) { + return !d.hasDistinct; + }); + } else if (options.selectionType === 'range') { + bars.each(function(d) { + d.inRange = inValueRange(options.query, d); + }); + bars.classed('selected', function(d) { + return d.inRange === 'yes'; + }); + bars.classed('half-selected', function(d) { + return d.inRange === 'partial'; + }); + bars.classed('unselected', function(d) { + return d.inRange === 'no'; + }); + } + } + + function chart(selection) { + /* eslint complexity: 0 */ + selection.each(function(data) { + const values = _.pluck(data, 'count'); + const maxValue = d3.max(values); + const sumValues = d3.sum(values); + const percentFormat = shared.friendlyPercentFormat(maxValue / sumValues * 100); + const labels = options.labels; + el = d3.select(this); + + xScale + .domain(_.pluck(data, 'label')) + .rangeRoundBands([0, width], 0.3, 0.0); + + brush.x(xScale); + brush.extent(brush.extent()); + + yScale + .domain([0, maxValue]) + .range([height, 0]); + + // set label defaults + if (options.labels) { + _.defaults(labels, { + 'text-anchor': function(d, i) { + if (i === 0) { + return 'start'; + } + if (i === data.length - 1) { + return 'end'; + } + return 'middle'; + }, + x: labels['text-anchor'] === 'middle' ? xScale.rangeBand() / 2 : function(d, i) { + if (i === 0) { + return 0; + } + if (i === data.length - 1) { + return xScale.rangeBand(); + } + return xScale.rangeBand() / 2; + }, + y: height + 5, + dy: '0.75em', + text: function(d) { + return d.count; + } + }); + } + + // setup tool tips + tip.html(function(d, i) { + if (typeof d.tooltip === 'function') { + return d.tooltip(d, i); + } + return d.tooltip || tooltipTemplate({ + label: shared.truncateTooltip(d.label), + count: percentFormat(d.count / sumValues * 100, false) + }); + }); + el.call(tip); + + // draw scale labels and lines if requested + if (options.scale) { + const triples = function(v) { + return [v, v / 2, 0]; + }; + + const scaleLabels = _.map(triples(maxValue / sumValues * 100), function(x) { + return percentFormat(x, true); + }); + + labelScale + .domain(scaleLabels) + .rangePoints([0, height]); + + const legend = el.selectAll('g.legend') + .data(scaleLabels); + + // create new legend elements + const legendEnter = legend.enter().append('g') + .attr('class', 'legend'); + + legendEnter + .append('text') + .attr('x', 0) + .attr('dx', '-1em') + .attr('dy', '0.3em') + .attr('text-anchor', 'end'); + + legendEnter + .append('line') + .attr('class', 'bg') + .attr('x1', -5) + .attr('y1', 0) + .attr('y2', 0); + + // update legend elements + legend + .attr('transform', function(d) { + return 'translate(0, ' + labelScale(d) + ')'; + }); + + legend.select('text') + .text(function(d) { + return d; + }); + + legend.select('line') + .attr('x2', width); + + legend.exit().remove(); + } + + if (options.selectable) { + const gBrush = el.selectAll('.brush').data([0]); + gBrush.enter().append('g') + .attr('class', 'brush') + .call(brush) + .selectAll('rect') + .attr('y', 0) + .attr('height', height); + } + + // select all g.bar elements + const bar = el.selectAll('.bar') + .data(data, function(d) { + return d.label; // identify data by its label + }); + + // create new bar elements as needed + const barEnter = bar.enter().append('g') + .attr('class', 'bar'); + + bar + .attr('transform', function(d) { + return 'translate(' + xScale(d.label) + ', 0)'; + }); + + // if background bars are used, fill whole area with background bar color first + if (options.bgbars) { + barEnter.append('rect') + .attr('class', 'bg'); + // .attr('width', xScale.rangeBand()) + // .attr('height', height); + } + + // now attach the foreground bars + barEnter + .append('rect') + .attr('class', options.selectable ? 'fg selectable' : 'fg') + .attr('x', 0); + // .attr('width', xScale.rangeBand()); + + // create mouseover and click handlers + if (options.bgbars) { + // ... on a separate front "glass" pane if we use background bars + barEnter.append('rect') + .attr('class', 'glass') + .attr('width', xScale.rangeBand()) + .attr('height', height) + .on('mouseover', tip.show) + .on('mouseout', tip.hide); + } else { + // ... or attach tooltips directly to foreground bars if we don't use background bars + barEnter.selectAll('.fg') + .on('mouseover', tip.show) + .on('mouseout', tip.hide); + + if (options.selectable) { + barEnter.selectAll('.selectable').on('mousedown', handleMouseDown); + } + } + + if (options.labels) { + barEnter.append('text') + .attr('x', labels.x) + .attr('dx', labels.dx) + .attr('y', labels.y) + .attr('dy', labels.dy) + .attr('text-anchor', labels['text-anchor']); + } + + + // now update _all_ bar elements (old and new) based on changes + // in data and width/height + bar.selectAll('.bg') + .attr('width', xScale.rangeBand()) + .attr('height', height); + + bar.selectAll('.fg') + // .transition() + .attr('y', function(d) { + return yScale(d.count); + }) + .attr('width', xScale.rangeBand()) + .attr('height', function(d) { + return height - yScale(d.count); + }); + + if (options.labels) { + bar.select('text').text(labels.text); + } else { + bar.select('text').remove(); + } + + // finally remove obsolete bar elements + bar.exit().remove(); + + // unset the non-shift clicked bar marker if the query is empty + if (options.query === undefined) { + lastNonShiftRangeValue = null; + } + + // paint remaining bars in correct color + el.selectAll('rect.selectable').call(selectFromQuery); + }); + } + + chart.width = function(value) { + if (!arguments.length) { + return width; + } + width = value; + return chart; + }; + + chart.height = function(value) { + if (!arguments.length) { + return height; + } + height = value; + return chart; + }; + + chart.options = function(value) { + if (!arguments.length) { + return options; + } + _.assign(options, value); + return chart; + }; + + return chart; +}; + +module.exports = minicharts_d3fns_many; diff --git a/src/internal-packages/schema/lib/d3/mapstyle.js b/src/internal-packages/schema/lib/d3/mapstyle.js new file mode 100644 index 00000000000..ec925d33e52 --- /dev/null +++ b/src/internal-packages/schema/lib/d3/mapstyle.js @@ -0,0 +1,89 @@ +module.exports = [ + { + featureType: 'administrative', + elementType: 'all', + stylers: [ + { + visibility: 'simplified' + } + ] + }, + { + featureType: 'landscape', + elementType: 'geometry', + stylers: [ + { + visibility: 'simplified' + }, + { + color: '#fcfcfc' + } + ] + }, + { + featureType: 'poi', + elementType: 'all', + stylers: [ + { + visibility: 'off' + } + ] + }, + { + featureType: 'transit.station', + elementType: 'all', + stylers: [ + { + visibility: 'off' + } + ] + }, + { + featureType: 'road.highway', + elementType: 'geometry', + stylers: [ + { + visibility: 'simplified' + }, + { + color: '#dddddd' + } + ] + }, + { + featureType: 'road.arterial', + elementType: 'geometry', + stylers: [ + { + visibility: 'simplified' + }, + { + color: '#dddddd' + } + ] + }, + { + featureType: 'road.local', + elementType: 'geometry', + stylers: [ + { + visibility: 'simplified' + }, + { + color: '#eeeeee' + } + ] + }, + { + featureType: 'water', + elementType: 'geometry', + stylers: [ + { + visibility: 'simplified' + }, + { + color: '#dddddd' + } + ] + } +]; diff --git a/src/internal-packages/schema/lib/d3/number.js b/src/internal-packages/schema/lib/d3/number.js new file mode 100644 index 00000000000..254660b4faf --- /dev/null +++ b/src/internal-packages/schema/lib/d3/number.js @@ -0,0 +1,136 @@ +/* eslint camelcase: 0 */ +const d3 = require('d3'); +const _ = require('lodash'); +const many = require('./many'); +const shared = require('./shared'); +// const debug = require('debug')('mongodb-compass:minicharts:number'); + +const minicharts_d3fns_number = function() { + let width = 400; + let height = 100; + const options = { + view: null + }; + const margin = shared.margin; + const xBinning = d3.scale.linear(); + const manyChart = many(); + + function chart(selection) { + selection.each(function(data) { + let grouped; + const el = d3.select(this); + const innerWidth = width - margin.left - margin.right; + const innerHeight = height - margin.top - margin.bottom; + + // transform data + if (options.unique < 20) { + grouped = _(data) + .groupBy(function(d) { + return d; + }) + .map(function(v, k) { + v.label = k; + v.x = parseFloat(k, 10); + v.value = v.x; + v.dx = 0; + v.count = v.length; + return v; + }) + .sortBy(function(v) { + return v.value; + }) + .value(); + } else { + // use the linear scale just to get nice binning values + xBinning + .domain(d3.extent(data)) + .range([0, innerWidth]); + + // Generate a histogram using approx. twenty uniformly-spaced bins + const ticks = xBinning.ticks(20); + const hist = d3.layout.histogram() + .bins(ticks); + + grouped = hist(data); + + _.each(grouped, function(d, i) { + let label; + if (i === 0) { + label = '< ' + (d.x + d.dx); + } else if (i === data.length - 1) { + label = '≥ ' + d.x; + } else { + label = d.x + '-' + (d.x + d.dx); + } + // remapping keys to conform with all other types + d.count = d.y; + d.value = d.x; + d.label = label; + }); + } + + const g = el.selectAll('g').data([grouped]); + + // append g element if it doesn't exist yet + g.enter() + .append('g') + .attr('transform', 'translate(' + margin.left + ',' + margin.top + ')'); + + let labels; + if (options.unique < 20) { + labels = true; + } else { + labels = { + text: function(d, i) { + if (i === 0) { + return 'min: ' + d3.min(data); + } + if (i === grouped.length - 1) { + return 'max: ' + d3.max(data); + } + return ''; + } + }; + } + + options.labels = labels; + options.scale = true; + options.selectionType = 'range'; + + manyChart + .width(innerWidth) + .height(innerHeight - 10) + .options(options); + + g.call(manyChart); + }); + } + + chart.width = function(value) { + if (!arguments.length) { + return width; + } + width = value; + return chart; + }; + + chart.height = function(value) { + if (!arguments.length) { + return height; + } + height = value; + return chart; + }; + + chart.options = function(value) { + if (!arguments.length) { + return options; + } + _.assign(options, value); + return chart; + }; + + return chart; +}; + +module.exports = minicharts_d3fns_number; diff --git a/src/internal-packages/schema/lib/d3/shared.js b/src/internal-packages/schema/lib/d3/shared.js new file mode 100644 index 00000000000..1d039bad895 --- /dev/null +++ b/src/internal-packages/schema/lib/d3/shared.js @@ -0,0 +1,46 @@ +/* eslint camelcase: 0 */ +var d3 = require('d3'); + +// source: http://bit.ly/1Tc9Tp5 +function decimalPlaces(number) { + return ((+number).toFixed(20)).replace(/^-?\d*\.?|0+$/g, '').length; +} + +var minicharts_d3fns_shared = { + + margin: { + top: 10, + right: 0, + bottom: 10, + left: 40 + }, + + friendlyPercentFormat: function(vmax) { + var prec1Format = d3.format('.1r'); + var intFormat = d3.format('.0f'); + var format = vmax > 1 ? intFormat : prec1Format; + var maxFormatted = format(vmax); + var maxDecimals = decimalPlaces(maxFormatted); + + return function(v, incPrec) { + if (v === vmax) { + return maxFormatted + '%'; + } + if (v > 1 && !incPrec) { // v > vmax || maxFormatted % 2 === 0 + return d3.round(v, maxDecimals) + '%'; + } + // adjust for corrections, if increased precision required + return d3.round(v / vmax * maxFormatted, maxDecimals + 1) + '%'; + }; + }, + + truncateTooltip: function(text, maxLength) { + maxLength = maxLength || 500; + if (text.length > maxLength) { + text = text.substring(0, maxLength - 1) + '…'; + } + return text; + } + +}; +module.exports = minicharts_d3fns_shared; diff --git a/src/internal-packages/schema/lib/d3/string.js b/src/internal-packages/schema/lib/d3/string.js new file mode 100644 index 00000000000..432c744fea4 --- /dev/null +++ b/src/internal-packages/schema/lib/d3/string.js @@ -0,0 +1,91 @@ +/* eslint camelcase: 0 */ +const d3 = require('d3'); +const _ = require('lodash'); +const few = require('./few'); +const many = require('./many'); +const shared = require('./shared'); + +const minicharts_d3fns_string = function() { + // --- beginning chart setup --- + let width = 400; + let height = 100; + const options = { + query: {} + }; + + const manyChart = many(); + const fewChart = few(); + const margin = shared.margin; + // --- end chart setup --- + + function chart(selection) { + selection.each(function(data) { + const el = d3.select(this); + const innerWidth = width - margin.left - margin.right; + const innerHeight = height - margin.top - margin.bottom; + + // group into labels and values per bucket, sort descending + const grouped = _(data) + .groupBy(function(d) { + return d; + }) + .map(function(v, k) { + return { + label: k, + value: k, + count: v.length + }; + }) + .sortByOrder('count', [false]) // descending on value + .value(); + + const g = el.selectAll('g').data([grouped]); + + // append g element if it doesn't exist yet + g.enter() + .append('g') + .attr('transform', 'translate(' + margin.left + ',' + margin.top + ')') + .attr('width', innerWidth) + .attr('height', innerHeight); + + const chartFn = grouped.length <= 5 ? fewChart : manyChart; + options.scale = true; + options.selectionType = 'distinct'; + + chartFn + .width(innerWidth) + .height(innerHeight) + .options(options); + + g.call(chartFn); + }); + } + + chart.width = function(value) { + if (!arguments.length) { + return width; + } + width = value; + return chart; + }; + + chart.height = function(value) { + if (!arguments.length) { + return height; + } + height = value; + return chart; + }; + + chart.options = function(value) { + if (!arguments.length) { + return options; + } + _.assign(options, value); + return chart; + }; + + return chart; +}; + +module.exports = minicharts_d3fns_string; diff --git a/src/internal-packages/schema/lib/d3/tooltip.jade b/src/internal-packages/schema/lib/d3/tooltip.jade new file mode 100644 index 00000000000..e50c5321618 --- /dev/null +++ b/src/internal-packages/schema/lib/d3/tooltip.jade @@ -0,0 +1,3 @@ +.tooltip-wrapper + .tooltip-label!= label + .tooltip-value #{count} diff --git a/src/internal-packages/schema/lib/store/schema-store.jsx b/src/internal-packages/schema/lib/store/schema-store.jsx new file mode 100644 index 00000000000..674ca7b78af --- /dev/null +++ b/src/internal-packages/schema/lib/store/schema-store.jsx @@ -0,0 +1,201 @@ +const Reflux = require('reflux'); +const app = require('ampersand-app'); +const StateMixin = require('reflux-state-mixin'); +const Schema = require('mongodb-schema').Schema; +const _ = require('lodash'); + +// stores +const NamespaceStore = require('hadron-reflux-store').NamespaceStore; +const QueryStore = require('../../../query/lib/store'); + +// actions +const SchemaActions = require('../action'); + +const debug = require('debug')('mongodb-compass:stores:schema'); +// const metrics = require('mongodb-js-metrics')(); + +const DEFAULT_MAX_TIME_MS = 10000; +const DEFAULT_NUM_DOCUMENTS = 1000; + +/** + * The reflux store for the schema. + */ +const SchemaStore = Reflux.createStore({ + + mixins: [StateMixin.store], + listenables: SchemaActions, + + /** + * Initialize the document list store. + */ + init: function() { + // this.listenTo(QueryActions.apply, this._sampleSchema); + + NamespaceStore.listen(() => { + this._reset(); + SchemaActions.startSampling(); + }); + + this.samplingStream = null; + this.analyzingStream = null; + this.samplingTimer = null; + this.trickleStop = null; + }, + + /** + * Initialize the schema store. + * + * @return {Object} initial schema state. + */ + getInitialState() { + return { + samplingState: 'initial', + samplingProgress: 0, + samplingTimeMS: 0, + maxTimeMS: DEFAULT_MAX_TIME_MS, + schema: null + }; + }, + + + _reset: function() { + this.setState(this.getInitialState()); + }, + + setMaxTimeMS(maxTimeMS) { + this.setState({ + maxTimeMS: maxTimeMS + }); + }, + + resetMaxTimeMS() { + this.setState({ + maxTimeMS: DEFAULT_MAX_TIME_MS + }); + }, + + stopSampling() { + if (this.samplingTimer) { + clearInterval(this.samplingTimer); + this.samplingTimer = null; + } + if (this.samplingStream) { + this.samplingStream.destroy(); + this.samplingStream = null; + } + if (this.analyzingStream) { + this.analyzingStream.destroy(); + this.analyzingStream = null; + } + }, + + /** + * This function is called when the collection filter changes. + */ + startSampling() { + const query = QueryStore.state.query; + + if (_.includes(['counting', 'sampling', 'analyzing'], this.state.samplingState)) { + return; + } + + const ns = NamespaceStore.ns; + if (!ns) { + return; + } + + this.setState({ + samplingState: 'counting', + samplingProgress: -1, + samplingTimeMS: 0, + schema: null + }); + + const options = { + maxTimeMS: this.state.maxTimeMS, + query: query, + size: DEFAULT_NUM_DOCUMENTS, + fields: null + }; + + const samplingStart = new Date(); + this.samplingTimer = setInterval(() => { + this.setState({ + samplingTimeMS: new Date() - samplingStart + }); + }, 1000); + + this.samplingStream = app.dataService.sample(ns, options); + const schema = new Schema(); + this.analyzingStream = schema.stream(true); + + const onError = () => { + this.setState({ + samplingState: 'error' + }); + this.stopSampling(); + }; + + const onSuccess = (_schema) => { + this.setState({ + samplingState: 'complete', + samplingTimeMS: new Date() - samplingStart, + samplingProgress: 100, + schema: _schema + }); + this.stopSampling(); + }; + + app.dataService.count(ns, query, {maxTimeMS: this.state.maxTimeMS}, (err, count) => { + if (err) { + return onError(err); + } + + this.setState({ + samplingState: 'sampling', + samplingProgress: 0, + samplingTimeMS: new Date() - samplingStart + }); + const numSamples = Math.min(count, DEFAULT_NUM_DOCUMENTS); + let sampleCount = 0; + + this.samplingStream + .on('error', (sampleErr) => { + return onError(sampleErr); + }) + .pipe(this.analyzingStream) + .once('progress', () => { + this.setState({ + samplingState: 'analyzing', + samplingTimeMS: new Date() - samplingStart + }); + }) + .on('progress', () => { + sampleCount ++; + const newProgress = Math.ceil(sampleCount / numSamples * 100); + if (newProgress > this.state.samplingProgress) { + this.setState({ + samplingProgress: Math.ceil(sampleCount / numSamples * 100), + samplingTimeMS: new Date() - samplingStart + }); + } + }) + .on('error', (analysisErr) => { + onError(analysisErr); + }) + .on('end', () => { + debug('on end', schema); + if (sampleCount > 0) { + onSuccess(schema.serialize()); + } + }); + }); + }, + + storeDidUpdate(prevState) { + debug('schema store changed from %j to %j', prevState, this.state); + } + +}); + +module.exports = SchemaStore; diff --git a/src/internal-packages/schema/package.json b/src/internal-packages/schema/package.json new file mode 100644 index 00000000000..b0e2e4cfb50 --- /dev/null +++ b/src/internal-packages/schema/package.json @@ -0,0 +1,9 @@ +{ + "name": "schema", + "productName": "Compass Schema", + "description": "Schema for Compass as an internal package.", + "version": "0.0.1", + "authors": "MongoDB Inc.", + "private": true, + "main": "./index.js" +} diff --git a/src/internal-packages/schema/util b/src/internal-packages/schema/util new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/internal-packages/status/index.js b/src/internal-packages/status/index.js new file mode 100644 index 00000000000..9cf994093b7 --- /dev/null +++ b/src/internal-packages/status/index.js @@ -0,0 +1,21 @@ +'use strict'; + +const app = require('ampersand-app'); +const Status = require('./lib/components/status'); + +/** + * Activate all the components in the CRUD package. + */ +function activate() { + app.componentRegistry.register(Status, { role: 'App:Status' }); +} + +/** + * Deactivate all the components in the CRUD package. + */ +function deactivate() { + app.componentRegistry.deregister(Status); +} + +module.exports.activate = activate; +module.exports.deactivate = deactivate; diff --git a/src/internal-packages/status/lib/actions/index.js b/src/internal-packages/status/lib/actions/index.js new file mode 100644 index 00000000000..cd64c2828f8 --- /dev/null +++ b/src/internal-packages/status/lib/actions/index.js @@ -0,0 +1,112 @@ +const Reflux = require('reflux'); + +const StatusActions = Reflux.createActions([ + /** + * shows the progress bar. + */ + 'showProgressBar', + /** + * shows an indeterminate progress bar at 100 percent. + */ + 'showIndeterminateProgressBar', + /** + * hides the progress bar. + */ + 'hideProgressBar', + /** + * sets the value of the progres bar. + * + * @param {Number} value the value, must be between 0 and 100. + */ + 'setProgressValue', + /** + * increases the value of the progres bar. + * + * @param {Number} value increase by value. + */ + 'incProgressValue', + /** + * enable trickle: progress bar randomly advances a few percentage points + * every second to indicate progress. + */ + 'enableProgressTrickle', + /** + * disable trickle. + */ + 'disableProgressTrickle', + /** + * sets a message that is shown on the screen above the loading animation. + * + * @param {String} message the message to show + */ + 'setMessage', + /** + * clears and removes the message. + */ + 'clearMessage', + /** + * shows loading animation in the center of the screen. + */ + 'showAnimation', + /** + * hides loading animation. + */ + 'hideAnimation', + /** + * shows a static gray sidebar in the background. This is useful when + * no other content is on the screen yet (e.g. when connecting to a mongod) + * so that the message/loading animation look centered. + */ + 'showStaticSidebar', + /** + * hide static gray sidebar. + */ + 'hideStaticSidebar', + /** + * set a custom subview that is shown below the loading animation. For example, + * the schema view sets a subview to indicate longer than usual parsing. + * + * @param {View} subview the subview to render. + */ + 'setSubview', + /** + * clears the custom subview. + */ + 'clearSubview', + /** + * when enabled, overlays the screen with a transparent div, so that no other + * interaction can take place. + */ + 'enableModal', + /** + * disables the modal transparent div. + */ + 'disableModal', + /** + * custom configuration to set all the options above in a single call. + * + * @param {Object} options options to configure, see below: + * + * @param {Boolean} options.visible show/hide entire status view + * @param {Boolean} options.progressbar show/hide progress bar + * @param {Number} options.width progress bar width 0-100 + * @param {Boolean} options.modal activate/deactivate modal + * @param {Boolean} options.animation show/hide animation + * @param {String} options.message message to show, '' disables message + * @param {View} options.subview subview to show, or `null` + * @param {Boolean} options.sidebar show/hide static sidebar + */ + 'configure', + /** + * hide all status components (progress bar, message, animation, sidebar). + * Use when loading was interrupted. + */ + 'hide', + /** + * like `hide()` but animates the progress bar to 100% before hiding, so that + * the user gets feedback of success. Use when loading is complete. + */ + 'done' +]); + +module.exports = StatusActions; diff --git a/src/internal-packages/status/lib/components/status.jsx b/src/internal-packages/status/lib/components/status.jsx new file mode 100644 index 00000000000..1d5655ef869 --- /dev/null +++ b/src/internal-packages/status/lib/components/status.jsx @@ -0,0 +1,82 @@ +const React = require('react'); +const StatusStore = require('../stores/status-store'); +const StateMixin = require('reflux-state-mixin'); + +const debug = require('debug')('mongodb-compass:status'); + +const STATUS_ID = 'statusbar'; + +/** + * Component for the entire document list. + */ +const Status = React.createClass({ + + mixins: [ StateMixin.connect(StatusStore) ], + + /** + * Render the status elements: progress bar, message container, sidebar, + * animation container, subview container, ... + * + * @returns {React.Component} The status view. + */ + render() { + // derive styles from status state + const visible = this.state.visible ? '' : 'hidden'; + const progressBarWidth = this.state.progress; + const progressBarHeight = 4; + const outerBarStyle = { + display: this.state.progressbar ? 'block' : 'none', + height: progressBarHeight + }; + const innerBarStyle = { + width: `${progressBarWidth}%` + }; + const messageStyle = { + visibility: this.state.message !== '' ? 'visible' : 'hidden' + }; + const animationStyle = { + visibility: this.state.animation ? 'visible' : 'hidden' + }; + const sidebarStyle = { + display: this.state.sidebar ? 'block' : 'none' + }; + + // create subview component if state.subview is set + let statusSubview = null; + if (this.state.subview) { + const SubView = this.state.subview; + statusSubview = ; + } + + return ( +
    +
    +
    +
    +
    +
    +
    +
      +
    • +

      + {this.state.message} +

      +
      +
      +
      +
      +
      +
      +
      +
      + {statusSubview} +
      +
    • +
    +
    + ); + } +}); + + +module.exports = Status; diff --git a/src/internal-packages/status/lib/stores/status-store.jsx b/src/internal-packages/status/lib/stores/status-store.jsx new file mode 100644 index 00000000000..a13e9bf1fd0 --- /dev/null +++ b/src/internal-packages/status/lib/stores/status-store.jsx @@ -0,0 +1,203 @@ +const Reflux = require('reflux'); +const StatusActions = require('../actions'); +const StateMixin = require('reflux-state-mixin'); +const _ = require('lodash'); + +const debug = require('debug')('mongodb-compass:stores:status'); + +/** + * Status store. The store object consists of the following options: + * + * @param {Boolean} visible show/hide entire status view + * @param {Boolean} progressbar show/hide progress bar + * @param {Number} progress progress bar width in percent 0-100 + * @param {Boolean} modal activate/deactivate modal + * @param {Boolean} animation show/hide animation + * @param {String} message message to show, '' disables message + * @param {View} subview subview to show, or `null` + * @param {Boolean} sidebar show/hide static sidebar + */ + +const StatusStore = Reflux.createStore({ + // adds a state to the store, similar to React.Component's state + mixins: [StateMixin.store], + listenables: StatusActions, + + init() { + this._trickleTimer = null; + }, + + /** + * Initialize the status store. + * + * @return {Object} initial store state. + */ + getInitialState() { + return { + visible: false, + progressbar: false, + progress: 0, + modal: false, + animation: false, + message: '', + subview: null, + sidebar: false, + trickle: false + }; + }, + + showProgressBar() { + this.setState({ + visible: true, + progressbar: true + }); + }, + + showIndeterminateProgressBar() { + this.setState({ + visible: true, + progressbar: true, + progress: 100, + trickle: false + }); + }, + + hideProgressBar() { + this.setState({ + progressbar: false + }); + }, + + configure(options) { + // `trickle` is the only option with a "side-effect", all other + // state variables are handled by the status component. + if (options.trickle) { + this.enableProgressTrickle(); + } else { + this.disableProgressTrickle(); + } + this.setState(options); + }, + + setProgressValue(value) { + this.setState({ + visible: true, + progress: value + }); + }, + + incProgressValue(value) { + this.setState({ + visible: true, + progress: this.state.value + value + }); + }, + + enableProgressTrickle() { + this._trickleTimer = setInterval(() => { + const newValue = Math.min(98, this.state.progress + 1); + this.setState.call(this, { + progress: newValue + }); + }, 600); + this.setState({ + trickle: true + }); + }, + + disableProgressTrickle() { + if (this._trickleTimer !== null) { + clearInterval(this._trickleTimer); + this._trickleTimer = null; + } + this.setState({ + trickle: false + }); + }, + + setMessage(msg) { + this.setState({ + visible: true, + message: msg + }); + }, + + clearMessage() { + this.setState({ + message: '' + }); + }, + + showAnimation() { + this.setState({ + visible: true, + animation: true + }); + }, + + hideAnimation() { + this.setState({ + animation: false + }); + }, + + showStaticSidebar() { + this.setState({ + visible: true, + sidebar: true + }); + }, + + hideStaticSidebar() { + this.setState({ + sidebar: false + }); + }, + + setSubview(view) { + this.setState({ + subview: view + }); + }, + + onClearSubview() { + this.setState({ + subview: null + }); + }, + + enableModal() { + this.setState({ + modal: true + }); + }, + + disableModal() { + this.setState({ + modal: false + }); + }, + + hide() { + this.setState(this.getInitialState()); + }, + + done() { + this.disableProgressTrickle(); + this.setState({ + progress: 100, + animation: false, + message: '', + subview: null + }); + _.delay(() => { + this.hide(); + }, 700); + }, + + storeDidUpdate(prevState) { + debug('status store changed from %j to %j', prevState, this.state); + } +}); + +module.exports = StatusStore; diff --git a/src/internal-packages/status/package.json b/src/internal-packages/status/package.json new file mode 100644 index 00000000000..a37eaee7329 --- /dev/null +++ b/src/internal-packages/status/package.json @@ -0,0 +1,9 @@ +{ + "name": "status", + "productName": "Compass Status View", + "description": "progress bar, loading animations, status messages, etc.", + "version": "0.0.1", + "authors": "MongoDB Inc.", + "private": true, + "main": "./index.js" +} diff --git a/src/app/statusbar/index.less b/src/internal-packages/status/styles/index.less similarity index 100% rename from src/app/statusbar/index.less rename to src/internal-packages/status/styles/index.less From 8a98f795af3860004a268ba58fdc11c49db416de Mon Sep 17 00:00:00 2001 From: Thomas Rueckstiess Date: Fri, 29 Jul 2016 17:37:46 +0200 Subject: [PATCH 02/25] rebased and using new hadron-app-registry --- src/app/home/collection.js | 2 +- src/app/index.js | 2 +- src/app/schema/index.js | 2 +- src/internal-packages/query/index.js | 4 ++-- src/internal-packages/schema/index.js | 4 ++-- src/internal-packages/status/index.js | 4 ++-- 6 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/app/home/collection.js b/src/app/home/collection.js index ffdac97230e..f842094569a 100644 --- a/src/app/home/collection.js +++ b/src/app/home/collection.js @@ -142,7 +142,7 @@ var MongoDBCollectionView = View.extend({ render: function() { this.renderWithTemplate(this); // render query bar here for now - var queryBarComponent = app.componentRegistry.findByRole('App:QueryBar')[0]; + var queryBarComponent = app.appRegistry.getComponent('App:QueryBar'); ReactDOM.render(React.createElement(queryBarComponent), this.queryByHook('refine-bar-subview')); }, onTabClicked: function(e) { diff --git a/src/app/index.js b/src/app/index.js index e2c9c793d77..384bed4a0eb 100644 --- a/src/app/index.js +++ b/src/app/index.js @@ -326,7 +326,7 @@ var Application = View.extend({ // }); // this.statusbar.render(); - this.statusComponent = app.componentRegistry.findByRole('App:Status')[0]; + this.statusComponent = app.appRegistry.getComponent('App:Status'); ReactDOM.render(React.createElement(this.statusComponent), this.queryByHook('statusbar')); this.autoUpdate = new AutoUpdate({ diff --git a/src/app/schema/index.js b/src/app/schema/index.js index 3909965e70f..48440b41175 100644 --- a/src/app/schema/index.js +++ b/src/app/schema/index.js @@ -21,7 +21,7 @@ var SchemaView = View.extend({ } }, initialize: function() { - this.schemaView = app.componentRegistry.findByRole('Collection:Schema')[0]; + this.schemaView = app.appRegistry.getComponent('Collection:Schema'); }, render: function() { this.renderWithTemplate(); diff --git a/src/internal-packages/query/index.js b/src/internal-packages/query/index.js index ae3dcb33905..3a882a995e5 100644 --- a/src/internal-packages/query/index.js +++ b/src/internal-packages/query/index.js @@ -5,14 +5,14 @@ const QueryBar = require('./lib/component'); * Activate all the components in the Query Bar package. */ function activate() { - app.componentRegistry.register(QueryBar, { role: 'App:QueryBar' }); + app.appRegistry.registerComponent('App:QueryBar', QueryBar); } /** * Deactivate all the components in the Query Bar package. */ function deactivate() { - app.componentRegistry.deregister(QueryBar); + app.appRegistry.deregisterComponent('App:QueryBar'); } module.exports.activate = activate; diff --git a/src/internal-packages/schema/index.js b/src/internal-packages/schema/index.js index 336b0c12e83..876ce6d9586 100644 --- a/src/internal-packages/schema/index.js +++ b/src/internal-packages/schema/index.js @@ -7,14 +7,14 @@ const Schema = require('./lib/component/schema'); * Activate all the components in the CRUD package. */ function activate() { - app.componentRegistry.register(Schema, { role: 'Collection:Schema' }); + app.appRegistry.registerComponent('Collection:Schema', Schema); } /** * Deactivate all the components in the CRUD package. */ function deactivate() { - app.componentRegistry.deregister(Schema); + app.appRegistry.deregisterComponent('Collection:Schema'); } module.exports.activate = activate; diff --git a/src/internal-packages/status/index.js b/src/internal-packages/status/index.js index 9cf994093b7..b106a2b2e40 100644 --- a/src/internal-packages/status/index.js +++ b/src/internal-packages/status/index.js @@ -7,14 +7,14 @@ const Status = require('./lib/components/status'); * Activate all the components in the CRUD package. */ function activate() { - app.componentRegistry.register(Status, { role: 'App:Status' }); + app.appRegistry.registerComponent('App:Status', Status); } /** * Deactivate all the components in the CRUD package. */ function deactivate() { - app.componentRegistry.deregister(Status); + app.appRegistry.deregisterComponent('App:Status'); } module.exports.activate = activate; From 8c959dc8547209f20f2410ac9b7edbb0f17173af Mon Sep 17 00:00:00 2001 From: Thomas Rueckstiess Date: Fri, 29 Jul 2016 18:07:40 +0200 Subject: [PATCH 03/25] getting StatusAction from appRegistry --- src/app/connect/index.js | 10 ++++++---- src/app/index.js | 15 ++++++++------- src/help/index.js | 7 ++++--- src/internal-packages/schema/index.js | 10 ++++++++-- .../schema/lib/component/schema.jsx | 15 +++++++++------ .../component/status-subview/buttons-error.jsx | 14 +++++++++----- src/internal-packages/status/index.js | 11 ++++++++--- src/internal-packages/status/lib/actions/index.js | 4 ++-- .../status/lib/stores/status-store.jsx | 4 ++-- 9 files changed, 56 insertions(+), 34 deletions(-) diff --git a/src/app/connect/index.js b/src/app/connect/index.js index bb25328ac58..c4c06fb5d18 100644 --- a/src/app/connect/index.js +++ b/src/app/connect/index.js @@ -3,7 +3,6 @@ var ConnectFormView = require('./connect-form-view'); var Connection = require('../models/connection'); var ConnectionCollection = require('../models/connection-collection'); var MongoDBConnection = require('mongodb-connection-model'); -var StatusActions = require('../../internal-packages/status/lib/actions'); var SidebarWrapperView = require('./sidebar'); var View = require('ampersand-view'); @@ -38,6 +37,9 @@ var sslMethods = require('./ssl'); */ var sshTunnelMethods = require('./ssh-tunnel'); + +var StatusAction = app.appRegistry.getAction('StatusAction'); + var ConnectView = View.extend({ template: indexTemplate, screenName: 'Connect', @@ -428,7 +430,7 @@ var ConnectView = View.extend({ this.dispatch('error received'); return; } - StatusActions.showIndeterminateProgressBar(); + StatusAction.showIndeterminateProgressBar(); var onSave = function() { this.connections.add(this.connection, { merge: true }); @@ -438,7 +440,7 @@ var ConnectView = View.extend({ connection.test(function(err) { if (!err) { - StatusActions.hide(); + StatusAction.hide(); // now save connection this.connection = connection; this.connection.save({ last_used: new Date() }, { success: onSave.bind(this) }); @@ -459,7 +461,7 @@ var ConnectView = View.extend({ */ useConnection: function(connection) { connection = connection || this.connection; - StatusActions.hide(); + StatusAction.hide(); metrics.track('Connection', 'used', { authentication: connection.authentication, ssl: connection.ssl, diff --git a/src/app/index.js b/src/app/index.js index 384bed4a0eb..44e2e7e9bc9 100644 --- a/src/app/index.js +++ b/src/app/index.js @@ -55,8 +55,6 @@ var migrateApp = require('./migrations'); var metricsSetup = require('./metrics'); var metrics = require('mongodb-js-metrics')(); -var StatusActions = require('../internal-packages/status/lib/actions'); - var React = require('react'); var ReactDOM = require('react-dom'); var AutoUpdate = require('../auto-update'); @@ -258,7 +256,8 @@ var Application = View.extend({ pushState: false, root: '/' }); - StatusActions.hide(); + var StatusAction = app.appRegistry.getAction('StatusAction'); + StatusAction.hide(); }, onFatalError: function(id, err) { debug('clearing client stall timeout...'); @@ -266,7 +265,8 @@ var Application = View.extend({ console.error('Fatal Error!: ', id, err); metrics.error(err); - StatusActions.setMessage(err); + var StatusAction = app.appRegistry.getAction('StatusAction'); + StatusAction.setMessage(err); }, // ms we'll wait for a `mongodb-scope-client` instance // to become readable before giving up and showing @@ -383,8 +383,9 @@ app.extend({ state.startRouter(); return; } - - StatusActions.configure({ + var StatusAction = app.appRegistry.getAction('StatusAction'); + StatusAction.configure({ + visible: true, message: 'Retrieving connection details...', progressbar: true, progress: 100, @@ -401,7 +402,7 @@ app.extend({ state.onFatalError('fetch connection', err); return; } - StatusActions.setMessage('Connecting to MongoDB...'); + StatusAction.setMessage('Connecting to MongoDB...'); var DataService = require('mongodb-data-service'); app.dataService = new DataService(state.connection) diff --git a/src/help/index.js b/src/help/index.js index a30c7167b8d..c1f76b07635 100644 --- a/src/help/index.js +++ b/src/help/index.js @@ -6,7 +6,6 @@ var HelpEntryCollection = require('./help-entry-collection'); var HelpEntry = require('./help-entry'); var SidebarView = require('../app/sidebar'); var ViewSwitcher = require('ampersand-view-switcher'); -var StatusActions = require('../../internal-packages/status/lib/actions'); var app = require('ampersand-app'); var metrics = require('mongodb-js-metrics')(); var _ = require('lodash'); @@ -17,6 +16,8 @@ var tagsTemplate = require('./tags.jade'); var entries = new HelpEntryCollection(); +var StatusAction = app.appRegistry.getAction('StatusAction'); + var HelpPage = View.extend({ template: indexTemplate, screenName: 'Help', @@ -129,11 +130,11 @@ var HelpPage = View.extend({ if (!entry) { debug('Unknown help entry', entryId); this.viewSwitcher.clear(); - StatusActions.setMessage('Help entry not found.'); + StatusAction.setMessage('Help entry not found.'); return; } - StatusActions.hide(); + StatusAction.hide(); if (!entries.select(entry)) { debug('already selected'); diff --git a/src/internal-packages/schema/index.js b/src/internal-packages/schema/index.js index 876ce6d9586..956e9c70642 100644 --- a/src/internal-packages/schema/index.js +++ b/src/internal-packages/schema/index.js @@ -1,13 +1,17 @@ 'use strict'; const app = require('ampersand-app'); -const Schema = require('./lib/component/schema'); +const SchemaComponent = require('./lib/component/schema'); +const SchemaAction = require('./lib/action'); +const SchemaStore = require('./lib/store/schema-store'); /** * Activate all the components in the CRUD package. */ function activate() { - app.appRegistry.registerComponent('Collection:Schema', Schema); + app.appRegistry.registerComponent('Collection:Schema', SchemaComponent); + app.appRegistry.registerAction('SchemaAction', SchemaAction); + app.appRegistry.registerStore('SchemaStore', SchemaStore); } /** @@ -15,6 +19,8 @@ function activate() { */ function deactivate() { app.appRegistry.deregisterComponent('Collection:Schema'); + app.appRegistry.deregisterAction('SchemaAction'); + app.appRegistry.deregisterStore('SchemaStore'); } module.exports.activate = activate; diff --git a/src/internal-packages/schema/lib/component/schema.jsx b/src/internal-packages/schema/lib/component/schema.jsx index 6a87ffa9aa7..12f380dca25 100644 --- a/src/internal-packages/schema/lib/component/schema.jsx +++ b/src/internal-packages/schema/lib/component/schema.jsx @@ -1,11 +1,10 @@ +const app = require('ampersand-app'); const React = require('react'); const SchemaStore = require('../store/schema-store'); const StateMixin = require('reflux-state-mixin'); const Field = require('./field'); const StatusSubview = require('../component/status-subview'); const StatusStore = require('../../../status/lib/stores/status-store'); -const StatusActions = require('../../../status/lib/actions'); - const _ = require('lodash'); // const debug = require('debug')('mongodb-compass:schema'); @@ -19,6 +18,10 @@ const Schema = React.createClass({ StateMixin.connect(SchemaStore) ], + componentWillMount() { + this.StatusAction = app.appRegistry.getAction('StatusAction'); + }, + shouldComponentUpdate() { // @todo optimize this return true; @@ -31,7 +34,7 @@ const Schema = React.createClass({ */ _updateProgressBar() { if (this.state.samplingState === 'error') { - StatusActions.configure({ + this.StatusAction.configure({ progressbar: false, animation: false }); @@ -40,7 +43,7 @@ const Schema = React.createClass({ // initial schema phase, cannot measure progress, enable trickling if (this.state.samplingProgress === -1) { this.trickleStop = null; - StatusActions.configure({ + this.StatusAction.configure({ visible: true, progressbar: true, progress: 0, @@ -54,7 +57,7 @@ const Schema = React.createClass({ this.trickleStop = StatusStore.state.progress; } const newProgress = Math.ceil(this.trickleStop + (100 - this.trickleStop) / 100 * progress); - StatusActions.configure({ + this.StatusAction.configure({ visible: true, trickle: false, animation: true, @@ -63,7 +66,7 @@ const Schema = React.createClass({ progress: newProgress }); } else if (progress === 100) { - StatusActions.done(); + this.StatusAction.done(); } }, diff --git a/src/internal-packages/schema/lib/component/status-subview/buttons-error.jsx b/src/internal-packages/schema/lib/component/status-subview/buttons-error.jsx index 20eef389ea7..6bacfceafcf 100644 --- a/src/internal-packages/schema/lib/component/status-subview/buttons-error.jsx +++ b/src/internal-packages/schema/lib/component/status-subview/buttons-error.jsx @@ -1,7 +1,6 @@ +const app = require('ampersand-app'); const React = require('react'); const ms = require('ms'); -const StatusActions = require('../../../../status/lib/actions'); -const SchemaActions = require('../../action'); // const debug = require('debug')('mongodb-compass:schema:status-subview:buttons-error'); @@ -15,15 +14,20 @@ const ButtonsError = React.createClass({ samplingState: React.PropTypes.string.isRequired }, + componentWillMount() { + this.StatusAction = app.appRegistry.getAction('StatusAction'); + this.SchemaAction = app.appRegistry.getAction('SchemaAction'); + }, + onTryAgainButtonClick() { // increase maxTimeMS and sample again - SchemaActions.setMaxTimeMS(RETRY_INC_MAXTIMEMS_VALUE); - SchemaActions.startSampling(); + this.SchemaAction.setMaxTimeMS(RETRY_INC_MAXTIMEMS_VALUE); + this.SchemaAction.startSampling(); }, onNewQueryButtonClick() { // dismiss status view - StatusActions.hide(); + this.StatusAction.hide(); }, /** diff --git a/src/internal-packages/status/index.js b/src/internal-packages/status/index.js index b106a2b2e40..247661117bf 100644 --- a/src/internal-packages/status/index.js +++ b/src/internal-packages/status/index.js @@ -1,13 +1,16 @@ 'use strict'; const app = require('ampersand-app'); -const Status = require('./lib/components/status'); - +const StatusComponent = require('./lib/components/status'); +const StatusAction = require('./lib/actions'); +const StatusStore = require('./lib/stores/status-store'); /** * Activate all the components in the CRUD package. */ function activate() { - app.appRegistry.registerComponent('App:Status', Status); + app.appRegistry.registerComponent('App:Status', StatusComponent); + app.appRegistry.registerAction('StatusAction', StatusAction); + app.appRegistry.registerStore('StatusStore', StatusStore); } /** @@ -15,6 +18,8 @@ function activate() { */ function deactivate() { app.appRegistry.deregisterComponent('App:Status'); + app.appRegistry.deregisterAction('StatusAction'); + app.appRegistry.deregisterStore('StatusStore'); } module.exports.activate = activate; diff --git a/src/internal-packages/status/lib/actions/index.js b/src/internal-packages/status/lib/actions/index.js index cd64c2828f8..a5f332a0c33 100644 --- a/src/internal-packages/status/lib/actions/index.js +++ b/src/internal-packages/status/lib/actions/index.js @@ -1,6 +1,6 @@ const Reflux = require('reflux'); -const StatusActions = Reflux.createActions([ +const StatusAction = Reflux.createActions([ /** * shows the progress bar. */ @@ -109,4 +109,4 @@ const StatusActions = Reflux.createActions([ 'done' ]); -module.exports = StatusActions; +module.exports = StatusAction; diff --git a/src/internal-packages/status/lib/stores/status-store.jsx b/src/internal-packages/status/lib/stores/status-store.jsx index a13e9bf1fd0..37c27d6486e 100644 --- a/src/internal-packages/status/lib/stores/status-store.jsx +++ b/src/internal-packages/status/lib/stores/status-store.jsx @@ -1,5 +1,5 @@ const Reflux = require('reflux'); -const StatusActions = require('../actions'); +const StatusAction = require('../actions'); const StateMixin = require('reflux-state-mixin'); const _ = require('lodash'); @@ -21,7 +21,7 @@ const debug = require('debug')('mongodb-compass:stores:status'); const StatusStore = Reflux.createStore({ // adds a state to the store, similar to React.Component's state mixins: [StateMixin.store], - listenables: StatusActions, + listenables: StatusAction, init() { this._trickleTimer = null; From 7031402dffd0460598a62803a7f7b00b6292ec7f Mon Sep 17 00:00:00 2001 From: Thomas Rueckstiess Date: Fri, 29 Jul 2016 18:21:48 +0200 Subject: [PATCH 04/25] register QueryAction --- src/internal-packages/query/index.js | 11 ++++++++--- .../query/lib/component/input-form.jsx | 8 ++++---- src/internal-packages/query/lib/store/index.jsx | 11 ++++++----- src/internal-packages/schema/lib/action/index.jsx | 4 ++-- .../lib/component/status-subview/buttons-waiting.jsx | 4 ++-- .../schema/lib/store/schema-store.jsx | 11 ++++------- 6 files changed, 26 insertions(+), 23 deletions(-) diff --git a/src/internal-packages/query/index.js b/src/internal-packages/query/index.js index 3a882a995e5..dc513af1d72 100644 --- a/src/internal-packages/query/index.js +++ b/src/internal-packages/query/index.js @@ -1,11 +1,14 @@ const app = require('ampersand-app'); -const QueryBar = require('./lib/component'); - +const QueryBarComponent = require('./lib/component'); +const QueryAction = require('./lib/action'); +const QueryStore = require('./lib/store'); /** * Activate all the components in the Query Bar package. */ function activate() { - app.appRegistry.registerComponent('App:QueryBar', QueryBar); + app.appRegistry.registerComponent('App:QueryBar', QueryBarComponent); + app.appRegistry.registerAction('QueryAction', QueryAction); + app.appRegistry.registerStore('QueryStore', QueryStore); } /** @@ -13,6 +16,8 @@ function activate() { */ function deactivate() { app.appRegistry.deregisterComponent('App:QueryBar'); + app.appRegistry.deregisterAction('QueryAction'); + app.appRegistry.deregisterStore('QueryStore'); } module.exports.activate = activate; diff --git a/src/internal-packages/query/lib/component/input-form.jsx b/src/internal-packages/query/lib/component/input-form.jsx index 082cc7fd0f1..7db663abdb4 100644 --- a/src/internal-packages/query/lib/component/input-form.jsx +++ b/src/internal-packages/query/lib/component/input-form.jsx @@ -1,5 +1,5 @@ const React = require('react'); -const QueryActions = require('../action'); +const QueryAction = require('../action'); const EJSON = require('mongodb-extended-json'); // const debug = require('debug')('mongodb-compass:query-bar'); @@ -16,7 +16,7 @@ const QueryInputGroup = React.createClass({ }, onChange(evt) { - QueryActions.setQueryString(evt.target.value); + QueryAction.setQueryString(evt.target.value); }, onApplyButtonClicked(evt) { @@ -24,12 +24,12 @@ const QueryInputGroup = React.createClass({ evt.stopPropagation(); if (this.props.valid) { - QueryActions.apply(); + QueryAction.apply(); } }, onResetButtonClicked() { - QueryActions.reset(); + QueryAction.reset(); }, /** diff --git a/src/internal-packages/query/lib/store/index.jsx b/src/internal-packages/query/lib/store/index.jsx index 3724bc28488..729ead79a90 100644 --- a/src/internal-packages/query/lib/store/index.jsx +++ b/src/internal-packages/query/lib/store/index.jsx @@ -1,8 +1,8 @@ +const app = require('ampersand-app'); const Reflux = require('reflux'); const NamespaceStore = require('hadron-reflux-store').NamespaceStore; const StateMixin = require('reflux-state-mixin'); -const QueryActions = require('../action'); -const SchemaActions = require('../../../schema/lib/action'); +const QueryAction = require('../action'); const EJSON = require('mongodb-extended-json'); const Query = require('mongodb-language-model').Query; const _ = require('lodash'); @@ -17,7 +17,7 @@ const debug = require('debug')('mongodb-compass:stores:query'); */ const QueryStore = Reflux.createStore({ mixins: [StateMixin.store], - listenables: QueryActions, + listenables: QueryAction, /** * listen to Namespace store and reset if ns changes. @@ -339,7 +339,8 @@ const QueryStore = Reflux.createStore({ lastExecutedQuery: _.clone(this.state.query) }); // start queries for all tabs: schema, documents, explain, indexes - SchemaActions.startSampling(); + const SchemaAction = app.appRegistry.getAction('SchemaAction'); + SchemaAction.startSampling(); } }, @@ -350,7 +351,7 @@ const QueryStore = Reflux.createStore({ if (!_.isEqual(this.state.query, {})) { this.setQuery({}); if (!_.isEqual(this.state.lastExecutedQuery, {})) { - QueryActions.apply(); + QueryAction.apply(); } } }, diff --git a/src/internal-packages/schema/lib/action/index.jsx b/src/internal-packages/schema/lib/action/index.jsx index 0f664422929..369fd11a9f4 100644 --- a/src/internal-packages/schema/lib/action/index.jsx +++ b/src/internal-packages/schema/lib/action/index.jsx @@ -1,6 +1,6 @@ const Reflux = require('reflux'); -const SchemaActions = Reflux.createActions({ +const SchemaAction = Reflux.createActions({ /** * starts schema sampling with the current query */ @@ -19,4 +19,4 @@ const SchemaActions = Reflux.createActions({ resetMaxTimeMS: {sync: true} }); -module.exports = SchemaActions; +module.exports = SchemaAction; diff --git a/src/internal-packages/schema/lib/component/status-subview/buttons-waiting.jsx b/src/internal-packages/schema/lib/component/status-subview/buttons-waiting.jsx index 0b6b9b29fab..79c9fe205b0 100644 --- a/src/internal-packages/schema/lib/component/status-subview/buttons-waiting.jsx +++ b/src/internal-packages/schema/lib/component/status-subview/buttons-waiting.jsx @@ -1,5 +1,5 @@ const React = require('react'); -const SchemaActions = require('../../action'); +const SchemaAction = require('../../action'); // const debug = require('debug')('mongodb-compass:schema:status-subview:buttons-waiting'); @@ -15,7 +15,7 @@ const ButtonsWaiting = React.createClass({ }, onStopPartialButton() { - SchemaActions.stopSampling(); + SchemaAction.stopSampling(); }, render() { diff --git a/src/internal-packages/schema/lib/store/schema-store.jsx b/src/internal-packages/schema/lib/store/schema-store.jsx index 674ca7b78af..8d53c637591 100644 --- a/src/internal-packages/schema/lib/store/schema-store.jsx +++ b/src/internal-packages/schema/lib/store/schema-store.jsx @@ -9,7 +9,7 @@ const NamespaceStore = require('hadron-reflux-store').NamespaceStore; const QueryStore = require('../../../query/lib/store'); // actions -const SchemaActions = require('../action'); +const SchemaAction = require('../action'); const debug = require('debug')('mongodb-compass:stores:schema'); // const metrics = require('mongodb-js-metrics')(); @@ -23,17 +23,15 @@ const DEFAULT_NUM_DOCUMENTS = 1000; const SchemaStore = Reflux.createStore({ mixins: [StateMixin.store], - listenables: SchemaActions, + listenables: SchemaAction, /** * Initialize the document list store. */ init: function() { - // this.listenTo(QueryActions.apply, this._sampleSchema); - NamespaceStore.listen(() => { this._reset(); - SchemaActions.startSampling(); + SchemaAction.startSampling(); }); this.samplingStream = null; @@ -184,8 +182,7 @@ const SchemaStore = Reflux.createStore({ onError(analysisErr); }) .on('end', () => { - debug('on end', schema); - if (sampleCount > 0) { + if (sampleCount > 0 && this.state.samplingState !== 'error') { onSuccess(schema.serialize()); } }); From 34862ada572cd34925f4c1bb814e4de36c1c67df Mon Sep 17 00:00:00 2001 From: Thomas Rueckstiess Date: Fri, 29 Jul 2016 18:37:06 +0200 Subject: [PATCH 05/25] rename schema-store.jsx to index.jsx --- src/internal-packages/schema/index.js | 2 +- src/internal-packages/schema/lib/component/schema.jsx | 4 ++-- .../schema/lib/component/status-subview/index.jsx | 2 +- .../schema/lib/store/{schema-store.jsx => index.jsx} | 0 4 files changed, 4 insertions(+), 4 deletions(-) rename src/internal-packages/schema/lib/store/{schema-store.jsx => index.jsx} (100%) diff --git a/src/internal-packages/schema/index.js b/src/internal-packages/schema/index.js index 956e9c70642..6beb1c6faf8 100644 --- a/src/internal-packages/schema/index.js +++ b/src/internal-packages/schema/index.js @@ -3,7 +3,7 @@ const app = require('ampersand-app'); const SchemaComponent = require('./lib/component/schema'); const SchemaAction = require('./lib/action'); -const SchemaStore = require('./lib/store/schema-store'); +const SchemaStore = require('./lib/store'); /** * Activate all the components in the CRUD package. diff --git a/src/internal-packages/schema/lib/component/schema.jsx b/src/internal-packages/schema/lib/component/schema.jsx index 12f380dca25..72047936381 100644 --- a/src/internal-packages/schema/lib/component/schema.jsx +++ b/src/internal-packages/schema/lib/component/schema.jsx @@ -1,10 +1,9 @@ const app = require('ampersand-app'); const React = require('react'); -const SchemaStore = require('../store/schema-store'); +const SchemaStore = require('../store'); const StateMixin = require('reflux-state-mixin'); const Field = require('./field'); const StatusSubview = require('../component/status-subview'); -const StatusStore = require('../../../status/lib/stores/status-store'); const _ = require('lodash'); // const debug = require('debug')('mongodb-compass:schema'); @@ -54,6 +53,7 @@ const Schema = React.createClass({ } else if (progress >= 0 && progress < 100 && progress % 5 === 1) { if (this.trickleStop === null) { // remember where trickling stopped to calculate remaining progress + const StatusStore = app.appRegistry.getStore('StatusStore'); this.trickleStop = StatusStore.state.progress; } const newProgress = Math.ceil(this.trickleStop + (100 - this.trickleStop) / 100 * progress); diff --git a/src/internal-packages/schema/lib/component/status-subview/index.jsx b/src/internal-packages/schema/lib/component/status-subview/index.jsx index 439f9b7d528..b29831939d4 100644 --- a/src/internal-packages/schema/lib/component/status-subview/index.jsx +++ b/src/internal-packages/schema/lib/component/status-subview/index.jsx @@ -4,7 +4,7 @@ const ButtonsWaiting = require('./buttons-waiting'); const StateMixin = require('reflux-state-mixin'); const ButtonsError = require('./buttons-error'); -const SchemaStore = require('../../store/schema-store'); +const SchemaStore = require('../../store'); const SchemaSteps = require('./steps'); // const debug = require('debug')('mongodb-compass:schema:status-subview'); diff --git a/src/internal-packages/schema/lib/store/schema-store.jsx b/src/internal-packages/schema/lib/store/index.jsx similarity index 100% rename from src/internal-packages/schema/lib/store/schema-store.jsx rename to src/internal-packages/schema/lib/store/index.jsx From 8e8326f619ec5d3a0088bd18ba87478d46753309 Mon Sep 17 00:00:00 2001 From: Thomas Rueckstiess Date: Fri, 29 Jul 2016 18:40:08 +0200 Subject: [PATCH 06/25] singular names for lib/* folders in status package --- src/internal-packages/status/index.js | 6 +++--- .../status/lib/{actions => action}/index.js | 0 .../lib/{components/status.jsx => component/index.jsx} | 4 ++-- .../status/lib/{stores/status-store.jsx => store/index.jsx} | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) rename src/internal-packages/status/lib/{actions => action}/index.js (100%) rename src/internal-packages/status/lib/{components/status.jsx => component/index.jsx} (95%) rename src/internal-packages/status/lib/{stores/status-store.jsx => store/index.jsx} (98%) diff --git a/src/internal-packages/status/index.js b/src/internal-packages/status/index.js index 247661117bf..5e2773a2db3 100644 --- a/src/internal-packages/status/index.js +++ b/src/internal-packages/status/index.js @@ -1,9 +1,9 @@ 'use strict'; const app = require('ampersand-app'); -const StatusComponent = require('./lib/components/status'); -const StatusAction = require('./lib/actions'); -const StatusStore = require('./lib/stores/status-store'); +const StatusComponent = require('./lib/component'); +const StatusAction = require('./lib/action'); +const StatusStore = require('./lib/store'); /** * Activate all the components in the CRUD package. */ diff --git a/src/internal-packages/status/lib/actions/index.js b/src/internal-packages/status/lib/action/index.js similarity index 100% rename from src/internal-packages/status/lib/actions/index.js rename to src/internal-packages/status/lib/action/index.js diff --git a/src/internal-packages/status/lib/components/status.jsx b/src/internal-packages/status/lib/component/index.jsx similarity index 95% rename from src/internal-packages/status/lib/components/status.jsx rename to src/internal-packages/status/lib/component/index.jsx index 1d5655ef869..5b2c76c82a0 100644 --- a/src/internal-packages/status/lib/components/status.jsx +++ b/src/internal-packages/status/lib/component/index.jsx @@ -1,8 +1,8 @@ const React = require('react'); -const StatusStore = require('../stores/status-store'); +const StatusStore = require('../store'); const StateMixin = require('reflux-state-mixin'); -const debug = require('debug')('mongodb-compass:status'); +// const debug = require('debug')('mongodb-compass:status'); const STATUS_ID = 'statusbar'; diff --git a/src/internal-packages/status/lib/stores/status-store.jsx b/src/internal-packages/status/lib/store/index.jsx similarity index 98% rename from src/internal-packages/status/lib/stores/status-store.jsx rename to src/internal-packages/status/lib/store/index.jsx index 37c27d6486e..2740cb2b0fa 100644 --- a/src/internal-packages/status/lib/stores/status-store.jsx +++ b/src/internal-packages/status/lib/store/index.jsx @@ -1,5 +1,5 @@ const Reflux = require('reflux'); -const StatusAction = require('../actions'); +const StatusAction = require('../action'); const StateMixin = require('reflux-state-mixin'); const _ = require('lodash'); From 30414876040ca9db1678617ce892f93556df39f6 Mon Sep 17 00:00:00 2001 From: Thomas Rueckstiess Date: Fri, 29 Jul 2016 19:02:50 +0200 Subject: [PATCH 07/25] QueryAction, file renames, cleanup --- src/app/index.less | 6 +- .../query/lib/action/index.jsx | 4 +- src/internal-packages/query/styles/index.less | 28 +- src/internal-packages/schema/index.js | 2 +- .../lib/component/{schema.jsx => index.jsx} | 0 .../schema/lib/component/minichart.jsx | 6 +- .../schema/lib/component/unique.jsx | 15 +- .../schema/lib/d3/coordinates.js | 7 +- src/internal-packages/schema/lib/d3/date.js | 14 +- src/internal-packages/schema/lib/d3/few.js | 8 +- src/internal-packages/schema/lib/d3/many.js | 20 +- .../schema/lib/store/index.jsx | 4 +- .../schema/styles/index.less | 336 ++++++++++++++++++ 13 files changed, 406 insertions(+), 44 deletions(-) rename src/internal-packages/schema/lib/component/{schema.jsx => index.jsx} (100%) create mode 100644 src/internal-packages/schema/styles/index.less diff --git a/src/app/index.less b/src/app/index.less index f0e3672c472..0c85e28a73a 100644 --- a/src/app/index.less +++ b/src/app/index.less @@ -9,8 +9,6 @@ @import "documents/index.less"; @import "schema/index.less"; @import "home/index.less"; -@import "minicharts/index.less"; -@import "refine-view/index.less"; @import "sampling-message/index.less"; @import "tour/index.less"; @import "sidebar/index.less"; @@ -24,5 +22,9 @@ @import "metrics/index.less"; @import "./styles/mapbox-gl.css"; +// Packages +// @todo don't hard-code these, style manager needs to handle package styles @import "../internal-packages/crud/styles/crud.less"; @import "../internal-packages/status/styles/index.less"; +@import "../internal-packages/query/styles/index.less"; +@import "../internal-packages/schema/styles/index.less"; diff --git a/src/internal-packages/query/lib/action/index.jsx b/src/internal-packages/query/lib/action/index.jsx index d4636eb59b2..b0c2df79a05 100644 --- a/src/internal-packages/query/lib/action/index.jsx +++ b/src/internal-packages/query/lib/action/index.jsx @@ -1,6 +1,6 @@ const Reflux = require('reflux'); -const QueryBuilderActions = Reflux.createActions({ +const QueryAction = Reflux.createActions({ /* Generic actions */ /** @@ -74,4 +74,4 @@ const QueryBuilderActions = Reflux.createActions({ 'reset': {sync: true} }); -module.exports = QueryBuilderActions; +module.exports = QueryAction; diff --git a/src/internal-packages/query/styles/index.less b/src/internal-packages/query/styles/index.less index 2a7280ef085..07a945b6375 100644 --- a/src/internal-packages/query/styles/index.less +++ b/src/internal-packages/query/styles/index.less @@ -1 +1,27 @@ -// styles for Query Bar package +.refine-view-container { + position: relative; + z-index: 1; + + .query-input-container { + padding: 12px 10px 12px; + background: @gray8; + border-bottom: 1px solid @gray7; + + input[type='text'] { + font-family: @font-family-monospace; + background: @pw; + height: 28px; + & + .input-group-btn { + padding-left: 10px; + + .btn { + border-radius: 3px; + } + + &:last-child > .btn { + margin-left: 2px; + } + } + } + } +} diff --git a/src/internal-packages/schema/index.js b/src/internal-packages/schema/index.js index 6beb1c6faf8..010935e4532 100644 --- a/src/internal-packages/schema/index.js +++ b/src/internal-packages/schema/index.js @@ -1,7 +1,7 @@ 'use strict'; const app = require('ampersand-app'); -const SchemaComponent = require('./lib/component/schema'); +const SchemaComponent = require('./lib/component'); const SchemaAction = require('./lib/action'); const SchemaStore = require('./lib/store'); diff --git a/src/internal-packages/schema/lib/component/schema.jsx b/src/internal-packages/schema/lib/component/index.jsx similarity index 100% rename from src/internal-packages/schema/lib/component/schema.jsx rename to src/internal-packages/schema/lib/component/index.jsx diff --git a/src/internal-packages/schema/lib/component/minichart.jsx b/src/internal-packages/schema/lib/component/minichart.jsx index 2000a8d5aa9..a40ec8cabc0 100644 --- a/src/internal-packages/schema/lib/component/minichart.jsx +++ b/src/internal-packages/schema/lib/component/minichart.jsx @@ -1,14 +1,13 @@ +const app = require('ampersand-app'); const React = require('react'); const UniqueMinichart = require('./unique'); const _ = require('lodash'); // const DocumentMinichart = require('./document'); // const ArrayMinichart = require('./array'); -const QueryStore = require('../../../query/lib/store'); - const D3Component = require('./d3component'); const vizFns = require('../d3'); -const debug = require('debug')('mongodb-compass:schema:minichart'); +// const debug = require('debug')('mongodb-compass:schema:minichart'); const Minichart = React.createClass({ @@ -37,6 +36,7 @@ const Minichart = React.createClass({ }); window.addEventListener('resize', this.handleResize); + const QueryStore = app.appRegistry.getStore('QueryStore'); this.unsubscribeQueryStore = QueryStore.listen((store) => { this.setState({ query: store.query diff --git a/src/internal-packages/schema/lib/component/unique.jsx b/src/internal-packages/schema/lib/component/unique.jsx index f68d17ede1b..af7be550a25 100644 --- a/src/internal-packages/schema/lib/component/unique.jsx +++ b/src/internal-packages/schema/lib/component/unique.jsx @@ -1,7 +1,7 @@ +const app = require('ampersand-app'); const React = require('react'); const _ = require('lodash'); const NativeListener = require('react-native-listener'); -const QueryBuilderAction = require('../../../query/lib/action'); const hasDistinctValue = require('../../../query/lib/util').hasDistinctValue; // const debug = require('debug')('mongodb-compass:minichart:unique'); @@ -13,19 +13,10 @@ const ValueBubble = React.createClass({ query: React.PropTypes.any }, - // getInitialState() { - // return { - // selected: false - // }; - // }, - onBubbleClicked(e) { - // const newState = !this.state.selected; - // this.setState({ - // selected: newState - // }); + const QueryAction = app.appRegistry.getAction('QueryAction'); const action = e.shiftKey ? - QueryBuilderAction.toggleDistinctValue : QueryBuilderAction.setValue; + QueryAction.toggleDistinctValue : QueryAction.setValue; action({ field: this.props.fieldName, value: this.props.value, diff --git a/src/internal-packages/schema/lib/d3/coordinates.js b/src/internal-packages/schema/lib/d3/coordinates.js index 8595d6780b2..9a1ec54d1e1 100644 --- a/src/internal-packages/schema/lib/d3/coordinates.js +++ b/src/internal-packages/schema/lib/d3/coordinates.js @@ -3,7 +3,6 @@ const d3 = require('d3'); const _ = require('lodash'); const shared = require('./shared'); const app = require('ampersand-app'); -const QueryBuilderAction = require('../../../query/lib/action'); const turfDistance = require('turf-distance'); const turfPoint = require('turf-point'); const turfDestination = require('turf-destination'); @@ -11,6 +10,8 @@ const turfDestination = require('turf-destination'); // const metrics = require('mongodb-js-metrics')(); // const debug = require('debug')('mongodb-compass:minicharts:geo'); +const QueryAction = app.appRegistry.getAction('QueryAction'); + const SELECTED_COLOR = '#F68A1E'; const UNSELECTED_COLOR = '#43B1E5'; const CONTROL_COLOR = '#ed271c'; @@ -60,13 +61,13 @@ const minicharts_d3fns_geo = function() { turfPoint([circleOuter.lng, circleOuter.lat]), 'miles' ); - QueryBuilderAction.setGeoWithinValue({ + QueryAction.setGeoWithinValue({ field: options.fieldName, center: [circleCenter.lng, circleCenter.lat], radius: mileDistance / 3963.2 }); } else { - QueryBuilderAction.clearValue({ + QueryAction.clearValue({ field: options.fieldName }); } diff --git a/src/internal-packages/schema/lib/d3/date.js b/src/internal-packages/schema/lib/d3/date.js index cb686011c8a..b3816885f34 100644 --- a/src/internal-packages/schema/lib/d3/date.js +++ b/src/internal-packages/schema/lib/d3/date.js @@ -1,17 +1,19 @@ /* eslint no-use-before-define: 0, camelcase:0 */ +const app = require('ampersand-app'); const d3 = require('d3'); const _ = require('lodash'); const $ = require('jquery'); const moment = require('moment'); const shared = require('./shared'); const many = require('./many'); -const QueryBuilderAction = require('../../../query/lib/action'); const inValueRange = require('../../../query/lib/util').inValueRange; // const debug = require('debug')('mongodb-compass:minicharts:date'); require('./d3-tip')(d3); +const QueryAction = app.appRegistry.getAction('QueryAction'); + function generateDefaults(n) { const doc = {}; _.each(_.range(n), function(d) { @@ -91,7 +93,7 @@ const minicharts_d3fns_date = function() { // number of selected items has changed, trigger querybuilder event if (selected[0].length === 0) { // clear value - QueryBuilderAction.clearValue({ + QueryAction.clearValue({ field: options.fieldName }); return; @@ -107,14 +109,14 @@ const minicharts_d3fns_date = function() { if (_.isEqual(minValue.ts, maxValue.ts)) { // if values are the same, single equality query - QueryBuilderAction.setValue({ + QueryAction.setValue({ field: options.fieldName, value: minValue.value }); return; } // binned values, build range query with $gte and $lte - QueryBuilderAction.setRangeValues({ + QueryAction.setRangeValues({ field: options.fieldName, min: minValue.value, max: maxValue.value, @@ -135,7 +137,7 @@ const minicharts_d3fns_date = function() { if (d3.event.shiftKey && lastNonShiftRangeValue) { const minVal = d.ts < lastNonShiftRangeValue.ts ? d.value : lastNonShiftRangeValue.value; const maxVal = d.ts > lastNonShiftRangeValue.ts ? d.value : lastNonShiftRangeValue.value; - QueryBuilderAction.setRangeValues({ + QueryAction.setRangeValues({ field: options.fieldName, min: minVal, max: maxVal, @@ -144,7 +146,7 @@ const minicharts_d3fns_date = function() { } else { // remember non-shift value so that range can be extended with shift lastNonShiftRangeValue = d; - QueryBuilderAction.setValue({ + QueryAction.setValue({ field: options.fieldName, value: d.value, unsetIfSet: true diff --git a/src/internal-packages/schema/lib/d3/few.js b/src/internal-packages/schema/lib/d3/few.js index 8e44e154133..75f3a492ee0 100644 --- a/src/internal-packages/schema/lib/d3/few.js +++ b/src/internal-packages/schema/lib/d3/few.js @@ -1,9 +1,9 @@ /* eslint no-use-before-define: 0, camelcase: 0 */ +const app = require('ampersand-app'); const d3 = require('d3'); const $ = require('jquery'); const _ = require('lodash'); const shared = require('./shared'); -const QueryBuilderAction = require('../../../query/lib/action'); const tooltipTemplate = require('./tooltip.jade'); const hasDistinctValue = require('../../../query/lib/util').hasDistinctValue; @@ -11,6 +11,8 @@ const hasDistinctValue = require('../../../query/lib/util').hasDistinctValue; require('./d3-tip')(d3); +const QueryAction = app.appRegistry.getAction('QueryAction'); + const minicharts_d3fns_few = function() { // --- beginning chart setup --- let width = 400; // default width @@ -57,7 +59,7 @@ const minicharts_d3fns_few = function() { // if selection has changed, trigger query builder event if (numSelected !== selected[0].length) { const values = _.map(selected.data(), 'value'); - QueryBuilderAction.setDistinctValues({ + QueryAction.setDistinctValues({ field: options.fieldName, value: values }); @@ -80,7 +82,7 @@ const minicharts_d3fns_few = function() { const qbAction = d3.event.shiftKey ? - QueryBuilderAction.toggleDistinctValue : QueryBuilderAction.setValue; + QueryAction.toggleDistinctValue : QueryAction.setValue; qbAction({ field: options.fieldName, value: d.value, diff --git a/src/internal-packages/schema/lib/d3/many.js b/src/internal-packages/schema/lib/d3/many.js index 5a926253d51..1af667c169c 100644 --- a/src/internal-packages/schema/lib/d3/many.js +++ b/src/internal-packages/schema/lib/d3/many.js @@ -1,9 +1,9 @@ /* eslint no-use-before-define: 0, camelcase: 0 */ +const app = require('ampersand-app'); const d3 = require('d3'); const $ = require('jquery'); const _ = require('lodash'); const shared = require('./shared'); -const QueryBuilderAction = require('../../../query/lib/action'); const tooltipTemplate = require('./tooltip.jade'); const hasDistinctValue = require('../../../query/lib/util').hasDistinctValue; const inValueRange = require('../../../query/lib/util').inValueRange; @@ -12,6 +12,8 @@ const inValueRange = require('../../../query/lib/util').inValueRange; require('./d3-tip')(d3); +const QueryAction = app.appRegistry.getAction('QueryAction'); + const minicharts_d3fns_many = function() { // --- beginning chart setup --- let width = 400; // default width @@ -66,7 +68,7 @@ const minicharts_d3fns_many = function() { if (numSelected !== selected[0].length) { if (selected[0].length === 0) { // clear value - QueryBuilderAction.clearValue({ + QueryAction.clearValue({ field: options.fieldName }); return; @@ -74,7 +76,7 @@ const minicharts_d3fns_many = function() { // distinct values (strings) if (options.selectionType === 'distinct') { const values = _.map(selected.data(), 'value'); - QueryBuilderAction.setDistinctValues({ + QueryAction.setDistinctValues({ field: options.fieldName, value: values }); @@ -90,7 +92,7 @@ const minicharts_d3fns_many = function() { if (minValue.value === maxValue.value + maxValue.dx) { // if not binned and values are the same, single equality query - QueryBuilderAction.setValue({ + QueryAction.setValue({ field: options.fieldName, value: minValue.value }); @@ -98,7 +100,7 @@ const minicharts_d3fns_many = function() { } // binned values, build range query with $gte and $lt (if binned) // or $gte and $lte (if not binned) - QueryBuilderAction.setRangeValues({ + QueryAction.setRangeValues({ field: options.fieldName, min: minValue.value, max: maxValue.value + maxValue.dx, @@ -142,14 +144,14 @@ const minicharts_d3fns_many = function() { if (options.selectionType === 'distinct') { // distinct values, behavior dependent on shift key const qbAction = d3.event.shiftKey ? - QueryBuilderAction.toggleDistinctValue : QueryBuilderAction.setValue; + QueryAction.toggleDistinctValue : QueryAction.setValue; qbAction({ field: options.fieldName, value: d.value, unsetIfSet: true }); } else if (d3.event.shiftKey && lastNonShiftRangeValue) { - QueryBuilderAction.setRangeValues({ + QueryAction.setRangeValues({ field: options.fieldName, min: Math.min(d.value, lastNonShiftRangeValue.value), max: Math.max(d.value + d.dx, lastNonShiftRangeValue.value + lastNonShiftRangeValue.dx), @@ -160,7 +162,7 @@ const minicharts_d3fns_many = function() { lastNonShiftRangeValue = d; if (d.dx > 0) { // binned bars, turn single value into range - QueryBuilderAction.setRangeValues({ + QueryAction.setRangeValues({ field: options.fieldName, min: d.value, max: d.value + d.dx, @@ -168,7 +170,7 @@ const minicharts_d3fns_many = function() { }); } else { // bars don't represent bins, build single value query - QueryBuilderAction.setValue({ + QueryAction.setValue({ field: options.fieldName, value: d.value, unsetIfSet: true diff --git a/src/internal-packages/schema/lib/store/index.jsx b/src/internal-packages/schema/lib/store/index.jsx index 8d53c637591..305c807f698 100644 --- a/src/internal-packages/schema/lib/store/index.jsx +++ b/src/internal-packages/schema/lib/store/index.jsx @@ -1,12 +1,11 @@ -const Reflux = require('reflux'); const app = require('ampersand-app'); +const Reflux = require('reflux'); const StateMixin = require('reflux-state-mixin'); const Schema = require('mongodb-schema').Schema; const _ = require('lodash'); // stores const NamespaceStore = require('hadron-reflux-store').NamespaceStore; -const QueryStore = require('../../../query/lib/store'); // actions const SchemaAction = require('../action'); @@ -91,6 +90,7 @@ const SchemaStore = Reflux.createStore({ * This function is called when the collection filter changes. */ startSampling() { + const QueryStore = app.appRegistry.getStore('QueryStore'); const query = QueryStore.state.query; if (_.includes(['counting', 'sampling', 'analyzing'], this.state.samplingState)) { diff --git a/src/internal-packages/schema/styles/index.less b/src/internal-packages/schema/styles/index.less new file mode 100644 index 00000000000..9e201684a46 --- /dev/null +++ b/src/internal-packages/schema/styles/index.less @@ -0,0 +1,336 @@ +// minicharts +@mc-blue0: #43B1E5; +@mc-blue1: lighten(@mc-blue0, 7.5%); +@mc-blue2: lighten(@mc-blue0, 15%); +@mc-blue3: lighten(@mc-blue0, 22.5%); +@mc-blue4: lighten(@mc-blue0, 30%); +@mc-blue5: lighten(@mc-blue0, 37.5%); + +@mc-bg: @gray8; +@mc-fg: @mc-blue0; +@mc-fg-selected: @chart0; +@mc-fg-unselected: @gray6; + +div.minichart.unique { + font-size: 12px; + + dl.dl-horizontal { + margin-left: -32px; + padding-top: 12px; + max-height: 112px; + overflow: hidden; + + dt { + color: @gray3; + width: 20px; + + a { + color: @gray5; + display: inline-block; + + &:hover { + text-decoration: none; + color: @gray1; + } + } + i.mms-icon-continuous { // remove after wrapping this in an again + color: @gray5; + cursor: pointer; + + &:hover { + color: @gray1; + } + } + } + dd { + margin-left: 30px; + overflow: hidden; + + ul li { + margin-bottom: 6px; + + code { + cursor: pointer; + background-color: @gray7; + border: 1px solid transparent; + color: @gray1; + font-size: 12px; + line-height: 20px; + user-select: none; + -moz-user-select: none; + -webkit-user-select: none; + -ms-user-select: none; + } + + code.selected { + background-color: @mc-fg-selected; + color: @pw; + // border: 1px dotted @gray2; + } + } + } + } +} + +.minichart-wrapper { + svg.minichart { + margin-left: -40px; + } +} + +.layer, .layer svg { + position: absolute; +} + +.layer svg.marker { + width: 20px; + height: 20px; + + circle { + fill: @mc-fg; + stroke: @pw; + stroke-width: 1.5px; + + &.selected { + fill: @mc-fg-selected; + } + } +} + +.layer svg.selection { + visibility: hidden; + + circle { + fill: @mc-fg-selected; + fill-opacity:0.2; + stroke: @mc-fg-selected; + stroke-width: 2px; + } +} + +svg.minichart { + font-size: 10px; + + text { + fill: @gray4; + font-weight: bold; + } + + .glass { + opacity: 0; + } + + g.brush rect.extent { + fill: @mc-fg-selected; + fill-opacity:0.2; + } + + .hour, .weekday { + .bar { + cursor: default !important; + } + } + + .bar { + shape-rendering: crispEdges; + cursor: crosshair; + + rect.bg { + fill: @mc-bg; + } + + rect.fg { + fill: @mc-fg; + + &.selected { + fill: @mc-fg-selected; + } + + &.half-selected { + fill: @mc-fg-selected; + mask: url(#mask-stripe); + } + + &.unselected { + fill: @mc-fg-unselected; + } + } + + &.few { + + rect { + stroke: white; + stroke-width: 2px; + } + + rect.fg-0 { + fill: @mc-blue0; + } + + rect.fg-1 { + fill: @mc-blue1; + } + + rect.fg-2 { + fill: @mc-blue2; + } + + rect.fg-3 { + fill: @mc-blue3; + } + + rect.fg-4 { + fill: @mc-blue4; + } + + rect.fg-5 { + fill: @mc-blue5; + } + + rect.fg.selected { + fill: @mc-fg-selected; + } + rect.fg.unselected { + fill: @mc-fg-unselected; + } + + text { + fill: white; + font-size: 12px; + } + } + } + + .line { + stroke: @mc-fg; + + &.selected { + stroke: @mc-fg-selected; + } + } + + .legend { + text { + fill: @gray5; + } + + line { + stroke: @gray7; + } + shape-rendering: crispEdges; + } + + .axis path, .axis line { + fill: none; + stroke: @gray7; + shape-rendering: crispEdges; + } + + .circle { + fill: @mc-fg; + stroke: @pw; + stroke-width: 1.5px; + + &.selected { + fill: @mc-fg-selected; + } + } +} + +.tooltip-wrapper { + line-height: 120%; + max-width: 400px; +} + +.map { + position:absolute; + top:0; + bottom:0; + width:100%; + float: left; + svg { + position: absolute; + width: 100%; + height: 100%; + } + nav { + position: absolute; + top: 40px; + left: 20px; + z-index: 1; + } + #circle { + background-color: rgba(20, 20, 20, 0.1); + font-family: Helvetica, sans-serif; + color: #3b83bd; + padding: 5px 8px; + border-radius: 3px; + cursor: pointer; + border: 1px solid #111; + } + #circle.active { + background-color: rgba(250, 250, 250, 0.9); + } + i.help { + display: inline-block; + float: left; + } +} + +// -- d3-tip styling +.d3-tip { + z-index: 2; + line-height: 1; + padding: 8px; + background: #000; + color: #fff; + border-radius: 5px; + pointer-events: none; + font-size: 12px; +} + +/* Creates a small triangle extender for the tooltip */ +.d3-tip:after { + box-sizing: border-box; + display: inline; + font-size: 14px; + width: 100%; + line-height: 1; + color: #000; + position: absolute; + pointer-events: none; +} + +/* Northward tooltips */ +.d3-tip.n:after { + content: "\25BC"; + margin: -4px 0 0 0; + top: 100%; + left: 0; + text-align: center; +} + +/* Eastward tooltips */ +.d3-tip.e:after { + content: "\25C0"; + margin: -4px 0 0 0; + top: 50%; + left: -8px; +} + +/* Southward tooltips */ +.d3-tip.s:after { + content: "\25B2"; + margin: 0 0 1px 0; + top: -8px; + left: 0; + text-align: center; +} + +/* Westward tooltips */ +.d3-tip.w:after { + content: "\25B6"; + margin: -4px 0 0 -1px; + top: 50%; + left: 100%; +} From 5e96b85fac1fbae39dc1800e130efe596f649324 Mon Sep 17 00:00:00 2001 From: Thomas Rueckstiess Date: Fri, 29 Jul 2016 19:19:44 +0200 Subject: [PATCH 08/25] handle error when stream ends. --- src/internal-packages/schema/lib/component/index.jsx | 1 + src/internal-packages/schema/lib/store/index.jsx | 2 ++ 2 files changed, 3 insertions(+) diff --git a/src/internal-packages/schema/lib/component/index.jsx b/src/internal-packages/schema/lib/component/index.jsx index 72047936381..4809e09f635 100644 --- a/src/internal-packages/schema/lib/component/index.jsx +++ b/src/internal-packages/schema/lib/component/index.jsx @@ -37,6 +37,7 @@ const Schema = React.createClass({ progressbar: false, animation: false }); + return; } const progress = this.state.samplingProgress; // initial schema phase, cannot measure progress, enable trickling diff --git a/src/internal-packages/schema/lib/store/index.jsx b/src/internal-packages/schema/lib/store/index.jsx index 305c807f698..bdd6355c04a 100644 --- a/src/internal-packages/schema/lib/store/index.jsx +++ b/src/internal-packages/schema/lib/store/index.jsx @@ -184,6 +184,8 @@ const SchemaStore = Reflux.createStore({ .on('end', () => { if (sampleCount > 0 && this.state.samplingState !== 'error') { onSuccess(schema.serialize()); + } else { + return onError(); } }); }); From ccbc8a50d782ff3c18431d55026ee3a1d7afbc41 Mon Sep 17 00:00:00 2001 From: Thomas Rueckstiess Date: Fri, 29 Jul 2016 22:54:22 +0200 Subject: [PATCH 09/25] trigger filterChanged event on apply --- src/internal-packages/query/lib/store/index.jsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/internal-packages/query/lib/store/index.jsx b/src/internal-packages/query/lib/store/index.jsx index 729ead79a90..eaa3b52c850 100644 --- a/src/internal-packages/query/lib/store/index.jsx +++ b/src/internal-packages/query/lib/store/index.jsx @@ -7,6 +7,7 @@ const EJSON = require('mongodb-extended-json'); const Query = require('mongodb-language-model').Query; const _ = require('lodash'); const hasDistinctValue = require('../util').hasDistinctValue; +const filterChanged = require('hadron-action').filterChanged; const bsonEqual = require('../util').bsonEqual; const debug = require('debug')('mongodb-compass:stores:query'); @@ -341,6 +342,7 @@ const QueryStore = Reflux.createStore({ // start queries for all tabs: schema, documents, explain, indexes const SchemaAction = app.appRegistry.getAction('SchemaAction'); SchemaAction.startSampling(); + filterChanged(this.state.query); } }, From 00099ffbd54096345294077d9c33f123285e17b0 Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Thu, 4 Aug 2016 14:32:36 +0200 Subject: [PATCH 10/25] INT-1663: Move sampling message to query --- src/internal-packages/crud/index.js | 3 ++ .../crud/lib/component/document-actions.jsx | 2 +- .../crud/lib/component/document-footer.jsx | 2 +- .../crud/lib/component/document-list.jsx | 4 +- .../crud/lib/component/icon-button.jsx | 53 ------------------- .../lib/component/insert-document-dialog.jsx | 2 +- .../lib/component/remove-document-footer.jsx | 2 +- .../crud/lib/component/text-button.jsx | 52 ------------------ src/internal-packages/query/index.js | 4 ++ .../lib/component/sampling-message.jsx | 27 ++++++++-- 10 files changed, 37 insertions(+), 114 deletions(-) delete mode 100644 src/internal-packages/crud/lib/component/icon-button.jsx delete mode 100644 src/internal-packages/crud/lib/component/text-button.jsx rename src/internal-packages/{crud => query}/lib/component/sampling-message.jsx (59%) diff --git a/src/internal-packages/crud/index.js b/src/internal-packages/crud/index.js index 6412a88b019..d5a1cbd6984 100644 --- a/src/internal-packages/crud/index.js +++ b/src/internal-packages/crud/index.js @@ -4,6 +4,7 @@ const app = require('ampersand-app'); const DocumentList = require('./lib/component/document-list'); const Actions = require('./lib/actions'); const InsertDocumentStore = require('./lib/store/insert-document-store'); +const ResetDocumentListStore = require('./lib/store/reset-document-list-store'); /** * Activate all the components in the CRUD package. @@ -12,6 +13,7 @@ function activate() { app.appRegistry.registerComponent('Component::CRUD::DocumentList', DocumentList); app.appRegistry.registerAction('Action::CRUD::DocumentRemoved', Actions.documentRemoved); app.appRegistry.registerStore('Store::CRUD::InsertDocumentStore', InsertDocumentStore); + app.appRegistry.registerStore('Store::CRUD::ResetDocumentListStore', ResetDocumentListStore); } /** @@ -21,6 +23,7 @@ function deactivate() { app.appRegistry.deregisterComponent('Component::CRUD::DocumentList'); app.appRegistry.deregisterAction('Action::CRUD::DocumentRemoved'); app.appRegistry.deregisterStore('Store::CRUD::InsertDocumentStore'); + app.appRegistry.deregisterStore('Store::CRUD::ResetDocumentListStore'); } module.exports.activate = activate; diff --git a/src/internal-packages/crud/lib/component/document-actions.jsx b/src/internal-packages/crud/lib/component/document-actions.jsx index c2887717850..711e7a9089c 100644 --- a/src/internal-packages/crud/lib/component/document-actions.jsx +++ b/src/internal-packages/crud/lib/component/document-actions.jsx @@ -1,7 +1,7 @@ 'use strict'; const React = require('react'); -const IconButton = require('./icon-button'); +const IconButton = require('hadron-app-registry').IconButton; /** * Component for actions on the document. diff --git a/src/internal-packages/crud/lib/component/document-footer.jsx b/src/internal-packages/crud/lib/component/document-footer.jsx index 3f82eeb0efe..b63cf939d28 100644 --- a/src/internal-packages/crud/lib/component/document-footer.jsx +++ b/src/internal-packages/crud/lib/component/document-footer.jsx @@ -3,7 +3,7 @@ const _ = require('lodash'); const React = require('react'); const Element = require('hadron-document').Element; -const TextButton = require('./text-button'); +const TextButton = require('hadron-app-registry').TextButton; /** * The progress mode. diff --git a/src/internal-packages/crud/lib/component/document-list.jsx b/src/internal-packages/crud/lib/component/document-list.jsx index cad49ae232b..dafa04f64b5 100644 --- a/src/internal-packages/crud/lib/component/document-list.jsx +++ b/src/internal-packages/crud/lib/component/document-list.jsx @@ -13,7 +13,6 @@ const LoadMoreDocumentsStore = require('../store/load-more-documents-store'); const RemoveDocumentStore = require('../store/remove-document-store'); const InsertDocumentStore = require('../store/insert-document-store'); const InsertDocumentDialog = require('./insert-document-dialog'); -const SamplingMessage = require('./sampling-message'); const Actions = require('../actions'); /** @@ -70,6 +69,7 @@ class DocumentList extends React.Component { constructor(props) { super(props); this.loading = false; + this.samplingMessage = app.appRegistry.getComponent('Component::Query::SamplingMessage'); this.state = { docs: [], nextSkip: 0, namespace: NamespaceStore.ns }; } @@ -182,7 +182,7 @@ class DocumentList extends React.Component { render() { return (
    - +
      this._node = c}> diff --git a/src/internal-packages/crud/lib/component/icon-button.jsx b/src/internal-packages/crud/lib/component/icon-button.jsx deleted file mode 100644 index 3611a9ee6d7..00000000000 --- a/src/internal-packages/crud/lib/component/icon-button.jsx +++ /dev/null @@ -1,53 +0,0 @@ -'use strict'; - -const React = require('react'); - -/** - * The button constant. - */ -const BUTTON = 'button'; - -/** - * Component for a button with an icon. - */ -class IconButton extends React.Component { - - /** - * The component constructor. - * - * @param {Object} props - The properties. - */ - constructor(props) { - super(props); - } - - /** - * Render the button. - * - * @returns {Component} The button component. - */ - render() { - return ( - - ); - } - - /** - * By default should not need to to re-render itself. - * - * @returns {Boolean} Always false. - */ - shouldComponentUpdate() { - return false; - } -} - -IconButton.displayName = 'IconButton'; - -module.exports = IconButton; diff --git a/src/internal-packages/crud/lib/component/insert-document-dialog.jsx b/src/internal-packages/crud/lib/component/insert-document-dialog.jsx index a68312182d3..eeaeae0d5a9 100644 --- a/src/internal-packages/crud/lib/component/insert-document-dialog.jsx +++ b/src/internal-packages/crud/lib/component/insert-document-dialog.jsx @@ -4,7 +4,7 @@ const React = require('react'); const Modal = require('react-bootstrap').Modal; const OpenInsertDocumentDialogStore = require('../store/open-insert-document-dialog-store'); const InsertDocument = require('./insert-document'); -const TextButton = require('./text-button'); +const TextButton = require('hadron-app-registry').TextButton; const Actions = require('../actions'); /** diff --git a/src/internal-packages/crud/lib/component/remove-document-footer.jsx b/src/internal-packages/crud/lib/component/remove-document-footer.jsx index a76ddd5dbea..9e7611c0c9f 100644 --- a/src/internal-packages/crud/lib/component/remove-document-footer.jsx +++ b/src/internal-packages/crud/lib/component/remove-document-footer.jsx @@ -1,7 +1,7 @@ 'use strict'; const React = require('react'); -const TextButton = require('./text-button'); +const TextButton = require('hadron-app-registry').TextButton; /** * The progress mode. diff --git a/src/internal-packages/crud/lib/component/text-button.jsx b/src/internal-packages/crud/lib/component/text-button.jsx deleted file mode 100644 index ea17014b28a..00000000000 --- a/src/internal-packages/crud/lib/component/text-button.jsx +++ /dev/null @@ -1,52 +0,0 @@ -'use strict'; - -const React = require('react'); - -/** - * The button constant. - */ -const BUTTON = 'button'; - -/** - * Component for a button with text. - */ -class TextButton extends React.Component { - - /** - * The component constructor. - * - * @param {Object} props - The properties. - */ - constructor(props) { - super(props); - } - - /** - * Render the button. - * - * @returns {Component} The button component. - */ - render() { - return ( - - ); - } - - /** - * By default should not need to to re-render itself. - * - * @returns {Boolean} Always false. - */ - shouldComponentUpdate() { - return false; - } -} - -TextButton.displayName = 'TextButton'; - -module.exports = TextButton; diff --git a/src/internal-packages/query/index.js b/src/internal-packages/query/index.js index dc513af1d72..12e59204eb7 100644 --- a/src/internal-packages/query/index.js +++ b/src/internal-packages/query/index.js @@ -1,12 +1,15 @@ const app = require('ampersand-app'); const QueryBarComponent = require('./lib/component'); +const SamplingMessage = require('./lib/component/sampling-message'); const QueryAction = require('./lib/action'); const QueryStore = require('./lib/store'); + /** * Activate all the components in the Query Bar package. */ function activate() { app.appRegistry.registerComponent('App:QueryBar', QueryBarComponent); + app.appRegistry.registerComponent('Component::Query::SamplingMessage', SamplingMessage); app.appRegistry.registerAction('QueryAction', QueryAction); app.appRegistry.registerStore('QueryStore', QueryStore); } @@ -16,6 +19,7 @@ function activate() { */ function deactivate() { app.appRegistry.deregisterComponent('App:QueryBar'); + app.appRegistry.deregisterComponent('Component::Query::SamplingMessage'); app.appRegistry.deregisterAction('QueryAction'); app.appRegistry.deregisterStore('QueryStore'); } diff --git a/src/internal-packages/crud/lib/component/sampling-message.jsx b/src/internal-packages/query/lib/component/sampling-message.jsx similarity index 59% rename from src/internal-packages/crud/lib/component/sampling-message.jsx rename to src/internal-packages/query/lib/component/sampling-message.jsx index c367a3a064d..1000bb5b5ae 100644 --- a/src/internal-packages/crud/lib/component/sampling-message.jsx +++ b/src/internal-packages/query/lib/component/sampling-message.jsx @@ -1,8 +1,8 @@ 'use strict'; const React = require('react'); -const ResetDocumentListStore = require('../store/reset-document-list-store'); -const TextButton = require('./text-button'); +const app = require('ampersand-app'); +const TextButton = require('hadron-app-registry').TextButton; /** * Component for the sampling message. @@ -13,7 +13,9 @@ class SamplingMessage extends React.Component { * Fetch the state when the component mounts. */ componentDidMount() { - this.unsubscribeReset = ResetDocumentListStore.listen(this.handleReset.bind(this)); + this.unsubscribeReset = this.resetDocumentListStore.listen(this.handleReset.bind(this)); + this.unsubscribeInsert = this.insertDocumentStore.listen(this.handleInsert.bind(this)); + this.unsubscribeRemove = this.documentRemovedAction.listen(this.handleRemove.bind(this)); } /** @@ -21,6 +23,8 @@ class SamplingMessage extends React.Component { */ componentWillUnmount() { this.unsubscribeReset(); + this.unsubscribeInsert(); + this.unsubscribeRemove(); } /** @@ -31,6 +35,23 @@ class SamplingMessage extends React.Component { constructor(props) { super(props); this.state = { count: 0 }; + this.resetDocumentListStore = app.appRegistry.getStore('Store::CRUD::ResetDocumentListStore'); + this.insertDocumentStore = app.appRegistry.getStore('Store::CRUD::InsertDocumentStore'); + this.documentRemovedAction = app.appRegistry.getAction('Action::CRUD::DocumentRemoved'); + } + + /** + * Handle updating the count on document insert. + */ + handleInsert() { + this.setState({ count: this.state.count + 1 }); + } + + /** + * Handle updating the count on document removal. + */ + handleRemove() { + this.setState({ count: this.state.count - 1 }); } /** From 103978dc5031c63ba6405f921861fdfdbff828bb Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Thu, 4 Aug 2016 16:32:25 +0200 Subject: [PATCH 11/25] INT-1663: Use sampling message in schema view --- src/app/schema/index.js | 174 +----------------- .../query/lib/component/sampling-message.jsx | 12 +- .../schema/lib/component/index.jsx | 13 +- 3 files changed, 21 insertions(+), 178 deletions(-) diff --git a/src/app/schema/index.js b/src/app/schema/index.js index 48440b41175..a29634ef59a 100644 --- a/src/app/schema/index.js +++ b/src/app/schema/index.js @@ -1,13 +1,9 @@ var View = require('ampersand-view'); var app = require('ampersand-app'); - var React = require('react'); var ReactDOM = require('react-dom'); -// var debug = require('debug')('mongodb-compass:schema'); - var SchemaView = View.extend({ - template: require('./index.jade'), props: { loading: { type: 'boolean', @@ -24,8 +20,8 @@ var SchemaView = View.extend({ this.schemaView = app.appRegistry.getComponent('Collection:Schema'); }, render: function() { - this.renderWithTemplate(); - ReactDOM.render(React.createElement(this.schemaView), this.queryByHook('fields-subview')); + ReactDOM.render(React.createElement(this.schemaView), this.queryByHook('schema-subview')); + return this; }, remove: function() { View.prototype.remove.call(this); @@ -33,169 +29,3 @@ var SchemaView = View.extend({ }); module.exports = SchemaView; - -// var View = require('ampersand-view'); -// var FieldListView = require('./field-list.js'); -// var SampledSchema = require('../models/sampled-schema'); -// var SamplingMessageView = require('../sampling-message'); -// var app = require('ampersand-app'); -// var electron = require('electron'); -// var remote = electron.remote; -// var dialog = remote.dialog; -// var BrowserWindow = remote.BrowserWindow; -// var clipboard = remote.clipboard; -// var format = require('util').format; -// var metrics = require('mongodb-js-metrics')(); -// var ipc = require('hadron-ipc'); -// -// var debug = require('debug')('mongodb-compass:schema:index'); -// -// var indexTemplate = require('./index.jade'); -// -// var SchemaView = View.extend({ -// // modelType: 'Collection', -// template: indexTemplate, -// props: { -// visible: { -// type: 'boolean', -// default: false -// }, -// sampling: { -// type: 'boolean', -// default: false -// }, -// hasRefineBar: ['boolean', true, true] -// }, -// derived: { -// is_empty: { -// deps: ['schema.sample_size', 'schema.is_fetching'], -// fn: function() { -// return this.schema.sample_size === 0 && !this.schema.is_fetching; -// } -// } -// }, -// events: { -// 'click .splitter': 'onSplitterClick' -// }, -// bindings: { -// visible: { -// type: 'booleanClass', -// no: 'hidden' -// }, -// is_empty: [ -// { -// hook: 'empty', -// type: 'booleanClass', -// no: 'hidden' -// }, -// { -// hook: 'column-container', -// type: 'booleanClass', -// yes: 'hidden' -// } -// ] -// }, -// children: { -// schema: SampledSchema -// }, -// initialize: function() { -// this.listenTo(this.schema, 'sync error', this.schemaIsSynced.bind(this)); -// this.listenTo(this.schema, 'request', this.schemaIsRequested.bind(this)); -// this.listenTo(this.model, 'sync', this.onCollectionFetched.bind(this)); -// this.listenTo(this.parent, 'submit:query', this.onQueryChanged.bind(this)); -// this.on('change:visible', this.onVisibleChanged.bind(this)); -// -// ipc.on('window:menu-share-schema-json', this.onShareSchema.bind(this)); -// }, -// render: function() { -// this.renderWithTemplate(this); -// }, -// schemaIsSynced: function() { -// // only listen to share menu events if we have a sync'ed schema -// this.sampling = false; -// ipc.call('window:show-share-submenu'); -// }, -// schemaIsRequested: function() { -// ipc.call('window:hide-share-submenu'); -// this.sampling = true; -// }, -// onVisibleChanged: function() { -// if (this.visible) { -// this.parent.refineBarView.visible = this.hasRefineBar; -// } -// if (this.visible && this.sampling) { -// app.statusbar.visible = true; -// } else { -// app.statusbar.visible = false; -// } -// }, -// onShareSchema: function() { -// clipboard.writeText(JSON.stringify(this.schema.serialize(), null, ' ')); -// -// var detail = format('The schema definition of %s has been copied to your ' -// + 'clipboard in JSON format.', this.model._id); -// -// dialog.showMessageBox(BrowserWindow.getFocusedWindow(), { -// type: 'info', -// message: 'Share Schema', -// detail: detail, -// buttons: ['OK'] -// }); -// -// metrics.track('Share Schema', 'used'); -// }, -// onCollectionFetched: function() { -// debug('collection fetched in schema'); -// // track collection information -// -// // @todo thomasr fix this, maybe this.model._id ? -// // if (!ns) { -// // this.visible = false; -// // debug('No active collection namespace so no schema has been requested yet.'); -// // return; -// // } -// -// // this.visible = true; -// app.queryOptions.reset(); -// app.volatileQueryOptions.reset(); -// -// this.schema.ns = this.model._id; -// this.schema.reset(); -// var options = app.volatileQueryOptions.serialize(); -// if (this.visible) { -// app.statusbar.visible = true; -// } -// this.schema.fetch(options); -// }, -// onQueryChanged: function() { -// var options = app.queryOptions.serialize(); -// if (this.visible) { -// app.statusbar.visible = true; -// } -// this.schema.refine(options); -// }, -// subviews: { -// sampling_message: { -// hook: 'sampling-message-subview', -// prepareView: function(el) { -// return new SamplingMessageView({ -// el: el, -// parent: this, -// model: this.schema -// }); -// } -// }, -// fields: { -// hook: 'fields-subview', -// prepareView: function(el) { -// return new FieldListView({ -// el: el, -// parent: this, -// collection: this.schema.fields -// }); -// } -// } -// } -// }); -// -// module.exports = SchemaView; diff --git a/src/internal-packages/query/lib/component/sampling-message.jsx b/src/internal-packages/query/lib/component/sampling-message.jsx index 1000bb5b5ae..7976f6c7ce3 100644 --- a/src/internal-packages/query/lib/component/sampling-message.jsx +++ b/src/internal-packages/query/lib/component/sampling-message.jsx @@ -74,12 +74,20 @@ class SamplingMessage extends React.Component {
      Query returned {this.state.count} documents. + {this.renderInsertButton()} +
      + ); + } + + renderInsertButton() { + if (this.props.insertHandler) { + return ( -
    - ); + ); + } } /** diff --git a/src/internal-packages/schema/lib/component/index.jsx b/src/internal-packages/schema/lib/component/index.jsx index 4809e09f635..0a39497fef9 100644 --- a/src/internal-packages/schema/lib/component/index.jsx +++ b/src/internal-packages/schema/lib/component/index.jsx @@ -18,6 +18,7 @@ const Schema = React.createClass({ ], componentWillMount() { + this.samplingMessage = app.appRegistry.getComponent('Component::Query::SamplingMessage'); this.StatusAction = app.appRegistry.getAction('StatusAction'); }, @@ -71,7 +72,6 @@ const Schema = React.createClass({ } }, - /** * Render the schema * @@ -84,9 +84,14 @@ const Schema = React.createClass({ return ; }); return ( -
    -
    - {fieldList} +
    + +
    +
    +
    + {fieldList} +
    +
    ); From 001a6f669eb73ff018ab1de81641b272ad9d5180 Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Sun, 7 Aug 2016 14:31:55 +0200 Subject: [PATCH 12/25] INT-1663: Expose Document component INT-1663: Add indexes package INT-1663: Adding initial indexes react package INT-1663: Split index column components INT-1663: Move index work to new view INT-1663: Fixing tooltips INT-1663: Allow document to not be editable Upgrade react-bootstrap --- package.json | 2 +- src/app/home/collection.jade | 3 + src/app/home/collection.js | 24 ++++- src/app/indexes-new/index.js | 31 ++++++ src/internal-packages/crud/index.js | 3 + .../crud/lib/component/document.jsx | 42 +++++--- src/internal-packages/indexes/index.js | 24 +++++ .../indexes/lib/action/index-actions.js | 7 ++ .../lib/component/index-header-column.jsx | 37 +++++++ .../indexes/lib/component/index-header.jsx | 42 ++++++++ .../indexes/lib/component/index-list.jsx | 79 +++++++++++++++ .../indexes/lib/component/index.jsx | 48 +++++++++ .../indexes/lib/component/indexes.jsx | 42 ++++++++ .../indexes/lib/component/name-column.jsx | 93 +++++++++++++++++ .../indexes/lib/component/property-column.jsx | 99 +++++++++++++++++++ .../indexes/lib/component/size-column.jsx | 44 +++++++++ .../indexes/lib/component/type-column.jsx | 63 ++++++++++++ .../indexes/lib/component/usage-column.jsx | 45 +++++++++ .../indexes/lib/store/load-indexes-store.js | 32 ++++++ .../indexes/lib/store/sort-indexes-store.js | 28 ++++++ src/internal-packages/indexes/package.json | 9 ++ 21 files changed, 778 insertions(+), 19 deletions(-) create mode 100644 src/app/indexes-new/index.js create mode 100644 src/internal-packages/indexes/index.js create mode 100644 src/internal-packages/indexes/lib/action/index-actions.js create mode 100644 src/internal-packages/indexes/lib/component/index-header-column.jsx create mode 100644 src/internal-packages/indexes/lib/component/index-header.jsx create mode 100644 src/internal-packages/indexes/lib/component/index-list.jsx create mode 100644 src/internal-packages/indexes/lib/component/index.jsx create mode 100644 src/internal-packages/indexes/lib/component/indexes.jsx create mode 100644 src/internal-packages/indexes/lib/component/name-column.jsx create mode 100644 src/internal-packages/indexes/lib/component/property-column.jsx create mode 100644 src/internal-packages/indexes/lib/component/size-column.jsx create mode 100644 src/internal-packages/indexes/lib/component/type-column.jsx create mode 100644 src/internal-packages/indexes/lib/component/usage-column.jsx create mode 100644 src/internal-packages/indexes/lib/store/load-indexes-store.js create mode 100644 src/internal-packages/indexes/lib/store/sort-indexes-store.js create mode 100644 src/internal-packages/indexes/package.json diff --git a/package.json b/package.json index 08d8867fa50..603bbba1232 100644 --- a/package.json +++ b/package.json @@ -137,7 +137,7 @@ "qs": "^5.2.0", "raf": "^3.1.0", "react": "^15.2.1", - "react-bootstrap": "0.29.5", + "react-bootstrap": "0.30.2", "react-dom": "^15.2.1", "react-native-listener": "^1.0.1", "react-tooltip": "^2.0.3", diff --git a/src/app/home/collection.jade b/src/app/home/collection.jade index 3620a653e91..990887361c0 100644 --- a/src/app/home/collection.jade +++ b/src/app/home/collection.jade @@ -15,6 +15,8 @@ a Explain Plan li(role='presentation', data-hook='index-tab', id='index-tab') a Indexes + li(role='presentation', data-hook='indexes-new-tab', id='indexes-new-tab') + a Indexes New .row div(data-hook='refine-bar-subview') @@ -22,3 +24,4 @@ div(data-hook='schema-subview') div(data-hook='explain-subview') div(data-hook='index-subview') + div(data-hook='indexes-new-subview' class='index-container') diff --git a/src/app/home/collection.js b/src/app/home/collection.js index f842094569a..c75e7097d91 100644 --- a/src/app/home/collection.js +++ b/src/app/home/collection.js @@ -3,6 +3,7 @@ var CollectionStatsView = require('../collection-stats'); var DocumentView = require('../documents'); var SchemaView = require('../schema'); var IndexView = require('../indexes'); +var IndexesNewView = require('../indexes-new'); var ExplainView = require('../explain-plan'); var MongoDBCollection = require('../models/mongodb-collection'); var React = require('react'); @@ -21,7 +22,8 @@ var tabToViewMap = { 'DOCUMENTS': 'documentView', 'SCHEMA': 'schemaView', 'EXPLAIN PLAN': 'explainView', - 'INDEXES': 'indexView' + 'INDEXES': 'indexView', + 'INDEXES NEW': 'indexesNewView' }; var MongoDBCollectionView = View.extend({ @@ -37,7 +39,7 @@ var MongoDBCollectionView = View.extend({ type: 'string', required: true, default: 'schemaView', - values: ['documentView', 'schemaView', 'explainView', 'indexView'] + values: ['documentView', 'schemaView', 'explainView', 'indexView', 'indexesNewView'] }, ns: 'string' }, @@ -59,7 +61,8 @@ var MongoDBCollectionView = View.extend({ 'documentView': '[data-hook=document-tab]', 'schemaView': '[data-hook=schema-tab]', 'explainView': '[data-hook=explain-tab]', - 'indexView': '[data-hook=index-tab]' + 'indexView': '[data-hook=index-tab]', + 'indexesNewView': '[data-hook=indexes-new-tab]' } } }, @@ -117,6 +120,17 @@ var MongoDBCollectionView = View.extend({ model: this.model }); } + }, + indexesNewView: { + hook: 'indexes-new-subview', + waitFor: 'ns', + prepareView: function(el) { + return new IndexesNewView({ + el: el, + parent: this, + model: this.model + }); + } } // refineBarView: { // hook: 'refine-bar-subview', @@ -136,7 +150,8 @@ var MongoDBCollectionView = View.extend({ }, initialize: function() { this.model = new MongoDBCollection(); - NamespaceStore.listen( this.onCollectionChanged.bind(this) ); + NamespaceStore.listen(this.onCollectionChanged.bind(this)); + this.loadIndexesAction = app.appRegistry.getAction('Action::Indexes::LoadIndexes'); // this.listenToAndRun(this.parent, 'change:ns', this.onCollectionChanged.bind(this)); }, render: function() { @@ -185,6 +200,7 @@ var MongoDBCollectionView = View.extend({ metadata['collection name length'] = model.getId().length - model.database.length - 1; metrics.track('Collection', 'fetched', metadata); + this.loadIndexesAction(); } }); diff --git a/src/app/indexes-new/index.js b/src/app/indexes-new/index.js new file mode 100644 index 00000000000..ca87d3603d9 --- /dev/null +++ b/src/app/indexes-new/index.js @@ -0,0 +1,31 @@ +var View = require('ampersand-view'); +var app = require('ampersand-app'); +var React = require('react'); +var ReactDOM = require('react-dom'); + +var IndexesNewView = View.extend({ + props: { + loading: { + type: 'boolean', + default: false + } + }, + bindings: { + loading: { + type: 'toggle', + hook: 'loading' + } + }, + initialize: function() { + this.indexesView = app.appRegistry.getComponent('Component::Indexes::Indexes'); + }, + render: function() { + ReactDOM.render(React.createElement(this.indexesView), this.queryByHook('indexes-new-subview')); + return this; + }, + remove: function() { + View.prototype.remove.call(this); + } +}); + +module.exports = IndexesNewView; diff --git a/src/internal-packages/crud/index.js b/src/internal-packages/crud/index.js index d5a1cbd6984..8afd6bc78a0 100644 --- a/src/internal-packages/crud/index.js +++ b/src/internal-packages/crud/index.js @@ -2,6 +2,7 @@ const app = require('ampersand-app'); const DocumentList = require('./lib/component/document-list'); +const Document = require('./lib/component/document'); const Actions = require('./lib/actions'); const InsertDocumentStore = require('./lib/store/insert-document-store'); const ResetDocumentListStore = require('./lib/store/reset-document-list-store'); @@ -11,6 +12,7 @@ const ResetDocumentListStore = require('./lib/store/reset-document-list-store'); */ function activate() { app.appRegistry.registerComponent('Component::CRUD::DocumentList', DocumentList); + app.appRegistry.registerComponent('Component::CRUD::Document', Document); app.appRegistry.registerAction('Action::CRUD::DocumentRemoved', Actions.documentRemoved); app.appRegistry.registerStore('Store::CRUD::InsertDocumentStore', InsertDocumentStore); app.appRegistry.registerStore('Store::CRUD::ResetDocumentListStore', ResetDocumentListStore); @@ -21,6 +23,7 @@ function activate() { */ function deactivate() { app.appRegistry.deregisterComponent('Component::CRUD::DocumentList'); + app.appRegistry.deregisterComponent('Component::CRUD::Document'); app.appRegistry.deregisterAction('Action::CRUD::DocumentRemoved'); app.appRegistry.deregisterStore('Store::CRUD::InsertDocumentStore'); app.appRegistry.deregisterStore('Store::CRUD::ResetDocumentListStore'); diff --git a/src/internal-packages/crud/lib/component/document.jsx b/src/internal-packages/crud/lib/component/document.jsx index 2b16e5195cc..bf6a0c52883 100644 --- a/src/internal-packages/crud/lib/component/document.jsx +++ b/src/internal-packages/crud/lib/component/document.jsx @@ -39,14 +39,16 @@ class Document extends React.Component { this.doc = props.doc; this.state = { doc: this.doc, editing: false }; - // Actions need to be scoped to the single document component and not - // global singletons. - this.actions = Reflux.createActions([ 'update', 'remove', 'cancelRemove' ]); - - // The update store needs to be scoped to a document and not a global - // singleton. - this.updateStore = this.createUpdateStore(this.actions); - this.removeStore = this.createRemoveStore(this.actions); + if (this.isEditable()) { + // Actions need to be scoped to the single document component and not + // global singletons. + this.actions = Reflux.createActions([ 'update', 'remove', 'cancelRemove' ]); + + // The update store needs to be scoped to a document and not a global + // singleton. + this.updateStore = this.createUpdateStore(this.actions); + this.removeStore = this.createRemoveStore(this.actions); + } } /** @@ -143,16 +145,29 @@ class Document extends React.Component { * Subscribe to the update store on mount. */ componentDidMount() { - this.unsubscribeUpdate = this.updateStore.listen(this.handleStoreUpdate.bind(this)); - this.unsubscribeRemove = this.removeStore.listen(this.handleStoreRemove.bind(this)); + if (this.isEditable()) { + this.unsubscribeUpdate = this.updateStore.listen(this.handleStoreUpdate.bind(this)); + this.unsubscribeRemove = this.removeStore.listen(this.handleStoreRemove.bind(this)); + } } /** * Unsubscribe from the udpate store on unmount. */ componentWillUnmount() { - this.unsubscribeUpdate(); - this.unsubscribeRemove(); + if (this.isEditable()) { + this.unsubscribeUpdate(); + this.unsubscribeRemove(); + } + } + + /** + * Is the document editable? + * + * @returns {Boolean} If the document is editable. + */ + isEditable() { + return this.props.editable !== false; } /** @@ -300,7 +315,7 @@ class Document extends React.Component { * @returns {Component} The actions component. */ renderActions() { - if (!this.state.editing && !this.state.deleting) { + if (this.isEditable() && !this.state.editing && !this.state.deleting) { return ( ); } else if (this.state.deleting) { - console.log(this.state); return ( + {this.props.name} + + + + ); + } +} + +IndexHeaderColumn.displayName = 'IndexHeaderColumn'; + +module.exports = IndexHeaderColumn; diff --git a/src/internal-packages/indexes/lib/component/index-header.jsx b/src/internal-packages/indexes/lib/component/index-header.jsx new file mode 100644 index 00000000000..ee244d6cf0d --- /dev/null +++ b/src/internal-packages/indexes/lib/component/index-header.jsx @@ -0,0 +1,42 @@ +'use strict'; + +const React = require('react'); +const IndexHeaderColumn = require('./index-header-column'); + +/** + * Component for the index header. + */ +class IndexHeader extends React.Component { + + /** + * The component constructor. + * + * @param {Object} props - The properties. + */ + constructor(props) { + super(props); + } + + /** + * Render the index header. + * + * @returns {React.Component} The index header. + */ + render() { + return ( + + + + + + + + + + ); + } +} + +IndexHeader.displayName = 'IndexHeader'; + +module.exports = IndexHeader; diff --git a/src/internal-packages/indexes/lib/component/index-list.jsx b/src/internal-packages/indexes/lib/component/index-list.jsx new file mode 100644 index 00000000000..cc0c7cfc703 --- /dev/null +++ b/src/internal-packages/indexes/lib/component/index-list.jsx @@ -0,0 +1,79 @@ +'use strict'; + +const _ = require('lodash'); +const React = require('react'); +const IndexModel = require('mongodb-index-model'); +const Index = require('./index'); +const LoadIndexesStore = require('../store/load-indexes-store'); +const SortIndexesStore = require('../store/sort-indexes-store'); + +/** + * Component for the index list. + */ +class IndexList extends React.Component { + + /** + * Subscribe on mount. + */ + componentWillMount() { + this.unsubscribeLoad = LoadIndexesStore.listen(this.handleIndexChange.bind(this)); + this.unsubscribeSort = SortIndexesStore.listen(this.handleIndexChange.bind(this)); + } + + /** + * Unsubscribe on unmount. + */ + componentWillUnmount() { + this.unsubscribeLoad(); + this.unsubscribeSort(); + } + + /** + * The component constructor. + * + * @param {Object} props - The properties. + */ + constructor(props) { + super(props); + this.state = { indexes: [] }; + } + + /** + * Handles the sort indexes store triggering with indexes in a new order or the + * initial load of indexes. + * + * @param {Array} indexes - The indexes. + */ + handleIndexChange(indexes) { + this.setState({ indexes: indexes }); + } + + /** + * Render the index list. + * + * @returns {React.Component} The index list. + */ + render() { + var maxSize = this._computeMaxSize(); + var indexes = _.map(this.state.indexes, (index) => { + var model = new IndexModel(new IndexModel().parse(index)); + model.relativeSize = model.size / maxSize * 100; + return (); + }); + return ( + + {indexes} + + ); + } + + _computeMaxSize() { + return _.max(this.state.indexes, (index) => { + return index.size; + }).size; + } +} + +IndexList.displayName = 'IndexList'; + +module.exports = IndexList; diff --git a/src/internal-packages/indexes/lib/component/index.jsx b/src/internal-packages/indexes/lib/component/index.jsx new file mode 100644 index 00000000000..a58053520a6 --- /dev/null +++ b/src/internal-packages/indexes/lib/component/index.jsx @@ -0,0 +1,48 @@ +'use strict'; + +const React = require('react'); +const NameColumn = require('./name-column'); +const TypeColumn = require('./type-column'); +const SizeColumn = require('./size-column'); +const UsageColumn = require('./usage-column'); +const PropertyColumn = require('./property-column'); + +/** + * Component for the index. + */ +class Index extends React.Component { + + /** + * The component constructor. + * + * @param {Object} props - The properties. + */ + constructor(props) { + super(props); + console.log(props.index); + } + + /** + * Render the index. + * + * @returns {React.Component} The index. + */ + render() { + return ( + + + + + + + + ); + } +} + +Index.displayName = 'Index'; + +module.exports = Index; diff --git a/src/internal-packages/indexes/lib/component/indexes.jsx b/src/internal-packages/indexes/lib/component/indexes.jsx new file mode 100644 index 00000000000..c55278cf309 --- /dev/null +++ b/src/internal-packages/indexes/lib/component/indexes.jsx @@ -0,0 +1,42 @@ +'use strict'; + +const React = require('react'); +const IndexHeader = require('./index-header'); +const IndexList = require('./index-list'); + +/** + * Component for the indexes. + */ +class Indexes extends React.Component { + + /** + * The component constructor. + * + * @param {Object} props - The properties. + */ + constructor(props) { + super(props); + } + + /** + * Render the indexes. + * + * @returns {React.Component} The indexes. + */ + render() { + return ( +
    +
    + + + +
    +
    +
    + ); + } +} + +Indexes.displayName = 'Indexes'; + +module.exports = Indexes; diff --git a/src/internal-packages/indexes/lib/component/name-column.jsx b/src/internal-packages/indexes/lib/component/name-column.jsx new file mode 100644 index 00000000000..2382a2fc209 --- /dev/null +++ b/src/internal-packages/indexes/lib/component/name-column.jsx @@ -0,0 +1,93 @@ +'use strict'; + +const _ = require('lodash'); +const React = require('react'); + +/** + * Component for the name column. + */ +class NameColumn extends React.Component { + + /** + * The component constructor. + * + * @param {Object} props - The properties. + */ + constructor(props) { + super(props); + } + + /** + * Render the name column. + * + * @returns {React.Component} The name column. + */ + render() { + var fields = _.map(this.props.index.fields.serialize(), (field) => { + return this.renderField(field); + }); + return ( + +
    + {this.props.index.name} +
    +
    +

    + {fields} +

    +
    + + ); + } + + /** + * Render the direction of the index field. + * + * @param {Object} field - The field. + * + * @returns {React.Component} The field component. + */ + renderDirection(field) { + if (field.value === 1) { + return ( + + + + ); + } else if (field.value === -1) { + return ( + + + + ); + } else { + return ( + + {field.value} + + ); + } + } + + /** + * Render a field in an index. + * + * @param {Object} field - The field. + * + * @returns {React.Component} The field component. + */ + renderField(field) { + return ( + + + {field.field} + {this.renderDirection(field)} + + + ); + } +} + +NameColumn.displayName = 'NameColumn'; + +module.exports = NameColumn; diff --git a/src/internal-packages/indexes/lib/component/property-column.jsx b/src/internal-packages/indexes/lib/component/property-column.jsx new file mode 100644 index 00000000000..0ff6fbf9b92 --- /dev/null +++ b/src/internal-packages/indexes/lib/component/property-column.jsx @@ -0,0 +1,99 @@ +'use strict'; + +const _ = require('lodash'); +const format = require('util').format; +const React = require('react'); + +/** + * Component for the property column. + */ +class PropertyColumn extends React.Component { + + /** + * The component constructor. + * + * @param {Object} props - The properties. + */ + constructor(props) { + super(props); + } + + /** + * Render the property column. + * + * @returns {React.Component} The property column. + */ + render() { + var properties = _.map(this.props.index.properties, (prop) => { + return this.renderProperty(prop); + }); + return ( + +
    + {properties} + {this.renderCardinality()} +
    + + ); + } + + /** + * Render cardinality info. + * + * @returns {React.Component} The cardianlity info. + */ + renderCardinality() { + if (this.props.index.cardinality === 'compoun') { + return ( +
    + {this.props.index.cardinality} + +
    + ); + } + } + + /** + * Render the property column + * + * @param {String} prop - The property. + * + * @returns {React.Component} The property component. + */ + renderProperty(prop) { + if (prop === 'ttl') { + return ( +
    + {prop} + +
    + ); + } else if (prop === 'partial') { + return ( +
    + {prop} + +
    + ); + } else { + return ( +
    + {prop} + +
    + ); + } + } + + partialTooltip() { + return format('partialFilterExpression: %j', this.props.index.extra.partialFilterExpression); + } + + ttlTooltip() { + return format('expireAfterSeconds: %d', this.props.index.extra.expireAfterSeconds); + } +} + +PropertyColumn.displayProperty = 'PropertyColumn'; + +module.exports = PropertyColumn; diff --git a/src/internal-packages/indexes/lib/component/size-column.jsx b/src/internal-packages/indexes/lib/component/size-column.jsx new file mode 100644 index 00000000000..44aad6ba7f5 --- /dev/null +++ b/src/internal-packages/indexes/lib/component/size-column.jsx @@ -0,0 +1,44 @@ +'use strict'; + +const React = require('react'); + +/** + * Component for the size column. + */ +class SizeColumn extends React.Component { + + /** + * The component constructor. + * + * @param {Object} props - The properties. + */ + constructor(props) { + super(props); + } + + /** + * Render the size column. + * + * @returns {React.Component} The size column. + */ + render() { + return ( + +
    + {this.props.size} +
    +
    + {this.props.unit} +
    +
    +
    +
    +
    + + ); + } +} + +SizeColumn.displaySize = 'SizeColumn'; + +module.exports = SizeColumn; diff --git a/src/internal-packages/indexes/lib/component/type-column.jsx b/src/internal-packages/indexes/lib/component/type-column.jsx new file mode 100644 index 00000000000..60c179e98e5 --- /dev/null +++ b/src/internal-packages/indexes/lib/component/type-column.jsx @@ -0,0 +1,63 @@ +'use strict'; + +const _ = require('lodash'); +const React = require('react'); + +/** + * Component for the type column. + */ +class TypeColumn extends React.Component { + + /** + * The component constructor. + * + * @param {Object} props - The properties. + */ + constructor(props) { + super(props); + } + + /** + * Render the type column. + * + * @returns {React.Component} The type column. + */ + render() { + return ( + + {this.renderType()} + + ); + } + + /** + * Render the type div. + */ + renderType() { + if (this.props.type === 'text') { + return ( +
    + {this.props.index.type} + +
    + ); + } + return ( +
    + {this.props.index.type} + +
    + ); + } + + _textTooltip() { + var info = _.pick(this.props.index.extra, ['weights', 'default_language', 'language_override']); + return _.map(info, (v, k) => { + return format('%s: %j', k, v); + }).join('\n'); + } +} + +TypeColumn.displayType = 'TypeColumn'; + +module.exports = TypeColumn; diff --git a/src/internal-packages/indexes/lib/component/usage-column.jsx b/src/internal-packages/indexes/lib/component/usage-column.jsx new file mode 100644 index 00000000000..29bdaa84679 --- /dev/null +++ b/src/internal-packages/indexes/lib/component/usage-column.jsx @@ -0,0 +1,45 @@ +'use strict'; + +const React = require('react'); + +/** + * Component for the usage column. + */ +class UsageColumn extends React.Component { + + /** + * The component constructor. + * + * @param {Object} props - The properties. + */ + constructor(props) { + super(props); + } + + /** + * Render the usage column. + * + * @returns {React.Component} The usage column. + */ + render() { + return ( + + +
    + {this.props.usage} +
    +
    + since  + + {this.props.since.toDateString()} + +
    +
    + + ); + } +} + +UsageColumn.displayUsage = 'UsageColumn'; + +module.exports = UsageColumn; diff --git a/src/internal-packages/indexes/lib/store/load-indexes-store.js b/src/internal-packages/indexes/lib/store/load-indexes-store.js new file mode 100644 index 00000000000..b9df3706e00 --- /dev/null +++ b/src/internal-packages/indexes/lib/store/load-indexes-store.js @@ -0,0 +1,32 @@ +'use strict'; + +const Reflux = require('reflux'); +const app = require('ampersand-app'); +const NamespaceStore = require('hadron-reflux-store').NamespaceStore; +const Action = require('../action/index-actions'); + +/** + * The reflux store for sorting indexes + */ +const LoadIndexesStore = Reflux.createStore({ + + /** + * Initialize the load indexes store. + */ + init: function() { + this.listenTo(Action.loadIndexes, this.loadIndexes); + }, + + /** + * Load the indexes. + */ + loadIndexes: function() { + if (NamespaceStore.ns) { + app.dataService.indexes(NamespaceStore.ns, {}, (err, indexes) => { + this.trigger(indexes); + }); + } + } +}); + +module.exports = LoadIndexesStore; diff --git a/src/internal-packages/indexes/lib/store/sort-indexes-store.js b/src/internal-packages/indexes/lib/store/sort-indexes-store.js new file mode 100644 index 00000000000..f7e6b024343 --- /dev/null +++ b/src/internal-packages/indexes/lib/store/sort-indexes-store.js @@ -0,0 +1,28 @@ +'use strict'; + +const Reflux = require('reflux'); +const Action = require('../action/index-actions'); + +/** + * The reflux store for sorting indexes + */ +const SortIndexesStore = Reflux.createStore({ + + /** + * Initialize the sort indexes store. + */ + init: function() { + this.listenTo(Action.sortIndexes, this.sortIndexes); + }, + + /** + * Sort the indexes + * + * @param {Array} indexes - The indexes to sort. + */ + sortIndexes: function(indexes) { + this.trigger(indexes); + } +}); + +module.exports = SortIndexesStore; diff --git a/src/internal-packages/indexes/package.json b/src/internal-packages/indexes/package.json new file mode 100644 index 00000000000..0afed9b188f --- /dev/null +++ b/src/internal-packages/indexes/package.json @@ -0,0 +1,9 @@ +{ + "name": "indexes", + "productName": "Compass Indexes Support", + "description": "Indexes support for Compass as an internal package.", + "version": "0.0.1", + "authors": "MongoDB Inc.", + "private": true, + "main": "./index.js" +} From 93cdbc8e76138f5037193498d710b7d260407b26 Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Thu, 18 Aug 2016 21:18:22 +0200 Subject: [PATCH 13/25] Revert "INT-1663: Expose Document component" This reverts commit 57e644cfa3c7f8e663ab9c95bc24f5d02a06d8d3. --- package.json | 2 +- src/app/home/collection.jade | 3 - src/app/home/collection.js | 24 +---- src/app/indexes-new/index.js | 31 ------ src/internal-packages/crud/index.js | 3 - .../crud/lib/component/document.jsx | 42 +++----- src/internal-packages/indexes/index.js | 24 ----- .../indexes/lib/action/index-actions.js | 7 -- .../lib/component/index-header-column.jsx | 37 ------- .../indexes/lib/component/index-header.jsx | 42 -------- .../indexes/lib/component/index-list.jsx | 79 --------------- .../indexes/lib/component/index.jsx | 48 --------- .../indexes/lib/component/indexes.jsx | 42 -------- .../indexes/lib/component/name-column.jsx | 93 ----------------- .../indexes/lib/component/property-column.jsx | 99 ------------------- .../indexes/lib/component/size-column.jsx | 44 --------- .../indexes/lib/component/type-column.jsx | 63 ------------ .../indexes/lib/component/usage-column.jsx | 45 --------- .../indexes/lib/store/load-indexes-store.js | 32 ------ .../indexes/lib/store/sort-indexes-store.js | 28 ------ src/internal-packages/indexes/package.json | 9 -- 21 files changed, 19 insertions(+), 778 deletions(-) delete mode 100644 src/app/indexes-new/index.js delete mode 100644 src/internal-packages/indexes/index.js delete mode 100644 src/internal-packages/indexes/lib/action/index-actions.js delete mode 100644 src/internal-packages/indexes/lib/component/index-header-column.jsx delete mode 100644 src/internal-packages/indexes/lib/component/index-header.jsx delete mode 100644 src/internal-packages/indexes/lib/component/index-list.jsx delete mode 100644 src/internal-packages/indexes/lib/component/index.jsx delete mode 100644 src/internal-packages/indexes/lib/component/indexes.jsx delete mode 100644 src/internal-packages/indexes/lib/component/name-column.jsx delete mode 100644 src/internal-packages/indexes/lib/component/property-column.jsx delete mode 100644 src/internal-packages/indexes/lib/component/size-column.jsx delete mode 100644 src/internal-packages/indexes/lib/component/type-column.jsx delete mode 100644 src/internal-packages/indexes/lib/component/usage-column.jsx delete mode 100644 src/internal-packages/indexes/lib/store/load-indexes-store.js delete mode 100644 src/internal-packages/indexes/lib/store/sort-indexes-store.js delete mode 100644 src/internal-packages/indexes/package.json diff --git a/package.json b/package.json index 603bbba1232..08d8867fa50 100644 --- a/package.json +++ b/package.json @@ -137,7 +137,7 @@ "qs": "^5.2.0", "raf": "^3.1.0", "react": "^15.2.1", - "react-bootstrap": "0.30.2", + "react-bootstrap": "0.29.5", "react-dom": "^15.2.1", "react-native-listener": "^1.0.1", "react-tooltip": "^2.0.3", diff --git a/src/app/home/collection.jade b/src/app/home/collection.jade index 990887361c0..3620a653e91 100644 --- a/src/app/home/collection.jade +++ b/src/app/home/collection.jade @@ -15,8 +15,6 @@ a Explain Plan li(role='presentation', data-hook='index-tab', id='index-tab') a Indexes - li(role='presentation', data-hook='indexes-new-tab', id='indexes-new-tab') - a Indexes New .row div(data-hook='refine-bar-subview') @@ -24,4 +22,3 @@ div(data-hook='schema-subview') div(data-hook='explain-subview') div(data-hook='index-subview') - div(data-hook='indexes-new-subview' class='index-container') diff --git a/src/app/home/collection.js b/src/app/home/collection.js index c75e7097d91..f842094569a 100644 --- a/src/app/home/collection.js +++ b/src/app/home/collection.js @@ -3,7 +3,6 @@ var CollectionStatsView = require('../collection-stats'); var DocumentView = require('../documents'); var SchemaView = require('../schema'); var IndexView = require('../indexes'); -var IndexesNewView = require('../indexes-new'); var ExplainView = require('../explain-plan'); var MongoDBCollection = require('../models/mongodb-collection'); var React = require('react'); @@ -22,8 +21,7 @@ var tabToViewMap = { 'DOCUMENTS': 'documentView', 'SCHEMA': 'schemaView', 'EXPLAIN PLAN': 'explainView', - 'INDEXES': 'indexView', - 'INDEXES NEW': 'indexesNewView' + 'INDEXES': 'indexView' }; var MongoDBCollectionView = View.extend({ @@ -39,7 +37,7 @@ var MongoDBCollectionView = View.extend({ type: 'string', required: true, default: 'schemaView', - values: ['documentView', 'schemaView', 'explainView', 'indexView', 'indexesNewView'] + values: ['documentView', 'schemaView', 'explainView', 'indexView'] }, ns: 'string' }, @@ -61,8 +59,7 @@ var MongoDBCollectionView = View.extend({ 'documentView': '[data-hook=document-tab]', 'schemaView': '[data-hook=schema-tab]', 'explainView': '[data-hook=explain-tab]', - 'indexView': '[data-hook=index-tab]', - 'indexesNewView': '[data-hook=indexes-new-tab]' + 'indexView': '[data-hook=index-tab]' } } }, @@ -120,17 +117,6 @@ var MongoDBCollectionView = View.extend({ model: this.model }); } - }, - indexesNewView: { - hook: 'indexes-new-subview', - waitFor: 'ns', - prepareView: function(el) { - return new IndexesNewView({ - el: el, - parent: this, - model: this.model - }); - } } // refineBarView: { // hook: 'refine-bar-subview', @@ -150,8 +136,7 @@ var MongoDBCollectionView = View.extend({ }, initialize: function() { this.model = new MongoDBCollection(); - NamespaceStore.listen(this.onCollectionChanged.bind(this)); - this.loadIndexesAction = app.appRegistry.getAction('Action::Indexes::LoadIndexes'); + NamespaceStore.listen( this.onCollectionChanged.bind(this) ); // this.listenToAndRun(this.parent, 'change:ns', this.onCollectionChanged.bind(this)); }, render: function() { @@ -200,7 +185,6 @@ var MongoDBCollectionView = View.extend({ metadata['collection name length'] = model.getId().length - model.database.length - 1; metrics.track('Collection', 'fetched', metadata); - this.loadIndexesAction(); } }); diff --git a/src/app/indexes-new/index.js b/src/app/indexes-new/index.js deleted file mode 100644 index ca87d3603d9..00000000000 --- a/src/app/indexes-new/index.js +++ /dev/null @@ -1,31 +0,0 @@ -var View = require('ampersand-view'); -var app = require('ampersand-app'); -var React = require('react'); -var ReactDOM = require('react-dom'); - -var IndexesNewView = View.extend({ - props: { - loading: { - type: 'boolean', - default: false - } - }, - bindings: { - loading: { - type: 'toggle', - hook: 'loading' - } - }, - initialize: function() { - this.indexesView = app.appRegistry.getComponent('Component::Indexes::Indexes'); - }, - render: function() { - ReactDOM.render(React.createElement(this.indexesView), this.queryByHook('indexes-new-subview')); - return this; - }, - remove: function() { - View.prototype.remove.call(this); - } -}); - -module.exports = IndexesNewView; diff --git a/src/internal-packages/crud/index.js b/src/internal-packages/crud/index.js index 8afd6bc78a0..d5a1cbd6984 100644 --- a/src/internal-packages/crud/index.js +++ b/src/internal-packages/crud/index.js @@ -2,7 +2,6 @@ const app = require('ampersand-app'); const DocumentList = require('./lib/component/document-list'); -const Document = require('./lib/component/document'); const Actions = require('./lib/actions'); const InsertDocumentStore = require('./lib/store/insert-document-store'); const ResetDocumentListStore = require('./lib/store/reset-document-list-store'); @@ -12,7 +11,6 @@ const ResetDocumentListStore = require('./lib/store/reset-document-list-store'); */ function activate() { app.appRegistry.registerComponent('Component::CRUD::DocumentList', DocumentList); - app.appRegistry.registerComponent('Component::CRUD::Document', Document); app.appRegistry.registerAction('Action::CRUD::DocumentRemoved', Actions.documentRemoved); app.appRegistry.registerStore('Store::CRUD::InsertDocumentStore', InsertDocumentStore); app.appRegistry.registerStore('Store::CRUD::ResetDocumentListStore', ResetDocumentListStore); @@ -23,7 +21,6 @@ function activate() { */ function deactivate() { app.appRegistry.deregisterComponent('Component::CRUD::DocumentList'); - app.appRegistry.deregisterComponent('Component::CRUD::Document'); app.appRegistry.deregisterAction('Action::CRUD::DocumentRemoved'); app.appRegistry.deregisterStore('Store::CRUD::InsertDocumentStore'); app.appRegistry.deregisterStore('Store::CRUD::ResetDocumentListStore'); diff --git a/src/internal-packages/crud/lib/component/document.jsx b/src/internal-packages/crud/lib/component/document.jsx index bf6a0c52883..2b16e5195cc 100644 --- a/src/internal-packages/crud/lib/component/document.jsx +++ b/src/internal-packages/crud/lib/component/document.jsx @@ -39,16 +39,14 @@ class Document extends React.Component { this.doc = props.doc; this.state = { doc: this.doc, editing: false }; - if (this.isEditable()) { - // Actions need to be scoped to the single document component and not - // global singletons. - this.actions = Reflux.createActions([ 'update', 'remove', 'cancelRemove' ]); - - // The update store needs to be scoped to a document and not a global - // singleton. - this.updateStore = this.createUpdateStore(this.actions); - this.removeStore = this.createRemoveStore(this.actions); - } + // Actions need to be scoped to the single document component and not + // global singletons. + this.actions = Reflux.createActions([ 'update', 'remove', 'cancelRemove' ]); + + // The update store needs to be scoped to a document and not a global + // singleton. + this.updateStore = this.createUpdateStore(this.actions); + this.removeStore = this.createRemoveStore(this.actions); } /** @@ -145,29 +143,16 @@ class Document extends React.Component { * Subscribe to the update store on mount. */ componentDidMount() { - if (this.isEditable()) { - this.unsubscribeUpdate = this.updateStore.listen(this.handleStoreUpdate.bind(this)); - this.unsubscribeRemove = this.removeStore.listen(this.handleStoreRemove.bind(this)); - } + this.unsubscribeUpdate = this.updateStore.listen(this.handleStoreUpdate.bind(this)); + this.unsubscribeRemove = this.removeStore.listen(this.handleStoreRemove.bind(this)); } /** * Unsubscribe from the udpate store on unmount. */ componentWillUnmount() { - if (this.isEditable()) { - this.unsubscribeUpdate(); - this.unsubscribeRemove(); - } - } - - /** - * Is the document editable? - * - * @returns {Boolean} If the document is editable. - */ - isEditable() { - return this.props.editable !== false; + this.unsubscribeUpdate(); + this.unsubscribeRemove(); } /** @@ -315,7 +300,7 @@ class Document extends React.Component { * @returns {Component} The actions component. */ renderActions() { - if (this.isEditable() && !this.state.editing && !this.state.deleting) { + if (!this.state.editing && !this.state.deleting) { return ( ); } else if (this.state.deleting) { + console.log(this.state); return ( - {this.props.name} - - - - ); - } -} - -IndexHeaderColumn.displayName = 'IndexHeaderColumn'; - -module.exports = IndexHeaderColumn; diff --git a/src/internal-packages/indexes/lib/component/index-header.jsx b/src/internal-packages/indexes/lib/component/index-header.jsx deleted file mode 100644 index ee244d6cf0d..00000000000 --- a/src/internal-packages/indexes/lib/component/index-header.jsx +++ /dev/null @@ -1,42 +0,0 @@ -'use strict'; - -const React = require('react'); -const IndexHeaderColumn = require('./index-header-column'); - -/** - * Component for the index header. - */ -class IndexHeader extends React.Component { - - /** - * The component constructor. - * - * @param {Object} props - The properties. - */ - constructor(props) { - super(props); - } - - /** - * Render the index header. - * - * @returns {React.Component} The index header. - */ - render() { - return ( - - - - - - - - - - ); - } -} - -IndexHeader.displayName = 'IndexHeader'; - -module.exports = IndexHeader; diff --git a/src/internal-packages/indexes/lib/component/index-list.jsx b/src/internal-packages/indexes/lib/component/index-list.jsx deleted file mode 100644 index cc0c7cfc703..00000000000 --- a/src/internal-packages/indexes/lib/component/index-list.jsx +++ /dev/null @@ -1,79 +0,0 @@ -'use strict'; - -const _ = require('lodash'); -const React = require('react'); -const IndexModel = require('mongodb-index-model'); -const Index = require('./index'); -const LoadIndexesStore = require('../store/load-indexes-store'); -const SortIndexesStore = require('../store/sort-indexes-store'); - -/** - * Component for the index list. - */ -class IndexList extends React.Component { - - /** - * Subscribe on mount. - */ - componentWillMount() { - this.unsubscribeLoad = LoadIndexesStore.listen(this.handleIndexChange.bind(this)); - this.unsubscribeSort = SortIndexesStore.listen(this.handleIndexChange.bind(this)); - } - - /** - * Unsubscribe on unmount. - */ - componentWillUnmount() { - this.unsubscribeLoad(); - this.unsubscribeSort(); - } - - /** - * The component constructor. - * - * @param {Object} props - The properties. - */ - constructor(props) { - super(props); - this.state = { indexes: [] }; - } - - /** - * Handles the sort indexes store triggering with indexes in a new order or the - * initial load of indexes. - * - * @param {Array} indexes - The indexes. - */ - handleIndexChange(indexes) { - this.setState({ indexes: indexes }); - } - - /** - * Render the index list. - * - * @returns {React.Component} The index list. - */ - render() { - var maxSize = this._computeMaxSize(); - var indexes = _.map(this.state.indexes, (index) => { - var model = new IndexModel(new IndexModel().parse(index)); - model.relativeSize = model.size / maxSize * 100; - return (); - }); - return ( - - {indexes} - - ); - } - - _computeMaxSize() { - return _.max(this.state.indexes, (index) => { - return index.size; - }).size; - } -} - -IndexList.displayName = 'IndexList'; - -module.exports = IndexList; diff --git a/src/internal-packages/indexes/lib/component/index.jsx b/src/internal-packages/indexes/lib/component/index.jsx deleted file mode 100644 index a58053520a6..00000000000 --- a/src/internal-packages/indexes/lib/component/index.jsx +++ /dev/null @@ -1,48 +0,0 @@ -'use strict'; - -const React = require('react'); -const NameColumn = require('./name-column'); -const TypeColumn = require('./type-column'); -const SizeColumn = require('./size-column'); -const UsageColumn = require('./usage-column'); -const PropertyColumn = require('./property-column'); - -/** - * Component for the index. - */ -class Index extends React.Component { - - /** - * The component constructor. - * - * @param {Object} props - The properties. - */ - constructor(props) { - super(props); - console.log(props.index); - } - - /** - * Render the index. - * - * @returns {React.Component} The index. - */ - render() { - return ( - - - - - - - - ); - } -} - -Index.displayName = 'Index'; - -module.exports = Index; diff --git a/src/internal-packages/indexes/lib/component/indexes.jsx b/src/internal-packages/indexes/lib/component/indexes.jsx deleted file mode 100644 index c55278cf309..00000000000 --- a/src/internal-packages/indexes/lib/component/indexes.jsx +++ /dev/null @@ -1,42 +0,0 @@ -'use strict'; - -const React = require('react'); -const IndexHeader = require('./index-header'); -const IndexList = require('./index-list'); - -/** - * Component for the indexes. - */ -class Indexes extends React.Component { - - /** - * The component constructor. - * - * @param {Object} props - The properties. - */ - constructor(props) { - super(props); - } - - /** - * Render the indexes. - * - * @returns {React.Component} The indexes. - */ - render() { - return ( -
    -
    - - - -
    -
    -
    - ); - } -} - -Indexes.displayName = 'Indexes'; - -module.exports = Indexes; diff --git a/src/internal-packages/indexes/lib/component/name-column.jsx b/src/internal-packages/indexes/lib/component/name-column.jsx deleted file mode 100644 index 2382a2fc209..00000000000 --- a/src/internal-packages/indexes/lib/component/name-column.jsx +++ /dev/null @@ -1,93 +0,0 @@ -'use strict'; - -const _ = require('lodash'); -const React = require('react'); - -/** - * Component for the name column. - */ -class NameColumn extends React.Component { - - /** - * The component constructor. - * - * @param {Object} props - The properties. - */ - constructor(props) { - super(props); - } - - /** - * Render the name column. - * - * @returns {React.Component} The name column. - */ - render() { - var fields = _.map(this.props.index.fields.serialize(), (field) => { - return this.renderField(field); - }); - return ( - -
    - {this.props.index.name} -
    -
    -

    - {fields} -

    -
    - - ); - } - - /** - * Render the direction of the index field. - * - * @param {Object} field - The field. - * - * @returns {React.Component} The field component. - */ - renderDirection(field) { - if (field.value === 1) { - return ( - - - - ); - } else if (field.value === -1) { - return ( - - - - ); - } else { - return ( - - {field.value} - - ); - } - } - - /** - * Render a field in an index. - * - * @param {Object} field - The field. - * - * @returns {React.Component} The field component. - */ - renderField(field) { - return ( - - - {field.field} - {this.renderDirection(field)} - - - ); - } -} - -NameColumn.displayName = 'NameColumn'; - -module.exports = NameColumn; diff --git a/src/internal-packages/indexes/lib/component/property-column.jsx b/src/internal-packages/indexes/lib/component/property-column.jsx deleted file mode 100644 index 0ff6fbf9b92..00000000000 --- a/src/internal-packages/indexes/lib/component/property-column.jsx +++ /dev/null @@ -1,99 +0,0 @@ -'use strict'; - -const _ = require('lodash'); -const format = require('util').format; -const React = require('react'); - -/** - * Component for the property column. - */ -class PropertyColumn extends React.Component { - - /** - * The component constructor. - * - * @param {Object} props - The properties. - */ - constructor(props) { - super(props); - } - - /** - * Render the property column. - * - * @returns {React.Component} The property column. - */ - render() { - var properties = _.map(this.props.index.properties, (prop) => { - return this.renderProperty(prop); - }); - return ( - -
    - {properties} - {this.renderCardinality()} -
    - - ); - } - - /** - * Render cardinality info. - * - * @returns {React.Component} The cardianlity info. - */ - renderCardinality() { - if (this.props.index.cardinality === 'compoun') { - return ( -
    - {this.props.index.cardinality} - -
    - ); - } - } - - /** - * Render the property column - * - * @param {String} prop - The property. - * - * @returns {React.Component} The property component. - */ - renderProperty(prop) { - if (prop === 'ttl') { - return ( -
    - {prop} - -
    - ); - } else if (prop === 'partial') { - return ( -
    - {prop} - -
    - ); - } else { - return ( -
    - {prop} - -
    - ); - } - } - - partialTooltip() { - return format('partialFilterExpression: %j', this.props.index.extra.partialFilterExpression); - } - - ttlTooltip() { - return format('expireAfterSeconds: %d', this.props.index.extra.expireAfterSeconds); - } -} - -PropertyColumn.displayProperty = 'PropertyColumn'; - -module.exports = PropertyColumn; diff --git a/src/internal-packages/indexes/lib/component/size-column.jsx b/src/internal-packages/indexes/lib/component/size-column.jsx deleted file mode 100644 index 44aad6ba7f5..00000000000 --- a/src/internal-packages/indexes/lib/component/size-column.jsx +++ /dev/null @@ -1,44 +0,0 @@ -'use strict'; - -const React = require('react'); - -/** - * Component for the size column. - */ -class SizeColumn extends React.Component { - - /** - * The component constructor. - * - * @param {Object} props - The properties. - */ - constructor(props) { - super(props); - } - - /** - * Render the size column. - * - * @returns {React.Component} The size column. - */ - render() { - return ( - -
    - {this.props.size} -
    -
    - {this.props.unit} -
    -
    -
    -
    -
    - - ); - } -} - -SizeColumn.displaySize = 'SizeColumn'; - -module.exports = SizeColumn; diff --git a/src/internal-packages/indexes/lib/component/type-column.jsx b/src/internal-packages/indexes/lib/component/type-column.jsx deleted file mode 100644 index 60c179e98e5..00000000000 --- a/src/internal-packages/indexes/lib/component/type-column.jsx +++ /dev/null @@ -1,63 +0,0 @@ -'use strict'; - -const _ = require('lodash'); -const React = require('react'); - -/** - * Component for the type column. - */ -class TypeColumn extends React.Component { - - /** - * The component constructor. - * - * @param {Object} props - The properties. - */ - constructor(props) { - super(props); - } - - /** - * Render the type column. - * - * @returns {React.Component} The type column. - */ - render() { - return ( - - {this.renderType()} - - ); - } - - /** - * Render the type div. - */ - renderType() { - if (this.props.type === 'text') { - return ( -
    - {this.props.index.type} - -
    - ); - } - return ( -
    - {this.props.index.type} - -
    - ); - } - - _textTooltip() { - var info = _.pick(this.props.index.extra, ['weights', 'default_language', 'language_override']); - return _.map(info, (v, k) => { - return format('%s: %j', k, v); - }).join('\n'); - } -} - -TypeColumn.displayType = 'TypeColumn'; - -module.exports = TypeColumn; diff --git a/src/internal-packages/indexes/lib/component/usage-column.jsx b/src/internal-packages/indexes/lib/component/usage-column.jsx deleted file mode 100644 index 29bdaa84679..00000000000 --- a/src/internal-packages/indexes/lib/component/usage-column.jsx +++ /dev/null @@ -1,45 +0,0 @@ -'use strict'; - -const React = require('react'); - -/** - * Component for the usage column. - */ -class UsageColumn extends React.Component { - - /** - * The component constructor. - * - * @param {Object} props - The properties. - */ - constructor(props) { - super(props); - } - - /** - * Render the usage column. - * - * @returns {React.Component} The usage column. - */ - render() { - return ( - - -
    - {this.props.usage} -
    -
    - since  - - {this.props.since.toDateString()} - -
    -
    - - ); - } -} - -UsageColumn.displayUsage = 'UsageColumn'; - -module.exports = UsageColumn; diff --git a/src/internal-packages/indexes/lib/store/load-indexes-store.js b/src/internal-packages/indexes/lib/store/load-indexes-store.js deleted file mode 100644 index b9df3706e00..00000000000 --- a/src/internal-packages/indexes/lib/store/load-indexes-store.js +++ /dev/null @@ -1,32 +0,0 @@ -'use strict'; - -const Reflux = require('reflux'); -const app = require('ampersand-app'); -const NamespaceStore = require('hadron-reflux-store').NamespaceStore; -const Action = require('../action/index-actions'); - -/** - * The reflux store for sorting indexes - */ -const LoadIndexesStore = Reflux.createStore({ - - /** - * Initialize the load indexes store. - */ - init: function() { - this.listenTo(Action.loadIndexes, this.loadIndexes); - }, - - /** - * Load the indexes. - */ - loadIndexes: function() { - if (NamespaceStore.ns) { - app.dataService.indexes(NamespaceStore.ns, {}, (err, indexes) => { - this.trigger(indexes); - }); - } - } -}); - -module.exports = LoadIndexesStore; diff --git a/src/internal-packages/indexes/lib/store/sort-indexes-store.js b/src/internal-packages/indexes/lib/store/sort-indexes-store.js deleted file mode 100644 index f7e6b024343..00000000000 --- a/src/internal-packages/indexes/lib/store/sort-indexes-store.js +++ /dev/null @@ -1,28 +0,0 @@ -'use strict'; - -const Reflux = require('reflux'); -const Action = require('../action/index-actions'); - -/** - * The reflux store for sorting indexes - */ -const SortIndexesStore = Reflux.createStore({ - - /** - * Initialize the sort indexes store. - */ - init: function() { - this.listenTo(Action.sortIndexes, this.sortIndexes); - }, - - /** - * Sort the indexes - * - * @param {Array} indexes - The indexes to sort. - */ - sortIndexes: function(indexes) { - this.trigger(indexes); - } -}); - -module.exports = SortIndexesStore; diff --git a/src/internal-packages/indexes/package.json b/src/internal-packages/indexes/package.json deleted file mode 100644 index 0afed9b188f..00000000000 --- a/src/internal-packages/indexes/package.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "name": "indexes", - "productName": "Compass Indexes Support", - "description": "Indexes support for Compass as an internal package.", - "version": "0.0.1", - "authors": "MongoDB Inc.", - "private": true, - "main": "./index.js" -} From 345c493e19ee8c3edcbbad4004c1b1534d2c4539 Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Thu, 18 Aug 2016 21:53:39 +0200 Subject: [PATCH 14/25] INT-1663: Split rendering between tabs on message --- .../query/lib/component/sampling-message.jsx | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/src/internal-packages/query/lib/component/sampling-message.jsx b/src/internal-packages/query/lib/component/sampling-message.jsx index 7976f6c7ce3..877a9f84dd0 100644 --- a/src/internal-packages/query/lib/component/sampling-message.jsx +++ b/src/internal-packages/query/lib/component/sampling-message.jsx @@ -70,24 +70,31 @@ class SamplingMessage extends React.Component { * @returns {React.Component} The document list. */ render() { + if (this.props.insertHandler) { + return this.renderQueryMessage(); + } + return this.renderSamplingMessage(); + } + + renderSamplingMessage() { return (
    Query returned {this.state.count} documents. - {this.renderInsertButton()}
    ); } - renderInsertButton() { - if (this.props.insertHandler) { - return ( + renderQueryMessage() { + return ( +
    + Query returned {this.state.count} documents.  - ); - } +
    + ); } /** From 8e97608ea927e615fd9213b7e332f4fdc55b7478 Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Thu, 18 Aug 2016 22:37:08 +0200 Subject: [PATCH 15/25] INT-1663: Change sampling message based on tab --- .../query/lib/component/sampling-message.jsx | 29 +++++++++++++++++-- .../schema/lib/component/index.jsx | 3 +- 2 files changed, 27 insertions(+), 5 deletions(-) diff --git a/src/internal-packages/query/lib/component/sampling-message.jsx b/src/internal-packages/query/lib/component/sampling-message.jsx index 877a9f84dd0..5eb2871ef1a 100644 --- a/src/internal-packages/query/lib/component/sampling-message.jsx +++ b/src/internal-packages/query/lib/component/sampling-message.jsx @@ -3,6 +3,8 @@ const React = require('react'); const app = require('ampersand-app'); const TextButton = require('hadron-app-registry').TextButton; +const numeral = require('numeral'); +const pluralize = require('pluralize'); /** * Component for the sampling message. @@ -76,19 +78,34 @@ class SamplingMessage extends React.Component { return this.renderSamplingMessage(); } + /** + * If we are on the schema tab, the smapling message is rendered. + * + * @returns {React.Component} The sampling message. + */ renderSamplingMessage() { + var noun = pluralize('document', this.state.count); return (
    - Query returned {this.state.count} documents. + Query returned  + {this.state.count} {noun}. + This report is based on a sample of  + {this.props.sampleSize} {noun} ({this._samplePercentage()}).
    ); } + /** + * If we are on the documents tab, just display the count and insert button. + * + * @returns {React.Component} The count message. + */ renderQueryMessage() { + var noun = pluralize('document', this.state.count); return (
    - Query returned {this.state.count} documents.  + Query returned {this.state.count} {noun}.  { return ; }); return (
    - +
    From 6328a2ce35a84a075e3d711b031a2d93341bed2f Mon Sep 17 00:00:00 2001 From: Thomas Rueckstiess Date: Thu, 18 Aug 2016 21:39:41 +0200 Subject: [PATCH 16/25] fix const/let bug in coordinates.js --- src/internal-packages/schema/lib/d3/coordinates.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/internal-packages/schema/lib/d3/coordinates.js b/src/internal-packages/schema/lib/d3/coordinates.js index 9a1ec54d1e1..4d99fdba0ca 100644 --- a/src/internal-packages/schema/lib/d3/coordinates.js +++ b/src/internal-packages/schema/lib/d3/coordinates.js @@ -40,7 +40,7 @@ const minicharts_d3fns_geo = function() { const margin = shared.margin; function CircleSelector(container) { - const dragging = false; // track whether we are dragging + let dragging = false; // track whether we are dragging // we expose events on our component const dispatch = d3.dispatch('update', 'clear'); From 3d675bc08dd02a810ef535ad50b53c6609f035ea Mon Sep 17 00:00:00 2001 From: Thomas Rueckstiess Date: Thu, 18 Aug 2016 22:01:28 +0200 Subject: [PATCH 17/25] fix single vs. double quotes linting issue --- src/internal-packages/schema/lib/component/index.jsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/internal-packages/schema/lib/component/index.jsx b/src/internal-packages/schema/lib/component/index.jsx index 00f5b5aa540..a3facd6e5c2 100644 --- a/src/internal-packages/schema/lib/component/index.jsx +++ b/src/internal-packages/schema/lib/component/index.jsx @@ -85,8 +85,8 @@ const Schema = React.createClass({ return (
    -
    -
    +
    +
    {fieldList}
    From 8598917ca97391959360f4a661dfae1201a6f118 Mon Sep 17 00:00:00 2001 From: Thomas Rueckstiess Date: Thu, 18 Aug 2016 23:00:56 +0200 Subject: [PATCH 18/25] =?UTF-8?q?adding=20=E2=80=9Cminicharts=E2=80=9D=20f?= =?UTF-8?q?or=20document=20and=20array.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../schema/lib/component/array.jsx | 46 +++++++++++++++++++ .../schema/lib/component/document.jsx | 33 +++++++++++++ .../schema/lib/component/field.jsx | 4 +- .../schema/lib/component/minichart.jsx | 22 ++++++--- 4 files changed, 98 insertions(+), 7 deletions(-) create mode 100644 src/internal-packages/schema/lib/component/array.jsx create mode 100644 src/internal-packages/schema/lib/component/document.jsx diff --git a/src/internal-packages/schema/lib/component/array.jsx b/src/internal-packages/schema/lib/component/array.jsx new file mode 100644 index 00000000000..d928bf1326d --- /dev/null +++ b/src/internal-packages/schema/lib/component/array.jsx @@ -0,0 +1,46 @@ +const React = require('react'); +const _ = require('lodash'); +const pluralize = require('pluralize'); +const numeral = require('numeral'); + +// const debug = require('debug')('mongodb-compass:schema:array'); + +const ArrayMinichart = React.createClass({ + + propTypes: { + type: React.PropTypes.object.isRequired, + nestedDocType: React.PropTypes.object + }, + + render() { + let arrayOfFieldsMessage = ''; + if (this.props.nestedDocType) { + const numFields = this.props.nestedDocType.fields.length; + const nestedFields = pluralize('nested field', numFields, true); + arrayOfFieldsMessage = `Array of documents with ${nestedFields}.`; + } + + const minLength = _.min(this.props.type.lengths); + const averageLength = numeral(this.props.type.average_length).format('0.0[0]'); + const maxLength = _.max(this.props.type.lengths); + + return ( +
    +
    +
    {arrayOfFieldsMessage}
    +
    +
    Array lengths
    +
    +
      +
    • min: {minLength}
    • +
    • average: {averageLength}
    • +
    • max: {maxLength}
    • +
    +
    +
    +
    + ); + } +}); + +module.exports = ArrayMinichart; diff --git a/src/internal-packages/schema/lib/component/document.jsx b/src/internal-packages/schema/lib/component/document.jsx new file mode 100644 index 00000000000..fe1477483e6 --- /dev/null +++ b/src/internal-packages/schema/lib/component/document.jsx @@ -0,0 +1,33 @@ +const React = require('react'); +const pluralize = require('pluralize'); + +const debug = require('debug')('mongodb-compass:schema:array'); + +const DocumentMinichart = React.createClass({ + + propTypes: { + nestedDocType: React.PropTypes.object + }, + + render() { + debug('props', this.props); + + let docFieldsMessage = ''; + if (this.props.nestedDocType) { + const numFields = this.props.nestedDocType.fields.length; + const nestedFields = pluralize('nested field', numFields, true); + docFieldsMessage = `Document with ${nestedFields}.`; + } + + return ( +
    +
    +
    {docFieldsMessage}
    +
    +
    +
    + ); + } +}); + +module.exports = DocumentMinichart; diff --git a/src/internal-packages/schema/lib/component/field.jsx b/src/internal-packages/schema/lib/component/field.jsx index e70c560c9fa..13f55a4b5f9 100644 --- a/src/internal-packages/schema/lib/component/field.jsx +++ b/src/internal-packages/schema/lib/component/field.jsx @@ -161,6 +161,7 @@ const Field = React.createClass({ }); const activeType = this.state.activeType; + const nestedDocType = this.getNestedDocType(); // children fields in case of nested array / document return ( @@ -168,7 +169,7 @@ const Field = React.createClass({
    - + {this.props.name}
    @@ -179,6 +180,7 @@ const Field = React.createClass({
    diff --git a/src/internal-packages/schema/lib/component/minichart.jsx b/src/internal-packages/schema/lib/component/minichart.jsx index a40ec8cabc0..190df78b86e 100644 --- a/src/internal-packages/schema/lib/component/minichart.jsx +++ b/src/internal-packages/schema/lib/component/minichart.jsx @@ -2,8 +2,8 @@ const app = require('ampersand-app'); const React = require('react'); const UniqueMinichart = require('./unique'); const _ = require('lodash'); -// const DocumentMinichart = require('./document'); -// const ArrayMinichart = require('./array'); +const DocumentMinichart = require('./document'); +const ArrayMinichart = require('./array'); const D3Component = require('./d3component'); const vizFns = require('../d3'); @@ -13,7 +13,8 @@ const Minichart = React.createClass({ propTypes: { fieldName: React.PropTypes.string.isRequired, - type: React.PropTypes.object.isRequired + type: React.PropTypes.object.isRequired, + nestedDocType: React.PropTypes.object }, getInitialState() { @@ -90,16 +91,25 @@ const Minichart = React.createClass({ ); } if (typeName === 'Document') { - return
    Document Placeholder Minichart
    ; + return ( + + ); } if (typeName === 'Array') { - return
    Array Placeholder Minichart
    ; + return ( + + ); } if (typeName === 'Undefined') { return
    Undefined
    ; } if (!fn) { - return
    Unknown Type
    ; + return null; } return ( Date: Thu, 18 Aug 2016 23:02:50 +0200 Subject: [PATCH 19/25] bump eslint-config-mongodb-js@2.0.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 08d8867fa50..cbea1a8f7e0 100644 --- a/package.json +++ b/package.json @@ -156,7 +156,7 @@ "chai": "^3.4.1", "chai-as-promised": "^5.1.0", "electron-prebuilt": "1.2.8", - "eslint-config-mongodb-js": "^1.0.6", + "eslint-config-mongodb-js": "^2.0.1", "eslint-plugin-react": "^4.1.0", "hadron-build": "^0.7.2", "mocha": "^2.3.4", From dcbd81c324c231e1ec565ee66835722d296a5763 Mon Sep 17 00:00:00 2001 From: Thomas Rueckstiess Date: Thu, 18 Aug 2016 23:13:14 +0200 Subject: [PATCH 20/25] fix edge case for empty subdocs with 0 fields. --- src/internal-packages/schema/lib/component/array.jsx | 2 +- src/internal-packages/schema/lib/component/document.jsx | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/internal-packages/schema/lib/component/array.jsx b/src/internal-packages/schema/lib/component/array.jsx index d928bf1326d..4b4fd70420b 100644 --- a/src/internal-packages/schema/lib/component/array.jsx +++ b/src/internal-packages/schema/lib/component/array.jsx @@ -15,7 +15,7 @@ const ArrayMinichart = React.createClass({ render() { let arrayOfFieldsMessage = ''; if (this.props.nestedDocType) { - const numFields = this.props.nestedDocType.fields.length; + const numFields = _.get(this.props.nestedDocType.fields, 'length', 0); const nestedFields = pluralize('nested field', numFields, true); arrayOfFieldsMessage = `Array of documents with ${nestedFields}.`; } diff --git a/src/internal-packages/schema/lib/component/document.jsx b/src/internal-packages/schema/lib/component/document.jsx index fe1477483e6..049128b95ee 100644 --- a/src/internal-packages/schema/lib/component/document.jsx +++ b/src/internal-packages/schema/lib/component/document.jsx @@ -1,5 +1,6 @@ const React = require('react'); const pluralize = require('pluralize'); +const _ = require('lodash'); const debug = require('debug')('mongodb-compass:schema:array'); @@ -10,11 +11,9 @@ const DocumentMinichart = React.createClass({ }, render() { - debug('props', this.props); - let docFieldsMessage = ''; if (this.props.nestedDocType) { - const numFields = this.props.nestedDocType.fields.length; + const numFields = _.get(this.props.nestedDocType.fields, 'length', 0); const nestedFields = pluralize('nested field', numFields, true); docFieldsMessage = `Document with ${nestedFields}.`; } From 5b8438fac5c41f52f3eb9cf48400369e2e0dfb15 Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Thu, 18 Aug 2016 23:18:49 +0200 Subject: [PATCH 21/25] INT-1663: Sample didn't error with 0 sampled docs --- src/internal-packages/schema/lib/store/index.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/internal-packages/schema/lib/store/index.jsx b/src/internal-packages/schema/lib/store/index.jsx index bdd6355c04a..2a4d6bbb491 100644 --- a/src/internal-packages/schema/lib/store/index.jsx +++ b/src/internal-packages/schema/lib/store/index.jsx @@ -182,7 +182,7 @@ const SchemaStore = Reflux.createStore({ onError(analysisErr); }) .on('end', () => { - if (sampleCount > 0 && this.state.samplingState !== 'error') { + if ((numSamples === 0 || sampleCount > 0) && this.state.samplingState !== 'error') { onSuccess(schema.serialize()); } else { return onError(); From 78f6bce2b6ad26fa1c4ddc27d7607b93802419bb Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Mon, 22 Aug 2016 13:45:23 +0200 Subject: [PATCH 22/25] INT-1663: Ensure document list re-renders on collection change --- src/app/home/collection.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/app/home/collection.js b/src/app/home/collection.js index f842094569a..7dfeb2a2e71 100644 --- a/src/app/home/collection.js +++ b/src/app/home/collection.js @@ -1,4 +1,5 @@ var View = require('ampersand-view'); +var Action = require('hadron-action'); var CollectionStatsView = require('../collection-stats'); var DocumentView = require('../documents'); var SchemaView = require('../schema'); @@ -174,6 +175,7 @@ var MongoDBCollectionView = View.extend({ this.model._id = this.ns; this.model.once('sync', this.onCollectionFetched.bind(this)); this.model.fetch(); + Action.filterChanged(app.queryOptions.query.serialize()); }, onCollectionFetched: function(model) { this.switchView(this.activeView); From 2dbc77a2bff32167d155d14e32ed9ad897805838 Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Mon, 22 Aug 2016 14:15:53 +0200 Subject: [PATCH 23/25] INT-1663: Show number of loaded documents in sample bar --- src/internal-packages/crud/index.js | 3 ++ .../query/lib/component/sampling-message.jsx | 28 +++++++++++++++++-- 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/src/internal-packages/crud/index.js b/src/internal-packages/crud/index.js index d5a1cbd6984..b6ae3a28a91 100644 --- a/src/internal-packages/crud/index.js +++ b/src/internal-packages/crud/index.js @@ -5,6 +5,7 @@ const DocumentList = require('./lib/component/document-list'); const Actions = require('./lib/actions'); const InsertDocumentStore = require('./lib/store/insert-document-store'); const ResetDocumentListStore = require('./lib/store/reset-document-list-store'); +const LoadMoreDocumentsStore = require('./lib/store/load-more-documents-store'); /** * Activate all the components in the CRUD package. @@ -14,6 +15,7 @@ function activate() { app.appRegistry.registerAction('Action::CRUD::DocumentRemoved', Actions.documentRemoved); app.appRegistry.registerStore('Store::CRUD::InsertDocumentStore', InsertDocumentStore); app.appRegistry.registerStore('Store::CRUD::ResetDocumentListStore', ResetDocumentListStore); + app.appRegistry.registerStore('Store::CRUD::LoadMoreDocumentsStore', LoadMoreDocumentsStore); } /** @@ -24,6 +26,7 @@ function deactivate() { app.appRegistry.deregisterAction('Action::CRUD::DocumentRemoved'); app.appRegistry.deregisterStore('Store::CRUD::InsertDocumentStore'); app.appRegistry.deregisterStore('Store::CRUD::ResetDocumentListStore'); + app.appRegistry.deregisterStore('Store::CRUD::LoadMoreDocumentsStore'); } module.exports.activate = activate; diff --git a/src/internal-packages/query/lib/component/sampling-message.jsx b/src/internal-packages/query/lib/component/sampling-message.jsx index 5eb2871ef1a..d45e0d01234 100644 --- a/src/internal-packages/query/lib/component/sampling-message.jsx +++ b/src/internal-packages/query/lib/component/sampling-message.jsx @@ -18,6 +18,7 @@ class SamplingMessage extends React.Component { this.unsubscribeReset = this.resetDocumentListStore.listen(this.handleReset.bind(this)); this.unsubscribeInsert = this.insertDocumentStore.listen(this.handleInsert.bind(this)); this.unsubscribeRemove = this.documentRemovedAction.listen(this.handleRemove.bind(this)); + this.unsubscribeLoadMore = this.loadMoreDocumentsStore.listen(this.handleLoadMore.bind(this)); } /** @@ -27,6 +28,7 @@ class SamplingMessage extends React.Component { this.unsubscribeReset(); this.unsubscribeInsert(); this.unsubscribeRemove(); + this.unsubscribeLoadMore(); } /** @@ -36,10 +38,11 @@ class SamplingMessage extends React.Component { */ constructor(props) { super(props); - this.state = { count: 0 }; + this.state = { count: 0, loaded: 20 }; this.resetDocumentListStore = app.appRegistry.getStore('Store::CRUD::ResetDocumentListStore'); this.insertDocumentStore = app.appRegistry.getStore('Store::CRUD::InsertDocumentStore'); this.documentRemovedAction = app.appRegistry.getAction('Action::CRUD::DocumentRemoved'); + this.loadMoreDocumentsStore = app.appRegistry.getStore('Store::CRUD::LoadMoreDocumentsStore'); } /** @@ -63,7 +66,16 @@ class SamplingMessage extends React.Component { * @param {Integer} count - The count. */ handleReset(documents, count) { - this.setState({ count: count }); + this.setState({ count: count, loaded: 20 }); + } + + /** + * Handle scrolling that loads more documents. + * + * @param {Array} documents - The loaded documents. + */ + handleLoadMore(documents) { + this.setState({ loaded: this.state.loaded + documents.length }); } /** @@ -106,6 +118,7 @@ class SamplingMessage extends React.Component { return (
    Query returned {this.state.count} {noun}.  + {this._loadedMessage()} 20) { + return ( + + Displaying documents 1-{this.state.loaded}  + + ); + } + } + _samplePercentage() { var percent = (this.state.count === 0) ? 0 : this.props.sampleSize / this.state.count; return numeral(percent).format('0.00%'); From 233359dfec030cef1fb9675ba4a22f8ff6069ada Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Mon, 22 Aug 2016 14:18:19 +0200 Subject: [PATCH 24/25] INT-1663: Update react-bootstrap --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index cbea1a8f7e0..40bee772cb5 100644 --- a/package.json +++ b/package.json @@ -137,7 +137,7 @@ "qs": "^5.2.0", "raf": "^3.1.0", "react": "^15.2.1", - "react-bootstrap": "0.29.5", + "react-bootstrap": "0.30.2", "react-dom": "^15.2.1", "react-native-listener": "^1.0.1", "react-tooltip": "^2.0.3", From 59e267697a14c7e7bf697f2b3ca8548fae1da5bd Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Mon, 22 Aug 2016 17:13:16 +0200 Subject: [PATCH 25/25] INT-1663: Resize minicharts on schema tab click --- src/app/home/collection.js | 5 +++++ src/internal-packages/schema/lib/action/index.jsx | 6 +++++- src/internal-packages/schema/lib/component/minichart.jsx | 4 ++++ 3 files changed, 14 insertions(+), 1 deletion(-) diff --git a/src/app/home/collection.js b/src/app/home/collection.js index 7dfeb2a2e71..6f672ed7599 100644 --- a/src/app/home/collection.js +++ b/src/app/home/collection.js @@ -138,6 +138,7 @@ var MongoDBCollectionView = View.extend({ initialize: function() { this.model = new MongoDBCollection(); NamespaceStore.listen( this.onCollectionChanged.bind(this) ); + this.schemaActions = app.appRegistry.getAction('SchemaAction'); // this.listenToAndRun(this.parent, 'change:ns', this.onCollectionChanged.bind(this)); }, render: function() { @@ -163,6 +164,10 @@ var MongoDBCollectionView = View.extend({ this[subview].el.classList.add('hidden'); } }.bind(this)); + // Temporary hack to generate a resize when the schema is clicked. + if (viewStr === 'schemaView') { + this.schemaActions.resizeMiniCharts(); + } }, onCollectionChanged: function() { this.ns = NamespaceStore.ns; diff --git a/src/internal-packages/schema/lib/action/index.jsx b/src/internal-packages/schema/lib/action/index.jsx index 369fd11a9f4..8094f4d68f6 100644 --- a/src/internal-packages/schema/lib/action/index.jsx +++ b/src/internal-packages/schema/lib/action/index.jsx @@ -16,7 +16,11 @@ const SchemaAction = Reflux.createActions({ /** * reset maxTimeMS value to default */ - resetMaxTimeMS: {sync: true} + resetMaxTimeMS: {sync: true}, + /** + * Resize the minicharts. + */ + resizeMiniCharts: {sync: true} }); module.exports = SchemaAction; diff --git a/src/internal-packages/schema/lib/component/minichart.jsx b/src/internal-packages/schema/lib/component/minichart.jsx index 190df78b86e..0618794592d 100644 --- a/src/internal-packages/schema/lib/component/minichart.jsx +++ b/src/internal-packages/schema/lib/component/minichart.jsx @@ -6,6 +6,7 @@ const DocumentMinichart = require('./document'); const ArrayMinichart = require('./array'); const D3Component = require('./d3component'); const vizFns = require('../d3'); +const Actions = require('../action'); // const debug = require('debug')('mongodb-compass:schema:minichart'); @@ -43,11 +44,14 @@ const Minichart = React.createClass({ query: store.query }); }); + + this.unsubscribeMiniChartResize = Actions.resizeMiniCharts.listen(this.handleResize); }, componentWillUnmount() { window.removeEventListener('resize', this.handleResize); this.unsubscribeQueryStore(); + this.unsubscribeMiniChartResize(); }, handleResize() {