[Discover] Inline data fetching errors (#152311)

## Summary

**Note: If your team's review is requested on this PR related to
notification service changes, it's likely because somewhere in your
codebase `NotificationsSetup` was being passed to a function/component
requiring `NotificationsStart` or vice versa. The interfaces were
previously identical so it didn't matter, but an additional function was
added to `NotificationsStart` in this PR that created type errors which
needed to be resolved.**

This PR updates Discover to use inline error messages for data fetching
errors instead of toasts.

For most error messages, the title and full message will be displayed
inline as well as a "Show details" button which opens the standard error
dialog. For errors which have special handling and render custom content
instead of just the error message, the custom content will be rendered
in the callout body for the "No results" state, or rendered in a popover
when clicking the "Show details" button for the banner-style error
display.

No results with regular error message:
<img width="1825" alt="no_results"
src="https://user-images.githubusercontent.com/25592674/224198715-a9131e5c-4bc6-436f-a40a-b1596d44a60c.png">

Banner-style with regular error message:
<img width="1820" alt="inline"
src="https://user-images.githubusercontent.com/25592674/223456957-20690161-3df6-4954-9a49-96a71c01b74b.png">

Banner-style with regular error message in mobile:
<img width="404" alt="inline_mobile"
src="https://user-images.githubusercontent.com/25592674/223457106-a09c3d1e-2f66-4060-9888-21edb8504d70.png">

Standard error dialog:
<img width="1363" alt="error_dialog"
src="https://user-images.githubusercontent.com/25592674/223457178-ec9c2280-4df1-444a-94c7-a8d0580a8f9e.png">

No results with overridden content:
<img width="1705" alt="no_results_override"
src="https://user-images.githubusercontent.com/25592674/224198770-fea0ed31-ce15-4c77-bcb4-4f14c696c4d4.png">

Banner-style with overridden content modal:
<img width="1368" alt="modal"
src="https://user-images.githubusercontent.com/25592674/224198816-cbbe8765-a665-472f-8542-1ac108f2c100.png">

Resolves #149488.

### Checklist

- [x] Any text added follows [EUI's writing
guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses
sentence case text and includes [i18n
support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)
- [ ]
~[Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html)
was added for features that require explanation or tutorials~
- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios
- [x] Any UI touched in this PR is usable by keyboard only (learn more
about [keyboard accessibility](https://webaim.org/techniques/keyboard/))
- [ ] ~Any UI touched in this PR does not create any new axe failures
(run axe in browser:
[FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/),
[Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US))~
- [ ] ~If a plugin configuration key changed, check if it needs to be
allowlisted in the cloud and added to the [docker
list](https://github.com/elastic/kibana/blob/main/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker)~
- [x] This renders correctly on smaller devices using a responsive
layout. (You can test this [in your
browser](https://www.browserstack.com/guide/responsive-testing-on-local-server))
- [x] This was checked for [cross-browser
compatibility](https://www.elastic.co/support/matrix#matrix_browsers)

### For maintainers

- [ ] This was checked for breaking API changes and was [labeled
appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Davis McPhee 2023-03-20 20:48:43 -03:00 committed by GitHub
parent 2bd3e8dd20
commit 26f0f522b2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
47 changed files with 533 additions and 177 deletions

View file

@ -15,7 +15,7 @@ import type { IUiSettingsClient } from '@kbn/core-ui-settings-browser';
import type { OverlayStart } from '@kbn/core-overlays-browser';
import type { NotificationsSetup, NotificationsStart } from '@kbn/core-notifications-browser';
import type { PublicMethodsOf } from '@kbn/utility-types';
import { ToastsService } from './toasts';
import { showErrorDialog, ToastsService } from './toasts';
export interface SetupDeps {
uiSettings: IUiSettingsClient;
@ -70,6 +70,13 @@ export class NotificationsService {
theme,
targetDomElement: toastsContainer,
}),
showErrorDialog: ({ title, error }) =>
showErrorDialog({
title,
error,
openModal: overlays.openModal,
i18nContext: () => i18nDep.Context,
}),
};
}

View file

@ -48,7 +48,7 @@ const isRequestError = (e: Error | RequestError): e is RequestError => {
* does not disappear. NOTE: this should use a global modal in the overlay service
* in the future.
*/
function showErrorDialog({
export function showErrorDialog({
title,
error,
openModal,

View file

@ -8,3 +8,4 @@
export { ToastsService } from './toasts_service';
export type { ToastsApi } from './toasts_api';
export { showErrorDialog } from './error_toast';

View file

@ -6,7 +6,7 @@
* Side Public License, v 1.
*/
import type { MockedKeys } from '@kbn/utility-types-jest';
import type { DeeplyMockedKeys, MockedKeys } from '@kbn/utility-types-jest';
import type { NotificationsSetup, NotificationsStart } from '@kbn/core-notifications-browser';
import type { NotificationsServiceContract } from '@kbn/core-notifications-browser-internal';
import { toastsServiceMock } from './toasts_service.mock';
@ -20,9 +20,10 @@ const createSetupContractMock = () => {
};
const createStartContractMock = () => {
const startContract: MockedKeys<NotificationsStart> = {
const startContract: DeeplyMockedKeys<NotificationsStart> = {
// we have to suppress type errors until decide how to mock es6 class
toasts: toastsServiceMock.createStartContract(),
showErrorDialog: jest.fn(),
};
return startContract;
};

View file

@ -30,4 +30,5 @@ export interface NotificationsSetup {
export interface NotificationsStart {
/** {@link ToastsStart} */
toasts: ToastsStart;
showErrorDialog: (options: { title: string; error: Error }) => void;
}

View file

@ -17,5 +17,6 @@ export const notificationsServiceFactory: NotificationsServiceFactory = () => {
return {
toasts: pluginMock.toasts,
showErrorDialog: pluginMock.showErrorDialog,
};
};

View file

@ -17,10 +17,11 @@ export type NotificationsServiceFactory = KibanaPluginServiceFactory<
export const notificationsServiceFactory: NotificationsServiceFactory = ({ coreStart }) => {
const {
notifications: { toasts },
notifications: { toasts, showErrorDialog },
} = coreStart;
return {
toasts,
showErrorDialog,
};
};

View file

@ -10,4 +10,5 @@ import type { CoreStart } from '@kbn/core/public';
export interface DashboardNotificationsService {
toasts: CoreStart['notifications']['toasts'];
showErrorDialog: CoreStart['notifications']['showErrorDialog'];
}

View file

@ -181,6 +181,7 @@ export {
SEARCH_SESSIONS_MANAGEMENT_ID,
waitUntilNextSessionCompletes$,
isEsError,
getSearchErrorOverrideDisplay,
SearchSource,
SearchSessionState,
SortDirection,

View file

@ -57,6 +57,7 @@ export { getEsPreference } from './es_search';
export type { SearchInterceptorDeps } from './search_interceptor';
export { SearchInterceptor } from './search_interceptor';
export { getSearchErrorOverrideDisplay } from './search_interceptor/utils';
export * from './errors';
export { SearchService } from './search_service';

View file

@ -23,11 +23,15 @@ import { BehaviorSubject } from 'rxjs';
import { dataPluginMock } from '../../mocks';
import { UI_SETTINGS } from '../../../common';
jest.mock('./utils', () => ({
createRequestHash: jest.fn().mockImplementation((input) => {
return Promise.resolve(JSON.stringify(input));
}),
}));
jest.mock('./utils', () => {
const originalModule = jest.requireActual('./utils');
return {
...originalModule,
createRequestHash: jest.fn().mockImplementation((input) => {
return Promise.resolve(JSON.stringify(input));
}),
};
});
jest.mock('../errors/search_session_incomplete_warning', () => ({
SearchSessionIncompleteWarning: jest.fn(),

View file

@ -43,7 +43,6 @@ import {
ToastsSetup,
} from '@kbn/core/public';
import { i18n } from '@kbn/i18n';
import { BatchedFunc, BfetchPublicSetup, DISABLE_BFETCH } from '@kbn/bfetch-plugin/public';
import { toMountPoint } from '@kbn/kibana-react-plugin/public';
import { AbortError, KibanaServerError } from '@kbn/kibana-utils-plugin/public';
@ -70,7 +69,7 @@ import {
} from '../errors';
import { ISessionService, SearchSessionState } from '../session';
import { SearchResponseCache } from './search_response_cache';
import { createRequestHash } from './utils';
import { createRequestHash, getSearchErrorOverrideDisplay } from './utils';
import { SearchAbortController } from './search_abort_controller';
import { SearchConfigSchema } from '../../../config';
@ -518,25 +517,17 @@ export class SearchInterceptor {
if (e instanceof AbortError || e instanceof SearchTimeoutError) {
// The SearchTimeoutError is shown by the interceptor in getSearchError (regardless of how the app chooses to handle errors)
return;
} else if (e instanceof EsError) {
this.deps.toasts.addDanger({
title: i18n.translate('data.search.esErrorTitle', {
defaultMessage: 'Cannot retrieve search results',
}),
text: toMountPoint(e.getErrorMessage(this.application), { theme$: this.deps.theme.theme$ }),
});
} else if (e.constructor.name === 'HttpFetchError' || e instanceof BfetchRequestError) {
const defaultMsg = i18n.translate('data.errors.fetchError', {
defaultMessage: 'Check your network connection and try again.',
});
}
const overrideDisplay = getSearchErrorOverrideDisplay({
error: e,
application: this.application,
});
if (overrideDisplay) {
this.deps.toasts.addDanger({
title: i18n.translate('data.search.httpErrorTitle', {
defaultMessage: 'Unable to connect to the Kibana server',
}),
text: toMountPoint(e.message || defaultMsg, {
theme$: this.deps.theme.theme$,
}),
title: overrideDisplay.title,
text: toMountPoint(overrideDisplay.body, { theme$: this.deps.theme.theme$ }),
});
} else {
this.deps.toasts.addError(e, {

View file

@ -8,7 +8,42 @@
import stringify from 'json-stable-stringify';
import { Sha256 } from '@kbn/crypto-browser';
import { i18n } from '@kbn/i18n';
import { ReactNode } from 'react';
import { BfetchRequestError } from '@kbn/bfetch-plugin/public';
import { ApplicationStart } from '@kbn/core-application-browser';
import { EsError } from '../errors';
export async function createRequestHash(keys: Record<string, any>) {
return new Sha256().update(stringify(keys), 'utf8').digest('hex');
}
export function getSearchErrorOverrideDisplay({
error,
application,
}: {
error: Error;
application: ApplicationStart;
}): { title: string; body: ReactNode } | undefined {
if (error instanceof EsError) {
return {
title: i18n.translate('data.search.esErrorTitle', {
defaultMessage: 'Cannot retrieve search results',
}),
body: error.getErrorMessage(application),
};
}
if (error.constructor.name === 'HttpFetchError' || error instanceof BfetchRequestError) {
const defaultMsg = i18n.translate('data.errors.fetchError', {
defaultMessage: 'Check your network connection and try again.',
});
return {
title: i18n.translate('data.search.httpErrorTitle', {
defaultMessage: 'Unable to connect to the Kibana server',
}),
body: error.message || defaultMsg,
};
}
}

View file

@ -46,6 +46,7 @@
"@kbn/crypto-browser",
"@kbn/config",
"@kbn/config-schema",
"@kbn/core-application-browser",
],
"exclude": [
"target/**/*",

View file

@ -41,6 +41,7 @@ import { createSearchSessionMock } from '../../../../__mocks__/search_session';
import { getSessionServiceMock } from '@kbn/data-plugin/public/search/session/mocks';
import { DiscoverMainProvider } from '../../services/discover_state_provider';
import { act } from 'react-dom/test-utils';
import { ErrorCallout } from '../../../../components/common/error_callout';
setHeaderActionMenuMounter(jest.fn());
@ -49,7 +50,12 @@ async function mountComponent(
prevSidebarClosed?: boolean,
mountOptions: { attachTo?: HTMLElement } = {},
query?: Query | AggregateQuery,
isPlainRecord?: boolean
isPlainRecord?: boolean,
main$: DataMain$ = new BehaviorSubject({
fetchStatus: FetchStatus.COMPLETE,
recordRawType: isPlainRecord ? RecordRawType.PLAIN : RecordRawType.DOCUMENT,
foundDocuments: true,
}) as DataMain$
) {
const searchSourceMock = createSearchSourceMock({});
const services = {
@ -75,12 +81,6 @@ async function mountComponent(
const stateContainer = getDiscoverStateMock({ isTimeBased: true });
const main$ = new BehaviorSubject({
fetchStatus: FetchStatus.COMPLETE,
recordRawType: isPlainRecord ? RecordRawType.PLAIN : RecordRawType.DOCUMENT,
foundDocuments: true,
}) as DataMain$;
const documents$ = new BehaviorSubject({
fetchStatus: FetchStatus.COMPLETE,
result: esHits.map((esHit) => buildDataTableRecord(esHit, dataView)),
@ -204,4 +204,21 @@ describe('Discover component', () => {
expect(component.find(DiscoverSidebar).length).toBe(0);
}, 10000);
});
it('shows the no results error display', async () => {
const component = await mountComponent(
dataViewWithTimefieldMock,
undefined,
undefined,
undefined,
undefined,
new BehaviorSubject({
fetchStatus: FetchStatus.ERROR,
recordRawType: RecordRawType.DOCUMENT,
foundDocuments: false,
error: new Error('No results'),
}) as DataMain$
);
expect(component.find(ErrorCallout)).toHaveLength(1);
}, 10000);
});

View file

@ -44,6 +44,7 @@ import { useDataState } from '../../hooks/use_data_state';
import { getRawRecordType } from '../../utils/get_raw_record_type';
import { SavedSearchURLConflictCallout } from '../../../../components/saved_search_url_conflict_callout/saved_search_url_conflict_callout';
import { DiscoverHistogramLayout } from './discover_histogram_layout';
import { ErrorCallout } from '../../../../components/common/error_callout';
/**
* Local storage key for sidebar persistence state
@ -217,8 +218,6 @@ export function DiscoverLayout({
isTimeBased={isTimeBased}
query={globalQueryState.query}
filters={globalQueryState.filters}
data={data}
error={dataState.error}
dataView={dataView}
onDisableFilters={onDisableFilters}
/>
@ -257,7 +256,6 @@ export function DiscoverLayout({
}, [
currentColumns,
data,
dataState.error,
dataView,
expandedDoc,
inspectorAdapters,
@ -358,19 +356,29 @@ export function DiscoverLayout({
</EuiFlexItem>
</EuiHideFor>
<EuiFlexItem className="dscPageContent__wrapper">
<EuiPageContent
panelRef={resizeRef}
verticalPosition={contentCentered ? 'center' : undefined}
horizontalPosition={contentCentered ? 'center' : undefined}
paddingSize="none"
hasShadow={false}
className={classNames('dscPageContent', {
'dscPageContent--centered': contentCentered,
'dscPageContent--emptyPrompt': resultState === 'none',
})}
>
{mainDisplay}
</EuiPageContent>
{resultState === 'none' && dataState.error ? (
<ErrorCallout
title={i18n.translate('discover.noResults.searchExamples.noResultsErrorTitle', {
defaultMessage: 'Unable to retrieve search results',
})}
error={dataState.error}
data-test-subj="discoverNoResultsError"
/>
) : (
<EuiPageContent
panelRef={resizeRef}
verticalPosition={contentCentered ? 'center' : undefined}
horizontalPosition={contentCentered ? 'center' : undefined}
paddingSize="none"
hasShadow={false}
className={classNames('dscPageContent', {
'dscPageContent--centered': contentCentered,
'dscPageContent--emptyPrompt': resultState === 'none',
})}
>
{mainDisplay}
</EuiPageContent>
)}
</EuiFlexItem>
</EuiFlexGroup>
</EuiPageBody>

View file

@ -11,6 +11,7 @@ import { SavedSearch } from '@kbn/saved-search-plugin/public';
import React, { useCallback } from 'react';
import { DataView } from '@kbn/data-views-plugin/common';
import { METRIC_TYPE } from '@kbn/analytics';
import { i18n } from '@kbn/i18n';
import { VIEW_MODE } from '../../../../../common/constants';
import { useDiscoverServices } from '../../../../hooks/use_discover_services';
import { DataTableRecord } from '../../../../types';
@ -20,6 +21,8 @@ import { DiscoverStateContainer } from '../../services/discover_state';
import { FieldStatisticsTab } from '../field_stats_table';
import { DiscoverDocuments } from './discover_documents';
import { DOCUMENTS_VIEW_CLICK, FIELD_STATISTICS_VIEW_CLICK } from '../field_stats_table/constants';
import { ErrorCallout } from '../../../../components/common/error_callout';
import { useDataState } from '../../hooks/use_data_state';
export interface DiscoverMainContentProps {
dataView: DataView;
@ -65,6 +68,8 @@ export const DiscoverMainContent = ({
[trackUiMetric, stateContainer]
);
const dataState = useDataState(stateContainer.dataState.data$.main$);
return (
<EuiFlexGroup
className="eui-fullHeight"
@ -78,6 +83,16 @@ export const DiscoverMainContent = ({
<DocumentViewModeToggle viewMode={viewMode} setDiscoverViewMode={setDiscoverViewMode} />
</EuiFlexItem>
)}
{dataState.error && (
<ErrorCallout
title={i18n.translate('discover.documentsErrorTitle', {
defaultMessage: 'Search error',
})}
error={dataState.error}
inline
data-test-subj="discoverMainError"
/>
)}
{viewMode === VIEW_MODE.DOCUMENT_LEVEL ? (
<DiscoverDocuments
expandedDoc={expandedDoc}

View file

@ -409,7 +409,7 @@ describe('useDiscoverHistogram', () => {
act(() => {
hook.result.current.setUnifiedHistogramApi(api);
});
expect(sendErrorTo).toHaveBeenCalledWith(mockData, totalHits$);
expect(sendErrorTo).toHaveBeenCalledWith(totalHits$);
expect(totalHits$.value).toEqual({
fetchStatus: FetchStatus.ERROR,
error,

View file

@ -218,12 +218,17 @@ export const useDiscoverHistogram = ({
* Total hits
*/
const setTotalHitsError = useMemo(
() => sendErrorTo(savedSearchData$.totalHits$),
[savedSearchData$.totalHits$]
);
useEffect(() => {
const subscription = createTotalHitsObservable(unifiedHistogram?.state$)?.subscribe(
({ status, result }) => {
if (result instanceof Error) {
// Display the error and set totalHits$ to an error state
sendErrorTo(services.data, savedSearchData$.totalHits$)(result);
// Set totalHits$ to an error state
setTotalHitsError(result);
return;
}
@ -252,7 +257,13 @@ export const useDiscoverHistogram = ({
return () => {
subscription?.unsubscribe();
};
}, [savedSearchData$.main$, savedSearchData$.totalHits$, services.data, unifiedHistogram]);
}, [
savedSearchData$.main$,
savedSearchData$.totalHits$,
services.data,
setTotalHitsError,
unifiedHistogram,
]);
/**
* Data fetching

View file

@ -45,7 +45,6 @@ async function mountAndFindSubjects(
component = await mountWithIntl(
<KibanaContextProvider services={services}>
<DiscoverNoResults
data={services.data}
isTimeBased={props.dataView.isTimeBased()}
onDisableFilters={() => {}}
{...props}
@ -141,29 +140,5 @@ describe('DiscoverNoResults', () => {
expect(result).toHaveProperty('disableFiltersButton', true);
});
});
describe('error message', () => {
test('renders error message', async () => {
const error = new Error('Fatal error');
const result = await mountAndFindSubjects({
dataView: stubDataView,
error,
query: { language: 'lucene', query: '' },
filters: [{} as Filter],
});
expect(result).toMatchInlineSnapshot(`
Object {
"adjustFilters": false,
"adjustSearch": false,
"adjustTimeRange": false,
"checkIndices": false,
"disableFiltersButton": false,
"errorMsg": true,
"mainMsg": false,
"viewMatchesButton": false,
}
`);
});
});
});
});

View file

@ -6,12 +6,10 @@
* Side Public License, v 1.
*/
import React, { Fragment } from 'react';
import { FormattedMessage } from '@kbn/i18n-react';
import { EuiButton, EuiCallOut, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import React from 'react';
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import type { DataView } from '@kbn/data-views-plugin/common';
import type { AggregateQuery, Filter, Query } from '@kbn/es-query';
import type { DataPublicPluginStart } from '@kbn/data-plugin/public';
import { NoResultsSuggestions } from './no_results_suggestions';
import './_no_results.scss';
@ -19,8 +17,6 @@ export interface DiscoverNoResultsProps {
isTimeBased?: boolean;
query: Query | AggregateQuery | undefined;
filters: Filter[] | undefined;
error?: Error;
data: DataPublicPluginStart;
dataView: DataView;
onDisableFilters: () => void;
}
@ -29,51 +25,20 @@ export function DiscoverNoResults({
isTimeBased,
query,
filters,
error,
data,
dataView,
onDisableFilters,
}: DiscoverNoResultsProps) {
const callOut = !error ? (
<EuiFlexItem grow={false}>
<NoResultsSuggestions
isTimeBased={isTimeBased}
query={query}
filters={filters}
dataView={dataView}
onDisableFilters={onDisableFilters}
/>
</EuiFlexItem>
) : (
<EuiFlexItem grow={true} className="dscNoResults">
<EuiCallOut
title={
<FormattedMessage
id="discover.noResults.searchExamples.noResultsErrorTitle"
defaultMessage="Unable to retrieve search results"
/>
}
color="danger"
iconType="warning"
data-test-subj="discoverNoResultsError"
>
<EuiButton
size="s"
color="danger"
onClick={() => (data ? data.search.showError(error) : void 0)}
>
<FormattedMessage
id="discover.showErrorMessageAgain"
defaultMessage="Show error message"
/>
</EuiButton>
</EuiCallOut>
</EuiFlexItem>
);
return (
<Fragment>
<EuiFlexGroup justifyContent="center">{callOut}</EuiFlexGroup>
</Fragment>
<EuiFlexGroup justifyContent="center">
<EuiFlexItem grow={false}>
<NoResultsSuggestions
isTimeBased={isTimeBased}
query={query}
filters={filters}
dataView={dataView}
onDisableFilters={onDisableFilters}
/>
</EuiFlexItem>
</EuiFlexGroup>
);
}

View file

@ -18,7 +18,6 @@ import { FetchStatus } from '../../types';
import { BehaviorSubject } from 'rxjs';
import { DataMainMsg, RecordRawType } from '../services/discover_data_state_container';
import { filter } from 'rxjs/operators';
import { dataPluginMock } from '@kbn/data-plugin/public/mocks';
describe('test useSavedSearch message generators', () => {
test('sendCompleteMsg', (done) => {
@ -101,15 +100,13 @@ describe('test useSavedSearch message generators', () => {
test('sendErrorTo', (done) => {
const main$ = new BehaviorSubject<DataMainMsg>({ fetchStatus: FetchStatus.PARTIAL });
const data = dataPluginMock.createStartContract();
const error = new Error('Pls help!');
main$.subscribe((value) => {
expect(data.search.showError).toBeCalledWith(error);
expect(value.fetchStatus).toBe(FetchStatus.ERROR);
expect(value.error).toBe(error);
done();
});
sendErrorTo(data, main$)(error);
sendErrorTo(main$)(error);
});
test('checkHitCount with hits', (done) => {

View file

@ -6,7 +6,6 @@
* Side Public License, v 1.
*/
import { DataPublicPluginStart } from '@kbn/data-plugin/public';
import type { BehaviorSubject } from 'rxjs';
import { FetchStatus } from '../../types';
import type {
@ -108,19 +107,14 @@ export function sendResetMsg(data: SavedSearchData, initialFetchStatus: FetchSta
/**
* Method to create an error handler that will forward the received error
* to the specified subjects. It will ignore AbortErrors and will use the data
* plugin to show a toast for the error (e.g. allowing better insights into shard failures).
* to the specified subjects. It will ignore AbortErrors.
*/
export const sendErrorTo = (
data: DataPublicPluginStart,
...errorSubjects: Array<DataMain$ | DataDocuments$>
) => {
export const sendErrorTo = (...errorSubjects: Array<DataMain$ | DataDocuments$>) => {
return (error: Error) => {
if (error instanceof Error && error.name === 'AbortError') {
return;
}
data.search.showError(error);
errorSubjects.forEach((subject) => sendErrorMsg(subject, error));
};
};

View file

@ -117,7 +117,7 @@ export function fetchAll(
// Only the document query should send its errors to main$, to cause the full Discover app
// to get into an error state. The other queries will not cause all of Discover to error out
// but their errors will be shown in-place (e.g. of the chart).
.catch(sendErrorTo(data, dataSubjects.documents$, dataSubjects.main$));
.catch(sendErrorTo(dataSubjects.documents$, dataSubjects.main$));
// Return a promise that will resolve once all the requests have finished or failed
return firstValueFrom(

View file

@ -0,0 +1,149 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { EuiButton, EuiCallOut, EuiEmptyPrompt, EuiLink, EuiModal } from '@elastic/eui';
import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public';
import { findTestSubject } from '@kbn/test-jest-helpers';
import { mount } from 'enzyme';
import React, { ReactNode } from 'react';
import { discoverServiceMock } from '../../__mocks__/services';
import { ErrorCallout } from './error_callout';
const mockGetSearchErrorOverrideDisplay = jest.fn();
jest.mock('@kbn/data-plugin/public', () => {
const originalModule = jest.requireActual('@kbn/data-plugin/public');
return {
...originalModule,
getSearchErrorOverrideDisplay: () => mockGetSearchErrorOverrideDisplay(),
};
});
describe('ErrorCallout', () => {
const mountWithServices = (component: ReactNode) =>
mount(
<KibanaContextProvider services={discoverServiceMock}>{component}</KibanaContextProvider>
);
afterEach(() => {
mockGetSearchErrorOverrideDisplay.mockReset();
});
it('should render', () => {
const title = 'Error title';
const error = new Error('My error');
const wrapper = mountWithServices(
<ErrorCallout title={title} error={error} data-test-subj="errorCallout" />
);
const prompt = wrapper.find(EuiEmptyPrompt);
expect(prompt).toHaveLength(1);
expect(prompt.prop('title')).toBeDefined();
expect(prompt.prop('title')).not.toBeInstanceOf(String);
expect(prompt.prop('data-test-subj')).toBe('errorCallout');
expect(prompt.prop('body')).toBeDefined();
expect(findTestSubject(prompt, 'discoverErrorCalloutTitle').contains(title)).toBe(true);
expect(findTestSubject(prompt, 'discoverErrorCalloutMessage').contains(error.message)).toBe(
true
);
expect(prompt.find(EuiButton)).toHaveLength(1);
});
it('should render inline', () => {
const title = 'Error title';
const error = new Error('My error');
const wrapper = mountWithServices(
<ErrorCallout title={title} error={error} inline data-test-subj="errorCallout" />
);
const callout = wrapper.find(EuiCallOut);
expect(callout).toHaveLength(1);
expect(callout.prop('title')).toBeDefined();
expect(callout.prop('title')).not.toBeInstanceOf(String);
expect(callout.prop('size')).toBe('s');
expect(callout.prop('data-test-subj')).toBe('errorCallout');
expect(
findTestSubject(callout, 'discoverErrorCalloutMessage').contains(`${title}: ${error.message}`)
).toBe(true);
expect(callout.find(EuiLink)).toHaveLength(1);
});
it('should render with override display', () => {
const title = 'Override title';
const error = new Error('My error');
const overrideDisplay = <div>Override display</div>;
mockGetSearchErrorOverrideDisplay.mockReturnValue({ title, body: overrideDisplay });
const wrapper = mountWithServices(
<ErrorCallout title="Original title" error={error} data-test-subj="errorCallout" />
);
const prompt = wrapper.find(EuiEmptyPrompt);
expect(prompt).toHaveLength(1);
expect(prompt.prop('title')).toBeDefined();
expect(prompt.prop('title')).not.toBeInstanceOf(String);
expect(prompt.prop('data-test-subj')).toBe('errorCallout');
expect(prompt.prop('body')).toBeDefined();
expect(findTestSubject(prompt, 'discoverErrorCalloutTitle').contains(title)).toBe(true);
expect(prompt.contains(overrideDisplay)).toBe(true);
expect(prompt.find(EuiButton)).toHaveLength(0);
});
it('should render with override display and inline', () => {
const title = 'Override title';
const error = new Error('My error');
const overrideDisplay = <div>Override display</div>;
mockGetSearchErrorOverrideDisplay.mockReturnValue({ title, body: overrideDisplay });
const wrapper = mountWithServices(
<ErrorCallout title="Original title" error={error} inline data-test-subj="errorCallout" />
);
const callout = wrapper.find(EuiCallOut);
expect(callout).toHaveLength(1);
expect(callout.prop('title')).toBeDefined();
expect(callout.prop('title')).not.toBeInstanceOf(String);
expect(callout.prop('size')).toBe('s');
expect(callout.prop('data-test-subj')).toBe('errorCallout');
expect(callout.find(EuiLink)).toHaveLength(1);
expect(wrapper.find(EuiModal)).toHaveLength(0);
expect(wrapper.contains(title)).toBe(true);
expect(wrapper.contains(overrideDisplay)).toBe(false);
callout.find(EuiLink).simulate('click');
expect(wrapper.find(EuiModal)).toHaveLength(1);
expect(findTestSubject(wrapper, 'discoverErrorCalloutOverrideModalTitle').contains(title)).toBe(
true
);
expect(
findTestSubject(wrapper, 'discoverErrorCalloutOverrideModalBody').contains(overrideDisplay)
).toBe(true);
expect(wrapper.contains(overrideDisplay)).toBe(true);
});
it('should call showErrorDialog when the button is clicked', () => {
(discoverServiceMock.core.notifications.showErrorDialog as jest.Mock).mockClear();
const title = 'Error title';
const error = new Error('My error');
const wrapper = mountWithServices(
<ErrorCallout title={title} error={error} data-test-subj="errorCallout" />
);
wrapper.find(EuiButton).find('button').simulate('click');
expect(discoverServiceMock.core.notifications.showErrorDialog).toHaveBeenCalledWith({
title,
error,
});
});
it('should call showErrorDialog when the button is clicked inline', () => {
(discoverServiceMock.core.notifications.showErrorDialog as jest.Mock).mockClear();
const title = 'Error title';
const error = new Error('My error');
const wrapper = mountWithServices(
<ErrorCallout title={title} error={error} inline data-test-subj="errorCallout" />
);
wrapper.find(EuiLink).find('button').simulate('click');
expect(discoverServiceMock.core.notifications.showErrorDialog).toHaveBeenCalledWith({
title,
error,
});
});
});

View file

@ -0,0 +1,156 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import {
EuiButton,
EuiCallOut,
EuiEmptyPrompt,
EuiFlexGroup,
EuiFlexItem,
EuiIcon,
EuiLink,
EuiModal,
EuiModalBody,
EuiModalHeader,
EuiModalHeaderTitle,
EuiText,
useEuiTheme,
} from '@elastic/eui';
import { css } from '@emotion/react';
import { getSearchErrorOverrideDisplay } from '@kbn/data-plugin/public';
import { i18n } from '@kbn/i18n';
import React, { ReactNode, useState } from 'react';
import { useDiscoverServices } from '../../hooks/use_discover_services';
export interface ErrorCalloutProps {
title: string;
error: Error;
inline?: boolean;
'data-test-subj'?: string;
}
export const ErrorCallout = ({
title,
error,
inline,
'data-test-subj': dataTestSubj,
}: ErrorCalloutProps) => {
const { core } = useDiscoverServices();
const { euiTheme } = useEuiTheme();
const showErrorMessage = i18n.translate('discover.errorCalloutShowErrorMessage', {
defaultMessage: 'Show details',
});
const overrideDisplay = getSearchErrorOverrideDisplay({
error,
application: core.application,
});
const [overrideModalOpen, setOverrideModalOpen] = useState(false);
const showError = overrideDisplay?.body
? () => setOverrideModalOpen(true)
: () => core.notifications.showErrorDialog({ title, error });
let formattedTitle: ReactNode = overrideDisplay?.title || title;
if (inline) {
const formattedTitleMessage = overrideDisplay
? formattedTitle
: i18n.translate('discover.errorCalloutFormattedTitle', {
defaultMessage: '{title}: {errorMessage}',
values: { title, errorMessage: error.message },
});
formattedTitle = (
<>
<span className="eui-textTruncate" data-test-subj="discoverErrorCalloutMessage">
{formattedTitleMessage}
</span>
<EuiLink
onClick={showError}
css={css`
white-space: nowrap;
margin-inline-start: ${euiTheme.size.s};
`}
>
{showErrorMessage}
</EuiLink>
</>
);
}
return (
<>
{inline ? (
<EuiCallOut
title={formattedTitle}
color="danger"
iconType="error"
size="s"
css={css`
.euiTitle {
display: flex;
align-items: center;
}
`}
data-test-subj={dataTestSubj}
/>
) : (
<EuiEmptyPrompt
color="danger"
title={
<EuiFlexGroup alignItems="center" gutterSize="s" responsive={false}>
<EuiFlexItem grow={false}>
<EuiIcon type="error" color="danger" size="l" />
</EuiFlexItem>
<EuiFlexItem>
<h2 data-test-subj="discoverErrorCalloutTitle">{formattedTitle}</h2>
</EuiFlexItem>
</EuiFlexGroup>
}
body={
overrideDisplay?.body ?? (
<>
<p
css={css`
white-space: break-spaces;
font-family: ${euiTheme.font.familyCode};
`}
data-test-subj="discoverErrorCalloutMessage"
>
{error.message}
</p>
<EuiButton onClick={showError}>{showErrorMessage}</EuiButton>
</>
)
}
css={css`
text-align: left;
`}
data-test-subj={dataTestSubj}
/>
)}
{overrideDisplay && overrideModalOpen && (
<EuiModal onClose={() => setOverrideModalOpen(false)}>
<EuiModalHeader>
<EuiModalHeaderTitle data-test-subj="discoverErrorCalloutOverrideModalTitle">
{overrideDisplay.title}
</EuiModalHeaderTitle>
</EuiModalHeader>
<EuiModalBody>
<EuiText data-test-subj="discoverErrorCalloutOverrideModalBody">
{overrideDisplay.body}
</EuiText>
</EuiModalBody>
</EuiModal>
)}
</>
);
};

View file

@ -48,7 +48,7 @@ export function mockTelemetryService({
const telemetryService = new TelemetryService({
config,
http: httpServiceMock.createStartContract(),
notifications: notificationServiceMock.createStartContract(),
notifications: notificationServiceMock.createSetupContract(),
isScreenshotMode,
currentKibanaVersion,
reportOptInStatusChange,

View file

@ -7,7 +7,7 @@
*/
import { i18n } from '@kbn/i18n';
import { CoreStart } from '@kbn/core/public';
import { CoreSetup, CoreStart } from '@kbn/core/public';
import { TelemetryPluginConfig } from '../plugin';
import { getTelemetryChannelEndpoint } from '../../common/telemetry_config/get_telemetry_channel_endpoint';
import type {
@ -19,7 +19,7 @@ import { PAYLOAD_CONTENT_ENCODING } from '../../common/constants';
interface TelemetryServiceConstructor {
config: TelemetryPluginConfig;
http: CoreStart['http'];
notifications: CoreStart['notifications'];
notifications: CoreSetup['notifications'];
isScreenshotMode: boolean;
currentKibanaVersion: string;
reportOptInStatusChange?: boolean;
@ -32,7 +32,7 @@ interface TelemetryServiceConstructor {
export class TelemetryService {
private readonly http: CoreStart['http'];
private readonly reportOptInStatusChange: boolean;
private readonly notifications: CoreStart['notifications'];
private readonly notifications: CoreSetup['notifications'];
private readonly defaultConfig: TelemetryPluginConfig;
private readonly isScreenshotMode: boolean;
private updatedConfig?: TelemetryPluginConfig;

View file

@ -305,6 +305,7 @@ exports[`TelemetryManagementSectionComponent renders null because allowChangingO
},
"isScreenshotMode": false,
"notifications": Object {
"showErrorDialog": [MockFunction],
"toasts": Object {
"add": [MockFunction],
"addDanger": [MockFunction],

View file

@ -12,7 +12,6 @@ import { FtrProviderContext } from '../ftr_provider_context';
export default function ({ getService, getPageObjects }: FtrProviderContext) {
const esArchiver = getService('esArchiver');
const kibanaServer = getService('kibanaServer');
const toasts = getService('toasts');
const testSubjects = getService('testSubjects');
const PageObjects = getPageObjects(['common', 'header', 'discover', 'timePicker']);
@ -33,8 +32,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
describe('invalid scripted field error', () => {
it('is rendered', async () => {
const toast = await toasts.getToastElement(1);
const painlessStackTrace = await toast.findByTestSubject('painlessStackTrace');
expect(await PageObjects.discover.noResultsErrorVisible()).to.be(true);
const painlessStackTrace = await testSubjects.find('painlessStackTrace');
expect(painlessStackTrace).not.to.be(undefined);
});
});

View file

@ -14,7 +14,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
const retry = getService('retry');
const esArchiver = getService('esArchiver');
const kibanaServer = getService('kibanaServer');
const toasts = getService('toasts');
const queryBar = getService('queryBar');
const browser = getService('browser');
const PageObjects = getPageObjects(['common', 'header', 'discover', 'visualize', 'timePicker']);
@ -70,9 +69,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
'whitespace but "(" found.';
await queryBar.setQuery('xxx(yyy))');
await queryBar.submitQuery();
const { message } = await toasts.getErrorToast();
expect(await PageObjects.discover.mainErrorVisible()).to.be(true);
const message = await PageObjects.discover.getDiscoverErrorMessage();
expect(message).to.contain(expectedError);
await toasts.dismissToast();
});
it('shows top-level object keys', async function () {

View file

@ -14,7 +14,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
const retry = getService('retry');
const esArchiver = getService('esArchiver');
const kibanaServer = getService('kibanaServer');
const toasts = getService('toasts');
const queryBar = getService('queryBar');
const browser = getService('browser');
const PageObjects = getPageObjects(['common', 'header', 'discover', 'visualize', 'timePicker']);
@ -74,9 +73,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
'whitespace but "(" found.';
await queryBar.setQuery('xxx(yyy))');
await queryBar.submitQuery();
const { message } = await toasts.getErrorToast();
expect(await PageObjects.discover.mainErrorVisible()).to.be(true);
const message = await PageObjects.discover.getDiscoverErrorMessage();
expect(message).to.contain(expectedError);
await toasts.dismissToast();
});
it('shows top-level object keys', async function () {

View file

@ -13,7 +13,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
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']);
const defaultSettings = { defaultIndex: 'logstash-*' };
@ -85,9 +84,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
'whitespace but "(" found.';
await queryBar.setQuery('xxx(yyy))');
await queryBar.submitQuery();
const { message } = await toasts.getErrorToast();
expect(await PageObjects.discover.mainErrorVisible()).to.be(true);
const message = await PageObjects.discover.getDiscoverErrorMessage();
expect(message).to.contain(expectedError);
await toasts.dismissToast();
});
});
});

View file

@ -462,6 +462,18 @@ export class DiscoverPageObject extends FtrService {
return await this.testSubjects.exists('discoverNoResultsTimefilter');
}
public noResultsErrorVisible() {
return this.testSubjects.exists('discoverNoResultsError');
}
public mainErrorVisible() {
return this.testSubjects.exists('discoverMainError');
}
public getDiscoverErrorMessage() {
return this.testSubjects.getVisibleText('discoverErrorCalloutMessage');
}
public async expandTimeRangeAsSuggestedInNoResultsMessage() {
await this.retry.waitFor('the button before pressing it', async () => {
return await this.testSubjects.exists('discoverNoResultsViewAllMatches');

View file

@ -26,6 +26,7 @@ const notifications: NotificationsStart = {
remove: () => {},
get$: () => of([]),
},
showErrorDialog: () => {},
};
export const getNotifications = () => notifications;

View file

@ -68,7 +68,7 @@ const { Provider: KibanaReactContextProvider } = createKibanaReactContext({
export const setupEnvironment = () => {
breadcrumbService.setup(() => undefined);
documentationService.setup(docLinksServiceMock.createStartContract());
notificationService.setup(notificationServiceMock.createSetupContract());
notificationService.setup(notificationServiceMock.createStartContract());
return initHttpRequests();
};

View file

@ -7,7 +7,7 @@
import { i18n } from '@kbn/i18n';
import SemVer from 'semver/classes/semver';
import { CoreSetup } from '@kbn/core/public';
import { CoreSetup, CoreStart } from '@kbn/core/public';
import { ManagementAppMountParams } from '@kbn/management-plugin/public';
import { UsageCollectionSetup } from '@kbn/usage-collection-plugin/public';
@ -27,12 +27,12 @@ import { httpService } from './services/http';
function initSetup({
usageCollection,
coreSetup,
core,
}: {
coreSetup: CoreSetup<StartDependencies>;
core: CoreStart;
usageCollection: UsageCollectionSetup;
}) {
const { http, notifications } = coreSetup;
const { http, notifications } = core;
httpService.setup(http);
notificationService.setup(notifications);
@ -73,7 +73,7 @@ export async function mountManagementSection(
const { uiMetricService } = initSetup({
usageCollection,
coreSetup,
core,
});
const appDependencies: AppDependencies = {

View file

@ -205,6 +205,7 @@ exports[`LoginPage enabled form state renders as expected 1`] = `
loginAssistanceMessage=""
notifications={
Object {
"showErrorDialog": [MockFunction],
"toasts": Object {
"add": [MockFunction],
"addDanger": [MockFunction],
@ -243,6 +244,7 @@ exports[`LoginPage enabled form state renders as expected when loginAssistanceMe
loginAssistanceMessage="This is an *important* message"
notifications={
Object {
"showErrorDialog": [MockFunction],
"toasts": Object {
"add": [MockFunction],
"addDanger": [MockFunction],
@ -282,6 +284,7 @@ exports[`LoginPage enabled form state renders as expected when loginHelp is set
loginHelp="**some-help**"
notifications={
Object {
"showErrorDialog": [MockFunction],
"toasts": Object {
"add": [MockFunction],
"addDanger": [MockFunction],
@ -366,6 +369,7 @@ exports[`LoginPage page renders as expected 1`] = `
loginAssistanceMessage=""
notifications={
Object {
"showErrorDialog": [MockFunction],
"toasts": Object {
"add": [MockFunction],
"addDanger": [MockFunction],
@ -459,6 +463,7 @@ exports[`LoginPage page renders with custom branding 1`] = `
loginAssistanceMessage=""
notifications={
Object {
"showErrorDialog": [MockFunction],
"toasts": Object {
"add": [MockFunction],
"addDanger": [MockFunction],

View file

@ -8,7 +8,7 @@
import { EuiDescribedFormGroup } from '@elastic/eui';
import React, { Component } from 'react';
import type { NotificationsSetup } from '@kbn/core/public';
import type { NotificationsStart } from '@kbn/core/public';
import { FormattedMessage } from '@kbn/i18n-react';
import type { PublicMethodsOf } from '@kbn/utility-types';
@ -23,7 +23,7 @@ export interface ChangePasswordProps {
export interface ChangePasswordPropsInternal extends ChangePasswordProps {
userAPIClient: PublicMethodsOf<UserAPIClient>;
notifications: NotificationsSetup;
notifications: NotificationsStart;
}
export class ChangePassword extends Component<ChangePasswordPropsInternal, {}> {

View file

@ -44,7 +44,7 @@ const appDependencies: AppDependencies = {
docLinks: coreStart.docLinks,
i18n: coreStart.i18n,
fieldFormats: fieldFormatsServiceMock.createStartContract(),
notifications: coreSetup.notifications,
notifications: coreStart.notifications,
uiSettings: coreStart.uiSettings,
savedObjects: coreStart.savedObjects,
storage: { get: jest.fn() } as unknown as Storage,

View file

@ -12,7 +12,7 @@ import type {
HttpSetup,
I18nStart,
IUiSettingsClient,
NotificationsSetup,
NotificationsStart,
OverlayStart,
SavedObjectsStart,
ThemeServiceStart,
@ -45,7 +45,7 @@ export interface AppDependencies {
fieldFormats: FieldFormatsStart;
http: HttpSetup;
i18n: I18nStart;
notifications: NotificationsSetup;
notifications: NotificationsStart;
uiSettings: IUiSettingsClient;
savedObjects: SavedObjectsStart;
storage: Storage;

View file

@ -25,10 +25,20 @@ export async function mountManagementSection(
params: ManagementAppMountParams
) {
const { element, setBreadcrumbs, history } = params;
const { http, notifications, getStartServices } = coreSetup;
const { http, getStartServices } = coreSetup;
const startServices = await getStartServices();
const [core, plugins] = startServices;
const { application, chrome, docLinks, i18n, overlays, theme, savedObjects, uiSettings } = core;
const {
application,
chrome,
docLinks,
i18n,
overlays,
theme,
savedObjects,
uiSettings,
notifications,
} = core;
const {
data,
dataViews,

View file

@ -2463,7 +2463,6 @@
"discover.searchingTitle": "Recherche",
"discover.selectColumnHeader": "Sélectionner la colonne",
"discover.showAllDocuments": "Afficher tous les documents",
"discover.showErrorMessageAgain": "Afficher le message d'erreur",
"discover.showSelectedDocumentsOnly": "Afficher uniquement les documents sélectionnés",
"discover.singleDocRoute.errorTitle": "Une erreur s'est produite",
"discover.skipToBottomButtonLabel": "Atteindre la fin du tableau",

View file

@ -2463,7 +2463,6 @@
"discover.searchingTitle": "検索中",
"discover.selectColumnHeader": "列を選択",
"discover.showAllDocuments": "すべてのドキュメントを表示",
"discover.showErrorMessageAgain": "エラーメッセージを表示",
"discover.showSelectedDocumentsOnly": "選択したドキュメントのみを表示",
"discover.singleDocRoute.errorTitle": "エラーが発生しました",
"discover.skipToBottomButtonLabel": "テーブルの最後に移動",

View file

@ -2463,7 +2463,6 @@
"discover.searchingTitle": "正在搜索",
"discover.selectColumnHeader": "选择列",
"discover.showAllDocuments": "显示所有文档",
"discover.showErrorMessageAgain": "显示错误消息",
"discover.showSelectedDocumentsOnly": "仅显示选定的文档",
"discover.singleDocRoute.errorTitle": "发生错误",
"discover.skipToBottomButtonLabel": "转到表尾",

View file

@ -46,6 +46,7 @@ const notifications: NotificationsStart = {
remove: () => {},
get$: () => of([]),
},
showErrorDialog: () => {},
};
export const StorybookContextDecorator: React.FC<StorybookContextDecoratorProps> = (props) => {

View file

@ -11,7 +11,7 @@ import { FtrProviderContext } from '../../ftr_provider_context';
export default function ({ getService, getPageObjects }: FtrProviderContext) {
const esArchiver = getService('esArchiver');
const kibanaServer = getService('kibanaServer');
const toasts = getService('toasts');
const testSubjects = getService('testSubjects');
const PageObjects = getPageObjects(['common', 'discover', 'timePicker']);
describe('errors', function describeIndexTests() {
@ -31,8 +31,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
// this is the same test as in OSS but it catches different error message issue in different licences
describe('invalid scripted field error', () => {
it('is rendered', async () => {
const toast = await toasts.getToastElement(1);
const painlessStackTrace = await toast.findByTestSubject('painlessStackTrace');
expect(await PageObjects.discover.noResultsErrorVisible()).to.be(true);
const painlessStackTrace = await testSubjects.find('painlessStackTrace');
expect(painlessStackTrace).not.to.be(undefined);
});
});