+ interval="timefilter.refreshInterval">
diff --git a/src/ui/public/chrome/config/interval.html b/src/ui/public/chrome/config/interval.html
index 44d86b4140925..d41a601709709 100644
--- a/src/ui/public/chrome/config/interval.html
+++ b/src/ui/public/chrome/config/interval.html
@@ -1,7 +1,7 @@
+ interval="timefilter.refreshInterval">
diff --git a/src/ui/public/chrome/directives/index.js b/src/ui/public/chrome/directives/index.js
index 2523fae1a6020..c553db6a1d837 100644
--- a/src/ui/public/chrome/directives/index.js
+++ b/src/ui/public/chrome/directives/index.js
@@ -1,4 +1,4 @@
-import 'ui/directives/config';
+import 'ui/directives/kbn_top_nav';
import './app_switcher';
import kbnChromeProv from './kbn_chrome';
diff --git a/src/ui/public/chrome/directives/kbn_chrome.js b/src/ui/public/chrome/directives/kbn_chrome.js
index e40c57059f477..6f788b0ee61ee 100644
--- a/src/ui/public/chrome/directives/kbn_chrome.js
+++ b/src/ui/public/chrome/directives/kbn_chrome.js
@@ -1,7 +1,6 @@
import $ from 'jquery';
import UiModules from 'ui/modules';
-import ConfigTemplate from 'ui/config_template';
export default function (chrome, internals) {
@@ -46,10 +45,6 @@ export default function (chrome, internals) {
// and some local values
chrome.httpActive = $http.pendingRequests;
$scope.notifList = require('ui/notify')._notifs;
- $scope.appSwitcherTemplate = new ConfigTemplate({
- switcher: '
'
- });
-
return chrome;
}
};
diff --git a/src/ui/public/config_template.js b/src/ui/public/config_template.js
deleted file mode 100644
index 2430d033c7b76..0000000000000
--- a/src/ui/public/config_template.js
+++ /dev/null
@@ -1,34 +0,0 @@
-import _ from 'lodash';
-
-function ConfigTemplate(templates) {
- var template = this;
- template.current = null;
- template.toggle = _.partial(update, null);
- template.open = _.partial(update, true);
- template.close = _.partial(update, false);
-
- function update(newState, name) {
- var toUpdate = templates[name];
- var curState = template.is(name);
- if (newState == null) newState = !curState;
-
- if (newState) {
- template.current = toUpdate;
- } else {
- template.current = null;
- }
-
- return newState;
- }
-
- template.is = function (name) {
- return template.current === templates[name];
- };
-
- template.toString = function () {
- return template.current;
- };
-}
-
-export default ConfigTemplate;
-
diff --git a/src/ui/public/directives/__tests__/config.js b/src/ui/public/directives/__tests__/config.js
deleted file mode 100644
index 25396bec64025..0000000000000
--- a/src/ui/public/directives/__tests__/config.js
+++ /dev/null
@@ -1,59 +0,0 @@
-import ngMock from 'ng_mock';
-import expect from 'expect.js';
-import { assign } from 'lodash';
-import $ from 'jquery';
-
-describe('Config Directive', function () {
-
- var build = function () {};
-
- beforeEach(ngMock.module('kibana', function ($compileProvider) {
- var renderCount = 0;
- $compileProvider.directive('renderCounter', function () {
- return {
- link: function ($scope, $el) {
- $el.html(++renderCount);
- }
- };
- });
- }));
-
- beforeEach(ngMock.inject(function ($compile, $rootScope) {
-
- build = function (attrs, scopeVars) {
- var $el = $('
').attr(attrs);
- var $scope = $rootScope.$new();
- assign($scope, scopeVars || {});
- $compile($el)($scope);
- $scope.$digest();
- return $el;
- };
-
- }));
-
- it('renders it\'s config template', function () {
- var $config = build({ 'config-template': '""' });
- expect($config.find('uniqel').size()).to.be(1);
- });
-
- it('exposes an object a config object using it\'s name', function () {
- var $config = build(
- {
- 'config-template': '"{{ controller.name }}"',
- 'config-object': 'controller',
- },
- {
- controller: {
- name: 'foobar'
- }
- }
- );
-
- expect($config.find('uniqel').text()).to.be('foobar');
- });
-
- it('only renders the config-template once', function () {
- var $config = build({ 'config-template': '""' });
- expect($config.find('[render-counter]').text()).to.be('1');
- });
-});
diff --git a/src/ui/public/directives/__tests__/kbn_top_nav.js b/src/ui/public/directives/__tests__/kbn_top_nav.js
new file mode 100644
index 0000000000000..ece107d4e46ee
--- /dev/null
+++ b/src/ui/public/directives/__tests__/kbn_top_nav.js
@@ -0,0 +1,55 @@
+import ngMock from 'ng_mock';
+import expect from 'expect.js';
+import { assign } from 'lodash';
+import $ from 'jquery';
+
+import navbarExtensionsRegistry from 'ui/registry/navbar_extensions';
+import Registry from 'ui/registry/_registry';
+import 'ui/navbar_extensions';
+
+describe('kbnTopNav Directive', function () {
+
+ var build = function () {};
+ let $testScope = null;
+ let stubRegistry;
+
+ beforeEach(ngMock.module('kibana', function ($compileProvider, PrivateProvider) {
+ var renderCount = 0;
+ $compileProvider.directive('renderCounter', function () {
+ return {
+ link: function ($scope, $el) {
+ $el.html(++renderCount);
+ }
+ };
+ });
+
+ stubRegistry = new Registry({
+ index: ['name'],
+ group: ['appName'],
+ order: ['order']
+ });
+
+ PrivateProvider.swap(navbarExtensionsRegistry, stubRegistry);
+ ngMock.module('kibana/navbar');
+ }));
+
+ beforeEach(ngMock.inject(function ($compile, $rootScope) {
+ build = function (scopeVars) {
+ var $el = $('');
+ $testScope = $rootScope.$new();
+ assign($testScope, scopeVars || {});
+ $compile($el)($testScope);
+ $testScope.$digest();
+ return $el;
+ };
+
+ }));
+
+ it('sets the proper functions on the kbnTopNav prop on scope', function () {
+ var $config = build();
+ expect($testScope.kbnTopNav.open).to.be.a(Function);
+ expect($testScope.kbnTopNav.close).to.be.a(Function);
+ expect($testScope.kbnTopNav.is).to.be.a(Function);
+ expect($testScope.kbnTopNav.toggle).to.be.a(Function);
+ });
+});
diff --git a/src/ui/public/directives/config.js b/src/ui/public/directives/config.js
deleted file mode 100644
index 8a9690fbe181d..0000000000000
--- a/src/ui/public/directives/config.js
+++ /dev/null
@@ -1,82 +0,0 @@
-import _ from 'lodash';
-import 'ui/watch_multi';
-import ConfigTemplate from 'ui/config_template';
-import angular from 'angular';
-import 'ui/directives/input_focus';
-import uiModules from 'ui/modules';
-var module = uiModules.get('kibana');
-
-
-/**
- * config directive
- *
- * Creates a full width horizonal config section, usually under a nav/subnav.
- * ```
- *
- * ```
- */
-
-module.directive('config', function ($compile) {
- return {
- restrict: 'E',
- scope: {
- configTemplate: '=',
- configClose: '=',
- configSubmit: '=',
- configObject: '='
- },
- link: function ($scope, element, attr) {
- var tmpScope = $scope.$new();
-
- $scope.$watch('configObject', function (newVal) {
- $scope[attr.configObject] = $scope.configObject;
- });
-
- var wrapTmpl = function (tmpl) {
- if ($scope.configSubmit) {
- return '';
- } else {
- return '' + tmpl + '
';
- }
- };
-
- $scope.$watchMulti([
- 'configSubmit',
- 'configTemplate.current || configTemplate'
- ], function () {
- var tmpl = $scope.configTemplate;
- if (tmpl instanceof ConfigTemplate) {
- tmpl = tmpl.toString();
- }
-
- tmpScope.$destroy();
- tmpScope = $scope.$new();
-
- var html = '';
- if (tmpl) {
- html = $compile('' +
- '' +
- wrapTmpl(tmpl) +
- '
' +
- ' ' +
- '
' +
- '
' +
- ''
- )(tmpScope);
- }
-
- element.html(html);
- });
-
-
- $scope.close = function () {
- if (_.isFunction($scope.configClose)) $scope.configClose();
- if ($scope.configTemplate instanceof ConfigTemplate) {
- $scope.configTemplate.current = null;
- } else {
- $scope.configTemplate = null;
- }
- };
- }
- };
-});
diff --git a/src/ui/public/directives/kbn_top_nav.js b/src/ui/public/directives/kbn_top_nav.js
new file mode 100644
index 0000000000000..89c407879cb85
--- /dev/null
+++ b/src/ui/public/directives/kbn_top_nav.js
@@ -0,0 +1,116 @@
+import _ from 'lodash';
+import 'ui/watch_multi';
+import angular from 'angular';
+import 'ui/directives/input_focus';
+import uiModules from 'ui/modules';
+var module = uiModules.get('kibana');
+
+
+/**
+ * kbnTopNav directive
+ *
+ * The top section that shows the timepicker, load, share and save dialogues.
+ * ```
+ *
+ * ```
+ */
+
+module.directive('kbnTopNav', function (Private) {
+ const filterTemplate = require('ui/chrome/config/filter.html');
+ const intervalTemplate = require('ui/chrome/config/interval.html');
+ function optionsNormalizer(defaultFunction, opt) {
+ if (!opt.key) {
+ return false;
+ }
+ return _.assign({
+ label: _.capitalize(opt.key),
+ hasFunction: !!opt.run,
+ description: ('Toggle ' + opt.key),
+ run: defaultFunction
+ }, opt);
+ }
+ function getTemplatesMap(configs) {
+ const templateMap = {};
+ configs.forEach(conf => {
+ if (conf.template) {
+ templateMap[conf.key] = conf.template;
+ }
+ });
+ return templateMap;
+ }
+ return {
+ restrict: 'E',
+ transclude: true,
+ template: function ($el, $attrs) {
+ // This is ugly
+ // This is necessary because of navbar-extensions
+ // It will no accept any programatic way of setting its name
+ // besides this because it happens so early in the digest cycle
+ return `
+
+
+
+
+
+
+
+
+ `;
+ },
+ controller: ['$scope', '$compile', '$attrs', function ($scope, $compile, $attrs) {
+ const ctrlObj = this;
+ // toggleCurrTemplate(false) to turn it off
+ ctrlObj.toggleCurrTemplate = function (which) {
+ if (ctrlObj.curr === which || !which) {
+ ctrlObj.curr = null;
+ } else {
+ ctrlObj.curr = which;
+ }
+ const templateToCompile = ctrlObj.templates[ctrlObj.curr] || false;
+ $scope.kbnTopNav.currTemplate = templateToCompile ? $compile(templateToCompile)($scope) : false;
+ };
+ const normalizeOpts = _.partial(optionsNormalizer, (item) => {
+ ctrlObj.toggleCurrTemplate(item.key);
+ });
+
+ const niceMenuItems = _.compact(($scope[$attrs.config] || []).map(normalizeOpts));
+ ctrlObj.templates = _.assign({
+ interval: intervalTemplate,
+ filter: filterTemplate,
+ }, getTemplatesMap(niceMenuItems));
+
+
+ $scope.kbnTopNav = {
+ menuItems: niceMenuItems,
+ currTemplate: false,
+ is: which => { return ctrlObj.curr === which; },
+ close: () => { ctrlObj.toggleCurrTemplate(false); },
+ toggle: ctrlObj.toggleCurrTemplate,
+ open: which => {
+ if (ctrlObj.curr !== which) {
+ ctrlObj.toggleCurrTemplate(which);
+ }
+ }
+ };
+
+ }],
+ link: function ($scope, element, attr, configCtrl) {
+ $scope.$watch('kbnTopNav.currTemplate', newVal => {
+ element.find('#template_wrapper').html(newVal);
+ });
+ }
+ };
+});
diff --git a/src/ui/public/navbar/__tests__/navbar.js b/src/ui/public/navbar/__tests__/navbar.js
deleted file mode 100644
index 571445c8733f4..0000000000000
--- a/src/ui/public/navbar/__tests__/navbar.js
+++ /dev/null
@@ -1,140 +0,0 @@
-import ngMock from 'ng_mock';
-import sinon from 'sinon';
-import expect from 'expect.js';
-import angular from 'angular';
-import _ from 'lodash';
-
-import navbarExtensionsRegistry from 'ui/registry/navbar_extensions';
-import Registry from 'ui/registry/_registry';
-import 'ui/navbar';
-
-const defaultMarkup = `
-
-
-
-
-
-
- `;
-
-
-describe('navbar directive', function () {
- let $rootScope;
- let $compile;
- let stubRegistry;
-
- beforeEach(function () {
- ngMock.module('kibana', function (PrivateProvider) {
- stubRegistry = new Registry({
- index: ['name'],
- group: ['appName'],
- order: ['order']
- });
-
- PrivateProvider.swap(navbarExtensionsRegistry, stubRegistry);
- });
-
- ngMock.module('kibana/navbar');
-
- // Create the scope
- ngMock.inject(function ($injector) {
- $rootScope = $injector.get('$rootScope');
- $compile = $injector.get('$compile');
- });
- });
-
- function init(markup = defaultMarkup) {
- // Give us a scope
- const $el = angular.element(markup);
- $compile($el)($rootScope);
- $el.scope().$digest();
- return $el;
- }
-
- describe('incorrect use', function () {
- it('should throw if missing a name property', function () {
- const markup = ``;
- expect(() => init(markup)).to.throwException(/requires a name attribute/);
- });
-
- it('should throw if missing a button group', function () {
- const markup = ``;
- expect(() => init(markup)).to.throwException(/must have exactly 1 button group/);
- });
-
- it('should throw if multiple button groups', function () {
- const markup = `
-
-
-
-
-
-
-
- `;
- expect(() => init(markup)).to.throwException(/must have exactly 1 button group/);
- });
-
- it('should throw if button group not direct child', function () {
- const markup = ``;
- expect(() => init(markup)).to.throwException(/must have exactly 1 button group/);
- });
- });
-
- describe('injecting extensions', function () {
- function registerExtension(def = {}) {
- stubRegistry.register(function () {
- return _.defaults(def, {
- name: 'exampleButton',
- appName: 'testing',
- order: 0,
- template: `
- `
- });
- });
- }
-
- it('should use the default markup', function () {
- var $el = init();
- expect($el.find('.button-group button').length).to.equal(3);
- });
-
- it('should append to end then order == 0', function () {
- registerExtension({ order: 0 });
- var $el = init();
-
- expect($el.find('.button-group button').length).to.equal(4);
- expect($el.find('.button-group button').last().hasClass('test-button')).to.be.ok();
- });
-
- it('should append to end then order > 0', function () {
- registerExtension({ order: 1 });
- var $el = init();
-
- expect($el.find('.button-group button').length).to.equal(4);
- expect($el.find('.button-group button').last().hasClass('test-button')).to.be.ok();
- });
-
- it('should append to end then order < 0', function () {
- registerExtension({ order: -1 });
- var $el = init();
-
- expect($el.find('.button-group button').length).to.equal(4);
- expect($el.find('.button-group button').first().hasClass('test-button')).to.be.ok();
- });
- });
-});
diff --git a/src/ui/public/navbar_extensions/__tests__/navbar_extensions.js b/src/ui/public/navbar_extensions/__tests__/navbar_extensions.js
new file mode 100644
index 0000000000000..8f2e78652e701
--- /dev/null
+++ b/src/ui/public/navbar_extensions/__tests__/navbar_extensions.js
@@ -0,0 +1,106 @@
+import ngMock from 'ng_mock';
+import sinon from 'sinon';
+import expect from 'expect.js';
+import angular from 'angular';
+import _ from 'lodash';
+
+import navbarExtensionsRegistry from 'ui/registry/navbar_extensions';
+import Registry from 'ui/registry/_registry';
+import 'ui/navbar_extensions';
+
+const defaultMarkup = `
+ `;
+
+
+describe('navbar-extensions directive', function () {
+ let $rootScope;
+ let $compile;
+ let stubRegistry;
+
+ beforeEach(function () {
+ ngMock.module('kibana', function (PrivateProvider) {
+ stubRegistry = new Registry({
+ index: ['name'],
+ group: ['appName'],
+ order: ['order']
+ });
+
+ PrivateProvider.swap(navbarExtensionsRegistry, stubRegistry);
+ });
+
+ ngMock.module('kibana/navbar');
+
+ // Create the scope
+ ngMock.inject(function ($injector) {
+ $rootScope = $injector.get('$rootScope');
+ $compile = $injector.get('$compile');
+ });
+ });
+
+ function init(markup = defaultMarkup) {
+ // Give us a scope
+ const $el = angular.element(markup);
+ $compile($el)($rootScope);
+ $el.scope().$digest();
+ return $el;
+ }
+
+ describe('incorrect use', function () {
+ it('should throw if missing a name property', function () {
+ const markup = ``;
+ expect(() => init(markup)).to.throwException(/requires a name attribute/);
+ });
+ });
+
+ describe('injecting extensions', function () {
+ function registerExtension(def = {}) {
+ stubRegistry.register(function () {
+ return _.defaults(def, {
+ name: 'exampleButton',
+ appName: 'testing',
+ order: 0,
+ template: `
+ `
+ });
+ });
+ }
+
+ it('should append to end then order == 0', function () {
+ registerExtension({ order: 0 });
+ var $el = init();
+
+ expect($el.find('button').last().hasClass('test-button')).to.be.ok();
+ });
+
+ it('should enforce the order prop', function () {
+ registerExtension({
+ order: 1,
+ template: `
+ `
+ });
+ registerExtension({
+ order: 2,
+ template: `
+ `
+ });
+ registerExtension({
+ order: 0,
+ template: `
+ `
+ });
+ var $el = init();
+
+ expect($el.find('button').length).to.equal(3);
+ expect($el.find('button').last().hasClass('test-button-2')).to.be.ok();
+ expect($el.find('button').first().hasClass('test-button-0')).to.be.ok();
+ });
+ });
+});
diff --git a/src/ui/public/navbar/navbar.js b/src/ui/public/navbar_extensions/navbar_extensions.js
similarity index 58%
rename from src/ui/public/navbar/navbar.js
rename to src/ui/public/navbar_extensions/navbar_extensions.js
index 11e7a52451335..b86fae8972c60 100644
--- a/src/ui/public/navbar/navbar.js
+++ b/src/ui/public/navbar_extensions/navbar_extensions.js
@@ -6,7 +6,7 @@ import uiModules from 'ui/modules';
const navbar = uiModules.get('kibana/navbar');
-navbar.directive('navbar', function (Private, $compile) {
+navbar.directive('navbarExtensions', function (Private, $compile) {
const navbarExtensions = Private(RegistryNavbarExtensionsProvider);
const getExtensions = _.memoize(function (name) {
if (!name) throw new Error('navbar directive requires a name attribute');
@@ -16,36 +16,20 @@ navbar.directive('navbar', function (Private, $compile) {
return {
restrict: 'E',
template: function ($el, $attrs) {
- const $buttonGroup = $el.children('.button-group');
- if ($buttonGroup.length !== 1) throw new Error('navbar must have exactly 1 button group');
-
const extensions = getExtensions($attrs.name);
- const buttons = $buttonGroup.children().detach().toArray();
- const controls = [
- ...buttons.map(function (button) {
- return {
- order: 0,
- $el: $(button),
- };
- }),
- ...extensions.map(function (extension, i) {
- return {
- order: extension.order,
- index: i,
- extension: extension,
- };
- }),
- ];
+ const controls = extensions.map(function (extension, i) {
+ return {
+ order: extension.order,
+ index: i,
+ extension: extension,
+ };
+ });
_.sortBy(controls, 'order').forEach(function (control) {
- if (control.$el) {
- return $buttonGroup.append(control.$el);
- }
-
const { extension, index } = control;
const $ext = $(``);
$ext.html(extension.template);
- $buttonGroup.append($ext);
+ $el.append($ext);
});
return $el.html();
diff --git a/src/ui/public/partials/nav_config.html b/src/ui/public/partials/nav_config.html
deleted file mode 100644
index e8a89062a45b1..0000000000000
--- a/src/ui/public/partials/nav_config.html
+++ /dev/null
@@ -1,8 +0,0 @@
-
diff --git a/src/ui/public/styles/base.less b/src/ui/public/styles/base.less
index d2a55ff2b34fc..1933d84384ef6 100644
--- a/src/ui/public/styles/base.less
+++ b/src/ui/public/styles/base.less
@@ -144,8 +144,13 @@ a {
padding-bottom: 0;
padding-right: 0px;
+ kbn-global-timepicker {
+ line-height: 20px;
+ }
+
.kibana-nav-actions {
margin-left: auto;
+ padding-right: 0;
line-height: 20px;
.button-group > :last-child {
@@ -166,6 +171,7 @@ a {
z-index: 1;
}
> navbar { padding-bottom: 4px; }
+ > kbn-top-nav { z-index: 3; }
> nav,
> navbar {
diff --git a/src/ui/public/timepicker/kbn_global_timepicker.html b/src/ui/public/timepicker/kbn_global_timepicker.html
index 2e781d36e5c4f..bcfe878661bec 100644
--- a/src/ui/public/timepicker/kbn_global_timepicker.html
+++ b/src/ui/public/timepicker/kbn_global_timepicker.html
@@ -1,12 +1,32 @@
-
+
+ -
+
+
+
+ -
+
+
+
+
+
+ -
+
+
+
diff --git a/test/support/pages/discover_page.js b/test/support/pages/discover_page.js
index 62604fe27178a..ced12f51efb75 100644
--- a/test/support/pages/discover_page.js
+++ b/test/support/pages/discover_page.js
@@ -48,9 +48,7 @@ define(function (require) {
})
.then(function clickSave() {
common.debug('--find save button');
- return thisTime
- .findDisplayedByCssSelector('button[ng-disabled="!opts.savedSearch.title"]')
- .click();
+ return common.findTestSubject('discover-save-search-btn').click();
})
.catch(common.handleError(this));
},
@@ -76,13 +74,13 @@ define(function (require) {
clickLoadSavedSearchButton: function clickLoadSavedSearchButton() {
return thisTime
- .findByCssSelector('button[aria-label="Load Saved Search"]')
+ .findDisplayedByCssSelector('button[aria-label="Load Saved Search"]')
.click();
},
getCurrentQueryName: function getCurrentQueryName() {
return thisTime
- .findByCssSelector('span.kibana-nav-info-title')
+ .findByCssSelector('span.kibana-nav-info-title span')
.getVisibleText();
},
diff --git a/test/support/pages/visualize_page.js b/test/support/pages/visualize_page.js
index b19d938d4d188..02e969d6855e1 100644
--- a/test/support/pages/visualize_page.js
+++ b/test/support/pages/visualize_page.js
@@ -311,6 +311,7 @@ define(function (require) {
})
// // click save button
.then(function () {
+ common.debug('click submit button');
return self.remote
.setFindTimeout(defaultTimeout)
.findByCssSelector('.config button[type="submit"]')