-
Notifications
You must be signed in to change notification settings - Fork 8.1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Browse files
Browse the repository at this point in the history
- Loading branch information
Showing
13 changed files
with
562 additions
and
8 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,60 @@ | ||
# 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. | ||
- 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. | ||
|
||
## 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 synched to state storage | ||
// kbnUrlStateStorage updates the URL to "/#?_a=(count:2)" | ||
stateContainer.set({ count: 2 }); | ||
|
||
stop(); | ||
``` | ||
|
||
## Demos Plugins | ||
|
||
See demos plugins [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](./no_state_containers.md). | ||
- [Handling empty or incomplete incoming state](./empty_or_incomplete_incoming_state.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). |
7 changes: 7 additions & 0 deletions
7
src/plugins/kibana_utils/docs/state_sync/data_plugin_helpers.md
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
# Helpers for syncing state with data plugins (TimeRange, RefreshInterval and Filters) | ||
|
||
```ts | ||
// TODO: Waiting for | ||
// https://github.com/elastic/kibana/issues/55977 | ||
// https://github.com/elastic/kibana/pull/56128 | ||
``` |
54 changes: 54 additions & 0 deletions
54
src/plugins/kibana_utils/docs/state_sync/empty_or_incomplete_incoming_state.md
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,54 @@ | ||
# Handling empty or incomplete incoming state | ||
|
||
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` 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" | ||
const defaultState = { count: 0 }; // default application state | ||
|
||
const stateContainer = createStateContainer(defaultState); | ||
const stateStorage = createKbnUrlStateStorage(); | ||
|
||
const { start, stop } = syncState({ | ||
storageKey: '_a', | ||
stateContainer, | ||
stateStorage, | ||
}); | ||
|
||
start(); | ||
|
||
// At this point, state and storage are in sync | ||
// state: {count: 0} | ||
// storage: {count: 0} | ||
|
||
// Now user changes the URL manually to "/#?_a=(corrupt:0)", | ||
// triggering a state update with {corrupt: 0} | ||
``` | ||
|
||
The application could, for example, 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, the corrupt value will not get into state, preventing misshaped state. | ||
|
||
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). | ||
So when using `syncState` util applications are required to at least handle incoming `null`. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,71 @@ | ||
# Setting up initial state | ||
|
||
The `syncState` util doesn't do any initial state syncing between state and storage. | ||
Consider the following 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(); | ||
|
||
// Now the storage and state are out of sync | ||
// state: {count: 0} | ||
// storage: {count: 2} | ||
``` | ||
|
||
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 those? | ||
6. Are we storing the state both in the URL and in the `sessionStorage`? Which one should take precedence and in which case? | ||
|
||
A possible synchronization for the state conflict 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 the default state and initial state from the url and use it as initial application state | ||
const initialState = { | ||
...defaultState, | ||
...initialStateFromUrl, | ||
}; | ||
|
||
const stateContainer = createStateContainer(initialState); | ||
|
||
// preserve initial application state in the URL | ||
if (!initialStateFromUrl) { | ||
urlStateStorage.set('_a', initialState, { replace: true }); | ||
} | ||
|
||
const { start, stop } = syncState({ | ||
storageKey: '_a', | ||
stateContainer, | ||
stateStorage: urlStateStorage, | ||
}); | ||
|
||
start(); | ||
|
||
// Now the storage and state are in sync | ||
// state: {count: 2} | ||
// storage: {count: 2} | ||
``` |
51 changes: 51 additions & 0 deletions
51
src/plugins/kibana_utils/docs/state_sync/no_state_containers.md
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
# 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 implement this simple interface: | ||
|
||
```ts | ||
export interface BaseStateContainer<State extends BaseState> { | ||
get: () => State; | ||
set: (state: State | null) => void; | ||
state$: Observable<State>; | ||
} | ||
``` | ||
|
||
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'; | ||
|
||
export 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; | ||
} | ||
} | ||
``` | ||
|
||
```ts | ||
// app.ts | ||
import { syncState, createKbnUrlStateStorage } from 'src/plugins/kibana_utils/public'; | ||
import { MyStateManager } from './my_state_manager'; | ||
|
||
const myStateManager = new MyStateManager(); | ||
|
||
syncState({ | ||
get: () => ({ count: myStateManager.getCount() }), | ||
set: state => state && myStateManager.setCount(state.count), | ||
state$: myStateManager.updated$.pipe(map(() => ({ count: myStateManager.getCount() }))), | ||
}); | ||
``` |
48 changes: 48 additions & 0 deletions
48
src/plugins/kibana_utils/docs/state_sync/on_fly_state_migrations.md
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
# On-the-fly state migrations | ||
|
||
When retrieving initial state from storage we shouldn't forget about possible outdated state. | ||
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'; | ||
const urlStateStorage = createKbnUrlStateStorage(); | ||
const initialStateFromUrl = urlStateStorage.get('_a'); | ||
|
||
// merge default state and initial state and migrate it to the current version | ||
const initialState = migrate({ | ||
...defaultState, | ||
...initialStateFromUrl, | ||
}); | ||
|
||
const stateContainer = createStateContainer(initialState); | ||
``` | ||
|
||
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'; | ||
|
||
const urlStateStorage = createKbnUrlStateStorage(); | ||
const initialStateFromUrl = urlStateStorage.get('_a'); | ||
|
||
// merge default state and initial state and migrate them 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 file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
# State Storage | ||
|
||
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) - Serializes state and persists it to session storage. |
Oops, something went wrong.