From 5d22b9351e828834d9fdac235c93c71eebd6bfd3 Mon Sep 17 00:00:00 2001 From: spalger Date: Thu, 3 Sep 2015 15:00:25 -0700 Subject: [PATCH 01/44] [chrome] Implement app-level lastPath We have always tracked the lastPath for tabs on the page, which means that clicking from one to the other restores your previous state. Now we are tracking the lastUrls for each app and using them in the appSwitcher so that the appSwitcher properly links to the last state of each app. --- src/ui/public/chrome/Tab.js | 2 ++ src/ui/public/chrome/TabCollection.js | 21 ++++++++++++++------- src/ui/public/chrome/api/angular.js | 10 +++++++++- 3 files changed, 25 insertions(+), 8 deletions(-) diff --git a/src/ui/public/chrome/Tab.js b/src/ui/public/chrome/Tab.js index 4abe392ea450..38f4aac4bcbf 100644 --- a/src/ui/public/chrome/Tab.js +++ b/src/ui/public/chrome/Tab.js @@ -1,4 +1,5 @@ var _ = require('lodash'); +var reEsc = require('lodash').escapeRegExp; var storage = window.sessionStorage; function Tab(spec) { @@ -8,6 +9,7 @@ function Tab(spec) { this.resetWhenActive = !!spec.resetWhenActive; this.lastUrlStoreKey = spec.trackLastPath ? 'lastUrl:' + this.id : null; this.rootUrl = '/' + this.id; + this.rootRegExp = new RegExp(`^${reEsc(this.rootUrl)}(/|$|\\?)`); this.lastUrl = this.lastUrlStoreKey && storage.getItem(this.lastUrlStoreKey); this.activeIndicatorColor = spec.activeIndicatorColor || null; diff --git a/src/ui/public/chrome/TabCollection.js b/src/ui/public/chrome/TabCollection.js index 5f9aa01384e3..650ff9aaa963 100644 --- a/src/ui/public/chrome/TabCollection.js +++ b/src/ui/public/chrome/TabCollection.js @@ -1,7 +1,10 @@ var _ = require('lodash'); +var { startsWith } = require('lodash'); var Tab = require('ui/chrome/Tab'); -var format = require('url').format; -var parse = _.wrap(require('url').parse, function (parse, path) { +var { format, parse } = require('url'); +var storage = window.sessionStorage; + +parse = _.wrap(parse, function (parse, path) { var parsed = parse(path, true); return { pathname: parsed.pathname, @@ -42,19 +45,23 @@ function TabCollection() { return activeTab; }; - this.consumeRouteUpdate = function ($location, persist) { - var url = parse($location.url(), true); - var id = $location.path().split('/')[1] || ''; + this.consumeRouteUpdate = function (appId, href, path, persist) { + var url = parse(href, true); tabs.forEach(function (tab) { - var active = tab.active = (tab.id === id); - var lastUrl = active ? url : parse(tab.lastUrl || tab.rootUrl); + tab.active = tab.rootRegExp.test(path); + + var lastUrl = tab.active ? url : parse(tab.lastUrl || tab.rootUrl); lastUrl.query._g = url.query._g; if (tab.active) activeTab = tab; if (persist) { tab.persistLastUrl(format(lastUrl)); } + + if (tab.active) { + storage.setItem(`appLastUrl:${appId}`, href); + } }); }; } diff --git a/src/ui/public/chrome/api/angular.js b/src/ui/public/chrome/api/angular.js index 31fc3f418df5..a1912501a998 100644 --- a/src/ui/public/chrome/api/angular.js +++ b/src/ui/public/chrome/api/angular.js @@ -44,7 +44,15 @@ module.exports = function (chrome, internals) { chrome.setVisible(!Boolean($location.search().embed)); // listen for route changes, propogate to tabs - var onRouteChange = _.bindKey(internals.tabs, 'consumeRouteUpdate', $location, chrome.getVisible()); + var onRouteChange = function () { + internals.tabs.consumeRouteUpdate( + chrome.getAppId(), + window.location.href, + $location.url(), + chrome.getVisible() + ); + }; + $rootScope.$on('$routeChangeSuccess', onRouteChange); $rootScope.$on('$routeUpdate', onRouteChange); onRouteChange(); From 89cbf82f4f9ff93a6246dfb53f34fa16a06e911f Mon Sep 17 00:00:00 2001 From: spalger Date: Fri, 4 Sep 2015 19:10:41 -0700 Subject: [PATCH 02/44] [chrome] added tests for Tab and TabCollection --- src/plugins/kibana/public/kibana.js | 2 +- src/ui/public/chrome/Tab.js | 23 +-- src/ui/public/chrome/TabCollection.js | 14 +- src/ui/public/chrome/__tests__/Tab.js | 148 ++++++++++++++++++ .../public/chrome/__tests__/TabCollection.js | 96 ++++++++++++ .../chrome/__tests__/_utils/TabFakeStore.js | 7 + src/ui/public/chrome/api/tabs.js | 2 +- 7 files changed, 272 insertions(+), 20 deletions(-) create mode 100644 src/ui/public/chrome/__tests__/Tab.js create mode 100644 src/ui/public/chrome/__tests__/TabCollection.js create mode 100644 src/ui/public/chrome/__tests__/_utils/TabFakeStore.js diff --git a/src/plugins/kibana/public/kibana.js b/src/plugins/kibana/public/kibana.js index c2251bfea5f9..6a3676f5fc6a 100644 --- a/src/plugins/kibana/public/kibana.js +++ b/src/plugins/kibana/public/kibana.js @@ -23,7 +23,7 @@ chrome .setNavBackground('#222222') .setTabDefaults({ resetWhenActive: true, - trackLastPath: true, + trackLastUrl: true, activeIndicatorColor: '#656a76' }) .setTabs([ diff --git a/src/ui/public/chrome/Tab.js b/src/ui/public/chrome/Tab.js index 38f4aac4bcbf..8879d1cc3a39 100644 --- a/src/ui/public/chrome/Tab.js +++ b/src/ui/public/chrome/Tab.js @@ -1,17 +1,11 @@ var _ = require('lodash'); var reEsc = require('lodash').escapeRegExp; -var storage = window.sessionStorage; -function Tab(spec) { +function Tab(spec = {}) { this.id = spec.id; this.title = spec.title; - this.active = false; this.resetWhenActive = !!spec.resetWhenActive; - this.lastUrlStoreKey = spec.trackLastPath ? 'lastUrl:' + this.id : null; - this.rootUrl = '/' + this.id; - this.rootRegExp = new RegExp(`^${reEsc(this.rootUrl)}(/|$|\\?)`); - this.lastUrl = this.lastUrlStoreKey && storage.getItem(this.lastUrlStoreKey); - + this.trackLastUrl = !!spec.trackLastUrl; this.activeIndicatorColor = spec.activeIndicatorColor || null; if (_.isFunction(this.activeIndicatorColor)) { // convert to a getter @@ -19,12 +13,21 @@ function Tab(spec) { get: this.activeIndicatorColor }); } + + this.active = false; + this.rootUrl = '/' + this.id; + this.rootRegExp = new RegExp(`^${reEsc(this.rootUrl)}(/|$|\\?|#)`); + this.store = spec.store || window.sessionStorage; + + this.lastUrlStoreKey = 'lastUrl:' + this.id; + this.lastUrl = this.trackLastUrl && this.store.getItem(this.lastUrlStoreKey); } Tab.prototype.persistLastUrl = function (url) { - if (!this.lastUrlStoreKey) return; + if (!this.trackLastUrl) return; + this.lastUrl = url; - storage.setItem(this.lastUrlStoreKey, this.lastUrl); + this.store.setItem(this.lastUrlStoreKey, this.lastUrl); }; Tab.prototype.href = function () { diff --git a/src/ui/public/chrome/TabCollection.js b/src/ui/public/chrome/TabCollection.js index 650ff9aaa963..223029475971 100644 --- a/src/ui/public/chrome/TabCollection.js +++ b/src/ui/public/chrome/TabCollection.js @@ -2,7 +2,6 @@ var _ = require('lodash'); var { startsWith } = require('lodash'); var Tab = require('ui/chrome/Tab'); var { format, parse } = require('url'); -var storage = window.sessionStorage; parse = _.wrap(parse, function (parse, path) { var parsed = parse(path, true); @@ -13,12 +12,13 @@ parse = _.wrap(parse, function (parse, path) { }; }); -function TabCollection() { +function TabCollection(opts = {}) { var tabs = []; var specs = null; - var defaults = null; + var defaults = {}; var activeTab = null; + var store = opts.store || window.sessionStorage; this.set = function (_specs) { specs = _.cloneDeep([].concat(_specs || [])); @@ -37,7 +37,7 @@ function TabCollection() { this._rebuildTabs = function () { _.invoke(this.get(), 'destroy'); tabs = _.map(specs, function (spec) { - return new Tab(_.defaults({}, spec, defaults)); + return new Tab(_.defaults({}, spec, defaults, { store })); }); }; @@ -58,11 +58,9 @@ function TabCollection() { if (persist) { tab.persistLastUrl(format(lastUrl)); } - - if (tab.active) { - storage.setItem(`appLastUrl:${appId}`, href); - } }); + + store.setItem(`appLastUrl:${appId}`, href); }; } diff --git a/src/ui/public/chrome/__tests__/Tab.js b/src/ui/public/chrome/__tests__/Tab.js new file mode 100644 index 000000000000..8ce94f9960fb --- /dev/null +++ b/src/ui/public/chrome/__tests__/Tab.js @@ -0,0 +1,148 @@ +let Tab = require('../Tab'); +let expect = require('expect.js'); +let TabFakeStore = require('./_utils/TabFakeStore'); + +describe('Chrome Tab', function () { + describe('construction', function () { + it('accepts id, title, resetWhenActive, trackLastUrl, activeIndicatorColor', function () { + let tab = new Tab({ + id: 'foo', + title: 'Foo App', + resetWhenActive: false, + trackLastUrl: true, + activeIndicatorColor: true + }); + + expect(tab.id).to.equal('foo'); + expect(tab.title).to.equal('Foo App'); + expect(tab.resetWhenActive).to.equal(false); + expect(tab.trackLastUrl).to.equal(true); + expect(tab.activeIndicatorColor).to.equal(true); + + tab = new Tab({ + id: 'bar', + title: 'Bar App', + resetWhenActive: true, + trackLastUrl: false, + activeIndicatorColor: false + }); + + expect(tab.id).to.equal('bar'); + expect(tab.title).to.equal('Bar App'); + expect(tab.resetWhenActive).to.equal(true); + expect(tab.trackLastUrl).to.equal(false); + expect(tab.activeIndicatorColor).to.equal(null); + }); + + it('starts inactive', function () { + let tab = new Tab(); + expect(tab.active).to.equal(false); + }); + + it('uses the id to set the rootUrl', function () { + let id = 'foo'; + let tab = new Tab({ id }); + expect(tab.id).to.equal(id); + expect(tab.rootUrl).to.equal(`/${id}`); + }); + + it('creates a regexp for matching the rootUrl', function () { + let tab = new Tab({ id: 'foo' }); + + expect('/foo').to.match(tab.rootRegExp); + expect('/foo/bar').to.match(tab.rootRegExp); + expect('/foo/bar/max').to.match(tab.rootRegExp); + expect('/foo?bar=baz').to.match(tab.rootRegExp); + expect('/foo/?bar=baz').to.match(tab.rootRegExp); + expect('/foo#?bar=baz').to.match(tab.rootRegExp); + + expect('/foobar').to.not.match(tab.rootRegExp); + expect('site.com/foo#?bar=baz').to.not.match(tab.rootRegExp); + expect('http://site.com/foo#?bar=baz').to.not.match(tab.rootRegExp); + }); + + it('accepts a function for activeIndicatorColor', function () { + let i = 0; + let tab = new Tab({ + activeIndicatorColor: function () { + return i++; + } + }); + expect(tab.activeIndicatorColor).to.equal(0); + expect(tab.activeIndicatorColor).to.equal(1); + expect(tab.activeIndicatorColor).to.equal(2); + expect(tab.activeIndicatorColor).to.equal(3); + }); + + it('discovers the lastUrl', function () { + let store = new TabFakeStore(); + let tab = new Tab({ id: 'foo', trackLastUrl: true, store }); + expect(tab.lastUrl).to.not.equal('bar'); + + tab.persistLastUrl('bar'); + expect(tab.lastUrl).to.equal('bar'); + + tab = new Tab({ id: 'foo', trackLastUrl: true, store }); + expect(tab.lastUrl).to.equal('bar'); + }); + }); + + + describe('#persistLastUrl', function () { + it('updates the lastUrl and storage value if trackLastUrl:true', function () { + let store = new TabFakeStore(); + let tab = new Tab({ id: 'foo', trackLastUrl: true, store }); + + tab.lastUrl = null; + tab.persistLastUrl('foo'); + expect(tab.lastUrl).to.equal('foo'); + expect(store.getItem(tab.lastUrlStoreKey)).to.equal('foo'); + }); + + it('is noop if trackLastUrl:false', function () { + let store = new TabFakeStore(); + let tab = new Tab({ id: 'foo', trackLastUrl: false, store }); + + expect(tab.lastUrl).to.equal(undefined); + tab.persistLastUrl('foo'); + expect(tab.lastUrl).to.equal(null); + expect(store.getItem(tab.lastUrlStoreKey)).to.equal(undefined); + }); + }); + + + describe('#href', function () { + it('returns the rootUrl/id be default', function () { + let tab = new Tab({ id: 'foo' }); + expect(tab.href()).to.equal(tab.rootUrl); + }); + + it('returns the lastUrl if tracking is on', function () { + let store = new TabFakeStore(); + let tab = new Tab({ id: 'foo', trackLastUrl: true, store }); + tab.persistLastUrl('okay'); + + expect(tab.href()).to.equal('okay'); + }); + + describe('when the tab is active', function () { + it('returns the rootUrl when resetWhenActive: true', function () { + let store = new TabFakeStore(); + let tab = new Tab({ id: 'foo', resetWhenActive: true, store }); + tab.active = true; + + expect(tab.href()).to.not.equal('butt'); + expect(tab.href()).to.equal(tab.rootUrl); + }); + + it('or returns undefined when not', function () { + let store = new TabFakeStore(); + let tab = new Tab({ id: 'foo', resetWhenActive: false, store }); + tab.active = true; + + expect(tab.href()).to.not.equal('butt'); + expect(tab.href()).to.equal(tab.rootUrl); + }); + }); + }); +}); diff --git a/src/ui/public/chrome/__tests__/TabCollection.js b/src/ui/public/chrome/__tests__/TabCollection.js new file mode 100644 index 000000000000..9751b23988fa --- /dev/null +++ b/src/ui/public/chrome/__tests__/TabCollection.js @@ -0,0 +1,96 @@ +import expect from 'expect.js'; +import { indexBy } from 'lodash'; + +import TabFakeStore from './_utils/TabFakeStore'; +import TabCollection from '../TabCollection'; +import Tab from '../Tab'; + +describe.only('Chrome TabCollection', function () { + describe('empty state', function () { + it('has no tabs', function () { + let tabs = new TabCollection(); + expect(tabs.get()).to.eql([]); + }); + + it('has no active tab', function () { + let tabs = new TabCollection(); + expect(!tabs.getActive()).to.equal(true); + }); + }); + + describe('#set()', function () { + it('consumes an ordered list of Tab specs', function () { + let tabs = new TabCollection(); + tabs.set([ + { id: 'foo' }, + { id: 'bar' } + ]); + + let ts = tabs.get(); + expect(ts.length).to.equal(2); + expect(ts[0].id).to.equal('foo'); + expect(ts[1].id).to.equal('bar'); + }); + }); + + describe('#setDefaults()', function () { + it('applies the defaults used to create tabs', function () { + let tabs = new TabCollection(); + tabs.setDefaults({ id: 'thing' }); + tabs.set([ {} ]); + + expect(tabs.get()[0].id).to.equal('thing'); + }); + + it('recreates existing tabs with new defaults', function () { + let tabs = new TabCollection(); + tabs.set([ {} ]); + expect(!tabs.get()[0].id).to.equal(true); + + tabs.setDefaults({ id: 'thing' }); + expect(tabs.get()[0].id).to.equal('thing'); + }); + }); + + describe('#consumeRouteUpdate()', function () { + it('updates the active tab', function () { + let store = new TabFakeStore(); + let tabs = new TabCollection({ store }); + tabs.set([ + { id: 'a' }, + { id: 'b' } + ]); + + tabs.consumeRouteUpdate('app', 'http://localhost:9200/a', '/a'); + let tabById = indexBy(tabs.get(), 'id'); + expect(tabById.a.active).to.equal(true); + expect(tabById.b.active).to.equal(false); + expect(tabs.getActive()).to.equal(tabById.a); + }); + + it('updates the last url of each tab with the current global state', function () { + let store = new TabFakeStore(); + let tabs = new TabCollection({ store }); + tabs.set([ + { id: 'a', trackLastUrl: true }, + { id: 'b', trackLastUrl: true } + ]); + let tabById = indexBy(tabs.get(), 'id'); + + expect(tabById.a.lastUrl).to.not.match(/_g=1/); + expect(tabById.b.lastUrl).to.not.match(/_g=1/); + tabs.consumeRouteUpdate('app', 'http://localhost:9200/a?_g=1', '/a', true); + expect(tabById.a.lastUrl).to.match(/_g=1/); + expect(tabById.b.lastUrl).to.match(/_g=1/); + }); + + it('stores the lastUrl for the entire app in a safe place', function () { + let store = new TabFakeStore(); + let tabs = new TabCollection({ store }); + let url = 'http://localhost:9200/a'; + tabs.consumeRouteUpdate('app', url, '/a'); + expect(store.getItem(`appLastUrl:app`)).to.equal(url); + }); + }); + +}); diff --git a/src/ui/public/chrome/__tests__/_utils/TabFakeStore.js b/src/ui/public/chrome/__tests__/_utils/TabFakeStore.js new file mode 100644 index 000000000000..c438d2462445 --- /dev/null +++ b/src/ui/public/chrome/__tests__/_utils/TabFakeStore.js @@ -0,0 +1,7 @@ +let store = Symbol('store'); + +export default class TabFakeStore { + constructor() { this[store] = new Map(); } + getItem(k) { return this[store].get(k); } + setItem(k, v) { return this[store].set(k, v); } +} diff --git a/src/ui/public/chrome/api/tabs.js b/src/ui/public/chrome/api/tabs.js index 0d30385bf146..19e2550af3ba 100644 --- a/src/ui/public/chrome/api/tabs.js +++ b/src/ui/public/chrome/api/tabs.js @@ -26,7 +26,7 @@ module.exports = function (chrome, internals) { * when the the tab is considered active, should clicking it * cause a redirect to just the id? * - * trackLastPath {boolean} + * trackLastUrl {boolean} * When this tab is active, should the current path be tracked * and persisted to session storage, then used as the tabs href attribute when the user navigates * away from the tab? From e683e0adc035452fcab0e512349ab43fc1a7aa92 Mon Sep 17 00:00:00 2001 From: spalger Date: Fri, 4 Sep 2015 19:27:23 -0700 Subject: [PATCH 03/44] [chrome] move appLastUrl set/get into more appropriate place --- src/ui/public/chrome/TabCollection.js | 2 -- .../public/chrome/__tests__/TabCollection.js | 10 +------- src/ui/public/chrome/api/angular.js | 13 ++++++----- src/ui/public/chrome/api/apps.js | 23 ++++++++++++++----- 4 files changed, 25 insertions(+), 23 deletions(-) diff --git a/src/ui/public/chrome/TabCollection.js b/src/ui/public/chrome/TabCollection.js index 223029475971..e79f453874b1 100644 --- a/src/ui/public/chrome/TabCollection.js +++ b/src/ui/public/chrome/TabCollection.js @@ -59,8 +59,6 @@ function TabCollection(opts = {}) { tab.persistLastUrl(format(lastUrl)); } }); - - store.setItem(`appLastUrl:${appId}`, href); }; } diff --git a/src/ui/public/chrome/__tests__/TabCollection.js b/src/ui/public/chrome/__tests__/TabCollection.js index 9751b23988fa..e46d5c5f17ee 100644 --- a/src/ui/public/chrome/__tests__/TabCollection.js +++ b/src/ui/public/chrome/__tests__/TabCollection.js @@ -5,7 +5,7 @@ import TabFakeStore from './_utils/TabFakeStore'; import TabCollection from '../TabCollection'; import Tab from '../Tab'; -describe.only('Chrome TabCollection', function () { +describe('Chrome TabCollection', function () { describe('empty state', function () { it('has no tabs', function () { let tabs = new TabCollection(); @@ -83,14 +83,6 @@ describe.only('Chrome TabCollection', function () { expect(tabById.a.lastUrl).to.match(/_g=1/); expect(tabById.b.lastUrl).to.match(/_g=1/); }); - - it('stores the lastUrl for the entire app in a safe place', function () { - let store = new TabFakeStore(); - let tabs = new TabCollection({ store }); - let url = 'http://localhost:9200/a'; - tabs.consumeRouteUpdate('app', url, '/a'); - expect(store.getItem(`appLastUrl:app`)).to.equal(url); - }); }); }); diff --git a/src/ui/public/chrome/api/angular.js b/src/ui/public/chrome/api/angular.js index a1912501a998..0c14ca6a4d97 100644 --- a/src/ui/public/chrome/api/angular.js +++ b/src/ui/public/chrome/api/angular.js @@ -45,12 +45,13 @@ module.exports = function (chrome, internals) { // listen for route changes, propogate to tabs var onRouteChange = function () { - internals.tabs.consumeRouteUpdate( - chrome.getAppId(), - window.location.href, - $location.url(), - chrome.getVisible() - ); + let appId = chrome.getAppId(); + let { href } = window.location; + let path = $location.url(); + let persist = chrome.getVisible(); + + internals.tabs.consumeRouteUpdate(appId, href, path, persist); + chrome.setLastUrlFor(chrome.getAppId(), href); }; $rootScope.$on('$routeChangeSuccess', onRouteChange); diff --git a/src/ui/public/chrome/api/apps.js b/src/ui/public/chrome/api/apps.js index b5432408f4cf..dfba49071e32 100644 --- a/src/ui/public/chrome/api/apps.js +++ b/src/ui/public/chrome/api/apps.js @@ -1,7 +1,9 @@ -var _ = require('lodash'); +let { clone, get } = require('lodash'); module.exports = function (chrome, internals) { + internals.appUrlStore = internals.appUrlStore || window.sessionStorage; + /** * ui/chrome apps API * @@ -20,20 +22,29 @@ module.exports = function (chrome, internals) { }; chrome.getApp = function () { - return _.clone(internals.app); + return clone(internals.app); }; chrome.getAppTitle = function () { - return internals.app.title; + return get(internals, ['app', 'title']); }; chrome.getAppId = function () { - return internals.app.id; + return get(internals, ['app', 'id']); }; chrome.getInjected = function (name, def) { - if (name == null) return _.clone(internals.vars) || {}; - return _.get(internals.vars, name, def); + if (name == null) return clone(internals.vars) || {}; + return get(internals.vars, name, def); }; + chrome.getLastUrlFor = function (appId) { + return internals.appUrlStore.getItem(`appLastUrl:${appId}`); + }; + + chrome.setLastUrlFor = function (appId, url) { + internals.appUrlStore.setItem(`appLastUrl:${appId}`, url); + }; + + }; From bfd3f28b8174131ba2fa590d307926d0ecb8ec03 Mon Sep 17 00:00:00 2001 From: spalger Date: Fri, 4 Sep 2015 20:09:11 -0700 Subject: [PATCH 04/44] [chrome] added tests for apps API --- .../public/chrome/__tests__/TabCollection.js | 10 +- .../chrome/__tests__/_utils/TabFakeStore.js | 2 + src/ui/public/chrome/__tests__/api/apps.js | 160 ++++++++++++++++++ 3 files changed, 167 insertions(+), 5 deletions(-) create mode 100644 src/ui/public/chrome/__tests__/api/apps.js diff --git a/src/ui/public/chrome/__tests__/TabCollection.js b/src/ui/public/chrome/__tests__/TabCollection.js index e46d5c5f17ee..eb66b2b5cf97 100644 --- a/src/ui/public/chrome/__tests__/TabCollection.js +++ b/src/ui/public/chrome/__tests__/TabCollection.js @@ -1,9 +1,9 @@ -import expect from 'expect.js'; -import { indexBy } from 'lodash'; +let expect = require('expect.js'); +let { indexBy } = require('lodash'); -import TabFakeStore from './_utils/TabFakeStore'; -import TabCollection from '../TabCollection'; -import Tab from '../Tab'; +let TabFakeStore = require('./_utils/TabFakeStore'); +let TabCollection = require('../TabCollection'); +let Tab = require('../Tab'); describe('Chrome TabCollection', function () { describe('empty state', function () { diff --git a/src/ui/public/chrome/__tests__/_utils/TabFakeStore.js b/src/ui/public/chrome/__tests__/_utils/TabFakeStore.js index c438d2462445..680eec7522ea 100644 --- a/src/ui/public/chrome/__tests__/_utils/TabFakeStore.js +++ b/src/ui/public/chrome/__tests__/_utils/TabFakeStore.js @@ -4,4 +4,6 @@ export default class TabFakeStore { constructor() { this[store] = new Map(); } getItem(k) { return this[store].get(k); } setItem(k, v) { return this[store].set(k, v); } + getKeys() { return [ ...this[store].keys() ]; } + getValues() { return [ ...this[store].values() ]; } } diff --git a/src/ui/public/chrome/__tests__/api/apps.js b/src/ui/public/chrome/__tests__/api/apps.js new file mode 100644 index 000000000000..c57f33c3fe6b --- /dev/null +++ b/src/ui/public/chrome/__tests__/api/apps.js @@ -0,0 +1,160 @@ +let expect = require('expect.js'); + +let setup = require('../../api/apps'); +let TabFakeStore = require('../_utils/TabFakeStore'); + +describe('Chrome API :: apps', function () { + describe('#get/setShowAppsLink()', function () { + describe('defaults to false if there are less than two apps', function () { + it('appCount = 0', function () { + let chrome = {}; + setup(chrome, { appCount: 0 }); + expect(chrome.getShowAppsLink()).to.equal(false); + }); + + it('appCount = 1', function () { + let chrome = {}; + setup(chrome, { appCount: 1 }); + expect(chrome.getShowAppsLink()).to.equal(false); + }); + }); + + describe('defaults to true if there are two or more apps', function () { + it('appCount = 2', function () { + let chrome = {}; + setup(chrome, { appCount: 2 }); + expect(chrome.getShowAppsLink()).to.equal(true); + }); + + it('appCount = 3', function () { + let chrome = {}; + setup(chrome, { appCount: 3 }); + expect(chrome.getShowAppsLink()).to.equal(true); + }); + }); + + it('is chainable', function () { + let chrome = {}; + setup(chrome, { appCount: 1 }); + expect(chrome.setShowAppsLink(true)).to.equal(chrome); + }); + + it('can be changed', function () { + let chrome = {}; + setup(chrome, { appCount: 1 }); + + expect(chrome.setShowAppsLink(true).getShowAppsLink()).to.equal(true); + expect(chrome.getShowAppsLink()).to.equal(true); + + expect(chrome.setShowAppsLink(false).getShowAppsLink()).to.equal(false); + expect(chrome.getShowAppsLink()).to.equal(false); + }); + }); + + describe('#getApp()', function () { + it('returns a clone of the current app', function () { + let chrome = {}; + let app = { 1: 2 }; + setup(chrome, { app }); + + expect(chrome.getApp()).to.eql(app); + expect(chrome.getApp()).to.not.equal(app); + }); + + it('returns undefined if no active app', function () { + let chrome = {}; + setup(chrome, {}); + expect(chrome.getApp()).to.equal(undefined); + }); + }); + + describe('#getAppTitle()', function () { + it('returns the title property of the current app', function () { + let chrome = {}; + let app = { title: 'foo' }; + setup(chrome, { app }); + expect(chrome.getAppTitle()).to.eql('foo'); + }); + + it('returns undefined if no active app', function () { + let chrome = {}; + setup(chrome, {}); + expect(chrome.getAppTitle()).to.equal(undefined); + }); + }); + + describe('#getAppId()', function () { + it('returns the id property of the current app', function () { + let chrome = {}; + let app = { id: 'foo' }; + setup(chrome, { app }); + expect(chrome.getAppId()).to.eql('foo'); + }); + + it('returns undefined if no active app', function () { + let chrome = {}; + setup(chrome, {}); + expect(chrome.getAppId()).to.equal(undefined); + }); + }); + + describe('#getInjected()', function () { + describe('called without args', function () { + it('returns a clone of all injectedVars', function () { + let chrome = {}; + let vars = { name: 'foo' }; + setup(chrome, { vars }); + expect(chrome.getInjected()).to.eql(vars); + expect(chrome.getInjected()).to.not.equal(vars); + }); + }); + + describe('called with a var name', function () { + it('returns the var at that name', function () { + let chrome = {}; + let vars = { name: 'foo' }; + setup(chrome, { vars }); + expect(chrome.getInjected('name')).to.equal('foo'); + }); + }); + + describe('called with a var name and default', function () { + it('returns the default when the var is undefined', function () { + let chrome = {}; + let vars = { name: undefined }; + setup(chrome, { vars }); + expect(chrome.getInjected('name', 'bar')).to.equal('bar'); + }); + + it('returns null when the var is null', function () { + let chrome = {}; + let vars = { name: null }; + setup(chrome, { vars }); + expect(chrome.getInjected('name', 'bar')).to.equal(null); + }); + + it('returns var if not undefined', function () { + let chrome = {}; + let vars = { name: 'kim' }; + setup(chrome, { vars }); + expect(chrome.getInjected('name', 'bar')).to.equal('kim'); + }); + }); + + describe('#get/setLastUrlFor()', function () { + it('reads/writes last url from storage', function () { + let chrome = {}; + let store = new TabFakeStore(); + setup(chrome, { appUrlStore: store }); + expect(chrome.getLastUrlFor('app')).to.equal(undefined); + chrome.setLastUrlFor('app', 'url'); + expect(chrome.getLastUrlFor('app')).to.equal('url'); + expect(store.getKeys().length).to.equal(1); + expect(store.getValues().shift()).to.equal('url'); + }); + }); + }); + + + +}); From 215ddf1833731cf1f6f84626f8f1e08280c2e885 Mon Sep 17 00:00:00 2001 From: spalger Date: Fri, 11 Sep 2015 14:19:09 -0700 Subject: [PATCH 05/44] [chrome/tabs] only track the path when persisting tab.lastUrl --- src/ui/public/chrome/TabCollection.js | 8 ++++---- src/ui/public/chrome/api/angular.js | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/ui/public/chrome/TabCollection.js b/src/ui/public/chrome/TabCollection.js index e79f453874b1..95dd6c323618 100644 --- a/src/ui/public/chrome/TabCollection.js +++ b/src/ui/public/chrome/TabCollection.js @@ -45,14 +45,14 @@ function TabCollection(opts = {}) { return activeTab; }; - this.consumeRouteUpdate = function (appId, href, path, persist) { - var url = parse(href, true); + this.consumeRouteUpdate = function (appId, path, persist) { + var currentUrl = parse(path, true); tabs.forEach(function (tab) { tab.active = tab.rootRegExp.test(path); - var lastUrl = tab.active ? url : parse(tab.lastUrl || tab.rootUrl); - lastUrl.query._g = url.query._g; + var lastUrl = tab.active ? currentUrl : parse(tab.lastUrl || tab.rootUrl); + lastUrl.query._g = currentUrl.query._g; if (tab.active) activeTab = tab; if (persist) { diff --git a/src/ui/public/chrome/api/angular.js b/src/ui/public/chrome/api/angular.js index 0c14ca6a4d97..7ae841265165 100644 --- a/src/ui/public/chrome/api/angular.js +++ b/src/ui/public/chrome/api/angular.js @@ -50,8 +50,8 @@ module.exports = function (chrome, internals) { let path = $location.url(); let persist = chrome.getVisible(); - internals.tabs.consumeRouteUpdate(appId, href, path, persist); - chrome.setLastUrlFor(chrome.getAppId(), href); + internals.trackPossibleSubUrl(href); + internals.tabs.consumeRouteUpdate(appId, path, persist); }; $rootScope.$on('$routeChangeSuccess', onRouteChange); From 5de8a96840a69d6ad1e4951fe44ecf3994d298fd Mon Sep 17 00:00:00 2001 From: spalger Date: Fri, 11 Sep 2015 21:15:16 -0700 Subject: [PATCH 06/44] [chrome/tabs] properly track the last url of each tab, added tests --- src/plugins/kibana/public/kibana.js | 2 +- src/ui/public/chrome/Tab.js | 99 +++++++---- src/ui/public/chrome/TabCollection.js | 42 ++--- src/ui/public/chrome/__tests__/Tab.js | 167 ++++++++++++++---- .../public/chrome/__tests__/TabCollection.js | 33 +--- .../TabFakeStore.js => _TabFakeStore.js} | 0 .../{__tests__/api => api/__tests__}/apps.js | 14 +- src/ui/public/chrome/api/angular.js | 5 +- src/ui/public/chrome/api/apps.js | 9 +- src/ui/public/chrome/api/tabs.js | 7 + src/ui/public/chrome/chrome.js | 10 +- 11 files changed, 243 insertions(+), 145 deletions(-) rename src/ui/public/chrome/__tests__/{_utils/TabFakeStore.js => _TabFakeStore.js} (100%) rename src/ui/public/chrome/{__tests__/api => api/__tests__}/apps.js (93%) diff --git a/src/plugins/kibana/public/kibana.js b/src/plugins/kibana/public/kibana.js index 6a3676f5fc6a..5a2c325c1808 100644 --- a/src/plugins/kibana/public/kibana.js +++ b/src/plugins/kibana/public/kibana.js @@ -23,7 +23,7 @@ chrome .setNavBackground('#222222') .setTabDefaults({ resetWhenActive: true, - trackLastUrl: true, + lastUrlStore: window.sessionStore, activeIndicatorColor: '#656a76' }) .setTabs([ diff --git a/src/ui/public/chrome/Tab.js b/src/ui/public/chrome/Tab.js index 8879d1cc3a39..496386a4eca4 100644 --- a/src/ui/public/chrome/Tab.js +++ b/src/ui/public/chrome/Tab.js @@ -1,42 +1,71 @@ var _ = require('lodash'); var reEsc = require('lodash').escapeRegExp; +var { parse, format } = require('url'); -function Tab(spec = {}) { - this.id = spec.id; - this.title = spec.title; - this.resetWhenActive = !!spec.resetWhenActive; - this.trackLastUrl = !!spec.trackLastUrl; - this.activeIndicatorColor = spec.activeIndicatorColor || null; - if (_.isFunction(this.activeIndicatorColor)) { - // convert to a getter - Object.defineProperty(this, 'activeIndicatorColor', { - get: this.activeIndicatorColor - }); +const urlJoin = (a, b) => { + if (!b) return a; + return `${a}${ a.endsWith('/') ? '' : '/' }${b}`; +}; + +export default class Tab { + constructor(spec = {}) { + this.id = spec.id || ''; + this.title = spec.title || ''; + this.resetWhenActive = !!spec.resetWhenActive; + this.activeIndicatorColor = spec.activeIndicatorColor || null; + if (_.isFunction(this.activeIndicatorColor)) { + // convert to a getter + Object.defineProperty(this, 'activeIndicatorColor', { + get: this.activeIndicatorColor + }); + } + + this.active = false; + + this.baseUrl = spec.baseUrl || '/'; + this.rootUrl = urlJoin(this.baseUrl, this.id); + this.rootRegExp = new RegExp(`^${reEsc(this.rootUrl)}(/|$|\\?|#)`); + + this.lastUrlStoreKey = `lastUrl:${this.id}`; + this.lastUrlStore = spec.lastUrlStore; + this.lastUrl = this.lastUrlStore ? this.lastUrlStore.getItem(this.lastUrlStoreKey) : null; } - this.active = false; - this.rootUrl = '/' + this.id; - this.rootRegExp = new RegExp(`^${reEsc(this.rootUrl)}(/|$|\\?|#)`); - this.store = spec.store || window.sessionStorage; + href() { + if (this.active) { + return this.resetWhenActive ? this.rootUrl : null; + } + return this.lastUrl || this.rootUrl; + } - this.lastUrlStoreKey = 'lastUrl:' + this.id; - this.lastUrl = this.trackLastUrl && this.store.getItem(this.lastUrlStoreKey); + updateLastUrlGlobalState(globalState) { + let lastPath = this.getLastPath(); + let { pathname, query, hash } = parse(lastPath, true); + + query = query || {}; + if (!globalState) delete query._g; + else query._g = globalState; + + this.setLastUrl(`${this.rootUrl}${format({ pathname, query, hash })}`); + } + + getLastPath() { + let { id, rootUrl } = this; + let lastUrl = this.getLastUrl(); + + if (!lastUrl.startsWith(rootUrl)) { + throw new Error(`Tab "${id}" has invalid root "${rootUrl}" for last url "${lastUrl}"`); + } + + return lastUrl.slice(rootUrl.length); + } + + setLastUrl(url) { + this.lastUrl = url; + if (this.lastUrlStore) this.lastUrlStore.setItem(this.lastUrlStoreKey, this.lastUrl); + } + + getLastUrl() { + return this.lastUrl || this.rootUrl; + } } - -Tab.prototype.persistLastUrl = function (url) { - if (!this.trackLastUrl) return; - - this.lastUrl = url; - this.store.setItem(this.lastUrlStoreKey, this.lastUrl); -}; - -Tab.prototype.href = function () { - if (this.active) { - if (this.resetWhenActive) return '#' + this.rootUrl; - return null; - } - - return '#' + (this.lastUrl || this.rootUrl); -}; - -module.exports = Tab; diff --git a/src/ui/public/chrome/TabCollection.js b/src/ui/public/chrome/TabCollection.js index 95dd6c323618..7287d1a0e7b6 100644 --- a/src/ui/public/chrome/TabCollection.js +++ b/src/ui/public/chrome/TabCollection.js @@ -1,24 +1,13 @@ var _ = require('lodash'); -var { startsWith } = require('lodash'); +var { startsWith, get, set, omit, wrap, pick } = require('lodash'); var Tab = require('ui/chrome/Tab'); -var { format, parse } = require('url'); - -parse = _.wrap(parse, function (parse, path) { - var parsed = parse(path, true); - return { - pathname: parsed.pathname, - query: parsed.query || {}, - hash: parsed.hash - }; -}); +var { parse } = require('url'); function TabCollection(opts = {}) { - var tabs = []; var specs = null; - var defaults = {}; + var defaults = opts.defaults || {}; var activeTab = null; - var store = opts.store || window.sessionStorage; this.set = function (_specs) { specs = _.cloneDeep([].concat(_specs || [])); @@ -26,7 +15,7 @@ function TabCollection(opts = {}) { }; this.setDefaults = function () { - defaults = _.clone(arguments[0]); + defaults = _.defaults({}, arguments[0], defaults); this._rebuildTabs(); }; @@ -37,7 +26,7 @@ function TabCollection(opts = {}) { this._rebuildTabs = function () { _.invoke(this.get(), 'destroy'); tabs = _.map(specs, function (spec) { - return new Tab(_.defaults({}, spec, defaults, { store })); + return new Tab(_.defaults({}, spec, defaults)); }); }; @@ -45,20 +34,19 @@ function TabCollection(opts = {}) { return activeTab; }; - this.consumeRouteUpdate = function (appId, path, persist) { - var currentUrl = parse(path, true); - + this.consumeRouteUpdate = function (href, persist) { tabs.forEach(function (tab) { - tab.active = tab.rootRegExp.test(path); - - var lastUrl = tab.active ? currentUrl : parse(tab.lastUrl || tab.rootUrl); - lastUrl.query._g = currentUrl.query._g; - - if (tab.active) activeTab = tab; - if (persist) { - tab.persistLastUrl(format(lastUrl)); + tab.active = tab.rootRegExp.test(href); + if (tab.active) { + activeTab = tab; + activeTab.setLastUrl(href); } }); + + if (!persist || !activeTab) return; + + let globalState = get(parse(activeTab.getLastPath(), true), 'query._g'); + tabs.forEach(tab => tab.updateLastUrlGlobalState(globalState)); }; } diff --git a/src/ui/public/chrome/__tests__/Tab.js b/src/ui/public/chrome/__tests__/Tab.js index 8ce94f9960fb..49ea525a53dd 100644 --- a/src/ui/public/chrome/__tests__/Tab.js +++ b/src/ui/public/chrome/__tests__/Tab.js @@ -1,37 +1,37 @@ let Tab = require('../Tab'); let expect = require('expect.js'); -let TabFakeStore = require('./_utils/TabFakeStore'); +let TabFakeStore = require('./_TabFakeStore'); -describe('Chrome Tab', function () { +describe.only('Chrome Tab', function () { describe('construction', function () { - it('accepts id, title, resetWhenActive, trackLastUrl, activeIndicatorColor', function () { + it('accepts id, title, resetWhenActive, trackLastUrl, activeIndicatorColor, baseUrl', function () { let tab = new Tab({ id: 'foo', title: 'Foo App', resetWhenActive: false, - trackLastUrl: true, - activeIndicatorColor: true + activeIndicatorColor: true, + baseUrl: 'proto:host.domain:999' }); expect(tab.id).to.equal('foo'); expect(tab.title).to.equal('Foo App'); expect(tab.resetWhenActive).to.equal(false); - expect(tab.trackLastUrl).to.equal(true); expect(tab.activeIndicatorColor).to.equal(true); + expect(tab.rootUrl).to.equal('proto:host.domain:999/foo'); tab = new Tab({ id: 'bar', title: 'Bar App', resetWhenActive: true, - trackLastUrl: false, - activeIndicatorColor: false + activeIndicatorColor: false, + baseUrl: 'proto:host.domain:999/sub/#/' }); expect(tab.id).to.equal('bar'); expect(tab.title).to.equal('Bar App'); expect(tab.resetWhenActive).to.equal(true); - expect(tab.trackLastUrl).to.equal(false); expect(tab.activeIndicatorColor).to.equal(null); + expect(tab.rootUrl).to.equal('proto:host.domain:999/sub/#/bar'); }); it('starts inactive', function () { @@ -61,6 +61,18 @@ describe('Chrome Tab', function () { expect('http://site.com/foo#?bar=baz').to.not.match(tab.rootRegExp); }); + it('includes the baseUrl in the rootRegExp if specified', function () { + let tab = new Tab({ + id: 'foo', + baseUrl: 'http://spiderman.com/kibana' + }); + + expect('http://spiderman.com/kibana/foo/bar').to.match(tab.rootRegExp); + + expect('/foo').to.not.match(tab.rootRegExp); + expect('https://spiderman.com/kibana/foo/bar').to.not.match(tab.rootRegExp); + }); + it('accepts a function for activeIndicatorColor', function () { let i = 0; let tab = new Tab({ @@ -75,73 +87,154 @@ describe('Chrome Tab', function () { }); it('discovers the lastUrl', function () { - let store = new TabFakeStore(); - let tab = new Tab({ id: 'foo', trackLastUrl: true, store }); + let lastUrlStore = new TabFakeStore(); + let tab = new Tab({ id: 'foo', lastUrlStore }); expect(tab.lastUrl).to.not.equal('bar'); - tab.persistLastUrl('bar'); + tab.setLastUrl('bar'); expect(tab.lastUrl).to.equal('bar'); - tab = new Tab({ id: 'foo', trackLastUrl: true, store }); + tab = new Tab({ id: 'foo', lastUrlStore }); expect(tab.lastUrl).to.equal('bar'); }); }); - describe('#persistLastUrl', function () { - it('updates the lastUrl and storage value if trackLastUrl:true', function () { - let store = new TabFakeStore(); - let tab = new Tab({ id: 'foo', trackLastUrl: true, store }); + describe('#setLastUrl()', function () { + it('updates the lastUrl and storage value if passed a lastUrlStore', function () { + let lastUrlStore = new TabFakeStore(); + let tab = new Tab({ id: 'foo', lastUrlStore }); - tab.lastUrl = null; - tab.persistLastUrl('foo'); + expect(tab.lastUrl).to.not.equal('foo'); + tab.setLastUrl('foo'); expect(tab.lastUrl).to.equal('foo'); - expect(store.getItem(tab.lastUrlStoreKey)).to.equal('foo'); + expect(lastUrlStore.getItem(tab.lastUrlStoreKey)).to.equal('foo'); }); - it('is noop if trackLastUrl:false', function () { - let store = new TabFakeStore(); - let tab = new Tab({ id: 'foo', trackLastUrl: false, store }); + it('only updates lastUrl if no lastUrlStore', function () { + let tab = new Tab({ id: 'foo' }); - expect(tab.lastUrl).to.equal(undefined); - tab.persistLastUrl('foo'); expect(tab.lastUrl).to.equal(null); - expect(store.getItem(tab.lastUrlStoreKey)).to.equal(undefined); + tab.setLastUrl('foo'); + expect(tab.lastUrl).to.equal('foo'); + + tab = new Tab({ id: 'foo' }); + expect(tab.lastUrl).to.not.equal('foo'); }); }); - describe('#href', function () { + describe('#href()', function () { it('returns the rootUrl/id be default', function () { let tab = new Tab({ id: 'foo' }); expect(tab.href()).to.equal(tab.rootUrl); }); it('returns the lastUrl if tracking is on', function () { - let store = new TabFakeStore(); - let tab = new Tab({ id: 'foo', trackLastUrl: true, store }); - tab.persistLastUrl('okay'); - + let tab = new Tab({ id: 'foo' }); + tab.setLastUrl('okay'); expect(tab.href()).to.equal('okay'); }); describe('when the tab is active', function () { it('returns the rootUrl when resetWhenActive: true', function () { - let store = new TabFakeStore(); - let tab = new Tab({ id: 'foo', resetWhenActive: true, store }); + let id = 'foo'; + let resetWhenActive = true; + let tab = new Tab({ id, resetWhenActive }); + tab.active = true; expect(tab.href()).to.not.equal('butt'); expect(tab.href()).to.equal(tab.rootUrl); }); - it('or returns undefined when not', function () { - let store = new TabFakeStore(); - let tab = new Tab({ id: 'foo', resetWhenActive: false, store }); + it('or returns null when not', function () { + let tab = new Tab({ id: 'foo', resetWhenActive: false }); tab.active = true; expect(tab.href()).to.not.equal('butt'); - expect(tab.href()).to.equal(tab.rootUrl); + expect(tab.href()).to.equal(null); + }); + }); + }); + + describe('#getLastPath()', function () { + it('parses a path out of the lastUrl by removing the baseUrl', function () { + let baseUrl = 'http://local:5601/app/visualize#'; + let tab = new Tab({ baseUrl }); + + tab.setLastUrl('http://local:5601/app/visualize#/index'); + expect(tab.getLastPath()).to.equal('/index'); + }); + + it('throws an error if the lastUrl does not extend the root url', function () { + expect(function () { + let baseUrl = 'http://local:5601/app/visualize#'; + let tab = new Tab({ baseUrl }); + + tab.setLastUrl('http://local:5601/'); + tab.getLastPath(); + }).to.throwError(/invalid.*root/); + }); + }); + + describe('updateLastUrlGlobalState', function () { + let bases = [ + 'http://local:5601', + '', + 'weird.domain/with/subpath?path#', + 'weird.domain/with/#hashpath', + ]; + + context('with new state sets _g properly', function () { + let paths = [ + [ '/', '/?_g=newState' ], + [ '/?first', '/?first=&_g=newState' ], + [ '/path?first=1&_g=afterHash', '/path?first=1&_g=newState' ], + [ '/?first=1&_g=second', '/?first=1&_g=newState' ], + [ '/?g=first', '/?g=first&_g=newState' ], + [ '/a?first=1&_g=second', '/a?first=1&_g=newState' ], + [ '/?first=1&_g=second', '/?first=1&_g=newState' ], + [ '/?first&g=second', '/?first=&g=second&_g=newState' ], + ]; + + bases.forEach(baseUrl => { + paths.forEach(([pathFrom, pathTo]) => { + let fromUrl = `${baseUrl}${pathFrom}`; + let toUrl = `${baseUrl}${pathTo}`; + it(`${fromUrl} => ${toUrl}`, function () { + let tab = new Tab({ baseUrl }); + tab.setLastUrl(fromUrl); + tab.updateLastUrlGlobalState('newState'); + expect(tab.getLastUrl()).to.equal(toUrl); + }); + }); + }); + }); + + context('with new empty state removes _g', function () { + let paths = [ + [ '/', '/' ], + [ '/?first', '/?first=' ], + [ '/path?first=1&_g=afterHash', '/path?first=1' ], + [ '/?first=1&_g=second', '/?first=1' ], + [ '/?g=first', '/?g=first' ], + [ '/a?first=1&_g=second', '/a?first=1' ], + [ '/?first=1&_g=second', '/?first=1' ], + [ '/?first&g=second', '/?first=&g=second' ], + ]; + + bases.forEach(baseUrl => { + paths.forEach(([pathFrom, pathTo]) => { + let fromUrl = `${baseUrl}${pathFrom}`; + let toUrl = `${baseUrl}${pathTo}`; + it(`${fromUrl}`, function () { + let tab = new Tab({ baseUrl }); + tab.setLastUrl(fromUrl); + tab.updateLastUrlGlobalState(); + expect(tab.getLastUrl()).to.equal(toUrl); + }); + }); }); }); }); diff --git a/src/ui/public/chrome/__tests__/TabCollection.js b/src/ui/public/chrome/__tests__/TabCollection.js index eb66b2b5cf97..812eb20e4cff 100644 --- a/src/ui/public/chrome/__tests__/TabCollection.js +++ b/src/ui/public/chrome/__tests__/TabCollection.js @@ -1,7 +1,7 @@ let expect = require('expect.js'); -let { indexBy } = require('lodash'); +let { indexBy, random } = require('lodash'); -let TabFakeStore = require('./_utils/TabFakeStore'); +let TabFakeStore = require('./_TabFakeStore'); let TabCollection = require('../TabCollection'); let Tab = require('../Tab'); @@ -55,33 +55,18 @@ describe('Chrome TabCollection', function () { describe('#consumeRouteUpdate()', function () { it('updates the active tab', function () { let store = new TabFakeStore(); - let tabs = new TabCollection({ store }); + let baseUrl = `http://localhost:${random(1000, 9999)}`; + let tabs = new TabCollection({ store, defaults: { baseUrl } }); tabs.set([ { id: 'a' }, { id: 'b' } ]); - tabs.consumeRouteUpdate('app', 'http://localhost:9200/a', '/a'); - let tabById = indexBy(tabs.get(), 'id'); - expect(tabById.a.active).to.equal(true); - expect(tabById.b.active).to.equal(false); - expect(tabs.getActive()).to.equal(tabById.a); - }); - - it('updates the last url of each tab with the current global state', function () { - let store = new TabFakeStore(); - let tabs = new TabCollection({ store }); - tabs.set([ - { id: 'a', trackLastUrl: true }, - { id: 'b', trackLastUrl: true } - ]); - let tabById = indexBy(tabs.get(), 'id'); - - expect(tabById.a.lastUrl).to.not.match(/_g=1/); - expect(tabById.b.lastUrl).to.not.match(/_g=1/); - tabs.consumeRouteUpdate('app', 'http://localhost:9200/a?_g=1', '/a', true); - expect(tabById.a.lastUrl).to.match(/_g=1/); - expect(tabById.b.lastUrl).to.match(/_g=1/); + tabs.consumeRouteUpdate(`${baseUrl}/a`); + let {a, b} = indexBy(tabs.get(), 'id'); + expect(a.active).to.equal(true); + expect(b.active).to.equal(false); + expect(tabs.getActive()).to.equal(a); }); }); diff --git a/src/ui/public/chrome/__tests__/_utils/TabFakeStore.js b/src/ui/public/chrome/__tests__/_TabFakeStore.js similarity index 100% rename from src/ui/public/chrome/__tests__/_utils/TabFakeStore.js rename to src/ui/public/chrome/__tests__/_TabFakeStore.js diff --git a/src/ui/public/chrome/__tests__/api/apps.js b/src/ui/public/chrome/api/__tests__/apps.js similarity index 93% rename from src/ui/public/chrome/__tests__/api/apps.js rename to src/ui/public/chrome/api/__tests__/apps.js index c57f33c3fe6b..3b61b8c0b192 100644 --- a/src/ui/public/chrome/__tests__/api/apps.js +++ b/src/ui/public/chrome/api/__tests__/apps.js @@ -1,7 +1,7 @@ let expect = require('expect.js'); -let setup = require('../../api/apps'); -let TabFakeStore = require('../_utils/TabFakeStore'); +let setup = require('../apps'); +let TabFakeStore = require('../../__tests__/_TabFakeStore'); describe('Chrome API :: apps', function () { describe('#get/setShowAppsLink()', function () { @@ -83,18 +83,18 @@ describe('Chrome API :: apps', function () { }); }); - describe('#getAppId()', function () { - it('returns the id property of the current app', function () { + describe('#getAppUrl()', function () { + it('returns the url property of the current app', function () { let chrome = {}; - let app = { id: 'foo' }; + let app = { url: 'foo' }; setup(chrome, { app }); - expect(chrome.getAppId()).to.eql('foo'); + expect(chrome.getAppUrl()).to.eql('foo'); }); it('returns undefined if no active app', function () { let chrome = {}; setup(chrome, {}); - expect(chrome.getAppId()).to.equal(undefined); + expect(chrome.getAppUrl()).to.equal(undefined); }); }); diff --git a/src/ui/public/chrome/api/angular.js b/src/ui/public/chrome/api/angular.js index 7ae841265165..98ea64df6624 100644 --- a/src/ui/public/chrome/api/angular.js +++ b/src/ui/public/chrome/api/angular.js @@ -45,13 +45,10 @@ module.exports = function (chrome, internals) { // listen for route changes, propogate to tabs var onRouteChange = function () { - let appId = chrome.getAppId(); let { href } = window.location; - let path = $location.url(); let persist = chrome.getVisible(); - internals.trackPossibleSubUrl(href); - internals.tabs.consumeRouteUpdate(appId, path, persist); + internals.tabs.consumeRouteUpdate(href, persist); }; $rootScope.$on('$routeChangeSuccess', onRouteChange); diff --git a/src/ui/public/chrome/api/apps.js b/src/ui/public/chrome/api/apps.js index dfba49071e32..e5c04f630bc1 100644 --- a/src/ui/public/chrome/api/apps.js +++ b/src/ui/public/chrome/api/apps.js @@ -1,7 +1,12 @@ let { clone, get } = require('lodash'); +let { resolve } = require('url'); module.exports = function (chrome, internals) { + if (internals.app) { + internals.app.url = resolve(window.location.href, internals.app.url); + } + internals.appUrlStore = internals.appUrlStore || window.sessionStorage; /** @@ -29,8 +34,8 @@ module.exports = function (chrome, internals) { return get(internals, ['app', 'title']); }; - chrome.getAppId = function () { - return get(internals, ['app', 'id']); + chrome.getAppUrl = function () { + return get(internals, ['app', 'url']); }; chrome.getInjected = function (name, def) { diff --git a/src/ui/public/chrome/api/tabs.js b/src/ui/public/chrome/api/tabs.js index 19e2550af3ba..128f5ca8c267 100644 --- a/src/ui/public/chrome/api/tabs.js +++ b/src/ui/public/chrome/api/tabs.js @@ -1,7 +1,14 @@ var _ = require('lodash'); +var TabCollection = require('../TabCollection'); module.exports = function (chrome, internals) { + internals.tabs = new TabCollection({ + defaults: { + baseUrl: `${chrome.getAppUrl()}#/` + } + }); + /** * ui/chrome tabs API * diff --git a/src/ui/public/chrome/chrome.js b/src/ui/public/chrome/chrome.js index d070a23aab62..c9c9a243db9f 100644 --- a/src/ui/public/chrome/chrome.js +++ b/src/ui/public/chrome/chrome.js @@ -11,16 +11,10 @@ require('ui/promises'); var metadata = require('ui/metadata'); var TabCollection = require('ui/chrome/TabCollection'); -var chrome = { - navBackground: '#222222', - logo: null, - smallLogo: null -}; - -var internals = _.assign( +var chrome = {}; +var internals = _.defaults( _.cloneDeep(metadata), { - tabs: new TabCollection(), rootController: null, rootTemplate: null, showAppsLink: null, From 4df68cd0ad82893a1e8117aab4107435d09c82ab Mon Sep 17 00:00:00 2001 From: spalger Date: Fri, 18 Sep 2015 11:28:25 -0700 Subject: [PATCH 07/44] [chrome] ship the nav in initial chrome metadata --- src/plugins/appSwitcher/index.js | 11 +---------- src/plugins/statusPage/index.js | 2 +- src/ui/UiApp.js | 3 ++- src/ui/index.js | 1 + src/ui/public/chrome/api/apps.js | 2 +- src/ui/public/chrome/api/nav.js | 30 ++++++++++++++++++++++++++++++ src/ui/public/chrome/chrome.js | 1 + 7 files changed, 37 insertions(+), 13 deletions(-) create mode 100644 src/ui/public/chrome/api/nav.js diff --git a/src/plugins/appSwitcher/index.js b/src/plugins/appSwitcher/index.js index d20235503173..7d251dc1e5f9 100644 --- a/src/plugins/appSwitcher/index.js +++ b/src/plugins/appSwitcher/index.js @@ -1,12 +1,3 @@ module.exports = function (kibana) { - return new kibana.Plugin({ - uiExports: { - app: { - id: 'appSwitcher', - main: 'plugins/appSwitcher/appSwitcher', - hidden: true, - autoload: kibana.autoload.styles - } - } - }); + return new kibana.Plugin(); }; diff --git a/src/plugins/statusPage/index.js b/src/plugins/statusPage/index.js index 66930183ba9d..49f139afd46b 100644 --- a/src/plugins/statusPage/index.js +++ b/src/plugins/statusPage/index.js @@ -5,6 +5,7 @@ module.exports = function (kibana) { title: 'Server Status', main: 'plugins/statusPage/statusPage', hidden: true, + url: '/status', autoload: [].concat( kibana.autoload.styles, @@ -15,4 +16,3 @@ module.exports = function (kibana) { } }); }; - diff --git a/src/ui/UiApp.js b/src/ui/UiApp.js index 1df85329d177..9f1df9fe880e 100644 --- a/src/ui/UiApp.js +++ b/src/ui/UiApp.js @@ -19,6 +19,7 @@ class UiApp { this.hidden = this.spec.hidden; this.autoloadOverrides = this.spec.autoload; this.templateName = this.spec.templateName || 'uiApp'; + this.url = this.spec.url || '/app/' + this.id; // once this resolves, no reason to run it again this.getModules = _.once(this.getModules); @@ -39,7 +40,7 @@ class UiApp { } toJSON() { - return _.pick(this, ['id', 'title', 'description', 'icon', 'main']); + return _.pick(this, ['id', 'title', 'description', 'icon', 'main', 'url']); } } diff --git a/src/ui/index.js b/src/ui/index.js index 2af378381f29..0be21a842477 100644 --- a/src/ui/index.js +++ b/src/ui/index.js @@ -69,6 +69,7 @@ module.exports = async (kbnServer, server, config) => { let payload = { app: app, appCount: uiExports.apps.size, + nav: uiExports.apps, version: kbnServer.version, buildNum: config.get('pkg.buildNum'), buildSha: config.get('pkg.buildSha'), diff --git a/src/ui/public/chrome/api/apps.js b/src/ui/public/chrome/api/apps.js index e5c04f630bc1..099883db9371 100644 --- a/src/ui/public/chrome/api/apps.js +++ b/src/ui/public/chrome/api/apps.js @@ -23,7 +23,7 @@ module.exports = function (chrome, internals) { }; chrome.getShowAppsLink = function () { - return internals.showAppsLink == null ? internals.appCount > 1 : internals.showAppsLink; + return internals.showAppsLink == null ? internals.nav.length > 1 : internals.showAppsLink; }; chrome.getApp = function () { diff --git a/src/ui/public/chrome/api/nav.js b/src/ui/public/chrome/api/nav.js new file mode 100644 index 000000000000..fb67d45f1d18 --- /dev/null +++ b/src/ui/public/chrome/api/nav.js @@ -0,0 +1,30 @@ +module.exports = function (chrome, internals) { + let { startsWith } = require('lodash'); + + chrome.getNavLinks = function () { + return internals.nav; + }; + + chrome.getLastSubUrlFor = function (url) { + return internals.appUrlStore.getItem(`lastSubUrl:${url}`); + }; + + internals.trackPossibleSubUrl = function (url) { + for (let link of internals.nav) { + if (startsWith(url, link.url)) { + link.lastSubUrl = url; + internals.appUrlStore.setItem(`lastSubUrl:${link.url}`, url); + } + } + }; + + internals.nav.forEach(link => { + // convert all link urls to absolute urls + + var a = document.createElement('a'); + a.setAttribute('href', link.url); + link.url = a.href; + link.lastSubUrl = chrome.getLastSubUrlFor(link.url); + }); + +}; diff --git a/src/ui/public/chrome/chrome.js b/src/ui/public/chrome/chrome.js index c9c9a243db9f..854fc0f388d4 100644 --- a/src/ui/public/chrome/chrome.js +++ b/src/ui/public/chrome/chrome.js @@ -29,6 +29,7 @@ $('').attr({ }).appendTo('head'); require('./api/apps')(chrome, internals); +require('./api/nav')(chrome, internals); require('./api/angular')(chrome, internals); require('./api/controls')(chrome, internals); require('./api/tabs')(chrome, internals); From 9f9bbe2838c96d87ab62355049aef0ff4127536d Mon Sep 17 00:00:00 2001 From: spalger Date: Fri, 18 Sep 2015 11:49:32 -0700 Subject: [PATCH 08/44] [chrome] consume app switcher, convert to use chrome.getNavLinks() --- src/plugins/appSwitcher/index.js | 3 --- src/plugins/appSwitcher/package.json | 4 ---- src/ui/index.js | 9 --------- src/ui/public/chrome/api/angular.js | 9 ++++++++- .../public/chrome/appSwitcher}/appSwitcher.html | 9 +++------ .../public/chrome/appSwitcher}/appSwitcher.less | 0 src/ui/public/chrome/chrome.html | 12 ++++++------ src/ui/public/chrome/context.js | 15 +-------------- 8 files changed, 18 insertions(+), 43 deletions(-) delete mode 100644 src/plugins/appSwitcher/index.js delete mode 100644 src/plugins/appSwitcher/package.json rename src/{plugins/appSwitcher/public => ui/public/chrome/appSwitcher}/appSwitcher.html (54%) rename src/{plugins/appSwitcher/public => ui/public/chrome/appSwitcher}/appSwitcher.less (100%) diff --git a/src/plugins/appSwitcher/index.js b/src/plugins/appSwitcher/index.js deleted file mode 100644 index 7d251dc1e5f9..000000000000 --- a/src/plugins/appSwitcher/index.js +++ /dev/null @@ -1,3 +0,0 @@ -module.exports = function (kibana) { - return new kibana.Plugin(); -}; diff --git a/src/plugins/appSwitcher/package.json b/src/plugins/appSwitcher/package.json deleted file mode 100644 index 0483302d156d..000000000000 --- a/src/plugins/appSwitcher/package.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "name": "appSwitcher", - "version": "1.0.0" -} diff --git a/src/ui/index.js b/src/ui/index.js index 0be21a842477..82edc47c7ea9 100644 --- a/src/ui/index.js +++ b/src/ui/index.js @@ -40,15 +40,6 @@ module.exports = async (kbnServer, server, config) => { server.setupViews(resolve(__dirname, 'views')); server.exposeStaticFile('/loading.gif', resolve(__dirname, 'public/loading.gif')); - // serve the app switcher - server.route({ - path: '/api/apps', - method: 'GET', - handler: function (req, reply) { - return reply(uiExports.apps); - } - }); - server.route({ path: '/app/{id}', method: 'GET', diff --git a/src/ui/public/chrome/api/angular.js b/src/ui/public/chrome/api/angular.js index 98ea64df6624..4045776f0c32 100644 --- a/src/ui/public/chrome/api/angular.js +++ b/src/ui/public/chrome/api/angular.js @@ -1,7 +1,11 @@ -var modules = require('ui/modules'); var $ = require('jquery'); var _ = require('lodash'); +require('../appSwitcher/appSwitcher.less'); +var modules = require('ui/modules'); +var ConfigTemplate = require('ui/ConfigTemplate'); +require('ui/directives/config'); + module.exports = function (chrome, internals) { chrome.setupAngular = function () { var kibana = modules.get('kibana'); @@ -58,6 +62,9 @@ module.exports = function (chrome, internals) { // and some local values $scope.httpActive = $http.pendingRequests; $scope.notifList = require('ui/notify')._notifs; + $scope.appSwitcherTemplate = new ConfigTemplate({ + switcher: require('../appSwitcher/appSwitcher.html') + }); return chrome; } diff --git a/src/plugins/appSwitcher/public/appSwitcher.html b/src/ui/public/chrome/appSwitcher/appSwitcher.html similarity index 54% rename from src/plugins/appSwitcher/public/appSwitcher.html rename to src/ui/public/chrome/appSwitcher/appSwitcher.html index cd7d3dded78b..6deafe4b8e32 100644 --- a/src/plugins/appSwitcher/public/appSwitcher.html +++ b/src/ui/public/chrome/appSwitcher/appSwitcher.html @@ -1,9 +1,6 @@ -
-
-
-