Skip to content

Commit

Permalink
kbn_top_nav directive changes
Browse files Browse the repository at this point in the history
  • Loading branch information
spalger committed Apr 4, 2016
1 parent 0bdebc7 commit 9de94a8
Show file tree
Hide file tree
Showing 5 changed files with 132 additions and 82 deletions.
1 change: 0 additions & 1 deletion src/plugins/kibana/public/dashboard/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import _ from 'lodash';
import $ from 'jquery';
import angular from 'angular';
import chrome from 'ui/chrome';
import 'ui/directives/kbn_top_nav';
import 'ui/courier';
import 'ui/config';
import 'ui/notify';
Expand Down
1 change: 1 addition & 0 deletions src/ui/public/autoload/modules.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import 'angular';
import 'ui/chrome';
import 'ui/bind';
import 'ui/kbn_top_nav';
import 'ui/bound_to_config_obj';
import 'ui/config';
import 'ui/courier';
Expand Down
2 changes: 0 additions & 2 deletions src/ui/public/chrome/directives/index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import 'ui/directives/kbn_top_nav';

import './app_switcher';
import kbnChromeProv from './kbn_chrome';
import kbnChromeNavControlsProv from './append_nav_controls';
Expand Down
115 changes: 36 additions & 79 deletions src/ui/public/kbn_top_nav/kbn_top_nav.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,46 +3,48 @@ import 'ui/watch_multi';
import angular from 'angular';
import 'ui/directives/input_focus';
import uiModules from 'ui/modules';
var module = uiModules.get('kibana');
import KbnTopNavControllerProvider from './kbn_top_nav_controller';

const module = uiModules.get('kibana');

/**
* kbnTopNav directive
*
* The top section that shows the timepicker, load, share and save dialogues.
*
* ```
* <kbn-top-nav name="current-app-for-extensions" config="path.to.menuItems"></kbn-top-nav>
* ```
*
* Menu items/templates are passed to the kbnTopNav via the config attribute
* and should be defined as an array of objects. Each object represents a menu
* item and should have the following properties:
*
* @param {Array<Object>|KbnTopNavController} config
* @param {string} config[].key
* - the uniq key for this menu item.
* @param {string} [config[].label]
* - optional, string that will be displayed for the menu button.
* Defaults to the key
* @param {string} [config[].description]
* - optional, used for the screen-reader description of this menu
* item, defaults to "Toggle ${key} view" for templated menu items
* and just "${key}" for programatic menu items
* @param {boolean} [config[].hideButton]
* - optional, set to true to prevent a menu item from being created.
* This allow injecting templates into the navbar that don't have
* an associated template
* @param {function} [config[].run]
* - optional, function to call when the menu item is clicked, defaults
* to toggling the template
*
* Programatic control of the navbar can be acheived one of two ways
*/

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),
noButton: !!opt.noButton,
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) {
template($el, $attrs) {
// This is ugly
// This is necessary because of navbar-extensions
// It will no accept any programatic way of setting its name
Expand All @@ -64,65 +66,20 @@ module.directive('kbnTopNav', function (Private) {
</div>
<kbn-global-timepicker></kbn-global-timepicker>
</navbar>
<div class="config" ng-show="kbnTopNav.currTemplate">
<div class="config" ng-show="kbnTopNav.rendered">
<div id="template_wrapper" class="container-fluid"></div>
<div class="config-close remove">
<i class="fa fa-chevron-circle-up" ng-click="kbnTopNav.close()"></i>
</div>
</div>`;
</div>
`;
},
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;
}
controller($scope, $compile, $attrs, $element) {
const KbnTopNavController = Private(KbnTopNavControllerProvider);

const templateToCompile = ctrlObj.templates[ctrlObj.curr] || false;

if ($scope.kbnTopNav.currTemplate) {
$scope.kbnTopNav.currTemplateScope.$destroy();
$scope.kbnTopNav.currTemplate.remove();
}

if (templateToCompile) {
$scope.kbnTopNav.currTemplateScope = $scope.$new();
$scope.kbnTopNav.currTemplate = $compile(templateToCompile)($scope.kbnTopNav.currTemplateScope);
} else {
$scope.kbnTopNav.currTemplateScope = null;
$scope.kbnTopNav.currTemplate = false;
}
};
const normalizeOpts = _.partial(optionsNormalizer, (item) => {
ctrlObj.toggleCurrTemplate(item.key);
});

const niceMenuItems = _.compact(_.get($scope, $attrs.config, []).map(normalizeOpts));
ctrlObj.templates = _.assign({
interval: intervalTemplate,
filter: filterTemplate,
}, getTemplatesMap(niceMenuItems));

$scope.kbnTopNav = {
menuItems: niceMenuItems.filter(item => !item.noButton),
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);
});
$scope.kbnTopNav = new KbnTopNavController(_.get($scope, $attrs.config));
$scope.kbnTopNav._link($scope, $element);
return $scope.kbnTopNav;
}
};
});
95 changes: 95 additions & 0 deletions src/ui/public/kbn_top_nav/kbn_top_nav_controller.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import { defaults, capitalize } from 'lodash';

import uiModules from 'ui/modules';
import filterTemplate from 'ui/chrome/config/filter.html';
import intervalTemplate from 'ui/chrome/config/interval.html';

export default function ($compile) {
return class KbnTopNavController {
constructor(opts = []) {
if (opts instanceof KbnTopNavController) {
return opts;
}

this.opts = [];
this.menuItems = [];
this.currentKey = null;
this.templates = {
interval: intervalTemplate,
filter: filterTemplate,
};

opts.forEach(rawOpt => {
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.template) this.templates[opt.key] = opt.template;
});
}

// change the current key and rerender
set(key) {
if (key && !this.templates.hasOwnProperty(key)) {
throw new TypeError(`KbnTopNav: unknown template key "${key}"`);
}

this.currentKey = key || null;
this._render();
}

// little usability helpers
which() { return this.currentKey; }
is(key) { return this.which() === key; }
open(key) { this.set(key); }
close(key) { (!key || this.is(key)) && this.set(null); }
toggle(key) { this.set(this.is(key) ? null : key); }

// apply the defaults to individual options
_applyOptDefault(opt = {}) {
return defaults({}, opt, {
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)
});
}

// enable actual rendering
_link($scope, $element) {
this.$scope = $scope;
this.$element = $element;
this._render();
}

// render the current template to the $element if possible
// function is idempotent
_render() {
const { $scope, $element, rendered, currentKey } = this;
const templateToRender = currentKey && this.templates[currentKey];

if (rendered) {
if (rendered.key !== currentKey) {
// we have an invalid render, clear it
rendered.$childScope.$destroy();
rendered.$el.remove();
this.rendered = null;
} else {
// our previous render is still valid, keep it
return;
}
}

if (!templateToRender || !$scope || !$element) {
// we either have nothing to render, or we can't render
return;
}

const $childScope = $scope.$new();
const $el = $compile(templateToRender)($childScope);
$element.find('#template_wrapper').html($el);
this.rendered = { $childScope, $el, key: currentKey };
}
};
}

0 comments on commit 9de94a8

Please sign in to comment.