mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[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:
parent
2bd3e8dd20
commit
26f0f522b2
47 changed files with 533 additions and 177 deletions
|
@ -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,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -8,3 +8,4 @@
|
|||
|
||||
export { ToastsService } from './toasts_service';
|
||||
export type { ToastsApi } from './toasts_api';
|
||||
export { showErrorDialog } from './error_toast';
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -30,4 +30,5 @@ export interface NotificationsSetup {
|
|||
export interface NotificationsStart {
|
||||
/** {@link ToastsStart} */
|
||||
toasts: ToastsStart;
|
||||
showErrorDialog: (options: { title: string; error: Error }) => void;
|
||||
}
|
||||
|
|
|
@ -17,5 +17,6 @@ export const notificationsServiceFactory: NotificationsServiceFactory = () => {
|
|||
|
||||
return {
|
||||
toasts: pluginMock.toasts,
|
||||
showErrorDialog: pluginMock.showErrorDialog,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -17,10 +17,11 @@ export type NotificationsServiceFactory = KibanaPluginServiceFactory<
|
|||
|
||||
export const notificationsServiceFactory: NotificationsServiceFactory = ({ coreStart }) => {
|
||||
const {
|
||||
notifications: { toasts },
|
||||
notifications: { toasts, showErrorDialog },
|
||||
} = coreStart;
|
||||
|
||||
return {
|
||||
toasts,
|
||||
showErrorDialog,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -10,4 +10,5 @@ import type { CoreStart } from '@kbn/core/public';
|
|||
|
||||
export interface DashboardNotificationsService {
|
||||
toasts: CoreStart['notifications']['toasts'];
|
||||
showErrorDialog: CoreStart['notifications']['showErrorDialog'];
|
||||
}
|
||||
|
|
|
@ -181,6 +181,7 @@ export {
|
|||
SEARCH_SESSIONS_MANAGEMENT_ID,
|
||||
waitUntilNextSessionCompletes$,
|
||||
isEsError,
|
||||
getSearchErrorOverrideDisplay,
|
||||
SearchSource,
|
||||
SearchSessionState,
|
||||
SortDirection,
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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, {
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -46,6 +46,7 @@
|
|||
"@kbn/crypto-browser",
|
||||
"@kbn/config",
|
||||
"@kbn/config-schema",
|
||||
"@kbn/core-application-browser",
|
||||
],
|
||||
"exclude": [
|
||||
"target/**/*",
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
`);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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) => {
|
||||
|
|
|
@ -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));
|
||||
};
|
||||
};
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
});
|
||||
});
|
156
src/plugins/discover/public/components/common/error_callout.tsx
Normal file
156
src/plugins/discover/public/components/common/error_callout.tsx
Normal 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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -48,7 +48,7 @@ export function mockTelemetryService({
|
|||
const telemetryService = new TelemetryService({
|
||||
config,
|
||||
http: httpServiceMock.createStartContract(),
|
||||
notifications: notificationServiceMock.createStartContract(),
|
||||
notifications: notificationServiceMock.createSetupContract(),
|
||||
isScreenshotMode,
|
||||
currentKibanaVersion,
|
||||
reportOptInStatusChange,
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -305,6 +305,7 @@ exports[`TelemetryManagementSectionComponent renders null because allowChangingO
|
|||
},
|
||||
"isScreenshotMode": false,
|
||||
"notifications": Object {
|
||||
"showErrorDialog": [MockFunction],
|
||||
"toasts": Object {
|
||||
"add": [MockFunction],
|
||||
"addDanger": [MockFunction],
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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 () {
|
||||
|
|
|
@ -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 () {
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -26,6 +26,7 @@ const notifications: NotificationsStart = {
|
|||
remove: () => {},
|
||||
get$: () => of([]),
|
||||
},
|
||||
showErrorDialog: () => {},
|
||||
};
|
||||
|
||||
export const getNotifications = () => notifications;
|
||||
|
|
|
@ -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();
|
||||
};
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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],
|
||||
|
|
|
@ -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, {}> {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -2463,7 +2463,6 @@
|
|||
"discover.searchingTitle": "検索中",
|
||||
"discover.selectColumnHeader": "列を選択",
|
||||
"discover.showAllDocuments": "すべてのドキュメントを表示",
|
||||
"discover.showErrorMessageAgain": "エラーメッセージを表示",
|
||||
"discover.showSelectedDocumentsOnly": "選択したドキュメントのみを表示",
|
||||
"discover.singleDocRoute.errorTitle": "エラーが発生しました",
|
||||
"discover.skipToBottomButtonLabel": "テーブルの最後に移動",
|
||||
|
|
|
@ -2463,7 +2463,6 @@
|
|||
"discover.searchingTitle": "正在搜索",
|
||||
"discover.selectColumnHeader": "选择列",
|
||||
"discover.showAllDocuments": "显示所有文档",
|
||||
"discover.showErrorMessageAgain": "显示错误消息",
|
||||
"discover.showSelectedDocumentsOnly": "仅显示选定的文档",
|
||||
"discover.singleDocRoute.errorTitle": "发生错误",
|
||||
"discover.skipToBottomButtonLabel": "转到表尾",
|
||||
|
|
|
@ -46,6 +46,7 @@ const notifications: NotificationsStart = {
|
|||
remove: () => {},
|
||||
get$: () => of([]),
|
||||
},
|
||||
showErrorDialog: () => {},
|
||||
};
|
||||
|
||||
export const StorybookContextDecorator: React.FC<StorybookContextDecoratorProps> = (props) => {
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue