mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
Merge pull request #6791 from bevacqua/feature/custom-toaster-banner
Added a custom banner feature in advanced settings
This commit is contained in:
commit
6419e5a814
16 changed files with 186 additions and 50 deletions
|
@ -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",
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -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'];
|
||||
|
||||
/**
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
|
||||
|
|
5
src/ui/public/directives/partials/truncated.html
Normal file
5
src/ui/public/directives/partials/truncated.html
Normal 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>
|
|
@ -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';
|
||||
};
|
||||
}
|
||||
|
|
13
src/ui/public/filters/markdown.js
Normal file
13
src/ui/public/filters/markdown.js
Normal 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)) : '';
|
||||
});
|
|
@ -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();
|
||||
|
|
|
@ -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');
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 ' +
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue