Merge pull request #6791 from bevacqua/feature/custom-toaster-banner

Added a custom banner feature in advanced settings
This commit is contained in:
Nicolás Bevacqua 2016-06-16 16:12:54 -03:00 committed by GitHub
commit 6419e5a814
16 changed files with 186 additions and 50 deletions

View file

@ -137,6 +137,8 @@
"semver": "5.1.0",
"style-loader": "0.12.3",
"tar": "2.2.0",
"trunc-html": "1.0.2",
"trunc-text": "1.0.2",
"url-loader": "0.5.6",
"validate-npm-package-name": "2.2.2",
"webpack": "1.12.1",

View file

@ -2,7 +2,7 @@
<td class="name">
<b>{{conf.name}}</b>
<span class="smaller" ng-show="!conf.isCustom && conf.value !== undefined">
(Default: <i>{{conf.defVal == undefined ? 'null' : conf.defVal}}</i>)
(Default: <i>{{conf.defVal == undefined || conf.defVal === '' ? 'null' : conf.defVal}}</i>)
</span>
<span class="smaller" ng-show="conf.isCustom">
(Custom setting)
@ -21,13 +21,22 @@
role="form">
<input
ng-show="conf.normal"
ng-if="conf.normal"
ng-model="conf.unsavedValue"
ng-keyup="maybeCancel($event, conf)"
placeholder="{{conf.value || conf.defVal}}"
type="text"
class="form-control">
<textarea
ng-if="conf.markdown"
type="text"
class="form-control"
ng-model="conf.unsavedValue"
ng-keyup="maybeCancel($event, conf)"
elastic-textarea
></textarea>
<textarea
ng-if="conf.json"
type="text"
@ -40,7 +49,7 @@
<small ng-show="forms.configEdit.$error.jsonInput">Invalid JSON syntax</small>
<input
ng-show="conf.array"
ng-if="conf.array"
ng-list=","
ng-model="conf.unsavedValue"
ng-keyup="maybeCancel($event, conf)"
@ -49,13 +58,13 @@
class="form-control">
<input
ng-show="conf.bool"
ng-if="conf.bool"
ng-model="conf.unsavedValue"
type="checkbox"
class="form-control">
<select
ng-show="conf.select"
ng-if="conf.select"
name="conf.name"
ng-model="conf.unsavedValue"
ng-options="option as option for option in conf.options"
@ -66,9 +75,10 @@
<!-- Setting display formats -->
<span ng-if="!conf.editing" data-test-subj="currentValue">
<span ng-show="(conf.normal || conf.json || conf.select)">{{conf.value || conf.defVal}}</span>
<span ng-show="conf.array">{{(conf.value || conf.defVal).join(', ')}}</span>
<span ng-show="conf.bool">{{conf.value === undefined ? conf.defVal : conf.value}}</span>
<span ng-if="(conf.normal || conf.json || conf.select)">{{conf.value || conf.defVal}}</span>
<span ng-if="conf.array">{{(conf.value || conf.defVal).join(', ')}}</span>
<span ng-if="conf.bool">{{conf.value === undefined ? conf.defVal : conf.value}}</span>
<span ng-if="conf.markdown" ng-bind-html="conf.value | markdown"></span>
</span>
</td>

View file

@ -1,5 +1,6 @@
import _ from 'lodash';
import 'ui/elastic_textarea';
import 'ui/filters/markdown';
import uiModules from 'ui/modules';
import advancedRowTemplate from 'plugins/kibana/management/sections/settings/advanced_row.html';

View file

@ -1,6 +1,6 @@
import _ from 'lodash';
const NAMED_EDITORS = ['json', 'array', 'boolean', 'select'];
const NAMED_EDITORS = ['json', 'array', 'boolean', 'select', 'markdown'];
const NORMAL_EDITOR = ['number', 'string', 'null', 'undefined'];
/**

View file

@ -28,6 +28,7 @@ function toEditableConfig({ def, name, value, isCustom }) {
conf.select = editor === 'select';
conf.bool = editor === 'boolean';
conf.array = editor === 'array';
conf.markdown = editor === 'markdown';
conf.normal = editor === 'normal';
conf.tooComplex = !editor;

View file

@ -23,7 +23,7 @@ let init = function (text) {
// Create the element
$elem = angular.element(
'<kbn-truncated orig="' + text + '" length="10"></kbn-truncated>'
'<kbn-truncated source="' + text + '" length="10"></kbn-truncated>'
);
// And compile it
@ -37,6 +37,9 @@ let init = function (text) {
});
};
function trimmed(text) {
return text.trim().replace(/\s+/g, ' ');
}
describe('kbnTruncate directive', function () {
@ -47,7 +50,7 @@ describe('kbnTruncate directive', function () {
});
it('should trim long strings', function (done) {
expect($elem.text()).to.be('some strin... more');
expect(trimmed($elem.text())).to.be('some more');
done();
});
@ -56,15 +59,15 @@ describe('kbnTruncate directive', function () {
done();
});
it('should should more text if the link is clicked and less text if clicked again', function (done) {
it('should show more text if the link is clicked and less text if clicked again', function (done) {
$scope.toggle();
$scope.$digest();
expect($elem.text()).to.be('some string of text over 10 characters less');
expect(trimmed($elem.text())).to.be('some string of text over 10 characters less');
expect($elem.find('[ng-click="toggle()"]').text()).to.be('less');
$scope.toggle();
$scope.$digest();
expect($elem.text()).to.be('some strin... more');
expect(trimmed($elem.text())).to.be('some more');
expect($elem.find('[ng-click="toggle()"]').text()).to.be('more');
done();
@ -79,7 +82,7 @@ describe('kbnTruncate directive', function () {
});
it('should not trim short strings', function (done) {
expect($elem.text()).to.be('short');
expect(trimmed($elem.text())).to.be('short');
done();
});

View file

@ -0,0 +1,5 @@
<span ng-if="!isHtml">{{content}}</span>
<span ng-if="isHtml" ng-bind-html="content | trustAsHtml"></span>
<span ng-if="truncated">
<a ng-click="toggle()">{{action}}</a>
</span>

View file

@ -1,38 +1,38 @@
import $ from 'jquery';
import truncText from 'trunc-text';
import truncHTML from 'trunc-html';
import uiModules from 'ui/modules';
let module = uiModules.get('kibana');
import truncatedTemplate from 'ui/directives/partials/truncated.html';
import 'ui/filters/trust_as_html';
const module = uiModules.get('kibana');
module.directive('kbnTruncated', function ($compile) {
return {
restrict: 'E',
scope: {
orig: '@',
length: '@'
},
template: function ($element, attrs) {
let template = '<span>{{text}}</span>';
template += '<span ng-if="orig.length > length"> <a ng-click="toggle()">{{action}}</a></span>';
return template;
source: '@',
length: '@',
isHtml: '@'
},
template: truncatedTemplate,
link: function ($scope, $element, attrs) {
const source = $scope.source;
const max = $scope.length;
const truncated = $scope.isHtml
? truncHTML(source, max).html
: truncText(source, max);
let fullText = $scope.orig;
let truncated = fullText.substring(0, $scope.length);
$scope.content = truncated;
if (fullText === truncated) {
$scope.text = fullText;
if (source === truncated) {
return;
}
truncated += '...';
$scope.truncated = true;
$scope.expanded = false;
$scope.text = truncated;
$scope.action = 'more';
$scope.toggle = function () {
$scope.toggle = () => {
$scope.expanded = !$scope.expanded;
$scope.text = $scope.expanded ? fullText : truncated;
$scope.content = $scope.expanded ? source : truncated;
$scope.action = $scope.expanded ? 'less' : 'more';
};
}

View file

@ -0,0 +1,13 @@
import marked from 'marked';
import uiModules from 'ui/modules';
marked.setOptions({
gfm: true, // GitHub-flavored markdown
sanitize: true // Sanitize HTML tags
});
uiModules
.get('kibana')
.filter('markdown', function ($sce) {
return md => md ? $sce.trustAsHtml(marked(md)) : '';
});

View file

@ -185,6 +185,48 @@ describe('Notifier', function () {
});
});
describe('#banner', function () {
testVersionInfo('banner');
it('has no content', function () {
expect(notify('banner').content).not.to.be.defined;
});
it('prepends location to message for markdown', function () {
expect(notify('banner').markdown).to.equal(params.location + ': ' + message);
});
it('sets type to "banner"', function () {
expect(notify('banner').type).to.equal('banner');
});
it('sets icon to undefined', function () {
expect(notify('banner').icon).to.equal(undefined);
});
it('sets title to "Attention"', function () {
expect(notify('banner').title).to.equal('Attention');
});
it('sets lifetime to 3000000 by default', function () {
expect(notify('banner').lifetime).to.equal(3000000);
});
it('does not allow reporting', function () {
let includesReport = _.includes(notify('banner').actions, 'report');
expect(includesReport).to.false;
});
it('allows accepting', function () {
let includesAccept = _.includes(notify('banner').actions, 'accept');
expect(includesAccept).to.true;
});
it('does not include stack', function () {
expect(notify('banner').stack).not.to.be.defined;
});
});
function notify(fnName) {
notifier[fnName](message);
return latestNotification();

View file

@ -2,6 +2,8 @@ import _ from 'lodash';
import uiModules from 'ui/modules';
import toasterTemplate from 'ui/notify/partials/toaster.html';
import 'ui/notify/notify.less';
import 'ui/filters/markdown';
import 'ui/directives/truncated';
let notify = uiModules.get('kibana/notify');

View file

@ -59,9 +59,11 @@ function timerCanceler(notif, cb = _.noop, key) {
* intervals and clears the notif once the notif _lifetime_ has been reached.
*/
function startNotifTimer(notif, cb) {
let interval = 1000;
const interval = 1000;
if (notif.lifetime === Infinity) return;
if (notif.lifetime === Infinity) {
return;
}
notif.timeRemaining = Math.floor(notif.lifetime / interval);
@ -119,7 +121,19 @@ function add(notif, cb) {
return notif;
}
function set(opts, cb) {
if (this._sovereignNotif) {
this._sovereignNotif.clear();
}
if (!opts.content && !opts.markdown) {
return null;
}
this._sovereignNotif = add(opts, cb);
return this._sovereignNotif;
}
Notifier.prototype.add = add;
Notifier.prototype.set = set;
function formatInfo() {
let info = [];
@ -153,12 +167,13 @@ function Notifier(opts) {
// label type thing to say where notifications came from
self.from = opts.location;
'event lifecycle timed fatal error warning info'.split(' ').forEach(function (m) {
'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,
@ -271,6 +286,7 @@ Notifier.prototype._showFatal = function (err) {
/**
* Alert the user of an error that occured
* @param {Error|String} err
* @param {Function} cb
*/
Notifier.prototype.error = function (err, cb) {
return add({
@ -286,8 +302,8 @@ Notifier.prototype.error = function (err, cb) {
/**
* Warn the user abort something
* @param {[type]} msg [description]
* @return {[type]} [description]
* @param {String} msg
* @param {Function} cb
*/
Notifier.prototype.warning = function (msg, cb) {
return add({
@ -302,8 +318,8 @@ Notifier.prototype.warning = function (msg, cb) {
/**
* Display a debug message
* @param {String} msg [description]
* @return {[type]} [description]
* @param {String} msg
* @param {Function} cb
*/
Notifier.prototype.info = function (msg, cb) {
return add({
@ -316,6 +332,21 @@ Notifier.prototype.info = function (msg, cb) {
}, cb);
};
/**
* Display a banner message
* @param {String} msg
* @param {Function} cb
*/
Notifier.prototype.banner = function (msg, cb) {
return this.set({
type: 'banner',
title: 'Attention',
markdown: formatMsg(msg, this.from),
lifetime: Notifier.config.bannerLifetime,
actions: ['accept']
}, cb);
};
Notifier.prototype.describeError = formatMsg.describeError;
if (log === _.noop) {

View file

@ -20,11 +20,12 @@ module.factory('Notifier', function () {
});
// teach Notifier how to use angular interval services
module.run(function ($interval) {
module.run(function (config, $interval) {
Notifier.applyConfig({
setInterval: $interval,
clearInterval: $interval.cancel
});
applyConfig(config);
});
// if kibana is not included then the notify service can't
@ -32,16 +33,20 @@ module.run(function ($interval) {
if (!!kbnIndex) {
require('ui/config');
module.run(function (config) {
config.watchAll(() => {
Notifier.applyConfig({
errorLifetime: config.get('notifications:lifetime:error'),
warningLifetime: config.get('notifications:lifetime:warning'),
infoLifetime: config.get('notifications:lifetime:info')
});
});
config.watchAll(() => applyConfig(config));
});
}
function applyConfig(config) {
Notifier.applyConfig({
bannerLifetime: config.get('notifications:lifetime:banner'),
errorLifetime: config.get('notifications:lifetime:error'),
warningLifetime: config.get('notifications:lifetime:warning'),
infoLifetime: config.get('notifications:lifetime:info')
});
rootNotifier.banner(config.get('notifications:banner'));
}
window.onerror = function (err, url, line) {
rootNotifier.fatal(new Error(err + ' (' + url + ':' + line + ')'));
return true;

View file

@ -90,4 +90,12 @@
.alert-danger .badge {
background: darken(@alert-danger-bg, 25%);
}
.alert-banner {
background-color: #c0c0c0;
padding: 10px 15px;
}
.toast-message {
white-space: normal;
}
}

View file

@ -6,7 +6,10 @@
<span ng-show="notif.count > 1" class="badge">{{ notif.count }}</span>
<i class="fa" ng-class="'fa-' + notif.icon" tooltip="{{notif.title}}"></i>
<kbn-truncated orig="{{notif.content}}" length="250" class="toast-message" /></kbn-truncated>
<kbn-truncated ng-if="notif.content" source="{{notif.content}}" length="250" class="toast-message" /></kbn-truncated>
<kbn-truncated ng-if="notif.markdown" source="{{notif.markdown | markdown}}" is-html="true" length="250" class="toast-message" /></kbn-truncated>
<div class="btn-group pull-right toast-controls">
<button

View file

@ -219,6 +219,16 @@ export default function defaultSettingsProvider() {
value: false,
description: 'Whether the filters should have a global state (be pinned) by default'
},
'notifications:banner': {
type: 'markdown',
description: 'A custom banner intended for temporary notices to all users. <a href="https://help.github.com/articles/basic-writing-and-formatting-syntax/" target="_blank">Markdown supported</a>.',
value: ''
},
'notifications:lifetime:banner': {
value: 3000000,
description: 'The time in milliseconds which a banner notification ' +
'will be displayed on-screen for. Setting to Infinity will disable.'
},
'notifications:lifetime:error': {
value: 300000,
description: 'The time in milliseconds which an error notification ' +