diff --git a/src/plugins/discover/common/locator.ts b/src/plugins/discover/common/locator.ts index d4e33290737e1..6c265bd8556d6 100644 --- a/src/plugins/discover/common/locator.ts +++ b/src/plugins/discover/common/locator.ts @@ -11,6 +11,7 @@ import type { Filter, TimeRange, Query, AggregateQuery } from '@kbn/es-query'; import type { GlobalQueryStateFromUrl, RefreshInterval } from '@kbn/data-plugin/public'; import type { LocatorDefinition, LocatorPublic } from '@kbn/share-plugin/public'; import { DataViewSpec } from '@kbn/data-views-plugin/common'; +import { setStateToKbnUrl } from '@kbn/kibana-utils-plugin/common'; import { VIEW_MODE } from './constants'; export const DISCOVER_APP_LOCATOR = 'DISCOVER_APP_LOCATOR'; @@ -96,12 +97,7 @@ export type DiscoverAppLocator = LocatorPublic; export interface DiscoverAppLocatorDependencies { useHash: boolean; - setStateToKbnUrl: ( - key: string, - state: State, - options: { useHash: boolean; storeInHashQuery?: boolean }, - rawUrl: string - ) => string; + setStateToKbnUrl: typeof setStateToKbnUrl; } /** diff --git a/src/plugins/kibana_utils/common/state_management/encode_state.ts b/src/plugins/kibana_utils/common/state_management/encode_state.ts index 6a2775974be72..53026c716bfce 100644 --- a/src/plugins/kibana_utils/common/state_management/encode_state.ts +++ b/src/plugins/kibana_utils/common/state_management/encode_state.ts @@ -6,16 +6,20 @@ * Side Public License, v 1. */ -import rison, { RisonValue } from '@kbn/rison'; -import { createStateHash } from './state_hash'; +import rison from '@kbn/rison'; -/** - * Common 'encodeState' without HashedItemStore support - */ -export function encodeState(state: State, useHash: boolean): string { +// should be: +// export function encodeState but this leads to the chain of +// types mismatches up to BaseStateContainer interfaces, as in state containers we don't +// have any restrictions on state shape +export function encodeState( + state: State, + useHash: boolean, + createHash: (rawState: State) => string +): string { if (useHash) { - return createStateHash(JSON.stringify(state)); + return createHash(state); } else { - return rison.encode(state as unknown as RisonValue); + return rison.encodeUnknown(state) ?? ''; } } diff --git a/src/plugins/kibana_utils/common/state_management/set_state_to_kbn_url.test.ts b/src/plugins/kibana_utils/common/state_management/set_state_to_kbn_url.test.ts new file mode 100644 index 0000000000000..0d13171810c4d --- /dev/null +++ b/src/plugins/kibana_utils/common/state_management/set_state_to_kbn_url.test.ts @@ -0,0 +1,106 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { createSetStateToKbnUrl, setStateToKbnUrl } from './set_state_to_kbn_url'; + +describe('set_state_to_kbn_url', () => { + describe('createSetStateToKbnUrl', () => { + it('should call createHash', () => { + const createHash = jest.fn(() => 'hash'); + const localSetStateToKbnUrl = createSetStateToKbnUrl(createHash); + const url = 'http://localhost:5601/oxf/app/kibana#/yourApp'; + const state = { foo: 'bar' }; + const newUrl = localSetStateToKbnUrl('_s', state, { useHash: true }, url); + expect(createHash).toHaveBeenCalledTimes(1); + expect(createHash).toHaveBeenCalledWith(state); + expect(newUrl).toMatchInlineSnapshot( + `"http://localhost:5601/oxf/app/kibana#/yourApp?_s=hash"` + ); + }); + + it('should not call createHash', () => { + const createHash = jest.fn(); + const localSetStateToKbnUrl = createSetStateToKbnUrl(createHash); + const url = 'http://localhost:5601/oxf/app/kibana#/yourApp'; + const state = { foo: 'bar' }; + const newUrl = localSetStateToKbnUrl('_s', state, { useHash: false }, url); + expect(createHash).not.toHaveBeenCalled(); + expect(newUrl).toMatchInlineSnapshot( + `"http://localhost:5601/oxf/app/kibana#/yourApp?_s=(foo:bar)"` + ); + }); + }); + + describe('setStateToKbnUrl', () => { + const url = 'http://localhost:5601/oxf/app/kibana#/yourApp'; + const state1 = { + testStr: '123', + testNumber: 0, + testObj: { test: '123' }, + testNull: null, + testArray: [1, 2, {}], + }; + const state2 = { + test: '123', + }; + + it('should set expanded state to url', () => { + let newUrl = setStateToKbnUrl('_s', state1, { useHash: false }, url); + expect(newUrl).toMatchInlineSnapshot( + `"http://localhost:5601/oxf/app/kibana#/yourApp?_s=(testArray:!(1,2,()),testNull:!n,testNumber:0,testObj:(test:'123'),testStr:'123')"` + ); + newUrl = setStateToKbnUrl('_s', state2, { useHash: false }, newUrl); + expect(newUrl).toMatchInlineSnapshot( + `"http://localhost:5601/oxf/app/kibana#/yourApp?_s=(test:'123')"` + ); + }); + + it('should set expanded state to url before hash', () => { + let newUrl = setStateToKbnUrl('_s', state1, { useHash: false, storeInHashQuery: false }, url); + expect(newUrl).toMatchInlineSnapshot( + `"http://localhost:5601/oxf/app/kibana?_s=(testArray:!(1,2,()),testNull:!n,testNumber:0,testObj:(test:'123'),testStr:'123')#/yourApp"` + ); + newUrl = setStateToKbnUrl('_s', state2, { useHash: false, storeInHashQuery: false }, newUrl); + expect(newUrl).toMatchInlineSnapshot( + `"http://localhost:5601/oxf/app/kibana?_s=(test:'123')#/yourApp"` + ); + }); + + it('should set hashed state to url', () => { + let newUrl = setStateToKbnUrl('_s', state1, { useHash: true }, url); + expect(newUrl).toMatchInlineSnapshot( + `"http://localhost:5601/oxf/app/kibana#/yourApp?_s=h@a897fac"` + ); + newUrl = setStateToKbnUrl('_s', state2, { useHash: true }, newUrl); + expect(newUrl).toMatchInlineSnapshot( + `"http://localhost:5601/oxf/app/kibana#/yourApp?_s=h@40f94d5"` + ); + }); + + it('should set query to url with storeInHashQuery: false', () => { + let newUrl = setStateToKbnUrl( + '_a', + { tab: 'other' }, + { useHash: false, storeInHashQuery: false }, + 'http://localhost:5601/oxf/app/kibana/yourApp' + ); + expect(newUrl).toMatchInlineSnapshot( + `"http://localhost:5601/oxf/app/kibana/yourApp?_a=(tab:other)"` + ); + newUrl = setStateToKbnUrl( + '_b', + { f: 'test', i: '', l: '' }, + { useHash: false, storeInHashQuery: false }, + newUrl + ); + expect(newUrl).toMatchInlineSnapshot( + `"http://localhost:5601/oxf/app/kibana/yourApp?_a=(tab:other)&_b=(f:test,i:'',l:'')"` + ); + }); + }); +}); diff --git a/src/plugins/kibana_utils/common/state_management/set_state_to_kbn_url.ts b/src/plugins/kibana_utils/common/state_management/set_state_to_kbn_url.ts index e0ade8e12d218..ff40d7ea1cab2 100644 --- a/src/plugins/kibana_utils/common/state_management/set_state_to_kbn_url.ts +++ b/src/plugins/kibana_utils/common/state_management/set_state_to_kbn_url.ts @@ -8,6 +8,34 @@ import { encodeState } from './encode_state'; import { replaceUrlHashQuery, replaceUrlQuery } from './format'; +import { createStateHash } from './state_hash'; + +export type SetStateToKbnUrlHashOptions = { useHash: boolean; storeInHashQuery?: boolean }; + +export function createSetStateToKbnUrl(createHash: (rawState: State) => string) { + return ( + key: string, + state: State, + { useHash = false, storeInHashQuery = true }: SetStateToKbnUrlHashOptions = { + useHash: false, + storeInHashQuery: true, + }, + rawUrl: string + ): string => { + const replacer = storeInHashQuery ? replaceUrlHashQuery : replaceUrlQuery; + return replacer(rawUrl, (query) => { + const encoded = encodeState(state, useHash, createHash); + return { + ...query, + [key]: encoded, + }; + }); + }; +} + +const internalSetStateToKbnUrl = createSetStateToKbnUrl((rawState: State) => + createStateHash(JSON.stringify(rawState)) +); /** * Common 'setStateToKbnUrl' without HashedItemStore support @@ -15,18 +43,8 @@ import { replaceUrlHashQuery, replaceUrlQuery } from './format'; export function setStateToKbnUrl( key: string, state: State, - { useHash = false, storeInHashQuery = true }: { useHash: boolean; storeInHashQuery?: boolean } = { - useHash: false, - storeInHashQuery: true, - }, + hashOptions: SetStateToKbnUrlHashOptions, rawUrl: string ): string { - const replacer = storeInHashQuery ? replaceUrlHashQuery : replaceUrlQuery; - return replacer(rawUrl, (query) => { - const encoded = encodeState(state, useHash); - return { - ...query, - [key]: encoded, - }; - }); + return internalSetStateToKbnUrl(key, state, hashOptions, rawUrl); } diff --git a/src/plugins/kibana_utils/common/state_management/state_hash.test.ts b/src/plugins/kibana_utils/common/state_management/state_hash.test.ts index 480d99f4acc55..98c5295541d0b 100644 --- a/src/plugins/kibana_utils/common/state_management/state_hash.test.ts +++ b/src/plugins/kibana_utils/common/state_management/state_hash.test.ts @@ -32,6 +32,13 @@ describe('stateHash', () => { const hash2 = createStateHash(json2); expect(hash1).not.toEqual(hash2); }); + + it('calls existingJsonProvider if provided', () => { + const json = JSON.stringify({ a: 'a' }); + const existingJsonProvider = jest.fn(() => json); + createStateHash(json, existingJsonProvider); + expect(existingJsonProvider).toHaveBeenCalled(); + }); }); describe('#isStateHash', () => { diff --git a/src/plugins/kibana_utils/common/state_management/state_hash.ts b/src/plugins/kibana_utils/common/state_management/state_hash.ts index 9dc10efa27508..811cccc5bfd53 100644 --- a/src/plugins/kibana_utils/common/state_management/state_hash.ts +++ b/src/plugins/kibana_utils/common/state_management/state_hash.ts @@ -17,7 +17,7 @@ export function isStateHash(str: string) { export function createStateHash( json: string, - existingJsonProvider?: (hash: string) => string | null // TODO: temp while state.js relies on this in tests + existingJsonProvider?: (hash: string) => string | null ) { if (typeof json !== 'string') { throw new Error('createHash only accepts strings (JSON).'); @@ -33,7 +33,6 @@ export function createStateHash( for (let i = 7; i < hash.length; i++) { shortenedHash = hash.slice(0, i); const existingJson = existingJsonProvider ? existingJsonProvider(shortenedHash) : null; - // : hashedItemStore.getItem(shortenedHash); if (existingJson === null || existingJson === json) break; } diff --git a/src/plugins/kibana_utils/public/state_management/state_encoder/encode_decode_state.ts b/src/plugins/kibana_utils/public/state_management/state_encoder/encode_decode_state.ts index 71176a6ff4ebe..edfb71c32ceee 100644 --- a/src/plugins/kibana_utils/public/state_management/state_encoder/encode_decode_state.ts +++ b/src/plugins/kibana_utils/public/state_management/state_encoder/encode_decode_state.ts @@ -7,6 +7,7 @@ */ import rison from '@kbn/rison'; +import { encodeState } from '../../../common/state_management/encode_state'; import { isStateHash } from '../../../common/state_management/state_hash'; import { retrieveState, persistState } from '../state_hash'; @@ -22,21 +23,9 @@ export function decodeState(expandedOrHashedState: string): State { } } -// should be: -// export function encodeState but this leads to the chain of -// types mismatches up to BaseStateContainer interfaces, as in state containers we don't -// have any restrictions on state shape -export function encodeState(state: State, useHash: boolean): string { - if (useHash) { - return persistState(state); - } else { - return rison.encodeUnknown(state) ?? ''; - } -} - export function hashedStateToExpandedState(expandedOrHashedState: string): string { if (isStateHash(expandedOrHashedState)) { - return encodeState(retrieveState(expandedOrHashedState), false); + return encodeState(retrieveState(expandedOrHashedState), false, persistState); } return expandedOrHashedState; diff --git a/src/plugins/kibana_utils/public/state_management/state_encoder/index.ts b/src/plugins/kibana_utils/public/state_management/state_encoder/index.ts index b2174f1b0a3a7..0ab6fe580eb0a 100644 --- a/src/plugins/kibana_utils/public/state_management/state_encoder/index.ts +++ b/src/plugins/kibana_utils/public/state_management/state_encoder/index.ts @@ -7,7 +7,6 @@ */ export { - encodeState, decodeState, expandedStateToHashedState, hashedStateToExpandedState, diff --git a/src/plugins/kibana_utils/public/state_management/state_hash/state_hash.ts b/src/plugins/kibana_utils/public/state_management/state_hash/state_hash.ts index 2fc01c9972acb..03160f7c9f7a7 100644 --- a/src/plugins/kibana_utils/public/state_management/state_hash/state_hash.ts +++ b/src/plugins/kibana_utils/public/state_management/state_hash/state_hash.ts @@ -32,7 +32,7 @@ export function retrieveState(stateHash: string): State { export function persistState(state: State): string { const json = JSON.stringify(state); - const hash = createStateHash(json); + const hash = createStateHash(json, hashedItemStore.getItem.bind(hashedItemStore)); const isItemSet = hashedItemStore.setItem(hash, json); if (isItemSet) return hash; diff --git a/src/plugins/kibana_utils/public/state_management/url/kbn_url_storage.ts b/src/plugins/kibana_utils/public/state_management/url/kbn_url_storage.ts index 3b8e3aa504446..b81d3c1b81b63 100644 --- a/src/plugins/kibana_utils/public/state_management/url/kbn_url_storage.ts +++ b/src/plugins/kibana_utils/public/state_management/url/kbn_url_storage.ts @@ -10,9 +10,13 @@ import { format as formatUrl } from 'url'; import { stringify } from 'query-string'; import { createBrowserHistory, History } from 'history'; import { parseUrl, parseUrlHash } from '../../../common/state_management/parse'; -import { replaceUrlHashQuery, replaceUrlQuery } from '../../../common/state_management/format'; -import { decodeState, encodeState } from '../state_encoder'; +import { decodeState } from '../state_encoder'; import { url as urlUtils } from '../../../common'; +import { + createSetStateToKbnUrl, + SetStateToKbnUrlHashOptions, +} from '../../../common/state_management/set_state_to_kbn_url'; +import { persistState } from '../state_hash'; export const getCurrentUrl = (history: History) => history.createHref(history.location); @@ -98,22 +102,17 @@ export function getStateFromKbnUrl( export function setStateToKbnUrl( key: string, state: State, - { useHash = false, storeInHashQuery = true }: { useHash: boolean; storeInHashQuery?: boolean } = { + { useHash = false, storeInHashQuery = true }: SetStateToKbnUrlHashOptions = { useHash: false, storeInHashQuery: true, }, rawUrl = window.location.href -): string { - const replacer = storeInHashQuery ? replaceUrlHashQuery : replaceUrlQuery; - return replacer(rawUrl, (query) => { - const encoded = encodeState(state, useHash); - return { - ...query, - [key]: encoded, - }; - }); +) { + return internalSetStateToKbnUrl(key, state, { useHash, storeInHashQuery }, rawUrl); } +const internalSetStateToKbnUrl = createSetStateToKbnUrl(persistState); + /** * A tiny wrapper around history library to listen for url changes and update url * History library handles a bunch of cross browser edge cases