kibana-123
Version:
Kibana is an open source (Apache Licensed), browser based analytics and search dashboard for Elasticsearch. Kibana is a snap to setup and start using. Kibana strives to be easy to get started with, while also being flexible and powerful, just like Elastic
666 lines (571 loc) • 17.3 kB
JavaScript
import _ from 'lodash';
import angular from 'angular';
import $ from 'jquery';
import metadata from 'ui/metadata';
import formatMsg from 'ui/notify/lib/_format_msg';
import fatalSplashScreen from 'ui/notify/partials/fatal_splash_screen.html';
import 'ui/render_directive';
/* eslint no-console: 0 */
let notifs = [];
let version = metadata.version;
let buildNum = metadata.buildNum;
let consoleGroups = ('group' in window.console) && ('groupCollapsed' in window.console) && ('groupEnd' in window.console);
let log = _.bindKey(console, 'log');
// used to identify the first call to fatal, set to false there
let firstFatal = true;
let fatalToastTemplate = (function lazyTemplate(tmpl) {
let compiled;
return function (vars) {
return (compiled || (compiled = _.template(tmpl)))(vars);
};
}(require('ui/notify/partials/fatal.html')));
function now() {
if (window.performance && window.performance.now) {
return window.performance.now();
}
return Date.now();
}
function closeNotif(notif, cb = _.noop, key) {
return function () {
// this === notif
let i = notifs.indexOf(notif);
if (i !== -1) notifs.splice(i, 1);
cancelTimer(notif);
cb(key);
};
}
function cancelTimer(notif) {
if (notif.timerId) {
Notifier.config.clearInterval(notif.timerId);
notif.timerId = undefined;
}
}
function timerCanceler(notif, cb = _.noop, key) {
return function cancelNotifTimer() {
cancelTimer(notif);
cb(key);
};
}
/**
* Initiates a timer to update _timeRemaining_ on the notif at second
* intervals and clears the notif once the notif _lifetime_ has been reached.
*/
function startNotifTimer(notif, cb) {
const interval = 1000;
if (notif.lifetime === Infinity || notif.lifetime === 0) {
return;
}
notif.timeRemaining = Math.floor(notif.lifetime / interval);
notif.timerId = Notifier.config.setInterval(function () {
notif.timeRemaining -= 1;
if (notif.timeRemaining <= 0) {
closeNotif(notif, cb, 'ignore')();
}
}, interval, notif.timeRemaining);
notif.cancelTimer = timerCanceler(notif, cb);
}
function restartNotifTimer(notif, cb) {
cancelTimer(notif);
startNotifTimer(notif, cb);
}
const typeToButtonClassMap = {
danger: 'btn-danger', // NOTE: `error` type is internally named as `danger`
warning: 'btn-warning',
info: 'btn-info',
banner: 'btn-banner'
};
const buttonHierarchyClass = (index) => {
if (index === 0) {
// first action: primary className
return 'btn-primary';
}
// subsequent actions: secondary/default className
return 'btn-default';
};
const typeToAlertClassMap = {
danger: `alert-danger`,
warning: `alert-warning`,
info: `alert-info`,
banner: `alert-banner`,
};
function add(notif, cb) {
_.set(notif, 'info.version', version);
_.set(notif, 'info.buildNum', buildNum);
notif.clear = closeNotif(notif);
if (notif.actions) {
notif.actions.forEach(function (action) {
notif[action] = closeNotif(notif, cb, action);
});
} else if (notif.customActions) {
// wrap all of the custom functions in a close
notif.customActions = notif.customActions.map((action, index) => {
return {
key: action.text,
callback: closeNotif(notif, action.callback, action.text),
getButtonClass() {
const buttonTypeClass = typeToButtonClassMap[notif.type];
return `${buttonHierarchyClass(index)} ${buttonTypeClass}`;
}
};
});
}
notif.count = (notif.count || 0) + 1;
notif.isTimed = function isTimed() {
return notif.timerId ? true : false;
};
// decorate the notification with helper functions for the template
notif.getButtonClass = () => typeToButtonClassMap[notif.type];
notif.getAlertClassStack = () => `toast-stack alert ${typeToAlertClassMap[notif.type]}`;
notif.getIconClass = () => (notif.type === 'banner') ? '' : `fa fa-${notif.icon}`;
notif.getToastMessageClass = () => (notif.type === 'banner') ? 'toast-message-banner' : 'toast-message';
notif.getAlertClass = () => (notif.type === 'banner') ?
`alert ${typeToAlertClassMap[notif.type]}` : // not including `.toast` class leaves out the flex properties for banner
`toast alert ${typeToAlertClassMap[notif.type]}`;
notif.getButtonGroupClass = () => (notif.type === 'banner') ?
'toast-controls-banner' :
'toast-controls';
let dup = null;
if (notif.content) {
dup = _.find(notifs, function (item) {
return item.content === notif.content && item.lifetime === notif.lifetime;
});
}
if (dup) {
dup.count += 1;
dup.stacks = _.union(dup.stacks, [notif.stack]);
restartNotifTimer(dup, cb);
return dup;
}
startNotifTimer(notif, cb);
notif.stacks = [notif.stack];
notifs.push(notif);
return notif;
}
function set(opts, cb) {
if (!opts.content) {
return null;
}
if (this._sovereignNotif) {
this._sovereignNotif.clear();
}
this._sovereignNotif = add(opts, cb);
return this._sovereignNotif;
}
Notifier.prototype.add = add;
Notifier.prototype.set = set;
function formatInfo() {
let info = [];
if (!_.isUndefined(version)) {
info.push(`Version: ${version}`);
}
if (!_.isUndefined(buildNum)) {
info.push(`Build: ${buildNum}`);
}
return info.join('\n');
}
// browsers format Error.stack differently; always include message
function formatStack(err) {
if (err.stack && !~err.stack.indexOf(err.message)) {
return 'Error: ' + err.message + '\n' + err.stack;
}
return err.stack;
}
/**
* Functionality to check that
*/
function Notifier(opts) {
let self = this;
opts = opts || {};
// label type thing to say where notifications came from
self.from = opts.location;
'event lifecycle timed fatal error warning info banner'.split(' ').forEach(function (m) {
self[m] = _.bind(self[m], self);
});
}
Notifier.config = {
bannerLifetime: 3000000,
errorLifetime: 300000,
warningLifetime: 10000,
infoLifetime: 5000,
setInterval: window.setInterval,
clearInterval: window.clearInterval
};
Notifier.applyConfig = function (config) {
_.merge(Notifier.config, config);
};
// to be notified when the first fatal error occurs, push a function into this array.
Notifier.fatalCallbacks = [];
// "Constants"
Notifier.QS_PARAM_MESSAGE = 'notif_msg';
Notifier.QS_PARAM_LEVEL = 'notif_lvl';
Notifier.QS_PARAM_LOCATION = 'notif_loc';
Notifier.pullMessageFromUrl = ($location) => {
const queryString = $location.search();
if (!queryString.notif_msg) {
return;
}
const message = queryString[Notifier.QS_PARAM_MESSAGE];
const config = queryString[Notifier.QS_PARAM_LOCATION] ? { location: queryString[Notifier.QS_PARAM_LOCATION] } : {};
const level = queryString[Notifier.QS_PARAM_LEVEL] || 'info';
$location.search(Notifier.QS_PARAM_MESSAGE, null);
$location.search(Notifier.QS_PARAM_LOCATION, null);
$location.search(Notifier.QS_PARAM_LEVEL, null);
const notifier = new Notifier(config);
notifier[level](message);
};
// simply a pointer to the global notif list
Notifier.prototype._notifs = notifs;
/**
* Log a sometimes redundant event
* @param {string} name - The name of the group
* @param {boolean} success - Simple flag stating whether the event succeeded
*/
Notifier.prototype.event = createGroupLogger('event', {
open: true
});
/**
* Log a major, important, event in the lifecycle of the application
* @param {string} name - The name of the lifecycle event
* @param {boolean} success - Simple flag stating whether the lifecycle event succeeded
*/
Notifier.prototype.lifecycle = createGroupLogger('lifecycle', {
open: true
});
/**
* Wrap a function so that it's execution time gets logged.
*
* @param {function} fn - the function to wrap, it's .name property is
* read so make sure to set it
* @return {function} - the wrapped function
*/
Notifier.prototype.timed = function (name, fn) {
let self = this;
if (typeof name === 'function') {
fn = name;
name = fn.name;
}
return function WrappedNotifierFunction() {
let cntx = this;
let args = arguments;
return self.event(name, function () {
return fn.apply(cntx, args);
});
};
};
/**
* Kill the page, display an error, then throw the error.
* Used as a last-resort error back in many promise chains
* so it rethrows the error that's displayed on the page.
*
* @param {Error} err - The error that occured
*/
Notifier.prototype.fatal = function (err) {
this._showFatal(err);
throw err;
};
/**
* Display an error that destroys the entire app. Broken out so that
* global error handlers can display fatal errors without throwing another
* error like in #fatal()
*
* @param {Error} err - The fatal error that occured
*/
Notifier.prototype._showFatal = function (err) {
if (firstFatal) {
_.callEach(Notifier.fatalCallbacks);
firstFatal = false;
window.addEventListener('hashchange', function () {
window.location.reload();
});
}
let html = fatalToastTemplate({
info: formatInfo(),
msg: formatMsg(err, this.from),
stack: formatStack(err)
});
let $container = $('#fatal-splash-screen');
if (!$container.size()) {
$(document.body)
// in case the app has not completed boot
.removeAttr('ng-cloak')
.html(fatalSplashScreen);
$container = $('#fatal-splash-screen');
}
$container.append(html);
console.error(err.stack);
};
const overrideableOptions = ['lifetime', 'icon'];
/**
* Alert the user of an error that occured
* @param {Error|String} err
* @param {Function} cb
*/
Notifier.prototype.error = function (err, opts, cb) {
if (_.isFunction(opts)) {
cb = opts;
opts = {};
}
const config = _.assign({
type: 'danger',
content: formatMsg(err, this.from),
icon: 'warning',
title: 'Error',
lifetime: Notifier.config.errorLifetime,
actions: ['report', 'accept'],
stack: formatStack(err)
}, _.pick(opts, overrideableOptions));
return add(config, cb);
};
/**
* Warn the user abort something
* @param {String} msg
* @param {Function} cb
*/
Notifier.prototype.warning = function (msg, opts, cb) {
if (_.isFunction(opts)) {
cb = opts;
opts = {};
}
const config = _.assign({
type: 'warning',
content: formatMsg(msg, this.from),
icon: 'warning',
title: 'Warning',
lifetime: Notifier.config.warningLifetime,
actions: ['accept']
}, _.pick(opts, overrideableOptions));
return add(config, cb);
};
/**
* Display a debug message
* @param {String} msg
* @param {Function} cb
*/
Notifier.prototype.info = function (msg, opts, cb) {
if (_.isFunction(opts)) {
cb = opts;
opts = {};
}
const config = _.assign({
type: 'info',
content: formatMsg(msg, this.from),
icon: 'info-circle',
title: 'Debug',
lifetime: Notifier.config.infoLifetime,
actions: ['accept']
}, _.pick(opts, overrideableOptions));
return add(config, cb);
};
/**
* Display a banner message
* @param {String} msg
* @param {Function} cb
*/
Notifier.prototype.banner = function (msg, cb) {
return this.set({
type: 'banner',
title: 'Attention',
content: formatMsg(msg, this.from),
lifetime: Notifier.config.bannerLifetime,
actions: ['accept']
}, cb);
};
/**
* Helper for common behavior in custom and directive types
*/
function getDecoratedCustomConfig(config) {
// There is no helper condition that will allow for 2 parameters, as the
// other methods have. So check that config is an object
if (!_.isPlainObject(config)) {
throw new Error('Config param is required, and must be an object');
}
// workaround to allow callers to send `config.type` as `error` instead of
// reveal internal implementation that error notifications use a `danger`
// style
if (config.type === 'error') {
config.type = 'danger';
}
const getLifetime = (type) => {
switch (type) {
case 'banner':
return Notifier.config.bannerLifetime;
case 'warning':
return Notifier.config.warningLifetime;
case 'danger':
return Notifier.config.errorLifetime;
default: // info
return Notifier.config.infoLifetime;
}
};
const customConfig = _.assign({
type: 'info',
title: 'Notification',
lifetime: getLifetime(config.type)
}, config);
const hasActions = _.get(customConfig, 'actions.length');
if (hasActions) {
customConfig.customActions = customConfig.actions;
delete customConfig.actions;
} else {
customConfig.actions = ['accept'];
}
return customConfig;
}
/**
* Display a custom message
* @param {String} msg - required
* @param {Object} config - required
* @param {Function} cb - optional
*
* config = {
* title: 'Some Title here',
* type: 'info',
* actions: [{
* text: 'next',
* callback: function() { next(); }
* }, {
* text: 'prev',
* callback: function() { prev(); }
* }]
* }
*/
Notifier.prototype.custom = function (msg, config, cb) {
const customConfig = getDecoratedCustomConfig(config);
customConfig.content = formatMsg(msg, this.from);
return add(customConfig, cb);
};
/**
* Display a scope-bound directive using template rendering in the message area
* @param {Object} directive - required
* @param {Object} config - required
* @param {Function} cb - optional
*
* directive = {
* template: `<p>Hello World! <a ng-click="example.clickHandler()">Click me</a>.`,
* controllerAs: 'example',
* controller() {
* this.clickHandler = () {
* // do something
* };
* }
* }
*
* config = {
* title: 'Some Title here',
* type: 'info',
* actions: [{
* text: 'next',
* callback: function() { next(); }
* }, {
* text: 'prev',
* callback: function() { prev(); }
* }]
* }
*/
Notifier.prototype.directive = function (directive, config, cb) {
if (!_.isPlainObject(directive)) {
throw new Error('Directive param is required, and must be an object');
}
if (!Notifier.$compile) {
throw new Error('Unable to use the directive notification until Angular has initialized.');
}
if (directive.scope) {
throw new Error('Directive should not have a scope definition. Notifier has an internal implementation.');
}
if (directive.link) {
throw new Error('Directive should not have a link function. Notifier has an internal link function helper.');
}
// make a local copy of the directive param (helps unit tests)
const localDirective = _.clone(directive, true);
localDirective.scope = { notif: '=' };
localDirective.link = function link($scope, $el) {
const $template = angular.element($scope.notif.directive.template);
const postLinkFunction = Notifier.$compile($template);
$el.html($template);
postLinkFunction($scope);
};
const customConfig = getDecoratedCustomConfig(config);
customConfig.directive = localDirective;
return add(customConfig, cb);
};
Notifier.prototype.describeError = formatMsg.describeError;
if (log === _.noop) {
Notifier.prototype.log = _.noop;
} else {
Notifier.prototype.log = function () {
let args = [].slice.apply(arguments);
if (this.from) args.unshift(this.from + ':');
log.apply(null, args);
};
}
// general functionality used by .event() and .lifecycle()
function createGroupLogger(type, opts) {
// Track the groups managed by this logger
let groups = window[type + 'Groups'] = {};
return function logger(name, success) {
let status; // status of the timer
let exec; // function to execute and wrap
let ret; // return value
let complete = function (val) { logger(name, true); return val; };
let failure = function (err) { logger(name, false); throw err; };
if (typeof success === 'function' || success === void 0) {
// start
groups[name] = now();
if (success) {
// success === the function to time
exec = success;
} else {
// function that can report on the success or failure of an op, and pass their value along
ret = complete;
ret.failure = failure;
}
}
else {
groups[name] = now() - (groups[name] || 0);
let time = ' in ' + groups[name].toFixed(2) + 'ms';
// end
if (success) {
status = 'complete' + time;
} else {
groups[name] = false;
status = 'failure' + time;
}
}
if (consoleGroups) {
if (status) {
console.log(status);
console.groupEnd();
} else {
if (opts.open) {
console.group(name);
} else {
console.groupCollapsed(name);
}
}
} else {
log('KBN: ' + name + (status ? ' - ' + status : ''));
}
if (exec) {
try {
ret = exec();
} catch (e) {
return failure(e);
}
if (ret && typeof ret.then === 'function') {
// return a new promise that proxies the value
// and logs about the promise outcome
return ret.then(function (val) {
complete();
return val;
}, function (err) {
failure(err);
throw err;
});
}
// the function executed fine, and didn't return a promise, move along
complete();
}
return ret;
};
}
export default Notifier;