Merge pull request #7978 from tsullivan/notifications-render-directive

notifications: add directive notification type
This commit is contained in:
Tim Sullivan 2016-08-18 12:07:58 -07:00 committed by GitHub
commit 1c7153c613
4 changed files with 298 additions and 38 deletions

View file

@ -228,7 +228,7 @@ describe('Notifier', function () {
customNotification = notifier.custom(customText, badParam);
}
expect(callCustomIncorrectly).to.throwException(function (e) {
expect(e.message).to.be('config param is required, and must be an object');
expect(e.message).to.be('Config param is required, and must be an object');
});
});
@ -401,3 +401,179 @@ describe('Notifier', function () {
});
}
});
describe('Directive Notification', function () {
let notifier;
let compile;
let scope;
const directiveParam = {
template: '<h1>Hello world {{ unit.message }}</h1>',
controllerAs: 'unit',
controller() {
this.message = '🎉';
}
};
const customParams = {
title: 'fooTitle',
actions:[{
text: 'Cancel',
callback: sinon.spy()
}, {
text: 'OK',
callback: sinon.spy()
}]
};
let directiveNotification;
beforeEach(() => {
ngMock.module('kibana');
ngMock.inject(function ($rootScope, $compile) {
scope = $rootScope.$new();
compile = $compile;
compile;
scope;
});
while (Notifier.prototype._notifs.pop()); // clear global notifications
notifier = new Notifier({ location: 'directiveFoo' });
directiveNotification = notifier.directive(directiveParam, customParams);
});
afterEach(() => {
directiveNotification.clear();
scope.$destroy();
});
describe('returns a renderable notification', () => {
let element;
beforeEach(() => {
scope.notif = notifier.directive(directiveParam, customParams);
const template = `
<render-directive
definition="notif.directive"
notif="notif"
></render-directive>`;
element = compile(template)(scope);
scope.$apply();
});
it('that renders with the provided template', () => {
expect(element.find('h1').text()).to.contain('Hello world');
});
it('that renders with the provided controller', () => {
expect(element.text()).to.contain('🎉');
});
});
it('throws if first param is not an object', () => {
// destroy the default custom notification, avoid duplicate handling
directiveNotification.clear();
function callDirectiveIncorrectly() {
const badDirectiveParam = null;
directiveNotification = notifier.directive(badDirectiveParam, {});
}
expect(callDirectiveIncorrectly).to.throwException(function (e) {
expect(e.message).to.be('Directive param is required, and must be an object');
});
});
it('throws if second param is not an object', () => {
// destroy the default custom notification, avoid duplicate handling
directiveNotification.clear();
function callDirectiveIncorrectly() {
const badConfigParam = null;
directiveNotification = notifier.directive(directiveParam, badConfigParam);
}
expect(callDirectiveIncorrectly).to.throwException(function (e) {
expect(e.message).to.be('Config param is required, and must be an object');
});
});
it('throws if directive param has scope definition instead of allow the helper to do its work', () => {
// destroy the default custom notification, avoid duplicate handling
directiveNotification.clear();
function callDirectiveIncorrectly() {
const badDirectiveParam = {
scope: {
garbage: '='
}
};
directiveNotification = notifier.directive(badDirectiveParam, customParams);
}
expect(callDirectiveIncorrectly).to.throwException(function (e) {
expect(e.message).to.be('Directive should not have a scope definition. Notifier has an internal implementation.');
});
});
it('throws if directive param has link function instead of allow the helper to do its work', () => {
// destroy the default custom notification, avoid duplicate handling
directiveNotification.clear();
function callDirectiveIncorrectly() {
const badDirectiveParam = {
link: ($scope) => {
/*eslint-disable no-console*/
console.log($scope.nothing);
/*eslint-enable*/
}
};
directiveNotification = notifier.directive(badDirectiveParam, customParams);
}
expect(callDirectiveIncorrectly).to.throwException(function (e) {
expect(e.message).to.be('Directive should not have a link function. Notifier has an internal link function helper.');
});
});
it('has a directive function to make notifications with template and scope', () => {
expect(notifier.directive).to.be.a('function');
});
it('sets the scope property and link function', () => {
expect(directiveNotification).to.have.property('directive');
expect(directiveNotification.directive).to.be.an('object');
expect(directiveNotification.directive).to.have.property('scope');
expect(directiveNotification.directive.scope).to.be.an('object');
expect(directiveNotification.directive).to.have.property('link');
expect(directiveNotification.directive.link).to.be.an('function');
});
/* below copied from custom notification tests */
it('uses custom actions', () => {
expect(directiveNotification).to.have.property('customActions');
expect(directiveNotification.customActions).to.have.length(customParams.actions.length);
});
it('gives a default action if none are provided', () => {
// destroy the default custom notification, avoid duplicate handling
directiveNotification.clear();
const noActionParams = _.defaults({ actions: [] }, customParams);
directiveNotification = notifier.directive(directiveParam, noActionParams);
expect(directiveNotification).to.have.property('actions');
expect(directiveNotification.actions).to.have.length(1);
});
it('defaults type and lifetime for "info" config', () => {
expect(directiveNotification.type).to.be('info');
expect(directiveNotification.lifetime).to.be(5000);
});
it('should wrap the callback functions in a close function', () => {
directiveNotification.customActions.forEach((action, idx) => {
expect(action.callback).not.to.equal(customParams.actions[idx]);
action.callback();
});
customParams.actions.forEach(action => {
expect(action.callback.called).to.true;
});
});
});

