mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
[toasts] migrate toastNotifications to the new platform (#21772)
Fixes #20698
As part of the transition of APIs necessary for migrating the Chrome to the new platform, this moves the core logic for the toastNotifications out of `ui/notify` and into the new platform as the `core.notifications.toasts` service. I chose to use the `notifications` namespace here as I plan for the [`banners` service](494c267cd9/src/ui/public/notify/banners/banners.js
) from `ui/notify` to eventually live at `core.notifications.banners`. If you disagree with this strategy and would prefer that we use something like `core.toastNotifications` let me know.
For the most part this service just does the same thing as the ui service did, so functionality should be exactly the same. To test the notifications I suggest using the testbed like so: https://gist.github.com/spalger/81097177c88dee142700fab25de88932
This commit is contained in:
parent
1e6fb80be2
commit
c01c2f95e7
25 changed files with 751 additions and 142 deletions
|
@ -20,6 +20,7 @@
|
||||||
import { FatalErrorsService } from './fatal_errors';
|
import { FatalErrorsService } from './fatal_errors';
|
||||||
import { InjectedMetadataService } from './injected_metadata';
|
import { InjectedMetadataService } from './injected_metadata';
|
||||||
import { LegacyPlatformService } from './legacy_platform';
|
import { LegacyPlatformService } from './legacy_platform';
|
||||||
|
import { NotificationsService } from './notifications';
|
||||||
|
|
||||||
const MockLegacyPlatformService = jest.fn<LegacyPlatformService>(
|
const MockLegacyPlatformService = jest.fn<LegacyPlatformService>(
|
||||||
function _MockLegacyPlatformService(this: any) {
|
function _MockLegacyPlatformService(this: any) {
|
||||||
|
@ -52,6 +53,18 @@ jest.mock('./fatal_errors', () => ({
|
||||||
FatalErrorsService: MockFatalErrorsService,
|
FatalErrorsService: MockFatalErrorsService,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
const mockNotificationStartContract = {};
|
||||||
|
const MockNotificationsService = jest.fn<NotificationsService>(function _MockNotificationsService(
|
||||||
|
this: any
|
||||||
|
) {
|
||||||
|
this.start = jest.fn().mockReturnValue(mockNotificationStartContract);
|
||||||
|
this.add = jest.fn();
|
||||||
|
this.stop = jest.fn();
|
||||||
|
});
|
||||||
|
jest.mock('./notifications', () => ({
|
||||||
|
NotificationsService: MockNotificationsService,
|
||||||
|
}));
|
||||||
|
|
||||||
import { CoreSystem } from './core_system';
|
import { CoreSystem } from './core_system';
|
||||||
jest.spyOn(CoreSystem.prototype, 'stop');
|
jest.spyOn(CoreSystem.prototype, 'stop');
|
||||||
|
|
||||||
|
@ -74,6 +87,8 @@ describe('constructor', () => {
|
||||||
|
|
||||||
expect(MockInjectedMetadataService).toHaveBeenCalledTimes(1);
|
expect(MockInjectedMetadataService).toHaveBeenCalledTimes(1);
|
||||||
expect(MockLegacyPlatformService).toHaveBeenCalledTimes(1);
|
expect(MockLegacyPlatformService).toHaveBeenCalledTimes(1);
|
||||||
|
expect(MockFatalErrorsService).toHaveBeenCalledTimes(1);
|
||||||
|
expect(MockNotificationsService).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('passes injectedMetadata param to InjectedMetadataService', () => {
|
it('passes injectedMetadata param to InjectedMetadataService', () => {
|
||||||
|
@ -92,14 +107,12 @@ describe('constructor', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('passes requireLegacyFiles, useLegacyTestHarness, and a dom element to LegacyPlatformService', () => {
|
it('passes requireLegacyFiles, useLegacyTestHarness, and a dom element to LegacyPlatformService', () => {
|
||||||
const rootDomElement = document.createElement('div');
|
|
||||||
const requireLegacyFiles = { requireLegacyFiles: true } as any;
|
const requireLegacyFiles = { requireLegacyFiles: true } as any;
|
||||||
const useLegacyTestHarness = { useLegacyTestHarness: true } as any;
|
const useLegacyTestHarness = { useLegacyTestHarness: true } as any;
|
||||||
|
|
||||||
// tslint:disable no-unused-expression
|
// tslint:disable no-unused-expression
|
||||||
new CoreSystem({
|
new CoreSystem({
|
||||||
...defaultCoreSystemParams,
|
...defaultCoreSystemParams,
|
||||||
rootDomElement,
|
|
||||||
requireLegacyFiles,
|
requireLegacyFiles,
|
||||||
useLegacyTestHarness,
|
useLegacyTestHarness,
|
||||||
});
|
});
|
||||||
|
@ -112,6 +125,18 @@ describe('constructor', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('passes a dom element to NotificationsService', () => {
|
||||||
|
// tslint:disable no-unused-expression
|
||||||
|
new CoreSystem({
|
||||||
|
...defaultCoreSystemParams,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(MockNotificationsService).toHaveBeenCalledTimes(1);
|
||||||
|
expect(MockNotificationsService).toHaveBeenCalledWith({
|
||||||
|
targetDomElement: expect.any(HTMLElement),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('passes injectedMetadata, rootDomElement, and a stopCoreSystem function to FatalErrorsService', () => {
|
it('passes injectedMetadata, rootDomElement, and a stopCoreSystem function to FatalErrorsService', () => {
|
||||||
const rootDomElement = document.createElement('div');
|
const rootDomElement = document.createElement('div');
|
||||||
const injectedMetadata = { injectedMetadata: true } as any;
|
const injectedMetadata = { injectedMetadata: true } as any;
|
||||||
|
@ -161,11 +186,11 @@ describe('#start()', () => {
|
||||||
core.start();
|
core.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
it('clears the children of the rootDomElement and appends container for legacyPlatform', () => {
|
it('clears the children of the rootDomElement and appends container for legacyPlatform and notifications', () => {
|
||||||
const root = document.createElement('div');
|
const root = document.createElement('div');
|
||||||
root.innerHTML = '<p>foo bar</p>';
|
root.innerHTML = '<p>foo bar</p>';
|
||||||
startCore(root);
|
startCore(root);
|
||||||
expect(root.innerHTML).toBe('<div></div>');
|
expect(root.innerHTML).toBe('<div></div><div></div>');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('calls injectedMetadata#start()', () => {
|
it('calls injectedMetadata#start()', () => {
|
||||||
|
@ -181,6 +206,13 @@ describe('#start()', () => {
|
||||||
expect(mockInstance.start).toHaveBeenCalledTimes(1);
|
expect(mockInstance.start).toHaveBeenCalledTimes(1);
|
||||||
expect(mockInstance.start).toHaveBeenCalledWith();
|
expect(mockInstance.start).toHaveBeenCalledWith();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('calls notifications#start()', () => {
|
||||||
|
startCore();
|
||||||
|
const [mockInstance] = MockNotificationsService.mock.instances;
|
||||||
|
expect(mockInstance.start).toHaveBeenCalledTimes(1);
|
||||||
|
expect(mockInstance.start).toHaveBeenCalledWith();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('LegacyPlatform targetDomElement', () => {
|
describe('LegacyPlatform targetDomElement', () => {
|
||||||
|
@ -207,3 +239,28 @@ describe('LegacyPlatform targetDomElement', () => {
|
||||||
expect(targetDomElementParentInStart!).toBe(rootDomElement);
|
expect(targetDomElementParentInStart!).toBe(rootDomElement);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('Notifications targetDomElement', () => {
|
||||||
|
it('only mounts the element when started, before starting the notificationsService', () => {
|
||||||
|
const rootDomElement = document.createElement('div');
|
||||||
|
const core = new CoreSystem({
|
||||||
|
...defaultCoreSystemParams,
|
||||||
|
rootDomElement,
|
||||||
|
});
|
||||||
|
|
||||||
|
const [notifications] = MockNotificationsService.mock.instances;
|
||||||
|
|
||||||
|
let targetDomElementParentInStart: HTMLElement;
|
||||||
|
(notifications as any).start.mockImplementation(() => {
|
||||||
|
targetDomElementParentInStart = targetDomElement.parentElement;
|
||||||
|
});
|
||||||
|
|
||||||
|
// targetDomElement should not have a parent element when the LegacyPlatformService is constructed
|
||||||
|
const [[{ targetDomElement }]] = MockNotificationsService.mock.calls;
|
||||||
|
expect(targetDomElement).toHaveProperty('parentElement', null);
|
||||||
|
|
||||||
|
// starting the core system should mount the targetDomElement as a child of the rootDomElement
|
||||||
|
core.start();
|
||||||
|
expect(targetDomElementParentInStart!).toBe(rootDomElement);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
|
@ -21,6 +21,7 @@ import './core.css';
|
||||||
import { FatalErrorsService } from './fatal_errors';
|
import { FatalErrorsService } from './fatal_errors';
|
||||||
import { InjectedMetadataParams, InjectedMetadataService } from './injected_metadata';
|
import { InjectedMetadataParams, InjectedMetadataService } from './injected_metadata';
|
||||||
import { LegacyPlatformParams, LegacyPlatformService } from './legacy_platform';
|
import { LegacyPlatformParams, LegacyPlatformService } from './legacy_platform';
|
||||||
|
import { NotificationsService } from './notifications';
|
||||||
|
|
||||||
interface Params {
|
interface Params {
|
||||||
rootDomElement: HTMLElement;
|
rootDomElement: HTMLElement;
|
||||||
|
@ -39,8 +40,10 @@ export class CoreSystem {
|
||||||
private readonly fatalErrors: FatalErrorsService;
|
private readonly fatalErrors: FatalErrorsService;
|
||||||
private readonly injectedMetadata: InjectedMetadataService;
|
private readonly injectedMetadata: InjectedMetadataService;
|
||||||
private readonly legacyPlatform: LegacyPlatformService;
|
private readonly legacyPlatform: LegacyPlatformService;
|
||||||
|
private readonly notifications: NotificationsService;
|
||||||
|
|
||||||
private readonly rootDomElement: HTMLElement;
|
private readonly rootDomElement: HTMLElement;
|
||||||
|
private readonly notificationsTargetDomElement: HTMLDivElement;
|
||||||
private readonly legacyPlatformTargetDomElement: HTMLDivElement;
|
private readonly legacyPlatformTargetDomElement: HTMLDivElement;
|
||||||
|
|
||||||
constructor(params: Params) {
|
constructor(params: Params) {
|
||||||
|
@ -60,6 +63,11 @@ export class CoreSystem {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.notificationsTargetDomElement = document.createElement('div');
|
||||||
|
this.notifications = new NotificationsService({
|
||||||
|
targetDomElement: this.notificationsTargetDomElement,
|
||||||
|
});
|
||||||
|
|
||||||
this.legacyPlatformTargetDomElement = document.createElement('div');
|
this.legacyPlatformTargetDomElement = document.createElement('div');
|
||||||
this.legacyPlatform = new LegacyPlatformService({
|
this.legacyPlatform = new LegacyPlatformService({
|
||||||
targetDomElement: this.legacyPlatformTargetDomElement,
|
targetDomElement: this.legacyPlatformTargetDomElement,
|
||||||
|
@ -73,11 +81,13 @@ export class CoreSystem {
|
||||||
// ensure the rootDomElement is empty
|
// ensure the rootDomElement is empty
|
||||||
this.rootDomElement.textContent = '';
|
this.rootDomElement.textContent = '';
|
||||||
this.rootDomElement.classList.add('coreSystemRootDomElement');
|
this.rootDomElement.classList.add('coreSystemRootDomElement');
|
||||||
|
this.rootDomElement.appendChild(this.notificationsTargetDomElement);
|
||||||
this.rootDomElement.appendChild(this.legacyPlatformTargetDomElement);
|
this.rootDomElement.appendChild(this.legacyPlatformTargetDomElement);
|
||||||
|
|
||||||
|
const notifications = this.notifications.start();
|
||||||
const injectedMetadata = this.injectedMetadata.start();
|
const injectedMetadata = this.injectedMetadata.start();
|
||||||
const fatalErrors = this.fatalErrors.start();
|
const fatalErrors = this.fatalErrors.start();
|
||||||
this.legacyPlatform.start({ injectedMetadata, fatalErrors });
|
this.legacyPlatform.start({ injectedMetadata, fatalErrors, notifications });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.fatalErrors.add(error);
|
this.fatalErrors.add(error);
|
||||||
}
|
}
|
||||||
|
@ -85,6 +95,7 @@ export class CoreSystem {
|
||||||
|
|
||||||
public stop() {
|
public stop() {
|
||||||
this.legacyPlatform.stop();
|
this.legacyPlatform.stop();
|
||||||
|
this.notifications.stop();
|
||||||
this.rootDomElement.textContent = '';
|
this.rootDomElement.textContent = '';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,7 +17,6 @@
|
||||||
* under the License.
|
* under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// @ts-ignore EuiCallOut not available until we upgrade to EUI 3.1.0
|
|
||||||
import { EuiCallOut } from '@elastic/eui';
|
import { EuiCallOut } from '@elastic/eui';
|
||||||
import testSubjSelector from '@kbn/test-subj-selector';
|
import testSubjSelector from '@kbn/test-subj-selector';
|
||||||
import { mount, shallow } from 'enzyme';
|
import { mount, shallow } from 'enzyme';
|
||||||
|
|
|
@ -20,11 +20,8 @@
|
||||||
import {
|
import {
|
||||||
EuiButton,
|
EuiButton,
|
||||||
EuiButtonEmpty,
|
EuiButtonEmpty,
|
||||||
// @ts-ignore EuiCallOut not available until we upgrade to EUI 3.1.0
|
|
||||||
EuiCallOut,
|
EuiCallOut,
|
||||||
// @ts-ignore EuiCodeBlock not available until we upgrade to EUI 3.1.0
|
|
||||||
EuiCodeBlock,
|
EuiCodeBlock,
|
||||||
// @ts-ignore EuiEmptyPrompt not available until we upgrade to EUI 3.1.0
|
|
||||||
EuiEmptyPrompt,
|
EuiEmptyPrompt,
|
||||||
EuiPage,
|
EuiPage,
|
||||||
EuiPageBody,
|
EuiPageBody,
|
||||||
|
|
|
@ -53,9 +53,20 @@ jest.mock('ui/notify/fatal_error', () => {
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const mockNotifyToastsInit = jest.fn();
|
||||||
|
jest.mock('ui/notify/toasts', () => {
|
||||||
|
mockLoadOrder.push('ui/notify/toasts');
|
||||||
|
return {
|
||||||
|
__newPlatformInit__: mockNotifyToastsInit,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
import { LegacyPlatformService } from './legacy_platform_service';
|
import { LegacyPlatformService } from './legacy_platform_service';
|
||||||
|
|
||||||
const fatalErrorsStartContract = {} as any;
|
const fatalErrorsStartContract = {} as any;
|
||||||
|
const notificationsStartContract = {
|
||||||
|
toasts: {},
|
||||||
|
} as any;
|
||||||
|
|
||||||
const injectedMetadataStartContract = {
|
const injectedMetadataStartContract = {
|
||||||
getLegacyMetadata: jest.fn(),
|
getLegacyMetadata: jest.fn(),
|
||||||
|
@ -88,6 +99,7 @@ describe('#start()', () => {
|
||||||
legacyPlatform.start({
|
legacyPlatform.start({
|
||||||
fatalErrors: fatalErrorsStartContract,
|
fatalErrors: fatalErrorsStartContract,
|
||||||
injectedMetadata: injectedMetadataStartContract,
|
injectedMetadata: injectedMetadataStartContract,
|
||||||
|
notifications: notificationsStartContract,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(mockUiMetadataInit).toHaveBeenCalledTimes(1);
|
expect(mockUiMetadataInit).toHaveBeenCalledTimes(1);
|
||||||
|
@ -102,12 +114,28 @@ describe('#start()', () => {
|
||||||
legacyPlatform.start({
|
legacyPlatform.start({
|
||||||
fatalErrors: fatalErrorsStartContract,
|
fatalErrors: fatalErrorsStartContract,
|
||||||
injectedMetadata: injectedMetadataStartContract,
|
injectedMetadata: injectedMetadataStartContract,
|
||||||
|
notifications: notificationsStartContract,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(mockFatalErrorInit).toHaveBeenCalledTimes(1);
|
expect(mockFatalErrorInit).toHaveBeenCalledTimes(1);
|
||||||
expect(mockFatalErrorInit).toHaveBeenCalledWith(fatalErrorsStartContract);
|
expect(mockFatalErrorInit).toHaveBeenCalledWith(fatalErrorsStartContract);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('passes toasts service to ui/notify/toasts', () => {
|
||||||
|
const legacyPlatform = new LegacyPlatformService({
|
||||||
|
...defaultParams,
|
||||||
|
});
|
||||||
|
|
||||||
|
legacyPlatform.start({
|
||||||
|
fatalErrors: fatalErrorsStartContract,
|
||||||
|
injectedMetadata: injectedMetadataStartContract,
|
||||||
|
notifications: notificationsStartContract,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockNotifyToastsInit).toHaveBeenCalledTimes(1);
|
||||||
|
expect(mockNotifyToastsInit).toHaveBeenCalledWith(notificationsStartContract.toasts);
|
||||||
|
});
|
||||||
|
|
||||||
describe('useLegacyTestHarness = false', () => {
|
describe('useLegacyTestHarness = false', () => {
|
||||||
it('passes the targetDomElement to ui/chrome', () => {
|
it('passes the targetDomElement to ui/chrome', () => {
|
||||||
const legacyPlatform = new LegacyPlatformService({
|
const legacyPlatform = new LegacyPlatformService({
|
||||||
|
@ -117,6 +145,7 @@ describe('#start()', () => {
|
||||||
legacyPlatform.start({
|
legacyPlatform.start({
|
||||||
fatalErrors: fatalErrorsStartContract,
|
fatalErrors: fatalErrorsStartContract,
|
||||||
injectedMetadata: injectedMetadataStartContract,
|
injectedMetadata: injectedMetadataStartContract,
|
||||||
|
notifications: notificationsStartContract,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(mockUiTestHarnessBootstrap).not.toHaveBeenCalled();
|
expect(mockUiTestHarnessBootstrap).not.toHaveBeenCalled();
|
||||||
|
@ -134,6 +163,7 @@ describe('#start()', () => {
|
||||||
legacyPlatform.start({
|
legacyPlatform.start({
|
||||||
fatalErrors: fatalErrorsStartContract,
|
fatalErrors: fatalErrorsStartContract,
|
||||||
injectedMetadata: injectedMetadataStartContract,
|
injectedMetadata: injectedMetadataStartContract,
|
||||||
|
notifications: notificationsStartContract,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(mockUiChromeBootstrap).not.toHaveBeenCalled();
|
expect(mockUiChromeBootstrap).not.toHaveBeenCalled();
|
||||||
|
@ -155,11 +185,13 @@ describe('#start()', () => {
|
||||||
legacyPlatform.start({
|
legacyPlatform.start({
|
||||||
fatalErrors: fatalErrorsStartContract,
|
fatalErrors: fatalErrorsStartContract,
|
||||||
injectedMetadata: injectedMetadataStartContract,
|
injectedMetadata: injectedMetadataStartContract,
|
||||||
|
notifications: notificationsStartContract,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(mockLoadOrder).toEqual([
|
expect(mockLoadOrder).toEqual([
|
||||||
'ui/metadata',
|
'ui/metadata',
|
||||||
'ui/notify/fatal_error',
|
'ui/notify/fatal_error',
|
||||||
|
'ui/notify/toasts',
|
||||||
'ui/chrome',
|
'ui/chrome',
|
||||||
'legacy files',
|
'legacy files',
|
||||||
]);
|
]);
|
||||||
|
@ -178,11 +210,13 @@ describe('#start()', () => {
|
||||||
legacyPlatform.start({
|
legacyPlatform.start({
|
||||||
fatalErrors: fatalErrorsStartContract,
|
fatalErrors: fatalErrorsStartContract,
|
||||||
injectedMetadata: injectedMetadataStartContract,
|
injectedMetadata: injectedMetadataStartContract,
|
||||||
|
notifications: notificationsStartContract,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(mockLoadOrder).toEqual([
|
expect(mockLoadOrder).toEqual([
|
||||||
'ui/metadata',
|
'ui/metadata',
|
||||||
'ui/notify/fatal_error',
|
'ui/notify/fatal_error',
|
||||||
|
'ui/notify/toasts',
|
||||||
'ui/test_harness',
|
'ui/test_harness',
|
||||||
'legacy files',
|
'legacy files',
|
||||||
]);
|
]);
|
||||||
|
|
|
@ -20,10 +20,12 @@
|
||||||
import angular from 'angular';
|
import angular from 'angular';
|
||||||
import { FatalErrorsStartContract } from '../fatal_errors';
|
import { FatalErrorsStartContract } from '../fatal_errors';
|
||||||
import { InjectedMetadataStartContract } from '../injected_metadata';
|
import { InjectedMetadataStartContract } from '../injected_metadata';
|
||||||
|
import { NotificationsStartContract } from '../notifications';
|
||||||
|
|
||||||
interface Deps {
|
interface Deps {
|
||||||
injectedMetadata: InjectedMetadataStartContract;
|
injectedMetadata: InjectedMetadataStartContract;
|
||||||
fatalErrors: FatalErrorsStartContract;
|
fatalErrors: FatalErrorsStartContract;
|
||||||
|
notifications: NotificationsStartContract;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LegacyPlatformParams {
|
export interface LegacyPlatformParams {
|
||||||
|
@ -42,11 +44,12 @@ export interface LegacyPlatformParams {
|
||||||
export class LegacyPlatformService {
|
export class LegacyPlatformService {
|
||||||
constructor(private readonly params: LegacyPlatformParams) {}
|
constructor(private readonly params: LegacyPlatformParams) {}
|
||||||
|
|
||||||
public start({ injectedMetadata, fatalErrors }: Deps) {
|
public start({ injectedMetadata, fatalErrors, notifications }: Deps) {
|
||||||
// Inject parts of the new platform into parts of the legacy platform
|
// Inject parts of the new platform into parts of the legacy platform
|
||||||
// so that legacy APIs/modules can mimic their new platform counterparts
|
// so that legacy APIs/modules can mimic their new platform counterparts
|
||||||
require('ui/metadata').__newPlatformInit__(injectedMetadata.getLegacyMetadata());
|
require('ui/metadata').__newPlatformInit__(injectedMetadata.getLegacyMetadata());
|
||||||
require('ui/notify/fatal_error').__newPlatformInit__(fatalErrors);
|
require('ui/notify/fatal_error').__newPlatformInit__(fatalErrors);
|
||||||
|
require('ui/notify/toasts').__newPlatformInit__(notifications.toasts);
|
||||||
|
|
||||||
// Load the bootstrap module before loading the legacy platform files so that
|
// Load the bootstrap module before loading the legacy platform files so that
|
||||||
// the bootstrap module can modify the environment a bit first
|
// the bootstrap module can modify the environment a bit first
|
||||||
|
|
|
@ -17,5 +17,5 @@
|
||||||
* under the License.
|
* under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export { GlobalToastList } from './global_toast_list';
|
export { Toast, ToastInput, ToastsStartContract } from './toasts';
|
||||||
export { toastNotifications } from './toast_notifications';
|
export { NotificationsService, NotificationsStartContract } from './notifications_service';
|
53
src/core/public/notifications/notifications_service.ts
Normal file
53
src/core/public/notifications/notifications_service.ts
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
/*
|
||||||
|
* 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 { ToastsService } from './toasts';
|
||||||
|
|
||||||
|
interface Params {
|
||||||
|
targetDomElement: HTMLElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class NotificationsService {
|
||||||
|
private readonly toasts: ToastsService;
|
||||||
|
|
||||||
|
private readonly toastsContainer: HTMLElement;
|
||||||
|
|
||||||
|
constructor(private readonly params: Params) {
|
||||||
|
this.toastsContainer = document.createElement('div');
|
||||||
|
this.toasts = new ToastsService({
|
||||||
|
targetDomElement: this.toastsContainer,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public start() {
|
||||||
|
this.params.targetDomElement.appendChild(this.toastsContainer);
|
||||||
|
|
||||||
|
return {
|
||||||
|
toasts: this.toasts.start(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public stop() {
|
||||||
|
this.toasts.stop();
|
||||||
|
|
||||||
|
this.params.targetDomElement.textContent = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type NotificationsStartContract = ReturnType<NotificationsService['start']>;
|
|
@ -0,0 +1,9 @@
|
||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`renders matching snapshot 1`] = `
|
||||||
|
<EuiGlobalToastList
|
||||||
|
dismissToast={[MockFunction]}
|
||||||
|
toastLifeTimeMs={6000}
|
||||||
|
toasts={Array []}
|
||||||
|
/>
|
||||||
|
`;
|
|
@ -0,0 +1,38 @@
|
||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`#start() renders the GlobalToastList into the targetDomElement param 1`] = `
|
||||||
|
Array [
|
||||||
|
Array [
|
||||||
|
<GlobalToastList
|
||||||
|
dismissToast={[Function]}
|
||||||
|
toasts$={
|
||||||
|
Observable {
|
||||||
|
"_isScalar": false,
|
||||||
|
"source": BehaviorSubject {
|
||||||
|
"_isScalar": false,
|
||||||
|
"_value": Array [],
|
||||||
|
"closed": false,
|
||||||
|
"hasError": false,
|
||||||
|
"isStopped": false,
|
||||||
|
"observers": Array [],
|
||||||
|
"thrownError": null,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/>,
|
||||||
|
<div
|
||||||
|
test="target-dom-element"
|
||||||
|
/>,
|
||||||
|
],
|
||||||
|
]
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`#stop() unmounts the GlobalToastList from the targetDomElement 1`] = `
|
||||||
|
Array [
|
||||||
|
Array [
|
||||||
|
<div
|
||||||
|
test="target-dom-element"
|
||||||
|
/>,
|
||||||
|
],
|
||||||
|
]
|
||||||
|
`;
|
|
@ -0,0 +1,65 @@
|
||||||
|
/*
|
||||||
|
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||||
|
* license agreements. See the NOTICE file distributed with
|
||||||
|
* this work for additional information regarding copyright
|
||||||
|
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||||
|
* the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
* not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { EuiGlobalToastList } from '@elastic/eui';
|
||||||
|
import { shallow } from 'enzyme';
|
||||||
|
import React from 'react';
|
||||||
|
import * as Rx from 'rxjs';
|
||||||
|
|
||||||
|
import { GlobalToastList } from './global_toast_list';
|
||||||
|
|
||||||
|
function render(props: Partial<GlobalToastList['props']> = {}) {
|
||||||
|
return <GlobalToastList dismissToast={jest.fn()} toasts$={Rx.EMPTY} {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
it('renders matching snapshot', () => {
|
||||||
|
expect(shallow(render())).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('subscribes to toasts$ on mount and unsubscribes on unmount', () => {
|
||||||
|
const unsubscribeSpy = jest.fn();
|
||||||
|
const subscribeSpy = jest.fn(observer => {
|
||||||
|
observer.next([]);
|
||||||
|
return unsubscribeSpy;
|
||||||
|
});
|
||||||
|
|
||||||
|
const component = render({
|
||||||
|
toasts$: new Rx.Observable<any>(subscribeSpy),
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(subscribeSpy).not.toHaveBeenCalled();
|
||||||
|
|
||||||
|
const el = shallow(component);
|
||||||
|
expect(subscribeSpy).toHaveBeenCalledTimes(1);
|
||||||
|
expect(unsubscribeSpy).not.toHaveBeenCalled();
|
||||||
|
|
||||||
|
el.unmount();
|
||||||
|
expect(subscribeSpy).toHaveBeenCalledTimes(1);
|
||||||
|
expect(unsubscribeSpy).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('passes latest value from toasts$ to <EuiGlobalToastList />', () => {
|
||||||
|
const el = shallow(
|
||||||
|
render({
|
||||||
|
toasts$: Rx.from([[], [1], [1, 2]]) as any,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(el.find(EuiGlobalToastList).prop('toasts')).toEqual([1, 2]);
|
||||||
|
});
|
|
@ -17,47 +17,46 @@
|
||||||
* under the License.
|
* under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, {
|
import { EuiGlobalToastList, Toast } from '@elastic/eui';
|
||||||
Component,
|
|
||||||
} from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
|
|
||||||
import {
|
import React from 'react';
|
||||||
EuiGlobalToastList,
|
import * as Rx from 'rxjs';
|
||||||
EuiPortal,
|
|
||||||
} from '@elastic/eui';
|
|
||||||
|
|
||||||
export class GlobalToastList extends Component {
|
interface Props {
|
||||||
constructor(props) {
|
toasts$: Rx.Observable<Toast[]>;
|
||||||
super(props);
|
dismissToast: (t: Toast) => void;
|
||||||
|
}
|
||||||
|
|
||||||
if (this.props.subscribe) {
|
interface State {
|
||||||
this.props.subscribe(() => this.forceUpdate());
|
toasts: Toast[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export class GlobalToastList extends React.Component<Props, State> {
|
||||||
|
public state: State = {
|
||||||
|
toasts: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
private subscription?: Rx.Subscription;
|
||||||
|
|
||||||
|
public componentDidMount() {
|
||||||
|
this.subscription = this.props.toasts$.subscribe(toasts => {
|
||||||
|
this.setState({ toasts });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public componentWillUnmount() {
|
||||||
|
if (this.subscription) {
|
||||||
|
this.subscription.unsubscribe();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static propTypes = {
|
public render() {
|
||||||
subscribe: PropTypes.func,
|
|
||||||
toasts: PropTypes.array,
|
|
||||||
dismissToast: PropTypes.func.isRequired,
|
|
||||||
toastLifeTimeMs: PropTypes.number.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
toasts,
|
|
||||||
dismissToast,
|
|
||||||
toastLifeTimeMs,
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<EuiPortal>
|
<EuiGlobalToastList
|
||||||
<EuiGlobalToastList
|
toasts={this.state.toasts}
|
||||||
toasts={toasts}
|
dismissToast={this.props.dismissToast}
|
||||||
dismissToast={dismissToast}
|
toastLifeTimeMs={6000}
|
||||||
toastLifeTimeMs={toastLifeTimeMs}
|
/>
|
||||||
/>
|
|
||||||
</EuiPortal>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
22
src/core/public/notifications/toasts/index.ts
Normal file
22
src/core/public/notifications/toasts/index.ts
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
/*
|
||||||
|
* 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 { ToastsService } from './toasts_service';
|
||||||
|
export { ToastsStartContract, ToastInput } from './toasts_start_contract';
|
||||||
|
export { Toast } from '@elastic/eui';
|
80
src/core/public/notifications/toasts/toasts_service.test.tsx
Normal file
80
src/core/public/notifications/toasts/toasts_service.test.tsx
Normal file
|
@ -0,0 +1,80 @@
|
||||||
|
/*
|
||||||
|
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||||
|
* license agreements. See the NOTICE file distributed with
|
||||||
|
* this work for additional information regarding copyright
|
||||||
|
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||||
|
* the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
* not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const mockReactDomRender = jest.fn();
|
||||||
|
const mockReactDomUnmount = jest.fn();
|
||||||
|
jest.mock('react-dom', () => ({
|
||||||
|
render: mockReactDomRender,
|
||||||
|
unmountComponentAtNode: mockReactDomUnmount,
|
||||||
|
}));
|
||||||
|
|
||||||
|
import { ToastsService } from './toasts_service';
|
||||||
|
import { ToastsStartContract } from './toasts_start_contract';
|
||||||
|
|
||||||
|
describe('#start()', () => {
|
||||||
|
it('renders the GlobalToastList into the targetDomElement param', async () => {
|
||||||
|
const targetDomElement = document.createElement('div');
|
||||||
|
targetDomElement.setAttribute('test', 'target-dom-element');
|
||||||
|
const toasts = new ToastsService({ targetDomElement });
|
||||||
|
|
||||||
|
expect(mockReactDomRender).not.toHaveBeenCalled();
|
||||||
|
toasts.start();
|
||||||
|
expect(mockReactDomRender.mock.calls).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns a ToastsStartContract', () => {
|
||||||
|
const toasts = new ToastsService({
|
||||||
|
targetDomElement: document.createElement('div'),
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(toasts.start()).toBeInstanceOf(ToastsStartContract);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('#stop()', () => {
|
||||||
|
it('unmounts the GlobalToastList from the targetDomElement', () => {
|
||||||
|
const targetDomElement = document.createElement('div');
|
||||||
|
targetDomElement.setAttribute('test', 'target-dom-element');
|
||||||
|
const toasts = new ToastsService({ targetDomElement });
|
||||||
|
|
||||||
|
toasts.start();
|
||||||
|
|
||||||
|
expect(mockReactDomUnmount).not.toHaveBeenCalled();
|
||||||
|
toasts.stop();
|
||||||
|
expect(mockReactDomUnmount.mock.calls).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not fail if start() was never called', () => {
|
||||||
|
const targetDomElement = document.createElement('div');
|
||||||
|
targetDomElement.setAttribute('test', 'target-dom-element');
|
||||||
|
const toasts = new ToastsService({ targetDomElement });
|
||||||
|
expect(() => {
|
||||||
|
toasts.stop();
|
||||||
|
}).not.toThrowError();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('empties the content of the targetDomElement', () => {
|
||||||
|
const targetDomElement = document.createElement('div');
|
||||||
|
const toasts = new ToastsService({ targetDomElement });
|
||||||
|
|
||||||
|
targetDomElement.appendChild(document.createTextNode('foo bar'));
|
||||||
|
toasts.stop();
|
||||||
|
expect(targetDomElement.childNodes).toHaveLength(0);
|
||||||
|
});
|
||||||
|
});
|
53
src/core/public/notifications/toasts/toasts_service.tsx
Normal file
53
src/core/public/notifications/toasts/toasts_service.tsx
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
/*
|
||||||
|
* 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 { render, unmountComponentAtNode } from 'react-dom';
|
||||||
|
|
||||||
|
import { Toast } from '@elastic/eui';
|
||||||
|
import { GlobalToastList } from './global_toast_list';
|
||||||
|
import { ToastsStartContract } from './toasts_start_contract';
|
||||||
|
|
||||||
|
interface Params {
|
||||||
|
targetDomElement: HTMLElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ToastsService {
|
||||||
|
constructor(private readonly params: Params) {}
|
||||||
|
|
||||||
|
public start() {
|
||||||
|
const toasts = new ToastsStartContract();
|
||||||
|
|
||||||
|
render(
|
||||||
|
<GlobalToastList
|
||||||
|
dismissToast={(toast: Toast) => toasts.remove(toast)}
|
||||||
|
toasts$={toasts.get$()}
|
||||||
|
/>,
|
||||||
|
this.params.targetDomElement
|
||||||
|
);
|
||||||
|
|
||||||
|
return toasts;
|
||||||
|
}
|
||||||
|
|
||||||
|
public stop() {
|
||||||
|
unmountComponentAtNode(this.params.targetDomElement);
|
||||||
|
|
||||||
|
this.params.targetDomElement.textContent = '';
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,159 @@
|
||||||
|
/*
|
||||||
|
* 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 { take } from 'rxjs/operators';
|
||||||
|
|
||||||
|
import { ToastsStartContract } from './toasts_start_contract';
|
||||||
|
|
||||||
|
async function getCurrentToasts(toasts: ToastsStartContract) {
|
||||||
|
return await toasts
|
||||||
|
.get$()
|
||||||
|
.pipe(take(1))
|
||||||
|
.toPromise();
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('#get$()', () => {
|
||||||
|
it('returns observable that emits NEW toast list when something added or removed', () => {
|
||||||
|
const toasts = new ToastsStartContract();
|
||||||
|
const onToasts = jest.fn();
|
||||||
|
|
||||||
|
toasts.get$().subscribe(onToasts);
|
||||||
|
const foo = toasts.add('foo');
|
||||||
|
const bar = toasts.add('bar');
|
||||||
|
toasts.remove(foo);
|
||||||
|
|
||||||
|
expect(onToasts).toHaveBeenCalledTimes(4);
|
||||||
|
|
||||||
|
const initial = onToasts.mock.calls[0][0];
|
||||||
|
expect(initial).toEqual([]);
|
||||||
|
|
||||||
|
const afterFoo = onToasts.mock.calls[1][0];
|
||||||
|
expect(afterFoo).not.toBe(initial);
|
||||||
|
expect(afterFoo).toEqual([foo]);
|
||||||
|
|
||||||
|
const afterFooAndBar = onToasts.mock.calls[2][0];
|
||||||
|
expect(afterFooAndBar).not.toBe(afterFoo);
|
||||||
|
expect(afterFooAndBar).toEqual([foo, bar]);
|
||||||
|
|
||||||
|
const afterRemoveFoo = onToasts.mock.calls[3][0];
|
||||||
|
expect(afterRemoveFoo).not.toBe(afterFooAndBar);
|
||||||
|
expect(afterRemoveFoo).toEqual([bar]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not emit a new toast list when unknown toast is passed to remove()', () => {
|
||||||
|
const toasts = new ToastsStartContract();
|
||||||
|
const onToasts = jest.fn();
|
||||||
|
|
||||||
|
toasts.get$().subscribe(onToasts);
|
||||||
|
toasts.add('foo');
|
||||||
|
onToasts.mockClear();
|
||||||
|
|
||||||
|
toasts.remove({ id: 'bar' });
|
||||||
|
expect(onToasts).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('#add()', () => {
|
||||||
|
it('returns toast objects with auto assigned id', () => {
|
||||||
|
const toasts = new ToastsStartContract();
|
||||||
|
const toast = toasts.add({ title: 'foo' });
|
||||||
|
expect(toast).toHaveProperty('id');
|
||||||
|
expect(toast).toHaveProperty('title', 'foo');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('adds the toast to toasts list', async () => {
|
||||||
|
const toasts = new ToastsStartContract();
|
||||||
|
const toast = toasts.add({});
|
||||||
|
|
||||||
|
const currentToasts = await getCurrentToasts(toasts);
|
||||||
|
expect(currentToasts).toHaveLength(1);
|
||||||
|
expect(currentToasts[0]).toBe(toast);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('increments the toast ID for each additional toast', () => {
|
||||||
|
const toasts = new ToastsStartContract();
|
||||||
|
expect(toasts.add({})).toHaveProperty('id', '0');
|
||||||
|
expect(toasts.add({})).toHaveProperty('id', '1');
|
||||||
|
expect(toasts.add({})).toHaveProperty('id', '2');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('accepts a string, uses it as the title', async () => {
|
||||||
|
const toasts = new ToastsStartContract();
|
||||||
|
expect(toasts.add('foo')).toHaveProperty('title', 'foo');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('#remove()', () => {
|
||||||
|
it('removes a toast', async () => {
|
||||||
|
const toasts = new ToastsStartContract();
|
||||||
|
toasts.remove(toasts.add('Test'));
|
||||||
|
expect(await getCurrentToasts(toasts)).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('ignores unknown toast', async () => {
|
||||||
|
const toasts = new ToastsStartContract();
|
||||||
|
toasts.add('Test');
|
||||||
|
toasts.remove({ id: 'foo' });
|
||||||
|
|
||||||
|
const currentToasts = await getCurrentToasts(toasts);
|
||||||
|
expect(currentToasts).toHaveLength(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('#addSuccess()', () => {
|
||||||
|
it('adds a success toast', async () => {
|
||||||
|
const toasts = new ToastsStartContract();
|
||||||
|
expect(toasts.addSuccess({})).toHaveProperty('color', 'success');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns the created toast', async () => {
|
||||||
|
const toasts = new ToastsStartContract();
|
||||||
|
const toast = toasts.addSuccess({});
|
||||||
|
const currentToasts = await getCurrentToasts(toasts);
|
||||||
|
expect(currentToasts[0]).toBe(toast);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('#addWarning()', () => {
|
||||||
|
it('adds a warning toast', async () => {
|
||||||
|
const toasts = new ToastsStartContract();
|
||||||
|
expect(toasts.addWarning({})).toHaveProperty('color', 'warning');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns the created toast', async () => {
|
||||||
|
const toasts = new ToastsStartContract();
|
||||||
|
const toast = toasts.addWarning({});
|
||||||
|
const currentToasts = await getCurrentToasts(toasts);
|
||||||
|
expect(currentToasts[0]).toBe(toast);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('#addDanger()', () => {
|
||||||
|
it('adds a danger toast', async () => {
|
||||||
|
const toasts = new ToastsStartContract();
|
||||||
|
expect(toasts.addDanger({})).toHaveProperty('color', 'danger');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns the created toast', async () => {
|
||||||
|
const toasts = new ToastsStartContract();
|
||||||
|
const toast = toasts.addDanger({});
|
||||||
|
const currentToasts = await getCurrentToasts(toasts);
|
||||||
|
expect(currentToasts[0]).toBe(toast);
|
||||||
|
});
|
||||||
|
});
|
|
@ -17,7 +17,12 @@
|
||||||
* under the License.
|
* under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const normalizeToast = toastOrTitle => {
|
import { Toast } from '@elastic/eui';
|
||||||
|
import * as Rx from 'rxjs';
|
||||||
|
|
||||||
|
export type ToastInput = string | Pick<Toast, Exclude<keyof Toast, 'id'>>;
|
||||||
|
|
||||||
|
const normalizeToast = (toastOrTitle: ToastInput) => {
|
||||||
if (typeof toastOrTitle === 'string') {
|
if (typeof toastOrTitle === 'string') {
|
||||||
return {
|
return {
|
||||||
title: toastOrTitle,
|
title: toastOrTitle,
|
||||||
|
@ -27,67 +32,54 @@ const normalizeToast = toastOrTitle => {
|
||||||
return toastOrTitle;
|
return toastOrTitle;
|
||||||
};
|
};
|
||||||
|
|
||||||
export class ToastNotifications {
|
export class ToastsStartContract {
|
||||||
constructor() {
|
private toasts$ = new Rx.BehaviorSubject<Toast[]>([]);
|
||||||
this.list = [];
|
private idCounter = 0;
|
||||||
this.idCounter = 0;
|
|
||||||
this.onChangeCallback = null;
|
public get$() {
|
||||||
|
return this.toasts$.asObservable();
|
||||||
}
|
}
|
||||||
|
|
||||||
_changed = () => {
|
public add(toastOrTitle: ToastInput) {
|
||||||
if (this.onChangeCallback) {
|
const toast: Toast = {
|
||||||
this.onChangeCallback();
|
id: String(this.idCounter++),
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onChange = callback => {
|
|
||||||
this.onChangeCallback = callback;
|
|
||||||
};
|
|
||||||
|
|
||||||
add = toastOrTitle => {
|
|
||||||
const toast = {
|
|
||||||
id: this.idCounter++,
|
|
||||||
...normalizeToast(toastOrTitle),
|
...normalizeToast(toastOrTitle),
|
||||||
};
|
};
|
||||||
|
|
||||||
this.list.push(toast);
|
this.toasts$.next([...this.toasts$.getValue(), toast]);
|
||||||
this._changed();
|
|
||||||
|
|
||||||
return toast;
|
return toast;
|
||||||
};
|
}
|
||||||
|
|
||||||
remove = toast => {
|
public remove(toast: Toast) {
|
||||||
const index = this.list.indexOf(toast);
|
const list = this.toasts$.getValue();
|
||||||
|
const listWithoutToast = list.filter(t => t !== toast);
|
||||||
if (index !== -1) {
|
if (listWithoutToast.length !== list.length) {
|
||||||
this.list.splice(index, 1);
|
this.toasts$.next(listWithoutToast);
|
||||||
this._changed();
|
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
addSuccess = toastOrTitle => {
|
public addSuccess(toastOrTitle: ToastInput) {
|
||||||
return this.add({
|
return this.add({
|
||||||
color: 'success',
|
color: 'success',
|
||||||
iconType: 'check',
|
iconType: 'check',
|
||||||
...normalizeToast(toastOrTitle),
|
...normalizeToast(toastOrTitle),
|
||||||
});
|
});
|
||||||
};
|
}
|
||||||
|
|
||||||
addWarning = toastOrTitle => {
|
public addWarning(toastOrTitle: ToastInput) {
|
||||||
return this.add({
|
return this.add({
|
||||||
color: 'warning',
|
color: 'warning',
|
||||||
iconType: 'help',
|
iconType: 'help',
|
||||||
...normalizeToast(toastOrTitle),
|
...normalizeToast(toastOrTitle),
|
||||||
});
|
});
|
||||||
};
|
}
|
||||||
|
|
||||||
addDanger = toastOrTitle => {
|
public addDanger(toastOrTitle: ToastInput) {
|
||||||
return this.add({
|
return this.add({
|
||||||
color: 'danger',
|
color: 'danger',
|
||||||
iconType: 'alert',
|
iconType: 'alert',
|
||||||
...normalizeToast(toastOrTitle),
|
...normalizeToast(toastOrTitle),
|
||||||
});
|
});
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const toastNotifications = new ToastNotifications();
|
|
|
@ -15,7 +15,6 @@
|
||||||
></kbn-notifications>
|
></kbn-notifications>
|
||||||
|
|
||||||
<div id="globalBannerList"></div>
|
<div id="globalBannerList"></div>
|
||||||
<div id="globalToastList"></div>
|
|
||||||
|
|
||||||
<kbn-loading-indicator></kbn-loading-indicator>
|
<kbn-loading-indicator></kbn-loading-indicator>
|
||||||
|
|
||||||
|
|
|
@ -29,8 +29,6 @@ import {
|
||||||
} from '../../state_management/state_hashing';
|
} from '../../state_management/state_hashing';
|
||||||
import {
|
import {
|
||||||
notify,
|
notify,
|
||||||
GlobalToastList,
|
|
||||||
toastNotifications,
|
|
||||||
GlobalBannerList,
|
GlobalBannerList,
|
||||||
banners,
|
banners,
|
||||||
} from '../../notify';
|
} from '../../notify';
|
||||||
|
@ -99,17 +97,6 @@ export function kbnChromeProvider(chrome, internals) {
|
||||||
document.getElementById('globalBannerList')
|
document.getElementById('globalBannerList')
|
||||||
);
|
);
|
||||||
|
|
||||||
// Toast Notifications
|
|
||||||
ReactDOM.render(
|
|
||||||
<GlobalToastList
|
|
||||||
toasts={toastNotifications.list}
|
|
||||||
dismissToast={toastNotifications.remove}
|
|
||||||
toastLifeTimeMs={6000}
|
|
||||||
subscribe={toastNotifications.onChange}
|
|
||||||
/>,
|
|
||||||
document.getElementById('globalToastList')
|
|
||||||
);
|
|
||||||
|
|
||||||
return chrome;
|
return chrome;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
2
src/ui/public/notify/index.d.ts
vendored
2
src/ui/public/notify/index.d.ts
vendored
|
@ -17,4 +17,4 @@
|
||||||
* under the License.
|
* under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export { toastNotifications, ToastNotifications } from './toasts';
|
export { toastNotifications, Toast, ToastInput } from './toasts';
|
||||||
|
|
|
@ -20,6 +20,6 @@
|
||||||
export { notify } from './notify';
|
export { notify } from './notify';
|
||||||
export { Notifier } from './notifier';
|
export { Notifier } from './notifier';
|
||||||
export { fatalError, addFatalErrorCallback } from './fatal_error';
|
export { fatalError, addFatalErrorCallback } from './fatal_error';
|
||||||
export { GlobalToastList, toastNotifications } from './toasts';
|
export { toastNotifications } from './toasts';
|
||||||
export { GlobalBannerList, banners } from './banners';
|
export { GlobalBannerList, banners } from './banners';
|
||||||
export { addAppRedirectMessageToUrl, showAppRedirectNotification } from './app_redirect';
|
export { addAppRedirectMessageToUrl, showAppRedirectNotification } from './app_redirect';
|
||||||
|
|
|
@ -17,4 +17,5 @@
|
||||||
* under the License.
|
* under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export { toastNotifications, ToastNotifications } from './toast_notifications';
|
export { toastNotifications, __newPlatformInit__ } from './toasts';
|
||||||
|
export { Toast, ToastInput } from './toast_notifications';
|
|
@ -18,98 +18,111 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import sinon from 'sinon';
|
import sinon from 'sinon';
|
||||||
|
import { ToastsStartContract } from '../../../../core/public/notifications';
|
||||||
|
|
||||||
import {
|
import { ToastNotifications } from './toast_notifications';
|
||||||
ToastNotifications,
|
|
||||||
} from './toast_notifications';
|
|
||||||
|
|
||||||
describe('ToastNotifications', () => {
|
describe('ToastNotifications', () => {
|
||||||
describe('interface', () => {
|
describe('interface', () => {
|
||||||
let toastNotifications;
|
function setup() {
|
||||||
|
return {
|
||||||
beforeEach(() => {
|
toastNotifications: new ToastNotifications(new ToastsStartContract()),
|
||||||
toastNotifications = new ToastNotifications();
|
};
|
||||||
});
|
}
|
||||||
|
|
||||||
describe('add method', () => {
|
describe('add method', () => {
|
||||||
test('adds a toast', () => {
|
test('adds a toast', () => {
|
||||||
|
const { toastNotifications } = setup();
|
||||||
toastNotifications.add({});
|
toastNotifications.add({});
|
||||||
expect(toastNotifications.list.length).toBe(1);
|
expect(toastNotifications.list).toHaveLength(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('adds a toast with an ID property', () => {
|
test('adds a toast with an ID property', () => {
|
||||||
|
const { toastNotifications } = setup();
|
||||||
toastNotifications.add({});
|
toastNotifications.add({});
|
||||||
expect(toastNotifications.list[0].id).toBe(0);
|
expect(toastNotifications.list[0]).toHaveProperty('id', '0');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('increments the toast ID', () => {
|
test('increments the toast ID', () => {
|
||||||
|
const { toastNotifications } = setup();
|
||||||
toastNotifications.add({});
|
toastNotifications.add({});
|
||||||
toastNotifications.add({});
|
toastNotifications.add({});
|
||||||
expect(toastNotifications.list[1].id).toBe(1);
|
expect(toastNotifications.list[1]).toHaveProperty('id', '1');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('accepts a string', () => {
|
test('accepts a string', () => {
|
||||||
|
const { toastNotifications } = setup();
|
||||||
toastNotifications.add('New toast');
|
toastNotifications.add('New toast');
|
||||||
expect(toastNotifications.list[0].title).toBe('New toast');
|
expect(toastNotifications.list[0]).toHaveProperty('title', 'New toast');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('remove method', () => {
|
describe('remove method', () => {
|
||||||
test('removes a toast', () => {
|
test('removes a toast', () => {
|
||||||
|
const { toastNotifications } = setup();
|
||||||
const toast = toastNotifications.add('Test');
|
const toast = toastNotifications.add('Test');
|
||||||
toastNotifications.remove(toast);
|
toastNotifications.remove(toast);
|
||||||
expect(toastNotifications.list.length).toBe(0);
|
expect(toastNotifications.list).toHaveLength(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('ignores unknown toast', () => {
|
test('ignores unknown toast', () => {
|
||||||
toastNotifications.add('Test');
|
const { toastNotifications } = setup();
|
||||||
toastNotifications.remove({});
|
const toast = toastNotifications.add('Test');
|
||||||
expect(toastNotifications.list.length).toBe(1);
|
toastNotifications.remove({
|
||||||
|
id: `not ${toast.id}`,
|
||||||
|
});
|
||||||
|
expect(toastNotifications.list).toHaveLength(1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('onChange method', () => {
|
describe('onChange method', () => {
|
||||||
test('callback is called when a toast is added', () => {
|
test('callback is called when a toast is added', () => {
|
||||||
|
const { toastNotifications } = setup();
|
||||||
const onChangeSpy = sinon.spy();
|
const onChangeSpy = sinon.spy();
|
||||||
toastNotifications.onChange(onChangeSpy);
|
toastNotifications.onChange(onChangeSpy);
|
||||||
toastNotifications.add({});
|
toastNotifications.add({});
|
||||||
expect(onChangeSpy.callCount).toBe(1);
|
sinon.assert.calledOnce(onChangeSpy);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('callback is called when a toast is removed', () => {
|
test('callback is called when a toast is removed', () => {
|
||||||
|
const { toastNotifications } = setup();
|
||||||
const onChangeSpy = sinon.spy();
|
const onChangeSpy = sinon.spy();
|
||||||
toastNotifications.onChange(onChangeSpy);
|
toastNotifications.onChange(onChangeSpy);
|
||||||
const toast = toastNotifications.add({});
|
const toast = toastNotifications.add({});
|
||||||
toastNotifications.remove(toast);
|
toastNotifications.remove(toast);
|
||||||
expect(onChangeSpy.callCount).toBe(2);
|
sinon.assert.calledTwice(onChangeSpy);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('callback is not called when remove is ignored', () => {
|
test('callback is not called when remove is ignored', () => {
|
||||||
|
const { toastNotifications } = setup();
|
||||||
const onChangeSpy = sinon.spy();
|
const onChangeSpy = sinon.spy();
|
||||||
toastNotifications.onChange(onChangeSpy);
|
toastNotifications.onChange(onChangeSpy);
|
||||||
toastNotifications.remove({});
|
toastNotifications.remove({ id: 'foo' });
|
||||||
expect(onChangeSpy.callCount).toBe(0);
|
sinon.assert.notCalled(onChangeSpy);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('addSuccess method', () => {
|
describe('addSuccess method', () => {
|
||||||
test('adds a success toast', () => {
|
test('adds a success toast', () => {
|
||||||
|
const { toastNotifications } = setup();
|
||||||
toastNotifications.addSuccess({});
|
toastNotifications.addSuccess({});
|
||||||
expect(toastNotifications.list[0].color).toBe('success');
|
expect(toastNotifications.list[0]).toHaveProperty('color', 'success');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('addWarning method', () => {
|
describe('addWarning method', () => {
|
||||||
test('adds a warning toast', () => {
|
test('adds a warning toast', () => {
|
||||||
|
const { toastNotifications } = setup();
|
||||||
toastNotifications.addWarning({});
|
toastNotifications.addWarning({});
|
||||||
expect(toastNotifications.list[0].color).toBe('warning');
|
expect(toastNotifications.list[0]).toHaveProperty('color', 'warning');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('addDanger method', () => {
|
describe('addDanger method', () => {
|
||||||
test('adds a danger toast', () => {
|
test('adds a danger toast', () => {
|
||||||
|
const { toastNotifications } = setup();
|
||||||
toastNotifications.addDanger({});
|
toastNotifications.addDanger({});
|
||||||
expect(toastNotifications.list[0].color).toBe('danger');
|
expect(toastNotifications.list[0]).toHaveProperty('color', 'danger');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
48
src/ui/public/notify/toasts/toast_notifications.ts
Normal file
48
src/ui/public/notify/toasts/toast_notifications.ts
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
/*
|
||||||
|
* 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 { Toast, ToastInput, ToastsStartContract } from '../../../../core/public/notifications';
|
||||||
|
|
||||||
|
export { Toast, ToastInput };
|
||||||
|
|
||||||
|
export class ToastNotifications {
|
||||||
|
public list: Toast[] = [];
|
||||||
|
|
||||||
|
private onChangeCallback?: () => void;
|
||||||
|
|
||||||
|
constructor(private readonly toasts: ToastsStartContract) {
|
||||||
|
toasts.get$().subscribe(list => {
|
||||||
|
this.list = list;
|
||||||
|
|
||||||
|
if (this.onChangeCallback) {
|
||||||
|
this.onChangeCallback();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public onChange = (callback: () => void) => {
|
||||||
|
this.onChangeCallback = callback;
|
||||||
|
};
|
||||||
|
|
||||||
|
public add = (toastOrTitle: ToastInput) => this.toasts.add(toastOrTitle);
|
||||||
|
public remove = (toast: Toast) => this.toasts.remove(toast);
|
||||||
|
public addSuccess = (toastOrTitle: ToastInput) => this.toasts.addSuccess(toastOrTitle);
|
||||||
|
public addWarning = (toastOrTitle: ToastInput) => this.toasts.addWarning(toastOrTitle);
|
||||||
|
public addDanger = (toastOrTitle: ToastInput) => this.toasts.addDanger(toastOrTitle);
|
||||||
|
}
|
|
@ -17,25 +17,15 @@
|
||||||
* under the License.
|
* under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
interface Toast extends ToastDescription {
|
import { ToastsStartContract } from '../../../../core/public/notifications';
|
||||||
id: number;
|
import { ToastNotifications } from './toast_notifications';
|
||||||
}
|
|
||||||
|
|
||||||
interface ToastDescription {
|
export let toastNotifications: ToastNotifications;
|
||||||
title: string;
|
|
||||||
color?: string;
|
|
||||||
iconType?: string;
|
|
||||||
text?: string;
|
|
||||||
'data-test-subj'?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ToastNotifications {
|
export function __newPlatformInit__(toasts: ToastsStartContract) {
|
||||||
onChange(changeCallback: () => void): void;
|
if (toastNotifications) {
|
||||||
remove(toast: Toast): void;
|
throw new Error('ui/notify/toasts already initialized with new platform apis');
|
||||||
add(toast: ToastDescription | string): Toast;
|
}
|
||||||
addSuccess(toast: ToastDescription | string): Toast;
|
|
||||||
addWarning(toast: ToastDescription | string): Toast;
|
|
||||||
addDanger(toast: ToastDescription | string): Toast;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const toastNotifications: ToastNotifications;
|
toastNotifications = new ToastNotifications(toasts);
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue