On search source error, show 'view details' action that opens request in inspector (#170790)

Closes https://github.com/elastic/kibana/issues/167904

PR updates `EsError` with `getActions` method that returns "View
details" button. Clicking "View details" opens inspector to request that
failed. PR updates Discover and maps to display EsError action.

PR does not update lens to display "View details". Chatted with
@drewdaemon and the implementation path is more involved. This will be
completed in another PR.

### Test setup
1. install sample web logs

### Test discover with EsError
1. open discover 
2. Add filter
    ```
    {
      "error_query": {
        "indices": [
          {
            "error_type": "exception",
            "message": "local shard failure message 123",
            "name": "kibana_sample_data_logs"
          }
        ]
      }
    }
    ```
3. Verify `View details` action is displayed and clicking action opens
inspector
<img width="300" alt="Screenshot 2023-11-07 at 12 53 31 PM"
src="6b43e9c8-daab-4782-876e-ded6958d15cf">

### Test search embeddable with EsError
1. create new dashboard. Add saved search from `kibana_sample_data_logs`
data view
2. Add filter
    ```
    {
      "error_query": {
        "indices": [
          {
            "error_type": "exception",
            "message": "local shard failure message 123",
            "name": "kibana_sample_data_logs"
          }
        ]
      }
    }
    ```
3. Verify `View details` action is displayed and clicking action opens
inspector
<img width="300" alt="Screenshot 2023-11-07 at 12 55 46 PM"
src="5ebe37c6-467a-4d72-89e3-21fc53f59d89">

### Test discover with PainlessError
<img width="300" alt="Screenshot 2023-11-07 at 12 52 51 PM"
src="6d17498f-657c-46e8-86e8-dde461599267">

### Test Maps error
1. create new map
2. Add `documents` layer
3. Set scaling to "limit to 10000"
4. Add filter
    ```
    {
      "error_query": {
        "indices": [
          {
            "error_type": "exception",
            "message": "local shard failure message 123",
            "name": "kibana_sample_data_logs"
          }
        ]
      }
    }
    ```
5. Verify "View details" button is displayed in maps legend in error
callout
<img width="500" alt="Screenshot 2023-11-08 at 12 07 42 PM"
src="2eb2cc41-0919-49a3-9792-fda9707973cb">

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Nathan Reese 2023-11-14 14:52:24 -07:00 committed by GitHub
parent 36c0d8d5e0
commit f9870c160d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
31 changed files with 213 additions and 121 deletions

View file

@ -11,6 +11,7 @@ import { ReactWrapper } from 'enzyme';
import { act } from 'react-dom/test-utils';
import { mountWithIntl } from '@kbn/test-jest-helpers';
import { NotFoundPrompt } from '@kbn/shared-ux-prompt-not-found';
import { setStubKibanaServices } from '@kbn/embeddable-plugin/public/mocks';
import { DashboardContainerFactory } from '..';
import { DASHBOARD_CONTAINER_TYPE } from '../..';
@ -168,6 +169,9 @@ describe('dashboard renderer', () => {
});
test('renders a 404 page when initial dashboard creation returns a savedObjectNotFound error', async () => {
// mock embeddable dependencies so that the embeddable panel renders
setStubKibanaServices();
// ensure that the first attempt at creating a dashboard results in a 404
const mockErrorEmbeddable = {
error: new SavedObjectNotFound('dashboard', 'gat em'),

View file

@ -6,11 +6,11 @@
* Side Public License, v 1.
*/
import { ISearchOptions } from '../../types';
import { SearchSourceSearchOptions } from '../../search_source/types';
export const ENHANCED_ES_SEARCH_STRATEGY = 'ese';
export interface IAsyncSearchOptions extends ISearchOptions {
export interface IAsyncSearchOptions extends SearchSourceSearchOptions {
/**
* The number of milliseconds to wait between receiving a response and sending another request
* If not provided, then a default 1 second interval with back-off up to 5 seconds interval is used

View file

@ -21,7 +21,7 @@ describe('EsError', () => {
},
},
} as IEsError;
const esError = new EsError(error);
const esError = new EsError(error, () => {});
expect(typeof esError.attributes).toEqual('object');
expect(esError.attributes).toEqual(error.attributes);
@ -50,7 +50,7 @@ describe('EsError', () => {
},
},
} as IEsError;
const esError = new EsError(error);
const esError = new EsError(error, () => {});
expect(esError.message).toEqual(
'EsError: The supplied interval [2q] could not be parsed as a calendar interval.'
);

View file

@ -7,7 +7,7 @@
*/
import React from 'react';
import { EuiCodeBlock, EuiSpacer } from '@elastic/eui';
import { EuiButton, EuiCodeBlock, EuiSpacer } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { ApplicationStart } from '@kbn/core/public';
import { KbnError } from '@kbn/kibana-utils-plugin/common';
@ -17,7 +17,7 @@ import { getRootCause } from './utils';
export class EsError extends KbnError {
readonly attributes: IEsError['attributes'];
constructor(protected readonly err: IEsError) {
constructor(protected readonly err: IEsError, private readonly openInInspector: () => void) {
super(
`EsError: ${
getRootCause(err?.attributes?.error)?.reason ||
@ -27,7 +27,7 @@ export class EsError extends KbnError {
this.attributes = err.attributes;
}
public getErrorMessage(application: ApplicationStart) {
public getErrorMessage() {
if (!this.attributes?.error) {
return null;
}
@ -45,4 +45,14 @@ export class EsError extends KbnError {
</>
);
}
public getActions(application: ApplicationStart) {
return [
<EuiButton key="viewRequestDetails" color="primary" onClick={this.openInInspector} size="s">
{i18n.translate('data.esError.viewDetailsButtonLabel', {
defaultMessage: 'View details',
})}
</EuiButton>,
];
}
}

View file

@ -20,14 +20,17 @@ describe('PainlessError', () => {
});
it('Should show reason and code', () => {
const e = new PainlessError({
statusCode: 400,
message: 'search_phase_execution_exception',
attributes: {
error: searchPhaseException.error,
const e = new PainlessError(
{
statusCode: 400,
message: 'search_phase_execution_exception',
attributes: {
error: searchPhaseException.error,
},
},
});
const component = mount(e.getErrorMessage(startMock.application));
() => {}
);
const component = mount(e.getErrorMessage());
const failedShards = searchPhaseException.error.failed_shards![0];
@ -41,6 +44,7 @@ describe('PainlessError', () => {
).getDOMNode();
expect(humanReadableError.textContent).toBe(failedShards?.reason.caused_by?.reason);
expect(component.find('EuiButton').length).toBe(1);
const actions = e.getActions(startMock.application);
expect(actions.length).toBe(2);
});
});

View file

@ -8,8 +8,7 @@
import React from 'react';
import { i18n } from '@kbn/i18n';
import { EuiButton, EuiSpacer, EuiText, EuiCodeBlock } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import { EuiButtonEmpty, EuiSpacer, EuiText, EuiCodeBlock } from '@elastic/eui';
import { ApplicationStart } from '@kbn/core/public';
import type { DataView } from '@kbn/data-views-plugin/common';
import { IEsError, isEsError } from './types';
@ -19,18 +18,12 @@ import { getRootCause } from './utils';
export class PainlessError extends EsError {
painlessStack?: string;
indexPattern?: DataView;
constructor(err: IEsError, indexPattern?: DataView) {
super(err);
constructor(err: IEsError, openInInspector: () => void, indexPattern?: DataView) {
super(err, openInInspector);
this.indexPattern = indexPattern;
}
public getErrorMessage(application: ApplicationStart) {
function onClick(indexPatternId?: string) {
application.navigateToApp('management', {
path: `/kibana/indexPatterns${indexPatternId ? `/patterns/${indexPatternId}` : ''}`,
});
}
public getErrorMessage() {
const rootCause = getRootCause(this.err.attributes?.error);
const scriptFromStackTrace = rootCause?.script_stack
? rootCause?.script_stack?.slice(-2).join('\n')
@ -41,7 +34,6 @@ export class PainlessError extends EsError {
// fallback, show ES stacktrace
const painlessStack = rootCause?.script_stack ? rootCause?.script_stack.join('\n') : undefined;
const indexPatternId = this?.indexPattern?.id;
return (
<>
<EuiText size="s" data-test-subj="painlessScript">
@ -54,25 +46,40 @@ export class PainlessError extends EsError {
})}
</EuiText>
<EuiSpacer size="s" />
<EuiSpacer size="s" />
{scriptFromStackTrace || painlessStack ? (
<EuiCodeBlock data-test-subj="painlessStackTrace" isCopyable={true} paddingSize="s">
{hasScript ? scriptFromStackTrace : painlessStack}
</EuiCodeBlock>
) : null}
{humanReadableError ? (
<EuiText data-test-subj="painlessHumanReadableError">{humanReadableError}</EuiText>
<EuiText size="s" data-test-subj="painlessHumanReadableError">
{humanReadableError}
</EuiText>
) : null}
<EuiSpacer size="s" />
<EuiSpacer size="s" />
<EuiText textAlign="right">
<EuiButton color="danger" onClick={() => onClick(indexPatternId)} size="s">
<FormattedMessage id="data.painlessError.buttonTxt" defaultMessage="Edit script" />
</EuiButton>
</EuiText>
</>
);
}
getActions(application: ApplicationStart) {
function onClick(indexPatternId?: string) {
application.navigateToApp('management', {
path: `/kibana/indexPatterns${indexPatternId ? `/patterns/${indexPatternId}` : ''}`,
});
}
const actions = super.getActions(application) ?? [];
actions.push(
<EuiButtonEmpty
key="editPainlessScript"
onClick={() => onClick(this?.indexPattern?.id)}
size="s"
>
{i18n.translate('data.painlessError.buttonTxt', {
defaultMessage: 'Edit script',
})}
</EuiButtonEmpty>
);
return actions;
}
}
export function isPainlessError(err: Error | IEsError) {

View file

@ -23,6 +23,8 @@ import { BehaviorSubject } from 'rxjs';
import { dataPluginMock } from '../../mocks';
import { UI_SETTINGS } from '../../../common';
import type { IEsError } from '../errors';
import type { SearchServiceStartDependencies } from '../search_service';
import type { Start as InspectorStart } from '@kbn/inspector-plugin/public';
jest.mock('./utils', () => {
const originalModule = jest.requireActual('./utils');
@ -117,13 +119,21 @@ describe('SearchInterceptor', () => {
const bfetchMock = bfetchPluginMock.createSetupContract();
bfetchMock.batchedFunction.mockReturnValue(fetchMock);
const inspectorServiceMock = {
open: () => {},
} as unknown as InspectorStart;
bfetchSetup = bfetchPluginMock.createSetupContract();
bfetchSetup.batchedFunction.mockReturnValue(fetchMock);
searchInterceptor = new SearchInterceptor({
bfetch: bfetchSetup,
toasts: mockCoreSetup.notifications.toasts,
startServices: new Promise((resolve) => {
resolve([mockCoreStart, {}, {}]);
resolve([
mockCoreStart,
{ inspector: inspectorServiceMock } as unknown as SearchServiceStartDependencies,
{},
]);
}),
uiSettings: mockCoreSetup.uiSettings,
http: mockCoreSetup.http,
@ -149,13 +159,16 @@ describe('SearchInterceptor', () => {
test('Renders a PainlessError', async () => {
searchInterceptor.showError(
new PainlessError({
statusCode: 400,
message: 'search_phase_execution_exception',
attributes: {
error: searchPhaseException.error,
new PainlessError(
{
statusCode: 400,
message: 'search_phase_execution_exception',
attributes: {
error: searchPhaseException.error,
},
},
})
() => {}
)
);
expect(mockCoreSetup.notifications.toasts.addDanger).toBeCalledTimes(1);
expect(mockCoreSetup.notifications.toasts.addError).not.toBeCalled();

View file

@ -6,6 +6,7 @@
* Side Public License, v 1.
*/
import { v4 as uuidv4 } from 'uuid';
import { memoize, once } from 'lodash';
import {
BehaviorSubject,
@ -29,9 +30,12 @@ import {
takeUntil,
tap,
} from 'rxjs/operators';
import { estypes } from '@elastic/elasticsearch';
import { i18n } from '@kbn/i18n';
import { PublicMethodsOf } from '@kbn/utility-types';
import type { HttpSetup, IHttpFetchError } from '@kbn/core-http-browser';
import { BfetchRequestError } from '@kbn/bfetch-plugin/public';
import { type Start as InspectorStart, RequestAdapter } from '@kbn/inspector-plugin/public';
import {
ApplicationStart,
@ -73,13 +77,14 @@ import { SearchResponseCache } from './search_response_cache';
import { createRequestHash, getSearchErrorOverrideDisplay } from './utils';
import { SearchAbortController } from './search_abort_controller';
import { SearchConfigSchema } from '../../../config';
import type { SearchServiceStartDependencies } from '../search_service';
export interface SearchInterceptorDeps {
bfetch: BfetchPublicSetup;
http: HttpSetup;
executionContext: ExecutionContextSetup;
uiSettings: IUiSettingsClient;
startServices: Promise<[CoreStart, any, unknown]>;
startServices: Promise<[CoreStart, object, unknown]>;
toasts: ToastsSetup;
usageCollector?: SearchUsageCollector;
session: ISessionService;
@ -114,6 +119,7 @@ export class SearchInterceptor {
{ request: IKibanaSearchRequest; options: ISearchOptionsSerializable },
IKibanaSearchResponse
>;
private inspector!: InspectorStart;
/*
* @internal
@ -121,9 +127,10 @@ export class SearchInterceptor {
constructor(private readonly deps: SearchInterceptorDeps) {
this.deps.http.addLoadingCountSource(this.pendingCount$);
this.deps.startServices.then(([coreStart]) => {
this.deps.startServices.then(([coreStart, depsStart]) => {
this.application = coreStart.application;
this.docLinks = coreStart.docLinks;
this.inspector = (depsStart as SearchServiceStartDependencies).inspector;
});
this.batchedFetch = deps.bfetch.batchedFunction({
@ -184,7 +191,8 @@ export class SearchInterceptor {
*/
private handleSearchError(
e: KibanaServerError | AbortError,
options?: ISearchOptions,
requestBody: estypes.SearchRequest,
options?: IAsyncSearchOptions,
isTimeout?: boolean
): Error {
if (isTimeout || e.message === 'Request timed out') {
@ -203,7 +211,36 @@ export class SearchInterceptor {
}
if (isEsError(e)) {
return isPainlessError(e) ? new PainlessError(e, options?.indexPattern) : new EsError(e);
const openInInspector = () => {
const requestId = options?.inspector?.id ?? uuidv4();
const requestAdapter = options?.inspector?.adapter ?? new RequestAdapter();
if (!options?.inspector?.adapter) {
const requestResponder = requestAdapter.start(
i18n.translate('data.searchService.anonymousRequestTitle', {
defaultMessage: 'Request',
}),
{
id: requestId,
}
);
requestResponder.json(requestBody);
requestResponder.error({ json: e.attributes });
}
this.inspector.open(
{
requests: requestAdapter,
},
{
options: {
initialRequestId: requestId,
initialTabs: ['clusters', 'response'],
},
}
);
};
return isPainlessError(e)
? new PainlessError(e, openInInspector, options?.indexPattern)
: new EsError(e, openInInspector);
}
return e instanceof Error ? e : new Error(e.message);
@ -473,7 +510,12 @@ export class SearchInterceptor {
takeUntil(aborted$),
catchError((e) => {
return throwError(
this.handleSearchError(e, searchOptions, searchAbortController.isTimeout())
this.handleSearchError(
e,
request?.params?.body ?? {},
searchOptions,
searchAbortController.isTimeout()
)
);
}),
tap((response) => {

View file

@ -24,13 +24,14 @@ export function getSearchErrorOverrideDisplay({
}: {
error: Error;
application: ApplicationStart;
}): { title: string; body: ReactNode } | undefined {
}): { title: string; body: ReactNode; actions?: ReactNode[] } | undefined {
if (error instanceof EsError) {
return {
title: i18n.translate('data.search.esErrorTitle', {
defaultMessage: 'Cannot retrieve search results',
}),
body: error.getErrorMessage(application),
body: error.getErrorMessage(),
actions: error.getActions(application),
};
}

View file

@ -32,6 +32,7 @@ export const ErrorCallout = ({ title, error }: Props) => {
iconType="error"
color="danger"
title={<h2 data-test-subj="discoverErrorCalloutTitle">{overrideDisplay?.title ?? title}</h2>}
actions={overrideDisplay?.actions ?? []}
body={
<div
css={css`

View file

@ -13,7 +13,9 @@ import { EuiButtonEmpty, EuiEmptyPrompt, EuiText } from '@elastic/eui';
import type { MaybePromise } from '@kbn/utility-types';
import { Markdown } from '@kbn/kibana-react-plugin/public';
import { getSearchErrorOverrideDisplay } from '@kbn/data-plugin/public';
import { ErrorLike } from '@kbn/expressions-plugin/common';
import { core } from '../kibana_services';
import { EditPanelAction } from './panel_actions';
import { EmbeddableInput, EmbeddableOutput, IEmbeddable } from '../lib/embeddables';
@ -51,6 +53,20 @@ export function EmbeddablePanelError({
[label, title]
);
const overrideDisplay = getSearchErrorOverrideDisplay({
error,
application: core.application,
});
const actions = overrideDisplay?.actions ?? [];
if (isEditable) {
actions.push(
<EuiButtonEmpty aria-label={ariaLabel} onClick={handleErrorClick} size="s">
{label}
</EuiButtonEmpty>
);
}
useEffect(() => {
const subscription = merge(embeddable.getInput$(), embeddable.getOutput$())
.pipe(
@ -65,25 +81,21 @@ export function EmbeddablePanelError({
return (
<EuiEmptyPrompt
body={
<EuiText size="s">
<Markdown
markdown={error.message}
openLinksInNewTab={true}
data-test-subj="errorMessageMarkdown"
/>
</EuiText>
overrideDisplay?.body ?? (
<EuiText size="s">
<Markdown
markdown={error.message}
openLinksInNewTab={true}
data-test-subj="errorMessageMarkdown"
/>
</EuiText>
)
}
data-test-subj="embeddableStackError"
iconType="warning"
iconColor="danger"
layout="vertical"
actions={
isEditable && (
<EuiButtonEmpty aria-label={ariaLabel} onClick={handleErrorClick} size="s">
{label}
</EuiButtonEmpty>
)
}
actions={actions}
/>
);
}

View file

@ -54,7 +54,6 @@ import {
getTelemetryFunction,
} from '../common/lib';
import { getAllMigrations } from '../common/lib/get_all_migrations';
import { setTheme } from './services';
import { setKibanaServices } from './kibana_services';
import { CustomTimeRangeBadge, EditPanelAction } from './embeddable_panel/panel_actions';
@ -120,7 +119,6 @@ export class EmbeddablePublicPlugin implements Plugin<EmbeddableSetup, Embeddabl
constructor(initializerContext: PluginInitializerContext) {}
public setup(core: CoreSetup, { uiActions }: EmbeddableSetupDependencies) {
setTheme(core.theme);
bootstrap(uiActions);
return {

View file

@ -1,12 +0,0 @@
/*
* 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 { ThemeServiceSetup } from '@kbn/core/public';
import { createGetterSetter } from '@kbn/kibana-utils-plugin/public';
export const [getTheme, setTheme] = createGetterSetter<ThemeServiceSetup>('Theme');

View file

@ -123,5 +123,5 @@ export type DataRequestDescriptor = {
dataRequestToken?: symbol;
data?: object;
dataRequestMeta?: DataRequestMeta;
error?: string;
error?: Error;
};

View file

@ -62,7 +62,7 @@ export type DataRequestContext = {
data: object,
resultsMeta?: DataRequestMeta
): void;
onLoadError(dataId: string, requestToken: symbol, errorMessage: string): void;
onLoadError(dataId: string, requestToken: symbol, error: Error): void;
setJoinError(joinIndex: number, errorMessage?: string): void;
updateSourceData(newData: object): void;
isRequestStillActive(dataId: string, requestToken: symbol): boolean;
@ -132,8 +132,8 @@ function getDataRequestContext(
dispatch(startDataLoad(layerId, dataId, requestToken, meta)),
stopLoading: (dataId: string, requestToken: symbol, data: object, meta: DataRequestMeta) =>
dispatch(endDataLoad(layerId, dataId, requestToken, data, meta)),
onLoadError: (dataId: string, requestToken: symbol, errorMessage: string) =>
dispatch(onDataLoadError(layerId, dataId, requestToken, errorMessage)),
onLoadError: (dataId: string, requestToken: symbol, error: Error) =>
dispatch(onDataLoadError(layerId, dataId, requestToken, error)),
setJoinError: (joinIndex: number, errorMessage?: string) => {
dispatch(setJoinError(layerId, joinIndex, errorMessage));
},
@ -312,12 +312,7 @@ function endDataLoad(
};
}
function onDataLoadError(
layerId: string,
dataId: string,
requestToken: symbol,
errorMessage: string
) {
function onDataLoadError(layerId: string, dataId: string, requestToken: symbol, error: Error) {
return async (
dispatch: ThunkDispatch<MapStoreState, void, AnyAction>,
getState: () => MapStoreState
@ -329,7 +324,7 @@ function onDataLoadError(
eventHandlers.onDataLoadError({
layerId,
dataId,
errorMessage,
errorMessage: error.message,
});
}
@ -337,7 +332,7 @@ function onDataLoadError(
type: LAYER_DATA_LOAD_ERROR,
layerId,
dataId,
errorMessage,
error,
requestToken,
});
};

View file

@ -13,7 +13,7 @@ import { DataRequestMeta, DataFilters } from '../../../../common/descriptor_type
export class MockSyncContext implements DataRequestContext {
dataFilters: DataFilters;
isRequestStillActive: (dataId: string, requestToken: symbol) => boolean;
onLoadError: (dataId: string, requestToken: symbol, errorMessage: string) => void;
onLoadError: (dataId: string, requestToken: symbol, error: Error) => void;
registerCancelCallback: (requestToken: symbol, callback: () => void) => void;
startLoading: (dataId: string, requestToken: symbol, meta: DataRequestMeta) => void;
stopLoading: (dataId: string, requestToken: symbol, data: object, meta: DataRequestMeta) => void;

View file

@ -43,8 +43,8 @@ describe('EmsVectorTileLayer', () => {
startLoading: (requestId: string, token: string, meta: unknown) => {
actualMeta = meta;
},
onLoadError: (requestId: string, token: string, message: string) => {
actualErrorMessage = message;
onLoadError: (requestId: string, token: string, error: Error) => {
actualErrorMessage = error.message;
},
dataFilters: { foo: 'bar' } as unknown as DataFilters,
} as unknown as DataRequestContext;

View file

@ -137,7 +137,7 @@ export class EmsVectorTileLayer extends AbstractLayer {
};
stopLoading(SOURCE_DATA_REQUEST_ID, requestToken, data);
} catch (error) {
onLoadError(SOURCE_DATA_REQUEST_ID, requestToken, error.message);
onLoadError(SOURCE_DATA_REQUEST_ID, requestToken, error);
}
}

View file

@ -408,7 +408,7 @@ export class AbstractLayer implements ILayer {
getErrors(): LayerError[] {
const errors: LayerError[] = [];
const sourceError = this.getSourceDataRequest()?.getError();
const sourceError = this.getSourceDataRequest()?.renderError();
if (sourceError) {
errors.push({
title: this._getSourceErrorTitle(),

View file

@ -99,7 +99,7 @@ export class RasterTileLayer extends AbstractLayer {
};
stopLoading(SOURCE_DATA_REQUEST_ID, requestToken, data, {});
} catch (error) {
onLoadError(SOURCE_DATA_REQUEST_ID, requestToken, error.message);
onLoadError(SOURCE_DATA_REQUEST_ID, requestToken, error);
}
}

View file

@ -320,7 +320,7 @@ export class BlendedVectorLayer extends GeoJsonVectorLayer implements IVectorLay
syncContext.stopLoading(dataRequestId, requestToken, { isSyncClustered }, requestMeta);
} catch (error) {
if (!(error instanceof DataRequestAbortError) || !isSearchSourceAbortError(error)) {
syncContext.onLoadError(dataRequestId, requestToken, error.message);
syncContext.onLoadError(dataRequestId, requestToken, error);
}
return;
}

View file

@ -98,7 +98,7 @@ export async function syncGeojsonSourceData({
};
} catch (error) {
if (!(error instanceof DataRequestAbortError)) {
onLoadError(dataRequestId, requestToken, error.message);
onLoadError(dataRequestId, requestToken, error);
}
throw error;
}

View file

@ -97,6 +97,6 @@ export async function syncMvtSourceData({
};
syncContext.stopLoading(SOURCE_DATA_REQUEST_ID, requestToken, sourceData, {});
} catch (error) {
syncContext.onLoadError(SOURCE_DATA_REQUEST_ID, requestToken, error.message);
syncContext.onLoadError(SOURCE_DATA_REQUEST_ID, requestToken, error);
}
}

View file

@ -279,13 +279,13 @@ export class AbstractVectorLayer extends AbstractLayer implements IVectorLayer {
this.getValidJoins().forEach((join) => {
const joinDataRequest = this.getDataRequest(join.getSourceDataRequestId());
const error = joinDataRequest?.getError();
if (error) {
const joinError = joinDataRequest?.renderError();
if (joinError) {
errors.push({
title: i18n.translate('xpack.maps.vectorLayer.joinFetchErrorTitle', {
defaultMessage: `An error occurred when loading join metrics`,
}),
error,
error: joinError,
});
}
});
@ -484,7 +484,7 @@ export class AbstractVectorLayer extends AbstractLayer implements IVectorLayer {
stopLoading(dataRequestId, requestToken, styleMeta, nextMeta);
} catch (error) {
if (!(error instanceof DataRequestAbortError)) {
onLoadError(dataRequestId, requestToken, error.message);
onLoadError(dataRequestId, requestToken, error);
}
throw error;
}
@ -554,7 +554,7 @@ export class AbstractVectorLayer extends AbstractLayer implements IVectorLayer {
stopLoading(dataRequestId, requestToken, formatters, nextMeta);
} catch (error) {
onLoadError(dataRequestId, requestToken, error.message);
onLoadError(dataRequestId, requestToken, error);
throw error;
}
}
@ -629,7 +629,7 @@ export class AbstractVectorLayer extends AbstractLayer implements IVectorLayer {
};
} catch (error) {
if (!(error instanceof DataRequestAbortError)) {
onLoadError(sourceDataId, requestToken, `Join error: ${error.message}`);
onLoadError(sourceDataId, requestToken, error);
}
throw error;
}
@ -727,7 +727,7 @@ export class AbstractVectorLayer extends AbstractLayer implements IVectorLayer {
const supportsFeatureEditing = await source.supportsFeatureEditing();
stopLoading(dataRequestId, requestToken, { supportsFeatureEditing });
} catch (error) {
onLoadError(dataRequestId, requestToken, error.message);
onLoadError(dataRequestId, requestToken, error);
throw error;
}
}

View file

@ -198,12 +198,7 @@ export class AbstractESSource extends AbstractVectorSource implements IESSource
throw new DataRequestAbortError();
}
throw new Error(
i18n.translate('xpack.maps.source.esSource.requestFailedErrorMessage', {
defaultMessage: `Elasticsearch search request failed, error: {message}`,
values: { message: error.message },
})
);
throw error;
}
}

View file

@ -7,7 +7,10 @@
/* eslint-disable max-classes-per-file */
import { DataRequestDescriptor, DataRequestMeta } from '../../../common/descriptor_types';
import React, { ReactNode } from 'react';
import { getSearchErrorOverrideDisplay } from '@kbn/data-plugin/public';
import { getApplication } from '../../kibana_services';
import type { DataRequestDescriptor, DataRequestMeta } from '../../../common/descriptor_types';
export class DataRequest {
private readonly _descriptor: DataRequestDescriptor;
@ -52,8 +55,30 @@ export class DataRequest {
return this._descriptor.dataRequestToken;
}
getError(): string | undefined {
return this._descriptor.error;
renderError(): ReactNode {
if (!this._descriptor.error) {
return null;
}
const overrideDisplay = getSearchErrorOverrideDisplay({
error: this._descriptor.error,
application: getApplication(),
});
const body = overrideDisplay?.body ? (
overrideDisplay.body
) : (
<p>{this._descriptor.error.message}</p>
);
return overrideDisplay?.actions ? (
<>
{body}
{overrideDisplay.actions}
</>
) : (
body
);
}
}

View file

@ -51,7 +51,7 @@ export function stopDataRequest(
requestToken: symbol,
responseMeta?: DataRequestMeta,
data?: object,
errorMessage?: string
error?: Error
): MapState {
const dataRequest = getDataRequest(state, layerId, dataRequestId, requestToken);
return dataRequest
@ -65,7 +65,7 @@ export function stopDataRequest(
},
dataRequestMetaAtStart: undefined,
dataRequestToken: undefined, // active data request,
error: errorMessage,
error,
})
: state;
}

View file

@ -193,7 +193,7 @@ export function map(state: MapState = DEFAULT_MAP_STATE, action: Record<string,
action.requestToken,
undefined, // responseMeta meta
undefined, // response data
action.errorMessage
action.error
);
case LAYER_DATA_LOAD_ENDED:
return stopDataRequest(

View file

@ -23595,7 +23595,6 @@
"xpack.maps.source.esSearch.loadTooltipPropertiesErrorMsg": "Document introuvable, _id : {docId}",
"xpack.maps.source.esSearch.mvtScalingJoinMsg": "Les tuiles vectorielles prennent en charge une seule liaison de terme. Votre calque a {numberOfJoins} liaisons. Le passage aux tuiles vectorielles conservera la première liaison de terme et supprimera toutes les autres liaisons de votre configuration de calque.",
"xpack.maps.source.esSource.noGeoFieldErrorMessage": "La vue de données \"{indexPatternLabel}\" ne contient plus le champ géographique \"{geoField}\"",
"xpack.maps.source.esSource.requestFailedErrorMessage": "Échec de la demande de recherche Elasticsearch, erreur : {message}",
"xpack.maps.source.esSource.stylePropsMetaRequestName": "{layerName} - Métadonnées",
"xpack.maps.source.MVTSingleLayerVectorSourceEditor.urlHelpMessage": "URL du service de cartographie vectoriel .mvt. Par exemple, {url}",
"xpack.maps.source.pewPew.inspectorDescription": "Obtenir des chemins depuis la vue de données : {dataViewName} ; source : {sourceFieldName} ; destination : {destFieldName}",

View file

@ -23609,7 +23609,6 @@
"xpack.maps.source.esSearch.loadTooltipPropertiesErrorMsg": "ドキュメントが見つかりません。_id: {docId}",
"xpack.maps.source.esSearch.mvtScalingJoinMsg": "ベクトルタイルは1つの用語結合をサポートします。レイヤーには{numberOfJoins}個の結合があります。ベクトルタイルに切り替えると、最初の結合が保持され、すべての他の用語結合がレイヤー構成から削除されます。",
"xpack.maps.source.esSource.noGeoFieldErrorMessage": "データビュー\"{indexPatternLabel}\"には現在ジオフィールド\"{geoField}\"が含まれていません",
"xpack.maps.source.esSource.requestFailedErrorMessage": "Elasticsearch検索リクエストを実行できませんでした。エラー{message}",
"xpack.maps.source.esSource.stylePropsMetaRequestName": "{layerName} - メタデータ",
"xpack.maps.source.MVTSingleLayerVectorSourceEditor.urlHelpMessage": ".mvtベクトルタイルサービスのURL。例{url}",
"xpack.maps.source.pewPew.inspectorDescription": "データビューからパスを取得します:{dataViewName}、ソース:{sourceFieldName}、デスティネーション:{destFieldName}",

View file

@ -23609,7 +23609,6 @@
"xpack.maps.source.esSearch.loadTooltipPropertiesErrorMsg": "找不到文档_id{docId}",
"xpack.maps.source.esSearch.mvtScalingJoinMsg": "矢量磁贴支持一个词联接。您的图层具有 {numberOfJoins} 个联接。切换到矢量磁贴会保留第一个词联接,并从图层配置中移除所有其他联接。",
"xpack.maps.source.esSource.noGeoFieldErrorMessage": "数据视图“{indexPatternLabel}”不再包含地理字段“{geoField}”",
"xpack.maps.source.esSource.requestFailedErrorMessage": "Elasticsearch 搜索请求失败,错误:{message}",
"xpack.maps.source.esSource.stylePropsMetaRequestName": "{layerName} - 元数据",
"xpack.maps.source.MVTSingleLayerVectorSourceEditor.urlHelpMessage": ".mvt 矢量磁帖服务的 URL。例如 {url}",
"xpack.maps.source.pewPew.inspectorDescription": "获取路径的数据视图:{dataViewName}、源:{sourceFieldName}、目标:{destFieldName}",