From 8e94487cefceba0812fd62dba95ef25d2425b9fe Mon Sep 17 00:00:00 2001 From: Anton Dosov Date: Thu, 30 Jan 2020 12:47:15 +0100 Subject: [PATCH 1/7] wip --- .../kibana_utils/docs/state_sync/README.md | 59 +++++++++++++++ .../docs/state_sync/initial_state.md | 71 +++++++++++++++++++ .../docs/state_sync/storages/README.md | 7 ++ .../state_sync/storages/kbn_url_storage.md | 0 .../state_sync/storages/session_storage.md | 39 ++++++++++ 5 files changed, 176 insertions(+) create mode 100644 src/plugins/kibana_utils/docs/state_sync/README.md create mode 100644 src/plugins/kibana_utils/docs/state_sync/initial_state.md create mode 100644 src/plugins/kibana_utils/docs/state_sync/storages/README.md create mode 100644 src/plugins/kibana_utils/docs/state_sync/storages/kbn_url_storage.md create mode 100644 src/plugins/kibana_utils/docs/state_sync/storages/session_storage.md diff --git a/src/plugins/kibana_utils/docs/state_sync/README.md b/src/plugins/kibana_utils/docs/state_sync/README.md new file mode 100644 index 00000000000000..26b78b6eb38756 --- /dev/null +++ b/src/plugins/kibana_utils/docs/state_sync/README.md @@ -0,0 +1,59 @@ +# State Syncing Utilities + +State syncing utilities are a set of helpers for syncing your application state +with URL or browser storage. + +They are designed to work together with [state containers](../state_containers). But state containers are not required. + +State syncing utilities include: + +- `syncState` util which: + - Subscribes to state changes and pushes them to state storage. + - Optionally subscribes to state storage changes and pushes them to state. +- 2 storage types for `syncState` util: + - [KbnUrlStateStorage](./storages/kbn_url_storage.md) - Serialises state and persists it to url's query param in rison or hashed format (similar to what AppState & GlobalState did in legacy world). + Listens for state updates in the url and pushes updates back to state. + - [SessionStorageStateStorage](./storages/session_storage.md) - Serialises state and persists it to session storage. + +## Example + +```ts +import { + createStateContainer, + syncState, + createKbnUrlStateStorage, +} from 'src/plugins/kibana_utils/public'; + +const stateContainer = createStateContainer({ count: 0 }); +const stateStorage = createKbnUrlStateStorage(); + +const { start, stop } = syncState({ + storageKey: '_a', + stateContainer, + stateStorage, +}); + +start(); + +// state container change is synced to state storage +// in this case, kbnUrlStateStorage updates the url to "/#?_a=(count:2)" +stateContainer.set({ count: 2 }); + +stop(); +``` + +## Demos Plugins + +See demos [here](../../../../../examples/state_containers_examples). + +To run them, start kibana with `--run-examples` flag + +## Reference + +- [Syncing state with URL](./storages/kbn_url_storage.md). +- [Syncing state with sessionStorage](./storages/session_storage.md). +- [Setting up initial state](./initial_state.md). +- [Using without state containers](). +- [Handling empty incoming state](). +- [On-fly state migrations](). +- [syncStates helper](). diff --git a/src/plugins/kibana_utils/docs/state_sync/initial_state.md b/src/plugins/kibana_utils/docs/state_sync/initial_state.md new file mode 100644 index 00000000000000..d962dec79eaa93 --- /dev/null +++ b/src/plugins/kibana_utils/docs/state_sync/initial_state.md @@ -0,0 +1,71 @@ +# Setting up initial state + +The `syncState` util doesn't do any initial state syncing between state and storage. +Consider the scenario: + +```ts +// window.location.href is "/#?_a=(count:2)" +const defaultState = { count: 0 }; // default application state + +const stateContainer = createStateContainer(defaultState); +const stateStorage = createKbnUrlStateStorage(); + +const { start, stop } = syncState({ + storageKey: '_a', + stateContainer, + stateStorage, +}); + +start(); + +// on this point state in the storage and in the state is out of sync +// state: {count: 0} +// storage: {count: 2} +``` + +The `syncState` doesn't make a decision, how initial state should be synced and which one should take precedence. +It is up to the application to decide, depending on the specific use case. +Questions to consider: + +1. Should default state take precedence over URL? +2. Should URL take precedence? +3. Do we have to do any state migrations for what is coming from the url? +4. If URL doesn't have the whole state, should we merge it with default or leave it behind? +5. Is there any other state loading in parallel (e.g. from `SavedObject`)? Who should we merge it all together? +6. Are we storing the state both in the URL and in the sessionStorage? Which one should take precedence in which case? + +So if initial state syncing is required, for simple example above could look like this: + +```ts +// window.location.href is "/#?_a=(count:2)" +const defaultState = { count: 0 }; // default application state + +const urlStateStorage = createKbnUrlStateStorage(); + +const initialStateFromUrl = urlStateStorage.get('_a'); + +// merge together default state and initial state +const initialState = { + ...defaultState, + ...initialStateFromUrl, +}; + +const stateContainer = createStateContainer(initialState); + +if (!initialStateFromUrl) { + // can put default state to the url, for consistency + urlStateStorage.set('_a', defaultState, { replace: true }); +} + +const { start, stop } = syncState({ + storageKey: '_a', + stateContainer, + stateStorage: urlStateStorage, +}); + +start(); + +// on this point state in the storage and in synced +// state: {count: 2} +// storage: {count: 2} +``` diff --git a/src/plugins/kibana_utils/docs/state_sync/storages/README.md b/src/plugins/kibana_utils/docs/state_sync/storages/README.md new file mode 100644 index 00000000000000..7388ea4dc545d4 --- /dev/null +++ b/src/plugins/kibana_utils/docs/state_sync/storages/README.md @@ -0,0 +1,7 @@ +# State Storage + +2 storage types are supported: + +- [KbnUrlStateStorage](./kbn_url_storage.md) - Serialises state and persists it to url's query param in rison or hashed format (similar to what AppState & GlobalState did in legacy world). + Listens for state updates in the url and pushes updates back to state. +- [SessionStorageStateStorage](./session_storage.md) - Serialises state and persists it to session storage. diff --git a/src/plugins/kibana_utils/docs/state_sync/storages/kbn_url_storage.md b/src/plugins/kibana_utils/docs/state_sync/storages/kbn_url_storage.md new file mode 100644 index 00000000000000..e69de29bb2d1d6 diff --git a/src/plugins/kibana_utils/docs/state_sync/storages/session_storage.md b/src/plugins/kibana_utils/docs/state_sync/storages/session_storage.md new file mode 100644 index 00000000000000..d47e80cf419687 --- /dev/null +++ b/src/plugins/kibana_utils/docs/state_sync/storages/session_storage.md @@ -0,0 +1,39 @@ +# Session Storage + +To sync state from state containers to session storage use `sessionStorageStateStorage`. + +```ts +import { + createStateContainer, + syncState, + createSessionStorageStateStorage, +} from 'src/plugins/kibana_utils/public'; + +const stateContainer = createStateContainer({ count: 0 }); +const stateStorage = createSessionStorageStateStorage(); + +const { start, stop } = syncState({ + storageKey: '_a', + stateContainer, + stateStorage, +}); + +start(); + +// state container change is synced to state storage +// in this case, stateStorage serialises state and stores it in `window.sessionStorage` by key `_a` +stateContainer.set({ count: 2 }); + +stop(); +``` + +You can also imperative use `sessionStorageStateStorage`: + +```ts +const stateStorage = createSessionStorageStateStorage(); + +stateStorage.set('_a', { count: 2 }); +stateStorage.get('_a'); +``` + +**Note**: external changes to `sessionStorageStateStorage` or `window.sessionStorage` don't trigger state container updates. From 33b40d4dc2873701b9185f0f1b8e97a600220f4c Mon Sep 17 00:00:00 2001 From: Anton Dosov Date: Fri, 31 Jan 2020 11:31:02 +0100 Subject: [PATCH 2/7] docs wip --- .../kibana_utils/docs/state_sync/README.md | 9 +- .../docs/state_sync/data_plugin_helpers.md | 6 + .../empty_or_incomplete_incoming_state.md | 52 ++++++ .../docs/state_sync/no_state_containers.md | 46 ++++++ .../state_sync/on_fly_state_migrations.md | 46 ++++++ .../state_sync/storages/kbn_url_storage.md | 150 ++++++++++++++++++ .../docs/state_sync/sync_states.md | 34 ++++ .../public/state_sync/state_sync.test.ts | 22 +++ 8 files changed, 361 insertions(+), 4 deletions(-) create mode 100644 src/plugins/kibana_utils/docs/state_sync/data_plugin_helpers.md create mode 100644 src/plugins/kibana_utils/docs/state_sync/empty_or_incomplete_incoming_state.md create mode 100644 src/plugins/kibana_utils/docs/state_sync/no_state_containers.md create mode 100644 src/plugins/kibana_utils/docs/state_sync/on_fly_state_migrations.md create mode 100644 src/plugins/kibana_utils/docs/state_sync/sync_states.md diff --git a/src/plugins/kibana_utils/docs/state_sync/README.md b/src/plugins/kibana_utils/docs/state_sync/README.md index 26b78b6eb38756..8aacbec821a358 100644 --- a/src/plugins/kibana_utils/docs/state_sync/README.md +++ b/src/plugins/kibana_utils/docs/state_sync/README.md @@ -53,7 +53,8 @@ To run them, start kibana with `--run-examples` flag - [Syncing state with URL](./storages/kbn_url_storage.md). - [Syncing state with sessionStorage](./storages/session_storage.md). - [Setting up initial state](./initial_state.md). -- [Using without state containers](). -- [Handling empty incoming state](). -- [On-fly state migrations](). -- [syncStates helper](). +- [Using without state containers](./no_state_containers.md). +- [Handling empty or incomplete incoming state](./empty_or_incomplete_incoming_state.md). +- [On-fly state migrations](./on_fly_state_migrations.md). +- [syncStates helper](./sync_states.md). +- [Helpers for Data plugin (syncing TimeRange, RefreshInterval and Filters)](./data_plugin_helpers.md). diff --git a/src/plugins/kibana_utils/docs/state_sync/data_plugin_helpers.md b/src/plugins/kibana_utils/docs/state_sync/data_plugin_helpers.md new file mode 100644 index 00000000000000..78c529b2f1e046 --- /dev/null +++ b/src/plugins/kibana_utils/docs/state_sync/data_plugin_helpers.md @@ -0,0 +1,6 @@ +# Helpers for syncing state with data plugins (TimeRange, RefreshInterval and Filters) + +Waiting for + +- https://github.com/elastic/kibana/issues/55977 +- https://github.com/elastic/kibana/pull/56128 diff --git a/src/plugins/kibana_utils/docs/state_sync/empty_or_incomplete_incoming_state.md b/src/plugins/kibana_utils/docs/state_sync/empty_or_incomplete_incoming_state.md new file mode 100644 index 00000000000000..23315b195ac314 --- /dev/null +++ b/src/plugins/kibana_utils/docs/state_sync/empty_or_incomplete_incoming_state.md @@ -0,0 +1,52 @@ +# Handling empty or incomplete incoming state + +The `syncState` utility syncs with storage to which users have direct access. For example, URL. +User can manually change the url and remove or corrupt important data which application expects. +Or users may programmatically generate urls to Kibana and these generated urls could have mistakes which application can't handle. + +`syncState` utility doesn't do any handling for such edge cases and passes whatever received from storage to state container as is. +So it is up to application to decide, how to handle such scenarios. Consider example: + +```ts +// window.location.href is "/#?_a=(count:0)" +const defaultState = { count: 0 }; // default application state + +const stateContainer = createStateContainer(defaultState); +const stateStorage = createKbnUrlStateStorage(); + +const { start, stop } = syncState({ + storageKey: '_a', + stateContainer, + stateStorage, +}); + +start(); + +// on this point state in the storage and in the state is out of sync +// state: {count: 0} +// storage: {count: 0} + +// And now user changed the url to (let's assume) "/#?_a=(corrupt:0)" +// on this point state will recieve and update: {corrupt: 0} +``` + +Application could handle this gracefully by using simple composition during setup: + +```ts +const { start, stop } = syncState({ + storageKey: '_a', + stateContainer: { + ...stateContainer, + set: state => stateContainer.set({ ...defaultState, ...state }), + }, + stateStorage, +}); +``` + +In this case, app will not get into state, which is not shaped as app expects. + +To help application developers not forget about such edge cases, +`syncState` util sets constraint, +that setter to state container should be able to handle "null" value. (See [signature](../../public/state_sync/state_sync.ts) of `syncState` function). +Incoming `null` value usually mean empty state (e.g. url without `storageKey` query param) or corrupted state which can't be parsed. +So when using `syncState` util applications are required to at least handle incoming `null` diff --git a/src/plugins/kibana_utils/docs/state_sync/no_state_containers.md b/src/plugins/kibana_utils/docs/state_sync/no_state_containers.md new file mode 100644 index 00000000000000..28aba319b2b656 --- /dev/null +++ b/src/plugins/kibana_utils/docs/state_sync/no_state_containers.md @@ -0,0 +1,46 @@ +# Using state syncing utilities without state containers + +It is possible to use `syncState` utility even if your app not using [state containers](../state_containers). +The `state` which is passed into `syncState` function should implement following interface: + +```ts +export interface BaseStateContainer { + get: () => State; + set: (state: State | null) => void; + state$: Observable; +} +``` + +For example, assuming you have a custom state manager, setting up syncing state with url could look something like this: + +```ts +import { Subject } from 'rxjs'; +import { map } from 'rxjs/operators'; + +class MyStateManager { + private count: number = 0; + + updated$ = new Subject(); + + setCount(count: number) { + if (this.count !== count) { + this.count = count; + this.updated$.next(); + } + } + + getCount() { + return this.count; + } +} + +import { syncState, createKbnUrlStateStorage } from 'src/plugins/kibana_utils/public'; + +const myStateManager = new MyStateManager(); + +syncState({ + get: () => ({ count: myStateManager.getCount() }), + set: state => state && myStateManager.setCount(state.count), + state$: myStateManager.updated$.pipe(map(() => ({ count: myStateManager.getCount() }))), +}); +``` diff --git a/src/plugins/kibana_utils/docs/state_sync/on_fly_state_migrations.md b/src/plugins/kibana_utils/docs/state_sync/on_fly_state_migrations.md new file mode 100644 index 00000000000000..7ec8120aaf39b9 --- /dev/null +++ b/src/plugins/kibana_utils/docs/state_sync/on_fly_state_migrations.md @@ -0,0 +1,46 @@ +# On-fly state migrations + +When retrieving initial state from storage we can't forget about possible outdated state. +As described in [handling initial state](./initial_state.md), this could be the place where we can apply migrations. + +```ts +import { migrate } from '../app/state_helpers'; +const urlStateStorage = createKbnUrlStateStorage(); +const initialStateFromUrl = urlStateStorage.get('_a'); + +// merge together default state and initial state and migrate it to current version if needed +const initialState = migrate({ + ...defaultState, + ...initialStateFromUrl, +}); + +const stateContainer = createStateContainer(initialState); +``` + +But in addition it is also possible to apply migrations for any incoming state similar to how described in [handling empty or incomplete state](./empty_or_incomplete_incoming_state.md). + +```ts +import { migrate } from '../app/state_helpers'; + +const urlStateStorage = createKbnUrlStateStorage(); +const initialStateFromUrl = urlStateStorage.get('_a'); + +// merge together default state and initial state and migrate it to current version if needed +const initialState = migrate({ + ...defaultState, + ...initialStateFromUrl, +}); + +const stateContainer = createStateContainer(initialState); +const { start, stop } = syncState({ + storageKey: '_a', + stateContainer: { + ...stateContainer, + set: state => stateContainer.set(migrate({ ...defaultState, ...state })), + }, + stateStorage, +}); +``` + +This should cover edge case, when user already have, for example, dashboard opened and then user pastes an older dashboard link into browser window. +No dashboard remount will happen, so, as we need to transition to a new state on-fly, we also need to apply necessary migrations on-fly. diff --git a/src/plugins/kibana_utils/docs/state_sync/storages/kbn_url_storage.md b/src/plugins/kibana_utils/docs/state_sync/storages/kbn_url_storage.md index e69de29bb2d1d6..79917990e7632b 100644 --- a/src/plugins/kibana_utils/docs/state_sync/storages/kbn_url_storage.md +++ b/src/plugins/kibana_utils/docs/state_sync/storages/kbn_url_storage.md @@ -0,0 +1,150 @@ +# Kbn Url Storage + +`KbnUrlStateStorage` is a state storage for `syncState` utility which: + +- Keeps state in sync with the url +- Serialises data and stores data in url in 2 different formats: + 1. [Rison](https://github.com/w33ble/rison-node) encoded. + 2. Hashed url. In url we store only the hash from the serialized state, but the state itself is stored in sessionStorage. + See kibana's advanced option for more context `state:storeInSessionStorage` +- Takes care of listening to url updates and notifies state about updates +- Takes care of batching url updates to prevent redundant browser history records + +### Basic Example + +```ts +import { + createStateContainer, + syncState, + createKbnUrlStateStorage, +} from 'src/plugins/kibana_utils/public'; + +const stateContainer = createStateContainer({ count: 0 }); +const stateStorage = createKbnUrlStateStorage(); + +const { start, stop } = syncState({ + storageKey: '_a', + stateContainer, + stateStorage, +}); + +start(); + +// state container change is synced to state storage +// in this case, kbnUrlStateStorage updates the url to "/#?_a=(count:2)" +stateContainer.set({ count: 2 }); + +stop(); +``` + +### Setting up url format option + +```ts +const stateStorage = createKbnUrlStateStorage({ + useHash: core.uiSettings.get('state:storeInSessionStorage'), // put full encoded rison or just the hash into the url +}); +``` + +### Passing [history](https://github.com/ReactTraining/history) instance + +Under the hood `kbnUrlStateStorage` uses [history](https://github.com/ReactTraining/history) for updating the url and for listening for url updates. +To prevent bugs caused by missing history updates, make sure your app uses 1 instance of history for url changes which may interfere with each other. + +For example, if you use `react-router`: + +```tsx +const App = props => { + useEffect(() => { + const stateStorage = createKbnUrlStateStorage({ + useHash: props.uiSettings.get('state:storeInSessionStorage'), + history: props.history, + }); + + //.... + }); + + return ; +}; +``` + +### Url updates batching + +`kbnUrlStateStorage` batches synchronous URL updates. Consider the example. + +```ts +const urlStateStorage = createKbnUrlStateStorage(); + +urlStateStorage.set('_a', { state: 1 }); +urlStateStorage.set('_b', { state: 2 }); + +// url hasn't been updated yet +setTimeout(() => { + // now url is actually "/#?_a=(state:1)&_b=(state:2)" + // and 2 updates were batched into 1 history.push() call +}, 0); +``` + +For cases, where granular control over url updates is needed, `kbnUrlStateStorage` supports these advanced apis: + +- `kbnUrlStateStorage.flush({replace: boolean})` - allows to synchronously apply any pending updates. + `replace` option allows to use `history.replace()` instead of `history.push()`. Returned boolean indicates if any update happened. +- `kbnUrlStateStorage.cancel()` - cancels any pending updates + +### Sharing one instance of `kbnUrlStateStorage` + +`kbnUrlStateStorage` is stateful, as it keeps track of pending updates. +So if there are multiple state syncs happening in the time, then one instance of `kbnUrlStateStorage` should be used to make sure the same update queue is used. +Otherwise it could cause redundant browser history records. + +```ts +// wrong: + +const { start, stop } = syncStates([ + { + storageKey: '_a', + stateContainerA, + stateStorage: createKbnUrlStateStorage(), + }, + { + storageKey: '_b', + stateContainerB, + stateStorage: createKbnUrlStateStorage(), + }, +]); + +// better: + +const kbnUrlStateStorage = createKbnUrlStateStorage(); +const { start, stop } = syncStates([ + { + storageKey: '_a', + stateContainerA, + stateStorage: kbnUrlStateStorage, + }, + { + storageKey: '_b', + stateContainerB, + stateStorage: kbnUrlStateStorage, + }, +]); + +// correct: + +import { createBrowserHistory } from 'history'; +const history = createBrowserHistory(); +const kbnUrlStateStorage = createKbnUrlStateStorage({ history }); +const { start, stop } = syncStates([ + { + storageKey: '_a', + stateContainerA, + stateStorage: kbnUrlStateStorage, + }, + { + storageKey: '_b', + stateContainerB, + stateStorage: kbnUrlStateStorage, + }, +]); + +; +``` diff --git a/src/plugins/kibana_utils/docs/state_sync/sync_states.md b/src/plugins/kibana_utils/docs/state_sync/sync_states.md new file mode 100644 index 00000000000000..b5ff37068cebb2 --- /dev/null +++ b/src/plugins/kibana_utils/docs/state_sync/sync_states.md @@ -0,0 +1,34 @@ +# Sync states utility + +In case you need to sync multiple states or a state to multiple storages, there is a handy util for that. +It saves a bit of boilerplate by returning `start` and `stop` functions for all `stateSync` configs at once. + +```ts +import { + createStateContainer, + syncStates, + createKbnUrlStateStorage, + createSessionStorageStateStorage, +} from 'src/plugins/kibana_utils/public'; + +const stateContainer = createStateContainer({ count: 0 }); +const urlStateStorage = createKbnUrlStateStorage(); +const sessionStorageStateStorage = createSessionStorageStateStorage(); + +const { start, stop } = syncStates([ + { + storageKey: '_a', + stateContainer, + stateStorage: urlStateStorage, + }, + { + storageKey: '_a', + stateContainer, + stateStorage: sessionStorageStateStorage, + }, +]); + +start(); // start syncing to all storages at once + +stop(); // stop syncing to all storages at once +``` diff --git a/src/plugins/kibana_utils/public/state_sync/state_sync.test.ts b/src/plugins/kibana_utils/public/state_sync/state_sync.test.ts index 17f41483a0a21e..395076e95e727a 100644 --- a/src/plugins/kibana_utils/public/state_sync/state_sync.test.ts +++ b/src/plugins/kibana_utils/public/state_sync/state_sync.test.ts @@ -151,6 +151,28 @@ describe('state_sync', () => { stop(); }); + + it('storage change with incomplete or differently shaped object should notify state and set new object as is', () => { + container.set({ todos: [{ completed: false, id: 1, text: 'changed' }] }); + const { stop, start } = syncStates([ + { + stateContainer: container, + storageKey: '_s', + stateStorage: testStateStorage, + }, + ]); + start(); + + const differentlyShapedObject = { + different: 'test', + }; + (testStateStorage.get as jest.Mock).mockImplementation(() => differentlyShapedObject); + storageChange$.next(differentlyShapedObject as any); + + expect(container.getState()).toStrictEqual(differentlyShapedObject); + + stop(); + }); }); describe('integration', () => { From 3fd5a1f68af8cd8edefa639035a9e2480158d17d Mon Sep 17 00:00:00 2001 From: Anton Dosov Date: Fri, 31 Jan 2020 12:16:06 +0100 Subject: [PATCH 3/7] typos --- src/plugins/kibana_utils/README.md | 3 +- .../docs/state_containers/README.md | 12 ++--- .../kibana_utils/docs/state_sync/README.md | 14 +++--- .../docs/state_sync/data_plugin_helpers.md | 9 ++-- .../empty_or_incomplete_incoming_state.md | 22 ++++----- .../docs/state_sync/initial_state.md | 16 +++---- .../docs/state_sync/no_state_containers.md | 6 +-- .../state_sync/on_fly_state_migrations.md | 10 ++-- .../docs/state_sync/storages/README.md | 4 +- .../state_sync/storages/kbn_url_storage.md | 46 ++++++++++++------- .../state_sync/storages/session_storage.md | 2 +- .../docs/state_sync/sync_states.md | 4 +- 12 files changed, 80 insertions(+), 68 deletions(-) diff --git a/src/plugins/kibana_utils/README.md b/src/plugins/kibana_utils/README.md index 5501505dbb7e27..72a8479c7ac3d6 100644 --- a/src/plugins/kibana_utils/README.md +++ b/src/plugins/kibana_utils/README.md @@ -2,4 +2,5 @@ Utilities for building Kibana plugins. -- [State containers](./docs/state_containers/README.md). +- [State containers](./docs/state_containers). +- [State syncing utilities](./docs/state_sync). diff --git a/src/plugins/kibana_utils/docs/state_containers/README.md b/src/plugins/kibana_utils/docs/state_containers/README.md index 583f8f65ce6b69..ee5ebde9258e6a 100644 --- a/src/plugins/kibana_utils/docs/state_containers/README.md +++ b/src/plugins/kibana_utils/docs/state_containers/README.md @@ -12,7 +12,6 @@ your services or apps. container you can always access the latest state snapshot synchronously. - Unlike Redux, state containers are less verbose, see example below. - ## Example ```ts @@ -21,11 +20,11 @@ import { createStateContainer } from 'src/plugins/kibana_utils'; const container = createStateContainer( { count: 0 }, { - increment: (state: {count: number}) => (by: number) => ({ count: state.count + by }), - double: (state: {count: number}) => () => ({ count: state.count * 2 }), + increment: (state: { count: number }) => (by: number) => ({ count: state.count + by }), + double: (state: { count: number }) => () => ({ count: state.count * 2 }), }, { - count: (state: {count: number}) => () => state.count, + count: (state: { count: number }) => () => state.count, } ); @@ -35,7 +34,6 @@ container.transitions.double(); console.log(container.selectors.count()); // 10 ``` - ## Demos See demos [here](../../demos/state_containers/). @@ -47,11 +45,11 @@ npx -q ts-node src/plugins/kibana_utils/demos/state_containers/counter.ts npx -q ts-node src/plugins/kibana_utils/demos/state_containers/todomvc.ts ``` - ## Reference - [Creating a state container](./creation.md). - [State transitions](./transitions.md). - [Using with React](./react.md). -- [Using without React`](./no_react.md). +- [Using without React](./no_react.md). - [Parallels with Redux](./redux.md). +- [Syncing state with URL or SessionStorage](../state_sync) diff --git a/src/plugins/kibana_utils/docs/state_sync/README.md b/src/plugins/kibana_utils/docs/state_sync/README.md index 8aacbec821a358..e48349ab25b09a 100644 --- a/src/plugins/kibana_utils/docs/state_sync/README.md +++ b/src/plugins/kibana_utils/docs/state_sync/README.md @@ -11,9 +11,9 @@ State syncing utilities include: - Subscribes to state changes and pushes them to state storage. - Optionally subscribes to state storage changes and pushes them to state. - 2 storage types for `syncState` util: - - [KbnUrlStateStorage](./storages/kbn_url_storage.md) - Serialises state and persists it to url's query param in rison or hashed format (similar to what AppState & GlobalState did in legacy world). - Listens for state updates in the url and pushes updates back to state. - - [SessionStorageStateStorage](./storages/session_storage.md) - Serialises state and persists it to session storage. + - [KbnUrlStateStorage](./storages/kbn_url_storage.md) - Serializes state and persists it to URL's query param in rison or hashed format (similar to what `AppState` & `GlobalState` did in legacy world). + Listens for state updates in the URL and pushes them back to state. + - [SessionStorageStateStorage](./storages/session_storage.md) - Serializes state and persists it to session storage. ## Example @@ -35,8 +35,8 @@ const { start, stop } = syncState({ start(); -// state container change is synced to state storage -// in this case, kbnUrlStateStorage updates the url to "/#?_a=(count:2)" +// state container change is synched to state storage +// kbnUrlStateStorage updates the URL to "/#?_a=(count:2)" stateContainer.set({ count: 2 }); stop(); @@ -44,9 +44,9 @@ stop(); ## Demos Plugins -See demos [here](../../../../../examples/state_containers_examples). +See demos plugins [here](../../../../../examples/state_containers_examples). -To run them, start kibana with `--run-examples` flag +To run them, start kibana with `--run-examples` flag. ## Reference diff --git a/src/plugins/kibana_utils/docs/state_sync/data_plugin_helpers.md b/src/plugins/kibana_utils/docs/state_sync/data_plugin_helpers.md index 78c529b2f1e046..a6abf19c584017 100644 --- a/src/plugins/kibana_utils/docs/state_sync/data_plugin_helpers.md +++ b/src/plugins/kibana_utils/docs/state_sync/data_plugin_helpers.md @@ -1,6 +1,7 @@ # Helpers for syncing state with data plugins (TimeRange, RefreshInterval and Filters) -Waiting for - -- https://github.com/elastic/kibana/issues/55977 -- https://github.com/elastic/kibana/pull/56128 +```ts +// TODO: Waiting for +// https://github.com/elastic/kibana/issues/55977 +// https://github.com/elastic/kibana/pull/56128 +``` diff --git a/src/plugins/kibana_utils/docs/state_sync/empty_or_incomplete_incoming_state.md b/src/plugins/kibana_utils/docs/state_sync/empty_or_incomplete_incoming_state.md index 23315b195ac314..0f4bce78b6815a 100644 --- a/src/plugins/kibana_utils/docs/state_sync/empty_or_incomplete_incoming_state.md +++ b/src/plugins/kibana_utils/docs/state_sync/empty_or_incomplete_incoming_state.md @@ -1,11 +1,11 @@ # Handling empty or incomplete incoming state -The `syncState` utility syncs with storage to which users have direct access. For example, URL. -User can manually change the url and remove or corrupt important data which application expects. -Or users may programmatically generate urls to Kibana and these generated urls could have mistakes which application can't handle. +Users have direct access to storages where we sync our state to. For example, URL. +User can manually change the URL and remove or corrupt important data which we expect to be there. +Or users may programmatically generate URLs to Kibana and these URLs could have mistakes which application can't handle. `syncState` utility doesn't do any handling for such edge cases and passes whatever received from storage to state container as is. -So it is up to application to decide, how to handle such scenarios. Consider example: +So it is up to application to decide, how to handle such scenarios. Consider the example: ```ts // window.location.href is "/#?_a=(count:0)" @@ -26,8 +26,8 @@ start(); // state: {count: 0} // storage: {count: 0} -// And now user changed the url to (let's assume) "/#?_a=(corrupt:0)" -// on this point state will recieve and update: {corrupt: 0} +// And now user changed the URL to (let's assume) "/#?_a=(corrupt:0)" +// on this point state will recieve an update: {corrupt: 0} ``` Application could handle this gracefully by using simple composition during setup: @@ -45,8 +45,8 @@ const { start, stop } = syncState({ In this case, app will not get into state, which is not shaped as app expects. -To help application developers not forget about such edge cases, -`syncState` util sets constraint, -that setter to state container should be able to handle "null" value. (See [signature](../../public/state_sync/state_sync.ts) of `syncState` function). -Incoming `null` value usually mean empty state (e.g. url without `storageKey` query param) or corrupted state which can't be parsed. -So when using `syncState` util applications are required to at least handle incoming `null` +To help application developers to not forget about such edge cases, +`syncState` util sets a constraint, +that setter to state container should be able to handle "null" value (see [IStateSyncConfig](../../public/state_sync/types.ts)). +Incoming `null` value from state storage usually means empty state (e.g. URL without `storageKey` query param) or corrupted state which can't be parsed. +So when using `syncState` util applications are required to at least handle incoming `null`. diff --git a/src/plugins/kibana_utils/docs/state_sync/initial_state.md b/src/plugins/kibana_utils/docs/state_sync/initial_state.md index d962dec79eaa93..b5ab50d715d8ec 100644 --- a/src/plugins/kibana_utils/docs/state_sync/initial_state.md +++ b/src/plugins/kibana_utils/docs/state_sync/initial_state.md @@ -23,18 +23,18 @@ start(); // storage: {count: 2} ``` -The `syncState` doesn't make a decision, how initial state should be synced and which one should take precedence. +The `syncState` doesn't make a decision, how initial state should be synced and which state should take precedence. It is up to the application to decide, depending on the specific use case. Questions to consider: 1. Should default state take precedence over URL? 2. Should URL take precedence? -3. Do we have to do any state migrations for what is coming from the url? -4. If URL doesn't have the whole state, should we merge it with default or leave it behind? -5. Is there any other state loading in parallel (e.g. from `SavedObject`)? Who should we merge it all together? -6. Are we storing the state both in the URL and in the sessionStorage? Which one should take precedence in which case? +3. Do we have to do any state migrations for what is coming from the URL? +4. If URL doesn't have the whole state, should we merge it with default one or leave it behind? +5. Is there any other state loading in parallel (e.g. from a `SavedObject`)? How should we merge it all together? +6. Are we storing the state both in the URL and in the `sessionStorage`? Which one should take precedence and in which case? -So if initial state syncing is required, for simple example above could look like this: +So if initial state syncing is required, for simple example above it could look like this: ```ts // window.location.href is "/#?_a=(count:2)" @@ -53,7 +53,7 @@ const initialState = { const stateContainer = createStateContainer(initialState); if (!initialStateFromUrl) { - // can put default state to the url, for consistency + // for consistency put default state to the url urlStateStorage.set('_a', defaultState, { replace: true }); } @@ -65,7 +65,7 @@ const { start, stop } = syncState({ start(); -// on this point state in the storage and in synced +// on this point state in the storage and in state are synced // state: {count: 2} // storage: {count: 2} ``` diff --git a/src/plugins/kibana_utils/docs/state_sync/no_state_containers.md b/src/plugins/kibana_utils/docs/state_sync/no_state_containers.md index 28aba319b2b656..365043a527c35c 100644 --- a/src/plugins/kibana_utils/docs/state_sync/no_state_containers.md +++ b/src/plugins/kibana_utils/docs/state_sync/no_state_containers.md @@ -1,7 +1,7 @@ # Using state syncing utilities without state containers -It is possible to use `syncState` utility even if your app not using [state containers](../state_containers). -The `state` which is passed into `syncState` function should implement following interface: +It is possible to use `syncState` utility even if your app is not using [state containers](../state_containers). +The `state` which is passed into `syncState` function should just implement this simple interface: ```ts export interface BaseStateContainer { @@ -11,7 +11,7 @@ export interface BaseStateContainer { } ``` -For example, assuming you have a custom state manager, setting up syncing state with url could look something like this: +For example, assuming you have a custom state manager, setting up syncing state with URL could look something like this: ```ts import { Subject } from 'rxjs'; diff --git a/src/plugins/kibana_utils/docs/state_sync/on_fly_state_migrations.md b/src/plugins/kibana_utils/docs/state_sync/on_fly_state_migrations.md index 7ec8120aaf39b9..e3bcef67b247a7 100644 --- a/src/plugins/kibana_utils/docs/state_sync/on_fly_state_migrations.md +++ b/src/plugins/kibana_utils/docs/state_sync/on_fly_state_migrations.md @@ -1,14 +1,14 @@ # On-fly state migrations -When retrieving initial state from storage we can't forget about possible outdated state. -As described in [handling initial state](./initial_state.md), this could be the place where we can apply migrations. +When retrieving initial state from storage we shouldn't forget about possible outdated state. +Similar to [handling initial state](./initial_state.md) example, applications could handle migrations during initialisation. ```ts import { migrate } from '../app/state_helpers'; const urlStateStorage = createKbnUrlStateStorage(); const initialStateFromUrl = urlStateStorage.get('_a'); -// merge together default state and initial state and migrate it to current version if needed +// merge default state and initial state and migrate it to the current version const initialState = migrate({ ...defaultState, ...initialStateFromUrl, @@ -17,7 +17,7 @@ const initialState = migrate({ const stateContainer = createStateContainer(initialState); ``` -But in addition it is also possible to apply migrations for any incoming state similar to how described in [handling empty or incomplete state](./empty_or_incomplete_incoming_state.md). +It is also possible to apply migrations for any incoming state similar example in [handling empty or incomplete state](./empty_or_incomplete_incoming_state.md). ```ts import { migrate } from '../app/state_helpers'; @@ -43,4 +43,4 @@ const { start, stop } = syncState({ ``` This should cover edge case, when user already have, for example, dashboard opened and then user pastes an older dashboard link into browser window. -No dashboard remount will happen, so, as we need to transition to a new state on-fly, we also need to apply necessary migrations on-fly. +No dashboard remount will happen, so, as we are transitioning to a new state on-fly, we are also applying necessary migrations on-fly. diff --git a/src/plugins/kibana_utils/docs/state_sync/storages/README.md b/src/plugins/kibana_utils/docs/state_sync/storages/README.md index 7388ea4dc545d4..3411a49366c593 100644 --- a/src/plugins/kibana_utils/docs/state_sync/storages/README.md +++ b/src/plugins/kibana_utils/docs/state_sync/storages/README.md @@ -2,6 +2,6 @@ 2 storage types are supported: -- [KbnUrlStateStorage](./kbn_url_storage.md) - Serialises state and persists it to url's query param in rison or hashed format (similar to what AppState & GlobalState did in legacy world). - Listens for state updates in the url and pushes updates back to state. +- [KbnUrlStateStorage](./kbn_url_storage.md) - Serialises state and persists it to URL's query param in rison or hashed format (similar to what AppState & GlobalState did in legacy world). + Listens for state updates in the URL and pushes updates back to state. - [SessionStorageStateStorage](./session_storage.md) - Serialises state and persists it to session storage. diff --git a/src/plugins/kibana_utils/docs/state_sync/storages/kbn_url_storage.md b/src/plugins/kibana_utils/docs/state_sync/storages/kbn_url_storage.md index 79917990e7632b..35aec59e313a23 100644 --- a/src/plugins/kibana_utils/docs/state_sync/storages/kbn_url_storage.md +++ b/src/plugins/kibana_utils/docs/state_sync/storages/kbn_url_storage.md @@ -2,16 +2,18 @@ `KbnUrlStateStorage` is a state storage for `syncState` utility which: -- Keeps state in sync with the url -- Serialises data and stores data in url in 2 different formats: +- Keeps state in sync with the URL. +- Serializes data and stores it in the URL in 2 different formats: 1. [Rison](https://github.com/w33ble/rison-node) encoded. - 2. Hashed url. In url we store only the hash from the serialized state, but the state itself is stored in sessionStorage. + 2. Hashed URL: In URL we store only the hash from the serialized state, but the state itself is stored in `sessionStorage`. See kibana's advanced option for more context `state:storeInSessionStorage` -- Takes care of listening to url updates and notifies state about updates -- Takes care of batching url updates to prevent redundant browser history records +- Takes care of listening to the URL updates and notifies state about the updates. +- Takes care of batching URL updates to prevent redundant browser history records. ### Basic Example +With state sync utility: + ```ts import { createStateContainer, @@ -31,24 +33,34 @@ const { start, stop } = syncState({ start(); // state container change is synced to state storage -// in this case, kbnUrlStateStorage updates the url to "/#?_a=(count:2)" +// in this case, kbnUrlStateStorage updates the URL to "/#?_a=(count:2)" stateContainer.set({ count: 2 }); stop(); ``` -### Setting up url format option +Or just by itself: + +```ts +// assuming url is "/#?_a=(count:2)" +const stateStorage = createKbnUrlStateStorage(); + +stateStorage.get('_a'); // returns {count: a} +stateStorage.set('_a', { count: 0 }); // updates url to "/#?_a=(count:0)" +``` + +### Setting URL format option ```ts const stateStorage = createKbnUrlStateStorage({ - useHash: core.uiSettings.get('state:storeInSessionStorage'), // put full encoded rison or just the hash into the url + useHash: core.uiSettings.get('state:storeInSessionStorage'), // put the complete encoded rison or just the hash into the URL }); ``` ### Passing [history](https://github.com/ReactTraining/history) instance -Under the hood `kbnUrlStateStorage` uses [history](https://github.com/ReactTraining/history) for updating the url and for listening for url updates. -To prevent bugs caused by missing history updates, make sure your app uses 1 instance of history for url changes which may interfere with each other. +Under the hood `kbnUrlStateStorage` uses [history](https://github.com/ReactTraining/history) for updating the URL and for listening to the URL updates. +To prevent bugs caused by missing history updates, make sure your app uses one instance of history for URL changes which may interfere with each other. For example, if you use `react-router`: @@ -77,23 +89,23 @@ const urlStateStorage = createKbnUrlStateStorage(); urlStateStorage.set('_a', { state: 1 }); urlStateStorage.set('_b', { state: 2 }); -// url hasn't been updated yet +// URL hasn't been updated yet setTimeout(() => { - // now url is actually "/#?_a=(state:1)&_b=(state:2)" + // now URL is actually "/#?_a=(state:1)&_b=(state:2)" // and 2 updates were batched into 1 history.push() call }, 0); ``` -For cases, where granular control over url updates is needed, `kbnUrlStateStorage` supports these advanced apis: +For cases, where granular control over URL updates is needed, `kbnUrlStateStorage` provides these advanced apis: - `kbnUrlStateStorage.flush({replace: boolean})` - allows to synchronously apply any pending updates. - `replace` option allows to use `history.replace()` instead of `history.push()`. Returned boolean indicates if any update happened. + `replace` option allows to use `history.replace()` instead of `history.push()`. Returned boolean indicates if any update happened - `kbnUrlStateStorage.cancel()` - cancels any pending updates -### Sharing one instance of `kbnUrlStateStorage` +### Sharing one `kbnUrlStateStorage` instance `kbnUrlStateStorage` is stateful, as it keeps track of pending updates. -So if there are multiple state syncs happening in the time, then one instance of `kbnUrlStateStorage` should be used to make sure the same update queue is used. +So if there are multiple state syncs happening in the same time, then one instance of `kbnUrlStateStorage` should be used to make sure, that the same update queue is used. Otherwise it could cause redundant browser history records. ```ts @@ -128,7 +140,7 @@ const { start, stop } = syncStates([ }, ]); -// correct: +// the best: import { createBrowserHistory } from 'history'; const history = createBrowserHistory(); diff --git a/src/plugins/kibana_utils/docs/state_sync/storages/session_storage.md b/src/plugins/kibana_utils/docs/state_sync/storages/session_storage.md index d47e80cf419687..0cee08ef9318dc 100644 --- a/src/plugins/kibana_utils/docs/state_sync/storages/session_storage.md +++ b/src/plugins/kibana_utils/docs/state_sync/storages/session_storage.md @@ -1,6 +1,6 @@ # Session Storage -To sync state from state containers to session storage use `sessionStorageStateStorage`. +To sync state from state containers to `sessionStorage` use `sessionStorageStateStorage`. ```ts import { diff --git a/src/plugins/kibana_utils/docs/state_sync/sync_states.md b/src/plugins/kibana_utils/docs/state_sync/sync_states.md index b5ff37068cebb2..9a040cb9ba405b 100644 --- a/src/plugins/kibana_utils/docs/state_sync/sync_states.md +++ b/src/plugins/kibana_utils/docs/state_sync/sync_states.md @@ -1,7 +1,7 @@ # Sync states utility -In case you need to sync multiple states or a state to multiple storages, there is a handy util for that. -It saves a bit of boilerplate by returning `start` and `stop` functions for all `stateSync` configs at once. +In case you need to sync multiple states or one state to multiple storages, there is a handy util for that. +It saves a bit of boilerplate by returning `start` and `stop` functions for all `syncState` configs at once. ```ts import { From 87024a1f83dec65f59f494cd6c0a6f9a8a5db11e Mon Sep 17 00:00:00 2001 From: Anton Dosov Date: Fri, 31 Jan 2020 12:29:36 +0100 Subject: [PATCH 4/7] improve --- .../docs/state_sync/empty_or_incomplete_incoming_state.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/plugins/kibana_utils/docs/state_sync/empty_or_incomplete_incoming_state.md b/src/plugins/kibana_utils/docs/state_sync/empty_or_incomplete_incoming_state.md index 0f4bce78b6815a..0c3e265c314e66 100644 --- a/src/plugins/kibana_utils/docs/state_sync/empty_or_incomplete_incoming_state.md +++ b/src/plugins/kibana_utils/docs/state_sync/empty_or_incomplete_incoming_state.md @@ -47,6 +47,6 @@ In this case, app will not get into state, which is not shaped as app expects. To help application developers to not forget about such edge cases, `syncState` util sets a constraint, -that setter to state container should be able to handle "null" value (see [IStateSyncConfig](../../public/state_sync/types.ts)). -Incoming `null` value from state storage usually means empty state (e.g. URL without `storageKey` query param) or corrupted state which can't be parsed. +that setter to state container should be able to handle `null` value (see [IStateSyncConfig](../../public/state_sync/types.ts)). +Incoming `null` value from state storage usually means that state is empty (e.g. URL without `storageKey` query param). So when using `syncState` util applications are required to at least handle incoming `null`. From 03b35ef55efefcbc66d795a7faf659fb8a5c9fbe Mon Sep 17 00:00:00 2001 From: Anton Dosov Date: Mon, 3 Feb 2020 13:26:30 +0100 Subject: [PATCH 5/7] review comments --- .../kibana_utils/docs/state_sync/storages/session_storage.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/plugins/kibana_utils/docs/state_sync/storages/session_storage.md b/src/plugins/kibana_utils/docs/state_sync/storages/session_storage.md index 0cee08ef9318dc..cfefc6688f09dc 100644 --- a/src/plugins/kibana_utils/docs/state_sync/storages/session_storage.md +++ b/src/plugins/kibana_utils/docs/state_sync/storages/session_storage.md @@ -1,6 +1,6 @@ # Session Storage -To sync state from state containers to `sessionStorage` use `sessionStorageStateStorage`. +To sync state from state containers to `sessionStorage` use `createSessionStorageStateStorage`. ```ts import { @@ -27,7 +27,7 @@ stateContainer.set({ count: 2 }); stop(); ``` -You can also imperative use `sessionStorageStateStorage`: +You can also use `createSessionStorageStateStorage` imperatively: ```ts const stateStorage = createSessionStorageStateStorage(); From f89c65269a401187091744176aea574921af4b29 Mon Sep 17 00:00:00 2001 From: Anton Dosov Date: Thu, 6 Feb 2020 15:39:16 +0100 Subject: [PATCH 6/7] @lizozom review --- .../kibana_utils/docs/state_sync/README.md | 6 +++--- .../empty_or_incomplete_incoming_state.md | 12 +++++++----- .../docs/state_sync/on_fly_state_migrations.md | 16 +++++++++------- .../docs/state_sync/storages/README.md | 2 +- 4 files changed, 20 insertions(+), 16 deletions(-) diff --git a/src/plugins/kibana_utils/docs/state_sync/README.md b/src/plugins/kibana_utils/docs/state_sync/README.md index e48349ab25b09a..acfe6dcf76fe97 100644 --- a/src/plugins/kibana_utils/docs/state_sync/README.md +++ b/src/plugins/kibana_utils/docs/state_sync/README.md @@ -10,8 +10,8 @@ State syncing utilities include: - `syncState` util which: - Subscribes to state changes and pushes them to state storage. - Optionally subscribes to state storage changes and pushes them to state. -- 2 storage types for `syncState` util: - - [KbnUrlStateStorage](./storages/kbn_url_storage.md) - Serializes state and persists it to URL's query param in rison or hashed format (similar to what `AppState` & `GlobalState` did in legacy world). +- Two types of storage compatible with `syncState`: + - [KbnUrlStateStorage](./storages/kbn_url_storage.md) - Serializes state and persists it to URL's query param in rison or hashed format. Listens for state updates in the URL and pushes them back to state. - [SessionStorageStateStorage](./storages/session_storage.md) - Serializes state and persists it to session storage. @@ -55,6 +55,6 @@ To run them, start kibana with `--run-examples` flag. - [Setting up initial state](./initial_state.md). - [Using without state containers](./no_state_containers.md). - [Handling empty or incomplete incoming state](./empty_or_incomplete_incoming_state.md). -- [On-fly state migrations](./on_fly_state_migrations.md). +- [On-the-fly state migrations](./on_fly_state_migrations.md). - [syncStates helper](./sync_states.md). - [Helpers for Data plugin (syncing TimeRange, RefreshInterval and Filters)](./data_plugin_helpers.md). diff --git a/src/plugins/kibana_utils/docs/state_sync/empty_or_incomplete_incoming_state.md b/src/plugins/kibana_utils/docs/state_sync/empty_or_incomplete_incoming_state.md index 0c3e265c314e66..e8ed579c0aff2c 100644 --- a/src/plugins/kibana_utils/docs/state_sync/empty_or_incomplete_incoming_state.md +++ b/src/plugins/kibana_utils/docs/state_sync/empty_or_incomplete_incoming_state.md @@ -1,11 +1,13 @@ # Handling empty or incomplete incoming state -Users have direct access to storages where we sync our state to. For example, URL. -User can manually change the URL and remove or corrupt important data which we expect to be there. -Or users may programmatically generate URLs to Kibana and these URLs could have mistakes which application can't handle. +Users have direct access to storages where we sync our state to. +For example, in the URL, a user can manually change the URL and remove or corrupt important data which we expect to be there. +URLs may also be programmatically generated, increasing the risk for mistakes which application can't handle. -`syncState` utility doesn't do any handling for such edge cases and passes whatever received from storage to state container as is. -So it is up to application to decide, how to handle such scenarios. Consider the example: +`syncState` doesn't handle such edge cases passing input from storage to the state container as is. +It is up to the application to handle such scenarios. + +Consider the following example: ```ts // window.location.href is "/#?_a=(count:0)" diff --git a/src/plugins/kibana_utils/docs/state_sync/on_fly_state_migrations.md b/src/plugins/kibana_utils/docs/state_sync/on_fly_state_migrations.md index e3bcef67b247a7..65d1bce588eed3 100644 --- a/src/plugins/kibana_utils/docs/state_sync/on_fly_state_migrations.md +++ b/src/plugins/kibana_utils/docs/state_sync/on_fly_state_migrations.md @@ -1,7 +1,9 @@ -# On-fly state migrations +# On-the-fly state migrations When retrieving initial state from storage we shouldn't forget about possible outdated state. -Similar to [handling initial state](./initial_state.md) example, applications could handle migrations during initialisation. +Consider the scenario, where user launches application from a bookmarked link, which contains outdated state. + +Similar to [handling initial state](./initial_state.md) example, applications could handle migrations during Initialization. ```ts import { migrate } from '../app/state_helpers'; @@ -17,7 +19,10 @@ const initialState = migrate({ const stateContainer = createStateContainer(initialState); ``` -It is also possible to apply migrations for any incoming state similar example in [handling empty or incomplete state](./empty_or_incomplete_incoming_state.md). +It is also possible to apply migrations for any incoming state, similar to [handling empty or incomplete state](./empty_or_incomplete_incoming_state.md). + +Imagine an edge case scenario, where a user is working in your application, and then pastes an old link for the same application, containing older state with a different structure. +Since no application remount will happen, we need to transition to a new state on-the-fly, by applying necessary migrations. ```ts import { migrate } from '../app/state_helpers'; @@ -25,7 +30,7 @@ import { migrate } from '../app/state_helpers'; const urlStateStorage = createKbnUrlStateStorage(); const initialStateFromUrl = urlStateStorage.get('_a'); -// merge together default state and initial state and migrate it to current version if needed +// merge default state and initial state and migrate them to current version if needed const initialState = migrate({ ...defaultState, ...initialStateFromUrl, @@ -41,6 +46,3 @@ const { start, stop } = syncState({ stateStorage, }); ``` - -This should cover edge case, when user already have, for example, dashboard opened and then user pastes an older dashboard link into browser window. -No dashboard remount will happen, so, as we are transitioning to a new state on-fly, we are also applying necessary migrations on-fly. diff --git a/src/plugins/kibana_utils/docs/state_sync/storages/README.md b/src/plugins/kibana_utils/docs/state_sync/storages/README.md index 3411a49366c593..319dff9bae8c6d 100644 --- a/src/plugins/kibana_utils/docs/state_sync/storages/README.md +++ b/src/plugins/kibana_utils/docs/state_sync/storages/README.md @@ -1,6 +1,6 @@ # State Storage -2 storage types are supported: +Two types of storage compatible with `syncState`: - [KbnUrlStateStorage](./kbn_url_storage.md) - Serialises state and persists it to URL's query param in rison or hashed format (similar to what AppState & GlobalState did in legacy world). Listens for state updates in the URL and pushes updates back to state. From 94a9159e4557ec6bf9840651f96efa50d6ca093b Mon Sep 17 00:00:00 2001 From: Anton Dosov Date: Thu, 6 Feb 2020 15:54:21 +0100 Subject: [PATCH 7/7] @lizozom review 2 --- .../empty_or_incomplete_incoming_state.md | 14 ++++++------- .../docs/state_sync/initial_state.md | 20 +++++++++---------- .../docs/state_sync/no_state_containers.md | 11 +++++++--- .../docs/state_sync/storages/README.md | 2 +- .../state_sync/storages/kbn_url_storage.md | 2 +- 5 files changed, 27 insertions(+), 22 deletions(-) diff --git a/src/plugins/kibana_utils/docs/state_sync/empty_or_incomplete_incoming_state.md b/src/plugins/kibana_utils/docs/state_sync/empty_or_incomplete_incoming_state.md index e8ed579c0aff2c..22163d6f51ead2 100644 --- a/src/plugins/kibana_utils/docs/state_sync/empty_or_incomplete_incoming_state.md +++ b/src/plugins/kibana_utils/docs/state_sync/empty_or_incomplete_incoming_state.md @@ -10,7 +10,7 @@ It is up to the application to handle such scenarios. Consider the following example: ```ts -// window.location.href is "/#?_a=(count:0)" +// window.location.href is "/#?_a=(count:0" const defaultState = { count: 0 }; // default application state const stateContainer = createStateContainer(defaultState); @@ -24,15 +24,15 @@ const { start, stop } = syncState({ start(); -// on this point state in the storage and in the state is out of sync +// At this point, state and storage are in sync // state: {count: 0} // storage: {count: 0} -// And now user changed the URL to (let's assume) "/#?_a=(corrupt:0)" -// on this point state will recieve an update: {corrupt: 0} +// Now user changes the URL manually to "/#?_a=(corrupt:0)", +// triggering a state update with {corrupt: 0} ``` -Application could handle this gracefully by using simple composition during setup: +The application could, for example, handle this gracefully, by using simple composition during setup: ```ts const { start, stop } = syncState({ @@ -45,9 +45,9 @@ const { start, stop } = syncState({ }); ``` -In this case, app will not get into state, which is not shaped as app expects. +In this case, the corrupt value will not get into state, preventing misshaped state. -To help application developers to not forget about such edge cases, +To help application developers remember such edge cases, `syncState` util sets a constraint, that setter to state container should be able to handle `null` value (see [IStateSyncConfig](../../public/state_sync/types.ts)). Incoming `null` value from state storage usually means that state is empty (e.g. URL without `storageKey` query param). diff --git a/src/plugins/kibana_utils/docs/state_sync/initial_state.md b/src/plugins/kibana_utils/docs/state_sync/initial_state.md index b5ab50d715d8ec..306c9313178f35 100644 --- a/src/plugins/kibana_utils/docs/state_sync/initial_state.md +++ b/src/plugins/kibana_utils/docs/state_sync/initial_state.md @@ -1,7 +1,7 @@ # Setting up initial state The `syncState` util doesn't do any initial state syncing between state and storage. -Consider the scenario: +Consider the following scenario: ```ts // window.location.href is "/#?_a=(count:2)" @@ -18,23 +18,23 @@ const { start, stop } = syncState({ start(); -// on this point state in the storage and in the state is out of sync +// Now the storage and state are out of sync // state: {count: 0} // storage: {count: 2} ``` -The `syncState` doesn't make a decision, how initial state should be synced and which state should take precedence. -It is up to the application to decide, depending on the specific use case. +It is up to the application to decide, how initial state should be synced and which state should take precedence, depending on the specific use case. + Questions to consider: 1. Should default state take precedence over URL? 2. Should URL take precedence? 3. Do we have to do any state migrations for what is coming from the URL? 4. If URL doesn't have the whole state, should we merge it with default one or leave it behind? -5. Is there any other state loading in parallel (e.g. from a `SavedObject`)? How should we merge it all together? +5. Is there any other state loading in parallel (e.g. from a `SavedObject`)? How should we merge those? 6. Are we storing the state both in the URL and in the `sessionStorage`? Which one should take precedence and in which case? -So if initial state syncing is required, for simple example above it could look like this: +A possible synchronization for the state conflict above could look like this: ```ts // window.location.href is "/#?_a=(count:2)" @@ -44,7 +44,7 @@ const urlStateStorage = createKbnUrlStateStorage(); const initialStateFromUrl = urlStateStorage.get('_a'); -// merge together default state and initial state +// merge the default state and initial state from the url and use it as initial application state const initialState = { ...defaultState, ...initialStateFromUrl, @@ -52,9 +52,9 @@ const initialState = { const stateContainer = createStateContainer(initialState); +// preserve initial application state in the URL if (!initialStateFromUrl) { - // for consistency put default state to the url - urlStateStorage.set('_a', defaultState, { replace: true }); + urlStateStorage.set('_a', initialState, { replace: true }); } const { start, stop } = syncState({ @@ -65,7 +65,7 @@ const { start, stop } = syncState({ start(); -// on this point state in the storage and in state are synced +// Now the storage and state are in sync // state: {count: 2} // storage: {count: 2} ``` diff --git a/src/plugins/kibana_utils/docs/state_sync/no_state_containers.md b/src/plugins/kibana_utils/docs/state_sync/no_state_containers.md index 365043a527c35c..ad78774435825e 100644 --- a/src/plugins/kibana_utils/docs/state_sync/no_state_containers.md +++ b/src/plugins/kibana_utils/docs/state_sync/no_state_containers.md @@ -1,7 +1,7 @@ # Using state syncing utilities without state containers It is possible to use `syncState` utility even if your app is not using [state containers](../state_containers). -The `state` which is passed into `syncState` function should just implement this simple interface: +The `state` which is passed into `syncState` function should implement this simple interface: ```ts export interface BaseStateContainer { @@ -11,13 +11,14 @@ export interface BaseStateContainer { } ``` -For example, assuming you have a custom state manager, setting up syncing state with URL could look something like this: +For example, assuming you have a custom state manager, setting up syncing state with the URL could look something like this: ```ts +// my_state_manager.ts import { Subject } from 'rxjs'; import { map } from 'rxjs/operators'; -class MyStateManager { +export class MyStateManager { private count: number = 0; updated$ = new Subject(); @@ -33,8 +34,12 @@ class MyStateManager { return this.count; } } +``` +```ts +// app.ts import { syncState, createKbnUrlStateStorage } from 'src/plugins/kibana_utils/public'; +import { MyStateManager } from './my_state_manager'; const myStateManager = new MyStateManager(); diff --git a/src/plugins/kibana_utils/docs/state_sync/storages/README.md b/src/plugins/kibana_utils/docs/state_sync/storages/README.md index 319dff9bae8c6d..8cfffa31350f75 100644 --- a/src/plugins/kibana_utils/docs/state_sync/storages/README.md +++ b/src/plugins/kibana_utils/docs/state_sync/storages/README.md @@ -4,4 +4,4 @@ Two types of storage compatible with `syncState`: - [KbnUrlStateStorage](./kbn_url_storage.md) - Serialises state and persists it to URL's query param in rison or hashed format (similar to what AppState & GlobalState did in legacy world). Listens for state updates in the URL and pushes updates back to state. -- [SessionStorageStateStorage](./session_storage.md) - Serialises state and persists it to session storage. +- [SessionStorageStateStorage](./session_storage.md) - Serializes state and persists it to session storage. diff --git a/src/plugins/kibana_utils/docs/state_sync/storages/kbn_url_storage.md b/src/plugins/kibana_utils/docs/state_sync/storages/kbn_url_storage.md index 35aec59e313a23..3a31f5a326edb8 100644 --- a/src/plugins/kibana_utils/docs/state_sync/storages/kbn_url_storage.md +++ b/src/plugins/kibana_utils/docs/state_sync/storages/kbn_url_storage.md @@ -3,7 +3,7 @@ `KbnUrlStateStorage` is a state storage for `syncState` utility which: - Keeps state in sync with the URL. -- Serializes data and stores it in the URL in 2 different formats: +- Serializes data and stores it in the URL in one of the supported formats: 1. [Rison](https://github.com/w33ble/rison-node) encoded. 2. Hashed URL: In URL we store only the hash from the serialized state, but the state itself is stored in `sessionStorage`. See kibana's advanced option for more context `state:storeInSessionStorage`