From fb0814fcfde08ce7ce25f840b9198f0dad788160 Mon Sep 17 00:00:00 2001 From: Spencer Alger Date: Wed, 12 Mar 2014 10:40:18 -0700 Subject: [PATCH] Initial phase of notification service. --- src/index.html | 9 +- src/kibana/controllers/kibana.js | 52 +++++++- src/kibana/index.js | 11 +- src/kibana/notify/directives.js | 135 ++++++++++++++++++++ src/kibana/notify/errors.js | 72 +++++++++++ src/kibana/notify/manager.js | 163 ++++++++++++++++++++++++ src/kibana/notify/notify.js | 93 ++++++++++++++ src/kibana/notify/partials/fatal.html | 20 +++ src/kibana/notify/partials/toaster.html | 40 ++++++ src/kibana/require.config.js | 18 +-- src/kibana/services/config.js | 17 +-- src/kibana/services/es.js | 2 +- src/kibana/setup.js | 54 ++++++-- src/kibana/styles/_notify.less | 50 ++++++++ src/kibana/styles/main.css | 57 +++++++++ src/kibana/styles/main.less | 19 +++ src/kibana/utils/mutable_watcher.js | 48 +++++++ 17 files changed, 820 insertions(+), 40 deletions(-) create mode 100644 src/kibana/notify/directives.js create mode 100644 src/kibana/notify/errors.js create mode 100644 src/kibana/notify/manager.js create mode 100644 src/kibana/notify/notify.js create mode 100644 src/kibana/notify/partials/fatal.html create mode 100644 src/kibana/notify/partials/toaster.html create mode 100644 src/kibana/styles/_notify.less create mode 100644 src/kibana/utils/mutable_watcher.js diff --git a/src/index.html b/src/index.html index 4295a290a0729..d57d6d69a7500 100644 --- a/src/index.html +++ b/src/index.html @@ -22,9 +22,12 @@ @@ -34,7 +37,7 @@ config-submit="saveOpts">
- +
diff --git a/src/kibana/controllers/kibana.js b/src/kibana/controllers/kibana.js index b8a9189aa4806..0b9e906f08489 100644 --- a/src/kibana/controllers/kibana.js +++ b/src/kibana/controllers/kibana.js @@ -6,10 +6,19 @@ define(function (require) { require('services/config'); require('services/courier'); require('directives/view'); + require('angular-bootstrap'); require('modules') - .get('kibana/controllers') - .controller('kibana', function ($scope, courier, config, configFile) { + .get('kibana/controllers', ['ui.bootstrap']) + .config(function ($tooltipProvider) { + $tooltipProvider.options({ + placement: 'bottom', + animation: true, + popupDelay: 150, + appendToBody: false + }); + }) + .controller('kibana', function ($scope, courier, config, configFile, notify, $timeout) { $scope.apps = configFile.apps; $scope.$on('$locationChangeSuccess', function (event, uri) { @@ -38,6 +47,38 @@ define(function (require) { $scope.configureTemplateUrl = require('text!../partials/global_config.html'); }; + // expose the notification services list of notifs on the $scope so that the + // notification directive can show them on the screen + $scope.notifList = notify._notifs; + // provide alternate methods for setting timeouts, which will properly trigger digest cycles + notify._setTimerFns($timeout, $timeout.cancel); + + (function TODO_REMOVE() { + // stuff for testing notifications + $scope.levels = [ + { name: 'info', icon: 'info' }, + { name: 'warning', icon: 'info-circle' }, + { name: 'error', icon: 'warning' }, + { name: 'fatal', icon: 'fire' }, + ]; + $scope.notifTest = function (type) { + var arg = 'Something happened, just thought you should know.'; + var cb; + if (type === 'fatal' || type === 'error') { + arg = new Error('Ah fuck'); + } + if (type === 'error') { + cb = function (resp) { + if (resp !== 'report') return; + $timeout(function () { + notify.info('Report sent, thank you for your help.'); + }, 750); + }; + } + notify[type](arg, cb); + }; + }()); + /** * Persist current settings * @return {[type]} [description] @@ -71,6 +112,13 @@ define(function (require) { config.$watch('refreshInterval', $scope.setFetchInterval); $scope.$watch('opts.activeFetchInterval', $scope.setFetchInterval); + + // setup the courier + courier.on('error', function (err) { + $scope[$scope.$$phase ? '$eval' : '$apply'](function () { + notify.error(err); + }); + }); $scope.$on('application.load', function () { courier.start(); }); diff --git a/src/kibana/index.js b/src/kibana/index.js index 08eba44d71144..f154fef269dae 100644 --- a/src/kibana/index.js +++ b/src/kibana/index.js @@ -10,6 +10,7 @@ define(function (require) { var setup = require('./setup'); var configFile = require('../config'); var modules = require('modules'); + var notify = require('notify/notify'); require('elasticsearch'); require('angular-route'); @@ -46,8 +47,14 @@ define(function (require) { return 'apps/' + app.id + '/index'; })), function bootstrap() { $(function () { - angular.bootstrap(document, ['kibana']); - $(document.body).children().show(); + notify.lifecycle('bootstrap'); + angular + .bootstrap(document, ['kibana']) + .invoke(function (notify) { + notify.lifecycle('bootstrap', true); + $(document.body).children().show(); + }); + }); }); diff --git a/src/kibana/notify/directives.js b/src/kibana/notify/directives.js new file mode 100644 index 0000000000000..f5db632846713 --- /dev/null +++ b/src/kibana/notify/directives.js @@ -0,0 +1,135 @@ +define(function (require) { + var notify = require('modules').get('notify'); + var _ = require('lodash'); + var $ = require('jquery'); + var MutableWatcher = require('utils/mutable_watcher'); + var nextTick = require('utils/next_tick'); + + var defaultToastOpts = { + title: 'Notice', + lifetime: 7000 + }; + + var transformKey = (function () { + var el = document.createElement('div'); + return _.find(['transform', 'webkitTransform', 'OTransform', 'MozTransform', 'msTransform'], function (key) { + return el.style[key] !== void 0; + }); + }()); + + notify.directive('kbnNotifications', function () { + return { + restrict: 'A', + scope: { + list: '=list' + }, + template: require('text!./partials/toaster.html'), + link: function ($scope, $el) { + + $el.addClass('toaster-container'); + + // handles recalculating positions and offsets, schedules + // recalcs and waits for 100 seconds before running again. + var layoutList = (function () { + // lazy load the $nav element + var navSelector = '.content > nav.navbar:first()'; + var $nav; + + // pixels between the top of list and it's attachment(nav/window) + var spacing = 10; + // was the element set to postition: fixed last calc? + + var visible = false; + + var recalc = function () { + // set $nav lazily + if (!$nav || !$nav.length) $nav = $(navSelector); + + // if we can't find the nav, don't display the list + if (!$nav.length) return; + + // the top point at which the list should be secured + var fixedTop = $nav.height(); + + // height of the section at the top of the page that is hidden + var hiddenBottom = document.body.scrollTop; + + var top, left, css = { + visibility: 'visible' + }; + + if (hiddenBottom > fixedTop) { + // if we are already fixed, no reason to set the styles again + css.position = 'fixed'; + top = spacing; + } else { + css.position = 'absolute'; + top = fixedTop + spacing; + } + + // calculate the expected left value (keep it centered) + left = Math.floor((document.body.scrollWidth - $el.width()) / 2); + css[transformKey] = 'translateX(' + Math.round(left) + 'px) translateY(' + Math.round(top) + 'px)'; + if (transformKey !== 'msTransform') { + // The Z transform will keep this in the GPU (faster, and prevents artifacts), + // but IE9 doesn't support 3d transforms and will choke. + css[transformKey] += ' translateZ(0)'; + } + + $el.css(css); + }; + + // track the already scheduled recalcs + var timeoutId; + var clearSchedule = function () { + timeoutId = null; + }; + + var schedule = function () { + if (timeoutId) return; + else recalc(); + timeoutId = setTimeout(clearSchedule, 25); + }; + + // call to remove the $el from the view + schedule.hide = function () { + $el.css('visibility', 'hidden'); + visible = false; + }; + + return schedule; + }()); + + function listen(off) { + $(window)[off ? 'off' : 'on']('resize scroll', layoutList); + } + + var wat = new MutableWatcher({ + $scope: $scope, + expression: 'list', + type: 'collection' + }, showList); + + function showList(list) { + if (list && list.length) { + listen(); + wat.set(hideList); + + // delay so that angular has time to update the DOM + nextTick(layoutList); + } + } + + function hideList(list) { + if (!list || !list.length) { + listen(true); + wat.set(showList); + layoutList.hide(); + } + } + + $scope.$on('$destoy', _.partial(listen, true)); + } + }; + }); +}); \ No newline at end of file diff --git a/src/kibana/notify/errors.js b/src/kibana/notify/errors.js new file mode 100644 index 0000000000000..643efc3e8f87c --- /dev/null +++ b/src/kibana/notify/errors.js @@ -0,0 +1,72 @@ +define(function (require) { + var errors = {}; + var _ = require('lodash'); + var inherits = require('utils/inherits'); + + var canStack = (function () { + var err = new Error(); + return !!err.stack; + }()); + + // abstract error class + function KibanaError(msg, constructor) { + this.message = msg; + + Error.call(this, this.message); + if (!this.stack) { + if (Error.captureStackTrace) { + Error.captureStackTrace(this, constructor || KibanaError); + } else if (canStack) { + this.stack = (new Error()).stack; + } else { + this.stack = ''; + } + } + } + errors.KibanaError = KibanaError; + inherits(KibanaError, Error); + + /** + * Map of error text for different error types + * @type {Object} + */ + var requireTypeText = { + timeout: 'a network timeout', + nodefine: 'an invalid module definition', + scripterror: 'a generic script error' + }; + + /** + * ScriptLoadFailure error class for handling requirejs load failures + * @param {String} [msg] - + */ + errors.ScriptLoadFailure = function ScriptLoadFailure(err) { + var explain = requireTypeText[err.requireType] || err.requireType || 'an unknown error'; + + this.stack = err.stack; + var modules = err.requireModules; + if (_.isArray(modules) && modules.length > 0) { + modules = modules.map(JSON.stringify); + + if (modules.length > 1) { + modules = modules.slice(0, -1).join(', ') + ' and ' + modules.slice(-1)[0]; + } else { + modules = modules[0]; + } + + modules += ' modules'; + } + + if (!modules || !modules.length) { + modules = 'unknown modules'; + } + + + KibanaError.call(this, + 'Unable to load ' + modules + ' because of ' + explain + '.', + errors.ScriptLoadFailure); + }; + inherits(errors.ScriptLoadFailure, KibanaError); + + return errors; +}); \ No newline at end of file diff --git a/src/kibana/notify/manager.js b/src/kibana/notify/manager.js new file mode 100644 index 0000000000000..be0916ff0100d --- /dev/null +++ b/src/kibana/notify/manager.js @@ -0,0 +1,163 @@ +define(function (require) { + var _ = require('lodash'); + var $ = require('jquery'); + + var fatalToastTemplate = (function lazyTemplate(tmpl) { + var compiled; + return function (vars) { + compiled = compiled || _.template(tmpl); + return compiled(vars); + }; + }(require('text!./partials/fatal.html'))); + + /** + * Functionality to check that + */ + function NotifyManager() { + + var applicationBooted; + var notifs = this._notifs = []; + var setTO = setTimeout; + var clearTO = clearTimeout; + + function now() { + if (window.performance && window.performance.now) { + return window.performance.now(); + } + return Date.now(); + } + + var log = (typeof KIBANA_DIST === 'undefined') ? _.bindKey(console, 'log') : _.noop; + + function closeNotif(cb, key) { + return function () { + // this === notif + var i = notifs.indexOf(this); + if (i !== -1) notifs.splice(i, 1); + if (this.timerId) this.timerId = clearTO(this.timerId); + if (typeof cb === 'function') cb(key); + }; + } + + function add(notif, cb) { + if (notif.lifetime !== Infinity) { + notif.timerId = setTO(function () { + closeNotif(cb, 'ignore').call(notif); + }, notif.lifetime); + } + + if (notif.actions) { + notif.actions.forEach(function (action) { + notif[action] = closeNotif(cb, action); + }); + } + + notifs.push(notif); + } + + this._setTimerFns = function (set, clear) { + setTO = set; + clearTO = clear; + }; + + /** + * Notify the serivce of app lifecycle events + * @type {[type]} + */ + var lifecycleEvents = window.kibanaLifecycleEvents = {}; + this.lifecycle = function (name, success) { + var status; + if (name === 'bootstrap' && success === true) applicationBooted = true; + + if (success === void 0) { + // start + lifecycleEvents[name] = now(); + } else { + // end + if (success) { + lifecycleEvents[name] = now() - (lifecycleEvents[name] || 0); + status = lifecycleEvents[name].toFixed(2) + ' ms'; + } else { + lifecycleEvents[name] = false; + status = 'failure'; + } + } + + log('KBN: ' + name + (status ? ' - ' + status : '')); + }; + + /** + * Kill the page, and display an error + * @param {Error} err - The fatal error that occured + */ + this.fatal = function (err) { + var html = fatalToastTemplate({ + msg: err instanceof Error ? err.message : err, + stack: err.stack + }); + + var $container = $('#fatal-splash-screen'); + if ($container.size()) { + $container.append(html); + return; + } + + $container = $(); + + // in case the app has not completed boot + $(document.body) + .removeAttr('ng-cloak') + .html('
' + html + '
'); + }; + + /** + * Alert the user of an error that occured + * @param {Error|String} err + */ + this.error = function (err, cb) { + add({ + type: 'danger', + content: err instanceof Error ? err.message : err, + icon: 'warning', + title: 'Error', + lifetime: Infinity, + actions: ['report', 'accept'] + }, cb); + }; + + /** + * Warn the user abort something + * @param {[type]} msg [description] + * @return {[type]} [description] + */ + this.warning = function (msg, cb) { + add({ + type: 'warning', + content: msg, + icon: 'warning', + title: 'Warning', + lifetime: 7000, + actions: ['accept'] + }, cb); + }; + + /** + * Display a debug message + * @param {String} msg [description] + * @return {[type]} [description] + */ + this.info = function (msg, cb) { + add({ + type: 'info', + content: msg, + icon: 'info-circle', + title: 'Debug', + lifetime: 7000, + actions: ['accept'] + }, cb); + }; + } + + return NotifyManager; + +}); \ No newline at end of file diff --git a/src/kibana/notify/notify.js b/src/kibana/notify/notify.js new file mode 100644 index 0000000000000..ec246d14dacc5 --- /dev/null +++ b/src/kibana/notify/notify.js @@ -0,0 +1,93 @@ +define(function (require) { + var _ = require('lodash'); + var nextTick = require('utils/next_tick'); + var $ = require('jquery'); + var modules = require('modules'); + var module = modules.get('notify'); + var errors = require('./errors'); + var NotifyManager = require('./manager'); + var manager = new NotifyManager(); + + require('./directives'); + + module.service('notify', function () { + var service = this; + // modify the service to have bound proxies to the manager + _.forOwn(manager, function (val, key) { + service[key] = typeof val === 'function' ? _.bindKey(manager, key) : val; + }); + }); + + /** + * Global Angular uncaught exception handler + */ + modules + .get('exceptionOverride') + .factory('$exceptionHandler', function () { + return function (exception, cause) { + manager.fatal(exception, cause); + }; + }); + + /** + * Global Require.js exception handler + */ + window.requirejs.onError = function (err) { + manager.fatal(new errors.ScriptLoadFailure(err)); + }; + + window.onerror = function (err, url, line) { + manager.fatal(new Error(err + ' (' + url + ':' + line + ')')); + return true; + }; + + // function onTabFocus(onChange) { + // var current = true; + // // bind each individually + // var elem = window; + // var focus = 'focus'; + // var blur = 'blur'; + + // if (/*@cc_on!@*/false) { // check for Internet Explorer + // elem = document; + // focus = 'focusin'; + // blur = 'focusout'; + // } + + // function handler(event) { + // var state; + + // if (event.type === focus) { + // state = true; + // } else if (event.type === blur) { + // state = false; + // } else { + // return; + // } + + // if (current !== state) { + // current = state; + // onChange(current); + // } + // } + + // elem.addEventListener(focus, handler); + // elem.addEventListener(blur, handler); + + // // call the handler ASAP with the current status + // nextTick(handler, current); + + // // function that the user can call to unbind this handler + // return function unBind() { + // elem.removeEventListener(focus, handler); + // elem.removeEventListener(blur, handler); + // }; + // } + + // onTabFocus(function (focused) { + // // log(focused ? 'welcome back' : 'good bye'); + // }); + + return manager; + +}); \ No newline at end of file diff --git a/src/kibana/notify/partials/fatal.html b/src/kibana/notify/partials/fatal.html new file mode 100644 index 0000000000000..164a5d1ecb04a --- /dev/null +++ b/src/kibana/notify/partials/fatal.html @@ -0,0 +1,20 @@ + +
+
+

+ Fatal Error + + Reload + +

+
+
<%- msg %>
+ <% if (stack) { %> + + <% } %> +
\ No newline at end of file diff --git a/src/kibana/notify/partials/toaster.html b/src/kibana/notify/partials/toaster.html new file mode 100644 index 0000000000000..24ecf0c94e807 --- /dev/null +++ b/src/kibana/notify/partials/toaster.html @@ -0,0 +1,40 @@ + \ No newline at end of file diff --git a/src/kibana/require.config.js b/src/kibana/require.config.js index 55f1a6d68d9c3..9d2620139546e 100644 --- a/src/kibana/require.config.js +++ b/src/kibana/require.config.js @@ -6,6 +6,7 @@ require.config({ angular: '../bower_components/angular/angular', 'angular-mocks': '../bower_components/angular-mocks/angular-mocks', 'angular-route': '../bower_components/angular-route/angular-route', + 'angular-bootstrap': '../bower_components/angular-bootstrap/ui-bootstrap-tpls', async: '../bower_components/async/lib/async', css: '../bower_components/require-css/css', text: '../bower_components/requirejs-text/text', @@ -23,18 +24,11 @@ require.config({ deps: ['jquery'], exports: 'angular' }, - gridster: { - deps: ['jquery'] - }, - 'angular-route': { - deps: ['angular'] - }, - 'angular-mocks': { - deps: ['angular'] - }, - 'elasticsearch': { - deps: ['angular'] - } + gridster: ['jquery'], + 'angular-route': ['angular'], + 'angular-mocks': ['angular'], + 'elasticsearch': ['angular'], + 'angular-bootstrap': ['angular'] }, waitSeconds: 60 }); \ No newline at end of file diff --git a/src/kibana/services/config.js b/src/kibana/services/config.js index c999c8a9753b9..758aca1067e23 100644 --- a/src/kibana/services/config.js +++ b/src/kibana/services/config.js @@ -2,6 +2,7 @@ define(function (require) { var _ = require('lodash'); var nextTick = require('utils/next_tick'); var configFile = require('../../config'); + var notify = require('notify/notify'); require('services/courier'); @@ -40,17 +41,17 @@ define(function (require) { ******/ function init() { + notify.lifecycle('config init'); var defer = $q.defer(); - courier.fetch(); + doc.fetch(); doc.on('results', function completeInit(resp) { // ONLY ACT IF !resp.found if (!resp.found) { - console.log('creating empty config doc'); doc.doIndex({}); return; } - console.log('fetched config doc'); + notify.lifecycle('config init', !!resp); doc.removeListener('results', completeInit); defer.resolve(); }); @@ -87,7 +88,7 @@ define(function (require) { // probably a horrible idea if (!watchers[key]) watchers[key] = []; watchers[key].push(onChange); - _notify(onChange, vals[key]); + triggerWatchers(onChange, vals[key]); return function un$watcher() { _.pull(watchers[key], onChange); }; @@ -143,15 +144,15 @@ define(function (require) { *******/ function _change(key, val) { - _notify(watchers[key], val, vals[key]); + notify.lifecycle('config change: ' + key + ': ' + vals[key] + ' -> ' + val); + triggerWatchers(watchers[key], val, vals[key]); vals[key] = val; - console.log(key, 'is now', val); } - function _notify(fns, cur, prev) { + function triggerWatchers(fns, cur, prev) { if ($rootScope.$$phase) { // reschedule for next tick - nextTick(_notify, fns, cur, prev); + nextTick(triggerWatchers, fns, cur, prev); return; } diff --git a/src/kibana/services/es.js b/src/kibana/services/es.js index 195395dd73312..9154a4135b9ef 100644 --- a/src/kibana/services/es.js +++ b/src/kibana/services/es.js @@ -3,7 +3,7 @@ define(function (require) { var es; // share the client amoungst all apps require('modules') - .get('kibana/services') + .get('kibana/services', ['elasticsearch']) .service('es', function (esFactory, configFile, $q) { if (es) return es; diff --git a/src/kibana/setup.js b/src/kibana/setup.js index 63b593b8e2fdc..c013522e4bb35 100644 --- a/src/kibana/setup.js +++ b/src/kibana/setup.js @@ -2,6 +2,7 @@ define(function (require) { var angular = require('angular'); var async = require('async'); var $ = require('jquery'); + var _ = require('lodash'); var configFile = require('../config'); var nextTick = require('utils/next_tick'); var modules = require('modules'); @@ -15,30 +16,31 @@ define(function (require) { return function prebootSetup(done) { // load angular deps require([ - 'kibana', + 'notify/notify', - 'elasticsearch', 'services/es', 'services/config', 'constants/base' - ], function (kibana) { + ], function (notify) { $(function () { // create the setup module, it should require the same things // that kibana currently requires, which should only include the // loaded modules - var setup = modules.get('setup', ['elasticsearch']); + var setup = modules.get('setup'); var appEl = document.createElement('div'); var kibanaIndexExists; + modules.link(setup); setup .value('configFile', configFile); angular .bootstrap(appEl, ['setup']) - .invoke(function (es, config) { + .invoke(function (es, config, notify) { // init the setup module async.series([ + async.apply(checkForES, es), async.apply(checkForKibanaIndex, es), async.apply(createKibanaIndex, es), async.apply(checkForCurrentConfigDoc, es), @@ -50,25 +52,52 @@ define(function (require) { // linked modules should no longer depend on this module setup.close(); - console.log('booting kibana'); + if (err) throw err; return done(err); }); }); + function wrapError(err, tmpl) { + // if we pass a callback + if (typeof err === 'function') { + var cb = err; // wrap it + return function (err) { + cb(wrapError(err, tmpl)); + }; + } + + // if an error didn't actually occur + if (!err) return void 0; + + var err2 = new Error(_.template(tmpl, { configFile: configFile })); + err2.origError = err; + return err2; + } + + function checkForES(es, done) { + notify.lifecycle('es check'); + es.ping(function (err, alive) { + notify.lifecycle('es check', alive); + done(alive ? void 0 : new Error('Unable to connect to Elasticsearch at "' + configFile.elasticsearch + '"')); + }); + } + function checkForKibanaIndex(es, done) { + notify.lifecycle('kibana index check'); es.indices.exists({ index: configFile.kibanaIndex }, function (err, exists) { - console.log('kibana index does', (exists ? '' : 'not ') + 'exist'); + notify.lifecycle('kibana index check', !!exists); kibanaIndexExists = exists; - return done(err); + done(wrapError(err, 'Unable to check for Kibana index "<%= configFile.kibanaIndex %>"')); }); } // create the index if it doens't exist already function createKibanaIndex(es, done) { if (kibanaIndexExists) return done(); - console.log('creating kibana index'); + + notify.lifecycle('create kibana index'); es.indices.create({ index: configFile.kibanaIndex, body: { @@ -88,19 +117,20 @@ define(function (require) { } } } - }, done); + }, function (err) { + notify.lifecycle('create kibana index', !err); + done(wrapError(err, 'Unable to create Kibana index "<%= configFile.kibanaIndex %>"')); + }); } // if the index is brand new, no need to see if it is out of data function checkForCurrentConfigDoc(es, done) { if (!kibanaIndexExists) return done(); - console.log('checking if migration is necessary: not implemented'); // callbacks should always be called async nextTick(done); } function initConfig(config, done) { - console.log('initializing config service'); config.init().then(function () { done(); }, done); } }); diff --git a/src/kibana/styles/_notify.less b/src/kibana/styles/_notify.less new file mode 100644 index 0000000000000..f7cb2dd871983 --- /dev/null +++ b/src/kibana/styles/_notify.less @@ -0,0 +1,50 @@ +#fatal-splash-screen { + margin: 15px; +} + +.toaster-container { + position: absolute; + top: 0px; + left: 0px; + z-index: 1; + visibility: hidden; + width: 85%; + + .toaster { + margin: 0; + padding: 0; + list-style: none; + } + + .alert { + -webkit-box-shadow: 3px 0px 19px 0px rgba(50, 50, 50, 0.67); + -moz-box-shadow: 3px 0px 19px 0px rgba(50, 50, 50, 0.67); + box-shadow: 3px 0px 19px 0px rgba(50, 50, 50, 0.67); + padding: 0px 15px; + margin: 0 0 10px 0; + border: none; + + button.btn { + -webkit-border-radius: 0px; + -moz-border-radius: 0px; + border-radius: 0px; + border: none; + } + + table { + width: 100%; + td { + vertical-align: middle; + + &:first-child { + text-align: left; + width: 80%; + } + // :not(:first-child) + text-align: right; + width: 20%; + } + + } + } +} \ No newline at end of file diff --git a/src/kibana/styles/main.css b/src/kibana/styles/main.css index e828dbcf1a6f7..7645ff7271609 100644 --- a/src/kibana/styles/main.css +++ b/src/kibana/styles/main.css @@ -6842,6 +6842,21 @@ button.close { body { margin: 0px; } +.content { + position: relative; + z-index: 0; +} +.content .navbar { + position: relative; + z-index: 1; +} +.content .application { + position: relative; + z-index: 0; +} +notifications { + z-index: 1; +} .navbar-nav li a { cursor: pointer; } @@ -6998,6 +7013,48 @@ kbn-table .table .table td.field-name { kbn-table tr.even td { background-color: #f1f1f1; } +#fatal-splash-screen { + margin: 15px; +} +.toaster-container { + position: absolute; + top: 0px; + left: 0px; + z-index: 1; + visibility: hidden; + width: 85%; +} +.toaster-container .toaster { + margin: 0; + padding: 0; + list-style: none; +} +.toaster-container .alert { + -webkit-box-shadow: 3px 0px 19px 0px rgba(50, 50, 50, 0.67); + -moz-box-shadow: 3px 0px 19px 0px rgba(50, 50, 50, 0.67); + box-shadow: 3px 0px 19px 0px rgba(50, 50, 50, 0.67); + padding: 0px 15px; + margin: 0 0 10px 0; + border: none; +} +.toaster-container .alert button.btn { + -webkit-border-radius: 0px; + -moz-border-radius: 0px; + border-radius: 0px; + border: none; +} +.toaster-container .alert table { + width: 100%; +} +.toaster-container .alert table td { + vertical-align: middle; + text-align: right; + width: 20%; +} +.toaster-container .alert table td:first-child { + text-align: left; + width: 80%; +} disc-field-chooser ul { margin: 0; padding: 0; diff --git a/src/kibana/styles/main.less b/src/kibana/styles/main.less index 521dc52dafa8b..322c777675852 100644 --- a/src/kibana/styles/main.less +++ b/src/kibana/styles/main.less @@ -9,6 +9,24 @@ body { margin: 0px; } +.content { + position: relative; + z-index: 0; + + .navbar { + position: relative; + z-index: 1; + } + .application { + position: relative; + z-index: 0; + } +} + +notifications { + z-index: 1; +} + //== Subnav // // Use for adding a subnav to your app @@ -52,4 +70,5 @@ body { } @import "./_table.less"; +@import "./_notify.less"; @import "../apps/discover/styles/main.less"; diff --git a/src/kibana/utils/mutable_watcher.js b/src/kibana/utils/mutable_watcher.js new file mode 100644 index 0000000000000..1f4916bf0617c --- /dev/null +++ b/src/kibana/utils/mutable_watcher.js @@ -0,0 +1,48 @@ +define(function (require) { + + /** + * Helper to create a watcher than can be simply changed + * @param {[type]} opts [description] + * @param {[type]} initialFn [description] + */ + function MutableWatcher(opts, initialFn) { + opts = opts || {}; + + var $scope = opts.$scope; + if (!$scope) throw new TypeError('you must specify a scope.'); + + var expression = opts.expression; + if (!expression) throw new TypeError('you must specify an expression.'); + + // the watch method to call + var method = $scope[opts.type === 'collection' ? '$watchCollection' : '$watch']; + + // stores the unwatch function + var unwatcher; + + // change the function that the watcher triggers + function watch(watcher) { + if (typeof unwatcher === 'function') { + unwatcher(); + unwatcher = null; + } + + if (!watcher) return; + + // include the expression as the first argument + var args = [].slice.apply(arguments); + args.unshift(expression); + + // register a new unwatcher + unwatcher = method.apply($scope, args); + } + + watch(initialFn); + + // public API + this.set = watch; + } + + return MutableWatcher; + +}); \ No newline at end of file