diff --git a/src/kibana/apps/discover/controllers/discover.js b/src/kibana/apps/discover/controllers/discover.js index 4fa334dfd98b..28565f89c214 100644 --- a/src/kibana/apps/discover/controllers/discover.js +++ b/src/kibana/apps/discover/controllers/discover.js @@ -37,7 +37,9 @@ define(function (require) { }); - app.controller('discover', function ($scope, config, courier, $route, savedSearches, Notifier, $location, AppState, timefilter) { + app.controller('discover', function ($scope, config, courier, $route, savedSearches, + Notifier, $location, globalState, AppState, timefilter) { + var notify = new Notifier({ location: 'Discover' }); @@ -90,33 +92,23 @@ define(function (require) { $scope.time = timefilter.time; // TODO: Switch this to watching time.string when we implement it - $scope.$watchCollection('time', function () { - updateDataSource(); - $scope.fetch(); - }); + $scope.$watchCollection('time', _.bindKey($scope, 'fetch')); // stores the complete list of fields $scope.fields = null; var init = _.once(function () { return setFields() - .then(updateDataSource) .then(function () { - // the index to use when they don't specify one - $scope.$on('change:config.defaultIndex', function (event, val) { - if (!$state.index) $state.index = val; - }); + updateDataSource(); - // changes to state.columns don't require a refresh - var ignore = ['columns']; + // state fields that shouldn't trigger a fetch when changed + var ignoreStateChanges = ['columns']; // listen for changes, and relisten everytime something happens $state.onUpdate(function (changed) { // if we only have ignorable changes, do nothing - if (_.difference(changed, ignore).length) { - updateDataSource(); - courier.fetch(); - } + if (_.difference(changed, ignoreStateChanges).length) $scope.fetch(); }); $scope.$watch('state.sort', function (sort) { @@ -166,18 +158,20 @@ define(function (require) { }); $scope.opts.saveDataSource = function () { + updateDataSource(); savedSearch.id = savedSearch.title; savedSearch.save() .then(function () { notify.info('Saved Data Source "' + savedSearch.title + '"'); if (savedSearch.id !== $route.current.params.id) { - $location.url('/discover/' + savedSearch.id); + $location.url(globalState.writeToUrl('/discover/' + savedSearch.id)); } }, notify.error); }; $scope.fetch = function () { + updateDataSource(); $state.commit(); courier.fetch(); }; @@ -241,19 +235,22 @@ define(function (require) { if (!!$scope.opts.timefield) { timefilter.enabled(true); + chartOptions = interval.calculate(timefilter.time.from, timefilter.time.to, 100); + var bounds = timefilter.getBounds(); searchSource .aggs({ events: { date_histogram: { - field: $scope.opts.timefield, interval: chartOptions.interval + 'ms', format: chartOptions.format, min_doc_count: 0, + + field: $scope.opts.timefield, extended_bounds: { - min: datemath.parse(timefilter.time.from).valueOf(), - max: datemath.parse(timefilter.time.to, true).valueOf() + min: bounds.min, + max: bounds.max } } } @@ -362,7 +359,7 @@ define(function (require) { return; } - $state.commit(); + $scope.fetch(); } // TODO: Move to utility class diff --git a/src/kibana/apps/visualize/controllers/editor.js b/src/kibana/apps/visualize/controllers/editor.js index aadb639fae82..130eb2384c72 100644 --- a/src/kibana/apps/visualize/controllers/editor.js +++ b/src/kibana/apps/visualize/controllers/editor.js @@ -110,7 +110,7 @@ define(function (require) { var writeStateAndFetch = function () { updateDataSource(); _.assign($state, vis.getState()); - var changes = $state.commit(); + $state.commit(); vis.searchSource.fetch(); }; diff --git a/src/kibana/components/state_management/_state_sync.js b/src/kibana/components/state_management/_state_sync.js new file mode 100644 index 000000000000..62955b5f06db --- /dev/null +++ b/src/kibana/components/state_management/_state_sync.js @@ -0,0 +1,139 @@ +define(function (require) { + + var angular = require('angular'); + var _ = require('lodash'); + + // invokable/private angular dep + return function ($location) { + // feed in some of the private state from globalState + return function (globalState, updateListeners, app) { + var getAppStash = function (search) { + var appStash = search._a && rison.decode(search._a); + if (app.current) { + appStash = _.defaults(appStash || {}, app.defaults); + } + return appStash; + }; + + var diffTrans = function (trans) { + var obj = trans[0]; + var update = trans[1]; + + var diff = {}; + + // the keys that are currently set on obj, excluding methods + var objKeys = Object.keys(obj).filter(function (key) { + return typeof obj[key] !== 'function'; + }); + + if (update) { + // the keys obj should have after applying the update + var updateKeys = diff.keys = Object.keys(update).filter(function (key) { + return typeof update[key] !== 'function'; + }); + + // the keys that will be removed + diff.remove = _.difference(objKeys, updateKeys); + + // list of keys that will be added or changed + diff.change = updateKeys.filter(function (key) { + return !angular.equals(obj[key], update[key]); + }); + } else { + diff.keys = objKeys.slice(0); + diff.remove = []; + diff.change = []; + } + + // single list of all keys that are effected + diff.all = [].concat(diff.remove, diff.change); + + return diff; + }; + + var notify = function (trans, diff) { + var listeners = null; + + if (trans[0] === app.current) { + listeners = app.listeners; + } else if (trans[0] === globalState) { + listeners = updateListeners; + } + + listeners && listeners.splice(0).forEach(function (defer) { + defer.resolve(diff.all.slice(0)); + }); + }; + + var applyDiff = function (trans, diff) { + if (!diff.all.length) return; + + var obj = trans[0]; + var update = trans[1]; + + diff.remove.forEach(function (key) { + delete obj[key]; + }); + + diff.change.forEach(function (key) { + obj[key] = update[key]; + }); + }; + + var syncTrans = function (trans, forceNotify) { + // obj that will be modified by update(trans[1]) + // if it is empty, we can skip it all + var skipWrite = !trans[0]; + trans[0] = trans[0] || {}; + + var diff = diffTrans(trans); + if (!skipWrite && (forceNotify || diff.all.length)) { + applyDiff(trans, diff); + notify(trans, diff); + } + return diff; + }; + + return { + // sync by pushing to the url + push: function (forceNotify) { + var search = $location.search(); + + var appStash = getAppStash(search) || {}; + var globalStash = search._g ? rison.decode(search._g) : {}; + + var res = _.mapValues({ + app: [appStash, app.current], + global: [globalStash, globalState] + }, function (trans, key) { + var diff = syncTrans(trans, forceNotify); + var urlKey = '_' + key.charAt(0); + if (diff.keys.length === 0) { + delete search[urlKey]; + } else { + search[urlKey] = rison.encode(trans[0]); + } + return diff; + }); + + $location.search(search); + return res; + }, + // sync by pulling from the url + pull: function (forceNotify) { + var search = $location.search(); + + var appStash = getAppStash(search); + var globalStash = search._g && rison.decode(search._g); + + return _.mapValues({ + app: [app.current, appStash], + global: [globalState, globalStash] + }, function (trans) { + return syncTrans(trans, forceNotify); + }); + } + }; + }; + }; +}); \ No newline at end of file diff --git a/src/kibana/components/state_management/app_state.js b/src/kibana/components/state_management/app_state.js index 993c28d8ebe7..e6e8329cc9e9 100644 --- a/src/kibana/components/state_management/app_state.js +++ b/src/kibana/components/state_management/app_state.js @@ -9,8 +9,7 @@ define(function (require) { module.factory('AppState', function (globalState, $route, $location, Promise) { function AppState(defaults) { - _.assign(this, defaults); - globalState._setApp(this); + globalState._setApp(this, defaults); this.onUpdate = function (handler) { return globalState.onAppUpdate(handler); diff --git a/src/kibana/components/state_management/global_state.js b/src/kibana/components/state_management/global_state.js index a409bbf235ad..3dc3a40f8b67 100644 --- a/src/kibana/components/state_management/global_state.js +++ b/src/kibana/components/state_management/global_state.js @@ -1,14 +1,18 @@ define(function (require) { var _ = require('lodash'); var angular = require('angular'); + var qs = require('utils/query_string'); + var module = require('modules').get('kibana/global_state'); - module.service('globalState', function ($rootScope, $location, $route, $injector, Promise) { + module.service('globalState', function ($rootScope, $route, $injector, Promise) { var globalState = this; - // store the current app and it's metadata here - var app, appMeta; + var setupSync = $injector.invoke(require('./_state_sync')); + + // store app related stuff in here + var app = {}; // resolve all of these when a global update is detected coming in from the url var updateListeners = []; @@ -16,147 +20,29 @@ define(function (require) { // resolve all of these when ANY global update is detected coming in from the url var anyUpdateListeners = []; - globalState._setApp = function (newApp) { - app = newApp; - appMeta = { - name: $route.current.$$route.originalPath, - listeners: [] - }; + globalState._setApp = function (newAppState, defaults) { + app.current = newAppState; + app.defaults = defaults; + app.name = $route.current.$$route.originalPath; + app.listeners = []; sync.pull(); }; - var sync = (function () { - var diffTrans = function (trans) { - var obj = trans[0]; - var update = trans[1]; - - var diff = {}; - - // the keys that are currently set on obj, excluding methods - var objKeys = Object.keys(obj).filter(function (key) { - return typeof obj[key] !== 'function'; - }); - - if (update) { - // the keys obj should have after applying the update - var updateKeys = diff.keys = Object.keys(update).filter(function (key) { - return typeof update[key] !== 'function'; - }); - - // the keys that will be removed - diff.remove = _.difference(objKeys, updateKeys); - - // list of keys that will be added or changed - diff.change = updateKeys.filter(function (key) { - return !angular.equals(obj[key], update[key]); - }); - } else { - diff.keys = objKeys.slice(0); - diff.remove = []; - diff.change = []; - } - - // single list of all keys that are effected - diff.all = [].concat(diff.remove, diff.change); - - return diff; - }; - - var notify = function (trans, diff) { - var listeners = null; - - if (trans[0] === app) { - listeners = appMeta.listeners; - } else if (trans[0] === globalState) { - listeners = updateListeners; - } else if (trans[1] === globalState) { - // if the update is coming from the globalState, only onAnyUpdate listeners will be notified - listeners = anyUpdateListeners; - } - - listeners && listeners.splice(0).forEach(function (defer) { - defer.resolve(diff.all.slice(0)); - }); - }; - - var applyDiff = function (trans, diff) { - if (!diff.all.length) return; - - var obj = trans[0]; - var update = trans[1]; - - diff.remove.forEach(function (key) { - delete obj[key]; - }); - - diff.change.forEach(function (key) { - obj[key] = update[key]; - }); - }; - - var syncTrans = function (trans, forceNotify) { - trans[0] = trans[0] || {}; // obj that will be modified by update(trans[1]) - - var diff = diffTrans(trans); - if (forceNotify || diff.all.length) { - applyDiff(trans, diff); - notify(trans, diff); - } - return diff; - }; - - return { - // sync by pushing to the url - push: function (forceNotify) { - var qs = $location.search(); - - var res = _.mapValues({ - app: [ - qs._a ? rison.decode(qs._a) : {}, - app - ], - global: [ - qs._g ? rison.decode(qs._g) : {}, - globalState - ] - }, function (trans, key) { - var diff = syncTrans(trans, forceNotify); - var urlKey = '_' + key.charAt(0); - if (diff.keys.length === 0) { - delete qs[urlKey]; - } else { - qs[urlKey] = rison.encode(trans[0]); - } - return diff; - }); - - $location.search(qs); - return res; - }, - // sync by pulling from the url - pull: function (forceNotify) { - var qs = $location.search(); - - var appStash = qs._a && rison.decode(qs._a); - var globalStash = qs._g && rison.decode(qs._g); - - return _.mapValues({ - app: [app, appStash], - global: [globalState, globalStash] - }, function (trans) { - return syncTrans(trans, forceNotify); - }); - } - }; - }()); - - var unwatch = [ - // force the event arg out of the way \/ - $rootScope.$on('$locationChangeSuccess', sync.pull.bind(null, null)), - // force the event arg out of the way \/ - $rootScope.$on('$locationUpdate', sync.pull.bind(null, null)) - ]; + globalState.writeToUrl = function (url) { + return qs.replaceParamInUrl(url, '_g', rison.encode(globalState)); + }; + + // exposes sync.pull and sync.push + var sync = setupSync(globalState, updateListeners, app); + + $rootScope.$on('$locationChangeSuccess', function () { + sync.pull(); + }); + + $rootScope.$on('$locationUpdate', function () { + sync.pull(); + }); globalState.onUpdate = function (handler) { return new Promise.emitter(function (resolve, reject, defer) { @@ -164,15 +50,9 @@ define(function (require) { }, handler); }; - globalState.onAnyUpdate = function (handler) { - return new Promise.emitter(function (resolve, reject, defer) { - anyUpdateListeners.push(defer); - }, handler); - }; - globalState.onAppUpdate = function (handler) { return new Promise.emitter(function (resolve, reject, defer) { - appMeta.listeners.push(defer); + app.listeners.push(defer); }, handler); }; @@ -183,7 +63,7 @@ define(function (require) { return sync.push(true); }; - // set the on globalState + // pull in the default globalState sync.pull(); }); diff --git a/src/kibana/controllers/kibana.js b/src/kibana/controllers/kibana.js index d76063be2035..7294968ed4b8 100644 --- a/src/kibana/controllers/kibana.js +++ b/src/kibana/controllers/kibana.js @@ -96,14 +96,7 @@ define(function (require) { var url = lastPathFor(app); if (!url || url === currentUrl) return; - var loc = qs.findInUrl(url); - var parsed = qs.decode(url.substring(loc.start + 1, loc.end)); - parsed._g = _g; - - var chars = url.split(''); - chars.splice(loc.start, loc.end - loc.start, '?' + qs.encode(parsed)); - - lastPathFor(app, chars.join('')); + lastPathFor(app, qs.replaceParamInUrl(url, '_g', _g)); }); }; @@ -115,10 +108,6 @@ define(function (require) { writeGlobalStateToLastPaths(); }); - globalState.onUpdate(function readFromGlobalState() { - _.assign(timefilter.time, globalState.time); - }); - $scope.$on('application.load', function () { courier.start(); }); diff --git a/src/kibana/services/timefilter.js b/src/kibana/services/timefilter.js index 9e10236f9629..b93ef75373e0 100644 --- a/src/kibana/services/timefilter.js +++ b/src/kibana/services/timefilter.js @@ -17,26 +17,39 @@ define(function (require) { to: 'now' }); + globalState.onUpdate(function readFromGlobalState() { + _.assign(self.time, globalState.time); + }); + this.enabled = function (state) { if (!_.isUndefined(state)) enable = state; return enable; }; - this.get = function (indexPattern) { + this.get = function (fieldHash) { var timefield, filter; // TODO: time field should be stored in the pattern meta data. For now we just use the first date field we find - timefield = _.findKey(indexPattern, {type: 'date'}); + timefield = _.findKey(fieldHash, {type: 'date'}); + var bounds = this.getBounds(); + if (!!timefield) { filter = {range : {}}; filter.range[timefield] = { - gte: datemath.parse(self.time.from).valueOf(), - lte: datemath.parse(self.time.to, true).valueOf() + gte: bounds.min, + lte: bounds.max }; } return filter; }; + this.getBounds = function (timefield) { + return { + min: datemath.parse(self.time.from).valueOf(), + max: datemath.parse(self.time.to, true).valueOf() + }; + }; + }); }); \ No newline at end of file diff --git a/src/kibana/utils/query_string.js b/src/kibana/utils/query_string.js index 036c5910fee7..05b36b5513cf 100644 --- a/src/kibana/utils/query_string.js +++ b/src/kibana/utils/query_string.js @@ -108,5 +108,15 @@ define(function (require) { }; }; + qs.replaceParamInUrl = function (url, param, newVal) { + var loc = qs.findInUrl(url); + var parsed = qs.decode(url.substring(loc.start + 1, loc.end)); + parsed[param] = newVal; + + var chars = url.split(''); + chars.splice(loc.start, loc.end - loc.start, '?' + qs.encode(parsed)); + return chars.join(''); + }; + return qs; }); \ No newline at end of file