Merge pull request #4994 from spalger/fix/appSwitcher

Fix/app switcher
This commit is contained in:
Lukas Olson 2015-09-23 11:07:32 -07:00
commit 0b06d1a211
7 changed files with 333 additions and 9 deletions

View file

@ -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;

View file

@ -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;
}
});
};

View 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);
});
});
});

View file

@ -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>

View 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();
}
};
}
};
});

View file

@ -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;
}
}
}
}

View 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);
}
};
};