feat: kibana-react initial setup (#37206)
* feat: 🎸 add NP TypeScript typings * feat: 🎸 add kibana-react context provider * feat: 🎸 add initial in-memory core implementation * test: 💍 simplify useKibana() tests * feat: 🎸 add useObservable() hook * test: 💍 implement initial mock for uiSettings for testing * feat: 🎸 add useUiSetting() hook * docs: ✏️ update function name * test: 💍 use core mock for testing useUiSetting() hook * chore: 🤖 remove unused types * chore: 🤖 fix linter error * test: 💍 move core mock inline Currently it fails on CI because of the relative import, so moving the mock inline util it is resolved.
This commit is contained in:
parent
983f3a25a8
commit
a53e480881
|
@ -210,6 +210,7 @@
|
|||
"react-color": "^2.13.8",
|
||||
"react-dom": "^16.8.0",
|
||||
"react-grid-layout": "^0.16.2",
|
||||
"react-hooks-testing-library": "^0.5.0",
|
||||
"react-input-range": "^1.3.0",
|
||||
"react-markdown": "^3.4.1",
|
||||
"react-redux": "^5.0.7",
|
||||
|
|
|
@ -22,6 +22,7 @@ import { RESERVED_DIR_JEST_INTEGRATION_TESTS } from '../constants';
|
|||
export default {
|
||||
rootDir: '../../..',
|
||||
roots: [
|
||||
'<rootDir>/src/plugins',
|
||||
'<rootDir>/src/legacy/ui',
|
||||
'<rootDir>/src/core',
|
||||
'<rootDir>/src/legacy/core_plugins',
|
||||
|
|
80
src/plugins/kibana_react/public/core/context.test.tsx
Normal file
80
src/plugins/kibana_react/public/core/context.test.tsx
Normal file
|
@ -0,0 +1,80 @@
|
|||
/*
|
||||
* 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 * as ReactDOM from 'react-dom';
|
||||
import { context, createContext, useKibana } from './context';
|
||||
import { createMock } from './mock';
|
||||
|
||||
let container: HTMLDivElement | null;
|
||||
|
||||
beforeEach(() => {
|
||||
container = document.createElement('div');
|
||||
document.body.appendChild(container);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
document.body.removeChild(container!);
|
||||
container = null;
|
||||
});
|
||||
|
||||
test('can mount <Provider> without crashing', () => {
|
||||
const core = createMock();
|
||||
ReactDOM.render(
|
||||
<context.Provider value={{ core }}>
|
||||
<div>Hello world</div>
|
||||
</context.Provider>,
|
||||
container
|
||||
);
|
||||
});
|
||||
|
||||
const TestConsumer = () => {
|
||||
const { core } = useKibana();
|
||||
return <div>{(core as any).foo}</div>;
|
||||
};
|
||||
|
||||
test('useKibana() hook retrieves Kibana context', () => {
|
||||
const core = createMock();
|
||||
(core as any).foo = 'bar';
|
||||
ReactDOM.render(
|
||||
<context.Provider value={{ core }}>
|
||||
<TestConsumer />
|
||||
</context.Provider>,
|
||||
container
|
||||
);
|
||||
|
||||
const div = container!.querySelector('div');
|
||||
expect(div!.textContent).toBe('bar');
|
||||
});
|
||||
|
||||
test('createContext() creates context that can be consumed by useKibana() hook', () => {
|
||||
const core = createMock();
|
||||
(core as any).foo = 'baz';
|
||||
const { Provider } = createContext(core, {});
|
||||
|
||||
ReactDOM.render(
|
||||
<Provider>
|
||||
<TestConsumer />
|
||||
</Provider>,
|
||||
container
|
||||
);
|
||||
|
||||
const div = container!.querySelector('div');
|
||||
expect(div!.textContent).toBe('baz');
|
||||
});
|
38
src/plugins/kibana_react/public/core/context.ts
Normal file
38
src/plugins/kibana_react/public/core/context.ts
Normal file
|
@ -0,0 +1,38 @@
|
|||
/*
|
||||
* 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 { KibanaReactContextValue, Core } from './types';
|
||||
|
||||
export const context = React.createContext<KibanaReactContextValue>({
|
||||
core: {},
|
||||
});
|
||||
|
||||
export const createContext = (core: Core, plugins?: any) => {
|
||||
const value: KibanaReactContextValue = { core };
|
||||
const Provider: React.FC = ({ children }) =>
|
||||
React.createElement(context.Provider, { value, children });
|
||||
|
||||
return {
|
||||
Provider,
|
||||
Consumer: context.Consumer,
|
||||
};
|
||||
};
|
||||
|
||||
export const useKibana = (): KibanaReactContextValue => React.useContext(context);
|
21
src/plugins/kibana_react/public/core/index.ts
Normal file
21
src/plugins/kibana_react/public/core/index.ts
Normal file
|
@ -0,0 +1,21 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export { context, createContext, useKibana } from './context';
|
||||
export { KibanaReactContextValue } from './types';
|
50
src/plugins/kibana_react/public/core/mock.ts
Normal file
50
src/plugins/kibana_react/public/core/mock.ts
Normal file
|
@ -0,0 +1,50 @@
|
|||
/*
|
||||
* 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 { Core } from './types';
|
||||
|
||||
const createSetupContractMock = () => {
|
||||
const setupContract: jest.Mocked<any> = {
|
||||
getAll: jest.fn(),
|
||||
get: jest.fn(),
|
||||
get$: jest.fn(),
|
||||
set: jest.fn(),
|
||||
remove: jest.fn(),
|
||||
isDeclared: jest.fn(),
|
||||
isDefault: jest.fn(),
|
||||
isCustom: jest.fn(),
|
||||
isOverridden: jest.fn(),
|
||||
overrideLocalDefault: jest.fn(),
|
||||
getUpdate$: jest.fn(),
|
||||
getSaved$: jest.fn(),
|
||||
getUpdateErrors$: jest.fn(),
|
||||
stop: jest.fn(),
|
||||
};
|
||||
return setupContract as any;
|
||||
};
|
||||
|
||||
export const createMock = (): Core => {
|
||||
const uiSettings = createSetupContractMock();
|
||||
|
||||
const core: Partial<Core> = {
|
||||
uiSettings,
|
||||
};
|
||||
|
||||
return core as Core;
|
||||
};
|
28
src/plugins/kibana_react/public/core/types.ts
Normal file
28
src/plugins/kibana_react/public/core/types.ts
Normal file
|
@ -0,0 +1,28 @@
|
|||
/*
|
||||
* 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 { CoreSetup, CoreStart } from '../../../../core/public';
|
||||
|
||||
export { CoreSetup, CoreStart };
|
||||
|
||||
export type Core = Partial<CoreSetup> & Partial<CoreStart>;
|
||||
|
||||
export interface KibanaReactContextValue {
|
||||
core: Core;
|
||||
}
|
|
@ -0,0 +1,140 @@
|
|||
/*
|
||||
* 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 * as ReactDOM from 'react-dom';
|
||||
import { act, Simulate } from 'react-dom/test-utils';
|
||||
import { useUiSetting } from './use_ui_setting';
|
||||
import { createContext } from '../core';
|
||||
import { Core } from '../core/types';
|
||||
import { createMock } from '../core/mock';
|
||||
import { Subject } from 'rxjs';
|
||||
import { useObservable } from '../util/use_observable';
|
||||
|
||||
jest.mock('../util/use_observable');
|
||||
const useObservableSpy = (useObservable as any) as jest.SpyInstance;
|
||||
useObservableSpy.mockImplementation((observable, def) => def);
|
||||
|
||||
const mock = (): [Core, Subject<any>] => {
|
||||
const core = createMock() as Core;
|
||||
const get = (core.uiSettings!.get as any) as jest.SpyInstance;
|
||||
const get$ = (core.uiSettings!.get$ as any) as jest.SpyInstance;
|
||||
const subject = new Subject();
|
||||
|
||||
get.mockImplementation(() => 'bar');
|
||||
get$.mockImplementation(() => subject);
|
||||
|
||||
return [core, subject];
|
||||
};
|
||||
|
||||
let container: HTMLDivElement | null;
|
||||
|
||||
beforeEach(() => {
|
||||
container = document.createElement('div');
|
||||
document.body.appendChild(container);
|
||||
useObservableSpy.mockClear();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
document.body.removeChild(container!);
|
||||
container = null;
|
||||
});
|
||||
|
||||
const TestConsumer: React.FC<{
|
||||
setting: string;
|
||||
newValue?: string;
|
||||
}> = ({ setting, newValue = '' }) => {
|
||||
const [value, set] = useUiSetting(setting, 'DEFAULT');
|
||||
|
||||
return (
|
||||
<div>
|
||||
{setting}: <strong>{value}</strong>
|
||||
<button onClick={() => set(newValue)}>Set new value!</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
test('synchronously renders setting value', async () => {
|
||||
const [core] = mock();
|
||||
const { Provider } = createContext(core);
|
||||
|
||||
ReactDOM.render(
|
||||
<Provider>
|
||||
<TestConsumer setting="foo" />
|
||||
</Provider>,
|
||||
container
|
||||
);
|
||||
|
||||
const strong = container!.querySelector('strong');
|
||||
expect(strong!.textContent).toBe('bar');
|
||||
expect(core.uiSettings!.get).toHaveBeenCalledTimes(1);
|
||||
expect((core.uiSettings!.get as any).mock.calls[0][0]).toBe('foo');
|
||||
});
|
||||
|
||||
test('calls Core with correct arguments', async () => {
|
||||
const core = createMock();
|
||||
const { Provider } = createContext(core);
|
||||
|
||||
ReactDOM.render(
|
||||
<Provider>
|
||||
<TestConsumer setting="non_existing" />
|
||||
</Provider>,
|
||||
container
|
||||
);
|
||||
|
||||
expect(core.uiSettings!.get).toHaveBeenCalledWith('non_existing', 'DEFAULT');
|
||||
});
|
||||
|
||||
test('subscribes to observable using useObservable', async () => {
|
||||
const [core, subject] = mock();
|
||||
const { Provider } = createContext(core);
|
||||
|
||||
expect(useObservableSpy).toHaveBeenCalledTimes(0);
|
||||
|
||||
ReactDOM.render(
|
||||
<Provider>
|
||||
<TestConsumer setting="theme:darkMode" />
|
||||
</Provider>,
|
||||
container
|
||||
);
|
||||
|
||||
expect(useObservableSpy).toHaveBeenCalledTimes(1);
|
||||
expect(useObservableSpy.mock.calls[0][0]).toBe(subject);
|
||||
});
|
||||
|
||||
test('can set new hook value', async () => {
|
||||
const [core] = mock();
|
||||
const { Provider } = createContext(core);
|
||||
|
||||
ReactDOM.render(
|
||||
<Provider>
|
||||
<TestConsumer setting="a" newValue="c" />
|
||||
</Provider>,
|
||||
container
|
||||
);
|
||||
|
||||
expect(core.uiSettings!.set).toHaveBeenCalledTimes(0);
|
||||
|
||||
act(() => {
|
||||
Simulate.click(container!.querySelector('button')!, {});
|
||||
});
|
||||
|
||||
expect(core.uiSettings!.set).toHaveBeenCalledTimes(1);
|
||||
expect(core.uiSettings!.set).toHaveBeenCalledWith('a', 'c');
|
||||
});
|
|
@ -0,0 +1,50 @@
|
|||
/*
|
||||
* 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 { useCallback, useMemo } from 'react';
|
||||
import { useKibana } from '../core';
|
||||
import { useObservable } from '../util/use_observable';
|
||||
|
||||
type Setter<T> = (newValue: T) => Promise<boolean>;
|
||||
|
||||
/**
|
||||
* Returns a 2-tuple, where first entry is the setting value and second is a
|
||||
* function to update the setting value.
|
||||
*
|
||||
* Synchronously returns the most current value of the setting and subscribes
|
||||
* to all subsequent updates, which will re-render your component on new values.
|
||||
*
|
||||
* Usage:
|
||||
*
|
||||
* ```js
|
||||
* const [darkMode, setDarkMode] = useUiSetting('theme:darkMode');
|
||||
* ```
|
||||
*
|
||||
* @todo As of this writing `uiSettings` service exists only on *setup* `core`
|
||||
* object, but I assume it will be available on *start* `core` object, too,
|
||||
* thus postfix assertion is used `core.uiSetting!`.
|
||||
*/
|
||||
export const useUiSetting = <T>(key: string, defaultValue: T): [T, Setter<T>] => {
|
||||
const { core } = useKibana();
|
||||
const observable$ = useMemo(() => core.uiSettings!.get$(key, defaultValue), [key, defaultValue]);
|
||||
const value = useObservable<T>(observable$, core.uiSettings!.get(key, defaultValue));
|
||||
const set = useCallback((newValue: T) => core.uiSettings!.set(key, newValue), [key]);
|
||||
|
||||
return [value, set];
|
||||
};
|
54
src/plugins/kibana_react/public/util/use_observable.test.tsx
Normal file
54
src/plugins/kibana_react/public/util/use_observable.test.tsx
Normal file
|
@ -0,0 +1,54 @@
|
|||
/*
|
||||
* 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 { renderHook, act } from 'react-hooks-testing-library';
|
||||
import { Subject } from 'rxjs';
|
||||
import { useObservable } from './use_observable';
|
||||
|
||||
test('default initial value is undefined', () => {
|
||||
const subject$ = new Subject();
|
||||
const { result } = renderHook(() => useObservable(subject$));
|
||||
|
||||
expect(result.current).toBe(undefined);
|
||||
});
|
||||
|
||||
test('can specify initial value', () => {
|
||||
const subject$ = new Subject();
|
||||
const { result } = renderHook(() => useObservable(subject$, 123));
|
||||
|
||||
expect(result.current).toBe(123);
|
||||
});
|
||||
|
||||
test('returns the latest value of observables', () => {
|
||||
const subject$ = new Subject();
|
||||
const { result } = renderHook(() => useObservable(subject$, 123));
|
||||
|
||||
act(() => {
|
||||
subject$.next(125);
|
||||
});
|
||||
expect(result.current).toBe(125);
|
||||
|
||||
act(() => {
|
||||
subject$.next(300);
|
||||
subject$.next(400);
|
||||
});
|
||||
expect(result.current).toBe(400);
|
||||
});
|
||||
|
||||
xtest('subscribes to observable only once', () => {});
|
37
src/plugins/kibana_react/public/util/use_observable.ts
Normal file
37
src/plugins/kibana_react/public/util/use_observable.ts
Normal file
|
@ -0,0 +1,37 @@
|
|||
/*
|
||||
* 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 { useEffect, useState } from 'react';
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
export function useObservable<T>(observable$: Observable<T>): T | undefined;
|
||||
export function useObservable<T>(observable$: Observable<T>, initialValue: T): T;
|
||||
export function useObservable<T>(observable$: Observable<T>, initialValue?: T): T | undefined {
|
||||
const [value, update] = useState<T | undefined>(initialValue);
|
||||
|
||||
useEffect(
|
||||
() => {
|
||||
const s = observable$.subscribe(update);
|
||||
return () => s.unsubscribe();
|
||||
},
|
||||
[observable$]
|
||||
);
|
||||
|
||||
return value;
|
||||
}
|
|
@ -21834,6 +21834,13 @@ react-hooks-testing-library@^0.3.8:
|
|||
"@babel/runtime" "^7.4.2"
|
||||
react-testing-library "^6.0.2"
|
||||
|
||||
react-hooks-testing-library@^0.5.0:
|
||||
version "0.5.0"
|
||||
resolved "https://registry.yarnpkg.com/react-hooks-testing-library/-/react-hooks-testing-library-0.5.0.tgz#571af3522f88ea4ac23c634fb4deff84873f2bc2"
|
||||
integrity sha512-qX4SA28pcCCf1Q23Gtl1VKqQk26pSPTEsdLtfJanDqm4oacT5wadL+e2Xypk/H+AOXN5kdZrWmXkt+hAaiNHgg==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.4.2"
|
||||
|
||||
react-hotkeys@2.0.0-pre4:
|
||||
version "2.0.0-pre4"
|
||||
resolved "https://registry.yarnpkg.com/react-hotkeys/-/react-hotkeys-2.0.0-pre4.tgz#a1c248a51bdba4282c36bf3204f80d58abc73333"
|
||||
|
|
Loading…
Reference in a new issue