mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
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:
parent
36c0d8d5e0
commit
f9870c160d
31 changed files with 213 additions and 121 deletions
|
@ -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'),
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.'
|
||||
);
|
||||
|
|
|
@ -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>,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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) => {
|
||||
|
|
|
@ -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),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -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`
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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');
|
|
@ -123,5 +123,5 @@ export type DataRequestDescriptor = {
|
|||
dataRequestToken?: symbol;
|
||||
data?: object;
|
||||
dataRequestMeta?: DataRequestMeta;
|
||||
error?: string;
|
||||
error?: Error;
|
||||
};
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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}",
|
||||
|
|
|
@ -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}",
|
||||
|
|
|
@ -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}",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue