diff --git a/src/plugins/kibana_utils/public/state_sync/state_sync_state_storage/create_kbn_url_state_storage.test.ts b/src/plugins/kibana_utils/public/state_sync/state_sync_state_storage/create_kbn_url_state_storage.test.ts index c42472fb959de..1d659ae3fe61c 100644 --- a/src/plugins/kibana_utils/public/state_sync/state_sync_state_storage/create_kbn_url_state_storage.test.ts +++ b/src/plugins/kibana_utils/public/state_sync/state_sync_state_storage/create_kbn_url_state_storage.test.ts @@ -191,6 +191,114 @@ describe('KbnUrlStateStorage', () => { }); }); + describe('useHashQuery: false', () => { + let urlStateStorage: IKbnUrlStateStorage; + let history: History; + const getCurrentUrl = () => history.createHref(history.location); + beforeEach(() => { + history = createBrowserHistory(); + history.push('/'); + urlStateStorage = createKbnUrlStateStorage({ useHash: false, history, useHashQuery: false }); + }); + + it('should persist state to url', async () => { + const state = { test: 'test', ok: 1 }; + const key = '_s'; + await urlStateStorage.set(key, state); + expect(getCurrentUrl()).toMatchInlineSnapshot(`"/?_s=(ok:1,test:test)"`); + expect(urlStateStorage.get(key)).toEqual(state); + }); + + it('should flush state to url', () => { + const state = { test: 'test', ok: 1 }; + const key = '_s'; + urlStateStorage.set(key, state); + expect(getCurrentUrl()).toMatchInlineSnapshot(`"/"`); + expect(!!urlStateStorage.kbnUrlControls.flush()).toBe(true); + expect(getCurrentUrl()).toMatchInlineSnapshot(`"/?_s=(ok:1,test:test)"`); + expect(urlStateStorage.get(key)).toEqual(state); + + expect(!!urlStateStorage.kbnUrlControls.flush()).toBe(false); // nothing to flush, not update + }); + + it('should cancel url updates', async () => { + const state = { test: 'test', ok: 1 }; + const key = '_s'; + const pr = urlStateStorage.set(key, state); + expect(getCurrentUrl()).toMatchInlineSnapshot(`"/"`); + urlStateStorage.cancel(); + await pr; + expect(getCurrentUrl()).toMatchInlineSnapshot(`"/"`); + expect(urlStateStorage.get(key)).toEqual(null); + }); + + it('should cancel url updates if synchronously returned to the same state', async () => { + const state1 = { test: 'test', ok: 1 }; + const state2 = { test: 'test', ok: 2 }; + const key = '_s'; + const pr1 = urlStateStorage.set(key, state1); + await pr1; + const historyLength = history.length; + const pr2 = urlStateStorage.set(key, state2); + const pr3 = urlStateStorage.set(key, state1); + await Promise.all([pr2, pr3]); + expect(history.length).toBe(historyLength); + }); + + it('should notify about url changes', async () => { + expect(urlStateStorage.change$).toBeDefined(); + const key = '_s'; + const destroy$ = new Subject(); + const result = urlStateStorage.change$!(key).pipe(takeUntil(destroy$), toArray()).toPromise(); + + history.push(`/?${key}=(ok:1,test:test)`); + history.push(`/?query=test&${key}=(ok:2,test:test)&some=test`); + history.push(`/?query=test&some=test`); + + destroy$.next(); + destroy$.complete(); + + expect(await result).toEqual([{ test: 'test', ok: 1 }, { test: 'test', ok: 2 }, null]); + }); + + it("shouldn't throw in case of parsing error", async () => { + const key = '_s'; + history.replace(`/?${key}=(ok:2,test:`); // malformed rison + expect(() => urlStateStorage.get(key)).not.toThrow(); + expect(urlStateStorage.get(key)).toBeNull(); + }); + + it('should notify about errors', () => { + const cb = jest.fn(); + urlStateStorage = createKbnUrlStateStorage({ + useHash: false, + useHashQuery: false, + history, + onGetError: cb, + }); + const key = '_s'; + history.replace(`/?${key}=(ok:2,test:`); // malformed rison + expect(() => urlStateStorage.get(key)).not.toThrow(); + expect(cb).toBeCalledWith(expect.any(Error)); + }); + + describe('withNotifyOnErrors integration', () => { + test('toast is shown', () => { + const toasts = coreMock.createStart().notifications.toasts; + urlStateStorage = createKbnUrlStateStorage({ + useHash: true, + useHashQuery: false, + history, + ...withNotifyOnErrors(toasts), + }); + const key = '_s'; + history.replace(`/?${key}=(ok:2,test:`); // malformed rison + expect(() => urlStateStorage.get(key)).not.toThrow(); + expect(toasts.addError).toBeCalled(); + }); + }); + }); + describe('ScopedHistory integration', () => { let urlStateStorage: IKbnUrlStateStorage; let history: ScopedHistory; diff --git a/src/plugins/kibana_utils/public/state_sync/state_sync_state_storage/create_kbn_url_state_storage.ts b/src/plugins/kibana_utils/public/state_sync/state_sync_state_storage/create_kbn_url_state_storage.ts index fc28cd671695d..97f048ea0d916 100644 --- a/src/plugins/kibana_utils/public/state_sync/state_sync_state_storage/create_kbn_url_state_storage.ts +++ b/src/plugins/kibana_utils/public/state_sync/state_sync_state_storage/create_kbn_url_state_storage.ts @@ -58,16 +58,19 @@ export interface IKbnUrlStateStorage extends IStateStorage { export const createKbnUrlStateStorage = ( { useHash = false, + useHashQuery = true, history, onGetError, onSetError, }: { useHash: boolean; + useHashQuery?: boolean; history?: History; onGetError?: (error: Error) => void; onSetError?: (error: Error) => void; } = { useHash: false, + useHashQuery: true, } ): IKbnUrlStateStorage => { const url = createKbnUrlControls(history); @@ -80,7 +83,12 @@ export const createKbnUrlStateStorage = ( // syncState() utils doesn't wait for this promise return url.updateAsync((currentUrl) => { try { - return setStateToKbnUrl(key, state, { useHash }, currentUrl); + return setStateToKbnUrl( + key, + state, + { useHash, storeInHashQuery: useHashQuery }, + currentUrl + ); } catch (error) { if (onSetError) onSetError(error); } @@ -90,7 +98,7 @@ export const createKbnUrlStateStorage = ( // if there is a pending url update, then state will be extracted from that pending url, // otherwise current url will be used to retrieve state from try { - return getStateFromKbnUrl(key, url.getPendingUrl()); + return getStateFromKbnUrl(key, url.getPendingUrl(), { getFromHashQuery: useHashQuery }); } catch (e) { if (onGetError) onGetError(e); return null; @@ -106,7 +114,7 @@ export const createKbnUrlStateStorage = ( unlisten(); }; }).pipe( - map(() => getStateFromKbnUrl(key)), + map(() => getStateFromKbnUrl(key, undefined, { getFromHashQuery: useHashQuery })), catchError((error) => { if (onGetError) onGetError(error); return of(null); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_logs/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_logs/index.tsx index 5411e6313ebb7..76a7f0514a8a2 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_logs/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_logs/index.tsx @@ -56,7 +56,7 @@ export const AgentLogs: React.FunctionComponent(false); useEffect(() => { - const stateStorage = createKbnUrlStateStorage(); + const stateStorage = createKbnUrlStateStorage({ useHashQuery: false, useHash: false }); const { start, stop } = syncState({ storageKey: STATE_STORAGE_KEY, stateContainer: stateContainer as INullableBaseStateContainer,