mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -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 { InjectedMetadataService } from './injected_metadata';
|
||||
import { LegacyPlatformService } from './legacy_platform';
|
||||
import { NotificationsService } from './notifications';
|
||||
|
||||
const MockLegacyPlatformService = jest.fn<LegacyPlatformService>(
|
||||
function _MockLegacyPlatformService(this: any) {
|
||||
|
@ -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');
|
||||
|
||||
|
@ -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', () => {
|
||||
|
@ -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,
|
||||
});
|
||||
|
@ -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;
|
||||
|
@ -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()', () => {
|
||||
|
@ -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', () => {
|
||||
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
|
@ -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) {
|
||||
|
@ -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,
|
||||
|
@ -73,11 +81,13 @@ 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);
|
||||
}
|
||||
|
@ -85,6 +95,7 @@ export class CoreSystem {
|
|||
|
||||
public stop() {
|
||||
this.legacyPlatform.stop();
|
||||
this.notifications.stop();
|
||||
this.rootDomElement.textContent = '';
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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(),
|
||||
|
@ -88,6 +99,7 @@ describe('#start()', () => {
|
|||
legacyPlatform.start({
|
||||
fatalErrors: fatalErrorsStartContract,
|
||||
injectedMetadata: injectedMetadataStartContract,
|
||||
notifications: notificationsStartContract,
|
||||
});
|
||||
|
||||
expect(mockUiMetadataInit).toHaveBeenCalledTimes(1);
|
||||
|
@ -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({
|
||||
|
@ -117,6 +145,7 @@ describe('#start()', () => {
|
|||
legacyPlatform.start({
|
||||
fatalErrors: fatalErrorsStartContract,
|
||||
injectedMetadata: injectedMetadataStartContract,
|
||||
notifications: notificationsStartContract,
|
||||
});
|
||||
|
||||
expect(mockUiTestHarnessBootstrap).not.toHaveBeenCalled();
|
||||
|
@ -134,6 +163,7 @@ describe('#start()', () => {
|
|||
legacyPlatform.start({
|
||||
fatalErrors: fatalErrorsStartContract,
|
||||
injectedMetadata: injectedMetadataStartContract,
|
||||
notifications: notificationsStartContract,
|
||||
});
|
||||
|
||||
expect(mockUiChromeBootstrap).not.toHaveBeenCalled();
|
||||
|
@ -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',
|
||||
]);
|
||||
|
@ -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',
|
||||
]);
|
||||
|
|
|
@ -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 {
|
||||
|
@ -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
|
||||
|
|
|
@ -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
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.
|
||||
*/
|
||||
|
||||
import React, {
|
||||
Component,
|
||||
} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { EuiGlobalToastList, Toast } from '@elastic/eui';
|
||||
|
||||
import {
|
||||
EuiGlobalToastList,
|
||||
EuiPortal,
|
||||
} from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import * as Rx from 'rxjs';
|
||||
|
||||
export class GlobalToastList extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
interface Props {
|
||||
toasts$: Rx.Observable<Toast[]>;
|
||||
dismissToast: (t: Toast) => void;
|
||||
}
|
||||
|
||||
if (this.props.subscribe) {
|
||||
this.props.subscribe(() => this.forceUpdate());
|
||||
interface State {
|
||||
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 = {
|
||||
subscribe: PropTypes.func,
|
||||
toasts: PropTypes.array,
|
||||
dismissToast: PropTypes.func.isRequired,
|
||||
toastLifeTimeMs: PropTypes.number.isRequired,
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
toasts,
|
||||
dismissToast,
|
||||
toastLifeTimeMs,
|
||||
} = this.props;
|
||||
|
||||
public render() {
|
||||
return (
|
||||
<EuiPortal>
|
||||
<EuiGlobalToastList
|
||||
toasts={toasts}
|
||||
dismissToast={dismissToast}
|
||||
toastLifeTimeMs={toastLifeTimeMs}
|
||||
/>
|
||||
</EuiPortal>
|
||||
<EuiGlobalToastList
|
||||
toasts={this.state.toasts}
|
||||
dismissToast={this.props.dismissToast}
|
||||
toastLifeTimeMs={6000}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
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.
|
||||
*/
|
||||
|
||||
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') {
|
||||
return {
|
||||
title: toastOrTitle,
|
||||
|
@ -27,67 +32,54 @@ const normalizeToast = toastOrTitle => {
|
|||
return toastOrTitle;
|
||||
};
|
||||
|
||||
export class ToastNotifications {
|
||||
constructor() {
|
||||
this.list = [];
|
||||
this.idCounter = 0;
|
||||
this.onChangeCallback = null;
|
||||
export class ToastsStartContract {
|
||||
private toasts$ = new Rx.BehaviorSubject<Toast[]>([]);
|
||||
private idCounter = 0;
|
||||
|
||||
public get$() {
|
||||
return this.toasts$.asObservable();
|
||||
}
|
||||
|
||||
_changed = () => {
|
||||
if (this.onChangeCallback) {
|
||||
this.onChangeCallback();
|
||||
}
|
||||
}
|
||||
|
||||
onChange = callback => {
|
||||
this.onChangeCallback = callback;
|
||||
};
|
||||
|
||||
add = toastOrTitle => {
|
||||
const toast = {
|
||||
id: this.idCounter++,
|
||||
public add(toastOrTitle: ToastInput) {
|
||||
const toast: Toast = {
|
||||
id: String(this.idCounter++),
|
||||
...normalizeToast(toastOrTitle),
|
||||
};
|
||||
|
||||
this.list.push(toast);
|
||||
this._changed();
|
||||
this.toasts$.next([...this.toasts$.getValue(), toast]);
|
||||
|
||||
return toast;
|
||||
};
|
||||
}
|
||||
|
||||
remove = toast => {
|
||||
const index = this.list.indexOf(toast);
|
||||
|
||||
if (index !== -1) {
|
||||
this.list.splice(index, 1);
|
||||
this._changed();
|
||||
public remove(toast: Toast) {
|
||||
const list = this.toasts$.getValue();
|
||||
const listWithoutToast = list.filter(t => t !== toast);
|
||||
if (listWithoutToast.length !== list.length) {
|
||||
this.toasts$.next(listWithoutToast);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
addSuccess = toastOrTitle => {
|
||||
public addSuccess(toastOrTitle: ToastInput) {
|
||||
return this.add({
|
||||
color: 'success',
|
||||
iconType: 'check',
|
||||
...normalizeToast(toastOrTitle),
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
addWarning = toastOrTitle => {
|
||||
public addWarning(toastOrTitle: ToastInput) {
|
||||
return this.add({
|
||||
color: 'warning',
|
||||
iconType: 'help',
|
||||
...normalizeToast(toastOrTitle),
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
addDanger = toastOrTitle => {
|
||||
public addDanger(toastOrTitle: ToastInput) {
|
||||
return this.add({
|
||||
color: 'danger',
|
||||
iconType: 'alert',
|
||||
...normalizeToast(toastOrTitle),
|
||||
});
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export const toastNotifications = new ToastNotifications();
|
|
@ -15,7 +15,6 @@
|
|||
></kbn-notifications>
|
||||
|
||||
<div id="globalBannerList"></div>
|
||||
<div id="globalToastList"></div>
|
||||
|
||||
<kbn-loading-indicator></kbn-loading-indicator>
|
||||
|
||||
|
|
|
@ -29,8 +29,6 @@ import {
|
|||
} from '../../state_management/state_hashing';
|
||||
import {
|
||||
notify,
|
||||
GlobalToastList,
|
||||
toastNotifications,
|
||||
GlobalBannerList,
|
||||
banners,
|
||||
} from '../../notify';
|
||||
|
@ -99,17 +97,6 @@ export function kbnChromeProvider(chrome, internals) {
|
|||
document.getElementById('globalBannerList')
|
||||
);
|
||||
|
||||
// Toast Notifications
|
||||
ReactDOM.render(
|
||||
<GlobalToastList
|
||||
toasts={toastNotifications.list}
|
||||
dismissToast={toastNotifications.remove}
|
||||
toastLifeTimeMs={6000}
|
||||
subscribe={toastNotifications.onChange}
|
||||
/>,
|
||||
document.getElementById('globalToastList')
|
||||
);
|
||||
|
||||
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.
|
||||
*/
|
||||
|
||||
export { toastNotifications, ToastNotifications } from './toasts';
|
||||
export { toastNotifications, Toast, ToastInput } from './toasts';
|
||||
|
|
|
@ -20,6 +20,6 @@
|
|||
export { notify } from './notify';
|
||||
export { Notifier } from './notifier';
|
||||
export { fatalError, addFatalErrorCallback } from './fatal_error';
|
||||
export { GlobalToastList, toastNotifications } from './toasts';
|
||||
export { toastNotifications } from './toasts';
|
||||
export { GlobalBannerList, banners } from './banners';
|
||||
export { addAppRedirectMessageToUrl, showAppRedirectNotification } from './app_redirect';
|
||||
|
|
|
@ -17,4 +17,5 @@
|
|||
* 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 { ToastsStartContract } from '../../../../core/public/notifications';
|
||||
|
||||
import {
|
||||
ToastNotifications,
|
||||
} from './toast_notifications';
|
||||
import { ToastNotifications } from './toast_notifications';
|
||||
|
||||
describe('ToastNotifications', () => {
|
||||
describe('interface', () => {
|
||||
let toastNotifications;
|
||||
|
||||
beforeEach(() => {
|
||||
toastNotifications = new ToastNotifications();
|
||||
});
|
||||
function setup() {
|
||||
return {
|
||||
toastNotifications: new ToastNotifications(new ToastsStartContract()),
|
||||
};
|
||||
}
|
||||
|
||||
describe('add method', () => {
|
||||
test('adds a toast', () => {
|
||||
const { toastNotifications } = setup();
|
||||
toastNotifications.add({});
|
||||
expect(toastNotifications.list.length).toBe(1);
|
||||
expect(toastNotifications.list).toHaveLength(1);
|
||||
});
|
||||
|
||||
test('adds a toast with an ID property', () => {
|
||||
const { toastNotifications } = setup();
|
||||
toastNotifications.add({});
|
||||
expect(toastNotifications.list[0].id).toBe(0);
|
||||
expect(toastNotifications.list[0]).toHaveProperty('id', '0');
|
||||
});
|
||||
|
||||
test('increments the toast ID', () => {
|
||||
const { toastNotifications } = setup();
|
||||
toastNotifications.add({});
|
||||
toastNotifications.add({});
|
||||
expect(toastNotifications.list[1].id).toBe(1);
|
||||
expect(toastNotifications.list[1]).toHaveProperty('id', '1');
|
||||
});
|
||||
|
||||
test('accepts a string', () => {
|
||||
const { toastNotifications } = setup();
|
||||
toastNotifications.add('New toast');
|
||||
expect(toastNotifications.list[0].title).toBe('New toast');
|
||||
expect(toastNotifications.list[0]).toHaveProperty('title', 'New toast');
|
||||
});
|
||||
});
|
||||
|
||||
describe('remove method', () => {
|
||||
test('removes a toast', () => {
|
||||
const { toastNotifications } = setup();
|
||||
const toast = toastNotifications.add('Test');
|
||||
toastNotifications.remove(toast);
|
||||
expect(toastNotifications.list.length).toBe(0);
|
||||
expect(toastNotifications.list).toHaveLength(0);
|
||||
});
|
||||
|
||||
test('ignores unknown toast', () => {
|
||||
toastNotifications.add('Test');
|
||||
toastNotifications.remove({});
|
||||
expect(toastNotifications.list.length).toBe(1);
|
||||
const { toastNotifications } = setup();
|
||||
const toast = toastNotifications.add('Test');
|
||||
toastNotifications.remove({
|
||||
id: `not ${toast.id}`,
|
||||
});
|
||||
expect(toastNotifications.list).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('onChange method', () => {
|
||||
test('callback is called when a toast is added', () => {
|
||||
const { toastNotifications } = setup();
|
||||
const onChangeSpy = sinon.spy();
|
||||
toastNotifications.onChange(onChangeSpy);
|
||||
toastNotifications.add({});
|
||||
expect(onChangeSpy.callCount).toBe(1);
|
||||
sinon.assert.calledOnce(onChangeSpy);
|
||||
});
|
||||
|
||||
test('callback is called when a toast is removed', () => {
|
||||
const { toastNotifications } = setup();
|
||||
const onChangeSpy = sinon.spy();
|
||||
toastNotifications.onChange(onChangeSpy);
|
||||
const toast = toastNotifications.add({});
|
||||
toastNotifications.remove(toast);
|
||||
expect(onChangeSpy.callCount).toBe(2);
|
||||
sinon.assert.calledTwice(onChangeSpy);
|
||||
});
|
||||
|
||||
test('callback is not called when remove is ignored', () => {
|
||||
const { toastNotifications } = setup();
|
||||
const onChangeSpy = sinon.spy();
|
||||
toastNotifications.onChange(onChangeSpy);
|
||||
toastNotifications.remove({});
|
||||
expect(onChangeSpy.callCount).toBe(0);
|
||||
toastNotifications.remove({ id: 'foo' });
|
||||
sinon.assert.notCalled(onChangeSpy);
|
||||
});
|
||||
});
|
||||
|
||||
describe('addSuccess method', () => {
|
||||
test('adds a success toast', () => {
|
||||
const { toastNotifications } = setup();
|
||||
toastNotifications.addSuccess({});
|
||||
expect(toastNotifications.list[0].color).toBe('success');
|
||||
expect(toastNotifications.list[0]).toHaveProperty('color', 'success');
|
||||
});
|
||||
});
|
||||
|
||||
describe('addWarning method', () => {
|
||||
test('adds a warning toast', () => {
|
||||
const { toastNotifications } = setup();
|
||||
toastNotifications.addWarning({});
|
||||
expect(toastNotifications.list[0].color).toBe('warning');
|
||||
expect(toastNotifications.list[0]).toHaveProperty('color', 'warning');
|
||||
});
|
||||
});
|
||||
|
||||
describe('addDanger method', () => {
|
||||
test('adds a danger toast', () => {
|
||||
const { toastNotifications } = setup();
|
||||
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.
|
||||
*/
|
||||
|
||||
interface Toast extends ToastDescription {
|
||||
id: number;
|
||||
}
|
||||
import { ToastsStartContract } from '../../../../core/public/notifications';
|
||||
import { ToastNotifications } from './toast_notifications';
|
||||
|
||||
interface ToastDescription {
|
||||
title: string;
|
||||
color?: string;
|
||||
iconType?: string;
|
||||
text?: string;
|
||||
'data-test-subj'?: string;
|
||||
}
|
||||
export let toastNotifications: ToastNotifications;
|
||||
|
||||
export interface ToastNotifications {
|
||||
onChange(changeCallback: () => void): void;
|
||||
remove(toast: Toast): void;
|
||||
add(toast: ToastDescription | string): Toast;
|
||||
addSuccess(toast: ToastDescription | string): Toast;
|
||||
addWarning(toast: ToastDescription | string): Toast;
|
||||
addDanger(toast: ToastDescription | string): Toast;
|
||||
}
|
||||
export function __newPlatformInit__(toasts: ToastsStartContract) {
|
||||
if (toastNotifications) {
|
||||
throw new Error('ui/notify/toasts already initialized with new platform apis');
|
||||
}
|
||||
|
||||
export const toastNotifications: ToastNotifications;
|
||||
toastNotifications = new ToastNotifications(toasts);
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue