diff --git a/src/ui/public/chrome/directives/kbn_chrome.js b/src/ui/public/chrome/directives/kbn_chrome.js index 5491164c7d391..83efe3ae9ea28 100644 --- a/src/ui/public/chrome/directives/kbn_chrome.js +++ b/src/ui/public/chrome/directives/kbn_chrome.js @@ -2,7 +2,10 @@ import $ from 'jquery'; import './kbn_chrome.less'; import UiModules from 'ui/modules'; -import { UnhashStatesProvider } from 'ui/state_management/unhash_states'; +import { + getUnhashableStatesProvider, + unhashUrl, +} from 'ui/state_management/state_hashing'; export default function (chrome, internals) { @@ -28,15 +31,16 @@ export default function (chrome, internals) { controllerAs: 'chrome', controller($scope, $rootScope, $location, $http, Private) { - const unhashStates = Private(UnhashStatesProvider); + const getUnhashableStates = Private(getUnhashableStatesProvider); // are we showing the embedded version of the chrome? internals.setVisibleDefault(!$location.search().embed); // listen for route changes, propogate to tabs const onRouteChange = function () { - let { href } = window.location; - internals.trackPossibleSubUrl(unhashStates.inAbsUrl(href)); + const urlWithHashes = window.location.href; + const urlWithStates = unhashUrl(urlWithHashes, getUnhashableStates()); + internals.trackPossibleSubUrl(urlWithStates); }; $rootScope.$on('$routeChangeSuccess', onRouteChange); diff --git a/src/ui/public/share/directives/share_object_url.js b/src/ui/public/share/directives/share_object_url.js index c92d292d9772e..8faac1cb34ad6 100644 --- a/src/ui/public/share/directives/share_object_url.js +++ b/src/ui/public/share/directives/share_object_url.js @@ -4,7 +4,10 @@ import '../styles/index.less'; import LibUrlShortenerProvider from '../lib/url_shortener'; import uiModules from 'ui/modules'; import shareObjectUrlTemplate from 'ui/share/views/share_object_url.html'; -import { UnhashStatesProvider } from 'ui/state_management/unhash_states'; +import { + getUnhashableStatesProvider, + unhashUrl, +} from 'ui/state_management/state_hashing'; import { memoize } from 'lodash'; app.directive('shareObjectUrl', function (Private, Notifier) { @@ -71,12 +74,15 @@ app.directive('shareObjectUrl', function (Private, Notifier) { }; // since getUrl() is called within a watcher we cache the unhashing step - const unhashStatesInAbsUrl = memoize((absUrl) => { - return Private(UnhashStatesProvider).inAbsUrl(absUrl); + const unhashAndCacheUrl = memoize((urlWithHashes, unhashableStates) => { + const urlWithStates = unhashUrl(urlWithHashes, unhashableStates); + return urlWithStates; }); $scope.getUrl = function () { - let url = unhashStatesInAbsUrl($location.absUrl()); + const urlWithHashes = $location.absUrl(); + const getUnhashableStates = Private(getUnhashableStatesProvider); + let url = unhashAndCacheUrl(urlWithHashes, getUnhashableStates()); if ($scope.shareAsEmbed) { url = url.replace('?', '?embed=true&'); diff --git a/src/ui/public/state_management/__tests__/state.js b/src/ui/public/state_management/__tests__/state.js index 62ffc09adf677..c9e25b07494ac 100644 --- a/src/ui/public/state_management/__tests__/state.js +++ b/src/ui/public/state_management/__tests__/state.js @@ -7,8 +7,8 @@ import { encode as encodeRison } from 'rison-node'; import 'ui/private'; import Notifier from 'ui/notify/notifier'; import StateManagementStateProvider from 'ui/state_management/state'; -import { UnhashStatesProvider } from 'ui/state_management/unhash_states'; -import { HashingStore } from 'ui/state_management/hashing_store'; +import { unhashQueryString } from 'ui/state_management/state_hashing'; +import { HashingStore } from 'ui/state_management/state_storage'; import StubBrowserStorage from 'test_utils/stub_browser_storage'; import EventsProvider from 'ui/events'; @@ -35,9 +35,8 @@ describe('State Management', function () { const hashingStore = new HashingStore({ store }); const state = new State(param, initial, { hashingStore, notify }); - const getUnhashedSearch = (state) => { - const unhashStates = Private(UnhashStatesProvider); - return unhashStates.inParsedQueryString($location.search(), [ state ]); + const getUnhashedSearch = state => { + return unhashQueryString($location.search(), [ state ]); }; return { notify, store, hashingStore, state, getUnhashedSearch }; diff --git a/src/ui/public/state_management/__tests__/unhash_states.js b/src/ui/public/state_management/__tests__/unhash_states.js deleted file mode 100644 index 19b613d57853c..0000000000000 --- a/src/ui/public/state_management/__tests__/unhash_states.js +++ /dev/null @@ -1,87 +0,0 @@ -import expect from 'expect.js'; -import ngMock from 'ng_mock'; -import sinon from 'auto-release-sinon'; - -import StateProvider from 'ui/state_management/state'; -import { UnhashStatesProvider } from 'ui/state_management/unhash_states'; - -describe('State Management Unhash States', () => { - let setup; - - beforeEach(ngMock.module('kibana')); - beforeEach(ngMock.inject(Private => { - setup = () => { - const unhashStates = Private(UnhashStatesProvider); - - const State = Private(StateProvider); - const testState = new State('testParam'); - sinon.stub(testState, 'translateHashToRison').withArgs('hash').returns('replacement'); - - return { unhashStates, testState }; - }; - })); - - describe('#inAbsUrl()', () => { - it('does nothing if missing input', () => { - const { unhashStates } = setup(); - expect(() => { - unhashStates.inAbsUrl(); - }).to.not.throwError(); - }); - - it('does nothing if just a host and port', () => { - const { unhashStates } = setup(); - const url = 'https://localhost:5601'; - expect(unhashStates.inAbsUrl(url)).to.be(url); - }); - - it('does nothing if just a path', () => { - const { unhashStates } = setup(); - const url = 'https://localhost:5601/app/kibana'; - expect(unhashStates.inAbsUrl(url)).to.be(url); - }); - - it('does nothing if just a path and query', () => { - const { unhashStates } = setup(); - const url = 'https://localhost:5601/app/kibana?foo=bar'; - expect(unhashStates.inAbsUrl(url)).to.be(url); - }); - - it('does nothing if empty hash with query', () => { - const { unhashStates } = setup(); - const url = 'https://localhost:5601/app/kibana?foo=bar#'; - expect(unhashStates.inAbsUrl(url)).to.be(url); - }); - - it('does nothing if empty hash without query', () => { - const { unhashStates } = setup(); - const url = 'https://localhost:5601/app/kibana#'; - expect(unhashStates.inAbsUrl(url)).to.be(url); - }); - - it('does nothing if empty hash without query', () => { - const { unhashStates } = setup(); - const url = 'https://localhost:5601/app/kibana#'; - expect(unhashStates.inAbsUrl(url)).to.be(url); - }); - - it('does nothing if hash is just a path', () => { - const { unhashStates } = setup(); - const url = 'https://localhost:5601/app/kibana#/discover'; - expect(unhashStates.inAbsUrl(url)).to.be(url); - }); - - it('does nothing if hash does not have matching query string vals', () => { - const { unhashStates } = setup(); - const url = 'https://localhost:5601/app/kibana#/discover?foo=bar'; - expect(unhashStates.inAbsUrl(url)).to.be(url); - }); - - it('replaces query string vals in hash for matching states with output of state.toRISON()', () => { - const { unhashStates, testState } = setup(); - const url = 'https://localhost:5601/#/?foo=bar&testParam=hash'; - const exp = 'https://localhost:5601/#/?foo=bar&testParam=replacement'; - expect(unhashStates.inAbsUrl(url, [testState])).to.be(exp); - }); - }); -}); diff --git a/src/ui/public/state_management/state.js b/src/ui/public/state_management/state.js index 7196c389bc82f..f06862010c738 100644 --- a/src/ui/public/state_management/state.js +++ b/src/ui/public/state_management/state.js @@ -7,8 +7,10 @@ import EventsProvider from 'ui/events'; import Notifier from 'ui/notify/notifier'; import KbnUrlProvider from 'ui/url'; -import { HashingStore } from './hashing_store'; -import { LazyLruStore } from './lazy_lru_store'; +import { + HashingStore, + LazyLruStore, +} from './state_storage'; const MAX_BROWSER_HISTORY = 50; diff --git a/src/ui/public/state_management/state_hashing/__tests__/unhash_url.js b/src/ui/public/state_management/state_hashing/__tests__/unhash_url.js new file mode 100644 index 0000000000000..6cf07677b3c8e --- /dev/null +++ b/src/ui/public/state_management/state_hashing/__tests__/unhash_url.js @@ -0,0 +1,73 @@ +import expect from 'expect.js'; +import ngMock from 'ng_mock'; +import sinon from 'auto-release-sinon'; + +import StateProvider from 'ui/state_management/state'; +import { unhashUrl } from 'ui/state_management/state_hashing'; + +describe('unhashUrl', () => { + let unhashableStates; + + beforeEach(ngMock.module('kibana')); + + beforeEach(ngMock.inject(Private => { + const State = Private(StateProvider); + const unhashableState = new State('testParam'); + sinon.stub(unhashableState, 'translateHashToRison').withArgs('hash').returns('replacement'); + unhashableStates = [unhashableState]; + })); + + describe('does nothing', () => { + it('if missing input', () => { + expect(() => { + unhashUrl(); + }).to.not.throwError(); + }); + + it('if just a host and port', () => { + const url = 'https://localhost:5601'; + expect(unhashUrl(url, unhashableStates)).to.be(url); + }); + + it('if just a path', () => { + const url = 'https://localhost:5601/app/kibana'; + expect(unhashUrl(url, unhashableStates)).to.be(url); + }); + + it('if just a path and query', () => { + const url = 'https://localhost:5601/app/kibana?foo=bar'; + expect(unhashUrl(url, unhashableStates)).to.be(url); + }); + + it('if empty hash with query', () => { + const url = 'https://localhost:5601/app/kibana?foo=bar#'; + expect(unhashUrl(url, unhashableStates)).to.be(url); + }); + + it('if empty hash without query', () => { + const url = 'https://localhost:5601/app/kibana#'; + expect(unhashUrl(url, unhashableStates)).to.be(url); + }); + + it('if empty hash without query', () => { + const url = 'https://localhost:5601/app/kibana#'; + expect(unhashUrl(url, unhashableStates)).to.be(url); + }); + + it('if hash is just a path', () => { + const url = 'https://localhost:5601/app/kibana#/discover'; + expect(unhashUrl(url, unhashableStates)).to.be(url); + }); + + it('if hash does not have matching query string vals', () => { + const url = 'https://localhost:5601/app/kibana#/discover?foo=bar'; + expect(unhashUrl(url, unhashableStates)).to.be(url); + }); + }); + + it('replaces query string vals in hash for matching states with output of state.toRISON()', () => { + const urlWithHashes = 'https://localhost:5601/#/?foo=bar&testParam=hash'; + const exp = 'https://localhost:5601/#/?foo=bar&testParam=replacement'; + expect(unhashUrl(urlWithHashes, unhashableStates)).to.be(exp); + }); +}); diff --git a/src/ui/public/state_management/state_hashing/get_unhashable_states_provider.js b/src/ui/public/state_management/state_hashing/get_unhashable_states_provider.js new file mode 100644 index 0000000000000..7dfc64f3a4398 --- /dev/null +++ b/src/ui/public/state_management/state_hashing/get_unhashable_states_provider.js @@ -0,0 +1,5 @@ +export default function getUnhashableStatesProvider(getAppState, globalState) { + return function getUnhashableStates() { + return [getAppState(), globalState].filter(Boolean); + }; +} diff --git a/src/ui/public/state_management/state_hashing/index.js b/src/ui/public/state_management/state_hashing/index.js new file mode 100644 index 0000000000000..6905a1fd28b61 --- /dev/null +++ b/src/ui/public/state_management/state_hashing/index.js @@ -0,0 +1,11 @@ +export { + default as getUnhashableStatesProvider, +} from './get_unhashable_states_provider'; + +export { + default as unhashQueryString, +} from './unhash_query_string'; + +export { + default as unhashUrl, +} from './unhash_url'; diff --git a/src/ui/public/state_management/state_hashing/unhash_query_string.js b/src/ui/public/state_management/state_hashing/unhash_query_string.js new file mode 100644 index 0000000000000..f75dbf97e9042 --- /dev/null +++ b/src/ui/public/state_management/state_hashing/unhash_query_string.js @@ -0,0 +1,8 @@ +import { mapValues } from 'lodash'; + +export default function unhashQueryString(parsedQueryString, states) { + return mapValues(parsedQueryString, (val, key) => { + const state = states.find(s => key === s.getQueryParamName()); + return state ? state.translateHashToRison(val) : val; + }); +} diff --git a/src/ui/public/state_management/state_hashing/unhash_url.js b/src/ui/public/state_management/state_hashing/unhash_url.js new file mode 100644 index 0000000000000..3671b653dee22 --- /dev/null +++ b/src/ui/public/state_management/state_hashing/unhash_url.js @@ -0,0 +1,36 @@ +import { + parse as parseUrl, + format as formatUrl, +} from 'url'; + +import unhashQueryString from './unhash_query_string'; + +export default function unhashUrl(urlWithHashes, states) { + if (!urlWithHashes) return urlWithHashes; + + const urlWithHashesParsed = parseUrl(urlWithHashes, true); + if (!urlWithHashesParsed.hostname) { + // passing a url like "localhost:5601" or "/app/kibana" should be prevented + throw new TypeError( + 'Only absolute urls should be passed to `unhashUrl()`. ' + + 'Unable to detect url hostname.' + ); + } + + if (!urlWithHashesParsed.hash) return urlWithHashes; + + const appUrl = urlWithHashesParsed.hash.slice(1); // trim the # + if (!appUrl) return urlWithHashes; + + const appUrlParsed = parseUrl(urlWithHashesParsed.hash.slice(1), true); + if (!appUrlParsed.query) return urlWithHashes; + + const appQueryWithoutHashes = unhashQueryString(appUrlParsed.query || {}, states); + return formatUrl({ + ...urlWithHashesParsed, + hash: formatUrl({ + pathname: appUrlParsed.pathname, + query: appQueryWithoutHashes, + }) + }); +} diff --git a/src/ui/public/state_management/__tests__/hashing_store.js b/src/ui/public/state_management/state_storage/_tests__/hashing_store.js similarity index 98% rename from src/ui/public/state_management/__tests__/hashing_store.js rename to src/ui/public/state_management/state_storage/_tests__/hashing_store.js index ce15fcae34827..c8d42ac321cd5 100644 --- a/src/ui/public/state_management/__tests__/hashing_store.js +++ b/src/ui/public/state_management/state_storage/_tests__/hashing_store.js @@ -3,7 +3,7 @@ import sinon from 'sinon'; import { encode as encodeRison } from 'rison-node'; import StubBrowserStorage from 'test_utils/stub_browser_storage'; -import { HashingStore } from 'ui/state_management/hashing_store'; +import { HashingStore } from 'ui/state_management/state_storage'; const setup = ({ createHash } = {}) => { const store = new StubBrowserStorage(); diff --git a/src/ui/public/state_management/__tests__/lazy_lru_store.js b/src/ui/public/state_management/state_storage/_tests__/lazy_lru_store.js similarity index 99% rename from src/ui/public/state_management/__tests__/lazy_lru_store.js rename to src/ui/public/state_management/state_storage/_tests__/lazy_lru_store.js index 175dfe014db2e..2fa5f5cf335f8 100644 --- a/src/ui/public/state_management/__tests__/lazy_lru_store.js +++ b/src/ui/public/state_management/state_storage/_tests__/lazy_lru_store.js @@ -3,7 +3,7 @@ import sinon from 'sinon'; import { times, sum, padLeft } from 'lodash'; import StubBrowserStorage from 'test_utils/stub_browser_storage'; -import { LazyLruStore } from '../lazy_lru_store'; +import { LazyLruStore } from '..'; const setup = (opts = {}) => { const { diff --git a/src/ui/public/state_management/hashing_store.js b/src/ui/public/state_management/state_storage/hashing_store.js similarity index 98% rename from src/ui/public/state_management/hashing_store.js rename to src/ui/public/state_management/state_storage/hashing_store.js index ace62df06d209..b968f29f087ce 100644 --- a/src/ui/public/state_management/hashing_store.js +++ b/src/ui/public/state_management/state_storage/hashing_store.js @@ -13,7 +13,7 @@ const TAG = 'h@'; * hash. This hash is then returned so that the item can be received * at a later time. */ -export class HashingStore { +export default class HashingStore { constructor({ store, createHash, maxItems } = {}) { this._store = store || window.sessionStorage; if (createHash) this._createHash = createHash; diff --git a/src/ui/public/state_management/state_storage/index.js b/src/ui/public/state_management/state_storage/index.js new file mode 100644 index 0000000000000..5c80b0f471901 --- /dev/null +++ b/src/ui/public/state_management/state_storage/index.js @@ -0,0 +1,7 @@ +export { + default as HashingStore, +} from './hashing_store'; + +export { + default as LazyLruStore, +} from './lazy_lru_store'; diff --git a/src/ui/public/state_management/lazy_lru_store.js b/src/ui/public/state_management/state_storage/lazy_lru_store.js similarity index 99% rename from src/ui/public/state_management/lazy_lru_store.js rename to src/ui/public/state_management/state_storage/lazy_lru_store.js index d9b00253250ea..14f015287796d 100644 --- a/src/ui/public/state_management/lazy_lru_store.js +++ b/src/ui/public/state_management/state_storage/lazy_lru_store.js @@ -31,7 +31,7 @@ const DEFAULT_IDEAL_CLEAR_RATIO = 100; */ const DEFAULT_MAX_IDEAL_CLEAR_PERCENT = 0.3; -export class LazyLruStore { +export default class LazyLruStore { constructor(opts = {}) { const { id, diff --git a/src/ui/public/state_management/unhash_states.js b/src/ui/public/state_management/unhash_states.js deleted file mode 100644 index e007a29d6d718..0000000000000 --- a/src/ui/public/state_management/unhash_states.js +++ /dev/null @@ -1,43 +0,0 @@ -import { parse as parseUrl, format as formatUrl } from 'url'; -import { mapValues } from 'lodash'; - -export function UnhashStatesProvider(getAppState, globalState) { - const getDefaultStates = () => [getAppState(), globalState].filter(Boolean); - - this.inAbsUrl = (urlWithHashes, states = getDefaultStates()) => { - if (!urlWithHashes) return urlWithHashes; - - const urlWithHashesParsed = parseUrl(urlWithHashes, true); - if (!urlWithHashesParsed.hostname) { - // passing a url like "localhost:5601" or "/app/kibana" should be prevented - throw new TypeError( - 'Only absolute urls should be passed to `unhashStates.inAbsUrl()`. ' + - 'Unable to detect url hostname.' - ); - } - - if (!urlWithHashesParsed.hash) return urlWithHashes; - - const appUrl = urlWithHashesParsed.hash.slice(1); // trim the # - if (!appUrl) return urlWithHashes; - - const appUrlParsed = parseUrl(urlWithHashesParsed.hash.slice(1), true); - if (!appUrlParsed.query) return urlWithHashes; - - const appQueryWithoutHashes = this.inParsedQueryString(appUrlParsed.query || {}, states); - return formatUrl({ - ...urlWithHashesParsed, - hash: formatUrl({ - pathname: appUrlParsed.pathname, - query: appQueryWithoutHashes, - }) - }); - }; - - this.inParsedQueryString = (parsedQueryString, states = getDefaultStates()) => { - return mapValues(parsedQueryString, (val, key) => { - const state = states.find(s => key === s.getQueryParamName()); - return state ? state.translateHashToRison(val) : val; - }); - }; -}