[7.x] Move ui/notify banners to New Platform (#43610) (#45956)

* Move ui/notify banners to New Platform (#43610)

* Fix Lens component
This commit is contained in:
Josh Dover 2019-09-18 10:38:25 -05:00 committed by GitHub
parent c78d976dc2
commit 7c29e26926
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
58 changed files with 1244 additions and 576 deletions

View file

@ -63,6 +63,7 @@ The plugin integrates with the core system via lifecycle events: `setup`<!-- -->
| [LegacyNavLink](./kibana-plugin-public.legacynavlink.md) | |
| [NotificationsSetup](./kibana-plugin-public.notificationssetup.md) | |
| [NotificationsStart](./kibana-plugin-public.notificationsstart.md) | |
| [OverlayBannersStart](./kibana-plugin-public.overlaybannersstart.md) | |
| [OverlayRef](./kibana-plugin-public.overlayref.md) | |
| [OverlayStart](./kibana-plugin-public.overlaystart.md) | |
| [Plugin](./kibana-plugin-public.plugin.md) | The interface that should be returned by a <code>PluginInitializer</code>. |
@ -95,6 +96,8 @@ The plugin integrates with the core system via lifecycle events: `setup`<!-- -->
| [HttpStart](./kibana-plugin-public.httpstart.md) | |
| [IContextHandler](./kibana-plugin-public.icontexthandler.md) | A function registered by a plugin to perform some action. |
| [IContextProvider](./kibana-plugin-public.icontextprovider.md) | A function that returns a context value for a specific key of given context type. |
| [OverlayBannerMount](./kibana-plugin-public.overlaybannermount.md) | A function that will mount the banner inside the provided element. |
| [OverlayBannerUnmount](./kibana-plugin-public.overlaybannerunmount.md) | A function that will unmount the banner from the element. |
| [PluginInitializer](./kibana-plugin-public.plugininitializer.md) | The <code>plugin</code> export at the root of a plugin's <code>public</code> directory should conform to this interface. |
| [PluginOpaqueId](./kibana-plugin-public.pluginopaqueid.md) | |
| [RecursiveReadonly](./kibana-plugin-public.recursivereadonly.md) | |

View file

@ -0,0 +1,13 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-public](./kibana-plugin-public.md) &gt; [OverlayBannerMount](./kibana-plugin-public.overlaybannermount.md)
## OverlayBannerMount type
A function that will mount the banner inside the provided element.
<b>Signature:</b>
```typescript
export declare type OverlayBannerMount = (element: HTMLElement) => OverlayBannerUnmount;
```

View file

@ -0,0 +1,27 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-public](./kibana-plugin-public.md) &gt; [OverlayBannersStart](./kibana-plugin-public.overlaybannersstart.md) &gt; [add](./kibana-plugin-public.overlaybannersstart.add.md)
## OverlayBannersStart.add() method
Add a new banner
<b>Signature:</b>
```typescript
add(mount: OverlayBannerMount, priority?: number): string;
```
## Parameters
| Parameter | Type | Description |
| --- | --- | --- |
| mount | <code>OverlayBannerMount</code> | |
| priority | <code>number</code> | |
<b>Returns:</b>
`string`
a unique identifier for the given banner to be used with [OverlayBannersStart.remove()](./kibana-plugin-public.overlaybannersstart.remove.md) and [OverlayBannersStart.replace()](./kibana-plugin-public.overlaybannersstart.replace.md)

View file

@ -0,0 +1,15 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-public](./kibana-plugin-public.md) &gt; [OverlayBannersStart](./kibana-plugin-public.overlaybannersstart.md) &gt; [getComponent](./kibana-plugin-public.overlaybannersstart.getcomponent.md)
## OverlayBannersStart.getComponent() method
<b>Signature:</b>
```typescript
getComponent(): JSX.Element;
```
<b>Returns:</b>
`JSX.Element`

View file

@ -0,0 +1,22 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-public](./kibana-plugin-public.md) &gt; [OverlayBannersStart](./kibana-plugin-public.overlaybannersstart.md)
## OverlayBannersStart interface
<b>Signature:</b>
```typescript
export interface OverlayBannersStart
```
## Methods
| Method | Description |
| --- | --- |
| [add(mount, priority)](./kibana-plugin-public.overlaybannersstart.add.md) | Add a new banner |
| [getComponent()](./kibana-plugin-public.overlaybannersstart.getcomponent.md) | |
| [remove(id)](./kibana-plugin-public.overlaybannersstart.remove.md) | Remove a banner |
| [replace(id, mount, priority)](./kibana-plugin-public.overlaybannersstart.replace.md) | Replace a banner in place |

View file

@ -0,0 +1,26 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-public](./kibana-plugin-public.md) &gt; [OverlayBannersStart](./kibana-plugin-public.overlaybannersstart.md) &gt; [remove](./kibana-plugin-public.overlaybannersstart.remove.md)
## OverlayBannersStart.remove() method
Remove a banner
<b>Signature:</b>
```typescript
remove(id: string): boolean;
```
## Parameters
| Parameter | Type | Description |
| --- | --- | --- |
| id | <code>string</code> | |
<b>Returns:</b>
`boolean`
if the banner was found or not

View file

@ -0,0 +1,28 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-public](./kibana-plugin-public.md) &gt; [OverlayBannersStart](./kibana-plugin-public.overlaybannersstart.md) &gt; [replace](./kibana-plugin-public.overlaybannersstart.replace.md)
## OverlayBannersStart.replace() method
Replace a banner in place
<b>Signature:</b>
```typescript
replace(id: string | undefined, mount: OverlayBannerMount, priority?: number): string;
```
## Parameters
| Parameter | Type | Description |
| --- | --- | --- |
| id | <code>string &#124; undefined</code> | |
| mount | <code>OverlayBannerMount</code> | |
| priority | <code>number</code> | |
<b>Returns:</b>
`string`
a new identifier for the given banner to be used with [OverlayBannersStart.remove()](./kibana-plugin-public.overlaybannersstart.remove.md) and [OverlayBannersStart.replace()](./kibana-plugin-public.overlaybannersstart.replace.md)

View file

@ -0,0 +1,13 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-public](./kibana-plugin-public.md) &gt; [OverlayBannerUnmount](./kibana-plugin-public.overlaybannerunmount.md)
## OverlayBannerUnmount type
A function that will unmount the banner from the element.
<b>Signature:</b>
```typescript
export declare type OverlayBannerUnmount = () => void;
```

View file

@ -0,0 +1,13 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-public](./kibana-plugin-public.md) &gt; [OverlayStart](./kibana-plugin-public.overlaystart.md) &gt; [banners](./kibana-plugin-public.overlaystart.banners.md)
## OverlayStart.banners property
[OverlayBannersStart](./kibana-plugin-public.overlaybannersstart.md)
<b>Signature:</b>
```typescript
banners: OverlayBannersStart;
```

View file

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

View file

@ -275,6 +275,7 @@ describe('#start()', () => {
application: expect.any(Object),
chrome: expect.any(Object),
injectedMetadata: expect.any(Object),
overlays: expect.any(Object),
targetDomElement: expect.any(HTMLElement),
});
});

View file

