diff --git a/src/ui/public/chrome/__tests__/_tab_fake_store.js b/src/ui/public/chrome/__tests__/fixtures/stub_browser_storage.js similarity index 73% rename from src/ui/public/chrome/__tests__/_tab_fake_store.js rename to src/ui/public/chrome/__tests__/fixtures/stub_browser_storage.js index a678440d14b78..812cd85ce4d75 100644 --- a/src/ui/public/chrome/__tests__/_tab_fake_store.js +++ b/src/ui/public/chrome/__tests__/fixtures/stub_browser_storage.js @@ -1,9 +1,9 @@ const store = Symbol('store'); -export default class TabFakeStore { +export default class StubBrowserStorage { constructor() { this[store] = new Map(); } getItem(k) { return this[store].get(k); } - setItem(k, v) { return this[store].set(k, v); } + setItem(k, v) { return this[store].set(k, String(v)); } removeItem(k) { return this[store].delete(k); } getKeys() { return [ ...this[store].keys() ]; } getValues() { return [ ...this[store].values() ]; } diff --git a/src/ui/public/chrome/__tests__/tab.js b/src/ui/public/chrome/__tests__/tab.js index add635bf7e2c1..cc3d3ab1b4ba2 100644 --- a/src/ui/public/chrome/__tests__/tab.js +++ b/src/ui/public/chrome/__tests__/tab.js @@ -1,7 +1,8 @@ import sinon from 'auto-release-sinon'; + import Tab from '../tab'; import expect from 'expect.js'; -import TabFakeStore from './_tab_fake_store'; +import StubBrowserStorage from './fixtures/stub_browser_storage'; describe('Chrome Tab', function () { describe('construction', function () { @@ -88,7 +89,7 @@ describe('Chrome Tab', function () { }); it('discovers the lastUrl', function () { - const lastUrlStore = new TabFakeStore(); + const lastUrlStore = new StubBrowserStorage(); const tab = new Tab({ id: 'foo', lastUrlStore }); expect(tab.lastUrl).to.not.equal('/foo/bar'); @@ -100,7 +101,7 @@ describe('Chrome Tab', function () { }); it('logs a warning about last urls that do not match the rootUrl', function () { - const lastUrlStore = new TabFakeStore(); + const lastUrlStore = new StubBrowserStorage(); const tab = new Tab({ id: 'foo', baseUrl: '/bar', lastUrlStore }); tab.setLastUrl('/bar/foo/1'); @@ -114,7 +115,7 @@ describe('Chrome Tab', function () { describe('#setLastUrl()', function () { it('updates the lastUrl and storage value if passed a lastUrlStore', function () { - const lastUrlStore = new TabFakeStore(); + const lastUrlStore = new StubBrowserStorage(); const tab = new Tab({ id: 'foo', lastUrlStore }); expect(tab.lastUrl).to.not.equal('foo'); diff --git a/src/ui/public/chrome/__tests__/tab_collection.js b/src/ui/public/chrome/__tests__/tab_collection.js index 419e00a0fa321..1bfa9c20b8f34 100644 --- a/src/ui/public/chrome/__tests__/tab_collection.js +++ b/src/ui/public/chrome/__tests__/tab_collection.js @@ -1,6 +1,6 @@ import expect from 'expect.js'; -import TabFakeStore from './_tab_fake_store'; +import StubBrowserStorage from './fixtures/stub_browser_storage'; import TabCollection from '../tab_collection'; import Tab from '../tab'; import { indexBy, random } from 'lodash'; @@ -54,7 +54,7 @@ describe('Chrome TabCollection', function () { describe('#consumeRouteUpdate()', function () { it('updates the active tab', function () { - const store = new TabFakeStore(); + const store = new StubBrowserStorage(); const baseUrl = `http://localhost:${random(1000, 9999)}`; const tabs = new TabCollection({ store, defaults: { baseUrl } }); tabs.set([ diff --git a/src/ui/public/chrome/api/__tests__/angular.js b/src/ui/public/chrome/api/__tests__/angular.js index 51b77b57df0a6..4f93d284a6924 100644 --- a/src/ui/public/chrome/api/__tests__/angular.js +++ b/src/ui/public/chrome/api/__tests__/angular.js @@ -1,7 +1,6 @@ import expect from 'expect.js'; import kbnAngular from '../angular'; -import TabFakeStore from '../../__tests__/_tab_fake_store'; import { noop } from 'lodash'; describe('Chrome API :: Angular', () => { diff --git a/src/ui/public/chrome/api/__tests__/apps.js b/src/ui/public/chrome/api/__tests__/apps.js index e50fe59e57ef1..273635b9cc418 100644 --- a/src/ui/public/chrome/api/__tests__/apps.js +++ b/src/ui/public/chrome/api/__tests__/apps.js @@ -1,7 +1,7 @@ import expect from 'expect.js'; import setup from '../apps'; -import TabFakeStore from '../../__tests__/_tab_fake_store'; +import StubBrowserStorage from '../../__tests__/fixtures/stub_browser_storage'; describe('Chrome API :: apps', function () { describe('#get/setShowAppsLink()', function () { @@ -147,7 +147,7 @@ describe('Chrome API :: apps', function () { describe('#get/setLastUrlFor()', function () { it('reads/writes last url from storage', function () { const chrome = {}; - const store = new TabFakeStore(); + const store = new StubBrowserStorage(); setup(chrome, { appUrlStore: store }); expect(chrome.getLastUrlFor('app')).to.equal(undefined); chrome.setLastUrlFor('app', 'url'); diff --git a/src/ui/public/chrome/api/__tests__/nav.js b/src/ui/public/chrome/api/__tests__/nav.js index 2161c63d6ce7a..8dffa7e4eebbe 100644 --- a/src/ui/public/chrome/api/__tests__/nav.js +++ b/src/ui/public/chrome/api/__tests__/nav.js @@ -1,45 +1,73 @@ import expect from 'expect.js'; import initChromeNavApi from 'ui/chrome/api/nav'; +import StubBrowserStorage from '../../__tests__/fixtures/stub_browser_storage'; const basePath = '/someBasePath'; -function getChrome(customInternals = { basePath }) { +function init(customInternals = { basePath }) { const chrome = {}; - initChromeNavApi(chrome, { + const internals = { nav: [], ...customInternals, - }); - return chrome; + }; + initChromeNavApi(chrome, internals); + return { chrome, internals }; } describe('chrome nav apis', function () { describe('#getBasePath()', function () { it('returns the basePath', function () { - const chrome = getChrome(); + const { chrome } = init(); expect(chrome.getBasePath()).to.be(basePath); }); }); describe('#addBasePath()', function () { it('returns undefined when nothing is passed', function () { - const chrome = getChrome(); + const { chrome } = init(); expect(chrome.addBasePath()).to.be(undefined); }); it('prepends the base path when the input is a path', function () { - const chrome = getChrome(); + const { chrome } = init(); expect(chrome.addBasePath('/other/path')).to.be(`${basePath}/other/path`); }); it('ignores non-path urls', function () { - const chrome = getChrome(); + const { chrome } = init(); expect(chrome.addBasePath('http://github.com/elastic/kibana')).to.be('http://github.com/elastic/kibana'); }); it('includes the query string', function () { - const chrome = getChrome(); + const { chrome } = init(); expect(chrome.addBasePath('/app/kibana?a=b')).to.be(`${basePath}/app/kibana?a=b`); }); }); + + describe('internals.trackPossibleSubUrl()', function () { + it('injects the globalState of the current url to all links for the same app', function () { + const appUrlStore = new StubBrowserStorage(); + const nav = [ + { url: 'https://localhost:9200/app/kibana#discover' }, + { url: 'https://localhost:9200/app/kibana#visualize' }, + { url: 'https://localhost:9200/app/kibana#dashboard' }, + ].map(l => { + l.lastSubUrl = l.url; + return l; + }); + + const { chrome, internals } = init({ appUrlStore, nav }); + + internals.trackPossibleSubUrl('https://localhost:9200/app/kibana#dashboard?_g=globalstate'); + expect(internals.nav[0].lastSubUrl).to.be('https://localhost:9200/app/kibana#discover?_g=globalstate'); + expect(internals.nav[0].active).to.be(false); + + expect(internals.nav[1].lastSubUrl).to.be('https://localhost:9200/app/kibana#visualize?_g=globalstate'); + expect(internals.nav[1].active).to.be(false); + + expect(internals.nav[2].lastSubUrl).to.be('https://localhost:9200/app/kibana#dashboard?_g=globalstate'); + expect(internals.nav[2].active).to.be(true); + }); + }); }); diff --git a/src/ui/public/chrome/api/nav.js b/src/ui/public/chrome/api/nav.js index d993baa216ef9..e39b179e4b7c2 100644 --- a/src/ui/public/chrome/api/nav.js +++ b/src/ui/public/chrome/api/nav.js @@ -40,25 +40,72 @@ export default function (chrome, internals) { } function refreshLastUrl(link) { - link.lastSubUrl = internals.appUrlStore.getItem(lastSubUrlKey(link)); + link.lastSubUrl = internals.appUrlStore.getItem(lastSubUrlKey(link)) || link.lastSubUrl || link.url; + } + + function getAppId(url) { + const pathname = parse(url).pathname; + const pathnameWithoutBasepath = pathname.slice(chrome.getBasePath().length); + const match = String(pathnameWithoutBasepath).match(/^\/app\/([^\/]+)(?:\/|\?|#|$)/); + if (match) return match[1]; + } + + function decodeKibanaUrl(url) { + const parsedUrl = parse(url); + const appId = getAppId(parsedUrl); + const hash = parsedUrl.hash || ''; + const parsedHash = parse(hash.slice(1), true); + const globalState = parsedHash.query && parsedHash.query._g; + return { appId, globalState, parsedUrl, parsedHash }; + } + + function injectNewGlobalState(link, fromAppId, newGlobalState) { + // parse the lastSubUrl of this link so we can manipulate its parts + const { appId: toAppId, parsedHash: toHash, parsedUrl: toParsed } = decodeKibanaUrl(link.lastSubUrl); + + // don't copy global state if links are for different apps + if (fromAppId !== toAppId) return; + + // add the new globalState to the hashUrl in the linkurl + const toHashQuery = toHash.query || {}; + toHashQuery._g = newGlobalState; + + // format the new subUrl and include the newHash + link.lastSubUrl = format({ + protocol: toParsed.protocol, + port: toParsed.port, + hostname: toParsed.hostname, + pathname: toParsed.pathname, + query: toParsed.query, + hash: format({ + pathname: toHash.pathname, + query: toHashQuery, + hash: toHash.hash, + }), + }); } internals.trackPossibleSubUrl = function (url) { + const { appId, globalState: newGlobalState } = decodeKibanaUrl(url); + for (const link of internals.nav) { - link.active = startsWith(url, link.url); + const matchingTab = find(internals.tabs, { rootUrl: link.url }); + link.active = startsWith(url, link.url); if (link.active) { setLastUrl(link, url); continue; } - const matchingTab = find(internals.tabs, { rootUrl: link.url }); if (matchingTab) { setLastUrl(link, matchingTab.getLastUrl()); - continue; + } else { + refreshLastUrl(link); } - refreshLastUrl(link); + if (newGlobalState) { + injectNewGlobalState(link, appId, newGlobalState); + } } };