From cfe85639fcb7c5295aa4aec80aca7b62460d1147 Mon Sep 17 00:00:00 2001 From: LeeDr Date: Thu, 30 Jun 2016 14:44:57 -0500 Subject: [PATCH 01/67] Add getSpinnerDone after clicking a new page in index pattern field list --- test/support/page_objects/settings_page.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/test/support/page_objects/settings_page.js b/test/support/page_objects/settings_page.js index e6beb1e9ff1e65..caabcc66078741 100644 --- a/test/support/page_objects/settings_page.js +++ b/test/support/page_objects/settings_page.js @@ -244,6 +244,9 @@ export default (function () { ) .then(function (page) { return page.click(); + }) + .then(function () { + return headerPage.getSpinnerDone(); }); }, From 583c89012c45a3610b41a36f2f46cedbdf79ed5c Mon Sep 17 00:00:00 2001 From: LeeDr Date: Thu, 30 Jun 2016 15:13:56 -0500 Subject: [PATCH 02/67] Add a debug log so we can tell what page we're going to if we fail --- test/support/page_objects/settings_page.js | 1 + 1 file changed, 1 insertion(+) diff --git a/test/support/page_objects/settings_page.js b/test/support/page_objects/settings_page.js index caabcc66078741..d27534df468f8e 100644 --- a/test/support/page_objects/settings_page.js +++ b/test/support/page_objects/settings_page.js @@ -238,6 +238,7 @@ export default (function () { }, goToPage: function (pageNum) { + common.debug('goToPage (' + pageNum + ')'); return this.remote.setFindTimeout(defaultFindTimeout) .findByCssSelector('ul.pagination-other-pages-list.pagination-sm.ng-scope li.ng-scope:nth-child(' + (pageNum + 1) + ') a.ng-binding' From 8df249beec21f1cc7e3ed0a39dccda435f379446 Mon Sep 17 00:00:00 2001 From: Shaunak Kashyap Date: Fri, 1 Jul 2016 03:00:13 -0700 Subject: [PATCH 03/67] Method to get nav link by its title + control nav link display state --- src/ui/public/chrome/api/__tests__/nav.js | 13 +++++++++++++ src/ui/public/chrome/api/nav.js | 4 ++++ .../directives/app_switcher/app_switcher.html | 5 +++-- .../chrome/directives/app_switcher/app_switcher.js | 12 +++++++++--- .../directives/app_switcher/app_switcher.less | 9 ++++++++- 5 files changed, 37 insertions(+), 6 deletions(-) diff --git a/src/ui/public/chrome/api/__tests__/nav.js b/src/ui/public/chrome/api/__tests__/nav.js index 8dffa7e4eebbe9..3e728dfc60ead1 100644 --- a/src/ui/public/chrome/api/__tests__/nav.js +++ b/src/ui/public/chrome/api/__tests__/nav.js @@ -45,6 +45,19 @@ describe('chrome nav apis', function () { }); }); + describe('#getNavLinkByTitle', () => { + it ('retrieves the correct nav link, given its title', () => { + const nav = [ + { title: 'Discover', url: 'https://localhost:9200/app/kibana#discover' } + ]; + const { chrome, internals } = init({ nav }); + + const navLink = chrome.getNavLinkByTitle('Discover'); + expect(navLink).to.not.be(undefined); + expect(navLink.url).to.be('https://localhost:9200/app/kibana#discover'); + }); + }); + describe('internals.trackPossibleSubUrl()', function () { it('injects the globalState of the current url to all links for the same app', function () { const appUrlStore = new StubBrowserStorage(); diff --git a/src/ui/public/chrome/api/nav.js b/src/ui/public/chrome/api/nav.js index fc815a34163a83..706468029fade2 100644 --- a/src/ui/public/chrome/api/nav.js +++ b/src/ui/public/chrome/api/nav.js @@ -6,6 +6,10 @@ export default function (chrome, internals) { return internals.nav; }; + chrome.getNavLinkByTitle = (title) => { + return find(internals.nav, link => link.title === title); + }; + chrome.getBasePath = function () { return internals.basePath || ''; }; diff --git a/src/ui/public/chrome/directives/app_switcher/app_switcher.html b/src/ui/public/chrome/directives/app_switcher/app_switcher.html index 1e0a0d0e665da3..9f24453728f5b4 100644 --- a/src/ui/public/chrome/directives/app_switcher/app_switcher.html +++ b/src/ui/public/chrome/directives/app_switcher/app_switcher.html @@ -1,8 +1,9 @@ diff --git a/src/ui/public/kbn_top_nav/kbn_top_nav_controller.js b/src/ui/public/kbn_top_nav/kbn_top_nav_controller.js index 80916038e1c909..fadacd6cceccdb 100644 --- a/src/ui/public/kbn_top_nav/kbn_top_nav_controller.js +++ b/src/ui/public/kbn_top_nav/kbn_top_nav_controller.js @@ -1,4 +1,4 @@ -import { defaults, capitalize, isArray } from 'lodash'; +import { defaults, capitalize, isArray, isFunction } from 'lodash'; import uiModules from 'ui/modules'; import filterTemplate from 'ui/chrome/config/filter.html'; @@ -29,7 +29,7 @@ export default function ($compile) { const opt = this._applyOptDefault(rawOpt); if (!opt.key) throw new TypeError('KbnTopNav: menu items must have a key'); this.opts.push(opt); - if (!opt.hideButton) this.menuItems.push(opt); + if (!opt.hideButton()) this.menuItems.push(opt); if (opt.template) this.templates[opt.key] = opt.template; }); } @@ -57,8 +57,10 @@ export default function ($compile) { label: capitalize(opt.key), hasFunction: !!opt.run, description: opt.run ? opt.key : `Toggle ${opt.key} view`, - hideButton: !!opt.hideButton, - run: (item) => this.toggle(item.key) + hideButton: isFunction(opt.hideButton) ? opt.hideButton : () => false, + disableButton: isFunction(opt.disableButton) ? opt.disableButton : () => false, + tooltip: isFunction(opt.tooltip) ? opt.tooltip : () => '', + run: (item) => !item.disableButton() && this.toggle(item.key) }); } From fb854125a0719f54712d6ee87c4c0918abe7a75f Mon Sep 17 00:00:00 2001 From: Shaunak Kashyap Date: Tue, 5 Jul 2016 11:30:22 -0700 Subject: [PATCH 23/67] Adding styles for disabled button in top nav --- src/ui/public/styles/base.less | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/ui/public/styles/base.less b/src/ui/public/styles/base.less index 490e4dde191266..b98c664063e1dd 100644 --- a/src/ui/public/styles/base.less +++ b/src/ui/public/styles/base.less @@ -157,6 +157,16 @@ a { .button-group > :last-child { border-radius: 0; } + + button.is-kbn-top-nav-button-disabled { + opacity: 0.5; + cursor: default; + + &:active { + color: @kibanaGray2; + background-color: transparent; + } + } } .kibana-nav-info { From 0b0280c103bc72eb47986f62160b4eb31b03a3bb Mon Sep 17 00:00:00 2001 From: Shaunak Kashyap Date: Tue, 5 Jul 2016 11:30:44 -0700 Subject: [PATCH 24/67] If property value is non-function, convert it to a function that returns that value --- src/ui/public/kbn_top_nav/kbn_top_nav_controller.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/ui/public/kbn_top_nav/kbn_top_nav_controller.js b/src/ui/public/kbn_top_nav/kbn_top_nav_controller.js index fadacd6cceccdb..86e41e87b7ea52 100644 --- a/src/ui/public/kbn_top_nav/kbn_top_nav_controller.js +++ b/src/ui/public/kbn_top_nav/kbn_top_nav_controller.js @@ -57,9 +57,9 @@ export default function ($compile) { label: capitalize(opt.key), hasFunction: !!opt.run, description: opt.run ? opt.key : `Toggle ${opt.key} view`, - hideButton: isFunction(opt.hideButton) ? opt.hideButton : () => false, - disableButton: isFunction(opt.disableButton) ? opt.disableButton : () => false, - tooltip: isFunction(opt.tooltip) ? opt.tooltip : () => '', + hideButton: isFunction(opt.hideButton) ? opt.hideButton : () => (opt.hideButton || false), + disableButton: isFunction(opt.disableButton) ? opt.disableButton : () => (opt.disableButton || false), + tooltip: isFunction(opt.tooltip) ? opt.tooltip : () => (opt.tooltip || ''), run: (item) => !item.disableButton() && this.toggle(item.key) }); } From ed0c48a68028fbb85c495c5050c7b20dc117b6f6 Mon Sep 17 00:00:00 2001 From: Shaunak Kashyap Date: Tue, 5 Jul 2016 12:07:06 -0700 Subject: [PATCH 25/67] Fix defaulting of properties --- src/ui/public/kbn_top_nav/kbn_top_nav_controller.js | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/ui/public/kbn_top_nav/kbn_top_nav_controller.js b/src/ui/public/kbn_top_nav/kbn_top_nav_controller.js index 86e41e87b7ea52..da8ccd2ae62a9f 100644 --- a/src/ui/public/kbn_top_nav/kbn_top_nav_controller.js +++ b/src/ui/public/kbn_top_nav/kbn_top_nav_controller.js @@ -53,15 +53,18 @@ export default function ($compile) { // apply the defaults to individual options _applyOptDefault(opt = {}) { - return defaults({}, opt, { + const defaultedOpt = defaults({}, opt, { label: capitalize(opt.key), hasFunction: !!opt.run, description: opt.run ? opt.key : `Toggle ${opt.key} view`, - hideButton: isFunction(opt.hideButton) ? opt.hideButton : () => (opt.hideButton || false), - disableButton: isFunction(opt.disableButton) ? opt.disableButton : () => (opt.disableButton || false), - tooltip: isFunction(opt.tooltip) ? opt.tooltip : () => (opt.tooltip || ''), run: (item) => !item.disableButton() && this.toggle(item.key) }); + + defaultedOpt.hideButton = isFunction(opt.hideButton) ? opt.hideButton : () => (opt.hideButton || false); + defaultedOpt.disableButton = isFunction(opt.disableButton) ? opt.disableButton : () => (opt.disableButton || false); + defaultedOpt.tooltip = isFunction(opt.tooltip) ? opt.tooltip : () => (opt.tooltip || ''); + + return defaultedOpt; } // enable actual rendering From 08f5061247ce017faf9e38f239063b7da69e79b2 Mon Sep 17 00:00:00 2001 From: CJ Cenizal Date: Wed, 29 Jun 2016 18:03:06 -0700 Subject: [PATCH 26/67] Remove angular-bootstrap dependency. Copy required code into src/ui/public/angular-bootstrap directory for future refactoring and deprecation. --- package.json | 1 - .../angular-bootstrap/ui-bootstrap-tpls.js | 4268 +++++++++++++++++ webpackShims/ui-bootstrap.js | 2 +- 3 files changed, 4269 insertions(+), 2 deletions(-) create mode 100644 src/ui/public/angular-bootstrap/ui-bootstrap-tpls.js diff --git a/package.json b/package.json index 67db39f1216e4d..f852adae940f7f 100644 --- a/package.json +++ b/package.json @@ -71,7 +71,6 @@ "@bigfunger/decompress-zip": "0.2.0-stripfix2", "@bigfunger/jsondiffpatch": "0.1.38-webpack", "@elastic/datemath": "2.3.0", - "@spalger/angular-bootstrap": "0.12.1", "@spalger/filesaver": "1.1.2", "@spalger/leaflet-draw": "0.2.3", "@spalger/leaflet-heat": "0.1.3", diff --git a/src/ui/public/angular-bootstrap/ui-bootstrap-tpls.js b/src/ui/public/angular-bootstrap/ui-bootstrap-tpls.js new file mode 100644 index 00000000000000..86a31f7efc042f --- /dev/null +++ b/src/ui/public/angular-bootstrap/ui-bootstrap-tpls.js @@ -0,0 +1,4268 @@ +/* eslint-disable */ + +/** + * TODO: Write custom components that address our needs to directly and deprecate these Bootstrap components. + */ + +/* + * angular-ui-bootstrap + * http://angular-ui.github.io/bootstrap/ + + * Version: 0.12.1 - 2015-02-20 + * License: MIT + */ +angular.module("ui.bootstrap", [ + "ui.bootstrap.tpls", + "ui.bootstrap.transition", + "ui.bootstrap.collapse", + "ui.bootstrap.accordion", + "ui.bootstrap.alert", + "ui.bootstrap.bindHtml", + "ui.bootstrap.buttons", + "ui.bootstrap.carousel", + "ui.bootstrap.dateparser", + "ui.bootstrap.position", + "ui.bootstrap.datepicker", + "ui.bootstrap.dropdown", + "ui.bootstrap.modal", + "ui.bootstrap.pagination", + "ui.bootstrap.tooltip", + "ui.bootstrap.popover", + "ui.bootstrap.progressbar", + "ui.bootstrap.rating", + "ui.bootstrap.tabs", + "ui.bootstrap.timepicker", + "ui.bootstrap.typeahead" +]); + +angular.module("ui.bootstrap.tpls", [ + "template/accordion/accordion-group.html", + "template/accordion/accordion.html", + "template/alert/alert.html", + "template/carousel/carousel.html", + "template/carousel/slide.html", + "template/datepicker/datepicker.html", + "template/datepicker/day.html", + "template/datepicker/month.html", + "template/datepicker/popup.html", + "template/datepicker/year.html", + "template/modal/backdrop.html", + "template/modal/window.html", + "template/pagination/pager.html", + "template/pagination/pagination.html", + "template/tooltip/tooltip-html-unsafe-popup.html", + "template/tooltip/tooltip-popup.html", + "template/popover/popover.html", + "template/progressbar/bar.html", + "template/progressbar/progress.html", + "template/progressbar/progressbar.html", + "template/rating/rating.html", + "template/tabs/tab.html", + "template/tabs/tabset.html", + "template/timepicker/timepicker.html", + "template/typeahead/typeahead-match.html", + "template/typeahead/typeahead-popup.html" +]); + +angular.module('ui.bootstrap.transition', []) + +/** + * $transition service provides a consistent interface to trigger CSS 3 transitions and to be informed when they complete. + * @param {DOMElement} element The DOMElement that will be animated. + * @param {string|object|function} trigger The thing that will cause the transition to start: + * - As a string, it represents the css class to be added to the element. + * - As an object, it represents a hash of style attributes to be applied to the element. + * - As a function, it represents a function to be called that will cause the transition to occur. + * @return {Promise} A promise that is resolved when the transition finishes. + */ +.factory('$transition', ['$q', '$timeout', '$rootScope', function($q, $timeout, $rootScope) { + + var $transition = function(element, trigger, options) { + options = options || {}; + var deferred = $q.defer(); + var endEventName = $transition[options.animation ? 'animationEndEventName' : 'transitionEndEventName']; + + var transitionEndHandler = function(event) { + $rootScope.$apply(function() { + element.unbind(endEventName, transitionEndHandler); + deferred.resolve(element); + }); + }; + + if (endEventName) { + element.bind(endEventName, transitionEndHandler); + } + + // Wrap in a timeout to allow the browser time to update the DOM before the transition is to occur + $timeout(function() { + if ( angular.isString(trigger) ) { + element.addClass(trigger); + } else if ( angular.isFunction(trigger) ) { + trigger(element); + } else if ( angular.isObject(trigger) ) { + element.css(trigger); + } + //If browser does not support transitions, instantly resolve + if ( !endEventName ) { + deferred.resolve(element); + } + }); + + // Add our custom cancel function to the promise that is returned + // We can call this if we are about to run a new transition, which we know will prevent this transition from ending, + // i.e. it will therefore never raise a transitionEnd event for that transition + deferred.promise.cancel = function() { + if ( endEventName ) { + element.unbind(endEventName, transitionEndHandler); + } + deferred.reject('Transition cancelled'); + }; + + return deferred.promise; + }; + + // Work out the name of the transitionEnd event + var transElement = document.createElement('trans'); + var transitionEndEventNames = { + 'WebkitTransition': 'webkitTransitionEnd', + 'MozTransition': 'transitionend', + 'OTransition': 'oTransitionEnd', + 'transition': 'transitionend' + }; + var animationEndEventNames = { + 'WebkitTransition': 'webkitAnimationEnd', + 'MozTransition': 'animationend', + 'OTransition': 'oAnimationEnd', + 'transition': 'animationend' + }; + function findEndEventName(endEventNames) { + for (var name in endEventNames){ + if (transElement.style[name] !== undefined) { + return endEventNames[name]; + } + } + } + $transition.transitionEndEventName = findEndEventName(transitionEndEventNames); + $transition.animationEndEventName = findEndEventName(animationEndEventNames); + return $transition; +}]); + +angular.module('ui.bootstrap.collapse', ['ui.bootstrap.transition']) + + .directive('collapse', ['$transition', function ($transition) { + + return { + link: function (scope, element, attrs) { + + var initialAnimSkip = true; + var currentTransition; + + function doTransition(change) { + var newTransition = $transition(element, change); + if (currentTransition) { + currentTransition.cancel(); + } + currentTransition = newTransition; + newTransition.then(newTransitionDone, newTransitionDone); + return newTransition; + + function newTransitionDone() { + // Make sure it's this transition, otherwise, leave it alone. + if (currentTransition === newTransition) { + currentTransition = undefined; + } + } + } + + function expand() { + if (initialAnimSkip) { + initialAnimSkip = false; + expandDone(); + } else { + element.removeClass('collapse').addClass('collapsing'); + doTransition({ height: element[0].scrollHeight + 'px' }).then(expandDone); + } + } + + function expandDone() { + element.removeClass('collapsing'); + element.addClass('collapse in'); + element.css({height: 'auto'}); + } + + function collapse() { + if (initialAnimSkip) { + initialAnimSkip = false; + collapseDone(); + element.css({height: 0}); + } else { + // CSS transitions don't work with height: auto, so we have to manually change the height to a specific value + element.css({ height: element[0].scrollHeight + 'px' }); + //trigger reflow so a browser realizes that height was updated from auto to a specific value + var x = element[0].offsetWidth; + + element.removeClass('collapse in').addClass('collapsing'); + + doTransition({ height: 0 }).then(collapseDone); + } + } + + function collapseDone() { + element.removeClass('collapsing'); + element.addClass('collapse'); + } + + scope.$watch(attrs.collapse, function (shouldCollapse) { + if (shouldCollapse) { + collapse(); + } else { + expand(); + } + }); + } + }; + }]); + +angular.module('ui.bootstrap.accordion', ['ui.bootstrap.collapse']) + +.constant('accordionConfig', { + closeOthers: true +}) + +.controller('AccordionController', ['$scope', '$attrs', 'accordionConfig', function ($scope, $attrs, accordionConfig) { + + // This array keeps track of the accordion groups + this.groups = []; + + // Ensure that all the groups in this accordion are closed, unless close-others explicitly says not to + this.closeOthers = function(openGroup) { + var closeOthers = angular.isDefined($attrs.closeOthers) ? $scope.$eval($attrs.closeOthers) : accordionConfig.closeOthers; + if ( closeOthers ) { + angular.forEach(this.groups, function (group) { + if ( group !== openGroup ) { + group.isOpen = false; + } + }); + } + }; + + // This is called from the accordion-group directive to add itself to the accordion + this.addGroup = function(groupScope) { + var that = this; + this.groups.push(groupScope); + + groupScope.$on('$destroy', function (event) { + that.removeGroup(groupScope); + }); + }; + + // This is called from the accordion-group directive when to remove itself + this.removeGroup = function(group) { + var index = this.groups.indexOf(group); + if ( index !== -1 ) { + this.groups.splice(index, 1); + } + }; + +}]) + +// The accordion directive simply sets up the directive controller +// and adds an accordion CSS class to itself element. +.directive('accordion', function () { + return { + restrict:'EA', + controller:'AccordionController', + transclude: true, + replace: false, + templateUrl: 'template/accordion/accordion.html' + }; +}) + +// The accordion-group directive indicates a block of html that will expand and collapse in an accordion +.directive('accordionGroup', function() { + return { + require:'^accordion', // We need this directive to be inside an accordion + restrict:'EA', + transclude:true, // It transcludes the contents of the directive into the template + replace: true, // The element containing the directive will be replaced with the template + templateUrl:'template/accordion/accordion-group.html', + scope: { + heading: '@', // Interpolate the heading attribute onto this scope + isOpen: '=?', + isDisabled: '=?' + }, + controller: function() { + this.setHeading = function(element) { + this.heading = element; + }; + }, + link: function(scope, element, attrs, accordionCtrl) { + accordionCtrl.addGroup(scope); + + scope.$watch('isOpen', function(value) { + if ( value ) { + accordionCtrl.closeOthers(scope); + } + }); + + scope.toggleOpen = function() { + if ( !scope.isDisabled ) { + scope.isOpen = !scope.isOpen; + } + }; + } + }; +}) + +// Use accordion-heading below an accordion-group to provide a heading containing HTML +// +// Heading containing HTML - +// +.directive('accordionHeading', function() { + return { + restrict: 'EA', + transclude: true, // Grab the contents to be used as the heading + template: '', // In effect remove this element! + replace: true, + require: '^accordionGroup', + link: function(scope, element, attr, accordionGroupCtrl, transclude) { + // Pass the heading to the accordion-group controller + // so that it can be transcluded into the right place in the template + // [The second parameter to transclude causes the elements to be cloned so that they work in ng-repeat] + accordionGroupCtrl.setHeading(transclude(scope, function() {})); + } + }; +}) + +// Use in the accordion-group template to indicate where you want the heading to be transcluded +// You must provide the property on the accordion-group controller that will hold the transcluded element +//
+// +// ... +//
+.directive('accordionTransclude', function() { + return { + require: '^accordionGroup', + link: function(scope, element, attr, controller) { + scope.$watch(function() { return controller[attr.accordionTransclude]; }, function(heading) { + if ( heading ) { + element.html(''); + element.append(heading); + } + }); + } + }; +}); + +angular.module('ui.bootstrap.alert', []) + +.controller('AlertController', ['$scope', '$attrs', function ($scope, $attrs) { + $scope.closeable = 'close' in $attrs; + this.close = $scope.close; +}]) + +.directive('alert', function () { + return { + restrict:'EA', + controller:'AlertController', + templateUrl:'template/alert/alert.html', + transclude:true, + replace:true, + scope: { + type: '@', + close: '&' + } + }; +}) + +.directive('dismissOnTimeout', ['$timeout', function($timeout) { + return { + require: 'alert', + link: function(scope, element, attrs, alertCtrl) { + $timeout(function(){ + alertCtrl.close(); + }, parseInt(attrs.dismissOnTimeout, 10)); + } + }; +}]); + +angular.module('ui.bootstrap.bindHtml', []) + + .directive('bindHtmlUnsafe', function () { + return function (scope, element, attr) { + element.addClass('ng-binding').data('$binding', attr.bindHtmlUnsafe); + scope.$watch(attr.bindHtmlUnsafe, function bindHtmlUnsafeWatchAction(value) { + element.html(value || ''); + }); + }; + }); +angular.module('ui.bootstrap.buttons', []) + +.constant('buttonConfig', { + activeClass: 'active', + toggleEvent: 'click' +}) + +.controller('ButtonsController', ['buttonConfig', function(buttonConfig) { + this.activeClass = buttonConfig.activeClass || 'active'; + this.toggleEvent = buttonConfig.toggleEvent || 'click'; +}]) + +.directive('btnRadio', function () { + return { + require: ['btnRadio', 'ngModel'], + controller: 'ButtonsController', + link: function (scope, element, attrs, ctrls) { + var buttonsCtrl = ctrls[0], ngModelCtrl = ctrls[1]; + + //model -> UI + ngModelCtrl.$render = function () { + element.toggleClass(buttonsCtrl.activeClass, angular.equals(ngModelCtrl.$modelValue, scope.$eval(attrs.btnRadio))); + }; + + //ui->model + element.bind(buttonsCtrl.toggleEvent, function () { + var isActive = element.hasClass(buttonsCtrl.activeClass); + + if (!isActive || angular.isDefined(attrs.uncheckable)) { + scope.$apply(function () { + ngModelCtrl.$setViewValue(isActive ? null : scope.$eval(attrs.btnRadio)); + ngModelCtrl.$render(); + }); + } + }); + } + }; +}) + +.directive('btnCheckbox', function () { + return { + require: ['btnCheckbox', 'ngModel'], + controller: 'ButtonsController', + link: function (scope, element, attrs, ctrls) { + var buttonsCtrl = ctrls[0], ngModelCtrl = ctrls[1]; + + function getTrueValue() { + return getCheckboxValue(attrs.btnCheckboxTrue, true); + } + + function getFalseValue() { + return getCheckboxValue(attrs.btnCheckboxFalse, false); + } + + function getCheckboxValue(attributeValue, defaultValue) { + var val = scope.$eval(attributeValue); + return angular.isDefined(val) ? val : defaultValue; + } + + //model -> UI + ngModelCtrl.$render = function () { + element.toggleClass(buttonsCtrl.activeClass, angular.equals(ngModelCtrl.$modelValue, getTrueValue())); + }; + + //ui->model + element.bind(buttonsCtrl.toggleEvent, function () { + scope.$apply(function () { + ngModelCtrl.$setViewValue(element.hasClass(buttonsCtrl.activeClass) ? getFalseValue() : getTrueValue()); + ngModelCtrl.$render(); + }); + }); + } + }; +}); + +/** +* @ngdoc overview +* @name ui.bootstrap.carousel +* +* @description +* AngularJS version of an image carousel. +* +*/ +angular.module('ui.bootstrap.carousel', ['ui.bootstrap.transition']) +.controller('CarouselController', ['$scope', '$timeout', '$interval', '$transition', function ($scope, $timeout, $interval, $transition) { + var self = this, + slides = self.slides = $scope.slides = [], + currentIndex = -1, + currentInterval, isPlaying; + self.currentSlide = null; + + var destroyed = false; + /* direction: "prev" or "next" */ + self.select = $scope.select = function(nextSlide, direction) { + var nextIndex = slides.indexOf(nextSlide); + //Decide direction if it's not given + if (direction === undefined) { + direction = nextIndex > currentIndex ? 'next' : 'prev'; + } + if (nextSlide && nextSlide !== self.currentSlide) { + if ($scope.$currentTransition) { + $scope.$currentTransition.cancel(); + //Timeout so ng-class in template has time to fix classes for finished slide + $timeout(goNext); + } else { + goNext(); + } + } + function goNext() { + // Scope has been destroyed, stop here. + if (destroyed) { return; } + //If we have a slide to transition from and we have a transition type and we're allowed, go + if (self.currentSlide && angular.isString(direction) && !$scope.noTransition && nextSlide.$element) { + //We shouldn't do class manip in here, but it's the same weird thing bootstrap does. need to fix sometime + nextSlide.$element.addClass(direction); + var reflow = nextSlide.$element[0].offsetWidth; //force reflow + + //Set all other slides to stop doing their stuff for the new transition + angular.forEach(slides, function(slide) { + angular.extend(slide, {direction: '', entering: false, leaving: false, active: false}); + }); + angular.extend(nextSlide, {direction: direction, active: true, entering: true}); + angular.extend(self.currentSlide||{}, {direction: direction, leaving: true}); + + $scope.$currentTransition = $transition(nextSlide.$element, {}); + //We have to create new pointers inside a closure since next & current will change + (function(next,current) { + $scope.$currentTransition.then( + function(){ transitionDone(next, current); }, + function(){ transitionDone(next, current); } + ); + }(nextSlide, self.currentSlide)); + } else { + transitionDone(nextSlide, self.currentSlide); + } + self.currentSlide = nextSlide; + currentIndex = nextIndex; + //every time you change slides, reset the timer + restartTimer(); + } + function transitionDone(next, current) { + angular.extend(next, {direction: '', active: true, leaving: false, entering: false}); + angular.extend(current||{}, {direction: '', active: false, leaving: false, entering: false}); + $scope.$currentTransition = null; + } + }; + $scope.$on('$destroy', function () { + destroyed = true; + }); + + /* Allow outside people to call indexOf on slides array */ + self.indexOfSlide = function(slide) { + return slides.indexOf(slide); + }; + + $scope.next = function() { + var newIndex = (currentIndex + 1) % slides.length; + + //Prevent this user-triggered transition from occurring if there is already one in progress + if (!$scope.$currentTransition) { + return self.select(slides[newIndex], 'next'); + } + }; + + $scope.prev = function() { + var newIndex = currentIndex - 1 < 0 ? slides.length - 1 : currentIndex - 1; + + //Prevent this user-triggered transition from occurring if there is already one in progress + if (!$scope.$currentTransition) { + return self.select(slides[newIndex], 'prev'); + } + }; + + $scope.isActive = function(slide) { + return self.currentSlide === slide; + }; + + $scope.$watch('interval', restartTimer); + $scope.$on('$destroy', resetTimer); + + function restartTimer() { + resetTimer(); + var interval = +$scope.interval; + if (!isNaN(interval) && interval > 0) { + currentInterval = $interval(timerFn, interval); + } + } + + function resetTimer() { + if (currentInterval) { + $interval.cancel(currentInterval); + currentInterval = null; + } + } + + function timerFn() { + var interval = +$scope.interval; + if (isPlaying && !isNaN(interval) && interval > 0) { + $scope.next(); + } else { + $scope.pause(); + } + } + + $scope.play = function() { + if (!isPlaying) { + isPlaying = true; + restartTimer(); + } + }; + $scope.pause = function() { + if (!$scope.noPause) { + isPlaying = false; + resetTimer(); + } + }; + + self.addSlide = function(slide, element) { + slide.$element = element; + slides.push(slide); + //if this is the first slide or the slide is set to active, select it + if(slides.length === 1 || slide.active) { + self.select(slides[slides.length-1]); + if (slides.length == 1) { + $scope.play(); + } + } else { + slide.active = false; + } + }; + + self.removeSlide = function(slide) { + //get the index of the slide inside the carousel + var index = slides.indexOf(slide); + slides.splice(index, 1); + if (slides.length > 0 && slide.active) { + if (index >= slides.length) { + self.select(slides[index-1]); + } else { + self.select(slides[index]); + } + } else if (currentIndex > index) { + currentIndex--; + } + }; + +}]) + +/** + * @ngdoc directive + * @name ui.bootstrap.carousel.directive:carousel + * @restrict EA + * + * @description + * Carousel is the outer container for a set of image 'slides' to showcase. + * + * @param {number=} interval The time, in milliseconds, that it will take the carousel to go to the next slide. + * @param {boolean=} noTransition Whether to disable transitions on the carousel. + * @param {boolean=} noPause Whether to disable pausing on the carousel (by default, the carousel interval pauses on hover). + * + * @example + + + + + + + + + + + + + + + .carousel-indicators { + top: auto; + bottom: 15px; + } + + + */ +.directive('carousel', [function() { + return { + restrict: 'EA', + transclude: true, + replace: true, + controller: 'CarouselController', + require: 'carousel', + templateUrl: 'template/carousel/carousel.html', + scope: { + interval: '=', + noTransition: '=', + noPause: '=' + } + }; +}]) + +/** + * @ngdoc directive + * @name ui.bootstrap.carousel.directive:slide + * @restrict EA + * + * @description + * Creates a slide inside a {@link ui.bootstrap.carousel.directive:carousel carousel}. Must be placed as a child of a carousel element. + * + * @param {boolean=} active Model binding, whether or not this slide is currently active. + * + * @example + + +
+ + + + + + + Interval, in milliseconds: +
Enter a negative number to stop the interval. +
+
+ +function CarouselDemoCtrl($scope) { + $scope.myInterval = 5000; +} + + + .carousel-indicators { + top: auto; + bottom: 15px; + } + +
+*/ + +.directive('slide', function() { + return { + require: '^carousel', + restrict: 'EA', + transclude: true, + replace: true, + templateUrl: 'template/carousel/slide.html', + scope: { + active: '=?' + }, + link: function (scope, element, attrs, carouselCtrl) { + carouselCtrl.addSlide(scope, element); + //when the scope is destroyed then remove the slide from the current slides array + scope.$on('$destroy', function() { + carouselCtrl.removeSlide(scope); + }); + + scope.$watch('active', function(active) { + if (active) { + carouselCtrl.select(scope); + } + }); + } + }; +}); + +angular.module('ui.bootstrap.dateparser', []) + +.service('dateParser', ['$locale', 'orderByFilter', function($locale, orderByFilter) { + + this.parsers = {}; + + var formatCodeToRegex = { + 'yyyy': { + regex: '\\d{4}', + apply: function(value) { this.year = +value; } + }, + 'yy': { + regex: '\\d{2}', + apply: function(value) { this.year = +value + 2000; } + }, + 'y': { + regex: '\\d{1,4}', + apply: function(value) { this.year = +value; } + }, + 'MMMM': { + regex: $locale.DATETIME_FORMATS.MONTH.join('|'), + apply: function(value) { this.month = $locale.DATETIME_FORMATS.MONTH.indexOf(value); } + }, + 'MMM': { + regex: $locale.DATETIME_FORMATS.SHORTMONTH.join('|'), + apply: function(value) { this.month = $locale.DATETIME_FORMATS.SHORTMONTH.indexOf(value); } + }, + 'MM': { + regex: '0[1-9]|1[0-2]', + apply: function(value) { this.month = value - 1; } + }, + 'M': { + regex: '[1-9]|1[0-2]', + apply: function(value) { this.month = value - 1; } + }, + 'dd': { + regex: '[0-2][0-9]{1}|3[0-1]{1}', + apply: function(value) { this.date = +value; } + }, + 'd': { + regex: '[1-2]?[0-9]{1}|3[0-1]{1}', + apply: function(value) { this.date = +value; } + }, + 'EEEE': { + regex: $locale.DATETIME_FORMATS.DAY.join('|') + }, + 'EEE': { + regex: $locale.DATETIME_FORMATS.SHORTDAY.join('|') + } + }; + + function createParser(format) { + var map = [], regex = format.split(''); + + angular.forEach(formatCodeToRegex, function(data, code) { + var index = format.indexOf(code); + + if (index > -1) { + format = format.split(''); + + regex[index] = '(' + data.regex + ')'; + format[index] = '$'; // Custom symbol to define consumed part of format + for (var i = index + 1, n = index + code.length; i < n; i++) { + regex[i] = ''; + format[i] = '$'; + } + format = format.join(''); + + map.push({ index: index, apply: data.apply }); + } + }); + + return { + regex: new RegExp('^' + regex.join('') + '$'), + map: orderByFilter(map, 'index') + }; + } + + this.parse = function(input, format) { + if ( !angular.isString(input) || !format ) { + return input; + } + + format = $locale.DATETIME_FORMATS[format] || format; + + if ( !this.parsers[format] ) { + this.parsers[format] = createParser(format); + } + + var parser = this.parsers[format], + regex = parser.regex, + map = parser.map, + results = input.match(regex); + + if ( results && results.length ) { + var fields = { year: 1900, month: 0, date: 1, hours: 0 }, dt; + + for( var i = 1, n = results.length; i < n; i++ ) { + var mapper = map[i-1]; + if ( mapper.apply ) { + mapper.apply.call(fields, results[i]); + } + } + + if ( isValid(fields.year, fields.month, fields.date) ) { + dt = new Date( fields.year, fields.month, fields.date, fields.hours); + } + + return dt; + } + }; + + // Check if date is valid for specific month (and year for February). + // Month: 0 = Jan, 1 = Feb, etc + function isValid(year, month, date) { + if ( month === 1 && date > 28) { + return date === 29 && ((year % 4 === 0 && year % 100 !== 0) || year % 400 === 0); + } + + if ( month === 3 || month === 5 || month === 8 || month === 10) { + return date < 31; + } + + return true; + } +}]); + +angular.module('ui.bootstrap.position', []) + +/** + * A set of utility methods that can be use to retrieve position of DOM elements. + * It is meant to be used where we need to absolute-position DOM elements in + * relation to other, existing elements (this is the case for tooltips, popovers, + * typeahead suggestions etc.). + */ + .factory('$position', ['$document', '$window', function ($document, $window) { + + function getStyle(el, cssprop) { + if (el.currentStyle) { //IE + return el.currentStyle[cssprop]; + } else if ($window.getComputedStyle) { + return $window.getComputedStyle(el)[cssprop]; + } + // finally try and get inline style + return el.style[cssprop]; + } + + /** + * Checks if a given element is statically positioned + * @param element - raw DOM element + */ + function isStaticPositioned(element) { + return (getStyle(element, 'position') || 'static' ) === 'static'; + } + + /** + * returns the closest, non-statically positioned parentOffset of a given element + * @param element + */ + var parentOffsetEl = function (element) { + var docDomEl = $document[0]; + var offsetParent = element.offsetParent || docDomEl; + while (offsetParent && offsetParent !== docDomEl && isStaticPositioned(offsetParent) ) { + offsetParent = offsetParent.offsetParent; + } + return offsetParent || docDomEl; + }; + + return { + /** + * Provides read-only equivalent of jQuery's position function: + * http://api.jquery.com/position/ + */ + position: function (element) { + var elBCR = this.offset(element); + var offsetParentBCR = { top: 0, left: 0 }; + var offsetParentEl = parentOffsetEl(element[0]); + if (offsetParentEl != $document[0]) { + offsetParentBCR = this.offset(angular.element(offsetParentEl)); + offsetParentBCR.top += offsetParentEl.clientTop - offsetParentEl.scrollTop; + offsetParentBCR.left += offsetParentEl.clientLeft - offsetParentEl.scrollLeft; + } + + var boundingClientRect = element[0].getBoundingClientRect(); + return { + width: boundingClientRect.width || element.prop('offsetWidth'), + height: boundingClientRect.height || element.prop('offsetHeight'), + top: elBCR.top - offsetParentBCR.top, + left: elBCR.left - offsetParentBCR.left + }; + }, + + /** + * Provides read-only equivalent of jQuery's offset function: + * http://api.jquery.com/offset/ + */ + offset: function (element) { + var boundingClientRect = element[0].getBoundingClientRect(); + return { + width: boundingClientRect.width || element.prop('offsetWidth'), + height: boundingClientRect.height || element.prop('offsetHeight'), + top: boundingClientRect.top + ($window.pageYOffset || $document[0].documentElement.scrollTop), + left: boundingClientRect.left + ($window.pageXOffset || $document[0].documentElement.scrollLeft) + }; + }, + + /** + * Provides coordinates for the targetEl in relation to hostEl + */ + positionElements: function (hostEl, targetEl, positionStr, appendToBody) { + + var positionStrParts = positionStr.split('-'); + var pos0 = positionStrParts[0], pos1 = positionStrParts[1] || 'center'; + + var hostElPos, + targetElWidth, + targetElHeight, + targetElPos; + + hostElPos = appendToBody ? this.offset(hostEl) : this.position(hostEl); + + targetElWidth = targetEl.prop('offsetWidth'); + targetElHeight = targetEl.prop('offsetHeight'); + + var shiftWidth = { + center: function () { + return hostElPos.left + hostElPos.width / 2 - targetElWidth / 2; + }, + left: function () { + return hostElPos.left; + }, + right: function () { + return hostElPos.left + hostElPos.width; + } + }; + + var shiftHeight = { + center: function () { + return hostElPos.top + hostElPos.height / 2 - targetElHeight / 2; + }, + top: function () { + return hostElPos.top; + }, + bottom: function () { + return hostElPos.top + hostElPos.height; + } + }; + + switch (pos0) { + case 'right': + targetElPos = { + top: shiftHeight[pos1](), + left: shiftWidth[pos0]() + }; + break; + case 'left': + targetElPos = { + top: shiftHeight[pos1](), + left: hostElPos.left - targetElWidth + }; + break; + case 'bottom': + targetElPos = { + top: shiftHeight[pos0](), + left: shiftWidth[pos1]() + }; + break; + default: + targetElPos = { + top: hostElPos.top - targetElHeight, + left: shiftWidth[pos1]() + }; + break; + } + + return targetElPos; + } + }; + }]); + +angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.dateparser', 'ui.bootstrap.position']) + +.constant('datepickerConfig', { + formatDay: 'dd', + formatMonth: 'MMMM', + formatYear: 'yyyy', + formatDayHeader: 'EEE', + formatDayTitle: 'MMMM yyyy', + formatMonthTitle: 'yyyy', + datepickerMode: 'day', + minMode: 'day', + maxMode: 'year', + showWeeks: true, + startingDay: 0, + yearRange: 20, + minDate: null, + maxDate: null +}) + +.controller('DatepickerController', ['$scope', '$attrs', '$parse', '$interpolate', '$timeout', '$log', 'dateFilter', 'datepickerConfig', function($scope, $attrs, $parse, $interpolate, $timeout, $log, dateFilter, datepickerConfig) { + var self = this, + ngModelCtrl = { $setViewValue: angular.noop }; // nullModelCtrl; + + // Modes chain + this.modes = ['day', 'month', 'year']; + + // Configuration attributes + angular.forEach(['formatDay', 'formatMonth', 'formatYear', 'formatDayHeader', 'formatDayTitle', 'formatMonthTitle', + 'minMode', 'maxMode', 'showWeeks', 'startingDay', 'yearRange'], function( key, index ) { + self[key] = angular.isDefined($attrs[key]) ? (index < 8 ? $interpolate($attrs[key])($scope.$parent) : $scope.$parent.$eval($attrs[key])) : datepickerConfig[key]; + }); + + // Watchable date attributes + angular.forEach(['minDate', 'maxDate'], function( key ) { + if ( $attrs[key] ) { + $scope.$parent.$watch($parse($attrs[key]), function(value) { + self[key] = value ? new Date(value) : null; + self.refreshView(); + }); + } else { + self[key] = datepickerConfig[key] ? new Date(datepickerConfig[key]) : null; + } + }); + + $scope.datepickerMode = $scope.datepickerMode || datepickerConfig.datepickerMode; + $scope.uniqueId = 'datepicker-' + $scope.$id + '-' + Math.floor(Math.random() * 10000); + this.activeDate = angular.isDefined($attrs.initDate) ? $scope.$parent.$eval($attrs.initDate) : new Date(); + + $scope.isActive = function(dateObject) { + if (self.compare(dateObject.date, self.activeDate) === 0) { + $scope.activeDateId = dateObject.uid; + return true; + } + return false; + }; + + this.init = function( ngModelCtrl_ ) { + ngModelCtrl = ngModelCtrl_; + + ngModelCtrl.$render = function() { + self.render(); + }; + }; + + this.render = function() { + if ( ngModelCtrl.$modelValue ) { + var date = new Date( ngModelCtrl.$modelValue ), + isValid = !isNaN(date); + + if ( isValid ) { + this.activeDate = date; + } else { + $log.error('Datepicker directive: "ng-model" value must be a Date object, a number of milliseconds since 01.01.1970 or a string representing an RFC2822 or ISO 8601 date.'); + } + ngModelCtrl.$setValidity('date', isValid); + } + this.refreshView(); + }; + + this.refreshView = function() { + if ( this.element ) { + this._refreshView(); + + var date = ngModelCtrl.$modelValue ? new Date(ngModelCtrl.$modelValue) : null; + ngModelCtrl.$setValidity('date-disabled', !date || (this.element && !this.isDisabled(date))); + } + }; + + this.createDateObject = function(date, format) { + var model = ngModelCtrl.$modelValue ? new Date(ngModelCtrl.$modelValue) : null; + return { + date: date, + label: dateFilter(date, format), + selected: model && this.compare(date, model) === 0, + disabled: this.isDisabled(date), + current: this.compare(date, new Date()) === 0 + }; + }; + + this.isDisabled = function( date ) { + return ((this.minDate && this.compare(date, this.minDate) < 0) || (this.maxDate && this.compare(date, this.maxDate) > 0) || ($attrs.dateDisabled && $scope.dateDisabled({date: date, mode: $scope.datepickerMode}))); + }; + + // Split array into smaller arrays + this.split = function(arr, size) { + var arrays = []; + while (arr.length > 0) { + arrays.push(arr.splice(0, size)); + } + return arrays; + }; + + $scope.select = function( date ) { + if ( $scope.datepickerMode === self.minMode ) { + var dt = ngModelCtrl.$modelValue ? new Date( ngModelCtrl.$modelValue ) : new Date(0, 0, 0, 0, 0, 0, 0); + dt.setFullYear( date.getFullYear(), date.getMonth(), date.getDate() ); + ngModelCtrl.$setViewValue( dt ); + ngModelCtrl.$render(); + } else { + self.activeDate = date; + $scope.datepickerMode = self.modes[ self.modes.indexOf( $scope.datepickerMode ) - 1 ]; + } + }; + + $scope.move = function( direction ) { + var year = self.activeDate.getFullYear() + direction * (self.step.years || 0), + month = self.activeDate.getMonth() + direction * (self.step.months || 0); + self.activeDate.setFullYear(year, month, 1); + self.refreshView(); + }; + + $scope.toggleMode = function( direction ) { + direction = direction || 1; + + if (($scope.datepickerMode === self.maxMode && direction === 1) || ($scope.datepickerMode === self.minMode && direction === -1)) { + return; + } + + $scope.datepickerMode = self.modes[ self.modes.indexOf( $scope.datepickerMode ) + direction ]; + }; + + // Key event mapper + $scope.keys = { 13:'enter', 32:'space', 33:'pageup', 34:'pagedown', 35:'end', 36:'home', 37:'left', 38:'up', 39:'right', 40:'down' }; + + var focusElement = function() { + $timeout(function() { + self.element[0].focus(); + }, 0 , false); + }; + + // Listen for focus requests from popup directive + $scope.$on('datepicker.focus', focusElement); + + $scope.keydown = function( evt ) { + var key = $scope.keys[evt.which]; + + if ( !key || evt.shiftKey || evt.altKey ) { + return; + } + + evt.preventDefault(); + evt.stopPropagation(); + + if (key === 'enter' || key === 'space') { + if ( self.isDisabled(self.activeDate)) { + return; // do nothing + } + $scope.select(self.activeDate); + focusElement(); + } else if (evt.ctrlKey && (key === 'up' || key === 'down')) { + $scope.toggleMode(key === 'up' ? 1 : -1); + focusElement(); + } else { + self.handleKeyDown(key, evt); + self.refreshView(); + } + }; +}]) + +.directive( 'datepicker', function () { + return { + restrict: 'EA', + replace: true, + templateUrl: 'template/datepicker/datepicker.html', + scope: { + datepickerMode: '=?', + dateDisabled: '&' + }, + require: ['datepicker', '?^ngModel'], + controller: 'DatepickerController', + link: function(scope, element, attrs, ctrls) { + var datepickerCtrl = ctrls[0], ngModelCtrl = ctrls[1]; + + if ( ngModelCtrl ) { + datepickerCtrl.init( ngModelCtrl ); + } + } + }; +}) + +.directive('daypicker', ['dateFilter', function (dateFilter) { + return { + restrict: 'EA', + replace: true, + templateUrl: 'template/datepicker/day.html', + require: '^datepicker', + link: function(scope, element, attrs, ctrl) { + scope.showWeeks = ctrl.showWeeks; + + ctrl.step = { months: 1 }; + ctrl.element = element; + + var DAYS_IN_MONTH = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]; + function getDaysInMonth( year, month ) { + return ((month === 1) && (year % 4 === 0) && ((year % 100 !== 0) || (year % 400 === 0))) ? 29 : DAYS_IN_MONTH[month]; + } + + function getDates(startDate, n) { + var dates = new Array(n), current = new Date(startDate), i = 0; + current.setHours(12); // Prevent repeated dates because of timezone bug + while ( i < n ) { + dates[i++] = new Date(current); + current.setDate( current.getDate() + 1 ); + } + return dates; + } + + ctrl._refreshView = function() { + var year = ctrl.activeDate.getFullYear(), + month = ctrl.activeDate.getMonth(), + firstDayOfMonth = new Date(year, month, 1), + difference = ctrl.startingDay - firstDayOfMonth.getDay(), + numDisplayedFromPreviousMonth = (difference > 0) ? 7 - difference : - difference, + firstDate = new Date(firstDayOfMonth); + + if ( numDisplayedFromPreviousMonth > 0 ) { + firstDate.setDate( - numDisplayedFromPreviousMonth + 1 ); + } + + // 42 is the number of days on a six-month calendar + var days = getDates(firstDate, 42); + for (var i = 0; i < 42; i ++) { + days[i] = angular.extend(ctrl.createDateObject(days[i], ctrl.formatDay), { + secondary: days[i].getMonth() !== month, + uid: scope.uniqueId + '-' + i + }); + } + + scope.labels = new Array(7); + for (var j = 0; j < 7; j++) { + scope.labels[j] = { + abbr: dateFilter(days[j].date, ctrl.formatDayHeader), + full: dateFilter(days[j].date, 'EEEE') + }; + } + + scope.title = dateFilter(ctrl.activeDate, ctrl.formatDayTitle); + scope.rows = ctrl.split(days, 7); + + if ( scope.showWeeks ) { + scope.weekNumbers = []; + var weekNumber = getISO8601WeekNumber( scope.rows[0][0].date ), + numWeeks = scope.rows.length; + while( scope.weekNumbers.push(weekNumber++) < numWeeks ) {} + } + }; + + ctrl.compare = function(date1, date2) { + return (new Date( date1.getFullYear(), date1.getMonth(), date1.getDate() ) - new Date( date2.getFullYear(), date2.getMonth(), date2.getDate() ) ); + }; + + function getISO8601WeekNumber(date) { + var checkDate = new Date(date); + checkDate.setDate(checkDate.getDate() + 4 - (checkDate.getDay() || 7)); // Thursday + var time = checkDate.getTime(); + checkDate.setMonth(0); // Compare with Jan 1 + checkDate.setDate(1); + return Math.floor(Math.round((time - checkDate) / 86400000) / 7) + 1; + } + + ctrl.handleKeyDown = function( key, evt ) { + var date = ctrl.activeDate.getDate(); + + if (key === 'left') { + date = date - 1; // up + } else if (key === 'up') { + date = date - 7; // down + } else if (key === 'right') { + date = date + 1; // down + } else if (key === 'down') { + date = date + 7; + } else if (key === 'pageup' || key === 'pagedown') { + var month = ctrl.activeDate.getMonth() + (key === 'pageup' ? - 1 : 1); + ctrl.activeDate.setMonth(month, 1); + date = Math.min(getDaysInMonth(ctrl.activeDate.getFullYear(), ctrl.activeDate.getMonth()), date); + } else if (key === 'home') { + date = 1; + } else if (key === 'end') { + date = getDaysInMonth(ctrl.activeDate.getFullYear(), ctrl.activeDate.getMonth()); + } + ctrl.activeDate.setDate(date); + }; + + ctrl.refreshView(); + } + }; +}]) + +.directive('monthpicker', ['dateFilter', function (dateFilter) { + return { + restrict: 'EA', + replace: true, + templateUrl: 'template/datepicker/month.html', + require: '^datepicker', + link: function(scope, element, attrs, ctrl) { + ctrl.step = { years: 1 }; + ctrl.element = element; + + ctrl._refreshView = function() { + var months = new Array(12), + year = ctrl.activeDate.getFullYear(); + + for ( var i = 0; i < 12; i++ ) { + months[i] = angular.extend(ctrl.createDateObject(new Date(year, i, 1), ctrl.formatMonth), { + uid: scope.uniqueId + '-' + i + }); + } + + scope.title = dateFilter(ctrl.activeDate, ctrl.formatMonthTitle); + scope.rows = ctrl.split(months, 3); + }; + + ctrl.compare = function(date1, date2) { + return new Date( date1.getFullYear(), date1.getMonth() ) - new Date( date2.getFullYear(), date2.getMonth() ); + }; + + ctrl.handleKeyDown = function( key, evt ) { + var date = ctrl.activeDate.getMonth(); + + if (key === 'left') { + date = date - 1; // up + } else if (key === 'up') { + date = date - 3; // down + } else if (key === 'right') { + date = date + 1; // down + } else if (key === 'down') { + date = date + 3; + } else if (key === 'pageup' || key === 'pagedown') { + var year = ctrl.activeDate.getFullYear() + (key === 'pageup' ? - 1 : 1); + ctrl.activeDate.setFullYear(year); + } else if (key === 'home') { + date = 0; + } else if (key === 'end') { + date = 11; + } + ctrl.activeDate.setMonth(date); + }; + + ctrl.refreshView(); + } + }; +}]) + +.directive('yearpicker', ['dateFilter', function (dateFilter) { + return { + restrict: 'EA', + replace: true, + templateUrl: 'template/datepicker/year.html', + require: '^datepicker', + link: function(scope, element, attrs, ctrl) { + var range = ctrl.yearRange; + + ctrl.step = { years: range }; + ctrl.element = element; + + function getStartingYear( year ) { + return parseInt((year - 1) / range, 10) * range + 1; + } + + ctrl._refreshView = function() { + var years = new Array(range); + + for ( var i = 0, start = getStartingYear(ctrl.activeDate.getFullYear()); i < range; i++ ) { + years[i] = angular.extend(ctrl.createDateObject(new Date(start + i, 0, 1), ctrl.formatYear), { + uid: scope.uniqueId + '-' + i + }); + } + + scope.title = [years[0].label, years[range - 1].label].join(' - '); + scope.rows = ctrl.split(years, 5); + }; + + ctrl.compare = function(date1, date2) { + return date1.getFullYear() - date2.getFullYear(); + }; + + ctrl.handleKeyDown = function( key, evt ) { + var date = ctrl.activeDate.getFullYear(); + + if (key === 'left') { + date = date - 1; // up + } else if (key === 'up') { + date = date - 5; // down + } else if (key === 'right') { + date = date + 1; // down + } else if (key === 'down') { + date = date + 5; + } else if (key === 'pageup' || key === 'pagedown') { + date += (key === 'pageup' ? - 1 : 1) * ctrl.step.years; + } else if (key === 'home') { + date = getStartingYear( ctrl.activeDate.getFullYear() ); + } else if (key === 'end') { + date = getStartingYear( ctrl.activeDate.getFullYear() ) + range - 1; + } + ctrl.activeDate.setFullYear(date); + }; + + ctrl.refreshView(); + } + }; +}]) + +.constant('datepickerPopupConfig', { + datepickerPopup: 'yyyy-MM-dd', + currentText: 'Today', + clearText: 'Clear', + closeText: 'Done', + closeOnDateSelection: true, + appendToBody: false, + showButtonBar: true +}) + +.directive('datepickerPopup', ['$compile', '$parse', '$document', '$position', 'dateFilter', 'dateParser', 'datepickerPopupConfig', +function ($compile, $parse, $document, $position, dateFilter, dateParser, datepickerPopupConfig) { + return { + restrict: 'EA', + require: 'ngModel', + scope: { + isOpen: '=?', + currentText: '@', + clearText: '@', + closeText: '@', + dateDisabled: '&' + }, + link: function(scope, element, attrs, ngModel) { + var dateFormat, + closeOnDateSelection = angular.isDefined(attrs.closeOnDateSelection) ? scope.$parent.$eval(attrs.closeOnDateSelection) : datepickerPopupConfig.closeOnDateSelection, + appendToBody = angular.isDefined(attrs.datepickerAppendToBody) ? scope.$parent.$eval(attrs.datepickerAppendToBody) : datepickerPopupConfig.appendToBody; + + scope.showButtonBar = angular.isDefined(attrs.showButtonBar) ? scope.$parent.$eval(attrs.showButtonBar) : datepickerPopupConfig.showButtonBar; + + scope.getText = function( key ) { + return scope[key + 'Text'] || datepickerPopupConfig[key + 'Text']; + }; + + attrs.$observe('datepickerPopup', function(value) { + dateFormat = value || datepickerPopupConfig.datepickerPopup; + ngModel.$render(); + }); + + // popup element used to display calendar + var popupEl = angular.element('
'); + popupEl.attr({ + 'ng-model': 'date', + 'ng-change': 'dateSelection()' + }); + + function cameltoDash( string ){ + return string.replace(/([A-Z])/g, function($1) { return '-' + $1.toLowerCase(); }); + } + + // datepicker element + var datepickerEl = angular.element(popupEl.children()[0]); + if ( attrs.datepickerOptions ) { + angular.forEach(scope.$parent.$eval(attrs.datepickerOptions), function( value, option ) { + datepickerEl.attr( cameltoDash(option), value ); + }); + } + + scope.watchData = {}; + angular.forEach(['minDate', 'maxDate', 'datepickerMode'], function( key ) { + if ( attrs[key] ) { + var getAttribute = $parse(attrs[key]); + scope.$parent.$watch(getAttribute, function(value){ + scope.watchData[key] = value; + }); + datepickerEl.attr(cameltoDash(key), 'watchData.' + key); + + // Propagate changes from datepicker to outside + if ( key === 'datepickerMode' ) { + var setAttribute = getAttribute.assign; + scope.$watch('watchData.' + key, function(value, oldvalue) { + if ( value !== oldvalue ) { + setAttribute(scope.$parent, value); + } + }); + } + } + }); + if (attrs.dateDisabled) { + datepickerEl.attr('date-disabled', 'dateDisabled({ date: date, mode: mode })'); + } + + function parseDate(viewValue) { + if (!viewValue) { + ngModel.$setValidity('date', true); + return null; + } else if (angular.isDate(viewValue) && !isNaN(viewValue)) { + ngModel.$setValidity('date', true); + return viewValue; + } else if (angular.isString(viewValue)) { + var date = dateParser.parse(viewValue, dateFormat) || new Date(viewValue); + if (isNaN(date)) { + ngModel.$setValidity('date', false); + return undefined; + } else { + ngModel.$setValidity('date', true); + return date; + } + } else { + ngModel.$setValidity('date', false); + return undefined; + } + } + ngModel.$parsers.unshift(parseDate); + + // Inner change + scope.dateSelection = function(dt) { + if (angular.isDefined(dt)) { + scope.date = dt; + } + ngModel.$setViewValue(scope.date); + ngModel.$render(); + + if ( closeOnDateSelection ) { + scope.isOpen = false; + element[0].focus(); + } + }; + + element.bind('input change keyup', function() { + scope.$apply(function() { + scope.date = ngModel.$modelValue; + }); + }); + + // Outter change + ngModel.$render = function() { + var date = ngModel.$viewValue ? dateFilter(ngModel.$viewValue, dateFormat) : ''; + element.val(date); + scope.date = parseDate( ngModel.$modelValue ); + }; + + var documentClickBind = function(event) { + if (scope.isOpen && event.target !== element[0]) { + scope.$apply(function() { + scope.isOpen = false; + }); + } + }; + + var keydown = function(evt, noApply) { + scope.keydown(evt); + }; + element.bind('keydown', keydown); + + scope.keydown = function(evt) { + if (evt.which === 27) { + evt.preventDefault(); + evt.stopPropagation(); + scope.close(); + } else if (evt.which === 40 && !scope.isOpen) { + scope.isOpen = true; + } + }; + + scope.$watch('isOpen', function(value) { + if (value) { + scope.$broadcast('datepicker.focus'); + scope.position = appendToBody ? $position.offset(element) : $position.position(element); + scope.position.top = scope.position.top + element.prop('offsetHeight'); + + $document.bind('click', documentClickBind); + } else { + $document.unbind('click', documentClickBind); + } + }); + + scope.select = function( date ) { + if (date === 'today') { + var today = new Date(); + if (angular.isDate(ngModel.$modelValue)) { + date = new Date(ngModel.$modelValue); + date.setFullYear(today.getFullYear(), today.getMonth(), today.getDate()); + } else { + date = new Date(today.setHours(0, 0, 0, 0)); + } + } + scope.dateSelection( date ); + }; + + scope.close = function() { + scope.isOpen = false; + element[0].focus(); + }; + + var $popup = $compile(popupEl)(scope); + // Prevent jQuery cache memory leak (template is now redundant after linking) + popupEl.remove(); + + if ( appendToBody ) { + $document.find('body').append($popup); + } else { + element.after($popup); + } + + scope.$on('$destroy', function() { + $popup.remove(); + element.unbind('keydown', keydown); + $document.unbind('click', documentClickBind); + }); + } + }; +}]) + +.directive('datepickerPopupWrap', function() { + return { + restrict:'EA', + replace: true, + transclude: true, + templateUrl: 'template/datepicker/popup.html', + link:function (scope, element, attrs) { + element.bind('click', function(event) { + event.preventDefault(); + event.stopPropagation(); + }); + } + }; +}); + +angular.module('ui.bootstrap.dropdown', []) + +.constant('dropdownConfig', { + openClass: 'open' +}) + +.service('dropdownService', ['$document', function($document) { + var openScope = null; + + this.open = function( dropdownScope ) { + if ( !openScope ) { + $document.bind('click', closeDropdown); + $document.bind('keydown', escapeKeyBind); + } + + if ( openScope && openScope !== dropdownScope ) { + openScope.isOpen = false; + } + + openScope = dropdownScope; + }; + + this.close = function( dropdownScope ) { + if ( openScope === dropdownScope ) { + openScope = null; + $document.unbind('click', closeDropdown); + $document.unbind('keydown', escapeKeyBind); + } + }; + + var closeDropdown = function( evt ) { + // This method may still be called during the same mouse event that + // unbound this event handler. So check openScope before proceeding. + if (!openScope) { return; } + + var toggleElement = openScope.getToggleElement(); + if ( evt && toggleElement && toggleElement[0].contains(evt.target) ) { + return; + } + + openScope.$apply(function() { + openScope.isOpen = false; + }); + }; + + var escapeKeyBind = function( evt ) { + if ( evt.which === 27 ) { + openScope.focusToggleElement(); + closeDropdown(); + } + }; +}]) + +.controller('DropdownController', ['$scope', '$attrs', '$parse', 'dropdownConfig', 'dropdownService', '$animate', function($scope, $attrs, $parse, dropdownConfig, dropdownService, $animate) { + var self = this, + scope = $scope.$new(), // create a child scope so we are not polluting original one + openClass = dropdownConfig.openClass, + getIsOpen, + setIsOpen = angular.noop, + toggleInvoker = $attrs.onToggle ? $parse($attrs.onToggle) : angular.noop; + + this.init = function( element ) { + self.$element = element; + + if ( $attrs.isOpen ) { + getIsOpen = $parse($attrs.isOpen); + setIsOpen = getIsOpen.assign; + + $scope.$watch(getIsOpen, function(value) { + scope.isOpen = !!value; + }); + } + }; + + this.toggle = function( open ) { + return scope.isOpen = arguments.length ? !!open : !scope.isOpen; + }; + + // Allow other directives to watch status + this.isOpen = function() { + return scope.isOpen; + }; + + scope.getToggleElement = function() { + return self.toggleElement; + }; + + scope.focusToggleElement = function() { + if ( self.toggleElement ) { + self.toggleElement[0].focus(); + } + }; + + scope.$watch('isOpen', function( isOpen, wasOpen ) { + $animate[isOpen ? 'addClass' : 'removeClass'](self.$element, openClass); + + if ( isOpen ) { + scope.focusToggleElement(); + dropdownService.open( scope ); + } else { + dropdownService.close( scope ); + } + + setIsOpen($scope, isOpen); + if (angular.isDefined(isOpen) && isOpen !== wasOpen) { + toggleInvoker($scope, { open: !!isOpen }); + } + }); + + $scope.$on('$locationChangeSuccess', function() { + scope.isOpen = false; + }); + + $scope.$on('$destroy', function() { + scope.$destroy(); + }); +}]) + +.directive('dropdown', function() { + return { + controller: 'DropdownController', + link: function(scope, element, attrs, dropdownCtrl) { + dropdownCtrl.init( element ); + } + }; +}) + +.directive('dropdownToggle', function() { + return { + require: '?^dropdown', + link: function(scope, element, attrs, dropdownCtrl) { + if ( !dropdownCtrl ) { + return; + } + + dropdownCtrl.toggleElement = element; + + var toggleDropdown = function(event) { + event.preventDefault(); + + if ( !element.hasClass('disabled') && !attrs.disabled ) { + scope.$apply(function() { + dropdownCtrl.toggle(); + }); + } + }; + + element.bind('click', toggleDropdown); + + // WAI-ARIA + element.attr({ 'aria-haspopup': true, 'aria-expanded': false }); + scope.$watch(dropdownCtrl.isOpen, function( isOpen ) { + element.attr('aria-expanded', !!isOpen); + }); + + scope.$on('$destroy', function() { + element.unbind('click', toggleDropdown); + }); + } + }; +}); + +angular.module('ui.bootstrap.modal', ['ui.bootstrap.transition']) + +/** + * A helper, internal data structure that acts as a map but also allows getting / removing + * elements in the LIFO order + */ + .factory('$$stackedMap', function () { + return { + createNew: function () { + var stack = []; + + return { + add: function (key, value) { + stack.push({ + key: key, + value: value + }); + }, + get: function (key) { + for (var i = 0; i < stack.length; i++) { + if (key == stack[i].key) { + return stack[i]; + } + } + }, + keys: function() { + var keys = []; + for (var i = 0; i < stack.length; i++) { + keys.push(stack[i].key); + } + return keys; + }, + top: function () { + return stack[stack.length - 1]; + }, + remove: function (key) { + var idx = -1; + for (var i = 0; i < stack.length; i++) { + if (key == stack[i].key) { + idx = i; + break; + } + } + return stack.splice(idx, 1)[0]; + }, + removeTop: function () { + return stack.splice(stack.length - 1, 1)[0]; + }, + length: function () { + return stack.length; + } + }; + } + }; + }) + +/** + * A helper directive for the $modal service. It creates a backdrop element. + */ + .directive('modalBackdrop', ['$timeout', function ($timeout) { + return { + restrict: 'EA', + replace: true, + templateUrl: 'template/modal/backdrop.html', + link: function (scope, element, attrs) { + scope.backdropClass = attrs.backdropClass || ''; + + scope.animate = false; + + //trigger CSS transitions + $timeout(function () { + scope.animate = true; + }); + } + }; + }]) + + .directive('modalWindow', ['$modalStack', '$timeout', function ($modalStack, $timeout) { + return { + restrict: 'EA', + scope: { + index: '@', + animate: '=' + }, + replace: true, + transclude: true, + templateUrl: function(tElement, tAttrs) { + return tAttrs.templateUrl || 'template/modal/window.html'; + }, + link: function (scope, element, attrs) { + element.addClass(attrs.windowClass || ''); + scope.size = attrs.size; + + $timeout(function () { + // trigger CSS transitions + scope.animate = true; + + /** + * Auto-focusing of a freshly-opened modal element causes any child elements + * with the autofocus attribute to lose focus. This is an issue on touch + * based devices which will show and then hide the onscreen keyboard. + * Attempts to refocus the autofocus element via JavaScript will not reopen + * the onscreen keyboard. Fixed by updated the focusing logic to only autofocus + * the modal element if the modal does not contain an autofocus element. + */ + if (!element[0].querySelectorAll('[autofocus]').length) { + element[0].focus(); + } + }); + + scope.close = function (evt) { + var modal = $modalStack.getTop(); + if (modal && modal.value.backdrop && modal.value.backdrop != 'static' && (evt.target === evt.currentTarget)) { + evt.preventDefault(); + evt.stopPropagation(); + $modalStack.dismiss(modal.key, 'backdrop click'); + } + }; + } + }; + }]) + + .directive('modalTransclude', function () { + return { + link: function($scope, $element, $attrs, controller, $transclude) { + $transclude($scope.$parent, function(clone) { + $element.empty(); + $element.append(clone); + }); + } + }; + }) + + .factory('$modalStack', ['$transition', '$timeout', '$document', '$compile', '$rootScope', '$$stackedMap', + function ($transition, $timeout, $document, $compile, $rootScope, $$stackedMap) { + + var OPENED_MODAL_CLASS = 'modal-open'; + + var backdropDomEl, backdropScope; + var openedWindows = $$stackedMap.createNew(); + var $modalStack = {}; + + function backdropIndex() { + var topBackdropIndex = -1; + var opened = openedWindows.keys(); + for (var i = 0; i < opened.length; i++) { + if (openedWindows.get(opened[i]).value.backdrop) { + topBackdropIndex = i; + } + } + return topBackdropIndex; + } + + $rootScope.$watch(backdropIndex, function(newBackdropIndex){ + if (backdropScope) { + backdropScope.index = newBackdropIndex; + } + }); + + function removeModalWindow(modalInstance) { + + var body = $document.find('body').eq(0); + var modalWindow = openedWindows.get(modalInstance).value; + + //clean up the stack + openedWindows.remove(modalInstance); + + //remove window DOM element + removeAfterAnimate(modalWindow.modalDomEl, modalWindow.modalScope, 300, function() { + modalWindow.modalScope.$destroy(); + body.toggleClass(OPENED_MODAL_CLASS, openedWindows.length() > 0); + checkRemoveBackdrop(); + }); + } + + function checkRemoveBackdrop() { + //remove backdrop if no longer needed + if (backdropDomEl && backdropIndex() == -1) { + var backdropScopeRef = backdropScope; + removeAfterAnimate(backdropDomEl, backdropScope, 150, function () { + backdropScopeRef.$destroy(); + backdropScopeRef = null; + }); + backdropDomEl = undefined; + backdropScope = undefined; + } + } + + function removeAfterAnimate(domEl, scope, emulateTime, done) { + // Closing animation + scope.animate = false; + + var transitionEndEventName = $transition.transitionEndEventName; + if (transitionEndEventName) { + // transition out + var timeout = $timeout(afterAnimating, emulateTime); + + domEl.bind(transitionEndEventName, function () { + $timeout.cancel(timeout); + afterAnimating(); + scope.$apply(); + }); + } else { + // Ensure this call is async + $timeout(afterAnimating); + } + + function afterAnimating() { + if (afterAnimating.done) { + return; + } + afterAnimating.done = true; + + domEl.remove(); + if (done) { + done(); + } + } + } + + $document.bind('keydown', function (evt) { + var modal; + + if (evt.which === 27) { + modal = openedWindows.top(); + if (modal && modal.value.keyboard) { + evt.preventDefault(); + $rootScope.$apply(function () { + $modalStack.dismiss(modal.key, 'escape key press'); + }); + } + } + }); + + $modalStack.open = function (modalInstance, modal) { + + openedWindows.add(modalInstance, { + deferred: modal.deferred, + modalScope: modal.scope, + backdrop: modal.backdrop, + keyboard: modal.keyboard + }); + + var body = $document.find('body').eq(0), + currBackdropIndex = backdropIndex(); + + if (currBackdropIndex >= 0 && !backdropDomEl) { + backdropScope = $rootScope.$new(true); + backdropScope.index = currBackdropIndex; + var angularBackgroundDomEl = angular.element('
'); + angularBackgroundDomEl.attr('backdrop-class', modal.backdropClass); + backdropDomEl = $compile(angularBackgroundDomEl)(backdropScope); + body.append(backdropDomEl); + } + + var angularDomEl = angular.element('
'); + angularDomEl.attr({ + 'template-url': modal.windowTemplateUrl, + 'window-class': modal.windowClass, + 'size': modal.size, + 'index': openedWindows.length() - 1, + 'animate': 'animate' + }).html(modal.content); + + var modalDomEl = $compile(angularDomEl)(modal.scope); + openedWindows.top().value.modalDomEl = modalDomEl; + body.append(modalDomEl); + body.addClass(OPENED_MODAL_CLASS); + }; + + $modalStack.close = function (modalInstance, result) { + var modalWindow = openedWindows.get(modalInstance); + if (modalWindow) { + modalWindow.value.deferred.resolve(result); + removeModalWindow(modalInstance); + } + }; + + $modalStack.dismiss = function (modalInstance, reason) { + var modalWindow = openedWindows.get(modalInstance); + if (modalWindow) { + modalWindow.value.deferred.reject(reason); + removeModalWindow(modalInstance); + } + }; + + $modalStack.dismissAll = function (reason) { + var topModal = this.getTop(); + while (topModal) { + this.dismiss(topModal.key, reason); + topModal = this.getTop(); + } + }; + + $modalStack.getTop = function () { + return openedWindows.top(); + }; + + return $modalStack; + }]) + + .provider('$modal', function () { + + var $modalProvider = { + options: { + backdrop: true, //can be also false or 'static' + keyboard: true + }, + $get: ['$injector', '$rootScope', '$q', '$http', '$templateCache', '$controller', '$modalStack', + function ($injector, $rootScope, $q, $http, $templateCache, $controller, $modalStack) { + + var $modal = {}; + + function getTemplatePromise(options) { + return options.template ? $q.when(options.template) : + $http.get(angular.isFunction(options.templateUrl) ? (options.templateUrl)() : options.templateUrl, + {cache: $templateCache}).then(function (result) { + return result.data; + }); + } + + function getResolvePromises(resolves) { + var promisesArr = []; + angular.forEach(resolves, function (value) { + if (angular.isFunction(value) || angular.isArray(value)) { + promisesArr.push($q.when($injector.invoke(value))); + } + }); + return promisesArr; + } + + $modal.open = function (modalOptions) { + + var modalResultDeferred = $q.defer(); + var modalOpenedDeferred = $q.defer(); + + //prepare an instance of a modal to be injected into controllers and returned to a caller + var modalInstance = { + result: modalResultDeferred.promise, + opened: modalOpenedDeferred.promise, + close: function (result) { + $modalStack.close(modalInstance, result); + }, + dismiss: function (reason) { + $modalStack.dismiss(modalInstance, reason); + } + }; + + //merge and clean up options + modalOptions = angular.extend({}, $modalProvider.options, modalOptions); + modalOptions.resolve = modalOptions.resolve || {}; + + //verify options + if (!modalOptions.template && !modalOptions.templateUrl) { + throw new Error('One of template or templateUrl options is required.'); + } + + var templateAndResolvePromise = + $q.all([getTemplatePromise(modalOptions)].concat(getResolvePromises(modalOptions.resolve))); + + + templateAndResolvePromise.then(function resolveSuccess(tplAndVars) { + + var modalScope = (modalOptions.scope || $rootScope).$new(); + modalScope.$close = modalInstance.close; + modalScope.$dismiss = modalInstance.dismiss; + + var ctrlInstance, ctrlLocals = {}; + var resolveIter = 1; + + //controllers + if (modalOptions.controller) { + ctrlLocals.$scope = modalScope; + ctrlLocals.$modalInstance = modalInstance; + angular.forEach(modalOptions.resolve, function (value, key) { + ctrlLocals[key] = tplAndVars[resolveIter++]; + }); + + ctrlInstance = $controller(modalOptions.controller, ctrlLocals); + if (modalOptions.controllerAs) { + modalScope[modalOptions.controllerAs] = ctrlInstance; + } + } + + $modalStack.open(modalInstance, { + scope: modalScope, + deferred: modalResultDeferred, + content: tplAndVars[0], + backdrop: modalOptions.backdrop, + keyboard: modalOptions.keyboard, + backdropClass: modalOptions.backdropClass, + windowClass: modalOptions.windowClass, + windowTemplateUrl: modalOptions.windowTemplateUrl, + size: modalOptions.size + }); + + }, function resolveError(reason) { + modalResultDeferred.reject(reason); + }); + + templateAndResolvePromise.then(function () { + modalOpenedDeferred.resolve(true); + }, function () { + modalOpenedDeferred.reject(false); + }); + + return modalInstance; + }; + + return $modal; + }] + }; + + return $modalProvider; + }); + +angular.module('ui.bootstrap.pagination', []) + +.controller('PaginationController', ['$scope', '$attrs', '$parse', function ($scope, $attrs, $parse) { + var self = this, + ngModelCtrl = { $setViewValue: angular.noop }, // nullModelCtrl + setNumPages = $attrs.numPages ? $parse($attrs.numPages).assign : angular.noop; + + this.init = function(ngModelCtrl_, config) { + ngModelCtrl = ngModelCtrl_; + this.config = config; + + ngModelCtrl.$render = function() { + self.render(); + }; + + if ($attrs.itemsPerPage) { + $scope.$parent.$watch($parse($attrs.itemsPerPage), function(value) { + self.itemsPerPage = parseInt(value, 10); + $scope.totalPages = self.calculateTotalPages(); + }); + } else { + this.itemsPerPage = config.itemsPerPage; + } + }; + + this.calculateTotalPages = function() { + var totalPages = this.itemsPerPage < 1 ? 1 : Math.ceil($scope.totalItems / this.itemsPerPage); + return Math.max(totalPages || 0, 1); + }; + + this.render = function() { + $scope.page = parseInt(ngModelCtrl.$viewValue, 10) || 1; + }; + + $scope.selectPage = function(page) { + if ( $scope.page !== page && page > 0 && page <= $scope.totalPages) { + ngModelCtrl.$setViewValue(page); + ngModelCtrl.$render(); + } + }; + + $scope.getText = function( key ) { + return $scope[key + 'Text'] || self.config[key + 'Text']; + }; + $scope.noPrevious = function() { + return $scope.page === 1; + }; + $scope.noNext = function() { + return $scope.page === $scope.totalPages; + }; + + $scope.$watch('totalItems', function() { + $scope.totalPages = self.calculateTotalPages(); + }); + + $scope.$watch('totalPages', function(value) { + setNumPages($scope.$parent, value); // Readonly variable + + if ( $scope.page > value ) { + $scope.selectPage(value); + } else { + ngModelCtrl.$render(); + } + }); +}]) + +.constant('paginationConfig', { + itemsPerPage: 10, + boundaryLinks: false, + directionLinks: true, + firstText: 'First', + previousText: 'Previous', + nextText: 'Next', + lastText: 'Last', + rotate: true +}) + +.directive('pagination', ['$parse', 'paginationConfig', function($parse, paginationConfig) { + return { + restrict: 'EA', + scope: { + totalItems: '=', + firstText: '@', + previousText: '@', + nextText: '@', + lastText: '@' + }, + require: ['pagination', '?ngModel'], + controller: 'PaginationController', + templateUrl: 'template/pagination/pagination.html', + replace: true, + link: function(scope, element, attrs, ctrls) { + var paginationCtrl = ctrls[0], ngModelCtrl = ctrls[1]; + + if (!ngModelCtrl) { + return; // do nothing if no ng-model + } + + // Setup configuration parameters + var maxSize = angular.isDefined(attrs.maxSize) ? scope.$parent.$eval(attrs.maxSize) : paginationConfig.maxSize, + rotate = angular.isDefined(attrs.rotate) ? scope.$parent.$eval(attrs.rotate) : paginationConfig.rotate; + scope.boundaryLinks = angular.isDefined(attrs.boundaryLinks) ? scope.$parent.$eval(attrs.boundaryLinks) : paginationConfig.boundaryLinks; + scope.directionLinks = angular.isDefined(attrs.directionLinks) ? scope.$parent.$eval(attrs.directionLinks) : paginationConfig.directionLinks; + + paginationCtrl.init(ngModelCtrl, paginationConfig); + + if (attrs.maxSize) { + scope.$parent.$watch($parse(attrs.maxSize), function(value) { + maxSize = parseInt(value, 10); + paginationCtrl.render(); + }); + } + + // Create page object used in template + function makePage(number, text, isActive) { + return { + number: number, + text: text, + active: isActive + }; + } + + function getPages(currentPage, totalPages) { + var pages = []; + + // Default page limits + var startPage = 1, endPage = totalPages; + var isMaxSized = ( angular.isDefined(maxSize) && maxSize < totalPages ); + + // recompute if maxSize + if ( isMaxSized ) { + if ( rotate ) { + // Current page is displayed in the middle of the visible ones + startPage = Math.max(currentPage - Math.floor(maxSize/2), 1); + endPage = startPage + maxSize - 1; + + // Adjust if limit is exceeded + if (endPage > totalPages) { + endPage = totalPages; + startPage = endPage - maxSize + 1; + } + } else { + // Visible pages are paginated with maxSize + startPage = ((Math.ceil(currentPage / maxSize) - 1) * maxSize) + 1; + + // Adjust last page if limit is exceeded + endPage = Math.min(startPage + maxSize - 1, totalPages); + } + } + + // Add page number links + for (var number = startPage; number <= endPage; number++) { + var page = makePage(number, number, number === currentPage); + pages.push(page); + } + + // Add links to move between page sets + if ( isMaxSized && ! rotate ) { + if ( startPage > 1 ) { + var previousPageSet = makePage(startPage - 1, '...', false); + pages.unshift(previousPageSet); + } + + if ( endPage < totalPages ) { + var nextPageSet = makePage(endPage + 1, '...', false); + pages.push(nextPageSet); + } + } + + return pages; + } + + var originalRender = paginationCtrl.render; + paginationCtrl.render = function() { + originalRender(); + if (scope.page > 0 && scope.page <= scope.totalPages) { + scope.pages = getPages(scope.page, scope.totalPages); + } + }; + } + }; +}]) + +.constant('pagerConfig', { + itemsPerPage: 10, + previousText: '« Previous', + nextText: 'Next »', + align: true +}) + +.directive('pager', ['pagerConfig', function(pagerConfig) { + return { + restrict: 'EA', + scope: { + totalItems: '=', + previousText: '@', + nextText: '@' + }, + require: ['pager', '?ngModel'], + controller: 'PaginationController', + templateUrl: 'template/pagination/pager.html', + replace: true, + link: function(scope, element, attrs, ctrls) { + var paginationCtrl = ctrls[0], ngModelCtrl = ctrls[1]; + + if (!ngModelCtrl) { + return; // do nothing if no ng-model + } + + scope.align = angular.isDefined(attrs.align) ? scope.$parent.$eval(attrs.align) : pagerConfig.align; + paginationCtrl.init(ngModelCtrl, pagerConfig); + } + }; +}]); + +/** + * The following features are still outstanding: animation as a + * function, placement as a function, inside, support for more triggers than + * just mouse enter/leave, html tooltips, and selector delegation. + */ +angular.module( 'ui.bootstrap.tooltip', [ 'ui.bootstrap.position', 'ui.bootstrap.bindHtml' ] ) + +/** + * The $tooltip service creates tooltip- and popover-like directives as well as + * houses global options for them. + */ +.provider( '$tooltip', function () { + // The default options tooltip and popover. + var defaultOptions = { + placement: 'top', + animation: true, + popupDelay: 0 + }; + + // Default hide triggers for each show trigger + var triggerMap = { + 'mouseenter': 'mouseleave', + 'click': 'click', + 'focus': 'blur' + }; + + // The options specified to the provider globally. + var globalOptions = {}; + + /** + * `options({})` allows global configuration of all tooltips in the + * application. + * + * var app = angular.module( 'App', ['ui.bootstrap.tooltip'], function( $tooltipProvider ) { + * // place tooltips left instead of top by default + * $tooltipProvider.options( { placement: 'left' } ); + * }); + */ + this.options = function( value ) { + angular.extend( globalOptions, value ); + }; + + /** + * This allows you to extend the set of trigger mappings available. E.g.: + * + * $tooltipProvider.setTriggers( 'openTrigger': 'closeTrigger' ); + */ + this.setTriggers = function setTriggers ( triggers ) { + angular.extend( triggerMap, triggers ); + }; + + /** + * This is a helper function for translating camel-case to snake-case. + */ + function snake_case(name){ + var regexp = /[A-Z]/g; + var separator = '-'; + return name.replace(regexp, function(letter, pos) { + return (pos ? separator : '') + letter.toLowerCase(); + }); + } + + /** + * Returns the actual instance of the $tooltip service. + * TODO support multiple triggers + */ + this.$get = [ '$window', '$compile', '$timeout', '$document', '$position', '$interpolate', function ( $window, $compile, $timeout, $document, $position, $interpolate ) { + return function $tooltip ( type, prefix, defaultTriggerShow ) { + var options = angular.extend( {}, defaultOptions, globalOptions ); + + /** + * Returns an object of show and hide triggers. + * + * If a trigger is supplied, + * it is used to show the tooltip; otherwise, it will use the `trigger` + * option passed to the `$tooltipProvider.options` method; else it will + * default to the trigger supplied to this directive factory. + * + * The hide trigger is based on the show trigger. If the `trigger` option + * was passed to the `$tooltipProvider.options` method, it will use the + * mapped trigger from `triggerMap` or the passed trigger if the map is + * undefined; otherwise, it uses the `triggerMap` value of the show + * trigger; else it will just use the show trigger. + */ + function getTriggers ( trigger ) { + var show = trigger || options.trigger || defaultTriggerShow; + var hide = triggerMap[show] || show; + return { + show: show, + hide: hide + }; + } + + var directiveName = snake_case( type ); + + var startSym = $interpolate.startSymbol(); + var endSym = $interpolate.endSymbol(); + var template = + '
'+ + '
'; + + return { + restrict: 'EA', + compile: function (tElem, tAttrs) { + var tooltipLinker = $compile( template ); + + return function link ( scope, element, attrs ) { + var tooltip; + var tooltipLinkedScope; + var transitionTimeout; + var popupTimeout; + var appendToBody = angular.isDefined( options.appendToBody ) ? options.appendToBody : false; + var triggers = getTriggers( undefined ); + var hasEnableExp = angular.isDefined(attrs[prefix+'Enable']); + var ttScope = scope.$new(true); + + var positionTooltip = function () { + + var ttPosition = $position.positionElements(element, tooltip, ttScope.placement, appendToBody); + ttPosition.top += 'px'; + ttPosition.left += 'px'; + + // Now set the calculated positioning. + tooltip.css( ttPosition ); + }; + + // By default, the tooltip is not open. + // TODO add ability to start tooltip opened + ttScope.isOpen = false; + + function toggleTooltipBind () { + if ( ! ttScope.isOpen ) { + showTooltipBind(); + } else { + hideTooltipBind(); + } + } + + // Show the tooltip with delay if specified, otherwise show it immediately + function showTooltipBind() { + if(hasEnableExp && !scope.$eval(attrs[prefix+'Enable'])) { + return; + } + + prepareTooltip(); + + if ( ttScope.popupDelay ) { + // Do nothing if the tooltip was already scheduled to pop-up. + // This happens if show is triggered multiple times before any hide is triggered. + if (!popupTimeout) { + popupTimeout = $timeout( show, ttScope.popupDelay, false ); + popupTimeout.then(function(reposition){reposition();}); + } + } else { + show()(); + } + } + + function hideTooltipBind () { + scope.$apply(function () { + hide(); + }); + } + + // Show the tooltip popup element. + function show() { + + popupTimeout = null; + + // If there is a pending remove transition, we must cancel it, lest the + // tooltip be mysteriously removed. + if ( transitionTimeout ) { + $timeout.cancel( transitionTimeout ); + transitionTimeout = null; + } + + // Don't show empty tooltips. + if ( ! ttScope.content ) { + return angular.noop; + } + + createTooltip(); + + // Set the initial positioning. + tooltip.css({ top: 0, left: 0, display: 'block' }); + ttScope.$digest(); + + positionTooltip(); + + // And show the tooltip. + ttScope.isOpen = true; + ttScope.$digest(); // digest required as $apply is not called + + // Return positioning function as promise callback for correct + // positioning after draw. + return positionTooltip; + } + + // Hide the tooltip popup element. + function hide() { + // First things first: we don't show it anymore. + ttScope.isOpen = false; + + //if tooltip is going to be shown after delay, we must cancel this + $timeout.cancel( popupTimeout ); + popupTimeout = null; + + // And now we remove it from the DOM. However, if we have animation, we + // need to wait for it to expire beforehand. + // FIXME: this is a placeholder for a port of the transitions library. + if ( ttScope.animation ) { + if (!transitionTimeout) { + transitionTimeout = $timeout(removeTooltip, 500); + } + } else { + removeTooltip(); + } + } + + function createTooltip() { + // There can only be one tooltip element per directive shown at once. + if (tooltip) { + removeTooltip(); + } + tooltipLinkedScope = ttScope.$new(); + tooltip = tooltipLinker(tooltipLinkedScope, function (tooltip) { + if ( appendToBody ) { + $document.find( 'body' ).append( tooltip ); + } else { + element.after( tooltip ); + } + }); + } + + function removeTooltip() { + transitionTimeout = null; + if (tooltip) { + tooltip.remove(); + tooltip = null; + } + if (tooltipLinkedScope) { + tooltipLinkedScope.$destroy(); + tooltipLinkedScope = null; + } + } + + function prepareTooltip() { + prepPlacement(); + prepPopupDelay(); + } + + /** + * Observe the relevant attributes. + */ + attrs.$observe( type, function ( val ) { + ttScope.content = val; + + if (!val && ttScope.isOpen ) { + hide(); + } + }); + + attrs.$observe( prefix+'Title', function ( val ) { + ttScope.title = val; + }); + + function prepPlacement() { + var val = attrs[ prefix + 'Placement' ]; + ttScope.placement = angular.isDefined( val ) ? val : options.placement; + } + + function prepPopupDelay() { + var val = attrs[ prefix + 'PopupDelay' ]; + var delay = parseInt( val, 10 ); + ttScope.popupDelay = ! isNaN(delay) ? delay : options.popupDelay; + } + + var unregisterTriggers = function () { + element.unbind(triggers.show, showTooltipBind); + element.unbind(triggers.hide, hideTooltipBind); + }; + + function prepTriggers() { + var val = attrs[ prefix + 'Trigger' ]; + unregisterTriggers(); + + triggers = getTriggers( val ); + + if ( triggers.show === triggers.hide ) { + element.bind( triggers.show, toggleTooltipBind ); + } else { + element.bind( triggers.show, showTooltipBind ); + element.bind( triggers.hide, hideTooltipBind ); + } + } + prepTriggers(); + + var animation = scope.$eval(attrs[prefix + 'Animation']); + ttScope.animation = angular.isDefined(animation) ? !!animation : options.animation; + + var appendToBodyVal = scope.$eval(attrs[prefix + 'AppendToBody']); + appendToBody = angular.isDefined(appendToBodyVal) ? appendToBodyVal : appendToBody; + + // if a tooltip is attached to we need to remove it on + // location change as its parent scope will probably not be destroyed + // by the change. + if ( appendToBody ) { + scope.$on('$locationChangeSuccess', function closeTooltipOnLocationChangeSuccess () { + if ( ttScope.isOpen ) { + hide(); + } + }); + } + + // Make sure tooltip is destroyed and removed. + scope.$on('$destroy', function onDestroyTooltip() { + $timeout.cancel( transitionTimeout ); + $timeout.cancel( popupTimeout ); + unregisterTriggers(); + removeTooltip(); + ttScope = null; + }); + }; + } + }; + }; + }]; +}) + +.directive( 'tooltipPopup', function () { + return { + restrict: 'EA', + replace: true, + scope: { content: '@', placement: '@', animation: '&', isOpen: '&' }, + templateUrl: 'template/tooltip/tooltip-popup.html' + }; +}) + +.directive( 'tooltip', [ '$tooltip', function ( $tooltip ) { + return $tooltip( 'tooltip', 'tooltip', 'mouseenter' ); +}]) + +.directive( 'tooltipHtmlUnsafePopup', function () { + return { + restrict: 'EA', + replace: true, + scope: { content: '@', placement: '@', animation: '&', isOpen: '&' }, + templateUrl: 'template/tooltip/tooltip-html-unsafe-popup.html' + }; +}) + +.directive( 'tooltipHtmlUnsafe', [ '$tooltip', function ( $tooltip ) { + return $tooltip( 'tooltipHtmlUnsafe', 'tooltip', 'mouseenter' ); +}]); + +/** + * The following features are still outstanding: popup delay, animation as a + * function, placement as a function, inside, support for more triggers than + * just mouse enter/leave, html popovers, and selector delegatation. + */ +angular.module( 'ui.bootstrap.popover', [ 'ui.bootstrap.tooltip' ] ) + +.directive( 'popoverPopup', function () { + return { + restrict: 'EA', + replace: true, + scope: { title: '@', content: '@', placement: '@', animation: '&', isOpen: '&' }, + templateUrl: 'template/popover/popover.html' + }; +}) + +.directive( 'popover', [ '$tooltip', function ( $tooltip ) { + return $tooltip( 'popover', 'popover', 'click' ); +}]); + +angular.module('ui.bootstrap.progressbar', []) + +.constant('progressConfig', { + animate: true, + max: 100 +}) + +.controller('ProgressController', ['$scope', '$attrs', 'progressConfig', function($scope, $attrs, progressConfig) { + var self = this, + animate = angular.isDefined($attrs.animate) ? $scope.$parent.$eval($attrs.animate) : progressConfig.animate; + + this.bars = []; + $scope.max = angular.isDefined($attrs.max) ? $scope.$parent.$eval($attrs.max) : progressConfig.max; + + this.addBar = function(bar, element) { + if ( !animate ) { + element.css({'transition': 'none'}); + } + + this.bars.push(bar); + + bar.$watch('value', function( value ) { + bar.percent = +(100 * value / $scope.max).toFixed(2); + }); + + bar.$on('$destroy', function() { + element = null; + self.removeBar(bar); + }); + }; + + this.removeBar = function(bar) { + this.bars.splice(this.bars.indexOf(bar), 1); + }; +}]) + +.directive('progress', function() { + return { + restrict: 'EA', + replace: true, + transclude: true, + controller: 'ProgressController', + require: 'progress', + scope: {}, + templateUrl: 'template/progressbar/progress.html' + }; +}) + +.directive('bar', function() { + return { + restrict: 'EA', + replace: true, + transclude: true, + require: '^progress', + scope: { + value: '=', + type: '@' + }, + templateUrl: 'template/progressbar/bar.html', + link: function(scope, element, attrs, progressCtrl) { + progressCtrl.addBar(scope, element); + } + }; +}) + +.directive('progressbar', function() { + return { + restrict: 'EA', + replace: true, + transclude: true, + controller: 'ProgressController', + scope: { + value: '=', + type: '@' + }, + templateUrl: 'template/progressbar/progressbar.html', + link: function(scope, element, attrs, progressCtrl) { + progressCtrl.addBar(scope, angular.element(element.children()[0])); + } + }; +}); +angular.module('ui.bootstrap.rating', []) + +.constant('ratingConfig', { + max: 5, + stateOn: null, + stateOff: null +}) + +.controller('RatingController', ['$scope', '$attrs', 'ratingConfig', function($scope, $attrs, ratingConfig) { + var ngModelCtrl = { $setViewValue: angular.noop }; + + this.init = function(ngModelCtrl_) { + ngModelCtrl = ngModelCtrl_; + ngModelCtrl.$render = this.render; + + this.stateOn = angular.isDefined($attrs.stateOn) ? $scope.$parent.$eval($attrs.stateOn) : ratingConfig.stateOn; + this.stateOff = angular.isDefined($attrs.stateOff) ? $scope.$parent.$eval($attrs.stateOff) : ratingConfig.stateOff; + + var ratingStates = angular.isDefined($attrs.ratingStates) ? $scope.$parent.$eval($attrs.ratingStates) : + new Array( angular.isDefined($attrs.max) ? $scope.$parent.$eval($attrs.max) : ratingConfig.max ); + $scope.range = this.buildTemplateObjects(ratingStates); + }; + + this.buildTemplateObjects = function(states) { + for (var i = 0, n = states.length; i < n; i++) { + states[i] = angular.extend({ index: i }, { stateOn: this.stateOn, stateOff: this.stateOff }, states[i]); + } + return states; + }; + + $scope.rate = function(value) { + if ( !$scope.readonly && value >= 0 && value <= $scope.range.length ) { + ngModelCtrl.$setViewValue(value); + ngModelCtrl.$render(); + } + }; + + $scope.enter = function(value) { + if ( !$scope.readonly ) { + $scope.value = value; + } + $scope.onHover({value: value}); + }; + + $scope.reset = function() { + $scope.value = ngModelCtrl.$viewValue; + $scope.onLeave(); + }; + + $scope.onKeydown = function(evt) { + if (/(37|38|39|40)/.test(evt.which)) { + evt.preventDefault(); + evt.stopPropagation(); + $scope.rate( $scope.value + (evt.which === 38 || evt.which === 39 ? 1 : -1) ); + } + }; + + this.render = function() { + $scope.value = ngModelCtrl.$viewValue; + }; +}]) + +.directive('rating', function() { + return { + restrict: 'EA', + require: ['rating', 'ngModel'], + scope: { + readonly: '=?', + onHover: '&', + onLeave: '&' + }, + controller: 'RatingController', + templateUrl: 'template/rating/rating.html', + replace: true, + link: function(scope, element, attrs, ctrls) { + var ratingCtrl = ctrls[0], ngModelCtrl = ctrls[1]; + + if ( ngModelCtrl ) { + ratingCtrl.init( ngModelCtrl ); + } + } + }; +}); + +/** + * @ngdoc overview + * @name ui.bootstrap.tabs + * + * @description + * AngularJS version of the tabs directive. + */ + +angular.module('ui.bootstrap.tabs', []) + +.controller('TabsetController', ['$scope', function TabsetCtrl($scope) { + var ctrl = this, + tabs = ctrl.tabs = $scope.tabs = []; + + ctrl.select = function(selectedTab) { + angular.forEach(tabs, function(tab) { + if (tab.active && tab !== selectedTab) { + tab.active = false; + tab.onDeselect(); + } + }); + selectedTab.active = true; + selectedTab.onSelect(); + }; + + ctrl.addTab = function addTab(tab) { + tabs.push(tab); + // we can't run the select function on the first tab + // since that would select it twice + if (tabs.length === 1) { + tab.active = true; + } else if (tab.active) { + ctrl.select(tab); + } + }; + + ctrl.removeTab = function removeTab(tab) { + var index = tabs.indexOf(tab); + //Select a new tab if the tab to be removed is selected and not destroyed + if (tab.active && tabs.length > 1 && !destroyed) { + //If this is the last tab, select the previous tab. else, the next tab. + var newActiveIndex = index == tabs.length - 1 ? index - 1 : index + 1; + ctrl.select(tabs[newActiveIndex]); + } + tabs.splice(index, 1); + }; + + var destroyed; + $scope.$on('$destroy', function() { + destroyed = true; + }); +}]) + +/** + * @ngdoc directive + * @name ui.bootstrap.tabs.directive:tabset + * @restrict EA + * + * @description + * Tabset is the outer container for the tabs directive + * + * @param {boolean=} vertical Whether or not to use vertical styling for the tabs. + * @param {boolean=} justified Whether or not to use justified styling for the tabs. + * + * @example + + + + First Content! + Second Content! + +
+ + First Vertical Content! + Second Vertical Content! + + + First Justified Content! + Second Justified Content! + +
+
+ */ +.directive('tabset', function() { + return { + restrict: 'EA', + transclude: true, + replace: true, + scope: { + type: '@' + }, + controller: 'TabsetController', + templateUrl: 'template/tabs/tabset.html', + link: function(scope, element, attrs) { + scope.vertical = angular.isDefined(attrs.vertical) ? scope.$parent.$eval(attrs.vertical) : false; + scope.justified = angular.isDefined(attrs.justified) ? scope.$parent.$eval(attrs.justified) : false; + } + }; +}) + +/** + * @ngdoc directive + * @name ui.bootstrap.tabs.directive:tab + * @restrict EA + * + * @param {string=} heading The visible heading, or title, of the tab. Set HTML headings with {@link ui.bootstrap.tabs.directive:tabHeading tabHeading}. + * @param {string=} select An expression to evaluate when the tab is selected. + * @param {boolean=} active A binding, telling whether or not this tab is selected. + * @param {boolean=} disabled A binding, telling whether or not this tab is disabled. + * + * @description + * Creates a tab with a heading and content. Must be placed within a {@link ui.bootstrap.tabs.directive:tabset tabset}. + * + * @example + + +
+ + +
+ + First Tab + + Alert me! + Second Tab, with alert callback and html heading! + + + {{item.content}} + + +
+
+ + function TabsDemoCtrl($scope) { + $scope.items = [ + { title:"Dynamic Title 1", content:"Dynamic Item 0" }, + { title:"Dynamic Title 2", content:"Dynamic Item 1", disabled: true } + ]; + + $scope.alertMe = function() { + setTimeout(function() { + alert("You've selected the alert tab!"); + }); + }; + }; + +
+ */ + +/** + * @ngdoc directive + * @name ui.bootstrap.tabs.directive:tabHeading + * @restrict EA + * + * @description + * Creates an HTML heading for a {@link ui.bootstrap.tabs.directive:tab tab}. Must be placed as a child of a tab element. + * + * @example + + + + + HTML in my titles?! + And some content, too! + + + Icon heading?!? + That's right. + + + + + */ +.directive('tab', ['$parse', function($parse) { + return { + require: '^tabset', + restrict: 'EA', + replace: true, + templateUrl: 'template/tabs/tab.html', + transclude: true, + scope: { + active: '=?', + heading: '@', + onSelect: '&select', //This callback is called in contentHeadingTransclude + //once it inserts the tab's content into the dom + onDeselect: '&deselect' + }, + controller: function() { + //Empty controller so other directives can require being 'under' a tab + }, + compile: function(elm, attrs, transclude) { + return function postLink(scope, elm, attrs, tabsetCtrl) { + scope.$watch('active', function(active) { + if (active) { + tabsetCtrl.select(scope); + } + }); + + scope.disabled = false; + if ( attrs.disabled ) { + scope.$parent.$watch($parse(attrs.disabled), function(value) { + scope.disabled = !! value; + }); + } + + scope.select = function() { + if ( !scope.disabled ) { + scope.active = true; + } + }; + + tabsetCtrl.addTab(scope); + scope.$on('$destroy', function() { + tabsetCtrl.removeTab(scope); + }); + + //We need to transclude later, once the content container is ready. + //when this link happens, we're inside a tab heading. + scope.$transcludeFn = transclude; + }; + } + }; +}]) + +.directive('tabHeadingTransclude', [function() { + return { + restrict: 'A', + require: '^tab', + link: function(scope, elm, attrs, tabCtrl) { + scope.$watch('headingElement', function updateHeadingElement(heading) { + if (heading) { + elm.html(''); + elm.append(heading); + } + }); + } + }; +}]) + +.directive('tabContentTransclude', function() { + return { + restrict: 'A', + require: '^tabset', + link: function(scope, elm, attrs) { + var tab = scope.$eval(attrs.tabContentTransclude); + + //Now our tab is ready to be transcluded: both the tab heading area + //and the tab content area are loaded. Transclude 'em both. + tab.$transcludeFn(tab.$parent, function(contents) { + angular.forEach(contents, function(node) { + if (isTabHeading(node)) { + //Let tabHeadingTransclude know. + tab.headingElement = node; + } else { + elm.append(node); + } + }); + }); + } + }; + function isTabHeading(node) { + return node.tagName && ( + node.hasAttribute('tab-heading') || + node.hasAttribute('data-tab-heading') || + node.tagName.toLowerCase() === 'tab-heading' || + node.tagName.toLowerCase() === 'data-tab-heading' + ); + } +}) + +; + +angular.module('ui.bootstrap.timepicker', []) + +.constant('timepickerConfig', { + hourStep: 1, + minuteStep: 1, + showMeridian: true, + meridians: null, + readonlyInput: false, + mousewheel: true +}) + +.controller('TimepickerController', ['$scope', '$attrs', '$parse', '$log', '$locale', 'timepickerConfig', function($scope, $attrs, $parse, $log, $locale, timepickerConfig) { + var selected = new Date(), + ngModelCtrl = { $setViewValue: angular.noop }, // nullModelCtrl + meridians = angular.isDefined($attrs.meridians) ? $scope.$parent.$eval($attrs.meridians) : timepickerConfig.meridians || $locale.DATETIME_FORMATS.AMPMS; + + this.init = function( ngModelCtrl_, inputs ) { + ngModelCtrl = ngModelCtrl_; + ngModelCtrl.$render = this.render; + + var hoursInputEl = inputs.eq(0), + minutesInputEl = inputs.eq(1); + + var mousewheel = angular.isDefined($attrs.mousewheel) ? $scope.$parent.$eval($attrs.mousewheel) : timepickerConfig.mousewheel; + if ( mousewheel ) { + this.setupMousewheelEvents( hoursInputEl, minutesInputEl ); + } + + $scope.readonlyInput = angular.isDefined($attrs.readonlyInput) ? $scope.$parent.$eval($attrs.readonlyInput) : timepickerConfig.readonlyInput; + this.setupInputEvents( hoursInputEl, minutesInputEl ); + }; + + var hourStep = timepickerConfig.hourStep; + if ($attrs.hourStep) { + $scope.$parent.$watch($parse($attrs.hourStep), function(value) { + hourStep = parseInt(value, 10); + }); + } + + var minuteStep = timepickerConfig.minuteStep; + if ($attrs.minuteStep) { + $scope.$parent.$watch($parse($attrs.minuteStep), function(value) { + minuteStep = parseInt(value, 10); + }); + } + + // 12H / 24H mode + $scope.showMeridian = timepickerConfig.showMeridian; + if ($attrs.showMeridian) { + $scope.$parent.$watch($parse($attrs.showMeridian), function(value) { + $scope.showMeridian = !!value; + + if ( ngModelCtrl.$error.time ) { + // Evaluate from template + var hours = getHoursFromTemplate(), minutes = getMinutesFromTemplate(); + if (angular.isDefined( hours ) && angular.isDefined( minutes )) { + selected.setHours( hours ); + refresh(); + } + } else { + updateTemplate(); + } + }); + } + + // Get $scope.hours in 24H mode if valid + function getHoursFromTemplate ( ) { + var hours = parseInt( $scope.hours, 10 ); + var valid = ( $scope.showMeridian ) ? (hours > 0 && hours < 13) : (hours >= 0 && hours < 24); + if ( !valid ) { + return undefined; + } + + if ( $scope.showMeridian ) { + if ( hours === 12 ) { + hours = 0; + } + if ( $scope.meridian === meridians[1] ) { + hours = hours + 12; + } + } + return hours; + } + + function getMinutesFromTemplate() { + var minutes = parseInt($scope.minutes, 10); + return ( minutes >= 0 && minutes < 60 ) ? minutes : undefined; + } + + function pad( value ) { + return ( angular.isDefined(value) && value.toString().length < 2 ) ? '0' + value : value; + } + + // Respond on mousewheel spin + this.setupMousewheelEvents = function( hoursInputEl, minutesInputEl ) { + var isScrollingUp = function(e) { + if (e.originalEvent) { + e = e.originalEvent; + } + //pick correct delta variable depending on event + var delta = (e.wheelDelta) ? e.wheelDelta : -e.deltaY; + return (e.detail || delta > 0); + }; + + hoursInputEl.bind('mousewheel wheel', function(e) { + $scope.$apply( (isScrollingUp(e)) ? $scope.incrementHours() : $scope.decrementHours() ); + e.preventDefault(); + }); + + minutesInputEl.bind('mousewheel wheel', function(e) { + $scope.$apply( (isScrollingUp(e)) ? $scope.incrementMinutes() : $scope.decrementMinutes() ); + e.preventDefault(); + }); + + }; + + this.setupInputEvents = function( hoursInputEl, minutesInputEl ) { + if ( $scope.readonlyInput ) { + $scope.updateHours = angular.noop; + $scope.updateMinutes = angular.noop; + return; + } + + var invalidate = function(invalidHours, invalidMinutes) { + ngModelCtrl.$setViewValue( null ); + ngModelCtrl.$setValidity('time', false); + if (angular.isDefined(invalidHours)) { + $scope.invalidHours = invalidHours; + } + if (angular.isDefined(invalidMinutes)) { + $scope.invalidMinutes = invalidMinutes; + } + }; + + $scope.updateHours = function() { + var hours = getHoursFromTemplate(); + + if ( angular.isDefined(hours) ) { + selected.setHours( hours ); + refresh( 'h' ); + } else { + invalidate(true); + } + }; + + hoursInputEl.bind('blur', function(e) { + if ( !$scope.invalidHours && $scope.hours < 10) { + $scope.$apply( function() { + $scope.hours = pad( $scope.hours ); + }); + } + }); + + $scope.updateMinutes = function() { + var minutes = getMinutesFromTemplate(); + + if ( angular.isDefined(minutes) ) { + selected.setMinutes( minutes ); + refresh( 'm' ); + } else { + invalidate(undefined, true); + } + }; + + minutesInputEl.bind('blur', function(e) { + if ( !$scope.invalidMinutes && $scope.minutes < 10 ) { + $scope.$apply( function() { + $scope.minutes = pad( $scope.minutes ); + }); + } + }); + + }; + + this.render = function() { + var date = ngModelCtrl.$modelValue ? new Date( ngModelCtrl.$modelValue ) : null; + + if ( isNaN(date) ) { + ngModelCtrl.$setValidity('time', false); + $log.error('Timepicker directive: "ng-model" value must be a Date object, a number of milliseconds since 01.01.1970 or a string representing an RFC2822 or ISO 8601 date.'); + } else { + if ( date ) { + selected = date; + } + makeValid(); + updateTemplate(); + } + }; + + // Call internally when we know that model is valid. + function refresh( keyboardChange ) { + makeValid(); + ngModelCtrl.$setViewValue( new Date(selected) ); + updateTemplate( keyboardChange ); + } + + function makeValid() { + ngModelCtrl.$setValidity('time', true); + $scope.invalidHours = false; + $scope.invalidMinutes = false; + } + + function updateTemplate( keyboardChange ) { + var hours = selected.getHours(), minutes = selected.getMinutes(); + + if ( $scope.showMeridian ) { + hours = ( hours === 0 || hours === 12 ) ? 12 : hours % 12; // Convert 24 to 12 hour system + } + + $scope.hours = keyboardChange === 'h' ? hours : pad(hours); + $scope.minutes = keyboardChange === 'm' ? minutes : pad(minutes); + $scope.meridian = selected.getHours() < 12 ? meridians[0] : meridians[1]; + } + + function addMinutes( minutes ) { + var dt = new Date( selected.getTime() + minutes * 60000 ); + selected.setHours( dt.getHours(), dt.getMinutes() ); + refresh(); + } + + $scope.incrementHours = function() { + addMinutes( hourStep * 60 ); + }; + $scope.decrementHours = function() { + addMinutes( - hourStep * 60 ); + }; + $scope.incrementMinutes = function() { + addMinutes( minuteStep ); + }; + $scope.decrementMinutes = function() { + addMinutes( - minuteStep ); + }; + $scope.toggleMeridian = function() { + addMinutes( 12 * 60 * (( selected.getHours() < 12 ) ? 1 : -1) ); + }; +}]) + +.directive('timepicker', function () { + return { + restrict: 'EA', + require: ['timepicker', '?^ngModel'], + controller:'TimepickerController', + replace: true, + scope: {}, + templateUrl: 'template/timepicker/timepicker.html', + link: function(scope, element, attrs, ctrls) { + var timepickerCtrl = ctrls[0], ngModelCtrl = ctrls[1]; + + if ( ngModelCtrl ) { + timepickerCtrl.init( ngModelCtrl, element.find('input') ); + } + } + }; +}); + +angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position', 'ui.bootstrap.bindHtml']) + +/** + * A helper service that can parse typeahead's syntax (string provided by users) + * Extracted to a separate service for ease of unit testing + */ + .factory('typeaheadParser', ['$parse', function ($parse) { + + // 00000111000000000000022200000000000000003333333333333330000000000044000 + var TYPEAHEAD_REGEXP = /^\s*([\s\S]+?)(?:\s+as\s+([\s\S]+?))?\s+for\s+(?:([\$\w][\$\w\d]*))\s+in\s+([\s\S]+?)$/; + + return { + parse:function (input) { + + var match = input.match(TYPEAHEAD_REGEXP); + if (!match) { + throw new Error( + 'Expected typeahead specification in form of "_modelValue_ (as _label_)? for _item_ in _collection_"' + + ' but got "' + input + '".'); + } + + return { + itemName:match[3], + source:$parse(match[4]), + viewMapper:$parse(match[2] || match[1]), + modelMapper:$parse(match[1]) + }; + } + }; +}]) + + .directive('typeahead', ['$compile', '$parse', '$q', '$timeout', '$document', '$position', 'typeaheadParser', + function ($compile, $parse, $q, $timeout, $document, $position, typeaheadParser) { + + var HOT_KEYS = [9, 13, 27, 38, 40]; + + return { + require:'ngModel', + link:function (originalScope, element, attrs, modelCtrl) { + + //SUPPORTED ATTRIBUTES (OPTIONS) + + //minimal no of characters that needs to be entered before typeahead kicks-in + var minSearch = originalScope.$eval(attrs.typeaheadMinLength) || 1; + + //minimal wait time after last character typed before typehead kicks-in + var waitTime = originalScope.$eval(attrs.typeaheadWaitMs) || 0; + + //should it restrict model values to the ones selected from the popup only? + var isEditable = originalScope.$eval(attrs.typeaheadEditable) !== false; + + //binding to a variable that indicates if matches are being retrieved asynchronously + var isLoadingSetter = $parse(attrs.typeaheadLoading).assign || angular.noop; + + //a callback executed when a match is selected + var onSelectCallback = $parse(attrs.typeaheadOnSelect); + + var inputFormatter = attrs.typeaheadInputFormatter ? $parse(attrs.typeaheadInputFormatter) : undefined; + + var appendToBody = attrs.typeaheadAppendToBody ? originalScope.$eval(attrs.typeaheadAppendToBody) : false; + + var focusFirst = originalScope.$eval(attrs.typeaheadFocusFirst) !== false; + + //INTERNAL VARIABLES + + //model setter executed upon match selection + var $setModelValue = $parse(attrs.ngModel).assign; + + //expressions used by typeahead + var parserResult = typeaheadParser.parse(attrs.typeahead); + + var hasFocus; + + //create a child scope for the typeahead directive so we are not polluting original scope + //with typeahead-specific data (matches, query etc.) + var scope = originalScope.$new(); + originalScope.$on('$destroy', function(){ + scope.$destroy(); + }); + + // WAI-ARIA + var popupId = 'typeahead-' + scope.$id + '-' + Math.floor(Math.random() * 10000); + element.attr({ + 'aria-autocomplete': 'list', + 'aria-expanded': false, + 'aria-owns': popupId + }); + + //pop-up element used to display matches + var popUpEl = angular.element('
'); + popUpEl.attr({ + id: popupId, + matches: 'matches', + active: 'activeIdx', + select: 'select(activeIdx)', + query: 'query', + position: 'position' + }); + //custom item template + if (angular.isDefined(attrs.typeaheadTemplateUrl)) { + popUpEl.attr('template-url', attrs.typeaheadTemplateUrl); + } + + var resetMatches = function() { + scope.matches = []; + scope.activeIdx = -1; + element.attr('aria-expanded', false); + }; + + var getMatchId = function(index) { + return popupId + '-option-' + index; + }; + + // Indicate that the specified match is the active (pre-selected) item in the list owned by this typeahead. + // This attribute is added or removed automatically when the `activeIdx` changes. + scope.$watch('activeIdx', function(index) { + if (index < 0) { + element.removeAttr('aria-activedescendant'); + } else { + element.attr('aria-activedescendant', getMatchId(index)); + } + }); + + var getMatchesAsync = function(inputValue) { + + var locals = {$viewValue: inputValue}; + isLoadingSetter(originalScope, true); + $q.when(parserResult.source(originalScope, locals)).then(function(matches) { + + //it might happen that several async queries were in progress if a user were typing fast + //but we are interested only in responses that correspond to the current view value + var onCurrentRequest = (inputValue === modelCtrl.$viewValue); + if (onCurrentRequest && hasFocus) { + if (matches.length > 0) { + + scope.activeIdx = focusFirst ? 0 : -1; + scope.matches.length = 0; + + //transform labels + for(var i=0; i= minSearch) { + if (waitTime > 0) { + cancelPreviousTimeout(); + scheduleSearchWithTimeout(inputValue); + } else { + getMatchesAsync(inputValue); + } + } else { + isLoadingSetter(originalScope, false); + cancelPreviousTimeout(); + resetMatches(); + } + + if (isEditable) { + return inputValue; + } else { + if (!inputValue) { + // Reset in case user had typed something previously. + modelCtrl.$setValidity('editable', true); + return inputValue; + } else { + modelCtrl.$setValidity('editable', false); + return undefined; + } + } + }); + + modelCtrl.$formatters.push(function (modelValue) { + + var candidateViewValue, emptyViewValue; + var locals = {}; + + if (inputFormatter) { + + locals.$model = modelValue; + return inputFormatter(originalScope, locals); + + } else { + + //it might happen that we don't have enough info to properly render input value + //we need to check for this situation and simply return model value if we can't apply custom formatting + locals[parserResult.itemName] = modelValue; + candidateViewValue = parserResult.viewMapper(originalScope, locals); + locals[parserResult.itemName] = undefined; + emptyViewValue = parserResult.viewMapper(originalScope, locals); + + return candidateViewValue!== emptyViewValue ? candidateViewValue : modelValue; + } + }); + + scope.select = function (activeIdx) { + //called from within the $digest() cycle + var locals = {}; + var model, item; + + locals[parserResult.itemName] = item = scope.matches[activeIdx].model; + model = parserResult.modelMapper(originalScope, locals); + $setModelValue(originalScope, model); + modelCtrl.$setValidity('editable', true); + + onSelectCallback(originalScope, { + $item: item, + $model: model, + $label: parserResult.viewMapper(originalScope, locals) + }); + + resetMatches(); + + //return focus to the input element if a match was selected via a mouse click event + // use timeout to avoid $rootScope:inprog error + $timeout(function() { element[0].focus(); }, 0, false); + }; + + //bind keyboard events: arrows up(38) / down(40), enter(13) and tab(9), esc(27) + element.bind('keydown', function (evt) { + + //typeahead is open and an "interesting" key was pressed + if (scope.matches.length === 0 || HOT_KEYS.indexOf(evt.which) === -1) { + return; + } + + // if there's nothing selected (i.e. focusFirst) and enter is hit, don't do anything + if (scope.activeIdx == -1 && (evt.which === 13 || evt.which === 9)) { + return; + } + + evt.preventDefault(); + + if (evt.which === 40) { + scope.activeIdx = (scope.activeIdx + 1) % scope.matches.length; + scope.$digest(); + + } else if (evt.which === 38) { + scope.activeIdx = (scope.activeIdx > 0 ? scope.activeIdx : scope.matches.length) - 1; + scope.$digest(); + + } else if (evt.which === 13 || evt.which === 9) { + scope.$apply(function () { + scope.select(scope.activeIdx); + }); + + } else if (evt.which === 27) { + evt.stopPropagation(); + + resetMatches(); + scope.$digest(); + } + }); + + element.bind('blur', function (evt) { + hasFocus = false; + }); + + // Keep reference to click handler to unbind it. + var dismissClickHandler = function (evt) { + if (element[0] !== evt.target) { + resetMatches(); + scope.$digest(); + } + }; + + $document.bind('click', dismissClickHandler); + + originalScope.$on('$destroy', function(){ + $document.unbind('click', dismissClickHandler); + if (appendToBody) { + $popup.remove(); + } + }); + + var $popup = $compile(popUpEl)(scope); + if (appendToBody) { + $document.find('body').append($popup); + } else { + element.after($popup); + } + } + }; + +}]) + + .directive('typeaheadPopup', function () { + return { + restrict:'EA', + scope:{ + matches:'=', + query:'=', + active:'=', + position:'=', + select:'&' + }, + replace:true, + templateUrl:'template/typeahead/typeahead-popup.html', + link:function (scope, element, attrs) { + + scope.templateUrl = attrs.templateUrl; + + scope.isOpen = function () { + return scope.matches.length > 0; + }; + + scope.isActive = function (matchIdx) { + return scope.active == matchIdx; + }; + + scope.selectActive = function (matchIdx) { + scope.active = matchIdx; + }; + + scope.selectMatch = function (activeIdx) { + scope.select({activeIdx:activeIdx}); + }; + } + }; + }) + + .directive('typeaheadMatch', ['$http', '$templateCache', '$compile', '$parse', function ($http, $templateCache, $compile, $parse) { + return { + restrict:'EA', + scope:{ + index:'=', + match:'=', + query:'=' + }, + link:function (scope, element, attrs) { + var tplUrl = $parse(attrs.templateUrl)(scope.$parent) || 'template/typeahead/typeahead-match.html'; + $http.get(tplUrl, {cache: $templateCache}).success(function(tplContent){ + element.replaceWith($compile(tplContent.trim())(scope)); + }); + } + }; + }]) + + .filter('typeaheadHighlight', function() { + + function escapeRegexp(queryToEscape) { + return queryToEscape.replace(/([.?*+^$[\]\\(){}|-])/g, '\\$1'); + } + + return function(matchItem, query) { + return query ? ('' + matchItem).replace(new RegExp(escapeRegexp(query), 'gi'), '$&') : matchItem; + }; + }); + +angular.module("template/accordion/accordion-group.html", []).run(["$templateCache", function($templateCache) { + $templateCache.put("template/accordion/accordion-group.html", + "
\n" + + "
\n" + + "

\n" + + " {{heading}}\n" + + "

\n" + + "
\n" + + "
\n" + + "
\n" + + "
\n" + + "
\n" + + ""); +}]); + +angular.module("template/accordion/accordion.html", []).run(["$templateCache", function($templateCache) { + $templateCache.put("template/accordion/accordion.html", + "
"); +}]); + +angular.module("template/alert/alert.html", []).run(["$templateCache", function($templateCache) { + $templateCache.put("template/alert/alert.html", + "
\n" + + " \n" + + "
\n" + + "
\n" + + ""); +}]); + +angular.module("template/carousel/carousel.html", []).run(["$templateCache", function($templateCache) { + $templateCache.put("template/carousel/carousel.html", + "
\n" + + "
    1\">\n" + + "
  1. \n" + + "
\n" + + "
\n" + + " 1\">\n" + + " 1\">\n" + + "
\n" + + ""); +}]); + +angular.module("template/carousel/slide.html", []).run(["$templateCache", function($templateCache) { + $templateCache.put("template/carousel/slide.html", + "
\n" + + ""); +}]); + +angular.module("template/datepicker/datepicker.html", []).run(["$templateCache", function($templateCache) { + $templateCache.put("template/datepicker/datepicker.html", + "
\n" + + " \n" + + " \n" + + " \n" + + "
"); +}]); + +angular.module("template/datepicker/day.html", []).run(["$templateCache", function($templateCache) { + $templateCache.put("template/datepicker/day.html", + "\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + "
{{label.abbr}}
{{ weekNumbers[$index] }}\n" + + " \n" + + "
\n" + + ""); +}]); + +angular.module("template/datepicker/month.html", []).run(["$templateCache", function($templateCache) { + $templateCache.put("template/datepicker/month.html", + "\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + "
\n" + + " \n" + + "
\n" + + ""); +}]); + +angular.module("template/datepicker/popup.html", []).run(["$templateCache", function($templateCache) { + $templateCache.put("template/datepicker/popup.html", + "
    \n" + + "
  • \n" + + "
  • \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + "
  • \n" + + "
\n" + + ""); +}]); + +angular.module("template/datepicker/year.html", []).run(["$templateCache", function($templateCache) { + $templateCache.put("template/datepicker/year.html", + "\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + "
\n" + + " \n" + + "
\n" + + ""); +}]); + +angular.module("template/modal/backdrop.html", []).run(["$templateCache", function($templateCache) { + $templateCache.put("template/modal/backdrop.html", + "
\n" + + ""); +}]); + +angular.module("template/modal/window.html", []).run(["$templateCache", function($templateCache) { + $templateCache.put("template/modal/window.html", + "
\n" + + "
\n" + + "
"); +}]); + +angular.module("template/pagination/pager.html", []).run(["$templateCache", function($templateCache) { + $templateCache.put("template/pagination/pager.html", + ""); +}]); + +angular.module("template/pagination/pagination.html", []).run(["$templateCache", function($templateCache) { + $templateCache.put("template/pagination/pagination.html", + ""); +}]); + +angular.module("template/tooltip/tooltip-html-unsafe-popup.html", []).run(["$templateCache", function($templateCache) { + $templateCache.put("template/tooltip/tooltip-html-unsafe-popup.html", + "
\n" + + "
\n" + + "
\n" + + "
\n" + + ""); +}]); + +angular.module("template/tooltip/tooltip-popup.html", []).run(["$templateCache", function($templateCache) { + $templateCache.put("template/tooltip/tooltip-popup.html", + "
\n" + + "
\n" + + "
\n" + + "
\n" + + ""); +}]); + +angular.module("template/popover/popover.html", []).run(["$templateCache", function($templateCache) { + $templateCache.put("template/popover/popover.html", + "
\n" + + "
\n" + + "\n" + + "
\n" + + "

\n" + + "
\n" + + "
\n" + + "
\n" + + ""); +}]); + +angular.module("template/progressbar/bar.html", []).run(["$templateCache", function($templateCache) { + $templateCache.put("template/progressbar/bar.html", + "
"); +}]); + +angular.module("template/progressbar/progress.html", []).run(["$templateCache", function($templateCache) { + $templateCache.put("template/progressbar/progress.html", + "
"); +}]); + +angular.module("template/progressbar/progressbar.html", []).run(["$templateCache", function($templateCache) { + $templateCache.put("template/progressbar/progressbar.html", + "
\n" + + "
\n" + + "
"); +}]); + +angular.module("template/rating/rating.html", []).run(["$templateCache", function($templateCache) { + $templateCache.put("template/rating/rating.html", + "\n" + + " \n" + + " ({{ $index < value ? '*' : ' ' }})\n" + + " \n" + + ""); +}]); + +angular.module("template/tabs/tab.html", []).run(["$templateCache", function($templateCache) { + $templateCache.put("template/tabs/tab.html", + "
  • \n" + + " {{heading}}\n" + + "
  • \n" + + ""); +}]); + +angular.module("template/tabs/tabset.html", []).run(["$templateCache", function($templateCache) { + $templateCache.put("template/tabs/tabset.html", + "
    \n" + + "
      \n" + + "
      \n" + + "
      \n" + + "
      \n" + + "
      \n" + + "
      \n" + + ""); +}]); + +angular.module("template/timepicker/timepicker.html", []).run(["$templateCache", function($templateCache) { + $templateCache.put("template/timepicker/timepicker.html", + "\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + "
       
      \n" + + " \n" + + " :\n" + + " \n" + + "
       
      \n" + + ""); +}]); + +angular.module("template/typeahead/typeahead-match.html", []).run(["$templateCache", function($templateCache) { + $templateCache.put("template/typeahead/typeahead-match.html", + ""); +}]); + +angular.module("template/typeahead/typeahead-popup.html", []).run(["$templateCache", function($templateCache) { + $templateCache.put("template/typeahead/typeahead-popup.html", + "
        \n" + + "
      • \n" + + "
        \n" + + "
      • \n" + + "
      \n" + + ""); +}]); diff --git a/webpackShims/ui-bootstrap.js b/webpackShims/ui-bootstrap.js index 423ef222de2652..36da11d01dc3ed 100644 --- a/webpackShims/ui-bootstrap.js +++ b/webpackShims/ui-bootstrap.js @@ -1,6 +1,6 @@ define(function (require) { require('angular'); - require('node_modules/@spalger/angular-bootstrap/ui-bootstrap-tpls'); + require('../src/ui/public/angular-bootstrap/ui-bootstrap-tpls'); return require('ui/modules') .get('kibana', ['ui.bootstrap']) From 90ed8249be9051364d0c20d7750b5da82b1c2212 Mon Sep 17 00:00:00 2001 From: CJ Cenizal Date: Wed, 29 Jun 2016 18:25:04 -0700 Subject: [PATCH 27/67] Remove angular-bootstrap accordion. --- .../angular-bootstrap/ui-bootstrap-tpls.js | 154 ------------------ 1 file changed, 154 deletions(-) diff --git a/src/ui/public/angular-bootstrap/ui-bootstrap-tpls.js b/src/ui/public/angular-bootstrap/ui-bootstrap-tpls.js index 86a31f7efc042f..b0962814757624 100644 --- a/src/ui/public/angular-bootstrap/ui-bootstrap-tpls.js +++ b/src/ui/public/angular-bootstrap/ui-bootstrap-tpls.js @@ -15,7 +15,6 @@ angular.module("ui.bootstrap", [ "ui.bootstrap.tpls", "ui.bootstrap.transition", "ui.bootstrap.collapse", - "ui.bootstrap.accordion", "ui.bootstrap.alert", "ui.bootstrap.bindHtml", "ui.bootstrap.buttons", @@ -36,8 +35,6 @@ angular.module("ui.bootstrap", [ ]); angular.module("ui.bootstrap.tpls", [ - "template/accordion/accordion-group.html", - "template/accordion/accordion.html", "template/alert/alert.html", "template/carousel/carousel.html", "template/carousel/slide.html", @@ -223,137 +220,6 @@ angular.module('ui.bootstrap.collapse', ['ui.bootstrap.transition']) }; }]); -angular.module('ui.bootstrap.accordion', ['ui.bootstrap.collapse']) - -.constant('accordionConfig', { - closeOthers: true -}) - -.controller('AccordionController', ['$scope', '$attrs', 'accordionConfig', function ($scope, $attrs, accordionConfig) { - - // This array keeps track of the accordion groups - this.groups = []; - - // Ensure that all the groups in this accordion are closed, unless close-others explicitly says not to - this.closeOthers = function(openGroup) { - var closeOthers = angular.isDefined($attrs.closeOthers) ? $scope.$eval($attrs.closeOthers) : accordionConfig.closeOthers; - if ( closeOthers ) { - angular.forEach(this.groups, function (group) { - if ( group !== openGroup ) { - group.isOpen = false; - } - }); - } - }; - - // This is called from the accordion-group directive to add itself to the accordion - this.addGroup = function(groupScope) { - var that = this; - this.groups.push(groupScope); - - groupScope.$on('$destroy', function (event) { - that.removeGroup(groupScope); - }); - }; - - // This is called from the accordion-group directive when to remove itself - this.removeGroup = function(group) { - var index = this.groups.indexOf(group); - if ( index !== -1 ) { - this.groups.splice(index, 1); - } - }; - -}]) - -// The accordion directive simply sets up the directive controller -// and adds an accordion CSS class to itself element. -.directive('accordion', function () { - return { - restrict:'EA', - controller:'AccordionController', - transclude: true, - replace: false, - templateUrl: 'template/accordion/accordion.html' - }; -}) - -// The accordion-group directive indicates a block of html that will expand and collapse in an accordion -.directive('accordionGroup', function() { - return { - require:'^accordion', // We need this directive to be inside an accordion - restrict:'EA', - transclude:true, // It transcludes the contents of the directive into the template - replace: true, // The element containing the directive will be replaced with the template - templateUrl:'template/accordion/accordion-group.html', - scope: { - heading: '@', // Interpolate the heading attribute onto this scope - isOpen: '=?', - isDisabled: '=?' - }, - controller: function() { - this.setHeading = function(element) { - this.heading = element; - }; - }, - link: function(scope, element, attrs, accordionCtrl) { - accordionCtrl.addGroup(scope); - - scope.$watch('isOpen', function(value) { - if ( value ) { - accordionCtrl.closeOthers(scope); - } - }); - - scope.toggleOpen = function() { - if ( !scope.isDisabled ) { - scope.isOpen = !scope.isOpen; - } - }; - } - }; -}) - -// Use accordion-heading below an accordion-group to provide a heading containing HTML -// -// Heading containing HTML - -// -.directive('accordionHeading', function() { - return { - restrict: 'EA', - transclude: true, // Grab the contents to be used as the heading - template: '', // In effect remove this element! - replace: true, - require: '^accordionGroup', - link: function(scope, element, attr, accordionGroupCtrl, transclude) { - // Pass the heading to the accordion-group controller - // so that it can be transcluded into the right place in the template - // [The second parameter to transclude causes the elements to be cloned so that they work in ng-repeat] - accordionGroupCtrl.setHeading(transclude(scope, function() {})); - } - }; -}) - -// Use in the accordion-group template to indicate where you want the heading to be transcluded -// You must provide the property on the accordion-group controller that will hold the transcluded element -//
      -// -// ... -//
      -.directive('accordionTransclude', function() { - return { - require: '^accordionGroup', - link: function(scope, element, attr, controller) { - scope.$watch(function() { return controller[attr.accordionTransclude]; }, function(heading) { - if ( heading ) { - element.html(''); - element.append(heading); - } - }); - } - }; -}); - angular.module('ui.bootstrap.alert', []) .controller('AlertController', ['$scope', '$attrs', function ($scope, $attrs) { @@ -3957,26 +3823,6 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position', 'ui.bootstrap }; }); -angular.module("template/accordion/accordion-group.html", []).run(["$templateCache", function($templateCache) { - $templateCache.put("template/accordion/accordion-group.html", - "
      \n" + - "
      \n" + - "

      \n" + - " {{heading}}\n" + - "

      \n" + - "
      \n" + - "
      \n" + - "
      \n" + - "
      \n" + - "
      \n" + - ""); -}]); - -angular.module("template/accordion/accordion.html", []).run(["$templateCache", function($templateCache) { - $templateCache.put("template/accordion/accordion.html", - "
      "); -}]); - angular.module("template/alert/alert.html", []).run(["$templateCache", function($templateCache) { $templateCache.put("template/alert/alert.html", "
      \n" + From b556ccc0e2275f6a9a4519dd61778bcd0ea674a1 Mon Sep 17 00:00:00 2001 From: CJ Cenizal Date: Wed, 29 Jun 2016 19:06:25 -0700 Subject: [PATCH 28/67] Use ui alias to require ui-bootstrap in the webpackShim. --- webpackShims/ui-bootstrap.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webpackShims/ui-bootstrap.js b/webpackShims/ui-bootstrap.js index 36da11d01dc3ed..5cc959f2a2dc1b 100644 --- a/webpackShims/ui-bootstrap.js +++ b/webpackShims/ui-bootstrap.js @@ -1,6 +1,6 @@ define(function (require) { require('angular'); - require('../src/ui/public/angular-bootstrap/ui-bootstrap-tpls'); + require('ui/angular-bootstrap/ui-bootstrap-tpls'); return require('ui/modules') .get('kibana', ['ui.bootstrap']) From b08f016cc6af2e96b43d8a02fa4f1e33e05e9b06 Mon Sep 17 00:00:00 2001 From: CJ Cenizal Date: Mon, 4 Jul 2016 15:29:40 -0700 Subject: [PATCH 29/67] Replace compiled angular-bootstrap file with original HTML and JS source files. --- .eslintignore | 2 +- Gruntfile.js | 1 + .../accordion/accordion-group.html | 10 + .../accordion/accordion.html | 1 + .../angular-bootstrap/accordion/accordion.js | 130 + .../public/angular-bootstrap/alert/alert.html | 7 + .../public/angular-bootstrap/alert/alert.js | 31 + .../angular-bootstrap/bindHtml/bindHtml.js | 10 + .../angular-bootstrap/buttons/buttons.js | 74 + .../angular-bootstrap/carousel/carousel.html | 8 + .../angular-bootstrap/carousel/carousel.js | 293 ++ .../angular-bootstrap/carousel/slide.html | 7 + .../angular-bootstrap/collapse/collapse.js | 75 + .../dateparser/dateparser.js | 126 + .../datepicker/datepicker.html | 5 + .../datepicker/datepicker.js | 639 +++ .../angular-bootstrap/datepicker/day.html | 21 + .../angular-bootstrap/datepicker/month.html | 16 + .../angular-bootstrap/datepicker/popup.html | 10 + .../angular-bootstrap/datepicker/year.html | 16 + .../angular-bootstrap/dropdown/dropdown.js | 161 + src/ui/public/angular-bootstrap/index.js | 229 + .../angular-bootstrap/modal/backdrop.html | 4 + .../public/angular-bootstrap/modal/modal.js | 415 ++ .../angular-bootstrap/modal/window.html | 3 + .../angular-bootstrap/pagination/pager.html | 4 + .../pagination/pagination.html | 7 + .../pagination/pagination.js | 214 + .../angular-bootstrap/popover/popover.html | 8 + .../angular-bootstrap/popover/popover.js | 19 + .../angular-bootstrap/position/position.js | 152 + .../angular-bootstrap/progressbar/bar.html | 1 + .../progressbar/progress.html | 1 + .../progressbar/progressbar.html | 3 + .../progressbar/progressbar.js | 81 + .../angular-bootstrap/rating/rating.html | 5 + .../public/angular-bootstrap/rating/rating.js | 83 + src/ui/public/angular-bootstrap/tabs/tab.html | 3 + src/ui/public/angular-bootstrap/tabs/tabs.js | 279 ++ .../public/angular-bootstrap/tabs/tabset.html | 10 + .../timepicker/timepicker.html | 26 + .../timepicker/timepicker.js | 254 + .../tooltip/tooltip-html-unsafe-popup.html | 4 + .../tooltip/tooltip-popup.html | 4 + .../angular-bootstrap/tooltip/tooltip.js | 360 ++ .../transition/transition.js | 82 + .../typeahead/typeahead-match.html | 1 + .../typeahead/typeahead-popup.html | 5 + .../angular-bootstrap/typeahead/typeahead.js | 398 ++ .../angular-bootstrap/ui-bootstrap-tpls.js | 4114 ----------------- webpackShims/ui-bootstrap.js | 2 +- 51 files changed, 4298 insertions(+), 4116 deletions(-) create mode 100755 src/ui/public/angular-bootstrap/accordion/accordion-group.html create mode 100755 src/ui/public/angular-bootstrap/accordion/accordion.html create mode 100755 src/ui/public/angular-bootstrap/accordion/accordion.js create mode 100755 src/ui/public/angular-bootstrap/alert/alert.html create mode 100755 src/ui/public/angular-bootstrap/alert/alert.js create mode 100755 src/ui/public/angular-bootstrap/bindHtml/bindHtml.js create mode 100755 src/ui/public/angular-bootstrap/buttons/buttons.js create mode 100755 src/ui/public/angular-bootstrap/carousel/carousel.html create mode 100755 src/ui/public/angular-bootstrap/carousel/carousel.js create mode 100755 src/ui/public/angular-bootstrap/carousel/slide.html create mode 100755 src/ui/public/angular-bootstrap/collapse/collapse.js create mode 100755 src/ui/public/angular-bootstrap/dateparser/dateparser.js create mode 100755 src/ui/public/angular-bootstrap/datepicker/datepicker.html create mode 100755 src/ui/public/angular-bootstrap/datepicker/datepicker.js create mode 100755 src/ui/public/angular-bootstrap/datepicker/day.html create mode 100755 src/ui/public/angular-bootstrap/datepicker/month.html create mode 100755 src/ui/public/angular-bootstrap/datepicker/popup.html create mode 100755 src/ui/public/angular-bootstrap/datepicker/year.html create mode 100755 src/ui/public/angular-bootstrap/dropdown/dropdown.js create mode 100644 src/ui/public/angular-bootstrap/index.js create mode 100755 src/ui/public/angular-bootstrap/modal/backdrop.html create mode 100755 src/ui/public/angular-bootstrap/modal/modal.js create mode 100755 src/ui/public/angular-bootstrap/modal/window.html create mode 100755 src/ui/public/angular-bootstrap/pagination/pager.html create mode 100755 src/ui/public/angular-bootstrap/pagination/pagination.html create mode 100755 src/ui/public/angular-bootstrap/pagination/pagination.js create mode 100755 src/ui/public/angular-bootstrap/popover/popover.html create mode 100755 src/ui/public/angular-bootstrap/popover/popover.js create mode 100755 src/ui/public/angular-bootstrap/position/position.js create mode 100755 src/ui/public/angular-bootstrap/progressbar/bar.html create mode 100755 src/ui/public/angular-bootstrap/progressbar/progress.html create mode 100755 src/ui/public/angular-bootstrap/progressbar/progressbar.html create mode 100755 src/ui/public/angular-bootstrap/progressbar/progressbar.js create mode 100755 src/ui/public/angular-bootstrap/rating/rating.html create mode 100755 src/ui/public/angular-bootstrap/rating/rating.js create mode 100755 src/ui/public/angular-bootstrap/tabs/tab.html create mode 100755 src/ui/public/angular-bootstrap/tabs/tabs.js create mode 100755 src/ui/public/angular-bootstrap/tabs/tabset.html create mode 100755 src/ui/public/angular-bootstrap/timepicker/timepicker.html create mode 100755 src/ui/public/angular-bootstrap/timepicker/timepicker.js create mode 100755 src/ui/public/angular-bootstrap/tooltip/tooltip-html-unsafe-popup.html create mode 100755 src/ui/public/angular-bootstrap/tooltip/tooltip-popup.html create mode 100755 src/ui/public/angular-bootstrap/tooltip/tooltip.js create mode 100755 src/ui/public/angular-bootstrap/transition/transition.js create mode 100755 src/ui/public/angular-bootstrap/typeahead/typeahead-match.html create mode 100755 src/ui/public/angular-bootstrap/typeahead/typeahead-popup.html create mode 100755 src/ui/public/angular-bootstrap/typeahead/typeahead.js delete mode 100644 src/ui/public/angular-bootstrap/ui-bootstrap-tpls.js diff --git a/.eslintignore b/.eslintignore index 4690dfefc078d1..0de6d839dc2bac 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,4 +1,4 @@ -src/fixtures + test/fixtures/scenarios optimize test/fixtures/scenarios diff --git a/Gruntfile.js b/Gruntfile.js index 2be13738646bcb..ff09088a819dfb 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -44,6 +44,7 @@ module.exports = function (grunt) { '<%= root %>/tasks/**/*.js', '<%= root %>/test/**/*.js', '<%= src %>/**/*.js', + '!<%= src %>/ui/public/angular-bootstrap/**/*.js', '!<%= src %>/fixtures/**/*.js', '!<%= root %>/test/fixtures/scenarios/**/*.js' ], diff --git a/src/ui/public/angular-bootstrap/accordion/accordion-group.html b/src/ui/public/angular-bootstrap/accordion/accordion-group.html new file mode 100755 index 00000000000000..cd4a46b7eb042e --- /dev/null +++ b/src/ui/public/angular-bootstrap/accordion/accordion-group.html @@ -0,0 +1,10 @@ +
      +
      +

      + {{heading}} +

      +
      +
      +
      +
      +
      diff --git a/src/ui/public/angular-bootstrap/accordion/accordion.html b/src/ui/public/angular-bootstrap/accordion/accordion.html new file mode 100755 index 00000000000000..ba428f3b5e5046 --- /dev/null +++ b/src/ui/public/angular-bootstrap/accordion/accordion.html @@ -0,0 +1 @@ +
      \ No newline at end of file diff --git a/src/ui/public/angular-bootstrap/accordion/accordion.js b/src/ui/public/angular-bootstrap/accordion/accordion.js new file mode 100755 index 00000000000000..542e45c3f09371 --- /dev/null +++ b/src/ui/public/angular-bootstrap/accordion/accordion.js @@ -0,0 +1,130 @@ +angular.module('ui.bootstrap.accordion', ['ui.bootstrap.collapse']) + +.constant('accordionConfig', { + closeOthers: true +}) + +.controller('AccordionController', ['$scope', '$attrs', 'accordionConfig', function ($scope, $attrs, accordionConfig) { + + // This array keeps track of the accordion groups + this.groups = []; + + // Ensure that all the groups in this accordion are closed, unless close-others explicitly says not to + this.closeOthers = function(openGroup) { + var closeOthers = angular.isDefined($attrs.closeOthers) ? $scope.$eval($attrs.closeOthers) : accordionConfig.closeOthers; + if ( closeOthers ) { + angular.forEach(this.groups, function (group) { + if ( group !== openGroup ) { + group.isOpen = false; + } + }); + } + }; + + // This is called from the accordion-group directive to add itself to the accordion + this.addGroup = function(groupScope) { + var that = this; + this.groups.push(groupScope); + + groupScope.$on('$destroy', function (event) { + that.removeGroup(groupScope); + }); + }; + + // This is called from the accordion-group directive when to remove itself + this.removeGroup = function(group) { + var index = this.groups.indexOf(group); + if ( index !== -1 ) { + this.groups.splice(index, 1); + } + }; + +}]) + +// The accordion directive simply sets up the directive controller +// and adds an accordion CSS class to itself element. +.directive('accordion', function () { + return { + restrict:'EA', + controller:'AccordionController', + transclude: true, + replace: false, + templateUrl: 'template/accordion/accordion.html' + }; +}) + +// The accordion-group directive indicates a block of html that will expand and collapse in an accordion +.directive('accordionGroup', function() { + return { + require:'^accordion', // We need this directive to be inside an accordion + restrict:'EA', + transclude:true, // It transcludes the contents of the directive into the template + replace: true, // The element containing the directive will be replaced with the template + templateUrl:'template/accordion/accordion-group.html', + scope: { + heading: '@', // Interpolate the heading attribute onto this scope + isOpen: '=?', + isDisabled: '=?' + }, + controller: function() { + this.setHeading = function(element) { + this.heading = element; + }; + }, + link: function(scope, element, attrs, accordionCtrl) { + accordionCtrl.addGroup(scope); + + scope.$watch('isOpen', function(value) { + if ( value ) { + accordionCtrl.closeOthers(scope); + } + }); + + scope.toggleOpen = function() { + if ( !scope.isDisabled ) { + scope.isOpen = !scope.isOpen; + } + }; + } + }; +}) + +// Use accordion-heading below an accordion-group to provide a heading containing HTML +// +// Heading containing HTML - +// +.directive('accordionHeading', function() { + return { + restrict: 'EA', + transclude: true, // Grab the contents to be used as the heading + template: '', // In effect remove this element! + replace: true, + require: '^accordionGroup', + link: function(scope, element, attr, accordionGroupCtrl, transclude) { + // Pass the heading to the accordion-group controller + // so that it can be transcluded into the right place in the template + // [The second parameter to transclude causes the elements to be cloned so that they work in ng-repeat] + accordionGroupCtrl.setHeading(transclude(scope, function() {})); + } + }; +}) + +// Use in the accordion-group template to indicate where you want the heading to be transcluded +// You must provide the property on the accordion-group controller that will hold the transcluded element +//
      +// +// ... +//
      +.directive('accordionTransclude', function() { + return { + require: '^accordionGroup', + link: function(scope, element, attr, controller) { + scope.$watch(function() { return controller[attr.accordionTransclude]; }, function(heading) { + if ( heading ) { + element.html(''); + element.append(heading); + } + }); + } + }; +}); diff --git a/src/ui/public/angular-bootstrap/alert/alert.html b/src/ui/public/angular-bootstrap/alert/alert.html new file mode 100755 index 00000000000000..6415960996f5bf --- /dev/null +++ b/src/ui/public/angular-bootstrap/alert/alert.html @@ -0,0 +1,7 @@ + diff --git a/src/ui/public/angular-bootstrap/alert/alert.js b/src/ui/public/angular-bootstrap/alert/alert.js new file mode 100755 index 00000000000000..3fa9633abf5bd1 --- /dev/null +++ b/src/ui/public/angular-bootstrap/alert/alert.js @@ -0,0 +1,31 @@ +angular.module('ui.bootstrap.alert', []) + +.controller('AlertController', ['$scope', '$attrs', function ($scope, $attrs) { + $scope.closeable = 'close' in $attrs; + this.close = $scope.close; +}]) + +.directive('alert', function () { + return { + restrict:'EA', + controller:'AlertController', + templateUrl:'template/alert/alert.html', + transclude:true, + replace:true, + scope: { + type: '@', + close: '&' + } + }; +}) + +.directive('dismissOnTimeout', ['$timeout', function($timeout) { + return { + require: 'alert', + link: function(scope, element, attrs, alertCtrl) { + $timeout(function(){ + alertCtrl.close(); + }, parseInt(attrs.dismissOnTimeout, 10)); + } + }; +}]); diff --git a/src/ui/public/angular-bootstrap/bindHtml/bindHtml.js b/src/ui/public/angular-bootstrap/bindHtml/bindHtml.js new file mode 100755 index 00000000000000..cf635bc375a849 --- /dev/null +++ b/src/ui/public/angular-bootstrap/bindHtml/bindHtml.js @@ -0,0 +1,10 @@ +angular.module('ui.bootstrap.bindHtml', []) + + .directive('bindHtmlUnsafe', function () { + return function (scope, element, attr) { + element.addClass('ng-binding').data('$binding', attr.bindHtmlUnsafe); + scope.$watch(attr.bindHtmlUnsafe, function bindHtmlUnsafeWatchAction(value) { + element.html(value || ''); + }); + }; + }); \ No newline at end of file diff --git a/src/ui/public/angular-bootstrap/buttons/buttons.js b/src/ui/public/angular-bootstrap/buttons/buttons.js new file mode 100755 index 00000000000000..27237be38872f3 --- /dev/null +++ b/src/ui/public/angular-bootstrap/buttons/buttons.js @@ -0,0 +1,74 @@ +angular.module('ui.bootstrap.buttons', []) + +.constant('buttonConfig', { + activeClass: 'active', + toggleEvent: 'click' +}) + +.controller('ButtonsController', ['buttonConfig', function(buttonConfig) { + this.activeClass = buttonConfig.activeClass || 'active'; + this.toggleEvent = buttonConfig.toggleEvent || 'click'; +}]) + +.directive('btnRadio', function () { + return { + require: ['btnRadio', 'ngModel'], + controller: 'ButtonsController', + link: function (scope, element, attrs, ctrls) { + var buttonsCtrl = ctrls[0], ngModelCtrl = ctrls[1]; + + //model -> UI + ngModelCtrl.$render = function () { + element.toggleClass(buttonsCtrl.activeClass, angular.equals(ngModelCtrl.$modelValue, scope.$eval(attrs.btnRadio))); + }; + + //ui->model + element.bind(buttonsCtrl.toggleEvent, function () { + var isActive = element.hasClass(buttonsCtrl.activeClass); + + if (!isActive || angular.isDefined(attrs.uncheckable)) { + scope.$apply(function () { + ngModelCtrl.$setViewValue(isActive ? null : scope.$eval(attrs.btnRadio)); + ngModelCtrl.$render(); + }); + } + }); + } + }; +}) + +.directive('btnCheckbox', function () { + return { + require: ['btnCheckbox', 'ngModel'], + controller: 'ButtonsController', + link: function (scope, element, attrs, ctrls) { + var buttonsCtrl = ctrls[0], ngModelCtrl = ctrls[1]; + + function getTrueValue() { + return getCheckboxValue(attrs.btnCheckboxTrue, true); + } + + function getFalseValue() { + return getCheckboxValue(attrs.btnCheckboxFalse, false); + } + + function getCheckboxValue(attributeValue, defaultValue) { + var val = scope.$eval(attributeValue); + return angular.isDefined(val) ? val : defaultValue; + } + + //model -> UI + ngModelCtrl.$render = function () { + element.toggleClass(buttonsCtrl.activeClass, angular.equals(ngModelCtrl.$modelValue, getTrueValue())); + }; + + //ui->model + element.bind(buttonsCtrl.toggleEvent, function () { + scope.$apply(function () { + ngModelCtrl.$setViewValue(element.hasClass(buttonsCtrl.activeClass) ? getFalseValue() : getTrueValue()); + ngModelCtrl.$render(); + }); + }); + } + }; +}); diff --git a/src/ui/public/angular-bootstrap/carousel/carousel.html b/src/ui/public/angular-bootstrap/carousel/carousel.html new file mode 100755 index 00000000000000..9769b383a6f4d2 --- /dev/null +++ b/src/ui/public/angular-bootstrap/carousel/carousel.html @@ -0,0 +1,8 @@ + diff --git a/src/ui/public/angular-bootstrap/carousel/carousel.js b/src/ui/public/angular-bootstrap/carousel/carousel.js new file mode 100755 index 00000000000000..9d91004da70fc8 --- /dev/null +++ b/src/ui/public/angular-bootstrap/carousel/carousel.js @@ -0,0 +1,293 @@ +/** +* @ngdoc overview +* @name ui.bootstrap.carousel +* +* @description +* AngularJS version of an image carousel. +* +*/ +angular.module('ui.bootstrap.carousel', ['ui.bootstrap.transition']) +.controller('CarouselController', ['$scope', '$timeout', '$interval', '$transition', function ($scope, $timeout, $interval, $transition) { + var self = this, + slides = self.slides = $scope.slides = [], + currentIndex = -1, + currentInterval, isPlaying; + self.currentSlide = null; + + var destroyed = false; + /* direction: "prev" or "next" */ + self.select = $scope.select = function(nextSlide, direction) { + var nextIndex = slides.indexOf(nextSlide); + //Decide direction if it's not given + if (direction === undefined) { + direction = nextIndex > currentIndex ? 'next' : 'prev'; + } + if (nextSlide && nextSlide !== self.currentSlide) { + if ($scope.$currentTransition) { + $scope.$currentTransition.cancel(); + //Timeout so ng-class in template has time to fix classes for finished slide + $timeout(goNext); + } else { + goNext(); + } + } + function goNext() { + // Scope has been destroyed, stop here. + if (destroyed) { return; } + //If we have a slide to transition from and we have a transition type and we're allowed, go + if (self.currentSlide && angular.isString(direction) && !$scope.noTransition && nextSlide.$element) { + //We shouldn't do class manip in here, but it's the same weird thing bootstrap does. need to fix sometime + nextSlide.$element.addClass(direction); + var reflow = nextSlide.$element[0].offsetWidth; //force reflow + + //Set all other slides to stop doing their stuff for the new transition + angular.forEach(slides, function(slide) { + angular.extend(slide, {direction: '', entering: false, leaving: false, active: false}); + }); + angular.extend(nextSlide, {direction: direction, active: true, entering: true}); + angular.extend(self.currentSlide||{}, {direction: direction, leaving: true}); + + $scope.$currentTransition = $transition(nextSlide.$element, {}); + //We have to create new pointers inside a closure since next & current will change + (function(next,current) { + $scope.$currentTransition.then( + function(){ transitionDone(next, current); }, + function(){ transitionDone(next, current); } + ); + }(nextSlide, self.currentSlide)); + } else { + transitionDone(nextSlide, self.currentSlide); + } + self.currentSlide = nextSlide; + currentIndex = nextIndex; + //every time you change slides, reset the timer + restartTimer(); + } + function transitionDone(next, current) { + angular.extend(next, {direction: '', active: true, leaving: false, entering: false}); + angular.extend(current||{}, {direction: '', active: false, leaving: false, entering: false}); + $scope.$currentTransition = null; + } + }; + $scope.$on('$destroy', function () { + destroyed = true; + }); + + /* Allow outside people to call indexOf on slides array */ + self.indexOfSlide = function(slide) { + return slides.indexOf(slide); + }; + + $scope.next = function() { + var newIndex = (currentIndex + 1) % slides.length; + + //Prevent this user-triggered transition from occurring if there is already one in progress + if (!$scope.$currentTransition) { + return self.select(slides[newIndex], 'next'); + } + }; + + $scope.prev = function() { + var newIndex = currentIndex - 1 < 0 ? slides.length - 1 : currentIndex - 1; + + //Prevent this user-triggered transition from occurring if there is already one in progress + if (!$scope.$currentTransition) { + return self.select(slides[newIndex], 'prev'); + } + }; + + $scope.isActive = function(slide) { + return self.currentSlide === slide; + }; + + $scope.$watch('interval', restartTimer); + $scope.$on('$destroy', resetTimer); + + function restartTimer() { + resetTimer(); + var interval = +$scope.interval; + if (!isNaN(interval) && interval > 0) { + currentInterval = $interval(timerFn, interval); + } + } + + function resetTimer() { + if (currentInterval) { + $interval.cancel(currentInterval); + currentInterval = null; + } + } + + function timerFn() { + var interval = +$scope.interval; + if (isPlaying && !isNaN(interval) && interval > 0) { + $scope.next(); + } else { + $scope.pause(); + } + } + + $scope.play = function() { + if (!isPlaying) { + isPlaying = true; + restartTimer(); + } + }; + $scope.pause = function() { + if (!$scope.noPause) { + isPlaying = false; + resetTimer(); + } + }; + + self.addSlide = function(slide, element) { + slide.$element = element; + slides.push(slide); + //if this is the first slide or the slide is set to active, select it + if(slides.length === 1 || slide.active) { + self.select(slides[slides.length-1]); + if (slides.length == 1) { + $scope.play(); + } + } else { + slide.active = false; + } + }; + + self.removeSlide = function(slide) { + //get the index of the slide inside the carousel + var index = slides.indexOf(slide); + slides.splice(index, 1); + if (slides.length > 0 && slide.active) { + if (index >= slides.length) { + self.select(slides[index-1]); + } else { + self.select(slides[index]); + } + } else if (currentIndex > index) { + currentIndex--; + } + }; + +}]) + +/** + * @ngdoc directive + * @name ui.bootstrap.carousel.directive:carousel + * @restrict EA + * + * @description + * Carousel is the outer container for a set of image 'slides' to showcase. + * + * @param {number=} interval The time, in milliseconds, that it will take the carousel to go to the next slide. + * @param {boolean=} noTransition Whether to disable transitions on the carousel. + * @param {boolean=} noPause Whether to disable pausing on the carousel (by default, the carousel interval pauses on hover). + * + * @example + + + + + + + + + + + + + + + .carousel-indicators { + top: auto; + bottom: 15px; + } + + + */ +.directive('carousel', [function() { + return { + restrict: 'EA', + transclude: true, + replace: true, + controller: 'CarouselController', + require: 'carousel', + templateUrl: 'template/carousel/carousel.html', + scope: { + interval: '=', + noTransition: '=', + noPause: '=' + } + }; +}]) + +/** + * @ngdoc directive + * @name ui.bootstrap.carousel.directive:slide + * @restrict EA + * + * @description + * Creates a slide inside a {@link ui.bootstrap.carousel.directive:carousel carousel}. Must be placed as a child of a carousel element. + * + * @param {boolean=} active Model binding, whether or not this slide is currently active. + * + * @example + + +
      + + + + + + + Interval, in milliseconds: +
      Enter a negative number to stop the interval. +
      +
      + +function CarouselDemoCtrl($scope) { + $scope.myInterval = 5000; +} + + + .carousel-indicators { + top: auto; + bottom: 15px; + } + +
      +*/ + +.directive('slide', function() { + return { + require: '^carousel', + restrict: 'EA', + transclude: true, + replace: true, + templateUrl: 'template/carousel/slide.html', + scope: { + active: '=?' + }, + link: function (scope, element, attrs, carouselCtrl) { + carouselCtrl.addSlide(scope, element); + //when the scope is destroyed then remove the slide from the current slides array + scope.$on('$destroy', function() { + carouselCtrl.removeSlide(scope); + }); + + scope.$watch('active', function(active) { + if (active) { + carouselCtrl.select(scope); + } + }); + } + }; +}); diff --git a/src/ui/public/angular-bootstrap/carousel/slide.html b/src/ui/public/angular-bootstrap/carousel/slide.html new file mode 100755 index 00000000000000..451e4fba3e04c6 --- /dev/null +++ b/src/ui/public/angular-bootstrap/carousel/slide.html @@ -0,0 +1,7 @@ +
      diff --git a/src/ui/public/angular-bootstrap/collapse/collapse.js b/src/ui/public/angular-bootstrap/collapse/collapse.js new file mode 100755 index 00000000000000..5d6787a7fd6ce3 --- /dev/null +++ b/src/ui/public/angular-bootstrap/collapse/collapse.js @@ -0,0 +1,75 @@ +angular.module('ui.bootstrap.collapse', ['ui.bootstrap.transition']) + + .directive('collapse', ['$transition', function ($transition) { + + return { + link: function (scope, element, attrs) { + + var initialAnimSkip = true; + var currentTransition; + + function doTransition(change) { + var newTransition = $transition(element, change); + if (currentTransition) { + currentTransition.cancel(); + } + currentTransition = newTransition; + newTransition.then(newTransitionDone, newTransitionDone); + return newTransition; + + function newTransitionDone() { + // Make sure it's this transition, otherwise, leave it alone. + if (currentTransition === newTransition) { + currentTransition = undefined; + } + } + } + + function expand() { + if (initialAnimSkip) { + initialAnimSkip = false; + expandDone(); + } else { + element.removeClass('collapse').addClass('collapsing'); + doTransition({ height: element[0].scrollHeight + 'px' }).then(expandDone); + } + } + + function expandDone() { + element.removeClass('collapsing'); + element.addClass('collapse in'); + element.css({height: 'auto'}); + } + + function collapse() { + if (initialAnimSkip) { + initialAnimSkip = false; + collapseDone(); + element.css({height: 0}); + } else { + // CSS transitions don't work with height: auto, so we have to manually change the height to a specific value + element.css({ height: element[0].scrollHeight + 'px' }); + //trigger reflow so a browser realizes that height was updated from auto to a specific value + var x = element[0].offsetWidth; + + element.removeClass('collapse in').addClass('collapsing'); + + doTransition({ height: 0 }).then(collapseDone); + } + } + + function collapseDone() { + element.removeClass('collapsing'); + element.addClass('collapse'); + } + + scope.$watch(attrs.collapse, function (shouldCollapse) { + if (shouldCollapse) { + collapse(); + } else { + expand(); + } + }); + } + }; + }]); diff --git a/src/ui/public/angular-bootstrap/dateparser/dateparser.js b/src/ui/public/angular-bootstrap/dateparser/dateparser.js new file mode 100755 index 00000000000000..9d98bff2b7d978 --- /dev/null +++ b/src/ui/public/angular-bootstrap/dateparser/dateparser.js @@ -0,0 +1,126 @@ +angular.module('ui.bootstrap.dateparser', []) + +.service('dateParser', ['$locale', 'orderByFilter', function($locale, orderByFilter) { + + this.parsers = {}; + + var formatCodeToRegex = { + 'yyyy': { + regex: '\\d{4}', + apply: function(value) { this.year = +value; } + }, + 'yy': { + regex: '\\d{2}', + apply: function(value) { this.year = +value + 2000; } + }, + 'y': { + regex: '\\d{1,4}', + apply: function(value) { this.year = +value; } + }, + 'MMMM': { + regex: $locale.DATETIME_FORMATS.MONTH.join('|'), + apply: function(value) { this.month = $locale.DATETIME_FORMATS.MONTH.indexOf(value); } + }, + 'MMM': { + regex: $locale.DATETIME_FORMATS.SHORTMONTH.join('|'), + apply: function(value) { this.month = $locale.DATETIME_FORMATS.SHORTMONTH.indexOf(value); } + }, + 'MM': { + regex: '0[1-9]|1[0-2]', + apply: function(value) { this.month = value - 1; } + }, + 'M': { + regex: '[1-9]|1[0-2]', + apply: function(value) { this.month = value - 1; } + }, + 'dd': { + regex: '[0-2][0-9]{1}|3[0-1]{1}', + apply: function(value) { this.date = +value; } + }, + 'd': { + regex: '[1-2]?[0-9]{1}|3[0-1]{1}', + apply: function(value) { this.date = +value; } + }, + 'EEEE': { + regex: $locale.DATETIME_FORMATS.DAY.join('|') + }, + 'EEE': { + regex: $locale.DATETIME_FORMATS.SHORTDAY.join('|') + } + }; + + function createParser(format) { + var map = [], regex = format.split(''); + + angular.forEach(formatCodeToRegex, function(data, code) { + var index = format.indexOf(code); + + if (index > -1) { + format = format.split(''); + + regex[index] = '(' + data.regex + ')'; + format[index] = '$'; // Custom symbol to define consumed part of format + for (var i = index + 1, n = index + code.length; i < n; i++) { + regex[i] = ''; + format[i] = '$'; + } + format = format.join(''); + + map.push({ index: index, apply: data.apply }); + } + }); + + return { + regex: new RegExp('^' + regex.join('') + '$'), + map: orderByFilter(map, 'index') + }; + } + + this.parse = function(input, format) { + if ( !angular.isString(input) || !format ) { + return input; + } + + format = $locale.DATETIME_FORMATS[format] || format; + + if ( !this.parsers[format] ) { + this.parsers[format] = createParser(format); + } + + var parser = this.parsers[format], + regex = parser.regex, + map = parser.map, + results = input.match(regex); + + if ( results && results.length ) { + var fields = { year: 1900, month: 0, date: 1, hours: 0 }, dt; + + for( var i = 1, n = results.length; i < n; i++ ) { + var mapper = map[i-1]; + if ( mapper.apply ) { + mapper.apply.call(fields, results[i]); + } + } + + if ( isValid(fields.year, fields.month, fields.date) ) { + dt = new Date( fields.year, fields.month, fields.date, fields.hours); + } + + return dt; + } + }; + + // Check if date is valid for specific month (and year for February). + // Month: 0 = Jan, 1 = Feb, etc + function isValid(year, month, date) { + if ( month === 1 && date > 28) { + return date === 29 && ((year % 4 === 0 && year % 100 !== 0) || year % 400 === 0); + } + + if ( month === 3 || month === 5 || month === 8 || month === 10) { + return date < 31; + } + + return true; + } +}]); diff --git a/src/ui/public/angular-bootstrap/datepicker/datepicker.html b/src/ui/public/angular-bootstrap/datepicker/datepicker.html new file mode 100755 index 00000000000000..1ecb3c50b47063 --- /dev/null +++ b/src/ui/public/angular-bootstrap/datepicker/datepicker.html @@ -0,0 +1,5 @@ +
      + + + +
      \ No newline at end of file diff --git a/src/ui/public/angular-bootstrap/datepicker/datepicker.js b/src/ui/public/angular-bootstrap/datepicker/datepicker.js new file mode 100755 index 00000000000000..05784947d0505b --- /dev/null +++ b/src/ui/public/angular-bootstrap/datepicker/datepicker.js @@ -0,0 +1,639 @@ +angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.dateparser', 'ui.bootstrap.position']) + +.constant('datepickerConfig', { + formatDay: 'dd', + formatMonth: 'MMMM', + formatYear: 'yyyy', + formatDayHeader: 'EEE', + formatDayTitle: 'MMMM yyyy', + formatMonthTitle: 'yyyy', + datepickerMode: 'day', + minMode: 'day', + maxMode: 'year', + showWeeks: true, + startingDay: 0, + yearRange: 20, + minDate: null, + maxDate: null +}) + +.controller('DatepickerController', ['$scope', '$attrs', '$parse', '$interpolate', '$timeout', '$log', 'dateFilter', 'datepickerConfig', function($scope, $attrs, $parse, $interpolate, $timeout, $log, dateFilter, datepickerConfig) { + var self = this, + ngModelCtrl = { $setViewValue: angular.noop }; // nullModelCtrl; + + // Modes chain + this.modes = ['day', 'month', 'year']; + + // Configuration attributes + angular.forEach(['formatDay', 'formatMonth', 'formatYear', 'formatDayHeader', 'formatDayTitle', 'formatMonthTitle', + 'minMode', 'maxMode', 'showWeeks', 'startingDay', 'yearRange'], function( key, index ) { + self[key] = angular.isDefined($attrs[key]) ? (index < 8 ? $interpolate($attrs[key])($scope.$parent) : $scope.$parent.$eval($attrs[key])) : datepickerConfig[key]; + }); + + // Watchable date attributes + angular.forEach(['minDate', 'maxDate'], function( key ) { + if ( $attrs[key] ) { + $scope.$parent.$watch($parse($attrs[key]), function(value) { + self[key] = value ? new Date(value) : null; + self.refreshView(); + }); + } else { + self[key] = datepickerConfig[key] ? new Date(datepickerConfig[key]) : null; + } + }); + + $scope.datepickerMode = $scope.datepickerMode || datepickerConfig.datepickerMode; + $scope.uniqueId = 'datepicker-' + $scope.$id + '-' + Math.floor(Math.random() * 10000); + this.activeDate = angular.isDefined($attrs.initDate) ? $scope.$parent.$eval($attrs.initDate) : new Date(); + + $scope.isActive = function(dateObject) { + if (self.compare(dateObject.date, self.activeDate) === 0) { + $scope.activeDateId = dateObject.uid; + return true; + } + return false; + }; + + this.init = function( ngModelCtrl_ ) { + ngModelCtrl = ngModelCtrl_; + + ngModelCtrl.$render = function() { + self.render(); + }; + }; + + this.render = function() { + if ( ngModelCtrl.$modelValue ) { + var date = new Date( ngModelCtrl.$modelValue ), + isValid = !isNaN(date); + + if ( isValid ) { + this.activeDate = date; + } else { + $log.error('Datepicker directive: "ng-model" value must be a Date object, a number of milliseconds since 01.01.1970 or a string representing an RFC2822 or ISO 8601 date.'); + } + ngModelCtrl.$setValidity('date', isValid); + } + this.refreshView(); + }; + + this.refreshView = function() { + if ( this.element ) { + this._refreshView(); + + var date = ngModelCtrl.$modelValue ? new Date(ngModelCtrl.$modelValue) : null; + ngModelCtrl.$setValidity('date-disabled', !date || (this.element && !this.isDisabled(date))); + } + }; + + this.createDateObject = function(date, format) { + var model = ngModelCtrl.$modelValue ? new Date(ngModelCtrl.$modelValue) : null; + return { + date: date, + label: dateFilter(date, format), + selected: model && this.compare(date, model) === 0, + disabled: this.isDisabled(date), + current: this.compare(date, new Date()) === 0 + }; + }; + + this.isDisabled = function( date ) { + return ((this.minDate && this.compare(date, this.minDate) < 0) || (this.maxDate && this.compare(date, this.maxDate) > 0) || ($attrs.dateDisabled && $scope.dateDisabled({date: date, mode: $scope.datepickerMode}))); + }; + + // Split array into smaller arrays + this.split = function(arr, size) { + var arrays = []; + while (arr.length > 0) { + arrays.push(arr.splice(0, size)); + } + return arrays; + }; + + $scope.select = function( date ) { + if ( $scope.datepickerMode === self.minMode ) { + var dt = ngModelCtrl.$modelValue ? new Date( ngModelCtrl.$modelValue ) : new Date(0, 0, 0, 0, 0, 0, 0); + dt.setFullYear( date.getFullYear(), date.getMonth(), date.getDate() ); + ngModelCtrl.$setViewValue( dt ); + ngModelCtrl.$render(); + } else { + self.activeDate = date; + $scope.datepickerMode = self.modes[ self.modes.indexOf( $scope.datepickerMode ) - 1 ]; + } + }; + + $scope.move = function( direction ) { + var year = self.activeDate.getFullYear() + direction * (self.step.years || 0), + month = self.activeDate.getMonth() + direction * (self.step.months || 0); + self.activeDate.setFullYear(year, month, 1); + self.refreshView(); + }; + + $scope.toggleMode = function( direction ) { + direction = direction || 1; + + if (($scope.datepickerMode === self.maxMode && direction === 1) || ($scope.datepickerMode === self.minMode && direction === -1)) { + return; + } + + $scope.datepickerMode = self.modes[ self.modes.indexOf( $scope.datepickerMode ) + direction ]; + }; + + // Key event mapper + $scope.keys = { 13:'enter', 32:'space', 33:'pageup', 34:'pagedown', 35:'end', 36:'home', 37:'left', 38:'up', 39:'right', 40:'down' }; + + var focusElement = function() { + $timeout(function() { + self.element[0].focus(); + }, 0 , false); + }; + + // Listen for focus requests from popup directive + $scope.$on('datepicker.focus', focusElement); + + $scope.keydown = function( evt ) { + var key = $scope.keys[evt.which]; + + if ( !key || evt.shiftKey || evt.altKey ) { + return; + } + + evt.preventDefault(); + evt.stopPropagation(); + + if (key === 'enter' || key === 'space') { + if ( self.isDisabled(self.activeDate)) { + return; // do nothing + } + $scope.select(self.activeDate); + focusElement(); + } else if (evt.ctrlKey && (key === 'up' || key === 'down')) { + $scope.toggleMode(key === 'up' ? 1 : -1); + focusElement(); + } else { + self.handleKeyDown(key, evt); + self.refreshView(); + } + }; +}]) + +.directive( 'datepicker', function () { + return { + restrict: 'EA', + replace: true, + templateUrl: 'template/datepicker/datepicker.html', + scope: { + datepickerMode: '=?', + dateDisabled: '&' + }, + require: ['datepicker', '?^ngModel'], + controller: 'DatepickerController', + link: function(scope, element, attrs, ctrls) { + var datepickerCtrl = ctrls[0], ngModelCtrl = ctrls[1]; + + if ( ngModelCtrl ) { + datepickerCtrl.init( ngModelCtrl ); + } + } + }; +}) + +.directive('daypicker', ['dateFilter', function (dateFilter) { + return { + restrict: 'EA', + replace: true, + templateUrl: 'template/datepicker/day.html', + require: '^datepicker', + link: function(scope, element, attrs, ctrl) { + scope.showWeeks = ctrl.showWeeks; + + ctrl.step = { months: 1 }; + ctrl.element = element; + + var DAYS_IN_MONTH = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]; + function getDaysInMonth( year, month ) { + return ((month === 1) && (year % 4 === 0) && ((year % 100 !== 0) || (year % 400 === 0))) ? 29 : DAYS_IN_MONTH[month]; + } + + function getDates(startDate, n) { + var dates = new Array(n), current = new Date(startDate), i = 0; + current.setHours(12); // Prevent repeated dates because of timezone bug + while ( i < n ) { + dates[i++] = new Date(current); + current.setDate( current.getDate() + 1 ); + } + return dates; + } + + ctrl._refreshView = function() { + var year = ctrl.activeDate.getFullYear(), + month = ctrl.activeDate.getMonth(), + firstDayOfMonth = new Date(year, month, 1), + difference = ctrl.startingDay - firstDayOfMonth.getDay(), + numDisplayedFromPreviousMonth = (difference > 0) ? 7 - difference : - difference, + firstDate = new Date(firstDayOfMonth); + + if ( numDisplayedFromPreviousMonth > 0 ) { + firstDate.setDate( - numDisplayedFromPreviousMonth + 1 ); + } + + // 42 is the number of days on a six-month calendar + var days = getDates(firstDate, 42); + for (var i = 0; i < 42; i ++) { + days[i] = angular.extend(ctrl.createDateObject(days[i], ctrl.formatDay), { + secondary: days[i].getMonth() !== month, + uid: scope.uniqueId + '-' + i + }); + } + + scope.labels = new Array(7); + for (var j = 0; j < 7; j++) { + scope.labels[j] = { + abbr: dateFilter(days[j].date, ctrl.formatDayHeader), + full: dateFilter(days[j].date, 'EEEE') + }; + } + + scope.title = dateFilter(ctrl.activeDate, ctrl.formatDayTitle); + scope.rows = ctrl.split(days, 7); + + if ( scope.showWeeks ) { + scope.weekNumbers = []; + var weekNumber = getISO8601WeekNumber( scope.rows[0][0].date ), + numWeeks = scope.rows.length; + while( scope.weekNumbers.push(weekNumber++) < numWeeks ) {} + } + }; + + ctrl.compare = function(date1, date2) { + return (new Date( date1.getFullYear(), date1.getMonth(), date1.getDate() ) - new Date( date2.getFullYear(), date2.getMonth(), date2.getDate() ) ); + }; + + function getISO8601WeekNumber(date) { + var checkDate = new Date(date); + checkDate.setDate(checkDate.getDate() + 4 - (checkDate.getDay() || 7)); // Thursday + var time = checkDate.getTime(); + checkDate.setMonth(0); // Compare with Jan 1 + checkDate.setDate(1); + return Math.floor(Math.round((time - checkDate) / 86400000) / 7) + 1; + } + + ctrl.handleKeyDown = function( key, evt ) { + var date = ctrl.activeDate.getDate(); + + if (key === 'left') { + date = date - 1; // up + } else if (key === 'up') { + date = date - 7; // down + } else if (key === 'right') { + date = date + 1; // down + } else if (key === 'down') { + date = date + 7; + } else if (key === 'pageup' || key === 'pagedown') { + var month = ctrl.activeDate.getMonth() + (key === 'pageup' ? - 1 : 1); + ctrl.activeDate.setMonth(month, 1); + date = Math.min(getDaysInMonth(ctrl.activeDate.getFullYear(), ctrl.activeDate.getMonth()), date); + } else if (key === 'home') { + date = 1; + } else if (key === 'end') { + date = getDaysInMonth(ctrl.activeDate.getFullYear(), ctrl.activeDate.getMonth()); + } + ctrl.activeDate.setDate(date); + }; + + ctrl.refreshView(); + } + }; +}]) + +.directive('monthpicker', ['dateFilter', function (dateFilter) { + return { + restrict: 'EA', + replace: true, + templateUrl: 'template/datepicker/month.html', + require: '^datepicker', + link: function(scope, element, attrs, ctrl) { + ctrl.step = { years: 1 }; + ctrl.element = element; + + ctrl._refreshView = function() { + var months = new Array(12), + year = ctrl.activeDate.getFullYear(); + + for ( var i = 0; i < 12; i++ ) { + months[i] = angular.extend(ctrl.createDateObject(new Date(year, i, 1), ctrl.formatMonth), { + uid: scope.uniqueId + '-' + i + }); + } + + scope.title = dateFilter(ctrl.activeDate, ctrl.formatMonthTitle); + scope.rows = ctrl.split(months, 3); + }; + + ctrl.compare = function(date1, date2) { + return new Date( date1.getFullYear(), date1.getMonth() ) - new Date( date2.getFullYear(), date2.getMonth() ); + }; + + ctrl.handleKeyDown = function( key, evt ) { + var date = ctrl.activeDate.getMonth(); + + if (key === 'left') { + date = date - 1; // up + } else if (key === 'up') { + date = date - 3; // down + } else if (key === 'right') { + date = date + 1; // down + } else if (key === 'down') { + date = date + 3; + } else if (key === 'pageup' || key === 'pagedown') { + var year = ctrl.activeDate.getFullYear() + (key === 'pageup' ? - 1 : 1); + ctrl.activeDate.setFullYear(year); + } else if (key === 'home') { + date = 0; + } else if (key === 'end') { + date = 11; + } + ctrl.activeDate.setMonth(date); + }; + + ctrl.refreshView(); + } + }; +}]) + +.directive('yearpicker', ['dateFilter', function (dateFilter) { + return { + restrict: 'EA', + replace: true, + templateUrl: 'template/datepicker/year.html', + require: '^datepicker', + link: function(scope, element, attrs, ctrl) { + var range = ctrl.yearRange; + + ctrl.step = { years: range }; + ctrl.element = element; + + function getStartingYear( year ) { + return parseInt((year - 1) / range, 10) * range + 1; + } + + ctrl._refreshView = function() { + var years = new Array(range); + + for ( var i = 0, start = getStartingYear(ctrl.activeDate.getFullYear()); i < range; i++ ) { + years[i] = angular.extend(ctrl.createDateObject(new Date(start + i, 0, 1), ctrl.formatYear), { + uid: scope.uniqueId + '-' + i + }); + } + + scope.title = [years[0].label, years[range - 1].label].join(' - '); + scope.rows = ctrl.split(years, 5); + }; + + ctrl.compare = function(date1, date2) { + return date1.getFullYear() - date2.getFullYear(); + }; + + ctrl.handleKeyDown = function( key, evt ) { + var date = ctrl.activeDate.getFullYear(); + + if (key === 'left') { + date = date - 1; // up + } else if (key === 'up') { + date = date - 5; // down + } else if (key === 'right') { + date = date + 1; // down + } else if (key === 'down') { + date = date + 5; + } else if (key === 'pageup' || key === 'pagedown') { + date += (key === 'pageup' ? - 1 : 1) * ctrl.step.years; + } else if (key === 'home') { + date = getStartingYear( ctrl.activeDate.getFullYear() ); + } else if (key === 'end') { + date = getStartingYear( ctrl.activeDate.getFullYear() ) + range - 1; + } + ctrl.activeDate.setFullYear(date); + }; + + ctrl.refreshView(); + } + }; +}]) + +.constant('datepickerPopupConfig', { + datepickerPopup: 'yyyy-MM-dd', + currentText: 'Today', + clearText: 'Clear', + closeText: 'Done', + closeOnDateSelection: true, + appendToBody: false, + showButtonBar: true +}) + +.directive('datepickerPopup', ['$compile', '$parse', '$document', '$position', 'dateFilter', 'dateParser', 'datepickerPopupConfig', +function ($compile, $parse, $document, $position, dateFilter, dateParser, datepickerPopupConfig) { + return { + restrict: 'EA', + require: 'ngModel', + scope: { + isOpen: '=?', + currentText: '@', + clearText: '@', + closeText: '@', + dateDisabled: '&' + }, + link: function(scope, element, attrs, ngModel) { + var dateFormat, + closeOnDateSelection = angular.isDefined(attrs.closeOnDateSelection) ? scope.$parent.$eval(attrs.closeOnDateSelection) : datepickerPopupConfig.closeOnDateSelection, + appendToBody = angular.isDefined(attrs.datepickerAppendToBody) ? scope.$parent.$eval(attrs.datepickerAppendToBody) : datepickerPopupConfig.appendToBody; + + scope.showButtonBar = angular.isDefined(attrs.showButtonBar) ? scope.$parent.$eval(attrs.showButtonBar) : datepickerPopupConfig.showButtonBar; + + scope.getText = function( key ) { + return scope[key + 'Text'] || datepickerPopupConfig[key + 'Text']; + }; + + attrs.$observe('datepickerPopup', function(value) { + dateFormat = value || datepickerPopupConfig.datepickerPopup; + ngModel.$render(); + }); + + // popup element used to display calendar + var popupEl = angular.element('
      '); + popupEl.attr({ + 'ng-model': 'date', + 'ng-change': 'dateSelection()' + }); + + function cameltoDash( string ){ + return string.replace(/([A-Z])/g, function($1) { return '-' + $1.toLowerCase(); }); + } + + // datepicker element + var datepickerEl = angular.element(popupEl.children()[0]); + if ( attrs.datepickerOptions ) { + angular.forEach(scope.$parent.$eval(attrs.datepickerOptions), function( value, option ) { + datepickerEl.attr( cameltoDash(option), value ); + }); + } + + scope.watchData = {}; + angular.forEach(['minDate', 'maxDate', 'datepickerMode'], function( key ) { + if ( attrs[key] ) { + var getAttribute = $parse(attrs[key]); + scope.$parent.$watch(getAttribute, function(value){ + scope.watchData[key] = value; + }); + datepickerEl.attr(cameltoDash(key), 'watchData.' + key); + + // Propagate changes from datepicker to outside + if ( key === 'datepickerMode' ) { + var setAttribute = getAttribute.assign; + scope.$watch('watchData.' + key, function(value, oldvalue) { + if ( value !== oldvalue ) { + setAttribute(scope.$parent, value); + } + }); + } + } + }); + if (attrs.dateDisabled) { + datepickerEl.attr('date-disabled', 'dateDisabled({ date: date, mode: mode })'); + } + + function parseDate(viewValue) { + if (!viewValue) { + ngModel.$setValidity('date', true); + return null; + } else if (angular.isDate(viewValue) && !isNaN(viewValue)) { + ngModel.$setValidity('date', true); + return viewValue; + } else if (angular.isString(viewValue)) { + var date = dateParser.parse(viewValue, dateFormat) || new Date(viewValue); + if (isNaN(date)) { + ngModel.$setValidity('date', false); + return undefined; + } else { + ngModel.$setValidity('date', true); + return date; + } + } else { + ngModel.$setValidity('date', false); + return undefined; + } + } + ngModel.$parsers.unshift(parseDate); + + // Inner change + scope.dateSelection = function(dt) { + if (angular.isDefined(dt)) { + scope.date = dt; + } + ngModel.$setViewValue(scope.date); + ngModel.$render(); + + if ( closeOnDateSelection ) { + scope.isOpen = false; + element[0].focus(); + } + }; + + element.bind('input change keyup', function() { + scope.$apply(function() { + scope.date = ngModel.$modelValue; + }); + }); + + // Outter change + ngModel.$render = function() { + var date = ngModel.$viewValue ? dateFilter(ngModel.$viewValue, dateFormat) : ''; + element.val(date); + scope.date = parseDate( ngModel.$modelValue ); + }; + + var documentClickBind = function(event) { + if (scope.isOpen && event.target !== element[0]) { + scope.$apply(function() { + scope.isOpen = false; + }); + } + }; + + var keydown = function(evt, noApply) { + scope.keydown(evt); + }; + element.bind('keydown', keydown); + + scope.keydown = function(evt) { + if (evt.which === 27) { + evt.preventDefault(); + evt.stopPropagation(); + scope.close(); + } else if (evt.which === 40 && !scope.isOpen) { + scope.isOpen = true; + } + }; + + scope.$watch('isOpen', function(value) { + if (value) { + scope.$broadcast('datepicker.focus'); + scope.position = appendToBody ? $position.offset(element) : $position.position(element); + scope.position.top = scope.position.top + element.prop('offsetHeight'); + + $document.bind('click', documentClickBind); + } else { + $document.unbind('click', documentClickBind); + } + }); + + scope.select = function( date ) { + if (date === 'today') { + var today = new Date(); + if (angular.isDate(ngModel.$modelValue)) { + date = new Date(ngModel.$modelValue); + date.setFullYear(today.getFullYear(), today.getMonth(), today.getDate()); + } else { + date = new Date(today.setHours(0, 0, 0, 0)); + } + } + scope.dateSelection( date ); + }; + + scope.close = function() { + scope.isOpen = false; + element[0].focus(); + }; + + var $popup = $compile(popupEl)(scope); + // Prevent jQuery cache memory leak (template is now redundant after linking) + popupEl.remove(); + + if ( appendToBody ) { + $document.find('body').append($popup); + } else { + element.after($popup); + } + + scope.$on('$destroy', function() { + $popup.remove(); + element.unbind('keydown', keydown); + $document.unbind('click', documentClickBind); + }); + } + }; +}]) + +.directive('datepickerPopupWrap', function() { + return { + restrict:'EA', + replace: true, + transclude: true, + templateUrl: 'template/datepicker/popup.html', + link:function (scope, element, attrs) { + element.bind('click', function(event) { + event.preventDefault(); + event.stopPropagation(); + }); + } + }; +}); diff --git a/src/ui/public/angular-bootstrap/datepicker/day.html b/src/ui/public/angular-bootstrap/datepicker/day.html new file mode 100755 index 00000000000000..ca212a391a9c4d --- /dev/null +++ b/src/ui/public/angular-bootstrap/datepicker/day.html @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + +
      {{label.abbr}}
      {{ weekNumbers[$index] }} + +
      diff --git a/src/ui/public/angular-bootstrap/datepicker/month.html b/src/ui/public/angular-bootstrap/datepicker/month.html new file mode 100755 index 00000000000000..539219004b61c2 --- /dev/null +++ b/src/ui/public/angular-bootstrap/datepicker/month.html @@ -0,0 +1,16 @@ + + + + + + + + + + + + + +
      + +
      diff --git a/src/ui/public/angular-bootstrap/datepicker/popup.html b/src/ui/public/angular-bootstrap/datepicker/popup.html new file mode 100755 index 00000000000000..483bbe1e7f4952 --- /dev/null +++ b/src/ui/public/angular-bootstrap/datepicker/popup.html @@ -0,0 +1,10 @@ + diff --git a/src/ui/public/angular-bootstrap/datepicker/year.html b/src/ui/public/angular-bootstrap/datepicker/year.html new file mode 100755 index 00000000000000..978d80c2321e5d --- /dev/null +++ b/src/ui/public/angular-bootstrap/datepicker/year.html @@ -0,0 +1,16 @@ + + + + + + + + + + + + + +
      + +
      diff --git a/src/ui/public/angular-bootstrap/dropdown/dropdown.js b/src/ui/public/angular-bootstrap/dropdown/dropdown.js new file mode 100755 index 00000000000000..e01c8308474bdd --- /dev/null +++ b/src/ui/public/angular-bootstrap/dropdown/dropdown.js @@ -0,0 +1,161 @@ +angular.module('ui.bootstrap.dropdown', []) + +.constant('dropdownConfig', { + openClass: 'open' +}) + +.service('dropdownService', ['$document', function($document) { + var openScope = null; + + this.open = function( dropdownScope ) { + if ( !openScope ) { + $document.bind('click', closeDropdown); + $document.bind('keydown', escapeKeyBind); + } + + if ( openScope && openScope !== dropdownScope ) { + openScope.isOpen = false; + } + + openScope = dropdownScope; + }; + + this.close = function( dropdownScope ) { + if ( openScope === dropdownScope ) { + openScope = null; + $document.unbind('click', closeDropdown); + $document.unbind('keydown', escapeKeyBind); + } + }; + + var closeDropdown = function( evt ) { + // This method may still be called during the same mouse event that + // unbound this event handler. So check openScope before proceeding. + if (!openScope) { return; } + + var toggleElement = openScope.getToggleElement(); + if ( evt && toggleElement && toggleElement[0].contains(evt.target) ) { + return; + } + + openScope.$apply(function() { + openScope.isOpen = false; + }); + }; + + var escapeKeyBind = function( evt ) { + if ( evt.which === 27 ) { + openScope.focusToggleElement(); + closeDropdown(); + } + }; +}]) + +.controller('DropdownController', ['$scope', '$attrs', '$parse', 'dropdownConfig', 'dropdownService', '$animate', function($scope, $attrs, $parse, dropdownConfig, dropdownService, $animate) { + var self = this, + scope = $scope.$new(), // create a child scope so we are not polluting original one + openClass = dropdownConfig.openClass, + getIsOpen, + setIsOpen = angular.noop, + toggleInvoker = $attrs.onToggle ? $parse($attrs.onToggle) : angular.noop; + + this.init = function( element ) { + self.$element = element; + + if ( $attrs.isOpen ) { + getIsOpen = $parse($attrs.isOpen); + setIsOpen = getIsOpen.assign; + + $scope.$watch(getIsOpen, function(value) { + scope.isOpen = !!value; + }); + } + }; + + this.toggle = function( open ) { + return scope.isOpen = arguments.length ? !!open : !scope.isOpen; + }; + + // Allow other directives to watch status + this.isOpen = function() { + return scope.isOpen; + }; + + scope.getToggleElement = function() { + return self.toggleElement; + }; + + scope.focusToggleElement = function() { + if ( self.toggleElement ) { + self.toggleElement[0].focus(); + } + }; + + scope.$watch('isOpen', function( isOpen, wasOpen ) { + $animate[isOpen ? 'addClass' : 'removeClass'](self.$element, openClass); + + if ( isOpen ) { + scope.focusToggleElement(); + dropdownService.open( scope ); + } else { + dropdownService.close( scope ); + } + + setIsOpen($scope, isOpen); + if (angular.isDefined(isOpen) && isOpen !== wasOpen) { + toggleInvoker($scope, { open: !!isOpen }); + } + }); + + $scope.$on('$locationChangeSuccess', function() { + scope.isOpen = false; + }); + + $scope.$on('$destroy', function() { + scope.$destroy(); + }); +}]) + +.directive('dropdown', function() { + return { + controller: 'DropdownController', + link: function(scope, element, attrs, dropdownCtrl) { + dropdownCtrl.init( element ); + } + }; +}) + +.directive('dropdownToggle', function() { + return { + require: '?^dropdown', + link: function(scope, element, attrs, dropdownCtrl) { + if ( !dropdownCtrl ) { + return; + } + + dropdownCtrl.toggleElement = element; + + var toggleDropdown = function(event) { + event.preventDefault(); + + if ( !element.hasClass('disabled') && !attrs.disabled ) { + scope.$apply(function() { + dropdownCtrl.toggle(); + }); + } + }; + + element.bind('click', toggleDropdown); + + // WAI-ARIA + element.attr({ 'aria-haspopup': true, 'aria-expanded': false }); + scope.$watch(dropdownCtrl.isOpen, function( isOpen ) { + element.attr('aria-expanded', !!isOpen); + }); + + scope.$on('$destroy', function() { + element.unbind('click', toggleDropdown); + }); + } + }; +}); diff --git a/src/ui/public/angular-bootstrap/index.js b/src/ui/public/angular-bootstrap/index.js new file mode 100644 index 00000000000000..7945c4f25a1be5 --- /dev/null +++ b/src/ui/public/angular-bootstrap/index.js @@ -0,0 +1,229 @@ + +/* eslint-disable */ + +/** + * TODO: Write custom components that address our needs to directly and deprecate these Bootstrap components. + */ + +/* + * angular-ui-bootstrap + * http://angular-ui.github.io/bootstrap/ + + * Version: 0.12.1 - 2015-02-20 + * License: MIT + */ +angular.module('ui.bootstrap', [ + 'ui.bootstrap.tpls', + 'ui.bootstrap.transition', + 'ui.bootstrap.collapse', + 'ui.bootstrap.alert', + 'ui.bootstrap.bindHtml', + 'ui.bootstrap.buttons', + 'ui.bootstrap.carousel', + 'ui.bootstrap.dateparser', + 'ui.bootstrap.position', + 'ui.bootstrap.datepicker', + 'ui.bootstrap.dropdown', + 'ui.bootstrap.modal', + 'ui.bootstrap.pagination', + 'ui.bootstrap.tooltip', + 'ui.bootstrap.popover', + 'ui.bootstrap.progressbar', + 'ui.bootstrap.rating', + 'ui.bootstrap.tabs', + 'ui.bootstrap.timepicker', + 'ui.bootstrap.typeahead' +]); + +angular.module('ui.bootstrap.tpls', [ + 'template/alert/alert.html', + 'template/carousel/carousel.html', + 'template/carousel/slide.html', + 'template/datepicker/datepicker.html', + 'template/datepicker/day.html', + 'template/datepicker/month.html', + 'template/datepicker/popup.html', + 'template/datepicker/year.html', + 'template/modal/backdrop.html', + 'template/modal/window.html', + 'template/pagination/pager.html', + 'template/pagination/pagination.html', + 'template/tooltip/tooltip-html-unsafe-popup.html', + 'template/tooltip/tooltip-popup.html', + 'template/popover/popover.html', + 'template/progressbar/bar.html', + 'template/progressbar/progress.html', + 'template/progressbar/progressbar.html', + 'template/rating/rating.html', + 'template/tabs/tab.html', + 'template/tabs/tabset.html', + 'template/timepicker/timepicker.html', + 'template/typeahead/typeahead-match.html', + 'template/typeahead/typeahead-popup.html' +]); + +import './accordion'; +import './alert'; +import './bindHtml'; +import './buttons'; +import './carousel'; +import './collapse'; +import './dateparser'; +import './datepicker'; +import './dropdown'; +import './modal'; +import './pagination'; +import './popover'; +import './position'; +import './progressbar'; +import './rating'; +import './tabs'; +import './timepicker'; +import './tooltip'; +import './transition'; +import './typeahead'; + +import alert from './alert/alert.html'; + +angular.module('template/alert/alert.html', []).run(['$templateCache', function($templateCache) { + $templateCache.put('template/alert/alert.html', alert); +}]); + +import carousel from './carousel/carousel.html'; + +angular.module('template/carousel/carousel.html', []).run(['$templateCache', function($templateCache) { + $templateCache.put('template/carousel/carousel.html', carousel); +}]); + +import slide from './carousel/slide.html'; + +angular.module('template/carousel/slide.html', []).run(['$templateCache', function($templateCache) { + $templateCache.put('template/carousel/slide.html', slide); +}]); + +import datepicker from './datepicker/datepicker.html'; + +angular.module('template/datepicker/datepicker.html', []).run(['$templateCache', function($templateCache) { + $templateCache.put('template/datepicker/datepicker.html', datepicker); +}]); + +import day from './datepicker/day.html'; + +angular.module('template/datepicker/day.html', []).run(['$templateCache', function($templateCache) { + $templateCache.put('template/datepicker/day.html', day); +}]); + +import month from './datepicker/month.html'; + +angular.module('template/datepicker/month.html', []).run(['$templateCache', function($templateCache) { + $templateCache.put('template/datepicker/month.html', month); +}]); + +import popup from './datepicker/popup.html'; + +angular.module('template/datepicker/popup.html', []).run(['$templateCache', function($templateCache) { + $templateCache.put('template/datepicker/popup.html', popup); +}]); + +import year from './datepicker/year.html'; + +angular.module('template/datepicker/year.html', []).run(['$templateCache', function($templateCache) { + $templateCache.put('template/datepicker/year.html', year); +}]); + +import backdrop from './modal/backdrop.html'; + +angular.module('template/modal/backdrop.html', []).run(['$templateCache', function($templateCache) { + $templateCache.put('template/modal/backdrop.html', backdrop); +}]); + +import modal from './modal/window.html'; + +angular.module('template/modal/window.html', []).run(['$templateCache', function($templateCache) { + $templateCache.put('template/modal/window.html', modal); +}]); + +import pager from './pagination/pager.html'; + +angular.module('template/pagination/pager.html', []).run(['$templateCache', function($templateCache) { + $templateCache.put('template/pagination/pager.html', pager); +}]); + +import pagination from './pagination/pagination.html'; + +angular.module('template/pagination/pagination.html', []).run(['$templateCache', function($templateCache) { + $templateCache.put('template/pagination/pagination.html', pagination); +}]); + +import tooltipUnsafePopup from './tooltip/tooltip-html-unsafe-popup.html'; + +angular.module('template/tooltip/tooltip-html-unsafe-popup.html', []).run(['$templateCache', function($templateCache) { + $templateCache.put('template/tooltip/tooltip-html-unsafe-popup.html', tooltipUnsafePopup); +}]); + +import tooltipPopup from './tooltip/tooltip-popup.html'; + +angular.module('template/tooltip/tooltip-popup.html', []).run(['$templateCache', function($templateCache) { + $templateCache.put('template/tooltip/tooltip-popup.html', tooltipPopup); +}]); + +import popover from './popover/popover.html'; + +angular.module('template/popover/popover.html', []).run(['$templateCache', function($templateCache) { + $templateCache.put('template/popover/popover.html', popover); +}]); + +import bar from './progressbar/bar.html'; + +angular.module('template/progressbar/bar.html', []).run(['$templateCache', function($templateCache) { + $templateCache.put('template/progressbar/bar.html', bar); +}]); + +import progress from './progressbar/progress.html'; + +angular.module('template/progressbar/progress.html', []).run(['$templateCache', function($templateCache) { + $templateCache.put('template/progressbar/progress.html', progress); +}]); + +import progressbar from './progressbar/progressbar.html'; + +angular.module('template/progressbar/progressbar.html', []).run(['$templateCache', function($templateCache) { + $templateCache.put('template/progressbar/progressbar.html', progressbar); +}]); + +import rating from './rating/rating.html'; + +angular.module('template/rating/rating.html', []).run(['$templateCache', function($templateCache) { + $templateCache.put('template/rating/rating.html', rating); +}]); + +import tab from './tabs/tab.html'; + +angular.module('template/tabs/tab.html', []).run(['$templateCache', function($templateCache) { + $templateCache.put('template/tabs/tab.html', tab); +}]); + +import tabset from './tabs/tabset.html'; + +angular.module('template/tabs/tabset.html', []).run(['$templateCache', function($templateCache) { + $templateCache.put('template/tabs/tabset.html', tabset); +}]); + +import timepicker from './timepicker/timepicker.html'; + +angular.module('template/timepicker/timepicker.html', []).run(['$templateCache', function($templateCache) { + $templateCache.put('template/timepicker/timepicker.html', timepicker); +}]); + +import typeaheadMatch from './typeahead/typeahead-match.html'; + +angular.module('template/typeahead/typeahead-match.html', []).run(['$templateCache', function($templateCache) { + $templateCache.put('template/typeahead/typeahead-match.html', typeaheadMatch); +}]); + +import typeaheadPopup from './typeahead/typeahead-popup.html'; + +angular.module('template/typeahead/typeahead-popup.html', []).run(['$templateCache', function($templateCache) { + $templateCache.put('template/typeahead/typeahead-popup.html', typeaheadPopup); +}]); + diff --git a/src/ui/public/angular-bootstrap/modal/backdrop.html b/src/ui/public/angular-bootstrap/modal/backdrop.html new file mode 100755 index 00000000000000..9cbfcb6cbb1d1b --- /dev/null +++ b/src/ui/public/angular-bootstrap/modal/backdrop.html @@ -0,0 +1,4 @@ + diff --git a/src/ui/public/angular-bootstrap/modal/modal.js b/src/ui/public/angular-bootstrap/modal/modal.js new file mode 100755 index 00000000000000..662765d35749f4 --- /dev/null +++ b/src/ui/public/angular-bootstrap/modal/modal.js @@ -0,0 +1,415 @@ +angular.module('ui.bootstrap.modal', ['ui.bootstrap.transition']) + +/** + * A helper, internal data structure that acts as a map but also allows getting / removing + * elements in the LIFO order + */ + .factory('$$stackedMap', function () { + return { + createNew: function () { + var stack = []; + + return { + add: function (key, value) { + stack.push({ + key: key, + value: value + }); + }, + get: function (key) { + for (var i = 0; i < stack.length; i++) { + if (key == stack[i].key) { + return stack[i]; + } + } + }, + keys: function() { + var keys = []; + for (var i = 0; i < stack.length; i++) { + keys.push(stack[i].key); + } + return keys; + }, + top: function () { + return stack[stack.length - 1]; + }, + remove: function (key) { + var idx = -1; + for (var i = 0; i < stack.length; i++) { + if (key == stack[i].key) { + idx = i; + break; + } + } + return stack.splice(idx, 1)[0]; + }, + removeTop: function () { + return stack.splice(stack.length - 1, 1)[0]; + }, + length: function () { + return stack.length; + } + }; + } + }; + }) + +/** + * A helper directive for the $modal service. It creates a backdrop element. + */ + .directive('modalBackdrop', ['$timeout', function ($timeout) { + return { + restrict: 'EA', + replace: true, + templateUrl: 'template/modal/backdrop.html', + link: function (scope, element, attrs) { + scope.backdropClass = attrs.backdropClass || ''; + + scope.animate = false; + + //trigger CSS transitions + $timeout(function () { + scope.animate = true; + }); + } + }; + }]) + + .directive('modalWindow', ['$modalStack', '$timeout', function ($modalStack, $timeout) { + return { + restrict: 'EA', + scope: { + index: '@', + animate: '=' + }, + replace: true, + transclude: true, + templateUrl: function(tElement, tAttrs) { + return tAttrs.templateUrl || 'template/modal/window.html'; + }, + link: function (scope, element, attrs) { + element.addClass(attrs.windowClass || ''); + scope.size = attrs.size; + + $timeout(function () { + // trigger CSS transitions + scope.animate = true; + + /** + * Auto-focusing of a freshly-opened modal element causes any child elements + * with the autofocus attribute to lose focus. This is an issue on touch + * based devices which will show and then hide the onscreen keyboard. + * Attempts to refocus the autofocus element via JavaScript will not reopen + * the onscreen keyboard. Fixed by updated the focusing logic to only autofocus + * the modal element if the modal does not contain an autofocus element. + */ + if (!element[0].querySelectorAll('[autofocus]').length) { + element[0].focus(); + } + }); + + scope.close = function (evt) { + var modal = $modalStack.getTop(); + if (modal && modal.value.backdrop && modal.value.backdrop != 'static' && (evt.target === evt.currentTarget)) { + evt.preventDefault(); + evt.stopPropagation(); + $modalStack.dismiss(modal.key, 'backdrop click'); + } + }; + } + }; + }]) + + .directive('modalTransclude', function () { + return { + link: function($scope, $element, $attrs, controller, $transclude) { + $transclude($scope.$parent, function(clone) { + $element.empty(); + $element.append(clone); + }); + } + }; + }) + + .factory('$modalStack', ['$transition', '$timeout', '$document', '$compile', '$rootScope', '$$stackedMap', + function ($transition, $timeout, $document, $compile, $rootScope, $$stackedMap) { + + var OPENED_MODAL_CLASS = 'modal-open'; + + var backdropDomEl, backdropScope; + var openedWindows = $$stackedMap.createNew(); + var $modalStack = {}; + + function backdropIndex() { + var topBackdropIndex = -1; + var opened = openedWindows.keys(); + for (var i = 0; i < opened.length; i++) { + if (openedWindows.get(opened[i]).value.backdrop) { + topBackdropIndex = i; + } + } + return topBackdropIndex; + } + + $rootScope.$watch(backdropIndex, function(newBackdropIndex){ + if (backdropScope) { + backdropScope.index = newBackdropIndex; + } + }); + + function removeModalWindow(modalInstance) { + + var body = $document.find('body').eq(0); + var modalWindow = openedWindows.get(modalInstance).value; + + //clean up the stack + openedWindows.remove(modalInstance); + + //remove window DOM element + removeAfterAnimate(modalWindow.modalDomEl, modalWindow.modalScope, 300, function() { + modalWindow.modalScope.$destroy(); + body.toggleClass(OPENED_MODAL_CLASS, openedWindows.length() > 0); + checkRemoveBackdrop(); + }); + } + + function checkRemoveBackdrop() { + //remove backdrop if no longer needed + if (backdropDomEl && backdropIndex() == -1) { + var backdropScopeRef = backdropScope; + removeAfterAnimate(backdropDomEl, backdropScope, 150, function () { + backdropScopeRef.$destroy(); + backdropScopeRef = null; + }); + backdropDomEl = undefined; + backdropScope = undefined; + } + } + + function removeAfterAnimate(domEl, scope, emulateTime, done) { + // Closing animation + scope.animate = false; + + var transitionEndEventName = $transition.transitionEndEventName; + if (transitionEndEventName) { + // transition out + var timeout = $timeout(afterAnimating, emulateTime); + + domEl.bind(transitionEndEventName, function () { + $timeout.cancel(timeout); + afterAnimating(); + scope.$apply(); + }); + } else { + // Ensure this call is async + $timeout(afterAnimating); + } + + function afterAnimating() { + if (afterAnimating.done) { + return; + } + afterAnimating.done = true; + + domEl.remove(); + if (done) { + done(); + } + } + } + + $document.bind('keydown', function (evt) { + var modal; + + if (evt.which === 27) { + modal = openedWindows.top(); + if (modal && modal.value.keyboard) { + evt.preventDefault(); + $rootScope.$apply(function () { + $modalStack.dismiss(modal.key, 'escape key press'); + }); + } + } + }); + + $modalStack.open = function (modalInstance, modal) { + + openedWindows.add(modalInstance, { + deferred: modal.deferred, + modalScope: modal.scope, + backdrop: modal.backdrop, + keyboard: modal.keyboard + }); + + var body = $document.find('body').eq(0), + currBackdropIndex = backdropIndex(); + + if (currBackdropIndex >= 0 && !backdropDomEl) { + backdropScope = $rootScope.$new(true); + backdropScope.index = currBackdropIndex; + var angularBackgroundDomEl = angular.element('
      '); + angularBackgroundDomEl.attr('backdrop-class', modal.backdropClass); + backdropDomEl = $compile(angularBackgroundDomEl)(backdropScope); + body.append(backdropDomEl); + } + + var angularDomEl = angular.element('
      '); + angularDomEl.attr({ + 'template-url': modal.windowTemplateUrl, + 'window-class': modal.windowClass, + 'size': modal.size, + 'index': openedWindows.length() - 1, + 'animate': 'animate' + }).html(modal.content); + + var modalDomEl = $compile(angularDomEl)(modal.scope); + openedWindows.top().value.modalDomEl = modalDomEl; + body.append(modalDomEl); + body.addClass(OPENED_MODAL_CLASS); + }; + + $modalStack.close = function (modalInstance, result) { + var modalWindow = openedWindows.get(modalInstance); + if (modalWindow) { + modalWindow.value.deferred.resolve(result); + removeModalWindow(modalInstance); + } + }; + + $modalStack.dismiss = function (modalInstance, reason) { + var modalWindow = openedWindows.get(modalInstance); + if (modalWindow) { + modalWindow.value.deferred.reject(reason); + removeModalWindow(modalInstance); + } + }; + + $modalStack.dismissAll = function (reason) { + var topModal = this.getTop(); + while (topModal) { + this.dismiss(topModal.key, reason); + topModal = this.getTop(); + } + }; + + $modalStack.getTop = function () { + return openedWindows.top(); + }; + + return $modalStack; + }]) + + .provider('$modal', function () { + + var $modalProvider = { + options: { + backdrop: true, //can be also false or 'static' + keyboard: true + }, + $get: ['$injector', '$rootScope', '$q', '$http', '$templateCache', '$controller', '$modalStack', + function ($injector, $rootScope, $q, $http, $templateCache, $controller, $modalStack) { + + var $modal = {}; + + function getTemplatePromise(options) { + return options.template ? $q.when(options.template) : + $http.get(angular.isFunction(options.templateUrl) ? (options.templateUrl)() : options.templateUrl, + {cache: $templateCache}).then(function (result) { + return result.data; + }); + } + + function getResolvePromises(resolves) { + var promisesArr = []; + angular.forEach(resolves, function (value) { + if (angular.isFunction(value) || angular.isArray(value)) { + promisesArr.push($q.when($injector.invoke(value))); + } + }); + return promisesArr; + } + + $modal.open = function (modalOptions) { + + var modalResultDeferred = $q.defer(); + var modalOpenedDeferred = $q.defer(); + + //prepare an instance of a modal to be injected into controllers and returned to a caller + var modalInstance = { + result: modalResultDeferred.promise, + opened: modalOpenedDeferred.promise, + close: function (result) { + $modalStack.close(modalInstance, result); + }, + dismiss: function (reason) { + $modalStack.dismiss(modalInstance, reason); + } + }; + + //merge and clean up options + modalOptions = angular.extend({}, $modalProvider.options, modalOptions); + modalOptions.resolve = modalOptions.resolve || {}; + + //verify options + if (!modalOptions.template && !modalOptions.templateUrl) { + throw new Error('One of template or templateUrl options is required.'); + } + + var templateAndResolvePromise = + $q.all([getTemplatePromise(modalOptions)].concat(getResolvePromises(modalOptions.resolve))); + + + templateAndResolvePromise.then(function resolveSuccess(tplAndVars) { + + var modalScope = (modalOptions.scope || $rootScope).$new(); + modalScope.$close = modalInstance.close; + modalScope.$dismiss = modalInstance.dismiss; + + var ctrlInstance, ctrlLocals = {}; + var resolveIter = 1; + + //controllers + if (modalOptions.controller) { + ctrlLocals.$scope = modalScope; + ctrlLocals.$modalInstance = modalInstance; + angular.forEach(modalOptions.resolve, function (value, key) { + ctrlLocals[key] = tplAndVars[resolveIter++]; + }); + + ctrlInstance = $controller(modalOptions.controller, ctrlLocals); + if (modalOptions.controllerAs) { + modalScope[modalOptions.controllerAs] = ctrlInstance; + } + } + + $modalStack.open(modalInstance, { + scope: modalScope, + deferred: modalResultDeferred, + content: tplAndVars[0], + backdrop: modalOptions.backdrop, + keyboard: modalOptions.keyboard, + backdropClass: modalOptions.backdropClass, + windowClass: modalOptions.windowClass, + windowTemplateUrl: modalOptions.windowTemplateUrl, + size: modalOptions.size + }); + + }, function resolveError(reason) { + modalResultDeferred.reject(reason); + }); + + templateAndResolvePromise.then(function () { + modalOpenedDeferred.resolve(true); + }, function () { + modalOpenedDeferred.reject(false); + }); + + return modalInstance; + }; + + return $modal; + }] + }; + + return $modalProvider; + }); diff --git a/src/ui/public/angular-bootstrap/modal/window.html b/src/ui/public/angular-bootstrap/modal/window.html new file mode 100755 index 00000000000000..81ca197ebbd2db --- /dev/null +++ b/src/ui/public/angular-bootstrap/modal/window.html @@ -0,0 +1,3 @@ + \ No newline at end of file diff --git a/src/ui/public/angular-bootstrap/pagination/pager.html b/src/ui/public/angular-bootstrap/pagination/pager.html new file mode 100755 index 00000000000000..ca150de1642552 --- /dev/null +++ b/src/ui/public/angular-bootstrap/pagination/pager.html @@ -0,0 +1,4 @@ + \ No newline at end of file diff --git a/src/ui/public/angular-bootstrap/pagination/pagination.html b/src/ui/public/angular-bootstrap/pagination/pagination.html new file mode 100755 index 00000000000000..cd45d290d51428 --- /dev/null +++ b/src/ui/public/angular-bootstrap/pagination/pagination.html @@ -0,0 +1,7 @@ + \ No newline at end of file diff --git a/src/ui/public/angular-bootstrap/pagination/pagination.js b/src/ui/public/angular-bootstrap/pagination/pagination.js new file mode 100755 index 00000000000000..f022dce54046e6 --- /dev/null +++ b/src/ui/public/angular-bootstrap/pagination/pagination.js @@ -0,0 +1,214 @@ +angular.module('ui.bootstrap.pagination', []) + +.controller('PaginationController', ['$scope', '$attrs', '$parse', function ($scope, $attrs, $parse) { + var self = this, + ngModelCtrl = { $setViewValue: angular.noop }, // nullModelCtrl + setNumPages = $attrs.numPages ? $parse($attrs.numPages).assign : angular.noop; + + this.init = function(ngModelCtrl_, config) { + ngModelCtrl = ngModelCtrl_; + this.config = config; + + ngModelCtrl.$render = function() { + self.render(); + }; + + if ($attrs.itemsPerPage) { + $scope.$parent.$watch($parse($attrs.itemsPerPage), function(value) { + self.itemsPerPage = parseInt(value, 10); + $scope.totalPages = self.calculateTotalPages(); + }); + } else { + this.itemsPerPage = config.itemsPerPage; + } + }; + + this.calculateTotalPages = function() { + var totalPages = this.itemsPerPage < 1 ? 1 : Math.ceil($scope.totalItems / this.itemsPerPage); + return Math.max(totalPages || 0, 1); + }; + + this.render = function() { + $scope.page = parseInt(ngModelCtrl.$viewValue, 10) || 1; + }; + + $scope.selectPage = function(page) { + if ( $scope.page !== page && page > 0 && page <= $scope.totalPages) { + ngModelCtrl.$setViewValue(page); + ngModelCtrl.$render(); + } + }; + + $scope.getText = function( key ) { + return $scope[key + 'Text'] || self.config[key + 'Text']; + }; + $scope.noPrevious = function() { + return $scope.page === 1; + }; + $scope.noNext = function() { + return $scope.page === $scope.totalPages; + }; + + $scope.$watch('totalItems', function() { + $scope.totalPages = self.calculateTotalPages(); + }); + + $scope.$watch('totalPages', function(value) { + setNumPages($scope.$parent, value); // Readonly variable + + if ( $scope.page > value ) { + $scope.selectPage(value); + } else { + ngModelCtrl.$render(); + } + }); +}]) + +.constant('paginationConfig', { + itemsPerPage: 10, + boundaryLinks: false, + directionLinks: true, + firstText: 'First', + previousText: 'Previous', + nextText: 'Next', + lastText: 'Last', + rotate: true +}) + +.directive('pagination', ['$parse', 'paginationConfig', function($parse, paginationConfig) { + return { + restrict: 'EA', + scope: { + totalItems: '=', + firstText: '@', + previousText: '@', + nextText: '@', + lastText: '@' + }, + require: ['pagination', '?ngModel'], + controller: 'PaginationController', + templateUrl: 'template/pagination/pagination.html', + replace: true, + link: function(scope, element, attrs, ctrls) { + var paginationCtrl = ctrls[0], ngModelCtrl = ctrls[1]; + + if (!ngModelCtrl) { + return; // do nothing if no ng-model + } + + // Setup configuration parameters + var maxSize = angular.isDefined(attrs.maxSize) ? scope.$parent.$eval(attrs.maxSize) : paginationConfig.maxSize, + rotate = angular.isDefined(attrs.rotate) ? scope.$parent.$eval(attrs.rotate) : paginationConfig.rotate; + scope.boundaryLinks = angular.isDefined(attrs.boundaryLinks) ? scope.$parent.$eval(attrs.boundaryLinks) : paginationConfig.boundaryLinks; + scope.directionLinks = angular.isDefined(attrs.directionLinks) ? scope.$parent.$eval(attrs.directionLinks) : paginationConfig.directionLinks; + + paginationCtrl.init(ngModelCtrl, paginationConfig); + + if (attrs.maxSize) { + scope.$parent.$watch($parse(attrs.maxSize), function(value) { + maxSize = parseInt(value, 10); + paginationCtrl.render(); + }); + } + + // Create page object used in template + function makePage(number, text, isActive) { + return { + number: number, + text: text, + active: isActive + }; + } + + function getPages(currentPage, totalPages) { + var pages = []; + + // Default page limits + var startPage = 1, endPage = totalPages; + var isMaxSized = ( angular.isDefined(maxSize) && maxSize < totalPages ); + + // recompute if maxSize + if ( isMaxSized ) { + if ( rotate ) { + // Current page is displayed in the middle of the visible ones + startPage = Math.max(currentPage - Math.floor(maxSize/2), 1); + endPage = startPage + maxSize - 1; + + // Adjust if limit is exceeded + if (endPage > totalPages) { + endPage = totalPages; + startPage = endPage - maxSize + 1; + } + } else { + // Visible pages are paginated with maxSize + startPage = ((Math.ceil(currentPage / maxSize) - 1) * maxSize) + 1; + + // Adjust last page if limit is exceeded + endPage = Math.min(startPage + maxSize - 1, totalPages); + } + } + + // Add page number links + for (var number = startPage; number <= endPage; number++) { + var page = makePage(number, number, number === currentPage); + pages.push(page); + } + + // Add links to move between page sets + if ( isMaxSized && ! rotate ) { + if ( startPage > 1 ) { + var previousPageSet = makePage(startPage - 1, '...', false); + pages.unshift(previousPageSet); + } + + if ( endPage < totalPages ) { + var nextPageSet = makePage(endPage + 1, '...', false); + pages.push(nextPageSet); + } + } + + return pages; + } + + var originalRender = paginationCtrl.render; + paginationCtrl.render = function() { + originalRender(); + if (scope.page > 0 && scope.page <= scope.totalPages) { + scope.pages = getPages(scope.page, scope.totalPages); + } + }; + } + }; +}]) + +.constant('pagerConfig', { + itemsPerPage: 10, + previousText: '« Previous', + nextText: 'Next »', + align: true +}) + +.directive('pager', ['pagerConfig', function(pagerConfig) { + return { + restrict: 'EA', + scope: { + totalItems: '=', + previousText: '@', + nextText: '@' + }, + require: ['pager', '?ngModel'], + controller: 'PaginationController', + templateUrl: 'template/pagination/pager.html', + replace: true, + link: function(scope, element, attrs, ctrls) { + var paginationCtrl = ctrls[0], ngModelCtrl = ctrls[1]; + + if (!ngModelCtrl) { + return; // do nothing if no ng-model + } + + scope.align = angular.isDefined(attrs.align) ? scope.$parent.$eval(attrs.align) : pagerConfig.align; + paginationCtrl.init(ngModelCtrl, pagerConfig); + } + }; +}]); diff --git a/src/ui/public/angular-bootstrap/popover/popover.html b/src/ui/public/angular-bootstrap/popover/popover.html new file mode 100755 index 00000000000000..5929ee6e6ac68a --- /dev/null +++ b/src/ui/public/angular-bootstrap/popover/popover.html @@ -0,0 +1,8 @@ +
      +
      + +
      +

      +
      +
      +
      diff --git a/src/ui/public/angular-bootstrap/popover/popover.js b/src/ui/public/angular-bootstrap/popover/popover.js new file mode 100755 index 00000000000000..2bea0a3e10a6a5 --- /dev/null +++ b/src/ui/public/angular-bootstrap/popover/popover.js @@ -0,0 +1,19 @@ +/** + * The following features are still outstanding: popup delay, animation as a + * function, placement as a function, inside, support for more triggers than + * just mouse enter/leave, html popovers, and selector delegatation. + */ +angular.module( 'ui.bootstrap.popover', [ 'ui.bootstrap.tooltip' ] ) + +.directive( 'popoverPopup', function () { + return { + restrict: 'EA', + replace: true, + scope: { title: '@', content: '@', placement: '@', animation: '&', isOpen: '&' }, + templateUrl: 'template/popover/popover.html' + }; +}) + +.directive( 'popover', [ '$tooltip', function ( $tooltip ) { + return $tooltip( 'popover', 'popover', 'click' ); +}]); diff --git a/src/ui/public/angular-bootstrap/position/position.js b/src/ui/public/angular-bootstrap/position/position.js new file mode 100755 index 00000000000000..3444c33449152d --- /dev/null +++ b/src/ui/public/angular-bootstrap/position/position.js @@ -0,0 +1,152 @@ +angular.module('ui.bootstrap.position', []) + +/** + * A set of utility methods that can be use to retrieve position of DOM elements. + * It is meant to be used where we need to absolute-position DOM elements in + * relation to other, existing elements (this is the case for tooltips, popovers, + * typeahead suggestions etc.). + */ + .factory('$position', ['$document', '$window', function ($document, $window) { + + function getStyle(el, cssprop) { + if (el.currentStyle) { //IE + return el.currentStyle[cssprop]; + } else if ($window.getComputedStyle) { + return $window.getComputedStyle(el)[cssprop]; + } + // finally try and get inline style + return el.style[cssprop]; + } + + /** + * Checks if a given element is statically positioned + * @param element - raw DOM element + */ + function isStaticPositioned(element) { + return (getStyle(element, 'position') || 'static' ) === 'static'; + } + + /** + * returns the closest, non-statically positioned parentOffset of a given element + * @param element + */ + var parentOffsetEl = function (element) { + var docDomEl = $document[0]; + var offsetParent = element.offsetParent || docDomEl; + while (offsetParent && offsetParent !== docDomEl && isStaticPositioned(offsetParent) ) { + offsetParent = offsetParent.offsetParent; + } + return offsetParent || docDomEl; + }; + + return { + /** + * Provides read-only equivalent of jQuery's position function: + * http://api.jquery.com/position/ + */ + position: function (element) { + var elBCR = this.offset(element); + var offsetParentBCR = { top: 0, left: 0 }; + var offsetParentEl = parentOffsetEl(element[0]); + if (offsetParentEl != $document[0]) { + offsetParentBCR = this.offset(angular.element(offsetParentEl)); + offsetParentBCR.top += offsetParentEl.clientTop - offsetParentEl.scrollTop; + offsetParentBCR.left += offsetParentEl.clientLeft - offsetParentEl.scrollLeft; + } + + var boundingClientRect = element[0].getBoundingClientRect(); + return { + width: boundingClientRect.width || element.prop('offsetWidth'), + height: boundingClientRect.height || element.prop('offsetHeight'), + top: elBCR.top - offsetParentBCR.top, + left: elBCR.left - offsetParentBCR.left + }; + }, + + /** + * Provides read-only equivalent of jQuery's offset function: + * http://api.jquery.com/offset/ + */ + offset: function (element) { + var boundingClientRect = element[0].getBoundingClientRect(); + return { + width: boundingClientRect.width || element.prop('offsetWidth'), + height: boundingClientRect.height || element.prop('offsetHeight'), + top: boundingClientRect.top + ($window.pageYOffset || $document[0].documentElement.scrollTop), + left: boundingClientRect.left + ($window.pageXOffset || $document[0].documentElement.scrollLeft) + }; + }, + + /** + * Provides coordinates for the targetEl in relation to hostEl + */ + positionElements: function (hostEl, targetEl, positionStr, appendToBody) { + + var positionStrParts = positionStr.split('-'); + var pos0 = positionStrParts[0], pos1 = positionStrParts[1] || 'center'; + + var hostElPos, + targetElWidth, + targetElHeight, + targetElPos; + + hostElPos = appendToBody ? this.offset(hostEl) : this.position(hostEl); + + targetElWidth = targetEl.prop('offsetWidth'); + targetElHeight = targetEl.prop('offsetHeight'); + + var shiftWidth = { + center: function () { + return hostElPos.left + hostElPos.width / 2 - targetElWidth / 2; + }, + left: function () { + return hostElPos.left; + }, + right: function () { + return hostElPos.left + hostElPos.width; + } + }; + + var shiftHeight = { + center: function () { + return hostElPos.top + hostElPos.height / 2 - targetElHeight / 2; + }, + top: function () { + return hostElPos.top; + }, + bottom: function () { + return hostElPos.top + hostElPos.height; + } + }; + + switch (pos0) { + case 'right': + targetElPos = { + top: shiftHeight[pos1](), + left: shiftWidth[pos0]() + }; + break; + case 'left': + targetElPos = { + top: shiftHeight[pos1](), + left: hostElPos.left - targetElWidth + }; + break; + case 'bottom': + targetElPos = { + top: shiftHeight[pos0](), + left: shiftWidth[pos1]() + }; + break; + default: + targetElPos = { + top: hostElPos.top - targetElHeight, + left: shiftWidth[pos1]() + }; + break; + } + + return targetElPos; + } + }; + }]); diff --git a/src/ui/public/angular-bootstrap/progressbar/bar.html b/src/ui/public/angular-bootstrap/progressbar/bar.html new file mode 100755 index 00000000000000..bde46dca5515fb --- /dev/null +++ b/src/ui/public/angular-bootstrap/progressbar/bar.html @@ -0,0 +1 @@ +
      \ No newline at end of file diff --git a/src/ui/public/angular-bootstrap/progressbar/progress.html b/src/ui/public/angular-bootstrap/progressbar/progress.html new file mode 100755 index 00000000000000..19685370060875 --- /dev/null +++ b/src/ui/public/angular-bootstrap/progressbar/progress.html @@ -0,0 +1 @@ +
      \ No newline at end of file diff --git a/src/ui/public/angular-bootstrap/progressbar/progressbar.html b/src/ui/public/angular-bootstrap/progressbar/progressbar.html new file mode 100755 index 00000000000000..efb6503302e327 --- /dev/null +++ b/src/ui/public/angular-bootstrap/progressbar/progressbar.html @@ -0,0 +1,3 @@ +
      +
      +
      \ No newline at end of file diff --git a/src/ui/public/angular-bootstrap/progressbar/progressbar.js b/src/ui/public/angular-bootstrap/progressbar/progressbar.js new file mode 100755 index 00000000000000..9a6a6355d78b3a --- /dev/null +++ b/src/ui/public/angular-bootstrap/progressbar/progressbar.js @@ -0,0 +1,81 @@ +angular.module('ui.bootstrap.progressbar', []) + +.constant('progressConfig', { + animate: true, + max: 100 +}) + +.controller('ProgressController', ['$scope', '$attrs', 'progressConfig', function($scope, $attrs, progressConfig) { + var self = this, + animate = angular.isDefined($attrs.animate) ? $scope.$parent.$eval($attrs.animate) : progressConfig.animate; + + this.bars = []; + $scope.max = angular.isDefined($attrs.max) ? $scope.$parent.$eval($attrs.max) : progressConfig.max; + + this.addBar = function(bar, element) { + if ( !animate ) { + element.css({'transition': 'none'}); + } + + this.bars.push(bar); + + bar.$watch('value', function( value ) { + bar.percent = +(100 * value / $scope.max).toFixed(2); + }); + + bar.$on('$destroy', function() { + element = null; + self.removeBar(bar); + }); + }; + + this.removeBar = function(bar) { + this.bars.splice(this.bars.indexOf(bar), 1); + }; +}]) + +.directive('progress', function() { + return { + restrict: 'EA', + replace: true, + transclude: true, + controller: 'ProgressController', + require: 'progress', + scope: {}, + templateUrl: 'template/progressbar/progress.html' + }; +}) + +.directive('bar', function() { + return { + restrict: 'EA', + replace: true, + transclude: true, + require: '^progress', + scope: { + value: '=', + type: '@' + }, + templateUrl: 'template/progressbar/bar.html', + link: function(scope, element, attrs, progressCtrl) { + progressCtrl.addBar(scope, element); + } + }; +}) + +.directive('progressbar', function() { + return { + restrict: 'EA', + replace: true, + transclude: true, + controller: 'ProgressController', + scope: { + value: '=', + type: '@' + }, + templateUrl: 'template/progressbar/progressbar.html', + link: function(scope, element, attrs, progressCtrl) { + progressCtrl.addBar(scope, angular.element(element.children()[0])); + } + }; +}); \ No newline at end of file diff --git a/src/ui/public/angular-bootstrap/rating/rating.html b/src/ui/public/angular-bootstrap/rating/rating.html new file mode 100755 index 00000000000000..f4ab6bc7ef26e8 --- /dev/null +++ b/src/ui/public/angular-bootstrap/rating/rating.html @@ -0,0 +1,5 @@ + + + ({{ $index < value ? '*' : ' ' }}) + + \ No newline at end of file diff --git a/src/ui/public/angular-bootstrap/rating/rating.js b/src/ui/public/angular-bootstrap/rating/rating.js new file mode 100755 index 00000000000000..55ed9e0d44599f --- /dev/null +++ b/src/ui/public/angular-bootstrap/rating/rating.js @@ -0,0 +1,83 @@ +angular.module('ui.bootstrap.rating', []) + +.constant('ratingConfig', { + max: 5, + stateOn: null, + stateOff: null +}) + +.controller('RatingController', ['$scope', '$attrs', 'ratingConfig', function($scope, $attrs, ratingConfig) { + var ngModelCtrl = { $setViewValue: angular.noop }; + + this.init = function(ngModelCtrl_) { + ngModelCtrl = ngModelCtrl_; + ngModelCtrl.$render = this.render; + + this.stateOn = angular.isDefined($attrs.stateOn) ? $scope.$parent.$eval($attrs.stateOn) : ratingConfig.stateOn; + this.stateOff = angular.isDefined($attrs.stateOff) ? $scope.$parent.$eval($attrs.stateOff) : ratingConfig.stateOff; + + var ratingStates = angular.isDefined($attrs.ratingStates) ? $scope.$parent.$eval($attrs.ratingStates) : + new Array( angular.isDefined($attrs.max) ? $scope.$parent.$eval($attrs.max) : ratingConfig.max ); + $scope.range = this.buildTemplateObjects(ratingStates); + }; + + this.buildTemplateObjects = function(states) { + for (var i = 0, n = states.length; i < n; i++) { + states[i] = angular.extend({ index: i }, { stateOn: this.stateOn, stateOff: this.stateOff }, states[i]); + } + return states; + }; + + $scope.rate = function(value) { + if ( !$scope.readonly && value >= 0 && value <= $scope.range.length ) { + ngModelCtrl.$setViewValue(value); + ngModelCtrl.$render(); + } + }; + + $scope.enter = function(value) { + if ( !$scope.readonly ) { + $scope.value = value; + } + $scope.onHover({value: value}); + }; + + $scope.reset = function() { + $scope.value = ngModelCtrl.$viewValue; + $scope.onLeave(); + }; + + $scope.onKeydown = function(evt) { + if (/(37|38|39|40)/.test(evt.which)) { + evt.preventDefault(); + evt.stopPropagation(); + $scope.rate( $scope.value + (evt.which === 38 || evt.which === 39 ? 1 : -1) ); + } + }; + + this.render = function() { + $scope.value = ngModelCtrl.$viewValue; + }; +}]) + +.directive('rating', function() { + return { + restrict: 'EA', + require: ['rating', 'ngModel'], + scope: { + readonly: '=?', + onHover: '&', + onLeave: '&' + }, + controller: 'RatingController', + templateUrl: 'template/rating/rating.html', + replace: true, + link: function(scope, element, attrs, ctrls) { + var ratingCtrl = ctrls[0], ngModelCtrl = ctrls[1]; + + if ( ngModelCtrl ) { + ratingCtrl.init( ngModelCtrl ); + } + } + }; +}); \ No newline at end of file diff --git a/src/ui/public/angular-bootstrap/tabs/tab.html b/src/ui/public/angular-bootstrap/tabs/tab.html new file mode 100755 index 00000000000000..d76dd67caf234d --- /dev/null +++ b/src/ui/public/angular-bootstrap/tabs/tab.html @@ -0,0 +1,3 @@ +
    • + {{heading}} +
    • diff --git a/src/ui/public/angular-bootstrap/tabs/tabs.js b/src/ui/public/angular-bootstrap/tabs/tabs.js new file mode 100755 index 00000000000000..710959759a59b7 --- /dev/null +++ b/src/ui/public/angular-bootstrap/tabs/tabs.js @@ -0,0 +1,279 @@ + +/** + * @ngdoc overview + * @name ui.bootstrap.tabs + * + * @description + * AngularJS version of the tabs directive. + */ + +angular.module('ui.bootstrap.tabs', []) + +.controller('TabsetController', ['$scope', function TabsetCtrl($scope) { + var ctrl = this, + tabs = ctrl.tabs = $scope.tabs = []; + + ctrl.select = function(selectedTab) { + angular.forEach(tabs, function(tab) { + if (tab.active && tab !== selectedTab) { + tab.active = false; + tab.onDeselect(); + } + }); + selectedTab.active = true; + selectedTab.onSelect(); + }; + + ctrl.addTab = function addTab(tab) { + tabs.push(tab); + // we can't run the select function on the first tab + // since that would select it twice + if (tabs.length === 1) { + tab.active = true; + } else if (tab.active) { + ctrl.select(tab); + } + }; + + ctrl.removeTab = function removeTab(tab) { + var index = tabs.indexOf(tab); + //Select a new tab if the tab to be removed is selected and not destroyed + if (tab.active && tabs.length > 1 && !destroyed) { + //If this is the last tab, select the previous tab. else, the next tab. + var newActiveIndex = index == tabs.length - 1 ? index - 1 : index + 1; + ctrl.select(tabs[newActiveIndex]); + } + tabs.splice(index, 1); + }; + + var destroyed; + $scope.$on('$destroy', function() { + destroyed = true; + }); +}]) + +/** + * @ngdoc directive + * @name ui.bootstrap.tabs.directive:tabset + * @restrict EA + * + * @description + * Tabset is the outer container for the tabs directive + * + * @param {boolean=} vertical Whether or not to use vertical styling for the tabs. + * @param {boolean=} justified Whether or not to use justified styling for the tabs. + * + * @example + + + + First Content! + Second Content! + +
      + + First Vertical Content! + Second Vertical Content! + + + First Justified Content! + Second Justified Content! + +
      +
      + */ +.directive('tabset', function() { + return { + restrict: 'EA', + transclude: true, + replace: true, + scope: { + type: '@' + }, + controller: 'TabsetController', + templateUrl: 'template/tabs/tabset.html', + link: function(scope, element, attrs) { + scope.vertical = angular.isDefined(attrs.vertical) ? scope.$parent.$eval(attrs.vertical) : false; + scope.justified = angular.isDefined(attrs.justified) ? scope.$parent.$eval(attrs.justified) : false; + } + }; +}) + +/** + * @ngdoc directive + * @name ui.bootstrap.tabs.directive:tab + * @restrict EA + * + * @param {string=} heading The visible heading, or title, of the tab. Set HTML headings with {@link ui.bootstrap.tabs.directive:tabHeading tabHeading}. + * @param {string=} select An expression to evaluate when the tab is selected. + * @param {boolean=} active A binding, telling whether or not this tab is selected. + * @param {boolean=} disabled A binding, telling whether or not this tab is disabled. + * + * @description + * Creates a tab with a heading and content. Must be placed within a {@link ui.bootstrap.tabs.directive:tabset tabset}. + * + * @example + + +
      + + +
      + + First Tab + + Alert me! + Second Tab, with alert callback and html heading! + + + {{item.content}} + + +
      +
      + + function TabsDemoCtrl($scope) { + $scope.items = [ + { title:"Dynamic Title 1", content:"Dynamic Item 0" }, + { title:"Dynamic Title 2", content:"Dynamic Item 1", disabled: true } + ]; + + $scope.alertMe = function() { + setTimeout(function() { + alert("You've selected the alert tab!"); + }); + }; + }; + +
      + */ + +/** + * @ngdoc directive + * @name ui.bootstrap.tabs.directive:tabHeading + * @restrict EA + * + * @description + * Creates an HTML heading for a {@link ui.bootstrap.tabs.directive:tab tab}. Must be placed as a child of a tab element. + * + * @example + + + + + HTML in my titles?! + And some content, too! + + + Icon heading?!? + That's right. + + + + + */ +.directive('tab', ['$parse', function($parse) { + return { + require: '^tabset', + restrict: 'EA', + replace: true, + templateUrl: 'template/tabs/tab.html', + transclude: true, + scope: { + active: '=?', + heading: '@', + onSelect: '&select', //This callback is called in contentHeadingTransclude + //once it inserts the tab's content into the dom + onDeselect: '&deselect' + }, + controller: function() { + //Empty controller so other directives can require being 'under' a tab + }, + compile: function(elm, attrs, transclude) { + return function postLink(scope, elm, attrs, tabsetCtrl) { + scope.$watch('active', function(active) { + if (active) { + tabsetCtrl.select(scope); + } + }); + + scope.disabled = false; + if ( attrs.disabled ) { + scope.$parent.$watch($parse(attrs.disabled), function(value) { + scope.disabled = !! value; + }); + } + + scope.select = function() { + if ( !scope.disabled ) { + scope.active = true; + } + }; + + tabsetCtrl.addTab(scope); + scope.$on('$destroy', function() { + tabsetCtrl.removeTab(scope); + }); + + //We need to transclude later, once the content container is ready. + //when this link happens, we're inside a tab heading. + scope.$transcludeFn = transclude; + }; + } + }; +}]) + +.directive('tabHeadingTransclude', [function() { + return { + restrict: 'A', + require: '^tab', + link: function(scope, elm, attrs, tabCtrl) { + scope.$watch('headingElement', function updateHeadingElement(heading) { + if (heading) { + elm.html(''); + elm.append(heading); + } + }); + } + }; +}]) + +.directive('tabContentTransclude', function() { + return { + restrict: 'A', + require: '^tabset', + link: function(scope, elm, attrs) { + var tab = scope.$eval(attrs.tabContentTransclude); + + //Now our tab is ready to be transcluded: both the tab heading area + //and the tab content area are loaded. Transclude 'em both. + tab.$transcludeFn(tab.$parent, function(contents) { + angular.forEach(contents, function(node) { + if (isTabHeading(node)) { + //Let tabHeadingTransclude know. + tab.headingElement = node; + } else { + elm.append(node); + } + }); + }); + } + }; + function isTabHeading(node) { + return node.tagName && ( + node.hasAttribute('tab-heading') || + node.hasAttribute('data-tab-heading') || + node.tagName.toLowerCase() === 'tab-heading' || + node.tagName.toLowerCase() === 'data-tab-heading' + ); + } +}) + +; diff --git a/src/ui/public/angular-bootstrap/tabs/tabset.html b/src/ui/public/angular-bootstrap/tabs/tabset.html new file mode 100755 index 00000000000000..b953a49161925d --- /dev/null +++ b/src/ui/public/angular-bootstrap/tabs/tabset.html @@ -0,0 +1,10 @@ +
      + +
      +
      +
      +
      +
      diff --git a/src/ui/public/angular-bootstrap/timepicker/timepicker.html b/src/ui/public/angular-bootstrap/timepicker/timepicker.html new file mode 100755 index 00000000000000..bef8e27f1992d9 --- /dev/null +++ b/src/ui/public/angular-bootstrap/timepicker/timepicker.html @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + +
       
      + + : + +
       
      diff --git a/src/ui/public/angular-bootstrap/timepicker/timepicker.js b/src/ui/public/angular-bootstrap/timepicker/timepicker.js new file mode 100755 index 00000000000000..91ac86bcee3ff3 --- /dev/null +++ b/src/ui/public/angular-bootstrap/timepicker/timepicker.js @@ -0,0 +1,254 @@ +angular.module('ui.bootstrap.timepicker', []) + +.constant('timepickerConfig', { + hourStep: 1, + minuteStep: 1, + showMeridian: true, + meridians: null, + readonlyInput: false, + mousewheel: true +}) + +.controller('TimepickerController', ['$scope', '$attrs', '$parse', '$log', '$locale', 'timepickerConfig', function($scope, $attrs, $parse, $log, $locale, timepickerConfig) { + var selected = new Date(), + ngModelCtrl = { $setViewValue: angular.noop }, // nullModelCtrl + meridians = angular.isDefined($attrs.meridians) ? $scope.$parent.$eval($attrs.meridians) : timepickerConfig.meridians || $locale.DATETIME_FORMATS.AMPMS; + + this.init = function( ngModelCtrl_, inputs ) { + ngModelCtrl = ngModelCtrl_; + ngModelCtrl.$render = this.render; + + var hoursInputEl = inputs.eq(0), + minutesInputEl = inputs.eq(1); + + var mousewheel = angular.isDefined($attrs.mousewheel) ? $scope.$parent.$eval($attrs.mousewheel) : timepickerConfig.mousewheel; + if ( mousewheel ) { + this.setupMousewheelEvents( hoursInputEl, minutesInputEl ); + } + + $scope.readonlyInput = angular.isDefined($attrs.readonlyInput) ? $scope.$parent.$eval($attrs.readonlyInput) : timepickerConfig.readonlyInput; + this.setupInputEvents( hoursInputEl, minutesInputEl ); + }; + + var hourStep = timepickerConfig.hourStep; + if ($attrs.hourStep) { + $scope.$parent.$watch($parse($attrs.hourStep), function(value) { + hourStep = parseInt(value, 10); + }); + } + + var minuteStep = timepickerConfig.minuteStep; + if ($attrs.minuteStep) { + $scope.$parent.$watch($parse($attrs.minuteStep), function(value) { + minuteStep = parseInt(value, 10); + }); + } + + // 12H / 24H mode + $scope.showMeridian = timepickerConfig.showMeridian; + if ($attrs.showMeridian) { + $scope.$parent.$watch($parse($attrs.showMeridian), function(value) { + $scope.showMeridian = !!value; + + if ( ngModelCtrl.$error.time ) { + // Evaluate from template + var hours = getHoursFromTemplate(), minutes = getMinutesFromTemplate(); + if (angular.isDefined( hours ) && angular.isDefined( minutes )) { + selected.setHours( hours ); + refresh(); + } + } else { + updateTemplate(); + } + }); + } + + // Get $scope.hours in 24H mode if valid + function getHoursFromTemplate ( ) { + var hours = parseInt( $scope.hours, 10 ); + var valid = ( $scope.showMeridian ) ? (hours > 0 && hours < 13) : (hours >= 0 && hours < 24); + if ( !valid ) { + return undefined; + } + + if ( $scope.showMeridian ) { + if ( hours === 12 ) { + hours = 0; + } + if ( $scope.meridian === meridians[1] ) { + hours = hours + 12; + } + } + return hours; + } + + function getMinutesFromTemplate() { + var minutes = parseInt($scope.minutes, 10); + return ( minutes >= 0 && minutes < 60 ) ? minutes : undefined; + } + + function pad( value ) { + return ( angular.isDefined(value) && value.toString().length < 2 ) ? '0' + value : value; + } + + // Respond on mousewheel spin + this.setupMousewheelEvents = function( hoursInputEl, minutesInputEl ) { + var isScrollingUp = function(e) { + if (e.originalEvent) { + e = e.originalEvent; + } + //pick correct delta variable depending on event + var delta = (e.wheelDelta) ? e.wheelDelta : -e.deltaY; + return (e.detail || delta > 0); + }; + + hoursInputEl.bind('mousewheel wheel', function(e) { + $scope.$apply( (isScrollingUp(e)) ? $scope.incrementHours() : $scope.decrementHours() ); + e.preventDefault(); + }); + + minutesInputEl.bind('mousewheel wheel', function(e) { + $scope.$apply( (isScrollingUp(e)) ? $scope.incrementMinutes() : $scope.decrementMinutes() ); + e.preventDefault(); + }); + + }; + + this.setupInputEvents = function( hoursInputEl, minutesInputEl ) { + if ( $scope.readonlyInput ) { + $scope.updateHours = angular.noop; + $scope.updateMinutes = angular.noop; + return; + } + + var invalidate = function(invalidHours, invalidMinutes) { + ngModelCtrl.$setViewValue( null ); + ngModelCtrl.$setValidity('time', false); + if (angular.isDefined(invalidHours)) { + $scope.invalidHours = invalidHours; + } + if (angular.isDefined(invalidMinutes)) { + $scope.invalidMinutes = invalidMinutes; + } + }; + + $scope.updateHours = function() { + var hours = getHoursFromTemplate(); + + if ( angular.isDefined(hours) ) { + selected.setHours( hours ); + refresh( 'h' ); + } else { + invalidate(true); + } + }; + + hoursInputEl.bind('blur', function(e) { + if ( !$scope.invalidHours && $scope.hours < 10) { + $scope.$apply( function() { + $scope.hours = pad( $scope.hours ); + }); + } + }); + + $scope.updateMinutes = function() { + var minutes = getMinutesFromTemplate(); + + if ( angular.isDefined(minutes) ) { + selected.setMinutes( minutes ); + refresh( 'm' ); + } else { + invalidate(undefined, true); + } + }; + + minutesInputEl.bind('blur', function(e) { + if ( !$scope.invalidMinutes && $scope.minutes < 10 ) { + $scope.$apply( function() { + $scope.minutes = pad( $scope.minutes ); + }); + } + }); + + }; + + this.render = function() { + var date = ngModelCtrl.$modelValue ? new Date( ngModelCtrl.$modelValue ) : null; + + if ( isNaN(date) ) { + ngModelCtrl.$setValidity('time', false); + $log.error('Timepicker directive: "ng-model" value must be a Date object, a number of milliseconds since 01.01.1970 or a string representing an RFC2822 or ISO 8601 date.'); + } else { + if ( date ) { + selected = date; + } + makeValid(); + updateTemplate(); + } + }; + + // Call internally when we know that model is valid. + function refresh( keyboardChange ) { + makeValid(); + ngModelCtrl.$setViewValue( new Date(selected) ); + updateTemplate( keyboardChange ); + } + + function makeValid() { + ngModelCtrl.$setValidity('time', true); + $scope.invalidHours = false; + $scope.invalidMinutes = false; + } + + function updateTemplate( keyboardChange ) { + var hours = selected.getHours(), minutes = selected.getMinutes(); + + if ( $scope.showMeridian ) { + hours = ( hours === 0 || hours === 12 ) ? 12 : hours % 12; // Convert 24 to 12 hour system + } + + $scope.hours = keyboardChange === 'h' ? hours : pad(hours); + $scope.minutes = keyboardChange === 'm' ? minutes : pad(minutes); + $scope.meridian = selected.getHours() < 12 ? meridians[0] : meridians[1]; + } + + function addMinutes( minutes ) { + var dt = new Date( selected.getTime() + minutes * 60000 ); + selected.setHours( dt.getHours(), dt.getMinutes() ); + refresh(); + } + + $scope.incrementHours = function() { + addMinutes( hourStep * 60 ); + }; + $scope.decrementHours = function() { + addMinutes( - hourStep * 60 ); + }; + $scope.incrementMinutes = function() { + addMinutes( minuteStep ); + }; + $scope.decrementMinutes = function() { + addMinutes( - minuteStep ); + }; + $scope.toggleMeridian = function() { + addMinutes( 12 * 60 * (( selected.getHours() < 12 ) ? 1 : -1) ); + }; +}]) + +.directive('timepicker', function () { + return { + restrict: 'EA', + require: ['timepicker', '?^ngModel'], + controller:'TimepickerController', + replace: true, + scope: {}, + templateUrl: 'template/timepicker/timepicker.html', + link: function(scope, element, attrs, ctrls) { + var timepickerCtrl = ctrls[0], ngModelCtrl = ctrls[1]; + + if ( ngModelCtrl ) { + timepickerCtrl.init( ngModelCtrl, element.find('input') ); + } + } + }; +}); diff --git a/src/ui/public/angular-bootstrap/tooltip/tooltip-html-unsafe-popup.html b/src/ui/public/angular-bootstrap/tooltip/tooltip-html-unsafe-popup.html new file mode 100755 index 00000000000000..129016d9c14d6f --- /dev/null +++ b/src/ui/public/angular-bootstrap/tooltip/tooltip-html-unsafe-popup.html @@ -0,0 +1,4 @@ +
      +
      +
      +
      diff --git a/src/ui/public/angular-bootstrap/tooltip/tooltip-popup.html b/src/ui/public/angular-bootstrap/tooltip/tooltip-popup.html new file mode 100755 index 00000000000000..fd51120774fd15 --- /dev/null +++ b/src/ui/public/angular-bootstrap/tooltip/tooltip-popup.html @@ -0,0 +1,4 @@ +
      +
      +
      +
      diff --git a/src/ui/public/angular-bootstrap/tooltip/tooltip.js b/src/ui/public/angular-bootstrap/tooltip/tooltip.js new file mode 100755 index 00000000000000..4659e8bdc7c606 --- /dev/null +++ b/src/ui/public/angular-bootstrap/tooltip/tooltip.js @@ -0,0 +1,360 @@ +/** + * The following features are still outstanding: animation as a + * function, placement as a function, inside, support for more triggers than + * just mouse enter/leave, html tooltips, and selector delegation. + */ +angular.module( 'ui.bootstrap.tooltip', [ 'ui.bootstrap.position', 'ui.bootstrap.bindHtml' ] ) + +/** + * The $tooltip service creates tooltip- and popover-like directives as well as + * houses global options for them. + */ +.provider( '$tooltip', function () { + // The default options tooltip and popover. + var defaultOptions = { + placement: 'top', + animation: true, + popupDelay: 0 + }; + + // Default hide triggers for each show trigger + var triggerMap = { + 'mouseenter': 'mouseleave', + 'click': 'click', + 'focus': 'blur' + }; + + // The options specified to the provider globally. + var globalOptions = {}; + + /** + * `options({})` allows global configuration of all tooltips in the + * application. + * + * var app = angular.module( 'App', ['ui.bootstrap.tooltip'], function( $tooltipProvider ) { + * // place tooltips left instead of top by default + * $tooltipProvider.options( { placement: 'left' } ); + * }); + */ + this.options = function( value ) { + angular.extend( globalOptions, value ); + }; + + /** + * This allows you to extend the set of trigger mappings available. E.g.: + * + * $tooltipProvider.setTriggers( 'openTrigger': 'closeTrigger' ); + */ + this.setTriggers = function setTriggers ( triggers ) { + angular.extend( triggerMap, triggers ); + }; + + /** + * This is a helper function for translating camel-case to snake-case. + */ + function snake_case(name){ + var regexp = /[A-Z]/g; + var separator = '-'; + return name.replace(regexp, function(letter, pos) { + return (pos ? separator : '') + letter.toLowerCase(); + }); + } + + /** + * Returns the actual instance of the $tooltip service. + * TODO support multiple triggers + */ + this.$get = [ '$window', '$compile', '$timeout', '$document', '$position', '$interpolate', function ( $window, $compile, $timeout, $document, $position, $interpolate ) { + return function $tooltip ( type, prefix, defaultTriggerShow ) { + var options = angular.extend( {}, defaultOptions, globalOptions ); + + /** + * Returns an object of show and hide triggers. + * + * If a trigger is supplied, + * it is used to show the tooltip; otherwise, it will use the `trigger` + * option passed to the `$tooltipProvider.options` method; else it will + * default to the trigger supplied to this directive factory. + * + * The hide trigger is based on the show trigger. If the `trigger` option + * was passed to the `$tooltipProvider.options` method, it will use the + * mapped trigger from `triggerMap` or the passed trigger if the map is + * undefined; otherwise, it uses the `triggerMap` value of the show + * trigger; else it will just use the show trigger. + */ + function getTriggers ( trigger ) { + var show = trigger || options.trigger || defaultTriggerShow; + var hide = triggerMap[show] || show; + return { + show: show, + hide: hide + }; + } + + var directiveName = snake_case( type ); + + var startSym = $interpolate.startSymbol(); + var endSym = $interpolate.endSymbol(); + var template = + '
      '+ + '
      '; + + return { + restrict: 'EA', + compile: function (tElem, tAttrs) { + var tooltipLinker = $compile( template ); + + return function link ( scope, element, attrs ) { + var tooltip; + var tooltipLinkedScope; + var transitionTimeout; + var popupTimeout; + var appendToBody = angular.isDefined( options.appendToBody ) ? options.appendToBody : false; + var triggers = getTriggers( undefined ); + var hasEnableExp = angular.isDefined(attrs[prefix+'Enable']); + var ttScope = scope.$new(true); + + var positionTooltip = function () { + + var ttPosition = $position.positionElements(element, tooltip, ttScope.placement, appendToBody); + ttPosition.top += 'px'; + ttPosition.left += 'px'; + + // Now set the calculated positioning. + tooltip.css( ttPosition ); + }; + + // By default, the tooltip is not open. + // TODO add ability to start tooltip opened + ttScope.isOpen = false; + + function toggleTooltipBind () { + if ( ! ttScope.isOpen ) { + showTooltipBind(); + } else { + hideTooltipBind(); + } + } + + // Show the tooltip with delay if specified, otherwise show it immediately + function showTooltipBind() { + if(hasEnableExp && !scope.$eval(attrs[prefix+'Enable'])) { + return; + } + + prepareTooltip(); + + if ( ttScope.popupDelay ) { + // Do nothing if the tooltip was already scheduled to pop-up. + // This happens if show is triggered multiple times before any hide is triggered. + if (!popupTimeout) { + popupTimeout = $timeout( show, ttScope.popupDelay, false ); + popupTimeout.then(function(reposition){reposition();}); + } + } else { + show()(); + } + } + + function hideTooltipBind () { + scope.$apply(function () { + hide(); + }); + } + + // Show the tooltip popup element. + function show() { + + popupTimeout = null; + + // If there is a pending remove transition, we must cancel it, lest the + // tooltip be mysteriously removed. + if ( transitionTimeout ) { + $timeout.cancel( transitionTimeout ); + transitionTimeout = null; + } + + // Don't show empty tooltips. + if ( ! ttScope.content ) { + return angular.noop; + } + + createTooltip(); + + // Set the initial positioning. + tooltip.css({ top: 0, left: 0, display: 'block' }); + ttScope.$digest(); + + positionTooltip(); + + // And show the tooltip. + ttScope.isOpen = true; + ttScope.$digest(); // digest required as $apply is not called + + // Return positioning function as promise callback for correct + // positioning after draw. + return positionTooltip; + } + + // Hide the tooltip popup element. + function hide() { + // First things first: we don't show it anymore. + ttScope.isOpen = false; + + //if tooltip is going to be shown after delay, we must cancel this + $timeout.cancel( popupTimeout ); + popupTimeout = null; + + // And now we remove it from the DOM. However, if we have animation, we + // need to wait for it to expire beforehand. + // FIXME: this is a placeholder for a port of the transitions library. + if ( ttScope.animation ) { + if (!transitionTimeout) { + transitionTimeout = $timeout(removeTooltip, 500); + } + } else { + removeTooltip(); + } + } + + function createTooltip() { + // There can only be one tooltip element per directive shown at once. + if (tooltip) { + removeTooltip(); + } + tooltipLinkedScope = ttScope.$new(); + tooltip = tooltipLinker(tooltipLinkedScope, function (tooltip) { + if ( appendToBody ) { + $document.find( 'body' ).append( tooltip ); + } else { + element.after( tooltip ); + } + }); + } + + function removeTooltip() { + transitionTimeout = null; + if (tooltip) { + tooltip.remove(); + tooltip = null; + } + if (tooltipLinkedScope) { + tooltipLinkedScope.$destroy(); + tooltipLinkedScope = null; + } + } + + function prepareTooltip() { + prepPlacement(); + prepPopupDelay(); + } + + /** + * Observe the relevant attributes. + */ + attrs.$observe( type, function ( val ) { + ttScope.content = val; + + if (!val && ttScope.isOpen ) { + hide(); + } + }); + + attrs.$observe( prefix+'Title', function ( val ) { + ttScope.title = val; + }); + + function prepPlacement() { + var val = attrs[ prefix + 'Placement' ]; + ttScope.placement = angular.isDefined( val ) ? val : options.placement; + } + + function prepPopupDelay() { + var val = attrs[ prefix + 'PopupDelay' ]; + var delay = parseInt( val, 10 ); + ttScope.popupDelay = ! isNaN(delay) ? delay : options.popupDelay; + } + + var unregisterTriggers = function () { + element.unbind(triggers.show, showTooltipBind); + element.unbind(triggers.hide, hideTooltipBind); + }; + + function prepTriggers() { + var val = attrs[ prefix + 'Trigger' ]; + unregisterTriggers(); + + triggers = getTriggers( val ); + + if ( triggers.show === triggers.hide ) { + element.bind( triggers.show, toggleTooltipBind ); + } else { + element.bind( triggers.show, showTooltipBind ); + element.bind( triggers.hide, hideTooltipBind ); + } + } + prepTriggers(); + + var animation = scope.$eval(attrs[prefix + 'Animation']); + ttScope.animation = angular.isDefined(animation) ? !!animation : options.animation; + + var appendToBodyVal = scope.$eval(attrs[prefix + 'AppendToBody']); + appendToBody = angular.isDefined(appendToBodyVal) ? appendToBodyVal : appendToBody; + + // if a tooltip is attached to we need to remove it on + // location change as its parent scope will probably not be destroyed + // by the change. + if ( appendToBody ) { + scope.$on('$locationChangeSuccess', function closeTooltipOnLocationChangeSuccess () { + if ( ttScope.isOpen ) { + hide(); + } + }); + } + + // Make sure tooltip is destroyed and removed. + scope.$on('$destroy', function onDestroyTooltip() { + $timeout.cancel( transitionTimeout ); + $timeout.cancel( popupTimeout ); + unregisterTriggers(); + removeTooltip(); + ttScope = null; + }); + }; + } + }; + }; + }]; +}) + +.directive( 'tooltipPopup', function () { + return { + restrict: 'EA', + replace: true, + scope: { content: '@', placement: '@', animation: '&', isOpen: '&' }, + templateUrl: 'template/tooltip/tooltip-popup.html' + }; +}) + +.directive( 'tooltip', [ '$tooltip', function ( $tooltip ) { + return $tooltip( 'tooltip', 'tooltip', 'mouseenter' ); +}]) + +.directive( 'tooltipHtmlUnsafePopup', function () { + return { + restrict: 'EA', + replace: true, + scope: { content: '@', placement: '@', animation: '&', isOpen: '&' }, + templateUrl: 'template/tooltip/tooltip-html-unsafe-popup.html' + }; +}) + +.directive( 'tooltipHtmlUnsafe', [ '$tooltip', function ( $tooltip ) { + return $tooltip( 'tooltipHtmlUnsafe', 'tooltip', 'mouseenter' ); +}]); diff --git a/src/ui/public/angular-bootstrap/transition/transition.js b/src/ui/public/angular-bootstrap/transition/transition.js new file mode 100755 index 00000000000000..8e08838bdb0847 --- /dev/null +++ b/src/ui/public/angular-bootstrap/transition/transition.js @@ -0,0 +1,82 @@ +angular.module('ui.bootstrap.transition', []) + +/** + * $transition service provides a consistent interface to trigger CSS 3 transitions and to be informed when they complete. + * @param {DOMElement} element The DOMElement that will be animated. + * @param {string|object|function} trigger The thing that will cause the transition to start: + * - As a string, it represents the css class to be added to the element. + * - As an object, it represents a hash of style attributes to be applied to the element. + * - As a function, it represents a function to be called that will cause the transition to occur. + * @return {Promise} A promise that is resolved when the transition finishes. + */ +.factory('$transition', ['$q', '$timeout', '$rootScope', function($q, $timeout, $rootScope) { + + var $transition = function(element, trigger, options) { + options = options || {}; + var deferred = $q.defer(); + var endEventName = $transition[options.animation ? 'animationEndEventName' : 'transitionEndEventName']; + + var transitionEndHandler = function(event) { + $rootScope.$apply(function() { + element.unbind(endEventName, transitionEndHandler); + deferred.resolve(element); + }); + }; + + if (endEventName) { + element.bind(endEventName, transitionEndHandler); + } + + // Wrap in a timeout to allow the browser time to update the DOM before the transition is to occur + $timeout(function() { + if ( angular.isString(trigger) ) { + element.addClass(trigger); + } else if ( angular.isFunction(trigger) ) { + trigger(element); + } else if ( angular.isObject(trigger) ) { + element.css(trigger); + } + //If browser does not support transitions, instantly resolve + if ( !endEventName ) { + deferred.resolve(element); + } + }); + + // Add our custom cancel function to the promise that is returned + // We can call this if we are about to run a new transition, which we know will prevent this transition from ending, + // i.e. it will therefore never raise a transitionEnd event for that transition + deferred.promise.cancel = function() { + if ( endEventName ) { + element.unbind(endEventName, transitionEndHandler); + } + deferred.reject('Transition cancelled'); + }; + + return deferred.promise; + }; + + // Work out the name of the transitionEnd event + var transElement = document.createElement('trans'); + var transitionEndEventNames = { + 'WebkitTransition': 'webkitTransitionEnd', + 'MozTransition': 'transitionend', + 'OTransition': 'oTransitionEnd', + 'transition': 'transitionend' + }; + var animationEndEventNames = { + 'WebkitTransition': 'webkitAnimationEnd', + 'MozTransition': 'animationend', + 'OTransition': 'oAnimationEnd', + 'transition': 'animationend' + }; + function findEndEventName(endEventNames) { + for (var name in endEventNames){ + if (transElement.style[name] !== undefined) { + return endEventNames[name]; + } + } + } + $transition.transitionEndEventName = findEndEventName(transitionEndEventNames); + $transition.animationEndEventName = findEndEventName(animationEndEventNames); + return $transition; +}]); diff --git a/src/ui/public/angular-bootstrap/typeahead/typeahead-match.html b/src/ui/public/angular-bootstrap/typeahead/typeahead-match.html new file mode 100755 index 00000000000000..d79e10a18f8a0d --- /dev/null +++ b/src/ui/public/angular-bootstrap/typeahead/typeahead-match.html @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/ui/public/angular-bootstrap/typeahead/typeahead-popup.html b/src/ui/public/angular-bootstrap/typeahead/typeahead-popup.html new file mode 100755 index 00000000000000..e1bd0c1c476d82 --- /dev/null +++ b/src/ui/public/angular-bootstrap/typeahead/typeahead-popup.html @@ -0,0 +1,5 @@ + diff --git a/src/ui/public/angular-bootstrap/typeahead/typeahead.js b/src/ui/public/angular-bootstrap/typeahead/typeahead.js new file mode 100755 index 00000000000000..8b080d615f71b9 --- /dev/null +++ b/src/ui/public/angular-bootstrap/typeahead/typeahead.js @@ -0,0 +1,398 @@ +angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position', 'ui.bootstrap.bindHtml']) + +/** + * A helper service that can parse typeahead's syntax (string provided by users) + * Extracted to a separate service for ease of unit testing + */ + .factory('typeaheadParser', ['$parse', function ($parse) { + + // 00000111000000000000022200000000000000003333333333333330000000000044000 + var TYPEAHEAD_REGEXP = /^\s*([\s\S]+?)(?:\s+as\s+([\s\S]+?))?\s+for\s+(?:([\$\w][\$\w\d]*))\s+in\s+([\s\S]+?)$/; + + return { + parse:function (input) { + + var match = input.match(TYPEAHEAD_REGEXP); + if (!match) { + throw new Error( + 'Expected typeahead specification in form of "_modelValue_ (as _label_)? for _item_ in _collection_"' + + ' but got "' + input + '".'); + } + + return { + itemName:match[3], + source:$parse(match[4]), + viewMapper:$parse(match[2] || match[1]), + modelMapper:$parse(match[1]) + }; + } + }; +}]) + + .directive('typeahead', ['$compile', '$parse', '$q', '$timeout', '$document', '$position', 'typeaheadParser', + function ($compile, $parse, $q, $timeout, $document, $position, typeaheadParser) { + + var HOT_KEYS = [9, 13, 27, 38, 40]; + + return { + require:'ngModel', + link:function (originalScope, element, attrs, modelCtrl) { + + //SUPPORTED ATTRIBUTES (OPTIONS) + + //minimal no of characters that needs to be entered before typeahead kicks-in + var minSearch = originalScope.$eval(attrs.typeaheadMinLength) || 1; + + //minimal wait time after last character typed before typehead kicks-in + var waitTime = originalScope.$eval(attrs.typeaheadWaitMs) || 0; + + //should it restrict model values to the ones selected from the popup only? + var isEditable = originalScope.$eval(attrs.typeaheadEditable) !== false; + + //binding to a variable that indicates if matches are being retrieved asynchronously + var isLoadingSetter = $parse(attrs.typeaheadLoading).assign || angular.noop; + + //a callback executed when a match is selected + var onSelectCallback = $parse(attrs.typeaheadOnSelect); + + var inputFormatter = attrs.typeaheadInputFormatter ? $parse(attrs.typeaheadInputFormatter) : undefined; + + var appendToBody = attrs.typeaheadAppendToBody ? originalScope.$eval(attrs.typeaheadAppendToBody) : false; + + var focusFirst = originalScope.$eval(attrs.typeaheadFocusFirst) !== false; + + //INTERNAL VARIABLES + + //model setter executed upon match selection + var $setModelValue = $parse(attrs.ngModel).assign; + + //expressions used by typeahead + var parserResult = typeaheadParser.parse(attrs.typeahead); + + var hasFocus; + + //create a child scope for the typeahead directive so we are not polluting original scope + //with typeahead-specific data (matches, query etc.) + var scope = originalScope.$new(); + originalScope.$on('$destroy', function(){ + scope.$destroy(); + }); + + // WAI-ARIA + var popupId = 'typeahead-' + scope.$id + '-' + Math.floor(Math.random() * 10000); + element.attr({ + 'aria-autocomplete': 'list', + 'aria-expanded': false, + 'aria-owns': popupId + }); + + //pop-up element used to display matches + var popUpEl = angular.element('
      '); + popUpEl.attr({ + id: popupId, + matches: 'matches', + active: 'activeIdx', + select: 'select(activeIdx)', + query: 'query', + position: 'position' + }); + //custom item template + if (angular.isDefined(attrs.typeaheadTemplateUrl)) { + popUpEl.attr('template-url', attrs.typeaheadTemplateUrl); + } + + var resetMatches = function() { + scope.matches = []; + scope.activeIdx = -1; + element.attr('aria-expanded', false); + }; + + var getMatchId = function(index) { + return popupId + '-option-' + index; + }; + + // Indicate that the specified match is the active (pre-selected) item in the list owned by this typeahead. + // This attribute is added or removed automatically when the `activeIdx` changes. + scope.$watch('activeIdx', function(index) { + if (index < 0) { + element.removeAttr('aria-activedescendant'); + } else { + element.attr('aria-activedescendant', getMatchId(index)); + } + }); + + var getMatchesAsync = function(inputValue) { + + var locals = {$viewValue: inputValue}; + isLoadingSetter(originalScope, true); + $q.when(parserResult.source(originalScope, locals)).then(function(matches) { + + //it might happen that several async queries were in progress if a user were typing fast + //but we are interested only in responses that correspond to the current view value + var onCurrentRequest = (inputValue === modelCtrl.$viewValue); + if (onCurrentRequest && hasFocus) { + if (matches.length > 0) { + + scope.activeIdx = focusFirst ? 0 : -1; + scope.matches.length = 0; + + //transform labels + for(var i=0; i= minSearch) { + if (waitTime > 0) { + cancelPreviousTimeout(); + scheduleSearchWithTimeout(inputValue); + } else { + getMatchesAsync(inputValue); + } + } else { + isLoadingSetter(originalScope, false); + cancelPreviousTimeout(); + resetMatches(); + } + + if (isEditable) { + return inputValue; + } else { + if (!inputValue) { + // Reset in case user had typed something previously. + modelCtrl.$setValidity('editable', true); + return inputValue; + } else { + modelCtrl.$setValidity('editable', false); + return undefined; + } + } + }); + + modelCtrl.$formatters.push(function (modelValue) { + + var candidateViewValue, emptyViewValue; + var locals = {}; + + if (inputFormatter) { + + locals.$model = modelValue; + return inputFormatter(originalScope, locals); + + } else { + + //it might happen that we don't have enough info to properly render input value + //we need to check for this situation and simply return model value if we can't apply custom formatting + locals[parserResult.itemName] = modelValue; + candidateViewValue = parserResult.viewMapper(originalScope, locals); + locals[parserResult.itemName] = undefined; + emptyViewValue = parserResult.viewMapper(originalScope, locals); + + return candidateViewValue!== emptyViewValue ? candidateViewValue : modelValue; + } + }); + + scope.select = function (activeIdx) { + //called from within the $digest() cycle + var locals = {}; + var model, item; + + locals[parserResult.itemName] = item = scope.matches[activeIdx].model; + model = parserResult.modelMapper(originalScope, locals); + $setModelValue(originalScope, model); + modelCtrl.$setValidity('editable', true); + + onSelectCallback(originalScope, { + $item: item, + $model: model, + $label: parserResult.viewMapper(originalScope, locals) + }); + + resetMatches(); + + //return focus to the input element if a match was selected via a mouse click event + // use timeout to avoid $rootScope:inprog error + $timeout(function() { element[0].focus(); }, 0, false); + }; + + //bind keyboard events: arrows up(38) / down(40), enter(13) and tab(9), esc(27) + element.bind('keydown', function (evt) { + + //typeahead is open and an "interesting" key was pressed + if (scope.matches.length === 0 || HOT_KEYS.indexOf(evt.which) === -1) { + return; + } + + // if there's nothing selected (i.e. focusFirst) and enter is hit, don't do anything + if (scope.activeIdx == -1 && (evt.which === 13 || evt.which === 9)) { + return; + } + + evt.preventDefault(); + + if (evt.which === 40) { + scope.activeIdx = (scope.activeIdx + 1) % scope.matches.length; + scope.$digest(); + + } else if (evt.which === 38) { + scope.activeIdx = (scope.activeIdx > 0 ? scope.activeIdx : scope.matches.length) - 1; + scope.$digest(); + + } else if (evt.which === 13 || evt.which === 9) { + scope.$apply(function () { + scope.select(scope.activeIdx); + }); + + } else if (evt.which === 27) { + evt.stopPropagation(); + + resetMatches(); + scope.$digest(); + } + }); + + element.bind('blur', function (evt) { + hasFocus = false; + }); + + // Keep reference to click handler to unbind it. + var dismissClickHandler = function (evt) { + if (element[0] !== evt.target) { + resetMatches(); + scope.$digest(); + } + }; + + $document.bind('click', dismissClickHandler); + + originalScope.$on('$destroy', function(){ + $document.unbind('click', dismissClickHandler); + if (appendToBody) { + $popup.remove(); + } + }); + + var $popup = $compile(popUpEl)(scope); + if (appendToBody) { + $document.find('body').append($popup); + } else { + element.after($popup); + } + } + }; + +}]) + + .directive('typeaheadPopup', function () { + return { + restrict:'EA', + scope:{ + matches:'=', + query:'=', + active:'=', + position:'=', + select:'&' + }, + replace:true, + templateUrl:'template/typeahead/typeahead-popup.html', + link:function (scope, element, attrs) { + + scope.templateUrl = attrs.templateUrl; + + scope.isOpen = function () { + return scope.matches.length > 0; + }; + + scope.isActive = function (matchIdx) { + return scope.active == matchIdx; + }; + + scope.selectActive = function (matchIdx) { + scope.active = matchIdx; + }; + + scope.selectMatch = function (activeIdx) { + scope.select({activeIdx:activeIdx}); + }; + } + }; + }) + + .directive('typeaheadMatch', ['$http', '$templateCache', '$compile', '$parse', function ($http, $templateCache, $compile, $parse) { + return { + restrict:'EA', + scope:{ + index:'=', + match:'=', + query:'=' + }, + link:function (scope, element, attrs) { + var tplUrl = $parse(attrs.templateUrl)(scope.$parent) || 'template/typeahead/typeahead-match.html'; + $http.get(tplUrl, {cache: $templateCache}).success(function(tplContent){ + element.replaceWith($compile(tplContent.trim())(scope)); + }); + } + }; + }]) + + .filter('typeaheadHighlight', function() { + + function escapeRegexp(queryToEscape) { + return queryToEscape.replace(/([.?*+^$[\]\\(){}|-])/g, '\\$1'); + } + + return function(matchItem, query) { + return query ? ('' + matchItem).replace(new RegExp(escapeRegexp(query), 'gi'), '$&') : matchItem; + }; + }); diff --git a/src/ui/public/angular-bootstrap/ui-bootstrap-tpls.js b/src/ui/public/angular-bootstrap/ui-bootstrap-tpls.js deleted file mode 100644 index b0962814757624..00000000000000 --- a/src/ui/public/angular-bootstrap/ui-bootstrap-tpls.js +++ /dev/null @@ -1,4114 +0,0 @@ -/* eslint-disable */ - -/** - * TODO: Write custom components that address our needs to directly and deprecate these Bootstrap components. - */ - -/* - * angular-ui-bootstrap - * http://angular-ui.github.io/bootstrap/ - - * Version: 0.12.1 - 2015-02-20 - * License: MIT - */ -angular.module("ui.bootstrap", [ - "ui.bootstrap.tpls", - "ui.bootstrap.transition", - "ui.bootstrap.collapse", - "ui.bootstrap.alert", - "ui.bootstrap.bindHtml", - "ui.bootstrap.buttons", - "ui.bootstrap.carousel", - "ui.bootstrap.dateparser", - "ui.bootstrap.position", - "ui.bootstrap.datepicker", - "ui.bootstrap.dropdown", - "ui.bootstrap.modal", - "ui.bootstrap.pagination", - "ui.bootstrap.tooltip", - "ui.bootstrap.popover", - "ui.bootstrap.progressbar", - "ui.bootstrap.rating", - "ui.bootstrap.tabs", - "ui.bootstrap.timepicker", - "ui.bootstrap.typeahead" -]); - -angular.module("ui.bootstrap.tpls", [ - "template/alert/alert.html", - "template/carousel/carousel.html", - "template/carousel/slide.html", - "template/datepicker/datepicker.html", - "template/datepicker/day.html", - "template/datepicker/month.html", - "template/datepicker/popup.html", - "template/datepicker/year.html", - "template/modal/backdrop.html", - "template/modal/window.html", - "template/pagination/pager.html", - "template/pagination/pagination.html", - "template/tooltip/tooltip-html-unsafe-popup.html", - "template/tooltip/tooltip-popup.html", - "template/popover/popover.html", - "template/progressbar/bar.html", - "template/progressbar/progress.html", - "template/progressbar/progressbar.html", - "template/rating/rating.html", - "template/tabs/tab.html", - "template/tabs/tabset.html", - "template/timepicker/timepicker.html", - "template/typeahead/typeahead-match.html", - "template/typeahead/typeahead-popup.html" -]); - -angular.module('ui.bootstrap.transition', []) - -/** - * $transition service provides a consistent interface to trigger CSS 3 transitions and to be informed when they complete. - * @param {DOMElement} element The DOMElement that will be animated. - * @param {string|object|function} trigger The thing that will cause the transition to start: - * - As a string, it represents the css class to be added to the element. - * - As an object, it represents a hash of style attributes to be applied to the element. - * - As a function, it represents a function to be called that will cause the transition to occur. - * @return {Promise} A promise that is resolved when the transition finishes. - */ -.factory('$transition', ['$q', '$timeout', '$rootScope', function($q, $timeout, $rootScope) { - - var $transition = function(element, trigger, options) { - options = options || {}; - var deferred = $q.defer(); - var endEventName = $transition[options.animation ? 'animationEndEventName' : 'transitionEndEventName']; - - var transitionEndHandler = function(event) { - $rootScope.$apply(function() { - element.unbind(endEventName, transitionEndHandler); - deferred.resolve(element); - }); - }; - - if (endEventName) { - element.bind(endEventName, transitionEndHandler); - } - - // Wrap in a timeout to allow the browser time to update the DOM before the transition is to occur - $timeout(function() { - if ( angular.isString(trigger) ) { - element.addClass(trigger); - } else if ( angular.isFunction(trigger) ) { - trigger(element); - } else if ( angular.isObject(trigger) ) { - element.css(trigger); - } - //If browser does not support transitions, instantly resolve - if ( !endEventName ) { - deferred.resolve(element); - } - }); - - // Add our custom cancel function to the promise that is returned - // We can call this if we are about to run a new transition, which we know will prevent this transition from ending, - // i.e. it will therefore never raise a transitionEnd event for that transition - deferred.promise.cancel = function() { - if ( endEventName ) { - element.unbind(endEventName, transitionEndHandler); - } - deferred.reject('Transition cancelled'); - }; - - return deferred.promise; - }; - - // Work out the name of the transitionEnd event - var transElement = document.createElement('trans'); - var transitionEndEventNames = { - 'WebkitTransition': 'webkitTransitionEnd', - 'MozTransition': 'transitionend', - 'OTransition': 'oTransitionEnd', - 'transition': 'transitionend' - }; - var animationEndEventNames = { - 'WebkitTransition': 'webkitAnimationEnd', - 'MozTransition': 'animationend', - 'OTransition': 'oAnimationEnd', - 'transition': 'animationend' - }; - function findEndEventName(endEventNames) { - for (var name in endEventNames){ - if (transElement.style[name] !== undefined) { - return endEventNames[name]; - } - } - } - $transition.transitionEndEventName = findEndEventName(transitionEndEventNames); - $transition.animationEndEventName = findEndEventName(animationEndEventNames); - return $transition; -}]); - -angular.module('ui.bootstrap.collapse', ['ui.bootstrap.transition']) - - .directive('collapse', ['$transition', function ($transition) { - - return { - link: function (scope, element, attrs) { - - var initialAnimSkip = true; - var currentTransition; - - function doTransition(change) { - var newTransition = $transition(element, change); - if (currentTransition) { - currentTransition.cancel(); - } - currentTransition = newTransition; - newTransition.then(newTransitionDone, newTransitionDone); - return newTransition; - - function newTransitionDone() { - // Make sure it's this transition, otherwise, leave it alone. - if (currentTransition === newTransition) { - currentTransition = undefined; - } - } - } - - function expand() { - if (initialAnimSkip) { - initialAnimSkip = false; - expandDone(); - } else { - element.removeClass('collapse').addClass('collapsing'); - doTransition({ height: element[0].scrollHeight + 'px' }).then(expandDone); - } - } - - function expandDone() { - element.removeClass('collapsing'); - element.addClass('collapse in'); - element.css({height: 'auto'}); - } - - function collapse() { - if (initialAnimSkip) { - initialAnimSkip = false; - collapseDone(); - element.css({height: 0}); - } else { - // CSS transitions don't work with height: auto, so we have to manually change the height to a specific value - element.css({ height: element[0].scrollHeight + 'px' }); - //trigger reflow so a browser realizes that height was updated from auto to a specific value - var x = element[0].offsetWidth; - - element.removeClass('collapse in').addClass('collapsing'); - - doTransition({ height: 0 }).then(collapseDone); - } - } - - function collapseDone() { - element.removeClass('collapsing'); - element.addClass('collapse'); - } - - scope.$watch(attrs.collapse, function (shouldCollapse) { - if (shouldCollapse) { - collapse(); - } else { - expand(); - } - }); - } - }; - }]); - -angular.module('ui.bootstrap.alert', []) - -.controller('AlertController', ['$scope', '$attrs', function ($scope, $attrs) { - $scope.closeable = 'close' in $attrs; - this.close = $scope.close; -}]) - -.directive('alert', function () { - return { - restrict:'EA', - controller:'AlertController', - templateUrl:'template/alert/alert.html', - transclude:true, - replace:true, - scope: { - type: '@', - close: '&' - } - }; -}) - -.directive('dismissOnTimeout', ['$timeout', function($timeout) { - return { - require: 'alert', - link: function(scope, element, attrs, alertCtrl) { - $timeout(function(){ - alertCtrl.close(); - }, parseInt(attrs.dismissOnTimeout, 10)); - } - }; -}]); - -angular.module('ui.bootstrap.bindHtml', []) - - .directive('bindHtmlUnsafe', function () { - return function (scope, element, attr) { - element.addClass('ng-binding').data('$binding', attr.bindHtmlUnsafe); - scope.$watch(attr.bindHtmlUnsafe, function bindHtmlUnsafeWatchAction(value) { - element.html(value || ''); - }); - }; - }); -angular.module('ui.bootstrap.buttons', []) - -.constant('buttonConfig', { - activeClass: 'active', - toggleEvent: 'click' -}) - -.controller('ButtonsController', ['buttonConfig', function(buttonConfig) { - this.activeClass = buttonConfig.activeClass || 'active'; - this.toggleEvent = buttonConfig.toggleEvent || 'click'; -}]) - -.directive('btnRadio', function () { - return { - require: ['btnRadio', 'ngModel'], - controller: 'ButtonsController', - link: function (scope, element, attrs, ctrls) { - var buttonsCtrl = ctrls[0], ngModelCtrl = ctrls[1]; - - //model -> UI - ngModelCtrl.$render = function () { - element.toggleClass(buttonsCtrl.activeClass, angular.equals(ngModelCtrl.$modelValue, scope.$eval(attrs.btnRadio))); - }; - - //ui->model - element.bind(buttonsCtrl.toggleEvent, function () { - var isActive = element.hasClass(buttonsCtrl.activeClass); - - if (!isActive || angular.isDefined(attrs.uncheckable)) { - scope.$apply(function () { - ngModelCtrl.$setViewValue(isActive ? null : scope.$eval(attrs.btnRadio)); - ngModelCtrl.$render(); - }); - } - }); - } - }; -}) - -.directive('btnCheckbox', function () { - return { - require: ['btnCheckbox', 'ngModel'], - controller: 'ButtonsController', - link: function (scope, element, attrs, ctrls) { - var buttonsCtrl = ctrls[0], ngModelCtrl = ctrls[1]; - - function getTrueValue() { - return getCheckboxValue(attrs.btnCheckboxTrue, true); - } - - function getFalseValue() { - return getCheckboxValue(attrs.btnCheckboxFalse, false); - } - - function getCheckboxValue(attributeValue, defaultValue) { - var val = scope.$eval(attributeValue); - return angular.isDefined(val) ? val : defaultValue; - } - - //model -> UI - ngModelCtrl.$render = function () { - element.toggleClass(buttonsCtrl.activeClass, angular.equals(ngModelCtrl.$modelValue, getTrueValue())); - }; - - //ui->model - element.bind(buttonsCtrl.toggleEvent, function () { - scope.$apply(function () { - ngModelCtrl.$setViewValue(element.hasClass(buttonsCtrl.activeClass) ? getFalseValue() : getTrueValue()); - ngModelCtrl.$render(); - }); - }); - } - }; -}); - -/** -* @ngdoc overview -* @name ui.bootstrap.carousel -* -* @description -* AngularJS version of an image carousel. -* -*/ -angular.module('ui.bootstrap.carousel', ['ui.bootstrap.transition']) -.controller('CarouselController', ['$scope', '$timeout', '$interval', '$transition', function ($scope, $timeout, $interval, $transition) { - var self = this, - slides = self.slides = $scope.slides = [], - currentIndex = -1, - currentInterval, isPlaying; - self.currentSlide = null; - - var destroyed = false; - /* direction: "prev" or "next" */ - self.select = $scope.select = function(nextSlide, direction) { - var nextIndex = slides.indexOf(nextSlide); - //Decide direction if it's not given - if (direction === undefined) { - direction = nextIndex > currentIndex ? 'next' : 'prev'; - } - if (nextSlide && nextSlide !== self.currentSlide) { - if ($scope.$currentTransition) { - $scope.$currentTransition.cancel(); - //Timeout so ng-class in template has time to fix classes for finished slide - $timeout(goNext); - } else { - goNext(); - } - } - function goNext() { - // Scope has been destroyed, stop here. - if (destroyed) { return; } - //If we have a slide to transition from and we have a transition type and we're allowed, go - if (self.currentSlide && angular.isString(direction) && !$scope.noTransition && nextSlide.$element) { - //We shouldn't do class manip in here, but it's the same weird thing bootstrap does. need to fix sometime - nextSlide.$element.addClass(direction); - var reflow = nextSlide.$element[0].offsetWidth; //force reflow - - //Set all other slides to stop doing their stuff for the new transition - angular.forEach(slides, function(slide) { - angular.extend(slide, {direction: '', entering: false, leaving: false, active: false}); - }); - angular.extend(nextSlide, {direction: direction, active: true, entering: true}); - angular.extend(self.currentSlide||{}, {direction: direction, leaving: true}); - - $scope.$currentTransition = $transition(nextSlide.$element, {}); - //We have to create new pointers inside a closure since next & current will change - (function(next,current) { - $scope.$currentTransition.then( - function(){ transitionDone(next, current); }, - function(){ transitionDone(next, current); } - ); - }(nextSlide, self.currentSlide)); - } else { - transitionDone(nextSlide, self.currentSlide); - } - self.currentSlide = nextSlide; - currentIndex = nextIndex; - //every time you change slides, reset the timer - restartTimer(); - } - function transitionDone(next, current) { - angular.extend(next, {direction: '', active: true, leaving: false, entering: false}); - angular.extend(current||{}, {direction: '', active: false, leaving: false, entering: false}); - $scope.$currentTransition = null; - } - }; - $scope.$on('$destroy', function () { - destroyed = true; - }); - - /* Allow outside people to call indexOf on slides array */ - self.indexOfSlide = function(slide) { - return slides.indexOf(slide); - }; - - $scope.next = function() { - var newIndex = (currentIndex + 1) % slides.length; - - //Prevent this user-triggered transition from occurring if there is already one in progress - if (!$scope.$currentTransition) { - return self.select(slides[newIndex], 'next'); - } - }; - - $scope.prev = function() { - var newIndex = currentIndex - 1 < 0 ? slides.length - 1 : currentIndex - 1; - - //Prevent this user-triggered transition from occurring if there is already one in progress - if (!$scope.$currentTransition) { - return self.select(slides[newIndex], 'prev'); - } - }; - - $scope.isActive = function(slide) { - return self.currentSlide === slide; - }; - - $scope.$watch('interval', restartTimer); - $scope.$on('$destroy', resetTimer); - - function restartTimer() { - resetTimer(); - var interval = +$scope.interval; - if (!isNaN(interval) && interval > 0) { - currentInterval = $interval(timerFn, interval); - } - } - - function resetTimer() { - if (currentInterval) { - $interval.cancel(currentInterval); - currentInterval = null; - } - } - - function timerFn() { - var interval = +$scope.interval; - if (isPlaying && !isNaN(interval) && interval > 0) { - $scope.next(); - } else { - $scope.pause(); - } - } - - $scope.play = function() { - if (!isPlaying) { - isPlaying = true; - restartTimer(); - } - }; - $scope.pause = function() { - if (!$scope.noPause) { - isPlaying = false; - resetTimer(); - } - }; - - self.addSlide = function(slide, element) { - slide.$element = element; - slides.push(slide); - //if this is the first slide or the slide is set to active, select it - if(slides.length === 1 || slide.active) { - self.select(slides[slides.length-1]); - if (slides.length == 1) { - $scope.play(); - } - } else { - slide.active = false; - } - }; - - self.removeSlide = function(slide) { - //get the index of the slide inside the carousel - var index = slides.indexOf(slide); - slides.splice(index, 1); - if (slides.length > 0 && slide.active) { - if (index >= slides.length) { - self.select(slides[index-1]); - } else { - self.select(slides[index]); - } - } else if (currentIndex > index) { - currentIndex--; - } - }; - -}]) - -/** - * @ngdoc directive - * @name ui.bootstrap.carousel.directive:carousel - * @restrict EA - * - * @description - * Carousel is the outer container for a set of image 'slides' to showcase. - * - * @param {number=} interval The time, in milliseconds, that it will take the carousel to go to the next slide. - * @param {boolean=} noTransition Whether to disable transitions on the carousel. - * @param {boolean=} noPause Whether to disable pausing on the carousel (by default, the carousel interval pauses on hover). - * - * @example - - - - - - - - - - - - - - - .carousel-indicators { - top: auto; - bottom: 15px; - } - - - */ -.directive('carousel', [function() { - return { - restrict: 'EA', - transclude: true, - replace: true, - controller: 'CarouselController', - require: 'carousel', - templateUrl: 'template/carousel/carousel.html', - scope: { - interval: '=', - noTransition: '=', - noPause: '=' - } - }; -}]) - -/** - * @ngdoc directive - * @name ui.bootstrap.carousel.directive:slide - * @restrict EA - * - * @description - * Creates a slide inside a {@link ui.bootstrap.carousel.directive:carousel carousel}. Must be placed as a child of a carousel element. - * - * @param {boolean=} active Model binding, whether or not this slide is currently active. - * - * @example - - -
      - - - - - - - Interval, in milliseconds: -
      Enter a negative number to stop the interval. -
      -
      - -function CarouselDemoCtrl($scope) { - $scope.myInterval = 5000; -} - - - .carousel-indicators { - top: auto; - bottom: 15px; - } - -
      -*/ - -.directive('slide', function() { - return { - require: '^carousel', - restrict: 'EA', - transclude: true, - replace: true, - templateUrl: 'template/carousel/slide.html', - scope: { - active: '=?' - }, - link: function (scope, element, attrs, carouselCtrl) { - carouselCtrl.addSlide(scope, element); - //when the scope is destroyed then remove the slide from the current slides array - scope.$on('$destroy', function() { - carouselCtrl.removeSlide(scope); - }); - - scope.$watch('active', function(active) { - if (active) { - carouselCtrl.select(scope); - } - }); - } - }; -}); - -angular.module('ui.bootstrap.dateparser', []) - -.service('dateParser', ['$locale', 'orderByFilter', function($locale, orderByFilter) { - - this.parsers = {}; - - var formatCodeToRegex = { - 'yyyy': { - regex: '\\d{4}', - apply: function(value) { this.year = +value; } - }, - 'yy': { - regex: '\\d{2}', - apply: function(value) { this.year = +value + 2000; } - }, - 'y': { - regex: '\\d{1,4}', - apply: function(value) { this.year = +value; } - }, - 'MMMM': { - regex: $locale.DATETIME_FORMATS.MONTH.join('|'), - apply: function(value) { this.month = $locale.DATETIME_FORMATS.MONTH.indexOf(value); } - }, - 'MMM': { - regex: $locale.DATETIME_FORMATS.SHORTMONTH.join('|'), - apply: function(value) { this.month = $locale.DATETIME_FORMATS.SHORTMONTH.indexOf(value); } - }, - 'MM': { - regex: '0[1-9]|1[0-2]', - apply: function(value) { this.month = value - 1; } - }, - 'M': { - regex: '[1-9]|1[0-2]', - apply: function(value) { this.month = value - 1; } - }, - 'dd': { - regex: '[0-2][0-9]{1}|3[0-1]{1}', - apply: function(value) { this.date = +value; } - }, - 'd': { - regex: '[1-2]?[0-9]{1}|3[0-1]{1}', - apply: function(value) { this.date = +value; } - }, - 'EEEE': { - regex: $locale.DATETIME_FORMATS.DAY.join('|') - }, - 'EEE': { - regex: $locale.DATETIME_FORMATS.SHORTDAY.join('|') - } - }; - - function createParser(format) { - var map = [], regex = format.split(''); - - angular.forEach(formatCodeToRegex, function(data, code) { - var index = format.indexOf(code); - - if (index > -1) { - format = format.split(''); - - regex[index] = '(' + data.regex + ')'; - format[index] = '$'; // Custom symbol to define consumed part of format - for (var i = index + 1, n = index + code.length; i < n; i++) { - regex[i] = ''; - format[i] = '$'; - } - format = format.join(''); - - map.push({ index: index, apply: data.apply }); - } - }); - - return { - regex: new RegExp('^' + regex.join('') + '$'), - map: orderByFilter(map, 'index') - }; - } - - this.parse = function(input, format) { - if ( !angular.isString(input) || !format ) { - return input; - } - - format = $locale.DATETIME_FORMATS[format] || format; - - if ( !this.parsers[format] ) { - this.parsers[format] = createParser(format); - } - - var parser = this.parsers[format], - regex = parser.regex, - map = parser.map, - results = input.match(regex); - - if ( results && results.length ) { - var fields = { year: 1900, month: 0, date: 1, hours: 0 }, dt; - - for( var i = 1, n = results.length; i < n; i++ ) { - var mapper = map[i-1]; - if ( mapper.apply ) { - mapper.apply.call(fields, results[i]); - } - } - - if ( isValid(fields.year, fields.month, fields.date) ) { - dt = new Date( fields.year, fields.month, fields.date, fields.hours); - } - - return dt; - } - }; - - // Check if date is valid for specific month (and year for February). - // Month: 0 = Jan, 1 = Feb, etc - function isValid(year, month, date) { - if ( month === 1 && date > 28) { - return date === 29 && ((year % 4 === 0 && year % 100 !== 0) || year % 400 === 0); - } - - if ( month === 3 || month === 5 || month === 8 || month === 10) { - return date < 31; - } - - return true; - } -}]); - -angular.module('ui.bootstrap.position', []) - -/** - * A set of utility methods that can be use to retrieve position of DOM elements. - * It is meant to be used where we need to absolute-position DOM elements in - * relation to other, existing elements (this is the case for tooltips, popovers, - * typeahead suggestions etc.). - */ - .factory('$position', ['$document', '$window', function ($document, $window) { - - function getStyle(el, cssprop) { - if (el.currentStyle) { //IE - return el.currentStyle[cssprop]; - } else if ($window.getComputedStyle) { - return $window.getComputedStyle(el)[cssprop]; - } - // finally try and get inline style - return el.style[cssprop]; - } - - /** - * Checks if a given element is statically positioned - * @param element - raw DOM element - */ - function isStaticPositioned(element) { - return (getStyle(element, 'position') || 'static' ) === 'static'; - } - - /** - * returns the closest, non-statically positioned parentOffset of a given element - * @param element - */ - var parentOffsetEl = function (element) { - var docDomEl = $document[0]; - var offsetParent = element.offsetParent || docDomEl; - while (offsetParent && offsetParent !== docDomEl && isStaticPositioned(offsetParent) ) { - offsetParent = offsetParent.offsetParent; - } - return offsetParent || docDomEl; - }; - - return { - /** - * Provides read-only equivalent of jQuery's position function: - * http://api.jquery.com/position/ - */ - position: function (element) { - var elBCR = this.offset(element); - var offsetParentBCR = { top: 0, left: 0 }; - var offsetParentEl = parentOffsetEl(element[0]); - if (offsetParentEl != $document[0]) { - offsetParentBCR = this.offset(angular.element(offsetParentEl)); - offsetParentBCR.top += offsetParentEl.clientTop - offsetParentEl.scrollTop; - offsetParentBCR.left += offsetParentEl.clientLeft - offsetParentEl.scrollLeft; - } - - var boundingClientRect = element[0].getBoundingClientRect(); - return { - width: boundingClientRect.width || element.prop('offsetWidth'), - height: boundingClientRect.height || element.prop('offsetHeight'), - top: elBCR.top - offsetParentBCR.top, - left: elBCR.left - offsetParentBCR.left - }; - }, - - /** - * Provides read-only equivalent of jQuery's offset function: - * http://api.jquery.com/offset/ - */ - offset: function (element) { - var boundingClientRect = element[0].getBoundingClientRect(); - return { - width: boundingClientRect.width || element.prop('offsetWidth'), - height: boundingClientRect.height || element.prop('offsetHeight'), - top: boundingClientRect.top + ($window.pageYOffset || $document[0].documentElement.scrollTop), - left: boundingClientRect.left + ($window.pageXOffset || $document[0].documentElement.scrollLeft) - }; - }, - - /** - * Provides coordinates for the targetEl in relation to hostEl - */ - positionElements: function (hostEl, targetEl, positionStr, appendToBody) { - - var positionStrParts = positionStr.split('-'); - var pos0 = positionStrParts[0], pos1 = positionStrParts[1] || 'center'; - - var hostElPos, - targetElWidth, - targetElHeight, - targetElPos; - - hostElPos = appendToBody ? this.offset(hostEl) : this.position(hostEl); - - targetElWidth = targetEl.prop('offsetWidth'); - targetElHeight = targetEl.prop('offsetHeight'); - - var shiftWidth = { - center: function () { - return hostElPos.left + hostElPos.width / 2 - targetElWidth / 2; - }, - left: function () { - return hostElPos.left; - }, - right: function () { - return hostElPos.left + hostElPos.width; - } - }; - - var shiftHeight = { - center: function () { - return hostElPos.top + hostElPos.height / 2 - targetElHeight / 2; - }, - top: function () { - return hostElPos.top; - }, - bottom: function () { - return hostElPos.top + hostElPos.height; - } - }; - - switch (pos0) { - case 'right': - targetElPos = { - top: shiftHeight[pos1](), - left: shiftWidth[pos0]() - }; - break; - case 'left': - targetElPos = { - top: shiftHeight[pos1](), - left: hostElPos.left - targetElWidth - }; - break; - case 'bottom': - targetElPos = { - top: shiftHeight[pos0](), - left: shiftWidth[pos1]() - }; - break; - default: - targetElPos = { - top: hostElPos.top - targetElHeight, - left: shiftWidth[pos1]() - }; - break; - } - - return targetElPos; - } - }; - }]); - -angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.dateparser', 'ui.bootstrap.position']) - -.constant('datepickerConfig', { - formatDay: 'dd', - formatMonth: 'MMMM', - formatYear: 'yyyy', - formatDayHeader: 'EEE', - formatDayTitle: 'MMMM yyyy', - formatMonthTitle: 'yyyy', - datepickerMode: 'day', - minMode: 'day', - maxMode: 'year', - showWeeks: true, - startingDay: 0, - yearRange: 20, - minDate: null, - maxDate: null -}) - -.controller('DatepickerController', ['$scope', '$attrs', '$parse', '$interpolate', '$timeout', '$log', 'dateFilter', 'datepickerConfig', function($scope, $attrs, $parse, $interpolate, $timeout, $log, dateFilter, datepickerConfig) { - var self = this, - ngModelCtrl = { $setViewValue: angular.noop }; // nullModelCtrl; - - // Modes chain - this.modes = ['day', 'month', 'year']; - - // Configuration attributes - angular.forEach(['formatDay', 'formatMonth', 'formatYear', 'formatDayHeader', 'formatDayTitle', 'formatMonthTitle', - 'minMode', 'maxMode', 'showWeeks', 'startingDay', 'yearRange'], function( key, index ) { - self[key] = angular.isDefined($attrs[key]) ? (index < 8 ? $interpolate($attrs[key])($scope.$parent) : $scope.$parent.$eval($attrs[key])) : datepickerConfig[key]; - }); - - // Watchable date attributes - angular.forEach(['minDate', 'maxDate'], function( key ) { - if ( $attrs[key] ) { - $scope.$parent.$watch($parse($attrs[key]), function(value) { - self[key] = value ? new Date(value) : null; - self.refreshView(); - }); - } else { - self[key] = datepickerConfig[key] ? new Date(datepickerConfig[key]) : null; - } - }); - - $scope.datepickerMode = $scope.datepickerMode || datepickerConfig.datepickerMode; - $scope.uniqueId = 'datepicker-' + $scope.$id + '-' + Math.floor(Math.random() * 10000); - this.activeDate = angular.isDefined($attrs.initDate) ? $scope.$parent.$eval($attrs.initDate) : new Date(); - - $scope.isActive = function(dateObject) { - if (self.compare(dateObject.date, self.activeDate) === 0) { - $scope.activeDateId = dateObject.uid; - return true; - } - return false; - }; - - this.init = function( ngModelCtrl_ ) { - ngModelCtrl = ngModelCtrl_; - - ngModelCtrl.$render = function() { - self.render(); - }; - }; - - this.render = function() { - if ( ngModelCtrl.$modelValue ) { - var date = new Date( ngModelCtrl.$modelValue ), - isValid = !isNaN(date); - - if ( isValid ) { - this.activeDate = date; - } else { - $log.error('Datepicker directive: "ng-model" value must be a Date object, a number of milliseconds since 01.01.1970 or a string representing an RFC2822 or ISO 8601 date.'); - } - ngModelCtrl.$setValidity('date', isValid); - } - this.refreshView(); - }; - - this.refreshView = function() { - if ( this.element ) { - this._refreshView(); - - var date = ngModelCtrl.$modelValue ? new Date(ngModelCtrl.$modelValue) : null; - ngModelCtrl.$setValidity('date-disabled', !date || (this.element && !this.isDisabled(date))); - } - }; - - this.createDateObject = function(date, format) { - var model = ngModelCtrl.$modelValue ? new Date(ngModelCtrl.$modelValue) : null; - return { - date: date, - label: dateFilter(date, format), - selected: model && this.compare(date, model) === 0, - disabled: this.isDisabled(date), - current: this.compare(date, new Date()) === 0 - }; - }; - - this.isDisabled = function( date ) { - return ((this.minDate && this.compare(date, this.minDate) < 0) || (this.maxDate && this.compare(date, this.maxDate) > 0) || ($attrs.dateDisabled && $scope.dateDisabled({date: date, mode: $scope.datepickerMode}))); - }; - - // Split array into smaller arrays - this.split = function(arr, size) { - var arrays = []; - while (arr.length > 0) { - arrays.push(arr.splice(0, size)); - } - return arrays; - }; - - $scope.select = function( date ) { - if ( $scope.datepickerMode === self.minMode ) { - var dt = ngModelCtrl.$modelValue ? new Date( ngModelCtrl.$modelValue ) : new Date(0, 0, 0, 0, 0, 0, 0); - dt.setFullYear( date.getFullYear(), date.getMonth(), date.getDate() ); - ngModelCtrl.$setViewValue( dt ); - ngModelCtrl.$render(); - } else { - self.activeDate = date; - $scope.datepickerMode = self.modes[ self.modes.indexOf( $scope.datepickerMode ) - 1 ]; - } - }; - - $scope.move = function( direction ) { - var year = self.activeDate.getFullYear() + direction * (self.step.years || 0), - month = self.activeDate.getMonth() + direction * (self.step.months || 0); - self.activeDate.setFullYear(year, month, 1); - self.refreshView(); - }; - - $scope.toggleMode = function( direction ) { - direction = direction || 1; - - if (($scope.datepickerMode === self.maxMode && direction === 1) || ($scope.datepickerMode === self.minMode && direction === -1)) { - return; - } - - $scope.datepickerMode = self.modes[ self.modes.indexOf( $scope.datepickerMode ) + direction ]; - }; - - // Key event mapper - $scope.keys = { 13:'enter', 32:'space', 33:'pageup', 34:'pagedown', 35:'end', 36:'home', 37:'left', 38:'up', 39:'right', 40:'down' }; - - var focusElement = function() { - $timeout(function() { - self.element[0].focus(); - }, 0 , false); - }; - - // Listen for focus requests from popup directive - $scope.$on('datepicker.focus', focusElement); - - $scope.keydown = function( evt ) { - var key = $scope.keys[evt.which]; - - if ( !key || evt.shiftKey || evt.altKey ) { - return; - } - - evt.preventDefault(); - evt.stopPropagation(); - - if (key === 'enter' || key === 'space') { - if ( self.isDisabled(self.activeDate)) { - return; // do nothing - } - $scope.select(self.activeDate); - focusElement(); - } else if (evt.ctrlKey && (key === 'up' || key === 'down')) { - $scope.toggleMode(key === 'up' ? 1 : -1); - focusElement(); - } else { - self.handleKeyDown(key, evt); - self.refreshView(); - } - }; -}]) - -.directive( 'datepicker', function () { - return { - restrict: 'EA', - replace: true, - templateUrl: 'template/datepicker/datepicker.html', - scope: { - datepickerMode: '=?', - dateDisabled: '&' - }, - require: ['datepicker', '?^ngModel'], - controller: 'DatepickerController', - link: function(scope, element, attrs, ctrls) { - var datepickerCtrl = ctrls[0], ngModelCtrl = ctrls[1]; - - if ( ngModelCtrl ) { - datepickerCtrl.init( ngModelCtrl ); - } - } - }; -}) - -.directive('daypicker', ['dateFilter', function (dateFilter) { - return { - restrict: 'EA', - replace: true, - templateUrl: 'template/datepicker/day.html', - require: '^datepicker', - link: function(scope, element, attrs, ctrl) { - scope.showWeeks = ctrl.showWeeks; - - ctrl.step = { months: 1 }; - ctrl.element = element; - - var DAYS_IN_MONTH = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]; - function getDaysInMonth( year, month ) { - return ((month === 1) && (year % 4 === 0) && ((year % 100 !== 0) || (year % 400 === 0))) ? 29 : DAYS_IN_MONTH[month]; - } - - function getDates(startDate, n) { - var dates = new Array(n), current = new Date(startDate), i = 0; - current.setHours(12); // Prevent repeated dates because of timezone bug - while ( i < n ) { - dates[i++] = new Date(current); - current.setDate( current.getDate() + 1 ); - } - return dates; - } - - ctrl._refreshView = function() { - var year = ctrl.activeDate.getFullYear(), - month = ctrl.activeDate.getMonth(), - firstDayOfMonth = new Date(year, month, 1), - difference = ctrl.startingDay - firstDayOfMonth.getDay(), - numDisplayedFromPreviousMonth = (difference > 0) ? 7 - difference : - difference, - firstDate = new Date(firstDayOfMonth); - - if ( numDisplayedFromPreviousMonth > 0 ) { - firstDate.setDate( - numDisplayedFromPreviousMonth + 1 ); - } - - // 42 is the number of days on a six-month calendar - var days = getDates(firstDate, 42); - for (var i = 0; i < 42; i ++) { - days[i] = angular.extend(ctrl.createDateObject(days[i], ctrl.formatDay), { - secondary: days[i].getMonth() !== month, - uid: scope.uniqueId + '-' + i - }); - } - - scope.labels = new Array(7); - for (var j = 0; j < 7; j++) { - scope.labels[j] = { - abbr: dateFilter(days[j].date, ctrl.formatDayHeader), - full: dateFilter(days[j].date, 'EEEE') - }; - } - - scope.title = dateFilter(ctrl.activeDate, ctrl.formatDayTitle); - scope.rows = ctrl.split(days, 7); - - if ( scope.showWeeks ) { - scope.weekNumbers = []; - var weekNumber = getISO8601WeekNumber( scope.rows[0][0].date ), - numWeeks = scope.rows.length; - while( scope.weekNumbers.push(weekNumber++) < numWeeks ) {} - } - }; - - ctrl.compare = function(date1, date2) { - return (new Date( date1.getFullYear(), date1.getMonth(), date1.getDate() ) - new Date( date2.getFullYear(), date2.getMonth(), date2.getDate() ) ); - }; - - function getISO8601WeekNumber(date) { - var checkDate = new Date(date); - checkDate.setDate(checkDate.getDate() + 4 - (checkDate.getDay() || 7)); // Thursday - var time = checkDate.getTime(); - checkDate.setMonth(0); // Compare with Jan 1 - checkDate.setDate(1); - return Math.floor(Math.round((time - checkDate) / 86400000) / 7) + 1; - } - - ctrl.handleKeyDown = function( key, evt ) { - var date = ctrl.activeDate.getDate(); - - if (key === 'left') { - date = date - 1; // up - } else if (key === 'up') { - date = date - 7; // down - } else if (key === 'right') { - date = date + 1; // down - } else if (key === 'down') { - date = date + 7; - } else if (key === 'pageup' || key === 'pagedown') { - var month = ctrl.activeDate.getMonth() + (key === 'pageup' ? - 1 : 1); - ctrl.activeDate.setMonth(month, 1); - date = Math.min(getDaysInMonth(ctrl.activeDate.getFullYear(), ctrl.activeDate.getMonth()), date); - } else if (key === 'home') { - date = 1; - } else if (key === 'end') { - date = getDaysInMonth(ctrl.activeDate.getFullYear(), ctrl.activeDate.getMonth()); - } - ctrl.activeDate.setDate(date); - }; - - ctrl.refreshView(); - } - }; -}]) - -.directive('monthpicker', ['dateFilter', function (dateFilter) { - return { - restrict: 'EA', - replace: true, - templateUrl: 'template/datepicker/month.html', - require: '^datepicker', - link: function(scope, element, attrs, ctrl) { - ctrl.step = { years: 1 }; - ctrl.element = element; - - ctrl._refreshView = function() { - var months = new Array(12), - year = ctrl.activeDate.getFullYear(); - - for ( var i = 0; i < 12; i++ ) { - months[i] = angular.extend(ctrl.createDateObject(new Date(year, i, 1), ctrl.formatMonth), { - uid: scope.uniqueId + '-' + i - }); - } - - scope.title = dateFilter(ctrl.activeDate, ctrl.formatMonthTitle); - scope.rows = ctrl.split(months, 3); - }; - - ctrl.compare = function(date1, date2) { - return new Date( date1.getFullYear(), date1.getMonth() ) - new Date( date2.getFullYear(), date2.getMonth() ); - }; - - ctrl.handleKeyDown = function( key, evt ) { - var date = ctrl.activeDate.getMonth(); - - if (key === 'left') { - date = date - 1; // up - } else if (key === 'up') { - date = date - 3; // down - } else if (key === 'right') { - date = date + 1; // down - } else if (key === 'down') { - date = date + 3; - } else if (key === 'pageup' || key === 'pagedown') { - var year = ctrl.activeDate.getFullYear() + (key === 'pageup' ? - 1 : 1); - ctrl.activeDate.setFullYear(year); - } else if (key === 'home') { - date = 0; - } else if (key === 'end') { - date = 11; - } - ctrl.activeDate.setMonth(date); - }; - - ctrl.refreshView(); - } - }; -}]) - -.directive('yearpicker', ['dateFilter', function (dateFilter) { - return { - restrict: 'EA', - replace: true, - templateUrl: 'template/datepicker/year.html', - require: '^datepicker', - link: function(scope, element, attrs, ctrl) { - var range = ctrl.yearRange; - - ctrl.step = { years: range }; - ctrl.element = element; - - function getStartingYear( year ) { - return parseInt((year - 1) / range, 10) * range + 1; - } - - ctrl._refreshView = function() { - var years = new Array(range); - - for ( var i = 0, start = getStartingYear(ctrl.activeDate.getFullYear()); i < range; i++ ) { - years[i] = angular.extend(ctrl.createDateObject(new Date(start + i, 0, 1), ctrl.formatYear), { - uid: scope.uniqueId + '-' + i - }); - } - - scope.title = [years[0].label, years[range - 1].label].join(' - '); - scope.rows = ctrl.split(years, 5); - }; - - ctrl.compare = function(date1, date2) { - return date1.getFullYear() - date2.getFullYear(); - }; - - ctrl.handleKeyDown = function( key, evt ) { - var date = ctrl.activeDate.getFullYear(); - - if (key === 'left') { - date = date - 1; // up - } else if (key === 'up') { - date = date - 5; // down - } else if (key === 'right') { - date = date + 1; // down - } else if (key === 'down') { - date = date + 5; - } else if (key === 'pageup' || key === 'pagedown') { - date += (key === 'pageup' ? - 1 : 1) * ctrl.step.years; - } else if (key === 'home') { - date = getStartingYear( ctrl.activeDate.getFullYear() ); - } else if (key === 'end') { - date = getStartingYear( ctrl.activeDate.getFullYear() ) + range - 1; - } - ctrl.activeDate.setFullYear(date); - }; - - ctrl.refreshView(); - } - }; -}]) - -.constant('datepickerPopupConfig', { - datepickerPopup: 'yyyy-MM-dd', - currentText: 'Today', - clearText: 'Clear', - closeText: 'Done', - closeOnDateSelection: true, - appendToBody: false, - showButtonBar: true -}) - -.directive('datepickerPopup', ['$compile', '$parse', '$document', '$position', 'dateFilter', 'dateParser', 'datepickerPopupConfig', -function ($compile, $parse, $document, $position, dateFilter, dateParser, datepickerPopupConfig) { - return { - restrict: 'EA', - require: 'ngModel', - scope: { - isOpen: '=?', - currentText: '@', - clearText: '@', - closeText: '@', - dateDisabled: '&' - }, - link: function(scope, element, attrs, ngModel) { - var dateFormat, - closeOnDateSelection = angular.isDefined(attrs.closeOnDateSelection) ? scope.$parent.$eval(attrs.closeOnDateSelection) : datepickerPopupConfig.closeOnDateSelection, - appendToBody = angular.isDefined(attrs.datepickerAppendToBody) ? scope.$parent.$eval(attrs.datepickerAppendToBody) : datepickerPopupConfig.appendToBody; - - scope.showButtonBar = angular.isDefined(attrs.showButtonBar) ? scope.$parent.$eval(attrs.showButtonBar) : datepickerPopupConfig.showButtonBar; - - scope.getText = function( key ) { - return scope[key + 'Text'] || datepickerPopupConfig[key + 'Text']; - }; - - attrs.$observe('datepickerPopup', function(value) { - dateFormat = value || datepickerPopupConfig.datepickerPopup; - ngModel.$render(); - }); - - // popup element used to display calendar - var popupEl = angular.element('
      '); - popupEl.attr({ - 'ng-model': 'date', - 'ng-change': 'dateSelection()' - }); - - function cameltoDash( string ){ - return string.replace(/([A-Z])/g, function($1) { return '-' + $1.toLowerCase(); }); - } - - // datepicker element - var datepickerEl = angular.element(popupEl.children()[0]); - if ( attrs.datepickerOptions ) { - angular.forEach(scope.$parent.$eval(attrs.datepickerOptions), function( value, option ) { - datepickerEl.attr( cameltoDash(option), value ); - }); - } - - scope.watchData = {}; - angular.forEach(['minDate', 'maxDate', 'datepickerMode'], function( key ) { - if ( attrs[key] ) { - var getAttribute = $parse(attrs[key]); - scope.$parent.$watch(getAttribute, function(value){ - scope.watchData[key] = value; - }); - datepickerEl.attr(cameltoDash(key), 'watchData.' + key); - - // Propagate changes from datepicker to outside - if ( key === 'datepickerMode' ) { - var setAttribute = getAttribute.assign; - scope.$watch('watchData.' + key, function(value, oldvalue) { - if ( value !== oldvalue ) { - setAttribute(scope.$parent, value); - } - }); - } - } - }); - if (attrs.dateDisabled) { - datepickerEl.attr('date-disabled', 'dateDisabled({ date: date, mode: mode })'); - } - - function parseDate(viewValue) { - if (!viewValue) { - ngModel.$setValidity('date', true); - return null; - } else if (angular.isDate(viewValue) && !isNaN(viewValue)) { - ngModel.$setValidity('date', true); - return viewValue; - } else if (angular.isString(viewValue)) { - var date = dateParser.parse(viewValue, dateFormat) || new Date(viewValue); - if (isNaN(date)) { - ngModel.$setValidity('date', false); - return undefined; - } else { - ngModel.$setValidity('date', true); - return date; - } - } else { - ngModel.$setValidity('date', false); - return undefined; - } - } - ngModel.$parsers.unshift(parseDate); - - // Inner change - scope.dateSelection = function(dt) { - if (angular.isDefined(dt)) { - scope.date = dt; - } - ngModel.$setViewValue(scope.date); - ngModel.$render(); - - if ( closeOnDateSelection ) { - scope.isOpen = false; - element[0].focus(); - } - }; - - element.bind('input change keyup', function() { - scope.$apply(function() { - scope.date = ngModel.$modelValue; - }); - }); - - // Outter change - ngModel.$render = function() { - var date = ngModel.$viewValue ? dateFilter(ngModel.$viewValue, dateFormat) : ''; - element.val(date); - scope.date = parseDate( ngModel.$modelValue ); - }; - - var documentClickBind = function(event) { - if (scope.isOpen && event.target !== element[0]) { - scope.$apply(function() { - scope.isOpen = false; - }); - } - }; - - var keydown = function(evt, noApply) { - scope.keydown(evt); - }; - element.bind('keydown', keydown); - - scope.keydown = function(evt) { - if (evt.which === 27) { - evt.preventDefault(); - evt.stopPropagation(); - scope.close(); - } else if (evt.which === 40 && !scope.isOpen) { - scope.isOpen = true; - } - }; - - scope.$watch('isOpen', function(value) { - if (value) { - scope.$broadcast('datepicker.focus'); - scope.position = appendToBody ? $position.offset(element) : $position.position(element); - scope.position.top = scope.position.top + element.prop('offsetHeight'); - - $document.bind('click', documentClickBind); - } else { - $document.unbind('click', documentClickBind); - } - }); - - scope.select = function( date ) { - if (date === 'today') { - var today = new Date(); - if (angular.isDate(ngModel.$modelValue)) { - date = new Date(ngModel.$modelValue); - date.setFullYear(today.getFullYear(), today.getMonth(), today.getDate()); - } else { - date = new Date(today.setHours(0, 0, 0, 0)); - } - } - scope.dateSelection( date ); - }; - - scope.close = function() { - scope.isOpen = false; - element[0].focus(); - }; - - var $popup = $compile(popupEl)(scope); - // Prevent jQuery cache memory leak (template is now redundant after linking) - popupEl.remove(); - - if ( appendToBody ) { - $document.find('body').append($popup); - } else { - element.after($popup); - } - - scope.$on('$destroy', function() { - $popup.remove(); - element.unbind('keydown', keydown); - $document.unbind('click', documentClickBind); - }); - } - }; -}]) - -.directive('datepickerPopupWrap', function() { - return { - restrict:'EA', - replace: true, - transclude: true, - templateUrl: 'template/datepicker/popup.html', - link:function (scope, element, attrs) { - element.bind('click', function(event) { - event.preventDefault(); - event.stopPropagation(); - }); - } - }; -}); - -angular.module('ui.bootstrap.dropdown', []) - -.constant('dropdownConfig', { - openClass: 'open' -}) - -.service('dropdownService', ['$document', function($document) { - var openScope = null; - - this.open = function( dropdownScope ) { - if ( !openScope ) { - $document.bind('click', closeDropdown); - $document.bind('keydown', escapeKeyBind); - } - - if ( openScope && openScope !== dropdownScope ) { - openScope.isOpen = false; - } - - openScope = dropdownScope; - }; - - this.close = function( dropdownScope ) { - if ( openScope === dropdownScope ) { - openScope = null; - $document.unbind('click', closeDropdown); - $document.unbind('keydown', escapeKeyBind); - } - }; - - var closeDropdown = function( evt ) { - // This method may still be called during the same mouse event that - // unbound this event handler. So check openScope before proceeding. - if (!openScope) { return; } - - var toggleElement = openScope.getToggleElement(); - if ( evt && toggleElement && toggleElement[0].contains(evt.target) ) { - return; - } - - openScope.$apply(function() { - openScope.isOpen = false; - }); - }; - - var escapeKeyBind = function( evt ) { - if ( evt.which === 27 ) { - openScope.focusToggleElement(); - closeDropdown(); - } - }; -}]) - -.controller('DropdownController', ['$scope', '$attrs', '$parse', 'dropdownConfig', 'dropdownService', '$animate', function($scope, $attrs, $parse, dropdownConfig, dropdownService, $animate) { - var self = this, - scope = $scope.$new(), // create a child scope so we are not polluting original one - openClass = dropdownConfig.openClass, - getIsOpen, - setIsOpen = angular.noop, - toggleInvoker = $attrs.onToggle ? $parse($attrs.onToggle) : angular.noop; - - this.init = function( element ) { - self.$element = element; - - if ( $attrs.isOpen ) { - getIsOpen = $parse($attrs.isOpen); - setIsOpen = getIsOpen.assign; - - $scope.$watch(getIsOpen, function(value) { - scope.isOpen = !!value; - }); - } - }; - - this.toggle = function( open ) { - return scope.isOpen = arguments.length ? !!open : !scope.isOpen; - }; - - // Allow other directives to watch status - this.isOpen = function() { - return scope.isOpen; - }; - - scope.getToggleElement = function() { - return self.toggleElement; - }; - - scope.focusToggleElement = function() { - if ( self.toggleElement ) { - self.toggleElement[0].focus(); - } - }; - - scope.$watch('isOpen', function( isOpen, wasOpen ) { - $animate[isOpen ? 'addClass' : 'removeClass'](self.$element, openClass); - - if ( isOpen ) { - scope.focusToggleElement(); - dropdownService.open( scope ); - } else { - dropdownService.close( scope ); - } - - setIsOpen($scope, isOpen); - if (angular.isDefined(isOpen) && isOpen !== wasOpen) { - toggleInvoker($scope, { open: !!isOpen }); - } - }); - - $scope.$on('$locationChangeSuccess', function() { - scope.isOpen = false; - }); - - $scope.$on('$destroy', function() { - scope.$destroy(); - }); -}]) - -.directive('dropdown', function() { - return { - controller: 'DropdownController', - link: function(scope, element, attrs, dropdownCtrl) { - dropdownCtrl.init( element ); - } - }; -}) - -.directive('dropdownToggle', function() { - return { - require: '?^dropdown', - link: function(scope, element, attrs, dropdownCtrl) { - if ( !dropdownCtrl ) { - return; - } - - dropdownCtrl.toggleElement = element; - - var toggleDropdown = function(event) { - event.preventDefault(); - - if ( !element.hasClass('disabled') && !attrs.disabled ) { - scope.$apply(function() { - dropdownCtrl.toggle(); - }); - } - }; - - element.bind('click', toggleDropdown); - - // WAI-ARIA - element.attr({ 'aria-haspopup': true, 'aria-expanded': false }); - scope.$watch(dropdownCtrl.isOpen, function( isOpen ) { - element.attr('aria-expanded', !!isOpen); - }); - - scope.$on('$destroy', function() { - element.unbind('click', toggleDropdown); - }); - } - }; -}); - -angular.module('ui.bootstrap.modal', ['ui.bootstrap.transition']) - -/** - * A helper, internal data structure that acts as a map but also allows getting / removing - * elements in the LIFO order - */ - .factory('$$stackedMap', function () { - return { - createNew: function () { - var stack = []; - - return { - add: function (key, value) { - stack.push({ - key: key, - value: value - }); - }, - get: function (key) { - for (var i = 0; i < stack.length; i++) { - if (key == stack[i].key) { - return stack[i]; - } - } - }, - keys: function() { - var keys = []; - for (var i = 0; i < stack.length; i++) { - keys.push(stack[i].key); - } - return keys; - }, - top: function () { - return stack[stack.length - 1]; - }, - remove: function (key) { - var idx = -1; - for (var i = 0; i < stack.length; i++) { - if (key == stack[i].key) { - idx = i; - break; - } - } - return stack.splice(idx, 1)[0]; - }, - removeTop: function () { - return stack.splice(stack.length - 1, 1)[0]; - }, - length: function () { - return stack.length; - } - }; - } - }; - }) - -/** - * A helper directive for the $modal service. It creates a backdrop element. - */ - .directive('modalBackdrop', ['$timeout', function ($timeout) { - return { - restrict: 'EA', - replace: true, - templateUrl: 'template/modal/backdrop.html', - link: function (scope, element, attrs) { - scope.backdropClass = attrs.backdropClass || ''; - - scope.animate = false; - - //trigger CSS transitions - $timeout(function () { - scope.animate = true; - }); - } - }; - }]) - - .directive('modalWindow', ['$modalStack', '$timeout', function ($modalStack, $timeout) { - return { - restrict: 'EA', - scope: { - index: '@', - animate: '=' - }, - replace: true, - transclude: true, - templateUrl: function(tElement, tAttrs) { - return tAttrs.templateUrl || 'template/modal/window.html'; - }, - link: function (scope, element, attrs) { - element.addClass(attrs.windowClass || ''); - scope.size = attrs.size; - - $timeout(function () { - // trigger CSS transitions - scope.animate = true; - - /** - * Auto-focusing of a freshly-opened modal element causes any child elements - * with the autofocus attribute to lose focus. This is an issue on touch - * based devices which will show and then hide the onscreen keyboard. - * Attempts to refocus the autofocus element via JavaScript will not reopen - * the onscreen keyboard. Fixed by updated the focusing logic to only autofocus - * the modal element if the modal does not contain an autofocus element. - */ - if (!element[0].querySelectorAll('[autofocus]').length) { - element[0].focus(); - } - }); - - scope.close = function (evt) { - var modal = $modalStack.getTop(); - if (modal && modal.value.backdrop && modal.value.backdrop != 'static' && (evt.target === evt.currentTarget)) { - evt.preventDefault(); - evt.stopPropagation(); - $modalStack.dismiss(modal.key, 'backdrop click'); - } - }; - } - }; - }]) - - .directive('modalTransclude', function () { - return { - link: function($scope, $element, $attrs, controller, $transclude) { - $transclude($scope.$parent, function(clone) { - $element.empty(); - $element.append(clone); - }); - } - }; - }) - - .factory('$modalStack', ['$transition', '$timeout', '$document', '$compile', '$rootScope', '$$stackedMap', - function ($transition, $timeout, $document, $compile, $rootScope, $$stackedMap) { - - var OPENED_MODAL_CLASS = 'modal-open'; - - var backdropDomEl, backdropScope; - var openedWindows = $$stackedMap.createNew(); - var $modalStack = {}; - - function backdropIndex() { - var topBackdropIndex = -1; - var opened = openedWindows.keys(); - for (var i = 0; i < opened.length; i++) { - if (openedWindows.get(opened[i]).value.backdrop) { - topBackdropIndex = i; - } - } - return topBackdropIndex; - } - - $rootScope.$watch(backdropIndex, function(newBackdropIndex){ - if (backdropScope) { - backdropScope.index = newBackdropIndex; - } - }); - - function removeModalWindow(modalInstance) { - - var body = $document.find('body').eq(0); - var modalWindow = openedWindows.get(modalInstance).value; - - //clean up the stack - openedWindows.remove(modalInstance); - - //remove window DOM element - removeAfterAnimate(modalWindow.modalDomEl, modalWindow.modalScope, 300, function() { - modalWindow.modalScope.$destroy(); - body.toggleClass(OPENED_MODAL_CLASS, openedWindows.length() > 0); - checkRemoveBackdrop(); - }); - } - - function checkRemoveBackdrop() { - //remove backdrop if no longer needed - if (backdropDomEl && backdropIndex() == -1) { - var backdropScopeRef = backdropScope; - removeAfterAnimate(backdropDomEl, backdropScope, 150, function () { - backdropScopeRef.$destroy(); - backdropScopeRef = null; - }); - backdropDomEl = undefined; - backdropScope = undefined; - } - } - - function removeAfterAnimate(domEl, scope, emulateTime, done) { - // Closing animation - scope.animate = false; - - var transitionEndEventName = $transition.transitionEndEventName; - if (transitionEndEventName) { - // transition out - var timeout = $timeout(afterAnimating, emulateTime); - - domEl.bind(transitionEndEventName, function () { - $timeout.cancel(timeout); - afterAnimating(); - scope.$apply(); - }); - } else { - // Ensure this call is async - $timeout(afterAnimating); - } - - function afterAnimating() { - if (afterAnimating.done) { - return; - } - afterAnimating.done = true; - - domEl.remove(); - if (done) { - done(); - } - } - } - - $document.bind('keydown', function (evt) { - var modal; - - if (evt.which === 27) { - modal = openedWindows.top(); - if (modal && modal.value.keyboard) { - evt.preventDefault(); - $rootScope.$apply(function () { - $modalStack.dismiss(modal.key, 'escape key press'); - }); - } - } - }); - - $modalStack.open = function (modalInstance, modal) { - - openedWindows.add(modalInstance, { - deferred: modal.deferred, - modalScope: modal.scope, - backdrop: modal.backdrop, - keyboard: modal.keyboard - }); - - var body = $document.find('body').eq(0), - currBackdropIndex = backdropIndex(); - - if (currBackdropIndex >= 0 && !backdropDomEl) { - backdropScope = $rootScope.$new(true); - backdropScope.index = currBackdropIndex; - var angularBackgroundDomEl = angular.element('
      '); - angularBackgroundDomEl.attr('backdrop-class', modal.backdropClass); - backdropDomEl = $compile(angularBackgroundDomEl)(backdropScope); - body.append(backdropDomEl); - } - - var angularDomEl = angular.element('
      '); - angularDomEl.attr({ - 'template-url': modal.windowTemplateUrl, - 'window-class': modal.windowClass, - 'size': modal.size, - 'index': openedWindows.length() - 1, - 'animate': 'animate' - }).html(modal.content); - - var modalDomEl = $compile(angularDomEl)(modal.scope); - openedWindows.top().value.modalDomEl = modalDomEl; - body.append(modalDomEl); - body.addClass(OPENED_MODAL_CLASS); - }; - - $modalStack.close = function (modalInstance, result) { - var modalWindow = openedWindows.get(modalInstance); - if (modalWindow) { - modalWindow.value.deferred.resolve(result); - removeModalWindow(modalInstance); - } - }; - - $modalStack.dismiss = function (modalInstance, reason) { - var modalWindow = openedWindows.get(modalInstance); - if (modalWindow) { - modalWindow.value.deferred.reject(reason); - removeModalWindow(modalInstance); - } - }; - - $modalStack.dismissAll = function (reason) { - var topModal = this.getTop(); - while (topModal) { - this.dismiss(topModal.key, reason); - topModal = this.getTop(); - } - }; - - $modalStack.getTop = function () { - return openedWindows.top(); - }; - - return $modalStack; - }]) - - .provider('$modal', function () { - - var $modalProvider = { - options: { - backdrop: true, //can be also false or 'static' - keyboard: true - }, - $get: ['$injector', '$rootScope', '$q', '$http', '$templateCache', '$controller', '$modalStack', - function ($injector, $rootScope, $q, $http, $templateCache, $controller, $modalStack) { - - var $modal = {}; - - function getTemplatePromise(options) { - return options.template ? $q.when(options.template) : - $http.get(angular.isFunction(options.templateUrl) ? (options.templateUrl)() : options.templateUrl, - {cache: $templateCache}).then(function (result) { - return result.data; - }); - } - - function getResolvePromises(resolves) { - var promisesArr = []; - angular.forEach(resolves, function (value) { - if (angular.isFunction(value) || angular.isArray(value)) { - promisesArr.push($q.when($injector.invoke(value))); - } - }); - return promisesArr; - } - - $modal.open = function (modalOptions) { - - var modalResultDeferred = $q.defer(); - var modalOpenedDeferred = $q.defer(); - - //prepare an instance of a modal to be injected into controllers and returned to a caller - var modalInstance = { - result: modalResultDeferred.promise, - opened: modalOpenedDeferred.promise, - close: function (result) { - $modalStack.close(modalInstance, result); - }, - dismiss: function (reason) { - $modalStack.dismiss(modalInstance, reason); - } - }; - - //merge and clean up options - modalOptions = angular.extend({}, $modalProvider.options, modalOptions); - modalOptions.resolve = modalOptions.resolve || {}; - - //verify options - if (!modalOptions.template && !modalOptions.templateUrl) { - throw new Error('One of template or templateUrl options is required.'); - } - - var templateAndResolvePromise = - $q.all([getTemplatePromise(modalOptions)].concat(getResolvePromises(modalOptions.resolve))); - - - templateAndResolvePromise.then(function resolveSuccess(tplAndVars) { - - var modalScope = (modalOptions.scope || $rootScope).$new(); - modalScope.$close = modalInstance.close; - modalScope.$dismiss = modalInstance.dismiss; - - var ctrlInstance, ctrlLocals = {}; - var resolveIter = 1; - - //controllers - if (modalOptions.controller) { - ctrlLocals.$scope = modalScope; - ctrlLocals.$modalInstance = modalInstance; - angular.forEach(modalOptions.resolve, function (value, key) { - ctrlLocals[key] = tplAndVars[resolveIter++]; - }); - - ctrlInstance = $controller(modalOptions.controller, ctrlLocals); - if (modalOptions.controllerAs) { - modalScope[modalOptions.controllerAs] = ctrlInstance; - } - } - - $modalStack.open(modalInstance, { - scope: modalScope, - deferred: modalResultDeferred, - content: tplAndVars[0], - backdrop: modalOptions.backdrop, - keyboard: modalOptions.keyboard, - backdropClass: modalOptions.backdropClass, - windowClass: modalOptions.windowClass, - windowTemplateUrl: modalOptions.windowTemplateUrl, - size: modalOptions.size - }); - - }, function resolveError(reason) { - modalResultDeferred.reject(reason); - }); - - templateAndResolvePromise.then(function () { - modalOpenedDeferred.resolve(true); - }, function () { - modalOpenedDeferred.reject(false); - }); - - return modalInstance; - }; - - return $modal; - }] - }; - - return $modalProvider; - }); - -angular.module('ui.bootstrap.pagination', []) - -.controller('PaginationController', ['$scope', '$attrs', '$parse', function ($scope, $attrs, $parse) { - var self = this, - ngModelCtrl = { $setViewValue: angular.noop }, // nullModelCtrl - setNumPages = $attrs.numPages ? $parse($attrs.numPages).assign : angular.noop; - - this.init = function(ngModelCtrl_, config) { - ngModelCtrl = ngModelCtrl_; - this.config = config; - - ngModelCtrl.$render = function() { - self.render(); - }; - - if ($attrs.itemsPerPage) { - $scope.$parent.$watch($parse($attrs.itemsPerPage), function(value) { - self.itemsPerPage = parseInt(value, 10); - $scope.totalPages = self.calculateTotalPages(); - }); - } else { - this.itemsPerPage = config.itemsPerPage; - } - }; - - this.calculateTotalPages = function() { - var totalPages = this.itemsPerPage < 1 ? 1 : Math.ceil($scope.totalItems / this.itemsPerPage); - return Math.max(totalPages || 0, 1); - }; - - this.render = function() { - $scope.page = parseInt(ngModelCtrl.$viewValue, 10) || 1; - }; - - $scope.selectPage = function(page) { - if ( $scope.page !== page && page > 0 && page <= $scope.totalPages) { - ngModelCtrl.$setViewValue(page); - ngModelCtrl.$render(); - } - }; - - $scope.getText = function( key ) { - return $scope[key + 'Text'] || self.config[key + 'Text']; - }; - $scope.noPrevious = function() { - return $scope.page === 1; - }; - $scope.noNext = function() { - return $scope.page === $scope.totalPages; - }; - - $scope.$watch('totalItems', function() { - $scope.totalPages = self.calculateTotalPages(); - }); - - $scope.$watch('totalPages', function(value) { - setNumPages($scope.$parent, value); // Readonly variable - - if ( $scope.page > value ) { - $scope.selectPage(value); - } else { - ngModelCtrl.$render(); - } - }); -}]) - -.constant('paginationConfig', { - itemsPerPage: 10, - boundaryLinks: false, - directionLinks: true, - firstText: 'First', - previousText: 'Previous', - nextText: 'Next', - lastText: 'Last', - rotate: true -}) - -.directive('pagination', ['$parse', 'paginationConfig', function($parse, paginationConfig) { - return { - restrict: 'EA', - scope: { - totalItems: '=', - firstText: '@', - previousText: '@', - nextText: '@', - lastText: '@' - }, - require: ['pagination', '?ngModel'], - controller: 'PaginationController', - templateUrl: 'template/pagination/pagination.html', - replace: true, - link: function(scope, element, attrs, ctrls) { - var paginationCtrl = ctrls[0], ngModelCtrl = ctrls[1]; - - if (!ngModelCtrl) { - return; // do nothing if no ng-model - } - - // Setup configuration parameters - var maxSize = angular.isDefined(attrs.maxSize) ? scope.$parent.$eval(attrs.maxSize) : paginationConfig.maxSize, - rotate = angular.isDefined(attrs.rotate) ? scope.$parent.$eval(attrs.rotate) : paginationConfig.rotate; - scope.boundaryLinks = angular.isDefined(attrs.boundaryLinks) ? scope.$parent.$eval(attrs.boundaryLinks) : paginationConfig.boundaryLinks; - scope.directionLinks = angular.isDefined(attrs.directionLinks) ? scope.$parent.$eval(attrs.directionLinks) : paginationConfig.directionLinks; - - paginationCtrl.init(ngModelCtrl, paginationConfig); - - if (attrs.maxSize) { - scope.$parent.$watch($parse(attrs.maxSize), function(value) { - maxSize = parseInt(value, 10); - paginationCtrl.render(); - }); - } - - // Create page object used in template - function makePage(number, text, isActive) { - return { - number: number, - text: text, - active: isActive - }; - } - - function getPages(currentPage, totalPages) { - var pages = []; - - // Default page limits - var startPage = 1, endPage = totalPages; - var isMaxSized = ( angular.isDefined(maxSize) && maxSize < totalPages ); - - // recompute if maxSize - if ( isMaxSized ) { - if ( rotate ) { - // Current page is displayed in the middle of the visible ones - startPage = Math.max(currentPage - Math.floor(maxSize/2), 1); - endPage = startPage + maxSize - 1; - - // Adjust if limit is exceeded - if (endPage > totalPages) { - endPage = totalPages; - startPage = endPage - maxSize + 1; - } - } else { - // Visible pages are paginated with maxSize - startPage = ((Math.ceil(currentPage / maxSize) - 1) * maxSize) + 1; - - // Adjust last page if limit is exceeded - endPage = Math.min(startPage + maxSize - 1, totalPages); - } - } - - // Add page number links - for (var number = startPage; number <= endPage; number++) { - var page = makePage(number, number, number === currentPage); - pages.push(page); - } - - // Add links to move between page sets - if ( isMaxSized && ! rotate ) { - if ( startPage > 1 ) { - var previousPageSet = makePage(startPage - 1, '...', false); - pages.unshift(previousPageSet); - } - - if ( endPage < totalPages ) { - var nextPageSet = makePage(endPage + 1, '...', false); - pages.push(nextPageSet); - } - } - - return pages; - } - - var originalRender = paginationCtrl.render; - paginationCtrl.render = function() { - originalRender(); - if (scope.page > 0 && scope.page <= scope.totalPages) { - scope.pages = getPages(scope.page, scope.totalPages); - } - }; - } - }; -}]) - -.constant('pagerConfig', { - itemsPerPage: 10, - previousText: '« Previous', - nextText: 'Next »', - align: true -}) - -.directive('pager', ['pagerConfig', function(pagerConfig) { - return { - restrict: 'EA', - scope: { - totalItems: '=', - previousText: '@', - nextText: '@' - }, - require: ['pager', '?ngModel'], - controller: 'PaginationController', - templateUrl: 'template/pagination/pager.html', - replace: true, - link: function(scope, element, attrs, ctrls) { - var paginationCtrl = ctrls[0], ngModelCtrl = ctrls[1]; - - if (!ngModelCtrl) { - return; // do nothing if no ng-model - } - - scope.align = angular.isDefined(attrs.align) ? scope.$parent.$eval(attrs.align) : pagerConfig.align; - paginationCtrl.init(ngModelCtrl, pagerConfig); - } - }; -}]); - -/** - * The following features are still outstanding: animation as a - * function, placement as a function, inside, support for more triggers than - * just mouse enter/leave, html tooltips, and selector delegation. - */ -angular.module( 'ui.bootstrap.tooltip', [ 'ui.bootstrap.position', 'ui.bootstrap.bindHtml' ] ) - -/** - * The $tooltip service creates tooltip- and popover-like directives as well as - * houses global options for them. - */ -.provider( '$tooltip', function () { - // The default options tooltip and popover. - var defaultOptions = { - placement: 'top', - animation: true, - popupDelay: 0 - }; - - // Default hide triggers for each show trigger - var triggerMap = { - 'mouseenter': 'mouseleave', - 'click': 'click', - 'focus': 'blur' - }; - - // The options specified to the provider globally. - var globalOptions = {}; - - /** - * `options({})` allows global configuration of all tooltips in the - * application. - * - * var app = angular.module( 'App', ['ui.bootstrap.tooltip'], function( $tooltipProvider ) { - * // place tooltips left instead of top by default - * $tooltipProvider.options( { placement: 'left' } ); - * }); - */ - this.options = function( value ) { - angular.extend( globalOptions, value ); - }; - - /** - * This allows you to extend the set of trigger mappings available. E.g.: - * - * $tooltipProvider.setTriggers( 'openTrigger': 'closeTrigger' ); - */ - this.setTriggers = function setTriggers ( triggers ) { - angular.extend( triggerMap, triggers ); - }; - - /** - * This is a helper function for translating camel-case to snake-case. - */ - function snake_case(name){ - var regexp = /[A-Z]/g; - var separator = '-'; - return name.replace(regexp, function(letter, pos) { - return (pos ? separator : '') + letter.toLowerCase(); - }); - } - - /** - * Returns the actual instance of the $tooltip service. - * TODO support multiple triggers - */ - this.$get = [ '$window', '$compile', '$timeout', '$document', '$position', '$interpolate', function ( $window, $compile, $timeout, $document, $position, $interpolate ) { - return function $tooltip ( type, prefix, defaultTriggerShow ) { - var options = angular.extend( {}, defaultOptions, globalOptions ); - - /** - * Returns an object of show and hide triggers. - * - * If a trigger is supplied, - * it is used to show the tooltip; otherwise, it will use the `trigger` - * option passed to the `$tooltipProvider.options` method; else it will - * default to the trigger supplied to this directive factory. - * - * The hide trigger is based on the show trigger. If the `trigger` option - * was passed to the `$tooltipProvider.options` method, it will use the - * mapped trigger from `triggerMap` or the passed trigger if the map is - * undefined; otherwise, it uses the `triggerMap` value of the show - * trigger; else it will just use the show trigger. - */ - function getTriggers ( trigger ) { - var show = trigger || options.trigger || defaultTriggerShow; - var hide = triggerMap[show] || show; - return { - show: show, - hide: hide - }; - } - - var directiveName = snake_case( type ); - - var startSym = $interpolate.startSymbol(); - var endSym = $interpolate.endSymbol(); - var template = - '
      '+ - '
      '; - - return { - restrict: 'EA', - compile: function (tElem, tAttrs) { - var tooltipLinker = $compile( template ); - - return function link ( scope, element, attrs ) { - var tooltip; - var tooltipLinkedScope; - var transitionTimeout; - var popupTimeout; - var appendToBody = angular.isDefined( options.appendToBody ) ? options.appendToBody : false; - var triggers = getTriggers( undefined ); - var hasEnableExp = angular.isDefined(attrs[prefix+'Enable']); - var ttScope = scope.$new(true); - - var positionTooltip = function () { - - var ttPosition = $position.positionElements(element, tooltip, ttScope.placement, appendToBody); - ttPosition.top += 'px'; - ttPosition.left += 'px'; - - // Now set the calculated positioning. - tooltip.css( ttPosition ); - }; - - // By default, the tooltip is not open. - // TODO add ability to start tooltip opened - ttScope.isOpen = false; - - function toggleTooltipBind () { - if ( ! ttScope.isOpen ) { - showTooltipBind(); - } else { - hideTooltipBind(); - } - } - - // Show the tooltip with delay if specified, otherwise show it immediately - function showTooltipBind() { - if(hasEnableExp && !scope.$eval(attrs[prefix+'Enable'])) { - return; - } - - prepareTooltip(); - - if ( ttScope.popupDelay ) { - // Do nothing if the tooltip was already scheduled to pop-up. - // This happens if show is triggered multiple times before any hide is triggered. - if (!popupTimeout) { - popupTimeout = $timeout( show, ttScope.popupDelay, false ); - popupTimeout.then(function(reposition){reposition();}); - } - } else { - show()(); - } - } - - function hideTooltipBind () { - scope.$apply(function () { - hide(); - }); - } - - // Show the tooltip popup element. - function show() { - - popupTimeout = null; - - // If there is a pending remove transition, we must cancel it, lest the - // tooltip be mysteriously removed. - if ( transitionTimeout ) { - $timeout.cancel( transitionTimeout ); - transitionTimeout = null; - } - - // Don't show empty tooltips. - if ( ! ttScope.content ) { - return angular.noop; - } - - createTooltip(); - - // Set the initial positioning. - tooltip.css({ top: 0, left: 0, display: 'block' }); - ttScope.$digest(); - - positionTooltip(); - - // And show the tooltip. - ttScope.isOpen = true; - ttScope.$digest(); // digest required as $apply is not called - - // Return positioning function as promise callback for correct - // positioning after draw. - return positionTooltip; - } - - // Hide the tooltip popup element. - function hide() { - // First things first: we don't show it anymore. - ttScope.isOpen = false; - - //if tooltip is going to be shown after delay, we must cancel this - $timeout.cancel( popupTimeout ); - popupTimeout = null; - - // And now we remove it from the DOM. However, if we have animation, we - // need to wait for it to expire beforehand. - // FIXME: this is a placeholder for a port of the transitions library. - if ( ttScope.animation ) { - if (!transitionTimeout) { - transitionTimeout = $timeout(removeTooltip, 500); - } - } else { - removeTooltip(); - } - } - - function createTooltip() { - // There can only be one tooltip element per directive shown at once. - if (tooltip) { - removeTooltip(); - } - tooltipLinkedScope = ttScope.$new(); - tooltip = tooltipLinker(tooltipLinkedScope, function (tooltip) { - if ( appendToBody ) { - $document.find( 'body' ).append( tooltip ); - } else { - element.after( tooltip ); - } - }); - } - - function removeTooltip() { - transitionTimeout = null; - if (tooltip) { - tooltip.remove(); - tooltip = null; - } - if (tooltipLinkedScope) { - tooltipLinkedScope.$destroy(); - tooltipLinkedScope = null; - } - } - - function prepareTooltip() { - prepPlacement(); - prepPopupDelay(); - } - - /** - * Observe the relevant attributes. - */ - attrs.$observe( type, function ( val ) { - ttScope.content = val; - - if (!val && ttScope.isOpen ) { - hide(); - } - }); - - attrs.$observe( prefix+'Title', function ( val ) { - ttScope.title = val; - }); - - function prepPlacement() { - var val = attrs[ prefix + 'Placement' ]; - ttScope.placement = angular.isDefined( val ) ? val : options.placement; - } - - function prepPopupDelay() { - var val = attrs[ prefix + 'PopupDelay' ]; - var delay = parseInt( val, 10 ); - ttScope.popupDelay = ! isNaN(delay) ? delay : options.popupDelay; - } - - var unregisterTriggers = function () { - element.unbind(triggers.show, showTooltipBind); - element.unbind(triggers.hide, hideTooltipBind); - }; - - function prepTriggers() { - var val = attrs[ prefix + 'Trigger' ]; - unregisterTriggers(); - - triggers = getTriggers( val ); - - if ( triggers.show === triggers.hide ) { - element.bind( triggers.show, toggleTooltipBind ); - } else { - element.bind( triggers.show, showTooltipBind ); - element.bind( triggers.hide, hideTooltipBind ); - } - } - prepTriggers(); - - var animation = scope.$eval(attrs[prefix + 'Animation']); - ttScope.animation = angular.isDefined(animation) ? !!animation : options.animation; - - var appendToBodyVal = scope.$eval(attrs[prefix + 'AppendToBody']); - appendToBody = angular.isDefined(appendToBodyVal) ? appendToBodyVal : appendToBody; - - // if a tooltip is attached to we need to remove it on - // location change as its parent scope will probably not be destroyed - // by the change. - if ( appendToBody ) { - scope.$on('$locationChangeSuccess', function closeTooltipOnLocationChangeSuccess () { - if ( ttScope.isOpen ) { - hide(); - } - }); - } - - // Make sure tooltip is destroyed and removed. - scope.$on('$destroy', function onDestroyTooltip() { - $timeout.cancel( transitionTimeout ); - $timeout.cancel( popupTimeout ); - unregisterTriggers(); - removeTooltip(); - ttScope = null; - }); - }; - } - }; - }; - }]; -}) - -.directive( 'tooltipPopup', function () { - return { - restrict: 'EA', - replace: true, - scope: { content: '@', placement: '@', animation: '&', isOpen: '&' }, - templateUrl: 'template/tooltip/tooltip-popup.html' - }; -}) - -.directive( 'tooltip', [ '$tooltip', function ( $tooltip ) { - return $tooltip( 'tooltip', 'tooltip', 'mouseenter' ); -}]) - -.directive( 'tooltipHtmlUnsafePopup', function () { - return { - restrict: 'EA', - replace: true, - scope: { content: '@', placement: '@', animation: '&', isOpen: '&' }, - templateUrl: 'template/tooltip/tooltip-html-unsafe-popup.html' - }; -}) - -.directive( 'tooltipHtmlUnsafe', [ '$tooltip', function ( $tooltip ) { - return $tooltip( 'tooltipHtmlUnsafe', 'tooltip', 'mouseenter' ); -}]); - -/** - * The following features are still outstanding: popup delay, animation as a - * function, placement as a function, inside, support for more triggers than - * just mouse enter/leave, html popovers, and selector delegatation. - */ -angular.module( 'ui.bootstrap.popover', [ 'ui.bootstrap.tooltip' ] ) - -.directive( 'popoverPopup', function () { - return { - restrict: 'EA', - replace: true, - scope: { title: '@', content: '@', placement: '@', animation: '&', isOpen: '&' }, - templateUrl: 'template/popover/popover.html' - }; -}) - -.directive( 'popover', [ '$tooltip', function ( $tooltip ) { - return $tooltip( 'popover', 'popover', 'click' ); -}]); - -angular.module('ui.bootstrap.progressbar', []) - -.constant('progressConfig', { - animate: true, - max: 100 -}) - -.controller('ProgressController', ['$scope', '$attrs', 'progressConfig', function($scope, $attrs, progressConfig) { - var self = this, - animate = angular.isDefined($attrs.animate) ? $scope.$parent.$eval($attrs.animate) : progressConfig.animate; - - this.bars = []; - $scope.max = angular.isDefined($attrs.max) ? $scope.$parent.$eval($attrs.max) : progressConfig.max; - - this.addBar = function(bar, element) { - if ( !animate ) { - element.css({'transition': 'none'}); - } - - this.bars.push(bar); - - bar.$watch('value', function( value ) { - bar.percent = +(100 * value / $scope.max).toFixed(2); - }); - - bar.$on('$destroy', function() { - element = null; - self.removeBar(bar); - }); - }; - - this.removeBar = function(bar) { - this.bars.splice(this.bars.indexOf(bar), 1); - }; -}]) - -.directive('progress', function() { - return { - restrict: 'EA', - replace: true, - transclude: true, - controller: 'ProgressController', - require: 'progress', - scope: {}, - templateUrl: 'template/progressbar/progress.html' - }; -}) - -.directive('bar', function() { - return { - restrict: 'EA', - replace: true, - transclude: true, - require: '^progress', - scope: { - value: '=', - type: '@' - }, - templateUrl: 'template/progressbar/bar.html', - link: function(scope, element, attrs, progressCtrl) { - progressCtrl.addBar(scope, element); - } - }; -}) - -.directive('progressbar', function() { - return { - restrict: 'EA', - replace: true, - transclude: true, - controller: 'ProgressController', - scope: { - value: '=', - type: '@' - }, - templateUrl: 'template/progressbar/progressbar.html', - link: function(scope, element, attrs, progressCtrl) { - progressCtrl.addBar(scope, angular.element(element.children()[0])); - } - }; -}); -angular.module('ui.bootstrap.rating', []) - -.constant('ratingConfig', { - max: 5, - stateOn: null, - stateOff: null -}) - -.controller('RatingController', ['$scope', '$attrs', 'ratingConfig', function($scope, $attrs, ratingConfig) { - var ngModelCtrl = { $setViewValue: angular.noop }; - - this.init = function(ngModelCtrl_) { - ngModelCtrl = ngModelCtrl_; - ngModelCtrl.$render = this.render; - - this.stateOn = angular.isDefined($attrs.stateOn) ? $scope.$parent.$eval($attrs.stateOn) : ratingConfig.stateOn; - this.stateOff = angular.isDefined($attrs.stateOff) ? $scope.$parent.$eval($attrs.stateOff) : ratingConfig.stateOff; - - var ratingStates = angular.isDefined($attrs.ratingStates) ? $scope.$parent.$eval($attrs.ratingStates) : - new Array( angular.isDefined($attrs.max) ? $scope.$parent.$eval($attrs.max) : ratingConfig.max ); - $scope.range = this.buildTemplateObjects(ratingStates); - }; - - this.buildTemplateObjects = function(states) { - for (var i = 0, n = states.length; i < n; i++) { - states[i] = angular.extend({ index: i }, { stateOn: this.stateOn, stateOff: this.stateOff }, states[i]); - } - return states; - }; - - $scope.rate = function(value) { - if ( !$scope.readonly && value >= 0 && value <= $scope.range.length ) { - ngModelCtrl.$setViewValue(value); - ngModelCtrl.$render(); - } - }; - - $scope.enter = function(value) { - if ( !$scope.readonly ) { - $scope.value = value; - } - $scope.onHover({value: value}); - }; - - $scope.reset = function() { - $scope.value = ngModelCtrl.$viewValue; - $scope.onLeave(); - }; - - $scope.onKeydown = function(evt) { - if (/(37|38|39|40)/.test(evt.which)) { - evt.preventDefault(); - evt.stopPropagation(); - $scope.rate( $scope.value + (evt.which === 38 || evt.which === 39 ? 1 : -1) ); - } - }; - - this.render = function() { - $scope.value = ngModelCtrl.$viewValue; - }; -}]) - -.directive('rating', function() { - return { - restrict: 'EA', - require: ['rating', 'ngModel'], - scope: { - readonly: '=?', - onHover: '&', - onLeave: '&' - }, - controller: 'RatingController', - templateUrl: 'template/rating/rating.html', - replace: true, - link: function(scope, element, attrs, ctrls) { - var ratingCtrl = ctrls[0], ngModelCtrl = ctrls[1]; - - if ( ngModelCtrl ) { - ratingCtrl.init( ngModelCtrl ); - } - } - }; -}); - -/** - * @ngdoc overview - * @name ui.bootstrap.tabs - * - * @description - * AngularJS version of the tabs directive. - */ - -angular.module('ui.bootstrap.tabs', []) - -.controller('TabsetController', ['$scope', function TabsetCtrl($scope) { - var ctrl = this, - tabs = ctrl.tabs = $scope.tabs = []; - - ctrl.select = function(selectedTab) { - angular.forEach(tabs, function(tab) { - if (tab.active && tab !== selectedTab) { - tab.active = false; - tab.onDeselect(); - } - }); - selectedTab.active = true; - selectedTab.onSelect(); - }; - - ctrl.addTab = function addTab(tab) { - tabs.push(tab); - // we can't run the select function on the first tab - // since that would select it twice - if (tabs.length === 1) { - tab.active = true; - } else if (tab.active) { - ctrl.select(tab); - } - }; - - ctrl.removeTab = function removeTab(tab) { - var index = tabs.indexOf(tab); - //Select a new tab if the tab to be removed is selected and not destroyed - if (tab.active && tabs.length > 1 && !destroyed) { - //If this is the last tab, select the previous tab. else, the next tab. - var newActiveIndex = index == tabs.length - 1 ? index - 1 : index + 1; - ctrl.select(tabs[newActiveIndex]); - } - tabs.splice(index, 1); - }; - - var destroyed; - $scope.$on('$destroy', function() { - destroyed = true; - }); -}]) - -/** - * @ngdoc directive - * @name ui.bootstrap.tabs.directive:tabset - * @restrict EA - * - * @description - * Tabset is the outer container for the tabs directive - * - * @param {boolean=} vertical Whether or not to use vertical styling for the tabs. - * @param {boolean=} justified Whether or not to use justified styling for the tabs. - * - * @example - - - - First Content! - Second Content! - -
      - - First Vertical Content! - Second Vertical Content! - - - First Justified Content! - Second Justified Content! - -
      -
      - */ -.directive('tabset', function() { - return { - restrict: 'EA', - transclude: true, - replace: true, - scope: { - type: '@' - }, - controller: 'TabsetController', - templateUrl: 'template/tabs/tabset.html', - link: function(scope, element, attrs) { - scope.vertical = angular.isDefined(attrs.vertical) ? scope.$parent.$eval(attrs.vertical) : false; - scope.justified = angular.isDefined(attrs.justified) ? scope.$parent.$eval(attrs.justified) : false; - } - }; -}) - -/** - * @ngdoc directive - * @name ui.bootstrap.tabs.directive:tab - * @restrict EA - * - * @param {string=} heading The visible heading, or title, of the tab. Set HTML headings with {@link ui.bootstrap.tabs.directive:tabHeading tabHeading}. - * @param {string=} select An expression to evaluate when the tab is selected. - * @param {boolean=} active A binding, telling whether or not this tab is selected. - * @param {boolean=} disabled A binding, telling whether or not this tab is disabled. - * - * @description - * Creates a tab with a heading and content. Must be placed within a {@link ui.bootstrap.tabs.directive:tabset tabset}. - * - * @example - - -
      - - -
      - - First Tab - - Alert me! - Second Tab, with alert callback and html heading! - - - {{item.content}} - - -
      -
      - - function TabsDemoCtrl($scope) { - $scope.items = [ - { title:"Dynamic Title 1", content:"Dynamic Item 0" }, - { title:"Dynamic Title 2", content:"Dynamic Item 1", disabled: true } - ]; - - $scope.alertMe = function() { - setTimeout(function() { - alert("You've selected the alert tab!"); - }); - }; - }; - -
      - */ - -/** - * @ngdoc directive - * @name ui.bootstrap.tabs.directive:tabHeading - * @restrict EA - * - * @description - * Creates an HTML heading for a {@link ui.bootstrap.tabs.directive:tab tab}. Must be placed as a child of a tab element. - * - * @example - - - - - HTML in my titles?! - And some content, too! - - - Icon heading?!? - That's right. - - - - - */ -.directive('tab', ['$parse', function($parse) { - return { - require: '^tabset', - restrict: 'EA', - replace: true, - templateUrl: 'template/tabs/tab.html', - transclude: true, - scope: { - active: '=?', - heading: '@', - onSelect: '&select', //This callback is called in contentHeadingTransclude - //once it inserts the tab's content into the dom - onDeselect: '&deselect' - }, - controller: function() { - //Empty controller so other directives can require being 'under' a tab - }, - compile: function(elm, attrs, transclude) { - return function postLink(scope, elm, attrs, tabsetCtrl) { - scope.$watch('active', function(active) { - if (active) { - tabsetCtrl.select(scope); - } - }); - - scope.disabled = false; - if ( attrs.disabled ) { - scope.$parent.$watch($parse(attrs.disabled), function(value) { - scope.disabled = !! value; - }); - } - - scope.select = function() { - if ( !scope.disabled ) { - scope.active = true; - } - }; - - tabsetCtrl.addTab(scope); - scope.$on('$destroy', function() { - tabsetCtrl.removeTab(scope); - }); - - //We need to transclude later, once the content container is ready. - //when this link happens, we're inside a tab heading. - scope.$transcludeFn = transclude; - }; - } - }; -}]) - -.directive('tabHeadingTransclude', [function() { - return { - restrict: 'A', - require: '^tab', - link: function(scope, elm, attrs, tabCtrl) { - scope.$watch('headingElement', function updateHeadingElement(heading) { - if (heading) { - elm.html(''); - elm.append(heading); - } - }); - } - }; -}]) - -.directive('tabContentTransclude', function() { - return { - restrict: 'A', - require: '^tabset', - link: function(scope, elm, attrs) { - var tab = scope.$eval(attrs.tabContentTransclude); - - //Now our tab is ready to be transcluded: both the tab heading area - //and the tab content area are loaded. Transclude 'em both. - tab.$transcludeFn(tab.$parent, function(contents) { - angular.forEach(contents, function(node) { - if (isTabHeading(node)) { - //Let tabHeadingTransclude know. - tab.headingElement = node; - } else { - elm.append(node); - } - }); - }); - } - }; - function isTabHeading(node) { - return node.tagName && ( - node.hasAttribute('tab-heading') || - node.hasAttribute('data-tab-heading') || - node.tagName.toLowerCase() === 'tab-heading' || - node.tagName.toLowerCase() === 'data-tab-heading' - ); - } -}) - -; - -angular.module('ui.bootstrap.timepicker', []) - -.constant('timepickerConfig', { - hourStep: 1, - minuteStep: 1, - showMeridian: true, - meridians: null, - readonlyInput: false, - mousewheel: true -}) - -.controller('TimepickerController', ['$scope', '$attrs', '$parse', '$log', '$locale', 'timepickerConfig', function($scope, $attrs, $parse, $log, $locale, timepickerConfig) { - var selected = new Date(), - ngModelCtrl = { $setViewValue: angular.noop }, // nullModelCtrl - meridians = angular.isDefined($attrs.meridians) ? $scope.$parent.$eval($attrs.meridians) : timepickerConfig.meridians || $locale.DATETIME_FORMATS.AMPMS; - - this.init = function( ngModelCtrl_, inputs ) { - ngModelCtrl = ngModelCtrl_; - ngModelCtrl.$render = this.render; - - var hoursInputEl = inputs.eq(0), - minutesInputEl = inputs.eq(1); - - var mousewheel = angular.isDefined($attrs.mousewheel) ? $scope.$parent.$eval($attrs.mousewheel) : timepickerConfig.mousewheel; - if ( mousewheel ) { - this.setupMousewheelEvents( hoursInputEl, minutesInputEl ); - } - - $scope.readonlyInput = angular.isDefined($attrs.readonlyInput) ? $scope.$parent.$eval($attrs.readonlyInput) : timepickerConfig.readonlyInput; - this.setupInputEvents( hoursInputEl, minutesInputEl ); - }; - - var hourStep = timepickerConfig.hourStep; - if ($attrs.hourStep) { - $scope.$parent.$watch($parse($attrs.hourStep), function(value) { - hourStep = parseInt(value, 10); - }); - } - - var minuteStep = timepickerConfig.minuteStep; - if ($attrs.minuteStep) { - $scope.$parent.$watch($parse($attrs.minuteStep), function(value) { - minuteStep = parseInt(value, 10); - }); - } - - // 12H / 24H mode - $scope.showMeridian = timepickerConfig.showMeridian; - if ($attrs.showMeridian) { - $scope.$parent.$watch($parse($attrs.showMeridian), function(value) { - $scope.showMeridian = !!value; - - if ( ngModelCtrl.$error.time ) { - // Evaluate from template - var hours = getHoursFromTemplate(), minutes = getMinutesFromTemplate(); - if (angular.isDefined( hours ) && angular.isDefined( minutes )) { - selected.setHours( hours ); - refresh(); - } - } else { - updateTemplate(); - } - }); - } - - // Get $scope.hours in 24H mode if valid - function getHoursFromTemplate ( ) { - var hours = parseInt( $scope.hours, 10 ); - var valid = ( $scope.showMeridian ) ? (hours > 0 && hours < 13) : (hours >= 0 && hours < 24); - if ( !valid ) { - return undefined; - } - - if ( $scope.showMeridian ) { - if ( hours === 12 ) { - hours = 0; - } - if ( $scope.meridian === meridians[1] ) { - hours = hours + 12; - } - } - return hours; - } - - function getMinutesFromTemplate() { - var minutes = parseInt($scope.minutes, 10); - return ( minutes >= 0 && minutes < 60 ) ? minutes : undefined; - } - - function pad( value ) { - return ( angular.isDefined(value) && value.toString().length < 2 ) ? '0' + value : value; - } - - // Respond on mousewheel spin - this.setupMousewheelEvents = function( hoursInputEl, minutesInputEl ) { - var isScrollingUp = function(e) { - if (e.originalEvent) { - e = e.originalEvent; - } - //pick correct delta variable depending on event - var delta = (e.wheelDelta) ? e.wheelDelta : -e.deltaY; - return (e.detail || delta > 0); - }; - - hoursInputEl.bind('mousewheel wheel', function(e) { - $scope.$apply( (isScrollingUp(e)) ? $scope.incrementHours() : $scope.decrementHours() ); - e.preventDefault(); - }); - - minutesInputEl.bind('mousewheel wheel', function(e) { - $scope.$apply( (isScrollingUp(e)) ? $scope.incrementMinutes() : $scope.decrementMinutes() ); - e.preventDefault(); - }); - - }; - - this.setupInputEvents = function( hoursInputEl, minutesInputEl ) { - if ( $scope.readonlyInput ) { - $scope.updateHours = angular.noop; - $scope.updateMinutes = angular.noop; - return; - } - - var invalidate = function(invalidHours, invalidMinutes) { - ngModelCtrl.$setViewValue( null ); - ngModelCtrl.$setValidity('time', false); - if (angular.isDefined(invalidHours)) { - $scope.invalidHours = invalidHours; - } - if (angular.isDefined(invalidMinutes)) { - $scope.invalidMinutes = invalidMinutes; - } - }; - - $scope.updateHours = function() { - var hours = getHoursFromTemplate(); - - if ( angular.isDefined(hours) ) { - selected.setHours( hours ); - refresh( 'h' ); - } else { - invalidate(true); - } - }; - - hoursInputEl.bind('blur', function(e) { - if ( !$scope.invalidHours && $scope.hours < 10) { - $scope.$apply( function() { - $scope.hours = pad( $scope.hours ); - }); - } - }); - - $scope.updateMinutes = function() { - var minutes = getMinutesFromTemplate(); - - if ( angular.isDefined(minutes) ) { - selected.setMinutes( minutes ); - refresh( 'm' ); - } else { - invalidate(undefined, true); - } - }; - - minutesInputEl.bind('blur', function(e) { - if ( !$scope.invalidMinutes && $scope.minutes < 10 ) { - $scope.$apply( function() { - $scope.minutes = pad( $scope.minutes ); - }); - } - }); - - }; - - this.render = function() { - var date = ngModelCtrl.$modelValue ? new Date( ngModelCtrl.$modelValue ) : null; - - if ( isNaN(date) ) { - ngModelCtrl.$setValidity('time', false); - $log.error('Timepicker directive: "ng-model" value must be a Date object, a number of milliseconds since 01.01.1970 or a string representing an RFC2822 or ISO 8601 date.'); - } else { - if ( date ) { - selected = date; - } - makeValid(); - updateTemplate(); - } - }; - - // Call internally when we know that model is valid. - function refresh( keyboardChange ) { - makeValid(); - ngModelCtrl.$setViewValue( new Date(selected) ); - updateTemplate( keyboardChange ); - } - - function makeValid() { - ngModelCtrl.$setValidity('time', true); - $scope.invalidHours = false; - $scope.invalidMinutes = false; - } - - function updateTemplate( keyboardChange ) { - var hours = selected.getHours(), minutes = selected.getMinutes(); - - if ( $scope.showMeridian ) { - hours = ( hours === 0 || hours === 12 ) ? 12 : hours % 12; // Convert 24 to 12 hour system - } - - $scope.hours = keyboardChange === 'h' ? hours : pad(hours); - $scope.minutes = keyboardChange === 'm' ? minutes : pad(minutes); - $scope.meridian = selected.getHours() < 12 ? meridians[0] : meridians[1]; - } - - function addMinutes( minutes ) { - var dt = new Date( selected.getTime() + minutes * 60000 ); - selected.setHours( dt.getHours(), dt.getMinutes() ); - refresh(); - } - - $scope.incrementHours = function() { - addMinutes( hourStep * 60 ); - }; - $scope.decrementHours = function() { - addMinutes( - hourStep * 60 ); - }; - $scope.incrementMinutes = function() { - addMinutes( minuteStep ); - }; - $scope.decrementMinutes = function() { - addMinutes( - minuteStep ); - }; - $scope.toggleMeridian = function() { - addMinutes( 12 * 60 * (( selected.getHours() < 12 ) ? 1 : -1) ); - }; -}]) - -.directive('timepicker', function () { - return { - restrict: 'EA', - require: ['timepicker', '?^ngModel'], - controller:'TimepickerController', - replace: true, - scope: {}, - templateUrl: 'template/timepicker/timepicker.html', - link: function(scope, element, attrs, ctrls) { - var timepickerCtrl = ctrls[0], ngModelCtrl = ctrls[1]; - - if ( ngModelCtrl ) { - timepickerCtrl.init( ngModelCtrl, element.find('input') ); - } - } - }; -}); - -angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position', 'ui.bootstrap.bindHtml']) - -/** - * A helper service that can parse typeahead's syntax (string provided by users) - * Extracted to a separate service for ease of unit testing - */ - .factory('typeaheadParser', ['$parse', function ($parse) { - - // 00000111000000000000022200000000000000003333333333333330000000000044000 - var TYPEAHEAD_REGEXP = /^\s*([\s\S]+?)(?:\s+as\s+([\s\S]+?))?\s+for\s+(?:([\$\w][\$\w\d]*))\s+in\s+([\s\S]+?)$/; - - return { - parse:function (input) { - - var match = input.match(TYPEAHEAD_REGEXP); - if (!match) { - throw new Error( - 'Expected typeahead specification in form of "_modelValue_ (as _label_)? for _item_ in _collection_"' + - ' but got "' + input + '".'); - } - - return { - itemName:match[3], - source:$parse(match[4]), - viewMapper:$parse(match[2] || match[1]), - modelMapper:$parse(match[1]) - }; - } - }; -}]) - - .directive('typeahead', ['$compile', '$parse', '$q', '$timeout', '$document', '$position', 'typeaheadParser', - function ($compile, $parse, $q, $timeout, $document, $position, typeaheadParser) { - - var HOT_KEYS = [9, 13, 27, 38, 40]; - - return { - require:'ngModel', - link:function (originalScope, element, attrs, modelCtrl) { - - //SUPPORTED ATTRIBUTES (OPTIONS) - - //minimal no of characters that needs to be entered before typeahead kicks-in - var minSearch = originalScope.$eval(attrs.typeaheadMinLength) || 1; - - //minimal wait time after last character typed before typehead kicks-in - var waitTime = originalScope.$eval(attrs.typeaheadWaitMs) || 0; - - //should it restrict model values to the ones selected from the popup only? - var isEditable = originalScope.$eval(attrs.typeaheadEditable) !== false; - - //binding to a variable that indicates if matches are being retrieved asynchronously - var isLoadingSetter = $parse(attrs.typeaheadLoading).assign || angular.noop; - - //a callback executed when a match is selected - var onSelectCallback = $parse(attrs.typeaheadOnSelect); - - var inputFormatter = attrs.typeaheadInputFormatter ? $parse(attrs.typeaheadInputFormatter) : undefined; - - var appendToBody = attrs.typeaheadAppendToBody ? originalScope.$eval(attrs.typeaheadAppendToBody) : false; - - var focusFirst = originalScope.$eval(attrs.typeaheadFocusFirst) !== false; - - //INTERNAL VARIABLES - - //model setter executed upon match selection - var $setModelValue = $parse(attrs.ngModel).assign; - - //expressions used by typeahead - var parserResult = typeaheadParser.parse(attrs.typeahead); - - var hasFocus; - - //create a child scope for the typeahead directive so we are not polluting original scope - //with typeahead-specific data (matches, query etc.) - var scope = originalScope.$new(); - originalScope.$on('$destroy', function(){ - scope.$destroy(); - }); - - // WAI-ARIA - var popupId = 'typeahead-' + scope.$id + '-' + Math.floor(Math.random() * 10000); - element.attr({ - 'aria-autocomplete': 'list', - 'aria-expanded': false, - 'aria-owns': popupId - }); - - //pop-up element used to display matches - var popUpEl = angular.element('
      '); - popUpEl.attr({ - id: popupId, - matches: 'matches', - active: 'activeIdx', - select: 'select(activeIdx)', - query: 'query', - position: 'position' - }); - //custom item template - if (angular.isDefined(attrs.typeaheadTemplateUrl)) { - popUpEl.attr('template-url', attrs.typeaheadTemplateUrl); - } - - var resetMatches = function() { - scope.matches = []; - scope.activeIdx = -1; - element.attr('aria-expanded', false); - }; - - var getMatchId = function(index) { - return popupId + '-option-' + index; - }; - - // Indicate that the specified match is the active (pre-selected) item in the list owned by this typeahead. - // This attribute is added or removed automatically when the `activeIdx` changes. - scope.$watch('activeIdx', function(index) { - if (index < 0) { - element.removeAttr('aria-activedescendant'); - } else { - element.attr('aria-activedescendant', getMatchId(index)); - } - }); - - var getMatchesAsync = function(inputValue) { - - var locals = {$viewValue: inputValue}; - isLoadingSetter(originalScope, true); - $q.when(parserResult.source(originalScope, locals)).then(function(matches) { - - //it might happen that several async queries were in progress if a user were typing fast - //but we are interested only in responses that correspond to the current view value - var onCurrentRequest = (inputValue === modelCtrl.$viewValue); - if (onCurrentRequest && hasFocus) { - if (matches.length > 0) { - - scope.activeIdx = focusFirst ? 0 : -1; - scope.matches.length = 0; - - //transform labels - for(var i=0; i= minSearch) { - if (waitTime > 0) { - cancelPreviousTimeout(); - scheduleSearchWithTimeout(inputValue); - } else { - getMatchesAsync(inputValue); - } - } else { - isLoadingSetter(originalScope, false); - cancelPreviousTimeout(); - resetMatches(); - } - - if (isEditable) { - return inputValue; - } else { - if (!inputValue) { - // Reset in case user had typed something previously. - modelCtrl.$setValidity('editable', true); - return inputValue; - } else { - modelCtrl.$setValidity('editable', false); - return undefined; - } - } - }); - - modelCtrl.$formatters.push(function (modelValue) { - - var candidateViewValue, emptyViewValue; - var locals = {}; - - if (inputFormatter) { - - locals.$model = modelValue; - return inputFormatter(originalScope, locals); - - } else { - - //it might happen that we don't have enough info to properly render input value - //we need to check for this situation and simply return model value if we can't apply custom formatting - locals[parserResult.itemName] = modelValue; - candidateViewValue = parserResult.viewMapper(originalScope, locals); - locals[parserResult.itemName] = undefined; - emptyViewValue = parserResult.viewMapper(originalScope, locals); - - return candidateViewValue!== emptyViewValue ? candidateViewValue : modelValue; - } - }); - - scope.select = function (activeIdx) { - //called from within the $digest() cycle - var locals = {}; - var model, item; - - locals[parserResult.itemName] = item = scope.matches[activeIdx].model; - model = parserResult.modelMapper(originalScope, locals); - $setModelValue(originalScope, model); - modelCtrl.$setValidity('editable', true); - - onSelectCallback(originalScope, { - $item: item, - $model: model, - $label: parserResult.viewMapper(originalScope, locals) - }); - - resetMatches(); - - //return focus to the input element if a match was selected via a mouse click event - // use timeout to avoid $rootScope:inprog error - $timeout(function() { element[0].focus(); }, 0, false); - }; - - //bind keyboard events: arrows up(38) / down(40), enter(13) and tab(9), esc(27) - element.bind('keydown', function (evt) { - - //typeahead is open and an "interesting" key was pressed - if (scope.matches.length === 0 || HOT_KEYS.indexOf(evt.which) === -1) { - return; - } - - // if there's nothing selected (i.e. focusFirst) and enter is hit, don't do anything - if (scope.activeIdx == -1 && (evt.which === 13 || evt.which === 9)) { - return; - } - - evt.preventDefault(); - - if (evt.which === 40) { - scope.activeIdx = (scope.activeIdx + 1) % scope.matches.length; - scope.$digest(); - - } else if (evt.which === 38) { - scope.activeIdx = (scope.activeIdx > 0 ? scope.activeIdx : scope.matches.length) - 1; - scope.$digest(); - - } else if (evt.which === 13 || evt.which === 9) { - scope.$apply(function () { - scope.select(scope.activeIdx); - }); - - } else if (evt.which === 27) { - evt.stopPropagation(); - - resetMatches(); - scope.$digest(); - } - }); - - element.bind('blur', function (evt) { - hasFocus = false; - }); - - // Keep reference to click handler to unbind it. - var dismissClickHandler = function (evt) { - if (element[0] !== evt.target) { - resetMatches(); - scope.$digest(); - } - }; - - $document.bind('click', dismissClickHandler); - - originalScope.$on('$destroy', function(){ - $document.unbind('click', dismissClickHandler); - if (appendToBody) { - $popup.remove(); - } - }); - - var $popup = $compile(popUpEl)(scope); - if (appendToBody) { - $document.find('body').append($popup); - } else { - element.after($popup); - } - } - }; - -}]) - - .directive('typeaheadPopup', function () { - return { - restrict:'EA', - scope:{ - matches:'=', - query:'=', - active:'=', - position:'=', - select:'&' - }, - replace:true, - templateUrl:'template/typeahead/typeahead-popup.html', - link:function (scope, element, attrs) { - - scope.templateUrl = attrs.templateUrl; - - scope.isOpen = function () { - return scope.matches.length > 0; - }; - - scope.isActive = function (matchIdx) { - return scope.active == matchIdx; - }; - - scope.selectActive = function (matchIdx) { - scope.active = matchIdx; - }; - - scope.selectMatch = function (activeIdx) { - scope.select({activeIdx:activeIdx}); - }; - } - }; - }) - - .directive('typeaheadMatch', ['$http', '$templateCache', '$compile', '$parse', function ($http, $templateCache, $compile, $parse) { - return { - restrict:'EA', - scope:{ - index:'=', - match:'=', - query:'=' - }, - link:function (scope, element, attrs) { - var tplUrl = $parse(attrs.templateUrl)(scope.$parent) || 'template/typeahead/typeahead-match.html'; - $http.get(tplUrl, {cache: $templateCache}).success(function(tplContent){ - element.replaceWith($compile(tplContent.trim())(scope)); - }); - } - }; - }]) - - .filter('typeaheadHighlight', function() { - - function escapeRegexp(queryToEscape) { - return queryToEscape.replace(/([.?*+^$[\]\\(){}|-])/g, '\\$1'); - } - - return function(matchItem, query) { - return query ? ('' + matchItem).replace(new RegExp(escapeRegexp(query), 'gi'), '$&') : matchItem; - }; - }); - -angular.module("template/alert/alert.html", []).run(["$templateCache", function($templateCache) { - $templateCache.put("template/alert/alert.html", - "
      \n" + - " \n" + - "
      \n" + - "
      \n" + - ""); -}]); - -angular.module("template/carousel/carousel.html", []).run(["$templateCache", function($templateCache) { - $templateCache.put("template/carousel/carousel.html", - "
      \n" + - "
        1\">\n" + - "
      1. \n" + - "
      \n" + - "
      \n" + - " 1\">\n" + - " 1\">\n" + - "
      \n" + - ""); -}]); - -angular.module("template/carousel/slide.html", []).run(["$templateCache", function($templateCache) { - $templateCache.put("template/carousel/slide.html", - "
      \n" + - ""); -}]); - -angular.module("template/datepicker/datepicker.html", []).run(["$templateCache", function($templateCache) { - $templateCache.put("template/datepicker/datepicker.html", - "
      \n" + - " \n" + - " \n" + - " \n" + - "
      "); -}]); - -angular.module("template/datepicker/day.html", []).run(["$templateCache", function($templateCache) { - $templateCache.put("template/datepicker/day.html", - "\n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - "
      {{label.abbr}}
      {{ weekNumbers[$index] }}\n" + - " \n" + - "
      \n" + - ""); -}]); - -angular.module("template/datepicker/month.html", []).run(["$templateCache", function($templateCache) { - $templateCache.put("template/datepicker/month.html", - "\n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - "
      \n" + - " \n" + - "
      \n" + - ""); -}]); - -angular.module("template/datepicker/popup.html", []).run(["$templateCache", function($templateCache) { - $templateCache.put("template/datepicker/popup.html", - "
        \n" + - "
      • \n" + - "
      • \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - "
      • \n" + - "
      \n" + - ""); -}]); - -angular.module("template/datepicker/year.html", []).run(["$templateCache", function($templateCache) { - $templateCache.put("template/datepicker/year.html", - "\n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - "
      \n" + - " \n" + - "
      \n" + - ""); -}]); - -angular.module("template/modal/backdrop.html", []).run(["$templateCache", function($templateCache) { - $templateCache.put("template/modal/backdrop.html", - "
      \n" + - ""); -}]); - -angular.module("template/modal/window.html", []).run(["$templateCache", function($templateCache) { - $templateCache.put("template/modal/window.html", - "
      \n" + - "
      \n" + - "
      "); -}]); - -angular.module("template/pagination/pager.html", []).run(["$templateCache", function($templateCache) { - $templateCache.put("template/pagination/pager.html", - ""); -}]); - -angular.module("template/pagination/pagination.html", []).run(["$templateCache", function($templateCache) { - $templateCache.put("template/pagination/pagination.html", - ""); -}]); - -angular.module("template/tooltip/tooltip-html-unsafe-popup.html", []).run(["$templateCache", function($templateCache) { - $templateCache.put("template/tooltip/tooltip-html-unsafe-popup.html", - "
      \n" + - "
      \n" + - "
      \n" + - "
      \n" + - ""); -}]); - -angular.module("template/tooltip/tooltip-popup.html", []).run(["$templateCache", function($templateCache) { - $templateCache.put("template/tooltip/tooltip-popup.html", - "
      \n" + - "
      \n" + - "
      \n" + - "
      \n" + - ""); -}]); - -angular.module("template/popover/popover.html", []).run(["$templateCache", function($templateCache) { - $templateCache.put("template/popover/popover.html", - "
      \n" + - "
      \n" + - "\n" + - "
      \n" + - "

      \n" + - "
      \n" + - "
      \n" + - "
      \n" + - ""); -}]); - -angular.module("template/progressbar/bar.html", []).run(["$templateCache", function($templateCache) { - $templateCache.put("template/progressbar/bar.html", - "
      "); -}]); - -angular.module("template/progressbar/progress.html", []).run(["$templateCache", function($templateCache) { - $templateCache.put("template/progressbar/progress.html", - "
      "); -}]); - -angular.module("template/progressbar/progressbar.html", []).run(["$templateCache", function($templateCache) { - $templateCache.put("template/progressbar/progressbar.html", - "
      \n" + - "
      \n" + - "
      "); -}]); - -angular.module("template/rating/rating.html", []).run(["$templateCache", function($templateCache) { - $templateCache.put("template/rating/rating.html", - "\n" + - " \n" + - " ({{ $index < value ? '*' : ' ' }})\n" + - " \n" + - ""); -}]); - -angular.module("template/tabs/tab.html", []).run(["$templateCache", function($templateCache) { - $templateCache.put("template/tabs/tab.html", - "
    • \n" + - " {{heading}}\n" + - "
    • \n" + - ""); -}]); - -angular.module("template/tabs/tabset.html", []).run(["$templateCache", function($templateCache) { - $templateCache.put("template/tabs/tabset.html", - "
      \n" + - "
        \n" + - "
        \n" + - "
        \n" + - "
        \n" + - "
        \n" + - "
        \n" + - ""); -}]); - -angular.module("template/timepicker/timepicker.html", []).run(["$templateCache", function($templateCache) { - $templateCache.put("template/timepicker/timepicker.html", - "\n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - "
         
        \n" + - " \n" + - " :\n" + - " \n" + - "
         
        \n" + - ""); -}]); - -angular.module("template/typeahead/typeahead-match.html", []).run(["$templateCache", function($templateCache) { - $templateCache.put("template/typeahead/typeahead-match.html", - ""); -}]); - -angular.module("template/typeahead/typeahead-popup.html", []).run(["$templateCache", function($templateCache) { - $templateCache.put("template/typeahead/typeahead-popup.html", - "
          \n" + - "
        • \n" + - "
          \n" + - "
        • \n" + - "
        \n" + - ""); -}]); diff --git a/webpackShims/ui-bootstrap.js b/webpackShims/ui-bootstrap.js index 5cc959f2a2dc1b..92ddecab13d05e 100644 --- a/webpackShims/ui-bootstrap.js +++ b/webpackShims/ui-bootstrap.js @@ -1,6 +1,6 @@ define(function (require) { require('angular'); - require('ui/angular-bootstrap/ui-bootstrap-tpls'); + require('ui/angular-bootstrap/index'); return require('ui/modules') .get('kibana', ['ui.bootstrap']) From ce7b8e052f6ecdeb7ba40b2fefa536254817d641 Mon Sep 17 00:00:00 2001 From: Shaunak Kashyap Date: Tue, 5 Jul 2016 12:38:22 -0700 Subject: [PATCH 30/67] Reverting a change made in ea468f9dd60a64eacddaef0878322576712c7c09 This can be done because of better defaulting of non-function values, implemented in 0b0280c103bc72eb47986f62160b4eb31b03a3bb and ed0c48a68028fbb85c495c5050c7b20dc117b6f6. --- src/plugins/console/public/src/directives/sense_navbar.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugins/console/public/src/directives/sense_navbar.js b/src/plugins/console/public/src/directives/sense_navbar.js index edca4494e36bf1..a82377758b8e35 100644 --- a/src/plugins/console/public/src/directives/sense_navbar.js +++ b/src/plugins/console/public/src/directives/sense_navbar.js @@ -24,7 +24,7 @@ require('ui/modules') this.menu = new KbnTopNavController([ { key: 'welcome', - hideButton: () => true, + hideButton: true, template: `` }, { From 8adaecb9be22f9ba784825bbf6f0ac3bd52d1fa7 Mon Sep 17 00:00:00 2001 From: spalger Date: Tue, 5 Jul 2016 12:46:53 -0700 Subject: [PATCH 31/67] [ci] source the setup script instead of executing it --- test/scripts/{jenkins_setup.sh => _jenkins_setup.sh} | 2 -- test/scripts/jenkins_build_snapshot.sh | 5 ++--- test/scripts/jenkins_selenium.sh | 3 +-- test/scripts/jenkins_unit.sh | 5 ++--- 4 files changed, 5 insertions(+), 10 deletions(-) rename test/scripts/{jenkins_setup.sh => _jenkins_setup.sh} (95%) diff --git a/test/scripts/jenkins_setup.sh b/test/scripts/_jenkins_setup.sh similarity index 95% rename from test/scripts/jenkins_setup.sh rename to test/scripts/_jenkins_setup.sh index b023f5891d0fec..850bfd91c364dd 100755 --- a/test/scripts/jenkins_setup.sh +++ b/test/scripts/_jenkins_setup.sh @@ -37,5 +37,3 @@ if [ -z "$(npm bin)" ]; then echo "npm does not know where it stores executables..... huh??" exit 1 fi - -export GRUNT="$(npm bin)/grunt" diff --git a/test/scripts/jenkins_build_snapshot.sh b/test/scripts/jenkins_build_snapshot.sh index e20e1bece1113f..ffba35160afddc 100755 --- a/test/scripts/jenkins_build_snapshot.sh +++ b/test/scripts/jenkins_build_snapshot.sh @@ -1,7 +1,6 @@ #!/usr/bin/env bash set -e +source "$(dirname $0)/_jenkins_setup.sh" -"$(dirname $0)/jenkins_setup.sh" - -"$GRUNT" build; +"$(npm bin)/grunt" build; diff --git a/test/scripts/jenkins_selenium.sh b/test/scripts/jenkins_selenium.sh index be4e95d9027363..58224b32909b9e 100755 --- a/test/scripts/jenkins_selenium.sh +++ b/test/scripts/jenkins_selenium.sh @@ -1,7 +1,6 @@ #!/usr/bin/env bash set -e - -"$(dirname $0)/jenkins_setup.sh" +source "$(dirname $0)/_jenkins_setup.sh" xvfb-run "$(npm bin)/grunt" jenkins:selenium; diff --git a/test/scripts/jenkins_unit.sh b/test/scripts/jenkins_unit.sh index 44ff95d4d0f097..ebeceabed8eeb6 100755 --- a/test/scripts/jenkins_unit.sh +++ b/test/scripts/jenkins_unit.sh @@ -1,7 +1,6 @@ #!/usr/bin/env bash set -e +source "$(dirname $0)/_jenkins_setup.sh" -"$(dirname $0)/jenkins_setup.sh" - -xvfb-run "$GRUNT" jenkins:unit; +xvfb-run "$(npm bin)/grunt" jenkins:unit; From 03866c129241299c1a66aa9f1eabaffd745da179 Mon Sep 17 00:00:00 2001 From: Jonathan Budzenski Date: Tue, 24 May 2016 16:45:25 -0500 Subject: [PATCH 32/67] [build] update pleaserun path to be compatible with 0.0.22+ --- tasks/build/os_packages.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tasks/build/os_packages.js b/tasks/build/os_packages.js index 5c8b0e8919f2d9..e57bedc3f0372c 100644 --- a/tasks/build/os_packages.js +++ b/tasks/build/os_packages.js @@ -62,7 +62,7 @@ export default (grunt) => { `${buildDir}/config/=${packages.path.conf}/`, `${buildDir}/data/=${packages.path.data}/`, `${servicesByName.sysv.outputDir}/etc/=/etc/`, - `${servicesByName.systemd.outputDir}/lib/=/lib/` + `${servicesByName.systemd.outputDir}/etc/=/etc/` ]; //Manually find flags, multiple args without assignment are not entirely parsed From 315fc5dc91d4c389845d1cf1def4e952bebce942 Mon Sep 17 00:00:00 2001 From: Jonathan Budzenski Date: Fri, 27 May 2016 10:50:47 -0500 Subject: [PATCH 33/67] [build] update pleaserun version in docs --- CONTRIBUTING.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a87a854684c915..4b16b2be48dc8c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -171,8 +171,8 @@ Execute the front-end browser tests. This requires the server started by the `te Packages are built using fpm, pleaserun, dpkg, and rpm. fpm and pleaserun can be installed using gem. Package building has only been tested on Linux and is not supported on any other platform. ```sh apt-get install ruby-dev rpm -gem install fpm -v 1.5.0 # required by pleaserun 0.0.16 -gem install pleaserun -v 0.0.16 # higher versions fail at the moment +gem install fpm -v 1.5.0 +gem install pleaserun -v 0.0.24 npm run build -- --skip-archives ``` From c8b4d2de2166cfe8bb14852921b4eb1503ba11a6 Mon Sep 17 00:00:00 2001 From: Shaunak Kashyap Date: Tue, 5 Jul 2016 15:14:29 -0700 Subject: [PATCH 34/67] Return early from function --- src/ui/public/chrome/api/nav.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/ui/public/chrome/api/nav.js b/src/ui/public/chrome/api/nav.js index 441658f84983c0..f78ea3a3641623 100644 --- a/src/ui/public/chrome/api/nav.js +++ b/src/ui/public/chrome/api/nav.js @@ -8,11 +8,10 @@ export default function (chrome, internals) { chrome.getNavLinkById = (id) => { const navLink = internals.nav.find(link => link.id === id); - if (navLink) { - return navLink; - } else { + if (!navLink) { throw new Error(`Nav link for id = ${id} not found`); } + return navLink; }; chrome.getBasePath = function () { From dfaf777eef953a733ce2571a0b38aaf0ee0c36a1 Mon Sep 17 00:00:00 2001 From: Jonathan Budzenski Date: Tue, 5 Jul 2016 17:16:33 -0500 Subject: [PATCH 35/67] [folder structure] Cleanup missing folder changes, update docs --- docs/plugins.asciidoc | 30 +++++++++++++++--------------- src/cli/cluster/cluster_manager.js | 2 +- src/optimize/base_optimizer.js | 2 +- 3 files changed, 17 insertions(+), 17 deletions(-) diff --git a/docs/plugins.asciidoc b/docs/plugins.asciidoc index f1548b0ddd6322..76760dd81d8c70 100644 --- a/docs/plugins.asciidoc +++ b/docs/plugins.asciidoc @@ -1,9 +1,9 @@ [[kibana-plugins]] == Kibana Plugins -Add-on functionality for Kibana is implemented with plug-in modules. You can use the `bin/kibana-plugin` -command to manage these modules. You can also install a plugin manually by moving the plugin file to the -`installedPlugins` directory and unpacking the plugin files into a new directory. +Add-on functionality for Kibana is implemented with plug-in modules. You can use the `bin/kibana-plugin` +command to manage these modules. You can also install a plugin manually by moving the plugin file to the +`plugins` directory and unpacking the plugin files into a new directory. A list of existing Kibana plugins is available on https://github.com/elastic/kibana/wiki/Known-Plugins[GitHub]. @@ -38,7 +38,7 @@ You can specify URLs that use the HTTP, HTTPS, or `file` protocols. [float] === Installing Plugins to an Arbitrary Directory -Use the `-d` or `--plugin-dir` option after the `install` command to specify a directory for plugins, as in the following +Use the `-d` or `--plugin-dir` option after the `install` command to specify a directory for plugins, as in the following example: [source,shell] @@ -63,7 +63,7 @@ Use the `remove` command to remove a plugin, including any configuration informa [source,shell] $ bin/kibana-plugin remove timelion -You can also remove a plugin manually by deleting the plugin's subdirectory under the `installedPlugins/` directory. +You can also remove a plugin manually by deleting the plugin's subdirectory under the `plugins/` directory. [float] === Listing Installed Plugins @@ -78,9 +78,9 @@ To update a plugin, remove the current version and reinstall the plugin. [float] === Configuring the Plugin Manager -By default, the plugin manager provides you with feedback on the status of the activity you've asked the plugin manager -to perform. You can control the level of feedback for the `install` and `remove` commands with the `--quiet` and -`--silent` options. Use the `--quiet` option to suppress all non-error output. Use the `--silent` option to suppress all +By default, the plugin manager provides you with feedback on the status of the activity you've asked the plugin manager +to perform. You can control the level of feedback for the `install` and `remove` commands with the `--quiet` and +`--silent` options. Use the `--quiet` option to suppress all non-error output. Use the `--silent` option to suppress all output. By default, plugin manager installation requests do not time out. Use the `--timeout` option, followed by a time, to @@ -88,18 +88,18 @@ change this behavior, as in the following examples: [source,shell] .Waits for 30 seconds before failing -bin/kibana-plugin install --timeout 30s sample-plugin +bin/kibana-plugin install --timeout 30s sample-plugin [source,shell] .Waits for 1 minute before failing -bin/kibana-plugin install --timeout 1m sample-plugin +bin/kibana-plugin install --timeout 1m sample-plugin [float] ==== Plugins and Custom Kibana Configurations -Use the `-c` or `--config` options with the `install` and `remove` commands to specify the path to the configuration file -used to start Kibana. By default, Kibana uses the configuration file `config/kibana.yml`. When you change your installed -plugins, the `bin/kibana-plugin` command restarts the Kibana server. When you are using a customized configuration file, +Use the `-c` or `--config` options with the `install` and `remove` commands to specify the path to the configuration file +used to start Kibana. By default, Kibana uses the configuration file `config/kibana.yml`. When you change your installed +plugins, the `bin/kibana-plugin` command restarts the Kibana server. When you are using a customized configuration file, you must specify the path to that configuration file each time you use the `bin/kibana-plugin` command. [float] @@ -115,7 +115,7 @@ you must specify the path to that configuration file each time you use the `bin/ [[plugin-switcher]] == Switching Plugin Functionality -The Kibana UI serves as a framework that can contain several different plugins. You can switch between these +The Kibana UI serves as a framework that can contain several different plugins. You can switch between these plugins by clicking the icons for your desired plugins in the left-hand navigation bar. [float] @@ -126,4 +126,4 @@ Use the following command to disable a plugin: [source,shell] ./bin/kibana --.enabled=false -You can find a plugin's plugin ID as the value of the `name` property in the plugin's `package.json` file. \ No newline at end of file +You can find a plugin's plugin ID as the value of the `name` property in the plugin's `package.json` file. diff --git a/src/cli/cluster/cluster_manager.js b/src/cli/cluster/cluster_manager.js index ab7243d9ff78cb..685c64db1246fc 100644 --- a/src/cli/cluster/cluster_manager.js +++ b/src/cli/cluster/cluster_manager.js @@ -87,7 +87,7 @@ module.exports = class ClusterManager { const watchPaths = uniq( [ - fromRoot('src/plugins'), + fromRoot('src/core_plugins'), fromRoot('src/server'), fromRoot('src/ui'), fromRoot('src/utils'), diff --git a/src/optimize/base_optimizer.js b/src/optimize/base_optimizer.js index aa42693a127a9f..5e0e79d3d72a2f 100644 --- a/src/optimize/base_optimizer.js +++ b/src/optimize/base_optimizer.js @@ -114,7 +114,7 @@ class BaseOptimizer { { test: /\.(html|tmpl)$/, loader: 'raw' }, { test: /\.png$/, loader: 'url?limit=10000&name=[path][name].[ext]' }, { test: /\.(woff|woff2|ttf|eot|svg|ico)(\?|$)/, loader: 'file?name=[path][name].[ext]' }, - { test: /[\/\\]src[\/\\](plugins|ui)[\/\\].+\.js$/, loader: `rjs-repack${mapQ}` }, + { test: /[\/\\]src[\/\\](core_plugins|ui)[\/\\].+\.js$/, loader: `rjs-repack${mapQ}` }, { test: /\.js$/, exclude: babelExclude.concat(this.env.noParse), From fe1732ca0a26d18595c1898587498fad6487ad50 Mon Sep 17 00:00:00 2001 From: Shaunak Kashyap Date: Tue, 5 Jul 2016 17:17:20 -0700 Subject: [PATCH 36/67] Show notifier on page load, if notifier params are in query string --- src/core_plugins/kibana/public/kibana.js | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/core_plugins/kibana/public/kibana.js b/src/core_plugins/kibana/public/kibana.js index bf96cf92207d25..63cd2ac289a363 100644 --- a/src/core_plugins/kibana/public/kibana.js +++ b/src/core_plugins/kibana/public/kibana.js @@ -17,6 +17,7 @@ import 'ui/vislib'; import 'ui/agg_response'; import 'ui/agg_types'; import 'ui/timepicker'; +import Notifier from 'ui/notify/notifier'; import 'leaflet'; routes.enable(); @@ -44,3 +45,17 @@ chrome moment.tz.setDefault(tz); } }); + +function showNotifier($location) { + const queryString = $location.search(); + if (queryString.notif_msg) { + const message = queryString.notif_msg; + const config = queryString.notif_loc ? { location: queryString.notif_loc } : {}; + const level = queryString.notif_lvl || 'info'; + + const notifier = new Notifier(config); + notifier[level](message); + } +} + +modules.get('kibana').run(showNotifier); From 2f2742e11b0a9ab4cae4dc2db796b58aa6fca22f Mon Sep 17 00:00:00 2001 From: Court Ewing Date: Fri, 17 Jun 2016 09:20:44 -0400 Subject: [PATCH 37/67] Consistent build archives We do not release zip archives for any operating system except windows, and we do not release tar.gz archives for windows. For consistency and clarity sake, we'll explicitly list the architecture (x86) of the windows build as well. --- README.md | 2 +- tasks/build/archives.js | 10 +++------- tasks/build/download_node_builds.js | 2 +- tasks/config/platforms.js | 4 ++-- 4 files changed, 7 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 502ee787ee0903..365aa34e9aca94 100644 --- a/README.md +++ b/README.md @@ -46,4 +46,4 @@ For the daring, snapshot builds are available. These builds are created after ea | OSX | [tar](http://download.elastic.co/kibana/kibana-snapshot/kibana-5.0.0-alpha5-SNAPSHOT-darwin-x64.tar.gz) | | Linux x64 | [tar](http://download.elastic.co/kibana/kibana-snapshot/kibana-5.0.0-alpha5-SNAPSHOT-linux-x64.tar.gz) [deb](https://download.elastic.co/kibana/kibana-snapshot/kibana-5.0.0-alpha5-SNAPSHOT-amd64.deb) [rpm](https://download.elastic.co/kibana/kibana-snapshot/kibana-5.0.0-alpha5-SNAPSHOT-x86_64.rpm) | | Linux x86 | [tar](http://download.elastic.co/kibana/kibana-snapshot/kibana-5.0.0-alpha5-SNAPSHOT-linux-x86.tar.gz) [deb](https://download.elastic.co/kibana/kibana-snapshot/kibana-5.0.0-alpha5-SNAPSHOT-i386.deb) [rpm](https://download.elastic.co/kibana/kibana-snapshot/kibana-5.0.0-alpha5-SNAPSHOT-i686.rpm) | -| Windows | [zip](http://download.elastic.co/kibana/kibana-snapshot/kibana-5.0.0-alpha5-SNAPSHOT-windows.zip) | +| Windows | [zip](http://download.elastic.co/kibana/kibana-snapshot/kibana-5.0.0-alpha5-SNAPSHOT-windows-x86.zip) | diff --git a/tasks/build/archives.js b/tasks/build/archives.js index 1b8f85ffd7f834..08a09b176b34f7 100644 --- a/tasks/build/archives.js +++ b/tasks/build/archives.js @@ -1,6 +1,6 @@ module.exports = function createPackages(grunt) { let { config } = grunt; - let { resolve, relative } = require('path'); + let { resolve } = require('path'); let { execFile } = require('child_process'); let { all, fromNode } = require('bluebird'); @@ -13,14 +13,10 @@ module.exports = function createPackages(grunt) { let archives = async (platform) => { - // kibana.tar.gz - await exec('tar', ['-zchf', relative(buildPath, platform.tarPath), platform.buildName]); - - // kibana.zip if (/windows/.test(platform.name)) { - await exec('zip', ['-rq', '-ll', relative(buildPath, platform.zipPath), platform.buildName]); + await exec('zip', ['-rq', '-ll', platform.zipPath, platform.buildName]); } else { - await exec('zip', ['-rq', relative(buildPath, platform.zipPath), platform.buildName]); + await exec('tar', ['-zchf', platform.tarPath, platform.buildName]); } }; diff --git a/tasks/build/download_node_builds.js b/tasks/build/download_node_builds.js index f3938549aa4e13..cb74b55cec4466 100644 --- a/tasks/build/download_node_builds.js +++ b/tasks/build/download_node_builds.js @@ -40,7 +40,7 @@ module.exports = function (grunt) { platform.downloadPromise = (async function () { grunt.file.mkdir(downloadDir); - if (platform.name === 'windows') { + if (platform.win) { await fromNode(cb => { resp .pipe(createWriteStream(resolve(downloadDir, 'node.exe'))) diff --git a/tasks/config/platforms.js b/tasks/config/platforms.js index aff25a263b026d..ea80654215f516 100644 --- a/tasks/config/platforms.js +++ b/tasks/config/platforms.js @@ -10,9 +10,9 @@ module.exports = function (grunt) { 'darwin-x64', 'linux-x64', 'linux-x86', - 'windows' + 'windows-x86' ].map(function (name) { - let win = name === 'windows'; + let win = name === 'windows-x86'; let nodeUrl = win ? `${baseUri}/win-x86/node.exe` : `${baseUri}/node-v${nodeVersion}-${name}.tar.gz`; let nodeDir = resolve(rootPath, `.node_binaries/${nodeVersion}/${name}`); From c00e49dd062bc89796c241a56151d65e3f97b18b Mon Sep 17 00:00:00 2001 From: Court Ewing Date: Fri, 17 Jun 2016 09:39:14 -0400 Subject: [PATCH 38/67] internal: Cleanup code in _build:archives task This just a code cleanup - there are no functional changes to this task. --- tasks/build/archives.js | 36 ++++++++++++++++-------------------- 1 file changed, 16 insertions(+), 20 deletions(-) diff --git a/tasks/build/archives.js b/tasks/build/archives.js index 08a09b176b34f7..9d6ff8163317f4 100644 --- a/tasks/build/archives.js +++ b/tasks/build/archives.js @@ -1,34 +1,30 @@ -module.exports = function createPackages(grunt) { - let { config } = grunt; - let { resolve } = require('path'); - let { execFile } = require('child_process'); - let { all, fromNode } = require('bluebird'); +import { execFile } from 'child_process'; +import { all, fromNode } from 'bluebird'; +export default (grunt) => { + const { config, log } = grunt; + + const cwd = config.get('buildDir'); const targetDir = config.get('target'); - let buildPath = resolve(config.get('root'), 'build'); - let exec = async (cmd, args) => { - grunt.log.writeln(` > ${cmd} ${args.join(' ')}`); - await fromNode(cb => execFile(cmd, args, { cwd: buildPath }, cb)); - }; + async function exec(cmd, args) { + log.writeln(` > ${cmd} ${args.join(' ')}`); + await fromNode(cb => execFile(cmd, args, { cwd }, cb)); + }; - let archives = async (platform) => { - if (/windows/.test(platform.name)) { - await exec('zip', ['-rq', '-ll', platform.zipPath, platform.buildName]); + async function archives({ name, buildName, zipPath, tarPath }) { + if (/windows/.test(name)) { + await exec('zip', ['-rq', '-ll', zipPath, buildName]); } else { - await exec('tar', ['-zchf', platform.tarPath, platform.buildName]); + await exec('tar', ['-zchf', tarPath, buildName]); } }; grunt.registerTask('_build:archives', function () { + grunt.file.mkdir(targetDir); all( - grunt.config.get('platforms') - .map(async platform => { - - grunt.file.mkdir(targetDir); - await archives(platform); - }) + config.get('platforms').map(async platform => await archives(platform)) ) .nodeify(this.async()); From e5c5a3be15897961656bbb6bc98894fbabb68a46 Mon Sep 17 00:00:00 2001 From: Shaunak Kashyap Date: Wed, 6 Jul 2016 03:38:39 -0700 Subject: [PATCH 39/67] Remove notification parameters from query string after triggering notification --- src/core_plugins/kibana/public/kibana.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/core_plugins/kibana/public/kibana.js b/src/core_plugins/kibana/public/kibana.js index 63cd2ac289a363..4c05d4684a4fe1 100644 --- a/src/core_plugins/kibana/public/kibana.js +++ b/src/core_plugins/kibana/public/kibana.js @@ -53,6 +53,10 @@ function showNotifier($location) { const config = queryString.notif_loc ? { location: queryString.notif_loc } : {}; const level = queryString.notif_lvl || 'info'; + $location.search('notif_msg', null); + $location.search('notif_loc', null); + $location.search('notif_lvl', null); + const notifier = new Notifier(config); notifier[level](message); } From 59e31539b0742d400024fb9c7599aedc4c16d65d Mon Sep 17 00:00:00 2001 From: Jonathan Budzenski Date: Wed, 6 Jul 2016 11:10:41 -0500 Subject: [PATCH 40/67] [console autocomplete] Update fields->stored_fields, add docvalue_fields --- src/core_plugins/console/api_server/es_5_0/search.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/core_plugins/console/api_server/es_5_0/search.js b/src/core_plugins/console/api_server/es_5_0/search.js index f6f0fc117a1522..dda530f6e904de 100644 --- a/src/core_plugins/console/api_server/es_5_0/search.js +++ b/src/core_plugins/console/api_server/es_5_0/search.js @@ -16,7 +16,7 @@ module.exports = function (api) { _source: "", _source_include: "", _source_exclude: "", - fields: [], + stored_fields: [], sort: "", track_scores: "__flag__", timeout: 1, @@ -120,8 +120,9 @@ module.exports = function (api) { } ] }, - fields: ['{field}'], + stored_fields: ['{field}'], fielddata_fields: ["{field}"], + docvalue_fields: ["{field}"], script_fields: { __template: { 'FIELD': { From a1be97efcd4275517de5670d7ed56b91dff8a4fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20Bevacqua?= Date: Fri, 3 Jun 2016 17:37:19 -0300 Subject: [PATCH 41/67] [settings] Added .get, renamed .getAll as .getRaw, added new .getAll method. --- src/ui/settings/index.js | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/ui/settings/index.js b/src/ui/settings/index.js index fbc17c2075af3e..0f14673370c58e 100644 --- a/src/ui/settings/index.js +++ b/src/ui/settings/index.js @@ -4,7 +4,9 @@ import defaultsProvider from './defaults'; export default function setupSettings(kbnServer, server, config) { const status = kbnServer.status.create('ui settings'); const uiSettings = { + get, getAll, + getRaw, getDefaults, getUserProvided, set, @@ -15,7 +17,23 @@ export default function setupSettings(kbnServer, server, config) { server.decorate('server', 'uiSettings', () => uiSettings); kbnServer.ready().then(mirrorEsStatus); + function get(key) { + return getAll().then(all => all[key]); + } + function getAll() { + return getRaw() + .then(raw => Object.keys(raw) + .reduce((all, key) => { + const item = raw[key]; + const hasUserValue = 'userValue' in item; + all[key] = hasUserValue ? item.userValue : item.value; + return all; + }, {}) + ); + } + + function getRaw() { return Promise .all([getDefaults(), getUserProvided()]) .then(([defaults, user]) => defaultsDeep(user, defaults)); From 7e191b82dd2a5ad8f51dfed069e9151571938380 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20Bevacqua?= Date: Mon, 27 Jun 2016 14:48:12 -0300 Subject: [PATCH 42/67] [fix] @spalger identified as NSA mole. Run server-side tests in src/ui directory. --- tasks/config/simplemocha.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tasks/config/simplemocha.js b/tasks/config/simplemocha.js index 2dddcfbe1bd41e..95213d40e2f970 100644 --- a/tasks/config/simplemocha.js +++ b/tasks/config/simplemocha.js @@ -13,8 +13,7 @@ module.exports = { 'test/**/__tests__/**/*.js', 'src/**/__tests__/**/*.js', 'test/fixtures/__tests__/*.js', - '!src/**/public/**', - '!src/ui/**' + '!src/**/public/**' ] } }; From 415e6635f1399bc2f6883efce787e8fbae64faa2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20Bevacqua?= Date: Mon, 27 Jun 2016 14:49:00 -0300 Subject: [PATCH 43/67] [settings] Added .removeMany method for convenience single-roundtrip. --- src/ui/settings/index.js | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/ui/settings/index.js b/src/ui/settings/index.js index 0f14673370c58e..0cf2aac900443a 100644 --- a/src/ui/settings/index.js +++ b/src/ui/settings/index.js @@ -11,7 +11,8 @@ export default function setupSettings(kbnServer, server, config) { getUserProvided, set, setMany, - remove + remove, + removeMany }; server.decorate('server', 'uiSettings', () => uiSettings); @@ -77,6 +78,14 @@ export default function setupSettings(kbnServer, server, config) { return set(key, null); } + function removeMany(keys) { + const changes = {}; + keys.forEach(key => { + changes[key] = null; + }); + return setMany(changes); + } + function mirrorEsStatus() { const esStatus = kbnServer.status.getForPluginId('elasticsearch'); From 1657c65e9da529e916c5c9e80fbbda59e94c1bd4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20Bevacqua?= Date: Mon, 27 Jun 2016 14:49:17 -0300 Subject: [PATCH 44/67] [test] Introduced server-side tests for UI Settings feature. --- src/ui/settings/__tests__/index.js | 345 +++++++++++++++++++++++++++++ 1 file changed, 345 insertions(+) create mode 100644 src/ui/settings/__tests__/index.js diff --git a/src/ui/settings/__tests__/index.js b/src/ui/settings/__tests__/index.js new file mode 100644 index 00000000000000..d1534aa4d0b164 --- /dev/null +++ b/src/ui/settings/__tests__/index.js @@ -0,0 +1,345 @@ +import sinon from 'sinon'; +import expect from 'expect.js'; +import init from '..'; +import defaultsProvider from '../defaults'; + +describe('ui settings', function () { + describe('overview', function () { + it('has expected api surface', function () { + const { uiSettings } = instantiate(); + expect(typeof uiSettings.get).to.be('function'); + expect(typeof uiSettings.getAll).to.be('function'); + expect(typeof uiSettings.getDefaults).to.be('function'); + expect(typeof uiSettings.getRaw).to.be('function'); + expect(typeof uiSettings.getUserProvided).to.be('function'); + expect(typeof uiSettings.remove).to.be('function'); + expect(typeof uiSettings.set).to.be('function'); + expect(typeof uiSettings.setMany).to.be('function'); + }); + }); + + describe('#setMany()', function () { + it('updates a single value in one operation', function () { + const { server, uiSettings, configGet } = instantiate(); + const result = uiSettings.setMany({ one: 'value' }); + expect(typeof result.then).to.be('function'); + expect(server.plugins.elasticsearch.client.update.callCount).to.eql(1); + expect(server.plugins.elasticsearch.client.update.firstCall.args).to.eql([{ + index: configGet('kibana.index'), + id: configGet('pkg.version'), + type: 'config', + body: { + doc: { one: 'value' } + } + }]); + }); + + it('updates several values in one operation', function () { + const { server, uiSettings, configGet } = instantiate(); + const result = uiSettings.setMany({ one: 'value', another: 'val' }); + expect(typeof result.then).to.be('function'); + expect(server.plugins.elasticsearch.client.update.callCount).to.eql(1); + expect(server.plugins.elasticsearch.client.update.firstCall.args).to.eql([{ + index: configGet('kibana.index'), + id: configGet('pkg.version'), + type: 'config', + body: { + doc: { one: 'value', another: 'val' } + } + }]); + }); + }); + + describe('#set()', function () { + it('updates single values by (key, value)', function () { + const { server, uiSettings, configGet } = instantiate(); + const result = uiSettings.set('one', 'value'); + expect(typeof result.then).to.be('function'); + expect(server.plugins.elasticsearch.client.update.callCount).to.eql(1); + expect(server.plugins.elasticsearch.client.update.firstCall.args).to.eql([{ + index: configGet('kibana.index'), + id: configGet('pkg.version'), + type: 'config', + body: { + doc: { one: 'value' } + } + }]); + }); + }); + + describe('#remove()', function () { + it('removes single values by key', function () { + const { server, uiSettings, configGet } = instantiate(); + const result = uiSettings.remove('one'); + expect(typeof result.then).to.be('function'); + expect(server.plugins.elasticsearch.client.update.callCount).to.eql(1); + expect(server.plugins.elasticsearch.client.update.firstCall.args).to.eql([{ + index: configGet('kibana.index'), + id: configGet('pkg.version'), + type: 'config', + body: { + doc: { one: null } + } + }]); + }); + }); + + describe('#removeMany()', function () { + it('removes a single value', function () { + const { server, uiSettings, configGet } = instantiate(); + const result = uiSettings.removeMany(['one']); + expect(typeof result.then).to.be('function'); + expect(server.plugins.elasticsearch.client.update.callCount).to.eql(1); + expect(server.plugins.elasticsearch.client.update.firstCall.args).to.eql([{ + index: configGet('kibana.index'), + id: configGet('pkg.version'), + type: 'config', + body: { + doc: { one: null } + } + }]); + }); + + it('updates several values in one operation', function () { + const { server, uiSettings, configGet } = instantiate(); + const result = uiSettings.removeMany(['one', 'two', 'three']); + expect(typeof result.then).to.be('function'); + expect(server.plugins.elasticsearch.client.update.callCount).to.eql(1); + expect(server.plugins.elasticsearch.client.update.firstCall.args).to.eql([{ + index: configGet('kibana.index'), + id: configGet('pkg.version'), + type: 'config', + body: { + doc: { one: null, two: null, three: null } + } + }]); + }); + }); + + describe('#getDefaults()', function () { + it('is promised the default values', async function () { + const { server, uiSettings, configGet } = instantiate(); + const defaults = await uiSettings.getDefaults(); + expect(defaults).eql(defaultsProvider()); + }); + }); + + describe('#getUserProvided()', function () { + it('pulls user configuration from ES', async function () { + const getResult = { user: 'customized' }; + const { server, uiSettings, configGet } = instantiate({ getResult }); + const result = await uiSettings.getUserProvided(); + expect(server.plugins.elasticsearch.client.get.callCount).to.eql(1); + expect(server.plugins.elasticsearch.client.get.firstCall.args).to.eql([{ + index: configGet('kibana.index'), + id: configGet('pkg.version'), + type: 'config' + }]); + expect(result).to.eql({ + user: { userValue: 'customized' } + }); + }); + + it('ignores null user configuration (because default values)', async function () { + const getResult = { user: 'customized', usingDefault: null, something: 'else' }; + const { server, uiSettings, configGet } = instantiate({ getResult }); + const result = await uiSettings.getUserProvided(); + expect(server.plugins.elasticsearch.client.get.callCount).to.eql(1); + expect(server.plugins.elasticsearch.client.get.firstCall.args).to.eql([{ + index: configGet('kibana.index'), + id: configGet('pkg.version'), + type: 'config' + }]); + expect(result).to.eql({ + user: { userValue: 'customized' }, something: { userValue: 'else' } + }); + }); + }); + + describe('#getRaw()', function () { + it(`without user configuration it's equal to the defaults`, async function () { + const getResult = {}; + const { server, uiSettings, configGet } = instantiate({ getResult }); + const result = await uiSettings.getRaw(); + expect(server.plugins.elasticsearch.client.get.callCount).to.eql(1); + expect(server.plugins.elasticsearch.client.get.firstCall.args).to.eql([{ + index: configGet('kibana.index'), + id: configGet('pkg.version'), + type: 'config' + }]); + expect(result).to.eql(defaultsProvider()); + }); + + it(`user configuration gets merged with defaults`, async function () { + const getResult = { foo: 'bar' }; + const { server, uiSettings, configGet } = instantiate({ getResult }); + const result = await uiSettings.getRaw(); + expect(server.plugins.elasticsearch.client.get.callCount).to.eql(1); + expect(server.plugins.elasticsearch.client.get.firstCall.args).to.eql([{ + index: configGet('kibana.index'), + id: configGet('pkg.version'), + type: 'config' + }]); + const merged = defaultsProvider(); + merged.foo = { userValue: 'bar' }; + expect(result).to.eql(merged); + }); + + it(`user configuration gets merged into defaults`, async function () { + const getResult = { dateFormat: 'YYYY-MM-DD' }; + const { server, uiSettings, configGet } = instantiate({ getResult }); + const result = await uiSettings.getRaw(); + expect(server.plugins.elasticsearch.client.get.callCount).to.eql(1); + expect(server.plugins.elasticsearch.client.get.firstCall.args).to.eql([{ + index: configGet('kibana.index'), + id: configGet('pkg.version'), + type: 'config' + }]); + const merged = defaultsProvider(); + merged.dateFormat.userValue = 'YYYY-MM-DD'; + expect(result).to.eql(merged); + }); + }); + + describe('#getAll()', function () { + it(`returns key value pairs`, async function () { + const getResult = {}; + const { server, uiSettings, configGet } = instantiate({ getResult }); + const result = await uiSettings.getAll(); + expect(server.plugins.elasticsearch.client.get.callCount).to.eql(1); + expect(server.plugins.elasticsearch.client.get.firstCall.args).to.eql([{ + index: configGet('kibana.index'), + id: configGet('pkg.version'), + type: 'config' + }]); + const defaults = defaultsProvider(); + const expectation = {}; + Object.keys(defaults).forEach(key => { + expectation[key] = defaults[key].value; + }); + expect(result).to.eql(expectation); + }); + + it(`returns key value pairs including user configuration`, async function () { + const getResult = { something: 'user-provided' }; + const { server, uiSettings, configGet } = instantiate({ getResult }); + const result = await uiSettings.getAll(); + expect(server.plugins.elasticsearch.client.get.callCount).to.eql(1); + expect(server.plugins.elasticsearch.client.get.firstCall.args).to.eql([{ + index: configGet('kibana.index'), + id: configGet('pkg.version'), + type: 'config' + }]); + const defaults = defaultsProvider(); + const expectation = {}; + Object.keys(defaults).forEach(key => { + expectation[key] = defaults[key].value; + }); + expectation.something = 'user-provided'; + expect(result).to.eql(expectation); + }); + + it(`returns key value pairs including user configuration for existing settings`, async function () { + const getResult = { dateFormat: 'YYYY-MM-DD' }; + const { server, uiSettings, configGet } = instantiate({ getResult }); + const result = await uiSettings.getAll(); + expect(server.plugins.elasticsearch.client.get.callCount).to.eql(1); + expect(server.plugins.elasticsearch.client.get.firstCall.args).to.eql([{ + index: configGet('kibana.index'), + id: configGet('pkg.version'), + type: 'config' + }]); + const defaults = defaultsProvider(); + const expectation = {}; + Object.keys(defaults).forEach(key => { + expectation[key] = defaults[key].value; + }); + expectation.dateFormat = 'YYYY-MM-DD'; + expect(result).to.eql(expectation); + }); + }); + + describe('#get()', function () { + it(`returns the promised value for a key`, async function () { + const getResult = {}; + const { server, uiSettings, configGet } = instantiate({ getResult }); + const result = await uiSettings.get('dateFormat'); + expect(server.plugins.elasticsearch.client.get.callCount).to.eql(1); + expect(server.plugins.elasticsearch.client.get.firstCall.args).to.eql([{ + index: configGet('kibana.index'), + id: configGet('pkg.version'), + type: 'config' + }]); + const defaults = defaultsProvider(); + expect(result).to.eql(defaults.dateFormat.value); + }); + + it(`returns the user-configured value for a custom key`, async function () { + const getResult = { custom: 'value' }; + const { server, uiSettings, configGet } = instantiate({ getResult }); + const result = await uiSettings.get('custom'); + expect(server.plugins.elasticsearch.client.get.callCount).to.eql(1); + expect(server.plugins.elasticsearch.client.get.firstCall.args).to.eql([{ + index: configGet('kibana.index'), + id: configGet('pkg.version'), + type: 'config' + }]); + const defaults = defaultsProvider(); + expect(result).to.eql('value'); + }); + + it(`returns the user-configured value for a modified key`, async function () { + const getResult = { dateFormat: 'YYYY-MM-DD' }; + const { server, uiSettings, configGet } = instantiate({ getResult }); + const result = await uiSettings.get('dateFormat'); + expect(server.plugins.elasticsearch.client.get.callCount).to.eql(1); + expect(server.plugins.elasticsearch.client.get.firstCall.args).to.eql([{ + index: configGet('kibana.index'), + id: configGet('pkg.version'), + type: 'config' + }]); + const defaults = defaultsProvider(); + expect(result).to.eql('YYYY-MM-DD'); + }); + }); +}); + +function instantiate({ getResult } = {}) { + const esStatus = { + state: 'green', + on: sinon.spy() + }; + const settingsStatus = { + state: 'green', + red: sinon.spy(), + yellow: sinon.spy(), + green: sinon.spy() + }; + const kbnServer = { + status: { + create: sinon.stub().withArgs('ui settings').returns(settingsStatus), + getForPluginId: sinon.stub().withArgs('elasticsearch').returns(esStatus) + }, + ready: sinon.stub().returns(Promise.resolve()) + }; + const server = { + decorate: (_, key, value) => server[key] = value, + plugins: { + elasticsearch: { + client: { + get: sinon.stub().returns(Promise.resolve({ _source: getResult })), + update: sinon.stub().returns(Promise.resolve()) + } + } + } + }; + const configGet = sinon.stub(); + configGet.withArgs('kibana.index').returns('.kibana'); + configGet.withArgs('pkg.version').returns('1.2.3-test'); + const config = { + get: configGet + }; + const setupSettings = init(kbnServer, server, config); + const uiSettings = server.uiSettings(); + return { kbnServer, server, config, uiSettings, esStatus, settingsStatus, configGet }; +} From 8ace579ed41a2efabc2b0719dca370df7dbdcedd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20Bevacqua?= Date: Mon, 27 Jun 2016 14:59:57 -0300 Subject: [PATCH 45/67] [refactor] Update usage of uiSettings when pulling a single value --- .../kibana/server/routes/api/ingest/register_post.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/core_plugins/kibana/server/routes/api/ingest/register_post.js b/src/core_plugins/kibana/server/routes/api/ingest/register_post.js index a1b0c5976b54f5..caf4d23f057956 100644 --- a/src/core_plugins/kibana/server/routes/api/ingest/register_post.js +++ b/src/core_plugins/kibana/server/routes/api/ingest/register_post.js @@ -64,7 +64,8 @@ export function registerPost(server) { } }, handler: async function (req, reply) { - const config = await server.uiSettings().getAll(); + const uiSettings = server.uiSettings(); + const metaFields = await uiSettings.get('metaFields'); const boundCallWithRequest = _.partial(server.plugins.elasticsearch.callWithRequest, req); const requestDocument = _.cloneDeep(req.payload); const indexPattern = keysToCamelCaseShallow(requestDocument.index_pattern); @@ -74,8 +75,6 @@ export function registerPost(server) { delete indexPattern.id; const mappings = createMappingsFromPatternFields(indexPattern.fields); - - const metaFields = _.get(config, 'metaFields.userValue', config.metaFields.value); const indexPatternMetaFields = _.map(metaFields, name => ({name})); indexPattern.fields = initDefaultFieldProps(indexPattern.fields.concat(indexPatternMetaFields)); From 0d31bb8228a0bb589743c814fdd8286b2fe6d3d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20Bevacqua?= Date: Mon, 27 Jun 2016 15:16:40 -0300 Subject: [PATCH 46/67] [test] Simplified test cases. Single assertion. --- src/ui/settings/__tests__/index.js | 35 ++++++++++++++++++++++-------- 1 file changed, 26 insertions(+), 9 deletions(-) diff --git a/src/ui/settings/__tests__/index.js b/src/ui/settings/__tests__/index.js index d1534aa4d0b164..bc621826cff76d 100644 --- a/src/ui/settings/__tests__/index.js +++ b/src/ui/settings/__tests__/index.js @@ -13,16 +13,22 @@ describe('ui settings', function () { expect(typeof uiSettings.getRaw).to.be('function'); expect(typeof uiSettings.getUserProvided).to.be('function'); expect(typeof uiSettings.remove).to.be('function'); + expect(typeof uiSettings.removeMany).to.be('function'); expect(typeof uiSettings.set).to.be('function'); expect(typeof uiSettings.setMany).to.be('function'); }); }); describe('#setMany()', function () { + it('returns a promise', () => { + const { uiSettings } = instantiate(); + const result = uiSettings.setMany({ a: 'b' }); + expect(typeof result.then).to.be('function'); + }); + it('updates a single value in one operation', function () { const { server, uiSettings, configGet } = instantiate(); const result = uiSettings.setMany({ one: 'value' }); - expect(typeof result.then).to.be('function'); expect(server.plugins.elasticsearch.client.update.callCount).to.eql(1); expect(server.plugins.elasticsearch.client.update.firstCall.args).to.eql([{ index: configGet('kibana.index'), @@ -37,7 +43,6 @@ describe('ui settings', function () { it('updates several values in one operation', function () { const { server, uiSettings, configGet } = instantiate(); const result = uiSettings.setMany({ one: 'value', another: 'val' }); - expect(typeof result.then).to.be('function'); expect(server.plugins.elasticsearch.client.update.callCount).to.eql(1); expect(server.plugins.elasticsearch.client.update.firstCall.args).to.eql([{ index: configGet('kibana.index'), @@ -51,10 +56,15 @@ describe('ui settings', function () { }); describe('#set()', function () { + it('returns a promise', () => { + const { uiSettings } = instantiate(); + const result = uiSettings.set('a', 'b'); + expect(typeof result.then).to.be('function'); + }); + it('updates single values by (key, value)', function () { const { server, uiSettings, configGet } = instantiate(); const result = uiSettings.set('one', 'value'); - expect(typeof result.then).to.be('function'); expect(server.plugins.elasticsearch.client.update.callCount).to.eql(1); expect(server.plugins.elasticsearch.client.update.firstCall.args).to.eql([{ index: configGet('kibana.index'), @@ -68,10 +78,15 @@ describe('ui settings', function () { }); describe('#remove()', function () { + it('returns a promise', () => { + const { uiSettings } = instantiate(); + const result = uiSettings.remove('one'); + expect(typeof result.then).to.be('function'); + }); + it('removes single values by key', function () { const { server, uiSettings, configGet } = instantiate(); const result = uiSettings.remove('one'); - expect(typeof result.then).to.be('function'); expect(server.plugins.elasticsearch.client.update.callCount).to.eql(1); expect(server.plugins.elasticsearch.client.update.firstCall.args).to.eql([{ index: configGet('kibana.index'), @@ -85,10 +100,15 @@ describe('ui settings', function () { }); describe('#removeMany()', function () { + it('returns a promise', () => { + const { uiSettings } = instantiate(); + const result = uiSettings.removeMany(['one']); + expect(typeof result.then).to.be('function'); + }); + it('removes a single value', function () { const { server, uiSettings, configGet } = instantiate(); const result = uiSettings.removeMany(['one']); - expect(typeof result.then).to.be('function'); expect(server.plugins.elasticsearch.client.update.callCount).to.eql(1); expect(server.plugins.elasticsearch.client.update.firstCall.args).to.eql([{ index: configGet('kibana.index'), @@ -103,7 +123,6 @@ describe('ui settings', function () { it('updates several values in one operation', function () { const { server, uiSettings, configGet } = instantiate(); const result = uiSettings.removeMany(['one', 'two', 'three']); - expect(typeof result.then).to.be('function'); expect(server.plugins.elasticsearch.client.update.callCount).to.eql(1); expect(server.plugins.elasticsearch.client.update.firstCall.args).to.eql([{ index: configGet('kibana.index'), @@ -284,7 +303,6 @@ describe('ui settings', function () { id: configGet('pkg.version'), type: 'config' }]); - const defaults = defaultsProvider(); expect(result).to.eql('value'); }); @@ -298,7 +316,6 @@ describe('ui settings', function () { id: configGet('pkg.version'), type: 'config' }]); - const defaults = defaultsProvider(); expect(result).to.eql('YYYY-MM-DD'); }); }); @@ -341,5 +358,5 @@ function instantiate({ getResult } = {}) { }; const setupSettings = init(kbnServer, server, config); const uiSettings = server.uiSettings(); - return { kbnServer, server, config, uiSettings, esStatus, settingsStatus, configGet }; + return { server, uiSettings, configGet }; } From 11c3bf503e90d39baf6e5259d606ff387557349a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20Bevacqua?= Date: Mon, 27 Jun 2016 15:23:04 -0300 Subject: [PATCH 47/67] [test] Refactor to avoid code duplication. --- src/ui/settings/__tests__/index.js | 185 ++++++++++------------------- 1 file changed, 65 insertions(+), 120 deletions(-) diff --git a/src/ui/settings/__tests__/index.js b/src/ui/settings/__tests__/index.js index bc621826cff76d..f8dc0be4fd06e6 100644 --- a/src/ui/settings/__tests__/index.js +++ b/src/ui/settings/__tests__/index.js @@ -29,29 +29,17 @@ describe('ui settings', function () { it('updates a single value in one operation', function () { const { server, uiSettings, configGet } = instantiate(); const result = uiSettings.setMany({ one: 'value' }); - expect(server.plugins.elasticsearch.client.update.callCount).to.eql(1); - expect(server.plugins.elasticsearch.client.update.firstCall.args).to.eql([{ - index: configGet('kibana.index'), - id: configGet('pkg.version'), - type: 'config', - body: { - doc: { one: 'value' } - } - }]); + expectElasticsearchUpdateQuery(server, configGet, { + one: 'value' + }); }); it('updates several values in one operation', function () { const { server, uiSettings, configGet } = instantiate(); const result = uiSettings.setMany({ one: 'value', another: 'val' }); - expect(server.plugins.elasticsearch.client.update.callCount).to.eql(1); - expect(server.plugins.elasticsearch.client.update.firstCall.args).to.eql([{ - index: configGet('kibana.index'), - id: configGet('pkg.version'), - type: 'config', - body: { - doc: { one: 'value', another: 'val' } - } - }]); + expectElasticsearchUpdateQuery(server, configGet, { + one: 'value', another: 'val' + }); }); }); @@ -65,15 +53,9 @@ describe('ui settings', function () { it('updates single values by (key, value)', function () { const { server, uiSettings, configGet } = instantiate(); const result = uiSettings.set('one', 'value'); - expect(server.plugins.elasticsearch.client.update.callCount).to.eql(1); - expect(server.plugins.elasticsearch.client.update.firstCall.args).to.eql([{ - index: configGet('kibana.index'), - id: configGet('pkg.version'), - type: 'config', - body: { - doc: { one: 'value' } - } - }]); + expectElasticsearchUpdateQuery(server, configGet, { + one: 'value' + }); }); }); @@ -87,15 +69,9 @@ describe('ui settings', function () { it('removes single values by key', function () { const { server, uiSettings, configGet } = instantiate(); const result = uiSettings.remove('one'); - expect(server.plugins.elasticsearch.client.update.callCount).to.eql(1); - expect(server.plugins.elasticsearch.client.update.firstCall.args).to.eql([{ - index: configGet('kibana.index'), - id: configGet('pkg.version'), - type: 'config', - body: { - doc: { one: null } - } - }]); + expectElasticsearchUpdateQuery(server, configGet, { + one: null + }); }); }); @@ -109,29 +85,17 @@ describe('ui settings', function () { it('removes a single value', function () { const { server, uiSettings, configGet } = instantiate(); const result = uiSettings.removeMany(['one']); - expect(server.plugins.elasticsearch.client.update.callCount).to.eql(1); - expect(server.plugins.elasticsearch.client.update.firstCall.args).to.eql([{ - index: configGet('kibana.index'), - id: configGet('pkg.version'), - type: 'config', - body: { - doc: { one: null } - } - }]); + expectElasticsearchUpdateQuery(server, configGet, { + one: null + }); }); it('updates several values in one operation', function () { const { server, uiSettings, configGet } = instantiate(); const result = uiSettings.removeMany(['one', 'two', 'three']); - expect(server.plugins.elasticsearch.client.update.callCount).to.eql(1); - expect(server.plugins.elasticsearch.client.update.firstCall.args).to.eql([{ - index: configGet('kibana.index'), - id: configGet('pkg.version'), - type: 'config', - body: { - doc: { one: null, two: null, three: null } - } - }]); + expectElasticsearchUpdateQuery(server, configGet, { + one: null, two: null, three: null + }); }); }); @@ -148,12 +112,13 @@ describe('ui settings', function () { const getResult = { user: 'customized' }; const { server, uiSettings, configGet } = instantiate({ getResult }); const result = await uiSettings.getUserProvided(); - expect(server.plugins.elasticsearch.client.get.callCount).to.eql(1); - expect(server.plugins.elasticsearch.client.get.firstCall.args).to.eql([{ - index: configGet('kibana.index'), - id: configGet('pkg.version'), - type: 'config' - }]); + expectElasticsearchGetQuery(server, configGet); + }); + + it('returns user configuration', async function () { + const getResult = { user: 'customized' }; + const { server, uiSettings, configGet } = instantiate({ getResult }); + const result = await uiSettings.getUserProvided(); expect(result).to.eql({ user: { userValue: 'customized' } }); @@ -163,12 +128,6 @@ describe('ui settings', function () { const getResult = { user: 'customized', usingDefault: null, something: 'else' }; const { server, uiSettings, configGet } = instantiate({ getResult }); const result = await uiSettings.getUserProvided(); - expect(server.plugins.elasticsearch.client.get.callCount).to.eql(1); - expect(server.plugins.elasticsearch.client.get.firstCall.args).to.eql([{ - index: configGet('kibana.index'), - id: configGet('pkg.version'), - type: 'config' - }]); expect(result).to.eql({ user: { userValue: 'customized' }, something: { userValue: 'else' } }); @@ -176,16 +135,17 @@ describe('ui settings', function () { }); describe('#getRaw()', function () { + it('pulls user configuration from ES', async function () { + const getResult = {}; + const { server, uiSettings, configGet } = instantiate({ getResult }); + const result = await uiSettings.getRaw(); + expectElasticsearchGetQuery(server, configGet); + }); + it(`without user configuration it's equal to the defaults`, async function () { const getResult = {}; const { server, uiSettings, configGet } = instantiate({ getResult }); const result = await uiSettings.getRaw(); - expect(server.plugins.elasticsearch.client.get.callCount).to.eql(1); - expect(server.plugins.elasticsearch.client.get.firstCall.args).to.eql([{ - index: configGet('kibana.index'), - id: configGet('pkg.version'), - type: 'config' - }]); expect(result).to.eql(defaultsProvider()); }); @@ -193,12 +153,6 @@ describe('ui settings', function () { const getResult = { foo: 'bar' }; const { server, uiSettings, configGet } = instantiate({ getResult }); const result = await uiSettings.getRaw(); - expect(server.plugins.elasticsearch.client.get.callCount).to.eql(1); - expect(server.plugins.elasticsearch.client.get.firstCall.args).to.eql([{ - index: configGet('kibana.index'), - id: configGet('pkg.version'), - type: 'config' - }]); const merged = defaultsProvider(); merged.foo = { userValue: 'bar' }; expect(result).to.eql(merged); @@ -208,12 +162,6 @@ describe('ui settings', function () { const getResult = { dateFormat: 'YYYY-MM-DD' }; const { server, uiSettings, configGet } = instantiate({ getResult }); const result = await uiSettings.getRaw(); - expect(server.plugins.elasticsearch.client.get.callCount).to.eql(1); - expect(server.plugins.elasticsearch.client.get.firstCall.args).to.eql([{ - index: configGet('kibana.index'), - id: configGet('pkg.version'), - type: 'config' - }]); const merged = defaultsProvider(); merged.dateFormat.userValue = 'YYYY-MM-DD'; expect(result).to.eql(merged); @@ -221,16 +169,17 @@ describe('ui settings', function () { }); describe('#getAll()', function () { + it('pulls user configuration from ES', async function () { + const getResult = {}; + const { server, uiSettings, configGet } = instantiate({ getResult }); + const result = await uiSettings.getAll(); + expectElasticsearchGetQuery(server, configGet); + }); + it(`returns key value pairs`, async function () { const getResult = {}; const { server, uiSettings, configGet } = instantiate({ getResult }); const result = await uiSettings.getAll(); - expect(server.plugins.elasticsearch.client.get.callCount).to.eql(1); - expect(server.plugins.elasticsearch.client.get.firstCall.args).to.eql([{ - index: configGet('kibana.index'), - id: configGet('pkg.version'), - type: 'config' - }]); const defaults = defaultsProvider(); const expectation = {}; Object.keys(defaults).forEach(key => { @@ -243,12 +192,6 @@ describe('ui settings', function () { const getResult = { something: 'user-provided' }; const { server, uiSettings, configGet } = instantiate({ getResult }); const result = await uiSettings.getAll(); - expect(server.plugins.elasticsearch.client.get.callCount).to.eql(1); - expect(server.plugins.elasticsearch.client.get.firstCall.args).to.eql([{ - index: configGet('kibana.index'), - id: configGet('pkg.version'), - type: 'config' - }]); const defaults = defaultsProvider(); const expectation = {}; Object.keys(defaults).forEach(key => { @@ -262,12 +205,6 @@ describe('ui settings', function () { const getResult = { dateFormat: 'YYYY-MM-DD' }; const { server, uiSettings, configGet } = instantiate({ getResult }); const result = await uiSettings.getAll(); - expect(server.plugins.elasticsearch.client.get.callCount).to.eql(1); - expect(server.plugins.elasticsearch.client.get.firstCall.args).to.eql([{ - index: configGet('kibana.index'), - id: configGet('pkg.version'), - type: 'config' - }]); const defaults = defaultsProvider(); const expectation = {}; Object.keys(defaults).forEach(key => { @@ -279,16 +216,17 @@ describe('ui settings', function () { }); describe('#get()', function () { + it('pulls user configuration from ES', async function () { + const getResult = {}; + const { server, uiSettings, configGet } = instantiate({ getResult }); + const result = await uiSettings.get(); + expectElasticsearchGetQuery(server, configGet); + }); + it(`returns the promised value for a key`, async function () { const getResult = {}; const { server, uiSettings, configGet } = instantiate({ getResult }); const result = await uiSettings.get('dateFormat'); - expect(server.plugins.elasticsearch.client.get.callCount).to.eql(1); - expect(server.plugins.elasticsearch.client.get.firstCall.args).to.eql([{ - index: configGet('kibana.index'), - id: configGet('pkg.version'), - type: 'config' - }]); const defaults = defaultsProvider(); expect(result).to.eql(defaults.dateFormat.value); }); @@ -297,12 +235,6 @@ describe('ui settings', function () { const getResult = { custom: 'value' }; const { server, uiSettings, configGet } = instantiate({ getResult }); const result = await uiSettings.get('custom'); - expect(server.plugins.elasticsearch.client.get.callCount).to.eql(1); - expect(server.plugins.elasticsearch.client.get.firstCall.args).to.eql([{ - index: configGet('kibana.index'), - id: configGet('pkg.version'), - type: 'config' - }]); expect(result).to.eql('value'); }); @@ -310,17 +242,30 @@ describe('ui settings', function () { const getResult = { dateFormat: 'YYYY-MM-DD' }; const { server, uiSettings, configGet } = instantiate({ getResult }); const result = await uiSettings.get('dateFormat'); - expect(server.plugins.elasticsearch.client.get.callCount).to.eql(1); - expect(server.plugins.elasticsearch.client.get.firstCall.args).to.eql([{ - index: configGet('kibana.index'), - id: configGet('pkg.version'), - type: 'config' - }]); expect(result).to.eql('YYYY-MM-DD'); }); }); }); +function expectElasticsearchGetQuery(server, configGet) { + expect(server.plugins.elasticsearch.client.get.callCount).to.eql(1); + expect(server.plugins.elasticsearch.client.get.firstCall.args).to.eql([{ + index: configGet('kibana.index'), + id: configGet('pkg.version'), + type: 'config' + }]); +} + +function expectElasticsearchUpdateQuery(server, configGet, doc) { + expect(server.plugins.elasticsearch.client.update.callCount).to.eql(1); + expect(server.plugins.elasticsearch.client.update.firstCall.args).to.eql([{ + index: configGet('kibana.index'), + id: configGet('pkg.version'), + type: 'config', + body: { doc } + }]); +} + function instantiate({ getResult } = {}) { const esStatus = { state: 'green', From 308ee58c261fc78d4eb998f51a0e5722bd57c02e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20Bevacqua?= Date: Mon, 4 Jul 2016 16:36:19 -0300 Subject: [PATCH 48/67] [test] Semantic fix to compare against Promise. --- src/ui/settings/__tests__/index.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/ui/settings/__tests__/index.js b/src/ui/settings/__tests__/index.js index f8dc0be4fd06e6..b8e59ddb7eca69 100644 --- a/src/ui/settings/__tests__/index.js +++ b/src/ui/settings/__tests__/index.js @@ -23,7 +23,7 @@ describe('ui settings', function () { it('returns a promise', () => { const { uiSettings } = instantiate(); const result = uiSettings.setMany({ a: 'b' }); - expect(typeof result.then).to.be('function'); + expect(result).to.be.a(Promise); }); it('updates a single value in one operation', function () { @@ -47,7 +47,7 @@ describe('ui settings', function () { it('returns a promise', () => { const { uiSettings } = instantiate(); const result = uiSettings.set('a', 'b'); - expect(typeof result.then).to.be('function'); + expect(result).to.be.a(Promise); }); it('updates single values by (key, value)', function () { @@ -63,7 +63,7 @@ describe('ui settings', function () { it('returns a promise', () => { const { uiSettings } = instantiate(); const result = uiSettings.remove('one'); - expect(typeof result.then).to.be('function'); + expect(result).to.be.a(Promise); }); it('removes single values by key', function () { @@ -79,7 +79,7 @@ describe('ui settings', function () { it('returns a promise', () => { const { uiSettings } = instantiate(); const result = uiSettings.removeMany(['one']); - expect(typeof result.then).to.be('function'); + expect(result).to.be.a(Promise); }); it('removes a single value', function () { From af0d592817a75051f2551ae11db1c782bd11f824 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20Bevacqua?= Date: Tue, 5 Jul 2016 11:38:09 -0300 Subject: [PATCH 49/67] [chore] Use isEqual and add comments about the methods in uiSettings. --- src/ui/settings/__tests__/index.js | 41 +++++++++++++++--------------- src/ui/settings/index.js | 18 ++++++++++--- 2 files changed, 35 insertions(+), 24 deletions(-) diff --git a/src/ui/settings/__tests__/index.js b/src/ui/settings/__tests__/index.js index b8e59ddb7eca69..e1f687babe7339 100644 --- a/src/ui/settings/__tests__/index.js +++ b/src/ui/settings/__tests__/index.js @@ -1,3 +1,4 @@ +import { isEqual } from 'lodash'; import sinon from 'sinon'; import expect from 'expect.js'; import init from '..'; @@ -103,7 +104,7 @@ describe('ui settings', function () { it('is promised the default values', async function () { const { server, uiSettings, configGet } = instantiate(); const defaults = await uiSettings.getDefaults(); - expect(defaults).eql(defaultsProvider()); + expect(isEqual(defaults, defaultsProvider())); }); }); @@ -119,18 +120,18 @@ describe('ui settings', function () { const getResult = { user: 'customized' }; const { server, uiSettings, configGet } = instantiate({ getResult }); const result = await uiSettings.getUserProvided(); - expect(result).to.eql({ + expect(isEqual(result, { user: { userValue: 'customized' } - }); + })); }); it('ignores null user configuration (because default values)', async function () { const getResult = { user: 'customized', usingDefault: null, something: 'else' }; const { server, uiSettings, configGet } = instantiate({ getResult }); const result = await uiSettings.getUserProvided(); - expect(result).to.eql({ + expect(isEqual(result, { user: { userValue: 'customized' }, something: { userValue: 'else' } - }); + })); }); }); @@ -146,7 +147,7 @@ describe('ui settings', function () { const getResult = {}; const { server, uiSettings, configGet } = instantiate({ getResult }); const result = await uiSettings.getRaw(); - expect(result).to.eql(defaultsProvider()); + expect(isEqual(result, defaultsProvider())); }); it(`user configuration gets merged with defaults`, async function () { @@ -155,7 +156,7 @@ describe('ui settings', function () { const result = await uiSettings.getRaw(); const merged = defaultsProvider(); merged.foo = { userValue: 'bar' }; - expect(result).to.eql(merged); + expect(isEqual(result, merged)); }); it(`user configuration gets merged into defaults`, async function () { @@ -164,7 +165,7 @@ describe('ui settings', function () { const result = await uiSettings.getRaw(); const merged = defaultsProvider(); merged.dateFormat.userValue = 'YYYY-MM-DD'; - expect(result).to.eql(merged); + expect(isEqual(result, merged)); }); }); @@ -185,7 +186,7 @@ describe('ui settings', function () { Object.keys(defaults).forEach(key => { expectation[key] = defaults[key].value; }); - expect(result).to.eql(expectation); + expect(isEqual(result, expectation)); }); it(`returns key value pairs including user configuration`, async function () { @@ -198,7 +199,7 @@ describe('ui settings', function () { expectation[key] = defaults[key].value; }); expectation.something = 'user-provided'; - expect(result).to.eql(expectation); + expect(isEqual(result, expectation)); }); it(`returns key value pairs including user configuration for existing settings`, async function () { @@ -211,7 +212,7 @@ describe('ui settings', function () { expectation[key] = defaults[key].value; }); expectation.dateFormat = 'YYYY-MM-DD'; - expect(result).to.eql(expectation); + expect(isEqual(result, expectation)); }); }); @@ -228,42 +229,42 @@ describe('ui settings', function () { const { server, uiSettings, configGet } = instantiate({ getResult }); const result = await uiSettings.get('dateFormat'); const defaults = defaultsProvider(); - expect(result).to.eql(defaults.dateFormat.value); + expect(isEqual(result, defaults.dateFormat.value)); }); it(`returns the user-configured value for a custom key`, async function () { const getResult = { custom: 'value' }; const { server, uiSettings, configGet } = instantiate({ getResult }); const result = await uiSettings.get('custom'); - expect(result).to.eql('value'); + expect(isEqual(result, 'value')); }); it(`returns the user-configured value for a modified key`, async function () { const getResult = { dateFormat: 'YYYY-MM-DD' }; const { server, uiSettings, configGet } = instantiate({ getResult }); const result = await uiSettings.get('dateFormat'); - expect(result).to.eql('YYYY-MM-DD'); + expect(isEqual(result, 'YYYY-MM-DD')); }); }); }); function expectElasticsearchGetQuery(server, configGet) { - expect(server.plugins.elasticsearch.client.get.callCount).to.eql(1); - expect(server.plugins.elasticsearch.client.get.firstCall.args).to.eql([{ + expect(isEqual(server.plugins.elasticsearch.client.get.callCount, 1)); + expect(isEqual(server.plugins.elasticsearch.client.get.firstCall.args, [{ index: configGet('kibana.index'), id: configGet('pkg.version'), type: 'config' - }]); + }])); } function expectElasticsearchUpdateQuery(server, configGet, doc) { - expect(server.plugins.elasticsearch.client.update.callCount).to.eql(1); - expect(server.plugins.elasticsearch.client.update.firstCall.args).to.eql([{ + expect(isEqual(server.plugins.elasticsearch.client.update.callCount, 1)); + expect(isEqual(server.plugins.elasticsearch.client.update.firstCall.args, [{ index: configGet('kibana.index'), id: configGet('pkg.version'), type: 'config', body: { doc } - }]); + }])); } function instantiate({ getResult } = {}) { diff --git a/src/ui/settings/index.js b/src/ui/settings/index.js index 0cf2aac900443a..3709e48c8124b5 100644 --- a/src/ui/settings/index.js +++ b/src/ui/settings/index.js @@ -4,15 +4,25 @@ import defaultsProvider from './defaults'; export default function setupSettings(kbnServer, server, config) { const status = kbnServer.status.create('ui settings'); const uiSettings = { + // returns a Promise for the value of the requested setting get, + // returns a Promise for a hash of setting key/value pairs getAll, - getRaw, - getDefaults, - getUserProvided, + // .set(key, value), returns a Promise for persisting the new value to ES set, + // takes a key/value hash, returns a Promise for persisting the new values to ES setMany, + // returns a Promise for removing the provided key from user-specific settings remove, - removeMany + // takes an array, returns a Promise for removing every provided key from user-specific settings + removeMany, + + // returns a Promise for the default settings, follows metadata format (see ./defaults) + getDefaults, + // returns a Promise for user-specific settings stored in ES, follows metadata format + getUserProvided, + // returns a Promise merging results of getDefaults & getUserProvided, follows metadata format + getRaw }; server.decorate('server', 'uiSettings', () => uiSettings); From 1673b31e490fb1f30a101f4212bce99c7631341c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20Bevacqua?= Date: Tue, 5 Jul 2016 11:48:53 -0300 Subject: [PATCH 50/67] [test] .to.be.ok(), ugh. --- src/ui/settings/__tests__/index.js | 32 +++++++++++++++--------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/src/ui/settings/__tests__/index.js b/src/ui/settings/__tests__/index.js index e1f687babe7339..19fad9002fcb07 100644 --- a/src/ui/settings/__tests__/index.js +++ b/src/ui/settings/__tests__/index.js @@ -104,7 +104,7 @@ describe('ui settings', function () { it('is promised the default values', async function () { const { server, uiSettings, configGet } = instantiate(); const defaults = await uiSettings.getDefaults(); - expect(isEqual(defaults, defaultsProvider())); + expect(isEqual(defaults, defaultsProvider())).to.be.ok(); }); }); @@ -122,7 +122,7 @@ describe('ui settings', function () { const result = await uiSettings.getUserProvided(); expect(isEqual(result, { user: { userValue: 'customized' } - })); + })).to.be.ok(); }); it('ignores null user configuration (because default values)', async function () { @@ -131,7 +131,7 @@ describe('ui settings', function () { const result = await uiSettings.getUserProvided(); expect(isEqual(result, { user: { userValue: 'customized' }, something: { userValue: 'else' } - })); + })).to.be.ok(); }); }); @@ -147,7 +147,7 @@ describe('ui settings', function () { const getResult = {}; const { server, uiSettings, configGet } = instantiate({ getResult }); const result = await uiSettings.getRaw(); - expect(isEqual(result, defaultsProvider())); + expect(isEqual(result, defaultsProvider())).to.be.ok(); }); it(`user configuration gets merged with defaults`, async function () { @@ -156,7 +156,7 @@ describe('ui settings', function () { const result = await uiSettings.getRaw(); const merged = defaultsProvider(); merged.foo = { userValue: 'bar' }; - expect(isEqual(result, merged)); + expect(isEqual(result, merged)).to.be.ok(); }); it(`user configuration gets merged into defaults`, async function () { @@ -165,7 +165,7 @@ describe('ui settings', function () { const result = await uiSettings.getRaw(); const merged = defaultsProvider(); merged.dateFormat.userValue = 'YYYY-MM-DD'; - expect(isEqual(result, merged)); + expect(isEqual(result, merged)).to.be.ok(); }); }); @@ -186,7 +186,7 @@ describe('ui settings', function () { Object.keys(defaults).forEach(key => { expectation[key] = defaults[key].value; }); - expect(isEqual(result, expectation)); + expect(isEqual(result, expectation)).to.be.ok(); }); it(`returns key value pairs including user configuration`, async function () { @@ -199,7 +199,7 @@ describe('ui settings', function () { expectation[key] = defaults[key].value; }); expectation.something = 'user-provided'; - expect(isEqual(result, expectation)); + expect(isEqual(result, expectation)).to.be.ok(); }); it(`returns key value pairs including user configuration for existing settings`, async function () { @@ -212,7 +212,7 @@ describe('ui settings', function () { expectation[key] = defaults[key].value; }); expectation.dateFormat = 'YYYY-MM-DD'; - expect(isEqual(result, expectation)); + expect(isEqual(result, expectation)).to.be.ok(); }); }); @@ -229,42 +229,42 @@ describe('ui settings', function () { const { server, uiSettings, configGet } = instantiate({ getResult }); const result = await uiSettings.get('dateFormat'); const defaults = defaultsProvider(); - expect(isEqual(result, defaults.dateFormat.value)); + expect(isEqual(result, defaults.dateFormat.value)).to.be.ok(); }); it(`returns the user-configured value for a custom key`, async function () { const getResult = { custom: 'value' }; const { server, uiSettings, configGet } = instantiate({ getResult }); const result = await uiSettings.get('custom'); - expect(isEqual(result, 'value')); + expect(isEqual(result, 'value')).to.be.ok(); }); it(`returns the user-configured value for a modified key`, async function () { const getResult = { dateFormat: 'YYYY-MM-DD' }; const { server, uiSettings, configGet } = instantiate({ getResult }); const result = await uiSettings.get('dateFormat'); - expect(isEqual(result, 'YYYY-MM-DD')); + expect(isEqual(result, 'YYYY-MM-DD')).to.be.ok(); }); }); }); function expectElasticsearchGetQuery(server, configGet) { - expect(isEqual(server.plugins.elasticsearch.client.get.callCount, 1)); + expect(isEqual(server.plugins.elasticsearch.client.get.callCount, 1)).to.be.ok(); expect(isEqual(server.plugins.elasticsearch.client.get.firstCall.args, [{ index: configGet('kibana.index'), id: configGet('pkg.version'), type: 'config' - }])); + }])).to.be.ok(); } function expectElasticsearchUpdateQuery(server, configGet, doc) { - expect(isEqual(server.plugins.elasticsearch.client.update.callCount, 1)); + expect(isEqual(server.plugins.elasticsearch.client.update.callCount, 1)).to.be.ok(); expect(isEqual(server.plugins.elasticsearch.client.update.firstCall.args, [{ index: configGet('kibana.index'), id: configGet('pkg.version'), type: 'config', body: { doc } - }])); + }])).to.be.ok(); } function instantiate({ getResult } = {}) { From 829c858df3cd3f01685bdaaf67d4c140dabb7729 Mon Sep 17 00:00:00 2001 From: Shaunak Kashyap Date: Wed, 6 Jul 2016 11:21:33 -0700 Subject: [PATCH 51/67] Refactor notifier display code into Notifier module --- src/core_plugins/kibana/public/kibana.js | 18 +----------------- src/ui/public/notify/notifier.js | 16 ++++++++++++++++ 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/src/core_plugins/kibana/public/kibana.js b/src/core_plugins/kibana/public/kibana.js index 4c05d4684a4fe1..c4422571da6627 100644 --- a/src/core_plugins/kibana/public/kibana.js +++ b/src/core_plugins/kibana/public/kibana.js @@ -46,20 +46,4 @@ chrome } }); -function showNotifier($location) { - const queryString = $location.search(); - if (queryString.notif_msg) { - const message = queryString.notif_msg; - const config = queryString.notif_loc ? { location: queryString.notif_loc } : {}; - const level = queryString.notif_lvl || 'info'; - - $location.search('notif_msg', null); - $location.search('notif_loc', null); - $location.search('notif_lvl', null); - - const notifier = new Notifier(config); - notifier[level](message); - } -} - -modules.get('kibana').run(showNotifier); +modules.get('kibana').run(Notifier.run); diff --git a/src/ui/public/notify/notifier.js b/src/ui/public/notify/notifier.js index b2dbbb0ec8444b..d20277eec3e478 100644 --- a/src/ui/public/notify/notifier.js +++ b/src/ui/public/notify/notifier.js @@ -188,6 +188,22 @@ Notifier.applyConfig = function (config) { // to be notified when the first fatal error occurs, push a function into this array. Notifier.fatalCallbacks = []; +Notifier.run = ($location) => { + const queryString = $location.search(); + if (queryString.notif_msg) { + const message = queryString.notif_msg; + const config = queryString.notif_loc ? { location: queryString.notif_loc } : {}; + const level = queryString.notif_lvl || 'info'; + + $location.search('notif_msg', null); + $location.search('notif_loc', null); + $location.search('notif_lvl', null); + + const notifier = new Notifier(config); + notifier[level](message); + } +}; + // simply a pointer to the global notif list Notifier.prototype._notifs = notifs; From 0c0e1412592de73fcaa4fa097970ca9d560be842 Mon Sep 17 00:00:00 2001 From: Shaunak Kashyap Date: Wed, 6 Jul 2016 11:26:57 -0700 Subject: [PATCH 52/67] Renaming method to be more descriptive --- src/core_plugins/kibana/public/kibana.js | 2 +- src/ui/public/notify/notifier.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/core_plugins/kibana/public/kibana.js b/src/core_plugins/kibana/public/kibana.js index c4422571da6627..41ec204bde5a13 100644 --- a/src/core_plugins/kibana/public/kibana.js +++ b/src/core_plugins/kibana/public/kibana.js @@ -46,4 +46,4 @@ chrome } }); -modules.get('kibana').run(Notifier.run); +modules.get('kibana').run(Notifier.pullMessageFromUrl); diff --git a/src/ui/public/notify/notifier.js b/src/ui/public/notify/notifier.js index d20277eec3e478..9c13b49ab9f4d0 100644 --- a/src/ui/public/notify/notifier.js +++ b/src/ui/public/notify/notifier.js @@ -188,7 +188,7 @@ Notifier.applyConfig = function (config) { // to be notified when the first fatal error occurs, push a function into this array. Notifier.fatalCallbacks = []; -Notifier.run = ($location) => { +Notifier.pullMessageFromUrl = ($location) => { const queryString = $location.search(); if (queryString.notif_msg) { const message = queryString.notif_msg; From de3cf6c2cf057f3d71a2f9a41aba448b4ba6ed2c Mon Sep 17 00:00:00 2001 From: Shaunak Kashyap Date: Wed, 6 Jul 2016 11:28:09 -0700 Subject: [PATCH 53/67] Return early from function if there is no message to notify --- src/ui/public/notify/notifier.js | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/src/ui/public/notify/notifier.js b/src/ui/public/notify/notifier.js index 9c13b49ab9f4d0..801054e42da86f 100644 --- a/src/ui/public/notify/notifier.js +++ b/src/ui/public/notify/notifier.js @@ -190,18 +190,19 @@ Notifier.fatalCallbacks = []; Notifier.pullMessageFromUrl = ($location) => { const queryString = $location.search(); - if (queryString.notif_msg) { - const message = queryString.notif_msg; - const config = queryString.notif_loc ? { location: queryString.notif_loc } : {}; - const level = queryString.notif_lvl || 'info'; + if (!queryString.notif_msg) { + return; + } + const message = queryString.notif_msg; + const config = queryString.notif_loc ? { location: queryString.notif_loc } : {}; + const level = queryString.notif_lvl || 'info'; - $location.search('notif_msg', null); - $location.search('notif_loc', null); - $location.search('notif_lvl', null); + $location.search('notif_msg', null); + $location.search('notif_loc', null); + $location.search('notif_lvl', null); - const notifier = new Notifier(config); - notifier[level](message); - } + const notifier = new Notifier(config); + notifier[level](message); }; // simply a pointer to the global notif list From a731a3be8ffd698a145dd879352a13e2ea446187 Mon Sep 17 00:00:00 2001 From: Shaunak Kashyap Date: Wed, 6 Jul 2016 13:40:34 -0700 Subject: [PATCH 54/67] Use _.result --- src/ui/public/kbn_top_nav/kbn_top_nav.html | 4 ++-- src/ui/public/kbn_top_nav/kbn_top_nav_controller.js | 12 ++++++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/ui/public/kbn_top_nav/kbn_top_nav.html b/src/ui/public/kbn_top_nav/kbn_top_nav.html index ad3c3880a15ec3..0414340e2c2ac0 100644 --- a/src/ui/public/kbn_top_nav/kbn_top_nav.html +++ b/src/ui/public/kbn_top_nav/kbn_top_nav.html @@ -6,10 +6,10 @@ aria-label="{{::menuItem.description}}" aria-haspopup="{{!menuItem.hasFunction}}" aria-expanded="{{kbnTopNav.isCurrent(menuItem.key)}}" - ng-class="{active: kbnTopNav.isCurrent(menuItem.key), 'is-kbn-top-nav-button-disabled': menuItem.disableButton()}" + ng-class="{active: kbnTopNav.isCurrent(menuItem.key), 'is-kbn-top-nav-button-disabled': menuItem.disableButton}" ng-click="menuItem.run(menuItem, kbnTopNav)" ng-bind="menuItem.label" - tooltip="{{menuItem.tooltip()}}" + tooltip="{{menuItem.tooltip}}" tooltip-placement="bottom" tooltip-popup-delay="400" tooltip-append-to-body="1" diff --git a/src/ui/public/kbn_top_nav/kbn_top_nav_controller.js b/src/ui/public/kbn_top_nav/kbn_top_nav_controller.js index da8ccd2ae62a9f..60a57595b6828c 100644 --- a/src/ui/public/kbn_top_nav/kbn_top_nav_controller.js +++ b/src/ui/public/kbn_top_nav/kbn_top_nav_controller.js @@ -1,4 +1,4 @@ -import { defaults, capitalize, isArray, isFunction } from 'lodash'; +import { defaults, capitalize, isArray, isFunction, result } from 'lodash'; import uiModules from 'ui/modules'; import filterTemplate from 'ui/chrome/config/filter.html'; @@ -29,7 +29,7 @@ export default function ($compile) { const opt = this._applyOptDefault(rawOpt); if (!opt.key) throw new TypeError('KbnTopNav: menu items must have a key'); this.opts.push(opt); - if (!opt.hideButton()) this.menuItems.push(opt); + if (!opt.hideButton) this.menuItems.push(opt); if (opt.template) this.templates[opt.key] = opt.template; }); } @@ -57,12 +57,12 @@ export default function ($compile) { label: capitalize(opt.key), hasFunction: !!opt.run, description: opt.run ? opt.key : `Toggle ${opt.key} view`, - run: (item) => !item.disableButton() && this.toggle(item.key) + run: (item) => !item.disableButton && this.toggle(item.key) }); - defaultedOpt.hideButton = isFunction(opt.hideButton) ? opt.hideButton : () => (opt.hideButton || false); - defaultedOpt.disableButton = isFunction(opt.disableButton) ? opt.disableButton : () => (opt.disableButton || false); - defaultedOpt.tooltip = isFunction(opt.tooltip) ? opt.tooltip : () => (opt.tooltip || ''); + defaultedOpt.hideButton = result(opt, 'hideButton', false); + defaultedOpt.disableButton = result(opt, 'disableButton', false); + defaultedOpt.tooltip = result(opt, 'tooltip', ''); return defaultedOpt; } From 2945d75df6c056dfb33d3629a7aab672b3e6f1f8 Mon Sep 17 00:00:00 2001 From: LeeDr Date: Wed, 6 Jul 2016 16:44:44 -0500 Subject: [PATCH 55/67] Fix a headerPage typo, un-nest some promises. --- test/support/page_objects/settings_page.js | 39 ++++++++-------------- 1 file changed, 13 insertions(+), 26 deletions(-) diff --git a/test/support/page_objects/settings_page.js b/test/support/page_objects/settings_page.js index f177d9f555a429..2b09a6e0a1e61f 100644 --- a/test/support/page_objects/settings_page.js +++ b/test/support/page_objects/settings_page.js @@ -239,40 +239,30 @@ export default class SettingsPage { goToPage(pageNum) { return this.remote.setFindTimeout(defaultFindTimeout) .findByCssSelector('ul.pagination-other-pages-list.pagination-sm.ng-scope li.ng-scope:nth-child(' + - (pageNum + 1) + ') a.ng-binding' - ) - .then(page => { - return page.click(); - }) + (pageNum + 1) + ') a.ng-binding') + .click() .then(function () { - return PageObjects.headerPage.getSpinnerDone(); + return PageObjects.header.getSpinnerDone(); }); } openControlsRow(row) { return this.remote.setFindTimeout(defaultFindTimeout) .findByCssSelector('table.table.table-condensed tbody tr:nth-child(' + - (row + 1) + ') td.ng-scope div.actions a.btn.btn-xs.btn-default i.fa.fa-pencil' - ) - .then(page => { - return page.click(); - }); + (row + 1) + ') td.ng-scope div.actions a.btn.btn-xs.btn-default i.fa.fa-pencil') + .click(); } openControlsByName(name) { return this.remote.setFindTimeout(defaultFindTimeout) .findByCssSelector('div.actions a.btn.btn-xs.btn-default[href$="/' + name + '"]') - .then(button => { - return button.click(); - }); + .click(); } increasePopularity() { return this.remote.setFindTimeout(defaultFindTimeout) .findByCssSelector('button.btn.btn-default[aria-label="Plus"]') - .then(button => { - return button.click(); - }) + .click() .then(() => { return PageObjects.header.getSpinnerDone(); }); @@ -289,9 +279,7 @@ export default class SettingsPage { controlChangeCancel() { return this.remote.setFindTimeout(defaultFindTimeout) .findByCssSelector('button.btn.btn-primary[aria-label="Cancel"]') - .then(button => { - return button.click(); - }) + .click() .then(() => { return PageObjects.header.getSpinnerDone(); }); @@ -300,9 +288,7 @@ export default class SettingsPage { controlChangeSave() { return this.remote.setFindTimeout(defaultFindTimeout) .findByCssSelector('button.btn.btn-success.ng-binding[aria-label="Update Field"]') - .then(button => { - return button.click(); - }) + .click() .then(() => { return PageObjects.header.getSpinnerDone(); }); @@ -311,9 +297,7 @@ export default class SettingsPage { setPageSize(size) { return this.remote.setFindTimeout(defaultFindTimeout) .findByCssSelector('form.form-inline.pagination-size.ng-scope.ng-pristine.ng-valid div.form-group option[label="' + size + '"]') - .then(button => { - return button.click(); - }) + .click() .then(() => { return PageObjects.header.getSpinnerDone(); }); @@ -355,10 +339,12 @@ export default class SettingsPage { var alertText; return PageObjects.common.try(() => { + PageObjects.common.debug('click delete index pattern button'); return this.clickDeletePattern(); }) .then(() => { return PageObjects.common.try(() => { + PageObjects.common.debug('getAlertText'); return this.remote.getAlertText(); }); }) @@ -367,6 +353,7 @@ export default class SettingsPage { }) .then(() => { return PageObjects.common.try(() => { + PageObjects.common.debug('acceptAlert'); return this.remote.acceptAlert(); }); }) From d44fe946cd07c38137f7111855373ff47f696307 Mon Sep 17 00:00:00 2001 From: Paul Echeverri Date: Thu, 30 Jun 2016 14:32:21 -0700 Subject: [PATCH 56/67] Stripped trailing whitespace throughout Fixes #7655 --- docs/access.asciidoc | 8 +- docs/advanced-settings.asciidoc | 2 +- docs/apps.asciidoc | 2 +- docs/area.asciidoc | 38 +++---- docs/autorefresh.asciidoc | 6 +- docs/color-formatter.asciidoc | 11 +- docs/color-picker.asciidoc | 2 +- docs/dashboard.asciidoc | 46 ++++----- docs/datatable.asciidoc | 48 ++++----- docs/discover.asciidoc | 140 ++++++++++++------------- docs/filter-pinning.asciidoc | 20 ++-- docs/getting-started.asciidoc | 146 +++++++++++++-------------- docs/introduction.asciidoc | 34 +++---- docs/kibana-yml.asciidoc | 34 +++---- docs/line.asciidoc | 26 ++--- docs/markdown.asciidoc | 4 +- docs/metric.asciidoc | 4 +- docs/pie.asciidoc | 60 +++++------ docs/production.asciidoc | 8 +- docs/releasenotes.asciidoc | 2 +- docs/settings.asciidoc | 144 +++++++++++++------------- docs/setup.asciidoc | 4 +- docs/string-formatter.asciidoc | 2 +- docs/tilemap.asciidoc | 92 ++++++++--------- docs/url-formatter.asciidoc | 12 +-- docs/vertbar.asciidoc | 32 +++--- docs/visualization-raw-data.asciidoc | 12 +-- docs/visualize.asciidoc | 64 ++++++------ docs/x-axis-aggs.asciidoc | 42 ++++---- docs/y-axis-aggs.asciidoc | 24 ++--- 30 files changed, 534 insertions(+), 535 deletions(-) diff --git a/docs/access.asciidoc b/docs/access.asciidoc index 43b6ec6689314d..8de87d5efa3e8f 100644 --- a/docs/access.asciidoc +++ b/docs/access.asciidoc @@ -1,11 +1,11 @@ [[access]] == Accessing Kibana -Kibana is a web application that you access through port 5601. All you need to do is point your web browser at the -machine where Kibana is running and specify the port number. For example, `localhost:5601` or +Kibana is a web application that you access through port 5601. All you need to do is point your web browser at the +machine where Kibana is running and specify the port number. For example, `localhost:5601` or `http://YOURDOMAIN.com:5601`. -When you access Kibana, the <> page loads by default with the default index pattern selected. The +When you access Kibana, the <> page loads by default with the default index pattern selected. The time filter is set to the last 15 minutes and the search query is set to match-all (\*). If you don't see any documents, try setting the time filter to a wider time range. @@ -14,7 +14,7 @@ If you still don't see any results, it's possible that you don't *have* any docu [[status]] === Checking Kibana Status -You can reach the Kibana server's status page by navigating to `localhost:5601/status`. The status page displays +You can reach the Kibana server's status page by navigating to `localhost:5601/status`. The status page displays information about the server's resource usage and lists the installed plugins. image::images/kibana-status-page.png[] diff --git a/docs/advanced-settings.asciidoc b/docs/advanced-settings.asciidoc index 3480997d8e5df3..93db870c0abf85 100644 --- a/docs/advanced-settings.asciidoc +++ b/docs/advanced-settings.asciidoc @@ -53,7 +53,7 @@ mentioned use "_default_". `timepicker:refreshIntervalDefaults`:: The time filter's default refresh interval. `dashboard:defaultDarkTheme`:: Set this property to `true` to make new dashboards use the dark theme by default. `filters:pinnedByDefault`:: Set this property to `true` to make filters have a global state by default. -`notifications:banner`:: You can specify a custom banner to display temporary notices to all users. This field supports +`notifications:banner`:: You can specify a custom banner to display temporary notices to all users. This field supports Markdown. `notifications:lifetime:banner`:: Specifies the duration in milliseconds for banner notification displays. The default value is 3000000. Set this field to `Infinity` to disable banner notifications. `notifications:lifetime:error`:: Specifies the duration in milliseconds for error notification displays. The default value is 300000. Set this field to `Infinity` to disable error notifications. diff --git a/docs/apps.asciidoc b/docs/apps.asciidoc index 747619d14d4e35..f1b38e37759142 100644 --- a/docs/apps.asciidoc +++ b/docs/apps.asciidoc @@ -1,7 +1,7 @@ [[kibana-apps]] == Kibana Apps -The Kibana UI serves as a framework that can contain several different applications. You can switch between these +The Kibana UI serves as a framework that can contain several different applications. You can switch between these applications by clicking the image:images/app-button.png[App Picker] *App picker* button to display the app bar: image::images/app-picker.png[] diff --git a/docs/area.asciidoc b/docs/area.asciidoc index 50227d73815414..f64a5922aae517 100644 --- a/docs/area.asciidoc +++ b/docs/area.asciidoc @@ -3,32 +3,32 @@ This chart's Y axis is the _metrics_ axis. The following aggregations are available for this axis: -*Count*:: The {ref}/search-aggregations-metrics-valuecount-aggregation.html[_count_] aggregation returns a raw count of +*Count*:: The {ref}/search-aggregations-metrics-valuecount-aggregation.html[_count_] aggregation returns a raw count of the elements in the selected index pattern. -*Average*:: This aggregation returns the {ref}/search-aggregations-metrics-avg-aggregation.html[_average_] of a numeric +*Average*:: This aggregation returns the {ref}/search-aggregations-metrics-avg-aggregation.html[_average_] of a numeric field. Select a field from the drop-down. -*Sum*:: The {ref}/search-aggregations-metrics-sum-aggregation.html[_sum_] aggregation returns the total sum of a numeric +*Sum*:: The {ref}/search-aggregations-metrics-sum-aggregation.html[_sum_] aggregation returns the total sum of a numeric field. Select a field from the drop-down. -*Min*:: The {ref}/search-aggregations-metrics-min-aggregation.html[_min_] aggregation returns the minimum value of a +*Min*:: The {ref}/search-aggregations-metrics-min-aggregation.html[_min_] aggregation returns the minimum value of a numeric field. Select a field from the drop-down. -*Max*:: The {ref}/search-aggregations-metrics-max-aggregation.html[_max_] aggregation returns the maximum value of a +*Max*:: The {ref}/search-aggregations-metrics-max-aggregation.html[_max_] aggregation returns the maximum value of a numeric field. Select a field from the drop-down. -*Unique Count*:: The {ref}/search-aggregations-metrics-cardinality-aggregation.html[_cardinality_] aggregation returns +*Unique Count*:: The {ref}/search-aggregations-metrics-cardinality-aggregation.html[_cardinality_] aggregation returns the number of unique values in a field. Select a field from the drop-down. -*Percentiles*:: The {ref}/search-aggregations-metrics-percentile-aggregation.html[_percentile_] aggregation divides the -values in a numeric field into percentile bands that you specify. Select a field from the drop-down, then specify one -or more ranges in the *Percentiles* fields. Click the *X* to remove a percentile field. Click *+ Add* to add a +*Percentiles*:: The {ref}/search-aggregations-metrics-percentile-aggregation.html[_percentile_] aggregation divides the +values in a numeric field into percentile bands that you specify. Select a field from the drop-down, then specify one +or more ranges in the *Percentiles* fields. Click the *X* to remove a percentile field. Click *+ Add* to add a percentile field. -*Percentile Rank*:: The {ref}/search-aggregations-metrics-percentile-rank-aggregation.html[_percentile ranks_] -aggregation returns the percentile rankings for the values in the numeric field you specify. Select a numeric field +*Percentile Rank*:: The {ref}/search-aggregations-metrics-percentile-rank-aggregation.html[_percentile ranks_] +aggregation returns the percentile rankings for the values in the numeric field you specify. Select a numeric field from the drop-down, then specify one or more percentile rank values in the *Values* fields. Click the *X* to remove a values field. Click *+Add* to add a values field. You can add an aggregation by clicking the *+ Add Metrics* button. include::x-axis-aggs.asciidoc[] -For example, a chart of dates with incident counts can display dates in chronological order, or you can raise the -priority of the incident-reporting aggregation to show the most active dates first. The chronological order might show +For example, a chart of dates with incident counts can display dates in chronological order, or you can raise the +priority of the incident-reporting aggregation to show the most active dates first. The chronological order might show a time-dependent pattern in incident count, and sorting by active dates can reveal particular outliers in your data. include::color-picker.asciidoc[] @@ -37,20 +37,20 @@ You can click the *Advanced* link to display more customization options for your *Exclude Pattern*:: Specify a pattern in this field to exclude from the results. *Include Pattern*:: Specify a pattern in this field to include in the results. -*JSON Input*:: A text field where you can add specific JSON-formatted properties to merge with the aggregation +*JSON Input*:: A text field where you can add specific JSON-formatted properties to merge with the aggregation definition, as in the following example: [source,shell] { "script" : "doc['grade'].value * 1.2" } -NOTE: In Elasticsearch releases 1.4.3 and later, this functionality requires you to enable +NOTE: In Elasticsearch releases 1.4.3 and later, this functionality requires you to enable {ref}/modules-scripting.html[dynamic Groovy scripting]. The availability of these options varies depending on the aggregation you choose. Select the *Options* tab to change the following aspects of the chart: -*Chart Mode*:: When you have multiple Y-axis aggregations defined for your chart, you can use this drop-down to affect +*Chart Mode*:: When you have multiple Y-axis aggregations defined for your chart, you can use this drop-down to affect how the aggregations display on the chart: _stacked_:: Stacks the aggregations on top of each other. @@ -62,9 +62,9 @@ _silhouette_:: Displays each aggregation as variance from a central line. Checkboxes are available to enable and disable the following behaviors: *Smooth Lines*:: Check this box to curve the top boundary of the area from point to point. -*Set Y-Axis Extents*:: Check this box and enter values in the *y-max* and *y-min* fields to set the Y axis to specific -values. -*Scale Y-Axis to Data Bounds*:: The default Y axis bounds are zero and the maximum value returned in the data. Check +*Set Y-Axis Extents*:: Check this box and enter values in the *y-max* and *y-min* fields to set the Y axis to specific +values. +*Scale Y-Axis to Data Bounds*:: The default Y axis bounds are zero and the maximum value returned in the data. Check this box to change both upper and lower bounds to match the values returned in the data. *Show Tooltip*:: Check this box to enable the display of tooltips. diff --git a/docs/autorefresh.asciidoc b/docs/autorefresh.asciidoc index 927aff68f0d7bd..fff1a3d9236747 100644 --- a/docs/autorefresh.asciidoc +++ b/docs/autorefresh.asciidoc @@ -1,5 +1,5 @@ === Automatically Refreshing the Page -You can configure a refresh interval to automatically refresh the page with the latest index data. This periodically +You can configure a refresh interval to automatically refresh the page with the latest index data. This periodically resubmits the search query. When a refresh interval is set, it is displayed to the left of the Time Filter in the menu bar. @@ -10,10 +10,10 @@ To set the refresh interval: . Click the *Refresh Interval* tab. . Choose a refresh interval from the list. -To automatically refresh the data, click the image:images/autorefresh.png[] *Auto-refresh* button when the time picker +To automatically refresh the data, click the image:images/autorefresh.png[] *Auto-refresh* button when the time picker is open and select an autorefresh interval: image::images/autorefresh-intervals.png[] -When auto-refresh is enabled, Kibana's top bar displays a pause button and the auto-refresh interval: +When auto-refresh is enabled, Kibana's top bar displays a pause button and the auto-refresh interval: image:images/autorefresh-pause.png[]. Click the *Pause* button to pause auto-refresh. diff --git a/docs/color-formatter.asciidoc b/docs/color-formatter.asciidoc index 601a4b3d38ff75..d9ba5e9be11657 100644 --- a/docs/color-formatter.asciidoc +++ b/docs/color-formatter.asciidoc @@ -1,11 +1,10 @@ The `Color` field formatter enables you to specify colors with specific ranges of values for a numeric field. -When you select the `Color` field formatter, Kibana displays the *Range*, *Font Color*, *Background Color*, and *Example* fields. +When you select the `Color` field formatter, Kibana displays the *Range*, *Font Color*, *Background Color*, and +*Example* fields. -Click the *Add Color* button to add a range of values to associate with a particular color. You can click in the *Font Color* and -*Background Color* fields to display a color picker. You can also enter a specific hex code value in the field. The effect of your current -color choices are displayed in the *Example* field. +Click the *Add Color* button to add a range of values to associate with a particular color. You can click in the *Font +Color* and *Background Color* fields to display a color picker. You can also enter a specific hex code value in the +field. The effect of your current color choices are displayed in the *Example* field. image::images/colorformatter.png[] - -//update image diff --git a/docs/color-picker.asciidoc b/docs/color-picker.asciidoc index 5c3bf129d58f76..e0f23262068d39 100644 --- a/docs/color-picker.asciidoc +++ b/docs/color-picker.asciidoc @@ -1,4 +1,4 @@ -You can customize the colors of your visualization by clicking the color dot next to each label to display the +You can customize the colors of your visualization by clicking the color dot next to each label to display the _color picker_. image::images/color-picker.png[An array of color dots that users can select] diff --git a/docs/dashboard.asciidoc b/docs/dashboard.asciidoc index 31d957de8887b7..c46de14bc51b0e 100644 --- a/docs/dashboard.asciidoc +++ b/docs/dashboard.asciidoc @@ -1,7 +1,7 @@ [[dashboard]] == Dashboard -A Kibana _dashboard_ displays a set of saved visualizations in groups that you can arrange freely. You can save a +A Kibana _dashboard_ displays a set of saved visualizations in groups that you can arrange freely. You can save a dashboard to share or reload at a later time. .Sample dashboard @@ -21,7 +21,7 @@ The first time you click the *Dashboard* tab, Kibana displays an empty dashboard image:images/NewDashboard.png[New Dashboard screen] -Build your dashboard by adding visualizations. By default, Kibana dashboards use a light color theme. To use a dark color +Build your dashboard by adding visualizations. By default, Kibana dashboards use a light color theme. To use a dark color theme instead, click the *Options* button and check the *Use dark theme* box. NOTE: You can change the default theme in the *Advanced* section of the *Settings* tab. @@ -34,37 +34,37 @@ include::autorefresh.asciidoc[] [[adding-visualizations-to-a-dashboard]] ==== Adding Visualizations to a Dashboard -To add a visualization to the dashboard, click the *Add* button in the toolbar panel. Select a saved visualization -from the list. You can filter the list of visualizations by typing a filter string into the *Visualization Filter* +To add a visualization to the dashboard, click the *Add* button in the toolbar panel. Select a saved visualization +from the list. You can filter the list of visualizations by typing a filter string into the *Visualization Filter* field. The visualization you select appears in a _container_ on your dashboard. -NOTE: If you see a message about the container's height or width being too small, <>. [float] [[saving-dashboards]] ==== Saving Dashboards -To save the dashboard, click the *Save Dashboard* button in the toolbar panel, enter a name for the dashboard in the -*Save As* field, and click the *Save* button. By default, dashboards store the time period specified in the time filter -when you save a dashboard. To disable this behavior, clear the *Store time with dashboard* box before clicking the +To save the dashboard, click the *Save Dashboard* button in the toolbar panel, enter a name for the dashboard in the +*Save As* field, and click the *Save* button. By default, dashboards store the time period specified in the time filter +when you save a dashboard. To disable this behavior, clear the *Store time with dashboard* box before clicking the *Save* button. [float] [[loading-a-saved-dashboard]] ==== Loading a Saved Dashboard -Click the *Load Saved Dashboard* button to display a list of existing dashboards. The saved dashboard selector includes -a text field to filter by dashboard name and a link to the Object Editor for managing your saved dashboards. You can +Click the *Load Saved Dashboard* button to display a list of existing dashboards. The saved dashboard selector includes +a text field to filter by dashboard name and a link to the Object Editor for managing your saved dashboards. You can also access the Object Editor by clicking *Settings > Objects*. [float] [[sharing-dashboards]] ==== Sharing Dashboards -You can share dashboards with other users. You can share a direct link to the Kibana dashboard or embed the dashboard +You can share dashboards with other users. You can share a direct link to the Kibana dashboard or embed the dashboard in your Web page. NOTE: A user must have Kibana access in order to view embedded dashboards. @@ -72,7 +72,7 @@ NOTE: A user must have Kibana access in order to view embedded dashboards. To share a dashboard, click the *Share* button image:images/share-dashboard.png[] to display the _Sharing_ panel. Click the *Copy to Clipboard* button image:images/share-link.png[] to copy the native URL or embed HTML to the clipboard. -Click the *Generate short URL* button image:images/share-short-link.png[] to create a shortened URL for sharing or +Click the *Generate short URL* button image:images/share-short-link.png[] to create a shortened URL for sharing or embedding. [float] @@ -85,40 +85,40 @@ To embed a dashboard, copy the embed code from the _Share_ display into your ext [[customizing-your-dashboard]] === Customizing Dashboard Elements -The visualizations in your dashboard are stored in resizable _containers_ that you can arrange on the dashboard. This +The visualizations in your dashboard are stored in resizable _containers_ that you can arrange on the dashboard. This section discusses customizing these containers. [float] [[moving-containers]] ==== Moving Containers -Click and hold a container's header to move the container around the dashboard. Other containers will shift as needed +Click and hold a container's header to move the container around the dashboard. Other containers will shift as needed to make room for the moving container. Release the mouse button to confirm the container's new location. [float] [[resizing-containers]] ==== Resizing Containers -Move the cursor to the bottom right corner of the container until the cursor changes to point at the corner. After the -cursor changes, click and drag the corner of the container to change the container's size. Release the mouse button to +Move the cursor to the bottom right corner of the container until the cursor changes to point at the corner. After the +cursor changes, click and drag the corner of the container to change the container's size. Release the mouse button to confirm the new container size. [float] [[removing-containers]] ==== Removing Containers -Click the *x* icon at the top right corner of a container to remove that container from the dashboard. Removing a +Click the *x* icon at the top right corner of a container to remove that container from the dashboard. Removing a container from a dashboard does not delete the saved visualization in that container. [float] [[viewing-detailed-information]] ==== Viewing Detailed Information -To display the raw data behind the visualization, click the bar at the bottom of the container. Tabs with detailed +To display the raw data behind the visualization, click the bar at the bottom of the container. Tabs with detailed information about the raw data replace the visualization, as in this example: .Table -A representation of the underlying data, presented as a paginated data grid. You can sort the items +A representation of the underlying data, presented as a paginated data grid. You can sort the items in the table by clicking on the table headers at the top of each column. image:images/NYCTA-Table.jpg[] @@ -131,20 +131,20 @@ The raw response from the server, presented in JSON format. image:images/NYCTA-Response.jpg[] .Statistics -A summary of the statistics related to the request and the response, presented as a data grid. The data -grid includes the query duration, the request duration, the total number of records found on the server, and the +A summary of the statistics related to the request and the response, presented as a data grid. The data +grid includes the query duration, the request duration, the total number of records found on the server, and the index pattern used to make the query. image:images/NYCTA-Statistics.jpg[] To export the raw data behind the visualization as a comma-separated-values (CSV) file, click on either the -*Raw* or *Formatted* links at the bottom of any of the detailed information tabs. A raw export contains the data as it +*Raw* or *Formatted* links at the bottom of any of the detailed information tabs. A raw export contains the data as it is stored in Elasticsearch. A formatted export contains the results of any applicable Kibana [field formatters]. [float] [[changing-the-visualization]] === Changing the Visualization -Click the _Edit_ button image:images/EditVis.png[Pencil button] at the top right of a container to open the +Click the _Edit_ button image:images/EditVis.png[Pencil button] at the top right of a container to open the visualization in the <> page. [float] diff --git a/docs/datatable.asciidoc b/docs/datatable.asciidoc index 0c63d610b64d91..b5130b58131355 100644 --- a/docs/datatable.asciidoc +++ b/docs/datatable.asciidoc @@ -8,39 +8,39 @@ the table into additional tables. Each bucket type supports the following aggregations: -*Date Histogram*:: A {ref}search-aggregations-bucket-datehistogram-aggregation.html[_date histogram_] is built from a -numeric field and organized by date. You can specify a time frame for the intervals in seconds, minutes, hours, days, -weeks, months, or years. You can also specify a custom interval frame by selecting *Custom* as the interval and -specifying a number and a time unit in the text field. Custom interval time units are *s* for seconds, *m* for minutes, -*h* for hours, *d* for days, *w* for weeks, and *y* for years. Different units support different levels of precision, +*Date Histogram*:: A {ref}search-aggregations-bucket-datehistogram-aggregation.html[_date histogram_] is built from a +numeric field and organized by date. You can specify a time frame for the intervals in seconds, minutes, hours, days, +weeks, months, or years. You can also specify a custom interval frame by selecting *Custom* as the interval and +specifying a number and a time unit in the text field. Custom interval time units are *s* for seconds, *m* for minutes, +*h* for hours, *d* for days, *w* for weeks, and *y* for years. Different units support different levels of precision, down to one second. -*Histogram*:: A standard {ref}search-aggregations-bucket-histogram-aggregation.html[_histogram_] is built from a -numeric field. Specify an integer interval for this field. Select the *Show empty buckets* checkbox to include empty +*Histogram*:: A standard {ref}search-aggregations-bucket-histogram-aggregation.html[_histogram_] is built from a +numeric field. Specify an integer interval for this field. Select the *Show empty buckets* checkbox to include empty intervals in the histogram. -*Range*:: With a {ref}search-aggregations-bucket-range-aggregation.html[_range_] aggregation, you can specify ranges -of values for a numeric field. Click *Add Range* to add a set of range endpoints. Click the red *(x)* symbol to remove +*Range*:: With a {ref}search-aggregations-bucket-range-aggregation.html[_range_] aggregation, you can specify ranges +of values for a numeric field. Click *Add Range* to add a set of range endpoints. Click the red *(x)* symbol to remove a range. -*Date Range*:: A {ref}search-aggregations-bucket-daterange-aggregation.html[_date range_] aggregation reports values -that are within a range of dates that you specify. You can specify the ranges for the dates using -{ref}common-options.html#date-math[_date math_] expressions. Click *Add Range* to add a set of range endpoints. +*Date Range*:: A {ref}search-aggregations-bucket-daterange-aggregation.html[_date range_] aggregation reports values +that are within a range of dates that you specify. You can specify the ranges for the dates using +{ref}common-options.html#date-math[_date math_] expressions. Click *Add Range* to add a set of range endpoints. Click the red *(/)* symbol to remove a range. *IPv4 Range*:: The {ref}search-aggregations-bucket-iprange-aggregation.html[_IPv4 range_] aggregation enables you to -specify ranges of IPv4 addresses. Click *Add Range* to add a set of range endpoints. Click the red *(/)* symbol to +specify ranges of IPv4 addresses. Click *Add Range* to add a set of range endpoints. Click the red *(/)* symbol to remove a range. -*Terms*:: A {ref}search-aggregations-bucket-terms-aggregation.html[_terms_] aggregation enables you to specify the top +*Terms*:: A {ref}search-aggregations-bucket-terms-aggregation.html[_terms_] aggregation enables you to specify the top or bottom _n_ elements of a given field to display, ordered by count or a custom metric. -*Filters*:: You can specify a set of {ref}search-aggregations-bucket-filters-aggregation.html[_filters_] for the data. -You can specify a filter as a query string or in JSON format, just as in the Discover search bar. Click *Add Filter* to -add another filter. Click the image:images/labelbutton.png[] *label* button to open the label field, where you can type +*Filters*:: You can specify a set of {ref}search-aggregations-bucket-filters-aggregation.html[_filters_] for the data. +You can specify a filter as a query string or in JSON format, just as in the Discover search bar. Click *Add Filter* to +add another filter. Click the image:images/labelbutton.png[] *label* button to open the label field, where you can type in a name to display on the visualization. -*Significant Terms*:: Displays the results of the experimental -{ref}search-aggregations-bucket-significantterms-aggregation.html[_significant terms_] aggregation. The value of the +*Significant Terms*:: Displays the results of the experimental +{ref}search-aggregations-bucket-significantterms-aggregation.html[_significant terms_] aggregation. The value of the *Size* parameter defines the number of entries this aggregation returns. -*Geohash*:: The {ref}search-aggregations-bucket-geohashgrid-aggregation.html[_geohash_] aggregation displays points +*Geohash*:: The {ref}search-aggregations-bucket-geohashgrid-aggregation.html[_geohash_] aggregation displays points based on the geohash coordinates. -Once you've specified a bucket type aggregation, you can define sub-buckets to refine the visualization. Click -*+ Add sub-buckets* to define a sub-bucket, then choose *Split Rows* or *Split Table*, then select an +Once you've specified a bucket type aggregation, you can define sub-buckets to refine the visualization. Click +*+ Add sub-buckets* to define a sub-bucket, then choose *Split Rows* or *Split Table*, then select an aggregation from the list of types. You can use the up or down arrows to the right of the aggregation's type to change the aggregation's priority. @@ -51,13 +51,13 @@ You can click the *Advanced* link to display more customization options for your *Exclude Pattern*:: Specify a pattern in this field to exclude from the results. *Include Pattern*:: Specify a pattern in this field to include in the results. -*JSON Input*:: A text field where you can add specific JSON-formatted properties to merge with the aggregation +*JSON Input*:: A text field where you can add specific JSON-formatted properties to merge with the aggregation definition, as in the following example: [source,shell] { "script" : "doc['grade'].value * 1.2" } -NOTE: In Elasticsearch releases 1.4.3 and later, this functionality requires you to enable +NOTE: In Elasticsearch releases 1.4.3 and later, this functionality requires you to enable {ref}modules-scripting.html[dynamic Groovy scripting]. The availability of these options varies depending on the aggregation you choose. diff --git a/docs/discover.asciidoc b/docs/discover.asciidoc index 75f6e58ec5158e..0a003bba5d5241 100644 --- a/docs/discover.asciidoc +++ b/docs/discover.asciidoc @@ -1,17 +1,17 @@ [[discover]] == Discover -You can interactively explore your data from the Discover page. You have access to every document in every index that -matches the selected index pattern. You can submit search queries, filter the search results, and view document data. -You can also see the number of documents that match the search query and get field value statistics. If a time field is -configured for the selected index pattern, the distribution of documents over time is displayed in a histogram at the -top of the page. +You can interactively explore your data from the Discover page. You have access to every document in every index that +matches the selected index pattern. You can submit search queries, filter the search results, and view document data. +You can also see the number of documents that match the search query and get field value statistics. If a time field is +configured for the selected index pattern, the distribution of documents over time is displayed in a histogram at the +top of the page. image::images/Discover-Start-Annotated.jpg[Discover Page] [float] [[set-time-filter]] === Setting the Time Filter -The Time Filter restricts the search results to a specific time period. You can set a time filter if your index +The Time Filter restricts the search results to a specific time period. You can set a time filter if your index contains time-based events and a time-field is configured for the selected index pattern. By default the time filter is set to the last 15 minutes. You can use the Time Picker to change the time filter @@ -23,56 +23,56 @@ To set a time filter with the Time Picker: . To set a quick filter, simply click one of the shortcut links. . To specify a relative Time Filter, click *Relative* and enter the relative start time. You can specify the relative start time as any number of seconds, minutes, hours, days, months, or years ago. -. To specify an absolute Time Filter, click *Absolute* and enter the start date in the *From* field and the end date in +. To specify an absolute Time Filter, click *Absolute* and enter the start date in the *From* field and the end date in the *To* field. -. Click the caret at the bottom of the Time Picker to hide it. +. Click the caret at the bottom of the Time Picker to hide it. To set a Time Filter from the histogram, do one of the following: * Click the bar that represents the time interval you want to zoom in on. -* Click and drag to view a specific timespan. You must start the selection with the cursor over the background of the -chart--the cursor changes to a plus sign when you hover over a valid start point. +* Click and drag to view a specific timespan. You must start the selection with the cursor over the background of the +chart--the cursor changes to a plus sign when you hover over a valid start point. -You can use the browser Back button to undo your changes. +You can use the browser Back button to undo your changes. -The histogram lists the time range you're currently exploring, as well as the intervals that range is currently using. -To change the intervals, click the link and select an interval from the drop-down. The default behavior automatically +The histogram lists the time range you're currently exploring, as well as the intervals that range is currently using. +To change the intervals, click the link and select an interval from the drop-down. The default behavior automatically sets an interval based on the time range. [float] [[search]] === Searching Your Data You can search the indices that match the current index pattern by submitting a search from the Discover page. -You can enter simple query strings, use the -Lucene https://lucene.apache.org/core/2_9_4/queryparsersyntax.html[query syntax], or use the full JSON-based -{ref}/query-dsl.html[Elasticsearch Query DSL]. +You can enter simple query strings, use the +Lucene https://lucene.apache.org/core/2_9_4/queryparsersyntax.html[query syntax], or use the full JSON-based +{ref}/query-dsl.html[Elasticsearch Query DSL]. -When you submit a search, the histogram, Documents table, and Fields list are updated to reflect +When you submit a search, the histogram, Documents table, and Fields list are updated to reflect the search results. The total number of hits (matching documents) is shown in the upper right corner of the -histogram. The Documents table shows the first five hundred hits. By default, the hits are listed in reverse -chronological order, with the newest documents shown first. You can reverse the sort order by by clicking on the Time -column header. You can also sort the table using the values in any indexed field. For more information, see +histogram. The Documents table shows the first five hundred hits. By default, the hits are listed in reverse +chronological order, with the newest documents shown first. You can reverse the sort order by by clicking on the Time +column header. You can also sort the table using the values in any indexed field. For more information, see <>. To search your data: -. Enter a query string in the Search field: +. Enter a query string in the Search field: + -* To perform a free text search, simply enter a text string. For example, if you're searching web server logs, you +* To perform a free text search, simply enter a text string. For example, if you're searching web server logs, you could enter `safari` to search all fields for the term `safari`. + -* To search for a value in a specific field, you prefix the value with the name of the field. For example, you could +* To search for a value in a specific field, you prefix the value with the name of the field. For example, you could enter `status:200` to limit the results to entries that contain the value `200` in the `status` field. + -* To search for a range of values, you can use the bracketed range syntax, `[START_VALUE TO END_VALUE]`. For example, +* To search for a range of values, you can use the bracketed range syntax, `[START_VALUE TO END_VALUE]`. For example, to find entries that have 4xx status codes, you could enter `status:[400 TO 499]`. + * To specify more complex search criteria, you can use the Boolean operators `AND`, `OR`, and `NOT`. For example, -to find entries that have 4xx status codes and have an extension of `php` or `html`, you could enter `status:[400 TO +to find entries that have 4xx status codes and have an extension of `php` or `html`, you could enter `status:[400 TO 499] AND (extension:php OR extension:html)`. + -NOTE: These examples use the Lucene query syntax. You can also submit queries using the Elasticsearch Query DSL. For -examples, see {ref}/query-dsl-query-string-query.html#query-string-syntax[query string syntax] in the Elasticsearch +NOTE: These examples use the Lucene query syntax. You can also submit queries using the Elasticsearch Query DSL. For +examples, see {ref}/query-dsl-query-string-query.html#query-string-syntax[query string syntax] in the Elasticsearch Reference. + . Press *Enter* or click the *Search* button to submit your search query. @@ -90,7 +90,7 @@ Saving a search saves both the search query string and the currently selected in To save the current search: -. Click the *Save* button in the Discover toolbar. +. Click the *Save* button in the Discover toolbar. . Enter a name for the search and click *Save*. [float] @@ -101,13 +101,13 @@ To load a saved search: . Click the *Open* button in the Discover toolbar. . Select the search you want to open. -If the saved search is associated with a different index pattern than is currently selected, opening the saved search +If the saved search is associated with a different index pattern than is currently selected, opening the saved search also changes the selected index pattern. [float] [[select-pattern]] ==== Changing Which Indices You're Searching -When you submit a search request, the indices that match the currently-selected index pattern are searched. The current +When you submit a search request, the indices that match the currently-selected index pattern are searched. The current index pattern is shown below the search field. To change which indices you are searching, click the name of the current index pattern to display a list of the configured index patterns and select a different index pattern. @@ -121,36 +121,36 @@ include::autorefresh.asciidoc[] [float] [[field-filter]] === Filtering by Field -You can filter the search results to display only those documents that contain a particular value in a field. You can +You can filter the search results to display only those documents that contain a particular value in a field. You can also create negative filters that exclude documents that contain the specified field value. -You can add filters from the Fields list or from the Documents table. When you add a filter, it is displayed in the -filter bar below the search query. From the filter bar, you can enable or disable a filter, invert the filter (change -it from a positive filter to a negative filter and vice-versa), toggle the filter on or off, or remove it entirely. +You can add filters from the Fields list or from the Documents table. When you add a filter, it is displayed in the +filter bar below the search query. From the filter bar, you can enable or disable a filter, invert the filter (change +it from a positive filter to a negative filter and vice-versa), toggle the filter on or off, or remove it entirely. Click the small left-facing arrow to the right of the index pattern selection drop-down to collapse the Fields list. To add a filter from the Fields list: -. Click the name of the field you want to filter on. This displays the top five values for that field. To the right of -each value, there are two magnifying glass buttons--one for adding a regular (positive) filter, and -one for adding a negative filter. -. To add a positive filter, click the *Positive Filter* button image:images/PositiveFilter.jpg[Positive Filter Button]. +. Click the name of the field you want to filter on. This displays the top five values for that field. To the right of +each value, there are two magnifying glass buttons--one for adding a regular (positive) filter, and +one for adding a negative filter. +. To add a positive filter, click the *Positive Filter* button image:images/PositiveFilter.jpg[Positive Filter Button]. This filters out documents that don't contain that value in the field. -. To add a negative filter, click the *Negative Filter* button image:images/NegativeFilter.jpg[Negative Filter Button]. -This excludes documents that contain that value in the field. +. To add a negative filter, click the *Negative Filter* button image:images/NegativeFilter.jpg[Negative Filter Button]. +This excludes documents that contain that value in the field. To add a filter from the Documents table: -. Expand a document in the Documents table by clicking the *Expand* button image:images/ExpandButton.jpg[Expand Button] -to the left of the document's entry in the first column (the first column is usually Time). To the right of each field -name, there are two magnifying glass buttons--one for adding a regular (positive) filter, and one for adding a negative -filter. -. To add a positive filter based on the document's value in a field, click the -*Positive Filter* button image:images/PositiveFilter.jpg[Positive Filter Button]. This filters out documents that don't +. Expand a document in the Documents table by clicking the *Expand* button image:images/ExpandButton.jpg[Expand Button] +to the left of the document's entry in the first column (the first column is usually Time). To the right of each field +name, there are two magnifying glass buttons--one for adding a regular (positive) filter, and one for adding a negative +filter. +. To add a positive filter based on the document's value in a field, click the +*Positive Filter* button image:images/PositiveFilter.jpg[Positive Filter Button]. This filters out documents that don't contain the specified value in that field. -. To add a negative filter based on the document's value in a field, click the -*Negative Filter* button image:images/NegativeFilter.jpg[Negative Filter Button]. This excludes documents that contain -the specified value in that field. +. To add a negative filter based on the document's value in a field, click the +*Negative Filter* button image:images/NegativeFilter.jpg[Negative Filter Button]. This excludes documents that contain +the specified value in that field. [float] [[discover-filters]] @@ -159,52 +159,52 @@ include::filter-pinning.asciidoc[] [float] [[document-data]] === Viewing Document Data -When you submit a search query, the 500 most recent documents that match the query are listed in the Documents table. -You can configure the number of documents shown in the table by setting the `discover:sampleSize` property in -<>. By default, the table shows the localized version of the time field specified -in the selected index pattern and the document `_source`. You can <> +When you submit a search query, the 500 most recent documents that match the query are listed in the Documents table. +You can configure the number of documents shown in the table by setting the `discover:sampleSize` property in +<>. By default, the table shows the localized version of the time field specified +in the selected index pattern and the document `_source`. You can <> from the Fields list. You can <> by any indexed field that's included in the table. -To view a document's field data, click the *Expand* button image:images/ExpandButton.jpg[Expand Button] to the left of -the document's entry in the first column (the first column is usually Time). Kibana reads the document data from -Elasticsearch and displays the document fields in a table. The table contains a row for each field that contains the +To view a document's field data, click the *Expand* button image:images/ExpandButton.jpg[Expand Button] to the left of +the document's entry in the first column (the first column is usually Time). Kibana reads the document data from +Elasticsearch and displays the document fields in a table. The table contains a row for each field that contains the name of the field, add filter buttons, and the field value. image::images/Expanded-Document.png[] . To view the original JSON document (pretty-printed), click the *JSON* tab. -. To view the document data as a separate page, click the link. You can bookmark and share this link to provide direct +. To view the document data as a separate page, click the link. You can bookmark and share this link to provide direct access to a particular document. . To collapse the document details, click the *Collapse* button image:images/CollapseButton.jpg[Collapse Button]. -. To toggle a particular field's column in the Documents table, click the +. To toggle a particular field's column in the Documents table, click the image:images/add-column-button.png[Add Column] *Toggle column in table* button. [float] [[sorting]] ==== Sorting the Document List -You can sort the documents in the Documents table by the values in any indexed field. Documents in index patterns that +You can sort the documents in the Documents table by the values in any indexed field. Documents in index patterns that are configured with time fields are sorted in reverse chronological order by default. -To change the sort order, click the name of the field you want to sort by. The fields you can use for sorting have a +To change the sort order, click the name of the field you want to sort by. The fields you can use for sorting have a sort button to the right of the field name. Clicking the field name a second time reverses the sort order. [float] [[adding-columns]] ==== Adding Field Columns to the Documents Table -By default, the Documents table shows the localized version of the time field specified in the selected index pattern +By default, the Documents table shows the localized version of the time field specified in the selected index pattern and the document `_source`. You can add fields to the table from the Fields list or from a document's expanded view. To add field columns to the Documents table: -. Mouse over a field in the Fields list and click its *add* button image:images/AddFieldButton.jpg[Add Field Button]. +. Mouse over a field in the Fields list and click its *add* button image:images/AddFieldButton.jpg[Add Field Button]. . Repeat until you've added all the fields you want to display in the Documents table. -. Alternately, add a field column directly from a document's expanded view by clicking the +. Alternately, add a field column directly from a document's expanded view by clicking the image:images/add-column-button.png[Add Column] *Toggle column in table* button. The added field columns replace the `_source` column in the Documents table. The added fields are also -listed in the *Selected Fields* section at the top of the field list. +listed in the *Selected Fields* section at the top of the field list. -To rearrange the field columns in the table, mouse over the header of the column you want to move and click the *Move* +To rearrange the field columns in the table, mouse over the header of the column you want to move and click the *Move* button. image:images/Discover-MoveColumn.jpg[Move Column] @@ -214,18 +214,18 @@ image:images/Discover-MoveColumn.jpg[Move Column] ==== Removing Field Columns from the Documents Table To remove field columns from the Documents table: -. Mouse over the field you want to remove in the *Selected Fields* section of the Fields list and click its *remove* +. Mouse over the field you want to remove in the *Selected Fields* section of the Fields list and click its *remove* button image:images/RemoveFieldButton.jpg[Remove Field Button]. . Repeat until you've removed all the fields you want to drop from the Documents table. [float] [[viewing-field-stats]] === Viewing Field Data Statistics -From the field list, you can see how many documents in the Documents table contain a particular field, what the top 5 -values are, and what percentage of documents contain each value. +From the field list, you can see how many documents in the Documents table contain a particular field, what the top 5 +values are, and what percentage of documents contain each value. -To view field data statistics, click the name of a field in the Fields list. The field can be anywhere in the Fields -list. +To view field data statistics, click the name of a field in the Fields list. The field can be anywhere in the Fields +list. image:images/Discover-FieldStats.jpg[Field Statistics] diff --git a/docs/filter-pinning.asciidoc b/docs/filter-pinning.asciidoc index ac1176a245a6b0..055cf8f22d171d 100644 --- a/docs/filter-pinning.asciidoc +++ b/docs/filter-pinning.asciidoc @@ -1,6 +1,6 @@ === Working with Filters -When you create a filter anywhere in Kibana, the filter conditions display in an oval under the search text +When you create a filter anywhere in Kibana, the filter conditions display in an oval under the search text entry box: image::images/filter-sample.png[] @@ -9,16 +9,16 @@ Hovering on the filter oval displays the following icons: image::images/filter-allbuttons.png[] -Enable Filter image:images/filter-enable.png[]:: Click this icon to disable the filter without removing it. You can -enable the filter again later by clicking the icon again. Disabled filters display a striped shaded color, grey for +Enable Filter image:images/filter-enable.png[]:: Click this icon to disable the filter without removing it. You can +enable the filter again later by clicking the icon again. Disabled filters display a striped shaded color, grey for inclusion filters and red for exclusion filters. Pin Filter image:images/filter-pin.png[]:: Click this icon to _pin_ a filter. Pinned filters persist across Kibana tabs. -You can pin filters from the _Visualize_ tab, click on the _Discover_ or _Dashboard_ tabs, and those filters remain in +You can pin filters from the _Visualize_ tab, click on the _Discover_ or _Dashboard_ tabs, and those filters remain in place. -NOTE: If you have a pinned filter and you're not seeing any query results, that your current tab's index pattern is one -that the filter applies to. -Toggle Filter image:images/filter-toggle.png[]:: Click this icon to _toggle_ a filter. By default, filters are inclusion -filters, and display in grey. Only elements that match the filter are displayed. To change this to an exclusion +NOTE: If you have a pinned filter and you're not seeing any query results, that your current tab's index pattern is one +that the filter applies to. +Toggle Filter image:images/filter-toggle.png[]:: Click this icon to _toggle_ a filter. By default, filters are inclusion +filters, and display in grey. Only elements that match the filter are displayed. To change this to an exclusion filters, displaying only elements that _don't_ match, toggle the filter. Exclusion filters display in red. Remove Filter image:images/filter-delete.png[]:: Click this icon to remove a filter entirely. Custom Filter image:images/filter-custom.png[]:: Click this icon to display a text field where you can customize the JSON @@ -26,7 +26,7 @@ representation of the filter and specify an alias to use for the filter name: + image::images/filter-custom-json.png[] + -You can use JSON filter representation to implement predicate logic, with `should` for OR, `must` for AND, and `must_not` +You can use JSON filter representation to implement predicate logic, with `should` for OR, `must` for AND, and `must_not` for NOT: + .OR Example @@ -94,5 +94,5 @@ for NOT: ========== Click the *Done* button to update the filter with your changes. -To apply any of the filter actions to all the filters currently in place, click the image:images/filter-actions.png[] +To apply any of the filter actions to all the filters currently in place, click the image:images/filter-actions.png[] *Global Filter Actions* button and select an action. diff --git a/docs/getting-started.asciidoc b/docs/getting-started.asciidoc index db3c479c284d12..fbda773c72be47 100644 --- a/docs/getting-started.asciidoc +++ b/docs/getting-started.asciidoc @@ -1,7 +1,7 @@ [[getting-started]] == Getting Started with Kibana -Now that you have Kibana <>, you can step through this tutorial to get fast hands-on experience with +Now that you have Kibana <>, you can step through this tutorial to get fast hands-on experience with key Kibana functionality. By the end of this tutorial, you will have: * Loaded a sample data set into your Elasticsearch installation @@ -17,7 +17,7 @@ Video tutorials are also available: * https://www.elastic.co/blog/kibana-4-video-tutorials-part-1[High-level Kibana introduction, pie charts] * https://www.elastic.co/blog/kibana-4-video-tutorials-part-2[Data discovery, bar charts, and line charts] * https://www.elastic.co/blog/kibana-4-video-tutorials-part-3[Tile maps] -* https://www.elastic.co/blog/kibana-4-video-tutorials-part-4[Embedding Kibana visualizations] +* https://www.elastic.co/blog/kibana-4-video-tutorials-part-4[Embedding Kibana visualizations] [float] [[tutorial-load-dataset]] @@ -25,11 +25,11 @@ Video tutorials are also available: The tutorials in this section rely on the following data sets: -* The complete works of William Shakespeare, suitably parsed into fields. Download this data set by clicking here: +* The complete works of William Shakespeare, suitably parsed into fields. Download this data set by clicking here: https://www.elastic.co/guide/en/kibana/3.0/snippets/shakespeare.json[shakespeare.json]. -* A set of fictitious accounts with randomly generated data, in CSV format. Download this data set by clicking here: +* A set of fictitious accounts with randomly generated data, in CSV format. Download this data set by clicking here: https://www.github.com/elastic/kibana/docs/tutorial/accounts.csv[accounts.csv] -* A set of randomly generated log files. Download this data set by clicking here: +* A set of randomly generated log files. Download this data set by clicking here: https://download.elastic.co/demos/kibana/gettingstarted/logs.jsonl.gz[logs.jsonl.gz] Extract the logs with the following command: @@ -75,8 +75,8 @@ The schema for the logs data set has dozens of different fields, but the notable "@timestamp": "date" } -Before we load the Shakespeare and logs data sets, we need to set up {ref}mapping.html[_mappings_] for the fields. -Mapping divides the documents in the index into logical groups and specifies a field's characteristics, such as the +Before we load the Shakespeare and logs data sets, we need to set up {ref}mapping.html[_mappings_] for the fields. +Mapping divides the documents in the index into logical groups and specifies a field's characteristics, such as the field's searchability or whether or not it's _tokenized_, or broken up into separate words. Use the following command to set up a mapping for the Shakespeare data set: @@ -108,7 +108,7 @@ there are multiple words in the field. * The same applies to the _play_name_ field. * The _line_id_ and _speech_number_ fields are integers. -The logs data set requires a mapping to label the latitude/longitude pairs in the logs as geographic locations by +The logs data set requires a mapping to label the latitude/longitude pairs in the logs as geographic locations by applying the `geo_point` type to those fields. Use the following commands to establish `geo_point` mapping for the logs: @@ -170,7 +170,7 @@ curl -XPUT http://localhost:9200/logstash-2015.05.20 -d ' } '; -At this point we're ready to use the Elasticsearch {ref}/docs-bulk.html[`bulk`] API to load the data sets with the +At this point we're ready to use the Elasticsearch {ref}/docs-bulk.html[`bulk`] API to load the data sets with the following commands: [source,shell] @@ -179,7 +179,7 @@ curl -XPOST 'localhost:9200/_bulk?pretty' --data-binary @logs.jsonl These commands may take some time to execute, depending on the computing resources available. -To load the Accounts data set, click the *Management* image:images/SettingsButton.jpg[gear icon] tab, the +To load the Accounts data set, click the *Management* image:images/SettingsButton.jpg[gear icon] tab, the select *Upload CSV*. image::images/management-panel.png[kibana management panel] @@ -188,7 +188,7 @@ Click *Select File*, then navigate to the `accounts.csv` file. Review the sample image::images/csv-sample.png[sample csv import] -Review the index pattern built by the CSV import function. You can change any field types from the drop-downs, but for +Review the index pattern built by the CSV import function. You can change any field types from the drop-downs, but for this tutorial, accept the defaults. Enter `bank` as the name for the index pattern, then click *Save*. image::images/sample-index.png[sample index pattern] @@ -211,24 +211,24 @@ yellow open logstash-2015.05.20 5 1 4750 0 16.4mb [[tutorial-define-index]] === Defining Your Index Patterns -Each set of data loaded to Elasticsearch has an <>. In the previous section, the -Shakespeare data set has an index named `shakespeare`, and the accounts data set has an index named `bank`. An _index -pattern_ is a string with optional wildcards that can match multiple indices. For example, in the common logging use -case, a typical index name contains the date in MM-DD-YYYY format, and an index pattern for May would look something +Each set of data loaded to Elasticsearch has an <>. In the previous section, the +Shakespeare data set has an index named `shakespeare`, and the accounts data set has an index named `bank`. An _index +pattern_ is a string with optional wildcards that can match multiple indices. For example, in the common logging use +case, a typical index name contains the date in MM-DD-YYYY format, and an index pattern for May would look something like `logstash-2015.05*`. -For this tutorial, any pattern that matches the name of an index we've loaded will work. Open a browser and -navigate to `localhost:5601`. Click the *Settings* tab, then the *Indices* tab. Click *Add New* to define a new index +For this tutorial, any pattern that matches the name of an index we've loaded will work. Open a browser and +navigate to `localhost:5601`. Click the *Settings* tab, then the *Indices* tab. Click *Add New* to define a new index pattern. Two of the sample data sets, the Shakespeare plays and the financial accounts, don't contain time-series data. Make sure the *Index contains time-based events* box is unchecked when you create index patterns for these data sets. -Specify `shakes*` as the index pattern for the Shakespeare data set and click *Create* to define the index pattern, then +Specify `shakes*` as the index pattern for the Shakespeare data set and click *Create* to define the index pattern, then define a second index pattern named `ba*`. The Logstash data set does contain time-series data, so after clicking *Add New* to define the index for this data -set, make sure the *Index contains time-based events* box is checked and select the `@timestamp` field from the +set, make sure the *Index contains time-based events* box is checked and select the `@timestamp` field from the *Time-field name* drop-down. -NOTE: When you define an index pattern, indices that match that pattern must exist in Elasticsearch. Those indices must +NOTE: When you define an index pattern, indices that match that pattern must exist in Elasticsearch. Those indices must contain data. [float] @@ -239,14 +239,14 @@ Click the *Discover* image:images/discover-compass.png[Compass icon] tab to disp image::images/tutorial-discover.png[] -Right under the tab itself, there is a search box where you can search your data. Searches take a specific -{ref}/query-dsl-query-string-query.html#query-string-syntax[query syntax] that enable you to create custom searches, +Right under the tab itself, there is a search box where you can search your data. Searches take a specific +{ref}/query-dsl-query-string-query.html#query-string-syntax[query syntax] that enable you to create custom searches, which you can save and load by clicking the buttons to the right of the search box. -Beneath the search box, the current index pattern is displayed in a drop-down. You can change the index pattern by +Beneath the search box, the current index pattern is displayed in a drop-down. You can change the index pattern by selecting a different pattern from the drop-down selector. -You can construct searches by using the field names and the values you're interested in. With numeric fields you can +You can construct searches by using the field names and the values you're interested in. With numeric fields you can use comparison operators such as greater than (>), less than (<), or equals (=). You can link elements with the logical operators AND, OR, and NOT, all in uppercase. @@ -261,8 +261,8 @@ If you're using the linked sample data set, this search returns 5 results: Accou image::images/tutorial-discover-2.png[] -To narrow the display to only the specific fields of interest, highlight each field in the list that displays under the -index pattern and click the *Add* button. Note how, in this example, adding the `account_number` field changes the +To narrow the display to only the specific fields of interest, highlight each field in the list that displays under the +index pattern and click the *Add* button. Note how, in this example, adding the `account_number` field changes the display from the full text of five records to a simple list of five account numbers: image::images/tutorial-discover-3.png[] @@ -270,26 +270,26 @@ image::images/tutorial-discover-3.png[] [[tutorial-visualizing]] === Data Visualization: Beyond Discovery -The visualization tools available on the *Visualize* tab enable you to display aspects of your data sets in several -different ways. +The visualization tools available on the *Visualize* tab enable you to display aspects of your data sets in several +different ways. Click on the *Visualize* image:images/visualize-icon.png[Bar chart icon] tab to start: image::images/tutorial-visualize.png[] -Click on *Pie chart*, then *From a new search*. Select the `ba*` index pattern. +Click on *Pie chart*, then *From a new search*. Select the `ba*` index pattern. -Visualizations depend on Elasticsearch {ref}/search-aggregations.html[aggregations] in two different types: _bucket_ -aggregations and _metric_ aggregations. A bucket aggregation sorts your data according to criteria you specify. For -example, in our accounts data set, we can establish a range of account balances, then display what proportions of the +Visualizations depend on Elasticsearch {ref}/search-aggregations.html[aggregations] in two different types: _bucket_ +aggregations and _metric_ aggregations. A bucket aggregation sorts your data according to criteria you specify. For +example, in our accounts data set, we can establish a range of account balances, then display what proportions of the total fall into which range of balances. The whole pie displays, since we haven't specified any buckets yet. image::images/tutorial-visualize-pie-1.png[] -Select *Split Slices* from the *Select buckets type* list, then select *Range* from the *Aggregation* drop-down -selector. Select the *balance* field from the *Field* drop-down, then click on *Add Range* four times to bring the +Select *Split Slices* from the *Select buckets type* list, then select *Range* from the *Aggregation* drop-down +selector. Select the *balance* field from the *Field* drop-down, then click on *Add Range* four times to bring the total number of ranges to six. Enter the following ranges: [source,text] @@ -304,13 +304,13 @@ Click the *Apply changes* button image:images/apply-changes-button.png[] to disp image::images/tutorial-visualize-pie-2.png[] -This shows you what proportion of the 1000 accounts fall in these balance ranges. To see another dimension of the data, -we're going to add another bucket aggregation. We can break down each of the balance ranges further by the account +This shows you what proportion of the 1000 accounts fall in these balance ranges. To see another dimension of the data, +we're going to add another bucket aggregation. We can break down each of the balance ranges further by the account holder's age. -Click *Add sub-buckets* at the bottom, then select *Split Slices*. Choose the *Terms* aggregation and the *age* field from -the drop-downs. -Click the *Apply changes* button image:images/apply-changes-button.png[] to add an external ring with the new +Click *Add sub-buckets* at the bottom, then select *Split Slices*. Choose the *Terms* aggregation and the *age* field from +the drop-downs. +Click the *Apply changes* button image:images/apply-changes-button.png[] to add an external ring with the new results. image::images/tutorial-visualize-pie-3.png[] @@ -318,74 +318,74 @@ image::images/tutorial-visualize-pie-3.png[] Save this chart by clicking the *Save Visualization* button to the right of the search field. Name the visualization _Pie Example_. -Next, we're going to make a bar chart. Click on *New Visualization*, then *Vertical bar chart*. Select *From a new +Next, we're going to make a bar chart. Click on *New Visualization*, then *Vertical bar chart*. Select *From a new search* and the `shakes*` index pattern. You'll see a single big bar, since we haven't defined any buckets yet: image::images/tutorial-visualize-bar-1.png[] -For the Y-axis metrics aggregation, select *Unique Count*, with *speaker* as the field. For Shakespeare plays, it might -be useful to know which plays have the lowest number of distinct speaking parts, if your theater company is short on +For the Y-axis metrics aggregation, select *Unique Count*, with *speaker* as the field. For Shakespeare plays, it might +be useful to know which plays have the lowest number of distinct speaking parts, if your theater company is short on actors. For the X-Axis buckets, select the *Terms* aggregation with the *play_name* field. For the *Order*, select *Ascending*, leaving the *Size* at 5. Write a description for the axes in the *Custom Label* fields. -Leave the other elements at their default values and click the *Apply changes* button +Leave the other elements at their default values and click the *Apply changes* button image:images/apply-changes-button.png[]. Your chart should now look like this: image::images/tutorial-visualize-bar-2.png[] -Notice how the individual play names show up as whole phrases, instead of being broken down into individual words. This -is the result of the mapping we did at the beginning of the tutorial, when we marked the *play_name* field as 'not +Notice how the individual play names show up as whole phrases, instead of being broken down into individual words. This +is the result of the mapping we did at the beginning of the tutorial, when we marked the *play_name* field as 'not analyzed'. -Hovering on each bar shows you the number of speaking parts for each play as a tooltip. You can turn this behavior off, +Hovering on each bar shows you the number of speaking parts for each play as a tooltip. You can turn this behavior off, as well as change many other options for your visualizations, by clicking the *Options* tab in the top left. -Now that you have a list of the smallest casts for Shakespeare plays, you might also be curious to see which of these -plays makes the greatest demands on an individual actor by showing the maximum number of speeches for a given part. Add -a Y-axis aggregation with the *Add metrics* button, then choose the *Max* aggregation for the *speech_number* field. In -the *Options* tab, change the *Bar Mode* drop-down to *grouped*, then click the *Apply changes* button +Now that you have a list of the smallest casts for Shakespeare plays, you might also be curious to see which of these +plays makes the greatest demands on an individual actor by showing the maximum number of speeches for a given part. Add +a Y-axis aggregation with the *Add metrics* button, then choose the *Max* aggregation for the *speech_number* field. In +the *Options* tab, change the *Bar Mode* drop-down to *grouped*, then click the *Apply changes* button image:images/apply-changes-button.png[]. Your chart should now look like this: image::images/tutorial-visualize-bar-3.png[] -As you can see, _Love's Labours Lost_ has an unusually high maximum speech number, compared to the other plays, and +As you can see, _Love's Labours Lost_ has an unusually high maximum speech number, compared to the other plays, and might therefore make more demands on an actor's memory. -Note how the *Number of speaking parts* Y-axis starts at zero, but the bars don't begin to differentiate until 18. To -make the differences stand out, starting the Y-axis at a value closer to the minimum, check the +Note how the *Number of speaking parts* Y-axis starts at zero, but the bars don't begin to differentiate until 18. To +make the differences stand out, starting the Y-axis at a value closer to the minimum, check the *Scale Y-Axis to data bounds* box in the *Options* tab. Save this chart with the name _Bar Example_. -Next, we're going to make a tile map chart to visualize some geographic data. Click on *New Visualization*, then -*Tile map*. Select *From a new search* and the `logstash-*` index pattern. Define the time window for the events -we're exploring by clicking the time selector at the top right of the Kibana interface. Click on *Absolute*, then set +Next, we're going to make a tile map chart to visualize some geographic data. Click on *New Visualization*, then +*Tile map*. Select *From a new search* and the `logstash-*` index pattern. Define the time window for the events +we're exploring by clicking the time selector at the top right of the Kibana interface. Click on *Absolute*, then set the start time to May 18, 2015 and the end time for the range to May 20, 2015: image::images/tutorial-timepicker.png[] -Once you've got the time range set up, click the *Go* button, then close the time picker by clicking the small up arrow +Once you've got the time range set up, click the *Go* button, then close the time picker by clicking the small up arrow at the bottom. You'll see a map of the world, since we haven't defined any buckets yet: image::images/tutorial-visualize-map-1.png[] -Select *Geo Coordinates* as the bucket, then click the *Apply changes* button image:images/apply-changes-button.png[]. +Select *Geo Coordinates* as the bucket, then click the *Apply changes* button image:images/apply-changes-button.png[]. Your chart should now look like this: image::images/tutorial-visualize-map-2.png[] -You can navigate the map by clicking and dragging, zoom with the image:images/viz-zoom.png[] buttons, or hit the *Fit -Data Bounds* image:images/viz-fit-bounds.png[] button to zoom to the lowest level that includes all the points. You can -also create a filter to define a rectangle on the map, either to include or exclude, by clicking the -*Latitude/Longitude Filter* image:images/viz-lat-long-filter.png[] button and drawing a bounding box on the map. +You can navigate the map by clicking and dragging, zoom with the image:images/viz-zoom.png[] buttons, or hit the *Fit +Data Bounds* image:images/viz-fit-bounds.png[] button to zoom to the lowest level that includes all the points. You can +also create a filter to define a rectangle on the map, either to include or exclude, by clicking the +*Latitude/Longitude Filter* image:images/viz-lat-long-filter.png[] button and drawing a bounding box on the map. A green oval with the filter definition displays right under the query box: image::images/tutorial-visualize-map-3.png[] -Hover on the filter to display the controls to toggle, pin, invert, or delete the filter. Save this chart with the name +Hover on the filter to display the controls to toggle, pin, invert, or delete the filter. Save this chart with the name _Map Example_. -Finally, we're going to define a sample Markdown widget to display on our dashboard. Click on *New Visualization*, then +Finally, we're going to define a sample Markdown widget to display on our dashboard. Click on *New Visualization*, then *Markdown widget*, to display a very simple Markdown entry field: image::images/tutorial-visualize-md-1.png[] @@ -393,11 +393,11 @@ image::images/tutorial-visualize-md-1.png[] Write the following text in the field: [source,markdown] -# This is a tutorial dashboard! +# This is a tutorial dashboard! The Markdown widget uses **markdown** syntax. > Blockquotes in Markdown use the > character. -Click the *Apply changes* button image:images/apply-changes-button.png[] to display the rendered Markdown in the +Click the *Apply changes* button image:images/apply-changes-button.png[] to display the rendered Markdown in the preview pane: image::images/tutorial-visualize-md-2.png[] @@ -407,21 +407,21 @@ Save this visualization with the name _Markdown Example_. [[tutorial-dashboard]] === Putting it all Together with Dashboards -A Kibana dashboard is a collection of visualizations that you can arrange and share. To get started, click the -*Dashboard* tab, then the *Add Visualization* button at the far right of the search box to display the list of saved -visualizations. Select _Markdown Example_, _Pie Example_, _Bar Example_, and _Map Example_, then close the list of -visualizations by clicking the small up-arrow at the bottom of the list. You can move the containers for each -visualization by clicking and dragging the title bar. Resize the containers by dragging the lower right corner of a +A Kibana dashboard is a collection of visualizations that you can arrange and share. To get started, click the +*Dashboard* tab, then the *Add Visualization* button at the far right of the search box to display the list of saved +visualizations. Select _Markdown Example_, _Pie Example_, _Bar Example_, and _Map Example_, then close the list of +visualizations by clicking the small up-arrow at the bottom of the list. You can move the containers for each +visualization by clicking and dragging the title bar. Resize the containers by dragging the lower right corner of a visualization's container. Your sample dashboard should end up looking roughly like this: image::images/tutorial-dashboard.png[] -Click the *Save Dashboard* button, then name the dashboard _Tutorial Dashboard_. You can share a saved dashboard by +Click the *Save Dashboard* button, then name the dashboard _Tutorial Dashboard_. You can share a saved dashboard by clicking the *Share* button to display HTML embedding code as well as a direct link. [float] [[wrapping-up]] === Wrapping Up -Now that you've handled the basic aspects of Kibana's functionality, you're ready to explore Kibana in further detail. +Now that you've handled the basic aspects of Kibana's functionality, you're ready to explore Kibana in further detail. Take a look at the rest of the documentation for more details! diff --git a/docs/introduction.asciidoc b/docs/introduction.asciidoc index 664e73e89d7f2a..d5f2da894e4583 100644 --- a/docs/introduction.asciidoc +++ b/docs/introduction.asciidoc @@ -1,21 +1,21 @@ [[introduction]] == Introduction -Kibana is an open source analytics and visualization platform designed to work with Elasticsearch. You use Kibana to -search, view, and interact with data stored in Elasticsearch indices. You can easily perform advanced data analysis +Kibana is an open source analytics and visualization platform designed to work with Elasticsearch. You use Kibana to +search, view, and interact with data stored in Elasticsearch indices. You can easily perform advanced data analysis and visualize your data in a variety of charts, tables, and maps. -Kibana makes it easy to understand large volumes of data. Its simple, browser-based interface enables you to quickly +Kibana makes it easy to understand large volumes of data. Its simple, browser-based interface enables you to quickly create and share dynamic dashboards that display changes to Elasticsearch queries in real time. -Setting up Kibana is a snap. You can install Kibana and start exploring your Elasticsearch indices in minutes -- no -code, no additional infrastructure required. +Setting up Kibana is a snap. You can install Kibana and start exploring your Elasticsearch indices in minutes -- no +code, no additional infrastructure required. -For more information about creating and sharing visualizations and dashboards, see the <> -and <> topics. A complete <> covering several aspects of Kibana's +For more information about creating and sharing visualizations and dashboards, see the <> +and <> topics. A complete <> covering several aspects of Kibana's functionality is also available. -NOTE: This guide describes how to use Kibana {version}. For information about what's new in Kibana {version}, see +NOTE: This guide describes how to use Kibana {version}. For information about what's new in Kibana {version}, see the <>. //// @@ -23,25 +23,25 @@ the <>. [[data-discovery]] === Data Discovery and Visualization -Let's take a look at how you might use Kibana to explore and visualize data. -We've indexed some data from Transport for London (TFL) that shows one week +Let's take a look at how you might use Kibana to explore and visualize data. +We've indexed some data from Transport for London (TFL) that shows one week of transit (Oyster) card usage. -From Kibana's Discover page, we can submit search queries, filter the results, and -examine the data in the returned documents. For example, we can get all trips +From Kibana's Discover page, we can submit search queries, filter the results, and +examine the data in the returned documents. For example, we can get all trips completed by the Tube during the week by excluding incomplete trips and trips by bus: image:images/TFL-CompletedTrips.jpg[Discover] -Right away, we can see the peaks for the morning and afternoon commute hours in the -histogram. By default, the Discover page also shows the first 500 entries that match the -search criteria. You can change the time filter, interact with the histogram to drill -down into the data, and view the details of particular documents. For more +Right away, we can see the peaks for the morning and afternoon commute hours in the +histogram. By default, the Discover page also shows the first 500 entries that match the +search criteria. You can change the time filter, interact with the histogram to drill +down into the data, and view the details of particular documents. For more information about exploring your data from the Discover page, see <>. You can construct visualizations of your search results from the Visualization page. Each visualization is associated with a search. For example, we can create a histogram -that shows the weekly London commute traffic via the Tube using our previous search. +that shows the weekly London commute traffic via the Tube using our previous search. The Y-axis shows the number of trips. The X-axis shows the day and time. By adding a sub-aggregation, we can see the top 3 end stations during each hour: diff --git a/docs/kibana-yml.asciidoc b/docs/kibana-yml.asciidoc index 43272c0d73291a..5be95c03e65918 100644 --- a/docs/kibana-yml.asciidoc +++ b/docs/kibana-yml.asciidoc @@ -2,45 +2,45 @@ [horizontal] `server.port:`:: *Default: 5601* Kibana is served by a back end server. This setting specifies the port to use. `server.host:`:: *Default: "0.0.0.0"* This setting specifies the IP address of the back end server. -`server.basePath:`:: Enables you to specify a path to mount Kibana at if you are running behind a proxy. This setting +`server.basePath:`:: Enables you to specify a path to mount Kibana at if you are running behind a proxy. This setting cannot end in a slash (`/`). `server.maxPayloadBytes:`:: *Default: 1048576* The maximum payload size in bytes for incoming server requests. -`server.name:`:: *Default: "your-hostname"* A human-readable display name that identifies this Kibana instance. -`elasticsearch.url:`:: *Default: "http://localhost:9200"* The URL of the Elasticsearch instance to use for all your +`server.name:`:: *Default: "your-hostname"* A human-readable display name that identifies this Kibana instance. +`elasticsearch.url:`:: *Default: "http://localhost:9200"* The URL of the Elasticsearch instance to use for all your queries. -`elasticsearch.preserveHost:`:: *Default: true* When this setting’s value is true Kibana uses the hostname specified in -the `server.host` setting. When the value of this setting is `false`, Kibana uses the hostname of the host that connects +`elasticsearch.preserveHost:`:: *Default: true* When this setting’s value is true Kibana uses the hostname specified in +the `server.host` setting. When the value of this setting is `false`, Kibana uses the hostname of the host that connects to this Kibana instance. -`kibana.index:`:: *Default: ".kibana"* Kibana uses an index in Elasticsearch to store saved searches, visualizations and +`kibana.index:`:: *Default: ".kibana"* Kibana uses an index in Elasticsearch to store saved searches, visualizations and dashboards. Kibana creates a new index if the index doesn’t already exist. `kibana.defaultAppId:`:: *Default: "discover"* The default application to load. -`elasticsearch.username:` and `elasticsearch.password:`:: If your Elasticsearch is protected with basic authentication, -these settings provide the username and password that the Kibana server uses to perform maintenance on the Kibana index at +`elasticsearch.username:` and `elasticsearch.password:`:: If your Elasticsearch is protected with basic authentication, +these settings provide the username and password that the Kibana server uses to perform maintenance on the Kibana index at startup. Your Kibana users still need to authenticate with Elasticsearch, which is proxied through the Kibana server. -`server.ssl.cert:` and `server.ssl.key:`:: Paths to the PEM-format SSL certificate and SSL key files, respectively. These +`server.ssl.cert:` and `server.ssl.key:`:: Paths to the PEM-format SSL certificate and SSL key files, respectively. These files enable SSL for outgoing requests from the Kibana server to the browser. -`elasticsearch.ssl.cert:` and `elasticsearch.ssl.key:`:: Optional settings that provide the paths to the PEM-format SSL +`elasticsearch.ssl.cert:` and `elasticsearch.ssl.key:`:: Optional settings that provide the paths to the PEM-format SSL certificate and key files. These files validate that your Elasticsearch backend uses the same key files. -`elasticsearch.ssl.ca:`:: Optional setting that enables you to specify a path to the PEM file for the certificate +`elasticsearch.ssl.ca:`:: Optional setting that enables you to specify a path to the PEM file for the certificate authority for your Elasticsearch instance. -`elasticsearch.ssl.verify:`:: *Default: true* To disregard the validity of SSL certificates, change this setting’s value +`elasticsearch.ssl.verify:`:: *Default: true* To disregard the validity of SSL certificates, change this setting’s value to `false`. -`elasticsearch.pingTimeout:`:: *Default: the value of the `elasticsearch.requestTimeout` setting* Time in milliseconds to +`elasticsearch.pingTimeout:`:: *Default: the value of the `elasticsearch.requestTimeout` setting* Time in milliseconds to wait for Elasticsearch to respond to pings. -`elasticsearch.requestTimeout:`:: *Default: 30000* Time in milliseconds to wait for responses from the back end or +`elasticsearch.requestTimeout:`:: *Default: 30000* Time in milliseconds to wait for responses from the back end or Elasticsearch. This value must be a positive integer. `elasticsearch.requestHeadersWhitelist:`:: *Default: `[ 'authorization' ]`* List of Kibana client-side headers to send to Elasticsearch. To send *no* client-side headers, set this value to [] (an empty list). `elasticsearch.shardTimeout:`:: *Default: 0* Time in milliseconds for Elasticsearch to wait for responses from shards. Set to 0 to disable. -`elasticsearch.startupTimeout:`:: *Default: 5000* Time in milliseconds to wait for Elasticsearch at Kibana startup before +`elasticsearch.startupTimeout:`:: *Default: 5000* Time in milliseconds to wait for Elasticsearch at Kibana startup before retrying. `pid.file:`:: Specifies the path where Kibana creates the process ID file. `logging.dest:`:: *Default: `stdout`* Enables you specify a file where Kibana stores log output. `logging.silent:`:: *Default: false* Set the value of this setting to `true` to suppress all logging output. -`logging.quiet:`:: *Default: false* Set the value of this setting to `true` to suppress all logging output other than +`logging.quiet:`:: *Default: false* Set the value of this setting to `true` to suppress all logging output other than error messages. -`logging.verbose`:: *Default: false* Set the value of this setting to `true` to log all events, including system usage +`logging.verbose`:: *Default: false* Set the value of this setting to `true` to log all events, including system usage information and all requests. `ops.interval`:: *Default: 5000* Set the interval in milliseconds to sample system and process performance metrics. The minimum value is 100. diff --git a/docs/line.asciidoc b/docs/line.asciidoc index 42b9e7f850570b..b83431093c94f8 100644 --- a/docs/line.asciidoc +++ b/docs/line.asciidoc @@ -6,7 +6,7 @@ This chart's Y axis is the _metrics_ axis. The following aggregations are availa include::y-axis-aggs.asciidoc[] Before you choose a buckets aggregation, specify if you are splitting slices within a single chart or splitting into -multiple charts. A multiple chart split must run before any other aggregations. When you split a chart, you can change +multiple charts. A multiple chart split must run before any other aggregations. When you split a chart, you can change if the splits are displayed in a row or a column by clicking the *Rows | Columns* selector. include::x-axis-aggs.asciidoc[] @@ -19,37 +19,37 @@ You can click the *Advanced* link to display more customization options for your *Exclude Pattern Flags*:: A standard set of Java flags for the exclusion pattern. *Include Pattern*:: Specify a pattern in this field to include in the results. *Include Pattern Flags*:: A standard set of Java flags for the inclusion pattern. -*JSON Input*:: A text field where you can add specific JSON-formatted properties to merge with the aggregation +*JSON Input*:: A text field where you can add specific JSON-formatted properties to merge with the aggregation definition, as in the following example: [source,shell] { "script" : "doc['grade'].value * 1.2" } -NOTE: In Elasticsearch releases 1.4.3 and later, this functionality requires you to enable +NOTE: In Elasticsearch releases 1.4.3 and later, this functionality requires you to enable {ref}/modules-scripting.html[dynamic Groovy scripting]. The availability of these options varies depending on the aggregation you choose. Select the *Options* tab to change the following aspects of the chart: -*Y-Axis Scale*:: You can select *linear*, *log*, or *square root* scales for the chart's Y axis. You can use a log -scale to display data that varies exponentially, such as a compounding interest chart, or a square root scale to -regularize the display of data sets with variabilities that are themselves highly variable. This kind of data, where -the variability is itself variable over the domain being examined, is known as _heteroscedastic_ data. For example, if +*Y-Axis Scale*:: You can select *linear*, *log*, or *square root* scales for the chart's Y axis. You can use a log +scale to display data that varies exponentially, such as a compounding interest chart, or a square root scale to +regularize the display of data sets with variabilities that are themselves highly variable. This kind of data, where +the variability is itself variable over the domain being examined, is known as _heteroscedastic_ data. For example, if a data set of height versus weight has a relatively narrow range of variability at the short end of height, but a wider -range at the taller end, the data set is heteroscedastic. -*Smooth Lines*:: Check this box to curve the line from point to point. Bear in mind that smoothed lines necessarily +range at the taller end, the data set is heteroscedastic. +*Smooth Lines*:: Check this box to curve the line from point to point. Bear in mind that smoothed lines necessarily affect the representation of your data and create a potential for ambiguity. *Show Connecting Lines*:: Check this box to draw lines between the points on the chart. *Show Circles*:: Check this box to draw each data point on the chart as a small circle. *Current time marker*:: For charts of time-series data, check this box to draw a red line on the current time. -*Set Y-Axis Extents*:: Check this box and enter values in the *y-max* and *y-min* fields to set the Y axis to specific -values. +*Set Y-Axis Extents*:: Check this box and enter values in the *y-max* and *y-min* fields to set the Y axis to specific +values. *Show Tooltip*:: Check this box to enable the display of tooltips. -*Scale Y-Axis to Data Bounds*:: The default Y-axis bounds are zero and the maximum value returned in the data. Check +*Scale Y-Axis to Data Bounds*:: The default Y-axis bounds are zero and the maximum value returned in the data. Check this box to change both upper and lower bounds to match the values returned in the data. -After changing options, click the *Apply changes* button to update your visualization, or the grey *Discard +After changing options, click the *Apply changes* button to update your visualization, or the grey *Discard changes* button to keep your visualization in its current state. [float] diff --git a/docs/markdown.asciidoc b/docs/markdown.asciidoc index e107f3a06fbab1..8073390f77557d 100644 --- a/docs/markdown.asciidoc +++ b/docs/markdown.asciidoc @@ -1,7 +1,7 @@ [[markdown-widget]] === Markdown Widget -The Markdown widget is a text entry field that accepts GitHub-flavored Markdown text. Kibana renders the text you enter -in this field and displays the results on the dashboard. You can click the *Help* link to go to the +The Markdown widget is a text entry field that accepts GitHub-flavored Markdown text. Kibana renders the text you enter +in this field and displays the results on the dashboard. You can click the *Help* link to go to the https://help.github.com/articles/github-flavored-markdown/[help page] for GitHub flavored Markdown. Click *Apply* to display the rendered text in the Preview pane or *Discard* to revert to a previous version. diff --git a/docs/metric.asciidoc b/docs/metric.asciidoc index e4ce743a8210b5..d66144d5b6c567 100644 --- a/docs/metric.asciidoc +++ b/docs/metric.asciidoc @@ -7,13 +7,13 @@ include::y-axis-aggs.asciidoc[] You can click the *Advanced* link to display more customization options: -*JSON Input*:: A text field where you can add specific JSON-formatted properties to merge with the aggregation +*JSON Input*:: A text field where you can add specific JSON-formatted properties to merge with the aggregation definition, as in the following example: [source,shell] { "script" : "doc['grade'].value * 1.2" } -NOTE: In Elasticsearch releases 1.4.3 and later, this functionality requires you to enable +NOTE: In Elasticsearch releases 1.4.3 and later, this functionality requires you to enable {ref}/modules-scripting.html[dynamic Groovy scripting]. The availability of these options varies depending on the aggregation you choose. diff --git a/docs/pie.asciidoc b/docs/pie.asciidoc index 1c9f2049d79b3a..f3acb2c5c322be 100644 --- a/docs/pie.asciidoc +++ b/docs/pie.asciidoc @@ -1,14 +1,14 @@ [[pie-chart]] === Pie Charts -The slice size of a pie chart is determined by the _metrics_ aggregation. The following aggregations are available for +The slice size of a pie chart is determined by the _metrics_ aggregation. The following aggregations are available for this axis: -*Count*:: The {ref}search-aggregations-metrics-valuecount-aggregation.html[_count_] aggregation returns a raw count of +*Count*:: The {ref}search-aggregations-metrics-valuecount-aggregation.html[_count_] aggregation returns a raw count of the elements in the selected index pattern. -*Sum*:: The {ref}search-aggregations-metrics-sum-aggregation.html[_sum_] aggregation returns the total sum of a numeric +*Sum*:: The {ref}search-aggregations-metrics-sum-aggregation.html[_sum_] aggregation returns the total sum of a numeric field. Select a field from the drop-down. -*Unique Count*:: The {ref}search-aggregations-metrics-cardinality-aggregation.html[_cardinality_] aggregation returns +*Unique Count*:: The {ref}search-aggregations-metrics-cardinality-aggregation.html[_cardinality_] aggregation returns the number of unique values in a field. Select a field from the drop-down. Enter a string in the *Custom Label* field to change the display label. @@ -16,45 +16,45 @@ Enter a string in the *Custom Label* field to change the display label. The _buckets_ aggregations determine what information is being retrieved from your data set. Before you choose a buckets aggregation, specify if you are splitting slices within a single chart or splitting into -multiple charts. A multiple chart split must run before any other aggregations. When you split a chart, you can change +multiple charts. A multiple chart split must run before any other aggregations. When you split a chart, you can change if the splits are displayed in a row or a column by clicking the *Rows | Columns* selector. You can specify any of the following bucket aggregations for your pie chart: -*Date Histogram*:: A {ref}search-aggregations-bucket-datehistogram-aggregation.html[_date histogram_] is built from a -numeric field and organized by date. You can specify a time frame for the intervals in seconds, minutes, hours, days, -weeks, months, or years. You can also specify a custom interval frame by selecting *Custom* as the interval and -specifying a number and a time unit in the text field. Custom interval time units are *s* for seconds, *m* for minutes, -*h* for hours, *d* for days, *w* for weeks, and *y* for years. Different units support different levels of precision, +*Date Histogram*:: A {ref}search-aggregations-bucket-datehistogram-aggregation.html[_date histogram_] is built from a +numeric field and organized by date. You can specify a time frame for the intervals in seconds, minutes, hours, days, +weeks, months, or years. You can also specify a custom interval frame by selecting *Custom* as the interval and +specifying a number and a time unit in the text field. Custom interval time units are *s* for seconds, *m* for minutes, +*h* for hours, *d* for days, *w* for weeks, and *y* for years. Different units support different levels of precision, down to one second. -*Histogram*:: A standard {ref}search-aggregations-bucket-histogram-aggregation.html[_histogram_] is built from a -numeric field. Specify an integer interval for this field. Select the *Show empty buckets* checkbox to include empty +*Histogram*:: A standard {ref}search-aggregations-bucket-histogram-aggregation.html[_histogram_] is built from a +numeric field. Specify an integer interval for this field. Select the *Show empty buckets* checkbox to include empty intervals in the histogram. -*Range*:: With a {ref}search-aggregations-bucket-range-aggregation.html[_range_] aggregation, you can specify ranges -of values for a numeric field. Click *Add Range* to add a set of range endpoints. Click the red *(x)* symbol to remove +*Range*:: With a {ref}search-aggregations-bucket-range-aggregation.html[_range_] aggregation, you can specify ranges +of values for a numeric field. Click *Add Range* to add a set of range endpoints. Click the red *(x)* symbol to remove a range. -*Date Range*:: A {ref}search-aggregations-bucket-daterange-aggregation.html[_date range_] aggregation reports values -that are within a range of dates that you specify. You can specify the ranges for the dates using -{ref}common-options.html#date-math[_date math_] expressions. Click *Add Range* to add a set of range endpoints. +*Date Range*:: A {ref}search-aggregations-bucket-daterange-aggregation.html[_date range_] aggregation reports values +that are within a range of dates that you specify. You can specify the ranges for the dates using +{ref}common-options.html#date-math[_date math_] expressions. Click *Add Range* to add a set of range endpoints. Click the red *(/)* symbol to remove a range. *IPv4 Range*:: The {ref}search-aggregations-bucket-iprange-aggregation.html[_IPv4 range_] aggregation enables you to -specify ranges of IPv4 addresses. Click *Add Range* to add a set of range endpoints. Click the red *(/)* symbol to +specify ranges of IPv4 addresses. Click *Add Range* to add a set of range endpoints. Click the red *(/)* symbol to remove a range. -*Terms*:: A {ref}search-aggregations-bucket-terms-aggregation.html[_terms_] aggregation enables you to specify the top +*Terms*:: A {ref}search-aggregations-bucket-terms-aggregation.html[_terms_] aggregation enables you to specify the top or bottom _n_ elements of a given field to display, ordered by count or a custom metric. -*Filters*:: You can specify a set of {ref}search-aggregations-bucket-filters-aggregation.html[_filters_] for the data. -You can specify a filter as a query string or in JSON format, just as in the Discover search bar. Click *Add Filter* to -add another filter. Click the image:images/labelbutton.png[] *label* button to open the label field, where you can type +*Filters*:: You can specify a set of {ref}search-aggregations-bucket-filters-aggregation.html[_filters_] for the data. +You can specify a filter as a query string or in JSON format, just as in the Discover search bar. Click *Add Filter* to +add another filter. Click the image:images/labelbutton.png[] *label* button to open the label field, where you can type in a name to display on the visualization. -*Significant Terms*:: Displays the results of the experimental -{ref}search-aggregations-bucket-significantterms-aggregation.html[_significant terms_] aggregation. The value of the +*Significant Terms*:: Displays the results of the experimental +{ref}search-aggregations-bucket-significantterms-aggregation.html[_significant terms_] aggregation. The value of the *Size* parameter defines the number of entries this aggregation returns. -After defining an initial bucket aggregation, you can define sub-buckets to refine the visualization. Click *+ Add -sub-buckets* to define a sub-aggregation, then choose *Split Slices* to select a sub-bucket from the list of +After defining an initial bucket aggregation, you can define sub-buckets to refine the visualization. Click *+ Add +sub-buckets* to define a sub-aggregation, then choose *Split Slices* to select a sub-bucket from the list of types. -When multiple aggregations are defined on a chart's axis, you can use the up or down arrows to the right of the +When multiple aggregations are defined on a chart's axis, you can use the up or down arrows to the right of the aggregation's type to change the aggregation's priority. include::color-picker.asciidoc[] @@ -65,13 +65,13 @@ You can click the *Advanced* link to display more customization options for your *Exclude Pattern*:: Specify a pattern in this field to exclude from the results. *Include Pattern*:: Specify a pattern in this field to include in the results. -*JSON Input*:: A text field where you can add specific JSON-formatted properties to merge with the aggregation +*JSON Input*:: A text field where you can add specific JSON-formatted properties to merge with the aggregation definition, as in the following example: [source,shell] { "script" : "doc['grade'].value * 1.2" } -NOTE: In Elasticsearch releases 1.4.3 and later, this functionality requires you to enable +NOTE: In Elasticsearch releases 1.4.3 and later, this functionality requires you to enable {ref}modules-scripting.html[dynamic Groovy scripting]. The availability of these options varies depending on the aggregation you choose. @@ -81,7 +81,7 @@ Select the *Options* tab to change the following aspects of the table: *Donut*:: Display the chart as a sliced ring instead of a sliced pie. *Show Tooltip*:: Check this box to enable the display of tooltips. -After changing options, click the *Apply changes* button to update your visualization, or the grey *Discard +After changing options, click the *Apply changes* button to update your visualization, or the grey *Discard changes* button to keep your visualization in its current state. [float] diff --git a/docs/production.asciidoc b/docs/production.asciidoc index 90c46ea55aed39..7f98f1584c285b 100644 --- a/docs/production.asciidoc +++ b/docs/production.asciidoc @@ -25,11 +25,11 @@ Kibana users have to authenticate when your cluster has {scyld} enabled. You configure {scyld} roles for your Kibana users to control what data those users can access. Kibana runs a webserver that makes requests to Elasticsearch on the client's behalf, so you also need to configure credentials for the Kibana server -so those requests can be authenticated. +so those requests can be authenticated. -You must configure Kibana to encrypt communications between the browser and the -Kibana server to prevent user passwords from being sent in the clear. If are -using SSL/TLS to encrypt traffic to and from the nodes in your Elasticsearch +You must configure Kibana to encrypt communications between the browser and the +Kibana server to prevent user passwords from being sent in the clear. If are +using SSL/TLS to encrypt traffic to and from the nodes in your Elasticsearch cluster, you must also configure Kibana to connect to Elasticsearch via HTTPS. With {scyld} enabled, if you load a Kibana dashboard that accesses data in an diff --git a/docs/releasenotes.asciidoc b/docs/releasenotes.asciidoc index ceb585f470bab7..69a78d1af27d7d 100644 --- a/docs/releasenotes.asciidoc +++ b/docs/releasenotes.asciidoc @@ -37,7 +37,7 @@ The {version} release of Kibana requires Elasticsearch {esversion} or later. [[plugins-apis]] == Plugins, APIs, and Development Infrastructure -NOTE: The items in this section are not a complete list of the internal changes relating to development in Kibana. Plugin +NOTE: The items in this section are not a complete list of the internal changes relating to development in Kibana. Plugin framework and APIs are not formally documented and not guaranteed to be backward compatible from release to release. * {k4pull}7069[Pull Request 7069]: Adds `preInit` functionality. diff --git a/docs/settings.asciidoc b/docs/settings.asciidoc index 1b99bec2a93072..51ae6716105b05 100644 --- a/docs/settings.asciidoc +++ b/docs/settings.asciidoc @@ -1,55 +1,55 @@ [[settings]] == Settings -To use Kibana, you have to tell it about the Elasticsearch indices that you want to explore by configuring one or more +To use Kibana, you have to tell it about the Elasticsearch indices that you want to explore by configuring one or more index patterns. You can also: -* Create scripted fields that are computed on the fly from your data. You can browse and visualize scripted fields, but +* Create scripted fields that are computed on the fly from your data. You can browse and visualize scripted fields, but you cannot search them. -* Set advanced options such as the number of rows to show in a table and how many of the most popular fields to show. +* Set advanced options such as the number of rows to show in a table and how many of the most popular fields to show. Use caution when modifying advanced options, as it's possible to set values that are incompatible with one another. * Configure Kibana for a production environment [float] [[settings-create-pattern]] === Creating an Index Pattern to Connect to Elasticsearch -An _index pattern_ identifies one or more Elasticsearch indices that you want to explore with Kibana. Kibana looks for +An _index pattern_ identifies one or more Elasticsearch indices that you want to explore with Kibana. Kibana looks for index names that match the specified pattern. -An asterisk (*) in the pattern matches zero or more characters. For example, the pattern `myindex-*` matches all -indices whose names start with `myindex-`, such as `myindex-1` and `myindex-2`. +An asterisk (*) in the pattern matches zero or more characters. For example, the pattern `myindex-*` matches all +indices whose names start with `myindex-`, such as `myindex-1` and `myindex-2`. An index pattern can also simply be the name of a single index. To create an index pattern to connect to Elasticsearch: . Go to the *Settings > Indices* tab. -. Specify an index pattern that matches the name of one or more of your Elasticsearch indices. By default, Kibana +. Specify an index pattern that matches the name of one or more of your Elasticsearch indices. By default, Kibana guesses that you're you're working with log data being fed into Elasticsearch by Logstash. + -NOTE: When you switch between top-level tabs, Kibana remembers where you were. For example, if you view a particular -index pattern from the Settings tab, switch to the Discover tab, and then go back to the Settings tab, Kibana displays -the index pattern you last looked at. To get to the create pattern form, click the *Add* button in the Index Patterns +NOTE: When you switch between top-level tabs, Kibana remembers where you were. For example, if you view a particular +index pattern from the Settings tab, switch to the Discover tab, and then go back to the Settings tab, Kibana displays +the index pattern you last looked at. To get to the create pattern form, click the *Add* button in the Index Patterns list. -. If your index contains a timestamp field that you want to use to perform time-based comparisons, select the *Index -contains time-based events* option and select the index field that contains the timestamp. Kibana reads the index +. If your index contains a timestamp field that you want to use to perform time-based comparisons, select the *Index +contains time-based events* option and select the index field that contains the timestamp. Kibana reads the index mapping to list all of the fields that contain a timestamp. -. By default, Kibana restricts wildcard expansion of time-based index patterns to indices with data within the currently +. By default, Kibana restricts wildcard expansion of time-based index patterns to indices with data within the currently selected time range. Click *Do not expand index pattern when search* to disable this behavior. -. Click *Create* to add the index pattern. +. Click *Create* to add the index pattern. -. To designate the new pattern as the default pattern to load when you view the Discover tab, click the *favorite* -button. +. To designate the new pattern as the default pattern to load when you view the Discover tab, click the *favorite* +button. -NOTE: When you define an index pattern, indices that match that pattern must exist in Elasticsearch. Those indices must +NOTE: When you define an index pattern, indices that match that pattern must exist in Elasticsearch. Those indices must contain data. -To use an event time in an index name, enclose the static text in the pattern and specify the date format using the +To use an event time in an index name, enclose the static text in the pattern and specify the date format using the tokens described in the following table. -For example, `[logstash-]YYYY.MM.DD` matches all indices whose names have a timestamp of the form `YYYY.MM.DD` appended +For example, `[logstash-]YYYY.MM.DD` matches all indices whose names have a timestamp of the form `YYYY.MM.DD` appended to the prefix `logstash-`, such as `logstash-2015.01.31` and `logstash-2015-02-01`. [float] @@ -108,32 +108,32 @@ to the prefix `logstash-`, such as `logstash-2015.01.31` and `logstash-2015-02-0 [float] [[set-default-pattern]] === Setting the Default Index Pattern -The default index pattern is loaded by automatically when you view the *Discover* tab. Kibana displays a star to the -left of the name of the default pattern in the Index Patterns list on the *Settings > Indices* tab. The first pattern +The default index pattern is loaded by automatically when you view the *Discover* tab. Kibana displays a star to the +left of the name of the default pattern in the Index Patterns list on the *Settings > Indices* tab. The first pattern you create is automatically designated as the default pattern. To set a different pattern as the default index pattern: . Go to the *Settings > Indices* tab. . Select the pattern you want to set as the default in the Index Patterns list. -. Click the pattern's *Favorite* button. +. Click the pattern's *Favorite* button. -NOTE: You can also manually set the default index pattern in *Advanced > Settings*. +NOTE: You can also manually set the default index pattern in *Advanced > Settings*. [float] [[reload-fields]] === Reloading the Index Fields List -When you add an index mapping, Kibana automatically scans the indices that match the pattern to display a list of the -index fields. You can reload the index fields list to pick up any newly-added fields. +When you add an index mapping, Kibana automatically scans the indices that match the pattern to display a list of the +index fields. You can reload the index fields list to pick up any newly-added fields. -Reloading the index fields list also resets Kibana's popularity counters for the fields. The popularity counters keep -track of the fields you've used most often within Kibana and are used to sort fields within lists. +Reloading the index fields list also resets Kibana's popularity counters for the fields. The popularity counters keep +track of the fields you've used most often within Kibana and are used to sort fields within lists. To reload the index fields list: . Go to the *Settings > Indices* tab. . Select an index pattern from the Index Patterns list. -. Click the pattern's *Reload* button. +. Click the pattern's *Reload* button. [float] [[delete-pattern]] @@ -147,11 +147,11 @@ To delete an index pattern: [[managing-fields]] === Managing Fields -The fields for the index pattern are listed in a table. Click a column header to sort the table by that column. Click -the *Controls* button in the rightmost column for a given field to edit the field's properties. You can manually set +The fields for the index pattern are listed in a table. Click a column header to sort the table by that column. Click +the *Controls* button in the rightmost column for a given field to edit the field's properties. You can manually set the field's format from the *Format* drop-down. Format options vary based on the field's type. -You can also set the field's popularity value in the *Popularity* text entry box to any desired value. Click the +You can also set the field's popularity value in the *Popularity* text entry box to any desired value. Click the *Update Field* button to confirm your changes or *Cancel* to return to the list of fields. Kibana has https://www.elastic.co/blog/kibana-4-1-field-formatters[field formatters] for the following field types: @@ -193,23 +193,23 @@ include::duration-formatter.asciidoc[] include::color-formatter.asciidoc[] -The `Bytes`, `Number`, and `Percentage` formatters enable you to choose the display formats of numbers in this field using +The `Bytes`, `Number`, and `Percentage` formatters enable you to choose the display formats of numbers in this field using the https://adamwdraper.github.io/Numeral-js/[numeral.js] standard format definitions. [float] [[create-scripted-field]] === Creating a Scripted Field -Scripted fields compute data on the fly from the data in your Elasticsearch indices. Scripted field data is shown on +Scripted fields compute data on the fly from the data in your Elasticsearch indices. Scripted field data is shown on the Discover tab as part of the document data, and you can use scripted fields in your visualizations. Scripted field values are computed at query time so they aren't indexed and cannot be searched. NOTE: Kibana cannot query scripted fields. -WARNING: Computing data on the fly with scripted fields can be very resource intensive and can have a direct impact on -Kibana's performance. Keep in mind that there's no built-in validation of a scripted field. If your scripts are +WARNING: Computing data on the fly with scripted fields can be very resource intensive and can have a direct impact on +Kibana's performance. Keep in mind that there's no built-in validation of a scripted field. If your scripts are buggy, you'll get exceptions whenever you try to view the dynamically generated data. -Scripted fields use the Lucene expression syntax. For more information, +Scripted fields use the Lucene expression syntax. For more information, see http://www.elastic.co/guide/en/elasticsearch/reference/current/modules-scripting.html#_lucene_expressions_scripts[ Lucene Expressions Scripts]. @@ -224,15 +224,15 @@ To create a scripted field: . Go to *Settings > Indices* . Select the index pattern you want to add a scripted field to. . Go to the pattern's *Scripted Fields* tab. -. Click *Add Scripted Field*. +. Click *Add Scripted Field*. . Enter a name for the scripted field. . Enter the expression that you want to use to compute a value on the fly from your index data. . Click *Save Scripted Field*. -For more information about scripted fields in Elasticsearch, see +For more information about scripted fields in Elasticsearch, see http://www.elastic.co/guide/en/elasticsearch/reference/current/modules-scripting.html[Scripting]. -NOTE: In Elasticsearch releases 1.4.3 and later, this functionality requires you to enable +NOTE: In Elasticsearch releases 1.4.3 and later, this functionality requires you to enable {ref}/modules-scripting.html[dynamic Groovy scripting]. [float] @@ -244,7 +244,7 @@ To modify a scripted field: . Click the *Edit* button for the scripted field you want to change. . Make your changes and then click *Save Scripted Field* to update the field. -WARNING: Keep in mind that there's no built-in validation of a scripted field. If your scripts are buggy, you'll get +WARNING: Keep in mind that there's no built-in validation of a scripted field. If your scripts are buggy, you'll get exceptions whenever you try to view the dynamically generated data. [float] @@ -258,15 +258,15 @@ To delete a scripted field: [[advanced-options]] === Setting Advanced Options -The *Advanced Settings* page enables you to directly edit settings that control the behavior of the Kibana application. -For example, you can change the format used to display dates, specify the default index pattern, and set the precision -for displayed decimal values. +The *Advanced Settings* page enables you to directly edit settings that control the behavior of the Kibana application. +For example, you can change the format used to display dates, specify the default index pattern, and set the precision +for displayed decimal values. To set advanced options: . Go to *Settings > Advanced*. . Click the *Edit* button for the option you want to modify. -. Enter a new value for the option. +. Enter a new value for the option. . Click the *Save* button. include::advanced-settings.asciidoc[] @@ -274,8 +274,8 @@ include::advanced-settings.asciidoc[] [[kibana-server-properties]] === Setting Kibana Server Properties -The Kibana server reads properties from the `kibana.yml` file on startup. The default settings configure Kibana to run -on `localhost:5601`. To change the host or port number, or connect to Elasticsearch running on a different machine, +The Kibana server reads properties from the `kibana.yml` file on startup. The default settings configure Kibana to run +on `localhost:5601`. To change the host or port number, or connect to Elasticsearch running on a different machine, you'll need to update your `kibana.yml` file. You can also enable SSL and set a variety of other options. include::kibana-yml.asciidoc[] @@ -288,7 +288,7 @@ deprecated[4.2, The names of several Kibana server properties changed in the 4.2 `server.port` added[4.2]:: The port that the Kibana server runs on. + *alias*: `port` deprecated[4.2] -+ ++ *default*: `5601` `server.host` added[4.2]:: The host to bind the Kibana server to. @@ -306,7 +306,7 @@ deprecated[4.2, The names of several Kibana server properties changed in the 4.2 `elasticsearch.preserveHost` added[4.2]:: By default, the host specified in the incoming request from the browser is specified as the host in the corresponding request Kibana sends to Elasticsearch. If you set this option to `false`, Kibana uses the host specified in `elasticsearch_url`. + *alias*: `elasticsearch_preserve_host` deprecated[4.2] -+ ++ *default*: `true` `elasticsearch.ssl.cert` added[4.2]:: This parameter specifies the path to the SSL certificate for Elasticsearch instances that require a client certificate. @@ -328,25 +328,25 @@ deprecated[4.2, The names of several Kibana server properties changed in the 4.2 `elasticsearch.pingTimeout` added[4.2]:: This parameter specifies the maximum wait time in milliseconds for ping responses by Elasticsearch. + *alias*: `ping_timeout` deprecated[4.2] -+ ++ *default*: `1500` `elasticsearch.startupTimeout` added[4.2]:: This parameter specifies the maximum wait time in milliseconds for Elasticsearch discovery at Kibana startup. Kibana repeats attempts to discover an Elasticsearch cluster after the specified time elapses. + *alias*: `startup_timeout` deprecated[4.2] -+ ++ *default*: `5000` `kibana.index` added[4.2]:: The name of the index where saved searched, visualizations, and dashboards will be stored.. + *alias*: `kibana_index` deprecated[4.2] -+ ++ *default*: `.kibana` `kibana.defaultAppId` added[4.2]:: The page that will be displayed when you launch Kibana: `discover`, `visualize`, `dashboard`, or `settings`. + *alias*: `default_app_id` deprecated[4.2] -+ ++ *default*: `"discover"` `logging.silent` added[4.2]:: Set this value to `true` to suppress all logging output. @@ -373,7 +373,7 @@ deprecated[4.2, The names of several Kibana server properties changed in the 4.2 `elasticsearch.requestTimeout` added[4.2]:: How long to wait for responses from the Kibana backend or Elasticsearch, in milliseconds. + *alias*: `request_timeout` deprecated[4.2] -+ ++ *default*: `500000` `elasticsearch.requestHeadersWhitelist:` added[5.0]:: List of Kibana client-side headers to send to Elasticsearch. To send *no* client-side headers, set this value to [] (an empty list). @@ -383,16 +383,16 @@ deprecated[4.2, The names of several Kibana server properties changed in the 4.2 `elasticsearch.shardTimeout` added[4.2]:: How long Elasticsearch should wait for responses from shards. Set to 0 to disable. + *alias*: `shard_timeout` deprecated[4.2] -+ ++ *default*: `0` `elasticsearch.ssl.verify` added[4.2]:: Indicates whether or not to validate the Elasticsearch SSL certificate. Set to false to disable SSL verification. + *alias*: `verify_ssl` deprecated[4.2] -+ ++ *default*: `true` -`elasticsearch.ssl.ca`:: An array of paths to the CA certificates for your Elasticsearch instance. Specify if +`elasticsearch.ssl.ca`:: An array of paths to the CA certificates for your Elasticsearch instance. Specify if you are using a self-signed certificate so the certificate can be verified. Disable `elasticsearch.ssl.verify` otherwise. + *alias*: `ca` deprecated[4.2] @@ -417,36 +417,36 @@ you are using a self-signed certificate so the certificate can be verified. Disa //// [[managing-saved-objects]] -=== Managing Saved Searches, Visualizations, and Dashboards +=== Managing Saved Searches, Visualizations, and Dashboards -You can view, edit, and delete saved searches, visualizations, and dashboards from *Settings > Objects*. You can also +You can view, edit, and delete saved searches, visualizations, and dashboards from *Settings > Objects*. You can also export or import sets of searches, visualizations, and dashboards. -Viewing a saved object displays the selected item in the *Discover*, *Visualize*, or *Dashboard* page. To view a saved +Viewing a saved object displays the selected item in the *Discover*, *Visualize*, or *Dashboard* page. To view a saved object: . Go to *Settings > Objects*. -. Select the object you want to view. +. Select the object you want to view. . Click the *View* button. -Editing a saved object enables you to directly modify the object definition. You can change the name of the object, add -a description, and modify the JSON that defines the object's properties. +Editing a saved object enables you to directly modify the object definition. You can change the name of the object, add +a description, and modify the JSON that defines the object's properties. If you attempt to access an object whose index has been deleted, Kibana displays its Edit Object page. You can: -* Recreate the index so you can continue using the object. +* Recreate the index so you can continue using the object. * Delete the object and recreate it using a different index. -* Change the index name referenced in the object's `kibanaSavedObjectMeta.searchSourceJSON` to point to an existing -index pattern. This is useful if the index you were working with has been renamed. +* Change the index name referenced in the object's `kibanaSavedObjectMeta.searchSourceJSON` to point to an existing +index pattern. This is useful if the index you were working with has been renamed. -WARNING: No validation is performed for object properties. Submitting invalid changes will render the object unusable. -Generally, you should use the *Discover*, *Visualize*, or *Dashboard* pages to create new objects instead of directly -editing existing ones. +WARNING: No validation is performed for object properties. Submitting invalid changes will render the object unusable. +Generally, you should use the *Discover*, *Visualize*, or *Dashboard* pages to create new objects instead of directly +editing existing ones. To edit a saved object: . Go to *Settings > Objects*. -. Select the object you want to edit. +. Select the object you want to edit. . Click the *Edit* button. . Make your changes to the object definition. . Click the *Save Object* button. @@ -454,18 +454,18 @@ To edit a saved object: To delete a saved object: . Go to *Settings > Objects*. -. Select the object you want to delete. +. Select the object you want to delete. . Click the *Delete* button. . Confirm that you really want to delete the object. To export a set of objects: . Go to *Settings > Objects*. -. Select the type of object you want to export. You can export a set of dashboards, searches, or visualizations. +. Select the type of object you want to export. You can export a set of dashboards, searches, or visualizations. . Click the selection box for the objects you want to export, or click the *Select All* box. . Click *Export* to select a location to write the exported JSON. -WARNING: Exported dashboards do not include their associated index patterns. Re-create the index patterns manually before +WARNING: Exported dashboards do not include their associated index patterns. Re-create the index patterns manually before importing saved dashboards to a Kibana instance running on another Elasticsearch cluster. To import a set of objects: diff --git a/docs/setup.asciidoc b/docs/setup.asciidoc index 74b87345bfb212..9504aee0ba058c 100644 --- a/docs/setup.asciidoc +++ b/docs/setup.asciidoc @@ -83,8 +83,8 @@ simply be the name of a single index. reads the index mapping to list all of the fields that contain a timestamp. If your index doesn't have time-based data, disable the *Index contains time-based events* option. + -WARNING: Using event times to create index names is *deprecated* in this release of Kibana. Starting in the 2.1 -release, Elasticsearch includes sophisticated date parsing APIs that Kibana uses to determine date information, +WARNING: Using event times to create index names is *deprecated* in this release of Kibana. Starting in the 2.1 +release, Elasticsearch includes sophisticated date parsing APIs that Kibana uses to determine date information, removing the need to specify dates in the index pattern name. + . Click *Create* to add the index pattern. This first pattern is automatically configured as the default. diff --git a/docs/string-formatter.asciidoc b/docs/string-formatter.asciidoc index 63232c0b7fbea3..ed3aa45873284e 100644 --- a/docs/string-formatter.asciidoc +++ b/docs/string-formatter.asciidoc @@ -3,7 +3,7 @@ The `String` field formatter can apply the following transformations to the fiel * Convert to lowercase * Convert to uppercase * Convert to title case -* Apply the short dots transformation, which replaces the content before a `.` character with the first character of +* Apply the short dots transformation, which replaces the content before a `.` character with the first character of that content, as in the following example: [horizontal] diff --git a/docs/tilemap.asciidoc b/docs/tilemap.asciidoc index 8bcf21cdf5c536..c052ff66b5e297 100644 --- a/docs/tilemap.asciidoc +++ b/docs/tilemap.asciidoc @@ -3,72 +3,72 @@ A tile map displays a geographic area overlaid with circles keyed to the data determined by the buckets you specify. -The default _metrics_ aggregation for a tile map is the *Count* aggregation. You can select any of the following +The default _metrics_ aggregation for a tile map is the *Count* aggregation. You can select any of the following aggregations as the metrics aggregation: -*Count*:: The {ref}search-aggregations-metrics-valuecount-aggregation.html[_count_] aggregation returns a raw count of +*Count*:: The {ref}search-aggregations-metrics-valuecount-aggregation.html[_count_] aggregation returns a raw count of the elements in the selected index pattern. -*Average*:: This aggregation returns the {ref}search-aggregations-metrics-avg-aggregation.html[_average_] of a numeric +*Average*:: This aggregation returns the {ref}search-aggregations-metrics-avg-aggregation.html[_average_] of a numeric field. Select a field from the drop-down. -*Sum*:: The {ref}search-aggregations-metrics-sum-aggregation.html[_sum_] aggregation returns the total sum of a numeric +*Sum*:: The {ref}search-aggregations-metrics-sum-aggregation.html[_sum_] aggregation returns the total sum of a numeric field. Select a field from the drop-down. -*Min*:: The {ref}search-aggregations-metrics-min-aggregation.html[_min_] aggregation returns the minimum value of a +*Min*:: The {ref}search-aggregations-metrics-min-aggregation.html[_min_] aggregation returns the minimum value of a numeric field. Select a field from the drop-down. -*Max*:: The {ref}search-aggregations-metrics-max-aggregation.html[_max_] aggregation returns the maximum value of a +*Max*:: The {ref}search-aggregations-metrics-max-aggregation.html[_max_] aggregation returns the maximum value of a numeric field. Select a field from the drop-down. -*Unique Count*:: The {ref}search-aggregations-metrics-cardinality-aggregation.html[_cardinality_] aggregation returns +*Unique Count*:: The {ref}search-aggregations-metrics-cardinality-aggregation.html[_cardinality_] aggregation returns the number of unique values in a field. Select a field from the drop-down. Enter a string in the *Custom Label* field to change the display label. The _buckets_ aggregations determine what information is being retrieved from your data set. -Before you choose a buckets aggregation, specify if you are splitting the chart or displaying the buckets as *Geo +Before you choose a buckets aggregation, specify if you are splitting the chart or displaying the buckets as *Geo Coordinates* on a single chart. A multiple chart split must run before any other aggregations. Tile maps use the *Geohash* aggregation as their initial aggregation. Select a field, typically coordinates, from the -drop-down. The *Precision* slider determines the granularity of the results displayed on the map. See the documentation -for the {ref}/search-aggregations-bucket-geohashgrid-aggregation.html#_cell_dimensions_at_the_equator[geohash grid] +drop-down. The *Precision* slider determines the granularity of the results displayed on the map. See the documentation +for the {ref}/search-aggregations-bucket-geohashgrid-aggregation.html#_cell_dimensions_at_the_equator[geohash grid] aggregation for details on the area specified by each precision level. Kibana supports a maximum geohash length of 7. -NOTE: Higher precisions increase memory usage for the browser displaying Kibana as well as for the underlying +NOTE: Higher precisions increase memory usage for the browser displaying Kibana as well as for the underlying Elasticsearch cluster. -Once you've specified a buckets aggregation, you can define sub-aggregations to refine the visualization. Tile maps -only support sub-aggregations as split charts. Click *+ Add Sub Aggregation*, then *Split Chart* to select a +Once you've specified a buckets aggregation, you can define sub-aggregations to refine the visualization. Tile maps +only support sub-aggregations as split charts. Click *+ Add Sub Aggregation*, then *Split Chart* to select a sub-aggregation from the list of types: -*Date Histogram*:: A {ref}search-aggregations-bucket-datehistogram-aggregation.html[_date histogram_] is built from a -numeric field and organized by date. You can specify a time frame for the intervals in seconds, minutes, hours, days, -weeks, months, or years. You can also specify a custom interval frame by selecting *Custom* as the interval and -specifying a number and a time unit in the text field. Custom interval time units are *s* for seconds, *m* for minutes, -*h* for hours, *d* for days, *w* for weeks, and *y* for years. Different units support different levels of precision, +*Date Histogram*:: A {ref}search-aggregations-bucket-datehistogram-aggregation.html[_date histogram_] is built from a +numeric field and organized by date. You can specify a time frame for the intervals in seconds, minutes, hours, days, +weeks, months, or years. You can also specify a custom interval frame by selecting *Custom* as the interval and +specifying a number and a time unit in the text field. Custom interval time units are *s* for seconds, *m* for minutes, +*h* for hours, *d* for days, *w* for weeks, and *y* for years. Different units support different levels of precision, down to one second. -*Histogram*:: A standard {ref}search-aggregations-bucket-histogram-aggregation.html[_histogram_] is built from a -numeric field. Specify an integer interval for this field. Select the *Show empty buckets* checkbox to include empty +*Histogram*:: A standard {ref}search-aggregations-bucket-histogram-aggregation.html[_histogram_] is built from a +numeric field. Specify an integer interval for this field. Select the *Show empty buckets* checkbox to include empty intervals in the histogram. -*Range*:: With a {ref}search-aggregations-bucket-range-aggregation.html[_range_] aggregation, you can specify ranges -of values for a numeric field. Click *Add Range* to add a set of range endpoints. Click the red *(x)* symbol to remove +*Range*:: With a {ref}search-aggregations-bucket-range-aggregation.html[_range_] aggregation, you can specify ranges +of values for a numeric field. Click *Add Range* to add a set of range endpoints. Click the red *(x)* symbol to remove a range. -After changing options, click the *Apply changes* button to update your visualization, or the grey *Discard +After changing options, click the *Apply changes* button to update your visualization, or the grey *Discard changes* button to keep your visualization in its current state. -*Date Range*:: A {ref}search-aggregations-bucket-daterange-aggregation.html[_date range_] aggregation reports values -that are within a range of dates that you specify. You can specify the ranges for the dates using -{ref}common-options.html#date-math[_date math_] expressions. Click *Add Range* to add a set of range endpoints. +*Date Range*:: A {ref}search-aggregations-bucket-daterange-aggregation.html[_date range_] aggregation reports values +that are within a range of dates that you specify. You can specify the ranges for the dates using +{ref}common-options.html#date-math[_date math_] expressions. Click *Add Range* to add a set of range endpoints. Click the red *(/)* symbol to remove a range. *IPv4 Range*:: The {ref}search-aggregations-bucket-iprange-aggregation.html[_IPv4 range_] aggregation enables you to -specify ranges of IPv4 addresses. Click *Add Range* to add a set of range endpoints. Click the red *(/)* symbol to +specify ranges of IPv4 addresses. Click *Add Range* to add a set of range endpoints. Click the red *(/)* symbol to remove a range. -*Terms*:: A {ref}search-aggregations-bucket-terms-aggregation.html[_terms_] aggregation enables you to specify the top +*Terms*:: A {ref}search-aggregations-bucket-terms-aggregation.html[_terms_] aggregation enables you to specify the top or bottom _n_ elements of a given field to display, ordered by count or a custom metric. -*Filters*:: You can specify a set of {ref}search-aggregations-bucket-filters-aggregation.html[_filters_] for the data. -You can specify a filter as a query string or in JSON format, just as in the Discover search bar. Click *Add Filter* to -add another filter. Click the image:images/labelbutton.png[] *label* button to open the label field, where you can type +*Filters*:: You can specify a set of {ref}search-aggregations-bucket-filters-aggregation.html[_filters_] for the data. +You can specify a filter as a query string or in JSON format, just as in the Discover search bar. Click *Add Filter* to +add another filter. Click the image:images/labelbutton.png[] *label* button to open the label field, where you can type in a name to display on the visualization. -*Significant Terms*:: Displays the results of the experimental -{ref}search-aggregations-bucket-significantterms-aggregation.html[_significant terms_] aggregation. The value of the +*Significant Terms*:: Displays the results of the experimental +{ref}search-aggregations-bucket-significantterms-aggregation.html[_significant terms_] aggregation. The value of the *Size* parameter defines the number of entries this aggregation returns. -*Geohash*:: The {ref}search-aggregations-bucket-geohashgrid-aggregation.html[_geohash_] aggregation displays points +*Geohash*:: The {ref}search-aggregations-bucket-geohashgrid-aggregation.html[_geohash_] aggregation displays points based on the geohash coordinates. NOTE: By default, the *Change precision on map zoom* box is checked. Uncheck the box to disable this behavior. @@ -79,13 +79,13 @@ You can click the *Advanced* link to display more customization options for your *Exclude Pattern*:: Specify a pattern in this field to exclude from the results. *Include Pattern*:: Specify a pattern in this field to include in the results. -*JSON Input*:: A text field where you can add specific JSON-formatted properties to merge with the aggregation +*JSON Input*:: A text field where you can add specific JSON-formatted properties to merge with the aggregation definition, as in the following example: [source,shell] { "script" : "doc['grade'].value * 1.2" } -NOTE: In Elasticsearch releases 1.4.3 and later, this functionality requires you to enable +NOTE: In Elasticsearch releases 1.4.3 and later, this functionality requires you to enable {ref}modules-scripting.html[dynamic Groovy scripting]. The availability of these options varies depending on the aggregation you choose. @@ -95,14 +95,14 @@ Select the *Options* tab to change the following aspects of the chart: *Map type*:: Select one of the following options from the drop-down. *_Scaled Circle Markers_*:: Scale the size of the markers based on the metric aggregation's value. *_Shaded Circle Markers_*:: Displays the markers with different shades based on the metric aggregation's value. -*_Shaded Geohash Grid_*:: Displays the rectangular cells of the geohash grid instead of circular markers, with different +*_Shaded Geohash Grid_*:: Displays the rectangular cells of the geohash grid instead of circular markers, with different shades based on the metric aggregation's value. -*_Heatmap_*:: A heat map applies blurring to the circle markers and applies shading based on the amount of overlap. +*_Heatmap_*:: A heat map applies blurring to the circle markers and applies shading based on the amount of overlap. Heatmaps have the following options: * *Radius*: Sets the size of the individual heatmap dots. * *Blur*: Sets the amount of blurring for the heatmap dots. -* *Maximum zoom*: Tilemaps in Kibana support 18 zoom levels. This slider defines the maximum zoom level at which the +* *Maximum zoom*: Tilemaps in Kibana support 18 zoom levels. This slider defines the maximum zoom level at which the heatmap dots appear at full intensity. * *Minimum opacity*: Sets the opacity cutoff for the dots. * *Show Tooltip*: Check this box to have a tooltip with the values for a given dot when the cursor is on that dot. @@ -116,12 +116,12 @@ Map Service (WMS) standard. Specify the following elements: layers. * *WMS version*: The WMS version used by this map service. * *WMS format*: The image format used by this map service. The two most common formats are `image/png` and `image/jpeg`. -* *WMS attribution*: An optional, user-defined string that identifies the map source. Maps display the attribution string +* *WMS attribution*: An optional, user-defined string that identifies the map source. Maps display the attribution string in the lower right corner. -* *WMS styles*: A comma-separated list of the styles to use in this visualization. Each map server provides its own styling +* *WMS styles*: A comma-separated list of the styles to use in this visualization. Each map server provides its own styling options. -After changing options, click the *Apply changes* button to update your visualization, or the grey *Discard +After changing options, click the *Apply changes* button to update your visualization, or the grey *Discard changes* button to keep your visualization in its current state. [float] @@ -129,12 +129,12 @@ changes* button to keep your visualization in its current state. ==== Navigating the Map Once your tilemap visualization is ready, you can explore the map in several ways: -* Click and hold anywhere on the map and move the cursor to move the map center. Hold Shift and drag a bounding box -across the map to zoom in on the selection. +* Click and hold anywhere on the map and move the cursor to move the map center. Hold Shift and drag a bounding box +across the map to zoom in on the selection. * Click the *Zoom In/Out* image:images/viz-zoom.png[] buttons to change the zoom level manually. -* Click the *Fit Data Bounds* image:images/viz-fit-bounds.png[] button to automatically crop the map boundaries to the +* Click the *Fit Data Bounds* image:images/viz-fit-bounds.png[] button to automatically crop the map boundaries to the geohash buckets that have at least one result. -* Click the *Latitude/Longitude Filter* image:images/viz-lat-long-filter.png[] button, then drag a bounding box across the +* Click the *Latitude/Longitude Filter* image:images/viz-lat-long-filter.png[] button, then drag a bounding box across the map, to create a filter for the box coordinates. [float] diff --git a/docs/url-formatter.asciidoc b/docs/url-formatter.asciidoc index 819523c6cbf53c..b2c1d149b769e4 100644 --- a/docs/url-formatter.asciidoc +++ b/docs/url-formatter.asciidoc @@ -1,9 +1,9 @@ The `Url` field formatter can take on the following types: -* The *Link* type turn the contents of the field into an URL. +* The *Link* type turn the contents of the field into an URL. * The *Image* type can be used to specify an image directory where a specified image is located. -You can customize either type of URL field formats with templates. A _URL template_ enables you to add specific values +You can customize either type of URL field formats with templates. A _URL template_ enables you to add specific values to a partial URL. Use the string `{{value}}` to add the contents of the field to a fixed URL. For example, when: @@ -14,15 +14,15 @@ For example, when: The resulting URL replaces `{{value}}` with the user ID from the field. -The `{{value}}` template string URL-encodes the contents of the field. When a field encoded into a URL contains -non-ASCII characters, these characters are replaced with a `%` character and the appropriate hexadecimal code. For +The `{{value}}` template string URL-encodes the contents of the field. When a field encoded into a URL contains +non-ASCII characters, these characters are replaced with a `%` character and the appropriate hexadecimal code. For example, field contents `users/admin` result in the URL template adding `users%2Fadmin`. -When the formatter type is set to *Image*, the `{{value}}` template string specifies the name of an image at the +When the formatter type is set to *Image*, the `{{value}}` template string specifies the name of an image at the specified URI. In order to pass unescaped values directly to the URL, use the `{{rawValue}}` string. -A _Label Template_ enables you to specify a text string that displays instead of the raw URL. You can use the +A _Label Template_ enables you to specify a text string that displays instead of the raw URL. You can use the `{{value}}` template string normally in label templates. You can also use the `{{url}}` template string to display the formatted URL. diff --git a/docs/vertbar.asciidoc b/docs/vertbar.asciidoc index 5e7583690b4ce2..8b9938de4a5fd3 100644 --- a/docs/vertbar.asciidoc +++ b/docs/vertbar.asciidoc @@ -3,24 +3,24 @@ This chart's Y axis is the _metrics_ axis. The following aggregations are available for this axis: -*Count*:: The {ref}/search-aggregations-metrics-valuecount-aggregation.html[_count_] aggregation returns a raw count of +*Count*:: The {ref}/search-aggregations-metrics-valuecount-aggregation.html[_count_] aggregation returns a raw count of the elements in the selected index pattern. -*Average*:: This aggregation returns the {ref}/search-aggregations-metrics-avg-aggregation.html[_average_] of a numeric +*Average*:: This aggregation returns the {ref}/search-aggregations-metrics-avg-aggregation.html[_average_] of a numeric field. Select a field from the drop-down. -*Sum*:: The {ref}/search-aggregations-metrics-sum-aggregation.html[_sum_] aggregation returns the total sum of a numeric +*Sum*:: The {ref}/search-aggregations-metrics-sum-aggregation.html[_sum_] aggregation returns the total sum of a numeric field. Select a field from the drop-down. -*Min*:: The {ref}/search-aggregations-metrics-min-aggregation.html[_min_] aggregation returns the minimum value of a +*Min*:: The {ref}/search-aggregations-metrics-min-aggregation.html[_min_] aggregation returns the minimum value of a numeric field. Select a field from the drop-down. -*Max*:: The {ref}/search-aggregations-metrics-max-aggregation.html[_max_] aggregation returns the maximum value of a +*Max*:: The {ref}/search-aggregations-metrics-max-aggregation.html[_max_] aggregation returns the maximum value of a numeric field. Select a field from the drop-down. -*Unique Count*:: The {ref}/search-aggregations-metrics-cardinality-aggregation.html[_cardinality_] aggregation returns +*Unique Count*:: The {ref}/search-aggregations-metrics-cardinality-aggregation.html[_cardinality_] aggregation returns the number of unique values in a field. Select a field from the drop-down. -*Percentiles*:: The {ref}/search-aggregations-metrics-percentile-aggregation.html[_percentile_] aggregation divides the -values in a numeric field into percentile bands that you specify. Select a field from the drop-down, then specify one -or more ranges in the *Percentiles* fields. Click the *X* to remove a percentile field. Click *+ Add* to add a +*Percentiles*:: The {ref}/search-aggregations-metrics-percentile-aggregation.html[_percentile_] aggregation divides the +values in a numeric field into percentile bands that you specify. Select a field from the drop-down, then specify one +or more ranges in the *Percentiles* fields. Click the *X* to remove a percentile field. Click *+ Add* to add a percentile field. -*Percentile Rank*:: The {ref}/search-aggregations-metrics-percentile-rank-aggregation.html[_percentile ranks_] -aggregation returns the percentile rankings for the values in the numeric field you specify. Select a numeric field +*Percentile Rank*:: The {ref}/search-aggregations-metrics-percentile-rank-aggregation.html[_percentile ranks_] +aggregation returns the percentile rankings for the values in the numeric field you specify. Select a numeric field from the drop-down, then specify one or more percentile rank values in the *Values* fields. Click the *X* to remove a values field. Click *+Add* to add a values field. @@ -31,7 +31,7 @@ Enter a string in the *Custom Label* field to change the display label. The _buckets_ aggregations determine what information is being retrieved from your data set. Before you choose a buckets aggregation, specify if you are splitting slices within a single chart or splitting into -multiple charts. A multiple chart split must run before any other aggregations. When you split a chart, you can change +multiple charts. A multiple chart split must run before any other aggregations. When you split a chart, you can change if the splits are displayed in a row or a column by clicking the *Rows | Columns* selector. include::x-axis-aggs.asciidoc[] @@ -44,20 +44,20 @@ You can click the *Advanced* link to display more customization options for your *Exclude Pattern*:: Specify a pattern in this field to exclude from the results. *Include Pattern*:: Specify a pattern in this field to include in the results. -*JSON Input*:: A text field where you can add specific JSON-formatted properties to merge with the aggregation +*JSON Input*:: A text field where you can add specific JSON-formatted properties to merge with the aggregation definition, as in the following example: [source,shell] { "script" : "doc['grade'].value * 1.2" } -NOTE: In Elasticsearch releases 1.4.3 and later, this functionality requires you to enable +NOTE: In Elasticsearch releases 1.4.3 and later, this functionality requires you to enable {ref}/modules-scripting.html[dynamic Groovy scripting]. The availability of these options varies depending on the aggregation you choose. Select the *Options* to change the following aspects of the table: -*Bar Mode*:: When you have multiple Y-axis aggregations defined for your chart, you can use this drop-down to affect +*Bar Mode*:: When you have multiple Y-axis aggregations defined for your chart, you can use this drop-down to affect how the aggregations display on the chart: _stacked_:: Stacks the aggregations on top of each other. @@ -67,7 +67,7 @@ _grouped_:: Groups the results horizontally by the lowest-priority sub-aggregati Checkboxes are available to enable and disable the following behaviors: *Show Tooltip*:: Check this box to enable the display of tooltips. -*Scale Y-Axis to Data Bounds*:: The default Y axis bounds are zero and the maximum value returned in the data. Check +*Scale Y-Axis to Data Bounds*:: The default Y axis bounds are zero and the maximum value returned in the data. Check this box to change both upper and lower bounds to match the values returned in the data. [float] diff --git a/docs/visualization-raw-data.asciidoc b/docs/visualization-raw-data.asciidoc index 0c882a6854ea28..ef50426146e882 100644 --- a/docs/visualization-raw-data.asciidoc +++ b/docs/visualization-raw-data.asciidoc @@ -1,8 +1,8 @@ -To display the raw data behind the visualization, click the bar at the bottom of the container. Tabs with detailed +To display the raw data behind the visualization, click the bar at the bottom of the container. Tabs with detailed information about the raw data replace the visualization: .Table -A representation of the underlying data, presented as a paginated data grid. You can sort the items +A representation of the underlying data, presented as a paginated data grid. You can sort the items in the table by clicking on the table headers at the top of each column. .Request @@ -12,11 +12,11 @@ The raw request used to query the server, presented in JSON format. The raw response from the server, presented in JSON format. .Statistics -A summary of the statistics related to the request and the response, presented as a data grid. The data -grid includes the query duration, the request duration, the total number of records found on the server, and the +A summary of the statistics related to the request and the response, presented as a data grid. The data +grid includes the query duration, the request duration, the total number of records found on the server, and the index pattern used to make the query. To export the raw data behind the visualization as a comma-separated-values (CSV) file, click on either the -*Raw* or *Formatted* links at the bottom of any of the detailed information tabs. A raw export contains the data as it -is stored in Elasticsearch. A formatted export contains the results of any applicable Kibana +*Raw* or *Formatted* links at the bottom of any of the detailed information tabs. A raw export contains the data as it +is stored in Elasticsearch. A formatted export contains the results of any applicable Kibana <>. diff --git a/docs/visualize.asciidoc b/docs/visualize.asciidoc index c598b2ea47acce..87c296cbe58a26 100644 --- a/docs/visualize.asciidoc +++ b/docs/visualize.asciidoc @@ -1,15 +1,15 @@ [[visualize]] == Visualize -You can use the _Visualize_ page to design data visualizations. You can save these visualizations, use them -individually, or combine visualizations into a _dashboard_. A visualization can be based on one of the following +You can use the _Visualize_ page to design data visualizations. You can save these visualizations, use them +individually, or combine visualizations into a _dashboard_. A visualization can be based on one of the following data source types: * A new interactive search * A saved search * An existing saved visualization -Visualizations are based on the {ref}search-aggregations.html[aggregation] feature introduced in Elasticsearch 1.x. +Visualizations are based on the {ref}search-aggregations.html[aggregation] feature introduced in Elasticsearch 1.x. [float] [[createvis]] @@ -27,22 +27,22 @@ Choose a visualization type when you start the New Visualization wizard: [horizontal] <>:: Use area charts to visualize the total contribution of several different series. -<>:: Use data tables to display the raw data of a composed aggregation. You can display the data +<>:: Use data tables to display the raw data of a composed aggregation. You can display the data table for several other visualizations by clicking at the bottom of the visualization. <>:: Use line charts to compare different series. -<>:: Use the Markdown widget to display free-form information or instructions about your +<>:: Use the Markdown widget to display free-form information or instructions about your dashboard. <>:: Use the metric visualization to display a single number on your dashboard. <>:: Use pie charts to display each source's contribution to a total. <>:: Use tile maps to associate the results of an aggregation with geographic points. <>:: Use vertical bar charts as a general-purpose chart. -You can also load a saved visualization that you created earlier. The saved visualization selector includes a text -field to filter by visualization name and a link to the Object Editor, accessible through *Settings > Objects*, to +You can also load a saved visualization that you created earlier. The saved visualization selector includes a text +field to filter by visualization name and a link to the Object Editor, accessible through *Settings > Objects*, to manage your saved visualizations. -If your new visualization is a Markdown widget, selecting that type takes you to a text entry field where you enter the -text to display in the widget. For all other types of visualization, selecting the type takes you to data source +If your new visualization is a Markdown widget, selecting that type takes you to a text entry field where you enter the +text to display in the widget. For all other types of visualization, selecting the type takes you to data source selection. [float] @@ -50,8 +50,8 @@ selection. ==== Step 2: Choose a Data Source You can choose a new or saved search to serve as the data source for your visualization. Searches are associated with -an index or a set of indexes. When you select _new search_ on a system with multiple indices configured, select an -index pattern from the drop-down to bring up the visualization editor. +an index or a set of indexes. When you select _new search_ on a system with multiple indices configured, select an +index pattern from the drop-down to bring up the visualization editor. When you create a visualization from a saved search and save the visualization, the search is tied to the visualization. When you make changes to the search that is linked to the visualization, the visualization updates automatically. @@ -60,7 +60,7 @@ When you make changes to the search that is linked to the visualization, the vis [[visualization-editor]] ==== Step 3: The Visualization Editor -The visualization editor enables you to configure and edit visualizations. The visualization editor has the following +The visualization editor enables you to configure and edit visualizations. The visualization editor has the following main elements: 1. <> @@ -78,37 +78,37 @@ include::autorefresh.asciidoc[] ===== Toolbar The toolbar has a search field for interactive data searches, as well as controls to manage saving and loading -visualizations. For visualizations based on saved searches, the search bar is grayed out. To edit the search, replacing +visualizations. For visualizations based on saved searches, the search bar is grayed out. To edit the search, replacing the saved search with the edited version, double-click the search field. -The toolbar at the right of the search box has buttons for creating new visualizations, saving the current -visualization, loading an existing visualization, sharing or embedding the visualization, and refreshing the data for +The toolbar at the right of the search box has buttons for creating new visualizations, saving the current +visualization, loading an existing visualization, sharing or embedding the visualization, and refreshing the data for the current visualization. [float] [[aggregation-builder]] ===== Aggregation Builder -Use the aggregation builder on the left of the page to configure the {ref}search-aggregations-metrics.html[metric] and {ref}search-aggregations-bucket.html[bucket] aggregations used in your +Use the aggregation builder on the left of the page to configure the {ref}search-aggregations-metrics.html[metric] and {ref}search-aggregations-bucket.html[bucket] aggregations used in your visualization. Buckets are analogous to SQL `GROUP BY` statements. For more information on aggregations, see the main {ref}search-aggregations.html[Elasticsearch aggregations reference]. -Bar, line, or area chart visualizations use _metrics_ for the y-axis and _buckets_ are used for the x-axis, segment bar -colors, and row/column splits. For pie charts, use the metric for the slice size and the bucket for the number of +Bar, line, or area chart visualizations use _metrics_ for the y-axis and _buckets_ are used for the x-axis, segment bar +colors, and row/column splits. For pie charts, use the metric for the slice size and the bucket for the number of slices. -Choose the metric aggregation for your visualization's Y axis, such as -{ref}/search-aggregations-metrics-valuecount-aggregation.html[count], -{ref}/search-aggregations-metrics-avg-aggregation.html[average], -{ref}/search-aggregations-metrics-sum-aggregation.html[sum], -{ref}/search-aggregations-metrics-min-aggregation.html[min], -{ref}/search-aggregations-metrics-max-aggregation.html[max], or -{ref}/search-aggregations-metrics-cardinality-aggregation.html[cardinality] -(unique count). Use bucket aggregations for the visualization's X axis, color slices, and row/column splits. Common +Choose the metric aggregation for your visualization's Y axis, such as +{ref}/search-aggregations-metrics-valuecount-aggregation.html[count], +{ref}/search-aggregations-metrics-avg-aggregation.html[average], +{ref}/search-aggregations-metrics-sum-aggregation.html[sum], +{ref}/search-aggregations-metrics-min-aggregation.html[min], +{ref}/search-aggregations-metrics-max-aggregation.html[max], or +{ref}/search-aggregations-metrics-cardinality-aggregation.html[cardinality] +(unique count). Use bucket aggregations for the visualization's X axis, color slices, and row/column splits. Common bucket aggregations include date histogram, range, terms, filters, and significant terms. -You can set the order in which buckets execute. In Elasticsearch, the first aggregation determines the data set -for any subsequent aggregations. The following example involves a date bar chart of Web page hits for the top 5 file +You can set the order in which buckets execute. In Elasticsearch, the first aggregation determines the data set +for any subsequent aggregations. The following example involves a date bar chart of Web page hits for the top 5 file extensions. To use the same extension across all hits, set this order: @@ -123,7 +123,7 @@ To chart the top 5 extensions for each hour, use the following order: 1. *X-Axis:* Date bar chart of `@timestamp` (with 1 hour interval) 2. *Color:* Terms aggregation of extensions -For these requests, Elasticsearch creates a date bar chart from all the records, then groups the top five extensions +For these requests, Elasticsearch creates a date bar chart from all the records, then groups the top five extensions inside each bucket, which in this example is a one-hour interval. NOTE: Remember, each subsequent bucket slices the data from the previous bucket. @@ -131,8 +131,8 @@ NOTE: Remember, each subsequent bucket slices the data from the previous bucket. To render the visualization on the _preview canvas_, click the *Apply Changes* button at the top right of the Aggregation Builder. -You can learn more about aggregation and how altering the order of aggregations affects your visualizations -https://www.elastic.co/blog/kibana-aggregation-execution-order-and-you[here]. +You can learn more about aggregation and how altering the order of aggregations affects your visualizations +https://www.elastic.co/blog/kibana-aggregation-execution-order-and-you[here]. [float] [[visualize-filters]] @@ -142,7 +142,7 @@ include::filter-pinning.asciidoc[] [[preview-canvas]] ===== Preview Canvas -The preview canvas displays a preview of the visualization you've defined in the aggregation builder. To refresh the +The preview canvas displays a preview of the visualization you've defined in the aggregation builder. To refresh the visualization preview, clicking the *Apply Changes* image:images/apply-changes-button.png[] button on the toolbar. include::area.asciidoc[] diff --git a/docs/x-axis-aggs.asciidoc b/docs/x-axis-aggs.asciidoc index 3b9fd3e9486235..0c8fd2aa2fabaa 100644 --- a/docs/x-axis-aggs.asciidoc +++ b/docs/x-axis-aggs.asciidoc @@ -1,43 +1,43 @@ -The X axis of this chart is the _buckets_ axis. You can define buckets for the X axis, for a split area on the +The X axis of this chart is the _buckets_ axis. You can define buckets for the X axis, for a split area on the chart, or for split charts. This chart's X axis supports the following aggregations. Click the linked name of each aggregation to visit the main Elasticsearch documentation for that aggregation. -*Date Histogram*:: A {ref}search-aggregations-bucket-datehistogram-aggregation.html[_date histogram_] is built from a -numeric field and organized by date. You can specify a time frame for the intervals in seconds, minutes, hours, days, -weeks, months, or years. You can also specify a custom interval frame by selecting *Custom* as the interval and -specifying a number and a time unit in the text field. Custom interval time units are *s* for seconds, *m* for minutes, -*h* for hours, *d* for days, *w* for weeks, and *y* for years. Different units support different levels of precision, +*Date Histogram*:: A {ref}search-aggregations-bucket-datehistogram-aggregation.html[_date histogram_] is built from a +numeric field and organized by date. You can specify a time frame for the intervals in seconds, minutes, hours, days, +weeks, months, or years. You can also specify a custom interval frame by selecting *Custom* as the interval and +specifying a number and a time unit in the text field. Custom interval time units are *s* for seconds, *m* for minutes, +*h* for hours, *d* for days, *w* for weeks, and *y* for years. Different units support different levels of precision, down to one second. -*Histogram*:: A standard {ref}search-aggregations-bucket-histogram-aggregation.html[_histogram_] is built from a -numeric field. Specify an integer interval for this field. Select the *Show empty buckets* checkbox to include empty +*Histogram*:: A standard {ref}search-aggregations-bucket-histogram-aggregation.html[_histogram_] is built from a +numeric field. Specify an integer interval for this field. Select the *Show empty buckets* checkbox to include empty intervals in the histogram. -*Range*:: With a {ref}/search-aggregations-bucket-range-aggregation.html[_range_] aggregation, you can specify ranges -of values for a numeric field. Click *Add Range* to add a set of range endpoints. Click the red *(x)* symbol to remove +*Range*:: With a {ref}/search-aggregations-bucket-range-aggregation.html[_range_] aggregation, you can specify ranges +of values for a numeric field. Click *Add Range* to add a set of range endpoints. Click the red *(x)* symbol to remove a range. -*Date Range*:: A {ref}search-aggregations-bucket-daterange-aggregation.html[_date range_] aggregation reports values -that are within a range of dates that you specify. You can specify the ranges for the dates using -{ref}common-options.html#date-math[_date math_] expressions. Click *Add Range* to add a set of range endpoints. +*Date Range*:: A {ref}search-aggregations-bucket-daterange-aggregation.html[_date range_] aggregation reports values +that are within a range of dates that you specify. You can specify the ranges for the dates using +{ref}common-options.html#date-math[_date math_] expressions. Click *Add Range* to add a set of range endpoints. Click the red *(/)* symbol to remove a range. *IPv4 Range*:: The {ref}search-aggregations-bucket-iprange-aggregation.html[_IPv4 range_] aggregation enables you to -specify ranges of IPv4 addresses. Click *Add Range* to add a set of range endpoints. Click the red *(/)* symbol to +specify ranges of IPv4 addresses. Click *Add Range* to add a set of range endpoints. Click the red *(/)* symbol to remove a range. -*Terms*:: A {ref}search-aggregations-bucket-terms-aggregation.html[_terms_] aggregation enables you to specify the top +*Terms*:: A {ref}search-aggregations-bucket-terms-aggregation.html[_terms_] aggregation enables you to specify the top or bottom _n_ elements of a given field to display, ordered by count or a custom metric. -*Filters*:: You can specify a set of {ref}/search-aggregations-bucket-filters-aggregation.html[_filters_] for the data. -You can specify a filter as a query string or in JSON format, just as in the Discover search bar. Click *Add Filter* to -add another filter. Click the image:images/labelbutton.png[Label button icon] *label* button to open the label field, where +*Filters*:: You can specify a set of {ref}/search-aggregations-bucket-filters-aggregation.html[_filters_] for the data. +You can specify a filter as a query string or in JSON format, just as in the Discover search bar. Click *Add Filter* to +add another filter. Click the image:images/labelbutton.png[Label button icon] *label* button to open the label field, where you can type in a name to display on the visualization. -*Significant Terms*:: Displays the results of the experimental +*Significant Terms*:: Displays the results of the experimental {ref}/search-aggregations-bucket-significantterms-aggregation.html[_significant terms_] aggregation. -Once you've specified an X axis aggregation, you can define sub-aggregations to refine the visualization. Click *+ Add +Once you've specified an X axis aggregation, you can define sub-aggregations to refine the visualization. Click *+ Add Sub Aggregation* to define a sub-aggregation, then choose *Split Area* or *Split Chart*, then select a sub-aggregation from the list of types. -When multiple aggregations are defined on a chart's axis, you can use the up or down arrows to the right of the +When multiple aggregations are defined on a chart's axis, you can use the up or down arrows to the right of the aggregation's type to change the aggregation's priority. Enter a string in the *Custom Label* field to change the display label. diff --git a/docs/y-axis-aggs.asciidoc b/docs/y-axis-aggs.asciidoc index f404abb42dbbb4..397e167de8ffe8 100644 --- a/docs/y-axis-aggs.asciidoc +++ b/docs/y-axis-aggs.asciidoc @@ -1,23 +1,23 @@ -*Count*:: The {ref}/search-aggregations-metrics-valuecount-aggregation.html[_count_] aggregation returns a raw count of +*Count*:: The {ref}/search-aggregations-metrics-valuecount-aggregation.html[_count_] aggregation returns a raw count of the elements in the selected index pattern. -*Average*:: This aggregation returns the {ref}/search-aggregations-metrics-avg-aggregation.html[_average_] of a numeric +*Average*:: This aggregation returns the {ref}/search-aggregations-metrics-avg-aggregation.html[_average_] of a numeric field. Select a field from the drop-down. -*Sum*:: The {ref}/search-aggregations-metrics-sum-aggregation.html[_sum_] aggregation returns the total sum of a numeric +*Sum*:: The {ref}/search-aggregations-metrics-sum-aggregation.html[_sum_] aggregation returns the total sum of a numeric field. Select a field from the drop-down. -*Min*:: The {ref}/search-aggregations-metrics-min-aggregation.html[_min_] aggregation returns the minimum value of a +*Min*:: The {ref}/search-aggregations-metrics-min-aggregation.html[_min_] aggregation returns the minimum value of a numeric field. Select a field from the drop-down. -*Max*:: The {ref}/search-aggregations-metrics-max-aggregation.html[_max_] aggregation returns the maximum value of a +*Max*:: The {ref}/search-aggregations-metrics-max-aggregation.html[_max_] aggregation returns the maximum value of a numeric field. Select a field from the drop-down. -*Unique Count*:: The {ref}/search-aggregations-metrics-cardinality-aggregation.html[_cardinality_] aggregation returns +*Unique Count*:: The {ref}/search-aggregations-metrics-cardinality-aggregation.html[_cardinality_] aggregation returns the number of unique values in a field. Select a field from the drop-down. -*Standard Deviation*:: The {ref}/search-aggregations-metrics-extendedstats-aggregation.html[_extended stats_] +*Standard Deviation*:: The {ref}/search-aggregations-metrics-extendedstats-aggregation.html[_extended stats_] aggregation returns the standard deviation of data in a numeric field. Select a field from the drop-down. -*Percentiles*:: The {ref}/search-aggregations-metrics-percentile-aggregation.html[_percentile_] aggregation divides the -values in a numeric field into percentile bands that you specify. Select a field from the drop-down, then specify one -or more ranges in the *Percentiles* fields. Click the *X* to remove a percentile field. Click *+ Add* to add a +*Percentiles*:: The {ref}/search-aggregations-metrics-percentile-aggregation.html[_percentile_] aggregation divides the +values in a numeric field into percentile bands that you specify. Select a field from the drop-down, then specify one +or more ranges in the *Percentiles* fields. Click the *X* to remove a percentile field. Click *+ Add* to add a percentile field. -*Percentile Rank*:: The {ref}/search-aggregations-metrics-percentile-rank-aggregation.html[_percentile ranks_] -aggregation returns the percentile rankings for the values in the numeric field you specify. Select a numeric field +*Percentile Rank*:: The {ref}/search-aggregations-metrics-percentile-rank-aggregation.html[_percentile ranks_] +aggregation returns the percentile rankings for the values in the numeric field you specify. Select a numeric field from the drop-down, then specify one or more percentile rank values in the *Values* fields. Click the *X* to remove a values field. Click *+Add* to add a values field. From 9a0dca27397ff855dbe7f546058411df225d1136 Mon Sep 17 00:00:00 2001 From: Paul Echeverri Date: Wed, 6 Jul 2016 14:36:35 -0700 Subject: [PATCH 57/67] Fixes broken link in Getting Started Fixes #7655 --- docs/getting-started.asciidoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/getting-started.asciidoc b/docs/getting-started.asciidoc index fbda773c72be47..8173b465dee4c5 100644 --- a/docs/getting-started.asciidoc +++ b/docs/getting-started.asciidoc @@ -28,7 +28,7 @@ The tutorials in this section rely on the following data sets: * The complete works of William Shakespeare, suitably parsed into fields. Download this data set by clicking here: https://www.elastic.co/guide/en/kibana/3.0/snippets/shakespeare.json[shakespeare.json]. * A set of fictitious accounts with randomly generated data, in CSV format. Download this data set by clicking here: - https://www.github.com/elastic/kibana/docs/tutorial/accounts.csv[accounts.csv] + https://github.com/elastic/kibana/tree/master/docs/tutorial/accounts.csv[accounts.csv] * A set of randomly generated log files. Download this data set by clicking here: https://download.elastic.co/demos/kibana/gettingstarted/logs.jsonl.gz[logs.jsonl.gz] From 70858b06daa088459ff6823f389f85748f1dfa85 Mon Sep 17 00:00:00 2001 From: Paul Echeverri Date: Wed, 6 Jul 2016 14:46:38 -0700 Subject: [PATCH 58/67] Updated link to raw Fixes #7655 --- docs/getting-started.asciidoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/getting-started.asciidoc b/docs/getting-started.asciidoc index 8173b465dee4c5..7d392862b6718d 100644 --- a/docs/getting-started.asciidoc +++ b/docs/getting-started.asciidoc @@ -28,7 +28,7 @@ The tutorials in this section rely on the following data sets: * The complete works of William Shakespeare, suitably parsed into fields. Download this data set by clicking here: https://www.elastic.co/guide/en/kibana/3.0/snippets/shakespeare.json[shakespeare.json]. * A set of fictitious accounts with randomly generated data, in CSV format. Download this data set by clicking here: - https://github.com/elastic/kibana/tree/master/docs/tutorial/accounts.csv[accounts.csv] + https://raw.githubusercontent.com/elastic/kibana/master/docs/tutorial/accounts.csv[accounts.csv] * A set of randomly generated log files. Download this data set by clicking here: https://download.elastic.co/demos/kibana/gettingstarted/logs.jsonl.gz[logs.jsonl.gz] From 2953d84c6f280ea403e04bc120443f9214e982b8 Mon Sep 17 00:00:00 2001 From: LeeDr Date: Wed, 6 Jul 2016 17:14:12 -0500 Subject: [PATCH 59/67] change other headerPage to header --- test/support/page_objects/visualize_page.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/support/page_objects/visualize_page.js b/test/support/page_objects/visualize_page.js index 294dfe54f2c07e..22c0a91824b9b1 100644 --- a/test/support/page_objects/visualize_page.js +++ b/test/support/page_objects/visualize_page.js @@ -280,7 +280,7 @@ export default class VisualizePage { .findByCssSelector('.btn-success') .click() .then(function () { - return PageObjects.headerPage.getSpinnerDone(); + return PageObjects.header.getSpinnerDone(); }); } @@ -317,7 +317,7 @@ export default class VisualizePage { .click(); }) .then(function () { - return PageObjects.headerPage.getSpinnerDone(); + return PageObjects.header.getSpinnerDone(); }) // verify that green message at the top of the page. // it's only there for about 5 seconds From cf990292f02650fa1d57eb98b2b78abbaea5af7f Mon Sep 17 00:00:00 2001 From: Shaunak Kashyap Date: Wed, 6 Jul 2016 15:20:26 -0700 Subject: [PATCH 60/67] Fixing tests --- .../__tests__/kbn_top_nav_controller.js | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/src/ui/public/kbn_top_nav/__tests__/kbn_top_nav_controller.js b/src/ui/public/kbn_top_nav/__tests__/kbn_top_nav_controller.js index 301e53a2fb750f..f72f13f2cf083a 100644 --- a/src/ui/public/kbn_top_nav/__tests__/kbn_top_nav_controller.js +++ b/src/ui/public/kbn_top_nav/__tests__/kbn_top_nav_controller.js @@ -64,15 +64,13 @@ describe('KbnTopNavController', function () { }); describe('hideButton:', function () { - it('defaults to function that returns false', function () { + it('defaults to false', function () { const controller = new KbnTopNavController([ { key: 'foo' }, { key: '1234' }, ]); - pluck(controller.opts, 'hideButton').forEach(f => { - expect(f()).to.be(false); - }); + expect(pluck(controller.opts, 'hideButton')).to.eql([false, false]); }); it('excludes opts from opts when true', function () { @@ -86,28 +84,24 @@ describe('KbnTopNavController', function () { }); describe('disableButton:', function () { - it('defaults to function that returns false', function () { + it('defaults to false', function () { const controller = new KbnTopNavController([ { key: 'foo' }, { key: '1234' }, ]); - pluck(controller.opts, 'disableButton').forEach(f => { - expect(f()).to.be(false); - }); + expect(pluck(controller.opts, 'disableButton')).to.eql([false, false]); }); }); describe('tooltip:', function () { - it('defaults to function that returns empty string', function () { + it('defaults to empty string', function () { const controller = new KbnTopNavController([ { key: 'foo' }, { key: '1234' }, ]); - pluck(controller.opts, 'tooltip').forEach(f => { - expect(f()).to.be(''); - }); + expect(pluck(controller.opts, 'tooltip')).to.eql(['', '']); }); }); From c36fd12348f0e4a1ff1dc01672dd008ebc12a4a8 Mon Sep 17 00:00:00 2001 From: Shaunak Kashyap Date: Wed, 6 Jul 2016 16:03:32 -0700 Subject: [PATCH 61/67] Using Object.assign instead of _.defaults --- src/ui/public/kbn_top_nav/kbn_top_nav_controller.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/ui/public/kbn_top_nav/kbn_top_nav_controller.js b/src/ui/public/kbn_top_nav/kbn_top_nav_controller.js index 60a57595b6828c..9446639eaaea0f 100644 --- a/src/ui/public/kbn_top_nav/kbn_top_nav_controller.js +++ b/src/ui/public/kbn_top_nav/kbn_top_nav_controller.js @@ -1,4 +1,4 @@ -import { defaults, capitalize, isArray, isFunction, result } from 'lodash'; +import { capitalize, isArray, isFunction, result } from 'lodash'; import uiModules from 'ui/modules'; import filterTemplate from 'ui/chrome/config/filter.html'; @@ -53,12 +53,12 @@ export default function ($compile) { // apply the defaults to individual options _applyOptDefault(opt = {}) { - const defaultedOpt = defaults({}, opt, { + const defaultedOpt = Object.assign({ label: capitalize(opt.key), hasFunction: !!opt.run, description: opt.run ? opt.key : `Toggle ${opt.key} view`, run: (item) => !item.disableButton && this.toggle(item.key) - }); + }, opt); defaultedOpt.hideButton = result(opt, 'hideButton', false); defaultedOpt.disableButton = result(opt, 'disableButton', false); From a2d34b3a9dc5dccbe69003eeed3afe38cb3d4527 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20Bevacqua?= Date: Thu, 7 Jul 2016 14:52:47 -0300 Subject: [PATCH 62/67] [test] Refactor according to @cjcenizal's suggestions. --- src/ui/settings/__tests__/index.js | 50 +++++++++++++++--------------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/src/ui/settings/__tests__/index.js b/src/ui/settings/__tests__/index.js index 19fad9002fcb07..7ec95e416744a5 100644 --- a/src/ui/settings/__tests__/index.js +++ b/src/ui/settings/__tests__/index.js @@ -8,15 +8,15 @@ describe('ui settings', function () { describe('overview', function () { it('has expected api surface', function () { const { uiSettings } = instantiate(); - expect(typeof uiSettings.get).to.be('function'); - expect(typeof uiSettings.getAll).to.be('function'); - expect(typeof uiSettings.getDefaults).to.be('function'); - expect(typeof uiSettings.getRaw).to.be('function'); - expect(typeof uiSettings.getUserProvided).to.be('function'); - expect(typeof uiSettings.remove).to.be('function'); - expect(typeof uiSettings.removeMany).to.be('function'); - expect(typeof uiSettings.set).to.be('function'); - expect(typeof uiSettings.setMany).to.be('function'); + expect(typeof uiSettings.get).to.equal('function'); + expect(typeof uiSettings.getAll).to.equal('function'); + expect(typeof uiSettings.getDefaults).to.equal('function'); + expect(typeof uiSettings.getRaw).to.equal('function'); + expect(typeof uiSettings.getUserProvided).to.equal('function'); + expect(typeof uiSettings.remove).to.equal('function'); + expect(typeof uiSettings.removeMany).to.equal('function'); + expect(typeof uiSettings.set).to.equal('function'); + expect(typeof uiSettings.setMany).to.equal('function'); }); }); @@ -104,7 +104,7 @@ describe('ui settings', function () { it('is promised the default values', async function () { const { server, uiSettings, configGet } = instantiate(); const defaults = await uiSettings.getDefaults(); - expect(isEqual(defaults, defaultsProvider())).to.be.ok(); + expect(isEqual(defaults, defaultsProvider())).to.be.true(); }); }); @@ -122,7 +122,7 @@ describe('ui settings', function () { const result = await uiSettings.getUserProvided(); expect(isEqual(result, { user: { userValue: 'customized' } - })).to.be.ok(); + })).to.be.true(); }); it('ignores null user configuration (because default values)', async function () { @@ -131,7 +131,7 @@ describe('ui settings', function () { const result = await uiSettings.getUserProvided(); expect(isEqual(result, { user: { userValue: 'customized' }, something: { userValue: 'else' } - })).to.be.ok(); + })).to.be.true(); }); }); @@ -147,7 +147,7 @@ describe('ui settings', function () { const getResult = {}; const { server, uiSettings, configGet } = instantiate({ getResult }); const result = await uiSettings.getRaw(); - expect(isEqual(result, defaultsProvider())).to.be.ok(); + expect(isEqual(result, defaultsProvider())).to.be.true(); }); it(`user configuration gets merged with defaults`, async function () { @@ -156,7 +156,7 @@ describe('ui settings', function () { const result = await uiSettings.getRaw(); const merged = defaultsProvider(); merged.foo = { userValue: 'bar' }; - expect(isEqual(result, merged)).to.be.ok(); + expect(isEqual(result, merged)).to.be.true(); }); it(`user configuration gets merged into defaults`, async function () { @@ -165,7 +165,7 @@ describe('ui settings', function () { const result = await uiSettings.getRaw(); const merged = defaultsProvider(); merged.dateFormat.userValue = 'YYYY-MM-DD'; - expect(isEqual(result, merged)).to.be.ok(); + expect(isEqual(result, merged)).to.be.true(); }); }); @@ -186,7 +186,7 @@ describe('ui settings', function () { Object.keys(defaults).forEach(key => { expectation[key] = defaults[key].value; }); - expect(isEqual(result, expectation)).to.be.ok(); + expect(isEqual(result, expectation)).to.be.true(); }); it(`returns key value pairs including user configuration`, async function () { @@ -199,7 +199,7 @@ describe('ui settings', function () { expectation[key] = defaults[key].value; }); expectation.something = 'user-provided'; - expect(isEqual(result, expectation)).to.be.ok(); + expect(isEqual(result, expectation)).to.be.true(); }); it(`returns key value pairs including user configuration for existing settings`, async function () { @@ -212,7 +212,7 @@ describe('ui settings', function () { expectation[key] = defaults[key].value; }); expectation.dateFormat = 'YYYY-MM-DD'; - expect(isEqual(result, expectation)).to.be.ok(); + expect(isEqual(result, expectation)).to.be.true(); }); }); @@ -229,42 +229,42 @@ describe('ui settings', function () { const { server, uiSettings, configGet } = instantiate({ getResult }); const result = await uiSettings.get('dateFormat'); const defaults = defaultsProvider(); - expect(isEqual(result, defaults.dateFormat.value)).to.be.ok(); + expect(result).to.equal(defaults.dateFormat.value); }); it(`returns the user-configured value for a custom key`, async function () { const getResult = { custom: 'value' }; const { server, uiSettings, configGet } = instantiate({ getResult }); const result = await uiSettings.get('custom'); - expect(isEqual(result, 'value')).to.be.ok(); + expect(result).to.equal('value'); }); it(`returns the user-configured value for a modified key`, async function () { const getResult = { dateFormat: 'YYYY-MM-DD' }; const { server, uiSettings, configGet } = instantiate({ getResult }); const result = await uiSettings.get('dateFormat'); - expect(isEqual(result, 'YYYY-MM-DD')).to.be.ok(); + expect(result).to.equal('YYYY-MM-DD'); }); }); }); function expectElasticsearchGetQuery(server, configGet) { - expect(isEqual(server.plugins.elasticsearch.client.get.callCount, 1)).to.be.ok(); + expect(server.plugins.elasticsearch.client.get.callCount).to.equal(1); expect(isEqual(server.plugins.elasticsearch.client.get.firstCall.args, [{ index: configGet('kibana.index'), id: configGet('pkg.version'), type: 'config' - }])).to.be.ok(); + }])).to.be.true(); } function expectElasticsearchUpdateQuery(server, configGet, doc) { - expect(isEqual(server.plugins.elasticsearch.client.update.callCount, 1)).to.be.ok(); + expect(server.plugins.elasticsearch.client.update.callCount).to.equal(1); expect(isEqual(server.plugins.elasticsearch.client.update.firstCall.args, [{ index: configGet('kibana.index'), id: configGet('pkg.version'), type: 'config', body: { doc } - }])).to.be.ok(); + }])).to.be.true(); } function instantiate({ getResult } = {}) { From 9133c3ff03c6605cea3fe44101ce1656e6a8fd8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20Bevacqua?= Date: Thu, 7 Jul 2016 15:20:16 -0300 Subject: [PATCH 63/67] [test] To equal true. --- src/ui/settings/__tests__/index.js | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/ui/settings/__tests__/index.js b/src/ui/settings/__tests__/index.js index 7ec95e416744a5..2e09f4c00a4e92 100644 --- a/src/ui/settings/__tests__/index.js +++ b/src/ui/settings/__tests__/index.js @@ -104,7 +104,7 @@ describe('ui settings', function () { it('is promised the default values', async function () { const { server, uiSettings, configGet } = instantiate(); const defaults = await uiSettings.getDefaults(); - expect(isEqual(defaults, defaultsProvider())).to.be.true(); + expect(isEqual(defaults, defaultsProvider())).to.equal(true); }); }); @@ -122,7 +122,7 @@ describe('ui settings', function () { const result = await uiSettings.getUserProvided(); expect(isEqual(result, { user: { userValue: 'customized' } - })).to.be.true(); + })).to.equal(true); }); it('ignores null user configuration (because default values)', async function () { @@ -131,7 +131,7 @@ describe('ui settings', function () { const result = await uiSettings.getUserProvided(); expect(isEqual(result, { user: { userValue: 'customized' }, something: { userValue: 'else' } - })).to.be.true(); + })).to.equal(true); }); }); @@ -147,7 +147,7 @@ describe('ui settings', function () { const getResult = {}; const { server, uiSettings, configGet } = instantiate({ getResult }); const result = await uiSettings.getRaw(); - expect(isEqual(result, defaultsProvider())).to.be.true(); + expect(isEqual(result, defaultsProvider())).to.equal(true); }); it(`user configuration gets merged with defaults`, async function () { @@ -156,7 +156,7 @@ describe('ui settings', function () { const result = await uiSettings.getRaw(); const merged = defaultsProvider(); merged.foo = { userValue: 'bar' }; - expect(isEqual(result, merged)).to.be.true(); + expect(isEqual(result, merged)).to.equal(true); }); it(`user configuration gets merged into defaults`, async function () { @@ -165,7 +165,7 @@ describe('ui settings', function () { const result = await uiSettings.getRaw(); const merged = defaultsProvider(); merged.dateFormat.userValue = 'YYYY-MM-DD'; - expect(isEqual(result, merged)).to.be.true(); + expect(isEqual(result, merged)).to.equal(true); }); }); @@ -186,7 +186,7 @@ describe('ui settings', function () { Object.keys(defaults).forEach(key => { expectation[key] = defaults[key].value; }); - expect(isEqual(result, expectation)).to.be.true(); + expect(isEqual(result, expectation)).to.equal(true); }); it(`returns key value pairs including user configuration`, async function () { @@ -199,7 +199,7 @@ describe('ui settings', function () { expectation[key] = defaults[key].value; }); expectation.something = 'user-provided'; - expect(isEqual(result, expectation)).to.be.true(); + expect(isEqual(result, expectation)).to.equal(true); }); it(`returns key value pairs including user configuration for existing settings`, async function () { @@ -212,7 +212,7 @@ describe('ui settings', function () { expectation[key] = defaults[key].value; }); expectation.dateFormat = 'YYYY-MM-DD'; - expect(isEqual(result, expectation)).to.be.true(); + expect(isEqual(result, expectation)).to.equal(true); }); }); @@ -254,7 +254,7 @@ function expectElasticsearchGetQuery(server, configGet) { index: configGet('kibana.index'), id: configGet('pkg.version'), type: 'config' - }])).to.be.true(); + }])).to.equal(true); } function expectElasticsearchUpdateQuery(server, configGet, doc) { @@ -264,7 +264,7 @@ function expectElasticsearchUpdateQuery(server, configGet, doc) { id: configGet('pkg.version'), type: 'config', body: { doc } - }])).to.be.true(); + }])).to.equal(true); } function instantiate({ getResult } = {}) { From b54ef4ed0010a61c56f9d07dfa6792bab970746c Mon Sep 17 00:00:00 2001 From: Jonathan Budzenski Date: Mon, 20 Jun 2016 10:27:36 -0500 Subject: [PATCH 64/67] [build] Ensure group kibana is added, stricter user creation --- tasks/build/package_scripts/post_install.sh | 42 +++++++++++++++------ tasks/build/package_scripts/post_remove.sh | 24 +++++------- 2 files changed, 41 insertions(+), 25 deletions(-) diff --git a/tasks/build/package_scripts/post_install.sh b/tasks/build/package_scripts/post_install.sh index bc31f19f19953d..dd8638a0c20bfb 100644 --- a/tasks/build/package_scripts/post_install.sh +++ b/tasks/build/package_scripts/post_install.sh @@ -1,19 +1,39 @@ #!/bin/sh set -e -user_check() { - getent passwd "$1" > /dev/null 2>&1 -} +case $1 in + # Debian + configure) + if ! getent group "<%= group %>" >/dev/null; then + addgroup --quiet --system "<%= group %>" + fi -user_create() { - # Create a system user. A system user is one within the system uid range and - # has no expiration - useradd -r "$1" -} + if ! getent passwd "<%= user %>" >/dev/null; then + adduser --quiet --system --no-create-home --disabled-password \ + --ingroup "<%= group %>" --shell /bin/false "<%= user %>" + fi + ;; + abort-deconfigure|abort-upgrade|abort-remove) + ;; + + # Red Hat + 1|2) + if ! getent group "<%= group %>" >/dev/null; then + groupadd -r "<%= group %>" + fi + + if ! getent passwd "<%= user %>" >/dev/null; then + useradd -r -g "<%= group %>" -M -s /sbin/nologin \ + -c "kibana service user" "<%= user %>" + fi + ;; + + *) + echo "post install script called with unknown argument \`$1'" >&2 + exit 1 + ;; +esac -if ! user_check "<%= user %>" ; then - user_create "<%= user %>" -fi chown -R <%= user %>:<%= group %> <%= optimizeDir %> chown <%= user %>:<%= group %> <%= dataDir %> chown <%= user %>:<%= group %> <%= pluginsDir %> diff --git a/tasks/build/package_scripts/post_remove.sh b/tasks/build/package_scripts/post_remove.sh index c1499c2940d837..fa248c016feb3f 100644 --- a/tasks/build/package_scripts/post_remove.sh +++ b/tasks/build/package_scripts/post_remove.sh @@ -1,22 +1,14 @@ #!/bin/sh set -e -user_check() { - getent passwd "$1" > /dev/null 2>&1 -} - -user_remove() { - userdel "$1" -} - -REMOVE_USER=false +REMOVE_USER_AND_GROUP=false REMOVE_DIRS=false case $1 in # Includes cases for all valid arguments, exit 1 otherwise # Debian purge) - REMOVE_USER=true + REMOVE_USER_AND_GROUP=true REMOVE_DIRS=true ;; remove) @@ -28,7 +20,7 @@ case $1 in # Red Hat 0) - REMOVE_USER=true + REMOVE_USER_AND_GROUP=true REMOVE_DIRS=true ;; @@ -41,9 +33,13 @@ case $1 in ;; esac -if [ "$REMOVE_USER" = "true" ]; then - if user_check "<%= user %>" ; then - user_remove "<%= user %>" +if [ "$REMOVE_USER_AND_GROUP" = "true" ]; then + if getent group "<%= group %>" >/dev/null; then + groupdel "<%= group %>" + fi + + if getent passwd "<%= user %>" >/dev/null; then + userdel "<%= user %>" fi fi From 144a40b7803df4ea3e9792bf29ec436d56935f40 Mon Sep 17 00:00:00 2001 From: Jonathan Budzenski Date: Thu, 7 Jul 2016 15:40:18 -0500 Subject: [PATCH 65/67] [build] Remove user before group --- tasks/build/package_scripts/post_remove.sh | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tasks/build/package_scripts/post_remove.sh b/tasks/build/package_scripts/post_remove.sh index fa248c016feb3f..25c46a54a222a0 100644 --- a/tasks/build/package_scripts/post_remove.sh +++ b/tasks/build/package_scripts/post_remove.sh @@ -34,13 +34,13 @@ case $1 in esac if [ "$REMOVE_USER_AND_GROUP" = "true" ]; then - if getent group "<%= group %>" >/dev/null; then - groupdel "<%= group %>" - fi - if getent passwd "<%= user %>" >/dev/null; then userdel "<%= user %>" fi + + if getent group "<%= group %>" >/dev/null; then + groupdel "<%= group %>" + fi fi if [ "$REMOVE_DIRS" = "true" ]; then From 9eba210d4b77abc289df7573165a669462e469ad Mon Sep 17 00:00:00 2001 From: CJ Cenizal Date: Tue, 28 Jun 2016 11:38:29 -0700 Subject: [PATCH 66/67] Remove Bootstrap carousel and glyphicons. --- src/ui/public/styles/bootstrap/bootstrap.less | 2 - src/ui/public/styles/bootstrap/carousel.less | 270 ---------------- .../public/styles/bootstrap/glyphicons.less | 305 ------------------ src/ui/public/styles/bootstrap/variables.less | 28 -- .../fonts/glyphicons-halflings-regular.eot | Bin 20127 -> 0 bytes .../fonts/glyphicons-halflings-regular.svg | 288 ----------------- .../fonts/glyphicons-halflings-regular.ttf | Bin 45404 -> 0 bytes .../fonts/glyphicons-halflings-regular.woff | Bin 23424 -> 0 bytes .../fonts/glyphicons-halflings-regular.woff2 | Bin 18028 -> 0 bytes src/ui/public/styles/theme/bootstrap.less | 2 - .../styles/variables/bootstrap-mods.less | 22 -- 11 files changed, 917 deletions(-) delete mode 100644 src/ui/public/styles/bootstrap/carousel.less delete mode 100644 src/ui/public/styles/bootstrap/glyphicons.less delete mode 100644 src/ui/public/styles/fonts/glyphicons-halflings-regular.eot delete mode 100644 src/ui/public/styles/fonts/glyphicons-halflings-regular.svg delete mode 100644 src/ui/public/styles/fonts/glyphicons-halflings-regular.ttf delete mode 100644 src/ui/public/styles/fonts/glyphicons-halflings-regular.woff delete mode 100644 src/ui/public/styles/fonts/glyphicons-halflings-regular.woff2 diff --git a/src/ui/public/styles/bootstrap/bootstrap.less b/src/ui/public/styles/bootstrap/bootstrap.less index 1c0477805fa93f..5c387233edc3b3 100644 --- a/src/ui/public/styles/bootstrap/bootstrap.less +++ b/src/ui/public/styles/bootstrap/bootstrap.less @@ -11,7 +11,6 @@ // Reset and dependencies @import "normalize.less"; @import "print.less"; -@import "glyphicons.less"; // Core CSS @import "scaffolding.less"; @@ -49,7 +48,6 @@ @import "modals.less"; @import "tooltip.less"; @import "popovers.less"; -@import "carousel.less"; // Utility classes @import "utilities.less"; diff --git a/src/ui/public/styles/bootstrap/carousel.less b/src/ui/public/styles/bootstrap/carousel.less deleted file mode 100644 index 252011e9e25081..00000000000000 --- a/src/ui/public/styles/bootstrap/carousel.less +++ /dev/null @@ -1,270 +0,0 @@ -// -// Carousel -// -------------------------------------------------- - - -// Wrapper for the slide container and indicators -.carousel { - position: relative; -} - -.carousel-inner { - position: relative; - overflow: hidden; - width: 100%; - - > .item { - display: none; - position: relative; - .transition(.6s ease-in-out left); - - // Account for jankitude on images - > img, - > a > img { - &:extend(.img-responsive); - line-height: 1; - } - - // WebKit CSS3 transforms for supported devices - @media all and (transform-3d), (-webkit-transform-3d) { - .transition-transform(~'0.6s ease-in-out'); - .backface-visibility(~'hidden'); - .perspective(1000px); - - &.next, - &.active.right { - .translate3d(100%, 0, 0); - left: 0; - } - &.prev, - &.active.left { - .translate3d(-100%, 0, 0); - left: 0; - } - &.next.left, - &.prev.right, - &.active { - .translate3d(0, 0, 0); - left: 0; - } - } - } - - > .active, - > .next, - > .prev { - display: block; - } - - > .active { - left: 0; - } - - > .next, - > .prev { - position: absolute; - top: 0; - width: 100%; - } - - > .next { - left: 100%; - } - > .prev { - left: -100%; - } - > .next.left, - > .prev.right { - left: 0; - } - - > .active.left { - left: -100%; - } - > .active.right { - left: 100%; - } - -} - -// Left/right controls for nav -// --------------------------- - -.carousel-control { - position: absolute; - top: 0; - left: 0; - bottom: 0; - width: @carousel-control-width; - .opacity(@carousel-control-opacity); - font-size: @carousel-control-font-size; - color: @carousel-control-color; - text-align: center; - text-shadow: @carousel-text-shadow; - background-color: rgba(0, 0, 0, 0); // Fix IE9 click-thru bug - // We can't have this transition here because WebKit cancels the carousel - // animation if you trip this while in the middle of another animation. - - // Set gradients for backgrounds - &.left { - #gradient > .horizontal(@start-color: rgba(0,0,0,.5); @end-color: rgba(0,0,0,.0001)); - } - &.right { - left: auto; - right: 0; - #gradient > .horizontal(@start-color: rgba(0,0,0,.0001); @end-color: rgba(0,0,0,.5)); - } - - // Hover/focus state - &:hover, - &:focus { - outline: 0; - color: @carousel-control-color; - text-decoration: none; - .opacity(.9); - } - - // Toggles - .icon-prev, - .icon-next, - .glyphicon-chevron-left, - .glyphicon-chevron-right { - position: absolute; - top: 50%; - margin-top: -10px; - z-index: 5; - display: inline-block; - } - .icon-prev, - .glyphicon-chevron-left { - left: 50%; - margin-left: -10px; - } - .icon-next, - .glyphicon-chevron-right { - right: 50%; - margin-right: -10px; - } - .icon-prev, - .icon-next { - width: 20px; - height: 20px; - line-height: 1; - font-family: serif; - } - - - .icon-prev { - &:before { - content: '\2039';// SINGLE LEFT-POINTING ANGLE QUOTATION MARK (U+2039) - } - } - .icon-next { - &:before { - content: '\203a';// SINGLE RIGHT-POINTING ANGLE QUOTATION MARK (U+203A) - } - } -} - -// Optional indicator pips -// -// Add an unordered list with the following class and add a list item for each -// slide your carousel holds. - -.carousel-indicators { - position: absolute; - bottom: 10px; - left: 50%; - z-index: 15; - width: 60%; - margin-left: -30%; - padding-left: 0; - list-style: none; - text-align: center; - - li { - display: inline-block; - width: 10px; - height: 10px; - margin: 1px; - text-indent: -999px; - border: 1px solid @carousel-indicator-border-color; - border-radius: 10px; - cursor: pointer; - - // IE8-9 hack for event handling - // - // Internet Explorer 8-9 does not support clicks on elements without a set - // `background-color`. We cannot use `filter` since that's not viewed as a - // background color by the browser. Thus, a hack is needed. - // See https://developer.mozilla.org/en-US/docs/Web/Events/click#Internet_Explorer - // - // For IE8, we set solid black as it doesn't support `rgba()`. For IE9, we - // set alpha transparency for the best results possible. - background-color: #000 \9; // IE8 - background-color: rgba(0,0,0,0); // IE9 - } - .active { - margin: 0; - width: 12px; - height: 12px; - background-color: @carousel-indicator-active-bg; - } -} - -// Optional captions -// ----------------------------- -// Hidden by default for smaller viewports -.carousel-caption { - position: absolute; - left: 15%; - right: 15%; - bottom: 20px; - z-index: 10; - padding-top: 20px; - padding-bottom: 20px; - color: @carousel-caption-color; - text-align: center; - text-shadow: @carousel-text-shadow; - & .btn { - text-shadow: none; // No shadow for button elements in carousel-caption - } -} - - -// Scale up controls for tablets and up -@media screen and (min-width: @screen-sm-min) { - - // Scale up the controls a smidge - .carousel-control { - .glyphicon-chevron-left, - .glyphicon-chevron-right, - .icon-prev, - .icon-next { - width: (@carousel-control-font-size * 1.5); - height: (@carousel-control-font-size * 1.5); - margin-top: (@carousel-control-font-size / -2); - font-size: (@carousel-control-font-size * 1.5); - } - .glyphicon-chevron-left, - .icon-prev { - margin-left: (@carousel-control-font-size / -2); - } - .glyphicon-chevron-right, - .icon-next { - margin-right: (@carousel-control-font-size / -2); - } - } - - // Show and left align the captions - .carousel-caption { - left: 20%; - right: 20%; - padding-bottom: 30px; - } - - // Move up the indicators - .carousel-indicators { - bottom: 20px; - } -} diff --git a/src/ui/public/styles/bootstrap/glyphicons.less b/src/ui/public/styles/bootstrap/glyphicons.less deleted file mode 100644 index 7bc5852d2c07fa..00000000000000 --- a/src/ui/public/styles/bootstrap/glyphicons.less +++ /dev/null @@ -1,305 +0,0 @@ -// -// Glyphicons for Bootstrap -// -// Since icons are fonts, they can be placed anywhere text is placed and are -// thus automatically sized to match the surrounding child. To use, create an -// inline element with the appropriate classes, like so: -// -// Star - -// Import the fonts -@font-face { - font-family: 'Glyphicons Halflings'; - src: url('@{icon-font-path}@{icon-font-name}.eot'); - src: url('@{icon-font-path}@{icon-font-name}.eot?#iefix') format('embedded-opentype'), - url('@{icon-font-path}@{icon-font-name}.woff2') format('woff2'), - url('@{icon-font-path}@{icon-font-name}.woff') format('woff'), - url('@{icon-font-path}@{icon-font-name}.ttf') format('truetype'), - url('@{icon-font-path}@{icon-font-name}.svg#@{icon-font-svg-id}') format('svg'); -} - -// Catchall baseclass -.glyphicon { - position: relative; - top: 1px; - display: inline-block; - font-family: 'Glyphicons Halflings'; - font-style: normal; - font-weight: normal; - line-height: 1; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; -} - -// Individual icons -.glyphicon-asterisk { &:before { content: "\002a"; } } -.glyphicon-plus { &:before { content: "\002b"; } } -.glyphicon-euro, -.glyphicon-eur { &:before { content: "\20ac"; } } -.glyphicon-minus { &:before { content: "\2212"; } } -.glyphicon-cloud { &:before { content: "\2601"; } } -.glyphicon-envelope { &:before { content: "\2709"; } } -.glyphicon-pencil { &:before { content: "\270f"; } } -.glyphicon-glass { &:before { content: "\e001"; } } -.glyphicon-music { &:before { content: "\e002"; } } -.glyphicon-search { &:before { content: "\e003"; } } -.glyphicon-heart { &:before { content: "\e005"; } } -.glyphicon-star { &:before { content: "\e006"; } } -.glyphicon-star-empty { &:before { content: "\e007"; } } -.glyphicon-user { &:before { content: "\e008"; } } -.glyphicon-film { &:before { content: "\e009"; } } -.glyphicon-th-large { &:before { content: "\e010"; } } -.glyphicon-th { &:before { content: "\e011"; } } -.glyphicon-th-list { &:before { content: "\e012"; } } -.glyphicon-ok { &:before { content: "\e013"; } } -.glyphicon-remove { &:before { content: "\e014"; } } -.glyphicon-zoom-in { &:before { content: "\e015"; } } -.glyphicon-zoom-out { &:before { content: "\e016"; } } -.glyphicon-off { &:before { content: "\e017"; } } -.glyphicon-signal { &:before { content: "\e018"; } } -.glyphicon-cog { &:before { content: "\e019"; } } -.glyphicon-trash { &:before { content: "\e020"; } } -.glyphicon-home { &:before { content: "\e021"; } } -.glyphicon-file { &:before { content: "\e022"; } } -.glyphicon-time { &:before { content: "\e023"; } } -.glyphicon-road { &:before { content: "\e024"; } } -.glyphicon-download-alt { &:before { content: "\e025"; } } -.glyphicon-download { &:before { content: "\e026"; } } -.glyphicon-upload { &:before { content: "\e027"; } } -.glyphicon-inbox { &:before { content: "\e028"; } } -.glyphicon-play-circle { &:before { content: "\e029"; } } -.glyphicon-repeat { &:before { content: "\e030"; } } -.glyphicon-refresh { &:before { content: "\e031"; } } -.glyphicon-list-alt { &:before { content: "\e032"; } } -.glyphicon-lock { &:before { content: "\e033"; } } -.glyphicon-flag { &:before { content: "\e034"; } } -.glyphicon-headphones { &:before { content: "\e035"; } } -.glyphicon-volume-off { &:before { content: "\e036"; } } -.glyphicon-volume-down { &:before { content: "\e037"; } } -.glyphicon-volume-up { &:before { content: "\e038"; } } -.glyphicon-qrcode { &:before { content: "\e039"; } } -.glyphicon-barcode { &:before { content: "\e040"; } } -.glyphicon-tag { &:before { content: "\e041"; } } -.glyphicon-tags { &:before { content: "\e042"; } } -.glyphicon-book { &:before { content: "\e043"; } } -.glyphicon-bookmark { &:before { content: "\e044"; } } -.glyphicon-print { &:before { content: "\e045"; } } -.glyphicon-camera { &:before { content: "\e046"; } } -.glyphicon-font { &:before { content: "\e047"; } } -.glyphicon-bold { &:before { content: "\e048"; } } -.glyphicon-italic { &:before { content: "\e049"; } } -.glyphicon-text-height { &:before { content: "\e050"; } } -.glyphicon-text-width { &:before { content: "\e051"; } } -.glyphicon-align-left { &:before { content: "\e052"; } } -.glyphicon-align-center { &:before { content: "\e053"; } } -.glyphicon-align-right { &:before { content: "\e054"; } } -.glyphicon-align-justify { &:before { content: "\e055"; } } -.glyphicon-list { &:before { content: "\e056"; } } -.glyphicon-indent-left { &:before { content: "\e057"; } } -.glyphicon-indent-right { &:before { content: "\e058"; } } -.glyphicon-facetime-video { &:before { content: "\e059"; } } -.glyphicon-picture { &:before { content: "\e060"; } } -.glyphicon-map-marker { &:before { content: "\e062"; } } -.glyphicon-adjust { &:before { content: "\e063"; } } -.glyphicon-tint { &:before { content: "\e064"; } } -.glyphicon-edit { &:before { content: "\e065"; } } -.glyphicon-share { &:before { content: "\e066"; } } -.glyphicon-check { &:before { content: "\e067"; } } -.glyphicon-move { &:before { content: "\e068"; } } -.glyphicon-step-backward { &:before { content: "\e069"; } } -.glyphicon-fast-backward { &:before { content: "\e070"; } } -.glyphicon-backward { &:before { content: "\e071"; } } -.glyphicon-play { &:before { content: "\e072"; } } -.glyphicon-pause { &:before { content: "\e073"; } } -.glyphicon-stop { &:before { content: "\e074"; } } -.glyphicon-forward { &:before { content: "\e075"; } } -.glyphicon-fast-forward { &:before { content: "\e076"; } } -.glyphicon-step-forward { &:before { content: "\e077"; } } -.glyphicon-eject { &:before { content: "\e078"; } } -.glyphicon-chevron-left { &:before { content: "\e079"; } } -.glyphicon-chevron-right { &:before { content: "\e080"; } } -.glyphicon-plus-sign { &:before { content: "\e081"; } } -.glyphicon-minus-sign { &:before { content: "\e082"; } } -.glyphicon-remove-sign { &:before { content: "\e083"; } } -.glyphicon-ok-sign { &:before { content: "\e084"; } } -.glyphicon-question-sign { &:before { content: "\e085"; } } -.glyphicon-info-sign { &:before { content: "\e086"; } } -.glyphicon-screenshot { &:before { content: "\e087"; } } -.glyphicon-remove-circle { &:before { content: "\e088"; } } -.glyphicon-ok-circle { &:before { content: "\e089"; } } -.glyphicon-ban-circle { &:before { content: "\e090"; } } -.glyphicon-arrow-left { &:before { content: "\e091"; } } -.glyphicon-arrow-right { &:before { content: "\e092"; } } -.glyphicon-arrow-up { &:before { content: "\e093"; } } -.glyphicon-arrow-down { &:before { content: "\e094"; } } -.glyphicon-share-alt { &:before { content: "\e095"; } } -.glyphicon-resize-full { &:before { content: "\e096"; } } -.glyphicon-resize-small { &:before { content: "\e097"; } } -.glyphicon-exclamation-sign { &:before { content: "\e101"; } } -.glyphicon-gift { &:before { content: "\e102"; } } -.glyphicon-leaf { &:before { content: "\e103"; } } -.glyphicon-fire { &:before { content: "\e104"; } } -.glyphicon-eye-open { &:before { content: "\e105"; } } -.glyphicon-eye-close { &:before { content: "\e106"; } } -.glyphicon-warning-sign { &:before { content: "\e107"; } } -.glyphicon-plane { &:before { content: "\e108"; } } -.glyphicon-calendar { &:before { content: "\e109"; } } -.glyphicon-random { &:before { content: "\e110"; } } -.glyphicon-comment { &:before { content: "\e111"; } } -.glyphicon-magnet { &:before { content: "\e112"; } } -.glyphicon-chevron-up { &:before { content: "\e113"; } } -.glyphicon-chevron-down { &:before { content: "\e114"; } } -.glyphicon-retweet { &:before { content: "\e115"; } } -.glyphicon-shopping-cart { &:before { content: "\e116"; } } -.glyphicon-folder-close { &:before { content: "\e117"; } } -.glyphicon-folder-open { &:before { content: "\e118"; } } -.glyphicon-resize-vertical { &:before { content: "\e119"; } } -.glyphicon-resize-horizontal { &:before { content: "\e120"; } } -.glyphicon-hdd { &:before { content: "\e121"; } } -.glyphicon-bullhorn { &:before { content: "\e122"; } } -.glyphicon-bell { &:before { content: "\e123"; } } -.glyphicon-certificate { &:before { content: "\e124"; } } -.glyphicon-thumbs-up { &:before { content: "\e125"; } } -.glyphicon-thumbs-down { &:before { content: "\e126"; } } -.glyphicon-hand-right { &:before { content: "\e127"; } } -.glyphicon-hand-left { &:before { content: "\e128"; } } -.glyphicon-hand-up { &:before { content: "\e129"; } } -.glyphicon-hand-down { &:before { content: "\e130"; } } -.glyphicon-circle-arrow-right { &:before { content: "\e131"; } } -.glyphicon-circle-arrow-left { &:before { content: "\e132"; } } -.glyphicon-circle-arrow-up { &:before { content: "\e133"; } } -.glyphicon-circle-arrow-down { &:before { content: "\e134"; } } -.glyphicon-globe { &:before { content: "\e135"; } } -.glyphicon-wrench { &:before { content: "\e136"; } } -.glyphicon-tasks { &:before { content: "\e137"; } } -.glyphicon-filter { &:before { content: "\e138"; } } -.glyphicon-briefcase { &:before { content: "\e139"; } } -.glyphicon-fullscreen { &:before { content: "\e140"; } } -.glyphicon-dashboard { &:before { content: "\e141"; } } -.glyphicon-paperclip { &:before { content: "\e142"; } } -.glyphicon-heart-empty { &:before { content: "\e143"; } } -.glyphicon-link { &:before { content: "\e144"; } } -.glyphicon-phone { &:before { content: "\e145"; } } -.glyphicon-pushpin { &:before { content: "\e146"; } } -.glyphicon-usd { &:before { content: "\e148"; } } -.glyphicon-gbp { &:before { content: "\e149"; } } -.glyphicon-sort { &:before { content: "\e150"; } } -.glyphicon-sort-by-alphabet { &:before { content: "\e151"; } } -.glyphicon-sort-by-alphabet-alt { &:before { content: "\e152"; } } -.glyphicon-sort-by-order { &:before { content: "\e153"; } } -.glyphicon-sort-by-order-alt { &:before { content: "\e154"; } } -.glyphicon-sort-by-attributes { &:before { content: "\e155"; } } -.glyphicon-sort-by-attributes-alt { &:before { content: "\e156"; } } -.glyphicon-unchecked { &:before { content: "\e157"; } } -.glyphicon-expand { &:before { content: "\e158"; } } -.glyphicon-collapse-down { &:before { content: "\e159"; } } -.glyphicon-collapse-up { &:before { content: "\e160"; } } -.glyphicon-log-in { &:before { content: "\e161"; } } -.glyphicon-flash { &:before { content: "\e162"; } } -.glyphicon-log-out { &:before { content: "\e163"; } } -.glyphicon-new-window { &:before { content: "\e164"; } } -.glyphicon-record { &:before { content: "\e165"; } } -.glyphicon-save { &:before { content: "\e166"; } } -.glyphicon-open { &:before { content: "\e167"; } } -.glyphicon-saved { &:before { content: "\e168"; } } -.glyphicon-import { &:before { content: "\e169"; } } -.glyphicon-export { &:before { content: "\e170"; } } -.glyphicon-send { &:before { content: "\e171"; } } -.glyphicon-floppy-disk { &:before { content: "\e172"; } } -.glyphicon-floppy-saved { &:before { content: "\e173"; } } -.glyphicon-floppy-remove { &:before { content: "\e174"; } } -.glyphicon-floppy-save { &:before { content: "\e175"; } } -.glyphicon-floppy-open { &:before { content: "\e176"; } } -.glyphicon-credit-card { &:before { content: "\e177"; } } -.glyphicon-transfer { &:before { content: "\e178"; } } -.glyphicon-cutlery { &:before { content: "\e179"; } } -.glyphicon-header { &:before { content: "\e180"; } } -.glyphicon-compressed { &:before { content: "\e181"; } } -.glyphicon-earphone { &:before { content: "\e182"; } } -.glyphicon-phone-alt { &:before { content: "\e183"; } } -.glyphicon-tower { &:before { content: "\e184"; } } -.glyphicon-stats { &:before { content: "\e185"; } } -.glyphicon-sd-video { &:before { content: "\e186"; } } -.glyphicon-hd-video { &:before { content: "\e187"; } } -.glyphicon-subtitles { &:before { content: "\e188"; } } -.glyphicon-sound-stereo { &:before { content: "\e189"; } } -.glyphicon-sound-dolby { &:before { content: "\e190"; } } -.glyphicon-sound-5-1 { &:before { content: "\e191"; } } -.glyphicon-sound-6-1 { &:before { content: "\e192"; } } -.glyphicon-sound-7-1 { &:before { content: "\e193"; } } -.glyphicon-copyright-mark { &:before { content: "\e194"; } } -.glyphicon-registration-mark { &:before { content: "\e195"; } } -.glyphicon-cloud-download { &:before { content: "\e197"; } } -.glyphicon-cloud-upload { &:before { content: "\e198"; } } -.glyphicon-tree-conifer { &:before { content: "\e199"; } } -.glyphicon-tree-deciduous { &:before { content: "\e200"; } } -.glyphicon-cd { &:before { content: "\e201"; } } -.glyphicon-save-file { &:before { content: "\e202"; } } -.glyphicon-open-file { &:before { content: "\e203"; } } -.glyphicon-level-up { &:before { content: "\e204"; } } -.glyphicon-copy { &:before { content: "\e205"; } } -.glyphicon-paste { &:before { content: "\e206"; } } -// The following 2 Glyphicons are omitted for the time being because -// they currently use Unicode codepoints that are outside the -// Basic Multilingual Plane (BMP). Older buggy versions of WebKit can't handle -// non-BMP codepoints in CSS string escapes, and thus can't display these two icons. -// Notably, the bug affects some older versions of the Android Browser. -// More info: https://github.com/twbs/bootstrap/issues/10106 -// .glyphicon-door { &:before { content: "\1f6aa"; } } -// .glyphicon-key { &:before { content: "\1f511"; } } -.glyphicon-alert { &:before { content: "\e209"; } } -.glyphicon-equalizer { &:before { content: "\e210"; } } -.glyphicon-king { &:before { content: "\e211"; } } -.glyphicon-queen { &:before { content: "\e212"; } } -.glyphicon-pawn { &:before { content: "\e213"; } } -.glyphicon-bishop { &:before { content: "\e214"; } } -.glyphicon-knight { &:before { content: "\e215"; } } -.glyphicon-baby-formula { &:before { content: "\e216"; } } -.glyphicon-tent { &:before { content: "\26fa"; } } -.glyphicon-blackboard { &:before { content: "\e218"; } } -.glyphicon-bed { &:before { content: "\e219"; } } -.glyphicon-apple { &:before { content: "\f8ff"; } } -.glyphicon-erase { &:before { content: "\e221"; } } -.glyphicon-hourglass { &:before { content: "\231b"; } } -.glyphicon-lamp { &:before { content: "\e223"; } } -.glyphicon-duplicate { &:before { content: "\e224"; } } -.glyphicon-piggy-bank { &:before { content: "\e225"; } } -.glyphicon-scissors { &:before { content: "\e226"; } } -.glyphicon-bitcoin { &:before { content: "\e227"; } } -.glyphicon-btc { &:before { content: "\e227"; } } -.glyphicon-xbt { &:before { content: "\e227"; } } -.glyphicon-yen { &:before { content: "\00a5"; } } -.glyphicon-jpy { &:before { content: "\00a5"; } } -.glyphicon-ruble { &:before { content: "\20bd"; } } -.glyphicon-rub { &:before { content: "\20bd"; } } -.glyphicon-scale { &:before { content: "\e230"; } } -.glyphicon-ice-lolly { &:before { content: "\e231"; } } -.glyphicon-ice-lolly-tasted { &:before { content: "\e232"; } } -.glyphicon-education { &:before { content: "\e233"; } } -.glyphicon-option-horizontal { &:before { content: "\e234"; } } -.glyphicon-option-vertical { &:before { content: "\e235"; } } -.glyphicon-menu-hamburger { &:before { content: "\e236"; } } -.glyphicon-modal-window { &:before { content: "\e237"; } } -.glyphicon-oil { &:before { content: "\e238"; } } -.glyphicon-grain { &:before { content: "\e239"; } } -.glyphicon-sunglasses { &:before { content: "\e240"; } } -.glyphicon-text-size { &:before { content: "\e241"; } } -.glyphicon-text-color { &:before { content: "\e242"; } } -.glyphicon-text-background { &:before { content: "\e243"; } } -.glyphicon-object-align-top { &:before { content: "\e244"; } } -.glyphicon-object-align-bottom { &:before { content: "\e245"; } } -.glyphicon-object-align-horizontal{ &:before { content: "\e246"; } } -.glyphicon-object-align-left { &:before { content: "\e247"; } } -.glyphicon-object-align-vertical { &:before { content: "\e248"; } } -.glyphicon-object-align-right { &:before { content: "\e249"; } } -.glyphicon-triangle-right { &:before { content: "\e250"; } } -.glyphicon-triangle-left { &:before { content: "\e251"; } } -.glyphicon-triangle-bottom { &:before { content: "\e252"; } } -.glyphicon-triangle-top { &:before { content: "\e253"; } } -.glyphicon-console { &:before { content: "\e254"; } } -.glyphicon-superscript { &:before { content: "\e255"; } } -.glyphicon-subscript { &:before { content: "\e256"; } } -.glyphicon-menu-left { &:before { content: "\e257"; } } -.glyphicon-menu-right { &:before { content: "\e258"; } } -.glyphicon-menu-down { &:before { content: "\e259"; } } -.glyphicon-menu-up { &:before { content: "\e260"; } } diff --git a/src/ui/public/styles/bootstrap/variables.less b/src/ui/public/styles/bootstrap/variables.less index b057ef5bf907b8..eff13e55404dc5 100644 --- a/src/ui/public/styles/bootstrap/variables.less +++ b/src/ui/public/styles/bootstrap/variables.less @@ -71,18 +71,6 @@ @headings-color: inherit; -//== Iconography -// -//## Specify custom location and filename of the included Glyphicons icon font. Useful for those including Bootstrap via Bower. - -//** Load fonts from this directory. -@icon-font-path: "../fonts/"; -//** File name for all font files. -@icon-font-name: "glyphicons-halflings-regular"; -//** Element ID within SVG icon file. -@icon-font-svg-id: "glyphicons_halflingsregular"; - - //== Components // //## Define common padding and border radius sizes and more. Values based on 14px text and 1.428 line-height (~20px to start). @@ -799,22 +787,6 @@ @breadcrumb-separator: "/"; -//== Carousel -// -//## - -@carousel-text-shadow: 0 1px 2px rgba(0,0,0,.6); - -@carousel-control-color: #fff; -@carousel-control-width: 15%; -@carousel-control-opacity: .5; -@carousel-control-font-size: 20px; - -@carousel-indicator-active-bg: #fff; -@carousel-indicator-border-color: #fff; - -@carousel-caption-color: #fff; - //== Close // diff --git a/src/ui/public/styles/fonts/glyphicons-halflings-regular.eot b/src/ui/public/styles/fonts/glyphicons-halflings-regular.eot deleted file mode 100644 index b93a4953fff68df523aa7656497ee339d6026d64..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 20127 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 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/ui/public/styles/fonts/glyphicons-halflings-regular.ttf b/src/ui/public/styles/fonts/glyphicons-halflings-regular.ttf deleted file mode 100644 index 1413fc609ab6f21774de0cb7e01360095584f65b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 45404 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 G`u_lkUM?>H diff --git a/src/ui/public/styles/fonts/glyphicons-halflings-regular.woff2 b/src/ui/public/styles/fonts/glyphicons-halflings-regular.woff2 deleted file mode 100644 index 64539b54c3751a6d9adb44c8e3a45ba5a73b77f0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 18028 zcmV(~K+nH-Pew8T0RR9107h&84*&oF0I^&E07eM_0Rl|`00000000000000000000 z0000#Mn+Uk92y`7U;vDA2m}!b3WBL5f#qcZHUcCAhI9*rFaQJ~1&1OBl~F%;WnyLq z8)b|&?3j;$^FW}&KmNW53flIFARDZ7_Wz%hpoWaWlgHTHEHf()GI0&dMi#DFPaEt6 zCO)z0v0~C~q&0zBj^;=tv8q{$8JxX)>_`b}WQGgXi46R*CHJ}6r+;}OrvwA{_SY+o zK)H-vy{l!P`+NG*`*x6^PGgHH4!dsolgU4RKj@I8Xz~F6o?quCX&=VQ$Q{w01;M0? zKe|5r<_7CD z=eO3*x!r$aX2iFh3;}xNfx0v;SwBfGG+@Z;->HhvqfF4r__4$mU>Dl_1w;-9`~5rF~@!3;r~xP-hZvOfOx)A z#>8O3N{L{naf215f>m=bzbp7_(ssu&cx)Qo-{)!)Yz3A@Z0uZaM2yJ8#OGlzm?JO5gbrj~@)NB4@?>KE(K-$w}{};@dKY#K3+Vi64S<@!Z{(I{7l=!p9 z&kjG^P~0f46i13(w!hEDJga;*Eb z`!n|++@H8VaKG<9>VDh(y89J#=;Z$ei=GnD5TesW#|Wf)^D+9NKN4J3H5PF_t=V+Z zdeo8*h9+8&Zfc?>>1|E4B7MAx)^uy$L>szyXre7W|81fjy+RZ1>Gd}@@${~PCOXo) z$#HZd3)V3@lNGG%(3PyIbvyJTOJAWcN@Uh!FqUkx^&BuAvc)G}0~SKI`8ZZXw$*xP zum-ZdtPciTAUn$XWb6vrS=JX~f5?M%9S(=QsdYP?K%Odn0S0-Ad<-tBtS3W06I^FK z8}d2eR_n!(uK~APZ-#tl@SycxkRJ@5wmypdWV{MFtYBUY#g-Vv?5AEBj1 z`$T^tRKca*sn7gt%s@XUD-t>bij-4q-ilku9^;QJ3Mpc`HJ_EX4TGGQ-Og)`c~qm51<|gp7D@ zp#>Grssv^#A)&M8>ulnDM_5t#Al`#jaFpZ<#YJ@>!a$w@kEZ1<@PGs#L~kxOSz7jj zEhb?;W)eS}0IQQuk4~JT30>4rFJ3!b+77}>$_>v#2FFEnN^%(ls*o80pv0Q>#t#%H z@`Yy-FXQ9ULKh{Up&oA_A4B!(x^9&>i`+T|eD!&QOLVd(_avv-bFX~4^>o{%mzzrg_i~SBnr%DeE|i+^}|8?kaV(Z32{`vA^l!sp15>Z72z52FgXf z^8ZITvJ9eXBT1~iQjW|Q`Fac^ak$^N-vI^*geh5|*CdMz;n16gV_zk|Z7q8tFfCvU zJK^Pptnn0Rc~egGIAK}uv99VZm2WLPezQQ5K<`f zg{8Ll|GioPYfNheMj-7-S87=w4N0WxHP`1V6Y)0M&SkYzVrwp>yfsEF7wj&T0!}dB z)R~gGfP9pOR;GY_e0~K^^oJ-3AT+m~?Al!{>>5gNe17?OWz)$)sMH*xuQiB>FT2{i zQ>6U_8}Ay~r4li;jzG+$&?S12{)+<*k9 z<^SX#xY|jvlvTxt(m~C7{y{3g>7TX#o2q$xQO|fc<%8rE@A3=UW(o?gVg?gDV!0q6O!{MlX$6-Bu_m&0ms66 znWS&zr{O_4O&{2uCLQvA?xC5vGZ}KV1v6)#oTewgIMSnBur0PtM0&{R5t#UEy3I9) z`LVP?3f;o}sz*7g5qdTxJl^gk3>;8%SOPH@B)rmFOJ)m6?PlYa$y=RX%;}KId{m9R#2=LNwosF@OTivgMqxpRGe}5=LtAn?VVl6VWCFLD z7l#^^H8jY~42hR)OoVF#YDW(md!g(&pJ;yMj|UBAQa}UH?ED@%ci=*(q~Opn>kE2Q z_4Kgf|0kEA6ary41A;)^Ku(*nirvP!Y>{FZYBLXLP6QL~vRL+uMlZ?jWukMV*(dsn zL~~KA@jU)(UeoOz^4Gkw{fJsYQ%|UA7i79qO5=DOPBcWlv%pK!A+)*F`3WJ}t9FU3 zXhC4xMV7Z%5RjDs0=&vC4WdvD?Zi5tg4@xg8-GLUI>N$N&3aS4bHrp%3_1u9wqL)i z)XQLsI&{Hd&bQE!3m&D0vd!4D`l1$rt_{3NS?~lj#|$GN5RmvP(j3hzJOk=+0B*2v z)Bw133RMUM%wu_+$vbzOy?yk#kvR?xGsg-ipX4wKyXqd zROKp5))>tNy$HByaEHK%$mqd>-{Yoj`oSBK;w>+eZ&TVcj^DyXjo{DDbZ>vS2cCWB z(6&~GZ}kUdN(*2-nI!hvbnVy@z2E#F394OZD&Jb04}`Tgaj?MoY?1`{ejE2iud51% zQ~J0sijw(hqr_Ckbj@pm$FAVASKY(D4BS0GYPkSMqSDONRaFH+O2+jL{hIltJSJT~e)TNDr(}=Xt7|UhcU9eoXl&QZRR<9WomW%&m)FT~j zTgGd3-j}Uk%CRD;$@X)NNV9+RJbifYu>yr{FkO;p>_&njI> zyBHh_72bW;8}oGeY0gpHOxiV597j7mY<#?WMmkf5x~Kfk*re(&tG_mX<3&2cON*2u%V29tsXUv{#-ijs2>EuNH-x3) zPBpi+V6gI=wn}u164_j8xi-y(B?Au2o;UO=r6&)i5S3Mx*)*{_;u}~i4dh$`VgUS- zMG6t*?DXDYX0D2Oj31MI!HF>|aG8rjrOPnxHu4wZl;!=NGjjDoBpXf?ntrwt^dqxm zs(lE@*QB3NH)!`rH)5kks-D89g@UX&@DU9jvrsY)aI=9b4nPy3bfdX_U;#?zsan{G>DKob2LnhCJv8o}duQK)qP{7iaaf2=K`a-VNcfC582d4a z>sBJA*%S|NEazDxXcGPW_uZ&d7xG`~JB!U>U(}acUSn=FqOA~(pn^!aMXRnqiL0;? zebEZYouRv}-0r;Dq&z9>s#Rt1HL`0p4bB)A&sMyn|rE_9nh z?NO*RrjET8D4s(-`nS{MrdYtv*kyCnJKbsftG2D#ia@;42!8xd?a3P(&Y?vCf9na< zQ&Ni*1Qel&Xq{Z?=%f0SRqQt5m|Myg+8T=GDc)@^};=tM>9IDr7hdvE9-M@@<0pqv45xZTeNecbL- zWFQt4t`9>j8~X%lz}%We>Kzh_=`XO}!;4!OWH?=p*DOs#Nt({k^IvtBEL~Qafn)I^ zm*k{y7_bIs9YE}0B6%r`EIUH8US+MGY!KQA1fi-jCx9*}oz2k1nBsXp;4K<_&SN}}w<)!EylI_)v7}3&c)V;Cfuj*eJ2yc8LK=vugqTL><#65r6%#2e| zdYzZ)9Uq7)A$ol&ynM!|RDHc_7?FlWqjW>8TIHc`jExt)f5W|;D%GC#$u!%B*S%Z0 zsj&;bIU2jrt_7%$=!h4Q29n*A^^AI8R|stsW%O@?i+pN0YOU`z;TVuPy!N#~F8Z29 zzZh1`FU(q31wa>kmw{$q=MY>XBprL<1)Py~5TW4mgY%rg$S=4C^0qr+*A^T)Q)Q-U zGgRb9%MdE-&i#X3xW=I`%xDzAG95!RG9)s?v_5+qx`7NdkQ)If5}BoEp~h}XoeK>kweAMxJ8tehagx~;Nr_WP?jXa zJ&j7%Ef3w*XWf?V*nR)|IOMrX;$*$e23m?QN` zk>sC^GE=h6?*Cr~596s_QE@>Nnr?{EU+_^G=LZr#V&0fEXQ3IWtrM{=t^qJ62Sp=e zrrc>bzX^6yFV!^v7;>J9>j;`qHDQ4uc92eVe6nO@c>H=ouLQot``E~KLNqMqJ7(G+?GWO9Ol+q$w z!^kMv!n{vF?RqLnxVk{a_Ar;^sw0@=+~6!4&;SCh^utT=I zo&$CwvhNOjQpenw2`5*a6Gos6cs~*TD`8H9P4=#jOU_`%L!W;$57NjN%4 z39(61ZC#s7^tv`_4j}wMRT9rgDo*XtZwN-L;Qc$6v8kKkhmRrxSDkUAzGPgJ?}~_t zkwoGS4=6lsD`=RL|8L3O9L()N)lmEn-M15fRC{dhZ}7eYV%O-R^gsAp{q4 z!C1}_T8gy^v@SZ5R&Li5JMJy+K8iZw3LOGA0pN1~y@w7RRl#F()ii6Y5mr~Mdy@Kz z@FT4cm^I&#Fu_9IX(HAFP{XLbRALqm&)>m_we>a`hfv?eE|t z?YdDp2yAhj-~vuw^wzVDuj%w?exOcOT(ls(F*ceCe(C5HlN{lcQ;}|mRPqFDqLEzw zR7ldY+M6xe$$qLwekmk{Z&5cME$gpC?-8)f0m$rqaS|mj9ATNJvvyCgs(f2{r;2E!oy$k5{jik#(;S>do<#m0wVcU<}>)VtYmF9O0%(C>GDzPgh6X z9OkQLMR~y7=|MtaU!LDPPY7O)L{X#SC+M|v^X2CZ?$GS>U_|aC(VA(mIvCNk+biD| zSpj>gd(v>_Cbq>~-x^Y3o|?eHmuC?E&z>;Ij`%{$Pm$hI}bl0Kd`9KD~AchY+goL1?igDxf$qxL9< z4sW@sD)nwWr`T>e2B8MQN|p*DVTT8)3(%AZ&D|@Zh6`cJFT4G^y6`(UdPLY-&bJYJ z*L06f2~BX9qX}u)nrpmHPG#La#tiZ23<>`R@u8k;ueM6 znuSTY7>XEc+I-(VvL?Y>)adHo(cZ;1I7QP^q%hu#M{BEd8&mG_!EWR7ZV_&EGO;d(hGGJzX|tqyYEg2-m0zLT}a{COi$9!?9yK zGN7&yP$a|0gL`dPUt=4d^}?zrLN?HfKP0_gdRvb}1D73Hx!tXq>7{DWPV;^X{-)cm zFa^H5oBDL3uLkaFDWgFF@HL6Bt+_^g~*o*t`Hgy3M?nHhWvTp^|AQDc9_H< zg>IaSMzd7c(Sey;1SespO=8YUUArZaCc~}}tZZX80w%)fNpMExki-qB+;8xVX@dr; z#L52S6*aM-_$P9xFuIui;dN#qZ_MYy^C^hrY;YAMg;K`!ZpKKFc z9feHsool)`tFSS}Su|cL0%F;h!lpR+ym|P>kE-O`3QnHbJ%gJ$dQ_HPTT~>6WNX41 zoDEUpX-g&Hh&GP3koF4##?q*MX1K`@=W6(Gxm1=2Tb{hn8{sJyhQBoq}S>bZT zisRz-xDBYoYxt6--g2M1yh{#QWFCISux}4==r|7+fYdS$%DZ zXVQu{yPO<)Hn=TK`E@;l!09aY{!TMbT)H-l!(l{0j=SEj@JwW0a_h-2F0MZNpyucb zPPb+4&j?a!6ZnPTB>$t`(XSf-}`&+#rI#`GB> zl=$3HORwccTnA2%>$Nmz)u7j%_ywoGri1UXVNRxSf(<@vDLKKxFo;5pTI$R~a|-sQ zd5Rfwj+$k1t0{J`qOL^q>vZUHc7a^`cKKVa{66z?wMuQAfdZBaVVv@-wamPmes$d! z>gv^xx<0jXOz;7HIQS z4RBIFD?7{o^IQ=sNQ-k!ao*+V*|-^I2=UF?{d>bE9avsWbAs{sRE-y`7r zxVAKA9amvo4T}ZAHSF-{y1GqUHlDp4DO9I3mz5h8n|}P-9nKD|$r9AS3gbF1AX=2B zyaK3TbKYqv%~JHKQH8v+%zQ8UVEGDZY|mb>Oe3JD_Z{+Pq%HB+J1s*y6JOlk`6~H) zKt)YMZ*RkbU!GPHzJltmW-=6zqO=5;S)jz{ zFSx?ryqSMxgx|Nhv3z#kFBTuTBHsViaOHs5e&vXZ@l@mVI37<+^KvTE51!pB4Tggq zz!NlRY2ZLno0&6bA|KHPYOMY;;LZG&_lzuLy{@i$&B(}_*~Zk2 z>bkQ7u&Ww%CFh{aqkT{HCbPbRX&EvPRp=}WKmyHc>S_-qbwAr0<20vEoJ(!?-ucjE zKQ+nSlRL^VnOX0h+WcjGb6WI(8;7bsMaHXDb6ynPoOXMlf9nLKre;w*#E_whR#5!! z!^%_+X3eJVKc$fMZP;+xP$~e(CIP1R&{2m+iTQhDoC8Yl@kLM=Wily_cu>7C1wjVU z-^~I0P06ZSNVaN~A`#cSBH2L&tk6R%dU1(u1XdAx;g+5S^Hn9-L$v@p7CCF&PqV{Z?R$}4EJi36+u2JP7l(@fYfP!=e#76LGy^f>~vs0%s*x@X8`|5 zGd6JOHsQ=feES4Vo8%1P_7F5qjiIm#oRT0kO1(?Z_Dk6oX&j=Xd8Klk(;gk3S(ZFnc^8Gc=d;8O-R9tlGyp=2I@1teAZpGWUi;}`n zbJOS_Z2L16nVtDnPpMn{+wR9&yU9~C<-ncppPee`>@1k7hTl5Fn_3_KzQ)u{iJPp3 z)df?Xo%9ta%(dp@DhKuQj4D8=_!*ra#Ib&OXKrsYvAG%H7Kq|43WbayvsbeeimSa= z8~{7ya9ZUAIgLLPeuNmSB&#-`Je0Lja)M$}I41KHb7dQq$wgwX+EElNxBgyyLbA2* z=c1VJR%EPJEw(7!UE?4w@94{pI3E%(acEYd8*Wmr^R7|IM2RZ-RVXSkXy-8$!(iB* zQA`qh2Ze!EY6}Zs7vRz&nr|L60NlIgnO3L*Yz2k2Ivfen?drnVzzu3)1V&-t5S~S? zw#=Sdh>K@2vA25su*@>npw&7A%|Uh9T1jR$mV*H@)pU0&2#Se`7iJlOr$mp79`DKM z5vr*XLrg7w6lc4&S{So1KGKBqcuJ!E|HVFB?vTOjQHi)g+FwJqX@Y3q(qa#6T@3{q zhc@2T-W}XD9x4u+LCdce$*}x!Sc#+rH-sCz6j}0EE`Tk*irUq)y^za`}^1gFnF)C!yf_l_}I<6qfbT$Gc&Eyr?!QwJR~RE4!gKVmqjbI+I^*^ z&hz^7r-dgm@Mbfc#{JTH&^6sJCZt-NTpChB^fzQ}?etydyf~+)!d%V$0faN(f`rJb zm_YaJZ@>Fg>Ay2&bzTx3w^u-lsulc{mX4-nH*A(32O&b^EWmSuk{#HJk}_ULC}SB(L7`YAs>opp9o5UcnB^kVB*rmW6{s0&~_>J!_#+cEWib@v-Ms`?!&=3fDot`oH9v&$f<52>{n2l* z1FRzJ#yQbTHO}}wt0!y8Eh-0*|Um3vjX-nWH>`JN5tWB_gnW%; zUJ0V?_a#+!=>ahhrbGvmvObe8=v1uI8#gNHJ#>RwxL>E^pT05Br8+$@a9aDC1~$@* zicSQCbQcr=DCHM*?G7Hsovk|{$3oIwvymi#YoXeVfWj{Gd#XmnDgzQPRUKNAAI44y z{1WG&rhIR4ipmvBmq$BZ*5tmPIZmhhWgq|TcuR{6lA)+vhj(cH`0;+B^72{&a7ff* zkrIo|pd-Yxm+VVptC@QNCDk0=Re%Sz%ta7y{5Dn9(EapBS0r zLbDKeZepar5%cAcb<^;m>1{QhMzRmRem=+0I3ERot-)gb`i|sII^A#^Gz+x>TW5A& z3PQcpM$lDy`zb%1yf!e8&_>D02RN950KzW>GN6n@2so&Wu09x@PB=&IkIf|zZ1W}P zAKf*&Mo5@@G=w&290aG1@3=IMCB^|G4L7*xn;r3v&HBrD4D)Zg+)f~Ls$7*P-^i#B z4X7ac=0&58j^@2EBZCs}YPe3rqgLAA1L3Y}o?}$%u~)7Rk=LLFbAdSy@-Uw6lv?0K z&P@@M`o2Rll3GoYjotf@WNNjHbe|R?IKVn*?Rzf9v9QoFMq)ODF~>L}26@z`KA82t z43e!^z&WGqAk$Ww8j6bc3$I|;5^BHwt`?e)zf|&+l#!8uJV_Cwy-n1yS0^Q{W*a8B zTzTYL>tt&I&9vzGQUrO?YIm6C1r>eyh|qw~-&;7s7u1achP$K3VnXd8sV8J7ZTxTh z5+^*J5%_#X)XL2@>h(Gmv$@)fZ@ikR$v(2Rax89xscFEi!3_;ORI0dBxw)S{r50qf zg&_a*>2Xe{s@)7OX9O!C?^6fD8tc3bQTq9}fxhbx2@QeaO9Ej+2m!u~+u%Q6?Tgz{ zjYS}bleKcVhW~1$?t*AO^p!=Xkkgwx6OTik*R3~yg^L`wUU9Dq#$Z*iW%?s6pO_f8 zJ8w#u#Eaw7=8n{zJ}C>w{enA6XYHfUf7h)!Qaev)?V=yW{b@-z`hAz;I7^|DoFChP z1aYQnkGauh*ps6x*_S77@z1wwGmF8ky9fMbM$dr*`vsot4uvqWn)0vTRwJqH#&D%g zL3(0dP>%Oj&vm5Re%>*4x|h1J2X*mK5BH1?Nx_#7( zepgF`+n)rHXj!RiipusEq!X81;QQBXlTvLDj=Qub(ha&D=BDx3@-V*d!D9PeXUY?l zwZ0<4=iY!sUj4G>zTS+eYX7knN-8Oynl=NdwHS*nSz_5}*5LQ@=?Yr?uj$`C1m2OR zK`f5SD2|;=BhU#AmaTKe9QaSHQ_DUj1*cUPa*JICFt1<&S3P3zsrs^yUE;tx=x^cmW!Jq!+hohv_B> zPDMT0D&08dC4x@cTD$o1$x%So1Ir(G3_AVQMvQ13un~sP(cEWi$2%5q93E7t{3VJf%K? zuwSyDke~7KuB2?*#DV8YzJw z&}SCDexnUPD!%4|y~7}VzvJ4ch)WT4%sw@ItwoNt(C*RP)h?&~^g##vnhR0!HvIYx z0td2yz9=>t3JNySl*TszmfH6`Ir;ft@RdWs3}!J88UE|gj_GMQ6$ZYphUL2~4OY7} zB*33_bjkRf_@l;Y!7MIdb~bVe;-m78Pz|pdy=O*3kjak63UnLt!{^!!Ljg0rJD3a~ z1Q;y5Z^MF<=Hr}rdoz>yRczx+p3RxxgJE2GX&Si)14B@2t21j4hnnP#U?T3g#+{W+Zb z5s^@>->~-}4|_*!5pIzMCEp|3+i1XKcfUxW`8|ezAh>y{WiRcjSG*asw6;Ef(k#>V ztguN?EGkV_mGFdq!n#W)<7E}1#EZN8O$O|}qdoE|7K?F4zo1jL-v}E8v?9qz(d$&2 zMwyK&xlC9rXo_2xw7Qe0caC?o?Pc*-QAOE!+UvRuKjG+;dk|jQhDDBe?`XT7Y5lte zqSu0t5`;>Wv%|nhj|ZiE^IqA_lZu7OWh!2Y(627zb=r7Ends}wVk7Q5o09a@ojhH7 zU0m&h*8+j4e|OqWyJ&B`V`y=>MVO;K9=hk^6EsmVAGkLT{oUtR{JqSRY{Qi{kKw1k z6s;0SMPJOLp!som|A`*q3t0wIj-=bG8a#MC)MHcMSQU98Juv$?$CvYX)(n`P^!`5| zv3q@@|G@6wMqh;d;m4qvdibx2Yjml}vG9mDv&!0ne02M#D`Bo}xIB0VWh8>>WtNZQ z$&ISlJX;*ORQIO;k62qA{^6P%3!Z=Y1EbmY02{w^yB$`;%!{kur&XTGDiO2cjA)lr zsY^XZWy^DSAaz;kZ_VG?uWnJR7qdN18$~)>(kOoybY0~QYu9||K#|$Mby{3GduV~N zk9H7$7=RSo+?CUYF502`b76ytBy}sFak&|HIwRvB=0D|S`c#QCJPq zP)uOWI)#(n&{6|C4A^G~%B~BY21aOMoz9RuuM`Ip%oBz+NoAlb7?#`E^}7xXo!4S? zFg8I~G%!@nXi8&aJSGFcZAxQf;0m}942=i#p-&teLvE{AKm7Sl2f}Io?!IqbC|J;h z`=5LFOnU5?^w~SV@YwNZx$k_(kLNxZDE z3cf08^-rIT_>A$}B%IJBPcN^)4;90BQtiEi!gT#+EqyAUZ|}*b_}R>SGloq&6?opL zuT_+lwQMgg6!Cso$BwUA;k-1NcrzyE>(_X$B0HocjY~=Pk~Q08+N}(|%HjO_i+*=o z%G6C6A30Ch<0UlG;Zdj@ed!rfUY_i9mYwK8(aYuzcUzlTJ1yPz|Bb-9b33A9zRhGl>Ny-Q#JAq-+qtI@B@&w z$;PJbyiW=!py@g2hAi0)U1v=;avka`gd@8LC4=BEbNqL&K^UAQ5%r95#x%^qRB%KLaqMnG|6xKAm}sx!Qwo}J=2C;NROi$mfADui4)y(3wVA3k~{j^_5%H)C6K zlYAm1eY**HZOj($)xfKIQFtIVw$4&yvz9>(Crs>Gh{ zya6-FG7Dgi92#K)64=9Csj5?Zqe~_9TwSI!2quAwa1w-*uC5!}xY`?tltb0Hq740< zsq2QelPveZ4chr$=~U3!+c&>xyfvA1`)owOqj=i4wjY=A1577Gwg&Ko7;?il9r|_* z8P&IDV_g2D{in5OLFxsO!kx3AhO$5aKeoM|!q|VokqMlYM@HtsRuMtBY%I35#5$+G zpp|JOeoj^U=95HLemB04Yqv{a8X<^K9G2`&ShM_6&Bi1n?o?@MXsDj9Z*A3>#XK%J zRc*&SlFl>l)9DyRQ{*%Z+^e1XpH?0@vhpXrnPPU*d%vOhKkimm-u3c%Q^v3RKp9kx@A2dS?QfS=iigGr7m><)YkV=%LA5h@Uj@9=~ABPMJ z1UE;F&;Ttg5Kc^Qy!1SuvbNEqdgu3*l`=>s5_}dUv$B%BJbMiWrrMm7OXOdi=GOmh zZBvXXK7VqO&zojI2Om9};zCB5i|<210I{iwiGznGCx=FT89=Ef)5!lB1cZ6lbzgDn07*he}G&w7m!;|E(L-?+cz@0<9ZI~LqYQE7>HnPA436}oeN2Y(VfG6 zxNZuMK3Crm^Z_AFeHc~CVRrSl0W^?+Gbteu1g8NGYa3(8f*P{(ZT>%!jtSl6WbYVv zmE(37t0C8vJ6O-5+o*lL9XRcFbd~GSBGbGh3~R!67g&l)7n!kJlWd)~TUyXus#!&G6sR%(l(h1$xyrR5j_jM1zj#giA&@(Xl26@n<9>folx!92bQ z24h570+<)4!$!IQ(5yOU|4_E6aN@4v0+{Kx~Z z;q7fp%0cHziuI%!kB~w}g9@V+1wDz0wFlzX2UOvOy|&;e;t!lAR8tV2KQHgtfk8Uf zw;rs!(4JPODERk4ckd5I2Vq|0rd@@Mwd8MID%0^fITjYIQom^q;qhP8@|eJx{?5xX zc1@Fj*kDknlk{c-rnCloQ3hGh7OU+@efO3>fkRMcM>J?AeVP& zlfzX%cdp=N+4S#E*%^=BQ+N`A7C}|k%$|QUn0yI6S3$MS-NjO!4hm55uyju)Q6e!} z*OVO@A#-mfC9Pha6ng((Xl^V7{d+&u+yx)_B1{~t7d5e8L^i4J>;x<7@5;+l7-Gge zf#9diXJ$&v^rbN5V(ee%q0xBMEgS6%qZm7hNUP%G;^J44I!BmI@M*+FWz0!+s;+iQ zU4CuI+27bvNK8v>?7PZnVxB=heJ&_ymE0nN^W#-rqB%+JXkYGDuRw>JM_LdtLkiq* z6%%3&^BX$jnM@2bjiGc-DymKly)wVkA-pq;jSWL#7_*moZZ4I|-N}o8SK?sIv)p|c zu~9-B%tMc=!)YMFp*SiC0>kfnH8+X5>;+FFVN{~a9YVdIg1uGkZ~kegFy{^PU(4{( z`CbY`XmVA3esai686Yw8djCEyF7`bfB^F1)nwv+AqYLZ&Zy=eFhYT2uMd@{sP_qS4 zbJ&>PxajjZt?&c<1^!T|pLHfX=E^FJ>-l_XCZzvRV%x}@u(FtF(mS+Umw$e+IA74e>gCdTqi;6&=euAIpxd=Y3I5xWR zBhGoT+T`V1@91OlQ}2YO*~P4ukd*TBBdt?Plt)_ou6Y@Db`ss+Q~A-48s>?eaJYA2 zRGOa8^~Em}EFTmKIVVbMb|ob)hJJ7ITg>yHAn2i|{2ZJU!cwt9YNDT0=*WO7Bq#Xj zg@FjEaKoolrF8%c;49|`IT&25?O$dq8kp3#la9&6aH z6G|{>^C(>yP7#Dr$aeFyS0Ai_$ILhL43#*mgEl(c*4?Ae;tRL&S7Vc}Szl>B`mBuI zB9Y%xp%CZwlH!3V(`6W4-ZuETssvI&B~_O;CbULfl)X1V%(H7VSPf`_Ka9ak@8A=z z1l|B1QKT}NLI`WVTRd;2En5u{0CRqy9PTi$ja^inu){LJ&E&6W%JJPw#&PaTxpt?k zpC~gjN*22Q8tpGHR|tg~ye#9a8N<%odhZJnk7Oh=(PKfhYfzLAxdE36r<6a?A;rO&ELp_Y?8Pdw(PT^Fxn!eG_|LEbSYoBrsBA|6Fgr zt5LntyusI{Q2fdy=>ditS;}^B;I2MD4=(>7fWt0Jp~y=?VvfvzHvQhj6dyIef46J$ zl4Xu7U9v_NJV?uBBC0!kcTS0UcrV7+@~is?Fi+jrr@l3XwD|uG zr26jUWiv>Ju48Y^#qn7r9mwIH-Pv6Y|V|V-GZ&+&gQ?S?-`&ts{@5GXPqbmyZjUACC&oVXfNwUX0}ba(v978 zp8z!v9~8Zx8qB@7>oFPDm^iR@+yw`79YF)w^OHB_N;&&x7c3l^3!)IY#)}x)@D(iNaOm9 zC=^*!{`7={3*S=%iU=KsPXh=DDZcc``Ss>057i{pdW8M@4q+Ba@Tt%OytH!4>rbIbQw^-pR zGGYNPzw@n=PV@)b7yVbFr;glF*Qq3>F9oBN5PUXt!?2mdGcpv^o1?Thp`jP10G2Yi z(c93td3F3SW!Le5DUwdub!aDKoVLU6g!O?Ret21l$qOC;kdd@L#M&baVu&JZGt&<6 z!VCkvgRaav6QDW2x}tUy4~Y5(B+#Ej-8vM?DM-1?J_*&PntI3E96M!`WL#<&Z5n2u zo`P!~vBT$YOT~gU9#PB)%JZ zcd_u=m^LYzC!pH#W`yA1!(fA;D~b zG#73@l)NNd;n#XrKXZEfab;@kQRnOFU2Th-1m<4mJzlj9b3pv-GF$elX7ib9!uILM_$ke zHIGB*&=5=;ynQA{y7H93%i^d)T}y@(p>8vVhJ4L)M{0Q*@D^+SPp`EW+G6E%+`Z;u zS3goV@Dic7vc5`?!pCN44Ts@*{)zwy)9?B||AM{zKlN4T}qQRL2 zgv+{K8bv7w)#xge16;kI1fU87!W4pX)N&|cq8&i^1r`W|Hg4366r(?-ecEJ9u&Eaw zrhyikXQB>C9d>cpPGiu=VU3Z-u4|0V_iap!_J3o+K_R5EXk@sfu~zHwwYkpncVh!R zqNe7Cmf_|Wmeq4#(mIO&(wCK@b4(x0?W1Qtk(`$?+$uCJCGZm_%k?l32vuShgDFMa ztc`{$8DhB9)&?~(m&EUc=LzI1=qo#zjy#2{hLT_*aj<618qQ7mD#k2ZFGou&69;=2 z1j7=Su8k}{L*h&mfs7jg^PN&9C1Z@U!p6gXk&-7xM~{X`nqH#aGO`;Xy_zbz^rYacIq0AH%4!Oh93TzJ820%ur)8OyeS@K?sF1V(iFO z37Nnqj1z#1{|v7=_CX`lQA|$<1gtuNMHGNJYp1D_k;WQk-b+T6VmUK(x=bWviOZ~T z|4e%SpuaWLWD?qN2%`S*`P;BQBw(B__wTD6epvGdJ+>DBq2oVlf&F*lz+#avb4)3P1c^Mf#olQheVvZ|Z5 z>xXfgmv!5Z^SYn+_x}K5B%G^sRwiez&z9|f!E!#oJlT2kCOV0000$L_|bHBqAarB4TD{W@grX1CUr72@caw0faEd7-K|4L_|cawbojjHdpd6 zI6~Iv5J?-Q4*&oF000000FV;^004t70Z6Qk1Xl{X9oJ{sRC2(cs?- diff --git a/src/ui/public/styles/theme/bootstrap.less b/src/ui/public/styles/theme/bootstrap.less index a68568990f0933..4b9b7ed45a211c 100644 --- a/src/ui/public/styles/theme/bootstrap.less +++ b/src/ui/public/styles/theme/bootstrap.less @@ -8,7 +8,6 @@ // Reset and dependencies @import "~ui/styles/bootstrap/normalize.less"; @import "~ui/styles/bootstrap/print.less"; -@import "~ui/styles/bootstrap/glyphicons.less"; // Core CSS @import "~ui/styles/bootstrap/scaffolding.less"; @@ -46,7 +45,6 @@ @import "~ui/styles/bootstrap/modals.less"; @import "~ui/styles/bootstrap/tooltip.less"; @import "~ui/styles/bootstrap/popovers.less"; -@import "~ui/styles/bootstrap/carousel.less"; // Utility classes @import "~ui/styles/bootstrap/utilities.less"; diff --git a/src/ui/public/styles/variables/bootstrap-mods.less b/src/ui/public/styles/variables/bootstrap-mods.less index cf89cce9fda7b2..d4348f2a17c5f9 100644 --- a/src/ui/public/styles/variables/bootstrap-mods.less +++ b/src/ui/public/styles/variables/bootstrap-mods.less @@ -51,14 +51,6 @@ @headings-color: inherit; -//-- Iconography -// -//## Specify custom locations of the include Glyphicons icon font. Useful for those including Bootstrap via Bower. - -@icon-font-path: "../fonts/"; -@icon-font-name: "glyphicons-halflings-regular"; -@icon-font-svg-id: "glyphicons_halflingsregular"; - //== Components // //## Define common padding and border radius sizes and more. Values based on 14px text and 1.428 line-height (~20px to start). @@ -745,20 +737,6 @@ @breadcrumb-separator: "/"; -//== Carousel -// -//## - -@carousel-text-shadow: 0 1px 2px rgba(0,0,0,.6); -@carousel-control-color: @white; -@carousel-indicator-active-bg: @white; -@carousel-indicator-border-color: @white; -@carousel-caption-color: @white; -@carousel-control-width: 15%; -@carousel-control-opacity: .5; -@carousel-control-font-size: 20px; - - //== Close // //## From 7aefd6b92739870a16eb58c9e223e64870f57616 Mon Sep 17 00:00:00 2001 From: CJ Cenizal Date: Wed, 29 Jun 2016 04:43:14 -0700 Subject: [PATCH 67/67] Remove Bootstrap print. --- src/ui/public/styles/bootstrap/bootstrap.less | 1 - src/ui/public/styles/bootstrap/print.less | 101 ------------------ .../bootstrap/responsive-utilities.less | 41 ------- src/ui/public/styles/theme/bootstrap.less | 1 - 4 files changed, 144 deletions(-) delete mode 100644 src/ui/public/styles/bootstrap/print.less diff --git a/src/ui/public/styles/bootstrap/bootstrap.less b/src/ui/public/styles/bootstrap/bootstrap.less index 5c387233edc3b3..67d91f5cee6420 100644 --- a/src/ui/public/styles/bootstrap/bootstrap.less +++ b/src/ui/public/styles/bootstrap/bootstrap.less @@ -10,7 +10,6 @@ // Reset and dependencies @import "normalize.less"; -@import "print.less"; // Core CSS @import "scaffolding.less"; diff --git a/src/ui/public/styles/bootstrap/print.less b/src/ui/public/styles/bootstrap/print.less deleted file mode 100644 index 66e54ab489ea27..00000000000000 --- a/src/ui/public/styles/bootstrap/print.less +++ /dev/null @@ -1,101 +0,0 @@ -/*! Source: https://github.com/h5bp/html5-boilerplate/blob/master/src/css/main.css */ - -// ========================================================================== -// Print styles. -// Inlined to avoid the additional HTTP request: h5bp.com/r -// ========================================================================== - -@media print { - *, - *:before, - *:after { - background: transparent !important; - color: #000 !important; // Black prints faster: h5bp.com/s - box-shadow: none !important; - text-shadow: none !important; - } - - a, - a:visited { - text-decoration: underline; - } - - a[href]:after { - content: " (" attr(href) ")"; - } - - abbr[title]:after { - content: " (" attr(title) ")"; - } - - // Don't show links that are fragment identifiers, - // or use the `javascript:` pseudo protocol - a[href^="#"]:after, - a[href^="javascript:"]:after { - content: ""; - } - - pre, - blockquote { - border: 1px solid #999; - page-break-inside: avoid; - } - - thead { - display: table-header-group; // h5bp.com/t - } - - tr, - img { - page-break-inside: avoid; - } - - img { - max-width: 100% !important; - } - - p, - h2, - h3 { - orphans: 3; - widows: 3; - } - - h2, - h3 { - page-break-after: avoid; - } - - // Bootstrap specific changes start - - // Bootstrap components - .navbar { - display: none; - } - .btn, - .dropup > .btn { - > .caret { - border-top-color: #000 !important; - } - } - .label { - border: 1px solid #000; - } - - .table { - border-collapse: collapse !important; - - td, - th { - background-color: #fff !important; - } - } - .table-bordered { - th, - td { - border: 1px solid #ddd !important; - } - } - - // Bootstrap specific changes end -} diff --git a/src/ui/public/styles/bootstrap/responsive-utilities.less b/src/ui/public/styles/bootstrap/responsive-utilities.less index b1db31d7bfc19a..5931fdaa2ff1af 100644 --- a/src/ui/public/styles/bootstrap/responsive-utilities.less +++ b/src/ui/public/styles/bootstrap/responsive-utilities.less @@ -151,44 +151,3 @@ .responsive-invisibility(); } } - - -// Print utilities -// -// Media queries are placed on the inside to be mixin-friendly. - -// Note: Deprecated .visible-print as of v3.2.0 -.visible-print { - .responsive-invisibility(); - - @media print { - .responsive-visibility(); - } -} -.visible-print-block { - display: none !important; - - @media print { - display: block !important; - } -} -.visible-print-inline { - display: none !important; - - @media print { - display: inline !important; - } -} -.visible-print-inline-block { - display: none !important; - - @media print { - display: inline-block !important; - } -} - -.hidden-print { - @media print { - .responsive-invisibility(); - } -} diff --git a/src/ui/public/styles/theme/bootstrap.less b/src/ui/public/styles/theme/bootstrap.less index 4b9b7ed45a211c..6dd6f2d69ef7ca 100644 --- a/src/ui/public/styles/theme/bootstrap.less +++ b/src/ui/public/styles/theme/bootstrap.less @@ -7,7 +7,6 @@ // Reset and dependencies @import "~ui/styles/bootstrap/normalize.less"; -@import "~ui/styles/bootstrap/print.less"; // Core CSS @import "~ui/styles/bootstrap/scaffolding.less";