diff --git a/src/core/public/notifications/toasts/error_toast.tsx b/src/core/public/notifications/toasts/error_toast.tsx
index 10bc51559644b2..956e56999849ef 100644
--- a/src/core/public/notifications/toasts/error_toast.tsx
+++ b/src/core/public/notifications/toasts/error_toast.tsx
@@ -18,6 +18,7 @@
*/
import React from 'react';
+import ReactDOM from 'react-dom';
import {
EuiButton,
@@ -29,7 +30,7 @@ import {
EuiModalHeaderTitle,
} from '@elastic/eui';
import { EuiSpacer } from '@elastic/eui';
-import { FormattedMessage } from '@kbn/i18n/react';
+import { FormattedMessage, I18nProvider } from '@kbn/i18n/react';
import { OverlayStart } from '../../overlays';
@@ -40,6 +41,11 @@ interface ErrorToastProps {
openModal: OverlayStart['openModal'];
}
+const mount = (component: React.ReactElement) => (element: HTMLElement) => {
+ ReactDOM.render({component}, element);
+ return () => ReactDOM.unmountComponentAtNode(element);
+};
+
/**
* This should instead be replaced by the overlay service once it's available.
* This does not use React portals so that if the parent toast times out, this modal
@@ -52,27 +58,32 @@ function showErrorDialog({
openModal,
}: Pick) {
const modal = openModal(
-
-
- {title}
-
-
-
- {error.stack && (
-
-
-
- {error.stack}
-
-
- )}
-
-
- modal.close()} fill>
-
-
-
-
+ mount(
+
+
+ {title}
+
+
+
+ {error.stack && (
+
+
+
+ {error.stack}
+
+
+ )}
+
+
+ modal.close()} fill>
+
+
+
+
+ )
);
}
diff --git a/src/core/public/overlays/__snapshots__/modal.test.tsx.snap b/src/core/public/overlays/__snapshots__/modal.test.tsx.snap
index a4e6f5d6f72b8f..27932e313bef4b 100644
--- a/src/core/public/overlays/__snapshots__/modal.test.tsx.snap
+++ b/src/core/public/overlays/__snapshots__/modal.test.tsx.snap
@@ -17,9 +17,9 @@ Array [
maxWidth={true}
onClose={[Function]}
>
-
- Modal content
-
+
,
@@ -28,6 +28,8 @@ Array [
]
`;
+exports[`ModalService openModal() renders a modal to the DOM 2`] = `""`;
+
exports[`ModalService openModal() with a currently active modal replaces the current modal with a new one 1`] = `
Array [
Array [
@@ -37,9 +39,9 @@ Array [
maxWidth={true}
onClose={[Function]}
>
-
- Modal content 1
-
+
,
@@ -52,9 +54,9 @@ Array [
maxWidth={true}
onClose={[Function]}
>
-
- Flyout content 2
-
+
,
diff --git a/src/core/public/overlays/flyout.test.mocks.ts b/src/core/public/overlays/flyout.test.mocks.ts
index 35a046e9600774..563f414a0ae99d 100644
--- a/src/core/public/overlays/flyout.test.mocks.ts
+++ b/src/core/public/overlays/flyout.test.mocks.ts
@@ -19,7 +19,9 @@
export const mockReactDomRender = jest.fn();
export const mockReactDomUnmount = jest.fn();
+export const mockReactDomCreatePortal = jest.fn().mockImplementation(component => component);
jest.doMock('react-dom', () => ({
render: mockReactDomRender,
+ createPortal: mockReactDomCreatePortal,
unmountComponentAtNode: mockReactDomUnmount,
}));
diff --git a/src/core/public/overlays/modal.test.tsx b/src/core/public/overlays/modal.test.tsx
index ee8bba5d611129..8b712cb6638083 100644
--- a/src/core/public/overlays/modal.test.tsx
+++ b/src/core/public/overlays/modal.test.tsx
@@ -19,8 +19,10 @@
import { mockReactDomRender, mockReactDomUnmount } from './flyout.test.mocks';
import React from 'react';
+import { mount } from 'enzyme';
import { i18nServiceMock } from '../i18n/i18n_service.mock';
import { ModalService, ModalRef } from './modal';
+import { mountForComponent } from './utils';
const i18nMock = i18nServiceMock.createStartContract();
@@ -35,39 +37,51 @@ describe('ModalService', () => {
const target = document.createElement('div');
const modalService = new ModalService(target);
expect(mockReactDomRender).not.toHaveBeenCalled();
- modalService.openModal(i18nMock, Modal content);
+ modalService.openModal(i18nMock, container => {
+ const content = document.createElement('span');
+ content.textContent = 'Modal content';
+ container.append(content);
+ return () => {};
+ });
expect(mockReactDomRender.mock.calls).toMatchSnapshot();
+ const modalContent = mount(mockReactDomRender.mock.calls[0][0]);
+ expect(modalContent.html()).toMatchSnapshot();
});
+
describe('with a currently active modal', () => {
let target: HTMLElement;
let modalService: ModalService;
let ref1: ModalRef;
+
beforeEach(() => {
target = document.createElement('div');
modalService = new ModalService(target);
- ref1 = modalService.openModal(i18nMock, Modal content 1);
+ ref1 = modalService.openModal(i18nMock, mountForComponent(Modal content 1));
});
+
it('replaces the current modal with a new one', () => {
- modalService.openModal(i18nMock, Flyout content 2);
+ modalService.openModal(i18nMock, mountForComponent(Flyout content 2));
expect(mockReactDomRender.mock.calls).toMatchSnapshot();
expect(mockReactDomUnmount).toHaveBeenCalledTimes(1);
expect(() => ref1.close()).not.toThrowError();
expect(mockReactDomUnmount).toHaveBeenCalledTimes(1);
});
+
it('resolves onClose on the previous ref', async () => {
const onCloseComplete = jest.fn();
ref1.onClose.then(onCloseComplete);
- modalService.openModal(i18nMock, Flyout content 2);
+ modalService.openModal(i18nMock, mountForComponent(Flyout content 2));
await ref1.onClose;
expect(onCloseComplete).toBeCalledTimes(1);
});
});
});
+
describe('ModalRef#close()', () => {
it('resolves the onClose Promise', async () => {
const target = document.createElement('div');
const modalService = new ModalService(target);
- const ref = modalService.openModal(i18nMock, Flyout content);
+ const ref = modalService.openModal(i18nMock, mountForComponent(Flyout content));
const onCloseComplete = jest.fn();
ref.onClose.then(onCloseComplete);
@@ -75,21 +89,29 @@ describe('ModalService', () => {
await ref.close();
expect(onCloseComplete).toHaveBeenCalledTimes(1);
});
+
it('can be called multiple times on the same ModalRef', async () => {
const target = document.createElement('div');
const modalService = new ModalService(target);
- const ref = modalService.openModal(i18nMock, Flyout content);
+ const ref = modalService.openModal(i18nMock, mountForComponent(Flyout content));
expect(mockReactDomUnmount).not.toHaveBeenCalled();
await ref.close();
expect(mockReactDomUnmount.mock.calls).toMatchSnapshot();
await ref.close();
expect(mockReactDomUnmount).toHaveBeenCalledTimes(1);
});
+
it("on a stale ModalRef doesn't affect the active flyout", async () => {
const target = document.createElement('div');
const modalService = new ModalService(target);
- const ref1 = modalService.openModal(i18nMock, Modal content 1);
- const ref2 = modalService.openModal(i18nMock, Modal content 2);
+ const ref1 = modalService.openModal(
+ i18nMock,
+ mountForComponent(Modal content 1)
+ );
+ const ref2 = modalService.openModal(
+ i18nMock,
+ mountForComponent(Modal content 2)
+ );
const onCloseComplete = jest.fn();
ref2.onClose.then(onCloseComplete);
mockReactDomUnmount.mockClear();
diff --git a/src/core/public/overlays/modal.tsx b/src/core/public/overlays/modal.tsx
index 6f94788b84d717..200d4903694bb9 100644
--- a/src/core/public/overlays/modal.tsx
+++ b/src/core/public/overlays/modal.tsx
@@ -25,6 +25,8 @@ import { render, unmountComponentAtNode } from 'react-dom';
import { Subject } from 'rxjs';
import { I18nStart } from '../i18n';
import { OverlayRef } from './overlay_service';
+import { MountPoint } from './types';
+import { MountWrapper } from './utils';
/**
* A ModalRef is a reference to an opened modal. It offers methods to
@@ -65,12 +67,12 @@ export class ModalService {
* Opens a flyout panel with the given component inside. You can use
* `close()` on the returned FlyoutRef to close the flyout.
*
- * @param flyoutChildren - Mounts the children inside a flyout panel
- * @return {FlyoutRef} A reference to the opened flyout panel.
+ * @param modalMount - Mounts the children inside the modal
+ * @return {ModalRef} A reference to the opened modal.
*/
public openModal = (
i18n: I18nStart,
- modalChildren: React.ReactNode,
+ modalMount: MountPoint,
modalProps: {
closeButtonAriaLabel?: string;
'data-test-subj'?: string;
@@ -97,7 +99,7 @@ export class ModalService {
modal.close()}>
- {modalChildren}
+
,
diff --git a/src/core/public/overlays/overlay_service.ts b/src/core/public/overlays/overlay_service.ts
index 1a72bb5dbe435a..62b41db0581bdf 100644
--- a/src/core/public/overlays/overlay_service.ts
+++ b/src/core/public/overlays/overlay_service.ts
@@ -23,6 +23,7 @@ import { FlyoutService } from './flyout';
import { ModalService } from './modal';
import { I18nStart } from '../i18n';
import { OverlayBannersStart, OverlayBannersService } from './banners';
+import { MountPoint } from './types';
import { UiSettingsClientContract } from '../ui_settings';
/**
@@ -83,7 +84,7 @@ export interface OverlayStart {
}
) => OverlayRef;
openModal: (
- modalChildren: React.ReactNode,
+ modalChildren: MountPoint,
modalProps?: {
className?: string;
closeButtonAriaLabel?: string;
diff --git a/src/core/public/overlays/utils.test.tsx b/src/core/public/overlays/utils.test.tsx
new file mode 100644
index 00000000000000..caec542f9c4b2f
--- /dev/null
+++ b/src/core/public/overlays/utils.test.tsx
@@ -0,0 +1,62 @@
+/*
+ * 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 React from 'react';
+import { mount } from 'enzyme';
+import { MountWrapper, mountForComponent } from './utils';
+
+describe('MountWrapper', () => {
+ it('renders an html element in react tree', () => {
+ const mountPoint = (container: HTMLElement) => {
+ const el = document.createElement('p');
+ el.textContent = 'hello';
+ el.className = 'bar';
+ container.append(el);
+ return () => {};
+ };
+ const wrapper = ;
+ const container = mount(wrapper);
+ expect(container.html()).toMatchInlineSnapshot(`""`);
+ });
+
+ it('updates the react tree when the mounted element changes', () => {
+ const el = document.createElement('p');
+ el.textContent = 'initial';
+
+ const mountPoint = (container: HTMLElement) => {
+ container.append(el);
+ return () => {};
+ };
+
+ const wrapper = ;
+ const container = mount(wrapper);
+ expect(container.html()).toMatchInlineSnapshot(`""`);
+
+ el.textContent = 'changed';
+ container.update();
+ expect(container.html()).toMatchInlineSnapshot(`""`);
+ });
+
+ it('can render a detached react component', () => {
+ const mountPoint = mountForComponent(detached);
+ const wrapper = ;
+ const container = mount(wrapper);
+ expect(container.html()).toMatchInlineSnapshot(`"detached
"`);
+ });
+});
diff --git a/src/core/public/overlays/utils.tsx b/src/core/public/overlays/utils.tsx
new file mode 100644
index 00000000000000..e0029f394e307f
--- /dev/null
+++ b/src/core/public/overlays/utils.tsx
@@ -0,0 +1,46 @@
+/*
+ * 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 React, { useEffect, useRef } from 'react';
+import ReactDOM from 'react-dom';
+import { MountPoint } from 'kibana/public';
+import { I18nProvider } from '@kbn/i18n/src/react';
+
+/**
+ * Mount converter for react components.
+ *
+ * @param component to get a mount for
+ */
+export const mountForComponent = (component: React.ReactElement): MountPoint => (
+ element: HTMLElement
+) => {
+ ReactDOM.render({component}, element);
+ return () => ReactDOM.unmountComponentAtNode(element);
+};
+
+/**
+ *
+ * @param mount
+ * @constructor
+ */
+export const MountWrapper: React.FunctionComponent<{ mount: MountPoint }> = ({ mount }) => {
+ const element = useRef(null);
+ useEffect(() => mount(element.current!), [mount]);
+ return ;
+};
diff --git a/src/legacy/ui/public/courier/fetch/components/shard_failure_open_modal_button.tsx b/src/legacy/ui/public/courier/fetch/components/shard_failure_open_modal_button.tsx
index 5e53477b8ec04e..a5b28229279199 100644
--- a/src/legacy/ui/public/courier/fetch/components/shard_failure_open_modal_button.tsx
+++ b/src/legacy/ui/public/courier/fetch/components/shard_failure_open_modal_button.tsx
@@ -22,6 +22,7 @@ import { npStart } from 'ui/new_platform';
import { FormattedMessage } from '@kbn/i18n/react';
import { EuiButton, EuiTextAlign } from '@elastic/eui';
+import { mountForComponent } from '../../../../../../plugins/kibana_react/public';
import { ShardFailureModal } from './shard_failure_modal';
import { ResponseWithShardFailure, Request } from './shard_failure_types';
@@ -34,12 +35,14 @@ interface Props {
export function ShardFailureOpenModalButton({ request, response, title }: Props) {
function onClick() {
const modal = npStart.core.overlays.openModal(
- modal.close()}
- />,
+ mountForComponent(
+ modal.close()}
+ />
+ ),
{
className: 'shardFailureModal',
}
diff --git a/src/plugins/embeddable/public/lib/panel/embeddable_panel.tsx b/src/plugins/embeddable/public/lib/panel/embeddable_panel.tsx
index 8a8a2f44b7fd8a..c92599d44bf7a3 100644
--- a/src/plugins/embeddable/public/lib/panel/embeddable_panel.tsx
+++ b/src/plugins/embeddable/public/lib/panel/embeddable_panel.tsx
@@ -25,7 +25,8 @@ import {
TGetActionsCompatibleWithTrigger,
IAction,
} from '../ui_actions';
-import { CoreStart } from '../../../../../core/public';
+import { CoreStart, OverlayStart } from '../../../../../core/public';
+import { mountForComponent } from '../../../../kibana_react/public';
import { Start as InspectorStartContract } from '../inspector';
import { CONTEXT_MENU_TRIGGER, PANEL_BADGE_TRIGGER } from '../triggers';
@@ -200,17 +201,19 @@ export class EmbeddablePanel extends React.Component {
embeddable: this.props.embeddable,
});
- const createGetUserData = (overlays: CoreStart['overlays']) =>
+ const createGetUserData = (overlays: OverlayStart) =>
async function getUserData(context: { embeddable: IEmbeddable }) {
return new Promise<{ title: string | undefined }>(resolve => {
const session = overlays.openModal(
- {
- session.close();
- resolve({ title });
- }}
- />,
+ mountForComponent(
+ {
+ session.close();
+ resolve({ title });
+ }}
+ />
+ ),
{
'data-test-subj': 'customizePanel',
}
diff --git a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/add_panel_action.ts b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/add_panel_action.ts
index 3ca3a0864d9f14..9ecc4686c21b6e 100644
--- a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/add_panel_action.ts
+++ b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/add_panel_action.ts
@@ -18,8 +18,7 @@
*/
import { i18n } from '@kbn/i18n';
import { IAction } from 'src/plugins/ui_actions/public';
-import { NotificationsStart } from 'src/core/public';
-import { KibanaReactOverlays } from 'src/plugins/kibana_react/public';
+import { NotificationsStart, OverlayStart } from 'src/core/public';
import { ViewMode, GetEmbeddableFactory, GetEmbeddableFactories } from '../../../../types';
import { openAddPanelFlyout } from './open_add_panel_flyout';
import { IContainer } from '../../../../containers';
@@ -37,7 +36,7 @@ export class AddPanelAction implements IAction {
constructor(
private readonly getFactory: GetEmbeddableFactory,
private readonly getAllFactories: GetEmbeddableFactories,
- private readonly overlays: KibanaReactOverlays,
+ private readonly overlays: OverlayStart,
private readonly notifications: NotificationsStart,
private readonly SavedObjectFinder: React.ComponentType
) {}
diff --git a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/open_add_panel_flyout.tsx b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/open_add_panel_flyout.tsx
index bfa4f6e31d84e7..50ebebfe8ffeff 100644
--- a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/open_add_panel_flyout.tsx
+++ b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/open_add_panel_flyout.tsx
@@ -17,8 +17,7 @@
* under the License.
*/
import React from 'react';
-import { NotificationsStart } from 'src/core/public';
-import { KibanaReactOverlays } from 'src/plugins/kibana_react/public';
+import { NotificationsStart, OverlayStart } from 'src/core/public';
import { IContainer } from '../../../../containers';
import { AddPanelFlyout } from './add_panel_flyout';
import { GetEmbeddableFactory, GetEmbeddableFactories } from '../../../../types';
@@ -27,7 +26,7 @@ export async function openAddPanelFlyout(options: {
embeddable: IContainer;
getFactory: GetEmbeddableFactory;
getAllFactories: GetEmbeddableFactories;
- overlays: KibanaReactOverlays;
+ overlays: OverlayStart;
notifications: NotificationsStart;
SavedObjectFinder: React.ComponentType;
}) {
diff --git a/src/plugins/embeddable/public/lib/test_samples/actions/send_message_action.tsx b/src/plugins/embeddable/public/lib/test_samples/actions/send_message_action.tsx
index fc20a99987484d..ac540bff4a610c 100644
--- a/src/plugins/embeddable/public/lib/test_samples/actions/send_message_action.tsx
+++ b/src/plugins/embeddable/public/lib/test_samples/actions/send_message_action.tsx
@@ -20,6 +20,7 @@ import React from 'react';
import { EuiFlyoutBody } from '@elastic/eui';
import { createAction, IncompatibleActionError } from '../../ui_actions';
import { CoreStart } from '../../../../../../core/public';
+import { mountForComponent } from '../../../../../kibana_react/public';
import { Embeddable, EmbeddableInput } from '../../embeddables';
import { GetMessageModal } from './get_message_modal';
import { FullNameEmbeddableOutput, hasFullNameOutput } from './say_hello_action';
@@ -51,13 +52,15 @@ export function createSendMessageAction(overlays: CoreStart['overlays']) {
}
const modal = overlays.openModal(
- modal.close()}
- onDone={message => {
- modal.close();
- sendMessage(context, message);
- }}
- />
+ mountForComponent(
+ modal.close()}
+ onDone={message => {
+ modal.close();
+ sendMessage(context, message);
+ }}
+ />
+ )
);
},
});
diff --git a/src/plugins/embeddable/public/lib/test_samples/embeddables/contact_card/contact_card_embeddable_factory.tsx b/src/plugins/embeddable/public/lib/test_samples/embeddables/contact_card/contact_card_embeddable_factory.tsx
index 962cddfa3735f8..8e7352b24e87b2 100644
--- a/src/plugins/embeddable/public/lib/test_samples/embeddables/contact_card/contact_card_embeddable_factory.tsx
+++ b/src/plugins/embeddable/public/lib/test_samples/embeddables/contact_card/contact_card_embeddable_factory.tsx
@@ -22,6 +22,7 @@ import { i18n } from '@kbn/i18n';
import { TExecuteTriggerActions } from 'src/plugins/ui_actions/public';
import { CoreStart } from 'src/core/public';
+import { mountForComponent } from '../../../../../../kibana_react/public';
import { EmbeddableFactory } from '../../../embeddables';
import { Container } from '../../../containers';
import { ContactCardEmbeddable, ContactCardEmbeddableInput } from './contact_card_embeddable';
@@ -54,16 +55,18 @@ export class ContactCardEmbeddableFactory extends EmbeddableFactory> {
return new Promise(resolve => {
const modalSession = this.overlays.openModal(
- {
- modalSession.close();
- resolve(undefined);
- }}
- onCreate={(input: { firstName: string; lastName?: string }) => {
- modalSession.close();
- resolve(input);
- }}
- />,
+ mountForComponent(
+ {
+ modalSession.close();
+ resolve(undefined);
+ }}
+ onCreate={(input: { firstName: string; lastName?: string }) => {
+ modalSession.close();
+ resolve(input);
+ }}
+ />
+ ),
{
'data-test-subj': 'createContactCardEmbeddable',
}
diff --git a/src/plugins/kibana_react/public/overlays/create_react_overlays.test.tsx b/src/plugins/kibana_react/public/overlays/create_react_overlays.test.tsx
index b10cbbc87be7c6..1882d138ad5cd6 100644
--- a/src/plugins/kibana_react/public/overlays/create_react_overlays.test.tsx
+++ b/src/plugins/kibana_react/public/overlays/create_react_overlays.test.tsx
@@ -68,13 +68,10 @@ test('can open modal with React element', () => {
overlays.openModal(bar
);
expect(coreOverlays.openModal).toHaveBeenCalledTimes(1);
- expect(coreOverlays.openModal.mock.calls[0][0]).toMatchInlineSnapshot(`
-
-
- bar
-
-
- `);
+ const container = document.createElement('div');
+ const mount = coreOverlays.openModal.mock.calls[0][0];
+ mount(container);
+ expect(container.innerHTML).toMatchInlineSnapshot(`"bar
"`);
});
test('passes through flyout options when opening flyout', () => {
diff --git a/src/plugins/kibana_react/public/overlays/create_react_overlays.tsx b/src/plugins/kibana_react/public/overlays/create_react_overlays.tsx
index a62c0970cf5256..d645ed3356da85 100644
--- a/src/plugins/kibana_react/public/overlays/create_react_overlays.tsx
+++ b/src/plugins/kibana_react/public/overlays/create_react_overlays.tsx
@@ -20,6 +20,7 @@
import * as React from 'react';
import { KibanaServices } from '../context/types';
import { KibanaReactOverlays } from './types';
+import { mountForComponent } from './mount_for_component';
export const createReactOverlays = (services: KibanaServices): KibanaReactOverlays => {
const checkCoreService = () => {
@@ -35,7 +36,7 @@ export const createReactOverlays = (services: KibanaServices): KibanaReactOverla
const openModal: KibanaReactOverlays['openModal'] = (node, options?) => {
checkCoreService();
- return services.overlays!.openModal(<>{node}>, options);
+ return services.overlays!.openModal(mountForComponent(<>{node}>), options);
};
const overlays: KibanaReactOverlays = {
diff --git a/src/plugins/kibana_react/public/overlays/index.tsx b/src/plugins/kibana_react/public/overlays/index.tsx
index 844f617ceafdbe..2096d7e8586901 100644
--- a/src/plugins/kibana_react/public/overlays/index.tsx
+++ b/src/plugins/kibana_react/public/overlays/index.tsx
@@ -19,3 +19,4 @@
export * from './types';
export * from './create_react_overlays';
+export * from './mount_for_component';
diff --git a/src/plugins/kibana_react/public/overlays/mount_for_component.tsx b/src/plugins/kibana_react/public/overlays/mount_for_component.tsx
new file mode 100644
index 00000000000000..6ac6002c1b0609
--- /dev/null
+++ b/src/plugins/kibana_react/public/overlays/mount_for_component.tsx
@@ -0,0 +1,35 @@
+/*
+ * 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 React from 'react';
+import ReactDOM from 'react-dom';
+import { I18nProvider } from '@kbn/i18n/react';
+import { MountPoint } from 'kibana/public';
+
+/**
+ * Mount converter for react components.
+ *
+ * @param component to get a mount for
+ */
+export const mountForComponent = (component: React.ReactElement): MountPoint => (
+ element: HTMLElement
+) => {
+ ReactDOM.render({component}, element);
+ return () => ReactDOM.unmountComponentAtNode(element);
+};
diff --git a/src/plugins/kibana_react/public/overlays/types.ts b/src/plugins/kibana_react/public/overlays/types.ts
index 6a1fb25ca14835..f1a82113a26eee 100644
--- a/src/plugins/kibana_react/public/overlays/types.ts
+++ b/src/plugins/kibana_react/public/overlays/types.ts
@@ -27,6 +27,6 @@ export interface KibanaReactOverlays {
) => ReturnType;
openModal: (
node: React.ReactNode,
- options?: Parameters['1']
+ options?: Parameters['1']
) => ReturnType;
}
diff --git a/x-pack/legacy/plugins/graph/public/components/guidance_panel/guidance_panel.tsx b/x-pack/legacy/plugins/graph/public/components/guidance_panel/guidance_panel.tsx
index 369963fb460977..8dede207b803c7 100644
--- a/x-pack/legacy/plugins/graph/public/components/guidance_panel/guidance_panel.tsx
+++ b/x-pack/legacy/plugins/graph/public/components/guidance_panel/guidance_panel.tsx
@@ -80,7 +80,8 @@ function GuidancePanelComponent(props: GuidancePanelProps) {
} = props;
const kibana = useKibana();
- const { overlays, savedObjects, uiSettings, chrome, application } = kibana.services;
+ const { services, overlays } = kibana;
+ const { savedObjects, uiSettings, chrome, application } = services;
if (!overlays || !chrome || !application) return null;
const onOpenDatasourcePicker = () => {
diff --git a/x-pack/legacy/plugins/graph/public/components/search_bar.tsx b/x-pack/legacy/plugins/graph/public/components/search_bar.tsx
index 4fd1a162105f16..59c991760bce85 100644
--- a/x-pack/legacy/plugins/graph/public/components/search_bar.tsx
+++ b/x-pack/legacy/plugins/graph/public/components/search_bar.tsx
@@ -85,7 +85,8 @@ export function SearchBarComponent(props: SearchBarProps) {
}, [currentDatasource]);
const kibana = useKibana();
- const { overlays, savedObjects, uiSettings } = kibana.services;
+ const { services, overlays } = kibana;
+ const { savedObjects, uiSettings } = services;
if (!overlays) return null;
return (
diff --git a/x-pack/legacy/plugins/graph/public/services/source_modal.tsx b/x-pack/legacy/plugins/graph/public/services/source_modal.tsx
index c985271f4dfe09..20a5b6d0786bd3 100644
--- a/x-pack/legacy/plugins/graph/public/services/source_modal.tsx
+++ b/x-pack/legacy/plugins/graph/public/services/source_modal.tsx
@@ -6,6 +6,7 @@
import { CoreStart } from 'src/core/public';
import React from 'react';
+import { KibanaReactOverlays } from 'src/plugins/kibana_react/public';
import { SourceModal } from '../components/source_modal';
import { IndexPatternSavedObject } from '../types';
@@ -15,7 +16,7 @@ export function openSourceModal(
savedObjects,
uiSettings,
}: {
- overlays: CoreStart['overlays'];
+ overlays: KibanaReactOverlays;
savedObjects: CoreStart['savedObjects'];
uiSettings: CoreStart['uiSettings'];
},
diff --git a/x-pack/plugins/advanced_ui_actions/public/custom_time_range_action.test.ts b/x-pack/plugins/advanced_ui_actions/public/custom_time_range_action.test.ts
index bbdcf99495288b..55e913e0f31da4 100644
--- a/x-pack/plugins/advanced_ui_actions/public/custom_time_range_action.test.ts
+++ b/x-pack/plugins/advanced_ui_actions/public/custom_time_range_action.test.ts
@@ -14,7 +14,6 @@ import { EmbeddableFactory } from '../../../../src/plugins/embeddable/public';
import { TimeRangeEmbeddable, TimeRangeContainer, TIME_RANGE_EMBEDDABLE } from './test_helpers';
import { TimeRangeEmbeddableFactory } from './test_helpers/time_range_embeddable_factory';
import { CustomTimeRangeAction } from './custom_time_range_action';
-import { coreMock } from '../../../../src/core/public/mocks';
/* eslint-disable */
import {
HelloWorldEmbeddableFactory,
@@ -29,6 +28,12 @@ import { ReactElement } from 'react';
jest.mock('ui/new_platform');
+const createOpenModalMock = () => {
+ const mock = jest.fn();
+ mock.mockReturnValue({ close: jest.fn() });
+ return mock;
+};
+
test('Custom time range action prevents embeddable from using container time', async done => {
const embeddableFactories = new Map();
embeddableFactories.set(TIME_RANGE_EMBEDDABLE, new TimeRangeEmbeddableFactory());
@@ -66,11 +71,10 @@ test('Custom time range action prevents embeddable from using container time', a
expect(child2).toBeDefined();
expect(child2.getInput().timeRange).toEqual({ from: 'now-15m', to: 'now' });
- const start = coreMock.createStart();
- const overlayMock = start.overlays;
- overlayMock.openModal.mockClear();
+ const openModalMock = createOpenModalMock();
+
new CustomTimeRangeAction({
- openModal: start.overlays.openModal,
+ openModal: openModalMock,
commonlyUsedRanges: [],
dateFormat: 'MM YYY',
}).execute({
@@ -78,7 +82,7 @@ test('Custom time range action prevents embeddable from using container time', a
});
await nextTick();
- const openModal = overlayMock.openModal.mock.calls[0][0] as ReactElement;
+ const openModal = openModalMock.mock.calls[0][0] as ReactElement;
const wrapper = mount(openModal);
wrapper.setState({ timeRange: { from: 'now-30days', to: 'now-29days' } });
@@ -129,11 +133,9 @@ test('Removing custom time range action resets embeddable back to container time
const child1 = container.getChild('1');
const child2 = container.getChild('2');
- const start = coreMock.createStart();
- const overlayMock = start.overlays;
- overlayMock.openModal.mockClear();
+ const openModalMock = createOpenModalMock();
new CustomTimeRangeAction({
- openModal: start.overlays.openModal,
+ openModal: openModalMock,
commonlyUsedRanges: [],
dateFormat: 'MM YYY',
}).execute({
@@ -141,7 +143,7 @@ test('Removing custom time range action resets embeddable back to container time
});
await nextTick();
- const openModal = overlayMock.openModal.mock.calls[0][0] as ReactElement;
+ const openModal = openModalMock.mock.calls[0][0] as ReactElement;
const wrapper = mount(openModal);
wrapper.setState({ timeRange: { from: 'now-30days', to: 'now-29days' } });
@@ -151,7 +153,7 @@ test('Removing custom time range action resets embeddable back to container time
container.updateInput({ timeRange: { from: 'now-30m', to: 'now-1m' } });
new CustomTimeRangeAction({
- openModal: start.overlays.openModal,
+ openModal: openModalMock,
commonlyUsedRanges: [],
dateFormat: 'MM YYY',
}).execute({
@@ -159,7 +161,7 @@ test('Removing custom time range action resets embeddable back to container time
});
await nextTick();
- const openModal2 = (overlayMock.openModal as any).mock.calls[1][0];
+ const openModal2 = openModalMock.mock.calls[1][0];
const wrapper2 = mount(openModal2);
findTestSubject(wrapper2, 'removePerPanelTimeRangeButton').simulate('click');
@@ -209,11 +211,9 @@ test('Cancelling custom time range action leaves state alone', async done => {
const child1 = container.getChild('1');
const child2 = container.getChild('2');
- const start = coreMock.createStart();
- const overlayMock = start.overlays;
- overlayMock.openModal.mockClear();
+ const openModalMock = createOpenModalMock();
new CustomTimeRangeAction({
- openModal: start.overlays.openModal,
+ openModal: openModalMock,
commonlyUsedRanges: [],
dateFormat: 'MM YYY',
}).execute({
@@ -221,7 +221,7 @@ test('Cancelling custom time range action leaves state alone', async done => {
});
await nextTick();
- const openModal = overlayMock.openModal.mock.calls[0][0] as ReactElement;
+ const openModal = openModalMock.mock.calls[0][0] as ReactElement;
const wrapper = mount(openModal);
wrapper.setState({ timeRange: { from: 'now-300m', to: 'now-400m' } });
@@ -263,9 +263,9 @@ test(`badge is compatible with embeddable that inherits from parent`, async () =
const child = container.getChild('1');
- const start = coreMock.createStart();
+ const openModalMock = createOpenModalMock();
const compatible = await new CustomTimeRangeAction({
- openModal: start.overlays.openModal,
+ openModal: openModalMock,
commonlyUsedRanges: [],
dateFormat: 'MM YYY',
}).isCompatible({
@@ -333,9 +333,9 @@ test('Attempting to execute on incompatible embeddable throws an error', async (
const child = container.getChild('1');
- const start = coreMock.createStart();
+ const openModalMock = createOpenModalMock();
const action = await new CustomTimeRangeAction({
- openModal: start.overlays.openModal,
+ openModal: openModalMock,
dateFormat: 'MM YYYY',
commonlyUsedRanges: [],
});
diff --git a/x-pack/plugins/advanced_ui_actions/public/custom_time_range_badge.test.ts b/x-pack/plugins/advanced_ui_actions/public/custom_time_range_badge.test.ts
index c6046c02f0833d..d2b9fa9ac16555 100644
--- a/x-pack/plugins/advanced_ui_actions/public/custom_time_range_badge.test.ts
+++ b/x-pack/plugins/advanced_ui_actions/public/custom_time_range_badge.test.ts
@@ -13,7 +13,6 @@ import { EmbeddableFactory } from '../../../../src/plugins/embeddable/public';
import { TimeRangeEmbeddable, TimeRangeContainer, TIME_RANGE_EMBEDDABLE } from './test_helpers';
import { TimeRangeEmbeddableFactory } from './test_helpers/time_range_embeddable_factory';
import { CustomTimeRangeBadge } from './custom_time_range_badge';
-import { coreMock } from '../../../../src/core/public/mocks';
import { ReactElement } from 'react';
import { nextTick } from 'test_utils/enzyme_helpers';
@@ -50,11 +49,11 @@ test('Removing custom time range from badge resets embeddable back to container
const child1 = container.getChild('1');
const child2 = container.getChild('2');
- const start = coreMock.createStart();
- const overlayMock = start.overlays;
- overlayMock.openModal.mockClear();
+ const openModalMock = jest.fn();
+ openModalMock.mockReturnValue({ close: jest.fn() });
+
new CustomTimeRangeBadge({
- openModal: start.overlays.openModal,
+ openModal: openModalMock,
dateFormat: 'MM YYYY',
commonlyUsedRanges: [],
}).execute({
@@ -62,7 +61,7 @@ test('Removing custom time range from badge resets embeddable back to container
});
await nextTick();
- const openModal = overlayMock.openModal.mock.calls[0][0] as ReactElement;
+ const openModal = openModalMock.mock.calls[0][0] as ReactElement;
const wrapper = mount(openModal);
findTestSubject(wrapper, 'removePerPanelTimeRangeButton').simulate('click');
@@ -102,9 +101,9 @@ test(`badge is not compatible with embeddable that inherits from parent`, async
const child = container.getChild('1');
- const start = coreMock.createStart();
+ const openModalMock = jest.fn();
const compatible = await new CustomTimeRangeBadge({
- openModal: start.overlays.openModal,
+ openModal: openModalMock,
dateFormat: 'MM YYYY',
commonlyUsedRanges: [],
}).isCompatible({
@@ -137,9 +136,9 @@ test(`badge is compatible with embeddable that has custom time range`, async ()
const child = container.getChild('1');
- const start = coreMock.createStart();
+ const openModalMock = jest.fn();
const compatible = await new CustomTimeRangeBadge({
- openModal: start.overlays.openModal,
+ openModal: openModalMock,
dateFormat: 'MM YYYY',
commonlyUsedRanges: [],
}).isCompatible({
@@ -171,9 +170,9 @@ test('Attempting to execute on incompatible embeddable throws an error', async (
const child = container.getChild('1');
- const start = coreMock.createStart();
+ const openModalMock = jest.fn();
const badge = await new CustomTimeRangeBadge({
- openModal: start.overlays.openModal,
+ openModal: openModalMock,
dateFormat: 'MM YYYY',
commonlyUsedRanges: [],
});
diff --git a/x-pack/plugins/advanced_ui_actions/public/plugin.ts b/x-pack/plugins/advanced_ui_actions/public/plugin.ts
index fc106cc8ec26b9..e2d1892b1355e0 100644
--- a/x-pack/plugins/advanced_ui_actions/public/plugin.ts
+++ b/x-pack/plugins/advanced_ui_actions/public/plugin.ts
@@ -10,6 +10,7 @@ import {
CoreStart,
Plugin,
} from '../../../../src/core/public';
+import { createReactOverlays } from '../../../../src/plugins/kibana_react/public';
import { IUiActionsStart, IUiActionsSetup } from '../../../../src/plugins/ui_actions/public';
import {
CONTEXT_MENU_TRIGGER,
@@ -44,8 +45,9 @@ export class AdvancedUiActionsPublicPlugin
public start(core: CoreStart, { uiActions }: StartDependencies): Start {
const dateFormat = core.uiSettings.get('dateFormat') as string;
const commonlyUsedRanges = core.uiSettings.get('timepicker:quickRanges') as CommonlyUsedRange[];
+ const { openModal } = createReactOverlays(core);
const timeRangeAction = new CustomTimeRangeAction({
- openModal: core.overlays.openModal,
+ openModal,
dateFormat,
commonlyUsedRanges,
});
@@ -53,7 +55,7 @@ export class AdvancedUiActionsPublicPlugin
uiActions.attachAction(CONTEXT_MENU_TRIGGER, timeRangeAction.id);
const timeRangeBadge = new CustomTimeRangeBadge({
- openModal: core.overlays.openModal,
+ openModal,
dateFormat,
commonlyUsedRanges,
});
diff --git a/x-pack/plugins/advanced_ui_actions/public/types.ts b/x-pack/plugins/advanced_ui_actions/public/types.ts
index bbd7c5528276f1..313b09535b196a 100644
--- a/x-pack/plugins/advanced_ui_actions/public/types.ts
+++ b/x-pack/plugins/advanced_ui_actions/public/types.ts
@@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { OverlayRef } from '../../../../src/core/public';
+import { KibanaReactOverlays } from '../../../../src/plugins/kibana_react/public';
export interface CommonlyUsedRange {
from: string;
@@ -12,10 +12,4 @@ export interface CommonlyUsedRange {
display: string;
}
-export type OpenModal = (
- modalChildren: React.ReactNode,
- modalProps?: {
- closeButtonAriaLabel?: string;
- 'data-test-subj'?: string;
- }
-) => OverlayRef;
+export type OpenModal = KibanaReactOverlays['openModal'];