[State Management] Typescripify, jestify, simplify state_hashing and state_storage (#51835)

The hashUrl and unhashUrl functions no longer rely on states being provided as an argument, therefore getUnhashableStates/getUnhashableStatesProvider have been removed.
This commit is contained in:
Anton Dosov 2019-12-04 12:36:03 +01:00 committed by GitHub
parent 7ce0a37e3e
commit 217608d11e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
32 changed files with 709 additions and 651 deletions

View file

@ -120,10 +120,6 @@ export class DashboardAppController {
new FilterStateManager(globalState, getAppState, filterManager); new FilterStateManager(globalState, getAppState, filterManager);
const queryFilter = filterManager; const queryFilter = filterManager;
function getUnhashableStates(): State[] {
return [getAppState(), globalState].filter(Boolean);
}
let lastReloadRequestTime = 0; let lastReloadRequestTime = 0;
const dash = ($scope.dash = $route.current.locals.dash); const dash = ($scope.dash = $route.current.locals.dash);
@ -751,7 +747,7 @@ export class DashboardAppController {
anchorElement, anchorElement,
allowEmbed: true, allowEmbed: true,
allowShortUrl: !dashboardConfig.getHideWriteControls(), allowShortUrl: !dashboardConfig.getHideWriteControls(),
shareableUrl: unhashUrl(window.location.href, getUnhashableStates()), shareableUrl: unhashUrl(window.location.href),
objectId: dash.id, objectId: dash.id,
objectType: 'dashboard', objectType: 'dashboard',
sharingData: { sharingData: {

View file

@ -18,7 +18,7 @@
*/ */
import './np_core.test.mocks'; import './np_core.test.mocks';
import 'ui/state_management/state_storage/mock';
import { DashboardStateManager } from './dashboard_state_manager'; import { DashboardStateManager } from './dashboard_state_manager';
import { getAppStateMock, getSavedDashboardMock } from './__tests__'; import { getAppStateMock, getSavedDashboardMock } from './__tests__';
import { AppStateClass } from './legacy_imports'; import { AppStateClass } from './legacy_imports';

View file

@ -42,7 +42,6 @@ import {
getRequestInspectorStats, getRequestInspectorStats,
getResponseInspectorStats, getResponseInspectorStats,
getServices, getServices,
getUnhashableStatesProvider,
hasSearchStategyForIndexPattern, hasSearchStategyForIndexPattern,
intervalOptions, intervalOptions,
isDefaultTypeIndexPattern, isDefaultTypeIndexPattern,
@ -195,10 +194,8 @@ function discoverController(
globalState, globalState,
) { ) {
const responseHandler = vislibSeriesResponseHandlerProvider().handler; const responseHandler = vislibSeriesResponseHandlerProvider().handler;
const getUnhashableStates = Private(getUnhashableStatesProvider);
const filterStateManager = new FilterStateManager(globalState, getAppState, filterManager); const filterStateManager = new FilterStateManager(globalState, getAppState, filterManager);
const inspectorAdapters = { const inspectorAdapters = {
requests: new RequestAdapter() requests: new RequestAdapter()
}; };
@ -333,7 +330,7 @@ function discoverController(
anchorElement, anchorElement,
allowEmbed: false, allowEmbed: false,
allowShortUrl: uiCapabilities.discover.createShortUrl, allowShortUrl: uiCapabilities.discover.createShortUrl,
shareableUrl: unhashUrl(window.location.href, getUnhashableStates()), shareableUrl: unhashUrl(window.location.href),
objectId: savedSearch.id, objectId: savedSearch.id,
objectType: 'search', objectType: 'search',
sharingData: { sharingData: {

View file

@ -74,8 +74,6 @@ export { subscribeWithScope } from 'ui/utils/subscribe_with_scope';
// @ts-ignore // @ts-ignore
export { timezoneProvider } from 'ui/vis/lib/timezone'; export { timezoneProvider } from 'ui/vis/lib/timezone';
// @ts-ignore // @ts-ignore
export { getUnhashableStatesProvider } from 'ui/state_management/state_hashing';
// @ts-ignore
export { tabifyAggResponse } from 'ui/agg_response/tabify'; export { tabifyAggResponse } from 'ui/agg_response/tabify';
// @ts-ignore // @ts-ignore
export { vislibSeriesResponseHandlerProvider } from 'ui/vis/response_handlers/vislib'; export { vislibSeriesResponseHandlerProvider } from 'ui/vis/response_handlers/vislib';

View file

@ -39,7 +39,6 @@ import {
getServices, getServices,
angular, angular,
absoluteToParsedUrl, absoluteToParsedUrl,
getUnhashableStatesProvider,
KibanaParsedUrl, KibanaParsedUrl,
migrateLegacyQuery, migrateLegacyQuery,
SavedObjectSaveModal, SavedObjectSaveModal,
@ -166,7 +165,6 @@ function VisEditor(
localStorage, localStorage,
) { ) {
const queryFilter = Private(FilterBarQueryFilterProvider); const queryFilter = Private(FilterBarQueryFilterProvider);
const getUnhashableStates = Private(getUnhashableStatesProvider);
// Retrieve the resolved SavedVis instance. // Retrieve the resolved SavedVis instance.
const savedVis = $route.current.locals.savedVis; const savedVis = $route.current.locals.savedVis;
@ -250,7 +248,7 @@ function VisEditor(
anchorElement, anchorElement,
allowEmbed: true, allowEmbed: true,
allowShortUrl: capabilities.visualize.createShortUrl, allowShortUrl: capabilities.visualize.createShortUrl,
shareableUrl: unhashUrl(window.location.href, getUnhashableStates()), shareableUrl: unhashUrl(window.location.href),
objectId: savedVis.id, objectId: savedVis.id,
objectType: 'visualization', objectType: 'visualization',
sharingData: { sharingData: {

View file

@ -96,8 +96,6 @@ export { getFromSavedObject } from 'ui/index_patterns';
export { PersistedState } from 'ui/persisted_state'; export { PersistedState } from 'ui/persisted_state';
// @ts-ignore // @ts-ignore
export { VisEditorTypesRegistryProvider } from 'ui/registry/vis_editor_types'; export { VisEditorTypesRegistryProvider } from 'ui/registry/vis_editor_types';
// @ts-ignore
export { getUnhashableStatesProvider } from 'ui/state_management/state_hashing';
export { showSaveModal } from 'ui/saved_objects/show_saved_object_save_modal'; export { showSaveModal } from 'ui/saved_objects/show_saved_object_save_modal';
export { stateMonitorFactory } from 'ui/state_management/state_monitor_factory'; export { stateMonitorFactory } from 'ui/state_management/state_monitor_factory';
export { absoluteToParsedUrl } from 'ui/url/absolute_to_parsed_url'; export { absoluteToParsedUrl } from 'ui/url/absolute_to_parsed_url';

View file

@ -20,6 +20,7 @@
import chrome from 'ui/chrome'; import chrome from 'ui/chrome';
import { hashUrl } from 'ui/state_management/state_hashing'; import { hashUrl } from 'ui/state_management/state_hashing';
import uiRoutes from 'ui/routes'; import uiRoutes from 'ui/routes';
import { fatalError } from 'ui/notify';
uiRoutes.enable(); uiRoutes.enable();
uiRoutes uiRoutes
@ -27,11 +28,14 @@ uiRoutes
resolve: { resolve: {
url: function (AppState, globalState, $window) { url: function (AppState, globalState, $window) {
const redirectUrl = chrome.getInjected('redirectUrl'); const redirectUrl = chrome.getInjected('redirectUrl');
try {
const hashedUrl = hashUrl(redirectUrl);
const url = chrome.addBasePath(hashedUrl);
const hashedUrl = hashUrl([new AppState(), globalState], redirectUrl); $window.location = url;
const url = chrome.addBasePath(hashedUrl); } catch (e) {
fatalError(e);
$window.location = url; }
} }
} }
}); });

View file

@ -20,18 +20,16 @@
import url from 'url'; import url from 'url';
import { import {
getUnhashableStatesProvider,
unhashUrl, unhashUrl,
} from '../../state_management/state_hashing'; } from '../../state_management/state_hashing';
export function registerSubUrlHooks(angularModule, internals) { export function registerSubUrlHooks(angularModule, internals) {
angularModule.run(($rootScope, Private, $location) => { angularModule.run(($rootScope, Private, $location) => {
const getUnhashableStates = Private(getUnhashableStatesProvider);
const subUrlRouteFilter = Private(SubUrlRouteFilterProvider); const subUrlRouteFilter = Private(SubUrlRouteFilterProvider);
function updateSubUrls() { function updateSubUrls() {
const urlWithHashes = window.location.href; const urlWithHashes = window.location.href;
const urlWithStates = unhashUrl(urlWithHashes, getUnhashableStates()); const urlWithStates = unhashUrl(urlWithHashes);
internals.trackPossibleSubUrl(urlWithStates); internals.trackPossibleSubUrl(urlWithStates);
} }

View file

@ -25,13 +25,11 @@ import '../../private';
import { toastNotifications } from '../../notify'; import { toastNotifications } from '../../notify';
import * as FatalErrorNS from '../../notify/fatal_error'; import * as FatalErrorNS from '../../notify/fatal_error';
import { StateProvider } from '../state'; import { StateProvider } from '../state';
import {
unhashQueryString,
} from '../state_hashing';
import { import {
createStateHash, createStateHash,
isStateHash, isStateHash,
} from '../state_storage'; unhashQuery
} from '../state_hashing';
import { HashedItemStore } from '../state_storage/hashed_item_store'; import { HashedItemStore } from '../state_storage/hashed_item_store';
import { StubBrowserStorage } from 'test_utils/stub_browser_storage'; import { StubBrowserStorage } from 'test_utils/stub_browser_storage';
import { EventsProvider } from '../../events'; import { EventsProvider } from '../../events';
@ -60,9 +58,7 @@ describe('State Management', () => {
const hashedItemStore = new HashedItemStore(store); const hashedItemStore = new HashedItemStore(store);
const state = new State(param, initial, hashedItemStore); const state = new State(param, initial, hashedItemStore);
const getUnhashedSearch = state => { const getUnhashedSearch = () => unhashQuery($location.search());
return unhashQueryString($location.search(), [ state ]);
};
return { store, hashedItemStore, state, getUnhashedSearch }; return { store, hashedItemStore, state, getUnhashedSearch };
}; };

View file

@ -37,10 +37,12 @@ import { createLegacyClass } from '../utils/legacy_class';
import { callEach } from '../utils/function'; import { callEach } from '../utils/function';
import { import {
createStateHash,
HashedItemStoreSingleton, HashedItemStoreSingleton,
isStateHash,
} from './state_storage'; } from './state_storage';
import {
createStateHash,
isStateHash
} from './state_hashing';
export function StateProvider(Private, $rootScope, $location, stateManagementConfig, config, kbnUrl, $injector) { export function StateProvider(Private, $rootScope, $location, stateManagementConfig, config, kbnUrl, $injector) {
const Events = Private(EventsProvider); const Events = Private(EventsProvider);
@ -293,9 +295,7 @@ export function StateProvider(Private, $rootScope, $location, stateManagementCon
// We need to strip out Angular-specific properties. // We need to strip out Angular-specific properties.
const json = angular.toJson(state); const json = angular.toJson(state);
const hash = createStateHash(json, hash => { const hash = createStateHash(json);
return this._hashedItemStore.getItem(hash);
});
const isItemSet = this._hashedItemStore.setItem(hash, json); const isItemSet = this._hashedItemStore.setItem(hash, json);
if (isItemSet) { if (isItemSet) {

View file

@ -1,158 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import expect from '@kbn/expect';
import ngMock from 'ng_mock';
import sinon from 'sinon';
import { parse as parseUrl } from 'url';
import { StateProvider } from '../../state';
import { hashUrl } from '..';
describe('hashUrl', function () {
let State;
beforeEach(ngMock.module('kibana'));
beforeEach(ngMock.inject((Private, config) => {
State = Private(StateProvider);
sinon.stub(config, 'get').withArgs('state:storeInSessionStorage').returns(true);
}));
describe('throws error', () => {
it('if states parameter is null', () => {
expect(() => {
hashUrl(null, '');
}).to.throwError();
});
it('if states parameter is empty array', () => {
expect(() => {
hashUrl([], '');
}).to.throwError();
});
});
describe('does nothing', () => {
let states;
beforeEach(() => {
states = [new State('testParam')];
});
it('if url is empty', () => {
const url = '';
expect(hashUrl(states, url)).to.be(url);
});
it('if just a host and port', () => {
const url = 'https://localhost:5601';
expect(hashUrl(states, url)).to.be(url);
});
it('if just a path', () => {
const url = 'https://localhost:5601/app/kibana';
expect(hashUrl(states, url)).to.be(url);
});
it('if just a path and query', () => {
const url = 'https://localhost:5601/app/kibana?foo=bar';
expect(hashUrl(states, url)).to.be(url);
});
it('if empty hash with query', () => {
const url = 'https://localhost:5601/app/kibana?foo=bar#';
expect(hashUrl(states, url)).to.be(url);
});
it('if query parameter matches and there is no hash', () => {
const url = 'https://localhost:5601/app/kibana?testParam=(yes:!t)';
expect(hashUrl(states, url)).to.be(url);
});
it(`if query parameter matches and it's before the hash`, () => {
const url = 'https://localhost:5601/app/kibana?testParam=(yes:!t)';
expect(hashUrl(states, url)).to.be(url);
});
it('if empty hash without query', () => {
const url = 'https://localhost:5601/app/kibana#';
expect(hashUrl(states, url)).to.be(url);
});
it('if hash is just a path', () => {
const url = 'https://localhost:5601/app/kibana#/discover';
expect(hashUrl(states, url)).to.be(url);
});
it('if hash does not have matching query string vals', () => {
const url = 'https://localhost:5601/app/kibana#/discover?foo=bar';
expect(hashUrl(states, url)).to.be(url);
});
});
describe('replaces querystring value with hash', () => {
const getAppQuery = (url) => {
const parsedUrl = parseUrl(url);
const parsedAppUrl = parseUrl(parsedUrl.hash.slice(1), true);
return parsedAppUrl.query;
};
it('if using a single State', () => {
const stateParamKey = 'testParam';
const url = `https://localhost:5601/app/kibana#/discover?foo=bar&${stateParamKey}=(yes:!t)`;
const mockHashedItemStore = {
getItem: () => null,
setItem: sinon.stub().returns(true)
};
const state = new State(stateParamKey, {}, mockHashedItemStore);
const actualUrl = hashUrl([state], url);
expect(mockHashedItemStore.setItem.calledOnce).to.be(true);
const appQuery = getAppQuery(actualUrl);
const hashKey = mockHashedItemStore.setItem.firstCall.args[0];
expect(appQuery[stateParamKey]).to.eql(hashKey);
});
it('if using multiple States', () => {
const stateParamKey1 = 'testParam1';
const stateParamKey2 = 'testParam2';
const url = `https://localhost:5601/app/kibana#/discover?foo=bar&${stateParamKey1}=(yes:!t)&${stateParamKey2}=(yes:!f)`;
const mockHashedItemStore = {
getItem: () => null,
setItem: sinon.stub().returns(true)
};
const state1 = new State(stateParamKey1, {}, mockHashedItemStore);
const state2 = new State(stateParamKey2, {}, mockHashedItemStore);
const actualUrl = hashUrl([state1, state2], url);
expect(mockHashedItemStore.setItem.calledTwice).to.be(true);
const appQuery = getAppQuery(actualUrl);
const hashKey1 = mockHashedItemStore.setItem.firstCall.args[0];
const hashKey2 = mockHashedItemStore.setItem.secondCall.args[0];
expect(appQuery[stateParamKey1]).to.eql(hashKey1);
expect(appQuery[stateParamKey2]).to.eql(hashKey2);
});
});
});

View file

@ -1,87 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import expect from '@kbn/expect';
import ngMock from 'ng_mock';
import sinon from 'sinon';
import { StateProvider } from '../../state';
import { unhashUrl } from '..';
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 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);
});
});

View file

@ -0,0 +1,286 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { mockSessionStorage } from '../state_storage/mock';
import { HashedItemStore } from '../state_storage/hashed_item_store';
import { hashUrl, unhashUrl } from './hash_unhash_url';
describe('hash unhash url', () => {
beforeEach(() => {
mockSessionStorage.clear();
mockSessionStorage.setStubbedSizeLimit(5000000);
});
describe('hash url', () => {
describe('does nothing', () => {
it('if missing input', () => {
expect(() => {
// @ts-ignore
hashUrl();
}).not.toThrowError();
});
it('if url is empty', () => {
const url = '';
expect(hashUrl(url)).toBe(url);
});
it('if just a host and port', () => {
const url = 'https://localhost:5601';
expect(hashUrl(url)).toBe(url);
});
it('if just a path', () => {
const url = 'https://localhost:5601/app/kibana';
expect(hashUrl(url)).toBe(url);
});
it('if just a path and query', () => {
const url = 'https://localhost:5601/app/kibana?foo=bar';
expect(hashUrl(url)).toBe(url);
});
it('if empty hash with query', () => {
const url = 'https://localhost:5601/app/kibana?foo=bar#';
expect(hashUrl(url)).toBe(url);
});
it('if query parameter matches and there is no hash', () => {
const url = 'https://localhost:5601/app/kibana?testParam=(yes:!t)';
expect(hashUrl(url)).toBe(url);
});
it(`if query parameter matches and it's before the hash`, () => {
const url = 'https://localhost:5601/app/kibana?testParam=(yes:!t)';
expect(hashUrl(url)).toBe(url);
});
it('if empty hash without query', () => {
const url = 'https://localhost:5601/app/kibana#';
expect(hashUrl(url)).toBe(url);
});
it('if hash is just a path', () => {
const url = 'https://localhost:5601/app/kibana#/discover';
expect(hashUrl(url)).toBe(url);
});
it('if hash does not have matching query string vals', () => {
const url = 'https://localhost:5601/app/kibana#/discover?foo=bar';
expect(hashUrl(url)).toBe(url);
});
});
describe('replaces expanded state with hash', () => {
it('if uses single state param', () => {
const stateParamKey = '_g';
const stateParamValue = '(yes:!t)';
const url = `https://localhost:5601/app/kibana#/discover?foo=bar&${stateParamKey}=${stateParamValue}`;
const result = hashUrl(url);
expect(result).toMatchInlineSnapshot(
`"https://localhost:5601/app/kibana#/discover?foo=bar&_g=h@4e60e02"`
);
expect(mockSessionStorage.getItem('kbn.hashedItemsIndex.v1')).toBeTruthy();
expect(mockSessionStorage.getItem('h@4e60e02')).toEqual(JSON.stringify({ yes: true }));
});
it('if uses multiple states params', () => {
const stateParamKey1 = '_g';
const stateParamValue1 = '(yes:!t)';
const stateParamKey2 = '_a';
const stateParamValue2 = '(yes:!f)';
const stateParamKey3 = '_b';
const stateParamValue3 = '(yes:!f)';
const url = `https://localhost:5601/app/kibana#/discover?foo=bar&${stateParamKey1}=${stateParamValue1}&${stateParamKey2}=${stateParamValue2}&${stateParamKey3}=${stateParamValue3}`;
const result = hashUrl(url);
expect(result).toMatchInlineSnapshot(
`"https://localhost:5601/app/kibana#/discover?foo=bar&_g=h@4e60e02&_a=h@61fa078&_b=(yes:!f)"`
);
expect(mockSessionStorage.getItem('h@4e60e02')).toEqual(JSON.stringify({ yes: true }));
expect(mockSessionStorage.getItem('h@61fa078')).toEqual(JSON.stringify({ yes: false }));
if (!HashedItemStore.PERSISTED_INDEX_KEY) {
// This is very brittle and depends upon HashedItemStore implementation details,
// so let's protect ourselves from accidentally breaking this test.
throw new Error('Missing HashedItemStore.PERSISTED_INDEX_KEY');
}
expect(mockSessionStorage.getItem(HashedItemStore.PERSISTED_INDEX_KEY)).toBeTruthy();
expect(mockSessionStorage.length).toBe(3);
});
it('hashes only whitelisted properties', () => {
const stateParamKey1 = '_g';
const stateParamValue1 = '(yes:!t)';
const stateParamKey2 = '_a';
const stateParamValue2 = '(yes:!f)';
const stateParamKey3 = '_someother';
const stateParamValue3 = '(yes:!f)';
const url = `https://localhost:5601/app/kibana#/discover?foo=bar&${stateParamKey1}=${stateParamValue1}&${stateParamKey2}=${stateParamValue2}&${stateParamKey3}=${stateParamValue3}`;
const result = hashUrl(url);
expect(result).toMatchInlineSnapshot(
`"https://localhost:5601/app/kibana#/discover?foo=bar&_g=h@4e60e02&_a=h@61fa078&_someother=(yes:!f)"`
);
expect(mockSessionStorage.length).toBe(3); // 2 hashes + HashedItemStoreSingleton.PERSISTED_INDEX_KEY
});
});
it('throws error if unable to hash url', () => {
const stateParamKey1 = '_g';
const stateParamValue1 = '(yes:!t)';
mockSessionStorage.setStubbedSizeLimit(1);
const url = `https://localhost:5601/app/kibana#/discover?foo=bar&${stateParamKey1}=${stateParamValue1}`;
expect(() => hashUrl(url)).toThrowError();
});
});
describe('unhash url', () => {
describe('does nothing', () => {
it('if missing input', () => {
expect(() => {
// @ts-ignore
}).not.toThrowError();
});
it('if just a host and port', () => {
const url = 'https://localhost:5601';
expect(unhashUrl(url)).toBe(url);
});
it('if just a path', () => {
const url = 'https://localhost:5601/app/kibana';
expect(unhashUrl(url)).toBe(url);
});
it('if just a path and query', () => {
const url = 'https://localhost:5601/app/kibana?foo=bar';
expect(unhashUrl(url)).toBe(url);
});
it('if empty hash with query', () => {
const url = 'https://localhost:5601/app/kibana?foo=bar#';
expect(unhashUrl(url)).toBe(url);
});
it('if empty hash without query', () => {
const url = 'https://localhost:5601/app/kibana#';
expect(unhashUrl(url)).toBe(url);
});
it('if hash is just a path', () => {
const url = 'https://localhost:5601/app/kibana#/discover';
expect(unhashUrl(url)).toBe(url);
});
it('if hash does not have matching query string vals', () => {
const url = 'https://localhost:5601/app/kibana#/discover?foo=bar';
expect(unhashUrl(url)).toBe(url);
});
it("if hash has matching query, but it isn't hashed", () => {
const stateParamKey = '_g';
const stateParamValue = '(yes:!t)';
const url = `https://localhost:5601/app/kibana#/discover?foo=bar&${stateParamKey}=${stateParamValue}`;
expect(unhashUrl(url)).toBe(url);
});
});
describe('replaces expanded state with hash', () => {
it('if uses single state param', () => {
const stateParamKey = '_g';
const stateParamValueHashed = 'h@4e60e02';
const state = { yes: true };
mockSessionStorage.setItem(stateParamValueHashed, JSON.stringify(state));
const url = `https://localhost:5601/app/kibana#/discover?foo=bar&${stateParamKey}=${stateParamValueHashed}`;
const result = unhashUrl(url);
expect(result).toMatchInlineSnapshot(
`"https://localhost:5601/app/kibana#/discover?foo=bar&_g=(yes:!t)"`
);
});
it('if uses multiple state param', () => {
const stateParamKey1 = '_g';
const stateParamValueHashed1 = 'h@4e60e02';
const state1 = { yes: true };
const stateParamKey2 = '_a';
const stateParamValueHashed2 = 'h@61fa078';
const state2 = { yes: false };
mockSessionStorage.setItem(stateParamValueHashed1, JSON.stringify(state1));
mockSessionStorage.setItem(stateParamValueHashed2, JSON.stringify(state2));
const url = `https://localhost:5601/app/kibana#/discover?foo=bar&${stateParamKey1}=${stateParamValueHashed1}&${stateParamKey2}=${stateParamValueHashed2}`;
const result = unhashUrl(url);
expect(result).toMatchInlineSnapshot(
`"https://localhost:5601/app/kibana#/discover?foo=bar&_g=(yes:!t)&_a=(yes:!f)"`
);
});
it('unhashes only whitelisted properties', () => {
const stateParamKey1 = '_g';
const stateParamValueHashed1 = 'h@4e60e02';
const state1 = { yes: true };
const stateParamKey2 = '_a';
const stateParamValueHashed2 = 'h@61fa078';
const state2 = { yes: false };
const stateParamKey3 = '_someother';
const stateParamValueHashed3 = 'h@61fa078';
const state3 = { yes: false };
mockSessionStorage.setItem(stateParamValueHashed1, JSON.stringify(state1));
mockSessionStorage.setItem(stateParamValueHashed2, JSON.stringify(state2));
mockSessionStorage.setItem(stateParamValueHashed3, JSON.stringify(state3));
const url = `https://localhost:5601/app/kibana#/discover?foo=bar&${stateParamKey1}=${stateParamValueHashed1}&${stateParamKey2}=${stateParamValueHashed2}&${stateParamKey3}=${stateParamValueHashed3}`;
const result = unhashUrl(url);
expect(result).toMatchInlineSnapshot(
`"https://localhost:5601/app/kibana#/discover?foo=bar&_g=(yes:!t)&_a=(yes:!f)&_someother=h@61fa078"`
);
});
});
it('throws error if unable to restore the url', () => {
const stateParamKey1 = '_g';
const stateParamValueHashed1 = 'h@4e60e02';
const url = `https://localhost:5601/app/kibana#/discover?foo=bar&${stateParamKey1}=${stateParamValueHashed1}`;
expect(() => unhashUrl(url)).toThrowErrorMatchingInlineSnapshot(
`"Unable to completely restore the URL, be sure to use the share functionality."`
);
});
});
describe('hash unhash url integration', () => {
it('hashing and unhashing url should produce the same result', () => {
const stateParamKey1 = '_g';
const stateParamValue1 = '(yes:!t)';
const stateParamKey2 = '_a';
const stateParamValue2 = '(yes:!f)';
const stateParamKey3 = '_someother';
const stateParamValue3 = '(yes:!f)';
const url = `https://localhost:5601/app/kibana#/discover?foo=bar&${stateParamKey1}=${stateParamValue1}&${stateParamKey2}=${stateParamValue2}&${stateParamKey3}=${stateParamValue3}`;
const result = unhashUrl(hashUrl(url));
expect(url).toEqual(result);
});
});
});

View file

@ -0,0 +1,157 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { i18n } from '@kbn/i18n';
import rison, { RisonObject } from 'rison-node';
import { stringify as stringifyQueryString } from 'querystring';
import encodeUriQuery from 'encode-uri-query';
import { format as formatUrl, parse as parseUrl } from 'url';
import { HashedItemStoreSingleton } from '../state_storage';
import { createStateHash, isStateHash } from './state_hash';
export type IParsedUrlQuery = Record<string, any>;
interface IUrlQueryMapperOptions {
hashableParams: string[];
}
export type IUrlQueryReplacerOptions = IUrlQueryMapperOptions;
export const unhashQuery = createQueryMapper(stateHashToRisonState);
export const hashQuery = createQueryMapper(risonStateToStateHash);
export const unhashUrl = createQueryReplacer(unhashQuery);
export const hashUrl = createQueryReplacer(hashQuery);
// naive hack, but this allows to decouple these utils from AppState, GlobalState for now
// when removing AppState, GlobalState and migrating to IState containers,
// need to make sure that apps explicitly passing this whitelist to hash
const __HACK_HARDCODED_LEGACY_HASHABLE_PARAMS = ['_g', '_a', '_s'];
function createQueryMapper(queryParamMapper: (q: string) => string | null) {
return (
query: IParsedUrlQuery,
options: IUrlQueryMapperOptions = {
hashableParams: __HACK_HARDCODED_LEGACY_HASHABLE_PARAMS,
}
) =>
Object.fromEntries(
Object.entries(query || {}).map(([name, value]) => {
if (!options.hashableParams.includes(name)) return [name, value];
return [name, queryParamMapper(value) || value];
})
);
}
function createQueryReplacer(
queryMapper: (q: IParsedUrlQuery, options?: IUrlQueryMapperOptions) => IParsedUrlQuery,
options?: IUrlQueryReplacerOptions
) {
return (url: string) => {
if (!url) return url;
const parsedUrl = parseUrl(url, true);
if (!parsedUrl.hash) return url;
const appUrl = parsedUrl.hash.slice(1); // trim the #
if (!appUrl) return url;
const appUrlParsed = parseUrl(appUrl, true);
if (!appUrlParsed.query) return url;
const changedAppQuery = queryMapper(appUrlParsed.query, options);
// encodeUriQuery implements the less-aggressive encoding done naturally by
// the browser. We use it to generate the same urls the browser would
const changedAppQueryString = stringifyQueryString(changedAppQuery, undefined, undefined, {
encodeURIComponent: encodeUriQuery,
});
return formatUrl({
...parsedUrl,
hash: formatUrl({
pathname: appUrlParsed.pathname,
search: changedAppQueryString,
}),
});
};
}
// TODO: this helper should be merged with or replaced by
// src/legacy/ui/public/state_management/state_storage/hashed_item_store.ts
// maybe to become simplified stateless version
export function retrieveState(stateHash: string): RisonObject {
const json = HashedItemStoreSingleton.getItem(stateHash);
const throwUnableToRestoreUrlError = () => {
throw new Error(
i18n.translate('common.ui.stateManagement.unableToRestoreUrlErrorMessage', {
defaultMessage:
'Unable to completely restore the URL, be sure to use the share functionality.',
})
);
};
if (json === null) {
return throwUnableToRestoreUrlError();
}
try {
return JSON.parse(json);
} catch (e) {
return throwUnableToRestoreUrlError();
}
}
// TODO: this helper should be merged with or replaced by
// src/legacy/ui/public/state_management/state_storage/hashed_item_store.ts
// maybe to become simplified stateless version
export function persistState(state: RisonObject): string {
const json = JSON.stringify(state);
const hash = createStateHash(json);
const isItemSet = HashedItemStoreSingleton.setItem(hash, json);
if (isItemSet) return hash;
// If we ran out of space trying to persist the state, notify the user.
const message = i18n.translate(
'common.ui.stateManagement.unableToStoreHistoryInSessionErrorMessage',
{
defaultMessage:
'Kibana is unable to store history items in your session ' +
`because it is full and there don't seem to be items any items safe ` +
'to delete.\n\n' +
'This can usually be fixed by moving to a fresh tab, but could ' +
'be caused by a larger issue. If you are seeing this message regularly, ' +
'please file an issue at {gitHubIssuesUrl}.',
values: { gitHubIssuesUrl: 'https://github.com/elastic/kibana/issues' },
}
);
throw new Error(message);
}
function stateHashToRisonState(stateHashOrRison: string): string {
if (isStateHash(stateHashOrRison)) {
return rison.encode(retrieveState(stateHashOrRison));
}
return stateHashOrRison;
}
function risonStateToStateHash(stateHashOrRison: string): string | null {
if (isStateHash(stateHashOrRison)) {
return stateHashOrRison;
}
return persistState(rison.decode(stateHashOrRison) as RisonObject);
}

View file

@ -1,97 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import encodeUriQuery from 'encode-uri-query';
import rison from 'rison-node';
import { parse as parseUrl, format as formatUrl } from 'url';
import { stringify as stringifyQuerystring } from 'querystring';
const conservativeStringifyQuerystring = (query) => {
return stringifyQuerystring(query, null, null, {
encodeURIComponent: encodeUriQuery
});
};
const hashStateInQuery = (state, query) => {
const name = state.getQueryParamName();
const value = query[name];
if (!value) {
return { name, value };
}
const decodedValue = rison.decode(value);
const hashedValue = state.toQueryParam(decodedValue);
return { name, value: hashedValue };
};
const hashStatesInQuery = (states, query) => {
const hashedQuery = states.reduce((result, state) => {
const { name, value } = hashStateInQuery(state, query);
if (value) {
result[name] = value;
}
return result;
}, {});
return {
...query,
...hashedQuery
};
};
export const hashUrl = (states, redirectUrl) => {
// we need states to proceed, throwing an error if we don't have any
if (states === null || !states.length) {
throw new Error('states parameter must be an Array with length greater than 0');
}
const parsedUrl = parseUrl(redirectUrl);
// if we don't have a hash, we return the redirectUrl without hashing anything
if (!parsedUrl.hash) {
return redirectUrl;
}
// The URLs that we use aren't "conventional" and the hash is sometimes appearing before
// the querystring, even though conventionally they appear after it. The parsedUrl
// is the entire URL, and the parsedAppUrl is everything after the hash.
//
// EXAMPLE
// parsedUrl: /app/kibana#/visualize/edit/somelongguid?g=()&a=()
// parsedAppUrl: /visualize/edit/somelongguid?g=()&a=()
const parsedAppUrl = parseUrl(parsedUrl.hash.slice(1), true);
// the parsedAppUrl actually has the query that we care about
const query = parsedAppUrl.query;
const newQuery = hashStatesInQuery(states, query);
const newHash = formatUrl({
search: conservativeStringifyQuerystring(newQuery),
pathname: parsedAppUrl.pathname
});
return formatUrl({
hash: `#${newHash}`,
host: parsedUrl.host,
search: parsedUrl.search,
pathname: parsedUrl.pathname,
protocol: parsedUrl.protocol,
});
};

View file

@ -17,4 +17,5 @@
* under the License. * under the License.
*/ */
export function unhashUrl(url: string, kbnStates: any[]): any; export { hashUrl, unhashUrl, hashQuery, unhashQuery } from './hash_unhash_url';
export { createStateHash, isStateHash } from './state_hash';

View file

@ -17,58 +17,55 @@
* under the License. * under the License.
*/ */
import expect from '@kbn/expect';
import { encode as encodeRison } from 'rison-node'; import { encode as encodeRison } from 'rison-node';
import { mockSessionStorage } from '../state_storage/mock';
import { import { createStateHash, isStateHash } from '../state_hashing';
createStateHash,
isStateHash,
} from '../state_hash';
describe('stateHash', () => { describe('stateHash', () => {
const existingJsonProvider = () => null; beforeEach(() => {
mockSessionStorage.clear();
});
describe('#createStateHash', () => { describe('#createStateHash', () => {
it('returns a hash', () => {
describe('returns a hash', () => {
const json = JSON.stringify({ a: 'a' }); const json = JSON.stringify({ a: 'a' });
const hash = createStateHash(json, existingJsonProvider); const hash = createStateHash(json);
expect(isStateHash(hash)).to.be(true); expect(isStateHash(hash)).toBe(true);
}); });
describe('returns the same hash for the same input', () => { it('returns the same hash for the same input', () => {
const json = JSON.stringify({ a: 'a' }); const json = JSON.stringify({ a: 'a' });
const hash1 = createStateHash(json, existingJsonProvider); const hash1 = createStateHash(json);
const hash2 = createStateHash(json, existingJsonProvider); const hash2 = createStateHash(json);
expect(hash1).to.equal(hash2); expect(hash1).toEqual(hash2);
}); });
describe('returns a different hash for different input', () => { it('returns a different hash for different input', () => {
const json1 = JSON.stringify({ a: 'a' }); const json1 = JSON.stringify({ a: 'a' });
const hash1 = createStateHash(json1, existingJsonProvider); const hash1 = createStateHash(json1);
const json2 = JSON.stringify({ a: 'b' }); const json2 = JSON.stringify({ a: 'b' });
const hash2 = createStateHash(json2, existingJsonProvider); const hash2 = createStateHash(json2);
expect(hash1).to.not.equal(hash2); expect(hash1).not.toEqual(hash2);
}); });
}); });
describe('#isStateHash', () => { describe('#isStateHash', () => {
it('returns true for values created using #createStateHash', () => { it('returns true for values created using #createStateHash', () => {
const json = JSON.stringify({ a: 'a' }); const json = JSON.stringify({ a: 'a' });
const hash = createStateHash(json, existingJsonProvider); const hash = createStateHash(json);
expect(isStateHash(hash)).to.be(true); expect(isStateHash(hash)).toBe(true);
}); });
it('returns false for values not created using #createStateHash', () => { it('returns false for values not created using #createStateHash', () => {
const json = JSON.stringify({ a: 'a' }); const json = JSON.stringify({ a: 'a' });
expect(isStateHash(json)).to.be(false); expect(isStateHash(json)).toBe(false);
}); });
it('returns false for RISON', () => { it('returns false for RISON', () => {
// We're storing RISON in the URL, so let's test against this specifically. // We're storing RISON in the URL, so let's test against this specifically.
const rison = encodeRison({ a: 'a' }); const rison = encodeRison({ a: 'a' });
expect(isStateHash(rison)).to.be(false); expect(isStateHash(rison)).toBe(false);
}); });
}); });
}); });

View file

@ -17,12 +17,16 @@
* under the License. * under the License.
*/ */
import { Sha256 } from '../../../../../core/public/utils/'; import { Sha256 } from '../../../../../core/public/utils';
import { HashedItemStoreSingleton } from '../state_storage';
// This prefix is used to identify hash strings that have been encoded in the URL. // This prefix is used to identify hash strings that have been encoded in the URL.
const HASH_PREFIX = 'h@'; const HASH_PREFIX = 'h@';
export function createStateHash(json, existingJsonProvider) { export function createStateHash(
json: string,
existingJsonProvider?: (hash: string) => string | null // TODO: temp while state.js relies on this in tests
) {
if (typeof json !== 'string') { if (typeof json !== 'string') {
throw new Error('createHash only accepts strings (JSON).'); throw new Error('createHash only accepts strings (JSON).');
} }
@ -36,13 +40,15 @@ export function createStateHash(json, existingJsonProvider) {
// b) or has been used already, but with the JSON we're currently hashing. // b) or has been used already, but with the JSON we're currently hashing.
for (let i = 7; i < hash.length; i++) { for (let i = 7; i < hash.length; i++) {
shortenedHash = hash.slice(0, i); shortenedHash = hash.slice(0, i);
const existingJson = existingJsonProvider(shortenedHash); const existingJson = existingJsonProvider
? existingJsonProvider(shortenedHash)
: HashedItemStoreSingleton.getItem(shortenedHash);
if (existingJson === null || existingJson === json) break; if (existingJson === null || existingJson === json) break;
} }
return `${HASH_PREFIX}${shortenedHash}`; return `${HASH_PREFIX}${shortenedHash}`;
} }
export function isStateHash(str) { export function isStateHash(str: string) {
return String(str).indexOf(HASH_PREFIX) === 0; return String(str).indexOf(HASH_PREFIX) === 0;
} }

View file

@ -1,37 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { mapValues } from 'lodash';
import { ParsedUrlQuery } from 'querystring';
import { State } from '../state';
/**
* Takes in a parsed url query and state objects, finding the state objects that match the query parameters and expanding
* the hashed state. For example, a url query string like '?_a=@12353&_g=@19028df' will become
* '?_a=[expanded app state here]&_g=[expanded global state here]. This is used when storeStateInSessionStorage is turned on.
*/
export function unhashQueryString(
parsedQueryString: ParsedUrlQuery,
states: State[]
): ParsedUrlQuery {
return mapValues(parsedQueryString, (val, key) => {
const state = states.find(s => key === s.getQueryParamName());
return state ? state.translateHashToRison(val) : val;
});
}

View file

@ -1,68 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import {
parse as parseUrl,
format as formatUrl,
} from 'url';
import encodeUriQuery from 'encode-uri-query';
import {
stringify as stringifyQueryString
} from 'querystring';
import { unhashQueryString } from './unhash_query_string';
export 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);
// encodeUriQuery implements the less-aggressive encoding done naturally by
// the browser. We use it to generate the same urls the browser would
const appQueryStringWithoutHashes = stringifyQueryString(appQueryWithoutHashes, null, null, {
encodeURIComponent: encodeUriQuery
});
return formatUrl({
...urlWithHashesParsed,
hash: formatUrl({
pathname: appUrlParsed.pathname,
search: appQueryStringWithoutHashes,
})
});
}

View file

@ -17,22 +17,19 @@
* under the License. * under the License.
*/ */
import expect from '@kbn/expect';
import sinon from 'sinon';
import { StubBrowserStorage } from 'test_utils/stub_browser_storage'; import { StubBrowserStorage } from 'test_utils/stub_browser_storage';
import { HashedItemStore } from '../hashed_item_store'; import { HashedItemStore } from './hashed_item_store';
describe('hashedItemStore', () => { describe('hashedItemStore', () => {
describe('interface', () => { describe('interface', () => {
describe('#constructor', () => { describe('#constructor', () => {
it('retrieves persisted index from sessionStorage', () => { it('retrieves persisted index from sessionStorage', () => {
const sessionStorage = new StubBrowserStorage(); const sessionStorage = new StubBrowserStorage();
sinon.spy(sessionStorage, 'getItem'); const spy = jest.spyOn(sessionStorage, 'getItem');
new HashedItemStore(sessionStorage); new HashedItemStore(sessionStorage);
sinon.assert.calledWith(sessionStorage.getItem, HashedItemStore.PERSISTED_INDEX_KEY); expect(spy).toBeCalledWith(HashedItemStore.PERSISTED_INDEX_KEY);
sessionStorage.getItem.restore(); spy.mockReset();
}); });
it('sorts indexed items by touched property', () => { it('sorts indexed items by touched property', () => {
@ -57,14 +54,14 @@ describe('hashedItemStore', () => {
sessionStorage.setItem(HashedItemStore.PERSISTED_INDEX_KEY, JSON.stringify({ a, b, c })); sessionStorage.setItem(HashedItemStore.PERSISTED_INDEX_KEY, JSON.stringify({ a, b, c }));
const hashedItemStore = new HashedItemStore(sessionStorage); const hashedItemStore = new HashedItemStore(sessionStorage);
expect(hashedItemStore._indexedItems).to.eql([a, c, b]); expect((hashedItemStore as any).indexedItems).toEqual([a, c, b]);
}); });
}); });
describe('#setItem', () => { describe('#setItem', () => {
describe('if the item exists in sessionStorage', () => { describe('if the item exists in sessionStorage', () => {
let sessionStorage; let sessionStorage: Storage;
let hashedItemStore; let hashedItemStore: HashedItemStore;
const hash = 'a'; const hash = 'a';
const item = JSON.stringify({}); const item = JSON.stringify({});
@ -75,19 +72,19 @@ describe('hashedItemStore', () => {
it('persists the item in sessionStorage', () => { it('persists the item in sessionStorage', () => {
hashedItemStore.setItem(hash, item); hashedItemStore.setItem(hash, item);
expect(sessionStorage.getItem(hash)).to.equal(item); expect(sessionStorage.getItem(hash)).toEqual(item);
}); });
it('returns true', () => { it('returns true', () => {
const result = hashedItemStore.setItem(hash, item); const result = hashedItemStore.setItem(hash, item);
expect(result).to.equal(true); expect(result).toEqual(true);
}); });
}); });
describe(`if the item doesn't exist in sessionStorage`, () => { describe(`if the item doesn't exist in sessionStorage`, () => {
describe(`if there's storage space`, () => { describe(`if there's storage space`, () => {
let sessionStorage; let sessionStorage: Storage;
let hashedItemStore; let hashedItemStore: HashedItemStore;
const hash = 'a'; const hash = 'a';
const item = JSON.stringify({}); const item = JSON.stringify({});
@ -98,32 +95,31 @@ describe('hashedItemStore', () => {
it('persists the item in sessionStorage', () => { it('persists the item in sessionStorage', () => {
hashedItemStore.setItem(hash, item); hashedItemStore.setItem(hash, item);
expect(sessionStorage.getItem(hash)).to.equal(item); expect(sessionStorage.getItem(hash)).toEqual(item);
}); });
it('returns true', () => { it('returns true', () => {
const result = hashedItemStore.setItem(hash, item); const result = hashedItemStore.setItem(hash, item);
expect(result).to.equal(true); expect(result).toEqual(true);
}); });
}); });
describe(`if there isn't storage space`, () => { describe(`if there isn't storage space`, () => {
let fakeTimer; let sessionStorage: Storage;
let sessionStorage; let hashedItemStore: HashedItemStore;
let hashedItemStore; let storageSizeLimit: number;
let storageSizeLimit;
const hash = 'a'; const hash = 'a';
const item = JSON.stringify({}); const item = JSON.stringify({});
function setItemLater(hash, item) { function setItemLater(_hash: string, _item: string) {
// Move time forward, so this item will be "touched" most recently. // Move time forward, so this item will be "touched" most recently.
fakeTimer.tick(1); jest.advanceTimersByTime(1);
return hashedItemStore.setItem(hash, item); return hashedItemStore.setItem(_hash, _item);
} }
beforeEach(() => { beforeEach(() => {
// Control time. // Control time.
fakeTimer = sinon.useFakeTimers(Date.now()); jest.useFakeTimers();
sessionStorage = new StubBrowserStorage(); sessionStorage = new StubBrowserStorage();
hashedItemStore = new HashedItemStore(sessionStorage); hashedItemStore = new HashedItemStore(sessionStorage);
@ -141,29 +137,29 @@ describe('hashedItemStore', () => {
afterEach(() => { afterEach(() => {
// Stop controlling time. // Stop controlling time.
fakeTimer.restore(); jest.useRealTimers();
}); });
describe('and the item will fit', () => { describe('and the item will fit', () => {
it('removes older items until the new item fits', () => { it('removes older items until the new item fits', () => {
setItemLater(hash, item); setItemLater(hash, item);
expect(sessionStorage.getItem('b')).to.equal(null); expect(sessionStorage.getItem('b')).toEqual(null);
expect(sessionStorage.getItem('c')).to.equal(item); expect(sessionStorage.getItem('c')).toEqual(item);
}); });
it('persists the item in sessionStorage', () => { it('persists the item in sessionStorage', () => {
setItemLater(hash, item); setItemLater(hash, item);
expect(sessionStorage.getItem(hash)).to.equal(item); expect(sessionStorage.getItem(hash)).toEqual(item);
}); });
it('returns true', () => { it('returns true', () => {
const result = setItemLater(hash, item); const result = setItemLater(hash, item);
expect(result).to.equal(true); expect(result).toEqual(true);
}); });
}); });
describe(`and the item won't fit`, () => { describe(`and the item won't fit`, () => {
let itemTooBigToFit; let itemTooBigToFit: string;
beforeEach(() => { beforeEach(() => {
// Make sure the item is longer than the storage size limit. // Make sure the item is longer than the storage size limit.
@ -176,18 +172,18 @@ describe('hashedItemStore', () => {
it('removes all items', () => { it('removes all items', () => {
setItemLater(hash, itemTooBigToFit); setItemLater(hash, itemTooBigToFit);
expect(sessionStorage.getItem('b')).to.equal(null); expect(sessionStorage.getItem('b')).toEqual(null);
expect(sessionStorage.getItem('c')).to.equal(null); expect(sessionStorage.getItem('c')).toEqual(null);
}); });
it(`doesn't persist the item in sessionStorage`, () => { it(`doesn't persist the item in sessionStorage`, () => {
setItemLater(hash, itemTooBigToFit); setItemLater(hash, itemTooBigToFit);
expect(sessionStorage.getItem(hash)).to.equal(null); expect(sessionStorage.getItem(hash)).toEqual(null);
}); });
it('returns false', () => { it('returns false', () => {
const result = setItemLater(hash, itemTooBigToFit); const result = setItemLater(hash, itemTooBigToFit);
expect(result).to.equal(false); expect(result).toEqual(false);
}); });
}); });
}); });
@ -196,25 +192,24 @@ describe('hashedItemStore', () => {
describe('#getItem', () => { describe('#getItem', () => {
describe('if the item exists in sessionStorage', () => { describe('if the item exists in sessionStorage', () => {
let fakeTimer; let sessionStorage: Storage;
let sessionStorage; let hashedItemStore: HashedItemStore;
let hashedItemStore;
function setItemLater(hash, item) { function setItemLater(hash: string, item: string) {
// Move time forward, so this item will be "touched" most recently. // Move time forward, so this item will be "touched" most recently.
fakeTimer.tick(1); jest.advanceTimersByTime(1);
return hashedItemStore.setItem(hash, item); return hashedItemStore.setItem(hash, item);
} }
function getItemLater(hash) { function getItemLater(hash: string) {
// Move time forward, so this item will be "touched" most recently. // Move time forward, so this item will be "touched" most recently.
fakeTimer.tick(1); jest.advanceTimersByTime(1);
return hashedItemStore.getItem(hash); return hashedItemStore.getItem(hash);
} }
beforeEach(() => { beforeEach(() => {
// Control time. // Control time.
fakeTimer = sinon.useFakeTimers(Date.now()); jest.useFakeTimers();
sessionStorage = new StubBrowserStorage(); sessionStorage = new StubBrowserStorage();
hashedItemStore = new HashedItemStore(sessionStorage); hashedItemStore = new HashedItemStore(sessionStorage);
@ -223,12 +218,12 @@ describe('hashedItemStore', () => {
afterEach(() => { afterEach(() => {
// Stop controlling time. // Stop controlling time.
fakeTimer.restore(); jest.useRealTimers();
}); });
it('returns the item', () => { it('returns the item', () => {
const retrievedItem = hashedItemStore.getItem('1'); const retrievedItem = hashedItemStore.getItem('1');
expect(retrievedItem).to.be('a'); expect(retrievedItem).toBe('a');
}); });
it('prevents the item from being first to be removed when freeing up storage space', () => { it('prevents the item from being first to be removed when freeing up storage space', () => {
@ -244,14 +239,14 @@ describe('hashedItemStore', () => {
// Add a new item, causing the second item to be removed, but not the first. // Add a new item, causing the second item to be removed, but not the first.
setItemLater('3', 'c'); setItemLater('3', 'c');
expect(hashedItemStore.getItem('2')).to.equal(null); expect(hashedItemStore.getItem('2')).toEqual(null);
expect(hashedItemStore.getItem('1')).to.equal('a'); expect(hashedItemStore.getItem('1')).toEqual('a');
}); });
}); });
describe(`if the item doesn't exist in sessionStorage`, () => { describe(`if the item doesn't exist in sessionStorage`, () => {
let sessionStorage; let sessionStorage: Storage;
let hashedItemStore; let hashedItemStore: HashedItemStore;
const hash = 'a'; const hash = 'a';
beforeEach(() => { beforeEach(() => {
@ -261,40 +256,38 @@ describe('hashedItemStore', () => {
it('returns null', () => { it('returns null', () => {
const retrievedItem = hashedItemStore.getItem(hash); const retrievedItem = hashedItemStore.getItem(hash);
expect(retrievedItem).to.be(null); expect(retrievedItem).toBe(null);
}); });
}); });
}); });
}); });
describe('behavior', () => { describe('behavior', () => {
let fakeTimer; let sessionStorage: Storage;
let sessionStorage; let hashedItemStore: HashedItemStore;
let hashedItemStore;
function setItemLater(hash, item) { function setItemLater(hash: string, item: string) {
// Move time forward, so this item will be "touched" most recently. // Move time forward, so this item will be "touched" most recently.
fakeTimer.tick(1); jest.advanceTimersByTime(1);
return hashedItemStore.setItem(hash, item); return hashedItemStore.setItem(hash, item);
} }
function getItemLater(hash) { function getItemLater(hash: string) {
// Move time forward, so this item will be "touched" most recently. // Move time forward, so this item will be "touched" most recently.
fakeTimer.tick(1); jest.advanceTimersByTime(1);
return hashedItemStore.getItem(hash); return hashedItemStore.getItem(hash);
} }
beforeEach(() => { beforeEach(() => {
// Control time. // Control time.
fakeTimer = sinon.useFakeTimers(Date.now()); jest.useFakeTimers();
sessionStorage = new StubBrowserStorage(); sessionStorage = new StubBrowserStorage();
hashedItemStore = new HashedItemStore(sessionStorage); hashedItemStore = new HashedItemStore(sessionStorage);
}); });
afterEach(() => { afterEach(() => {
// Stop controlling time. // Stop controlling time.
fakeTimer.restore(); jest.useRealTimers();
}); });
it('orders items to be removed based on when they were last retrieved', () => { it('orders items to be removed based on when they were last retrieved', () => {
@ -314,39 +307,39 @@ describe('hashedItemStore', () => {
getItemLater('4'); getItemLater('4');
setItemLater('5', 'e'); setItemLater('5', 'e');
expect(hashedItemStore.getItem('1')).to.equal(null); expect(hashedItemStore.getItem('1')).toEqual(null);
expect(hashedItemStore.getItem('3')).to.equal('c'); expect(hashedItemStore.getItem('3')).toEqual('c');
expect(hashedItemStore.getItem('2')).to.equal('b'); expect(hashedItemStore.getItem('2')).toEqual('b');
expect(hashedItemStore.getItem('4')).to.equal('d'); expect(hashedItemStore.getItem('4')).toEqual('d');
expect(hashedItemStore.getItem('5')).to.equal('e'); expect(hashedItemStore.getItem('5')).toEqual('e');
setItemLater('6', 'f'); setItemLater('6', 'f');
expect(hashedItemStore.getItem('3')).to.equal(null); expect(hashedItemStore.getItem('3')).toEqual(null);
expect(hashedItemStore.getItem('2')).to.equal('b'); expect(hashedItemStore.getItem('2')).toEqual('b');
expect(hashedItemStore.getItem('4')).to.equal('d'); expect(hashedItemStore.getItem('4')).toEqual('d');
expect(hashedItemStore.getItem('5')).to.equal('e'); expect(hashedItemStore.getItem('5')).toEqual('e');
expect(hashedItemStore.getItem('6')).to.equal('f'); expect(hashedItemStore.getItem('6')).toEqual('f');
setItemLater('7', 'g'); setItemLater('7', 'g');
expect(hashedItemStore.getItem('2')).to.equal(null); expect(hashedItemStore.getItem('2')).toEqual(null);
expect(hashedItemStore.getItem('4')).to.equal('d'); expect(hashedItemStore.getItem('4')).toEqual('d');
expect(hashedItemStore.getItem('5')).to.equal('e'); expect(hashedItemStore.getItem('5')).toEqual('e');
expect(hashedItemStore.getItem('6')).to.equal('f'); expect(hashedItemStore.getItem('6')).toEqual('f');
expect(hashedItemStore.getItem('7')).to.equal('g'); expect(hashedItemStore.getItem('7')).toEqual('g');
setItemLater('8', 'h'); setItemLater('8', 'h');
expect(hashedItemStore.getItem('4')).to.equal(null); expect(hashedItemStore.getItem('4')).toEqual(null);
expect(hashedItemStore.getItem('5')).to.equal('e'); expect(hashedItemStore.getItem('5')).toEqual('e');
expect(hashedItemStore.getItem('6')).to.equal('f'); expect(hashedItemStore.getItem('6')).toEqual('f');
expect(hashedItemStore.getItem('7')).to.equal('g'); expect(hashedItemStore.getItem('7')).toEqual('g');
expect(hashedItemStore.getItem('8')).to.equal('h'); expect(hashedItemStore.getItem('8')).toEqual('h');
setItemLater('9', 'i'); setItemLater('9', 'i');
expect(hashedItemStore.getItem('5')).to.equal(null); expect(hashedItemStore.getItem('5')).toEqual(null);
expect(hashedItemStore.getItem('6')).to.equal('f'); expect(hashedItemStore.getItem('6')).toEqual('f');
expect(hashedItemStore.getItem('7')).to.equal('g'); expect(hashedItemStore.getItem('7')).toEqual('g');
expect(hashedItemStore.getItem('8')).to.equal('h'); expect(hashedItemStore.getItem('8')).toEqual('h');
expect(hashedItemStore.getItem('9')).to.equal('i'); expect(hashedItemStore.getItem('9')).toEqual('i');
}); });
}); });
}); });

View file

@ -72,59 +72,66 @@
import { pull, sortBy } from 'lodash'; import { pull, sortBy } from 'lodash';
interface IndexedItem {
hash: string;
touched?: number; // Date.now()
}
export class HashedItemStore { export class HashedItemStore {
static readonly PERSISTED_INDEX_KEY = 'kbn.hashedItemsIndex.v1';
private sessionStorage: Storage;
// Store indexed items in descending order by touched (oldest first, newest last). We'll use
// this to remove older items when we run out of storage space.
private indexedItems: IndexedItem[] = [];
/** /**
* HashedItemStore uses objects called indexed items to refer to items that have been persisted * HashedItemStore uses objects called indexed items to refer to items that have been persisted
* in sessionStorage. An indexed item is shaped {hash, touched}. The touched date is when the item * in sessionStorage. An indexed item is shaped {hash, touched}. The touched date is when the item
* was last referenced by the browser history. * was last referenced by the browser history.
*/ */
constructor(sessionStorage) { constructor(sessionStorage: Storage) {
this._sessionStorage = sessionStorage; this.sessionStorage = sessionStorage;
// Store indexed items in descending order by touched (oldest first, newest last). We'll use
// this to remove older items when we run out of storage space.
this._indexedItems = [];
// Potentially restore a previously persisted index. This happens when // Potentially restore a previously persisted index. This happens when
// we re-open a closed tab. // we re-open a closed tab.
const persistedItemIndex = this._sessionStorage.getItem(HashedItemStore.PERSISTED_INDEX_KEY); const persistedItemIndex = this.sessionStorage.getItem(HashedItemStore.PERSISTED_INDEX_KEY);
if (persistedItemIndex) { if (persistedItemIndex) {
this._indexedItems = sortBy(JSON.parse(persistedItemIndex) || [], 'touched'); this.indexedItems = sortBy(JSON.parse(persistedItemIndex) || [], 'touched');
} }
} }
setItem(hash, item) { setItem(hash: string, item: string): boolean {
const isItemPersisted = this._persistItem(hash, item); const isItemPersisted = this.persistItem(hash, item);
if (isItemPersisted) { if (isItemPersisted) {
this._touchHash(hash); this.touchHash(hash);
} }
return isItemPersisted; return isItemPersisted;
} }
getItem(hash) { getItem(hash: string): string | null {
const item = this._sessionStorage.getItem(hash); const item = this.sessionStorage.getItem(hash);
if (item !== null) { if (item !== null) {
this._touchHash(hash); this.touchHash(hash);
} }
return item; return item;
} }
_getIndexedItem(hash) { private getIndexedItem(hash: string) {
return this._indexedItems.find(indexedItem => indexedItem.hash === hash); return this.indexedItems.find(indexedItem => indexedItem.hash === hash);
} }
_persistItem(hash, item) { private persistItem(hash: string, item: string): boolean {
try { try {
this._sessionStorage.setItem(hash, item); this.sessionStorage.setItem(hash, item);
return true; return true;
} catch (e) { } catch (e) {
// If there was an error then we need to make some space for the item. // If there was an error then we need to make some space for the item.
if (this._indexedItems.length === 0) { if (this.indexedItems.length === 0) {
// If there's nothing left to remove, then we've run out of space and we're trying to // If there's nothing left to remove, then we've run out of space and we're trying to
// persist too large an item. // persist too large an item.
return false; return false;
@ -132,39 +139,39 @@ export class HashedItemStore {
// We need to try to make some space for the item by removing older items (i.e. items that // We need to try to make some space for the item by removing older items (i.e. items that
// haven't been accessed recently). // haven't been accessed recently).
this._removeOldestItem(); this.removeOldestItem();
// Try to persist again. // Try to persist again.
return this._persistItem(hash, item); return this.persistItem(hash, item);
} }
} }
_removeOldestItem() { private removeOldestItem() {
const oldestIndexedItem = this._indexedItems.shift(); const oldestIndexedItem = this.indexedItems.shift();
// Remove oldest item from storage. if (oldestIndexedItem) {
this._sessionStorage.removeItem(oldestIndexedItem.hash); // Remove oldest item from storage.
this.sessionStorage.removeItem(oldestIndexedItem.hash);
}
} }
_touchHash(hash) { private touchHash(hash: string) {
// Touching a hash indicates that it's been used recently, so it won't be the first in line // Touching a hash indicates that it's been used recently, so it won't be the first in line
// when we remove items to free up storage space. // when we remove items to free up storage space.
// either get or create an indexedItem // either get or create an indexedItem
const indexedItem = this._getIndexedItem(hash) || { hash }; const indexedItem = this.getIndexedItem(hash) || { hash };
// set/update the touched time to now so that it's the "newest" item in the index // set/update the touched time to now so that it's the "newest" item in the index
indexedItem.touched = Date.now(); indexedItem.touched = Date.now();
// ensure that the item is last in the index // ensure that the item is last in the index
pull(this._indexedItems, indexedItem); pull(this.indexedItems, indexedItem);
this._indexedItems.push(indexedItem); this.indexedItems.push(indexedItem);
// Regardless of whether this is a new or updated item, we need to persist the index. // Regardless of whether this is a new or updated item, we need to persist the index.
this._sessionStorage.setItem( this.sessionStorage.setItem(
HashedItemStore.PERSISTED_INDEX_KEY, HashedItemStore.PERSISTED_INDEX_KEY,
JSON.stringify(this._indexedItems) JSON.stringify(this.indexedItems)
); );
} }
} }
HashedItemStore.PERSISTED_INDEX_KEY = 'kbn.hashedItemsIndex.v1';

View file

@ -18,8 +18,3 @@
*/ */
export { HashedItemStoreSingleton } from './hashed_item_store_singleton'; export { HashedItemStoreSingleton } from './hashed_item_store_singleton';
export {
createStateHash,
isStateHash,
} from './state_hash';

View file

@ -17,12 +17,21 @@
* under the License. * under the License.
*/ */
import { AppState } from '../app_state'; import { StubBrowserStorage } from 'test_utils/stub_browser_storage';
import { GlobalState } from '../global_state'; import { HashedItemStore } from './hashed_item_store';
import { State } from '../state';
export function getUnhashableStatesProvider(getAppState: () => AppState, globalState: GlobalState) { /**
return function getUnhashableStates(): State[] { * Useful for mocking state_storage from jest,
return [getAppState(), globalState].filter(Boolean); *
* import { mockSessionStorage } from '../state_storage/mock;
*
* And all tests in the test file will use HashedItemStoreSingleton
* with underlying mockSessionStorage we have access to
*/
export const mockSessionStorage = new StubBrowserStorage();
const mockHashedItemStore = new HashedItemStore(mockSessionStorage);
jest.mock('../state_storage', () => {
return {
HashedItemStoreSingleton: mockHashedItemStore,
}; };
} });

View file

@ -39,6 +39,18 @@ describe('StubBrowserStorage', () => {
}); });
}); });
describe('#clear()', () => {
it('clears items', () => {
const store = new StubBrowserStorage();
store.setItem('1', '1');
store.setItem('2', '2');
store.clear();
expect(store.getItem('1')).toBe(null);
expect(store.getItem('2')).toBe(null);
expect(store.length).toBe(0);
});
});
describe('#length', () => { describe('#length', () => {
it('reports the number of items stored', () => { it('reports the number of items stored', () => {
const store = new StubBrowserStorage(); const store = new StubBrowserStorage();

View file

@ -17,9 +17,9 @@
* under the License. * under the License.
*/ */
export class StubBrowserStorage { export class StubBrowserStorage implements Storage {
private readonly keys: string[] = []; private keys: string[] = [];
private readonly values: string[] = []; private values: string[] = [];
private size = 0; private size = 0;
private sizeLimit = 5000000; // 5mb, minimum browser storage size; private sizeLimit = 5000000; // 5mb, minimum browser storage size;
@ -73,6 +73,12 @@ export class StubBrowserStorage {
this.values.splice(i, 1); this.values.splice(i, 1);
} }
public clear() {
this.size = 0;
this.keys = [];
this.values = [];
}
// ----------------------------------------------------------------------------------------------- // -----------------------------------------------------------------------------------------------
// Test-specific methods. // Test-specific methods.
// ----------------------------------------------------------------------------------------------- // -----------------------------------------------------------------------------------------------

View file

@ -105,7 +105,7 @@ export default function({ getService, getPageObjects }: FtrProviderContext) {
await browser.get(`${basePath}/app/kibana#/home`, false); await browser.get(`${basePath}/app/kibana#/home`, false);
await retry.waitFor( await retry.waitFor(
'navigation to home app', 'navigation to home app',
async () => (await browser.getCurrentUrl()) === `${basePath}/app/kibana#/home?_g=()` async () => (await browser.getCurrentUrl()) === `${basePath}/app/kibana#/home`
); );
await browser.get(`${basePath}/app/kibana#/home?_g=()&a=b/c`, false); await browser.get(`${basePath}/app/kibana#/home?_g=()&a=b/c`, false);

View file

@ -234,7 +234,7 @@ export default function ({ getService, getPageObjects }) {
describe('embedded mode', () => { describe('embedded mode', () => {
it('should hide side editor if embed is set to true in url', async () => { it('should hide side editor if embed is set to true in url', async () => {
const url = await browser.getCurrentUrl(); const url = await browser.getCurrentUrl();
const embedUrl = url.split('/visualize/').pop().replace('?_g=', '?embed=true&_g='); const embedUrl = url.split('/visualize/').pop() + '&embed=true';
await PageObjects.common.navigateToUrl('visualize', embedUrl); await PageObjects.common.navigateToUrl('visualize', embedUrl);
await PageObjects.header.waitUntilLoadingHasFinished(); await PageObjects.header.waitUntilLoadingHasFinished();
const sideEditorExists = await PageObjects.visualize.getSideEditorExists(); const sideEditorExists = await PageObjects.visualize.getSideEditorExists();
@ -243,7 +243,7 @@ export default function ({ getService, getPageObjects }) {
after(async () => { after(async () => {
const url = await browser.getCurrentUrl(); const url = await browser.getCurrentUrl();
const embedUrl = url.split('/visualize/').pop().replace('?embed=true&', '?'); const embedUrl = url.split('/visualize/').pop().replace('embed=true', '');
await PageObjects.common.navigateToUrl('visualize', embedUrl); await PageObjects.common.navigateToUrl('visualize', embedUrl);
}); });
}); });

View file

@ -17,7 +17,8 @@
* under the License. * under the License.
*/ */
export { getUnhashableStatesProvider } from './get_unhashable_states_provider'; declare module 'encode-uri-query' {
export { hashUrl } from './hash_url'; function encodeUriQuery(query: string, usePercentageSpace?: boolean): string;
export { unhashQueryString } from './unhash_query_string'; // eslint-disable-next-line import/no-default-export
export { unhashUrl } from './unhash_url'; export default encodeUriQuery;
}

39
typings/rison_node.d.ts vendored Normal file
View file

@ -0,0 +1,39 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
declare module 'rison-node' {
export type RisonValue = null | boolean | number | string | RisonObject | RisonArray;
// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface RisonArray extends Array<RisonValue> {}
export interface RisonObject {
[key: string]: RisonValue;
}
export const decode: (input: string) => RisonValue;
// eslint-disable-next-line @typescript-eslint/camelcase
export const decode_object: (input: string) => RisonObject;
export const encode: <Input extends RisonValue>(input: Input) => string;
// eslint-disable-next-line @typescript-eslint/camelcase
export const encode_object: <Input extends RisonObject>(input: Input) => string;
}

11
x-pack/typings/encode_uri_query.d.ts vendored Normal file
View file

@ -0,0 +1,11 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
declare module 'encode-uri-query' {
function encodeUriQuery(query: string, usePercentageSpace?: boolean): string;
// eslint-disable-next-line import/no-default-export
export default encodeUriQuery;
}