diff --git a/.i18nrc.json b/.i18nrc.json index d53285671b768..2359c6328e41b 100644 --- a/.i18nrc.json +++ b/.i18nrc.json @@ -10,6 +10,7 @@ "src/ui/ui_render/bootstrap/app_bootstrap.js", "src/ui/ui_render/ui_render_mixin.js", "x-pack/plugins/monitoring/public/components/cluster/overview/alerts_panel.js", - "x-pack/plugins/monitoring/public/directives/alerts/index.js" + "x-pack/plugins/monitoring/public/directives/alerts/index.js", + "src/ui/public/notify/banners.tsx" ] } diff --git a/src/core/public/legacy_platform/legacy_platform_service.test.ts b/src/core/public/legacy_platform/legacy_platform_service.test.ts index 913cfc79a6ae9..84314b9a5bb42 100644 --- a/src/core/public/legacy_platform/legacy_platform_service.test.ts +++ b/src/core/public/legacy_platform/legacy_platform_service.test.ts @@ -62,6 +62,14 @@ jest.mock('ui/notify/toasts', () => { }; }); +const mockNotifyBannersInit = jest.fn(); +jest.mock('ui/notify/banners', () => { + mockLoadOrder.push('ui/notify/banners'); + return { + __newPlatformInit__: mockNotifyBannersInit, + }; +}); + const mockLoadingCountInit = jest.fn(); jest.mock('ui/chrome/api/loading_count', () => { mockLoadOrder.push('ui/chrome/api/loading_count'); @@ -99,6 +107,7 @@ import { LegacyPlatformService } from './legacy_platform_service'; const fatalErrorsStartContract = {} as any; const notificationsStartContract = { toasts: {}, + banners: {}, } as any; const injectedMetadataStartContract: any = { @@ -180,6 +189,17 @@ describe('#start()', () => { expect(mockNotifyToastsInit).toHaveBeenCalledWith(notificationsStartContract.toasts); }); + it('passes banners service to ui/notify/banners', () => { + const legacyPlatform = new LegacyPlatformService({ + ...defaultParams, + }); + + legacyPlatform.start(defaultStartDeps); + + expect(mockNotifyBannersInit).toHaveBeenCalledTimes(1); + expect(mockNotifyBannersInit).toHaveBeenCalledWith(notificationsStartContract.banners); + }); + it('passes loadingCount service to ui/chrome/api/loading_count', () => { const legacyPlatform = new LegacyPlatformService({ ...defaultParams, diff --git a/src/core/public/legacy_platform/legacy_platform_service.ts b/src/core/public/legacy_platform/legacy_platform_service.ts index 2b7ae5f2bc894..1232dd3d685eb 100644 --- a/src/core/public/legacy_platform/legacy_platform_service.ts +++ b/src/core/public/legacy_platform/legacy_platform_service.ts @@ -63,6 +63,7 @@ export class LegacyPlatformService { require('ui/metadata').__newPlatformInit__(injectedMetadata.getLegacyMetadata()); require('ui/notify/fatal_error').__newPlatformInit__(fatalErrors); require('ui/notify/toasts').__newPlatformInit__(notifications.toasts); + require('ui/notify/banners').__newPlatformInit__(notifications.banners); require('ui/chrome/api/loading_count').__newPlatformInit__(loadingCount); require('ui/chrome/api/base_path').__newPlatformInit__(basePath); require('ui/chrome/api/ui_settings').__newPlatformInit__(uiSettings); diff --git a/src/core/public/notifications/banners/banners_service.test.tsx b/src/core/public/notifications/banners/banners_service.test.tsx new file mode 100644 index 0000000000000..086d96d584abe --- /dev/null +++ b/src/core/public/notifications/banners/banners_service.test.tsx @@ -0,0 +1,190 @@ +/* + * 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 { unmountComponentAtNode } from 'react-dom'; +import { BannersService } from './banners_service'; + +function renderFn(innerHTML: string) { + return (el: HTMLDivElement) => { + el.innerHTML = innerHTML; + return () => (el.innerHTML = ''); + }; +} + +describe('start.add()', () => { + it('renders the component in the targetDomElement', () => { + const targetDomElement = document.createElement('div'); + const start = new BannersService({ targetDomElement }).start(); + start.add(renderFn('foo')); + + expect(targetDomElement).toMatchInlineSnapshot(` +
+
+
+ foo +
+
+
+`); + }); + + it('renders higher-priority banners abover lower-priority ones', () => { + const targetDomElement = document.createElement('div'); + const start = new BannersService({ targetDomElement }).start(); + start.add(renderFn('100'), 100); + start.add(renderFn('1'), 1); + start.add(renderFn('200'), 200); + + expect(targetDomElement).toMatchInlineSnapshot(` +
+
+
+ 200 +
+
+ 100 +
+
+ 1 +
+
+
+`); + }); +}); + +describe('start.remove()', () => { + it('removes the component from the targetDomElement', () => { + const targetDomElement = document.createElement('div'); + const start = new BannersService({ targetDomElement }).start(); + const id = start.add(renderFn('foo')); + start.remove(id); + expect(targetDomElement).toMatchInlineSnapshot(`
`); + }); + + it('does nothing if the id is unknown', () => { + const targetDomElement = document.createElement('div'); + const start = new BannersService({ targetDomElement }).start(); + start.add(renderFn('foo')); + start.remove('something random'); + expect(targetDomElement).toMatchInlineSnapshot(` +
+
+
+ foo +
+
+
+`); + }); +}); + +describe('start.replace()', () => { + it('replaces the banner with the matching id', () => { + const targetDomElement = document.createElement('div'); + const start = new BannersService({ targetDomElement }).start(); + const id = start.add(renderFn('foo')); + expect(targetDomElement).toMatchInlineSnapshot(` +
+
+
+ foo +
+
+
+`); + start.replace(id, renderFn('bar')); + expect(targetDomElement).toMatchInlineSnapshot(` +
+
+
+ bar +
+
+
+`); + }); + + it('adds the banner if the id is unknown', () => { + const targetDomElement = document.createElement('div'); + const start = new BannersService({ targetDomElement }).start(); + start.add(renderFn('foo')); + start.replace('something random', renderFn('bar')); + expect(targetDomElement).toMatchInlineSnapshot(` +
+
+
+ foo +
+
+ bar +
+
+
+`); + }); +}); + +describe('stop', () => { + it('unmounts the component from the targetDomElement', () => { + const targetDomElement = document.createElement('div'); + const service = new BannersService({ targetDomElement }); + service.start(); + service.stop(); + expect(unmountComponentAtNode(targetDomElement)).toBe(false); + }); + + it('cleans out the content of the targetDomElement', () => { + const targetDomElement = document.createElement('div'); + const service = new BannersService({ targetDomElement }); + service.start().add(renderFn('foo-bar')); + service.stop(); + expect(targetDomElement).toMatchInlineSnapshot(`
`); + }); +}); diff --git a/src/core/public/notifications/banners/banners_service.tsx b/src/core/public/notifications/banners/banners_service.tsx new file mode 100644 index 0000000000000..50935ab543cac --- /dev/null +++ b/src/core/public/notifications/banners/banners_service.tsx @@ -0,0 +1,88 @@ +/* + * 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 * as Rx from 'rxjs'; + +import { GlobalBannersContainer } from './containers/global_banners_container'; + +export interface Banner { + readonly id: string; + readonly priority: number; + readonly render: (targetDomElement: HTMLDivElement) => (() => void) | void; +} + +export type Banners = Banner[]; + +interface Params { + targetDomElement: HTMLElement; +} + +export class BannersService { + constructor(private readonly params: Params) {} + + public start() { + let uniqueId = 0; + const banners$ = new Rx.BehaviorSubject([]); + + render( + , + this.params.targetDomElement + ); + + return { + /** + * Add a banner that should be rendered at the top of the page along with an optional priority. + */ + add: (renderFn: Banner['render'], priority = 0) => { + const id = `${++uniqueId}`; + banners$.next([...banners$.getValue(), { id, priority, render: renderFn }]); + return id; + }, + + /** + * Remove a banner from the top of the page. + */ + remove: (id: string) => { + banners$.next(banners$.getValue().filter(banner => banner.id !== id)); + }, + + /** + * Replace a banner and its priority. If the render function is not === to the + * previous render function the previous banner will be unmounted and re-rendered. + */ + replace: (id: string, renderFn: Banner['render'], priority = 0) => { + const newId = `${++uniqueId}`; + banners$.next([ + ...banners$.getValue().filter(banner => banner.id !== id), + { id: newId, priority, render: renderFn }, + ]); + return newId; + }, + }; + } + + public stop() { + unmountComponentAtNode(this.params.targetDomElement); + this.params.targetDomElement.textContent = ''; + } +} + +export type BannersStartContract = ReturnType; diff --git a/src/ui/public/notify/banners/global_banner_list.less b/src/core/public/notifications/banners/components/global_banner_item.css similarity index 61% rename from src/ui/public/notify/banners/global_banner_list.less rename to src/core/public/notifications/banners/components/global_banner_item.css index f1ead7231f4f4..2850c314fecf3 100644 --- a/src/ui/public/notify/banners/global_banner_list.less +++ b/src/core/public/notifications/banners/components/global_banner_item.css @@ -1,7 +1,3 @@ -.globalBanner__list { - padding: 16px; -} - .globalBanner__item + .globalBanner__item { margin-top: 16px; } diff --git a/src/core/public/notifications/banners/components/global_banner_item.test.tsx b/src/core/public/notifications/banners/components/global_banner_item.test.tsx new file mode 100644 index 0000000000000..32337cb5a1950 --- /dev/null +++ b/src/core/public/notifications/banners/components/global_banner_item.test.tsx @@ -0,0 +1,84 @@ +/* + * 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 { mount } from 'enzyme'; +import React from 'react'; +import { GlobalBannerItem } from './global_banner_item'; + +it('calls banner render function with div element', () => { + const render = jest.fn(); + + mount( + + ); + + expect(render.mock.calls).toMatchInlineSnapshot(` +Array [ + Array [ +
, + ], +] +`); +}); + +it('calls unrender function before new render function when replaced, calls unrender when unmounted', () => { + expect.assertions(4); + + const unrender = jest.fn(); + const render = jest.fn(() => unrender); + + const unrender2 = jest.fn(); + const render2 = jest.fn(() => { + expect(unrender).toHaveBeenCalledTimes(1); + return unrender2; + }); + + const component = mount( + + ); + + component.setProps({ + banner: { + id: '123', + priority: 123, + render: render2, + }, + }); + + expect(render2).toHaveBeenCalled(); + expect(unrender2).not.toHaveBeenCalled(); + + component.unmount(); + + expect(unrender2).toHaveBeenCalled(); +}); diff --git a/src/core/public/notifications/banners/components/global_banner_item.tsx b/src/core/public/notifications/banners/components/global_banner_item.tsx new file mode 100644 index 0000000000000..80ee711fea95e --- /dev/null +++ b/src/core/public/notifications/banners/components/global_banner_item.tsx @@ -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 React from 'react'; +import { Banner } from '../banners_service'; +import './global_banner_item.css'; + +interface Props { + banner: Banner; +} + +export class GlobalBannerItem extends React.Component { + private readonly ref = React.createRef(); + private unrenderBanner?: () => void; + + public componentDidMount() { + if (!this.ref.current) { + throw new Error(' mounted without ref'); + } + + this.unrenderBanner = this.props.banner.render(this.ref.current) || undefined; + } + + public componentDidUpdate(prevProps: Props) { + if (this.props.banner.render === prevProps.banner.render) { + return; + } + + if (!this.ref.current) { + throw new Error(' updated without ref'); + } + + if (this.unrenderBanner) { + this.unrenderBanner(); + } + + this.unrenderBanner = this.props.banner.render(this.ref.current) || undefined; + } + + public componentWillUnmount() { + if (this.unrenderBanner) { + this.unrenderBanner(); + } + } + + public render() { + return
; + } +} diff --git a/src/core/public/notifications/banners/components/global_banner_list.css b/src/core/public/notifications/banners/components/global_banner_list.css new file mode 100644 index 0000000000000..f186b7be608bd --- /dev/null +++ b/src/core/public/notifications/banners/components/global_banner_list.css @@ -0,0 +1,3 @@ +.globalBanner__list { + padding: 16px; +} diff --git a/src/core/public/notifications/banners/components/global_banner_list.test.tsx b/src/core/public/notifications/banners/components/global_banner_list.test.tsx new file mode 100644 index 0000000000000..f5257717669b2 --- /dev/null +++ b/src/core/public/notifications/banners/components/global_banner_list.test.tsx @@ -0,0 +1,117 @@ +/* + * 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 { shallow } from 'enzyme'; +import React from 'react'; +import { GlobalBannerList } from './global_banner_list'; + +it('renders nothing without banners', () => { + const component = shallow(); + expect(component).toMatchInlineSnapshot(` +
+`); +}); + +it('renders banners in descending priority order', () => { + expect( + shallow( + + ) + ).toMatchInlineSnapshot(` +
+ + +
+`); + + expect( + shallow( + + ) + ).toMatchInlineSnapshot(` +
+ + +
+`); +}); diff --git a/src/core/public/notifications/banners/components/global_banner_list.tsx b/src/core/public/notifications/banners/components/global_banner_list.tsx new file mode 100644 index 0000000000000..169965b283c4a --- /dev/null +++ b/src/core/public/notifications/banners/components/global_banner_list.tsx @@ -0,0 +1,41 @@ +/* + * 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 { Banners } from '../banners_service'; +import { GlobalBannerItem } from './global_banner_item'; +import './global_banner_list.css'; + +interface Props { + banners: Banners; +} + +const sortByPriority = (banners: Banners) => + banners.slice().sort((a, b) => b.priority - a.priority); + +export function GlobalBannerList(props: Props) { + return ( +
+ {sortByPriority(props.banners).map(banner => ( + + ))} +
+ ); +} diff --git a/src/ui/public/notify/banners/global_banner_list.test.js b/src/core/public/notifications/banners/containers/global_banners_container.test.tsx similarity index 51% rename from src/ui/public/notify/banners/global_banner_list.test.js rename to src/core/public/notifications/banners/containers/global_banners_container.test.tsx index 5a008f33c57d9..29d03665770dc 100644 --- a/src/ui/public/notify/banners/global_banner_list.test.js +++ b/src/core/public/notifications/banners/containers/global_banners_container.test.tsx @@ -17,49 +17,26 @@ * under the License. */ +import { shallow } from 'enzyme'; import React from 'react'; -import { render } from 'enzyme'; -import { GlobalBannerList } from './global_banner_list'; +import * as Rx from 'rxjs'; +import { GlobalBannersContainer } from './global_banners_container'; -describe('GlobalBannerList', () => { - - test('is rendered', () => { - const component = render( - - ); - - expect(component) - .toMatchSnapshot(); - - }); - - describe('props', () => { - - describe('banners', () => { - - test('is rendered', () => { - const banners = [{ - id: 'a', - component: 'a component', - priority: 1, - }, { - 'data-test-subj': 'b', - id: 'b', - component: 'b good', - }]; - - const component = render( - - ); - - expect(component) - .toMatchSnapshot(); - }); - - }); - - }); +it('renders nothing without banners', () => { + const component = shallow(); + expect(component).toMatchInlineSnapshot(`""`); +}); +it('passes banners to the GlobalBannerList', () => { + expect(shallow()) + .toMatchInlineSnapshot(` + +`); }); diff --git a/src/core/public/notifications/banners/containers/global_banners_container.tsx b/src/core/public/notifications/banners/containers/global_banners_container.tsx new file mode 100644 index 0000000000000..61e5f73738db6 --- /dev/null +++ b/src/core/public/notifications/banners/containers/global_banners_container.tsx @@ -0,0 +1,69 @@ +/* + * 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 * as Rx from 'rxjs'; + +import { Banners } from '../banners_service'; +import { GlobalBannerList } from '../components/global_banner_list'; + +interface Props { + banners$: Rx.Observable; +} + +interface State { + error?: Error; + banners?: Banners; +} + +export class GlobalBannersContainer extends React.Component { + public state: State = {}; + private subscription?: Rx.Subscription; + + public componentDidMount() { + this.subscription = this.props.banners$.subscribe({ + next: banners => { + this.setState({ banners }); + }, + error: error => { + this.setState({ error }); + }, + }); + } + + public componentWillUnmount() { + if (this.subscription) { + this.subscription.unsubscribe(); + } + } + + public render() { + const { error, banners } = this.state; + + if (error) { + throw error; + } + + if (!banners || !banners.length) { + return null; + } + + return ; + } +} diff --git a/src/ui/public/notify/banners/index.js b/src/core/public/notifications/banners/index.ts similarity index 89% rename from src/ui/public/notify/banners/index.js rename to src/core/public/notifications/banners/index.ts index b4d2a11f61003..81f4e0e751821 100644 --- a/src/ui/public/notify/banners/index.js +++ b/src/core/public/notifications/banners/index.ts @@ -17,5 +17,4 @@ * under the License. */ -export { GlobalBannerList } from './global_banner_list'; -export { banners } from './banners'; \ No newline at end of file +export { Banners, Banner, BannersService, BannersStartContract } from './banners_service'; diff --git a/src/core/public/notifications/notifications_service.test.ts b/src/core/public/notifications/notifications_service.test.ts new file mode 100644 index 0000000000000..8ef33e2700042 --- /dev/null +++ b/src/core/public/notifications/notifications_service.test.ts @@ -0,0 +1,79 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +jest.mock('./toasts/toasts_service'); +jest.mock('./banners/banners_service'); + +import { NotificationsService } from './notifications_service'; + +const { ToastsService } = require.requireMock('./toasts/toasts_service'); +const { BannersService } = require.requireMock('./banners/banners_service'); + +beforeEach(() => { + jest.clearAllMocks(); +}); + +it('creates toasts and banner services, passing unique containers to each which are mounted in start and unmounted after stop()', () => { + expect.assertions(16); + const targetDomElement = document.createElement('div'); + const notifications = new NotificationsService({ + targetDomElement, + }); + + expect(ToastsService).toHaveBeenCalledTimes(1); + expect(ToastsService).toHaveBeenCalledWith({ + targetDomElement: expect.any(HTMLDivElement), + }); + + expect(BannersService).toHaveBeenCalledTimes(1); + expect(BannersService).toHaveBeenCalledWith({ + targetDomElement: expect.any(HTMLDivElement), + }); + + const [[{ targetDomElement: toastsContainer }]] = ToastsService.mock.calls; + const [[{ targetDomElement: bannersContainer }]] = BannersService.mock.calls; + expect(toastsContainer).not.toBe(bannersContainer); + expect(toastsContainer.parentElement).toBe(null); + expect(bannersContainer.parentElement).toBe(null); + + ToastsService.prototype.start.mockImplementation(() => { + expect(toastsContainer.parentElement).toBe(targetDomElement); + }); + + ToastsService.prototype.stop.mockImplementation(() => { + expect(toastsContainer.parentElement).toBe(targetDomElement); + }); + + BannersService.prototype.start.mockImplementation(() => { + expect(bannersContainer.parentElement).toBe(targetDomElement); + }); + + BannersService.prototype.stop.mockImplementation(() => { + expect(bannersContainer.parentElement).toBe(targetDomElement); + }); + + notifications.start(); + notifications.stop(); + + expect(BannersService.prototype.stop).toHaveBeenCalledTimes(1); + expect(ToastsService.prototype.stop).toHaveBeenCalledTimes(1); + expect(bannersContainer.parentElement).toBe(null); + expect(toastsContainer.parentElement).toBe(null); + expect(targetDomElement).toMatchInlineSnapshot(`
`); +}); diff --git a/src/core/public/notifications/notifications_service.ts b/src/core/public/notifications/notifications_service.ts index b1f50497aec70..738c9e6549f32 100644 --- a/src/core/public/notifications/notifications_service.ts +++ b/src/core/public/notifications/notifications_service.ts @@ -17,6 +17,7 @@ * under the License. */ +import { BannersService } from './banners'; import { ToastsService } from './toasts'; interface Params { @@ -25,26 +26,36 @@ interface Params { export class NotificationsService { private readonly toasts: ToastsService; + private readonly banners: BannersService; private readonly toastsContainer: HTMLElement; + private readonly bannersContainer: HTMLElement; constructor(private readonly params: Params) { this.toastsContainer = document.createElement('div'); this.toasts = new ToastsService({ targetDomElement: this.toastsContainer, }); + + this.bannersContainer = document.createElement('div'); + this.banners = new BannersService({ + targetDomElement: this.bannersContainer, + }); } public start() { this.params.targetDomElement.appendChild(this.toastsContainer); + const toasts = this.toasts.start(); + + this.params.targetDomElement.appendChild(this.bannersContainer); + const banners = this.banners.start(); - return { - toasts: this.toasts.start(), - }; + return { toasts, banners }; } public stop() { this.toasts.stop(); + this.banners.stop(); this.params.targetDomElement.textContent = ''; } diff --git a/src/ui/public/chrome/directives/kbn_chrome.js b/src/ui/public/chrome/directives/kbn_chrome.js index 31bdf62646a7f..d29defba76e67 100644 --- a/src/ui/public/chrome/directives/kbn_chrome.js +++ b/src/ui/public/chrome/directives/kbn_chrome.js @@ -17,8 +17,6 @@ * under the License. */ -import React from 'react'; -import ReactDOM from 'react-dom'; import $ from 'jquery'; import './kbn_chrome.less'; @@ -27,11 +25,7 @@ import { getUnhashableStatesProvider, unhashUrl, } from '../../state_management/state_hashing'; -import { - notify, - GlobalBannerList, - banners, -} from '../../notify'; +import { notify } from '../../notify'; import { SubUrlRouteFilterProvider } from './sub_url_route_filter'; export function kbnChromeProvider(chrome, internals) { @@ -86,17 +80,6 @@ export function kbnChromeProvider(chrome, internals) { // Notifications $scope.notifList = notify._notifs; - // Non-scope based code (e.g., React) - - // Banners - ReactDOM.render( - , - document.getElementById('globalBannerList') - ); - return chrome; } }; diff --git a/src/ui/public/notify/banners.test.tsx b/src/ui/public/notify/banners.test.tsx new file mode 100644 index 0000000000000..1bb14396cf1ac --- /dev/null +++ b/src/ui/public/notify/banners.test.tsx @@ -0,0 +1,103 @@ +/* + * 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 { __newPlatformInit__, banners } from './banners'; + +const newPlatformBanners = { + add: jest.fn((...args) => ['add', args]), + remove: jest.fn((...args) => ['remove', args]), + replace: jest.fn((...args) => ['replace', args]), +}; + +__newPlatformInit__(newPlatformBanners); + +beforeEach(() => { + jest.clearAllMocks(); +}); + +it('forwards calls to newPlatformBanners.add()', () => { + expect(banners.add({ component: 'foo', priority: 10 })).toMatchInlineSnapshot(` +Array [ + "add", + Array [ + [Function], + 10, + ], +] +`); +}); + +it('forwards calls to newPlatformBanners.remove()', () => { + expect(banners.remove('id')).toMatchInlineSnapshot(` +Array [ + "remove", + Array [ + "id", + ], +] +`); +}); + +it('forwards calls to newPlatformBanners.replace()', () => { + expect(banners.set({ id: 'id', component: 'foo', priority: 100 })).toMatchInlineSnapshot(` +Array [ + "replace", + Array [ + "id", + [Function], + 100, + ], +] +`); +}); + +describe('component is a react element', () => { + it('renders with react, returns unrender function', () => { + banners.add({ component: foo }); + const [[renderFn]] = newPlatformBanners.add.mock.calls; + const div = document.createElement('div'); + const unrender = renderFn(div); + expect(div).toMatchInlineSnapshot(` +
+ + foo + +
+`); + unrender(); + expect(div).toMatchInlineSnapshot(`
`); + }); +}); + +describe('component is a string', () => { + it('renders string with react, returns unrender function', () => { + banners.add({ component: 'foo' }); + const [[renderFn]] = newPlatformBanners.add.mock.calls; + const div = document.createElement('div'); + const unrender = renderFn(div); + expect(div).toMatchInlineSnapshot(` +
+ foo +
+`); + unrender(); + expect(div).toMatchInlineSnapshot(`
`); + }); +}); diff --git a/src/ui/public/notify/banners/banners.js b/src/ui/public/notify/banners.tsx similarity index 51% rename from src/ui/public/notify/banners/banners.js rename to src/ui/public/notify/banners.tsx index 3f8dd08771d43..a9220ad10a8e4 100644 --- a/src/ui/public/notify/banners/banners.js +++ b/src/ui/public/notify/banners.tsx @@ -17,47 +17,36 @@ * under the License. */ -/** - * Banners represents a prioritized list of displayed components. - */ -export class Banners { - - constructor() { - // sorted in descending order (100, 99, 98...) so that higher priorities are in front - this.list = []; - this.uniqueId = 0; - this.onChangeCallback = null; - } +import React from 'react'; +import { render, unmountComponentAtNode } from 'react-dom'; - _changed = () => { - if (this.onChangeCallback) { - this.onChangeCallback(); - } - } +import { BannersStartContract } from '../../../core/public/notifications/banners'; - _remove = id => { - const index = this.list.findIndex(details => details.id === id); +let newPlatformBanners: BannersStartContract; - if (index !== -1) { - this.list.splice(index, 1); - - return true; - } - - return false; +export function __newPlatformInit__(instance: BannersStartContract) { + if (newPlatformBanners) { + throw new Error('ui/notify/banners is already initialized'); } - /** - * Set the {@code callback} to invoke whenever changes are made to the banner list. - * - * Use {@code null} or {@code undefined} to unset it. - * - * @param {Function} callback The callback to use. - */ - onChange = callback => { - this.onChangeCallback = callback; - } + newPlatformBanners = instance; +} + +function renderWithReact(componentOrString: React.ReactElement | string) { + const component = + typeof componentOrString === 'string' ? ( + {componentOrString} + ) : ( + componentOrString + ); + + return (el: HTMLDivElement) => { + render(component, el); + return () => unmountComponentAtNode(el); + }; +} +export const banners = { /** * Add a new banner. * @@ -65,25 +54,9 @@ export class Banners { * @param {Number} priority The optional priority order to display this banner. Higher priority values are shown first. * @return {String} A newly generated ID. This value can be used to remove/replace the banner. */ - add = ({ component, priority = 0 }) => { - const id = `${++this.uniqueId}`; - const bannerDetails = { id, component, priority }; - - // find the lowest priority item to put this banner in front of - const index = this.list.findIndex(details => priority > details.priority); - - if (index !== -1) { - // we found something with a lower priority; so stick it in front of that item - this.list.splice(index, 0, bannerDetails); - } else { - // nothing has a lower priority, so put it at the end - this.list.push(bannerDetails); - } - - this._changed(); - - return id; - } + add(banner: { component: React.ReactElement | string; priority?: number }) { + return newPlatformBanners.add(renderWithReact(banner.component), banner.priority); + }, /** * Remove an existing banner. @@ -91,15 +64,9 @@ export class Banners { * @param {String} id The ID of the banner to remove. * @return {Boolean} {@code true} if the ID is recognized and the banner is removed. {@code false} otherwise. */ - remove = id => { - const removed = this._remove(id); - - if (removed) { - this._changed(); - } - - return removed; - } + remove(id: string) { + return newPlatformBanners.remove(id); + }, /** * Replace an existing banner by removing it, if it exists, and adding a new one in its place. @@ -112,15 +79,11 @@ export class Banners { * @param {Number} priority The optional priority order to display this banner. Higher priority values are shown first. * @return {String} A newly generated ID. This value can be used to remove/replace the banner. */ - set = ({ component, id, priority = 0 }) => { - this._remove(id); - - return this.add({ component, priority }); - } - -} - -/** - * A singleton instance meant to represent all Kibana banners. - */ -export const banners = new Banners(); + set(banner: { id: string; component: React.ReactElement | string; priority?: number }) { + return newPlatformBanners.replace( + banner.id, + renderWithReact(banner.component), + banner.priority + ); + }, +}; diff --git a/src/ui/public/notify/banners/BANNERS.md b/src/ui/public/notify/banners/BANNERS.md deleted file mode 100644 index fc6bddc3aa4ed..0000000000000 --- a/src/ui/public/notify/banners/BANNERS.md +++ /dev/null @@ -1,360 +0,0 @@ -# Banners - -Use this service to surface banners at the top of the screen. The expectation is that the banner will used an -`` to render, but that is not a requirement. See [the EUI docs](https://elastic.github.io/eui/) for -more information on banners and their role within the UI. - -Banners should be considered with respect to their lifecycle. Most banners are best served by using the `add` and -`remove` functions. - -## Importing the module - -```js -import { banners } from 'ui/notify'; -``` - -## Interface - -There are three methods defined to manipulate the list of banners: `add`, `set`, and `remove`. A fourth method, -`onChange` exists to listen to changes made via `add`, `set`, and `remove`. - -### `add()` - -This is the preferred way to add banners because it implies the best usage of the banner: added once during a page's -lifecycle. For other usages, consider *not* using a banner. - -#### Syntax - -```js -const bannerId = banners.add({ - // required: - component, - // optional: - priority, -}); -``` - -##### Parameters - -| Field | Type | Description | -|-------|------|-------------| -| `component` | Any | The value displayed as the banner. | -| `priority` | Number | Optional priority, which defaults to `0` used to place the banner. | - -To add a banner, you only need to define the `component` field. - -The `priority` sorts in descending order. Items sharing the same priority are sorted from oldest to newest. For example: - -```js -const banner1 = banners.add({ component: }); -const banner2 = banners.add({ component: , priority: 0 }); -const banner3 = banners.add({ component: , priority: 1 }); -``` - -That would be displayed as: - -``` -[ fake3 ] -[ fake1 ] -[ fake2 ] -``` - -##### Returns - -| Type | Description | -|------|-------------| -| String | A newly generated ID. | - -#### Example - -This example includes buttons that allow the user to remove the banner. In some cases, you may not want any buttons -and in other cases you will want an action to proceed the banner's removal (e.g., apply an Advanced Setting). - -This makes the most sense to use when a banner is added at the beginning of the page life cycle and not expected to -be touched, except by its own buttons triggering an action or navigating away. - -```js -const bannerId = banners.add({ - component: ( - - - - banners.remove(bannerId)} - > - Dismiss - - - - window.alert('Do Something Else')} - > - Do Something Else - - - - - ), -}); -``` - -### `remove()` - -Unlike toast notifications, banners stick around until they are explicitly removed. Using the `add` example above,you can remove it by calling `remove`. - -Note: They will stick around as long as the scope is remembered by whatever set it; navigating away won't remove it -unless the scope is forgotten (e.g., when the "app" changes)! - -#### Syntax - -```js -const removed = banners.remove(bannerId); -``` - -##### Parameters - -| Field | Type | Description | -|-------|------|-------------| -| `id` | String | ID of a banner. | - -##### Returns - -| Type | Description | -|------|-------------| -| Boolean | `true` if the ID was recognized and the banner was removed. `false` otherwise. | - -#### Example - -To remove a banner, you need to pass the `id` of the banner. - -```js -if (banners.remove(bannerId)) { - // removed; otherwise it didn't exist (maybe it was already removed) -} -``` - -#### Scheduled removal - -Like toast notifications do automatically, you can have a banner automatically removed after a set of time, by -setting a timer: - -```js -setTimeout(() => banners.remove(bannerId), 15000); -``` - -Note: It is safe to remove a banner more than once as unknown IDs will be ignored. - -### `set()` - -Banners can be replaced once added by supplying their `id`. If one is supplied, then the ID will be used to replace -any banner with the same ID and a **new** `id` will be returned. - -You should only consider using `set` when the banner is manipulated frequently in the lifecycle of the page, where -maintaining the banner's `id` can be a burden. It is easier to allow `banners` to create the ID for you in most -situations where a banner is useful (e.g., set once), which safely avoids any chance to have an ID-based collision, -which happens automatically with `add`. - -Usage of `set` can imply that your use case is abusing the banner system. - -Note: `set` will only trigger the callback once for both the implicit remove and add operation. - -#### Syntax - -```js -const id = banners.set({ - // required: - component, - // optional: - id, - priority, -}); -``` - -##### Parameters - -| Field | Type | Description | -|-------|------|-------------| -| `component` | Any | The value displayed as the banner. | -| `id` | String | Optional ID used to remove an existing banner. | -| `priority` | Number | Optional priority, which defaults to `0` used to place the banner. | - -The `id` is optional because it follows the same semantics as the `remove` method: unknown IDs are ignored. This -is useful when first creating a banner so that you do not have to call `add` instead. - -##### Returns - -| Type | Description | -|------|-------------| -| String | A newly generated ID. | - -#### Example - -This example does not include any way for the user to clear the banner directly. Instead, it is cleared based on -time. Related to it being cleared by time, it can also reappear within the same page life cycle by navigating between -different paths that need it displayed. Instead of adding a new banner for every navigation, you should replace any -existing banner. - -```js -let bannerId; -let timeoutId; - -function displayBanner() { - clearTimeout(timeoutId); - - bannerId = banners.set({ - id: bannerId, // the first time it will be undefined, but reused as long as this is in the same lifecycle - component: ( - - ) - }); - - // hide the message after the user has had a chance to acknowledge it -- so it doesn't permanently stick around - banner.timeoutId = setTimeout(() => { - banners.remove(bannerId); - timeoutId = undefined; - }, 6000); -} -``` - -### `onChange()` - -For React components that intend to display the banners, it is not enough to simply `render` the `banners.list` -values. Because they can change after being rendered, the React component that renders the list must be alerted -to changes to the list. - -#### Syntax - -```js -// inside your React component -banners.onChange(() => this.forceUpdate()); -``` - -##### Parameters - -| Field | Type | Description | -|-------|------|-------------| -| `callback` | Function | The function to invoke whenever the internal banner list is changed. | - -Every new `callback` replaces the previous callback. So calling this with `null` or `undefined` will unset the -callback. - -##### Returns - -Nothing. - -#### Example - -This can be used inside of a React component to trigger a re-`render` of the banners. - -```js -import { GlobalBannerList } from 'ui/notify'; - - -``` - -### `list` - -For React components that intend to display the banners, it is not enough to simply `render` the `banners.list` -values. Because they can change after being rendered, the React component that renders the list must be alerted -to changes to the list. - -#### Syntax - -```js - -``` - -##### Returns - -| Type | Description | -|------|-------------| -| Array | The array of banner objects. | - -Banner objects are sorted in descending order based on their `priority`, in the form: - -```js -{ - id: 'banner-123', - component: , - priority: 12, -} -``` - -| Field | Type | Description | -|-------|------|-------------| -| `component` | Any | The value displayed as the banner. | -| `id` | String | The ID of the banner, which can be used as a React "key". | -| `priority` | Number | The priority of the banner. | - -#### Example - -This can be used to supply the banners to the `GlobalBannerList` React component (which is done for you). - -```js -import { GlobalBannerList } from 'ui/notify'; - - -``` - -## Use in functional tests - -Functional tests are commonly used to verify that an action yielded a successful outcome. You can place a -`data-test-subj` attribute on the banner and use it to check if the banner exists inside of your functional test. -This acts as a proxy for verifying the successful outcome. Any unrecognized field will be added as a property of the -containing element. - -```js -banners.add({ - component: ( - - ), - data-test-subj: 'my-tested-banner', -}); -``` - -This will apply the `data-test-subj` to the element containing the `component`, so the inner HTML of that element -will exclusively be the specified `component`. - -Given that `component` is expected to be a React component, you could also add the `data-test-subj` directly to it: - -```js -banners.add({ - component: ( - - ), -}); -``` \ No newline at end of file diff --git a/src/ui/public/notify/banners/__snapshots__/global_banner_list.test.js.snap b/src/ui/public/notify/banners/__snapshots__/global_banner_list.test.js.snap deleted file mode 100644 index e66ff83886adb..0000000000000 --- a/src/ui/public/notify/banners/__snapshots__/global_banner_list.test.js.snap +++ /dev/null @@ -1,22 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`GlobalBannerList is rendered 1`] = `null`; - -exports[`GlobalBannerList props banners is rendered 1`] = ` -
-
- a component -
-
- b good -
-
-`; diff --git a/src/ui/public/notify/banners/banners.test.js b/src/ui/public/notify/banners/banners.test.js deleted file mode 100644 index 69b046bcb4d8f..0000000000000 --- a/src/ui/public/notify/banners/banners.test.js +++ /dev/null @@ -1,161 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import sinon from 'sinon'; - -import { - Banners, -} from './banners'; - -describe('Banners', () => { - - describe('interface', () => { - let banners; - - beforeEach(() => { - banners = new Banners(); - }); - - describe('onChange method', () => { - - test('callback is called when a banner is added', () => { - const onChangeSpy = sinon.spy(); - banners.onChange(onChangeSpy); - banners.add({ component: 'bruce-banner' }); - expect(onChangeSpy.callCount).toBe(1); - }); - - test('callback is called when a banner is removed', () => { - const onChangeSpy = sinon.spy(); - banners.onChange(onChangeSpy); - banners.remove(banners.add({ component: 'bruce-banner' })); - expect(onChangeSpy.callCount).toBe(2); - }); - - test('callback is not called when remove is ignored', () => { - const onChangeSpy = sinon.spy(); - banners.onChange(onChangeSpy); - banners.remove('hulk'); // should not invoke callback - expect(onChangeSpy.callCount).toBe(0); - }); - - test('callback is called once when banner is replaced', () => { - const onChangeSpy = sinon.spy(); - banners.onChange(onChangeSpy); - const addBannerId = banners.add({ component: 'bruce-banner' }); - banners.set({ id: addBannerId, component: 'hulk' }); - expect(onChangeSpy.callCount).toBe(2); - }); - - }); - - describe('add method', () => { - - test('adds a banner', () => { - const id = banners.add({}); - expect(banners.list.length).toBe(1); - expect(id).toEqual(expect.stringMatching(/^\d+$/)); - }); - - test('adds a banner and ignores an ID property', () => { - const bannerId = banners.add({ id: 'bruce-banner' }); - expect(banners.list[0].id).toBe(bannerId); - expect(bannerId).not.toBe('bruce-banner'); - }); - - test('sorts banners based on priority', () => { - const test0 = banners.add({ }); - // the fact that it was set explicitly is irrelevant; that it was added second means it should be after test0 - const test0Explicit = banners.add({ priority: 0 }); - const test1 = banners.add({ priority: 1 }); - const testMinus1 = banners.add({ priority: -1 }); - const test1000 = banners.add({ priority: 1000 }); - - expect(banners.list.length).toBe(5); - expect(banners.list[0].id).toBe(test1000); - expect(banners.list[1].id).toBe(test1); - expect(banners.list[2].id).toBe(test0); - expect(banners.list[3].id).toBe(test0Explicit); - expect(banners.list[4].id).toBe(testMinus1); - }); - - }); - - describe('remove method', () => { - - test('removes a banner', () => { - const bannerId = banners.add({ component: 'bruce-banner' }); - banners.remove(bannerId); - expect(banners.list.length).toBe(0); - }); - - test('ignores unknown id', () => { - banners.add({ component: 'bruce-banner' }); - banners.remove('hulk'); - expect(banners.list.length).toBe(1); - }); - - }); - - describe('set method', () => { - - test('replaces banners', () => { - const addBannerId = banners.add({ component: 'bruce-banner' }); - const setBannerId = banners.set({ id: addBannerId, component: 'hulk' }); - - expect(banners.list.length).toBe(1); - expect(banners.list[0].component).toBe('hulk'); - expect(banners.list[0].id).toBe(setBannerId); - expect(addBannerId).not.toBe(setBannerId); - }); - - test('ignores unknown id', () => { - const id = banners.set({ id: 'fake', component: 'hulk' }); - - expect(banners.list.length).toBe(1); - expect(banners.list[0].component).toBe('hulk'); - expect(banners.list[0].id).toBe(id); - }); - - test('replaces a banner with the same ID property', () => { - const test0 = banners.add({ }); - const test0Explicit = banners.add({ priority: 0 }); - let test1 = banners.add({ priority: 1, component: 'old' }); - const testMinus1 = banners.add({ priority: -1 }); - let test1000 = banners.add({ priority: 1000, component: 'old' }); - - // change one with the same priority - test1 = banners.set({ id: test1, priority: 1, component: 'new' }); - // change one with a different priority - test1000 = banners.set({ id: test1000, priority: 1, component: 'new' }); - - expect(banners.list.length).toBe(5); - expect(banners.list[0].id).toBe(test1); - expect(banners.list[0].component).toBe('new'); - expect(banners.list[1].id).toBe(test1000); // priority became 1, so it goes after the other "1" - expect(banners.list[1].component).toBe('new'); - expect(banners.list[2].id).toBe(test0); - expect(banners.list[3].id).toBe(test0Explicit); - expect(banners.list[4].id).toBe(testMinus1); - }); - - }); - - }); -}); diff --git a/src/ui/public/notify/banners/global_banner_list.js b/src/ui/public/notify/banners/global_banner_list.js deleted file mode 100644 index ca60ae373853f..0000000000000 --- a/src/ui/public/notify/banners/global_banner_list.js +++ /dev/null @@ -1,85 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import React, { Component } from 'react'; -import PropTypes from 'prop-types'; - -import './global_banner_list.less'; - -/** - * GlobalBannerList is a list of "banners". A banner something that is displayed at the top of Kibana that may or may not disappear. - * - * Whether or not a banner can be closed is completely up to the author of the banner. Some banners make sense to be static, such as - * banners meant to indicate the sensitivity (e.g., classification) of the information being represented. - * - * Banners are currently expected to be instances, but that is not required. - * - * @param {Array} banners The array of banners represented by objects in the form of { id, component }. - */ -export class GlobalBannerList extends Component { - - static propTypes = { - banners: PropTypes.array, - subscribe: PropTypes.func, - }; - - static defaultProps = { - banners: [], - }; - - constructor(props) { - super(props); - - if (this.props.subscribe) { - this.props.subscribe(() => this.forceUpdate()); - } - } - - render() { - if (this.props.banners.length === 0) { - return null; - } - - const flexBanners = this.props.banners.map(banner => { - const { - id, - component, - priority, - ...rest - } = banner; - - return ( -
- { component } -
- ); - }); - - return ( -
- {flexBanners} -
- ); - } -} diff --git a/src/ui/public/notify/index.js b/src/ui/public/notify/index.js index 078b4807430dd..c3ac93f89e8d8 100644 --- a/src/ui/public/notify/index.js +++ b/src/ui/public/notify/index.js @@ -21,5 +21,5 @@ export { notify } from './notify'; export { Notifier } from './notifier'; export { fatalError, addFatalErrorCallback } from './fatal_error'; export { toastNotifications } from './toasts'; -export { GlobalBannerList, banners } from './banners'; +export { banners } from './banners'; export { addAppRedirectMessageToUrl, showAppRedirectNotification } from './app_redirect';