View file

@ -1,8 +1,10 @@
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 = [];
@ -109,9 +111,12 @@ function add(notif, cb) {
return notif.timerId ? true : false;
};
let dup = _.find(notifs, function (item) {
return item.content === notif.content && item.lifetime === notif.lifetime;
});
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;
@ -405,28 +410,13 @@ Notifier.prototype.banner = function (msg, cb) {
};
/**
* 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(); }
* }]
* }
* Helper for common behavior in custom and directive types
*/
Notifier.prototype.custom = function (msg, config, cb) {
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');
throw new Error('Config param is required, and must be an object');
}
// workaround to allow callers to send `config.type` as `error` instead of
@ -449,23 +439,104 @@ Notifier.prototype.custom = function (msg, config, cb) {
}
};
const mergedConfig = _.assign({
const customConfig = _.assign({
type: 'info',
title: 'Notification',
content: formatMsg(msg, this.from),
truncationLength: config.truncationLength || Notifier.config.defaultTruncationLength,
lifetime: getLifetime(config.type)
}, config);
const hasActions = _.get(mergedConfig, 'actions.length');
const hasActions = _.get(customConfig, 'actions.length');
if (hasActions) {
mergedConfig.customActions = mergedConfig.actions;
delete mergedConfig.actions;
customConfig.customActions = customConfig.actions;
delete customConfig.actions;
} else {
mergedConfig.actions = ['accept'];
customConfig.actions = ['accept'];
}
return add(mergedConfig, cb);
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;

View file

@ -20,12 +20,13 @@ module.factory('Notifier', function () {
});
// teach Notifier how to use angular interval services
module.run(function (config, $interval) {
module.run(function (config, $interval, $compile) {
Notifier.applyConfig({
setInterval: $interval,
clearInterval: $interval.cancel
});
applyConfig(config);
Notifier.$compile = $compile;
});
// if kibana is not included then the notify service can't

View file

@ -7,7 +7,20 @@
<i class="fa" ng-class="'fa-' + notif.icon" tooltip="{{notif.title}}"></i>
<kbn-truncated source="{{notif.content | markdown}}" is-html="true" length="{{notif.truncationLength}}" class="toast-message" /></kbn-truncated>
<kbn-truncated
ng-if="notif.content"
source="{{notif.content | markdown}}"
is-html="true"
length="{{notif.truncationLength}}"
class="toast-message"
></kbn-truncated>
<render-directive
ng-if="notif.directive"
definition="notif.directive"
notif="notif"
class="toast-message"
></render-directive>
<div class="btn-group pull-right toast-controls">
<button
@ -16,36 +29,35 @@
class="btn toaster-countdown"
ng-class="'btn-' + notif.type"
ng-click="notif.cancelTimer()"
><span class="badge" hover-text="stop">{{notif.timeRemaining}}s</span></button>
><span class="badge" hover-text="stop">{{notif.timeRemaining}}s</span></button>
<button
type="button"
ng-if="notif.stack && !notif.showStack"
class="btn"
ng-class="'btn-' + notif.type"
ng-click="notif.cancelTimer(); notif.showStack = true"
>More Info</button>
>More Info</button>
<button
type="button"
ng-if="notif.stack && notif.showStack"
class="btn"
ng-class="'btn-' + notif.type"
ng-click="notif.showStack = false"
>Less Info</button>
>Less Info</button>
<button
type="button"
ng-if="notif.accept"
class="btn"
ng-class="'btn-' + notif.type"
ng-click="notif.accept()"
>OK</button>
>OK</button>
<button
type="button"
ng-if="notif.address"
class="btn"
ng-class="'btn-' + notif.type"
ng-click="notif.address()"
>Fix it</button>
>Fix it</button>
<button
type="button"
class="btn"
@ -53,7 +65,7 @@
ng-class="'btn-' + notif.type"
ng-click="action.callback()"
ng-bind="action.key"
></button>
></button>
</div>
</div>