mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
Add addError function to toastNotifications (#32187)
This commit is contained in:
parent
3f78d29ee5
commit
58ef3a3c49
44 changed files with 658 additions and 103 deletions
|
@ -0,0 +1,19 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [ErrorToastOptions](./kibana-plugin-public.errortoastoptions.md)
|
||||
|
||||
## ErrorToastOptions interface
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
export interface ErrorToastOptions
|
||||
```
|
||||
|
||||
## Properties
|
||||
|
||||
| Property | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| [title](./kibana-plugin-public.errortoastoptions.title.md) | <code>string</code> | The title of the toast and the dialog when expanding the message. |
|
||||
| [toastMessage](./kibana-plugin-public.errortoastoptions.toastmessage.md) | <code>string</code> | The message to be shown in the toast. If this is not specified the error's message will be shown in the toast instead. Overwriting that message can be used to provide more user-friendly toasts. If you specify this, the error message will still be shown in the detailed error modal. |
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [ErrorToastOptions](./kibana-plugin-public.errortoastoptions.md) > [title](./kibana-plugin-public.errortoastoptions.title.md)
|
||||
|
||||
## ErrorToastOptions.title property
|
||||
|
||||
The title of the toast and the dialog when expanding the message.
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
title: string;
|
||||
```
|
|
@ -0,0 +1,13 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [ErrorToastOptions](./kibana-plugin-public.errortoastoptions.md) > [toastMessage](./kibana-plugin-public.errortoastoptions.toastmessage.md)
|
||||
|
||||
## ErrorToastOptions.toastMessage property
|
||||
|
||||
The message to be shown in the toast. If this is not specified the error's message will be shown in the toast instead. Overwriting that message can be used to provide more user-friendly toasts. If you specify this, the error message will still be shown in the detailed error modal.
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
toastMessage?: string;
|
||||
```
|
|
@ -30,12 +30,14 @@ The plugin integrates with the core system via lifecycle events: `setup`<!-- -->
|
|||
| [ChromeNavLink](./kibana-plugin-public.chromenavlink.md) | |
|
||||
| [CoreSetup](./kibana-plugin-public.coresetup.md) | Core services exposed to the <code>Plugin</code> setup lifecycle |
|
||||
| [CoreStart](./kibana-plugin-public.corestart.md) | Core services exposed to the <code>Plugin</code> start lifecycle |
|
||||
| [ErrorToastOptions](./kibana-plugin-public.errortoastoptions.md) | |
|
||||
| [FatalErrorInfo](./kibana-plugin-public.fatalerrorinfo.md) | Represents the <code>message</code> and <code>stack</code> of a fatal Error |
|
||||
| [FatalErrorsSetup](./kibana-plugin-public.fatalerrorssetup.md) | FatalErrors stop the Kibana Public Core and displays a fatal error screen with details about the Kibana build and the error. |
|
||||
| [HttpServiceBase](./kibana-plugin-public.httpservicebase.md) | |
|
||||
| [I18nSetup](./kibana-plugin-public.i18nsetup.md) | I18nSetup.Context is required by any localizable React component from @<!-- -->kbn/i18n and @<!-- -->elastic/eui packages and is supposed to be used as the topmost component for any i18n-compatible React tree. |
|
||||
| [LegacyNavLink](./kibana-plugin-public.legacynavlink.md) | |
|
||||
| [NotificationsSetup](./kibana-plugin-public.notificationssetup.md) | |
|
||||
| [NotificationsStart](./kibana-plugin-public.notificationsstart.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>. |
|
||||
|
@ -52,7 +54,6 @@ The plugin integrates with the core system via lifecycle events: `setup`<!-- -->
|
|||
| [HttpSetup](./kibana-plugin-public.httpsetup.md) | |
|
||||
| [HttpStart](./kibana-plugin-public.httpstart.md) | |
|
||||
| [I18nStart](./kibana-plugin-public.i18nstart.md) | |
|
||||
| [NotificationsStart](./kibana-plugin-public.notificationsstart.md) | |
|
||||
| [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. |
|
||||
| [ToastInput](./kibana-plugin-public.toastinput.md) | |
|
||||
| [UiSettingsSetup](./kibana-plugin-public.uisettingssetup.md) | |
|
||||
|
|
|
@ -15,5 +15,5 @@ export interface NotificationsSetup
|
|||
|
||||
| Property | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| [toasts](./kibana-plugin-public.notificationssetup.toasts.md) | <code>ToastsApi</code> | |
|
||||
| [toasts](./kibana-plugin-public.notificationssetup.toasts.md) | <code>ToastsSetup</code> | |
|
||||
|
||||
|
|
|
@ -7,5 +7,5 @@
|
|||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
toasts: ToastsApi;
|
||||
toasts: ToastsSetup;
|
||||
```
|
||||
|
|
|
@ -2,11 +2,18 @@
|
|||
|
||||
[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [NotificationsStart](./kibana-plugin-public.notificationsstart.md)
|
||||
|
||||
## NotificationsStart type
|
||||
## NotificationsStart interface
|
||||
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
export declare type NotificationsStart = NotificationsSetup;
|
||||
export interface NotificationsStart
|
||||
```
|
||||
|
||||
## Properties
|
||||
|
||||
| Property | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| [toasts](./kibana-plugin-public.notificationsstart.toasts.md) | <code>ToastsStart</code> | |
|
||||
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [NotificationsStart](./kibana-plugin-public.notificationsstart.md) > [toasts](./kibana-plugin-public.notificationsstart.toasts.md)
|
||||
|
||||
## NotificationsStart.toasts property
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
toasts: ToastsStart;
|
||||
```
|
|
@ -8,5 +8,5 @@
|
|||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
export declare type ToastInput = string | Pick<Toast, Exclude<keyof Toast, 'id'>>;
|
||||
export declare type ToastInput = string | ToastInputFields | Promise<ToastInputFields>;
|
||||
```
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [ToastsApi](./kibana-plugin-public.toastsapi.md) > [addError](./kibana-plugin-public.toastsapi.adderror.md)
|
||||
|
||||
## ToastsApi.addError() method
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
addError(error: Error, options: ErrorToastOptions): Toast;
|
||||
```
|
||||
|
||||
## Parameters
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| error | <code>Error</code> | |
|
||||
| options | <code>ErrorToastOptions</code> | |
|
||||
|
||||
<b>Returns:</b>
|
||||
|
||||
`Toast`
|
||||
|
|
@ -17,8 +17,10 @@ export declare class ToastsApi
|
|||
| --- | --- | --- |
|
||||
| [add(toastOrTitle)](./kibana-plugin-public.toastsapi.add.md) | | |
|
||||
| [addDanger(toastOrTitle)](./kibana-plugin-public.toastsapi.adddanger.md) | | |
|
||||
| [addError(error, options)](./kibana-plugin-public.toastsapi.adderror.md) | | |
|
||||
| [addSuccess(toastOrTitle)](./kibana-plugin-public.toastsapi.addsuccess.md) | | |
|
||||
| [addWarning(toastOrTitle)](./kibana-plugin-public.toastsapi.addwarning.md) | | |
|
||||
| [get$()](./kibana-plugin-public.toastsapi.get$.md) | | |
|
||||
| [registerOverlays(overlays)](./kibana-plugin-public.toastsapi.registeroverlays.md) | | |
|
||||
| [remove(toast)](./kibana-plugin-public.toastsapi.remove.md) | | |
|
||||
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [ToastsApi](./kibana-plugin-public.toastsapi.md) > [registerOverlays](./kibana-plugin-public.toastsapi.registeroverlays.md)
|
||||
|
||||
## ToastsApi.registerOverlays() method
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
registerOverlays(overlays: OverlayStart): void;
|
||||
```
|
||||
|
||||
## Parameters
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| overlays | <code>OverlayStart</code> | |
|
||||
|
||||
<b>Returns:</b>
|
||||
|
||||
`void`
|
||||
|
|
@ -235,6 +235,7 @@ describe('#start()', () => {
|
|||
expect(MockNotificationsService.start).toHaveBeenCalledTimes(1);
|
||||
expect(MockNotificationsService.start).toHaveBeenCalledWith({
|
||||
i18n: expect.any(Object),
|
||||
overlays: expect.any(Object),
|
||||
targetDomElement: expect.any(HTMLElement),
|
||||
});
|
||||
});
|
||||
|
|
|
@ -116,7 +116,7 @@ export class CoreSystem {
|
|||
this.fatalErrorsSetup = this.fatalErrors.setup({ injectedMetadata, i18n });
|
||||
const http = this.http.setup({ injectedMetadata, fatalErrors: this.fatalErrorsSetup });
|
||||
const uiSettings = this.uiSettings.setup({ http, injectedMetadata });
|
||||
const notifications = this.notifications.setup({ uiSettings });
|
||||
const notifications = this.notifications.setup({ uiSettings, i18n });
|
||||
const application = this.application.setup();
|
||||
const chrome = this.chrome.setup({ injectedMetadata, notifications });
|
||||
|
||||
|
@ -166,11 +166,12 @@ export class CoreSystem {
|
|||
this.rootDomElement.appendChild(legacyPlatformTargetDomElement);
|
||||
this.rootDomElement.appendChild(overlayTargetDomElement);
|
||||
|
||||
const overlays = this.overlay.start({ i18n, targetDomElement: overlayTargetDomElement });
|
||||
const notifications = await this.notifications.start({
|
||||
i18n,
|
||||
overlays,
|
||||
targetDomElement: notificationsTargetDomElement,
|
||||
});
|
||||
const overlays = this.overlay.start({ i18n, targetDomElement: overlayTargetDomElement });
|
||||
|
||||
const core: InternalCoreStart = {
|
||||
application,
|
||||
|
|
|
@ -49,11 +49,12 @@ import { HttpServiceBase, HttpSetup, HttpStart } from './http';
|
|||
import { I18nSetup, I18nStart } from './i18n';
|
||||
import { InjectedMetadataSetup, InjectedMetadataStart, LegacyNavLink } from './injected_metadata';
|
||||
import {
|
||||
ErrorToastOptions,
|
||||
NotificationsSetup,
|
||||
NotificationsStart,
|
||||
Toast,
|
||||
ToastInput,
|
||||
ToastsApi,
|
||||
NotificationsStart,
|
||||
} from './notifications';
|
||||
import { OverlayRef, OverlayStart } from './overlays';
|
||||
import { Plugin, PluginInitializer, PluginInitializerContext } from './plugins';
|
||||
|
@ -128,6 +129,7 @@ export {
|
|||
HttpServiceBase,
|
||||
HttpSetup,
|
||||
HttpStart,
|
||||
ErrorToastOptions,
|
||||
FatalErrorsSetup,
|
||||
FatalErrorInfo,
|
||||
Capabilities,
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
export { Toast, ToastInput, ToastsApi } from './toasts';
|
||||
export { ErrorToastOptions, Toast, ToastInput, ToastsApi } from './toasts';
|
||||
export {
|
||||
NotificationsService,
|
||||
NotificationsSetup,
|
||||
|
|
|
@ -20,17 +20,19 @@
|
|||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import { Subscription } from 'rxjs';
|
||||
import { I18nStart } from '../i18n';
|
||||
import { ToastsService } from './toasts';
|
||||
import { ToastsApi } from './toasts/toasts_api';
|
||||
import { I18nStart, I18nSetup } from '../i18n';
|
||||
import { ToastsService, ToastsSetup, ToastsStart } from './toasts';
|
||||
import { UiSettingsSetup } from '../ui_settings';
|
||||
import { OverlayStart } from '../overlays';
|
||||
|
||||
interface SetupDeps {
|
||||
i18n: I18nSetup;
|
||||
uiSettings: UiSettingsSetup;
|
||||
}
|
||||
|
||||
interface StartDeps {
|
||||
i18n: I18nStart;
|
||||
overlays: OverlayStart;
|
||||
targetDomElement: HTMLElement;
|
||||
}
|
||||
|
||||
|
@ -44,8 +46,8 @@ export class NotificationsService {
|
|||
this.toasts = new ToastsService();
|
||||
}
|
||||
|
||||
public setup({ uiSettings }: SetupDeps): NotificationsSetup {
|
||||
const notificationSetup = { toasts: this.toasts.setup() };
|
||||
public setup({ i18n: i18nSetup, uiSettings }: SetupDeps): NotificationsSetup {
|
||||
const notificationSetup = { toasts: this.toasts.setup({ i18n: i18nSetup, uiSettings }) };
|
||||
|
||||
this.uiSettingsErrorSubscription = uiSettings.getUpdateErrors$().subscribe(error => {
|
||||
notificationSetup.toasts.addDanger({
|
||||
|
@ -59,13 +61,13 @@ export class NotificationsService {
|
|||
return notificationSetup;
|
||||
}
|
||||
|
||||
public start({ i18n: i18nDep, targetDomElement }: StartDeps): NotificationsStart {
|
||||
public start({ i18n: i18nDep, overlays, targetDomElement }: StartDeps): NotificationsStart {
|
||||
this.targetDomElement = targetDomElement;
|
||||
const toastsContainer = document.createElement('div');
|
||||
targetDomElement.appendChild(toastsContainer);
|
||||
|
||||
return {
|
||||
toasts: this.toasts.start({ i18n: i18nDep, targetDomElement: toastsContainer }),
|
||||
toasts: this.toasts.start({ i18n: i18nDep, overlays, targetDomElement: toastsContainer }),
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -84,8 +86,10 @@ export class NotificationsService {
|
|||
|
||||
/** @public */
|
||||
export interface NotificationsSetup {
|
||||
toasts: ToastsApi;
|
||||
toasts: ToastsSetup;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export type NotificationsStart = NotificationsSetup;
|
||||
export interface NotificationsStart {
|
||||
toasts: ToastsStart;
|
||||
}
|
||||
|
|
29
src/core/public/notifications/toasts/__snapshots__/error_toast.test.tsx.snap
generated
Normal file
29
src/core/public/notifications/toasts/__snapshots__/error_toast.test.tsx.snap
generated
Normal file
|
@ -0,0 +1,29 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`renders matching snapshot 1`] = `
|
||||
<Fragment>
|
||||
<p
|
||||
data-test-subj="errorToastMessage"
|
||||
>
|
||||
This is the toast message
|
||||
</p>
|
||||
<div
|
||||
className="eui-textRight"
|
||||
>
|
||||
<EuiButton
|
||||
color="danger"
|
||||
fill={false}
|
||||
iconSide="left"
|
||||
onClick={[Function]}
|
||||
size="s"
|
||||
type="button"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="See the full error"
|
||||
id="core.toasts.errorToast.seeFullError"
|
||||
values={Object {}}
|
||||
/>
|
||||
</EuiButton>
|
||||
</div>
|
||||
</Fragment>
|
||||
`;
|
|
@ -2,8 +2,9 @@
|
|||
|
||||
exports[`renders matching snapshot 1`] = `
|
||||
<EuiGlobalToastList
|
||||
data-test-subj="globalToastList"
|
||||
dismissToast={[MockFunction]}
|
||||
toastLifeTimeMs={6000}
|
||||
toastLifeTimeMs={Infinity}
|
||||
toasts={Array []}
|
||||
/>
|
||||
`;
|
||||
|
|
64
src/core/public/notifications/toasts/error_toast.test.tsx
Normal file
64
src/core/public/notifications/toasts/error_toast.test.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 { shallow } from 'enzyme';
|
||||
import React from 'react';
|
||||
import { mountWithIntl } from 'test_utils/enzyme_helpers';
|
||||
|
||||
import { ErrorToast } from './error_toast';
|
||||
|
||||
import { i18nServiceMock } from '../../i18n/i18n_service.mock';
|
||||
|
||||
interface ErrorToastProps {
|
||||
error?: Error;
|
||||
title?: string;
|
||||
toastMessage?: string;
|
||||
}
|
||||
|
||||
let openModal: jest.Mock;
|
||||
|
||||
beforeEach(() => (openModal = jest.fn()));
|
||||
|
||||
function render(props: ErrorToastProps = {}) {
|
||||
return (
|
||||
<ErrorToast
|
||||
openModal={openModal}
|
||||
error={props.error || new Error('error message')}
|
||||
title={props.title || 'An error occured'}
|
||||
toastMessage={props.toastMessage || 'This is the toast message'}
|
||||
i18nContext={i18nServiceMock.createSetupContract().Context}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
it('renders matching snapshot', () => {
|
||||
expect(shallow(render())).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should open a modal when clicking button', () => {
|
||||
const wrapper = mountWithIntl(render());
|
||||
expect(openModal).not.toHaveBeenCalled();
|
||||
wrapper.find('button').simulate('click');
|
||||
expect(openModal).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
// Cleanup document.body to cleanup any modals which might be left over from tests.
|
||||
document.body.innerHTML = '';
|
||||
});
|
106
src/core/public/notifications/toasts/error_toast.tsx
Normal file
106
src/core/public/notifications/toasts/error_toast.tsx
Normal file
|
@ -0,0 +1,106 @@
|
|||
/*
|
||||
* 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 {
|
||||
EuiButton,
|
||||
EuiCallOut,
|
||||
EuiCodeBlock,
|
||||
EuiModalBody,
|
||||
EuiModalFooter,
|
||||
EuiModalHeader,
|
||||
EuiModalHeaderTitle,
|
||||
} from '@elastic/eui';
|
||||
import { EuiSpacer } from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
|
||||
import { I18nSetup } from '../../i18n';
|
||||
import { OverlayStart } from '../../overlays';
|
||||
|
||||
interface ErrorToastProps {
|
||||
title: string;
|
||||
error: Error;
|
||||
toastMessage: string;
|
||||
i18nContext: I18nSetup['Context'];
|
||||
openModal: OverlayStart['openModal'];
|
||||
}
|
||||
|
||||
/**
|
||||
* This should instead be replaced by the overlay service once it's available.
|
||||
* This does not use React portals so that if the parent toast times out, this modal
|
||||
* does not disappear. NOTE: this should use a global modal in the overlay service
|
||||
* in the future.
|
||||
*/
|
||||
function showErrorDialog({
|
||||
title,
|
||||
error,
|
||||
i18nContext: I18nContext,
|
||||
openModal,
|
||||
}: Pick<ErrorToastProps, 'error' | 'title' | 'i18nContext' | 'openModal'>) {
|
||||
const modal = openModal(
|
||||
<React.Fragment>
|
||||
<EuiModalHeader>
|
||||
<EuiModalHeaderTitle>{title}</EuiModalHeaderTitle>
|
||||
</EuiModalHeader>
|
||||
<EuiModalBody>
|
||||
<EuiCallOut size="s" color="danger" iconType="alert" title={error.message} />
|
||||
{error.stack && (
|
||||
<React.Fragment>
|
||||
<EuiSpacer size="s" />
|
||||
<EuiCodeBlock isCopyable={true} paddingSize="s">
|
||||
{error.stack}
|
||||
</EuiCodeBlock>
|
||||
</React.Fragment>
|
||||
)}
|
||||
</EuiModalBody>
|
||||
<EuiModalFooter>
|
||||
<EuiButton onClick={() => modal.close()} fill>
|
||||
<FormattedMessage id="core.notifications.errorToast.closeModal" defaultMessage="Close" />
|
||||
</EuiButton>
|
||||
</EuiModalFooter>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
export function ErrorToast({
|
||||
title,
|
||||
error,
|
||||
toastMessage,
|
||||
i18nContext,
|
||||
openModal,
|
||||
}: ErrorToastProps) {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<p data-test-subj="errorToastMessage">{toastMessage}</p>
|
||||
<div className="eui-textRight">
|
||||
<EuiButton
|
||||
size="s"
|
||||
color="danger"
|
||||
onClick={() => showErrorDialog({ title, error, openModal, i18nContext })}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="core.toasts.errorToast.seeFullError"
|
||||
defaultMessage="See the full error"
|
||||
/>
|
||||
</EuiButton>
|
||||
</div>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
|
@ -53,9 +53,15 @@ export class GlobalToastList extends React.Component<Props, State> {
|
|||
public render() {
|
||||
return (
|
||||
<EuiGlobalToastList
|
||||
data-test-subj="globalToastList"
|
||||
toasts={this.state.toasts}
|
||||
dismissToast={this.props.dismissToast}
|
||||
toastLifeTimeMs={6000}
|
||||
/**
|
||||
* This prop is overriden by the individual toasts that are added.
|
||||
* Use `Infinity` here so that it's obvious a timeout hasn't been
|
||||
* provided in development.
|
||||
*/
|
||||
toastLifeTimeMs={Infinity}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -17,6 +17,6 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
export { ToastsService } from './toasts_service';
|
||||
export { ToastsApi, ToastInput } from './toasts_api';
|
||||
export { ToastsService, ToastsSetup, ToastsStart } from './toasts_service';
|
||||
export { ErrorToastOptions, ToastsApi, ToastInput } from './toasts_api';
|
||||
export { Toast } from '@elastic/eui';
|
||||
|
|
|
@ -21,6 +21,9 @@ import { take } from 'rxjs/operators';
|
|||
|
||||
import { ToastsApi } from './toasts_api';
|
||||
|
||||
import { uiSettingsServiceMock } from '../../ui_settings/ui_settings_service.mock';
|
||||
import { i18nServiceMock } from '../../i18n/i18n_service.mock';
|
||||
|
||||
async function getCurrentToasts(toasts: ToastsApi) {
|
||||
return await toasts
|
||||
.get$()
|
||||
|
@ -28,9 +31,33 @@ async function getCurrentToasts(toasts: ToastsApi) {
|
|||
.toPromise();
|
||||
}
|
||||
|
||||
function uiSettingsMock() {
|
||||
const mock = uiSettingsServiceMock.createSetupContract();
|
||||
(mock.get as jest.Mock<typeof mock['get']>).mockImplementation(() => (config: string) => {
|
||||
switch (config) {
|
||||
case 'notifications:lifetime:info':
|
||||
return 5000;
|
||||
case 'notifications:lifetime:warning':
|
||||
return 10000;
|
||||
case 'notification:lifetime:error':
|
||||
return 30000;
|
||||
default:
|
||||
throw new Error(`Accessing ${config} is not supported in the mock.`);
|
||||
}
|
||||
});
|
||||
return mock;
|
||||
}
|
||||
|
||||
function toastDeps() {
|
||||
return {
|
||||
uiSettings: uiSettingsMock(),
|
||||
i18n: i18nServiceMock.createSetupContract(),
|
||||
};
|
||||
}
|
||||
|
||||
describe('#get$()', () => {
|
||||
it('returns observable that emits NEW toast list when something added or removed', () => {
|
||||
const toasts = new ToastsApi();
|
||||
const toasts = new ToastsApi(toastDeps());
|
||||
const onToasts = jest.fn();
|
||||
|
||||
toasts.get$().subscribe(onToasts);
|
||||
|
@ -57,7 +84,7 @@ describe('#get$()', () => {
|
|||
});
|
||||
|
||||
it('does not emit a new toast list when unknown toast is passed to remove()', () => {
|
||||
const toasts = new ToastsApi();
|
||||
const toasts = new ToastsApi(toastDeps());
|
||||
const onToasts = jest.fn();
|
||||
|
||||
toasts.get$().subscribe(onToasts);
|
||||
|
@ -71,14 +98,14 @@ describe('#get$()', () => {
|
|||
|
||||
describe('#add()', () => {
|
||||
it('returns toast objects with auto assigned id', () => {
|
||||
const toasts = new ToastsApi();
|
||||
const toasts = new ToastsApi(toastDeps());
|
||||
const toast = toasts.add({ title: 'foo' });
|
||||
expect(toast).toHaveProperty('id');
|
||||
expect(toast).toHaveProperty('title', 'foo');
|
||||
});
|
||||
|
||||
it('adds the toast to toasts list', async () => {
|
||||
const toasts = new ToastsApi();
|
||||
const toasts = new ToastsApi(toastDeps());
|
||||
const toast = toasts.add({});
|
||||
|
||||
const currentToasts = await getCurrentToasts(toasts);
|
||||
|
@ -87,27 +114,27 @@ describe('#add()', () => {
|
|||
});
|
||||
|
||||
it('increments the toast ID for each additional toast', () => {
|
||||
const toasts = new ToastsApi();
|
||||
const toasts = new ToastsApi(toastDeps());
|
||||
expect(toasts.add({})).toHaveProperty('id', '0');
|
||||
expect(toasts.add({})).toHaveProperty('id', '1');
|
||||
expect(toasts.add({})).toHaveProperty('id', '2');
|
||||
});
|
||||
|
||||
it('accepts a string, uses it as the title', async () => {
|
||||
const toasts = new ToastsApi();
|
||||
const toasts = new ToastsApi(toastDeps());
|
||||
expect(toasts.add('foo')).toHaveProperty('title', 'foo');
|
||||
});
|
||||
});
|
||||
|
||||
describe('#remove()', () => {
|
||||
it('removes a toast', async () => {
|
||||
const toasts = new ToastsApi();
|
||||
const toasts = new ToastsApi(toastDeps());
|
||||
toasts.remove(toasts.add('Test'));
|
||||
expect(await getCurrentToasts(toasts)).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('ignores unknown toast', async () => {
|
||||
const toasts = new ToastsApi();
|
||||
const toasts = new ToastsApi(toastDeps());
|
||||
toasts.add('Test');
|
||||
toasts.remove({ id: 'foo' });
|
||||
|
||||
|
@ -118,12 +145,12 @@ describe('#remove()', () => {
|
|||
|
||||
describe('#addSuccess()', () => {
|
||||
it('adds a success toast', async () => {
|
||||
const toasts = new ToastsApi();
|
||||
const toasts = new ToastsApi(toastDeps());
|
||||
expect(toasts.addSuccess({})).toHaveProperty('color', 'success');
|
||||
});
|
||||
|
||||
it('returns the created toast', async () => {
|
||||
const toasts = new ToastsApi();
|
||||
const toasts = new ToastsApi(toastDeps());
|
||||
const toast = toasts.addSuccess({});
|
||||
const currentToasts = await getCurrentToasts(toasts);
|
||||
expect(currentToasts[0]).toBe(toast);
|
||||
|
@ -132,12 +159,12 @@ describe('#addSuccess()', () => {
|
|||
|
||||
describe('#addWarning()', () => {
|
||||
it('adds a warning toast', async () => {
|
||||
const toasts = new ToastsApi();
|
||||
const toasts = new ToastsApi(toastDeps());
|
||||
expect(toasts.addWarning({})).toHaveProperty('color', 'warning');
|
||||
});
|
||||
|
||||
it('returns the created toast', async () => {
|
||||
const toasts = new ToastsApi();
|
||||
const toasts = new ToastsApi(toastDeps());
|
||||
const toast = toasts.addWarning({});
|
||||
const currentToasts = await getCurrentToasts(toasts);
|
||||
expect(currentToasts[0]).toBe(toast);
|
||||
|
@ -146,14 +173,30 @@ describe('#addWarning()', () => {
|
|||
|
||||
describe('#addDanger()', () => {
|
||||
it('adds a danger toast', async () => {
|
||||
const toasts = new ToastsApi();
|
||||
const toasts = new ToastsApi(toastDeps());
|
||||
expect(toasts.addDanger({})).toHaveProperty('color', 'danger');
|
||||
});
|
||||
|
||||
it('returns the created toast', async () => {
|
||||
const toasts = new ToastsApi();
|
||||
const toasts = new ToastsApi(toastDeps());
|
||||
const toast = toasts.addDanger({});
|
||||
const currentToasts = await getCurrentToasts(toasts);
|
||||
expect(currentToasts[0]).toBe(toast);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#addError', () => {
|
||||
it('adds an error toast', async () => {
|
||||
const toasts = new ToastsApi(toastDeps());
|
||||
const toast = toasts.addError(new Error('unexpected error'), { title: 'Something went wrong' });
|
||||
expect(toast).toHaveProperty('color', 'danger');
|
||||
expect(toast).toHaveProperty('title', 'Something went wrong');
|
||||
});
|
||||
|
||||
it('returns the created toast', async () => {
|
||||
const toasts = new ToastsApi(toastDeps());
|
||||
const toast = toasts.addError(new Error('unexpected error'), { title: 'Something went wrong' });
|
||||
const currentToasts = await getCurrentToasts(toasts);
|
||||
expect(currentToasts[0]).toBe(toast);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -18,10 +18,32 @@
|
|||
*/
|
||||
|
||||
import { Toast } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import * as Rx from 'rxjs';
|
||||
|
||||
import { ErrorToast } from './error_toast';
|
||||
import { UiSettingsSetup } from '../../ui_settings';
|
||||
import { I18nSetup } from '../../i18n';
|
||||
import { OverlayStart } from '../../overlays';
|
||||
|
||||
type ToastInputFields = Pick<Toast, Exclude<keyof Toast, 'id'>>;
|
||||
|
||||
/** @public */
|
||||
export type ToastInput = string | Pick<Toast, Exclude<keyof Toast, 'id'>>;
|
||||
export type ToastInput = string | ToastInputFields | Promise<ToastInputFields>;
|
||||
|
||||
export interface ErrorToastOptions {
|
||||
/**
|
||||
* The title of the toast and the dialog when expanding the message.
|
||||
*/
|
||||
title: string;
|
||||
/**
|
||||
* The message to be shown in the toast. If this is not specified the error's
|
||||
* message will be shown in the toast instead. Overwriting that message can
|
||||
* be used to provide more user-friendly toasts. If you specify this, the error
|
||||
* message will still be shown in the detailed error modal.
|
||||
*/
|
||||
toastMessage?: string;
|
||||
}
|
||||
|
||||
const normalizeToast = (toastOrTitle: ToastInput) => {
|
||||
if (typeof toastOrTitle === 'string') {
|
||||
|
@ -37,6 +59,19 @@ const normalizeToast = (toastOrTitle: ToastInput) => {
|
|||
export class ToastsApi {
|
||||
private toasts$ = new Rx.BehaviorSubject<Toast[]>([]);
|
||||
private idCounter = 0;
|
||||
private uiSettings: UiSettingsSetup;
|
||||
private i18n: I18nSetup;
|
||||
|
||||
private overlays?: OverlayStart;
|
||||
|
||||
constructor(deps: { uiSettings: UiSettingsSetup; i18n: I18nSetup }) {
|
||||
this.uiSettings = deps.uiSettings;
|
||||
this.i18n = deps.i18n;
|
||||
}
|
||||
|
||||
public registerOverlays(overlays: OverlayStart) {
|
||||
this.overlays = overlays;
|
||||
}
|
||||
|
||||
public get$() {
|
||||
return this.toasts$.asObservable();
|
||||
|
@ -45,6 +80,7 @@ export class ToastsApi {
|
|||
public add(toastOrTitle: ToastInput) {
|
||||
const toast: Toast = {
|
||||
id: String(this.idCounter++),
|
||||
toastLifeTimeMs: this.uiSettings.get('notifications:lifetime:info'),
|
||||
...normalizeToast(toastOrTitle),
|
||||
};
|
||||
|
||||
|
@ -73,6 +109,7 @@ export class ToastsApi {
|
|||
return this.add({
|
||||
color: 'warning',
|
||||
iconType: 'help',
|
||||
toastLifeTimeMs: this.uiSettings.get('notifications:lifetime:warning'),
|
||||
...normalizeToast(toastOrTitle),
|
||||
});
|
||||
}
|
||||
|
@ -81,7 +118,39 @@ export class ToastsApi {
|
|||
return this.add({
|
||||
color: 'danger',
|
||||
iconType: 'alert',
|
||||
toastLifeTimeMs: this.uiSettings.get('notifications:lifetime:warning'),
|
||||
...normalizeToast(toastOrTitle),
|
||||
});
|
||||
}
|
||||
|
||||
public addError(error: Error, options: ErrorToastOptions) {
|
||||
const message = options.toastMessage || error.message;
|
||||
return this.add({
|
||||
color: 'danger',
|
||||
iconType: 'alert',
|
||||
title: options.title,
|
||||
toastLifeTimeMs: this.uiSettings.get('notifications:lifetime:error'),
|
||||
text: (
|
||||
<ErrorToast
|
||||
openModal={this.openModal.bind(this)}
|
||||
error={error}
|
||||
title={options.title}
|
||||
toastMessage={message}
|
||||
i18nContext={this.i18n.Context}
|
||||
/>
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
private openModal(
|
||||
...args: Parameters<OverlayStart['openModal']>
|
||||
): ReturnType<OverlayStart['openModal']> {
|
||||
if (!this.overlays) {
|
||||
// This case should never happen because no rendering should be occurring
|
||||
// before the ToastService is started.
|
||||
throw new Error(`Modal opened before ToastService was started.`);
|
||||
}
|
||||
|
||||
return this.overlays.openModal(...args);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,16 +16,17 @@
|
|||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { ToastsApi } from './toasts_api';
|
||||
import { ToastsSetup } from './toasts_service';
|
||||
|
||||
const createToastsApiMock = () => {
|
||||
const api: jest.Mocked<PublicMethodsOf<ToastsApi>> = {
|
||||
const api: jest.Mocked<PublicMethodsOf<ToastsSetup>> = {
|
||||
get$: jest.fn(),
|
||||
add: jest.fn(),
|
||||
remove: jest.fn(),
|
||||
addSuccess: jest.fn(),
|
||||
addWarning: jest.fn(),
|
||||
addDanger: jest.fn(),
|
||||
addError: jest.fn(),
|
||||
};
|
||||
return api;
|
||||
};
|
||||
|
|
|
@ -21,6 +21,8 @@ import { mockReactDomRender, mockReactDomUnmount } from './toasts_service.test.m
|
|||
|
||||
import { ToastsService } from './toasts_service';
|
||||
import { ToastsApi } from './toasts_api';
|
||||
import { overlayServiceMock } from '../../overlays/overlay_service.mock';
|
||||
import { uiSettingsServiceMock } from '../../ui_settings/ui_settings_service.mock';
|
||||
|
||||
const mockI18n: any = {
|
||||
Context: function I18nContext() {
|
||||
|
@ -28,11 +30,15 @@ const mockI18n: any = {
|
|||
},
|
||||
};
|
||||
|
||||
const mockOverlays = overlayServiceMock.createStartContract();
|
||||
|
||||
describe('#setup()', () => {
|
||||
it('returns a ToastsApi', () => {
|
||||
const toasts = new ToastsService();
|
||||
|
||||
expect(toasts.setup()).toBeInstanceOf(ToastsApi);
|
||||
expect(
|
||||
toasts.setup({ i18n: mockI18n, uiSettings: uiSettingsServiceMock.createSetupContract() })
|
||||
).toBeInstanceOf(ToastsApi);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -43,8 +49,8 @@ describe('#start()', () => {
|
|||
const toasts = new ToastsService();
|
||||
|
||||
expect(mockReactDomRender).not.toHaveBeenCalled();
|
||||
toasts.setup();
|
||||
toasts.start({ i18n: mockI18n, targetDomElement });
|
||||
toasts.setup({ i18n: mockI18n, uiSettings: uiSettingsServiceMock.createSetupContract() });
|
||||
toasts.start({ i18n: mockI18n, targetDomElement, overlays: mockOverlays });
|
||||
expect(mockReactDomRender.mock.calls).toMatchSnapshot();
|
||||
});
|
||||
|
||||
|
@ -52,8 +58,12 @@ describe('#start()', () => {
|
|||
const targetDomElement = document.createElement('div');
|
||||
const toasts = new ToastsService();
|
||||
|
||||
toasts.setup();
|
||||
expect(toasts.start({ i18n: mockI18n, targetDomElement })).toBeInstanceOf(ToastsApi);
|
||||
expect(
|
||||
toasts.setup({ i18n: mockI18n, uiSettings: uiSettingsServiceMock.createSetupContract() })
|
||||
).toBeInstanceOf(ToastsApi);
|
||||
expect(
|
||||
toasts.start({ i18n: mockI18n, targetDomElement, overlays: mockOverlays })
|
||||
).toBeInstanceOf(ToastsApi);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -63,8 +73,8 @@ describe('#stop()', () => {
|
|||
targetDomElement.setAttribute('test', 'target-dom-element');
|
||||
const toasts = new ToastsService();
|
||||
|
||||
toasts.setup();
|
||||
toasts.start({ i18n: mockI18n, targetDomElement });
|
||||
toasts.setup({ i18n: mockI18n, uiSettings: uiSettingsServiceMock.createSetupContract() });
|
||||
toasts.start({ i18n: mockI18n, targetDomElement, overlays: mockOverlays });
|
||||
|
||||
expect(mockReactDomUnmount).not.toHaveBeenCalled();
|
||||
toasts.stop();
|
||||
|
@ -82,8 +92,8 @@ describe('#stop()', () => {
|
|||
const targetDomElement = document.createElement('div');
|
||||
const toasts = new ToastsService();
|
||||
|
||||
toasts.setup();
|
||||
toasts.start({ i18n: mockI18n, targetDomElement });
|
||||
toasts.setup({ i18n: mockI18n, uiSettings: uiSettingsServiceMock.createSetupContract() });
|
||||
toasts.start({ i18n: mockI18n, targetDomElement, overlays: mockOverlays });
|
||||
toasts.stop();
|
||||
expect(targetDomElement.childNodes).toHaveLength(0);
|
||||
});
|
||||
|
|
|
@ -21,25 +21,40 @@ import React from 'react';
|
|||
import { render, unmountComponentAtNode } from 'react-dom';
|
||||
|
||||
import { Toast } from '@elastic/eui';
|
||||
import { I18nSetup } from '../../i18n';
|
||||
import { I18nSetup, I18nStart } from '../../i18n';
|
||||
import { UiSettingsSetup } from '../../ui_settings';
|
||||
import { GlobalToastList } from './global_toast_list';
|
||||
import { ToastsApi } from './toasts_api';
|
||||
import { OverlayStart } from '../../overlays';
|
||||
|
||||
interface SetupDeps {
|
||||
i18n: I18nSetup;
|
||||
uiSettings: UiSettingsSetup;
|
||||
}
|
||||
|
||||
interface StartDeps {
|
||||
i18n: I18nSetup;
|
||||
i18n: I18nStart;
|
||||
overlays: OverlayStart;
|
||||
targetDomElement: HTMLElement;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export type ToastsSetup = Pick<ToastsApi, Exclude<keyof ToastsApi, 'registerOverlays'>>;
|
||||
|
||||
/** @public */
|
||||
export type ToastsStart = ToastsSetup;
|
||||
|
||||
export class ToastsService {
|
||||
private api?: ToastsApi;
|
||||
private targetDomElement?: HTMLElement;
|
||||
|
||||
public setup() {
|
||||
this.api = new ToastsApi();
|
||||
public setup({ i18n, uiSettings }: SetupDeps) {
|
||||
this.api = new ToastsApi({ i18n, uiSettings });
|
||||
return this.api!;
|
||||
}
|
||||
|
||||
public start({ i18n, targetDomElement }: StartDeps) {
|
||||
public start({ i18n, overlays, targetDomElement }: StartDeps) {
|
||||
this.api!.registerOverlays(overlays);
|
||||
this.targetDomElement = targetDomElement;
|
||||
|
||||
render(
|
||||
|
|
|
@ -152,6 +152,14 @@ export class CoreSystem {
|
|||
stop(): void;
|
||||
}
|
||||
|
||||
// Warning: (ae-missing-release-tag) "ErrorToastOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public (undocumented)
|
||||
export interface ErrorToastOptions {
|
||||
title: string;
|
||||
toastMessage?: string;
|
||||
}
|
||||
|
||||
// @public
|
||||
export interface FatalErrorInfo {
|
||||
// (undocumented)
|
||||
|
@ -254,12 +262,19 @@ export interface LegacyNavLink {
|
|||
|
||||
// @public (undocumented)
|
||||
export interface NotificationsSetup {
|
||||
// Warning: (ae-forgotten-export) The symbol "ToastsSetup" needs to be exported by the entry point index.d.ts
|
||||
//
|
||||
// (undocumented)
|
||||
toasts: ToastsApi;
|
||||
toasts: ToastsSetup;
|
||||
}
|
||||
|
||||
// @public (undocumented)
|
||||
export type NotificationsStart = NotificationsSetup;
|
||||
export interface NotificationsStart {
|
||||
// Warning: (ae-forgotten-export) The symbol "ToastsStart" needs to be exported by the entry point index.d.ts
|
||||
//
|
||||
// (undocumented)
|
||||
toasts: ToastsStart;
|
||||
}
|
||||
|
||||
// Warning: (ae-missing-release-tag) "OverlayRef" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
|
@ -304,22 +319,32 @@ export interface PluginInitializerContext {
|
|||
|
||||
export { Toast }
|
||||
|
||||
// Warning: (ae-forgotten-export) The symbol "ToastInputFields" needs to be exported by the entry point index.d.ts
|
||||
//
|
||||
// @public (undocumented)
|
||||
export type ToastInput = string | Pick<Toast, Exclude<keyof Toast, 'id'>>;
|
||||
export type ToastInput = string | ToastInputFields | Promise<ToastInputFields>;
|
||||
|
||||
// @public (undocumented)
|
||||
export class ToastsApi {
|
||||
constructor(deps: {
|
||||
uiSettings: UiSettingsSetup;
|
||||
i18n: I18nSetup;
|
||||
});
|
||||
// (undocumented)
|
||||
add(toastOrTitle: ToastInput): Toast;
|
||||
// (undocumented)
|
||||
addDanger(toastOrTitle: ToastInput): Toast;
|
||||
// (undocumented)
|
||||
addError(error: Error, options: ErrorToastOptions): Toast;
|
||||
// (undocumented)
|
||||
addSuccess(toastOrTitle: ToastInput): Toast;
|
||||
// (undocumented)
|
||||
addWarning(toastOrTitle: ToastInput): Toast;
|
||||
// (undocumented)
|
||||
get$(): Rx.Observable<Toast[]>;
|
||||
// (undocumented)
|
||||
registerOverlays(overlays: OverlayStart): void;
|
||||
// (undocumented)
|
||||
remove(toast: Toast): void;
|
||||
}
|
||||
|
||||
|
|
|
@ -185,7 +185,6 @@ function discoverController(
|
|||
$timeout,
|
||||
$window,
|
||||
AppState,
|
||||
Notifier,
|
||||
Private,
|
||||
Promise,
|
||||
config,
|
||||
|
@ -201,9 +200,6 @@ function discoverController(
|
|||
const queryFilter = Private(FilterBarQueryFilterProvider);
|
||||
const responseHandler = Private(VislibSeriesResponseHandlerProvider).handler;
|
||||
const filterManager = Private(FilterManagerProvider);
|
||||
const notify = new Notifier({
|
||||
location: 'Discover'
|
||||
});
|
||||
const getUnhashableStates = Private(getUnhashableStatesProvider);
|
||||
const shareContextMenuExtensions = Private(ShareContextMenuExtensionsRegistryProvider);
|
||||
const inspectorAdapters = {
|
||||
|
@ -718,7 +714,13 @@ function discoverController(
|
|||
logInspectorRequest();
|
||||
return courier.fetch();
|
||||
})
|
||||
.catch(notify.error);
|
||||
.catch((error) => {
|
||||
toastNotifications.addError(error, {
|
||||
title: i18n.translate('kbn.discover.discoverError', {
|
||||
defaultMessage: 'Discover error',
|
||||
}),
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
$scope.updateQueryAndFetch = function ({ query, dateRange }) {
|
||||
|
@ -799,7 +801,11 @@ function discoverController(
|
|||
if (fetchError) {
|
||||
$scope.fetchError = fetchError;
|
||||
} else {
|
||||
notify.error(error);
|
||||
toastNotifications.addError(error, {
|
||||
title: i18n.translate('kbn.discover.errorLoadingData', {
|
||||
defaultMessage: 'Error loading data',
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
// Restart. This enables auto-refresh functionality.
|
||||
|
|
|
@ -19,13 +19,35 @@
|
|||
|
||||
import sinon from 'sinon';
|
||||
import { ToastsApi } from '../../../../../core/public';
|
||||
|
||||
import { uiSettingsServiceMock, i18nServiceMock } from '../../../../../core/public/mocks';
|
||||
import { ToastNotifications } from './toast_notifications';
|
||||
|
||||
function toastDeps() {
|
||||
const uiSettingsMock = uiSettingsServiceMock.createSetupContract();
|
||||
(uiSettingsMock.get as jest.Mock<typeof uiSettingsMock['get']>).mockImplementation(
|
||||
() => (config: string) => {
|
||||
switch (config) {
|
||||
case 'notifications:lifetime:info':
|
||||
return 5000;
|
||||
case 'notifications:lifetime:warning':
|
||||
return 10000;
|
||||
case 'notification:lifetime:error':
|
||||
return 30000;
|
||||
default:
|
||||
throw new Error(`Accessing ${config} is not supported in the mock.`);
|
||||
}
|
||||
}
|
||||
);
|
||||
return {
|
||||
uiSettings: uiSettingsMock,
|
||||
i18n: i18nServiceMock.createSetupContract(),
|
||||
};
|
||||
}
|
||||
|
||||
describe('ToastNotifications', () => {
|
||||
describe('interface', () => {
|
||||
function setup() {
|
||||
return { toastNotifications: new ToastNotifications(new ToastsApi()) };
|
||||
return { toastNotifications: new ToastNotifications(new ToastsApi(toastDeps())) };
|
||||
}
|
||||
|
||||
describe('add method', () => {
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
import { Toast, ToastInput, ToastsApi } from '../../../../../core/public';
|
||||
import { ErrorToastOptions, Toast, ToastInput, ToastsApi } from '../../../../../core/public';
|
||||
|
||||
export { Toast, ToastInput };
|
||||
|
||||
|
@ -45,4 +45,6 @@ export class ToastNotifications {
|
|||
public addSuccess = (toastOrTitle: ToastInput) => this.toasts.addSuccess(toastOrTitle);
|
||||
public addWarning = (toastOrTitle: ToastInput) => this.toasts.addWarning(toastOrTitle);
|
||||
public addDanger = (toastOrTitle: ToastInput) => this.toasts.addDanger(toastOrTitle);
|
||||
public addError = (error: Error, options: ErrorToastOptions) =>
|
||||
this.toasts.addError(error, options);
|
||||
}
|
||||
|
|
|
@ -20,9 +20,6 @@
|
|||
import Wreck from '@hapi/wreck';
|
||||
import { get } from 'lodash';
|
||||
|
||||
const MINUTE = 60 * 1000;
|
||||
const HOUR = 60 * MINUTE;
|
||||
|
||||
export class KibanaServerUiSettings {
|
||||
constructor(url, log, defaults, lifecycle) {
|
||||
this._log = log;
|
||||
|
@ -51,23 +48,6 @@ export class KibanaServerUiSettings {
|
|||
return defaultIndex;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the auto-hide timeout to 1 hour so that auto-hide is
|
||||
* effectively disabled. This gives the tests more time to
|
||||
* interact with the notifications without having to worry about
|
||||
* them disappearing if the tests are too slow.
|
||||
*
|
||||
* @return {Promise<undefined>}
|
||||
*/
|
||||
async disableToastAutohide() {
|
||||
await this.update({
|
||||
'notifications:lifetime:banner': HOUR,
|
||||
'notifications:lifetime:error': HOUR,
|
||||
'notifications:lifetime:warning': HOUR,
|
||||
'notifications:lifetime:info': HOUR,
|
||||
});
|
||||
}
|
||||
|
||||
async replace(doc) {
|
||||
const { payload } = await this._wreck.get('/api/kibana/settings');
|
||||
|
||||
|
|
|
@ -66,9 +66,7 @@ export default function ({ getService, getPageObjects, updateBaselines }) {
|
|||
await PageObjects.dashboard.clickNewDashboard();
|
||||
await PageObjects.dashboard.setTimepickerInLogstashDataRange();
|
||||
await dashboardAddPanel.addVisualization('Rendering Test: area with not filter');
|
||||
await PageObjects.common.closeToast();
|
||||
await PageObjects.dashboard.saveDashboard('area');
|
||||
await PageObjects.common.closeToast();
|
||||
|
||||
await PageObjects.dashboard.clickFullScreenMode();
|
||||
await dashboardPanelActions.openContextMenu();
|
||||
|
|
|
@ -25,7 +25,6 @@ import {
|
|||
} from '../../../../src/legacy/core_plugins/kibana/public/visualize/visualize_constants';
|
||||
|
||||
export default function ({ getService, getPageObjects }) {
|
||||
const kibanaServer = getService('kibanaServer');
|
||||
const browser = getService('browser');
|
||||
const dashboardPanelActions = getService('dashboardPanelActions');
|
||||
const dashboardAddPanel = getService('dashboardAddPanel');
|
||||
|
@ -35,7 +34,6 @@ export default function ({ getService, getPageObjects }) {
|
|||
describe('dashboard panel controls', function viewEditModeTests() {
|
||||
before(async function () {
|
||||
await PageObjects.dashboard.initTests();
|
||||
await kibanaServer.uiSettings.disableToastAutohide();
|
||||
await browser.refresh();
|
||||
|
||||
// This flip between apps fixes the url so state is preserved when switching apps in test mode.
|
||||
|
|
|
@ -23,6 +23,7 @@ export default function ({ getService, getPageObjects }) {
|
|||
const retry = getService('retry');
|
||||
const esArchiver = getService('esArchiver');
|
||||
const kibanaServer = getService('kibanaServer');
|
||||
const toasts = getService('toasts');
|
||||
const queryBar = getService('queryBar');
|
||||
const PageObjects = getPageObjects(['common', 'header', 'discover', 'visualize', 'timePicker']);
|
||||
|
||||
|
@ -93,13 +94,13 @@ export default function ({ getService, getPageObjects }) {
|
|||
});
|
||||
|
||||
it('a bad syntax query should show an error message', async function () {
|
||||
const expectedError = 'Discover: Expected ":", "<", "<=", ">", ">=", AND, OR, end of input, ' +
|
||||
const expectedError = 'Expected ":", "<", "<=", ">", ">=", AND, OR, end of input, ' +
|
||||
'whitespace but "(" found.';
|
||||
await queryBar.setQuery('xxx(yyy))');
|
||||
await queryBar.submitQuery();
|
||||
const toastMessage = await PageObjects.header.getToastMessage();
|
||||
expect(toastMessage).to.contain(expectedError);
|
||||
await PageObjects.header.clickToastOK();
|
||||
const { message } = await toasts.getErrorToast();
|
||||
expect(message).to.contain(expectedError);
|
||||
await toasts.dismissToast();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -24,7 +24,6 @@ export default function ({ getService, getPageObjects }) {
|
|||
const browser = getService('browser');
|
||||
const PageObjects = getPageObjects(['common', 'home', 'timePicker']);
|
||||
const appsMenu = getService('appsMenu');
|
||||
const kibanaServer = getService('kibanaServer');
|
||||
const esArchiver = getService('esArchiver');
|
||||
const retry = getService('retry');
|
||||
const fromTime = '2015-09-19 06:31:44.000';
|
||||
|
@ -34,7 +33,6 @@ export default function ({ getService, getPageObjects }) {
|
|||
|
||||
before(async () => {
|
||||
await esArchiver.loadIfNeeded('makelogs');
|
||||
await kibanaServer.uiSettings.disableToastAutohide();
|
||||
await browser.refresh();
|
||||
});
|
||||
|
||||
|
|
|
@ -54,7 +54,6 @@ export function DashboardPageProvider({ getService, getPageObjects }) {
|
|||
'defaultIndex': defaultIndex,
|
||||
});
|
||||
await this.selectDefaultIndex(defaultIndex);
|
||||
await kibanaServer.uiSettings.disableToastAutohide();
|
||||
await PageObjects.common.navigateToApp('dashboard');
|
||||
}
|
||||
|
||||
|
|
|
@ -53,6 +53,7 @@ import { SnapshotsProvider } from './snapshots';
|
|||
// @ts-ignore not TS yet
|
||||
import { TableProvider } from './table';
|
||||
import { TestSubjectsProvider } from './test_subjects';
|
||||
import { ToastsProvider } from './toasts';
|
||||
// @ts-ignore not TS yet
|
||||
import { PieChartProvider } from './visualizations';
|
||||
// @ts-ignore not TS yet
|
||||
|
@ -83,4 +84,5 @@ export const services = {
|
|||
inspector: InspectorProvider,
|
||||
appsMenu: AppsMenuProvider,
|
||||
globalNav: GlobalNavProvider,
|
||||
toasts: ToastsProvider,
|
||||
};
|
||||
|
|
67
test/functional/services/toasts.ts
Normal file
67
test/functional/services/toasts.ts
Normal file
|
@ -0,0 +1,67 @@
|
|||
/*
|
||||
* 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 { FtrProviderContext } from '../ftr_provider_context';
|
||||
|
||||
export function ToastsProvider({ getService }: FtrProviderContext) {
|
||||
const testSubjects = getService('testSubjects');
|
||||
|
||||
class Toasts {
|
||||
/**
|
||||
* Returns the title and message of a specific error toast.
|
||||
* This method is specific to toasts created via `.addError` since they contain
|
||||
* an additional button, that should not be part of the message.
|
||||
*
|
||||
* @param index The index of the toast (1-based, NOT 0-based!) of the toast. Use first by default.
|
||||
* @returns The title and message of the specified error toast.https://github.com/elastic/kibana/issues/17087
|
||||
*/
|
||||
public async getErrorToast(index: number = 1) {
|
||||
const toast = await this.getToastElement(index);
|
||||
const titleElement = await testSubjects.findDescendant('euiToastHeader', toast);
|
||||
const title: string = await titleElement.getVisibleText();
|
||||
const messageElement = await testSubjects.findDescendant('errorToastMessage', toast);
|
||||
const message: string = await messageElement.getVisibleText();
|
||||
return { title, message };
|
||||
}
|
||||
|
||||
/**
|
||||
* Dismiss a specific toast from the toast list. Since toasts usually should time out themselves,
|
||||
* you only need to call this for permanent toasts (e.g. error toasts).
|
||||
*
|
||||
* @param index The 1-based index of the toast to dismiss. Use first by default.
|
||||
*/
|
||||
public async dismissToast(index: number = 1) {
|
||||
const toast = await this.getToastElement(index);
|
||||
await toast.moveMouseTo();
|
||||
const dismissButton = await testSubjects.findDescendant('toastCloseButton', toast);
|
||||
await dismissButton.click();
|
||||
}
|
||||
|
||||
private async getToastElement(index: number) {
|
||||
const list = await this.getGlobalToastList();
|
||||
return await list.findByCssSelector(`.euiToast:nth-child(${index})`);
|
||||
}
|
||||
|
||||
private async getGlobalToastList() {
|
||||
return await testSubjects.find('globalToastList');
|
||||
}
|
||||
}
|
||||
|
||||
return new Toasts();
|
||||
}
|
|
@ -39,7 +39,6 @@ export default function ({ getService, getPageObjects }) {
|
|||
await kibanaServer.uiSettings.replace({
|
||||
'defaultIndex': 'logstash-*'
|
||||
});
|
||||
await kibanaServer.uiSettings.disableToastAutohide();
|
||||
browser.setWindowSize(1600, 1000);
|
||||
|
||||
await PageObjects.common.navigateToApp('discover');
|
||||
|
|
|
@ -17,7 +17,6 @@ export default function ({ loadTestFile, getService }) {
|
|||
await kibanaServer.uiSettings.replace({
|
||||
'defaultIndex': 'logstash-*'
|
||||
});
|
||||
await kibanaServer.uiSettings.disableToastAutohide();
|
||||
browser.setWindowSize(1600, 1000);
|
||||
|
||||
});
|
||||
|
|
|
@ -12,7 +12,6 @@ export function SecurityPageProvider({ getService, getPageObjects }) {
|
|||
const retry = getService('retry');
|
||||
const find = getService('find');
|
||||
const log = getService('log');
|
||||
const kibanaServer = getService('kibanaServer');
|
||||
const testSubjects = getService('testSubjects');
|
||||
const esArchiver = getService('esArchiver');
|
||||
const userMenu = getService('userMenu');
|
||||
|
@ -76,7 +75,6 @@ export function SecurityPageProvider({ getService, getPageObjects }) {
|
|||
async initTests() {
|
||||
log.debug('SecurityPage:initTests');
|
||||
await esArchiver.load('empty_kibana');
|
||||
await kibanaServer.uiSettings.disableToastAutohide();
|
||||
await esArchiver.loadIfNeeded('logstash_functional');
|
||||
browser.setWindowSize(1600, 1000);
|
||||
}
|
||||
|
|
|
@ -17,7 +17,6 @@ const REPORTS_FOLDER = path.resolve(__dirname, 'reports');
|
|||
|
||||
export default function ({ getService, getPageObjects }) {
|
||||
const retry = getService('retry');
|
||||
const kibanaServer = getService('kibanaServer');
|
||||
const config = getService('config');
|
||||
const PageObjects = getPageObjects(['reporting', 'common', 'dashboard', 'header', 'discover', 'visualize']);
|
||||
const log = getService('log');
|
||||
|
@ -25,7 +24,6 @@ export default function ({ getService, getPageObjects }) {
|
|||
describe('Reporting', () => {
|
||||
|
||||
before('initialize tests', async () => {
|
||||
await kibanaServer.uiSettings.disableToastAutohide();
|
||||
await PageObjects.reporting.initTests();
|
||||
});
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue