Remove react references from core OverlayService apis (#48431) (#51006)

* move/rename overlay mount and unmount types from banner to parent module

Signed-off-by: pgayvallet <pierre.gayvallet@elastic.co>

* migrate openModal / modalService

Signed-off-by: pgayvallet <pierre.gayvallet@elastic.co>

fix I18nProvider import path

Signed-off-by: pgayvallet <pierre.gayvallet@elastic.co>

* updates core doc

Signed-off-by: pgayvallet <pierre.gayvallet@elastic.co>

update doc bis

Signed-off-by: pgayvallet <pierre.gayvallet@elastic.co>

* migrate openFlyout / flyout service

Signed-off-by: pgayvallet <pierre.gayvallet@elastic.co>

* remove CoreStart export from kibana-react

Signed-off-by: pgayvallet <pierre.gayvallet@elastic.co>

* adapt some calls to new signature

Signed-off-by: pgayvallet <pierre.gayvallet@elastic.co>

* adapt new calls

Signed-off-by: pgayvallet <pierre.gayvallet@elastic.co>

* adapt js call

Signed-off-by: pgayvallet <pierre.gayvallet@elastic.co>

* add flex layout on mountWrapper component to avoid losing scroll on overlays

Signed-off-by: pgayvallet <pierre.gayvallet@elastic.co>

* create proper flyout/modal services

* update generated doc

* update snapshot on data/query_bar

* use ReactNode instead of ReactElement

* rename mountForComponent to reactMount

* change reactMount usages to toMountPoint

* remove duplicate MountPoint type in overlays

* remove duplicate mount utilities from overlays

* allow to specify custom class name to MountWrapper

* Allow to specialize MountPoint on HTMLElement subtypes

* updates generated doc

* undeprecates openFlyout/openModal & remove direct subservice access from overlayService

* adapt toast api to get i18n context from service

* use MountPoint instead of inline definition
This commit is contained in:
Pierre Gayvallet 2019-11-19 11:42:18 +01:00 committed by GitHub
parent 27102c2404
commit e5eddc8abc
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
63 changed files with 1031 additions and 675 deletions

View file

@ -15,11 +15,6 @@ export interface ChromeNavControl
| Property | Type | Description |
| --- | --- | --- |
| [mount](./kibana-plugin-public.chromenavcontrol.mount.md) | <code>MountPoint</code> | |
| [order](./kibana-plugin-public.chromenavcontrol.order.md) | <code>number</code> | |
## Methods
| Method | Description |
| --- | --- |
| [mount(targetDomElement)](./kibana-plugin-public.chromenavcontrol.mount.md) | |

View file

@ -2,21 +2,10 @@
[Home](./index.md) &gt; [kibana-plugin-public](./kibana-plugin-public.md) &gt; [ChromeNavControl](./kibana-plugin-public.chromenavcontrol.md) &gt; [mount](./kibana-plugin-public.chromenavcontrol.mount.md)
## ChromeNavControl.mount() method
## ChromeNavControl.mount property
<b>Signature:</b>
```typescript
mount(targetDomElement: HTMLElement): () => void;
mount: MountPoint;
```
## Parameters
| Parameter | Type | Description |
| --- | --- | --- |
| targetDomElement | <code>HTMLElement</code> | |
<b>Returns:</b>
`() => void`

View file

@ -9,5 +9,5 @@ A function that should mount DOM content inside the provided container element a
<b>Signature:</b>
```typescript
export declare type MountPoint = (element: HTMLElement) => UnmountCallback;
export declare type MountPoint<T extends HTMLElement = HTMLElement> = (element: T) => UnmountCallback;
```

View file

@ -16,6 +16,6 @@ export interface OverlayStart
| Property | Type | Description |
| --- | --- | --- |
| [banners](./kibana-plugin-public.overlaystart.banners.md) | <code>OverlayBannersStart</code> | [OverlayBannersStart](./kibana-plugin-public.overlaybannersstart.md) |
| [openFlyout](./kibana-plugin-public.overlaystart.openflyout.md) | <code>(flyoutChildren: React.ReactNode, flyoutProps?: {</code><br/><code> closeButtonAriaLabel?: string;</code><br/><code> 'data-test-subj'?: string;</code><br/><code> }) =&gt; OverlayRef</code> | |
| [openModal](./kibana-plugin-public.overlaystart.openmodal.md) | <code>(modalChildren: React.ReactNode, modalProps?: {</code><br/><code> className?: string;</code><br/><code> closeButtonAriaLabel?: string;</code><br/><code> 'data-test-subj'?: string;</code><br/><code> }) =&gt; OverlayRef</code> | |
| [openFlyout](./kibana-plugin-public.overlaystart.openflyout.md) | <code>OverlayFlyoutStart['open']</code> | |
| [openModal](./kibana-plugin-public.overlaystart.openmodal.md) | <code>OverlayModalStart['open']</code> | |

View file

@ -4,11 +4,9 @@
## OverlayStart.openFlyout property
<b>Signature:</b>
```typescript
openFlyout: (flyoutChildren: React.ReactNode, flyoutProps?: {
closeButtonAriaLabel?: string;
'data-test-subj'?: string;
}) => OverlayRef;
openFlyout: OverlayFlyoutStart['open'];
```

View file

@ -4,12 +4,9 @@
## OverlayStart.openModal property
<b>Signature:</b>
```typescript
openModal: (modalChildren: React.ReactNode, modalProps?: {
className?: string;
closeButtonAriaLabel?: string;
'data-test-subj'?: string;
}) => OverlayRef;
openModal: OverlayModalStart['open'];
```

View file

@ -20,11 +20,12 @@
import { sortBy } from 'lodash';
import { BehaviorSubject, ReplaySubject, Observable } from 'rxjs';
import { map, takeUntil } from 'rxjs/operators';
import { MountPoint } from '../../types';
/** @public */
export interface ChromeNavControl {
order?: number;
mount(targetDomElement: HTMLElement): () => void;
mount: MountPoint;
}
/**

View file

@ -18,9 +18,10 @@
*/
import React from 'react';
import { MountPoint } from '../../../types';
interface Props {
extension?: (el: HTMLDivElement) => () => void;
extension?: MountPoint<HTMLDivElement>;
}
export class HeaderExtension extends React.Component<Props> {

View file

@ -19,7 +19,7 @@
import angular from 'angular';
import { InternalCoreSetup, InternalCoreStart } from '../core_system';
import { LegacyCoreSetup, LegacyCoreStart } from '../';
import { LegacyCoreSetup, LegacyCoreStart, MountPoint } from '../';
/** @internal */
export interface LegacyPlatformParams {
@ -40,7 +40,7 @@ interface StartDeps {
}
interface BootstrapModule {
bootstrap: (targetDomElement: HTMLElement) => void;
bootstrap: MountPoint;
}
/**

View file

@ -40,6 +40,7 @@ function render(props: ErrorToastProps = {}) {
error={props.error || new Error('error message')}
title={props.title || 'An error occured'}
toastMessage={props.toastMessage || 'This is the toast message'}
i18nContext={() => ({ children }) => <React.Fragment>{children}</React.Fragment>}
/>
);
}

View file

@ -18,6 +18,7 @@
*/
import React from 'react';
import ReactDOM from 'react-dom';
import {
EuiButton,
@ -32,12 +33,14 @@ import { EuiSpacer } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { OverlayStart } from '../../overlays';
import { I18nStart } from '../../i18n';
interface ErrorToastProps {
title: string;
error: Error;
toastMessage: string;
openModal: OverlayStart['openModal'];
i18nContext: () => I18nStart['Context'];
}
/**
@ -50,33 +53,48 @@ function showErrorDialog({
title,
error,
openModal,
}: Pick<ErrorToastProps, 'error' | 'title' | 'openModal'>) {
i18nContext,
}: Pick<ErrorToastProps, 'error' | 'title' | 'openModal' | 'i18nContext'>) {
const I18nContext = i18nContext();
const modal = openModal(
<React.Fragment>
<EuiModalHeader>
<EuiModalHeaderTitle>{title}</EuiModalHeaderTitle>
</EuiModalHeader>
<EuiModalBody>
<EuiCallOut size="s" color="danger" iconType="alert" title={error.message} />
{error.stack && (
<React.Fragment>
<EuiSpacer size="s" />
<EuiCodeBlock isCopyable={true} paddingSize="s">
{error.stack}
</EuiCodeBlock>
</React.Fragment>
)}
</EuiModalBody>
<EuiModalFooter>
<EuiButton onClick={() => modal.close()} fill>
<FormattedMessage id="core.notifications.errorToast.closeModal" defaultMessage="Close" />
</EuiButton>
</EuiModalFooter>
</React.Fragment>
mount(
<React.Fragment>
<I18nContext>
<EuiModalHeader>
<EuiModalHeaderTitle>{title}</EuiModalHeaderTitle>
</EuiModalHeader>
<EuiModalBody>
<EuiCallOut size="s" color="danger" iconType="alert" title={error.message} />
{error.stack && (
<React.Fragment>
<EuiSpacer size="s" />
<EuiCodeBlock isCopyable={true} paddingSize="s">
{error.stack}
</EuiCodeBlock>
</React.Fragment>
)}
</EuiModalBody>
<EuiModalFooter>
<EuiButton onClick={() => modal.close()} fill>
<FormattedMessage
id="core.notifications.errorToast.closeModal"
defaultMessage="Close"
/>
</EuiButton>
</EuiModalFooter>
</I18nContext>
</React.Fragment>
)
);
}
export function ErrorToast({ title, error, toastMessage, openModal }: ErrorToastProps) {
export function ErrorToast({
title,
error,
toastMessage,
openModal,
i18nContext,
}: ErrorToastProps) {
return (
<React.Fragment>
<p data-test-subj="errorToastMessage">{toastMessage}</p>
@ -84,7 +102,7 @@ export function ErrorToast({ title, error, toastMessage, openModal }: ErrorToast
<EuiButton
size="s"
color="danger"
onClick={() => showErrorDialog({ title, error, openModal })}
onClick={() => showErrorDialog({ title, error, openModal, i18nContext })}
>
<FormattedMessage
id="core.toasts.errorToast.seeFullError"
@ -95,3 +113,8 @@ export function ErrorToast({ title, error, toastMessage, openModal }: ErrorToast
</React.Fragment>
);
}
const mount = (component: React.ReactElement) => (container: HTMLElement) => {
ReactDOM.render(component, container);
return () => ReactDOM.unmountComponentAtNode(container);
};

View file

@ -51,10 +51,13 @@ function uiSettingsMock() {
function toastDeps() {
return {
uiSettings: uiSettingsMock(),
i18n: i18nServiceMock.createStartContract(),
};
}
function startDeps() {
return { overlays: {} as any, i18n: i18nServiceMock.createStartContract() };
}
describe('#get$()', () => {
it('returns observable that emits NEW toast list when something added or removed', () => {
const toasts = new ToastsApi(toastDeps());
@ -188,6 +191,7 @@ describe('#addDanger()', () => {
describe('#addError', () => {
it('adds an error toast', async () => {
const toasts = new ToastsApi(toastDeps());
toasts.start(startDeps());
const toast = toasts.addError(new Error('unexpected error'), { title: 'Something went wrong' });
expect(toast).toHaveProperty('color', 'danger');
expect(toast).toHaveProperty('title', 'Something went wrong');
@ -195,6 +199,7 @@ describe('#addError', () => {
it('returns the created toast', async () => {
const toasts = new ToastsApi(toastDeps());
toasts.start(startDeps());
const toast = toasts.addError(new Error('unexpected error'), { title: 'Something went wrong' });
const currentToasts = await getCurrentToasts(toasts);
expect(currentToasts[0]).toBe(toast);

View file

@ -26,6 +26,7 @@ import { MountPoint } from '../../types';
import { mountReactNode } from '../../utils';
import { UiSettingsClientContract } from '../../ui_settings';
import { OverlayStart } from '../../overlays';
import { I18nStart } from '../../i18n';
/**
* Allowed fields for {@link ToastInput}.
@ -96,14 +97,16 @@ export class ToastsApi implements IToasts {
private uiSettings: UiSettingsClientContract;
private overlays?: OverlayStart;
private i18n?: I18nStart;
constructor(deps: { uiSettings: UiSettingsClientContract }) {
this.uiSettings = deps.uiSettings;
}
/** @internal */
public registerOverlays(overlays: OverlayStart) {
public start({ overlays, i18n }: { overlays: OverlayStart; i18n: I18nStart }) {
this.overlays = overlays;
this.i18n = i18n;
}
/** Observable of the toast messages to show to the user. */
@ -206,6 +209,7 @@ export class ToastsApi implements IToasts {
error={error}
title={options.title}
toastMessage={message}
i18nContext={() => this.i18n!.Context}
/>
),
});

View file

@ -58,7 +58,7 @@ export class ToastsService {
}
public start({ i18n, overlays, targetDomElement }: StartDeps) {
this.api!.registerOverlays(overlays);
this.api!.start({ overlays, i18n });
this.targetDomElement = targetDomElement;
render(

View file

@ -1,70 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`FlyoutService FlyoutRef#close() can be called multiple times on the same FlyoutRef 1`] = `
Array [
Array [
<div />,
],
]
`;
exports[`FlyoutService openFlyout() renders a flyout to the DOM 1`] = `
Array [
Array [
<mockConstructor>
<EuiFlyout
closeButtonAriaLabel="Closes this dialog"
hideCloseButton={false}
maxWidth={false}
onClose={[Function]}
ownFocus={false}
size="m"
>
<span>
Flyout content
</span>
</EuiFlyout>
</mockConstructor>,
<div />,
],
]
`;
exports[`FlyoutService openFlyout() with a currently active flyout replaces the current flyout with a new one 1`] = `
Array [
Array [
<mockConstructor>
<EuiFlyout
closeButtonAriaLabel="Closes this dialog"
hideCloseButton={false}
maxWidth={false}
onClose={[Function]}
ownFocus={false}
size="m"
>
<span>
Flyout content 1
</span>
</EuiFlyout>
</mockConstructor>,
<div />,
],
Array [
<mockConstructor>
<EuiFlyout
closeButtonAriaLabel="Closes this dialog"
hideCloseButton={false}
maxWidth={false}
onClose={[Function]}
ownFocus={false}
size="m"
>
<span>
Flyout content 2
</span>
</EuiFlyout>
</mockConstructor>,
<div />,
],
]
`;

View file

@ -1,64 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ModalService ModalRef#close() can be called multiple times on the same ModalRef 1`] = `
Array [
Array [
<div />,
],
]
`;
exports[`ModalService openModal() renders a modal to the DOM 1`] = `
Array [
Array [
<EuiOverlayMask>
<mockConstructor>
<EuiModal
maxWidth={true}
onClose={[Function]}
>
<span>
Modal content
</span>
</EuiModal>
</mockConstructor>
</EuiOverlayMask>,
<div />,
],
]
`;
exports[`ModalService openModal() with a currently active modal replaces the current modal with a new one 1`] = `
Array [
Array [
<EuiOverlayMask>
<mockConstructor>
<EuiModal
maxWidth={true}
onClose={[Function]}
>
<span>
Modal content 1
</span>
</EuiModal>
</mockConstructor>
</EuiOverlayMask>,
<div />,
],
Array [
<EuiOverlayMask>
<mockConstructor>
<EuiModal
maxWidth={true}
onClose={[Function]}
>
<span>
Flyout content 2
</span>
</EuiModal>
</mockConstructor>
</EuiOverlayMask>,
<div />,
],
]
`;

View file

@ -1 +1,2 @@
@import './banners/index';
@import './mount_wrapper';

View file

@ -0,0 +1,5 @@
.kbnOverlayMountWrapper {
display: flex;
flex-direction: column;
height: 100%;
}

View file

@ -97,9 +97,7 @@ export class OverlayBannersService {
if (!banners$.value.has(id)) {
return false;
}
banners$.next(banners$.value.remove(id));
return true;
},
@ -107,10 +105,8 @@ export class OverlayBannersService {
if (!id || !banners$.value.has(id)) {
return this.add(mount, priority);
}
const nextId = genId();
const nextBanner = { id: nextId, mount, priority };
banners$.next(banners$.value.remove(id).add(nextId, nextBanner));
return nextId;
},

View file

@ -0,0 +1,77 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`FlyoutService FlyoutRef#close() can be called multiple times on the same FlyoutRef 1`] = `
Array [
Array [
<div />,
],
]
`;
exports[`FlyoutService openFlyout() renders a flyout to the DOM 1`] = `
Array [
Array [
<mockConstructor>
<EuiFlyout
closeButtonAriaLabel="Closes this dialog"
hideCloseButton={false}
maxWidth={false}
onClose={[Function]}
ownFocus={false}
size="m"
>
<MountWrapper
className="kbnOverlayMountWrapper"
mount={[Function]}
/>
</EuiFlyout>
</mockConstructor>,
<div />,
],
]
`;
exports[`FlyoutService openFlyout() renders a flyout to the DOM 2`] = `"<span><div><div data-focus-guard=\\"true\\" tabindex=\\"0\\" style=\\"width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;\\"></div><div data-focus-guard=\\"true\\" tabindex=\\"1\\" style=\\"width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;\\"></div><div data-focus-lock-disabled=\\"false\\"><div role=\\"dialog\\" class=\\"euiFlyout euiFlyout--medium\\" tabindex=\\"0\\"><button class=\\"euiButtonIcon euiButtonIcon--text euiFlyout__closeButton\\" type=\\"button\\" aria-label=\\"Closes this dialog\\" data-test-subj=\\"euiFlyoutCloseButton\\"><svg width=\\"16\\" height=\\"16\\" viewBox=\\"0 0 16 16\\" xmlns=\\"http://www.w3.org/2000/svg\\" class=\\"euiIcon euiIcon--medium euiIcon-isLoading euiButtonIcon__icon\\" focusable=\\"false\\" aria-hidden=\\"true\\"></svg></button><div class=\\"kbnOverlayMountWrapper\\"><span>Flyout content</span></div></div></div><div data-focus-guard=\\"true\\" tabindex=\\"0\\" style=\\"width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;\\"></div></div></span>"`;
exports[`FlyoutService openFlyout() with a currently active flyout replaces the current flyout with a new one 1`] = `
Array [
Array [
<mockConstructor>
<EuiFlyout
closeButtonAriaLabel="Closes this dialog"
hideCloseButton={false}
maxWidth={false}
onClose={[Function]}
ownFocus={false}
size="m"
>
<MountWrapper
className="kbnOverlayMountWrapper"
mount={[Function]}
/>
</EuiFlyout>
</mockConstructor>,
<div />,
],
Array [
<mockConstructor>
<EuiFlyout
closeButtonAriaLabel="Closes this dialog"
hideCloseButton={false}
maxWidth={false}
onClose={[Function]}
ownFocus={false}
size="m"
>
<MountWrapper
className="kbnOverlayMountWrapper"
mount={[Function]}
/>
</EuiFlyout>
</mockConstructor>,
<div />,
],
]
`;
exports[`FlyoutService openFlyout() with a currently active flyout replaces the current flyout with a new one 2`] = `"<span><div><div data-focus-guard=\\"true\\" tabindex=\\"0\\" style=\\"width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;\\"></div><div data-focus-guard=\\"true\\" tabindex=\\"1\\" style=\\"width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;\\"></div><div data-focus-lock-disabled=\\"false\\"><div role=\\"dialog\\" class=\\"euiFlyout euiFlyout--medium\\" tabindex=\\"0\\"><button class=\\"euiButtonIcon euiButtonIcon--text euiFlyout__closeButton\\" type=\\"button\\" aria-label=\\"Closes this dialog\\" data-test-subj=\\"euiFlyoutCloseButton\\"><svg width=\\"16\\" height=\\"16\\" viewBox=\\"0 0 16 16\\" xmlns=\\"http://www.w3.org/2000/svg\\" class=\\"euiIcon euiIcon--medium euiIcon-isLoading euiButtonIcon__icon\\" focusable=\\"false\\" aria-hidden=\\"true\\"></svg></button><div class=\\"kbnOverlayMountWrapper\\"><span>Flyout content 2</span></div></div></div><div data-focus-guard=\\"true\\" tabindex=\\"0\\" style=\\"width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;\\"></div></div></span>"`;

View file

@ -0,0 +1,43 @@
/*
* 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 { FlyoutService, OverlayFlyoutStart } from './flyout_service';
const createStartContractMock = () => {
const startContract: jest.Mocked<OverlayFlyoutStart> = {
open: jest.fn().mockReturnValue({
close: jest.fn(),
onClose: Promise.resolve(),
}),
};
return startContract;
};
const createMock = () => {
const mocked: jest.Mocked<PublicMethodsOf<FlyoutService>> = {
start: jest.fn(),
};
mocked.start.mockReturnValue(createStartContractMock());
return mocked;
};
export const overlayFlyoutServiceMock = {
create: createMock,
createStartContract: createStartContractMock,
};

View file

@ -16,11 +16,12 @@
* specific language governing permissions and limitations
* under the License.
*/
import { mockReactDomRender, mockReactDomUnmount } from './flyout.test.mocks';
import { mockReactDomRender, mockReactDomUnmount } from '../overlay.test.mocks';
import React from 'react';
import { i18nServiceMock } from '../i18n/i18n_service.mock';
import { FlyoutRef, FlyoutService } from './flyout';
import { mount } from 'enzyme';
import { i18nServiceMock } from '../../i18n/i18n_service.mock';
import { FlyoutService, OverlayFlyoutStart } from './flyout_service';
import { OverlayRef } from '../types';
const i18nMock = i18nServiceMock.createStartContract();
@ -29,35 +30,50 @@ beforeEach(() => {
mockReactDomUnmount.mockClear();
});
const mountText = (text: string) => (container: HTMLElement) => {
const content = document.createElement('span');
content.textContent = text;
container.append(content);
return () => {};
};
const getServiceStart = () => {
const service = new FlyoutService();
return service.start({ i18n: i18nMock, targetDomElement: document.createElement('div') });
};
describe('FlyoutService', () => {
let flyouts: OverlayFlyoutStart;
beforeEach(() => {
flyouts = getServiceStart();
});
describe('openFlyout()', () => {
it('renders a flyout to the DOM', () => {
const target = document.createElement('div');
const flyoutService = new FlyoutService(target);
expect(mockReactDomRender).not.toHaveBeenCalled();
flyoutService.openFlyout(i18nMock, <span>Flyout content</span>);
flyouts.open(mountText('Flyout content'));
expect(mockReactDomRender.mock.calls).toMatchSnapshot();
const modalContent = mount(mockReactDomRender.mock.calls[0][0]);
expect(modalContent.html()).toMatchSnapshot();
});
describe('with a currently active flyout', () => {
let target: HTMLElement;
let flyoutService: FlyoutService;
let ref1: FlyoutRef;
let ref1: OverlayRef;
beforeEach(() => {
target = document.createElement('div');
flyoutService = new FlyoutService(target);
ref1 = flyoutService.openFlyout(i18nMock, <span>Flyout content 1</span>);
ref1 = flyouts.open(mountText('Flyout content'));
});
it('replaces the current flyout with a new one', () => {
flyoutService.openFlyout(i18nMock, <span>Flyout content 2</span>);
flyouts.open(mountText('Flyout content 2'));
expect(mockReactDomRender.mock.calls).toMatchSnapshot();
expect(mockReactDomUnmount).toHaveBeenCalledTimes(1);
const modalContent = mount(mockReactDomRender.mock.calls[1][0]);
expect(modalContent.html()).toMatchSnapshot();
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);
flyoutService.openFlyout(i18nMock, <span>Flyout content 2</span>);
flyouts.open(mountText('Flyout content 2'));
await ref1.onClose;
expect(onCloseComplete).toBeCalledTimes(1);
});
@ -65,9 +81,7 @@ describe('FlyoutService', () => {
});
describe('FlyoutRef#close()', () => {
it('resolves the onClose Promise', async () => {
const target = document.createElement('div');
const flyoutService = new FlyoutService(target);
const ref = flyoutService.openFlyout(i18nMock, <span>Flyout content</span>);
const ref = flyouts.open(mountText('Flyout content'));
const onCloseComplete = jest.fn();
ref.onClose.then(onCloseComplete);
@ -76,9 +90,7 @@ describe('FlyoutService', () => {
expect(onCloseComplete).toHaveBeenCalledTimes(1);
});
it('can be called multiple times on the same FlyoutRef', async () => {
const target = document.createElement('div');
const flyoutService = new FlyoutService(target);
const ref = flyoutService.openFlyout(i18nMock, <span>Flyout content</span>);
const ref = flyouts.open(mountText('Flyout content'));
expect(mockReactDomUnmount).not.toHaveBeenCalled();
await ref.close();
expect(mockReactDomUnmount.mock.calls).toMatchSnapshot();
@ -86,10 +98,8 @@ describe('FlyoutService', () => {
expect(mockReactDomUnmount).toHaveBeenCalledTimes(1);
});
it("on a stale FlyoutRef doesn't affect the active flyout", async () => {
const target = document.createElement('div');
const flyoutService = new FlyoutService(target);
const ref1 = flyoutService.openFlyout(i18nMock, <span>Flyout content 1</span>);
const ref2 = flyoutService.openFlyout(i18nMock, <span>Flyout content 2</span>);
const ref1 = flyouts.open(mountText('Flyout content 1'));
const ref2 = flyouts.open(mountText('Flyout content 2'));
const onCloseComplete = jest.fn();
ref2.onClose.then(onCloseComplete);
mockReactDomUnmount.mockClear();

View file

@ -23,8 +23,10 @@ import { EuiFlyout } from '@elastic/eui';
import React from 'react';
import { render, unmountComponentAtNode } from 'react-dom';
import { Subject } from 'rxjs';
import { I18nStart } from '../i18n';
import { OverlayRef } from './overlay_service';
import { I18nStart } from '../../i18n';
import { MountPoint } from '../../types';
import { OverlayRef } from '../types';
import { MountWrapper } from '../../utils';
/**
* A FlyoutRef is a reference to an opened flyout panel. It offers methods to
@ -37,7 +39,7 @@ import { OverlayRef } from './overlay_service';
*
* @public
*/
export class FlyoutRef implements OverlayRef {
class FlyoutRef implements OverlayRef {
/**
* An Promise that will resolve once this flyout is closed.
*
@ -66,55 +68,77 @@ export class FlyoutRef implements OverlayRef {
}
}
/**
* APIs to open and manage fly-out dialogs.
*
* @public
*/
export interface OverlayFlyoutStart {
/**
* Opens a flyout panel with the given mount point inside. You can use
* `close()` on the returned FlyoutRef to close the flyout.
*
* @param mount {@link MountPoint} - Mounts the children inside a flyout panel
* @param options {@link OverlayFlyoutOpenOptions} - options for the flyout
* @return {@link OverlayRef} A reference to the opened flyout panel.
*/
open(mount: MountPoint, options?: OverlayFlyoutOpenOptions): OverlayRef;
}
/**
* @public
*/
export interface OverlayFlyoutOpenOptions {
className?: string;
closeButtonAriaLabel?: string;
'data-test-subj'?: string;
}
interface StartDeps {
i18n: I18nStart;
targetDomElement: Element;
}
/** @internal */
export class FlyoutService {
private activeFlyout: FlyoutRef | null = null;
private targetDomElement: Element | null = null;
constructor(private readonly targetDomElement: Element) {}
public start({ i18n, targetDomElement }: StartDeps): OverlayFlyoutStart {
this.targetDomElement = targetDomElement;
/**
* 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.
*/
public openFlyout = (
i18n: I18nStart,
flyoutChildren: React.ReactNode,
flyoutProps: {
closeButtonAriaLabel?: string;
'data-test-subj'?: string;
} = {}
): FlyoutRef => {
// If there is an active flyout session close it before opening a new one.
if (this.activeFlyout) {
this.activeFlyout.close();
this.cleanupDom();
}
return {
open: (mount: MountPoint, options: OverlayFlyoutOpenOptions = {}): OverlayRef => {
// If there is an active flyout session close it before opening a new one.
if (this.activeFlyout) {
this.activeFlyout.close();
this.cleanupDom();
}
const flyout = new FlyoutRef();
const flyout = new FlyoutRef();
// If a flyout gets closed through it's FlyoutRef, remove it from the dom
flyout.onClose.then(() => {
if (this.activeFlyout === flyout) {
this.cleanupDom();
}
});
// If a flyout gets closed through it's FlyoutRef, remove it from the dom
flyout.onClose.then(() => {
if (this.activeFlyout === flyout) {
this.cleanupDom();
}
});
this.activeFlyout = flyout;
this.activeFlyout = flyout;
render(
<i18n.Context>
<EuiFlyout {...flyoutProps} onClose={() => flyout.close()}>
{flyoutChildren}
</EuiFlyout>
</i18n.Context>,
this.targetDomElement
);
render(
<i18n.Context>
<EuiFlyout {...options} onClose={() => flyout.close()}>
<MountWrapper mount={mount} className="kbnOverlayMountWrapper" />
</EuiFlyout>
</i18n.Context>,
this.targetDomElement
);
return flyout;
};
return flyout;
},
};
}
/**
* Using React.Render to re-render into a target DOM element will replace
@ -124,8 +148,10 @@ export class FlyoutService {
* depend on unmounting for cleanup behaviour.
*/
private cleanupDom(): void {
unmountComponentAtNode(this.targetDomElement);
this.targetDomElement.innerHTML = '';
if (this.targetDomElement != null) {
unmountComponentAtNode(this.targetDomElement);
this.targetDomElement.innerHTML = '';
}
this.activeFlyout = null;
}
}

View 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 { FlyoutService, OverlayFlyoutStart, OverlayFlyoutOpenOptions } from './flyout_service';

View file

@ -17,5 +17,8 @@
* under the License.
*/
export { OverlayRef } from './types';
export { OverlayBannersStart } from './banners';
export { OverlayService, OverlayStart, OverlayRef } from './overlay_service';
export { OverlayFlyoutStart, OverlayFlyoutOpenOptions } from './flyout';
export { OverlayModalStart, OverlayModalOpenOptions } from './modal';
export { OverlayService, OverlayStart } from './overlay_service';

View file

@ -1,122 +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.
*/
/* eslint-disable max-classes-per-file */
import { EuiModal, EuiOverlayMask } from '@elastic/eui';
import React from 'react';
import { render, unmountComponentAtNode } from 'react-dom';
import { Subject } from 'rxjs';
import { I18nStart } from '../i18n';
import { OverlayRef } from './overlay_service';
/**
* A ModalRef is a reference to an opened modal. It offers methods to
* close the modal.
*
* @public
*/
export class ModalRef implements OverlayRef {
public readonly onClose: Promise<void>;
private closeSubject = new Subject<void>();
constructor() {
this.onClose = this.closeSubject.toPromise();
}
/**
* Closes the referenced modal if it's still open which in turn will
* resolve the `onClose` Promise. If the modal had already been
* closed this method does nothing.
*/
public close(): Promise<void> {
if (!this.closeSubject.closed) {
this.closeSubject.next();
this.closeSubject.complete();
}
return this.onClose;
}
}
/** @internal */
export class ModalService {
private activeModal: ModalRef | null = null;
constructor(private readonly targetDomElement: Element) {}
/**
* 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.
*/
public openModal = (
i18n: I18nStart,
modalChildren: React.ReactNode,
modalProps: {
closeButtonAriaLabel?: string;
'data-test-subj'?: string;
} = {}
): ModalRef => {
// If there is an active flyout session close it before opening a new one.
if (this.activeModal) {
this.activeModal.close();
this.cleanupDom();
}
const modal = new ModalRef();
// If a modal gets closed through it's ModalRef, remove it from the dom
modal.onClose.then(() => {
if (this.activeModal === modal) {
this.cleanupDom();
}
});
this.activeModal = modal;
render(
<EuiOverlayMask>
<i18n.Context>
<EuiModal {...modalProps} onClose={() => modal.close()}>
{modalChildren}
</EuiModal>
</i18n.Context>
</EuiOverlayMask>,
this.targetDomElement
);
return modal;
};
/**
* Using React.Render to re-render into a target DOM element will replace
* the content of the target but won't call unmountComponent on any
* components inside the target or any of their children. So we properly
* cleanup the DOM here to prevent subtle bugs in child components which
* depend on unmounting for cleanup behaviour.
*/
private cleanupDom(): void {
unmountComponentAtNode(this.targetDomElement);
this.targetDomElement.innerHTML = '';
this.activeModal = null;
}
}

View file

@ -0,0 +1,69 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ModalService ModalRef#close() can be called multiple times on the same ModalRef 1`] = `
Array [
Array [
<div />,
],
]
`;
exports[`ModalService openModal() renders a modal to the DOM 1`] = `
Array [
Array [
<EuiOverlayMask>
<mockConstructor>
<EuiModal
maxWidth={true}
onClose={[Function]}
>
<MountWrapper
className="kbnOverlayMountWrapper"
mount={[Function]}
/>
</EuiModal>
</mockConstructor>
</EuiOverlayMask>,
<div />,
],
]
`;
exports[`ModalService openModal() renders a modal to the DOM 2`] = `"<div data-focus-guard=\\"true\\" tabindex=\\"0\\" style=\\"width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;\\"></div><div data-focus-guard=\\"true\\" tabindex=\\"1\\" style=\\"width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;\\"></div><div data-focus-lock-disabled=\\"false\\"><div class=\\"euiModal euiModal--maxWidth-default\\" tabindex=\\"0\\"><button class=\\"euiButtonIcon euiButtonIcon--text euiModal__closeIcon\\" type=\\"button\\" aria-label=\\"Closes this modal window\\"><svg width=\\"16\\" height=\\"16\\" viewBox=\\"0 0 16 16\\" xmlns=\\"http://www.w3.org/2000/svg\\" class=\\"euiIcon euiIcon--medium euiIcon-isLoading euiButtonIcon__icon\\" focusable=\\"false\\" aria-hidden=\\"true\\"></svg></button><div class=\\"euiModal__flex\\"><div class=\\"kbnOverlayMountWrapper\\"><span>Modal content</span></div></div></div></div><div data-focus-guard=\\"true\\" tabindex=\\"0\\" style=\\"width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;\\"></div>"`;
exports[`ModalService openModal() with a currently active modal replaces the current modal with a new one 1`] = `
Array [
Array [
<EuiOverlayMask>
<mockConstructor>
<EuiModal
maxWidth={true}
onClose={[Function]}
>
<MountWrapper
className="kbnOverlayMountWrapper"
mount={[Function]}
/>
</EuiModal>
</mockConstructor>
</EuiOverlayMask>,
<div />,
],
Array [
<EuiOverlayMask>
<mockConstructor>
<EuiModal
maxWidth={true}
onClose={[Function]}
>
<MountWrapper
className="kbnOverlayMountWrapper"
mount={[Function]}
/>
</EuiModal>
</mockConstructor>
</EuiOverlayMask>,
<div />,
],
]
`;

View 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 { ModalService, OverlayModalStart, OverlayModalOpenOptions } from './modal_service';

View file

@ -0,0 +1,43 @@
/*
* 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 { ModalService, OverlayModalStart } from './modal_service';
const createStartContractMock = () => {
const startContract: jest.Mocked<OverlayModalStart> = {
open: jest.fn().mockReturnValue({
close: jest.fn(),
onClose: Promise.resolve(),
}),
};
return startContract;
};
const createMock = () => {
const mocked: jest.Mocked<PublicMethodsOf<ModalService>> = {
start: jest.fn(),
};
mocked.start.mockReturnValue(createStartContractMock());
return mocked;
};
export const overlayModalServiceMock = {
create: createMock,
createStartContract: createStartContractMock,
};

View file

@ -16,11 +16,14 @@
* specific language governing permissions and limitations
* under the License.
*/
import { mockReactDomRender, mockReactDomUnmount } from './flyout.test.mocks';
import { mockReactDomRender, mockReactDomUnmount } from '../overlay.test.mocks';
import React from 'react';
import { i18nServiceMock } from '../i18n/i18n_service.mock';
import { ModalService, ModalRef } from './modal';
import { mount } from 'enzyme';
import { i18nServiceMock } from '../../i18n/i18n_service.mock';
import { ModalService, OverlayModalStart } from './modal_service';
import { mountReactNode } from '../../utils';
import { OverlayRef } from '../types';
const i18nMock = i18nServiceMock.createStartContract();
@ -29,45 +32,59 @@ beforeEach(() => {
mockReactDomUnmount.mockClear();
});
const getServiceStart = () => {
const service = new ModalService();
return service.start({ i18n: i18nMock, targetDomElement: document.createElement('div') });
};
describe('ModalService', () => {
let modals: OverlayModalStart;
beforeEach(() => {
modals = getServiceStart();
});
describe('openModal()', () => {
it('renders a modal to the DOM', () => {
const target = document.createElement('div');
const modalService = new ModalService(target);
expect(mockReactDomRender).not.toHaveBeenCalled();
modalService.openModal(i18nMock, <span>Modal content</span>);
expect(mockReactDomRender.mock.calls).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, <span>Modal content 1</span>);
modals.open(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 ref1: OverlayRef;
beforeEach(() => {
ref1 = modals.open(mountReactNode(<span>Modal content 1</span>));
});
it('replaces the current modal with a new one', () => {
modalService.openModal(i18nMock, <span>Flyout content 2</span>);
modals.open(mountReactNode(<span>Flyout content 2</span>));
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, <span>Flyout content 2</span>);
modals.open(mountReactNode(<span>Flyout content 2</span>));
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, <span>Flyout content</span>);
const ref = modals.open(mountReactNode(<span>Flyout content</span>));
const onCloseComplete = jest.fn();
ref.onClose.then(onCloseComplete);
@ -75,21 +92,19 @@ 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, <span>Flyout content</span>);
const ref = modals.open(mountReactNode(<span>Flyout content</span>));
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, <span>Modal content 1</span>);
const ref2 = modalService.openModal(i18nMock, <span>Modal content 2</span>);
const ref1 = modals.open(mountReactNode(<span>Modal content 1</span>));
const ref2 = modals.open(mountReactNode(<span>Modal content 2</span>));
const onCloseComplete = jest.fn();
ref2.onClose.then(onCloseComplete);
mockReactDomUnmount.mockClear();

View file

@ -0,0 +1,148 @@
/*
* 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.
*/
/* eslint-disable max-classes-per-file */
import { EuiModal, EuiOverlayMask } from '@elastic/eui';
import React from 'react';
import { render, unmountComponentAtNode } from 'react-dom';
import { Subject } from 'rxjs';
import { I18nStart } from '../../i18n';
import { MountPoint } from '../../types';
import { OverlayRef } from '../types';
import { MountWrapper } from '../../utils';
/**
* A ModalRef is a reference to an opened modal. It offers methods to
* close the modal.
*
* @public
*/
class ModalRef implements OverlayRef {
public readonly onClose: Promise<void>;
private closeSubject = new Subject<void>();
constructor() {
this.onClose = this.closeSubject.toPromise();
}
/**
* Closes the referenced modal if it's still open which in turn will
* resolve the `onClose` Promise. If the modal had already been
* closed this method does nothing.
*/
public close(): Promise<void> {
if (!this.closeSubject.closed) {
this.closeSubject.next();
this.closeSubject.complete();
}
return this.onClose;
}
}
/**
* APIs to open and manage modal dialogs.
*
* @public
*/
export interface OverlayModalStart {
/**
* Opens a modal panel with the given mount point inside. You can use
* `close()` on the returned OverlayRef to close the modal.
*
* @param mount {@link MountPoint} - Mounts the children inside the modal
* @param options {@link OverlayModalOpenOptions} - options for the modal
* @return {@link OverlayRef} A reference to the opened modal.
*/
open(mount: MountPoint, options?: OverlayModalOpenOptions): OverlayRef;
}
/**
* @public
*/
export interface OverlayModalOpenOptions {
className?: string;
closeButtonAriaLabel?: string;
'data-test-subj'?: string;
}
interface StartDeps {
i18n: I18nStart;
targetDomElement: Element;
}
/** @internal */
export class ModalService {
private activeModal: ModalRef | null = null;
private targetDomElement: Element | null = null;
public start({ i18n, targetDomElement }: StartDeps): OverlayModalStart {
this.targetDomElement = targetDomElement;
return {
open: (mount: MountPoint, options: OverlayModalOpenOptions = {}): OverlayRef => {
// If there is an active flyout session close it before opening a new one.
if (this.activeModal) {
this.activeModal.close();
this.cleanupDom();
}
const modal = new ModalRef();
// If a modal gets closed through it's ModalRef, remove it from the dom
modal.onClose.then(() => {
if (this.activeModal === modal) {
this.cleanupDom();
}
});
this.activeModal = modal;
render(
<EuiOverlayMask>
<i18n.Context>
<EuiModal {...options} onClose={() => modal.close()}>
<MountWrapper mount={mount} className="kbnOverlayMountWrapper" />
</EuiModal>
</i18n.Context>
</EuiOverlayMask>,
targetDomElement
);
return modal;
},
};
}
/**
* Using React.Render to re-render into a target DOM element will replace
* the content of the target but won't call unmountComponent on any
* components inside the target or any of their children. So we properly
* cleanup the DOM here to prevent subtle bugs in child components which
* depend on unmounting for cleanup behaviour.
*/
private cleanupDom(): void {
if (this.targetDomElement != null) {
unmountComponentAtNode(this.targetDomElement);
this.targetDomElement.innerHTML = '';
}
this.activeModal = null;
}
}

View file

@ -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,
}));

View file

@ -18,17 +18,15 @@
*/
import { OverlayService, OverlayStart } from './overlay_service';
import { overlayBannersServiceMock } from './banners/banners_service.mock';
import { overlayFlyoutServiceMock } from './flyout/flyout_service.mock';
import { overlayModalServiceMock } from './modal/modal_service.mock';
const createStartContractMock = () => {
const startContract: DeeplyMockedKeys<OverlayStart> = {
openFlyout: jest.fn(),
openModal: jest.fn(),
openFlyout: overlayFlyoutServiceMock.createStartContract().open,
openModal: overlayModalServiceMock.createStartContract().open,
banners: overlayBannersServiceMock.createStartContract(),
};
startContract.openModal.mockReturnValue({
close: jest.fn(),
onClose: Promise.resolve(),
});
return startContract;
};

View file

@ -17,34 +17,11 @@
* under the License.
*/
import React from 'react';
import { FlyoutService } from './flyout';
import { ModalService } from './modal';
import { I18nStart } from '../i18n';
import { OverlayBannersStart, OverlayBannersService } from './banners';
import { UiSettingsClientContract } from '../ui_settings';
/**
* Returned by {@link OverlayStart} methods for closing a mounted overlay.
* @public
*/
export interface OverlayRef {
/**
* A Promise that will resolve once this overlay is closed.
*
* Overlays can close from user interaction, calling `close()` on the overlay
* reference or another overlay replacing yours via `openModal` or `openFlyout`.
*/
onClose: Promise<void>;
/**
* Closes the referenced overlay if it's still open which in turn will
* resolve the `onClose` Promise. If the overlay had already been
* closed this method does nothing.
*/
close(): Promise<void>;
}
import { OverlayBannersStart, OverlayBannersService } from './banners';
import { FlyoutService, OverlayFlyoutStart } from './flyout';
import { ModalService, OverlayModalStart } from './modal';
interface StartDeps {
i18n: I18nStart;
@ -54,19 +31,25 @@ interface StartDeps {
/** @internal */
export class OverlayService {
private bannersService = new OverlayBannersService();
private modalService = new ModalService();
private flyoutService = new FlyoutService();
public start({ i18n, targetDomElement, uiSettings }: StartDeps): OverlayStart {
const flyoutElement = document.createElement('div');
const modalElement = document.createElement('div');
targetDomElement.appendChild(flyoutElement);
const flyouts = this.flyoutService.start({ i18n, targetDomElement: flyoutElement });
const banners = this.bannersService.start({ i18n, uiSettings });
const modalElement = document.createElement('div');
targetDomElement.appendChild(modalElement);
const flyoutService = new FlyoutService(flyoutElement);
const modalService = new ModalService(modalElement);
const bannersService = new OverlayBannersService();
const modals = this.modalService.start({ i18n, targetDomElement: modalElement });
return {
banners: bannersService.start({ i18n, uiSettings }),
openFlyout: flyoutService.openFlyout.bind(flyoutService, i18n),
openModal: modalService.openModal.bind(modalService, i18n),
banners,
openFlyout: flyouts.open.bind(flyouts),
openModal: modals.open.bind(modals),
};
}
}
@ -75,19 +58,8 @@ export class OverlayService {
export interface OverlayStart {
/** {@link OverlayBannersStart} */
banners: OverlayBannersStart;
openFlyout: (
flyoutChildren: React.ReactNode,
flyoutProps?: {
closeButtonAriaLabel?: string;
'data-test-subj'?: string;
}
) => OverlayRef;
openModal: (
modalChildren: React.ReactNode,
modalProps?: {
className?: string;
closeButtonAriaLabel?: string;
'data-test-subj'?: string;
}
) => OverlayRef;
/** {@link OverlayFlyoutStart#open} */
openFlyout: OverlayFlyoutStart['open'];
/** {@link OverlayModalStart#open} */
openModal: OverlayModalStart['open'];
}

View file

@ -0,0 +1,39 @@
/*
* 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.
*/
/**
* Returned by {@link OverlayStart} methods for closing a mounted overlay.
* @public
*/
export interface OverlayRef {
/**
* A Promise that will resolve once this overlay is closed.
*
* Overlays can close from user interaction, calling `close()` on the overlay
* reference or another overlay replacing yours via `openModal` or `openFlyout`.
*/
onClose: Promise<void>;
/**
* Closes the referenced overlay if it's still open which in turn will
* resolve the `onClose` Promise. If the overlay had already been
* closed this method does nothing.
*/
close(): Promise<void>;
}

View file

@ -124,7 +124,7 @@ export type ChromeHelpExtension = (element: HTMLDivElement) => () => void;
// @public (undocumented)
export interface ChromeNavControl {
// (undocumented)
mount(targetDomElement: HTMLElement): () => void;
mount: MountPoint;
// (undocumented)
order?: number;
}
@ -620,7 +620,7 @@ export interface LegacyNavLink {
}
// @public
export type MountPoint = (element: HTMLElement) => UnmountCallback;
export type MountPoint<T extends HTMLElement = HTMLElement> = (element: T) => UnmountCallback;
// @public (undocumented)
export interface NotificationsSetup {
@ -657,17 +657,14 @@ export interface OverlayRef {
export interface OverlayStart {
// (undocumented)
banners: OverlayBannersStart;
// Warning: (ae-forgotten-export) The symbol "OverlayFlyoutStart" needs to be exported by the entry point index.d.ts
//
// (undocumented)
openFlyout: (flyoutChildren: React.ReactNode, flyoutProps?: {
closeButtonAriaLabel?: string;
'data-test-subj'?: string;
}) => OverlayRef;
openFlyout: OverlayFlyoutStart['open'];
// Warning: (ae-forgotten-export) The symbol "OverlayModalStart" needs to be exported by the entry point index.d.ts
//
// (undocumented)
openModal: (modalChildren: React.ReactNode, modalProps?: {
className?: string;
closeButtonAriaLabel?: string;
'data-test-subj'?: string;
}) => OverlayRef;
openModal: OverlayModalStart['open'];
}
// @public (undocumented)
@ -941,9 +938,12 @@ export class ToastsApi implements IToasts {
addSuccess(toastOrTitle: ToastInput): Toast;
addWarning(toastOrTitle: ToastInput): Toast;
get$(): Rx.Observable<Toast[]>;
// @internal (undocumented)
registerOverlays(overlays: OverlayStart): void;
remove(toastOrId: Toast | string): void;
// @internal (undocumented)
start({ overlays, i18n }: {
overlays: OverlayStart;
i18n: I18nStart;
}): void;
}
// @public (undocumented)

View file

@ -26,7 +26,7 @@
*
* @public
*/
export type MountPoint = (element: HTMLElement) => UnmountCallback;
export type MountPoint<T extends HTMLElement = HTMLElement> = (element: T) => UnmountCallback;
/**
* A function that will unmount the element previously mounted by

View file

@ -0,0 +1,79 @@
/*
* 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, mountReactNode } from './mount';
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 = <MountWrapper mount={mountPoint} />;
const container = mount(wrapper);
expect(container.html()).toMatchInlineSnapshot(
`"<div class=\\"kbnMountWrapper\\"><p class=\\"bar\\">hello</p></div>"`
);
});
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 = <MountWrapper mount={mountPoint} />;
const container = mount(wrapper);
expect(container.html()).toMatchInlineSnapshot(
`"<div class=\\"kbnMountWrapper\\"><p>initial</p></div>"`
);
el.textContent = 'changed';
container.update();
expect(container.html()).toMatchInlineSnapshot(
`"<div class=\\"kbnMountWrapper\\"><p>changed</p></div>"`
);
});
it('can render a detached react component', () => {
const mountPoint = mountReactNode(<span>detached</span>);
const wrapper = <MountWrapper mount={mountPoint} />;
const container = mount(wrapper);
expect(container.html()).toMatchInlineSnapshot(
`"<div class=\\"kbnMountWrapper\\"><span>detached</span></div>"`
);
});
it('accepts a className prop to override default className', () => {
const mountPoint = mountReactNode(<span>detached</span>);
const wrapper = <MountWrapper mount={mountPoint} className="customClass" />;
const container = mount(wrapper);
expect(container.html()).toMatchInlineSnapshot(
`"<div class=\\"customClass\\"><span>detached</span></div>"`
);
});
});

View file

@ -22,23 +22,26 @@ import { render, unmountComponentAtNode } from 'react-dom';
import { I18nProvider } from '@kbn/i18n/react';
import { MountPoint } from '../types';
const defaultWrapperClass = 'kbnMountWrapper';
/**
* MountWrapper is a react component to mount a {@link MountPoint} inside a react tree.
*/
export const MountWrapper: React.FunctionComponent<{ mount: MountPoint }> = ({ mount }) => {
export const MountWrapper: React.FunctionComponent<{ mount: MountPoint; className?: string }> = ({
mount,
className = defaultWrapperClass,
}) => {
const element = useRef(null);
useEffect(() => mount(element.current!), [mount]);
return <div className="kbnMountWrapper" ref={element} />;
return <div className={className} ref={element} />;
};
/**
* Mount converter for react components.
* Mount converter for react node.
*
* @param component to get a mount for
* @param node to get a mount for
*/
export const mountReactNode = (component: React.ReactNode): MountPoint => (
element: HTMLElement
) => {
render(<I18nProvider>{component}</I18nProvider>, element);
export const mountReactNode = (node: React.ReactNode): MountPoint => (element: HTMLElement) => {
render(<I18nProvider>{node}</I18nProvider>, element);
return () => unmountComponentAtNode(element);
};

View file

@ -19,6 +19,7 @@
import { i18n } from '@kbn/i18n';
import { CoreStart } from 'src/core/public';
import { toMountPoint } from '../../../../../../plugins/kibana_react/public';
import {
IAction,
createAction,
@ -79,17 +80,19 @@ export function createFilterAction(
const filterSelectionPromise: Promise<esFilters.Filter[]> = new Promise(resolve => {
const overlay = overlays.openModal(
applyFiltersPopover(
filters,
indexPatterns,
() => {
overlay.close();
resolve([]);
},
(filterSelection: esFilters.Filter[]) => {
overlay.close();
resolve(filterSelection);
}
toMountPoint(
applyFiltersPopover(
filters,
indexPatterns,
() => {
overlay.close();
resolve([]);
},
(filterSelection: esFilters.Filter[]) => {
overlay.close();
resolve(filterSelection);
}
)
),
{
'data-test-subj': 'test',

View file

@ -22,6 +22,7 @@ import { npStart } from 'ui/new_platform';
import { FormattedMessage } from '@kbn/i18n/react';
import { EuiButton, EuiTextAlign } from '@elastic/eui';
import { toMountPoint } 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(
<ShardFailureModal
request={request}
response={response}
title={title}
onClose={() => modal.close()}
/>,
toMountPoint(
<ShardFailureModal
request={request}
response={response}
title={title}
onClose={() => modal.close()}
/>
),
{
className: 'shardFailureModal',
}

View file

@ -18,6 +18,7 @@
*/
import React from 'react';
import { CoreStart } from '../../../../core/public';
import { toMountPoint } from '../../../../plugins/kibana_react/public';
import { ReplacePanelFlyout } from './replace_panel_flyout';
import {
IEmbeddable,
@ -44,18 +45,20 @@ export async function openReplacePanelFlyout(options: {
getEmbeddableFactories,
} = options;
const flyoutSession = core.overlays.openFlyout(
<ReplacePanelFlyout
container={embeddable}
onClose={() => {
if (flyoutSession) {
flyoutSession.close();
}
}}
panelToRemove={panelToRemove}
savedObjectsFinder={savedObjectFinder}
notifications={notifications}
getEmbeddableFactories={getEmbeddableFactories}
/>,
toMountPoint(
<ReplacePanelFlyout
container={embeddable}
onClose={() => {
if (flyoutSession) {
flyoutSession.close();
}
}}
panelToRemove={panelToRemove}
savedObjectsFinder={savedObjectFinder}
notifications={notifications}
getEmbeddableFactories={getEmbeddableFactories}
/>
),
{
'data-test-subj': 'replacePanelFlyout',
}

View file

@ -25,7 +25,8 @@ import {
TGetActionsCompatibleWithTrigger,
IAction,
} from '../ui_actions';
import { CoreStart } from '../../../../../core/public';
import { CoreStart, OverlayStart } from '../../../../../core/public';
import { toMountPoint } 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<Props, State> {
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(
<CustomizePanelModal
embeddable={context.embeddable}
updateTitle={title => {
session.close();
resolve({ title });
}}
/>,
toMountPoint(
<CustomizePanelModal
embeddable={context.embeddable}
updateTitle={title => {
session.close();
resolve({ title });
}}
/>
),
{
'data-test-subj': 'customizePanel',
}

View file

@ -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<ActionContext> {
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<any>
) {}

View file

@ -17,8 +17,8 @@
* 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 { toMountPoint } from '../../../../../../../kibana_react/public';
import { IContainer } from '../../../../containers';
import { AddPanelFlyout } from './add_panel_flyout';
import { GetEmbeddableFactory, GetEmbeddableFactories } from '../../../../types';
@ -27,7 +27,7 @@ export async function openAddPanelFlyout(options: {
embeddable: IContainer;
getFactory: GetEmbeddableFactory;
getAllFactories: GetEmbeddableFactories;
overlays: KibanaReactOverlays;
overlays: OverlayStart;
notifications: NotificationsStart;
SavedObjectFinder: React.ComponentType<any>;
}) {
@ -40,18 +40,20 @@ export async function openAddPanelFlyout(options: {
SavedObjectFinder,
} = options;
const flyoutSession = overlays.openFlyout(
<AddPanelFlyout
container={embeddable}
onClose={() => {
if (flyoutSession) {
flyoutSession.close();
}
}}
getFactory={getFactory}
getAllFactories={getAllFactories}
notifications={notifications}
SavedObjectFinder={SavedObjectFinder}
/>,
toMountPoint(
<AddPanelFlyout
container={embeddable}
onClose={() => {
if (flyoutSession) {
flyoutSession.close();
}
}}
getFactory={getFactory}
getAllFactories={getAllFactories}
notifications={notifications}
SavedObjectFinder={SavedObjectFinder}
/>
),
{
'data-test-subj': 'addPanelFlyout',
}

View file

@ -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 { toMountPoint } from '../../../../../kibana_react/public';
import { Embeddable, EmbeddableInput } from '../../embeddables';
import { GetMessageModal } from './get_message_modal';
import { FullNameEmbeddableOutput, hasFullNameOutput } from './say_hello_action';
@ -38,7 +39,7 @@ export function createSendMessageAction(overlays: CoreStart['overlays']) {
const greeting = `Hello, ${context.embeddable.getOutput().fullName}`;
const content = message ? `${greeting}. ${message}` : greeting;
overlays.openFlyout(<EuiFlyoutBody>{content}</EuiFlyoutBody>);
overlays.openFlyout(toMountPoint(<EuiFlyoutBody>{content}</EuiFlyoutBody>));
};
return createAction<ActionContext>({
@ -51,13 +52,15 @@ export function createSendMessageAction(overlays: CoreStart['overlays']) {
}
const modal = overlays.openModal(
<GetMessageModal
onCancel={() => modal.close()}
onDone={message => {
modal.close();
sendMessage(context, message);
}}
/>
toMountPoint(
<GetMessageModal
onCancel={() => modal.close()}
onDone={message => {
modal.close();
sendMessage(context, message);
}}
/>
)
);
},
});

View file

@ -22,6 +22,7 @@ import { i18n } from '@kbn/i18n';
import { TExecuteTriggerActions } from 'src/plugins/ui_actions/public';
import { CoreStart } from 'src/core/public';
import { toMountPoint } 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<ContactCardE
public getExplicitInput(): Promise<Partial<ContactCardEmbeddableInput>> {
return new Promise(resolve => {
const modalSession = this.overlays.openModal(
<ContactCardInitializer
onCancel={() => {
modalSession.close();
resolve(undefined);
}}
onCreate={(input: { firstName: string; lastName?: string }) => {
modalSession.close();
resolve(input);
}}
/>,
toMountPoint(
<ContactCardInitializer
onCancel={() => {
modalSession.close();
resolve(undefined);
}}
onCreate={(input: { firstName: string; lastName?: string }) => {
modalSession.close();
resolve(input);
}}
/>
),
{
'data-test-subj': 'createContactCardEmbeddable',
}

View file

@ -20,6 +20,7 @@
import { i18n } from '@kbn/i18n';
import * as React from 'react';
import { PluginInitializerContext, CoreSetup, CoreStart, Plugin } from '../../../core/public';
import { toMountPoint } from '../../kibana_react/public';
import { InspectorViewRegistry } from './view_registry';
import { Adapters, InspectorOptions, InspectorSession } from './types';
import { InspectorPanel } from './ui/inspector_panel';
@ -99,7 +100,7 @@ export class InspectorPublicPlugin implements Plugin<Setup, Start> {
}
return core.overlays.openFlyout(
<InspectorPanel views={views} adapters={adapters} title={options.title} />,
toMountPoint(<InspectorPanel views={views} adapters={adapters} title={options.title} />),
{
'data-test-subj': 'inspectorPanel',
closeButtonAriaLabel: closeButtonLabel,

View file

@ -48,13 +48,11 @@ test('can open flyout with React element', () => {
overlays.openFlyout(<div>foo</div>);
expect(coreOverlays.openFlyout).toHaveBeenCalledTimes(1);
expect(coreOverlays.openFlyout.mock.calls[0][0]).toMatchInlineSnapshot(`
<React.Fragment>
<div>
foo
</div>
</React.Fragment>
`);
const container = document.createElement('div');
const mount = coreOverlays.openFlyout.mock.calls[0][0];
mount(container);
expect(container.innerHTML).toMatchInlineSnapshot(`"<div>foo</div>"`);
});
test('can open modal with React element', () => {
@ -68,13 +66,10 @@ test('can open modal with React element', () => {
overlays.openModal(<div>bar</div>);
expect(coreOverlays.openModal).toHaveBeenCalledTimes(1);
expect(coreOverlays.openModal.mock.calls[0][0]).toMatchInlineSnapshot(`
<React.Fragment>
<div>
bar
</div>
</React.Fragment>
`);
const container = document.createElement('div');
const mount = coreOverlays.openModal.mock.calls[0][0];
mount(container);
expect(container.innerHTML).toMatchInlineSnapshot(`"<div>bar</div>"`);
});
test('passes through flyout options when opening flyout', () => {

View file

@ -20,6 +20,7 @@
import * as React from 'react';
import { KibanaServices } from '../context/types';
import { KibanaReactOverlays } from './types';
import { toMountPoint } from '../util';
export const createReactOverlays = (services: KibanaServices): KibanaReactOverlays => {
const checkCoreService = () => {
@ -30,12 +31,12 @@ export const createReactOverlays = (services: KibanaServices): KibanaReactOverla
const openFlyout: KibanaReactOverlays['openFlyout'] = (node, options?) => {
checkCoreService();
return services.overlays!.openFlyout(<>{node}</>, options);
return services.overlays!.openFlyout(toMountPoint(<>{node}</>), options);
};
const openModal: KibanaReactOverlays['openModal'] = (node, options?) => {
checkCoreService();
return services.overlays!.openModal(<>{node}</>, options);
return services.overlays!.openModal(toMountPoint(<>{node}</>), options);
};
const overlays: KibanaReactOverlays = {

View file

@ -27,6 +27,6 @@ export interface KibanaReactOverlays {
) => ReturnType<CoreStart['overlays']['openFlyout']>;
openModal: (
node: React.ReactNode,
options?: Parameters<CoreStart['overlays']['openFlyout']>['1']
options?: Parameters<CoreStart['overlays']['openModal']>['1']
) => ReturnType<CoreStart['overlays']['openModal']>;
}

View file

@ -21,6 +21,7 @@ import React from 'react';
import { EuiFlyout } from '@elastic/eui';
import { CoreStart } from 'src/core/public';
import { createAction, IAction } from '../../actions';
import { toMountPoint } from '../../../../kibana_react/public';
export const HELLO_WORLD_ACTION_ID = 'HELLO_WORLD_ACTION_ID';
@ -29,9 +30,11 @@ export function createHelloWorldAction(overlays: CoreStart['overlays']): IAction
type: HELLO_WORLD_ACTION_ID,
execute: async () => {
const flyoutSession = overlays.openFlyout(
<EuiFlyout ownFocus onClose={() => flyoutSession && flyoutSession.close()}>
Hello World, I am a hello world action!
</EuiFlyout>,
toMountPoint(
<EuiFlyout ownFocus onClose={() => flyoutSession && flyoutSession.close()}>
Hello World, I am a hello world action!
</EuiFlyout>
),
{
'data-test-subj': 'helloWorldAction',
}

View file

@ -21,6 +21,7 @@ import React from 'react';
import { EuiFlyout } from '@elastic/eui';
import { CoreStart } from 'src/core/public';
import { IAction, createAction } from '../../actions';
import { toMountPoint } from '../../../../kibana_react/public';
export const SAY_HELLO_ACTION = 'SAY_HELLO_ACTION';
@ -31,9 +32,11 @@ export function createSayHelloAction(overlays: CoreStart['overlays']): IAction<{
isCompatible: async ({ name }) => name !== undefined,
execute: async context => {
const flyoutSession = overlays.openFlyout(
<EuiFlyout ownFocus onClose={() => flyoutSession && flyoutSession.close()}>
this.getDisplayName(context)
</EuiFlyout>,
toMountPoint(
<EuiFlyout ownFocus onClose={() => flyoutSession && flyoutSession.close()}>
this.getDisplayName(context)
</EuiFlyout>
),
{
'data-test-subj': 'sayHelloAction',
}

View file

@ -22,6 +22,7 @@ import { npStart, npSetup } from 'ui/new_platform';
import { CONTEXT_MENU_TRIGGER, IEmbeddable } from '../../../../../src/plugins/embeddable/public';
import { createAction } from '../../../../../src/plugins/ui_actions/public';
import { toMountPoint } from '../../../../../src/plugins/kibana_react/public';
interface ActionContext {
embeddable: IEmbeddable;
@ -36,16 +37,18 @@ function createSamplePanelAction() {
return;
}
npStart.core.overlays.openFlyout(
<React.Fragment>
<EuiFlyoutHeader>
<EuiTitle size="m" data-test-subj="samplePanelActionTitle">
<h1>{embeddable.getTitle()}</h1>
</EuiTitle>
</EuiFlyoutHeader>
<EuiFlyoutBody>
<h3 data-test-subj="samplePanelActionBody">This is a sample action</h3>
</EuiFlyoutBody>
</React.Fragment>,
toMountPoint(
<React.Fragment>
<EuiFlyoutHeader>
<EuiTitle size="m" data-test-subj="samplePanelActionTitle">
<h1>{embeddable.getTitle()}</h1>
</EuiTitle>
</EuiFlyoutHeader>
<EuiFlyoutBody>
<h3 data-test-subj="samplePanelActionBody">This is a sample action</h3>
</EuiFlyoutBody>
</React.Fragment>
),
{
'data-test-subj': 'samplePanelActionFlyout',
}

View file

@ -11,6 +11,7 @@ import React from 'react';
import { Provider } from 'react-redux';
import { isColorDark, hexToRgb } from '@elastic/eui';
import { toMountPoint } from '../../../../../src/plugins/kibana_react/public';
import { showSaveModal } from 'ui/saved_objects/show_saved_object_save_modal';
import { addAppRedirectMessageToUrl } from 'ui/notify';
@ -551,9 +552,11 @@ export function initGraphApp(angularModule, deps) {
canEditDrillDownUrls: canEditDrillDownUrls
}), $scope.$digest.bind($scope));
coreStart.overlays.openFlyout(
<Provider store={store}>
<Settings observable={settingsObservable} />
</Provider>, {
toMountPoint(
<Provider store={store}>
<Settings observable={settingsObservable} />
</Provider>
), {
size: 'm',
closeButtonAriaLabel: i18n.translate('xpack.graph.settings.closeLabel', { defaultMessage: 'Close' }),
'data-test-subj': 'graphSettingsFlyout',

View file

@ -80,7 +80,8 @@ function GuidancePanelComponent(props: GuidancePanelProps) {
} = props;
const kibana = useKibana<IDataPluginServices>();
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 = () => {

View file

@ -86,7 +86,8 @@ export function SearchBarComponent(props: SearchBarProps) {
}, [currentDatasource]);
const kibana = useKibana<IDataPluginServices>();
const { overlays, savedObjects, uiSettings } = kibana.services;
const { services, overlays } = kibana;
const { savedObjects, uiSettings } = services;
if (!overlays) return null;
return (

View file

@ -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'];
},

View file

@ -19,6 +19,7 @@ import {
import { i18n } from '@kbn/i18n';
import { npStart } from 'ui/new_platform';
import { toMountPoint } from '../../../../../../../src/plugins/kibana_react/public';
const MAX_SIMPLE_MESSAGE_LENGTH = 140;
@ -43,27 +44,29 @@ export const ToastNotificationText: FC<{ text: any }> = ({ text }) => {
const openModal = () => {
const modal = npStart.core.overlays.openModal(
<EuiModal onClose={() => modal.close()}>
<EuiModalHeader>
<EuiModalHeaderTitle>
{i18n.translate('xpack.transform.toastText.modalTitle', {
defaultMessage: 'Error details',
})}
</EuiModalHeaderTitle>
</EuiModalHeader>
<EuiModalBody>
<EuiCodeBlock language="json" fontSize="m" paddingSize="s" isCopyable>
{formattedText}
</EuiCodeBlock>
</EuiModalBody>
<EuiModalFooter>
<EuiButtonEmpty onClick={() => modal.close()}>
{i18n.translate('xpack.transform.toastText.closeModalButtonText', {
defaultMessage: 'Close',
})}
</EuiButtonEmpty>
</EuiModalFooter>
</EuiModal>
toMountPoint(
<EuiModal onClose={() => modal.close()}>
<EuiModalHeader>
<EuiModalHeaderTitle>
{i18n.translate('xpack.transform.toastText.modalTitle', {
defaultMessage: 'Error details',
})}
</EuiModalHeaderTitle>
</EuiModalHeader>
<EuiModalBody>
<EuiCodeBlock language="json" fontSize="m" paddingSize="s" isCopyable>
{formattedText}
</EuiCodeBlock>
</EuiModalBody>
<EuiModalFooter>
<EuiButtonEmpty onClick={() => modal.close()}>
{i18n.translate('xpack.transform.toastText.closeModalButtonText', {
defaultMessage: 'Close',
})}
</EuiButtonEmpty>
</EuiModalFooter>
</EuiModal>
)
);
};

View file

@ -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<string, EmbeddableFactory>();
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<TimeRangeEmbeddable>('1');
const child2 = container.getChild<TimeRangeEmbeddable>('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<TimeRangeEmbeddable>('1');
const child2 = container.getChild<TimeRangeEmbeddable>('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<TimeRangeEmbeddable>('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<HelloWorldEmbeddable>('1');
const start = coreMock.createStart();
const openModalMock = createOpenModalMock();
const action = await new CustomTimeRangeAction({
openModal: start.overlays.openModal,
openModal: openModalMock,
dateFormat: 'MM YYYY',
commonlyUsedRanges: [],
});

View file

@ -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<TimeRangeEmbeddable>('1');
const child2 = container.getChild<TimeRangeEmbeddable>('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<TimeRangeEmbeddable>('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<TimeRangeEmbeddable>('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<TimeRangeEmbeddable>('1');
const start = coreMock.createStart();
const openModalMock = jest.fn();
const badge = await new CustomTimeRangeBadge({
openModal: start.overlays.openModal,
openModal: openModalMock,
dateFormat: 'MM YYYY',
commonlyUsedRanges: [],
});

View file

@ -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,
});

View file

@ -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'];