Skip to content

Commit

Permalink
[toasts] migrate toastNotifications to the new platform (elastic#21772)
Browse files Browse the repository at this point in the history
Fixes elastic#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](https://github.com/elastic/kibana/blob/494c267cd97ba0c48c2acd10a8fff2c27defd0bb/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
  • Loading branch information
Spencer authored and cjcenizal committed Aug 21, 2018
1 parent d90ac3f commit de503ee
Show file tree
Hide file tree
Showing 25 changed files with 752 additions and 143 deletions.
65 changes: 61 additions & 4 deletions src/core/public/core_system.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import { FatalErrorsService } from './fatal_errors';
import { InjectedMetadataService } from './injected_metadata';
import { LegacyPlatformService } from './legacy_platform';
import { NotificationsService } from './notifications';

const MockLegacyPlatformService = jest.fn<LegacyPlatformService>(
function _MockLegacyPlatformService(this: any) {
Expand Down Expand Up @@ -52,6 +53,18 @@ jest.mock('./fatal_errors', () => ({
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';
jest.spyOn(CoreSystem.prototype, 'stop');

Expand All @@ -74,6 +87,8 @@ describe('constructor', () => {

expect(MockInjectedMetadataService).toHaveBeenCalledTimes(1);
expect(MockLegacyPlatformService).toHaveBeenCalledTimes(1);
expect(MockFatalErrorsService).toHaveBeenCalledTimes(1);
expect(MockNotificationsService).toHaveBeenCalledTimes(1);
});

it('passes injectedMetadata param to InjectedMetadataService', () => {
Expand All @@ -92,14 +107,12 @@ describe('constructor', () => {
});

it('passes requireLegacyFiles, useLegacyTestHarness, and a dom element to LegacyPlatformService', () => {
const rootDomElement = document.createElement('div');
const requireLegacyFiles = { requireLegacyFiles: true } as any;
const useLegacyTestHarness = { useLegacyTestHarness: true } as any;

// tslint:disable no-unused-expression
new CoreSystem({
...defaultCoreSystemParams,
rootDomElement,
requireLegacyFiles,
useLegacyTestHarness,
});
Expand All @@ -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', () => {
const rootDomElement = document.createElement('div');
const injectedMetadata = { injectedMetadata: true } as any;
Expand Down Expand Up @@ -161,11 +186,11 @@ describe('#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');
root.innerHTML = '<p>foo bar</p>';
startCore(root);
expect(root.innerHTML).toBe('<div></div>');
expect(root.innerHTML).toBe('<div></div><div></div>');
});

it('calls injectedMetadata#start()', () => {
Expand All @@ -181,6 +206,13 @@ describe('#start()', () => {
expect(mockInstance.start).toHaveBeenCalledTimes(1);
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', () => {
Expand All @@ -207,3 +239,28 @@ describe('LegacyPlatform targetDomElement', () => {
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);
});
});
13 changes: 12 additions & 1 deletion src/core/public/core_system.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import './core.css';
import { FatalErrorsService } from './fatal_errors';
import { InjectedMetadataParams, InjectedMetadataService } from './injected_metadata';
import { LegacyPlatformParams, LegacyPlatformService } from './legacy_platform';
import { NotificationsService } from './notifications';

interface Params {
rootDomElement: HTMLElement;
Expand All @@ -39,8 +40,10 @@ export class CoreSystem {
private readonly fatalErrors: FatalErrorsService;
private readonly injectedMetadata: InjectedMetadataService;
private readonly legacyPlatform: LegacyPlatformService;
private readonly notifications: NotificationsService;

private readonly rootDomElement: HTMLElement;
private readonly notificationsTargetDomElement: HTMLDivElement;
private readonly legacyPlatformTargetDomElement: HTMLDivElement;

constructor(params: Params) {
Expand All @@ -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.legacyPlatform = new LegacyPlatformService({
targetDomElement: this.legacyPlatformTargetDomElement,
Expand All @@ -73,18 +81,21 @@ export class CoreSystem {
// ensure the rootDomElement is empty
this.rootDomElement.textContent = '';
this.rootDomElement.classList.add('coreSystemRootDomElement');
this.rootDomElement.appendChild(this.notificationsTargetDomElement);
this.rootDomElement.appendChild(this.legacyPlatformTargetDomElement);

const notifications = this.notifications.start();
const injectedMetadata = this.injectedMetadata.start();
const fatalErrors = this.fatalErrors.start();
this.legacyPlatform.start({ injectedMetadata, fatalErrors });
this.legacyPlatform.start({ injectedMetadata, fatalErrors, notifications });
} catch (error) {
this.fatalErrors.add(error);
}
}

public stop() {
this.legacyPlatform.stop();
this.notifications.stop();
this.rootDomElement.textContent = '';
}
}
1 change: 0 additions & 1 deletion src/core/public/fatal_errors/fatal_errors_screen.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@
* under the License.
*/

// @ts-ignore EuiCallOut not available until we upgrade to EUI 3.1.0
import { EuiCallOut } from '@elastic/eui';
import testSubjSelector from '@kbn/test-subj-selector';
import { mount, shallow } from 'enzyme';
Expand Down
3 changes: 0 additions & 3 deletions src/core/public/fatal_errors/fatal_errors_screen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,8 @@
import {
EuiButton,
EuiButtonEmpty,
// @ts-ignore EuiCallOut not available until we upgrade to EUI 3.1.0
EuiCallOut,
// @ts-ignore EuiCodeBlock not available until we upgrade to EUI 3.1.0
EuiCodeBlock,
// @ts-ignore EuiEmptyPrompt not available until we upgrade to EUI 3.1.0
EuiEmptyPrompt,
EuiPage,
EuiPageBody,
Expand Down
34 changes: 34 additions & 0 deletions src/core/public/legacy_platform/legacy_platform_service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

const fatalErrorsStartContract = {} as any;
const notificationsStartContract = {
toasts: {},
} as any;

const injectedMetadataStartContract = {
getLegacyMetadata: jest.fn(),
Expand Down Expand Up @@ -88,6 +99,7 @@ describe('#start()', () => {
legacyPlatform.start({
fatalErrors: fatalErrorsStartContract,
injectedMetadata: injectedMetadataStartContract,
notifications: notificationsStartContract,
});

expect(mockUiMetadataInit).toHaveBeenCalledTimes(1);
Expand All @@ -102,12 +114,28 @@ describe('#start()', () => {
legacyPlatform.start({
fatalErrors: fatalErrorsStartContract,
injectedMetadata: injectedMetadataStartContract,
notifications: notificationsStartContract,
});

expect(mockFatalErrorInit).toHaveBeenCalledTimes(1);
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', () => {
it('passes the targetDomElement to ui/chrome', () => {
const legacyPlatform = new LegacyPlatformService({
Expand All @@ -117,6 +145,7 @@ describe('#start()', () => {
legacyPlatform.start({
fatalErrors: fatalErrorsStartContract,
injectedMetadata: injectedMetadataStartContract,
notifications: notificationsStartContract,
});

expect(mockUiTestHarnessBootstrap).not.toHaveBeenCalled();
Expand All @@ -134,6 +163,7 @@ describe('#start()', () => {
legacyPlatform.start({
fatalErrors: fatalErrorsStartContract,
injectedMetadata: injectedMetadataStartContract,
notifications: notificationsStartContract,
});

expect(mockUiChromeBootstrap).not.toHaveBeenCalled();
Expand All @@ -155,11 +185,13 @@ describe('#start()', () => {
legacyPlatform.start({
fatalErrors: fatalErrorsStartContract,
injectedMetadata: injectedMetadataStartContract,
notifications: notificationsStartContract,
});

expect(mockLoadOrder).toEqual([
'ui/metadata',
'ui/notify/fatal_error',
'ui/notify/toasts',
'ui/chrome',
'legacy files',
]);
Expand All @@ -178,11 +210,13 @@ describe('#start()', () => {
legacyPlatform.start({
fatalErrors: fatalErrorsStartContract,
injectedMetadata: injectedMetadataStartContract,
notifications: notificationsStartContract,
});

expect(mockLoadOrder).toEqual([
'ui/metadata',
'ui/notify/fatal_error',
'ui/notify/toasts',
'ui/test_harness',
'legacy files',
]);
Expand Down
5 changes: 4 additions & 1 deletion src/core/public/legacy_platform/legacy_platform_service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,12 @@
import angular from 'angular';
import { FatalErrorsStartContract } from '../fatal_errors';
import { InjectedMetadataStartContract } from '../injected_metadata';
import { NotificationsStartContract } from '../notifications';

interface Deps {
injectedMetadata: InjectedMetadataStartContract;
fatalErrors: FatalErrorsStartContract;
notifications: NotificationsStartContract;
}

export interface LegacyPlatformParams {
Expand All @@ -42,11 +44,12 @@ export interface LegacyPlatformParams {
export class LegacyPlatformService {
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
// so that legacy APIs/modules can mimic their new platform counterparts
require('ui/metadata').__newPlatformInit__(injectedMetadata.getLegacyMetadata());
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
// the bootstrap module can modify the environment a bit first
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,5 @@
* under the License.
*/

export { GlobalToastList } from './global_toast_list';
export { toastNotifications } from './toast_notifications';
export { Toast, ToastInput, ToastsStartContract } from './toasts';
export { NotificationsService, NotificationsStartContract } from './notifications_service';
53 changes: 53 additions & 0 deletions src/core/public/notifications/notifications_service.ts
Original file line number Diff line number Diff line change
@@ -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']>;
Loading

0 comments on commit de503ee

Please sign in to comment.