Merge pull request #4582 from spalger/expose/routingSetupApi

Expose/routing setup api
This commit is contained in:
Chris Cowan 2015-08-05 15:46:13 -07:00
commit 0a151a4b2b
11 changed files with 192 additions and 121 deletions

View file

@ -1,6 +1,7 @@
define(function (require, module, exports) {
var _ = require('lodash');
var sections = require('plugins/kibana/settings/sections/index');
require('plugins/kibana/settings/styles/main.less');
require('ui/filters/start_from');
@ -9,7 +10,10 @@ define(function (require, module, exports) {
redirectTo: '/settings/indices'
});
var sections = require('plugins/kibana/settings/sections/index');
require('ui/index_patterns/routeSetup/loadDefault')({
notRequiredRe: /^\/settings\//,
whenMissingRedirectTo: '/settings/indices'
});
require('ui/modules')
.get('apps/settings')

View file

@ -3,6 +3,10 @@ define(function (require) {
'kibana/notify'
]);
require('ui/routes').addSetupWork(function (config) {
return config.init();
});
// service for delivering config variables to everywhere else
module.service('config', function (Private, Notifier, kbnVersion, kbnIndex, $rootScope, buildNum) {
var config = this;

View file

@ -1,5 +1,5 @@
define(function (require) {
return function RootSearchSource(Private, $rootScope, config, Promise, indexPatterns, timefilter, Notifier) {
return function RootSearchSource(Private, $rootScope, timefilter, Notifier) {
var SearchSource = Private(require('ui/courier/data_source/search_source'));
var notify = new Notifier({ location: 'Root Search Source' });
@ -14,24 +14,6 @@ define(function (require) {
var appSource; // set in setAppSource()
resetAppSource();
/**
* Get the default index from the config, and hook it up to the globalSource.
*
* @return {Promise}
*/
function loadDefaultPattern() {
return notify.event('loading default index pattern', function () {
var defId = config.get('defaultIndex');
return Promise.cast(defId && indexPatterns.get(defId))
.then(function (pattern) {
pattern = pattern || undefined;
globalSource.set('index', pattern);
notify.log('index pattern set to', defId);
});
});
}
// when the route changes, clear the appSource
$rootScope.$on('$routeChangeStart', resetAppSource);
@ -73,7 +55,10 @@ define(function (require) {
return {
get: getAppSource,
set: setAppSource,
loadDefault: loadDefaultPattern
getGlobalSource: function () {
return globalSource;
}
};
};
});

View file

@ -13,8 +13,6 @@ define(function (require) {
SearchSource.Super.call(this, initialState, searchStrategy);
}
// expose a ready state for the route setup to read
/*****
* PUBLIC API
*****/

View file

@ -0,0 +1,68 @@
let _ = require('lodash');
let { NoDefaultIndexPattern, NoDefinedIndexPatterns } = require('ui/errors');
let Notifier = require('ui/notify/Notifier');
let notify = new Notifier({
location: 'Index Patterns'
});
module.exports = function (opts) {
opts = opts || {};
let notRequiredRe = opts.notRequiredRe || null;
let whenMissingRedirectTo = opts.whenMissingRedirectTo || null;
let defaultRequiredToasts = null;
require('ui/routes')
.addSetupWork(function loadDefaultIndexPattern(Private, Promise, $route, config, indexPatterns) {
let getIds = Private(require('../_get_ids'));
let rootSearchSource = Private(require('ui/courier/data_source/_root_search_source'));
let path = _.get($route, 'current.$$route.originalPath');
return config.init()
.then(function () {
return getIds();
})
.then(function (patterns) {
let defaultId = config.get('defaultIndex');
let defined = !!defaultId;
let exists = _.contains(patterns, defaultId);
let required = !notRequiredRe || !path.match(notRequiredRe);
if (defined && !exists) {
config.clear('defaultIndex');
defaultId = defined = false;
}
if (!defined && required) {
throw new NoDefaultIndexPattern();
}
return notify.event('loading default index pattern', function () {
return indexPatterns.get(defaultId).then(function (pattern) {
rootSearchSource.getGlobalSource().set('index', pattern);
notify.log('index pattern set to', defaultId);
});
});
});
})
.afterWork(
// success
function () {
if (defaultRequiredToasts) {
_.invoke(defaultRequiredToasts, 'clear');
defaultRequiredToasts = null;
}
},
// failure
function (err, kbnUrl) {
let hasDefault = !(err instanceof NoDefaultIndexPattern);
if (hasDefault || !whenMissingRedirectTo) throw err; // rethrow
kbnUrl.change(whenMissingRedirectTo);
if (!defaultRequiredToasts) defaultRequiredToasts = [];
else defaultRequiredToasts.push(notify.error(err));
}
);
};

View file

@ -50,6 +50,14 @@ define(function (require) {
return Promise.try(fn, [i, el, list]);
}));
};
Promise.each = function (arr, fn) {
let queue = arr.slice(0);
let i = 0;
return (function next() {
if (!queue.length) return arr;
return Promise.try(fn, [arr.shift(), i++]).then(next);
}());
};
Promise.is = function (obj) {
// $q doesn't create instances of any constructor, promises are just objects with a then function
// https://github.com/angular/angular.js/blob/58f5da86645990ef984353418cd1ed83213b111e/src/ng/q.js#L335

View file

@ -1,9 +1,11 @@
var _ = require('lodash');
var wrapRouteWithPrep = require('./wrapRouteWithPrep');
var RouteSetupManager = require('./RouteSetupManager');
function RouteManager() {
var self = this;
var setup = new RouteSetupManager();
var when = [];
var defaults = [];
var otherwise;
@ -24,16 +26,25 @@ function RouteManager() {
route.reloadOnSearch = false;
}
wrapRouteWithPrep(route);
wrapRouteWithPrep(route, setup);
$routeProvider.when(path, route);
});
if (otherwise) {
wrapRouteWithPrep(otherwise);
wrapRouteWithPrep(otherwise, setup);
$routeProvider.otherwise(otherwise);
}
};
let wrapSetupAndChain = (fn, ...args) => {
fn.apply(setup, args);
return this;
};
this.addSetupWork = _.wrap(setup.addSetupWork, wrapSetupAndChain);
this.afterSetupWork = _.wrap(setup.afterSetupWork, wrapSetupAndChain);
this.afterWork = _.wrap(setup.afterWork, wrapSetupAndChain);
self.when = function (path, route) {
when.push([path, route]);
return self;

View file

@ -0,0 +1,73 @@
let _ = require('lodash');
module.exports = class RouteSetupManager {
constructor() {
this.setupWork = [];
this.onSetupComplete = [];
this.onSetupError = [];
this.onWorkComplete = [];
this.onWorkError = [];
}
addSetupWork(fn) {
this.setupWork.push(fn);
}
afterSetupWork(onComplete, onError) {
this.onSetupComplete.push(onComplete);
this.onSetupError.push(onError);
}
afterWork(onComplete, onError) {
this.onWorkComplete.push(onComplete);
this.onWorkError.push(onError);
}
/**
* Do each setupWork function by injecting it with angular dependencies
* and accepting promises from it.
* @return {[type]} [description]
*/
doWork(Promise, $injector, userWork) {
let invokeEach = (arr, locals) => {
return Promise.map(arr, fn => {
return $injector.invoke(fn, null, locals);
});
};
// call each error handler in order, until one of them resolves
// or we run out of handlers
let callErrorHandlers = (handlers, origError) => {
if (!_.size(handlers)) throw origError;
// clone so we don't discard handlers or loose them
handlers = handlers.slice(0);
let next = (err) => {
let handler = handlers.shift();
if (!handler) throw err;
return $injector.invoke(handler, null, { err }).catch(next);
};
return next(origError);
};
return invokeEach(this.setupWork)
.then(
() => invokeEach(this.onSetupComplete),
err => callErrorHandlers(this.onSetupError)
)
.then(() => {
// wait for the queue to fill up, then do all the work
var defer = Promise.defer();
userWork.resolveWhenFull(defer);
return defer.promise.then(() => Promise.all(userWork.doWork()));
})
.then(
() => invokeEach(this.onWorkComplete),
err => callErrorHandlers(this.onWorkError)
);
}
};

View file

@ -39,8 +39,9 @@ describe('wrapRouteWithPrep fn', function () {
$injector = _$injector_;
});
var setup = Private(require('ui/routes/setup'));
stub(setup, 'routeSetupWork', function () {
routes
.addSetupWork(function () {
return new Promise(function (resolve, reject) {
setTimeout(function () {
setupComplete = true;
@ -50,17 +51,17 @@ describe('wrapRouteWithPrep fn', function () {
});
routes
.when('/', {
resolve: {
test: function () {
expect(setupComplete).to.be(true);
userWorkComplete = true;
}
.when('/', {
resolve: {
test: function () {
expect(setupComplete).to.be(true);
userWorkComplete = true;
}
})
.config({
when: function (p, _r) { route = _r; }
});
}
})
.config({
when: function (p, _r) { route = _r; }
});
return new Promise(function (resolve, reject) {
setTimeout(function () {

View file

@ -1,65 +0,0 @@
define(function (require) {
return function routeSetup(Promise, config, $route, kbnUrl, courier, Notifier, Private, $rootScope) {
var _ = require('lodash');
var errors = require('ui/errors');
var NoDefaultIndexPattern = errors.NoDefaultIndexPattern;
var NoDefinedIndexPatterns = errors.NoDefinedIndexPatterns;
var firstNoDefaultError = true;
var rootSearchSource = Private(require('ui/courier/data_source/_root_search_source'));
var allowedRoutesRE = /^\/settings\//;
var notify = new Notifier();
return {
routeSetupWork: function () {
return Promise.all([
config.init(),
courier.SearchSource.ready,
$rootScope.kibana && $rootScope.kibana.ready
])
.then(function () {
var path = _.get($route, 'current.$$route.originalPath');
var defaultIndexRequired = path && !path.match(allowedRoutesRE);
return courier.indexPatterns.getIds()
.then(function (patterns) {
var defined = !!config.get('defaultIndex');
var exists = _.contains(patterns, config.get('defaultIndex'));
if (defined && !exists) {
config.clear('defaultIndex');
defined = false;
}
if (!defined) {
if (defaultIndexRequired) {
throw new NoDefaultIndexPattern();
} else {
firstNoDefaultError = false;
}
}
return rootSearchSource.loadDefault();
});
});
},
handleKnownError: function (err) {
if (err instanceof NoDefaultIndexPattern || err instanceof NoDefinedIndexPatterns) {
// .change short circuits the routes by calling $route.refresh(). We can safely swallow this error
// after reporting it to the user
kbnUrl.change('/settings/indices');
if (err instanceof NoDefaultIndexPattern) {
if (firstNoDefaultError) {
firstNoDefaultError = false;
} else {
notify.error(err);
}
}
} else {
return Promise.reject(err);
}
}
};
};
});

View file

@ -5,7 +5,7 @@ define(function (require) {
var WorkQueue = require('ui/routes/WorkQueue');
var errors = require('ui/errors');
function wrapRouteWithPrep(route) {
function wrapRouteWithPrep(route, setup) {
if (!route.resolve && route.redirectTo) return;
var userWork = new WorkQueue();
@ -13,24 +13,8 @@ define(function (require) {
userWork.limit = _.keys(route.resolve).length;
var resolve = {
__prep__: function (Private, Promise, $route, $injector, Notifier) {
var setup = Private(require('ui/routes/setup'));
return setup.routeSetupWork()
.then(function () {
// wait for the queue to fill up, then do all the work
var defer = Promise.defer();
userWork.resolveWhenFull(defer);
return defer.promise.then(function () {
return Promise.all(userWork.doWork());
});
})
.catch(function (err) {
// discard any remaining user work
userWork.empty();
return setup.handleKnownError(err);
});
__prep__: function ($injector) {
return $injector.invoke(setup.doWork, setup, { userWork });
}
};