mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
* docs: ✏️ add demo command * feat: 🎸 add openFlyout method to kibana-react * chore: 🤖 revert master changes * refactor: 💡 update core types and use mocks provided by NP * chore: 🤖 remove old mock * feat: 🎸 export react-use context, use useUiSetting in infra * feat: 🎸 create React wrapper for Overlays service * chore: 🤖 App Architecture team as kibana-react code owners * feat: 🎸 improve kibana-react context interface * feat: 🎸 check for uiSettings service in useSetting hook * feat: 🎸 improve interface of KibanaReactOverlays * feat: 🎸 improve Dashboard container options * test: 💍 adapt tests to use context * fix: 🐛 fix TypeScript types * feat: 🎸 add notifications service wrapper * feat: 🎸 add withKibana HOC * feat: 🎸 add <UseKibana> render prop * refactor: 💡 use React context in DashboardGrid * test: 💍 use context in dashboard_grid tests * test: 💍 add tests for createReactOverlays() * test: 💍 add tests for React notification service wrapper * docs: ✏️ add kibana-react documentation * docs: ✏️ fixes to README * docs: ✏️ add overlays and notifications context examples * refactor: 💡 rename useUiSetting to useUiSetting$ * feat: 🎸 add useUiSetting hook * docs: ✏️ remove un-necessary HOC usage * feat: 🎸 add <KibanaContextProvider> component * refactor: 💡 rename createContext to createKibanaReactContext * feat: 🎸 make notifications and overlays always available * refactor: 💡 use <KibanaContextProvider> component in infra * test: 💍 fix dashboard embeddable tests * refactor: 💡 remove context creation from Dashboard factory * fix: 🐛 improve error messages * fix: 🐛 fix TypeScript type check
This commit is contained in:
parent
2b2ed4c686
commit
9d786da965
32 changed files with 1598 additions and 471 deletions
|
@ -17,7 +17,7 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
import { isErrorEmbeddable, EmbeddableFactory, GetEmbeddableFactory } from '../embeddable_api';
|
||||
import { isErrorEmbeddable, EmbeddableFactory } from '../embeddable_api';
|
||||
import { ExpandPanelAction } from './expand_panel_action';
|
||||
import { DashboardContainer } from '../embeddable';
|
||||
import { getSampleDashboardInput, getSampleDashboardPanel } from '../test_helpers';
|
||||
|
@ -30,29 +30,39 @@ import {
|
|||
ContactCardEmbeddableInput,
|
||||
ContactCardEmbeddableOutput,
|
||||
} from '../../../../../../embeddable_api/public/np_ready/public/lib/test_samples/embeddables/contact_card/contact_card_embeddable';
|
||||
import { DashboardOptions } from '../embeddable/dashboard_container_factory';
|
||||
|
||||
const __embeddableFactories = new Map<string, EmbeddableFactory>();
|
||||
__embeddableFactories.set(
|
||||
const embeddableFactories = new Map<string, EmbeddableFactory>();
|
||||
embeddableFactories.set(
|
||||
CONTACT_CARD_EMBEDDABLE,
|
||||
new ContactCardEmbeddableFactory({} as any, (() => null) as any, {} as any)
|
||||
);
|
||||
const getEmbeddableFactory: GetEmbeddableFactory = (id: string) => __embeddableFactories.get(id);
|
||||
|
||||
let container: DashboardContainer;
|
||||
let embeddable: ContactCardEmbeddable;
|
||||
|
||||
beforeEach(async () => {
|
||||
container = new DashboardContainer(
|
||||
getSampleDashboardInput({
|
||||
panels: {
|
||||
'123': getSampleDashboardPanel<ContactCardEmbeddableInput>({
|
||||
explicitInput: { firstName: 'Sam', id: '123' },
|
||||
type: CONTACT_CARD_EMBEDDABLE,
|
||||
}),
|
||||
},
|
||||
}),
|
||||
{ getEmbeddableFactory } as any
|
||||
);
|
||||
const options: DashboardOptions = {
|
||||
ExitFullScreenButton: () => null,
|
||||
SavedObjectFinder: () => null,
|
||||
application: {} as any,
|
||||
embeddable: {
|
||||
getEmbeddableFactory: (id: string) => embeddableFactories.get(id)!,
|
||||
} as any,
|
||||
inspector: {} as any,
|
||||
notifications: {} as any,
|
||||
overlays: {} as any,
|
||||
savedObjectMetaData: {} as any,
|
||||
};
|
||||
const input = getSampleDashboardInput({
|
||||
panels: {
|
||||
'123': getSampleDashboardPanel<ContactCardEmbeddableInput>({
|
||||
explicitInput: { firstName: 'Sam', id: '123' },
|
||||
type: CONTACT_CARD_EMBEDDABLE,
|
||||
}),
|
||||
},
|
||||
});
|
||||
container = new DashboardContainer(input, options);
|
||||
|
||||
const contactCardEmbeddable = await container.addNewEmbeddable<
|
||||
ContactCardEmbeddableInput,
|
||||
|
|
|
@ -21,7 +21,7 @@
|
|||
import { findTestSubject } from '@elastic/eui/lib/test';
|
||||
import { nextTick } from 'test_utils/enzyme_helpers';
|
||||
import { isErrorEmbeddable, ViewMode, EmbeddableFactory } from '../embeddable_api';
|
||||
import { DashboardContainer, ViewportProps } from './dashboard_container';
|
||||
import { DashboardContainer, DashboardContainerOptions } from './dashboard_container';
|
||||
import { getSampleDashboardInput, getSampleDashboardPanel } from '../test_helpers';
|
||||
import {
|
||||
CONTACT_CARD_EMBEDDABLE,
|
||||
|
@ -33,10 +33,13 @@ import {
|
|||
ContactCardEmbeddableOutput,
|
||||
} from '../../../../../../embeddable_api/public/np_ready/public/lib/test_samples/embeddables/contact_card/contact_card_embeddable';
|
||||
|
||||
const viewportProps: ViewportProps = {
|
||||
getActions: (() => []) as any,
|
||||
getAllEmbeddableFactories: (() => []) as any,
|
||||
getEmbeddableFactory: undefined as any,
|
||||
const options: DashboardContainerOptions = {
|
||||
application: {} as any,
|
||||
embeddable: {
|
||||
getTriggerCompatibleActions: (() => []) as any,
|
||||
getEmbeddableFactories: (() => []) as any,
|
||||
getEmbeddableFactory: undefined as any,
|
||||
} as any,
|
||||
notifications: {} as any,
|
||||
overlays: {} as any,
|
||||
inspector: {} as any,
|
||||
|
@ -45,26 +48,24 @@ const viewportProps: ViewportProps = {
|
|||
};
|
||||
|
||||
beforeEach(() => {
|
||||
const __embeddableFactories = new Map<string, EmbeddableFactory>();
|
||||
__embeddableFactories.set(
|
||||
const embeddableFactories = new Map<string, EmbeddableFactory>();
|
||||
embeddableFactories.set(
|
||||
CONTACT_CARD_EMBEDDABLE,
|
||||
new ContactCardEmbeddableFactory({} as any, (() => null) as any, {} as any)
|
||||
);
|
||||
viewportProps.getEmbeddableFactory = (id: string) => __embeddableFactories.get(id);
|
||||
options.embeddable.getEmbeddableFactory = (id: string) => embeddableFactories.get(id) as any;
|
||||
});
|
||||
|
||||
test('DashboardContainer initializes embeddables', async done => {
|
||||
const container = new DashboardContainer(
|
||||
getSampleDashboardInput({
|
||||
panels: {
|
||||
'123': getSampleDashboardPanel<ContactCardEmbeddableInput>({
|
||||
explicitInput: { firstName: 'Sam', id: '123' },
|
||||
type: CONTACT_CARD_EMBEDDABLE,
|
||||
}),
|
||||
},
|
||||
}),
|
||||
viewportProps
|
||||
);
|
||||
const initialInput = getSampleDashboardInput({
|
||||
panels: {
|
||||
'123': getSampleDashboardPanel<ContactCardEmbeddableInput>({
|
||||
explicitInput: { firstName: 'Sam', id: '123' },
|
||||
type: CONTACT_CARD_EMBEDDABLE,
|
||||
}),
|
||||
},
|
||||
});
|
||||
const container = new DashboardContainer(initialInput, options);
|
||||
|
||||
const subscription = container.getOutput$().subscribe(output => {
|
||||
if (container.getOutput().embeddableLoaded['123']) {
|
||||
|
@ -85,7 +86,7 @@ test('DashboardContainer initializes embeddables', async done => {
|
|||
});
|
||||
|
||||
test('DashboardContainer.addNewEmbeddable', async () => {
|
||||
const container = new DashboardContainer(getSampleDashboardInput(), viewportProps);
|
||||
const container = new DashboardContainer(getSampleDashboardInput(), options);
|
||||
const embeddable = await container.addNewEmbeddable<ContactCardEmbeddableInput>(
|
||||
CONTACT_CARD_EMBEDDABLE,
|
||||
{
|
||||
|
@ -106,17 +107,15 @@ test('DashboardContainer.addNewEmbeddable', async () => {
|
|||
});
|
||||
|
||||
test('Container view mode change propagates to existing children', async () => {
|
||||
const container = new DashboardContainer(
|
||||
getSampleDashboardInput({
|
||||
panels: {
|
||||
'123': getSampleDashboardPanel<ContactCardEmbeddableInput>({
|
||||
explicitInput: { firstName: 'Sam', id: '123' },
|
||||
type: CONTACT_CARD_EMBEDDABLE,
|
||||
}),
|
||||
},
|
||||
}),
|
||||
viewportProps
|
||||
);
|
||||
const initialInput = getSampleDashboardInput({
|
||||
panels: {
|
||||
'123': getSampleDashboardPanel<ContactCardEmbeddableInput>({
|
||||
explicitInput: { firstName: 'Sam', id: '123' },
|
||||
type: CONTACT_CARD_EMBEDDABLE,
|
||||
}),
|
||||
},
|
||||
});
|
||||
const container = new DashboardContainer(initialInput, options);
|
||||
await nextTick();
|
||||
|
||||
const embeddable = await container.getChild('123');
|
||||
|
@ -126,7 +125,7 @@ test('Container view mode change propagates to existing children', async () => {
|
|||
});
|
||||
|
||||
test('Container view mode change propagates to new children', async () => {
|
||||
const container = new DashboardContainer(getSampleDashboardInput(), viewportProps);
|
||||
const container = new DashboardContainer(getSampleDashboardInput(), options);
|
||||
const embeddable = await container.addNewEmbeddable<
|
||||
ContactCardEmbeddableInput,
|
||||
ContactCardEmbeddableOutput,
|
||||
|
|
|
@ -21,9 +21,7 @@ import React from 'react';
|
|||
import ReactDOM from 'react-dom';
|
||||
import { I18nProvider } from '@kbn/i18n/react';
|
||||
import { Filter } from '@kbn/es-query';
|
||||
import { CoreStart } from '../../../../../../../../core/public';
|
||||
import { RefreshInterval, TimeRange } from '../../../../../../../../plugins/data/public';
|
||||
|
||||
import {
|
||||
Container,
|
||||
ContainerInput,
|
||||
|
@ -31,16 +29,20 @@ import {
|
|||
ViewMode,
|
||||
EmbeddableFactory,
|
||||
IEmbeddable,
|
||||
GetEmbeddableFactory,
|
||||
GetActionsCompatibleWithTrigger,
|
||||
GetEmbeddableFactories,
|
||||
} from '../../../../../../embeddable_api/public/np_ready/public';
|
||||
import { DASHBOARD_CONTAINER_TYPE } from './dashboard_container_factory';
|
||||
import { createPanelState } from './panel';
|
||||
import { DashboardPanelState } from './types';
|
||||
import { DashboardViewport } from './viewport/dashboard_viewport';
|
||||
import { Query } from '../../../../../../data/public';
|
||||
import { CoreStart } from '../../../../../../../../core/public';
|
||||
import { Start as InspectorStartContract } from '../../../../../../../../plugins/inspector/public';
|
||||
import { Start as EmbeddableStartContract } from '../../../../../../embeddable_api/public/np_ready/public';
|
||||
import {
|
||||
KibanaReactContext,
|
||||
KibanaReactContextValue,
|
||||
KibanaContextProvider,
|
||||
} from '../../../../../../../../plugins/kibana_react/public';
|
||||
|
||||
export interface DashboardContainerInput extends ContainerInput {
|
||||
viewMode: ViewMode;
|
||||
|
@ -72,23 +74,25 @@ export interface InheritedChildInput extends IndexSignature {
|
|||
id: string;
|
||||
}
|
||||
|
||||
export interface ViewportProps {
|
||||
getActions: GetActionsCompatibleWithTrigger;
|
||||
getEmbeddableFactory: GetEmbeddableFactory;
|
||||
getAllEmbeddableFactories: GetEmbeddableFactories;
|
||||
export interface DashboardContainerOptions {
|
||||
application: CoreStart['application'];
|
||||
overlays: CoreStart['overlays'];
|
||||
notifications: CoreStart['notifications'];
|
||||
embeddable: EmbeddableStartContract;
|
||||
inspector: InspectorStartContract;
|
||||
SavedObjectFinder: React.ComponentType<any>;
|
||||
ExitFullScreenButton: React.ComponentType<any>;
|
||||
}
|
||||
|
||||
export type DashboardReactContextValue = KibanaReactContextValue<DashboardContainerOptions>;
|
||||
export type DashboardReactContext = KibanaReactContext<DashboardContainerOptions>;
|
||||
|
||||
export class DashboardContainer extends Container<InheritedChildInput, DashboardContainerInput> {
|
||||
public readonly type = DASHBOARD_CONTAINER_TYPE;
|
||||
|
||||
constructor(
|
||||
initialInput: DashboardContainerInput,
|
||||
private readonly viewportProps: ViewportProps,
|
||||
private readonly options: DashboardContainerOptions,
|
||||
parent?: Container
|
||||
) {
|
||||
super(
|
||||
|
@ -100,7 +104,7 @@ export class DashboardContainer extends Container<InheritedChildInput, Dashboard
|
|||
...initialInput,
|
||||
},
|
||||
{ embeddableLoaded: {} },
|
||||
viewportProps.getEmbeddableFactory,
|
||||
options.embeddable.getEmbeddableFactory,
|
||||
parent
|
||||
);
|
||||
}
|
||||
|
@ -119,7 +123,9 @@ export class DashboardContainer extends Container<InheritedChildInput, Dashboard
|
|||
public render(dom: HTMLElement) {
|
||||
ReactDOM.render(
|
||||
<I18nProvider>
|
||||
<DashboardViewport container={this} {...this.viewportProps} />
|
||||
<KibanaContextProvider services={this.options}>
|
||||
<DashboardViewport container={this} />
|
||||
</KibanaContextProvider>
|
||||
</I18nProvider>,
|
||||
dom
|
||||
);
|
||||
|
|
|
@ -20,25 +20,18 @@
|
|||
import { i18n } from '@kbn/i18n';
|
||||
import { SavedObjectAttributes } from '../../../../../../../../core/server';
|
||||
import { SavedObjectMetaData } from '../types';
|
||||
|
||||
import { ContainerOutput, EmbeddableFactory, ErrorEmbeddable, Container } from '../embeddable_api';
|
||||
import {
|
||||
ContainerOutput,
|
||||
EmbeddableFactory,
|
||||
ErrorEmbeddable,
|
||||
Container,
|
||||
GetEmbeddableFactory,
|
||||
} from '../embeddable_api';
|
||||
import { DashboardContainer, DashboardContainerInput, ViewportProps } from './dashboard_container';
|
||||
DashboardContainer,
|
||||
DashboardContainerInput,
|
||||
DashboardContainerOptions,
|
||||
} from './dashboard_container';
|
||||
import { DashboardCapabilities } from '../types';
|
||||
|
||||
export const DASHBOARD_CONTAINER_TYPE = 'dashboard';
|
||||
|
||||
export interface DashboardOptions {
|
||||
export interface DashboardOptions extends DashboardContainerOptions {
|
||||
savedObjectMetaData?: SavedObjectMetaData<SavedObjectAttributes>;
|
||||
capabilities: {
|
||||
showWriteControls: boolean;
|
||||
createNew: boolean;
|
||||
};
|
||||
getFactory: GetEmbeddableFactory;
|
||||
}
|
||||
|
||||
export class DashboardContainerFactory extends EmbeddableFactory<
|
||||
|
@ -47,11 +40,20 @@ export class DashboardContainerFactory extends EmbeddableFactory<
|
|||
> {
|
||||
public readonly isContainerType = true;
|
||||
public readonly type = DASHBOARD_CONTAINER_TYPE;
|
||||
private allowEditing: boolean;
|
||||
|
||||
constructor(options: DashboardOptions, private readonly containerOptions: ViewportProps) {
|
||||
private readonly allowEditing: boolean;
|
||||
|
||||
constructor(private readonly options: DashboardOptions) {
|
||||
super({ savedObjectMetaData: options.savedObjectMetaData });
|
||||
this.allowEditing = options.capabilities.createNew && options.capabilities.showWriteControls;
|
||||
|
||||
const capabilities = (options.application.capabilities
|
||||
.dashboard as unknown) as DashboardCapabilities;
|
||||
|
||||
if (!capabilities || typeof capabilities !== 'object') {
|
||||
throw new TypeError('Dashboard capabilities not found.');
|
||||
}
|
||||
|
||||
this.allowEditing = !!capabilities.createNew && !!capabilities.showWriteControls;
|
||||
}
|
||||
|
||||
public isEditable() {
|
||||
|
@ -76,6 +78,6 @@ export class DashboardContainerFactory extends EmbeddableFactory<
|
|||
initialInput: DashboardContainerInput,
|
||||
parent?: Container
|
||||
): Promise<DashboardContainer | ErrorEmbeddable> {
|
||||
return new DashboardContainer(initialInput, this.containerOptions, parent);
|
||||
return new DashboardContainer(initialInput, this.options, parent);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,56 +21,65 @@
|
|||
import sizeMe from 'react-sizeme';
|
||||
|
||||
import React from 'react';
|
||||
import { shallowWithIntl, nextTick, mountWithIntl } from 'test_utils/enzyme_helpers';
|
||||
import { nextTick, mountWithIntl } from 'test_utils/enzyme_helpers';
|
||||
import { skip } from 'rxjs/operators';
|
||||
import { EmbeddableFactory, GetEmbeddableFactory } from '../../embeddable_api';
|
||||
import { DashboardGrid, DashboardGridProps } from './dashboard_grid';
|
||||
import { DashboardContainer } from '../dashboard_container';
|
||||
import { DashboardContainer, DashboardContainerOptions } from '../dashboard_container';
|
||||
import { getSampleDashboardInput } from '../../test_helpers';
|
||||
import {
|
||||
CONTACT_CARD_EMBEDDABLE,
|
||||
ContactCardEmbeddableFactory,
|
||||
} from '../../../../../../../embeddable_api/public/np_ready/public/lib/test_samples/embeddables/contact_card/contact_card_embeddable_factory';
|
||||
import { KibanaContextProvider } from '../../../../../../../../../plugins/kibana_react/public';
|
||||
|
||||
let dashboardContainer: DashboardContainer | undefined;
|
||||
|
||||
function getProps(props?: Partial<DashboardGridProps>): DashboardGridProps {
|
||||
const __embeddableFactories = new Map<string, EmbeddableFactory>();
|
||||
__embeddableFactories.set(
|
||||
function prepare(props?: Partial<DashboardGridProps>) {
|
||||
const embeddableFactories = new Map<string, EmbeddableFactory>();
|
||||
embeddableFactories.set(
|
||||
CONTACT_CARD_EMBEDDABLE,
|
||||
new ContactCardEmbeddableFactory({} as any, (() => {}) as any, {} as any)
|
||||
);
|
||||
const getEmbeddableFactory: GetEmbeddableFactory = (id: string) => __embeddableFactories.get(id);
|
||||
|
||||
dashboardContainer = new DashboardContainer(
|
||||
getSampleDashboardInput({
|
||||
panels: {
|
||||
'1': {
|
||||
gridData: { x: 0, y: 0, w: 6, h: 6, i: '1' },
|
||||
type: CONTACT_CARD_EMBEDDABLE,
|
||||
explicitInput: { id: '1' },
|
||||
},
|
||||
'2': {
|
||||
gridData: { x: 6, y: 6, w: 6, h: 6, i: '2' },
|
||||
type: CONTACT_CARD_EMBEDDABLE,
|
||||
explicitInput: { id: '2' },
|
||||
},
|
||||
const getEmbeddableFactory: GetEmbeddableFactory = (id: string) => embeddableFactories.get(id);
|
||||
const initialInput = getSampleDashboardInput({
|
||||
panels: {
|
||||
'1': {
|
||||
gridData: { x: 0, y: 0, w: 6, h: 6, i: '1' },
|
||||
type: CONTACT_CARD_EMBEDDABLE,
|
||||
explicitInput: { id: '1' },
|
||||
},
|
||||
}),
|
||||
{ getEmbeddableFactory } as any
|
||||
);
|
||||
const defaultTestProps: DashboardGridProps = {
|
||||
container: dashboardContainer,
|
||||
intl: null as any,
|
||||
getActions: (() => []) as any,
|
||||
getAllEmbeddableFactories: (() => []) as any,
|
||||
getEmbeddableFactory: (() => {}) as any,
|
||||
'2': {
|
||||
gridData: { x: 6, y: 6, w: 6, h: 6, i: '2' },
|
||||
type: CONTACT_CARD_EMBEDDABLE,
|
||||
explicitInput: { id: '2' },
|
||||
},
|
||||
},
|
||||
});
|
||||
const options: DashboardContainerOptions = {
|
||||
application: {} as any,
|
||||
embeddable: {
|
||||
getTriggerCompatibleActions: (() => []) as any,
|
||||
getEmbeddableFactories: (() => []) as any,
|
||||
getEmbeddableFactory,
|
||||
} as any,
|
||||
notifications: {} as any,
|
||||
overlays: {} as any,
|
||||
inspector: {} as any,
|
||||
SavedObjectFinder: () => null,
|
||||
ExitFullScreenButton: () => null,
|
||||
};
|
||||
dashboardContainer = new DashboardContainer(initialInput, options);
|
||||
const defaultTestProps: DashboardGridProps = {
|
||||
container: dashboardContainer,
|
||||
kibana: null as any,
|
||||
intl: null as any,
|
||||
};
|
||||
|
||||
return {
|
||||
props: Object.assign(defaultTestProps, props),
|
||||
options,
|
||||
};
|
||||
return Object.assign(defaultTestProps, props);
|
||||
}
|
||||
|
||||
beforeAll(() => {
|
||||
|
@ -84,14 +93,25 @@ afterAll(() => {
|
|||
});
|
||||
|
||||
test('renders DashboardGrid', () => {
|
||||
const component = shallowWithIntl(<DashboardGrid.WrappedComponent {...getProps()} />);
|
||||
const panelElements = component.find('InjectIntl(EmbeddableChildPanelUi)');
|
||||
const { props, options } = prepare();
|
||||
const component = mountWithIntl(
|
||||
<KibanaContextProvider services={options}>
|
||||
<DashboardGrid {...props} />
|
||||
</KibanaContextProvider>
|
||||
);
|
||||
|
||||
const panelElements = component.find('EmbeddableChildPanelUi');
|
||||
expect(panelElements.length).toBe(2);
|
||||
});
|
||||
|
||||
test('renders DashboardGrid with no visualizations', async () => {
|
||||
const props = getProps();
|
||||
const component = shallowWithIntl(<DashboardGrid.WrappedComponent {...props} />);
|
||||
const { props, options } = prepare();
|
||||
const component = mountWithIntl(
|
||||
<KibanaContextProvider services={options}>
|
||||
<DashboardGrid {...props} />
|
||||
</KibanaContextProvider>
|
||||
);
|
||||
|
||||
props.container.updateInput({ panels: {} });
|
||||
await nextTick();
|
||||
component.update();
|
||||
|
@ -99,8 +119,13 @@ test('renders DashboardGrid with no visualizations', async () => {
|
|||
});
|
||||
|
||||
test('DashboardGrid removes panel when removed from container', async () => {
|
||||
const props = getProps();
|
||||
const component = shallowWithIntl(<DashboardGrid.WrappedComponent {...props} />);
|
||||
const { props, options } = prepare();
|
||||
const component = mountWithIntl(
|
||||
<KibanaContextProvider services={options}>
|
||||
<DashboardGrid {...props} />
|
||||
</KibanaContextProvider>
|
||||
);
|
||||
|
||||
const originalPanels = props.container.getInput().panels;
|
||||
const filteredPanels = { ...originalPanels };
|
||||
delete filteredPanels['1'];
|
||||
|
@ -112,27 +137,41 @@ test('DashboardGrid removes panel when removed from container', async () => {
|
|||
});
|
||||
|
||||
test('DashboardGrid renders expanded panel', async () => {
|
||||
const props = getProps();
|
||||
const component = shallowWithIntl(<DashboardGrid.WrappedComponent {...props} />);
|
||||
const { props, options } = prepare();
|
||||
const component = mountWithIntl(
|
||||
<KibanaContextProvider services={options}>
|
||||
<DashboardGrid {...props} />
|
||||
</KibanaContextProvider>
|
||||
);
|
||||
|
||||
props.container.updateInput({ expandedPanelId: '1' });
|
||||
await nextTick();
|
||||
component.update();
|
||||
// Both panels should still exist in the dom, so nothing needs to be re-fetched once minimized.
|
||||
expect(component.find('InjectIntl(EmbeddableChildPanelUi)').length).toBe(2);
|
||||
|
||||
expect((component.state() as { expandedPanelId?: string }).expandedPanelId).toBe('1');
|
||||
expect(
|
||||
(component.find('DashboardGridUi').state() as { expandedPanelId?: string }).expandedPanelId
|
||||
).toBe('1');
|
||||
|
||||
props.container.updateInput({ expandedPanelId: undefined });
|
||||
await nextTick();
|
||||
component.update();
|
||||
expect(component.find('InjectIntl(EmbeddableChildPanelUi)').length).toBe(2);
|
||||
|
||||
expect((component.state() as { expandedPanelId?: string }).expandedPanelId).toBeUndefined();
|
||||
expect(
|
||||
(component.find('DashboardGridUi').state() as { expandedPanelId?: string }).expandedPanelId
|
||||
).toBeUndefined();
|
||||
});
|
||||
|
||||
test('DashboardGrid unmount unsubscribes', async done => {
|
||||
const props = getProps();
|
||||
const component = mountWithIntl(<DashboardGrid.WrappedComponent {...props} />);
|
||||
const { props, options } = prepare();
|
||||
const component = mountWithIntl(
|
||||
<KibanaContextProvider services={options}>
|
||||
<DashboardGrid {...props} />
|
||||
</KibanaContextProvider>
|
||||
);
|
||||
|
||||
component.unmount();
|
||||
|
||||
props.container
|
||||
|
|
|
@ -28,17 +28,11 @@ import { Subscription } from 'rxjs';
|
|||
import ReactGridLayout, { Layout } from 'react-grid-layout';
|
||||
// @ts-ignore
|
||||
import sizeMe from 'react-sizeme';
|
||||
import {
|
||||
GetActionsCompatibleWithTrigger,
|
||||
GetEmbeddableFactory,
|
||||
GetEmbeddableFactories,
|
||||
} from 'src/legacy/core_plugins/embeddable_api/public/np_ready/public';
|
||||
import { CoreStart } from 'src/core/public';
|
||||
import { ViewMode, EmbeddableChildPanel } from '../../embeddable_api';
|
||||
import { DASHBOARD_GRID_COLUMN_COUNT, DASHBOARD_GRID_HEIGHT } from '../dashboard_constants';
|
||||
import { DashboardContainer } from '../dashboard_container';
|
||||
import { DashboardContainer, DashboardReactContextValue } from '../dashboard_container';
|
||||
import { DashboardPanelState, GridData } from '../types';
|
||||
import { Start as InspectorStartContract } from '../../../../../../../../../plugins/inspector/public';
|
||||
import { withKibana } from '../../../../../../../../../plugins/kibana_react/public';
|
||||
|
||||
let lastValidGridSize = 0;
|
||||
|
||||
|
@ -117,14 +111,8 @@ const config = { monitorWidth: true };
|
|||
const ResponsiveSizedGrid = sizeMe(config)(ResponsiveGrid);
|
||||
|
||||
export interface DashboardGridProps extends ReactIntl.InjectedIntlProps {
|
||||
kibana: DashboardReactContextValue;
|
||||
container: DashboardContainer;
|
||||
getActions: GetActionsCompatibleWithTrigger;
|
||||
getEmbeddableFactory: GetEmbeddableFactory;
|
||||
getAllEmbeddableFactories: GetEmbeddableFactories;
|
||||
overlays: CoreStart['overlays'];
|
||||
notifications: CoreStart['notifications'];
|
||||
inspector: InspectorStartContract;
|
||||
SavedObjectFinder: React.ComponentType<any>;
|
||||
}
|
||||
|
||||
interface State {
|
||||
|
@ -172,12 +160,13 @@ class DashboardGridUi extends React.Component<DashboardGridProps, State> {
|
|||
console.error(error); // eslint-disable-line no-console
|
||||
|
||||
isLayoutInvalid = true;
|
||||
this.props.notifications.toasts.addDanger({
|
||||
this.props.kibana.notifications.toasts.danger({
|
||||
title: this.props.intl.formatMessage({
|
||||
id: 'dashboardEmbeddableContainer.dashboardGrid.toast.unableToLoadDashboardDangerMessage',
|
||||
defaultMessage: 'Unable to load dashboard.',
|
||||
}),
|
||||
text: error.message,
|
||||
body: error.message,
|
||||
toastLifeTimeMs: 5000,
|
||||
});
|
||||
}
|
||||
this.setState({
|
||||
|
@ -241,7 +230,7 @@ class DashboardGridUi extends React.Component<DashboardGridProps, State> {
|
|||
}
|
||||
};
|
||||
|
||||
public renderDOM() {
|
||||
public renderPanels() {
|
||||
const { focusedPanelIndex, panels, expandedPanelId } = this.state;
|
||||
|
||||
// Part of our unofficial API - need to render in a consistent order for plugins.
|
||||
|
@ -277,13 +266,13 @@ class DashboardGridUi extends React.Component<DashboardGridProps, State> {
|
|||
<EmbeddableChildPanel
|
||||
embeddableId={panel.explicitInput.id}
|
||||
container={this.props.container}
|
||||
getActions={this.props.getActions}
|
||||
getEmbeddableFactory={this.props.getEmbeddableFactory}
|
||||
getAllEmbeddableFactories={this.props.getAllEmbeddableFactories}
|
||||
overlays={this.props.overlays}
|
||||
notifications={this.props.notifications}
|
||||
inspector={this.props.inspector}
|
||||
SavedObjectFinder={this.props.SavedObjectFinder}
|
||||
getActions={this.props.kibana.services.embeddable.getTriggerCompatibleActions}
|
||||
getEmbeddableFactory={this.props.kibana.services.embeddable.getEmbeddableFactory}
|
||||
getAllEmbeddableFactories={this.props.kibana.services.embeddable.getEmbeddableFactories}
|
||||
overlays={this.props.kibana.services.overlays}
|
||||
notifications={this.props.kibana.services.notifications}
|
||||
inspector={this.props.kibana.services.inspector}
|
||||
SavedObjectFinder={this.props.kibana.services.SavedObjectFinder}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
@ -305,10 +294,10 @@ class DashboardGridUi extends React.Component<DashboardGridProps, State> {
|
|||
maximizedPanelId={this.state.expandedPanelId}
|
||||
useMargins={this.state.useMargins}
|
||||
>
|
||||
{this.renderDOM()}
|
||||
{this.renderPanels()}
|
||||
</ResponsiveSizedGrid>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export const DashboardGrid = injectI18n(DashboardGridUi);
|
||||
export const DashboardGrid = injectI18n(withKibana(DashboardGridUi));
|
||||
|
|
|
@ -26,22 +26,34 @@ import { I18nProvider } from '@kbn/i18n/react';
|
|||
import { nextTick } from 'test_utils/enzyme_helpers';
|
||||
import { EmbeddableFactory } from '../../embeddable_api';
|
||||
import { DashboardViewport, DashboardViewportProps } from './dashboard_viewport';
|
||||
import { DashboardContainer, ViewportProps } from '../dashboard_container';
|
||||
import { DashboardContainer, DashboardContainerOptions } from '../dashboard_container';
|
||||
import { getSampleDashboardInput } from '../../test_helpers';
|
||||
import {
|
||||
CONTACT_CARD_EMBEDDABLE,
|
||||
ContactCardEmbeddableFactory,
|
||||
} from '../../../../../../../embeddable_api/public/np_ready/public/lib/test_samples/embeddables/contact_card/contact_card_embeddable_factory';
|
||||
import { KibanaContextProvider } from '../../../../../../../../../plugins/kibana_react/public';
|
||||
|
||||
let dashboardContainer: DashboardContainer | undefined;
|
||||
|
||||
const ExitFullScreenButton = () => <div data-test-subj="exitFullScreenModeText">EXIT</div>;
|
||||
|
||||
function getProps(props?: Partial<DashboardViewportProps>): DashboardViewportProps {
|
||||
const viewportProps: ViewportProps = {
|
||||
getActions: (() => []) as any,
|
||||
getAllEmbeddableFactories: (() => []) as any,
|
||||
getEmbeddableFactory: undefined as any,
|
||||
function getProps(
|
||||
props?: Partial<DashboardViewportProps>
|
||||
): { props: DashboardViewportProps; options: DashboardContainerOptions } {
|
||||
const embeddableFactories = new Map<string, EmbeddableFactory>();
|
||||
embeddableFactories.set(
|
||||
CONTACT_CARD_EMBEDDABLE,
|
||||
new ContactCardEmbeddableFactory({}, (() => null) as any, {} as any)
|
||||
);
|
||||
|
||||
const options: DashboardContainerOptions = {
|
||||
application: {} as any,
|
||||
embeddable: {
|
||||
getTriggerCompatibleActions: (() => []) as any,
|
||||
getEmbeddableFactories: (() => []) as any,
|
||||
getEmbeddableFactory: (id: string) => embeddableFactories.get(id),
|
||||
} as any,
|
||||
notifications: {} as any,
|
||||
overlays: {} as any,
|
||||
inspector: {} as any,
|
||||
|
@ -49,44 +61,39 @@ function getProps(props?: Partial<DashboardViewportProps>): DashboardViewportPro
|
|||
ExitFullScreenButton,
|
||||
};
|
||||
|
||||
const __embeddableFactories = new Map<string, EmbeddableFactory>();
|
||||
__embeddableFactories.set(
|
||||
CONTACT_CARD_EMBEDDABLE,
|
||||
new ContactCardEmbeddableFactory({}, (() => null) as any, {} as any)
|
||||
);
|
||||
viewportProps.getEmbeddableFactory = (id: string) => __embeddableFactories.get(id);
|
||||
|
||||
dashboardContainer = new DashboardContainer(
|
||||
getSampleDashboardInput({
|
||||
panels: {
|
||||
'1': {
|
||||
gridData: { x: 0, y: 0, w: 6, h: 6, i: '1' },
|
||||
type: CONTACT_CARD_EMBEDDABLE,
|
||||
explicitInput: { id: '1' },
|
||||
},
|
||||
'2': {
|
||||
gridData: { x: 6, y: 6, w: 6, h: 6, i: '2' },
|
||||
type: CONTACT_CARD_EMBEDDABLE,
|
||||
explicitInput: { id: '2' },
|
||||
},
|
||||
const input = getSampleDashboardInput({
|
||||
panels: {
|
||||
'1': {
|
||||
gridData: { x: 0, y: 0, w: 6, h: 6, i: '1' },
|
||||
type: CONTACT_CARD_EMBEDDABLE,
|
||||
explicitInput: { id: '1' },
|
||||
},
|
||||
}),
|
||||
viewportProps
|
||||
);
|
||||
'2': {
|
||||
gridData: { x: 6, y: 6, w: 6, h: 6, i: '2' },
|
||||
type: CONTACT_CARD_EMBEDDABLE,
|
||||
explicitInput: { id: '2' },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
dashboardContainer = new DashboardContainer(input, options);
|
||||
const defaultTestProps: DashboardViewportProps = {
|
||||
container: dashboardContainer,
|
||||
...viewportProps,
|
||||
inspector: {} as any,
|
||||
ExitFullScreenButton: () => null,
|
||||
};
|
||||
return Object.assign(defaultTestProps, props);
|
||||
|
||||
return {
|
||||
props: Object.assign(defaultTestProps, props),
|
||||
options,
|
||||
};
|
||||
}
|
||||
|
||||
test('renders DashboardViewport', () => {
|
||||
const props = getProps();
|
||||
const { props, options } = getProps();
|
||||
const component = mount(
|
||||
<I18nProvider>
|
||||
<DashboardViewport {...props} />
|
||||
<KibanaContextProvider services={options}>
|
||||
<DashboardViewport {...props} />
|
||||
</KibanaContextProvider>
|
||||
</I18nProvider>
|
||||
);
|
||||
const panels = findTestSubject(component, 'dashboardPanel');
|
||||
|
@ -94,11 +101,13 @@ test('renders DashboardViewport', () => {
|
|||
});
|
||||
|
||||
test('renders DashboardViewport with no visualizations', () => {
|
||||
const props = getProps();
|
||||
const { props, options } = getProps();
|
||||
props.container.updateInput({ panels: {} });
|
||||
const component = mount(
|
||||
<I18nProvider>
|
||||
<DashboardViewport {...props} />
|
||||
<KibanaContextProvider services={options}>
|
||||
<DashboardViewport {...props} />
|
||||
</KibanaContextProvider>
|
||||
</I18nProvider>
|
||||
);
|
||||
const panels = findTestSubject(component, 'dashboardPanel');
|
||||
|
@ -108,11 +117,13 @@ test('renders DashboardViewport with no visualizations', () => {
|
|||
});
|
||||
|
||||
test('renders exit full screen button when in full screen mode', async () => {
|
||||
const props = getProps();
|
||||
const { props, options } = getProps();
|
||||
props.container.updateInput({ isFullScreenMode: true });
|
||||
const component = mount(
|
||||
<I18nProvider>
|
||||
<DashboardViewport {...props} />
|
||||
<KibanaContextProvider services={options}>
|
||||
<DashboardViewport {...props} />
|
||||
</KibanaContextProvider>
|
||||
</I18nProvider>
|
||||
);
|
||||
|
||||
|
@ -138,10 +149,12 @@ test('renders exit full screen button when in full screen mode', async () => {
|
|||
});
|
||||
|
||||
test('DashboardViewport unmount unsubscribes', async done => {
|
||||
const props = getProps();
|
||||
const { props, options } = getProps();
|
||||
const component = mount(
|
||||
<I18nProvider>
|
||||
<DashboardViewport {...props} />
|
||||
<KibanaContextProvider services={options}>
|
||||
<DashboardViewport {...props} />
|
||||
</KibanaContextProvider>
|
||||
</I18nProvider>
|
||||
);
|
||||
component.unmount();
|
||||
|
|
|
@ -19,27 +19,13 @@
|
|||
|
||||
import React from 'react';
|
||||
import { Subscription } from 'rxjs';
|
||||
import {
|
||||
GetActionsCompatibleWithTrigger,
|
||||
GetEmbeddableFactory,
|
||||
GetEmbeddableFactories,
|
||||
} from 'src/legacy/core_plugins/embeddable_api/public/np_ready/public';
|
||||
import { CoreStart } from 'src/core/public';
|
||||
import { PanelState } from '../../embeddable_api';
|
||||
import { DashboardContainer } from '../dashboard_container';
|
||||
import { DashboardContainer, DashboardReactContextValue } from '../dashboard_container';
|
||||
import { DashboardGrid } from '../grid';
|
||||
import { Start as InspectorStartContract } from '../../../../../../../../../plugins/inspector/public';
|
||||
import { context } from '../../../../../../../../../plugins/kibana_react/public';
|
||||
|
||||
export interface DashboardViewportProps {
|
||||
container: DashboardContainer;
|
||||
getActions: GetActionsCompatibleWithTrigger;
|
||||
getEmbeddableFactory: GetEmbeddableFactory;
|
||||
getAllEmbeddableFactories: GetEmbeddableFactories;
|
||||
overlays: CoreStart['overlays'];
|
||||
notifications: CoreStart['notifications'];
|
||||
inspector: InspectorStartContract;
|
||||
SavedObjectFinder: React.ComponentType<any>;
|
||||
ExitFullScreenButton: React.ComponentType<any>;
|
||||
}
|
||||
|
||||
interface State {
|
||||
|
@ -51,6 +37,9 @@ interface State {
|
|||
}
|
||||
|
||||
export class DashboardViewport extends React.Component<DashboardViewportProps, State> {
|
||||
static contextType = context;
|
||||
|
||||
public readonly context!: DashboardReactContextValue;
|
||||
private subscription?: Subscription;
|
||||
private mounted: boolean = false;
|
||||
constructor(props: DashboardViewportProps) {
|
||||
|
@ -106,18 +95,11 @@ export class DashboardViewport extends React.Component<DashboardViewportProps, S
|
|||
}
|
||||
>
|
||||
{this.state.isFullScreenMode && (
|
||||
<this.props.ExitFullScreenButton onExitFullScreenMode={this.onExitFullScreenMode} />
|
||||
<this.context.services.ExitFullScreenButton
|
||||
onExitFullScreenMode={this.onExitFullScreenMode}
|
||||
/>
|
||||
)}
|
||||
<DashboardGrid
|
||||
container={container}
|
||||
getActions={this.props.getActions}
|
||||
getAllEmbeddableFactories={this.props.getAllEmbeddableFactories}
|
||||
getEmbeddableFactory={this.props.getEmbeddableFactory}
|
||||
notifications={this.props.notifications}
|
||||
overlays={this.props.overlays}
|
||||
inspector={this.props.inspector}
|
||||
SavedObjectFinder={this.props.SavedObjectFinder}
|
||||
/>
|
||||
<DashboardGrid container={container} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
|
||||
import { PluginInitializerContext, CoreSetup, CoreStart, Plugin } from 'src/core/public';
|
||||
import { CONTEXT_MENU_TRIGGER, Plugin as EmbeddablePlugin } from './lib/embeddable_api';
|
||||
import { ExpandPanelAction, DashboardContainerFactory, DashboardCapabilities } from './lib';
|
||||
import { ExpandPanelAction, DashboardContainerFactory } from './lib';
|
||||
import { Start as InspectorStartContract } from '../../../../../../plugins/inspector/public';
|
||||
|
||||
interface SetupDependencies {
|
||||
|
@ -52,16 +52,11 @@ export class DashboardEmbeddableContainerPublicPlugin
|
|||
const { application, notifications, overlays } = core;
|
||||
const { embeddable, inspector, __LEGACY } = plugins;
|
||||
|
||||
const dashboardOptions = {
|
||||
capabilities: (application.capabilities.dashboard as unknown) as DashboardCapabilities,
|
||||
getFactory: embeddable.getEmbeddableFactory,
|
||||
};
|
||||
const factory = new DashboardContainerFactory(dashboardOptions, {
|
||||
getActions: embeddable.getTriggerCompatibleActions,
|
||||
getAllEmbeddableFactories: embeddable.getEmbeddableFactories,
|
||||
getEmbeddableFactory: embeddable.getEmbeddableFactory,
|
||||
const factory = new DashboardContainerFactory({
|
||||
application,
|
||||
notifications,
|
||||
overlays,
|
||||
embeddable,
|
||||
inspector,
|
||||
SavedObjectFinder: __LEGACY.SavedObjectFinder,
|
||||
ExitFullScreenButton: __LEGACY.ExitFullScreenButton,
|
||||
|
|
|
@ -24,7 +24,10 @@ import { mount } from 'enzyme';
|
|||
import { nextTick } from 'test_utils/enzyme_helpers';
|
||||
import { I18nProvider } from '@kbn/i18n/react';
|
||||
import { ViewMode, CONTEXT_MENU_TRIGGER, EmbeddablePanel } from '../lib/embeddable_api';
|
||||
import { DashboardContainer } from '../lib/embeddable/dashboard_container';
|
||||
import {
|
||||
DashboardContainer,
|
||||
DashboardContainerOptions,
|
||||
} from '../lib/embeddable/dashboard_container';
|
||||
import { getSampleDashboardInput } from '../lib/test_helpers';
|
||||
import {
|
||||
CONTACT_CARD_EMBEDDABLE,
|
||||
|
@ -39,6 +42,7 @@ import { embeddablePluginMock } from '../../../../../embeddable_api/public/np_re
|
|||
import { EditModeAction } from '../../../../../embeddable_api/public/np_ready/public/lib/test_samples/actions/edit_mode_action';
|
||||
// eslint-disable-next-line
|
||||
import { inspectorPluginMock } from '../../../../../../../plugins/inspector/public/mocks';
|
||||
import { KibanaContextProvider } from '../../../../../../../plugins/kibana_react/public';
|
||||
|
||||
test('DashboardContainer in edit mode shows edit mode actions', async () => {
|
||||
const inspector = inspectorPluginMock.createStartContract();
|
||||
|
@ -54,9 +58,17 @@ test('DashboardContainer in edit mode shows edit mode actions', async () => {
|
|||
|
||||
const start = doStart();
|
||||
|
||||
const container = new DashboardContainer(getSampleDashboardInput({ viewMode: ViewMode.VIEW }), {
|
||||
getEmbeddableFactory: start.getEmbeddableFactory,
|
||||
} as any);
|
||||
const initialInput = getSampleDashboardInput({ viewMode: ViewMode.VIEW });
|
||||
const options: DashboardContainerOptions = {
|
||||
application: {} as any,
|
||||
embeddable: start,
|
||||
notifications: {} as any,
|
||||
overlays: {} as any,
|
||||
inspector: {} as any,
|
||||
SavedObjectFinder: () => null,
|
||||
ExitFullScreenButton: () => null,
|
||||
};
|
||||
const container = new DashboardContainer(initialInput, options);
|
||||
|
||||
const embeddable = await container.addNewEmbeddable<
|
||||
ContactCardEmbeddableInput,
|
||||
|
@ -68,16 +80,18 @@ test('DashboardContainer in edit mode shows edit mode actions', async () => {
|
|||
|
||||
const component = mount(
|
||||
<I18nProvider>
|
||||
<EmbeddablePanel
|
||||
embeddable={embeddable}
|
||||
getActions={() => Promise.resolve([])}
|
||||
getAllEmbeddableFactories={(() => []) as any}
|
||||
getEmbeddableFactory={(() => null) as any}
|
||||
notifications={{} as any}
|
||||
overlays={{} as any}
|
||||
inspector={inspector}
|
||||
SavedObjectFinder={() => null}
|
||||
/>
|
||||
<KibanaContextProvider services={options}>
|
||||
<EmbeddablePanel
|
||||
embeddable={embeddable}
|
||||
getActions={() => Promise.resolve([])}
|
||||
getAllEmbeddableFactories={(() => []) as any}
|
||||
getEmbeddableFactory={(() => null) as any}
|
||||
notifications={{} as any}
|
||||
overlays={{} as any}
|
||||
inspector={inspector}
|
||||
SavedObjectFinder={() => null}
|
||||
/>
|
||||
</KibanaContextProvider>
|
||||
</I18nProvider>
|
||||
);
|
||||
|
||||
|
|
|
@ -70,3 +70,4 @@ export function plugin(initializerContext: PluginInitializerContext) {
|
|||
}
|
||||
|
||||
export { EmbeddablePublicPlugin as Plugin };
|
||||
export * from './plugin';
|
||||
|
|
|
@ -20,7 +20,8 @@ import { i18n } from '@kbn/i18n';
|
|||
import { ViewMode, GetEmbeddableFactory, GetEmbeddableFactories } from '../../../../types';
|
||||
import { Action, ActionContext } from '../../../../actions';
|
||||
import { openAddPanelFlyout } from './open_add_panel_flyout';
|
||||
import { OverlayStart, NotificationsStart } from '../../../../../../../../../../../core/public';
|
||||
import { NotificationsStart } from '../../../../../../../../../../../core/public';
|
||||
import { KibanaReactOverlays } from '../../../../../../../../../../../plugins/kibana_react/public';
|
||||
|
||||
export const ADD_PANEL_ACTION_ID = 'ADD_PANEL_ACTION_ID';
|
||||
|
||||
|
@ -30,7 +31,7 @@ export class AddPanelAction extends Action {
|
|||
constructor(
|
||||
private readonly getFactory: GetEmbeddableFactory,
|
||||
private readonly getAllFactories: GetEmbeddableFactories,
|
||||
private readonly overlays: OverlayStart,
|
||||
private readonly overlays: KibanaReactOverlays,
|
||||
private readonly notifications: NotificationsStart,
|
||||
private readonly SavedObjectFinder: React.ComponentType<any>
|
||||
) {
|
||||
|
|
|
@ -20,13 +20,14 @@ import React from 'react';
|
|||
import { IContainer } from '../../../../containers';
|
||||
import { AddPanelFlyout } from './add_panel_flyout';
|
||||
import { GetEmbeddableFactory, GetEmbeddableFactories } from '../../../../types';
|
||||
import { OverlayStart, NotificationsStart } from '../../../../../../../../../../../core/public';
|
||||
import { NotificationsStart } from '../../../../../../../../../../../core/public';
|
||||
import { KibanaReactOverlays } from '../../../../../../../../../../../plugins/kibana_react/public';
|
||||
|
||||
export async function openAddPanelFlyout(options: {
|
||||
embeddable: IContainer;
|
||||
getFactory: GetEmbeddableFactory;
|
||||
getAllFactories: GetEmbeddableFactories;
|
||||
overlays: OverlayStart;
|
||||
overlays: KibanaReactOverlays;
|
||||
notifications: NotificationsStart;
|
||||
SavedObjectFinder: React.ComponentType<any>;
|
||||
}) {
|
||||
|
|
236
src/plugins/kibana_react/README.md
Normal file
236
src/plugins/kibana_react/README.md
Normal file
|
@ -0,0 +1,236 @@
|
|||
# `kibana-react`
|
||||
|
||||
Tools for building React applications in Kibana.
|
||||
|
||||
|
||||
## Context
|
||||
|
||||
You can create React context that holds Core or plugin services that your plugin depends on.
|
||||
|
||||
```ts
|
||||
import { createContext } from 'kibana-react';
|
||||
|
||||
class MyPlugin {
|
||||
start(core, plugins) {
|
||||
const context = createContext({ ...core, ...plugins });
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
You may also want to be explicit about services you depend on.
|
||||
|
||||
```ts
|
||||
import { createContext } from 'kibana-react';
|
||||
|
||||
class MyPlugin {
|
||||
start({ notifications, overlays }, { embeddable }) {
|
||||
const context = createContext({ notifications, overlays, embeddable });
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Wrap your React application in the created context.
|
||||
|
||||
```jsx
|
||||
<context.Provider>
|
||||
<KibanaApplication />
|
||||
</context.Provider>
|
||||
```
|
||||
|
||||
Or use already pre-created `<KibanaContextProvider>` component.
|
||||
|
||||
```jsx
|
||||
import { KibanaContextProvider } from 'kibana-react';
|
||||
|
||||
<KibanaContextProvider services={{ ...core, ...plugins }}>
|
||||
<KibanaApplication />
|
||||
</KibanaContextProvider>
|
||||
|
||||
<KibanaContextProvider services={{ notifications, overlays, embeddable }}>
|
||||
<KibanaApplication />
|
||||
</KibanaContextProvider>
|
||||
```
|
||||
|
||||
|
||||
## Accessing context
|
||||
|
||||
Using `useKibana` hook.
|
||||
|
||||
```tsx
|
||||
import { useKibana } from 'kibana-react';
|
||||
|
||||
const Demo = () => {
|
||||
const kibana = useKibana();
|
||||
return (
|
||||
<div>
|
||||
{kibana.services.uiSettings.get('theme:darkMode') ? 'dark' : 'light'}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
Using `withKibana()` higher order component.
|
||||
|
||||
```tsx
|
||||
import { withKibana } from 'kibana-react';
|
||||
|
||||
const Demo = ({ kibana }) => {
|
||||
return (
|
||||
<div>
|
||||
{kibana.services.uiSettings.get('theme:darkMode') ? 'dark' : 'light'}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default withKibana(Demo);
|
||||
```
|
||||
|
||||
Using `<UseKibana>` render prop.
|
||||
|
||||
```tsx
|
||||
import { UseKibana } from 'kibana-react';
|
||||
|
||||
const Demo = () => {
|
||||
return (
|
||||
<UseKibana>{kibana =>
|
||||
<div>
|
||||
{kibana.services.uiSettings.get('theme:darkMode') ? 'dark' : 'light'}
|
||||
</div>
|
||||
}</UseKibana>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
|
||||
## `uiSettings` service
|
||||
|
||||
Wrappers around Core's `uiSettings` service.
|
||||
|
||||
|
||||
### `useUiSetting` hook
|
||||
|
||||
`useUiSetting` synchronously returns the latest setting from `CoreStart['uiSettings']` service.
|
||||
|
||||
```tsx
|
||||
import { useUiSetting } from 'kibana-react';
|
||||
|
||||
const Demo = () => {
|
||||
const darkMode = useUiSetting<boolean>('theme:darkMode');
|
||||
return (
|
||||
<div>
|
||||
{darkMode ? 'dark' : 'light'}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
#### Reference
|
||||
|
||||
```tsx
|
||||
useUiSetting<T>(key: string, defaultValue: T): T;
|
||||
```
|
||||
|
||||
|
||||
### `useUiSetting$` hook
|
||||
|
||||
`useUiSetting$` synchronously returns the latest setting from `CoreStart['uiSettings']` service and
|
||||
subscribes to changes, re-rendering your component with latest values.
|
||||
|
||||
```tsx
|
||||
import { useUiSetting$ } from 'kibana-react';
|
||||
|
||||
const Demo = () => {
|
||||
const [darkMode] = useUiSetting$<boolean>('theme:darkMode');
|
||||
return (
|
||||
<div>
|
||||
{darkMode ? 'dark' : 'light'}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
#### Reference
|
||||
|
||||
```tsx
|
||||
useUiSetting$<T>(key: string, defaultValue: T): [T, (newValue: T) => void];
|
||||
```
|
||||
|
||||
|
||||
## `overlays` service
|
||||
|
||||
Wrapper around Core's `overlays` service, allows you to display React modals and flyouts
|
||||
directly without having to use `react-dom` library to mount to DOM nodes.
|
||||
|
||||
```tsx
|
||||
import { createContext } from 'kibana-react';
|
||||
|
||||
class MyPlugin {
|
||||
start(core) {
|
||||
const { value: { overlays } } = createContext(core);
|
||||
|
||||
overlays.openModal(
|
||||
<div>
|
||||
Hello world!
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- `overlays.openModal` — opens modal window.
|
||||
- `overlays.openFlyout` — opens right side panel.
|
||||
|
||||
You can access `overlays` service through React context.
|
||||
|
||||
```tsx
|
||||
const Demo = () => {
|
||||
const { overlays } = useKibana();
|
||||
useEffect(() => {
|
||||
overlays.openModal(
|
||||
<div>
|
||||
Oooops! {errorMessage}
|
||||
</div>
|
||||
);
|
||||
}, [errorMessage]);
|
||||
};
|
||||
```
|
||||
|
||||
|
||||
## `notifications` service
|
||||
|
||||
Wrapper around Core's `notifications` service, allows you to render React elements
|
||||
directly without having to use `react-dom` library to mount to DOM nodes.
|
||||
|
||||
```tsx
|
||||
import { createContext } from 'kibana-react';
|
||||
|
||||
class MyPlugin {
|
||||
start(core) {
|
||||
const { value: { notifications } } = createContext(core);
|
||||
|
||||
notifications.toasts.show({
|
||||
title: <div>Hello</div>,
|
||||
body: <div>world!</div>
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- `notifications.toasts.show()` — show generic toast message.
|
||||
- `notifications.toasts.success()` — show positive toast message.
|
||||
- `notifications.toasts.warning()` — show warning toast message.
|
||||
- `notifications.toasts.danger()` — show error toast message.
|
||||
|
||||
You can access `notifications` service through React context.
|
||||
|
||||
```tsx
|
||||
const Demo = () => {
|
||||
const { notifications } = useKibana();
|
||||
useEffect(() => {
|
||||
notifications.toasts.danger({
|
||||
title: 'Oooops!',
|
||||
body: errorMessage,
|
||||
});
|
||||
}, [errorMessage]);
|
||||
};
|
||||
```
|
271
src/plugins/kibana_react/public/context/context.test.tsx
Normal file
271
src/plugins/kibana_react/public/context/context.test.tsx
Normal file
|
@ -0,0 +1,271 @@
|
|||
/*
|
||||
* 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, createKibanaReactContext, useKibana, KibanaContextProvider } from './context';
|
||||
import { coreMock } from '../../../../core/public/mocks';
|
||||
import { CoreStart } from './types';
|
||||
|
||||
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 services = coreMock.createStart();
|
||||
ReactDOM.render(
|
||||
<context.Provider value={{ services } as any}>
|
||||
<div>Hello world</div>
|
||||
</context.Provider>,
|
||||
container
|
||||
);
|
||||
});
|
||||
|
||||
const TestConsumer = () => {
|
||||
const { services } = useKibana<{ foo: string }>();
|
||||
return <div>{services.foo}</div>;
|
||||
};
|
||||
|
||||
test('useKibana() hook retrieves Kibana context', () => {
|
||||
const core = coreMock.createStart();
|
||||
(core as any).foo = 'bar';
|
||||
ReactDOM.render(
|
||||
<context.Provider value={{ services: core } as any}>
|
||||
<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 services = {
|
||||
foo: 'baz',
|
||||
} as Partial<CoreStart>;
|
||||
const { Provider } = createKibanaReactContext(services);
|
||||
|
||||
ReactDOM.render(
|
||||
<Provider>
|
||||
<TestConsumer />
|
||||
</Provider>,
|
||||
container
|
||||
);
|
||||
|
||||
const div = container!.querySelector('div');
|
||||
expect(div!.textContent).toBe('baz');
|
||||
});
|
||||
|
||||
test('services, notifications and overlays objects are always available', () => {
|
||||
const { Provider } = createKibanaReactContext({});
|
||||
const Test: React.FC = () => {
|
||||
const kibana = useKibana();
|
||||
expect(kibana).toMatchObject({
|
||||
services: expect.any(Object),
|
||||
notifications: expect.any(Object),
|
||||
overlays: expect.any(Object),
|
||||
});
|
||||
return null;
|
||||
};
|
||||
|
||||
ReactDOM.render(
|
||||
<Provider>
|
||||
<Test />
|
||||
</Provider>,
|
||||
container
|
||||
);
|
||||
});
|
||||
|
||||
test('<KibanaContextProvider> provider provides default kibana-react context', () => {
|
||||
const Test: React.FC = () => {
|
||||
const kibana = useKibana();
|
||||
expect(kibana).toMatchObject({
|
||||
services: expect.any(Object),
|
||||
notifications: expect.any(Object),
|
||||
overlays: expect.any(Object),
|
||||
});
|
||||
return null;
|
||||
};
|
||||
|
||||
ReactDOM.render(
|
||||
<KibanaContextProvider>
|
||||
<Test />
|
||||
</KibanaContextProvider>,
|
||||
container
|
||||
);
|
||||
});
|
||||
|
||||
test('<KibanaContextProvider> can set custom services in context', () => {
|
||||
const Test: React.FC = () => {
|
||||
const { services } = useKibana();
|
||||
expect(services.test).toBe('quux');
|
||||
return null;
|
||||
};
|
||||
|
||||
ReactDOM.render(
|
||||
<KibanaContextProvider services={{ test: 'quux' }}>
|
||||
<Test />
|
||||
</KibanaContextProvider>,
|
||||
container
|
||||
);
|
||||
});
|
||||
|
||||
test('nested <KibanaContextProvider> override and merge services', () => {
|
||||
const Test: React.FC = () => {
|
||||
const { services } = useKibana();
|
||||
expect(services.foo).toBe('foo2');
|
||||
expect(services.bar).toBe('bar');
|
||||
expect(services.baz).toBe('baz3');
|
||||
return null;
|
||||
};
|
||||
|
||||
ReactDOM.render(
|
||||
<KibanaContextProvider services={{ foo: 'foo', bar: 'bar', baz: 'baz' }}>
|
||||
<KibanaContextProvider services={{ foo: 'foo2' }}>
|
||||
<KibanaContextProvider services={{ baz: 'baz3' }}>
|
||||
<Test />
|
||||
</KibanaContextProvider>
|
||||
</KibanaContextProvider>
|
||||
</KibanaContextProvider>,
|
||||
container
|
||||
);
|
||||
});
|
||||
|
||||
test('overlays wrapper uses the closest overlays service', () => {
|
||||
const Test: React.FC = () => {
|
||||
const { overlays } = useKibana();
|
||||
overlays.openFlyout({} as any);
|
||||
overlays.openModal({} as any);
|
||||
return null;
|
||||
};
|
||||
|
||||
const core1 = {
|
||||
overlays: {
|
||||
openFlyout: jest.fn(),
|
||||
openModal: jest.fn(),
|
||||
},
|
||||
} as Partial<CoreStart>;
|
||||
|
||||
const core2 = {
|
||||
overlays: {
|
||||
openFlyout: jest.fn(),
|
||||
openModal: jest.fn(),
|
||||
},
|
||||
} as Partial<CoreStart>;
|
||||
|
||||
ReactDOM.render(
|
||||
<KibanaContextProvider services={core1}>
|
||||
<KibanaContextProvider services={core2}>
|
||||
<Test />
|
||||
</KibanaContextProvider>
|
||||
</KibanaContextProvider>,
|
||||
container
|
||||
);
|
||||
|
||||
expect(core1.overlays!.openFlyout).toHaveBeenCalledTimes(0);
|
||||
expect(core1.overlays!.openModal).toHaveBeenCalledTimes(0);
|
||||
expect(core2.overlays!.openFlyout).toHaveBeenCalledTimes(1);
|
||||
expect(core2.overlays!.openModal).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('notifications wrapper uses the closest notifications service', () => {
|
||||
const Test: React.FC = () => {
|
||||
const { notifications } = useKibana();
|
||||
notifications.toasts.show({} as any);
|
||||
return null;
|
||||
};
|
||||
|
||||
const core1 = {
|
||||
notifications: ({
|
||||
toasts: {
|
||||
add: jest.fn(),
|
||||
},
|
||||
} as unknown) as CoreStart['notifications'],
|
||||
} as Partial<CoreStart>;
|
||||
|
||||
const core2 = {
|
||||
notifications: ({
|
||||
toasts: {
|
||||
add: jest.fn(),
|
||||
},
|
||||
} as unknown) as CoreStart['notifications'],
|
||||
} as Partial<CoreStart>;
|
||||
|
||||
ReactDOM.render(
|
||||
<KibanaContextProvider services={core1}>
|
||||
<KibanaContextProvider services={core2}>
|
||||
<Test />
|
||||
</KibanaContextProvider>
|
||||
</KibanaContextProvider>,
|
||||
container
|
||||
);
|
||||
|
||||
expect(core1.notifications!.toasts.add).toHaveBeenCalledTimes(0);
|
||||
expect(core2.notifications!.toasts.add).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('overlays wrapper uses available overlays service, higher up in <KibanaContextProvider> tree', () => {
|
||||
const Test: React.FC = () => {
|
||||
const { overlays } = useKibana();
|
||||
overlays.openFlyout({} as any);
|
||||
return null;
|
||||
};
|
||||
|
||||
const core1 = {
|
||||
overlays: {
|
||||
openFlyout: jest.fn(),
|
||||
openModal: jest.fn(),
|
||||
},
|
||||
notifications: ({
|
||||
toasts: {
|
||||
add: jest.fn(),
|
||||
},
|
||||
} as unknown) as CoreStart['notifications'],
|
||||
} as Partial<CoreStart>;
|
||||
|
||||
const core2 = {
|
||||
notifications: ({
|
||||
toasts: {
|
||||
add: jest.fn(),
|
||||
},
|
||||
} as unknown) as CoreStart['notifications'],
|
||||
} as Partial<CoreStart>;
|
||||
|
||||
expect(core1.overlays!.openFlyout).toHaveBeenCalledTimes(0);
|
||||
|
||||
ReactDOM.render(
|
||||
<KibanaContextProvider services={core1}>
|
||||
<KibanaContextProvider services={core2}>
|
||||
<Test />
|
||||
</KibanaContextProvider>
|
||||
</KibanaContextProvider>,
|
||||
container
|
||||
);
|
||||
|
||||
expect(core1.overlays!.openFlyout).toHaveBeenCalledTimes(1);
|
||||
});
|
87
src/plugins/kibana_react/public/context/context.tsx
Normal file
87
src/plugins/kibana_react/public/context/context.tsx
Normal file
|
@ -0,0 +1,87 @@
|
|||
/*
|
||||
* 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 { KibanaReactContext, KibanaReactContextValue, KibanaServices } from './types';
|
||||
import { createReactOverlays } from '../overlays';
|
||||
import { createNotifications } from '../notifications';
|
||||
|
||||
const { useMemo, useContext, createElement, createContext } = React;
|
||||
|
||||
const defaultContextValue = {
|
||||
services: {},
|
||||
overlays: createReactOverlays({}),
|
||||
notifications: createNotifications({}),
|
||||
};
|
||||
|
||||
export const context = createContext<KibanaReactContextValue<KibanaServices>>(defaultContextValue);
|
||||
|
||||
export const useKibana = <Extra extends object = {}>(): KibanaReactContextValue<
|
||||
KibanaServices & Extra
|
||||
> =>
|
||||
useContext((context as unknown) as React.Context<
|
||||
KibanaReactContextValue<KibanaServices & Extra>
|
||||
>);
|
||||
|
||||
export const withKibana = <Props extends { kibana: KibanaReactContextValue<any> }>(
|
||||
type: React.ComponentType<Props>
|
||||
): React.FC<Omit<Props, 'kibana'>> => {
|
||||
const enhancedType: React.FC<Omit<Props, 'kibana'>> = (props: Omit<Props, 'kibana'>) => {
|
||||
const kibana = useKibana();
|
||||
return React.createElement(type, { ...props, kibana } as Props);
|
||||
};
|
||||
return enhancedType;
|
||||
};
|
||||
|
||||
export const UseKibana: React.FC<{
|
||||
children: (kibana: KibanaReactContextValue<any>) => React.ReactNode;
|
||||
}> = ({ children }) => <>{children(useKibana())}</>;
|
||||
|
||||
export const createKibanaReactContext = <Services extends KibanaServices>(
|
||||
services: Services
|
||||
): KibanaReactContext<Services> => {
|
||||
const value: KibanaReactContextValue<Services> = {
|
||||
services,
|
||||
overlays: createReactOverlays(services),
|
||||
notifications: createNotifications(services),
|
||||
};
|
||||
|
||||
const Provider: React.FC<{ services?: Services }> = ({
|
||||
services: newServices = {},
|
||||
children,
|
||||
}) => {
|
||||
const oldValue = useKibana();
|
||||
const { value: newValue } = useMemo(
|
||||
() => createKibanaReactContext({ ...services, ...oldValue.services, ...newServices }),
|
||||
Object.keys(services)
|
||||
);
|
||||
return createElement(context.Provider as React.ComponentType<any>, {
|
||||
value: newValue,
|
||||
children,
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
value,
|
||||
Provider,
|
||||
Consumer: (context.Consumer as unknown) as React.Consumer<KibanaReactContextValue<Services>>,
|
||||
};
|
||||
};
|
||||
|
||||
export const { Provider: KibanaContextProvider } = createKibanaReactContext({});
|
|
@ -17,12 +17,12 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
import { CoreSetup, CoreStart } from '../../../../core/public';
|
||||
|
||||
export { CoreSetup, CoreStart };
|
||||
|
||||
export type Core = Partial<CoreSetup> & Partial<CoreStart>;
|
||||
|
||||
export interface KibanaReactContextValue {
|
||||
core: Core;
|
||||
}
|
||||
export {
|
||||
context,
|
||||
createKibanaReactContext,
|
||||
KibanaContextProvider,
|
||||
useKibana,
|
||||
withKibana,
|
||||
UseKibana,
|
||||
} from './context';
|
||||
export { KibanaReactContext, KibanaReactContextValue } from './types';
|
|
@ -17,34 +17,23 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
import { Core } from './types';
|
||||
import * as React from 'react';
|
||||
import { CoreStart } from '../../../../core/public';
|
||||
import { KibanaReactOverlays } from '../overlays';
|
||||
import { KibanaReactNotifications } from '../notifications';
|
||||
|
||||
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 { CoreStart };
|
||||
|
||||
export const createMock = (): Core => {
|
||||
const uiSettings = createSetupContractMock();
|
||||
export type KibanaServices = Partial<CoreStart>;
|
||||
|
||||
const core: Partial<Core> = {
|
||||
uiSettings,
|
||||
};
|
||||
export interface KibanaReactContextValue<Services extends KibanaServices> {
|
||||
readonly services: Services;
|
||||
readonly overlays: KibanaReactOverlays;
|
||||
readonly notifications: KibanaReactNotifications;
|
||||
}
|
||||
|
||||
return core as Core;
|
||||
};
|
||||
export interface KibanaReactContext<T extends KibanaServices> {
|
||||
value: KibanaReactContextValue<T>;
|
||||
Provider: React.FC<{ services?: T }>;
|
||||
Consumer: React.Consumer<KibanaReactContextValue<T>>;
|
||||
}
|
|
@ -1,80 +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 * 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');
|
||||
});
|
|
@ -17,5 +17,6 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
export { context, createContext, useKibana } from './context';
|
||||
export { KibanaReactContextValue } from './types';
|
||||
export * from './context';
|
||||
export * from './overlays';
|
||||
export * from './ui_settings';
|
|
@ -0,0 +1,153 @@
|
|||
/*
|
||||
* 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 { createNotifications } from './create_notifications';
|
||||
// eslint-disable-next-lien
|
||||
import { notificationServiceMock } from '../../../../core/public/mocks';
|
||||
|
||||
test('throws if no overlays service provided', () => {
|
||||
const notifications = createNotifications({});
|
||||
expect(() => notifications.toasts.show({})).toThrowErrorMatchingInlineSnapshot(
|
||||
`"Could not show notification as notifications service is not available."`
|
||||
);
|
||||
});
|
||||
|
||||
test('creates wrapped notifications service', () => {
|
||||
const notifications = notificationServiceMock.createStartContract();
|
||||
const wrapper = createNotifications({ notifications });
|
||||
|
||||
expect(wrapper).toMatchObject({
|
||||
toasts: {
|
||||
show: expect.any(Function),
|
||||
success: expect.any(Function),
|
||||
warning: expect.any(Function),
|
||||
danger: expect.any(Function),
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('can display string element as title', () => {
|
||||
const notifications = notificationServiceMock.createStartContract();
|
||||
const wrapper = createNotifications({ notifications });
|
||||
|
||||
expect(notifications.toasts.add).toHaveBeenCalledTimes(0);
|
||||
|
||||
wrapper.toasts.show({ title: 'foo' });
|
||||
|
||||
expect(notifications.toasts.add).toHaveBeenCalledTimes(1);
|
||||
expect(notifications.toasts.add.mock.calls[0][0]).toMatchObject({
|
||||
title: 'foo',
|
||||
});
|
||||
});
|
||||
|
||||
test('can display React element as title', () => {
|
||||
const notifications = notificationServiceMock.createStartContract();
|
||||
const wrapper = createNotifications({ notifications });
|
||||
|
||||
expect(notifications.toasts.add).toHaveBeenCalledTimes(0);
|
||||
|
||||
wrapper.toasts.show({ title: <div>bar</div> });
|
||||
|
||||
expect(notifications.toasts.add).toHaveBeenCalledTimes(1);
|
||||
expect((notifications.toasts.add.mock.calls[0][0] as any).title).toMatchInlineSnapshot(`
|
||||
<div>
|
||||
bar
|
||||
</div>
|
||||
`);
|
||||
});
|
||||
|
||||
test('can display React element as toast body', () => {
|
||||
const notifications = notificationServiceMock.createStartContract();
|
||||
const wrapper = createNotifications({ notifications });
|
||||
|
||||
wrapper.toasts.show({ body: <div>baz</div> });
|
||||
|
||||
expect(notifications.toasts.add).toHaveBeenCalledTimes(1);
|
||||
expect((notifications.toasts.add.mock.calls[0][0] as any).text).toMatchInlineSnapshot(`
|
||||
<React.Fragment>
|
||||
<div>
|
||||
baz
|
||||
</div>
|
||||
</React.Fragment>
|
||||
`);
|
||||
});
|
||||
|
||||
test('can set toast properties', () => {
|
||||
const notifications = notificationServiceMock.createStartContract();
|
||||
const wrapper = createNotifications({ notifications });
|
||||
|
||||
wrapper.toasts.show({
|
||||
body: '1',
|
||||
color: 'danger',
|
||||
iconType: 'foo',
|
||||
title: '2',
|
||||
toastLifeTimeMs: 3,
|
||||
});
|
||||
|
||||
expect(notifications.toasts.add.mock.calls[0][0]).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"color": "danger",
|
||||
"iconType": "foo",
|
||||
"onClose": undefined,
|
||||
"text": <React.Fragment>
|
||||
1
|
||||
</React.Fragment>,
|
||||
"title": "2",
|
||||
"toastLifeTimeMs": 3,
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
test('can display success, warning and danger toasts', () => {
|
||||
const notifications = notificationServiceMock.createStartContract();
|
||||
const wrapper = createNotifications({ notifications });
|
||||
|
||||
wrapper.toasts.success({ title: '1' });
|
||||
wrapper.toasts.warning({ title: '2' });
|
||||
wrapper.toasts.danger({ title: '3' });
|
||||
|
||||
expect(notifications.toasts.add).toHaveBeenCalledTimes(3);
|
||||
expect(notifications.toasts.add.mock.calls[0][0]).toMatchObject({
|
||||
title: '1',
|
||||
color: 'success',
|
||||
iconType: 'check',
|
||||
});
|
||||
expect(notifications.toasts.add.mock.calls[1][0]).toMatchObject({
|
||||
title: '2',
|
||||
color: 'warning',
|
||||
iconType: 'help',
|
||||
});
|
||||
expect(notifications.toasts.add.mock.calls[2][0]).toMatchObject({
|
||||
title: '3',
|
||||
color: 'danger',
|
||||
iconType: 'alert',
|
||||
});
|
||||
});
|
||||
|
||||
test('if body is not set, renders it empty', () => {
|
||||
const notifications = notificationServiceMock.createStartContract();
|
||||
const wrapper = createNotifications({ notifications });
|
||||
|
||||
wrapper.toasts.success({ title: '1' });
|
||||
|
||||
expect((notifications.toasts.add.mock.calls[0][0] as any).text).toMatchInlineSnapshot(
|
||||
`<React.Fragment />`
|
||||
);
|
||||
});
|
|
@ -0,0 +1,65 @@
|
|||
/*
|
||||
* 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 { KibanaServices } from '../context/types';
|
||||
import { KibanaReactNotifications } from './types';
|
||||
|
||||
export const createNotifications = (services: KibanaServices): KibanaReactNotifications => {
|
||||
const show: KibanaReactNotifications['toasts']['show'] = ({
|
||||
title,
|
||||
body,
|
||||
color,
|
||||
iconType,
|
||||
toastLifeTimeMs,
|
||||
onClose,
|
||||
}) => {
|
||||
if (!services.notifications) {
|
||||
throw new TypeError('Could not show notification as notifications service is not available.');
|
||||
}
|
||||
services.notifications!.toasts.add({
|
||||
title,
|
||||
text: <>{body || null}</>,
|
||||
color,
|
||||
iconType,
|
||||
toastLifeTimeMs,
|
||||
onClose,
|
||||
});
|
||||
};
|
||||
|
||||
const success: KibanaReactNotifications['toasts']['success'] = input =>
|
||||
show({ color: 'success', iconType: 'check', ...input });
|
||||
|
||||
const warning: KibanaReactNotifications['toasts']['warning'] = input =>
|
||||
show({ color: 'warning', iconType: 'help', ...input });
|
||||
|
||||
const danger: KibanaReactNotifications['toasts']['danger'] = input =>
|
||||
show({ color: 'danger', iconType: 'alert', ...input });
|
||||
|
||||
const notifications: KibanaReactNotifications = {
|
||||
toasts: {
|
||||
show,
|
||||
success,
|
||||
warning,
|
||||
danger,
|
||||
},
|
||||
};
|
||||
|
||||
return notifications;
|
||||
};
|
21
src/plugins/kibana_react/public/notifications/index.tsx
Normal file
21
src/plugins/kibana_react/public/notifications/index.tsx
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 * from './types';
|
||||
export * from './create_notifications';
|
|
@ -18,21 +18,22 @@
|
|||
*/
|
||||
|
||||
import * as React from 'react';
|
||||
import { KibanaReactContextValue, Core } from './types';
|
||||
import { Toast } from '../../../../core/public';
|
||||
|
||||
export const context = React.createContext<KibanaReactContextValue>({
|
||||
core: {},
|
||||
});
|
||||
export interface ToastInput {
|
||||
title?: React.ReactNode;
|
||||
body?: React.ReactNode;
|
||||
color?: Toast['color'];
|
||||
iconType?: Toast['iconType'];
|
||||
toastLifeTimeMs?: Toast['toastLifeTimeMs'];
|
||||
onClose?: Toast['onClose'];
|
||||
}
|
||||
|
||||
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 interface KibanaReactNotifications {
|
||||
toasts: {
|
||||
show: (input: ToastInput) => void;
|
||||
success: (input: ToastInput) => void;
|
||||
warning: (input: ToastInput) => void;
|
||||
danger: (input: ToastInput) => void;
|
||||
};
|
||||
};
|
||||
|
||||
export const useKibana = (): KibanaReactContextValue => React.useContext(context);
|
||||
}
|
|
@ -0,0 +1,127 @@
|
|||
/*
|
||||
* 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 { createReactOverlays } from './create_react_overlays';
|
||||
|
||||
test('throws if no overlays service provided', () => {
|
||||
const overlays = createReactOverlays({});
|
||||
expect(() => overlays.openFlyout(null)).toThrowErrorMatchingInlineSnapshot(
|
||||
`"Could not show overlay as overlays service is not available."`
|
||||
);
|
||||
});
|
||||
|
||||
test('creates wrapped overlays service', () => {
|
||||
const overlays = createReactOverlays({
|
||||
overlays: {
|
||||
openFlyout: jest.fn(),
|
||||
openModal: jest.fn(),
|
||||
},
|
||||
});
|
||||
|
||||
expect(typeof overlays.openFlyout).toBe('function');
|
||||
expect(typeof overlays.openModal).toBe('function');
|
||||
});
|
||||
|
||||
test('can open flyout with React element', () => {
|
||||
const openFlyout = jest.fn();
|
||||
const overlays = createReactOverlays({
|
||||
overlays: {
|
||||
openFlyout,
|
||||
openModal: jest.fn(),
|
||||
},
|
||||
});
|
||||
|
||||
expect(openFlyout).toHaveBeenCalledTimes(0);
|
||||
|
||||
overlays.openFlyout(<div>foo</div>);
|
||||
|
||||
expect(openFlyout).toHaveBeenCalledTimes(1);
|
||||
expect(openFlyout.mock.calls[0][0]).toMatchInlineSnapshot(`
|
||||
<React.Fragment>
|
||||
<div>
|
||||
foo
|
||||
</div>
|
||||
</React.Fragment>
|
||||
`);
|
||||
});
|
||||
|
||||
test('can open modal with React element', () => {
|
||||
const openFlyout = jest.fn();
|
||||
const openModal = jest.fn();
|
||||
const overlays = createReactOverlays({
|
||||
overlays: {
|
||||
openFlyout,
|
||||
openModal,
|
||||
},
|
||||
});
|
||||
|
||||
expect(openModal).toHaveBeenCalledTimes(0);
|
||||
|
||||
overlays.openModal(<div>bar</div>);
|
||||
|
||||
expect(openModal).toHaveBeenCalledTimes(1);
|
||||
expect(openModal.mock.calls[0][0]).toMatchInlineSnapshot(`
|
||||
<React.Fragment>
|
||||
<div>
|
||||
bar
|
||||
</div>
|
||||
</React.Fragment>
|
||||
`);
|
||||
});
|
||||
|
||||
test('passes through flyout options when opening flyout', () => {
|
||||
const openFlyout = jest.fn();
|
||||
const overlays = createReactOverlays({
|
||||
overlays: {
|
||||
openFlyout,
|
||||
openModal: jest.fn(),
|
||||
},
|
||||
});
|
||||
|
||||
overlays.openFlyout(<>foo</>, {
|
||||
'data-test-subj': 'foo',
|
||||
closeButtonAriaLabel: 'bar',
|
||||
});
|
||||
|
||||
expect(openFlyout.mock.calls[0][1]).toEqual({
|
||||
'data-test-subj': 'foo',
|
||||
closeButtonAriaLabel: 'bar',
|
||||
});
|
||||
});
|
||||
|
||||
test('passes through modal options when opening modal', () => {
|
||||
const openModal = jest.fn();
|
||||
const overlays = createReactOverlays({
|
||||
overlays: {
|
||||
openFlyout: jest.fn(),
|
||||
openModal,
|
||||
},
|
||||
});
|
||||
|
||||
overlays.openModal(<>foo</>, {
|
||||
'data-test-subj': 'foo2',
|
||||
closeButtonAriaLabel: 'bar2',
|
||||
});
|
||||
|
||||
expect(openModal.mock.calls[0][1]).toEqual({
|
||||
'data-test-subj': 'foo2',
|
||||
closeButtonAriaLabel: 'bar2',
|
||||
});
|
||||
});
|
|
@ -0,0 +1,47 @@
|
|||
/*
|
||||
* 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 { KibanaServices } from '../context/types';
|
||||
import { KibanaReactOverlays } from './types';
|
||||
|
||||
export const createReactOverlays = (services: KibanaServices): KibanaReactOverlays => {
|
||||
const checkCoreService = () => {
|
||||
if (!services.overlays) {
|
||||
throw new TypeError('Could not show overlay as overlays service is not available.');
|
||||
}
|
||||
};
|
||||
|
||||
const openFlyout: KibanaReactOverlays['openFlyout'] = (node, options?) => {
|
||||
checkCoreService();
|
||||
return services.overlays!.openFlyout(<>{node}</>, options);
|
||||
};
|
||||
|
||||
const openModal: KibanaReactOverlays['openModal'] = (node, options?) => {
|
||||
checkCoreService();
|
||||
return services.overlays!.openModal(<>{node}</>, options);
|
||||
};
|
||||
|
||||
const overlays: KibanaReactOverlays = {
|
||||
openFlyout,
|
||||
openModal,
|
||||
};
|
||||
|
||||
return overlays;
|
||||
};
|
21
src/plugins/kibana_react/public/overlays/index.tsx
Normal file
21
src/plugins/kibana_react/public/overlays/index.tsx
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 * from './types';
|
||||
export * from './create_react_overlays';
|
32
src/plugins/kibana_react/public/overlays/types.ts
Normal file
32
src/plugins/kibana_react/public/overlays/types.ts
Normal file
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
* 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 { CoreStart } from '../context/types';
|
||||
|
||||
export interface KibanaReactOverlays {
|
||||
openFlyout: (
|
||||
node: React.ReactNode,
|
||||
options?: Parameters<CoreStart['overlays']['openFlyout']>['1']
|
||||
) => ReturnType<CoreStart['overlays']['openFlyout']>;
|
||||
openModal: (
|
||||
node: React.ReactNode,
|
||||
options?: Parameters<CoreStart['overlays']['openFlyout']>['1']
|
||||
) => ReturnType<CoreStart['overlays']['openModal']>;
|
||||
}
|
20
src/plugins/kibana_react/public/ui_settings/index.ts
Normal file
20
src/plugins/kibana_react/public/ui_settings/index.ts
Normal file
|
@ -0,0 +1,20 @@
|
|||
/*
|
||||
* 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 * from './use_ui_setting';
|
|
@ -20,21 +20,21 @@
|
|||
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 { useUiSetting$ } from './use_ui_setting';
|
||||
import { createKibanaReactContext } from '../context';
|
||||
import { KibanaServices } from '../context/types';
|
||||
import { Subject } from 'rxjs';
|
||||
import { useObservable } from '../util/use_observable';
|
||||
import { coreMock } from '../../../../core/public/mocks';
|
||||
|
||||
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 mock = (): [KibanaServices, Subject<any>] => {
|
||||
const core = coreMock.createStart();
|
||||
const get = core.uiSettings.get;
|
||||
const get$ = core.uiSettings.get$;
|
||||
const subject = new Subject();
|
||||
|
||||
get.mockImplementation(() => 'bar');
|
||||
|
@ -56,85 +56,136 @@ afterEach(() => {
|
|||
container = null;
|
||||
});
|
||||
|
||||
const TestConsumer: React.FC<{
|
||||
setting: string;
|
||||
newValue?: string;
|
||||
}> = ({ setting, newValue = '' }) => {
|
||||
const [value, set] = useUiSetting(setting, 'DEFAULT');
|
||||
describe('useUiSetting', () => {
|
||||
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>
|
||||
);
|
||||
};
|
||||
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);
|
||||
test('returns setting value', async () => {
|
||||
const [core] = mock();
|
||||
const { Provider } = createKibanaReactContext(core);
|
||||
|
||||
ReactDOM.render(
|
||||
<Provider>
|
||||
<TestConsumer setting="foo" />
|
||||
</Provider>,
|
||||
container
|
||||
);
|
||||
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')!, {});
|
||||
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');
|
||||
});
|
||||
|
||||
expect(core.uiSettings!.set).toHaveBeenCalledTimes(1);
|
||||
expect(core.uiSettings!.set).toHaveBeenCalledWith('a', 'c');
|
||||
test('calls uiSettings.get() method with correct key and default value', async () => {
|
||||
const [core] = mock();
|
||||
const { Provider } = createKibanaReactContext(core);
|
||||
|
||||
ReactDOM.render(
|
||||
<Provider>
|
||||
<TestConsumer setting="foo" />
|
||||
</Provider>,
|
||||
container
|
||||
);
|
||||
|
||||
expect(core.uiSettings!.get).toHaveBeenCalledTimes(1);
|
||||
expect((core.uiSettings!.get as any).mock.calls[0][0]).toBe('foo');
|
||||
expect((core.uiSettings!.get as any).mock.calls[0][1]).toBe('DEFAULT');
|
||||
});
|
||||
});
|
||||
|
||||
describe('useUiSetting$', () => {
|
||||
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 } = createKibanaReactContext(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 = coreMock.createStart();
|
||||
const { Provider } = createKibanaReactContext(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 } = createKibanaReactContext(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 } = createKibanaReactContext(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');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -18,9 +18,28 @@
|
|||
*/
|
||||
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { useKibana } from '../core';
|
||||
import { useKibana } from '../context';
|
||||
import { useObservable } from '../util/use_observable';
|
||||
|
||||
/**
|
||||
* Returns the current UI-settings value.
|
||||
*
|
||||
* Usage:
|
||||
*
|
||||
* ```js
|
||||
* const darkMode = useUiSetting('theme:darkMode');
|
||||
* ```
|
||||
*/
|
||||
export const useUiSetting = <T>(key: string, defaultValue?: T): T => {
|
||||
const { services } = useKibana();
|
||||
|
||||
if (typeof services.uiSettings !== 'object') {
|
||||
throw new TypeError('uiSettings service not available in kibana-react context.');
|
||||
}
|
||||
|
||||
return services.uiSettings.get(key, defaultValue);
|
||||
};
|
||||
|
||||
type Setter<T> = (newValue: T) => Promise<boolean>;
|
||||
|
||||
/**
|
||||
|
@ -33,18 +52,22 @@ type Setter<T> = (newValue: T) => Promise<boolean>;
|
|||
* Usage:
|
||||
*
|
||||
* ```js
|
||||
* const [darkMode, setDarkMode] = useUiSetting('theme:darkMode');
|
||||
* 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]);
|
||||
export const useUiSetting$ = <T>(key: string, defaultValue?: T): [T, Setter<T>] => {
|
||||
const { services } = useKibana();
|
||||
|
||||
if (typeof services.uiSettings !== 'object') {
|
||||
throw new TypeError('uiSettings service not available in kibana-react context.');
|
||||
}
|
||||
|
||||
const observable$ = useMemo(() => services.uiSettings!.get$(key, defaultValue), [
|
||||
key,
|
||||
defaultValue,
|
||||
]);
|
||||
const value = useObservable<T>(observable$, services.uiSettings!.get(key, defaultValue));
|
||||
const set = useCallback((newValue: T) => services.uiSettings!.set(key, newValue), [key]);
|
||||
|
||||
return [value, set];
|
||||
};
|
||||
|
|
|
@ -16,13 +16,19 @@ import { pluck } from 'rxjs/operators';
|
|||
import { EuiErrorBoundary } from '@elastic/eui';
|
||||
import { UICapabilitiesProvider } from 'ui/capabilities/react';
|
||||
import { I18nContext } from 'ui/i18n';
|
||||
import { npStart } from 'ui/new_platform';
|
||||
import { EuiThemeProvider } from '../../../../common/eui_styled_components';
|
||||
import { InfraFrontendLibs } from '../lib/lib';
|
||||
import { PageRouter } from '../routes';
|
||||
import { createStore } from '../store';
|
||||
import { ApolloClientContext } from '../utils/apollo_context';
|
||||
import { HistoryContext } from '../utils/history_context';
|
||||
import { useKibanaUiSetting } from '../utils/use_kibana_ui_setting';
|
||||
import {
|
||||
useUiSetting$,
|
||||
KibanaContextProvider,
|
||||
} from '../../../../../../src/plugins/kibana_react/public';
|
||||
|
||||
const { uiSettings } = npStart.core;
|
||||
|
||||
export async function startApp(libs: InfraFrontendLibs) {
|
||||
const history = createHashHistory();
|
||||
|
@ -34,7 +40,7 @@ export async function startApp(libs: InfraFrontendLibs) {
|
|||
});
|
||||
|
||||
const InfraPluginRoot: React.FunctionComponent = () => {
|
||||
const [darkMode] = useKibanaUiSetting('theme:darkMode');
|
||||
const [darkMode] = useUiSetting$<boolean>('theme:darkMode');
|
||||
|
||||
return (
|
||||
<I18nContext>
|
||||
|
@ -59,5 +65,9 @@ export async function startApp(libs: InfraFrontendLibs) {
|
|||
);
|
||||
};
|
||||
|
||||
libs.framework.render(<InfraPluginRoot />);
|
||||
libs.framework.render(
|
||||
<KibanaContextProvider services={{ uiSettings }}>
|
||||
<InfraPluginRoot />
|
||||
</KibanaContextProvider>
|
||||
);
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue