[7.x] Add open modal core service (#36057) (#36377)

This commit is contained in:
Josh Dover 2019-05-09 15:27:05 -05:00 committed by GitHub
parent 46a224a96e
commit 000102877c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
22 changed files with 425 additions and 90 deletions

View file

@ -1,17 +0,0 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index) &gt; [kibana-plugin-public](./kibana-plugin-public.md) &gt; [FlyoutRef](./kibana-plugin-public.flyoutref.md) &gt; [close](./kibana-plugin-public.flyoutref.close.md)
## FlyoutRef.close() method
Closes the referenced flyout if it's still open which in turn will resolve the `onClose` Promise. If the flyout had already been closed this method does nothing.
<b>Signature:</b>
```typescript
close(): Promise<void>;
```
<b>Returns:</b>
`Promise<void>`

View file

@ -1,26 +0,0 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index) &gt; [kibana-plugin-public](./kibana-plugin-public.md) &gt; [FlyoutRef](./kibana-plugin-public.flyoutref.md)
## FlyoutRef class
A FlyoutRef is a reference to an opened flyout panel. It offers methods to close the flyout panel again. If you open a flyout panel you should make sure you call `close()` when it should be closed. Since a flyout could also be closed by a user or from another flyout being opened, you must bind to the `onClose` Promise on the FlyoutRef instance. The Promise will resolve whenever the flyout was closed at which point you should discard the FlyoutRef.
<b>Signature:</b>
```typescript
export declare class FlyoutRef
```
## Properties
| Property | Modifiers | Type | Description |
| --- | --- | --- | --- |
| [onClose](./kibana-plugin-public.flyoutref.onclose.md) | | <code>Promise&lt;void&gt;</code> | An Promise that will resolve once this flyout is closed.<!-- -->Flyouts can close from user interaction, calling <code>close()</code> on the flyout reference or another call to <code>openFlyout()</code> replacing your flyout. |
## Methods
| Method | Modifiers | Description |
| --- | --- | --- |
| [close()](./kibana-plugin-public.flyoutref.close.md) | | Closes the referenced flyout if it's still open which in turn will resolve the <code>onClose</code> Promise. If the flyout had already been closed this method does nothing. |

View file

@ -1,15 +0,0 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index) &gt; [kibana-plugin-public](./kibana-plugin-public.md) &gt; [FlyoutRef](./kibana-plugin-public.flyoutref.md) &gt; [onClose](./kibana-plugin-public.flyoutref.onclose.md)
## FlyoutRef.onClose property
An Promise that will resolve once this flyout is closed.
Flyouts can close from user interaction, calling `close()` on the flyout reference or another call to `openFlyout()` replacing your flyout.
<b>Signature:</b>
```typescript
readonly onClose: Promise<void>;
```

View file

@ -8,7 +8,6 @@
| Class | Description |
| --- | --- |
| [FlyoutRef](./kibana-plugin-public.flyoutref.md) | A FlyoutRef is a reference to an opened flyout panel. It offers methods to close the flyout panel again. If you open a flyout panel you should make sure you call <code>close()</code> when it should be closed. Since a flyout could also be closed by a user or from another flyout being opened, you must bind to the <code>onClose</code> Promise on the FlyoutRef instance. The Promise will resolve whenever the flyout was closed at which point you should discard the FlyoutRef. |
| [ToastsApi](./kibana-plugin-public.toastsapi.md) | |
| [UiSettingsClient](./kibana-plugin-public.uisettingsclient.md) | |
@ -30,6 +29,7 @@
| [InjectedMetadataSetup](./kibana-plugin-public.injectedmetadatasetup.md) | Provides access to the metadata injected by the server into the page |
| [LegacyNavLink](./kibana-plugin-public.legacynavlink.md) | |
| [NotificationsSetup](./kibana-plugin-public.notificationssetup.md) | |
| [OverlayRef](./kibana-plugin-public.overlayref.md) | |
| [OverlayStart](./kibana-plugin-public.overlaystart.md) | |
| [Plugin](./kibana-plugin-public.plugin.md) | The interface that should be returned by a <code>PluginInitializer</code>. |
| [PluginInitializerContext](./kibana-plugin-public.plugininitializercontext.md) | The available core services passed to a <code>PluginInitializer</code> |

View file

@ -0,0 +1,17 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index) &gt; [kibana-plugin-public](./kibana-plugin-public.md) &gt; [OverlayRef](./kibana-plugin-public.overlayref.md) &gt; [close](./kibana-plugin-public.overlayref.close.md)
## OverlayRef.close() method
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.
<b>Signature:</b>
```typescript
close(): Promise<void>;
```
<b>Returns:</b>
`Promise<void>`

View file

@ -0,0 +1,24 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index) &gt; [kibana-plugin-public](./kibana-plugin-public.md) &gt; [OverlayRef](./kibana-plugin-public.overlayref.md)
## OverlayRef interface
<b>Signature:</b>
```typescript
export interface OverlayRef
```
## Properties
| Property | Type | Description |
| --- | --- | --- |
| [onClose](./kibana-plugin-public.overlayref.onclose.md) | <code>Promise&lt;void&gt;</code> | A Promise that will resolve once this overlay is closed.<!-- -->Overlays can close from user interaction, calling <code>close()</code> on the overlay reference or another overlay replacing yours via <code>openModal</code> or <code>openFlyout</code>. |
## Methods
| Method | Description |
| --- | --- |
| [close()](./kibana-plugin-public.overlayref.close.md) | Closes the referenced overlay if it's still open which in turn will resolve the <code>onClose</code> Promise. If the overlay had already been closed this method does nothing. |

View file

@ -0,0 +1,15 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index) &gt; [kibana-plugin-public](./kibana-plugin-public.md) &gt; [OverlayRef](./kibana-plugin-public.overlayref.md) &gt; [onClose](./kibana-plugin-public.overlayref.onclose.md)
## OverlayRef.onClose property
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`<!-- -->.
<b>Signature:</b>
```typescript
onClose: Promise<void>;
```

View file

@ -15,5 +15,6 @@ export interface OverlayStart
| Property | Type | Description |
| --- | --- | --- |
| [openFlyout](./kibana-plugin-public.overlaystart.openflyout.md) | <code>(flyoutChildren: React.ReactNode, flyoutProps?: {`<p/>` closeButtonAriaLabel?: string;`<p/>` 'data-test-subj'?: string;`<p/>` }) =&gt; FlyoutRef</code> | |
| [openFlyout](./kibana-plugin-public.overlaystart.openflyout.md) | <code>(flyoutChildren: React.ReactNode, flyoutProps?: {`<p/>` closeButtonAriaLabel?: string;`<p/>` 'data-test-subj'?: string;`<p/>` }) =&gt; OverlayRef</code> | |
| [openModal](./kibana-plugin-public.overlaystart.openmodal.md) | <code>(modalChildren: React.ReactNode, modalProps?: {`<p/>` closeButtonAriaLabel?: string;`<p/>` 'data-test-subj'?: string;`<p/>` }) =&gt; OverlayRef</code> | |

View file

@ -10,5 +10,5 @@
openFlyout: (flyoutChildren: React.ReactNode, flyoutProps?: {
closeButtonAriaLabel?: string;
'data-test-subj'?: string;
}) => FlyoutRef;
}) => OverlayRef;
```

View file

@ -0,0 +1,14 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index) &gt; [kibana-plugin-public](./kibana-plugin-public.md) &gt; [OverlayStart](./kibana-plugin-public.overlaystart.md) &gt; [openModal](./kibana-plugin-public.overlaystart.openmodal.md)
## OverlayStart.openModal property
<b>Signature:</b>
```typescript
openModal: (modalChildren: React.ReactNode, modalProps?: {
closeButtonAriaLabel?: string;
'data-test-subj'?: string;
}) => OverlayRef;
```

View file

@ -68,7 +68,6 @@ export class CoreSystem {
private readonly application: ApplicationService;
private readonly rootDomElement: HTMLElement;
private readonly overlayTargetDomElement: HTMLDivElement;
private fatalErrorsSetup: FatalErrorsSetup | null = null;
constructor(params: Params) {
@ -97,8 +96,7 @@ export class CoreSystem {
this.http = new HttpService();
this.basePath = new BasePathService();
this.uiSettings = new UiSettingsService();
this.overlayTargetDomElement = document.createElement('div');
this.overlay = new OverlayService(this.overlayTargetDomElement);
this.overlay = new OverlayService();
this.application = new ApplicationService();
this.chrome = new ChromeService({ browserSupportsCsp });
@ -169,6 +167,7 @@ export class CoreSystem {
const application = await this.application.start({ basePath, injectedMetadata });
const notificationsTargetDomElement = document.createElement('div');
const overlayTargetDomElement = document.createElement('div');
const legacyPlatformTargetDomElement = document.createElement('div');
// ensure the rootDomElement is empty
@ -176,13 +175,13 @@ export class CoreSystem {
this.rootDomElement.classList.add('coreSystemRootDomElement');
this.rootDomElement.appendChild(notificationsTargetDomElement);
this.rootDomElement.appendChild(legacyPlatformTargetDomElement);
this.rootDomElement.appendChild(this.overlayTargetDomElement);
this.rootDomElement.appendChild(overlayTargetDomElement);
const notifications = await this.notifications.start({
i18n,
targetDomElement: notificationsTargetDomElement,
});
const overlays = await this.overlay.start({ i18n });
const overlays = this.overlay.start({ i18n, targetDomElement: overlayTargetDomElement });
const core: CoreStart = {
application,

View file

@ -41,7 +41,7 @@ import {
ToastsApi,
NotificationsStart,
} from './notifications';
import { FlyoutRef, OverlayStart } from './overlays';
import { OverlayRef, OverlayStart } from './overlays';
import { Plugin, PluginInitializer, PluginInitializerContext, PluginSetupContext } from './plugins';
import { UiSettingsClient, UiSettingsSetup, UiSettingsState } from './ui_settings';
import { ApplicationSetup, Capabilities, ApplicationStart } from './application';
@ -119,8 +119,8 @@ export {
PluginSetupContext,
NotificationsSetup,
NotificationsStart,
OverlayRef,
OverlayStart,
FlyoutRef,
Toast,
ToastInput,
ToastsApi,

View file

@ -64,7 +64,9 @@ export class NotificationsService {
const toastsContainer = document.createElement('div');
targetDomElement.appendChild(toastsContainer);
return { toasts: this.toasts.start({ i18n: i18nDep, targetDomElement: toastsContainer }) };
return {
toasts: this.toasts.start({ i18n: i18nDep, targetDomElement: toastsContainer }),
};
}
public stop() {

View file

@ -0,0 +1,64 @@
// 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

@ -24,6 +24,7 @@ import React from 'react';
import { render, unmountComponentAtNode } from 'react-dom';
import { Subject } from 'rxjs';
import { I18nSetup } from '../i18n';
import { OverlayRef } from './overlay_service';
/**
* A FlyoutRef is a reference to an opened flyout panel. It offers methods to
@ -36,7 +37,7 @@ import { I18nSetup } from '../i18n';
*
* @public
*/
export class FlyoutRef {
export class FlyoutRef implements OverlayRef {
/**
* An Promise that will resolve once this flyout is closed.
*

View file

@ -17,5 +17,4 @@
* under the License.
*/
export { OverlayService, OverlayStart } from './overlay_service';
export { FlyoutRef } from './flyout';
export { OverlayService, OverlayStart, OverlayRef } from './overlay_service';

View file

@ -0,0 +1,99 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { mockReactDomRender, mockReactDomUnmount } from './flyout.test.mocks';
import React from 'react';
import { i18nServiceMock } from '../i18n/i18n_service.mock';
import { ModalService, ModalRef } from './modal';
const i18nMock = i18nServiceMock.createSetupContract();
beforeEach(() => {
mockReactDomRender.mockClear();
mockReactDomUnmount.mockClear();
});
describe('ModalService', () => {
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, modalService: ModalService, ref1: ModalRef;
beforeEach(() => {
target = document.createElement('div');
modalService = new ModalService(target);
ref1 = modalService.openModal(i18nMock, <span>Modal content 1</span>);
});
it('replaces the current modal with a new one', () => {
modalService.openModal(i18nMock, <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>);
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 onCloseComplete = jest.fn();
ref.onClose.then(onCloseComplete);
await ref.close();
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>);
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 onCloseComplete = jest.fn();
ref2.onClose.then(onCloseComplete);
mockReactDomUnmount.mockClear();
await ref1.close();
expect(mockReactDomUnmount).toBeCalledTimes(0);
expect(onCloseComplete).toBeCalledTimes(0);
});
});
});

View file

@ -0,0 +1,122 @@
/*
* 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 { I18nSetup } 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: I18nSetup,
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

@ -21,6 +21,7 @@ import { OverlayService, OverlayStart } from './overlay_service';
const createStartContractMock = () => {
const startContract: jest.Mocked<PublicMethodsOf<OverlayStart>> = {
openFlyout: jest.fn(),
openModal: jest.fn(),
};
return startContract;
};

View file

@ -18,25 +18,47 @@
*/
import { FlyoutService } from './flyout';
import { FlyoutRef } from '..';
import { ModalService } from './modal';
import { I18nStart } from '../i18n';
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>;
}
interface StartDeps {
i18n: I18nStart;
targetDomElement: HTMLElement;
}
/** @internal */
export class OverlayService {
private flyoutService: FlyoutService;
private flyoutService?: FlyoutService;
private modalService?: ModalService;
constructor(targetDomElement: HTMLElement) {
this.flyoutService = new FlyoutService(targetDomElement);
}
public start({ i18n, targetDomElement }: StartDeps): OverlayStart {
const flyoutElement = document.createElement('div');
const modalElement = document.createElement('div');
targetDomElement.appendChild(flyoutElement);
targetDomElement.appendChild(modalElement);
this.flyoutService = new FlyoutService(flyoutElement);
this.modalService = new ModalService(modalElement);
public start({ i18n }: StartDeps): OverlayStart {
return {
openFlyout: this.flyoutService.openFlyout.bind(this.flyoutService, i18n),
openModal: this.modalService.openModal.bind(this.modalService, i18n),
};
}
}
@ -49,5 +71,12 @@ export interface OverlayStart {
closeButtonAriaLabel?: string;
'data-test-subj'?: string;
}
) => FlyoutRef;
) => OverlayRef;
openModal: (
modalChildren: React.ReactNode,
modalProps?: {
closeButtonAriaLabel?: string;
'data-test-subj'?: string;
}
) => OverlayRef;
}

View file

@ -157,13 +157,6 @@ export interface FatalErrorsSetup {
get$: () => Rx.Observable<ErrorInfo>;
}
// @public
export class FlyoutRef {
constructor();
close(): Promise<void>;
readonly onClose: Promise<void>;
}
// Warning: (ae-forgotten-export) The symbol "HttpService" needs to be exported by the entry point index.d.ts
//
// @public (undocumented)
@ -288,6 +281,14 @@ export interface NotificationsSetup {
// @public (undocumented)
export type NotificationsStart = NotificationsSetup;
// Warning: (ae-missing-release-tag) "OverlayRef" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export interface OverlayRef {
close(): Promise<void>;
onClose: Promise<void>;
}
// @public (undocumented)
export interface OverlayStart {
// Warning: (ae-forgotten-export) The symbol "React" needs to be exported by the entry point index.d.ts
@ -296,7 +297,12 @@ export interface OverlayStart {
openFlyout: (flyoutChildren: React.ReactNode, flyoutProps?: {
closeButtonAriaLabel?: string;
'data-test-subj'?: string;
}) => FlyoutRef;
}) => OverlayRef;
// (undocumented)
openModal: (modalChildren: React.ReactNode, modalProps?: {
closeButtonAriaLabel?: string;
'data-test-subj'?: string;
}) => OverlayRef;
}
// @public

View file

@ -19,7 +19,7 @@
import React from 'react';
import { i18n } from '@kbn/i18n';
import { FlyoutRef } from '../../../../core/public';
import { OverlayRef } from '../../../../core/public';
import { getNewPlatform } from '../new_platform';
import { Adapters } from './types';
import { InspectorPanel } from './ui/inspector_panel';
@ -50,7 +50,7 @@ interface InspectorOptions {
title?: string;
}
export type InspectorSession = FlyoutRef;
export type InspectorSession = OverlayRef;
/**
* Opens the inspector panel for the given adapters and close any previously opened