├── .eslintrc ├── .gitignore ├── demo.gif ├── server ├── routes │ ├── index.js │ └── notification │ │ ├── index.js │ │ ├── push.js │ │ └── pull.js ├── lib │ ├── constants.js │ ├── notification_mappings.js │ ├── replace_injected_vars.js │ ├── parse_index_pattern.js │ └── register_template.js └── init.js ├── public ├── images │ └── ic_message_white_18px.svg ├── nav_control │ ├── nav_control.html │ └── nav_control.js └── components │ └── notification_center │ ├── lib │ ├── get_notification_classes.js │ ├── stored_config.js │ ├── polling_notifications.js │ └── stored_notifications.js │ ├── index.less │ ├── index.js │ └── template.html ├── package.json ├── LICENSE ├── index.js ├── TRANSLATION.md └── README.md /.eslintrc: -------------------------------------------------------------------------------- 1 | --- 2 | extends: "@elastic/kibana" 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | npm-debug.log* 2 | node_modules 3 | /build/ 4 | .*.md 5 | .vscode -------------------------------------------------------------------------------- /demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sw-jung/kibana_notification_center/HEAD/demo.gif -------------------------------------------------------------------------------- /server/routes/index.js: -------------------------------------------------------------------------------- 1 | import { notification } from './notification'; 2 | 3 | export function routes(server) { 4 | notification(server); 5 | }; 6 | -------------------------------------------------------------------------------- /server/lib/constants.js: -------------------------------------------------------------------------------- 1 | export const constants = { 2 | API_BASE_URL: '/api/notification_center', 3 | 4 | RESPONSE: { 5 | OK: { 6 | acknowledged: true 7 | } 8 | } 9 | }; -------------------------------------------------------------------------------- /server/routes/notification/index.js: -------------------------------------------------------------------------------- 1 | import { push } from './push'; 2 | import { pull } from './pull'; 3 | 4 | export function notification(server) { 5 | push(server); 6 | pull(server); 7 | }; 8 | -------------------------------------------------------------------------------- /server/lib/notification_mappings.js: -------------------------------------------------------------------------------- 1 | export const mappings = { 2 | notification: { 3 | properties: { 4 | timestamp: { 5 | type: 'date' 6 | }, 7 | content: { 8 | type: 'string' 9 | } 10 | } 11 | } 12 | }; 13 | -------------------------------------------------------------------------------- /public/images/ic_message_white_18px.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /server/init.js: -------------------------------------------------------------------------------- 1 | import { constants } from './lib/constants'; 2 | import { registerTemplate } from './lib/register_template'; 3 | import { routes } from './routes'; 4 | 5 | export function init(server) { 6 | if (!!server.config().get('notification_center.api.enabled')) { 7 | registerTemplate(server); 8 | routes(server); 9 | } 10 | }; 11 | -------------------------------------------------------------------------------- /server/lib/replace_injected_vars.js: -------------------------------------------------------------------------------- 1 | import { extend } from 'lodash'; 2 | 3 | export async function replaceInjectedVars(originalInjectedVars, request, server) { 4 | const { callWithRequest } = server.plugins.elasticsearch.getCluster('data'); 5 | return callWithRequest(request, 'info') 6 | .then(resp => extend(originalInjectedVars, { 7 | clusterUuid: resp.cluster_uuid 8 | })); 9 | }; 10 | 11 | -------------------------------------------------------------------------------- /public/nav_control/nav_control.html: -------------------------------------------------------------------------------- 1 |
2 | 10 |
-------------------------------------------------------------------------------- /public/components/notification_center/lib/get_notification_classes.js: -------------------------------------------------------------------------------- 1 | export function getNotificationClasses(notif = {}) { 2 | const color = ((type) => { 3 | switch (type) { 4 | case 'banner': 5 | return 'success'; 6 | case 'danger': 7 | return 'error'; 8 | case 'warning': 9 | return 'warning'; 10 | default: 11 | return 'info'; 12 | }; 13 | })(notif.type); 14 | 15 | const icon = notif.icon || 'info-circle'; 16 | return `kuiIcon--${color} fa-${icon}`; 17 | }; -------------------------------------------------------------------------------- /server/lib/parse_index_pattern.js: -------------------------------------------------------------------------------- 1 | import moment from 'moment'; 2 | import { find } from 'lodash'; 3 | 4 | const dateExprPattern = /%{\+([^}]+)}/g; 5 | 6 | export function parseWithWildcard(index) { 7 | return parseWithReplacer(index, '*'); 8 | }; 9 | 10 | export function parseWithTimestamp(index, timestamp = moment()) { 11 | return parseWithReplacer(index, (match, format) => timestamp.format(format)); 12 | }; 13 | 14 | export function parseWithReplacer(index, replacer) { 15 | return index.replace(dateExprPattern, replacer); 16 | }; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "notification_center", 3 | "version": "6.2.4", 4 | "description": "Notification center for kibana", 5 | "main": "index.js", 6 | "kibana": { 7 | "version": "6.2.4" 8 | }, 9 | "scripts": { 10 | "lint": "eslint", 11 | "start": "plugin-helpers start", 12 | "test:server": "plugin-helpers test:server", 13 | "test:browser": "plugin-helpers test:browser", 14 | "build": "plugin-helpers build", 15 | "postinstall": "plugin-helpers postinstall" 16 | }, 17 | "devDependencies": { 18 | "@elastic/eslint-config-kibana": "0.0.2", 19 | "@elastic/plugin-helpers": "^6.0.0", 20 | "babel-eslint": "4.1.8", 21 | "eslint": "1.10.3", 22 | "eslint-plugin-mocha": "1.1.0", 23 | "expect.js": "^0.3.1" 24 | }, 25 | "dependencies": { 26 | "joi": "^10.6.0", 27 | "lodash": "^4.17.4", 28 | "moment": "^2.18.1" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /public/components/notification_center/lib/stored_config.js: -------------------------------------------------------------------------------- 1 | import { has, get, set, isObject } from 'lodash'; 2 | 3 | export class StoredConfig { 4 | constructor(defaultConfig = {}) { 5 | this.key = `KBN::NOTIFS::CONFIG`; 6 | this.config = defaultConfig; 7 | }; 8 | 9 | has(path) { 10 | return has(this.config, path); 11 | }; 12 | 13 | get(path, defaultValue) { 14 | return get(this.config, path, defaultValue); 15 | }; 16 | 17 | set(path, value) { 18 | return set(this.config, path, value); 19 | }; 20 | 21 | save() { 22 | localStorage.setItem(this.key, JSON.stringify(this.config)); 23 | return this; 24 | }; 25 | 26 | load() { 27 | try { 28 | const storedConfig = JSON.parse(localStorage.getItem(this.key)); 29 | if (isObject(storedConfig)) { 30 | this.config = storedConfig; 31 | } 32 | } catch (e) { 33 | console.warn('Failed to load stored config.\n', e); 34 | } 35 | return this; 36 | }; 37 | }; -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 sw-jung 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /server/lib/register_template.js: -------------------------------------------------------------------------------- 1 | import { parseWithWildcard } from './parse_index_pattern'; 2 | import { mappings } from './notification_mappings'; 3 | 4 | const tags = ['register_template', 'notification_center']; 5 | export function registerTemplate(server) { 6 | const { index, template } = server.config().get('notification_center'); 7 | const { waitUntilReady, getCluster } = server.plugins.elasticsearch; 8 | const { callWithInternalUser } = getCluster('admin'); 9 | 10 | return waitUntilReady() 11 | .then(() => { 12 | return !template.overwrite && callWithInternalUser('indices.existsTemplate', { 13 | name: template.name, 14 | ignore: 404 15 | }) 16 | .then(exists => exists); 17 | }) 18 | .then(skip => { 19 | return skip || Promise.resolve() 20 | .then(server.log([...tags, 'info'], `Try to put template '${template.name}'.`)) 21 | .then(() => callWithInternalUser('indices.putTemplate', { 22 | name: template.name, 23 | create: !template.overwrite, 24 | body: { 25 | template: parseWithWildcard(index), 26 | mappings 27 | } 28 | })) 29 | .then(resp => server.log([...tags, 'info'], `Success to put template '${template.name}'.`)) 30 | .catch(err => server.log([...tags, 'error'], err)); 31 | }); 32 | }; 33 | -------------------------------------------------------------------------------- /server/routes/notification/push.js: -------------------------------------------------------------------------------- 1 | import { extend } from 'lodash'; 2 | import Joi from 'joi'; 3 | import moment from 'moment'; 4 | import { constants } from '../../lib/constants'; 5 | import { parseWithTimestamp } from '../../lib/parse_index_pattern'; 6 | 7 | export function push(server) { 8 | const index = server.config().get('notification_center.index'); 9 | const type = 'notification'; 10 | const { callWithRequest } = server.plugins.elasticsearch.getCluster('data'); 11 | 12 | server.route({ 13 | path: `${constants.API_BASE_URL}/notification`, 14 | method: ['POST', 'PUT'], 15 | handler(request, reply) { 16 | const { payload } = request; 17 | const timestamp = Date.now(); 18 | 19 | callWithRequest(request, 'index', { 20 | index: parseWithTimestamp(index, moment(timestamp)), 21 | type, 22 | body: extend(payload, { timestamp }) 23 | }) 24 | .then(resp => { 25 | return reply(constants.RESPONSE.OK); 26 | }) 27 | .catch(reply); 28 | 29 | }, 30 | config: { 31 | validate: { 32 | payload: Joi.object({ 33 | type: Joi.string().only('error', 'warning', 'info').default('info'), 34 | content: Joi.string().required() 35 | }) 36 | } 37 | } 38 | }); 39 | }; 40 | -------------------------------------------------------------------------------- /public/components/notification_center/lib/polling_notifications.js: -------------------------------------------------------------------------------- 1 | import { chain } from 'lodash'; 2 | import { getVisible, addBasePath } from 'ui/chrome'; 3 | import { addSystemApiHeader } from 'ui/system_api'; 4 | 5 | export function pollingNotifications($timeout, $http, NotificationCenter, Notifier) { 6 | if (!getVisible()) { 7 | return; 8 | } 9 | 10 | const { config } = NotificationCenter; 11 | const notify = new Notifier(); 12 | $timeout(function pullNotifications() { 13 | return $http.get(addBasePath('/api/notification_center/notification'), { 14 | headers: addSystemApiHeader({}), 15 | params: { 16 | from: config.get('lastPulledAt'), 17 | size: config.get('maxSize') 18 | } 19 | }) 20 | .then(({ data }) => { 21 | const notifications = data || []; 22 | const lastPulledAt = chain(notifications) 23 | .forEach(notification => { 24 | notify[notification.type || 'info'](notification.content); 25 | }) 26 | .map('timestamp') 27 | .max() 28 | .value(); 29 | 30 | if (lastPulledAt > 0) { 31 | config.set('lastPulledAt', lastPulledAt); 32 | config.save(); 33 | } 34 | }) 35 | .then(() => $timeout(pullNotifications, config.get('pollingInterval'))); 36 | }, config.get('pollingInterval')); 37 | }; -------------------------------------------------------------------------------- /public/nav_control/nav_control.js: -------------------------------------------------------------------------------- 1 | import { constant, includes } from 'lodash'; 2 | import { element } from 'angular'; 3 | import { getInjected } from 'ui/chrome'; 4 | import { uiModules } from 'ui/modules'; 5 | import { chromeNavControlsRegistry } from 'ui/registry/chrome_nav_controls'; 6 | import '../components/notification_center'; 7 | import template from './nav_control.html'; 8 | import 'ui/angular-bootstrap'; 9 | 10 | chromeNavControlsRegistry.register(constant({ 11 | name: 'notification_center', 12 | order: 1000, 13 | template 14 | })); 15 | 16 | const supportDarkTheme = getInjected('notificationCenter.supportDarkTheme', true); 17 | const module = uiModules.get('notification_center', []); 18 | module.controller('notificationCenterNavController', ($scope, $compile, $document, NotificationCenter) => { 19 | function initNotificationCenter() { 20 | const $elem = $scope.$notificationCenter = $compile(``)($scope) 21 | .toggleClass('support-dark-theme', supportDarkTheme) 22 | .appendTo('.app-wrapper-panel'); 23 | $document.on('click', () => $elem.hide()); 24 | $elem.on('click', e => e.stopPropagation()); 25 | }; 26 | 27 | $scope.openNotificationCenter = event => { 28 | event.preventDefault(); 29 | if (!$scope.$notificationCenter) { 30 | initNotificationCenter(); 31 | } else { 32 | $scope.$notificationCenter.toggle(); 33 | } 34 | event.stopPropagation(); 35 | }; 36 | 37 | $scope.formatTooltip = () =>{ 38 | return `${NotificationCenter.notifications.length} new notifications`; 39 | }; 40 | }); 41 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | import { resolve } from 'path'; 2 | import { init } from './server/init'; 3 | import { replaceInjectedVars } from './server/lib/replace_injected_vars'; 4 | 5 | export default function (kibana) { 6 | return new kibana.Plugin({ 7 | id: 'notification_center', 8 | configPrefix: 'notification_center', 9 | require: ['elasticsearch'], 10 | name: 'notification_center', 11 | publicDir: resolve(__dirname, 'public'), 12 | uiExports: { 13 | 14 | chromeNavControls: [ 15 | 'plugins/notification_center/nav_control/nav_control' 16 | ], 17 | 18 | injectDefaultVars(server) { 19 | return { 20 | notificationCenter: { 21 | supportDarkTheme: server.config().get('notification_center.supportDarkTheme') 22 | } 23 | }; 24 | }, 25 | 26 | replaceInjectedVars 27 | 28 | }, 29 | 30 | config(Joi) { 31 | return Joi.object({ 32 | enabled: Joi.boolean().default(true), 33 | index: Joi.string().default('notification-%{+YYYY.MM.DD}'), 34 | template: Joi.object({ 35 | name: Joi.string().default('notification_center_template'), 36 | overwrite: Joi.boolean().default(false) 37 | }).default(), 38 | api: Joi.object({ 39 | enabled: Joi.boolean().default(true), 40 | pull: Joi.object({ 41 | maxSize: Joi.number().default(100) 42 | }).default() 43 | }).default(), 44 | supportDarkTheme: Joi.boolean().default(true) 45 | }).default(); 46 | }, 47 | 48 | init 49 | 50 | }); 51 | }; -------------------------------------------------------------------------------- /TRANSLATION.md: -------------------------------------------------------------------------------- 1 | A Kibana translation plugin structure. 2 | 3 | The main goal is to keep the plugin extremely simple so non-technical translators will have no trouble 4 | creating new translations for Kibana. Everything except for the translations themselves can be generated 5 | automatically with some enhancements to the Kibana plugin generator. The generator would only need a 6 | plugin name and a list of one or more languages the user wants to create translations for. 7 | 8 | The plugin exports its translation file(s) on the server when it it starts up. This is achieved by publishing the files 9 | via 'uiExports'.This is configurable by modifying the 'translations' item in the 'uiExports'. 10 | 11 | Translation files are broken up by language and must have names that match IETF BCP 47 language codes. 12 | Each translation file contains a single flat object with translation strings matched to their unique keys. Keys are 13 | prefixed with plugin names and a dash to ensure uniqueness between plugins. A translation plugin is not restricted to 14 | providing translations only for itself, the provided translations can cover other plugins as well. 15 | 16 | For example, this template plugin shows how a third party plugin might provide spanish translations for the Kibana core "kibana" app, which is itself a separate plugin. 17 | 18 | To create a translation plugin using this template, follow these steps: 19 | 1. Generate the plugin using the generator 20 | 2. Add your translations files to /translations directory. Remove/Overwrite the existing translation file (i.e. 'es.json'). 21 | 3. Edit /index.js, updating the 'translations' item as per your plugin translations. 22 | 4. Restart the Kibana server to publish your plugin translations. 23 | -------------------------------------------------------------------------------- /server/routes/notification/pull.js: -------------------------------------------------------------------------------- 1 | import { defaults, set, get } from 'lodash'; 2 | import Joi from 'joi'; 3 | import moment from 'moment'; 4 | import { constants } from '../../lib/constants'; 5 | import { parseWithWildcard } from '../../lib/parse_index_pattern'; 6 | 7 | export function pull(server) { 8 | const index = parseWithWildcard(server.config().get('notification_center.index')); 9 | const type = 'notification'; 10 | const { callWithRequest } = server.plugins.elasticsearch.getCluster('data'); 11 | const { maxSize } = server.config().get('notification_center.api.pull'); 12 | 13 | server.route({ 14 | path: `${constants.API_BASE_URL}/notification`, 15 | method: 'GET', 16 | handler(request, reply) { 17 | const { size, from, to } = request.query; 18 | 19 | callWithRequest(request, 'search', { 20 | index, 21 | type, 22 | size, 23 | ignoreUnavailable: true, 24 | body: (() => { 25 | const rangeQueries = []; 26 | 27 | if (from) { 28 | rangeQueries.push(set({}, 'range.timestamp.gt', moment(from).valueOf())); 29 | } 30 | 31 | if (to) { 32 | rangeQueries.push(set({}, 'range.timestamp.lt', moment(to).valueOf())); 33 | } 34 | 35 | return rangeQueries.length ? set({}, 'query.bool.must', rangeQueries) : undefined; 36 | })() 37 | }) 38 | .then(resp => { 39 | return reply(get(resp, 'hits.hits', []).map(hit => hit._source)); 40 | }) 41 | .catch(reply); 42 | }, 43 | config: { 44 | validate: { 45 | query: Joi.object({ 46 | size: Joi.number().min(1).max(maxSize).default(maxSize), 47 | from: Joi.date().optional(), 48 | to: Joi.date().optional() 49 | }).default() 50 | } 51 | } 52 | }); 53 | }; 54 | -------------------------------------------------------------------------------- /public/components/notification_center/index.less: -------------------------------------------------------------------------------- 1 | @globalTextColor--darkTheme: #cecece; 2 | @globalLinkColor-isHover--darkTheme: #def2f6; 3 | @globalColorMediumGray: #999; 4 | @globalColorDarkGray: #666; 5 | @borderColor--darkTheme: #000; 6 | @backgroundColor--darkTheme: #525252; 7 | 8 | notification-center { 9 | position: fixed!important; 10 | right: 0; 11 | width: 500px; 12 | max-width: ~'calc(100% - 53px)'; 13 | height: 100%; 14 | background-color: #fff; 15 | border-left: 1px solid #6EADC1; 16 | box-shadow: 0 5px 22px rgba(0, 0, 0, 0.25); 17 | overflow-y: scroll; 18 | z-index: 100; 19 | 20 | .kuiEventBody__message { 21 | p:last-child { 22 | margin: 0; 23 | }; 24 | }; 25 | 26 | .notification-stack { 27 | &:last-child { 28 | padding-bottom: 0; 29 | }; 30 | 31 | pre { 32 | margin: 0; 33 | }; 34 | }; 35 | 36 | .theme-dark+&.support-dark-theme { 37 | background-color: @backgroundColor--darkTheme; 38 | border-color: @borderColor--darkTheme; 39 | color: @globalTextColor--darkTheme; 40 | 41 | .kuiView { 42 | background-color: @backgroundColor--darkTheme; 43 | border-color: @borderColor--darkTheme; 44 | } 45 | 46 | .kuiHeaderBar { 47 | border-color: @globalColorDarkGray; 48 | } 49 | 50 | .kuiMenuItem { 51 | border-color: @globalColorDarkGray; 52 | } 53 | 54 | .kuiMenuButton--basic { 55 | background-color: @backgroundColor--darkTheme; 56 | color: @globalTextColor--darkTheme; 57 | 58 | &:hover { 59 | color: #fff!important; 60 | } 61 | } 62 | 63 | .kuiEventBody__message { 64 | color: @globalTextColor--darkTheme; 65 | } 66 | 67 | .kuiEventBody__metadata { 68 | color: @globalColorMediumGray; 69 | } 70 | 71 | .well { 72 | background-color: @globalColorDarkGray; 73 | color: @globalTextColor--darkTheme; 74 | } 75 | } 76 | }; -------------------------------------------------------------------------------- /public/components/notification_center/lib/stored_notifications.js: -------------------------------------------------------------------------------- 1 | import { find, isArray, remove, chain, pick, defaults, union } from 'lodash'; 2 | 3 | const NOT_INSERTABLE_ERROR = new Error('StoredNotifications cannot insert directly. Please use `StoredNotifications.merge`.'); 4 | export class StoredNotifications extends Array { 5 | constructor() { 6 | super(); 7 | this.key = `KBN::NOTIFS`; 8 | 9 | this.push = (...items) => { 10 | throw NOT_INSERTABLE_ERROR; 11 | }; 12 | 13 | this.unshift = (...items) => { 14 | throw NOT_INSERTABLE_ERROR; 15 | }; 16 | 17 | this.merge = (...items) => { 18 | const timestamp = new Date().valueOf(); 19 | const freshItems = chain(items) 20 | .filter(item => { 21 | const mergeable = find(this, pick(item, ['type', 'content'])); 22 | if (mergeable) { 23 | mergeable.timestamp = item.timestamp; 24 | mergeable.count += (item.count || 1); 25 | mergeable.stacks = union(mergeable.stacks, item.stacks); 26 | } 27 | return !mergeable; 28 | }) 29 | .map(item => defaults(item, { timestamp, count: 1 })) 30 | .value(); 31 | const result = super.push(...freshItems); 32 | this.save(); 33 | return result; 34 | }; 35 | 36 | this.remove = (predicate) => { 37 | const result = remove(this, predicate); 38 | this.save(); 39 | return result; 40 | }; 41 | 42 | this.save = () => { 43 | const requiredFields = ['timestamp', 'type', 'content', 'stack', 'stacks', 'title', 'icon', 'count']; 44 | const items = this.map(item => pick(item,requiredFields)); 45 | localStorage.setItem(this.key, JSON.stringify(items)); 46 | return this; 47 | }; 48 | 49 | this.load = () => { 50 | try { 51 | const storedList = JSON.parse(localStorage.getItem(this.key)); 52 | if (isArray(storedList)) { 53 | this.length = 0; 54 | this.merge(...storedList); 55 | } 56 | } catch (e) { 57 | console.warn('Failed to load stored notifications.\n', e); 58 | } 59 | return this; 60 | }; 61 | 62 | this.isEmpty = () => { 63 | return this.length === 0; 64 | }; 65 | }; 66 | 67 | }; 68 | -------------------------------------------------------------------------------- /public/components/notification_center/index.js: -------------------------------------------------------------------------------- 1 | import { chain } from 'lodash'; 2 | import { element } from 'angular'; 3 | import moment from 'moment-timezone'; 4 | import { uiModules } from 'ui/modules'; 5 | import { notify } from 'ui/notify'; 6 | import { StoredNotifications } from './lib/stored_notifications'; 7 | import { StoredConfig } from './lib/stored_config'; 8 | import { pollingNotifications } from './lib/polling_notifications'; 9 | import { getNotificationClasses } from './lib/get_notification_classes'; 10 | import template from './template.html'; 11 | import './index.less'; 12 | 13 | const module = uiModules.get('notification_center', []); 14 | 15 | module.run(pollingNotifications); 16 | 17 | module.service('NotificationCenter', () => { 18 | const notifications = new StoredNotifications().load(); 19 | const config = new StoredConfig({ 20 | pollingInterval: 10000, 21 | lastPulledAt: Date.now() 22 | }).load().save(); 23 | 24 | return { 25 | notifications, 26 | config 27 | }; 28 | }); 29 | 30 | module.directive('notificationCenter', (config, NotificationCenter, $filter) => { 31 | return { 32 | restrict: 'E', 33 | template, 34 | controller: ($scope) => { 35 | const notifs = $scope.notifs = NotificationCenter.notifications; 36 | $scope.$watchCollection(() => notify._notifs, change => { 37 | const timestamp = new Date().valueOf(); 38 | const newNotifs = chain(change) 39 | .filter(notif => !notif.timestamp) 40 | .forEach(notif => notif.timestamp = timestamp) 41 | .value(); 42 | if (newNotifs.length) { 43 | notifs.merge(...newNotifs); 44 | } 45 | }); 46 | 47 | config.watch('dateFormat', setDateFormat, $scope); 48 | config.watch('dateFormat:tz', setDefaultTimezone, $scope); 49 | config.watch('dateFormat:dow', setStartDayOfWeek, $scope); 50 | 51 | function setDateFormat(format) { 52 | $scope.dateFormat = format; 53 | }; 54 | 55 | function setDefaultTimezone(tz) { 56 | moment.tz.setDefault(tz); 57 | } 58 | 59 | function setStartDayOfWeek(day) { 60 | const dow = moment.weekdays().indexOf(day); 61 | moment.updateLocale(moment.locale(), { week: { dow } }); 62 | } 63 | 64 | setDateFormat(config.get('dateFormat')); 65 | 66 | $scope.moment = moment; 67 | $scope.getNotificationClasses = getNotificationClasses; 68 | } 69 | }; 70 | }); 71 | -------------------------------------------------------------------------------- /public/components/notification_center/template.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
5 |
6 |
7 |

Notification Center

8 |
9 |
10 | 13 |
14 |
15 |
16 |
17 |

18 | No new notifications. 19 |

20 |
21 |
24 |
25 |
26 | 27 |
28 |
29 |
30 | 36 |
37 | 43 |
44 |
45 | 75 |
76 |
77 | 78 |
79 |
80 |

81 |             
82 |
83 |
84 |
85 |
86 |
87 |
-------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Notification Center 2 | 3 | Notification Center is a kibana plugin for better experience of notifier toasts. 4 | 5 | ![Demo](demo.gif) 6 | 7 | ## Features 8 | 9 | ### Webhook Endpoint 10 | 11 | Provides a webhook endpoint that can generate a notification toast. 12 | 13 | This feature is useful with the x-pack alerting. 14 | 15 | * See [API](#api) for usage. 16 | 17 | ### Notification History 18 | 19 | You can see notification history in the notification center.(It's appear when you click the navbar button.) 20 | 21 | This feature is useful when there are notifications that not be checked, such as when you are not looking at the screen. 22 | 23 | In particular, you can check notifications received via [webhook](#webhook-endpoint) when you are not connected. 24 | 25 | Of course, you can clear the checked notification. 26 | 27 | > **NOTE: Not only notifications pushed through api, all notifications of generated by kibana's `Notifier` are displayed in notification center panel.** 28 | 29 | ## Installation 30 | 31 | Copy installation file's url as same version of your kibana from [the repository releases](https://github.com/sw-jung/kibana_notification_center/releases). 32 | 33 | And 34 | ```bash 35 | $ cd path/to/your/kibana 36 | $ bin/kibana-plugin install 37 | ``` 38 | 39 | If you want more information, See [this document](https://www.elastic.co/guide/en/kibana/current/_installing_plugins.html). 40 | 41 | > **NOTE: How can I do if I cannot find version I want?** 42 | > I'm sorry for that. This plugin not all of kibana versions are supported. 43 | > If you need unsupported version, please test nearest version. 44 | > In this case, you need to modify the kibana.version field in package.json. 45 | > **Currently minimum support version is v5.4.3** 46 | 47 | ## Configuration 48 | 49 | The following configurable options are set by default. 50 | 51 | ### kibana.yml 52 | 53 | ```yml 54 | notification_center: 55 | enabled: true 56 | 57 | # Name of the index where pushed notification is stored. 58 | # You can use momentjs's date format in wrapped by `%{}`. See https://momentjs.com/docs/#/displaying/format/ 59 | index: "notification-%{+YYYY.MM.DD}" 60 | 61 | # The index template is created when you start kibana. 62 | template: 63 | name: "notification_center_template" 64 | overwrite: false 65 | 66 | api: 67 | # If `false` then disable all of REST APIs. 68 | enabled: true 69 | # The maximum size of the notification that user can pull from the server in a single request. 70 | pull.maxSize: 100 71 | 72 |  supportDarkTheme: true 73 | ``` 74 | 75 | ## API 76 | 77 | ### Push notification 78 | 79 | * **URL**: /api/notification_center/notification 80 | * **Method**: POST|PUT 81 | * **Headers** 82 | ```javascript 83 | { 84 |  "Authorization": "your credential token here", // If you use x-pack security. 85 | "Content-Type": "application/json", 86 | "kbn-version": "your kibana version here" // This is kibana spec. ex: 5.6.0 87 | } 88 | ``` 89 | 90 | * **Data Params** 91 | ```javascript 92 | { 93 | "type": "error|warning|info" // Optional. Default "info" 94 | "content": "Write your notification content here." // Required 95 | } 96 | ``` 97 | 98 | * **Response** 99 | ```javascript 100 | // 200 OK 101 | { 102 | "acknowledged": true 103 | } 104 | 105 | // The others are error. 106 | ``` 107 | --- 108 | 109 | ## development 110 | 111 | See the [kibana contributing guide](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md) for instructions setting up your development environment. Once you have completed that, use the following npm tasks. 112 | 113 | - `npm start` 114 | 115 | Start kibana and have it include this plugin 116 | 117 | - `npm start -- --config kibana.yml` 118 | 119 | You can pass any argument that you would normally send to `bin/kibana` by putting them after `--` when running `npm start` 120 | 121 | - `npm run build` 122 | 123 | Build a distributable archive 124 | 125 | - `npm run test:browser` 126 | 127 | Run the browser tests in a real web browser 128 | 129 | - `npm run test:server` 130 | 131 | Run the server tests using mocha 132 | 133 | For more information about any of these commands run `npm run ${task} -- --help`. 134 | --------------------------------------------------------------------------------