mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
commit
0b06d1a211
7 changed files with 333 additions and 9 deletions
4
src/ui/public/chrome/api/angular.js
vendored
4
src/ui/public/chrome/api/angular.js
vendored
|
@ -1,7 +1,7 @@
|
|||
var $ = require('jquery');
|
||||
var _ = require('lodash');
|
||||
|
||||
require('../appSwitcher/appSwitcher.less');
|
||||
require('../appSwitcher');
|
||||
var modules = require('ui/modules');
|
||||
var ConfigTemplate = require('ui/ConfigTemplate');
|
||||
require('ui/directives/config');
|
||||
|
@ -63,7 +63,7 @@ module.exports = function (chrome, internals) {
|
|||
$scope.httpActive = $http.pendingRequests;
|
||||
$scope.notifList = require('ui/notify')._notifs;
|
||||
$scope.appSwitcherTemplate = new ConfigTemplate({
|
||||
switcher: require('../appSwitcher/appSwitcher.html')
|
||||
switcher: '<app-switcher></app-switcher>'
|
||||
});
|
||||
|
||||
return chrome;
|
||||
|
|
|
@ -25,6 +25,10 @@ module.exports = function (chrome, internals) {
|
|||
a.setAttribute('href', link.url);
|
||||
link.url = a.href;
|
||||
link.lastSubUrl = chrome.getLastSubUrlFor(link.url);
|
||||
|
||||
if (link.url === chrome.getAppUrl()) {
|
||||
link.active = true;
|
||||
}
|
||||
});
|
||||
|
||||
};
|
||||
|
|
237
src/ui/public/chrome/appSwitcher/__tests__/appSwitcher.js
Normal file
237
src/ui/public/chrome/appSwitcher/__tests__/appSwitcher.js
Normal file
|
@ -0,0 +1,237 @@
|
|||
var sinon = require('auto-release-sinon');
|
||||
var ngMock = require('ngMock');
|
||||
var $ = require('jquery');
|
||||
var expect = require('expect.js');
|
||||
var constant = require('lodash').constant;
|
||||
var set = require('lodash').set;
|
||||
var cloneDeep = require('lodash').cloneDeep;
|
||||
var indexBy = require('lodash').indexBy;
|
||||
|
||||
require('ui/chrome');
|
||||
require('ui/chrome/appSwitcher');
|
||||
var DomLocationProvider = require('ui/domLocation');
|
||||
|
||||
function findTestSubject() {
|
||||
var subjectSelectors = [].slice.apply(arguments);
|
||||
var $context = subjectSelectors.shift();
|
||||
var $els = $();
|
||||
|
||||
subjectSelectors.forEach(function (subjects) {
|
||||
var selector = subjects.split(/\s+/).map(function (subject) {
|
||||
return '[data-test-subj~="' + subject + '"]';
|
||||
}).join(' ');
|
||||
|
||||
$els = $els.add($context.find(selector));
|
||||
});
|
||||
|
||||
return $els;
|
||||
}
|
||||
|
||||
describe('appSwitcher directive', function () {
|
||||
var env;
|
||||
|
||||
beforeEach(ngMock.module('kibana'));
|
||||
|
||||
function setup(href, links) {
|
||||
return ngMock.inject(function ($window, $rootScope, $compile, Private) {
|
||||
var domLocation = Private(DomLocationProvider);
|
||||
|
||||
$rootScope.chrome = {
|
||||
getNavLinks: constant(cloneDeep(links)),
|
||||
};
|
||||
|
||||
env = {
|
||||
$scope: $rootScope,
|
||||
$el: $compile($('<app-switcher>'))($rootScope),
|
||||
currentHref: href,
|
||||
location: domLocation
|
||||
};
|
||||
|
||||
Object.defineProperties(domLocation, {
|
||||
href: {
|
||||
get: function () { return env.currentHref; },
|
||||
set: function (val) { return env.currentHref = val; },
|
||||
},
|
||||
reload: {
|
||||
value: sinon.stub()
|
||||
}
|
||||
});
|
||||
|
||||
env.$scope.$digest();
|
||||
});
|
||||
}
|
||||
|
||||
context('when one link is for the active app', function () {
|
||||
var myLink = {
|
||||
active: true,
|
||||
title: 'myLink',
|
||||
url: 'http://localhost:555/app/myApp',
|
||||
lastSubUrl: 'http://localhost:555/app/myApp#/lastSubUrl'
|
||||
};
|
||||
|
||||
var notMyLink = {
|
||||
active: false,
|
||||
title: 'notMyLink',
|
||||
url: 'http://localhost:555/app/notMyApp',
|
||||
lastSubUrl: 'http://localhost:555/app/notMyApp#/lastSubUrl'
|
||||
};
|
||||
|
||||
beforeEach(setup('http://localhost:5555/app/myApp/', [myLink, notMyLink]));
|
||||
|
||||
it('links to the inactive apps base url', function () {
|
||||
var $myLink = findTestSubject(env.$el, 'appLink').eq(0);
|
||||
expect($myLink.prop('href')).to.be(myLink.url);
|
||||
expect($myLink.prop('href')).to.not.be(myLink.lastSubUrl);
|
||||
});
|
||||
|
||||
it('links to the inactive apps last sub url', function () {
|
||||
var $notMyLink = findTestSubject(env.$el, 'appLink').eq(1);
|
||||
expect($notMyLink.prop('href')).to.be(notMyLink.lastSubUrl);
|
||||
expect($notMyLink.prop('href')).to.not.be(notMyLink.url);
|
||||
});
|
||||
});
|
||||
|
||||
context('when none of the links are for the active app', function () {
|
||||
var myLink = {
|
||||
active: false,
|
||||
title: 'myLink',
|
||||
url: 'http://localhost:555/app/myApp',
|
||||
lastSubUrl: 'http://localhost:555/app/myApp#/lastSubUrl'
|
||||
};
|
||||
|
||||
var notMyLink = {
|
||||
active: false,
|
||||
title: 'notMyLink',
|
||||
url: 'http://localhost:555/app/notMyApp',
|
||||
lastSubUrl: 'http://localhost:555/app/notMyApp#/lastSubUrl'
|
||||
};
|
||||
|
||||
beforeEach(setup('http://localhost:5555/app/myApp/', [myLink, notMyLink]));
|
||||
|
||||
it('links to the lastSubUrl for each', function () {
|
||||
var $links = findTestSubject(env.$el, 'appLink');
|
||||
var $myLink = $links.eq(0);
|
||||
var $notMyLink = $links.eq(1);
|
||||
|
||||
expect($myLink.prop('href')).to.be(myLink.lastSubUrl);
|
||||
expect($myLink.prop('href')).to.not.be(myLink.url);
|
||||
|
||||
expect($notMyLink.prop('href')).to.be(notMyLink.lastSubUrl);
|
||||
expect($notMyLink.prop('href')).to.not.be(notMyLink.url);
|
||||
});
|
||||
});
|
||||
|
||||
context('clicking a link with matching href but missing hash', function () {
|
||||
var url = 'http://localhost:555/app/myApp?query=1';
|
||||
beforeEach(setup(url + '#/lastSubUrl', [
|
||||
{ url: url }
|
||||
]));
|
||||
|
||||
it('just prevents propogation (no reload)', function () {
|
||||
var event = new $.Event('click');
|
||||
|
||||
expect(env.location.reload.callCount).to.be(0);
|
||||
expect(event.isDefaultPrevented()).to.be(false);
|
||||
expect(event.isPropagationStopped()).to.be(false);
|
||||
|
||||
var $link = findTestSubject(env.$el, 'appLink');
|
||||
expect($link.prop('href')).to.be(url);
|
||||
$link.trigger(event);
|
||||
|
||||
expect(env.location.reload.callCount).to.be(0);
|
||||
expect(event.isDefaultPrevented()).to.be(false);
|
||||
expect(event.isPropagationStopped()).to.be(true);
|
||||
});
|
||||
});
|
||||
|
||||
context('clicking a link that matches entire url', function () {
|
||||
var url = 'http://localhost:555/app/myApp#/lastSubUrl';
|
||||
beforeEach(setup(url, [
|
||||
{ url: url }
|
||||
]));
|
||||
|
||||
it('calls window.location.reload and prevents propogation', function () {
|
||||
var event = new $.Event('click');
|
||||
|
||||
expect(env.location.reload.callCount).to.be(0);
|
||||
expect(event.isDefaultPrevented()).to.be(false);
|
||||
expect(event.isPropagationStopped()).to.be(false);
|
||||
|
||||
var $link = findTestSubject(env.$el, 'appLink');
|
||||
expect($link.prop('href')).to.be(env.currentHref);
|
||||
$link.trigger(event);
|
||||
|
||||
expect(env.location.reload.callCount).to.be(1);
|
||||
expect(event.isDefaultPrevented()).to.be(false);
|
||||
expect(event.isPropagationStopped()).to.be(true);
|
||||
});
|
||||
});
|
||||
|
||||
context('clicking a link with matching href but changed hash', function () {
|
||||
var rootUrl = 'http://localhost:555/app/myApp?query=1';
|
||||
var url = rootUrl + '#/lastSubUrl2';
|
||||
|
||||
beforeEach(setup(url + '#/lastSubUrl', [
|
||||
{ url: url }
|
||||
]));
|
||||
|
||||
it('calls window.location.reload and prevents propogation', function () {
|
||||
var event = new $.Event('click');
|
||||
|
||||
expect(env.location.reload.callCount).to.be(0);
|
||||
expect(event.isDefaultPrevented()).to.be(false);
|
||||
expect(event.isPropagationStopped()).to.be(false);
|
||||
|
||||
var $link = findTestSubject(env.$el, 'appLink');
|
||||
expect($link.prop('href')).to.be(url);
|
||||
$link.trigger(event);
|
||||
|
||||
expect(env.location.reload.callCount).to.be(1);
|
||||
expect(event.isDefaultPrevented()).to.be(false);
|
||||
expect(event.isPropagationStopped()).to.be(true);
|
||||
});
|
||||
});
|
||||
|
||||
context('clicking a link with matching host', function () {
|
||||
beforeEach(setup('http://localhost:555/someOtherPath', [
|
||||
{
|
||||
active: true,
|
||||
url: 'http://localhost:555/app/myApp'
|
||||
}
|
||||
]));
|
||||
|
||||
it('allows click through', function () {
|
||||
var event = new $.Event('click');
|
||||
|
||||
expect(env.location.reload.callCount).to.be(0);
|
||||
expect(event.isPropagationStopped()).to.be(false);
|
||||
|
||||
findTestSubject(env.$el, 'appLink').trigger(event);
|
||||
|
||||
expect(env.location.reload.callCount).to.be(0);
|
||||
expect(event.isPropagationStopped()).to.be(false);
|
||||
});
|
||||
});
|
||||
|
||||
context('clicking a link with matching host and path', function () {
|
||||
beforeEach(setup('http://localhost:555/app/myApp?someQuery=true', [
|
||||
{
|
||||
active: true,
|
||||
url: 'http://localhost:555/app/myApp?differentQuery=true'
|
||||
}
|
||||
]));
|
||||
|
||||
it('allows click through', function () {
|
||||
var event = new $.Event('click');
|
||||
|
||||
expect(env.location.reload.callCount).to.be(0);
|
||||
expect(event.isPropagationStopped()).to.be(false);
|
||||
|
||||
findTestSubject(env.$el, 'appLink').trigger(event);
|
||||
|
||||
expect(env.location.reload.callCount).to.be(0);
|
||||
expect(event.isPropagationStopped()).to.be(false);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
|
@ -1,11 +1,18 @@
|
|||
<div class="app-links">
|
||||
<div class="app-link" ng-repeat="app in chrome.getNavLinks() | orderBy:'title'">
|
||||
<a ng-href="{{ app.lastSubUrl || app.url }}">
|
||||
<div
|
||||
class="app-link"
|
||||
ng-repeat="link in switcher.getNavLinks() | orderBy:'title'"
|
||||
ng-class="{ active: link.active }">
|
||||
|
||||
<div ng-if="app.icon" ng-style="{ 'background-image': 'url(../' + app.icon + ')' }" class="app-icon"></div>
|
||||
<div ng-if="!app.icon" class="app-icon app-icon-missing">{{app.title[0]}}</div>
|
||||
<a
|
||||
ng-click="switcher.ensureNavigation($event, link)"
|
||||
ng-href="{{ link.active ? link.url : (link.lastSubUrl || link.url) }}"
|
||||
data-test-subj="appLink">
|
||||
|
||||
<div class="app-title">{{ app.title }}</div>
|
||||
<div ng-if="link.icon" ng-style="{ 'background-image': 'url(../' + link.icon + ')' }" class="app-icon"></div>
|
||||
<div ng-if="!link.icon" class="app-icon app-icon-missing">{{ link.title[0] }}</div>
|
||||
|
||||
<div class="app-title">{{ link.title }}</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
|
51
src/ui/public/chrome/appSwitcher/appSwitcher.js
Normal file
51
src/ui/public/chrome/appSwitcher/appSwitcher.js
Normal file
|
@ -0,0 +1,51 @@
|
|||
var parse = require('url').parse;
|
||||
var bindKey = require('lodash').bindKey;
|
||||
|
||||
require('../appSwitcher/appSwitcher.less');
|
||||
var DomLocationProvider = require('ui/domLocation');
|
||||
|
||||
require('ui/modules')
|
||||
.get('kibana')
|
||||
.directive('appSwitcher', function () {
|
||||
return {
|
||||
restrict: 'E',
|
||||
template: require('./appSwitcher.html'),
|
||||
controllerAs: 'switcher',
|
||||
controller: function ($scope, Private) {
|
||||
var domLocation = Private(DomLocationProvider);
|
||||
|
||||
// since we render this in an isolate scope we can't "require: ^chrome", but
|
||||
// rather than remove all helpfull checks we can just check here.
|
||||
if (!$scope.chrome || !$scope.chrome.getNavLinks) {
|
||||
throw new TypeError('appSwitcher directive requires the "chrome" config-object');
|
||||
}
|
||||
|
||||
this.getNavLinks = bindKey($scope.chrome, 'getNavLinks');
|
||||
|
||||
// links don't cause full-navigation events in certain scenarios
|
||||
// so we force them when needed
|
||||
this.ensureNavigation = function (event, app) {
|
||||
if (event.isDefaultPrevented() || event.altKey || event.metaKey || event.ctrlKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
var toParsed = parse(event.delegateTarget.href);
|
||||
var fromParsed = parse(domLocation.href);
|
||||
var sameProto = toParsed.protocol === fromParsed.protocol;
|
||||
var sameHost = toParsed.host === fromParsed.host;
|
||||
var samePath = toParsed.path === fromParsed.path;
|
||||
|
||||
if (sameProto && sameHost && samePath) {
|
||||
toParsed.hash && domLocation.reload();
|
||||
|
||||
// event.preventDefault() keeps the browser from seeing the new url as an update
|
||||
// and even setting window.location does not mimic that behavior, so instead
|
||||
// we use stopPropagation() to prevent angular from seeing the click and
|
||||
// starting a digest cycle/attempting to handle it in the router.
|
||||
event.stopPropagation();
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
};
|
||||
});
|
|
@ -1,6 +1,7 @@
|
|||
@import (reference) "~ui/styles/variables";
|
||||
|
||||
@app-icon-size: 48px;
|
||||
@app-icon-padding: 10px;
|
||||
|
||||
.app-links {
|
||||
text-align: justify;
|
||||
|
@ -8,9 +9,11 @@
|
|||
.app-link {
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
width: @app-icon-size;
|
||||
margin: 0px 10px;
|
||||
text-align: left;
|
||||
width: @app-icon-size + (@app-icon-padding * 2);
|
||||
margin: 0px 10px;
|
||||
padding: @app-icon-padding;
|
||||
border-radius: @border-radius-base;
|
||||
|
||||
.app-icon {
|
||||
display: block;
|
||||
|
@ -43,6 +46,13 @@
|
|||
text-decoration: underline;
|
||||
}
|
||||
|
||||
&.active {
|
||||
background: @gray-lighter;
|
||||
.app-title {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
15
src/ui/public/domLocation.js
Normal file
15
src/ui/public/domLocation.js
Normal file
|
@ -0,0 +1,15 @@
|
|||
module.exports = function DomLocationProvider($window) {
|
||||
return {
|
||||
reload: function (forceFetch) {
|
||||
$window.location.reload(forceFetch);
|
||||
},
|
||||
|
||||
get href() {
|
||||
return $window.location.href;
|
||||
},
|
||||
|
||||
set href(val) {
|
||||
return ($window.location.href = val);
|
||||
}
|
||||
};
|
||||
};
|
Loading…
Add table
Add a link
Reference in a new issue