Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improved painless error toasts #91346

Merged
merged 11 commits into from
Feb 16, 2021
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->

[Home](./index.md) &gt; [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) &gt; [ISearchOptions](./kibana-plugin-plugins-data-public.isearchoptions.md) &gt; [indexPattern](./kibana-plugin-plugins-data-public.isearchoptions.indexpattern.md)

## ISearchOptions.indexPattern property

Index pattern reference is used for better error messages

<b>Signature:</b>

```typescript
indexPattern?: IndexPattern;
```
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export interface ISearchOptions
| Property | Type | Description |
| --- | --- | --- |
| [abortSignal](./kibana-plugin-plugins-data-public.isearchoptions.abortsignal.md) | <code>AbortSignal</code> | An <code>AbortSignal</code> that allows the caller of <code>search</code> to abort a search request. |
| [indexPattern](./kibana-plugin-plugins-data-public.isearchoptions.indexpattern.md) | <code>IndexPattern</code> | Index pattern reference is used for better error messages |
| [isRestore](./kibana-plugin-plugins-data-public.isearchoptions.isrestore.md) | <code>boolean</code> | Whether the session is restored (i.e. search requests should re-use the stored search IDs, rather than starting from scratch) |
| [isStored](./kibana-plugin-plugins-data-public.isearchoptions.isstored.md) | <code>boolean</code> | Whether the session is already saved (i.e. sent to background) |
| [legacyHitsTotal](./kibana-plugin-plugins-data-public.isearchoptions.legacyhitstotal.md) | <code>boolean</code> | Request the legacy format for the total number of hits. If sending <code>rest_total_hits_as_int</code> to something other than <code>true</code>, this should be set to <code>false</code>. |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,13 @@ Constructs a new instance of the `PainlessError` class
<b>Signature:</b>

```typescript
constructor(err: IEsError);
constructor(err: IEsError, indexPattern?: IndexPattern);
```

## Parameters

| Parameter | Type | Description |
| --- | --- | --- |
| err | <code>IEsError</code> | |
| indexPattern | <code>IndexPattern</code> | |

Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->

[Home](./index.md) &gt; [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) &gt; [PainlessError](./kibana-plugin-plugins-data-public.painlesserror.md) &gt; [indexPattern](./kibana-plugin-plugins-data-public.painlesserror.indexpattern.md)

## PainlessError.indexPattern property

<b>Signature:</b>

```typescript
indexPattern?: IndexPattern;
```
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,13 @@ export declare class PainlessError extends EsError

| Constructor | Modifiers | Description |
| --- | --- | --- |
| [(constructor)(err)](./kibana-plugin-plugins-data-public.painlesserror._constructor_.md) | | Constructs a new instance of the <code>PainlessError</code> class |
| [(constructor)(err, indexPattern)](./kibana-plugin-plugins-data-public.painlesserror._constructor_.md) | | Constructs a new instance of the <code>PainlessError</code> class |

## Properties

| Property | Modifiers | Type | Description |
| --- | --- | --- | --- |
| [indexPattern](./kibana-plugin-plugins-data-public.painlesserror.indexpattern.md) | | <code>IndexPattern</code> | |
| [painlessStack](./kibana-plugin-plugins-data-public.painlesserror.painlessstack.md) | | <code>string</code> | |

## Methods
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->

[Home](./index.md) &gt; [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) &gt; [ISearchOptions](./kibana-plugin-plugins-data-server.isearchoptions.md) &gt; [indexPattern](./kibana-plugin-plugins-data-server.isearchoptions.indexpattern.md)

## ISearchOptions.indexPattern property

Index pattern reference is used for better error messages

<b>Signature:</b>

