mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
* Move ui/notify banners to New Platform (#43610) * Fix Lens component
This commit is contained in:
parent
c78d976dc2
commit
7c29e26926
58 changed files with 1244 additions and 576 deletions
|
@ -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) | |
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [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;
|
||||
```
|
|
@ -0,0 +1,27 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [OverlayBannersStart](./kibana-plugin-public.overlaybannersstart.md) > [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)
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [OverlayBannersStart](./kibana-plugin-public.overlaybannersstart.md) > [getComponent](./kibana-plugin-public.overlaybannersstart.getcomponent.md)
|
||||
|
||||
## OverlayBannersStart.getComponent() method
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
getComponent(): JSX.Element;
|
||||
```
|
||||
<b>Returns:</b>
|
||||
|
||||
`JSX.Element`
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [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 |
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [OverlayBannersStart](./kibana-plugin-public.overlaybannersstart.md) > [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
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [OverlayBannersStart](./kibana-plugin-public.overlaybannersstart.md) > [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 | 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)
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [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;
|
||||
```
|
|
@ -0,0 +1,13 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [OverlayStart](./kibana-plugin-public.overlaystart.md) > [banners](./kibana-plugin-public.overlaystart.banners.md)
|
||||
|
||||
## OverlayStart.banners property
|
||||
|
||||
[OverlayBannersStart](./kibana-plugin-public.overlaybannersstart.md)
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
banners: OverlayBannersStart;
|
||||
```
|
|
@ -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> }) => 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> }) => OverlayRef</code> | |
|
||||
|
||||
|
|
|
@ -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),
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
|
||||
|
|
|
@ -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
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,3 +8,4 @@
|
|||
@import '@elastic/eui/src/global_styling/mixins/index';
|
||||
|
||||
@import './chrome/index';
|
||||
@import './overlays/index';
|
||||
|
|
|
@ -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,
|
||||
|
|
1
src/core/public/overlays/banners/_index.scss
Normal file
1
src/core/public/overlays/banners/_index.scss
Normal file
|
@ -0,0 +1 @@
|
|||
@import './banners_list';
|
105
src/core/public/overlays/banners/banners_list.test.tsx
Normal file
105
src/core/public/overlays/banners/banners_list.test.tsx
Normal 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();
|
||||
});
|
||||
});
|
64
src/core/public/overlays/banners/banners_list.tsx
Normal file
64
src/core/public/overlays/banners/banners_list.tsx
Normal 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} />
|
||||
);
|
||||
};
|
45
src/core/public/overlays/banners/banners_service.mock.ts
Normal file
45
src/core/public/overlays/banners/banners_service.mock.ts
Normal 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,
|
||||
};
|
124
src/core/public/overlays/banners/banners_service.test.ts
Normal file
124
src/core/public/overlays/banners/banners_service.test.ts
Normal 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 }]);
|
||||
});
|
||||
});
|
||||
});
|
148
src/core/public/overlays/banners/banners_service.tsx
Normal file
148
src/core/public/overlays/banners/banners_service.tsx
Normal 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();
|
||||
}
|
||||
}
|
25
src/core/public/overlays/banners/index.ts
Normal file
25
src/core/public/overlays/banners/index.ts
Normal 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';
|
59
src/core/public/overlays/banners/priority_map.test.ts
Normal file
59
src/core/public/overlays/banners/priority_map.test.ts
Normal 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 }],
|
||||
]);
|
||||
});
|
||||
});
|
61
src/core/public/overlays/banners/priority_map.ts
Normal file
61
src/core/public/overlays/banners/priority_map.ts
Normal 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();
|
132
src/core/public/overlays/banners/user_banner_service.test.ts
Normal file
132
src/core/public/overlays/banners/user_banner_service.test.ts
Normal 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
|
||||
});
|
||||
});
|
116
src/core/public/overlays/banners/user_banner_service.tsx
Normal file
116
src/core/public/overlays/banners/user_banner_service.tsx
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -17,4 +17,5 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
export { OverlayBannerMount, OverlayBannerUnmount, OverlayBannersStart } from './banners';
|
||||
export { OverlayService, OverlayStart, OverlayRef } from './overlay_service';
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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?: {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 });
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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(),
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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(),
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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>
|
||||
`;
|
|
@ -1 +0,0 @@
|
|||
@import './global_banner_list';
|
|
@ -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);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
});
|
|
@ -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);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
|
@ -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>;
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
|
@ -17,5 +17,4 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
export { GlobalBannerList } from './global_banner_list';
|
||||
export { banners } from './banners';
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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
|
||||
});
|
||||
}
|
||||
|
|
@ -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(),
|
||||
|
|
|
@ -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',
|
||||
});
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
|
|
|
@ -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}",
|
||||
|
|
|
@ -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}",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue