Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor setStateToKbnUrl to share code #8

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 2 additions & 6 deletions src/plugins/discover/common/locator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -96,12 +97,7 @@ export type DiscoverAppLocator = LocatorPublic<DiscoverAppLocatorParams>;

export interface DiscoverAppLocatorDependencies {
useHash: boolean;
setStateToKbnUrl: <State>(
key: string,
state: State,
options: { useHash: boolean; storeInHashQuery?: boolean },
rawUrl: string
) => string;
setStateToKbnUrl: typeof setStateToKbnUrl;
}

/**
Expand Down
20 changes: 12 additions & 8 deletions src/plugins/kibana_utils/common/state_management/encode_state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: State, useHash: boolean): string {
// should be:
// export function encodeState<State extends RisonValue> 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: 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) ?? '';
}
}
Original file line number Diff line number Diff line change
@@ -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:'')"`
);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -8,25 +8,43 @@

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: <State>(rawState: State) => string) {
return <State>(
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(<State>(rawState: State) =>
createStateHash(JSON.stringify(rawState))
);

/**
* Common 'setStateToKbnUrl' without HashedItemStore support
*/
export function setStateToKbnUrl<State>(
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);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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).');
Expand All @@ -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;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -22,21 +23,9 @@ export function decodeState<State>(expandedOrHashedState: string): State {
}
}

// should be:
// export function encodeState<State extends RisonValue> 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: 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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
*/

export {
encodeState,
decodeState,
expandedStateToHashedState,
hashedStateToExpandedState,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export function retrieveState<State>(stateHash: string): State {

export function persistState<State>(state: State): string {
const json = JSON.stringify(state);
const hash = createStateHash(json);
const hash = createStateHash(json, hashedItemStore.getItem.bind(hashedItemStore));
dimaanj marked this conversation as resolved.
Show resolved Hide resolved

const isItemSet = hashedItemStore.setItem(hash, json);
if (isItemSet) return hash;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down Expand Up @@ -98,22 +102,17 @@ export function getStateFromKbnUrl<State>(
export function setStateToKbnUrl<State>(
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
Expand Down