```typescript
indexPattern?: IndexPattern;
```
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export interface ISearchOptions
| Property | Type | Description |
| --- | --- | --- |
| [abortSignal](./kibana-plugin-plugins-data-server.isearchoptions.abortsignal.md) | <code>AbortSignal</code> | An <code>AbortSignal</code> that allows the caller of <code>search</code> to abort a search request. |
| [indexPattern](./kibana-plugin-plugins-data-server.isearchoptions.indexpattern.md) | <code>IndexPattern</code> | Index pattern reference is used for better error messages |
| [isRestore](./kibana-plugin-plugins-data-server.isearchoptions.isrestore.md) | <code>boolean</code> | Whether the session is restored (i.e. search requests should re-use the stored search IDs, rather than starting from scratch) |
| [isStored](./kibana-plugin-plugins-data-server.isearchoptions.isstored.md) | <code>boolean</code> | Whether the session is already saved (i.e. sent to background) |
| [legacyHitsTotal](./kibana-plugin-plugins-data-server.isearchoptions.legacyhitstotal.md) | <code>boolean</code> | Request the legacy format for the total number of hits. If sending <code>rest_total_hits_as_int</code> to something other than <code>true</code>, this should be set to <code>false</code>. |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ start(core: CoreStart): {
fieldFormatServiceFactory: (uiSettings: import("../../../core/server").IUiSettingsClient) => Promise<import("../common").FieldFormatsRegistry>;
};
indexPatterns: {
indexPatternsServiceFactory: (savedObjectsClient: Pick<import("../../../core/server").SavedObjectsClient, "get" | "delete" | "create" | "update" | "bulkCreate" | "checkConflicts" | "find" | "bulkGet" | "resolve" | "addToNamespaces" | "deleteFromNamespaces" | "bulkUpdate" | "removeReferencesTo" | "errors">, elasticsearchClient: import("../../../core/server").ElasticsearchClient) => Promise<import("../public").IndexPatternsService>;
indexPatternsServiceFactory: (savedObjectsClient: Pick<import("../../../core/server").SavedObjectsClient, "get" | "delete" | "closePointInTime" | "create" | "update" | "bulkCreate" | "checkConflicts" | "find" | "bulkGet" | "resolve" | "addToNamespaces" | "deleteFromNamespaces" | "bulkUpdate" | "removeReferencesTo" | "openPointInTimeForType" | "errors">, elasticsearchClient: import("../../../core/server").ElasticsearchClient) => Promise<import("../public").IndexPatternsService>;
};
search: ISearchStart<import("./search").IEsSearchRequest, import("./search").IEsSearchResponse<any>>;
};
Expand All @@ -31,7 +31,7 @@ start(core: CoreStart): {
fieldFormatServiceFactory: (uiSettings: import("../../../core/server").IUiSettingsClient) => Promise<import("../common").FieldFormatsRegistry>;
};
indexPatterns: {
indexPatternsServiceFactory: (savedObjectsClient: Pick<import("../../../core/server").SavedObjectsClient, "get" | "delete" | "create" | "update" | "bulkCreate" | "checkConflicts" | "find" | "bulkGet" | "resolve" | "addToNamespaces" | "deleteFromNamespaces" | "bulkUpdate" | "removeReferencesTo" | "errors">, elasticsearchClient: import("../../../core/server").ElasticsearchClient) => Promise<import("../public").IndexPatternsService>;
indexPatternsServiceFactory: (savedObjectsClient: Pick<import("../../../core/server").SavedObjectsClient, "get" | "delete" | "closePointInTime" | "create" | "update" | "bulkCreate" | "checkConflicts" | "find" | "bulkGet" | "resolve" | "addToNamespaces" | "deleteFromNamespaces" | "bulkUpdate" | "removeReferencesTo" | "openPointInTimeForType" | "errors">, elasticsearchClient: import("../../../core/server").ElasticsearchClient) => Promise<import("../public").IndexPatternsService>;
};
search: ISearchStart<import("./search").IEsSearchRequest, import("./search").IEsSearchResponse<any>>;
}`
Expand Down
3 changes: 3 additions & 0 deletions src/plugins/data/common/search/search_source/search_source.ts
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,9 @@ export class SearchSource {
switchMap(() => {
const searchRequest = this.flatten();
this.history = [searchRequest];
if (searchRequest.index) {
options.indexPattern = searchRequest.index;
}

return getConfig(UI_SETTINGS.COURIER_BATCH_SEARCHES)
? from(this.legacyFetch(searchRequest, options))
Expand Down
7 changes: 7 additions & 0 deletions src/plugins/data/common/search/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

import { Observable } from 'rxjs';
import { IEsSearchRequest, IEsSearchResponse } from './es_search';
import { IndexPattern } from '..';

export type ISearchGeneric = <
SearchStrategyRequest extends IKibanaSearchRequest = IEsSearchRequest,
Expand Down Expand Up @@ -111,4 +112,10 @@ export interface ISearchOptions {
* rather than starting from scratch)
*/
isRestore?: boolean;

/**
* Index pattern reference is used for better error messages
*/

indexPattern?: IndexPattern;
}
5 changes: 4 additions & 1 deletion src/plugins/data/public/public.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -1640,6 +1640,7 @@ export type ISearchGeneric = <SearchStrategyRequest extends IKibanaSearchRequest
// @public (undocumented)
export interface ISearchOptions {
abortSignal?: AbortSignal;
indexPattern?: IndexPattern;
isRestore?: boolean;
isStored?: boolean;
legacyHitsTotal?: boolean;
Expand Down Expand Up @@ -1870,10 +1871,12 @@ export interface OptionedValueProp {
// @public (undocumented)
export class PainlessError extends EsError {
// Warning: (ae-forgotten-export) The symbol "IEsError" needs to be exported by the entry point index.d.ts
constructor(err: IEsError);
constructor(err: IEsError, indexPattern?: IndexPattern);
// (undocumented)
getErrorMessage(application: ApplicationStart): JSX.Element;
// (undocumented)
indexPattern?: IndexPattern;
// (undocumented)
painlessStack?: string;
}

Expand Down
12 changes: 7 additions & 5 deletions src/plugins/data/public/search/errors/painless_error.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,16 +27,18 @@ describe('PainlessError', () => {
});
const component = mount(e.getErrorMessage(startMock.application));

const scriptElem = findTestSubject(component, 'painlessScript').getDOMNode();

const failedShards = e.attributes?.failed_shards![0];
const script = failedShards!.reason.script;
expect(scriptElem.textContent).toBe(`Error executing Painless script: '${script}'`);

const stackTraceElem = findTestSubject(component, 'painlessStackTrace').getDOMNode();
const stackTrace = failedShards!.reason.script_stack!.join('\n');
const stackTrace = failedShards!.reason.script_stack!.splice(-2).join('\n');
expect(stackTraceElem.textContent).toBe(stackTrace);

const humanReadableError = findTestSubject(
component,
'painlessHumanReadableError'
).getDOMNode();
expect(humanReadableError.textContent).toBe(failedShards?.reason.caused_by?.reason);

expect(component.find('EuiButton').length).toBe(1);
});
});
37 changes: 28 additions & 9 deletions src/plugins/data/public/search/errors/painless_error.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,40 +14,59 @@ import { ApplicationStart } from 'kibana/public';
import { IEsError, isEsError } from './types';
import { EsError } from './es_error';
import { getRootCause } from './utils';
import { IndexPattern } from '../..';

export class PainlessError extends EsError {
painlessStack?: string;
constructor(err: IEsError) {
indexPattern?: IndexPattern;
constructor(err: IEsError, indexPattern?: IndexPattern) {
super(err);
this.indexPattern = indexPattern;
}

public getErrorMessage(application: ApplicationStart) {
function onClick() {
function onClick(indexPatternId?: string) {
application.navigateToApp('management', {
path: `/kibana/indexPatterns`,
path: `/kibana/indexPatterns${indexPatternId ? `/patterns/${indexPatternId}` : ''}`,
});
}

const rootCause = getRootCause(this.err);
const scriptFromStackTrace = rootCause?.script_stack
? rootCause?.script_stack?.slice(-2).join('\n')
: undefined;
// if the error has been properly processed it will highlight where it occurred.
const hasScript = rootCause?.script_stack?.slice(-1)[0]?.indexOf('HERE') || -1 >= 0;
const humanReadableError = rootCause?.caused_by?.reason;
// fallback, show ES stacktrace
const painlessStack = rootCause?.script_stack ? rootCause?.script_stack.join('\n') : undefined;

const indexPatternId = this?.indexPattern?.id;
return (
<>
<EuiText data-test-subj="painlessScript">
<EuiText size="s" data-test-subj="painlessScript">
{i18n.translate('data.painlessError.painlessScriptedFieldErrorMessage', {
defaultMessage: "Error executing Painless script: '{script}'",
values: { script: rootCause?.script },
defaultMessage:
'Error executing runtime field or scripted field on index pattern {indexPatternName}',
values: {
indexPatternName: this?.indexPattern?.title,
},
})}
</EuiText>
<EuiSpacer size="s" />
<EuiSpacer size="s" />
{painlessStack ? (
{scriptFromStackTrace || painlessStack ? (
<EuiCodeBlock data-test-subj="painlessStackTrace" isCopyable={true} paddingSize="s">
{painlessStack}
{hasScript ? scriptFromStackTrace : painlessStack}
</EuiCodeBlock>
) : null}
{humanReadableError ? (
<EuiText data-test-subj="painlessHumanReadableError">{humanReadableError}</EuiText>
) : null}
<EuiSpacer size="s" />
<EuiSpacer size="s" />
<EuiText textAlign="right">
<EuiButton color="danger" onClick={onClick} size="s">
<EuiButton color="danger" onClick={() => onClick(indexPatternId)} size="s">
<FormattedMessage id="data.painlessError.buttonTxt" defaultMessage="Edit script" />
</EuiButton>
</EuiText>
Expand Down
2 changes: 1 addition & 1 deletion src/plugins/data/public/search/search_interceptor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ export class SearchInterceptor {
return e;
} else if (isEsError(e)) {
if (isPainlessError(e)) {
return new PainlessError(e);
return new PainlessError(e, options?.indexPattern);
} else {
return new EsError(e);
}
Expand Down
1 change: 1 addition & 0 deletions src/plugins/data/server/server.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -912,6 +912,7 @@ export class IndexPatternsService implements Plugin_3<void, IndexPatternsService
// @public (undocumented)
export interface ISearchOptions {
abortSignal?: AbortSignal;
indexPattern?: IndexPattern;
isRestore?: boolean;
isStored?: boolean;
legacyHitsTotal?: boolean;
Expand Down
1 change: 0 additions & 1 deletion x-pack/plugins/translations/translations/ja-JP.json
Original file line number Diff line number Diff line change
Expand Up @@ -892,7 +892,6 @@
"data.noDataPopover.subtitle": "ヒント",
"data.noDataPopover.title": "空のデータセット",
"data.painlessError.buttonTxt": "スクリプトを編集",
"data.painlessError.painlessScriptedFieldErrorMessage": "Painlessスクリプトの実行エラー:「{script}」。",
"data.parseEsInterval.invalidEsCalendarIntervalErrorMessage": "無効なカレンダー間隔:{interval}、1よりも大きな値が必要です",
"data.parseEsInterval.invalidEsIntervalFormatErrorMessage": "無効な間隔形式:{interval}",
"data.query.queryBar.comboboxAriaLabel": "{pageType} ページの検索とフィルタリング",
Expand Down
1 change: 0 additions & 1 deletion x-pack/plugins/translations/translations/zh-CN.json
Original file line number Diff line number Diff line change
Expand Up @@ -892,7 +892,6 @@
"data.noDataPopover.subtitle": "提示",
"data.noDataPopover.title": "空数据集",
"data.painlessError.buttonTxt": "编辑脚本",
"data.painlessError.painlessScriptedFieldErrorMessage": "执行 Painless 脚本时出错:“{script}”。",
"data.parseEsInterval.invalidEsCalendarIntervalErrorMessage": "无效的日历时间间隔:{interval},值必须为 1",
"data.parseEsInterval.invalidEsIntervalFormatErrorMessage": "时间间隔格式无效:{interval}",
"data.query.queryBar.comboboxAriaLabel": "搜索并筛选 {pageType} 页面",
Expand Down