@ -190,6 +190,7 @@ export class CoreSystem {
public async start() {
try {
const injectedMetadata = await this.injectedMetadata.start();
const uiSettings = await this.uiSettings.start();
const docLinks = await this.docLinks.start({ injectedMetadata });
const http = await this.http.start({ injectedMetadata, fatalErrors: this.fatalErrorsSetup });
const savedObjects = await this.savedObjects.start({ http });
@ -208,7 +209,11 @@ export class CoreSystem {
this.rootDomElement.appendChild(notificationsTargetDomElement);
this.rootDomElement.appendChild(overlayTargetDomElement);
const overlays = this.overlay.start({ i18n, targetDomElement: overlayTargetDomElement });
const overlays = this.overlay.start({
i18n,
targetDomElement: overlayTargetDomElement,
uiSettings,
});
const notifications = await this.notifications.start({
i18n,
overlays,
@ -221,7 +226,6 @@ export class CoreSystem {
injectedMetadata,
notifications,
});
const uiSettings = await this.uiSettings.start();
application.registerMountContext(this.coreContext.coreId, 'core', () => ({
application: pick(application, ['capabilities', 'navigateToApp']),
@ -252,6 +256,7 @@ export class CoreSystem {
application,
chrome,
injectedMetadata,
overlays,
targetDomElement: coreUiTargetDomElement,
});

View file

@ -101,6 +101,8 @@ export class FatalErrorsService {
},
};
this.setupGlobalErrorHandlers(fatalErrorsSetup);
return fatalErrorsSetup;
}
@ -123,4 +125,12 @@ export class FatalErrorsService {
container
);
}
private setupGlobalErrorHandlers(fatalErrorsSetup: FatalErrorsSetup) {
if (window.addEventListener) {
window.addEventListener('unhandledrejection', function(e) {
console.log(`Detected an unhandled Promise rejection.\n${e.reason}`); // eslint-disable-line no-console
});
}
}
}

View file

@ -8,3 +8,4 @@
@import '@elastic/eui/src/global_styling/mixins/index';
@import './chrome/index';
@import './overlays/index';

View file

@ -61,7 +61,7 @@ import {
ToastInput,
ToastsApi,
} from './notifications';
import { OverlayRef, OverlayStart } from './overlays';
import { OverlayStart } from './overlays';
import { Plugin, PluginInitializer, PluginInitializerContext, PluginOpaqueId } from './plugins';
import { UiSettingsClient, UiSettingsState, UiSettingsClientContract } from './ui_settings';
import { ApplicationSetup, Capabilities, ApplicationStart } from './application';
@ -108,6 +108,14 @@ export {
HttpBody,
} from './http';
export {
OverlayStart,
OverlayBannerMount,
OverlayBannerUnmount,
OverlayBannersStart,
OverlayRef,
} from './overlays';
/**
* Core services exposed to the `Plugin` setup lifecycle
*
@ -222,8 +230,6 @@ export {
LegacyNavLink,
NotificationsSetup,
NotificationsStart,
OverlayRef,
OverlayStart,
Plugin,
PluginInitializer,
PluginInitializerContext,

View file

@ -0,0 +1 @@
@import './banners_list';

View file

@ -0,0 +1,105 @@
/*
* 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 { act } from 'react-dom/test-utils';
import { mount } from 'enzyme';
import { BannersList } from './banners_list';
import { BehaviorSubject } from 'rxjs';
import { OverlayBanner } from './banners_service';
describe('BannersList', () => {
test('renders null if no banners', () => {
expect(mount(<BannersList banners$={new BehaviorSubject([])} />).html()).toEqual(null);
});
test('renders a list of banners', () => {
const banners$ = new BehaviorSubject<OverlayBanner[]>([
{
id: '1',
mount: (el: HTMLElement) => {
el.innerHTML = '<h1>Hello!</h1>';
return () => (el.innerHTML = '');
},
priority: 0,
},
]);
expect(mount(<BannersList banners$={banners$} />).html()).toMatchInlineSnapshot(
`"<div class=\\"kbnGlobalBannerList\\"><div data-test-priority=\\"0\\" class=\\"kbnGlobalBannerList__item\\"><h1>Hello!</h1></div></div>"`
);
});
test('updates banners', () => {
const unmount = jest.fn();
const banners$ = new BehaviorSubject<OverlayBanner[]>([
{
id: '1',
mount: (el: HTMLElement) => {
el.innerHTML = '<h1>Hello!</h1>';
return unmount;
},
priority: 0,
},
]);
const component = mount(<BannersList banners$={banners$} />);
act(() => {
banners$.next([
{
id: '1',
mount: (el: HTMLElement) => {
el.innerHTML = '<h1>First Banner!</h1>';
return () => (el.innerHTML = '');
},
priority: 1,
},
{
id: '2',
mount: (el: HTMLElement) => {
el.innerHTML = '<h1>Second banner!</h1>';
return () => (el.innerHTML = '');
},
priority: 0,
},
]);
});
// Two new banners should be rendered
expect(component.html()).toMatchInlineSnapshot(
`"<div class=\\"kbnGlobalBannerList\\"><div data-test-priority=\\"1\\" class=\\"kbnGlobalBannerList__item\\"><h1>First Banner!</h1></div><div data-test-priority=\\"0\\" class=\\"kbnGlobalBannerList__item\\"><h1>Second banner!</h1></div></div>"`
);
// Original banner should be unmounted
expect(unmount).toHaveBeenCalled();
});
test('unsubscribe on unmount', () => {
const banners$ = new BehaviorSubject([]);
const subscribe = jest.spyOn(banners$, 'subscribe');
const component = mount(<BannersList banners$={banners$} />);
// Grab the returned subscription and spy its `unsubscribe` method
const subscription = subscribe.mock.results[0].value;
const unsubscribe = jest.spyOn(subscription, 'unsubscribe');
component.unmount();
expect(unsubscribe).toHaveBeenCalled();
});
});

View file

@ -0,0 +1,64 @@
/*
* 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, { useEffect, useRef, useState } from 'react';
import { Observable } from 'rxjs';
import { OverlayBanner } from './banners_service';
interface Props {
banners$: Observable<OverlayBanner[]>;
}
/**
* BannersList 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.
*/
export const BannersList: React.FunctionComponent<Props> = ({ banners$ }) => {
const [banners, setBanners] = useState<OverlayBanner[]>([]);
useEffect(() => {
const subscription = banners$.subscribe(setBanners);
return () => subscription.unsubscribe();
}, [banners$]); // Only un/re-subscribe if the Observable changes
if (banners.length === 0) {
return null;
}
return (
<div className="kbnGlobalBannerList">
{banners.map(banner => (
<BannerItem key={banner.id} banner={banner} />
))}
</div>
);
};
const BannerItem: React.FunctionComponent<{ banner: OverlayBanner }> = ({ banner }) => {
const element = useRef(null);
useEffect(() => banner.mount(element.current!), [banner]); // Only unmount / remount if banner object changed.
return (
<div data-test-priority={banner.priority} className="kbnGlobalBannerList__item" ref={element} />
);
};

View file

@ -0,0 +1,45 @@
/*
* 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 { OverlayBannersStart, OverlayBannersService } from './banners_service';
const createStartContractMock = () => {
const startContract: jest.Mocked<OverlayBannersStart> = {
add: jest.fn(),
remove: jest.fn(),
replace: jest.fn(),
get$: jest.fn(),
getComponent: jest.fn(),
};
return startContract;
};
const createMock = () => {
const mocked: jest.Mocked<PublicMethodsOf<OverlayBannersService>> = {
start: jest.fn(),
stop: jest.fn(),
};
mocked.start.mockReturnValue(createStartContractMock());
return mocked;
};
export const overlayBannersServiceMock = {
create: createMock,
createStartContract: createStartContractMock,
};

View file

@ -0,0 +1,124 @@
/*
* 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 { OverlayBannersService, OverlayBannersStart } from './banners_service';
import { take } from 'rxjs/operators';
import { i18nServiceMock } from '../../i18n/i18n_service.mock';
import { uiSettingsServiceMock } from '../../ui_settings/ui_settings_service.mock';
describe('OverlayBannersService', () => {
let service: OverlayBannersStart;
beforeEach(() => {
service = new OverlayBannersService().start({
i18n: i18nServiceMock.createStartContract(),
uiSettings: uiSettingsServiceMock.createStartContract(),
});
});
const currentBanners = () =>
service
.get$()
.pipe(take(1))
.toPromise();
describe('adding banners', () => {
test('adds a single banner', async () => {
const mount = jest.fn();
const banner = service.add(mount);
expect(await currentBanners()).toEqual([{ id: banner, mount, priority: 0 }]);
});
test('sorts banners by priority', async () => {
const mount1 = jest.fn();
const banner1 = service.add(mount1);
const mount2 = jest.fn();
const banner2 = service.add(mount2, 10);
const mount3 = jest.fn();
const banner3 = service.add(mount3, 5);
expect(await currentBanners()).toEqual([
{ id: banner2, mount: mount2, priority: 10 },
{ id: banner3, mount: mount3, priority: 5 },
{ id: banner1, mount: mount1, priority: 0 },
]);
});
});
describe('removing banners', () => {
test('removes a single banner', async () => {
const mount = jest.fn();
const banner = service.add(mount);
expect(service.remove(banner)).toBe(true);
expect(await currentBanners()).toEqual([]);
expect(service.remove(banner)).toBe(false);
});
test('preserves priority order', async () => {
const mount1 = jest.fn();
const banner1 = service.add(mount1);
const mount2 = jest.fn();
const banner2 = service.add(mount2, 10);
const mount3 = jest.fn();
const banner3 = service.add(mount3, 5);
service.remove(banner2);
expect(await currentBanners()).toEqual([
{ id: banner3, mount: mount3, priority: 5 },
{ id: banner1, mount: mount1, priority: 0 },
]);
});
});
describe('replacing banners', () => {
test('replaces mount function', async () => {
const mount1 = jest.fn();
const banner = service.add(mount1);
const mount2 = jest.fn();
const updatedBanner = service.replace(banner, mount2);
expect(await currentBanners()).toEqual([{ id: updatedBanner, mount: mount2, priority: 0 }]);
});
test('updates priority', async () => {
const mount1 = jest.fn();
const banner1 = service.add(mount1);
const mount2 = jest.fn();
const banner2 = service.add(mount2, 10);
const mount3 = jest.fn();
const banner3 = service.add(mount3, 5);
const updatedBanner2 = service.replace(banner2, mount2, -10);
expect(await currentBanners()).toEqual([
{ id: banner3, mount: mount3, priority: 5 },
{ id: banner1, mount: mount1, priority: 0 },
{ id: updatedBanner2, mount: mount2, priority: -10 },
]);
});
test('can be replaced multiple times using new id', async () => {
const mount1 = jest.fn();
const banner = service.add(mount1);
const mount2 = jest.fn();
const updatedBanner = service.replace(banner, mount2);
expect(banner).not.toEqual(updatedBanner);
// Make sure we can use the new id to replace again
const mount3 = jest.fn();
const updatedBanner2 = service.replace(updatedBanner, mount3);
expect(updatedBanner2).not.toEqual(updatedBanner);
// Should only be a single banner
expect(await currentBanners()).toEqual([{ id: updatedBanner2, mount: mount3, priority: 0 }]);
});
});
});

View file

@ -0,0 +1,148 @@
/*
* 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 { BehaviorSubject, Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { PriorityMap } from './priority_map';
import { BannersList } from './banners_list';
import { UiSettingsClientContract } from '../../ui_settings';
import { I18nStart } from '../../i18n';
import { UserBannerService } from './user_banner_service';
/**
* A function that will unmount the banner from the element.
* @public
*/
export type OverlayBannerUnmount = () => void;
/**
* A function that will mount the banner inside the provided element.
* @param element an element to render into
* @returns a {@link OverlayBannerUnmount}
* @public
*/
export type OverlayBannerMount = (element: HTMLElement) => OverlayBannerUnmount;
/** @public */
export interface OverlayBannersStart {
/**
* Add a new banner
*
* @param mount {@link OverlayBannerMount}
* @param priority optional priority order to display this banner. Higher priority values are shown first.
* @returns a unique identifier for the given banner to be used with {@link OverlayBannersStart.remove} and
* {@link OverlayBannersStart.replace}
*/
add(mount: OverlayBannerMount, priority?: number): string;
/**
* Remove a banner
*
* @param id the unique identifier for the banner returned by {@link OverlayBannersStart.add}
* @returns if the banner was found or not
*/
remove(id: string): boolean;
/**
* Replace a banner in place
*
* @param id the unique identifier for the banner returned by {@link OverlayBannersStart.add}
* @param mount {@link OverlayBannerMount}
* @param priority optional priority order to display this banner. Higher priority values are shown first.
* @returns a new identifier for the given banner to be used with {@link OverlayBannersStart.remove} and
* {@link OverlayBannersStart.replace}
*/
replace(id: string | undefined, mount: OverlayBannerMount, priority?: number): string;
/** @internal */
get$(): Observable<OverlayBanner[]>;
getComponent(): JSX.Element;
}
/** @internal */
export interface OverlayBanner {
readonly id: string;
readonly mount: OverlayBannerMount;
readonly priority: number;
}
interface StartDeps {
i18n: I18nStart;
uiSettings: UiSettingsClientContract;
}
/** @internal */
export class OverlayBannersService {
private readonly userBanner = new UserBannerService();
public start({ i18n, uiSettings }: StartDeps): OverlayBannersStart {
let uniqueId = 0;
const genId = () => `${uniqueId++}`;
const banners$ = new BehaviorSubject(new PriorityMap<string, OverlayBanner>());
const service: OverlayBannersStart = {
add: (mount, priority = 0) => {
const id = genId();
const nextBanner: OverlayBanner = { id, mount, priority };
banners$.next(banners$.value.add(id, nextBanner));
return id;
},
remove: (id: string) => {
if (!banners$.value.has(id)) {
return false;
}
banners$.next(banners$.value.remove(id));
return true;
},
replace(id: string | undefined, mount: OverlayBannerMount, priority = 0) {
if (!id || !banners$.value.has(id)) {
return this.add(mount, priority);
}
const nextId = genId();
const nextBanner = { id: nextId, mount, priority };
banners$.next(banners$.value.remove(id).add(nextId, nextBanner));
return nextId;
},
get$() {
return banners$.pipe(map(bannerMap => [...bannerMap.values()]));
},
getComponent() {
return <BannersList banners$={this.get$()} />;
},
};
this.userBanner.start({ banners: service, i18n, uiSettings });
return service;
}
public stop() {
this.userBanner.stop();
}
}

View file

@ -0,0 +1,25 @@
/*
* 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 {
OverlayBannerMount,
OverlayBannerUnmount,
OverlayBannersStart,
OverlayBannersService,
} from './banners_service';

View file

@ -0,0 +1,59 @@
/*
* 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 { PriorityMap } from './priority_map';
interface MyPrioType {
readonly priority: number;
}
describe('PriorityMap', () => {
it('sorts added keys by priority', () => {
let map = new PriorityMap<string, MyPrioType>();
map = map.add('a', { priority: 1 });
map = map.add('b', { priority: 3 });
map = map.add('c', { priority: 2 });
expect([...map]).toEqual([
['b', { priority: 3 }],
['c', { priority: 2 }],
['a', { priority: 1 }],
]);
});
it('retains sort order when keys are removed', () => {
let map = new PriorityMap<string, MyPrioType>();
map = map.add('a', { priority: 1 });
map = map.add('b', { priority: 3 });
map = map.add('c', { priority: 2 });
map = map.remove('c');
expect([...map]).toEqual([['b', { priority: 3 }], ['a', { priority: 1 }]]);
});
it('adds duplicate priorities to end', () => {
let map = new PriorityMap<string, MyPrioType>();
map = map.add('a', { priority: 1 });
map = map.add('b', { priority: 1 });
map = map.add('c', { priority: 1 });
expect([...map]).toEqual([
['a', { priority: 1 }],
['b', { priority: 1 }],
['c', { priority: 1 }],
]);
});
});

View file

@ -0,0 +1,61 @@
/*
* 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 { sortBy } from 'lodash';
interface PriorityValue {
readonly priority: number;
}
/**
* Immutable map that ensures entries are always in descending order based on
* the values 'priority' property.
*/
export class PriorityMap<K, V extends PriorityValue> implements Iterable<[K, V]> {
private readonly map: ReadonlyMap<K, V>;
constructor(map?: ReadonlyMap<K, V>) {
this.map = map ? new Map(sortEntries(map)) : new Map();
}
public add(key: K, value: V) {
return new PriorityMap<K, V>(new Map<K, V>(sortEntries([...this.map, [key, value]])));
}
public remove(key: K) {
return new PriorityMap<K, V>(
new Map<K, V>([...this.map].filter(([itemKey]) => itemKey !== key))
);
}
public has(key: K) {
return this.map.has(key);
}
public [Symbol.iterator]() {
return this.map[Symbol.iterator]();
}
public values() {
return this.map.values();
}
}
const sortEntries = <K, V extends PriorityValue>(map: Iterable<[K, V]>): Iterable<[K, V]> =>
sortBy([...map] as Array<[K, V]>, '1.priority').reverse();

View file

@ -0,0 +1,132 @@
/*
* 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 { uiSettingsServiceMock } from '../../ui_settings/ui_settings_service.mock';
import { UserBannerService } from './user_banner_service';
import { overlayBannersServiceMock } from './banners_service.mock';
import { i18nServiceMock } from '../../i18n/i18n_service.mock';
import { Subject } from 'rxjs';
describe('OverlayBannersService', () => {
let bannerContent: string | undefined;
let service: UserBannerService;
let uiSettings: ReturnType<typeof uiSettingsServiceMock.createStartContract>;
let banners: ReturnType<typeof overlayBannersServiceMock.createStartContract>;
const startService = (content?: string) => {
bannerContent = content;
uiSettings = uiSettingsServiceMock.createStartContract();
uiSettings.get.mockImplementation((key: string) => {
if (key === 'notifications:banner') {
return bannerContent;
} else if (key === 'notifications:lifetime:banner') {
return 1000;
}
});
banners = overlayBannersServiceMock.createStartContract();
service = new UserBannerService();
service.start({
banners,
i18n: i18nServiceMock.createStartContract(),
uiSettings,
});
};
afterEach(() => service.stop());
it('does not add banner if setting is unspecified', () => {
startService();
expect(banners.replace).not.toHaveBeenCalled();
});
it('adds banner if setting is specified', () => {
startService('testing banner!');
expect(banners.replace).toHaveBeenCalled();
const mount = banners.replace.mock.calls[0][1];
const div = document.createElement('div');
mount(div);
expect(div.querySelector('.euiCallOut')).toBeInstanceOf(HTMLDivElement);
});
it('dismisses banner after timeout', async () => {
jest.useFakeTimers();
startService('testing banner!');
expect(banners.remove).not.toHaveBeenCalled();
// Must mount in order for timer to start
const mount = banners.replace.mock.calls[0][1];
mount(document.createElement('div'));
// Process all timers
jest.runAllTimers();
expect(banners.remove).toHaveBeenCalled();
});
it('updates banner on change', () => {
startService();
expect(banners.replace).toHaveBeenCalledTimes(0);
const update$ = (uiSettings.getUpdate$() as any) as Subject<{
key: string;
}>;
bannerContent = 'update 1';
update$.next({ key: 'notifications:banner' });
expect(banners.replace).toHaveBeenCalledTimes(1);
bannerContent = 'update 2';
update$.next({ key: 'notifications:banner' });
expect(banners.replace).toHaveBeenCalledTimes(2);
});
it('removes banner when changed to empty string', () => {
startService('remove me!');
const update$ = (uiSettings.getUpdate$() as any) as Subject<{
key: string;
}>;
bannerContent = '';
update$.next({ key: 'notifications:banner' });
expect(banners.remove).toHaveBeenCalled();
});
it('removes banner when changed to undefined', () => {
startService('remove me!');
const update$ = (uiSettings.getUpdate$() as any) as Subject<{
key: string;
}>;
bannerContent = undefined;
update$.next({ key: 'notifications:banner' });
expect(banners.remove).toHaveBeenCalled();
});
it('does not update banner if other settings change', () => {
startService('initial banner!');
expect(banners.replace).toHaveBeenCalledTimes(1);
const update$ = (uiSettings.getUpdate$() as any) as Subject<{
key: string;
}>;
update$.next({ key: 'other:setting' });
expect(banners.replace).toHaveBeenCalledTimes(1); // still only the initial call
});
});

View file

@ -0,0 +1,116 @@
/*
* 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, { Fragment } from 'react';
import ReactDOM from 'react-dom';
import ReactMarkdown from 'react-markdown';
import { filter } from 'rxjs/operators';
import { Subscription } from 'rxjs';
import { FormattedMessage } from '@kbn/i18n/react';
import { EuiCallOut, EuiButton } from '@elastic/eui';
import { I18nStart } from '../../i18n';
import { UiSettingsClientContract } from '../../ui_settings';
import { OverlayBannersStart } from './banners_service';
interface StartDeps {
banners: OverlayBannersStart;
i18n: I18nStart;
uiSettings: UiSettingsClientContract;
}
/**
* Sets up the custom banner that can be specified in advanced settings.
* @internal
*/
export class UserBannerService {
private settingsSubscription?: Subscription;
public start({ banners, i18n, uiSettings }: StartDeps) {
let id: string | undefined;
let timeout: any;
const dismiss = () => {
banners.remove(id!);
clearTimeout(timeout);
};
const updateBanner = () => {
const content = uiSettings.get('notifications:banner');
const lifetime = uiSettings.get('notifications:lifetime:banner');
if (typeof content !== 'string' || content.length === 0 || typeof lifetime !== 'number') {
dismiss();
return;
}
id = banners.replace(
id,
el => {
ReactDOM.render(
<i18n.Context>
<EuiCallOut
title={
<FormattedMessage
id="core.ui.overlays.banner.attentionTitle"
defaultMessage="Attention"
/>
}
iconType="help"
>
<ReactMarkdown renderers={{ root: Fragment }}>{content.trim()}</ReactMarkdown>
<EuiButton type="primary" size="s" onClick={() => banners.remove(id!)}>
<FormattedMessage
id="core.ui.overlays.banner.closeButtonLabel"
defaultMessage="Close"
/>
</EuiButton>
</EuiCallOut>
</i18n.Context>,
el
);
timeout = setTimeout(dismiss, lifetime);
return () => ReactDOM.unmountComponentAtNode(el);
},
100
);
};
updateBanner();
this.settingsSubscription = uiSettings
.getUpdate$()
.pipe(
filter(
({ key }) => key === 'notifications:banner' || key === 'notifications:lifetime:banner'
)
)
.subscribe(() => updateBanner());
}
public stop() {
if (this.settingsSubscription) {
this.settingsSubscription.unsubscribe();
this.settingsSubscription = undefined;
}
}
}

View file

@ -17,4 +17,5 @@
* under the License.
*/
export { OverlayBannerMount, OverlayBannerUnmount, OverlayBannersStart } from './banners';
export { OverlayService, OverlayStart, OverlayRef } from './overlay_service';

View file

@ -17,11 +17,13 @@
* under the License.
*/
import { OverlayService, OverlayStart } from './overlay_service';
import { overlayBannersServiceMock } from './banners/banners_service.mock';
const createStartContractMock = () => {
const startContract: jest.Mocked<PublicMethodsOf<OverlayStart>> = {
const startContract: DeeplyMockedKeys<OverlayStart> = {
openFlyout: jest.fn(),
openModal: jest.fn(),
banners: overlayBannersServiceMock.createStartContract(),
};
startContract.openModal.mockReturnValue({
close: jest.fn(),

View file

@ -22,6 +22,8 @@ import React from 'react';
import { FlyoutService } from './flyout';
import { ModalService } from './modal';
import { I18nStart } from '../i18n';
import { OverlayBannersStart, OverlayBannersService } from './banners';
import { UiSettingsClientContract } from '../ui_settings';
export interface OverlayRef {
/**
@ -43,30 +45,32 @@ export interface OverlayRef {
interface StartDeps {
i18n: I18nStart;
targetDomElement: HTMLElement;
uiSettings: UiSettingsClientContract;
}
/** @internal */
export class OverlayService {
private flyoutService?: FlyoutService;
private modalService?: ModalService;
public start({ i18n, targetDomElement }: StartDeps): OverlayStart {
public start({ i18n, targetDomElement, uiSettings }: StartDeps): OverlayStart {
const flyoutElement = document.createElement('div');
const modalElement = document.createElement('div');
targetDomElement.appendChild(flyoutElement);
targetDomElement.appendChild(modalElement);
this.flyoutService = new FlyoutService(flyoutElement);
this.modalService = new ModalService(modalElement);
const flyoutService = new FlyoutService(flyoutElement);
const modalService = new ModalService(modalElement);
const bannersService = new OverlayBannersService();
return {
openFlyout: this.flyoutService.openFlyout.bind(this.flyoutService, i18n),
openModal: this.modalService.openModal.bind(this.modalService, i18n),
banners: bannersService.start({ i18n, uiSettings }),
openFlyout: flyoutService.openFlyout.bind(flyoutService, i18n),
openModal: modalService.openModal.bind(modalService, i18n),
};
}
}
/** @public */
export interface OverlayStart {
/** {@link OverlayBannersStart} */
banners: OverlayBannersStart;
openFlyout: (
flyoutChildren: React.ReactNode,
flyoutProps?: {

View file

@ -596,6 +596,25 @@ export interface NotificationsStart {
toasts: ToastsStart;
}
// @public
export type OverlayBannerMount = (element: HTMLElement) => OverlayBannerUnmount;
// @public (undocumented)
export interface OverlayBannersStart {
add(mount: OverlayBannerMount, priority?: number): string;
// Warning: (ae-forgotten-export) The symbol "OverlayBanner" needs to be exported by the entry point index.d.ts
//
// @internal (undocumented)
get$(): Observable<OverlayBanner[]>;
// (undocumented)
getComponent(): JSX.Element;
remove(id: string): boolean;
replace(id: string | undefined, mount: OverlayBannerMount, priority?: number): string;
}
// @public
export type OverlayBannerUnmount = () => void;
// Warning: (ae-missing-release-tag) "OverlayRef" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
@ -606,6 +625,8 @@ export interface OverlayRef {
// @public (undocumented)
export interface OverlayStart {
// (undocumented)
banners: OverlayBannersStart;
// (undocumented)
openFlyout: (flyoutChildren: React.ReactNode, flyoutProps?: {
closeButtonAriaLabel?: string;

View file

@ -23,6 +23,7 @@ import { chromeServiceMock } from '../chrome/chrome_service.mock';
import { RenderingService } from './rendering_service';
import { InternalApplicationStart } from '../application';
import { injectedMetadataServiceMock } from '../injected_metadata/injected_metadata_service.mock';
import { overlayServiceMock } from '../overlays/overlay_service.mock';
describe('RenderingService#start', () => {
const getService = ({ legacyMode = false }: { legacyMode?: boolean } = {}) => {
@ -32,10 +33,19 @@ describe('RenderingService#start', () => {
} as InternalApplicationStart;
const chrome = chromeServiceMock.createStartContract();
chrome.getHeaderComponent.mockReturnValue(<div>Hello chrome!</div>);
const overlays = overlayServiceMock.createStartContract();
overlays.banners.getComponent.mockReturnValue(<div>I'm a banner!</div>);
const injectedMetadata = injectedMetadataServiceMock.createStartContract();
injectedMetadata.getLegacyMode.mockReturnValue(legacyMode);
const targetDomElement = document.createElement('div');
const start = rendering.start({ application, chrome, injectedMetadata, targetDomElement });
const start = rendering.start({
application,
chrome,
injectedMetadata,
overlays,
targetDomElement,
});
return { start, targetDomElement };
};
@ -58,6 +68,19 @@ describe('RenderingService#start', () => {
expect(targetDomElement.querySelector('div.app-wrapper-pannel')).toBeDefined();
});
it('renders the banner UI', () => {
const { targetDomElement } = getService();
expect(targetDomElement.querySelector('#globalBannerList')).toMatchInlineSnapshot(`
<div
id="globalBannerList"
>
<div>
I'm a banner!
</div>
</div>
`);
});
describe('legacyMode', () => {
it('renders into provided DOM element', () => {
const { targetDomElement } = getService({ legacyMode: true });

View file

@ -24,11 +24,13 @@ import { I18nProvider } from '@kbn/i18n/react';
import { InternalChromeStart } from '../chrome';
import { InternalApplicationStart } from '../application';
import { InjectedMetadataStart } from '../injected_metadata';
import { OverlayStart } from '../overlays';
interface StartDeps {
application: InternalApplicationStart;
chrome: InternalChromeStart;
injectedMetadata: InjectedMetadataStart;
overlays: OverlayStart;
targetDomElement: HTMLDivElement;
}
@ -43,9 +45,16 @@ interface StartDeps {
* @internal
*/
export class RenderingService {
start({ application, chrome, injectedMetadata, targetDomElement }: StartDeps): RenderingStart {
start({
application,
chrome,
injectedMetadata,
overlays,
targetDomElement,
}: StartDeps): RenderingStart {
const chromeUi = chrome.getHeaderComponent();
const appUi = application.getComponent();
const bannerUi = overlays.banners.getComponent();
const legacyMode = injectedMetadata.getLegacyMode();
const legacyRef = legacyMode ? React.createRef<HTMLDivElement>() : null;
@ -58,6 +67,7 @@ export class RenderingService {
{!legacyMode && (
<div className="app-wrapper">
<div className="app-wrapper-panel">
<div id="globalBannerList">{bannerUi}</div>
<div className="application">{appUi}</div>
</div>
</div>

View file

@ -100,6 +100,7 @@ describe('QueryBarTopRowTopRow', () => {
<QueryBarTopRow.WrappedComponent
uiSettings={startMock.uiSettings}
savedObjectsClient={startMock.savedObjects.client}
toasts={startMock.notifications.toasts}
query={kqlQuery}
onSubmit={noop}
appName={'discover'}
@ -122,6 +123,7 @@ describe('QueryBarTopRowTopRow', () => {
<QueryBarTopRow.WrappedComponent
uiSettings={startMock.uiSettings}
savedObjectsClient={startMock.savedObjects.client}
toasts={startMock.notifications.toasts}
query={kqlQuery}
onSubmit={noop}
appName={'discover'}
@ -144,6 +146,7 @@ describe('QueryBarTopRowTopRow', () => {
<QueryBarTopRow.WrappedComponent
uiSettings={startMock.uiSettings}
savedObjectsClient={startMock.savedObjects.client}
toasts={startMock.notifications.toasts}
onSubmit={noop}
onChange={noop}
isDirty={false}
@ -163,6 +166,7 @@ describe('QueryBarTopRowTopRow', () => {
<QueryBarTopRow.WrappedComponent
uiSettings={startMock.uiSettings}
savedObjectsClient={startMock.savedObjects.client}
toasts={startMock.notifications.toasts}
onSubmit={noop}
onChange={noop}
isDirty={false}
@ -183,6 +187,7 @@ describe('QueryBarTopRowTopRow', () => {
<QueryBarTopRow.WrappedComponent
uiSettings={startMock.uiSettings}
savedObjectsClient={startMock.savedObjects.client}
toasts={startMock.notifications.toasts}
onSubmit={noop}
onChange={noop}
isDirty={false}
@ -206,6 +211,7 @@ describe('QueryBarTopRowTopRow', () => {
<QueryBarTopRow.WrappedComponent
uiSettings={startMock.uiSettings}
savedObjectsClient={startMock.savedObjects.client}
toasts={startMock.notifications.toasts}
query={kqlQuery}
onSubmit={noop}
onChange={noop}
@ -229,6 +235,7 @@ describe('QueryBarTopRowTopRow', () => {
<QueryBarTopRow.WrappedComponent
uiSettings={startMock.uiSettings}
savedObjectsClient={startMock.savedObjects.client}
toasts={startMock.notifications.toasts}
query={kqlQuery}
onSubmit={noop}
onChange={noop}
@ -253,6 +260,7 @@ describe('QueryBarTopRowTopRow', () => {
<QueryBarTopRow.WrappedComponent
uiSettings={startMock.uiSettings}
savedObjectsClient={startMock.savedObjects.client}
toasts={startMock.notifications.toasts}
onSubmit={noop}
onChange={noop}
isDirty={false}

View file

@ -31,11 +31,12 @@ import { EuiSuperUpdateButton } from '@elastic/eui';
import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react';
import { documentationLinks } from 'ui/documentation_links';
import { Toast, toastNotifications } from 'ui/notify';
import { PersistedLog } from 'ui/persisted_log';
import {
UiSettingsClientContract,
SavedObjectsClientContract,
Toast,
CoreStart,
HttpServiceBase,
} from 'src/core/public';
import { IndexPattern } from '../../../index_patterns';
@ -70,6 +71,7 @@ interface Props {
onRefreshChange?: (options: { isPaused: boolean; refreshInterval: number }) => void;
customSubmitButton?: any;
isDirty: boolean;
toasts: CoreStart['notifications']['toasts'];
uiSettings: UiSettingsClientContract;
savedObjectsClient: SavedObjectsClientContract;
http: HttpServiceBase;
@ -304,7 +306,7 @@ export class QueryBarTopRowUI extends Component<Props, State> {
private handleLuceneSyntaxWarning() {
if (!this.props.query) return;
const { intl, store } = this.props;
const { intl, store, toasts } = this.props;
const { query, language } = this.props.query;
if (
language === 'kuery' &&
@ -312,7 +314,7 @@ export class QueryBarTopRowUI extends Component<Props, State> {
(!store || !store.get('kibana.luceneSyntaxWarningOptOut')) &&
doesKueryExpressionHaveLuceneSyntaxError(query)
) {
const toast = toastNotifications.addWarning({
const toast = toasts.addWarning({
title: intl.formatMessage({
id: 'data.query.queryBar.luceneSyntaxWarningTitle',
defaultMessage: 'Lucene syntax warning',
@ -355,7 +357,7 @@ export class QueryBarTopRowUI extends Component<Props, State> {
private onLuceneSyntaxWarningOptOut(toast: Toast) {
if (!this.props.store) return;
this.props.store.set('kibana.luceneSyntaxWarningOptOut', true);
toastNotifications.remove(toast);
this.props.toasts.remove(toast);
}
}

View file

@ -98,6 +98,7 @@ describe('SearchBar', () => {
<SearchBar.WrappedComponent
savedObjectsClient={startMock.savedObjects.client}
uiSettings={startMock.uiSettings}
toasts={startMock.notifications.toasts}
appName={'test'}
indexPatterns={[mockIndexPattern]}
intl={null as any}
@ -115,6 +116,7 @@ describe('SearchBar', () => {
<SearchBar.WrappedComponent
savedObjectsClient={startMock.savedObjects.client}
uiSettings={startMock.uiSettings}
toasts={startMock.notifications.toasts}
appName={'test'}
indexPatterns={[mockIndexPattern]}
intl={null as any}
@ -133,6 +135,7 @@ describe('SearchBar', () => {
<SearchBar.WrappedComponent
savedObjectsClient={startMock.savedObjects.client}
uiSettings={startMock.uiSettings}
toasts={startMock.notifications.toasts}
appName={'test'}
indexPatterns={[mockIndexPattern]}
intl={null as any}
@ -153,6 +156,7 @@ describe('SearchBar', () => {
<SearchBar.WrappedComponent
savedObjectsClient={startMock.savedObjects.client}
uiSettings={startMock.uiSettings}
toasts={startMock.notifications.toasts}
appName={'test'}
indexPatterns={[mockIndexPattern]}
intl={null as any}
@ -174,6 +178,7 @@ describe('SearchBar', () => {
<SearchBar.WrappedComponent
savedObjectsClient={startMock.savedObjects.client}
uiSettings={startMock.uiSettings}
toasts={startMock.notifications.toasts}
appName={'test'}
indexPatterns={[mockIndexPattern]}
intl={null as any}
@ -195,6 +200,7 @@ describe('SearchBar', () => {
<SearchBar.WrappedComponent
uiSettings={startMock.uiSettings}
savedObjectsClient={startMock.savedObjects.client}
toasts={startMock.notifications.toasts}
appName={'test'}
indexPatterns={[mockIndexPattern]}
intl={null as any}
@ -217,6 +223,7 @@ describe('SearchBar', () => {
<SearchBar.WrappedComponent
uiSettings={startMock.uiSettings}
savedObjectsClient={startMock.savedObjects.client}
toasts={startMock.notifications.toasts}
appName={'test'}
indexPatterns={[mockIndexPattern]}
intl={null as any}

View file

@ -28,6 +28,7 @@ import { get, isEqual } from 'lodash';
import { toastNotifications } from 'ui/notify';
import {
CoreStart,
UiSettingsClientContract,
SavedObjectsClientContract,
HttpServiceBase,
@ -52,6 +53,7 @@ interface DateRange {
export interface SearchBarProps {
appName: string;
intl: InjectedIntl;
toasts: CoreStart['notifications']['toasts'];
uiSettings: UiSettingsClientContract;
savedObjectsClient: SavedObjectsClientContract;
indexPatterns?: IndexPattern[];
@ -374,6 +376,7 @@ class SearchBarUI extends Component<SearchBarProps, State> {
if (this.shouldRenderQueryBar()) {
queryBar = (
<QueryBarTopRow
toasts={this.props.toasts}
http={this.props.http}
uiSettings={this.props.uiSettings}
savedObjectsClient={this.props.savedObjectsClient}

View file

@ -17,7 +17,7 @@
* under the License.
*/
import { notificationServiceMock } from '../../../../../../core/public/mocks';
import { notificationServiceMock, overlayServiceMock } from '../../../../../../core/public/mocks';
jest.doMock('ui/new_platform', () => {
return {
@ -26,6 +26,11 @@ jest.doMock('ui/new_platform', () => {
notifications: notificationServiceMock.createSetupContract(),
},
},
npStart: {
core: {
overlays: overlayServiceMock.createStartContract(),
},
},
};
});

View file

@ -57,6 +57,7 @@ describe('TopNavMenu', () => {
const component = shallowWithIntl(
<TopNavMenu
name="test"
toasts={startMock.notifications.toasts}
uiSettings={startMock.uiSettings}
savedObjectsClient={startMock.savedObjects.client}
/>
@ -69,6 +70,7 @@ describe('TopNavMenu', () => {
const component = shallowWithIntl(
<TopNavMenu
name="test"
toasts={startMock.notifications.toasts}
uiSettings={startMock.uiSettings}
savedObjectsClient={startMock.savedObjects.client}
config={[menuItems[0]]}
@ -82,6 +84,7 @@ describe('TopNavMenu', () => {
const component = shallowWithIntl(
<TopNavMenu
name="test"
toasts={startMock.notifications.toasts}
uiSettings={startMock.uiSettings}
savedObjectsClient={startMock.savedObjects.client}
config={menuItems}
@ -95,6 +98,7 @@ describe('TopNavMenu', () => {
const component = shallowWithIntl(
<TopNavMenu
name="test"
toasts={startMock.notifications.toasts}
uiSettings={startMock.uiSettings}
savedObjectsClient={startMock.savedObjects.client}
http={startMock.http}

View file

@ -21,7 +21,7 @@ import React from 'react';
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { I18nProvider } from '@kbn/i18n/react';
import { UiSettingsClientContract, SavedObjectsClientContract } from 'src/core/public';
import { UiSettingsClientContract, SavedObjectsClientContract, CoreStart } from 'src/core/public';
import { TopNavMenuData } from './top_nav_menu_data';
import { TopNavMenuItem } from './top_nav_menu_item';
import { SearchBar, SearchBarProps } from '../../../../core_plugins/data/public';
@ -30,6 +30,7 @@ type Props = Partial<SearchBarProps> & {
name: string;
uiSettings: UiSettingsClientContract;
savedObjectsClient: SavedObjectsClientContract;
toasts: CoreStart['notifications']['toasts'];
config?: TopNavMenuData[];
showSearchBar?: boolean;
};
@ -64,6 +65,7 @@ export function TopNavMenu(props: Props) {
http={props.http}
query={props.query}
filters={props.filters}
toasts={props.toasts}
uiSettings={props.uiSettings}
showQueryBar={props.showQueryBar}
showQueryInput={props.showQueryInput}

View file

@ -17,7 +17,7 @@
* under the License.
*/
import { fatalErrorsServiceMock, notificationServiceMock } from '../../../../../core/public/mocks';
import { fatalErrorsServiceMock, notificationServiceMock, overlayServiceMock } from '../../../../../core/public/mocks';
jest.doMock('ui/new_platform', () => ({
npSetup: {
@ -26,4 +26,9 @@ jest.doMock('ui/new_platform', () => ({
notifications: notificationServiceMock.createSetupContract(),
}
},
npStart: {
core: {
overlays: overlayServiceMock.createStartContract(),
},
},
}));

View file

@ -18,7 +18,6 @@
@import './error_url_overflow/index';
@import './exit_full_screen/index';
@import './field_editor/index';
@import './notify/index';
@import './saved_objects/index';
@import './share/index';
@import './style_compile/index';

View file

@ -24,11 +24,6 @@ import $ from 'jquery';
import { uiModules } from '../../modules';
import template from './kbn_chrome.html';
import {
GlobalBannerList,
banners,
} from '../../notify';
import { I18nContext } from '../../i18n';
import { npStart } from '../../new_platform';
import { chromeHeaderNavControlsRegistry, NavControlSide } from '../../registry/chrome_header_nav_controls';
@ -78,21 +73,16 @@ export function kbnChromeProvider(chrome, internals) {
// Banners
const bannerListContainer = document.getElementById('globalBannerList');
// Banners not supported in New Platform yet
// https://github.com/elastic/kibana/issues/41986
if (bannerListContainer) {
// This gets rendered manually by the legacy platform because this component must be inside the .app-wrapper
ReactDOM.render(
<I18nContext>
<GlobalBannerList
banners={banners.list}
subscribe={banners.onChange}
/>
{npStart.core.overlays.banners.getComponent()}
</I18nContext>,
bannerListContainer
);
}
return chrome;
}
};

View file

@ -1,22 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`GlobalBannerList is rendered 1`] = `null`;
exports[`GlobalBannerList props banners is rendered 1`] = `
<div
class="kbnGlobalBannerList"
>
<div
class="kbnGlobalBannerList__item"
data-test-priority="1"
>
a component
</div>
<div
class="kbnGlobalBannerList__item"
data-test-subj="b"
>
b good
</div>
</div>
`;

View file

@ -1 +0,0 @@
@import './global_banner_list';

View file

@ -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);
});
});
});
});

View file

@ -17,47 +17,23 @@
* under the License.
*/
import React from 'react';
import ReactDOM from 'react-dom';
import { npStart } from 'ui/new_platform';
import { I18nProvider } from '@kbn/i18n/react';
const npBanners = npStart.core.overlays.banners;
/** compatibility layer for new platform */
const mountForComponent = (component: React.ReactElement) => (element: HTMLElement) => {
ReactDOM.render(<I18nProvider>{component}</I18nProvider>, element);
return () => ReactDOM.unmountComponentAtNode(element);
};
/**
* 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;
}
_changed = () => {
if (this.onChangeCallback) {
this.onChangeCallback();
}
}
_remove = id => {
const index = this.list.findIndex(details => details.id === id);
if (index !== -1) {
this.list.splice(index, 1);
return true;
}
return false;
}
/**
* 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;
}
/**
* Add a new banner.
*
@ -65,25 +41,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 = ({ component, priority }: { component: React.ReactElement; priority?: number }) => {
return npBanners.add(mountForComponent(component), priority);
};
/**
* Remove an existing banner.
@ -91,15 +51,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): boolean => {
return npBanners.remove(id);
};
/**
* Replace an existing banner by removing it, if it exists, and adding a new one in its place.
@ -112,12 +66,17 @@ 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 });
}
set = ({
component,
id,
priority = 0,
}: {
component: React.ReactElement;
id: string;
priority?: number;
}): string => {
return npBanners.replace(id, mountForComponent(component), priority);
};
}
/**

View file

@ -1,68 +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';
/**
* 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 <EuiCallout /> 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 (
<div key={id} data-test-priority={priority} className="kbnGlobalBannerList__item" {...rest}>
{component}
</div>
);
});
return <div className="kbnGlobalBannerList">{flexBanners}</div>;
}
}

View file

@ -1,65 +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 from 'react';
import { render } from 'enzyme';
import { GlobalBannerList } from './global_banner_list';
describe('GlobalBannerList', () => {
test('is rendered', () => {
const component = render(
<GlobalBannerList />
);
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(
<GlobalBannerList
banners={banners}
/>
);
expect(component)
.toMatchSnapshot();
});
});
});
});

View file

@ -17,5 +17,4 @@
* under the License.
*/
export { GlobalBannerList } from './global_banner_list';
export { banners } from './banners';

View file

@ -19,5 +19,5 @@
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';

View file

@ -1,100 +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 from 'react';
import { MarkdownSimple } from '../../../core_plugins/kibana_react/public';
import chrome from '../chrome';
import { fatalError } from './fatal_error';
import { banners } from './banners';
import './filters/markdown';
import { FormattedMessage } from '@kbn/i18n/react';
import {
EuiCallOut,
EuiButton,
} from '@elastic/eui';
const config = chrome.getUiSettingsClient();
config.getUpdate$().subscribe(() => {
applyConfig(config);
});
let bannerId;
let bannerTimeoutId;
function applyConfig(config) {
// Show user-defined banner.
const bannerContent = config.get('notifications:banner');
const bannerLifetime = config.get('notifications:lifetime:banner');
if (typeof bannerContent === 'string' && bannerContent.trim()) {
const BANNER_PRIORITY = 100;
const dismissBanner = () => {
banners.remove(bannerId);
clearTimeout(bannerTimeoutId);
};
const banner = (
<EuiCallOut
title={(
<FormattedMessage
id="common.ui.notify.banner.attentionTitle"
defaultMessage="Attention"
/>
)}
iconType="help"
>
<MarkdownSimple data-test-subj="userDefinedBanner">
{bannerContent}
</MarkdownSimple>
<EuiButton type="primary" size="s" onClick={dismissBanner}>
<FormattedMessage
id="common.ui.notify.banner.closeButtonLabel"
defaultMessage="Close"
/>
</EuiButton>
</EuiCallOut>
);
bannerId = banners.set({
component: banner,
id: bannerId,
priority: BANNER_PRIORITY,
});
bannerTimeoutId = setTimeout(() => {
dismissBanner();
}, bannerLifetime);
}
}
window.onerror = function (err, url, line) {
fatalError(new Error(`${err} (${url}:${line})`));
return true;
};
if (window.addEventListener) {
window.addEventListener('unhandledrejection', function (e) {
console.log(`Detected an unhandled Promise rejection.\n${e.reason}`); // eslint-disable-line no-console
});
}

View file

@ -20,7 +20,7 @@
import * as React from 'react';
import * as ReactDOM from 'react-dom';
import { context, createKibanaReactContext, useKibana, KibanaContextProvider } from './context';
import { coreMock } from '../../../../core/public/mocks';
import { coreMock, overlayServiceMock } from '../../../../core/public/mocks';
import { CoreStart } from './types';
let container: HTMLDivElement | null;
@ -165,17 +165,11 @@ test('overlays wrapper uses the closest overlays service', () => {
};
const core1 = {
overlays: {
openFlyout: jest.fn(),
openModal: jest.fn(),
},
overlays: overlayServiceMock.createStartContract(),
} as Partial<CoreStart>;
const core2 = {
overlays: {
openFlyout: jest.fn(),
openModal: jest.fn(),
},
overlays: overlayServiceMock.createStartContract(),
} as Partial<CoreStart>;
ReactDOM.render(
@ -237,10 +231,7 @@ test('overlays wrapper uses available overlays service, higher up in <KibanaCont
};
const core1 = {
overlays: {
openFlyout: jest.fn(),
openModal: jest.fn(),
},
overlays: overlayServiceMock.createStartContract(),
notifications: ({
toasts: {
add: jest.fn(),

View file

@ -19,6 +19,7 @@
import * as React from 'react';
import { createReactOverlays } from './create_react_overlays';
import { overlayServiceMock } from '../../../../core/public/mocks';
test('throws if no overlays service provided', () => {
const overlays = createReactOverlays({});
@ -29,10 +30,7 @@ test('throws if no overlays service provided', () => {
test('creates wrapped overlays service', () => {
const overlays = createReactOverlays({
overlays: {
openFlyout: jest.fn(),
openModal: jest.fn(),
},
overlays: overlayServiceMock.createStartContract(),
});
expect(typeof overlays.openFlyout).toBe('function');
@ -40,20 +38,17 @@ test('creates wrapped overlays service', () => {
});
test('can open flyout with React element', () => {
const openFlyout = jest.fn();
const coreOverlays = overlayServiceMock.createStartContract();
const overlays = createReactOverlays({
overlays: {
openFlyout,
openModal: jest.fn(),
},
overlays: coreOverlays,
});
expect(openFlyout).toHaveBeenCalledTimes(0);
expect(coreOverlays.openFlyout).toHaveBeenCalledTimes(0);
overlays.openFlyout(<div>foo</div>);
expect(openFlyout).toHaveBeenCalledTimes(1);
expect(openFlyout.mock.calls[0][0]).toMatchInlineSnapshot(`
expect(coreOverlays.openFlyout).toHaveBeenCalledTimes(1);
expect(coreOverlays.openFlyout.mock.calls[0][0]).toMatchInlineSnapshot(`
<React.Fragment>
<div>
foo
@ -63,21 +58,17 @@ test('can open flyout with React element', () => {
});
test('can open modal with React element', () => {
const openFlyout = jest.fn();
const openModal = jest.fn();
const coreOverlays = overlayServiceMock.createStartContract();
const overlays = createReactOverlays({
overlays: {
openFlyout,
openModal,
},
overlays: coreOverlays,
});
expect(openModal).toHaveBeenCalledTimes(0);
expect(coreOverlays.openModal).toHaveBeenCalledTimes(0);
overlays.openModal(<div>bar</div>);
expect(openModal).toHaveBeenCalledTimes(1);
expect(openModal.mock.calls[0][0]).toMatchInlineSnapshot(`
expect(coreOverlays.openModal).toHaveBeenCalledTimes(1);
expect(coreOverlays.openModal.mock.calls[0][0]).toMatchInlineSnapshot(`
<React.Fragment>
<div>
bar
@ -87,12 +78,9 @@ test('can open modal with React element', () => {
});
test('passes through flyout options when opening flyout', () => {
const openFlyout = jest.fn();
const coreOverlays = overlayServiceMock.createStartContract();
const overlays = createReactOverlays({
overlays: {
openFlyout,
openModal: jest.fn(),
},
overlays: coreOverlays,
});
overlays.openFlyout(<>foo</>, {
@ -100,19 +88,16 @@ test('passes through flyout options when opening flyout', () => {
closeButtonAriaLabel: 'bar',
});
expect(openFlyout.mock.calls[0][1]).toEqual({
expect(coreOverlays.openFlyout.mock.calls[0][1]).toEqual({
'data-test-subj': 'foo',
closeButtonAriaLabel: 'bar',
});
});
test('passes through modal options when opening modal', () => {
const openModal = jest.fn();
const coreOverlays = overlayServiceMock.createStartContract();
const overlays = createReactOverlays({
overlays: {
openFlyout: jest.fn(),
openModal,
},
overlays: coreOverlays,
});
overlays.openModal(<>foo</>, {
@ -120,7 +105,7 @@ test('passes through modal options when opening modal', () => {
closeButtonAriaLabel: 'bar2',
});
expect(openModal.mock.calls[0][1]).toEqual({
expect(coreOverlays.openModal.mock.calls[0][1]).toEqual({
'data-test-subj': 'foo2',
closeButtonAriaLabel: 'bar2',
});

View file

@ -230,6 +230,7 @@ export function App({
state.localQueryBarState.dateRange && state.localQueryBarState.dateRange.to
}
uiSettings={core.uiSettings}
toasts={core.notifications.toasts}
savedObjectsClient={savedObjectsClient}
http={core.http}
/>

View file

@ -484,8 +484,8 @@
"common.ui.management.editIndexPattern.createIndex.defaultTypeName": "インデックスパターン",
"common.ui.management.nav.menu": "管理メニュー",
"common.ui.modals.cancelButtonLabel": "キャンセル",
"common.ui.notify.banner.attentionTitle": "注意",
"common.ui.notify.banner.closeButtonLabel": "閉じる",
"core.ui.overlays.banner.attentionTitle": "注意",
"core.ui.overlays.banner.closeButtonLabel": "閉じる",
"common.ui.notify.fatalError.errorStatusMessage": "エラー {errStatus} {errStatusText}: {errMessage}",
"common.ui.notify.fatalError.unavailableServerErrorMessage": "HTTP リクエストが接続に失敗しました。Kibana サーバーが実行されていて、ご使用のブラウザの接続が正常に動作していることを確認するか、システム管理者にお問い合わせください。",
"common.ui.notify.toaster.errorMessage": "エラー: {errorMessage}\n {errorStack}",

View file

@ -484,8 +484,8 @@
"common.ui.management.editIndexPattern.createIndex.defaultTypeName": "索引模式",
"common.ui.management.nav.menu": "管理菜单",
"common.ui.modals.cancelButtonLabel": "取消",
"common.ui.notify.banner.attentionTitle": "注意",
"common.ui.notify.banner.closeButtonLabel": "关闭",
"core.ui.overlays.banner.attentionTitle": "注意",
"core.ui.overlays.banner.closeButtonLabel": "关闭",
"common.ui.notify.fatalError.errorStatusMessage": "错误 {errStatus} {errStatusText}{errMessage}",
"common.ui.notify.fatalError.unavailableServerErrorMessage": "HTTP 请求无法连接。请检查 Kibana 服务器是否正在运行以及您的浏览器是否具有有效的连接,或请联系您的系统管理员。",
"common.ui.notify.toaster.errorMessage": "错误:{errorMessage}\n {errorStack}",