* feat: 🎸 add state containers * docs: ✏️ add state container demos * docs: ✏️ refrech state container docs * chore: 🤖 install default comparator * chore: 🤖 remove old state container implementation * feat: 🎸 add selectors * chore: 🤖 move Ensure tyep to type utils * fix: 🐛 fix useSelector() types and demo CLI command * test: 💍 add tests for state container demos * feat: 🎸 add ReacursiveReadonly to kbn-utility-types * feat: 🎸 shallow freeze state when not in production * test: 💍 fix Jest tests * refactor: 💡 remove .state and use BehaviourSubject
This commit is contained in:
parent
7671ce3eb4
commit
db342e9e05
|
@ -163,6 +163,7 @@
|
|||
"encode-uri-query": "1.0.1",
|
||||
"execa": "^3.2.0",
|
||||
"expiry-js": "0.1.7",
|
||||
"fast-deep-equal": "^3.1.1",
|
||||
"file-loader": "4.2.0",
|
||||
"font-awesome": "4.7.0",
|
||||
"getos": "^3.1.0",
|
||||
|
@ -226,6 +227,7 @@
|
|||
"react-resize-detector": "^4.2.0",
|
||||
"react-router-dom": "^4.3.1",
|
||||
"react-sizeme": "^2.3.6",
|
||||
"react-use": "^13.10.2",
|
||||
"reactcss": "1.2.3",
|
||||
"redux": "4.0.0",
|
||||
"redux-actions": "2.2.1",
|
||||
|
|
|
@ -18,7 +18,9 @@ type B = UnwrapPromise<A>; // string
|
|||
|
||||
## Reference
|
||||
|
||||
- `UnwrapPromise<T>` — Returns wrapped type of a promise.
|
||||
- `UnwrapObservable<T>` — Returns wrapped type of an observable.
|
||||
- `ShallowPromise<T>` — Same as `Promise` type, but it flat maps the wrapped type.
|
||||
- `Ensure<T, X>` — Makes sure `T` is of type `X`.
|
||||
- `ObservableLike<T>` — Minimal interface for an object resembling an `Observable`.
|
||||
- `RecursiveReadonly<T>` — Like `Readonly<T>`, but freezes object recursively.
|
||||
- `ShallowPromise<T>` — Same as `Promise` type, but it flat maps the wrapped type.
|
||||
- `UnwrapObservable<T>` — Returns wrapped type of an observable.
|
||||
- `UnwrapPromise<T>` — Returns wrapped type of a promise.
|
||||
|
|
|
@ -42,3 +42,19 @@ export type UnwrapObservable<T extends ObservableLike<any>> = T extends Observab
|
|||
* Converts a type to a `Promise`, unless it is already a `Promise`. Useful when proxying the return value of a possibly async function.
|
||||
*/
|
||||
export type ShallowPromise<T> = T extends Promise<infer U> ? Promise<U> : Promise<T>;
|
||||
|
||||
/**
|
||||
* Ensures T is of type X.
|
||||
*/
|
||||
export type Ensure<T, X> = T extends X ? T : never;
|
||||
|
||||
// If we define this inside RecursiveReadonly TypeScript complains.
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
||||
interface RecursiveReadonlyArray<T> extends Array<RecursiveReadonly<T>> {}
|
||||
export type RecursiveReadonly<T> = T extends (...args: any) => any
|
||||
? T
|
||||
: T extends any[]
|
||||
? RecursiveReadonlyArray<T[number]>
|
||||
: T extends object
|
||||
? Readonly<{ [K in keyof T]: RecursiveReadonly<T[K]> }>
|
||||
: T;
|
||||
|
|
|
@ -2,4 +2,4 @@
|
|||
|
||||
Utilities for building Kibana plugins.
|
||||
|
||||
- [Store reactive serializable app state in state containers, `createStore`](./docs/store/README.md).
|
||||
- [State containers](./docs/state_containers/README.md).
|
||||
|
|
36
src/plugins/kibana_utils/demos/demos.test.ts
Normal file
36
src/plugins/kibana_utils/demos/demos.test.ts
Normal file
|
@ -0,0 +1,36 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { result as counterResult } from './state_containers/counter';
|
||||
import { result as todomvcResult } from './state_containers/todomvc';
|
||||
|
||||
describe('demos', () => {
|
||||
describe('state containers', () => {
|
||||
test('counter demo works', () => {
|
||||
expect(counterResult).toBe(10);
|
||||
});
|
||||
|
||||
test('TodoMVC demo works', () => {
|
||||
expect(todomvcResult).toEqual([
|
||||
{ id: 0, text: 'Learning state containers', completed: true },
|
||||
{ id: 1, text: 'Learning transitions...', completed: true },
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -17,26 +17,16 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
import { Observable } from 'rxjs';
|
||||
import { Store as ReduxStore } from 'redux';
|
||||
import { createStateContainer } from '../../public/state_containers';
|
||||
|
||||
export interface AppStore<
|
||||
State extends {},
|
||||
StateMutators extends Mutators<PureMutators<State>> = {}
|
||||
> {
|
||||
redux: ReduxStore;
|
||||
get: () => State;
|
||||
set: (state: State) => void;
|
||||
state$: Observable<State>;
|
||||
createMutators: <M extends PureMutators<State>>(pureMutators: M) => Mutators<M>;
|
||||
mutators: StateMutators;
|
||||
}
|
||||
const container = createStateContainer(0, {
|
||||
increment: (cnt: number) => (by: number) => cnt + by,
|
||||
double: (cnt: number) => () => cnt * 2,
|
||||
});
|
||||
|
||||
export type PureMutator<State extends {}> = (state: State) => (...args: any[]) => State;
|
||||
export type Mutator<M extends PureMutator<any>> = (...args: Parameters<ReturnType<M>>) => void;
|
||||
container.transitions.increment(5);
|
||||
container.transitions.double();
|
||||
|
||||
export interface PureMutators<State extends {}> {
|
||||
[name: string]: PureMutator<State>;
|
||||
}
|
||||
console.log(container.get()); // eslint-disable-line
|
||||
|
||||
export type Mutators<M extends PureMutators<any>> = { [K in keyof M]: Mutator<M[K]> };
|
||||
export const result = container.get();
|
69
src/plugins/kibana_utils/demos/state_containers/todomvc.ts
Normal file
69
src/plugins/kibana_utils/demos/state_containers/todomvc.ts
Normal file
|
@ -0,0 +1,69 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { createStateContainer, PureTransition } from '../../public/state_containers';
|
||||
|
||||
export interface TodoItem {
|
||||
text: string;
|
||||
completed: boolean;
|
||||
id: number;
|
||||
}
|
||||
|
||||
export type TodoState = TodoItem[];
|
||||
|
||||
export const defaultState: TodoState = [
|
||||
{
|
||||
id: 0,
|
||||
text: 'Learning state containers',
|
||||
completed: false,
|
||||
},
|
||||
];
|
||||
|
||||
export interface TodoActions {
|
||||
add: PureTransition<TodoState, [TodoItem]>;
|
||||
edit: PureTransition<TodoState, [TodoItem]>;
|
||||
delete: PureTransition<TodoState, [number]>;
|
||||
complete: PureTransition<TodoState, [number]>;
|
||||
completeAll: PureTransition<TodoState, []>;
|
||||
clearCompleted: PureTransition<TodoState, []>;
|
||||
}
|
||||
|
||||
export const pureTransitions: TodoActions = {
|
||||
add: state => todo => [...state, todo],
|
||||
edit: state => todo => state.map(item => (item.id === todo.id ? { ...item, ...todo } : item)),
|
||||
delete: state => id => state.filter(item => item.id !== id),
|
||||
complete: state => id =>
|
||||
state.map(item => (item.id === id ? { ...item, completed: true } : item)),
|
||||
completeAll: state => () => state.map(item => ({ ...item, completed: true })),
|
||||
clearCompleted: state => () => state.filter(({ completed }) => !completed),
|
||||
};
|
||||
|
||||
const container = createStateContainer<TodoState, TodoActions>(defaultState, pureTransitions);
|
||||
|
||||
container.transitions.add({
|
||||
id: 1,
|
||||
text: 'Learning transitions...',
|
||||
completed: false,
|
||||
});
|
||||
container.transitions.complete(0);
|
||||
container.transitions.complete(1);
|
||||
|
||||
console.log(container.get()); // eslint-disable-line
|
||||
|
||||
export const result = container.get();
|
50
src/plugins/kibana_utils/docs/state_containers/README.md
Normal file
50
src/plugins/kibana_utils/docs/state_containers/README.md
Normal file
|
@ -0,0 +1,50 @@
|
|||
# State containers
|
||||
|
||||
State containers are Redux-store-like objects meant to help you manage state in
|
||||
your services or apps.
|
||||
|
||||
- State containers are strongly typed, you will get TypeScript autocompletion suggestions from
|
||||
your editor when accessing state, executing transitions and using React helpers.
|
||||
- State containers can be easily hooked up with your React components.
|
||||
- State containers can be used without React, too.
|
||||
- State containers provide you central place where to store state, instead of spreading
|
||||
state around multiple RxJs observables, which you need to coordinate. With state
|
||||
container you can always access the latest state snapshot synchronously.
|
||||
- Unlike Redux, state containers are less verbose, see example below.
|
||||
|
||||
|
||||
## Example
|
||||
|
||||
```ts
|
||||
import { createStateContainer } from 'src/plugins/kibana_utils';
|
||||
|
||||
const container = createStateContainer(0, {
|
||||
increment: (cnt: number) => (by: number) => cnt + by,
|
||||
double: (cnt: number) => () => cnt * 2,
|
||||
});
|
||||
|
||||
container.transitions.increment(5);
|
||||
container.transitions.double();
|
||||
console.log(container.get()); // 10
|
||||
```
|
||||
|
||||
|
||||
## Demos
|
||||
|
||||
See demos [here](../../demos/state_containers/).
|
||||
|
||||
You can run them with
|
||||
|
||||
```
|
||||
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).
|
||||
- [Parallels with Redux](./redux.md).
|
|
@ -17,7 +17,7 @@ interface MyState {
|
|||
}
|
||||
```
|
||||
|
||||
Create default state of your *store*.
|
||||
Create default state of your container.
|
||||
|
||||
```ts
|
||||
const defaultState: MyState = {
|
||||
|
@ -27,17 +27,12 @@ const defaultState: MyState = {
|
|||
};
|
||||
```
|
||||
|
||||
Create your state container, i.e *store*.
|
||||
Create your a state container.
|
||||
|
||||
```ts
|
||||
import { createStore } from 'kibana-utils';
|
||||
import { createStateContainer } from 'src/plugins/kibana_utils';
|
||||
|
||||
const store = createStore<MyState>(defaultState);
|
||||
console.log(store.get());
|
||||
const container = createStateContainer<MyState>(defaultState, {});
|
||||
|
||||
console.log(container.get());
|
||||
```
|
||||
|
||||
> ##### N.B.
|
||||
>
|
||||
> State must always be an object `{}`.
|
||||
>
|
||||
> You cannot create a store out of an array, e.g ~~`createStore([])`~~.
|
|
@ -1,4 +1,4 @@
|
|||
# Reading state
|
||||
# Consuming state in non-React setting
|
||||
|
||||
To read the current `state` of the store use `.get()` method.
|
||||
|
41
src/plugins/kibana_utils/docs/state_containers/react.md
Normal file
41
src/plugins/kibana_utils/docs/state_containers/react.md
Normal file
|
@ -0,0 +1,41 @@
|
|||
# React
|
||||
|
||||
`createStateContainerReactHelpers` factory allows you to easily use state containers with React.
|
||||
|
||||
|
||||
## Example
|
||||
|
||||
|
||||
```ts
|
||||
import { createStateContainer, createStateContainerReactHelpers } from 'src/plugins/kibana_utils';
|
||||
|
||||
const container = createStateContainer({}, {});
|
||||
export const {
|
||||
Provider,
|
||||
Consumer,
|
||||
context,
|
||||
useContainer,
|
||||
useState,
|
||||
useTransitions,
|
||||
useSelector,
|
||||
connect,
|
||||
} = createStateContainerReactHelpers<typeof container>();
|
||||
```
|
||||
|
||||
Wrap your app with `<Provider>`.
|
||||
|
||||
```tsx
|
||||
<Provider value={container}>
|
||||
<MyApplication />
|
||||
</Provider>
|
||||
```
|
||||
|
||||
|
||||
## Reference
|
||||
|
||||
- [`useContainer()`](./react/use_container.md)
|
||||
- [`useState()`](./react/use_state.md)
|
||||
- [`useSelector()`](./react/use_selector.md)
|
||||
- [`useTransitions()`](./react/use_transitions.md)
|
||||
- [`connect()()`](./react/connect.md)
|
||||
- [Context](./react/context.md)
|
|
@ -0,0 +1,22 @@
|
|||
# `connect()()` higher order component
|
||||
|
||||
Use `connect()()` higher-order-component to inject props from state into your component.
|
||||
|
||||
```tsx
|
||||
interface Props {
|
||||
name: string;
|
||||
punctuation: '.' | ',' | '!',
|
||||
}
|
||||
const Demo: React.FC<Props> = ({ name, punctuation }) =>
|
||||
<div>Hello, {name}{punctuation}</div>;
|
||||
|
||||
const store = createStateContainer({ userName: 'John' });
|
||||
const { Provider, connect } = createStateContainerReactHelpers(store);
|
||||
|
||||
const mapStateToProps = ({ userName }) => ({ name: userName });
|
||||
const DemoConnected = connect<Props, 'name'>(mapStateToProps)(Demo);
|
||||
|
||||
<Provider>
|
||||
<DemoConnected punctuation="!" />
|
||||
</Provider>
|
||||
```
|
|
@ -0,0 +1,24 @@
|
|||
# React context
|
||||
|
||||
`createStateContainerReactHelpers` returns `<Provider>` and `<Consumer>` components
|
||||
as well as `context` React context object.
|
||||
|
||||
```ts
|
||||
export const {
|
||||
Provider,
|
||||
Consumer,
|
||||
context,
|
||||
} = createStateContainerReactHelpers<typeof container>();
|
||||
```
|
||||
|
||||
`<Provider>` and `<Consumer>` are just regular React context components.
|
||||
|
||||
```tsx
|
||||
<Provider value={container}>
|
||||
<div>
|
||||
<Consumer>{container =>
|
||||
<pre>{JSON.stringify(container.get())}</pre>
|
||||
}</Consumer>
|
||||
</div>
|
||||
</Provider>
|
||||
```
|
|
@ -0,0 +1,10 @@
|
|||
# `useContainer` hook
|
||||
|
||||
`useContainer` React hook will simply return you `container` object from React context.
|
||||
|
||||
```tsx
|
||||
const Demo = () => {
|
||||
const store = useContainer();
|
||||
return <div>{store.get().isDarkMode ? '🌑' : '☀️'}</div>;
|
||||
};
|
||||
```
|
|
@ -0,0 +1,20 @@
|
|||
# `useSelector()` hook
|
||||
|
||||
With `useSelector` React hook you specify a selector function, which will pick specific
|
||||
data from the state. *Your component will update only when that specific part of the state changes.*
|
||||
|
||||
```tsx
|
||||
const selector = state => state.isDarkMode;
|
||||
const Demo = () => {
|
||||
const isDarkMode = useSelector(selector);
|
||||
return <div>{isDarkMode ? '🌑' : '☀️'}</div>;
|
||||
};
|
||||
```
|
||||
|
||||
As an optional second argument for `useSelector` you can provide a `comparator` function, which
|
||||
compares currently selected value with the previous and your component will re-render only if
|
||||
`comparator` returns `true`. By default it uses [`fast-deep-equal`](https://github.com/epoberezkin/fast-deep-equal).
|
||||
|
||||
```
|
||||
useSelector(selector, comparator?)
|
||||
```
|
|
@ -0,0 +1,11 @@
|
|||
# `useState()` hook
|
||||
|
||||
- `useState` hook returns you directly the state of the container.
|
||||
- It also forces component to re-render every time state changes.
|
||||
|
||||
```tsx
|
||||
const Demo = () => {
|
||||
const { isDarkMode } = useState();
|
||||
return <div>{isDarkMode ? '🌑' : '☀️'}</div>;
|
||||
};
|
||||
```
|
|
@ -0,0 +1,17 @@
|
|||
# `useTransitions` hook
|
||||
|
||||
Access [state transitions](../transitions.md) by `useTransitions` React hook.
|
||||
|
||||
```tsx
|
||||
const Demo = () => {
|
||||
const { isDarkMode } = useState();
|
||||
const { setDarkMode } = useTransitions();
|
||||
return (
|
||||
<>
|
||||
<div>{isDarkMode ? '🌑' : '☀️'}</div>
|
||||
<button onClick={() => setDarkMode(true)}>Go dark</button>
|
||||
<button onClick={() => setDarkMode(false)}>Go light</button>
|
||||
</>
|
||||
);
|
||||
};
|
||||
```
|
40
src/plugins/kibana_utils/docs/state_containers/redux.md
Normal file
40
src/plugins/kibana_utils/docs/state_containers/redux.md
Normal file
|
@ -0,0 +1,40 @@
|
|||
# Redux
|
||||
|
||||
State containers similar to Redux stores but without the boilerplate.
|
||||
|
||||
State containers expose Redux-like API:
|
||||
|
||||
```js
|
||||
container.getState()
|
||||
container.dispatch()
|
||||
container.replaceReducer()
|
||||
container.subscribe()
|
||||
container.addMiddleware()
|
||||
```
|
||||
|
||||
State containers have a reducer and every time you execute a state transition it
|
||||
actually dispatches an "action". For example, this
|
||||
|
||||
```js
|
||||
container.transitions.increment(25);
|
||||
```
|
||||
|
||||
is equivalent to
|
||||
|
||||
```js
|
||||
container.dispatch({
|
||||
type: 'increment',
|
||||
args: [25],
|
||||
});
|
||||
```
|
||||
|
||||
Because all transitions happen through `.dispatch()` interface, you can add middleware—similar how you
|
||||
would do with Redux—to monitor or intercept transitions.
|
||||
|
||||
For example, you can add `redux-logger` middleware to log in console all transitions happening with your store.
|
||||
|
||||
```js
|
||||
import logger from 'redux-logger';
|
||||
|
||||
container.addMiddleware(logger);
|
||||
```
|
|
@ -0,0 +1,61 @@
|
|||
# State transitions
|
||||
|
||||
*State transitions* describe possible state changes over time. Transitions are pure functions which
|
||||
receive `state` object and other—optional—arguments and must return a new `state` object back.
|
||||
|
||||
```ts
|
||||
type Transition = (state: State) => (...args) => State;
|
||||
```
|
||||
|
||||
Transitions must not mutate `state` object in-place, instead they must return a
|
||||
shallow copy of it, e.g. `{ ...state }`. Example:
|
||||
|
||||
```ts
|
||||
const setUiMode: PureTransition = state => uiMode => ({ ...state, uiMode });
|
||||
```
|
||||
|
||||
You provide transitions as a second argument when you create your state container.
|
||||
|
||||
```ts
|
||||
import { createStateContainer } from 'src/plugins/kibana_utils';
|
||||
|
||||
const container = createStateContainer(0, {
|
||||
increment: (cnt: number) => (by: number) => cnt + by,
|
||||
double: (cnt: number) => () => cnt * 2,
|
||||
});
|
||||
```
|
||||
|
||||
Now you can execute the transitions by calling them with only optional parameters (`state` is
|
||||
provided to your transitions automatically).
|
||||
|
||||
```ts
|
||||
container.transitions.increment(25);
|
||||
container.transitions.increment(5);
|
||||
container.state; // 30
|
||||
```
|
||||
|
||||
Your transitions are bound to the container so you can treat each of them as a
|
||||
standalone function for export.
|
||||
|
||||
```ts
|
||||
const defaultState = {
|
||||
uiMode: 'light',
|
||||
};
|
||||
|
||||
const container = createStateContainer(defaultState, {
|
||||
setUiMode: state => uiMode => ({ ...state, uiMode }),
|
||||
resetUiMode: state => () => ({ ...state, uiMode: defaultState.uiMode }),
|
||||
});
|
||||
|
||||
export const {
|
||||
setUiMode,
|
||||
resetUiMode
|
||||
} = container.transitions;
|
||||
```
|
||||
|
||||
You can add TypeScript annotations for your transitions as the second generic argument
|
||||
to `createStateContainer()` function.
|
||||
|
||||
```ts
|
||||
const container = createStateContainer<MyState, MyTransitions>(defaultState, pureTransitions);
|
||||
```
|
|
@ -1,9 +0,0 @@
|
|||
# State containers
|
||||
|
||||
- State containers for holding serializable state.
|
||||
- [Each plugin/app that needs runtime state will create a *store* using `store = createStore()`](./creation.md).
|
||||
- [*Store* can be updated using mutators `mutators = store.createMutators({ ... })`](./mutators.md).
|
||||
- [*Store* can be connected to React `{Provider, connect} = createContext(store)`](./react.md).
|
||||
- [In no-React setting *store* is consumed using `store.get()` and `store.state$`](./getters.md).
|
||||
- [Under-the-hood uses Redux `store.redux`](./redux.md) (but you should never need it explicitly).
|
||||
- [See idea doc with samples and rationale](https://docs.google.com/document/d/18eitHkcyKSsEHUfUIqFKChc8Pp62Z4gcRxdu903hbA0/edit#heading=h.iaxc9whxifl5).
|
|
@ -1,70 +0,0 @@
|
|||
# Mutators
|
||||
|
||||
State *mutators* are pure functions which receive `state` object and other—optional—arguments
|
||||
and must return a new `state` object back.
|
||||
|
||||
```ts
|
||||
type Mutator = (state: State) => (...args) => State;
|
||||
```
|
||||
|
||||
Mutator must not mutate `state` object in-place, instead it should return a
|
||||
shallow copy of it, e.g. `{ ...state }`.
|
||||
|
||||
```ts
|
||||
const setUiMode: Mutator = state => uiMode => ({ ...state, uiMode });
|
||||
```
|
||||
|
||||
You create mutators using `.createMutator(...)` method.
|
||||
|
||||
```ts
|
||||
const store = createStore({uiMode: 'light'});
|
||||
const mutators = store.createMutators({
|
||||
setUiMode: state => uiMode => ({ ...state, uiMode }),
|
||||
});
|
||||
```
|
||||
|
||||
Now you can use your mutators by calling them with only optional parameters (`state` is
|
||||
provided to your mutator automatically).
|
||||
|
||||
```ts
|
||||
mutators.setUiMode('dark');
|
||||
```
|
||||
|
||||
Your mutators are bound to the `store` so you can treat each of them as a
|
||||
standalone function for export.
|
||||
|
||||
```ts
|
||||
const { setUiMode, resetUiMode } = store.createMutators({
|
||||
setUiMode: state => uiMode => ({ ...state, uiMode }),
|
||||
resetUiMode: state => () => ({ ...state, uiMode: 'light' }),
|
||||
});
|
||||
|
||||
export {
|
||||
setUiMode,
|
||||
resetUiMode,
|
||||
};
|
||||
```
|
||||
|
||||
The mutators you create are also available on the `store` object.
|
||||
|
||||
```ts
|
||||
const store = createStore({ cnt: 0 });
|
||||
store.createMutators({
|
||||
add: state => value => ({ ...state, cnt: state.cnt + value }),
|
||||
});
|
||||
|
||||
store.mutators.add(5);
|
||||
store.get(); // { cnt: 5 }
|
||||
```
|
||||
|
||||
You can add TypeScript annotations to your `.mutators` property of `store` object.
|
||||
|
||||
```ts
|
||||
const store = createStore<{
|
||||
cnt: number;
|
||||
}, {
|
||||
add: (value: number) => void;
|
||||
}>({
|
||||
cnt: 0
|
||||
});
|
||||
```
|
|
@ -1,101 +0,0 @@
|
|||
# React
|
||||
|
||||
`createContext` factory allows you to easily use state containers with React.
|
||||
|
||||
```ts
|
||||
import { createStore, createContext } from 'kibana-utils';
|
||||
|
||||
const store = createStore({});
|
||||
const {
|
||||
Provider,
|
||||
Consumer,
|
||||
connect,
|
||||
context,
|
||||
useStore,
|
||||
useState,
|
||||
useMutators,
|
||||
useSelector,
|
||||
} = createContext(store);
|
||||
```
|
||||
|
||||
Wrap your app with `<Provider>`.
|
||||
|
||||
```tsx
|
||||
<Provider>
|
||||
<MyApplication />
|
||||
</Provider>
|
||||
```
|
||||
|
||||
Use `connect()()` higer-order-component to inject props from state into your component.
|
||||
|
||||
```tsx
|
||||
interface Props {
|
||||
name: string;
|
||||
punctuation: '.' | ',' | '!',
|
||||
}
|
||||
const Demo: React.FC<Props> = ({ name, punctuation }) =>
|
||||
<div>Hello, {name}{punctuation}</div>;
|
||||
|
||||
const store = createStore({ userName: 'John' });
|
||||
const { Provider, connect } = createContext(store);
|
||||
|
||||
const mapStateToProps = ({ userName }) => ({ name: userName });
|
||||
const DemoConnected = connect<Props, 'name'>(mapStateToProps)(Demo);
|
||||
|
||||
<Provider>
|
||||
<DemoConnected punctuation="!" />
|
||||
</Provider>
|
||||
```
|
||||
|
||||
`useStore` React hook will fetch the `store` object from the context.
|
||||
|
||||
```tsx
|
||||
const Demo = () => {
|
||||
const store = useStore();
|
||||
return <div>{store.get().isDarkMode ? '🌑' : '☀️'}</div>;
|
||||
};
|
||||
```
|
||||
|
||||
If you want your component to always re-render when the state changes use `useState` React hook.
|
||||
|
||||
```tsx
|
||||
const Demo = () => {
|
||||
const { isDarkMode } = useState();
|
||||
return <div>{isDarkMode ? '🌑' : '☀️'}</div>;
|
||||
};
|
||||
```
|
||||
|
||||
For `useSelector` React hook you specify a selector function, which will pick specific
|
||||
data from the state. *Your component will update only when that specific part of the state changes.*
|
||||
|
||||
```tsx
|
||||
const selector = state => state.isDarkMode;
|
||||
const Demo = () => {
|
||||
const isDarkMode = useSelector(selector);
|
||||
return <div>{isDarkMode ? '🌑' : '☀️'}</div>;
|
||||
};
|
||||
```
|
||||
|
||||
As an optional second argument for `useSelector` you can provide a `comparator` function, which
|
||||
compares currently selected value with the previous and your component will re-render only if
|
||||
`comparator` returns `true`. By default, it simply uses tripple equals `===` comparison.
|
||||
|
||||
```
|
||||
useSelector(selector, comparator?)
|
||||
```
|
||||
|
||||
Access state mutators by `useMutators` React hook.
|
||||
|
||||
```tsx
|
||||
const Demo = () => {
|
||||
const { isDarkMode } = useState();
|
||||
const { setDarkMode } = useMutators();
|
||||
return (
|
||||
<>
|
||||
<div>{isDarkMode ? '🌑' : '☀️'}</div>
|
||||
<button onClick={() => setDarkMode(true)}>Go dark</button>
|
||||
<button onClick={() => setDarkMode(false)}>Go light</button>
|
||||
</>
|
||||
);
|
||||
};
|
||||
```
|
|
@ -1,19 +0,0 @@
|
|||
# Redux
|
||||
|
||||
Internally `createStore()` uses Redux to manage the state. When you call `store.get()`
|
||||
it is actually calling the Redux `.getState()` method. When you execute a mutation
|
||||
it is actually dispatching a Redux action.
|
||||
|
||||
You can access Redux *store* using `.redux`.
|
||||
|
||||
```ts
|
||||
store.redux;
|
||||
```
|
||||
|
||||
But you should never need it, if you think you do, consult with Kibana App Architecture team.
|
||||
|
||||
We use Redux internally for 3 main reasons:
|
||||
|
||||
- We can reuse `react-redux` library to easily connect state containers to React.
|
||||
- We can reuse Redux devtools.
|
||||
- We can reuse battle-tested Redux library and action/reducer paradigm.
|
|
@ -17,9 +17,9 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
import { createStore, createContext } from '.';
|
||||
import { createStateContainer, createStateContainerReactHelpers } from '.';
|
||||
|
||||
test('exports store methods', () => {
|
||||
expect(typeof createStore).toBe('function');
|
||||
expect(typeof createContext).toBe('function');
|
||||
expect(typeof createStateContainer).toBe('function');
|
||||
expect(typeof createStateContainerReactHelpers).toBe('function');
|
||||
});
|
||||
|
|
|
@ -19,13 +19,12 @@
|
|||
|
||||
export * from './core';
|
||||
export * from './errors';
|
||||
export * from './store';
|
||||
export * from './parse';
|
||||
export * from './resize_checker';
|
||||
export * from './render_complete';
|
||||
export * from './store';
|
||||
export * from './errors';
|
||||
export * from './field_mapping';
|
||||
export * from './parse';
|
||||
export * from './render_complete';
|
||||
export * from './resize_checker';
|
||||
export * from './state_containers';
|
||||
export * from './storage';
|
||||
export * from './storage/hashed_item_store';
|
||||
export * from './state_management/state_hash';
|
||||
|
|
|
@ -0,0 +1,303 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { createStateContainer } from './create_state_container';
|
||||
|
||||
const create = <S, T extends object>(state: S, transitions: T = {} as T) => {
|
||||
const pureTransitions = {
|
||||
set: () => (newState: S) => newState,
|
||||
...transitions,
|
||||
};
|
||||
const store = createStateContainer<typeof state, typeof pureTransitions>(state, pureTransitions);
|
||||
return { store, mutators: store.transitions };
|
||||
};
|
||||
|
||||
test('can create store', () => {
|
||||
const { store } = create({});
|
||||
expect(store).toMatchObject({
|
||||
getState: expect.any(Function),
|
||||
state$: expect.any(Object),
|
||||
transitions: expect.any(Object),
|
||||
dispatch: expect.any(Function),
|
||||
subscribe: expect.any(Function),
|
||||
replaceReducer: expect.any(Function),
|
||||
addMiddleware: expect.any(Function),
|
||||
});
|
||||
});
|
||||
|
||||
test('can set default state', () => {
|
||||
const defaultState = {
|
||||
foo: 'bar',
|
||||
};
|
||||
const { store } = create(defaultState);
|
||||
expect(store.get()).toEqual(defaultState);
|
||||
expect(store.getState()).toEqual(defaultState);
|
||||
});
|
||||
|
||||
test('can set state', () => {
|
||||
const defaultState = {
|
||||
foo: 'bar',
|
||||
};
|
||||
const newState = {
|
||||
foo: 'baz',
|
||||
};
|
||||
const { store, mutators } = create(defaultState);
|
||||
|
||||
mutators.set(newState);
|
||||
|
||||
expect(store.get()).toEqual(newState);
|
||||
expect(store.getState()).toEqual(newState);
|
||||
});
|
||||
|
||||
test('does not shallow merge states', () => {
|
||||
const defaultState = {
|
||||
foo: 'bar',
|
||||
};
|
||||
const newState = {
|
||||
foo2: 'baz',
|
||||
};
|
||||
const { store, mutators } = create(defaultState);
|
||||
|
||||
mutators.set(newState as any);
|
||||
|
||||
expect(store.get()).toEqual(newState);
|
||||
expect(store.getState()).toEqual(newState);
|
||||
});
|
||||
|
||||
test('can subscribe and unsubscribe to state changes', () => {
|
||||
const { store, mutators } = create({});
|
||||
const spy = jest.fn();
|
||||
const subscription = store.state$.subscribe(spy);
|
||||
mutators.set({ a: 1 });
|
||||
mutators.set({ a: 2 });
|
||||
subscription.unsubscribe();
|
||||
mutators.set({ a: 3 });
|
||||
|
||||
expect(spy).toHaveBeenCalledTimes(2);
|
||||
expect(spy.mock.calls[0][0]).toEqual({ a: 1 });
|
||||
expect(spy.mock.calls[1][0]).toEqual({ a: 2 });
|
||||
});
|
||||
|
||||
test('multiple subscribers can subscribe', () => {
|
||||
const { store, mutators } = create({});
|
||||
const spy1 = jest.fn();
|
||||
const spy2 = jest.fn();
|
||||
const subscription1 = store.state$.subscribe(spy1);
|
||||
const subscription2 = store.state$.subscribe(spy2);
|
||||
mutators.set({ a: 1 });
|
||||
subscription1.unsubscribe();
|
||||
mutators.set({ a: 2 });
|
||||
subscription2.unsubscribe();
|
||||
mutators.set({ a: 3 });
|
||||
|
||||
expect(spy1).toHaveBeenCalledTimes(1);
|
||||
expect(spy2).toHaveBeenCalledTimes(2);
|
||||
expect(spy1.mock.calls[0][0]).toEqual({ a: 1 });
|
||||
expect(spy2.mock.calls[0][0]).toEqual({ a: 1 });
|
||||
expect(spy2.mock.calls[1][0]).toEqual({ a: 2 });
|
||||
});
|
||||
|
||||
test('creates impure mutators from pure mutators', () => {
|
||||
const { mutators } = create(
|
||||
{},
|
||||
{
|
||||
setFoo: () => (bar: any) => ({ foo: bar }),
|
||||
}
|
||||
);
|
||||
|
||||
expect(typeof mutators.setFoo).toBe('function');
|
||||
});
|
||||
|
||||
test('mutators can update state', () => {
|
||||
const { store, mutators } = create(
|
||||
{
|
||||
value: 0,
|
||||
foo: 'bar',
|
||||
},
|
||||
{
|
||||
add: (state: any) => (increment: any) => ({ ...state, value: state.value + increment }),
|
||||
setFoo: (state: any) => (bar: any) => ({ ...state, foo: bar }),
|
||||
}
|
||||
);
|
||||
|
||||
expect(store.get()).toEqual({
|
||||
value: 0,
|
||||
foo: 'bar',
|
||||
});
|
||||
|
||||
mutators.add(11);
|
||||
mutators.setFoo('baz');
|
||||
|
||||
expect(store.get()).toEqual({
|
||||
value: 11,
|
||||
foo: 'baz',
|
||||
});
|
||||
|
||||
mutators.add(-20);
|
||||
mutators.setFoo('bazooka');
|
||||
|
||||
expect(store.get()).toEqual({
|
||||
value: -9,
|
||||
foo: 'bazooka',
|
||||
});
|
||||
});
|
||||
|
||||
test('mutators methods are not bound', () => {
|
||||
const { store, mutators } = create(
|
||||
{ value: -3 },
|
||||
{
|
||||
add: (state: { value: number }) => (increment: number) => ({
|
||||
...state,
|
||||
value: state.value + increment,
|
||||
}),
|
||||
}
|
||||
);
|
||||
|
||||
expect(store.get()).toEqual({ value: -3 });
|
||||
mutators.add(4);
|
||||
expect(store.get()).toEqual({ value: 1 });
|
||||
});
|
||||
|
||||
test('created mutators are saved in store object', () => {
|
||||
const { store, mutators } = create(
|
||||
{ value: -3 },
|
||||
{
|
||||
add: (state: { value: number }) => (increment: number) => ({
|
||||
...state,
|
||||
value: state.value + increment,
|
||||
}),
|
||||
}
|
||||
);
|
||||
|
||||
expect(typeof store.transitions.add).toBe('function');
|
||||
mutators.add(5);
|
||||
expect(store.get()).toEqual({ value: 2 });
|
||||
});
|
||||
|
||||
test('throws when state is modified inline - 1', () => {
|
||||
const container = createStateContainer({ a: 'b' }, {});
|
||||
|
||||
let error: TypeError | null = null;
|
||||
try {
|
||||
(container.get().a as any) = 'c';
|
||||
} catch (err) {
|
||||
error = err;
|
||||
}
|
||||
|
||||
expect(error).toBeInstanceOf(TypeError);
|
||||
});
|
||||
|
||||
test('throws when state is modified inline - 2', () => {
|
||||
const container = createStateContainer({ a: 'b' }, {});
|
||||
|
||||
let error: TypeError | null = null;
|
||||
try {
|
||||
(container.getState().a as any) = 'c';
|
||||
} catch (err) {
|
||||
error = err;
|
||||
}
|
||||
|
||||
expect(error).toBeInstanceOf(TypeError);
|
||||
});
|
||||
|
||||
test('throws when state is modified inline in subscription', done => {
|
||||
const container = createStateContainer({ a: 'b' }, { set: () => (newState: any) => newState });
|
||||
|
||||
container.subscribe(value => {
|
||||
let error: TypeError | null = null;
|
||||
try {
|
||||
(value.a as any) = 'd';
|
||||
} catch (err) {
|
||||
error = err;
|
||||
}
|
||||
expect(error).toBeInstanceOf(TypeError);
|
||||
done();
|
||||
});
|
||||
container.transitions.set({ a: 'c' });
|
||||
});
|
||||
|
||||
describe('selectors', () => {
|
||||
test('can specify no selectors, or can skip them', () => {
|
||||
createStateContainer({}, {});
|
||||
createStateContainer({}, {}, {});
|
||||
});
|
||||
|
||||
test('selector object is available on .selectors key', () => {
|
||||
const container1 = createStateContainer({}, {}, {});
|
||||
const container2 = createStateContainer({}, {}, { foo: () => () => 123 });
|
||||
const container3 = createStateContainer({}, {}, { bar: () => () => 1, baz: () => () => 1 });
|
||||
|
||||
expect(Object.keys(container1.selectors).sort()).toEqual([]);
|
||||
expect(Object.keys(container2.selectors).sort()).toEqual(['foo']);
|
||||
expect(Object.keys(container3.selectors).sort()).toEqual(['bar', 'baz']);
|
||||
});
|
||||
|
||||
test('selector without arguments returns correct state slice', () => {
|
||||
const container = createStateContainer(
|
||||
{ name: 'Oleg' },
|
||||
{
|
||||
changeName: (state: { name: string }) => (name: string) => ({ ...state, name }),
|
||||
},
|
||||
{ getName: (state: { name: string }) => () => state.name }
|
||||
);
|
||||
|
||||
expect(container.selectors.getName()).toBe('Oleg');
|
||||
container.transitions.changeName('Britney');
|
||||
expect(container.selectors.getName()).toBe('Britney');
|
||||
});
|
||||
|
||||
test('selector can accept an argument', () => {
|
||||
const container = createStateContainer(
|
||||
{
|
||||
users: {
|
||||
1: {
|
||||
name: 'Darth',
|
||||
},
|
||||
},
|
||||
},
|
||||
{},
|
||||
{
|
||||
getUser: (state: any) => (id: number) => state.users[id],
|
||||
}
|
||||
);
|
||||
|
||||
expect(container.selectors.getUser(1)).toEqual({ name: 'Darth' });
|
||||
expect(container.selectors.getUser(2)).toBe(undefined);
|
||||
});
|
||||
|
||||
test('selector can accept multiple arguments', () => {
|
||||
const container = createStateContainer(
|
||||
{
|
||||
users: {
|
||||
5: {
|
||||
name: 'Darth',
|
||||
surname: 'Vader',
|
||||
},
|
||||
},
|
||||
},
|
||||
{},
|
||||
{
|
||||
getName: (state: any) => (id: number, which: 'name' | 'surname') => state.users[id][which],
|
||||
}
|
||||
);
|
||||
|
||||
expect(container.selectors.getName(5, 'name')).toEqual('Darth');
|
||||
expect(container.selectors.getName(5, 'surname')).toEqual('Vader');
|
||||
});
|
||||
});
|
|
@ -0,0 +1,89 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
import { skip } from 'rxjs/operators';
|
||||
import { RecursiveReadonly } from '@kbn/utility-types';
|
||||
import {
|
||||
PureTransitionsToTransitions,
|
||||
PureTransition,
|
||||
ReduxLikeStateContainer,
|
||||
PureSelectorsToSelectors,
|
||||
} from './types';
|
||||
|
||||
const $$observable = (typeof Symbol === 'function' && (Symbol as any).observable) || '@@observable';
|
||||
|
||||
const freeze: <T>(value: T) => RecursiveReadonly<T> =
|
||||
process.env.NODE_ENV !== 'production'
|
||||
? <T>(value: T): RecursiveReadonly<T> => {
|
||||
if (!value) return value as RecursiveReadonly<T>;
|
||||
if (value instanceof Array) return value as RecursiveReadonly<T>;
|
||||
if (typeof value === 'object') return Object.freeze({ ...value }) as RecursiveReadonly<T>;
|
||||
else return value as RecursiveReadonly<T>;
|
||||
}
|
||||
: <T>(value: T) => value as RecursiveReadonly<T>;
|
||||
|
||||
export const createStateContainer = <
|
||||
State,
|
||||
PureTransitions extends object,
|
||||
PureSelectors extends object = {}
|
||||
>(
|
||||
defaultState: State,
|
||||
pureTransitions: PureTransitions,
|
||||
pureSelectors: PureSelectors = {} as PureSelectors
|
||||
): ReduxLikeStateContainer<State, PureTransitions, PureSelectors> => {
|
||||
const data$ = new BehaviorSubject<RecursiveReadonly<State>>(freeze(defaultState));
|
||||
const state$ = data$.pipe(skip(1));
|
||||
const get = () => data$.getValue();
|
||||
const container: ReduxLikeStateContainer<State, PureTransitions, PureSelectors> = {
|
||||
get,
|
||||
state$,
|
||||
getState: () => data$.getValue(),
|
||||
set: (state: State) => {
|
||||
data$.next(freeze(state));
|
||||
},
|
||||
reducer: (state, action) => {
|
||||
const pureTransition = (pureTransitions as Record<string, PureTransition<State, any[]>>)[
|
||||
action.type
|
||||
];
|
||||
return pureTransition ? freeze(pureTransition(state)(...action.args)) : state;
|
||||
},
|
||||
replaceReducer: nextReducer => (container.reducer = nextReducer),
|
||||
dispatch: action => data$.next(container.reducer(get(), action)),
|
||||
transitions: Object.keys(pureTransitions).reduce<PureTransitionsToTransitions<PureTransitions>>(
|
||||
(acc, type) => ({ ...acc, [type]: (...args: any) => container.dispatch({ type, args }) }),
|
||||
{} as PureTransitionsToTransitions<PureTransitions>
|
||||
),
|
||||
selectors: Object.keys(pureSelectors).reduce<PureSelectorsToSelectors<PureSelectors>>(
|
||||
(acc, selector) => ({
|
||||
...acc,
|
||||
[selector]: (...args: any) => (pureSelectors as any)[selector](get())(...args),
|
||||
}),
|
||||
{} as PureSelectorsToSelectors<PureSelectors>
|
||||
),
|
||||
addMiddleware: middleware =>
|
||||
(container.dispatch = middleware(container as any)(container.dispatch)),
|
||||
subscribe: (listener: (state: RecursiveReadonly<State>) => void) => {
|
||||
const subscription = state$.subscribe(listener);
|
||||
return () => subscription.unsubscribe();
|
||||
},
|
||||
[$$observable]: state$,
|
||||
};
|
||||
return container;
|
||||
};
|
|
@ -20,8 +20,17 @@
|
|||
import * as React from 'react';
|
||||
import * as ReactDOM from 'react-dom';
|
||||
import { act, Simulate } from 'react-dom/test-utils';
|
||||
import { createStore } from './create_store';
|
||||
import { createContext } from './react';
|
||||
import { createStateContainer } from './create_state_container';
|
||||
import { createStateContainerReactHelpers } from './create_state_container_react_helpers';
|
||||
|
||||
const create = <S, T extends object>(state: S, transitions: T = {} as T) => {
|
||||
const pureTransitions = {
|
||||
set: () => (newState: S) => newState,
|
||||
...transitions,
|
||||
};
|
||||
const store = createStateContainer<typeof state, typeof pureTransitions>(state, pureTransitions);
|
||||
return { store, mutators: store.transitions };
|
||||
};
|
||||
|
||||
let container: HTMLDivElement | null;
|
||||
|
||||
|
@ -36,27 +45,23 @@ afterEach(() => {
|
|||
});
|
||||
|
||||
test('can create React context', () => {
|
||||
const store = createStore({ foo: 'bar' });
|
||||
const context = createContext(store);
|
||||
const context = createStateContainerReactHelpers();
|
||||
|
||||
expect(context).toMatchObject({
|
||||
Provider: expect.any(Function),
|
||||
Consumer: expect.any(Function),
|
||||
Provider: expect.any(Object),
|
||||
Consumer: expect.any(Object),
|
||||
connect: expect.any(Function),
|
||||
context: {
|
||||
Provider: expect.any(Object),
|
||||
Consumer: expect.any(Object),
|
||||
},
|
||||
context: expect.any(Object),
|
||||
});
|
||||
});
|
||||
|
||||
test('<Provider> passes state to <Consumer>', () => {
|
||||
const store = createStore({ hello: 'world' });
|
||||
const { Provider, Consumer } = createContext(store);
|
||||
const { store } = create({ hello: 'world' });
|
||||
const { Provider, Consumer } = createStateContainerReactHelpers<typeof store>();
|
||||
|
||||
ReactDOM.render(
|
||||
<Provider>
|
||||
<Consumer>{({ hello }) => hello}</Consumer>
|
||||
<Provider value={store}>
|
||||
<Consumer>{(s: typeof store) => s.get().hello}</Consumer>
|
||||
</Provider>,
|
||||
container
|
||||
);
|
||||
|
@ -74,8 +79,8 @@ interface Props1 {
|
|||
}
|
||||
|
||||
test('<Provider> passes state to connect()()', () => {
|
||||
const store = createStore<State1>({ hello: 'Bob' });
|
||||
const { Provider, connect } = createContext(store);
|
||||
const { store } = create({ hello: 'Bob' });
|
||||
const { Provider, connect } = createStateContainerReactHelpers();
|
||||
|
||||
const Demo: React.FC<Props1> = ({ message, stop }) => (
|
||||
<>
|
||||
|
@ -87,7 +92,7 @@ test('<Provider> passes state to connect()()', () => {
|
|||
const DemoConnected = connect<Props1, 'message'>(mergeProps)(Demo);
|
||||
|
||||
ReactDOM.render(
|
||||
<Provider>
|
||||
<Provider value={store}>
|
||||
<DemoConnected stop="?" />
|
||||
</Provider>,
|
||||
container
|
||||
|
@ -97,13 +102,13 @@ test('<Provider> passes state to connect()()', () => {
|
|||
});
|
||||
|
||||
test('context receives Redux store', () => {
|
||||
const store = createStore({ foo: 'bar' });
|
||||
const { Provider, context } = createContext(store);
|
||||
const { store } = create({ foo: 'bar' });
|
||||
const { Provider, context } = createStateContainerReactHelpers<typeof store>();
|
||||
|
||||
ReactDOM.render(
|
||||
/* eslint-disable no-shadow */
|
||||
<Provider>
|
||||
<context.Consumer>{({ store }) => store.getState().foo}</context.Consumer>
|
||||
<Provider value={store}>
|
||||
<context.Consumer>{store => store.get().foo}</context.Consumer>
|
||||
</Provider>,
|
||||
/* eslint-enable no-shadow */
|
||||
container
|
||||
|
@ -117,16 +122,16 @@ xtest('can use multiple stores in one React app', () => {});
|
|||
describe('hooks', () => {
|
||||
describe('useStore', () => {
|
||||
test('can select store using useStore hook', () => {
|
||||
const store = createStore({ foo: 'bar' });
|
||||
const { Provider, useStore } = createContext(store);
|
||||
const { store } = create({ foo: 'bar' });
|
||||
const { Provider, useContainer } = createStateContainerReactHelpers<typeof store>();
|
||||
const Demo: React.FC<{}> = () => {
|
||||
// eslint-disable-next-line no-shadow
|
||||
const store = useStore();
|
||||
const store = useContainer();
|
||||
return <>{store.get().foo}</>;
|
||||
};
|
||||
|
||||
ReactDOM.render(
|
||||
<Provider>
|
||||
<Provider value={store}>
|
||||
<Demo />
|
||||
</Provider>,
|
||||
container
|
||||
|
@ -138,15 +143,15 @@ describe('hooks', () => {
|
|||
|
||||
describe('useState', () => {
|
||||
test('can select state using useState hook', () => {
|
||||
const store = createStore({ foo: 'qux' });
|
||||
const { Provider, useState } = createContext(store);
|
||||
const { store } = create({ foo: 'qux' });
|
||||
const { Provider, useState } = createStateContainerReactHelpers<typeof store>();
|
||||
const Demo: React.FC<{}> = () => {
|
||||
const { foo } = useState();
|
||||
return <>{foo}</>;
|
||||
};
|
||||
|
||||
ReactDOM.render(
|
||||
<Provider>
|
||||
<Provider value={store}>
|
||||
<Demo />
|
||||
</Provider>,
|
||||
container
|
||||
|
@ -156,18 +161,23 @@ describe('hooks', () => {
|
|||
});
|
||||
|
||||
test('re-renders when state changes', () => {
|
||||
const store = createStore({ foo: 'bar' });
|
||||
const { setFoo } = store.createMutators({
|
||||
setFoo: state => foo => ({ ...state, foo }),
|
||||
});
|
||||
const { Provider, useState } = createContext(store);
|
||||
const {
|
||||
store,
|
||||
mutators: { setFoo },
|
||||
} = create(
|
||||
{ foo: 'bar' },
|
||||
{
|
||||
setFoo: (state: { foo: string }) => (foo: string) => ({ ...state, foo }),
|
||||
}
|
||||
);
|
||||
const { Provider, useState } = createStateContainerReactHelpers<typeof store>();
|
||||
const Demo: React.FC<{}> = () => {
|
||||
const { foo } = useState();
|
||||
return <>{foo}</>;
|
||||
};
|
||||
|
||||
ReactDOM.render(
|
||||
<Provider>
|
||||
<Provider value={store}>
|
||||
<Demo />
|
||||
</Provider>,
|
||||
container
|
||||
|
@ -181,26 +191,31 @@ describe('hooks', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('useMutations', () => {
|
||||
test('useMutations hook returns mutations that can update state', () => {
|
||||
const store = createStore<
|
||||
describe('useTransitions', () => {
|
||||
test('useTransitions hook returns mutations that can update state', () => {
|
||||
const { store } = create<
|
||||
{
|
||||
cnt: number;
|
||||
},
|
||||
any
|
||||
>(
|
||||
{
|
||||
increment: (value: number) => void;
|
||||
cnt: 0,
|
||||
},
|
||||
{
|
||||
increment: (state: { cnt: number }) => (value: number) => ({
|
||||
...state,
|
||||
cnt: state.cnt + value,
|
||||
}),
|
||||
}
|
||||
>({
|
||||
cnt: 0,
|
||||
});
|
||||
store.createMutators({
|
||||
increment: state => value => ({ ...state, cnt: state.cnt + value }),
|
||||
});
|
||||
);
|
||||
|
||||
const { Provider, useState, useMutators } = createContext(store);
|
||||
const { Provider, useState, useTransitions } = createStateContainerReactHelpers<
|
||||
typeof store
|
||||
>();
|
||||
const Demo: React.FC<{}> = () => {
|
||||
const { cnt } = useState();
|
||||
const { increment } = useMutators();
|
||||
const { increment } = useTransitions();
|
||||
return (
|
||||
<>
|
||||
<strong>{cnt}</strong>
|
||||
|
@ -210,7 +225,7 @@ describe('hooks', () => {
|
|||
};
|
||||
|
||||
ReactDOM.render(
|
||||
<Provider>
|
||||
<Provider value={store}>
|
||||
<Demo />
|
||||
</Provider>,
|
||||
container
|
||||
|
@ -230,7 +245,7 @@ describe('hooks', () => {
|
|||
|
||||
describe('useSelector', () => {
|
||||
test('can select deeply nested value', () => {
|
||||
const store = createStore({
|
||||
const { store } = create({
|
||||
foo: {
|
||||
bar: {
|
||||
baz: 'qux',
|
||||
|
@ -238,14 +253,14 @@ describe('hooks', () => {
|
|||
},
|
||||
});
|
||||
const selector = (state: { foo: { bar: { baz: string } } }) => state.foo.bar.baz;
|
||||
const { Provider, useSelector } = createContext(store);
|
||||
const { Provider, useSelector } = createStateContainerReactHelpers<typeof store>();
|
||||
const Demo: React.FC<{}> = () => {
|
||||
const value = useSelector(selector);
|
||||
return <>{value}</>;
|
||||
};
|
||||
|
||||
ReactDOM.render(
|
||||
<Provider>
|
||||
<Provider value={store}>
|
||||
<Demo />
|
||||
</Provider>,
|
||||
container
|
||||
|
@ -255,7 +270,7 @@ describe('hooks', () => {
|
|||
});
|
||||
|
||||
test('re-renders when state changes', () => {
|
||||
const store = createStore({
|
||||
const { store, mutators } = create({
|
||||
foo: {
|
||||
bar: {
|
||||
baz: 'qux',
|
||||
|
@ -263,14 +278,14 @@ describe('hooks', () => {
|
|||
},
|
||||
});
|
||||
const selector = (state: { foo: { bar: { baz: string } } }) => state.foo.bar.baz;
|
||||
const { Provider, useSelector } = createContext(store);
|
||||
const { Provider, useSelector } = createStateContainerReactHelpers();
|
||||
const Demo: React.FC<{}> = () => {
|
||||
const value = useSelector(selector);
|
||||
return <>{value}</>;
|
||||
};
|
||||
|
||||
ReactDOM.render(
|
||||
<Provider>
|
||||
<Provider value={store}>
|
||||
<Demo />
|
||||
</Provider>,
|
||||
container
|
||||
|
@ -278,7 +293,7 @@ describe('hooks', () => {
|
|||
|
||||
expect(container!.innerHTML).toBe('qux');
|
||||
act(() => {
|
||||
store.set({
|
||||
mutators.set({
|
||||
foo: {
|
||||
bar: {
|
||||
baz: 'quux',
|
||||
|
@ -290,9 +305,9 @@ describe('hooks', () => {
|
|||
});
|
||||
|
||||
test("re-renders only when selector's result changes", async () => {
|
||||
const store = createStore({ a: 'b', foo: 'bar' });
|
||||
const { store, mutators } = create({ a: 'b', foo: 'bar' });
|
||||
const selector = (state: { foo: string }) => state.foo;
|
||||
const { Provider, useSelector } = createContext(store);
|
||||
const { Provider, useSelector } = createStateContainerReactHelpers<typeof store>();
|
||||
|
||||
let cnt = 0;
|
||||
const Demo: React.FC<{}> = () => {
|
||||
|
@ -301,7 +316,7 @@ describe('hooks', () => {
|
|||
return <>{value}</>;
|
||||
};
|
||||
ReactDOM.render(
|
||||
<Provider>
|
||||
<Provider value={store}>
|
||||
<Demo />
|
||||
</Provider>,
|
||||
container
|
||||
|
@ -311,24 +326,24 @@ describe('hooks', () => {
|
|||
expect(cnt).toBe(1);
|
||||
|
||||
act(() => {
|
||||
store.set({ a: 'c', foo: 'bar' });
|
||||
mutators.set({ a: 'c', foo: 'bar' });
|
||||
});
|
||||
|
||||
await new Promise(r => setTimeout(r, 1));
|
||||
expect(cnt).toBe(1);
|
||||
|
||||
act(() => {
|
||||
store.set({ a: 'd', foo: 'bar 2' });
|
||||
mutators.set({ a: 'd', foo: 'bar 2' });
|
||||
});
|
||||
|
||||
await new Promise(r => setTimeout(r, 1));
|
||||
expect(cnt).toBe(2);
|
||||
});
|
||||
|
||||
test('re-renders on same shape object', async () => {
|
||||
const store = createStore({ foo: { bar: 'baz' } });
|
||||
test('does not re-render on same shape object', async () => {
|
||||
const { store, mutators } = create({ foo: { bar: 'baz' } });
|
||||
const selector = (state: { foo: any }) => state.foo;
|
||||
const { Provider, useSelector } = createContext(store);
|
||||
const { Provider, useSelector } = createStateContainerReactHelpers<typeof store>();
|
||||
|
||||
let cnt = 0;
|
||||
const Demo: React.FC<{}> = () => {
|
||||
|
@ -337,7 +352,7 @@ describe('hooks', () => {
|
|||
return <>{JSON.stringify(value)}</>;
|
||||
};
|
||||
ReactDOM.render(
|
||||
<Provider>
|
||||
<Provider value={store}>
|
||||
<Demo />
|
||||
</Provider>,
|
||||
container
|
||||
|
@ -347,7 +362,14 @@ describe('hooks', () => {
|
|||
expect(cnt).toBe(1);
|
||||
|
||||
act(() => {
|
||||
store.set({ foo: { bar: 'baz' } });
|
||||
mutators.set({ foo: { bar: 'baz' } });
|
||||
});
|
||||
|
||||
await new Promise(r => setTimeout(r, 1));
|
||||
expect(cnt).toBe(1);
|
||||
|
||||
act(() => {
|
||||
mutators.set({ foo: { bar: 'qux' } });
|
||||
});
|
||||
|
||||
await new Promise(r => setTimeout(r, 1));
|
||||
|
@ -355,10 +377,15 @@ describe('hooks', () => {
|
|||
});
|
||||
|
||||
test('can set custom comparator function to prevent re-renders on deep equality', async () => {
|
||||
const store = createStore({ foo: { bar: 'baz' } });
|
||||
const { store, mutators } = create(
|
||||
{ foo: { bar: 'baz' } },
|
||||
{
|
||||
set: () => (newState: { foo: { bar: string } }) => newState,
|
||||
}
|
||||
);
|
||||
const selector = (state: { foo: any }) => state.foo;
|
||||
const comparator = (prev: any, curr: any) => JSON.stringify(prev) === JSON.stringify(curr);
|
||||
const { Provider, useSelector } = createContext(store);
|
||||
const { Provider, useSelector } = createStateContainerReactHelpers<typeof store>();
|
||||
|
||||
let cnt = 0;
|
||||
const Demo: React.FC<{}> = () => {
|
||||
|
@ -367,7 +394,7 @@ describe('hooks', () => {
|
|||
return <>{JSON.stringify(value)}</>;
|
||||
};
|
||||
ReactDOM.render(
|
||||
<Provider>
|
||||
<Provider value={store}>
|
||||
<Demo />
|
||||
</Provider>,
|
||||
container
|
||||
|
@ -377,7 +404,7 @@ describe('hooks', () => {
|
|||
expect(cnt).toBe(1);
|
||||
|
||||
act(() => {
|
||||
store.set({ foo: { bar: 'baz' } });
|
||||
mutators.set({ foo: { bar: 'baz' } });
|
||||
});
|
||||
|
||||
await new Promise(r => setTimeout(r, 1));
|
|
@ -0,0 +1,77 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import * as React from 'react';
|
||||
import useObservable from 'react-use/lib/useObservable';
|
||||
import defaultComparator from 'fast-deep-equal';
|
||||
import { Comparator, Connect, StateContainer, UnboxState } from './types';
|
||||
|
||||
const { useContext, useLayoutEffect, useRef, createElement: h } = React;
|
||||
|
||||
export const createStateContainerReactHelpers = <Container extends StateContainer<any, any>>() => {
|
||||
const context = React.createContext<Container>(null as any);
|
||||
|
||||
const useContainer = (): Container => useContext(context);
|
||||
|
||||
const useState = (): UnboxState<Container> => {
|
||||
const { state$, get } = useContainer();
|
||||
const value = useObservable(state$, get());
|
||||
return value;
|
||||
};
|
||||
|
||||
const useTransitions = () => useContainer().transitions;
|
||||
|
||||
const useSelector = <Result>(
|
||||
selector: (state: UnboxState<Container>) => Result,
|
||||
comparator: Comparator<Result> = defaultComparator
|
||||
): Result => {
|
||||
const { state$, get } = useContainer();
|
||||
const lastValueRef = useRef<Result>(get());
|
||||
const [value, setValue] = React.useState<Result>(() => {
|
||||
const newValue = selector(get());
|
||||
lastValueRef.current = newValue;
|
||||
return newValue;
|
||||
});
|
||||
useLayoutEffect(() => {
|
||||
const subscription = state$.subscribe((currentState: UnboxState<Container>) => {
|
||||
const newValue = selector(currentState);
|
||||
if (!comparator(lastValueRef.current, newValue)) {
|
||||
lastValueRef.current = newValue;
|
||||
setValue(newValue);
|
||||
}
|
||||
});
|
||||
return () => subscription.unsubscribe();
|
||||
}, [state$, comparator]);
|
||||
return value;
|
||||
};
|
||||
|
||||
const connect: Connect<UnboxState<Container>> = mapStateToProp => component => props =>
|
||||
h(component, { ...useSelector(mapStateToProp), ...props } as any);
|
||||
|
||||
return {
|
||||
Provider: context.Provider,
|
||||
Consumer: context.Consumer,
|
||||
context,
|
||||
useContainer,
|
||||
useState,
|
||||
useTransitions,
|
||||
useSelector,
|
||||
connect,
|
||||
};
|
||||
};
|
|
@ -17,5 +17,6 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
export * from './create_store';
|
||||
export * from './react';
|
||||
export * from './types';
|
||||
export * from './create_state_container';
|
||||
export * from './create_state_container_react_helpers';
|
99
src/plugins/kibana_utils/public/state_containers/types.ts
Normal file
99
src/plugins/kibana_utils/public/state_containers/types.ts
Normal file
|
@ -0,0 +1,99 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { Observable } from 'rxjs';
|
||||
import { Ensure, RecursiveReadonly } from '@kbn/utility-types';
|
||||
|
||||
export interface TransitionDescription<Type extends string = string, Args extends any[] = any[]> {
|
||||
type: Type;
|
||||
args: Args;
|
||||
}
|
||||
export type Transition<State, Args extends any[]> = (...args: Args) => State;
|
||||
export type PureTransition<State, Args extends any[]> = (
|
||||
state: RecursiveReadonly<State>
|
||||
) => Transition<State, Args>;
|
||||
export type EnsurePureTransition<T> = Ensure<T, PureTransition<any, any>>;
|
||||
export type PureTransitionToTransition<T extends PureTransition<any, any>> = ReturnType<T>;
|
||||
export type PureTransitionsToTransitions<T extends object> = {
|
||||
[K in keyof T]: PureTransitionToTransition<EnsurePureTransition<T[K]>>;
|
||||
};
|
||||
|
||||
export interface BaseStateContainer<State> {
|
||||
get: () => RecursiveReadonly<State>;
|
||||
set: (state: State) => void;
|
||||
state$: Observable<RecursiveReadonly<State>>;
|
||||
}
|
||||
|
||||
export interface StateContainer<
|
||||
State,
|
||||
PureTransitions extends object,
|
||||
PureSelectors extends object = {}
|
||||
> extends BaseStateContainer<State> {
|
||||
transitions: Readonly<PureTransitionsToTransitions<PureTransitions>>;
|
||||
selectors: Readonly<PureSelectorsToSelectors<PureSelectors>>;
|
||||
}
|
||||
|
||||
export interface ReduxLikeStateContainer<
|
||||
State,
|
||||
PureTransitions extends object,
|
||||
PureSelectors extends object = {}
|
||||
> extends StateContainer<State, PureTransitions, PureSelectors> {
|
||||
getState: () => RecursiveReadonly<State>;
|
||||
reducer: Reducer<RecursiveReadonly<State>>;
|
||||
replaceReducer: (nextReducer: Reducer<RecursiveReadonly<State>>) => void;
|
||||
dispatch: (action: TransitionDescription) => void;
|
||||
addMiddleware: (middleware: Middleware<RecursiveReadonly<State>>) => void;
|
||||
subscribe: (listener: (state: RecursiveReadonly<State>) => void) => () => void;
|
||||
}
|
||||
|
||||
export type Dispatch<T> = (action: T) => void;
|
||||
|
||||
export type Middleware<State = any> = (
|
||||
store: Pick<ReduxLikeStateContainer<State, any>, 'getState' | 'dispatch'>
|
||||
) => (
|
||||
next: (action: TransitionDescription) => TransitionDescription | any
|
||||
) => Dispatch<TransitionDescription>;
|
||||
|
||||
export type Reducer<State> = (state: State, action: TransitionDescription) => State;
|
||||
|
||||
export type UnboxState<
|
||||
Container extends StateContainer<any, any>
|
||||
> = Container extends StateContainer<infer T, any> ? T : never;
|
||||
export type UnboxTransitions<
|
||||
Container extends StateContainer<any, any>
|
||||
> = Container extends StateContainer<any, infer T> ? T : never;
|
||||
|
||||
export type Selector<Result, Args extends any[] = []> = (...args: Args) => Result;
|
||||
export type PureSelector<State, Result, Args extends any[] = []> = (
|
||||
state: State
|
||||
) => Selector<Result, Args>;
|
||||
export type EnsurePureSelector<T> = Ensure<T, PureSelector<any, any, any>>;
|
||||
export type PureSelectorToSelector<T extends PureSelector<any, any, any>> = ReturnType<
|
||||
EnsurePureSelector<T>
|
||||
>;
|
||||
export type PureSelectorsToSelectors<T extends object> = {
|
||||
[K in keyof T]: PureSelectorToSelector<EnsurePureSelector<T[K]>>;
|
||||
};
|
||||
|
||||
export type Comparator<Result> = (previous: Result, current: Result) => boolean;
|
||||
|
||||
export type MapStateToProps<State, StateProps extends object> = (state: State) => StateProps;
|
||||
export type Connect<State> = <Props extends object, StatePropKeys extends keyof Props>(
|
||||
mapStateToProp: MapStateToProps<State, Pick<Props, StatePropKeys>>
|
||||
) => (component: React.ComponentType<Props>) => React.FC<Omit<Props, StatePropKeys>>;
|
|
@ -1,177 +0,0 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { createStore } from './create_store';
|
||||
|
||||
test('can create store', () => {
|
||||
const store = createStore({});
|
||||
expect(store).toMatchObject({
|
||||
get: expect.any(Function),
|
||||
set: expect.any(Function),
|
||||
state$: expect.any(Object),
|
||||
createMutators: expect.any(Function),
|
||||
mutators: expect.any(Object),
|
||||
redux: {
|
||||
getState: expect.any(Function),
|
||||
dispatch: expect.any(Function),
|
||||
subscribe: expect.any(Function),
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('can set default state', () => {
|
||||
const defaultState = {
|
||||
foo: 'bar',
|
||||
};
|
||||
const store = createStore(defaultState);
|
||||
expect(store.get()).toEqual(defaultState);
|
||||
expect(store.redux.getState()).toEqual(defaultState);
|
||||
});
|
||||
|
||||
test('can set state', () => {
|
||||
const defaultState = {
|
||||
foo: 'bar',
|
||||
};
|
||||
const newState = {
|
||||
foo: 'baz',
|
||||
};
|
||||
const store = createStore(defaultState);
|
||||
|
||||
store.set(newState);
|
||||
|
||||
expect(store.get()).toEqual(newState);
|
||||
expect(store.redux.getState()).toEqual(newState);
|
||||
});
|
||||
|
||||
test('does not shallow merge states', () => {
|
||||
const defaultState = {
|
||||
foo: 'bar',
|
||||
};
|
||||
const newState = {
|
||||
foo2: 'baz',
|
||||
};
|
||||
const store = createStore<any>(defaultState);
|
||||
|
||||
store.set(newState);
|
||||
|
||||
expect(store.get()).toEqual(newState);
|
||||
expect(store.redux.getState()).toEqual(newState);
|
||||
});
|
||||
|
||||
test('can subscribe and unsubscribe to state changes', () => {
|
||||
const store = createStore<any>({});
|
||||
const spy = jest.fn();
|
||||
const subscription = store.state$.subscribe(spy);
|
||||
store.set({ a: 1 });
|
||||
store.set({ a: 2 });
|
||||
subscription.unsubscribe();
|
||||
store.set({ a: 3 });
|
||||
|
||||
expect(spy).toHaveBeenCalledTimes(2);
|
||||
expect(spy.mock.calls[0][0]).toEqual({ a: 1 });
|
||||
expect(spy.mock.calls[1][0]).toEqual({ a: 2 });
|
||||
});
|
||||
|
||||
test('multiple subscribers can subscribe', () => {
|
||||
const store = createStore<any>({});
|
||||
const spy1 = jest.fn();
|
||||
const spy2 = jest.fn();
|
||||
const subscription1 = store.state$.subscribe(spy1);
|
||||
const subscription2 = store.state$.subscribe(spy2);
|
||||
store.set({ a: 1 });
|
||||
subscription1.unsubscribe();
|
||||
store.set({ a: 2 });
|
||||
subscription2.unsubscribe();
|
||||
store.set({ a: 3 });
|
||||
|
||||
expect(spy1).toHaveBeenCalledTimes(1);
|
||||
expect(spy2).toHaveBeenCalledTimes(2);
|
||||
expect(spy1.mock.calls[0][0]).toEqual({ a: 1 });
|
||||
expect(spy2.mock.calls[0][0]).toEqual({ a: 1 });
|
||||
expect(spy2.mock.calls[1][0]).toEqual({ a: 2 });
|
||||
});
|
||||
|
||||
test('creates impure mutators from pure mutators', () => {
|
||||
const store = createStore<any>({});
|
||||
const mutators = store.createMutators({
|
||||
setFoo: _ => bar => ({ foo: bar }),
|
||||
});
|
||||
|
||||
expect(typeof mutators.setFoo).toBe('function');
|
||||
});
|
||||
|
||||
test('mutators can update state', () => {
|
||||
const store = createStore<any>({
|
||||
value: 0,
|
||||
foo: 'bar',
|
||||
});
|
||||
const mutators = store.createMutators({
|
||||
add: state => increment => ({ ...state, value: state.value + increment }),
|
||||
setFoo: state => bar => ({ ...state, foo: bar }),
|
||||
});
|
||||
|
||||
expect(store.get()).toEqual({
|
||||
value: 0,
|
||||
foo: 'bar',
|
||||
});
|
||||
|
||||
mutators.add(11);
|
||||
mutators.setFoo('baz');
|
||||
|
||||
expect(store.get()).toEqual({
|
||||
value: 11,
|
||||
foo: 'baz',
|
||||
});
|
||||
|
||||
mutators.add(-20);
|
||||
mutators.setFoo('bazooka');
|
||||
|
||||
expect(store.get()).toEqual({
|
||||
value: -9,
|
||||
foo: 'bazooka',
|
||||
});
|
||||
});
|
||||
|
||||
test('mutators methods are not bound', () => {
|
||||
const store = createStore<any>({ value: -3 });
|
||||
const { add } = store.createMutators({
|
||||
add: state => increment => ({ ...state, value: state.value + increment }),
|
||||
});
|
||||
|
||||
expect(store.get()).toEqual({ value: -3 });
|
||||
add(4);
|
||||
expect(store.get()).toEqual({ value: 1 });
|
||||
});
|
||||
|
||||
test('created mutators are saved in store object', () => {
|
||||
const store = createStore<
|
||||
any,
|
||||
{
|
||||
add: (increment: number) => void;
|
||||
}
|
||||
>({ value: -3 });
|
||||
|
||||
store.createMutators({
|
||||
add: state => increment => ({ ...state, value: state.value + increment }),
|
||||
});
|
||||
|
||||
expect(typeof store.mutators.add).toBe('function');
|
||||
store.mutators.add(5);
|
||||
expect(store.get()).toEqual({ value: 2 });
|
||||
});
|
|
@ -1,85 +0,0 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { createStore as createReduxStore, Reducer } from 'redux';
|
||||
import { Subject, Observable } from 'rxjs';
|
||||
import { AppStore, Mutators, PureMutators } from './types';
|
||||
|
||||
const SET = '__SET__';
|
||||
|
||||
export const createStore = <
|
||||
State extends {},
|
||||
StateMutators extends Mutators<PureMutators<State>> = {}
|
||||
>(
|
||||
defaultState: State
|
||||
): AppStore<State, StateMutators> => {
|
||||
const pureMutators: PureMutators<State> = {};
|
||||
const mutators: StateMutators = {} as StateMutators;
|
||||
const reducer: Reducer = (state, action) => {
|
||||
const pureMutator = pureMutators[action.type];
|
||||
if (pureMutator) {
|
||||
return pureMutator(state)(...action.args);
|
||||
}
|
||||
|
||||
switch (action.type) {
|
||||
case SET:
|
||||
return action.state;
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
const redux = createReduxStore<State, any, any, any>(reducer, defaultState as any);
|
||||
|
||||
const get = redux.getState;
|
||||
|
||||
const set = (state: State) =>
|
||||
redux.dispatch({
|
||||
type: SET,
|
||||
state,
|
||||
});
|
||||
|
||||
const state$ = new Subject();
|
||||
redux.subscribe(() => {
|
||||
state$.next(get());
|
||||
});
|
||||
|
||||
const createMutators: AppStore<State>['createMutators'] = newPureMutators => {
|
||||
const result: Mutators<any> = {};
|
||||
for (const type of Object.keys(newPureMutators)) {
|
||||
result[type] = (...args) => {
|
||||
redux.dispatch({
|
||||
type,
|
||||
args,
|
||||
});
|
||||
};
|
||||
}
|
||||
Object.assign(pureMutators, newPureMutators);
|
||||
Object.assign(mutators, result);
|
||||
return result;
|
||||
};
|
||||
|
||||
return {
|
||||
get,
|
||||
set,
|
||||
redux,
|
||||
state$: (state$ as unknown) as Observable<State>,
|
||||
createMutators,
|
||||
mutators,
|
||||
};
|
||||
};
|
|
@ -1,47 +0,0 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { Observable, BehaviorSubject } from 'rxjs';
|
||||
|
||||
export type Selector<State, Result> = (state: State) => Result;
|
||||
export type Comparator<Result> = (previous: Result, current: Result) => boolean;
|
||||
export type Unsubscribe = () => void;
|
||||
|
||||
const defaultComparator: Comparator<any> = (previous, current) => previous === current;
|
||||
|
||||
export const observableSelector = <State extends {}, Result>(
|
||||
state: State,
|
||||
state$: Observable<State>,
|
||||
selector: Selector<State, Result>,
|
||||
comparator: Comparator<Result> = defaultComparator
|
||||
): [Observable<Result>, Unsubscribe] => {
|
||||
let previousResult: Result = selector(state);
|
||||
const result$ = new BehaviorSubject<Result>(previousResult);
|
||||
|
||||
const subscription = state$.subscribe(value => {
|
||||
const result = selector(value);
|
||||
const isEqual: boolean = comparator(previousResult, result);
|
||||
if (!isEqual) {
|
||||
result$.next(result);
|
||||
}
|
||||
previousResult = result;
|
||||
});
|
||||
|
||||
return [(result$ as unknown) as Observable<Result>, subscription.unsubscribe];
|
||||
};
|
|
@ -1,126 +0,0 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import * as React from 'react';
|
||||
import { Provider as ReactReduxProvider, connect as reactReduxConnect } from 'react-redux';
|
||||
import { Store } from 'redux';
|
||||
import { AppStore, Mutators, PureMutators } from './types';
|
||||
import { observableSelector, Selector, Comparator } from './observable_selector';
|
||||
// TODO: Below import is temporary, use `react-use` lib instead.
|
||||
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
|
||||
import { useObservable } from '../../../kibana_react/public/util/use_observable';
|
||||
|
||||
const { useMemo, useLayoutEffect, useContext, createElement, Fragment } = React;
|
||||
|
||||
/**
|
||||
* @note
|
||||
* Types in `react-redux` seem to be quite off compared to reality
|
||||
* that's why a lot of `any`s below.
|
||||
*/
|
||||
|
||||
export interface ConsumerProps<State> {
|
||||
children: (state: State) => React.ReactChild;
|
||||
}
|
||||
|
||||
export type MapStateToProps<State extends {}, StateProps extends {}> = (state: State) => StateProps;
|
||||
|
||||
// TODO: `Omit` is generally part of TypeScript, but it currently does not exist in our build.
|
||||
type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;
|
||||
export type Connect<State extends {}> = <Props extends {}, StatePropKeys extends keyof Props>(
|
||||
mapStateToProp: MapStateToProps<State, Pick<Props, StatePropKeys>>
|
||||
) => (component: React.ComponentType<Props>) => React.FC<Omit<Props, StatePropKeys>>;
|
||||
|
||||
interface ReduxContextValue {
|
||||
store: Store;
|
||||
}
|
||||
|
||||
const mapDispatchToProps = () => ({});
|
||||
const mergeProps: any = (stateProps: any, dispatchProps: any, ownProps: any) => ({
|
||||
...ownProps,
|
||||
...stateProps,
|
||||
...dispatchProps,
|
||||
});
|
||||
|
||||
export const createContext = <
|
||||
State extends {},
|
||||
StateMutators extends Mutators<PureMutators<State>> = {}
|
||||
>(
|
||||
store: AppStore<State, StateMutators>
|
||||
) => {
|
||||
const { redux } = store;
|
||||
(redux as any).__appStore = store;
|
||||
const context = React.createContext<ReduxContextValue>({ store: redux });
|
||||
|
||||
const useStore = (): AppStore<State, StateMutators> => {
|
||||
// eslint-disable-next-line no-shadow
|
||||
const { store } = useContext(context);
|
||||
return (store as any).__appStore;
|
||||
};
|
||||
|
||||
const useState = (): State => {
|
||||
const { state$, get } = useStore();
|
||||
const state = useObservable(state$, get());
|
||||
return state;
|
||||
};
|
||||
|
||||
const useMutators = (): StateMutators => useStore().mutators;
|
||||
|
||||
const useSelector = <Result>(
|
||||
selector: Selector<State, Result>,
|
||||
comparator?: Comparator<Result>
|
||||
): Result => {
|
||||
const { state$, get } = useStore();
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
const [observable$, unsubscribe] = useMemo(
|
||||
() => observableSelector(get(), state$, selector, comparator),
|
||||
[state$]
|
||||
);
|
||||
/* eslint-enable react-hooks/exhaustive-deps */
|
||||
useLayoutEffect(() => unsubscribe, [observable$, unsubscribe]);
|
||||
const value = useObservable(observable$, selector(get()));
|
||||
return value;
|
||||
};
|
||||
|
||||
const Provider: React.FC<{}> = ({ children }) =>
|
||||
createElement(ReactReduxProvider, {
|
||||
store: redux,
|
||||
context,
|
||||
children,
|
||||
} as any);
|
||||
|
||||
const Consumer: React.FC<ConsumerProps<State>> = ({ children }) => {
|
||||
const state = useState();
|
||||
return createElement(Fragment, { children: children(state) });
|
||||
};
|
||||
|
||||
const options: any = { context };
|
||||
const connect: Connect<State> = mapStateToProps =>
|
||||
reactReduxConnect(mapStateToProps, mapDispatchToProps, mergeProps, options) as any;
|
||||
|
||||
return {
|
||||
Provider,
|
||||
Consumer,
|
||||
connect,
|
||||
context,
|
||||
useStore,
|
||||
useState,
|
||||
useMutators,
|
||||
useSelector,
|
||||
};
|
||||
};
|
164
yarn.lock
164
yarn.lock
|
@ -6776,6 +6776,11 @@ bounce@1.x.x:
|
|||
boom "7.x.x"
|
||||
hoek "5.x.x"
|
||||
|
||||
bowser@^1.7.3:
|
||||
version "1.9.4"
|
||||
resolved "https://registry.yarnpkg.com/bowser/-/bowser-1.9.4.tgz#890c58a2813a9d3243704334fa81b96a5c150c9a"
|
||||
integrity sha512-9IdMmj2KjigRq6oWhmwv1W36pDuA4STQZ8q6YO9um+x07xgYNCD3Oou+WP/3L1HNz7iqythGet3/p4wvc8AAwQ==
|
||||
|
||||
boxen@^1.2.1, boxen@^1.2.2:
|
||||
version "1.3.0"
|
||||
resolved "https://registry.yarnpkg.com/boxen/-/boxen-1.3.0.tgz#55c6c39a8ba58d9c61ad22cd877532deb665a20b"
|
||||
|
@ -8686,6 +8691,13 @@ copy-to-clipboard@^3.0.8:
|
|||
dependencies:
|
||||
toggle-selection "^1.0.3"
|
||||
|
||||
copy-to-clipboard@^3.2.0:
|
||||
version "3.2.0"
|
||||
resolved "https://registry.yarnpkg.com/copy-to-clipboard/-/copy-to-clipboard-3.2.0.tgz#d2724a3ccbfed89706fac8a894872c979ac74467"
|
||||
integrity sha512-eOZERzvCmxS8HWzugj4Uxl8OJxa7T2k1Gi0X5qavwydHIfuSHq2dTD09LOg/XyGq4Zpb5IsR/2OJ5lbOegz78w==
|
||||
dependencies:
|
||||
toggle-selection "^1.0.6"
|
||||
|
||||
copy-webpack-plugin@^5.0.4:
|
||||
version "5.0.4"
|
||||
resolved "https://registry.yarnpkg.com/copy-webpack-plugin/-/copy-webpack-plugin-5.0.4.tgz#c78126f604e24f194c6ec2f43a64e232b5d43655"
|
||||
|
@ -9053,6 +9065,14 @@ css-color-keywords@^1.0.0:
|
|||
resolved "https://registry.yarnpkg.com/css-color-keywords/-/css-color-keywords-1.0.0.tgz#fea2616dc676b2962686b3af8dbdbe180b244e05"
|
||||
integrity sha1-/qJhbcZ2spYmhrOvjb2+GAskTgU=
|
||||
|
||||
css-in-js-utils@^2.0.0:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/css-in-js-utils/-/css-in-js-utils-2.0.1.tgz#3b472b398787291b47cfe3e44fecfdd9e914ba99"
|
||||
integrity sha512-PJF0SpJT+WdbVVt0AOYp9C8GnuruRlL/UFW7932nLWmFLQTaWEzTBQEx7/hn4BuV+WON75iAViSUJLiU3PKbpA==
|
||||
dependencies:
|
||||
hyphenate-style-name "^1.0.2"
|
||||
isobject "^3.0.1"
|
||||
|
||||
css-loader@2.1.1, css-loader@^2.1.1:
|
||||
version "2.1.1"
|
||||
resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-2.1.1.tgz#d8254f72e412bb2238bb44dd674ffbef497333ea"
|
||||
|
@ -9138,6 +9158,14 @@ css-tree@1.0.0-alpha.29:
|
|||
mdn-data "~1.1.0"
|
||||
source-map "^0.5.3"
|
||||
|
||||
css-tree@^1.0.0-alpha.28:
|
||||
version "1.0.0-alpha.39"
|
||||
resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-1.0.0-alpha.39.tgz#2bff3ffe1bb3f776cf7eefd91ee5cba77a149eeb"
|
||||
integrity sha512-7UvkEYgBAHRG9Nt980lYxjsTrCyHFN53ky3wVsDkiMdVqylqRt+Zc+jm5qw7/qyOvN2dHSYtX0e4MbCCExSvnA==
|
||||
dependencies:
|
||||
mdn-data "2.0.6"
|
||||
source-map "^0.6.1"
|
||||
|
||||
css-url-regex@^1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/css-url-regex/-/css-url-regex-1.1.0.tgz#83834230cc9f74c457de59eebd1543feeb83b7ec"
|
||||
|
@ -9209,7 +9237,7 @@ cssstyle@^2.0.0:
|
|||
dependencies:
|
||||
cssom "~0.3.6"
|
||||
|
||||
csstype@^2.2.0, csstype@^2.5.7, csstype@^2.6.7:
|
||||
csstype@^2.2.0, csstype@^2.5.5, csstype@^2.5.7, csstype@^2.6.7:
|
||||
version "2.6.7"
|
||||
resolved "https://registry.yarnpkg.com/csstype/-/csstype-2.6.7.tgz#20b0024c20b6718f4eda3853a1f5a1cce7f5e4a5"
|
||||
integrity sha512-9Mcn9sFbGBAdmimWb2gLVDtFJzeKtDGIr76TUqmjZrw9LFXBMSU70lcs+C0/7fyCd6iBDqmksUcCOUIkisPHsQ==
|
||||
|
@ -10955,6 +10983,13 @@ error-ex@^1.2.0, error-ex@^1.3.1:
|
|||
dependencies:
|
||||
is-arrayish "^0.2.1"
|
||||
|
||||
error-stack-parser@^2.0.4:
|
||||
version "2.0.4"
|
||||
resolved "https://registry.yarnpkg.com/error-stack-parser/-/error-stack-parser-2.0.4.tgz#a757397dc5d9de973ac9a5d7d4e8ade7cfae9101"
|
||||
integrity sha512-fZ0KkoxSjLFmhW5lHbUT3tLwy3nX1qEzMYo8koY1vrsAco53CMT1djnBSeC/wUjTEZRhZl9iRw7PaMaxfJ4wzQ==
|
||||
dependencies:
|
||||
stackframe "^1.1.0"
|
||||
|
||||
error@^7.0.0, error@^7.0.2:
|
||||
version "7.0.2"
|
||||
resolved "https://registry.yarnpkg.com/error/-/error-7.0.2.tgz#a5f75fff4d9926126ddac0ea5dc38e689153cb02"
|
||||
|
@ -12131,6 +12166,11 @@ fast-deep-equal@^2.0.1:
|
|||
resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz#7b05218ddf9667bf7f370bf7fdb2cb15fdd0aa49"
|
||||
integrity sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=
|
||||
|
||||
fast-deep-equal@^3.1.1:
|
||||
version "3.1.1"
|
||||
resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.1.tgz#545145077c501491e33b15ec408c294376e94ae4"
|
||||
integrity sha512-8UEa58QDLauDNfpbrX55Q9jrGHThw2ZMdOky5Gl1CDtVeJDPVrG4Jxx1N8jw2gkWaff5UUuX1KJd+9zGe2B+ZA==
|
||||
|
||||
fast-diff@^1.1.2:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/fast-diff/-/fast-diff-1.2.0.tgz#73ee11982d86caaf7959828d519cfe927fac5f03"
|
||||
|
@ -12197,6 +12237,11 @@ fast-stream-to-buffer@^1.0.0:
|
|||
dependencies:
|
||||
end-of-stream "^1.4.1"
|
||||
|
||||
fastest-stable-stringify@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/fastest-stable-stringify/-/fastest-stable-stringify-1.0.1.tgz#9122d406d4c9d98bea644a6b6853d5874b87b028"
|
||||
integrity sha1-kSLUBtTJ2YvqZEpraFPVh0uHsCg=
|
||||
|
||||
fastparse@^1.1.1:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/fastparse/-/fastparse-1.1.1.tgz#d1e2643b38a94d7583b479060e6c4affc94071f8"
|
||||
|
@ -15110,6 +15155,11 @@ hyperlinker@^1.0.0:
|
|||
resolved "https://registry.yarnpkg.com/hyperlinker/-/hyperlinker-1.0.0.tgz#23dc9e38a206b208ee49bc2d6c8ef47027df0c0e"
|
||||
integrity sha512-Ty8UblRWFEcfSuIaajM34LdPXIhbs1ajEX/BBPv24J+enSVaEVY63xQ6lTO9VRYS5LAoghIG0IDJ+p+IPzKUQQ==
|
||||
|
||||
hyphenate-style-name@^1.0.2:
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/hyphenate-style-name/-/hyphenate-style-name-1.0.3.tgz#097bb7fa0b8f1a9cf0bd5c734cf95899981a9b48"
|
||||
integrity sha512-EcuixamT82oplpoJ2XU4pDtKGWQ7b00CD9f1ug9IaQ3p1bkHMiKCZ9ut9QDI6qsa6cpUuB+A/I+zLtdNK4n2DQ==
|
||||
|
||||
i18n-iso-countries@^4.3.1:
|
||||
version "4.3.1"
|
||||
resolved "https://registry.yarnpkg.com/i18n-iso-countries/-/i18n-iso-countries-4.3.1.tgz#f110a8824ce14edbb0eb8f3b0bd817ff950af37c"
|
||||
|
@ -15389,6 +15439,14 @@ ini@^1.2.0, ini@^1.3.4, ini@^1.3.5, ini@~1.3.0:
|
|||
resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927"
|
||||
integrity sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==
|
||||
|
||||
inline-style-prefixer@^4.0.0:
|
||||
version "4.0.2"
|
||||
resolved "https://registry.yarnpkg.com/inline-style-prefixer/-/inline-style-prefixer-4.0.2.tgz#d390957d26f281255fe101da863158ac6eb60911"
|
||||
integrity sha512-N8nVhwfYga9MiV9jWlwfdj1UDIaZlBFu4cJSJkIr7tZX7sHpHhGR5su1qdpW+7KPL8ISTvCIkcaFi/JdBknvPg==
|
||||
dependencies:
|
||||
bowser "^1.7.3"
|
||||
css-in-js-utils "^2.0.0"
|
||||
|
||||
inline-style@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/inline-style/-/inline-style-2.0.0.tgz#2fa9cf624596a8109355b925094e138bbd5ea29b"
|
||||
|
@ -18922,6 +18980,11 @@ mdast-add-list-metadata@1.0.1:
|
|||
dependencies:
|
||||
unist-util-visit-parents "1.1.2"
|
||||
|
||||
mdn-data@2.0.6:
|
||||
version "2.0.6"
|
||||
resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.6.tgz#852dc60fcaa5daa2e8cf6c9189c440ed3e042978"
|
||||
integrity sha512-rQvjv71olwNHgiTbfPZFkJtjNMciWgswYeciZhtvWLO8bmX3TnhyA62I6sTWOyZssWHJJjY6/KiWwqQsWWsqOA==
|
||||
|
||||
mdn-data@~1.1.0:
|
||||
version "1.1.4"
|
||||
resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-1.1.4.tgz#50b5d4ffc4575276573c4eedb8780812a8419f01"
|
||||
|
@ -19792,6 +19855,20 @@ nan@^2.10.0, nan@^2.9.2:
|
|||
resolved "https://registry.yarnpkg.com/nan/-/nan-2.10.0.tgz#96d0cd610ebd58d4b4de9cc0c6828cda99c7548f"
|
||||
integrity sha512-bAdJv7fBLhWC+/Bls0Oza+mvTaNQtP+1RyhhhvD95pgUJz6XM5IzgmxOkItJ9tkoCiplvAnXI1tNmmUD/eScyA==
|
||||
|
||||
nano-css@^5.2.1:
|
||||
version "5.2.1"
|
||||
resolved "https://registry.yarnpkg.com/nano-css/-/nano-css-5.2.1.tgz#73b8470fa40b028a134d3393ae36bbb34b9fa332"
|
||||
integrity sha512-T54okxMAha0+de+W8o3qFtuWhTxYvqQh2ku1cYEqTTP9mR62nWV2lLK9qRuAGWmoaYWhU7K4evT9Lc1iF65wuw==
|
||||
dependencies:
|
||||
css-tree "^1.0.0-alpha.28"
|
||||
csstype "^2.5.5"
|
||||
fastest-stable-stringify "^1.0.1"
|
||||
inline-style-prefixer "^4.0.0"
|
||||
rtl-css-js "^1.9.0"
|
||||
sourcemap-codec "^1.4.1"
|
||||
stacktrace-js "^2.0.0"
|
||||
stylis "3.5.0"
|
||||
|
||||
nano-time@1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/nano-time/-/nano-time-1.0.0.tgz#b0554f69ad89e22d0907f7a12b0993a5d96137ef"
|
||||
|
@ -23507,6 +23584,21 @@ react-transition-group@^2.2.1:
|
|||
prop-types "^15.6.2"
|
||||
react-lifecycles-compat "^3.0.4"
|
||||
|
||||
react-use@^13.10.2:
|
||||
version "13.10.2"
|
||||
resolved "https://registry.yarnpkg.com/react-use/-/react-use-13.10.2.tgz#4250d258ca9068662943299c01794a136408c8e9"
|
||||
integrity sha512-z3VFSiPHW6arViGVnajO7YKY5OD+Z9LWcImoJdYHkau23cLSoTctxM3XENLpGxjhJlHaYiQZ6pPgq7pwGTqSZA==
|
||||
dependencies:
|
||||
copy-to-clipboard "^3.2.0"
|
||||
nano-css "^5.2.1"
|
||||
react-fast-compare "^2.0.4"
|
||||
resize-observer-polyfill "^1.5.1"
|
||||
screenfull "^5.0.0"
|
||||
set-harmonic-interval "^1.0.1"
|
||||
throttle-debounce "^2.1.0"
|
||||
ts-easing "^0.2.0"
|
||||
tslib "^1.10.0"
|
||||
|
||||
react-virtualized@^9.18.5:
|
||||
version "9.20.1"
|
||||
resolved "https://registry.yarnpkg.com/react-virtualized/-/react-virtualized-9.20.1.tgz#02dc08fe9070386b8c48e2ac56bce7af0208d22d"
|
||||
|
@ -24869,6 +24961,13 @@ rsvp@^4.8.4:
|
|||
resolved "https://registry.yarnpkg.com/rsvp/-/rsvp-4.8.5.tgz#c8f155311d167f68f21e168df71ec5b083113734"
|
||||
integrity sha512-nfMOlASu9OnRJo1mbEk2cz0D56a1MBNrJ7orjRZQG10XDyuvwksKbuXNp6qa+kbn839HwjwhBzhFmdsaEAfauA==
|
||||
|
||||
rtl-css-js@^1.9.0:
|
||||
version "1.13.1"
|
||||
resolved "https://registry.yarnpkg.com/rtl-css-js/-/rtl-css-js-1.13.1.tgz#80deabf6e8f36d6767d495cd3eb60fecb20c67e1"
|
||||
integrity sha512-jgkIDj6Xi25kAEm5oYM3ZMFiOQhpLEcXi2LY/6bVr91cVz73hciHKneL5AMVPxOcks/JuizSaaNsvNRkeAWe3w==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.1.2"
|
||||
|
||||
run-async@^0.1.0:
|
||||
version "0.1.0"
|
||||
resolved "https://registry.yarnpkg.com/run-async/-/run-async-0.1.0.tgz#c8ad4a5e110661e402a7d21b530e009f25f8e389"
|
||||
|
@ -25156,6 +25255,11 @@ scoped-regex@^1.0.0:
|
|||
resolved "https://registry.yarnpkg.com/scoped-regex/-/scoped-regex-1.0.0.tgz#a346bb1acd4207ae70bd7c0c7ca9e566b6baddb8"
|
||||
integrity sha1-o0a7Gs1CB65wvXwMfKnlZra63bg=
|
||||
|
||||
screenfull@^5.0.0:
|
||||
version "5.0.0"
|
||||
resolved "https://registry.yarnpkg.com/screenfull/-/screenfull-5.0.0.tgz#5c2010c0e84fd4157bf852877698f90b8cbe96f6"
|
||||
integrity sha512-yShzhaIoE9OtOhWVyBBffA6V98CDCoyHTsp8228blmqYy1Z5bddzE/4FPiJKlr8DVR4VBiiUyfPzIQPIYDkeMA==
|
||||
|
||||
script-loader@0.7.2:
|
||||
version "0.7.2"
|
||||
resolved "https://registry.yarnpkg.com/script-loader/-/script-loader-0.7.2.tgz#2016db6f86f25f5cf56da38915d83378bb166ba7"
|
||||
|
@ -25393,6 +25497,11 @@ set-getter@^0.1.0:
|
|||
dependencies:
|
||||
to-object-path "^0.3.0"
|
||||
|
||||
set-harmonic-interval@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/set-harmonic-interval/-/set-harmonic-interval-1.0.1.tgz#e1773705539cdfb80ce1c3d99e7f298bb3995249"
|
||||
integrity sha512-AhICkFV84tBP1aWqPwLZqFvAwqEoVA9kxNMniGEUvzOlm4vLmOFLiTT3UZ6bziJTy4bOVpzWGTfSCbmaayGx8g==
|
||||
|
||||
set-immediate-shim@^1.0.0, set-immediate-shim@~1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz#4b2b1b27eb808a9f8dcc481a58e5e56f599f3f61"
|
||||
|
@ -25869,6 +25978,11 @@ source-map@0.1.32:
|
|||
dependencies:
|
||||
amdefine ">=0.0.4"
|
||||
|
||||
source-map@0.5.6:
|
||||
version "0.5.6"
|
||||
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.6.tgz#75ce38f52bf0733c5a7f0c118d81334a2bb5f412"
|
||||
integrity sha1-dc449SvwczxafwwRjYEzSiu19BI=
|
||||
|
||||
"source-map@>= 0.1.2":
|
||||
version "0.7.3"
|
||||
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.3.tgz#5302f8169031735226544092e64981f751750383"
|
||||
|
@ -25905,6 +26019,11 @@ source-map@~0.2.0:
|
|||
dependencies:
|
||||
amdefine ">=0.0.4"
|
||||
|
||||
sourcemap-codec@^1.4.1:
|
||||
version "1.4.6"
|
||||
resolved "https://registry.yarnpkg.com/sourcemap-codec/-/sourcemap-codec-1.4.6.tgz#e30a74f0402bad09807640d39e971090a08ce1e9"
|
||||
integrity sha512-1ZooVLYFxC448piVLBbtOxFcXwnymH9oUF8nRd3CuYDVvkRBxRl6pB4Mtas5a4drtL+E8LDgFkQNcgIw6tc8Hg==
|
||||
|
||||
space-separated-tokens@^1.0.0:
|
||||
version "1.1.2"
|
||||
resolved "https://registry.yarnpkg.com/space-separated-tokens/-/space-separated-tokens-1.1.2.tgz#e95ab9d19ae841e200808cd96bc7bd0adbbb3412"
|
||||
|
@ -26106,6 +26225,13 @@ stable@^0.1.8:
|
|||
resolved "https://registry.yarnpkg.com/stable/-/stable-0.1.8.tgz#836eb3c8382fe2936feaf544631017ce7d47a3cf"
|
||||
integrity sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==
|
||||
|
||||
stack-generator@^2.0.4:
|
||||
version "2.0.4"
|
||||
resolved "https://registry.yarnpkg.com/stack-generator/-/stack-generator-2.0.4.tgz#027513eab2b195bbb43b9c8360ba2dd0ab54de09"
|
||||
integrity sha512-ha1gosTNcgxwzo9uKTQ8zZ49aUp5FIUW58YHFxCqaAHtE0XqBg0chGFYA1MfmW//x1KWq3F4G7Ug7bJh4RiRtg==
|
||||
dependencies:
|
||||
stackframe "^1.1.0"
|
||||
|
||||
stack-trace@0.0.10, stack-trace@0.0.x:
|
||||
version "0.0.10"
|
||||
resolved "https://registry.yarnpkg.com/stack-trace/-/stack-trace-0.0.10.tgz#547c70b347e8d32b4e108ea1a2a159e5fdde19c0"
|
||||
|
@ -26116,6 +26242,11 @@ stack-utils@^1.0.1:
|
|||
resolved "https://registry.yarnpkg.com/stack-utils/-/stack-utils-1.0.1.tgz#d4f33ab54e8e38778b0ca5cfd3b3afb12db68620"
|
||||
integrity sha1-1PM6tU6OOHeLDKXP07OvsS22hiA=
|
||||
|
||||
stackframe@^1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/stackframe/-/stackframe-1.1.0.tgz#e3fc2eb912259479c9822f7d1f1ff365bd5cbc83"
|
||||
integrity sha512-Vx6W1Yvy+AM1R/ckVwcHQHV147pTPBKWCRLrXMuPrFVfvBUc3os7PR1QLIWCMhPpRg5eX9ojzbQIMLGBwyLjqg==
|
||||
|
||||
stackman@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/stackman/-/stackman-4.0.0.tgz#3ccdc8682fee36373ed2492dc3dad546eb44647d"
|
||||
|
@ -26127,6 +26258,23 @@ stackman@^4.0.0:
|
|||
error-callsites "^2.0.2"
|
||||
load-source-map "^1.0.0"
|
||||
|
||||
stacktrace-gps@^3.0.3:
|
||||
version "3.0.3"
|
||||
resolved "https://registry.yarnpkg.com/stacktrace-gps/-/stacktrace-gps-3.0.3.tgz#b89f84cc13bb925b96607e737b617c8715facf57"
|
||||
integrity sha512-51Rr7dXkyFUKNmhY/vqZWK+EvdsfFSRiQVtgHTFlAdNIYaDD7bVh21yBHXaNWAvTD+w+QSjxHg7/v6Tz4veExA==
|
||||
dependencies:
|
||||
source-map "0.5.6"
|
||||
stackframe "^1.1.0"
|
||||
|
||||
stacktrace-js@^2.0.0:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/stacktrace-js/-/stacktrace-js-2.0.1.tgz#ebdb0e9a16e6f171f96ca7878404e7f15c3d42ba"
|
||||
integrity sha512-13oDNgBSeWtdGa4/2BycNyKqe+VktCoJ8VLx4pDoJkwGGJVtiHdfMOAj3aW9xTi8oR2v34z9IcvfCvT6XNdNAw==
|
||||
dependencies:
|
||||
error-stack-parser "^2.0.4"
|
||||
stack-generator "^2.0.4"
|
||||
stacktrace-gps "^3.0.3"
|
||||
|
||||
state-toggle@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/state-toggle/-/state-toggle-1.0.0.tgz#d20f9a616bb4f0c3b98b91922d25b640aa2bc425"
|
||||
|
@ -26649,6 +26797,11 @@ stylis-rule-sheet@^0.0.10:
|
|||
resolved "https://registry.yarnpkg.com/stylis-rule-sheet/-/stylis-rule-sheet-0.0.10.tgz#44e64a2b076643f4b52e5ff71efc04d8c3c4a430"
|
||||
integrity sha512-nTbZoaqoBnmK+ptANthb10ZRZOGC+EmTLLUxeYIuHNkEKcmKgXX1XWKkUBT2Ac4es3NybooPe0SmvKdhKJZAuw==
|
||||
|
||||
stylis@3.5.0:
|
||||
version "3.5.0"
|
||||
resolved "https://registry.yarnpkg.com/stylis/-/stylis-3.5.0.tgz#016fa239663d77f868fef5b67cf201c4b7c701e1"
|
||||
integrity sha512-pP7yXN6dwMzAR29Q0mBrabPCe0/mNO1MSr93bhay+hcZondvMMTpeGyd8nbhYJdyperNT2DRxONQuUGcJr5iPw==
|
||||
|
||||
stylus-lookup@^3.0.1:
|
||||
version "3.0.2"
|
||||
resolved "https://registry.yarnpkg.com/stylus-lookup/-/stylus-lookup-3.0.2.tgz#c9eca3ff799691020f30b382260a67355fefdddd"
|
||||
|
@ -27475,7 +27628,7 @@ to-through@^2.0.0:
|
|||
dependencies:
|
||||
through2 "^2.0.3"
|
||||
|
||||
toggle-selection@^1.0.3:
|
||||
toggle-selection@^1.0.3, toggle-selection@^1.0.6:
|
||||
version "1.0.6"
|
||||
resolved "https://registry.yarnpkg.com/toggle-selection/-/toggle-selection-1.0.6.tgz#6e45b1263f2017fa0acc7d89d78b15b8bf77da32"
|
||||
integrity sha1-bkWxJj8gF/oKzH2J14sVuL932jI=
|
||||
|
@ -27621,6 +27774,11 @@ ts-dedent@^1.1.0:
|
|||
resolved "https://registry.yarnpkg.com/ts-dedent/-/ts-dedent-1.1.0.tgz#67983940793183dc7c7f820acb66ba02cdc33c6e"
|
||||
integrity sha512-CVCvDwMBWZKjDxpN3mU/Dx1v3k+sJgE8nrhXcC9vRopRfoa7vVzilNvHEAUi5jQnmFHpnxDx5jZdI1TpG8ny2g==
|
||||
|
||||
ts-easing@^0.2.0:
|
||||
version "0.2.0"
|
||||
resolved "https://registry.yarnpkg.com/ts-easing/-/ts-easing-0.2.0.tgz#c8a8a35025105566588d87dbda05dd7fbfa5a4ec"
|
||||
integrity sha512-Z86EW+fFFh/IFB1fqQ3/+7Zpf9t2ebOAxNI/V6Wo7r5gqiqtxmgTlQ1qbqQcjLKYeSHPTsEmvlJUDg/EuL0uHQ==
|
||||
|
||||
ts-invariant@^0.2.1:
|
||||
version "0.2.1"
|
||||
resolved "https://registry.yarnpkg.com/ts-invariant/-/ts-invariant-0.2.1.tgz#3d587f9d6e3bded97bf9ec17951dd9814d5a9d3f"
|
||||
|
@ -27676,7 +27834,7 @@ tsd@^0.7.4:
|
|||
typescript "^3.0.1"
|
||||
update-notifier "^2.5.0"
|
||||
|
||||
tslib@^1:
|
||||
tslib@^1, tslib@^1.10.0:
|
||||
version "1.10.0"
|
||||
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.10.0.tgz#c3c19f95973fb0a62973fb09d90d961ee43e5c8a"
|
||||
integrity sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ==
|
||||
|
|
Loading…
Reference in a new issue