mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
* OSS error alignemnt * Adjust error messages in xpack * Add getErrorMessage * Use showError in vizualize Add original error to expression exception * Cleanup * ts, doc and i18n fixes * Fix jest tests * Fix functional test * functional test * ts * Update functional tests * Add unit tests to interceptor and timeout error * expose toasts test function * doc * typos * review 1 * Code review * doc * doc fix * visualization type fix * fix jest * Fix xpack functional test * fix xpack test * code review * delete debubg flag * Update texts by @gchaps * docs and ts Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com> Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
parent
9b3e54ed03
commit
7918405edc
50 changed files with 849 additions and 465 deletions
|
@ -19,4 +19,5 @@ export interface ISearchStart
|
|||
| [aggs](./kibana-plugin-plugins-data-public.isearchstart.aggs.md) | <code>AggsStart</code> | agg config sub service [AggsStart](./kibana-plugin-plugins-data-public.aggsstart.md) |
|
||||
| [search](./kibana-plugin-plugins-data-public.isearchstart.search.md) | <code>ISearchGeneric</code> | low level search [ISearchGeneric](./kibana-plugin-plugins-data-public.isearchgeneric.md) |
|
||||
| [searchSource](./kibana-plugin-plugins-data-public.isearchstart.searchsource.md) | <code>ISearchStartSearchSource</code> | high level search [ISearchStartSearchSource](./kibana-plugin-plugins-data-public.isearchstartsearchsource.md) |
|
||||
| [showError](./kibana-plugin-plugins-data-public.isearchstart.showerror.md) | <code>(e: Error) => void</code> | |
|
||||
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [ISearchStart](./kibana-plugin-plugins-data-public.isearchstart.md) > [showError](./kibana-plugin-plugins-data-public.isearchstart.showerror.md)
|
||||
|
||||
## ISearchStart.showError property
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
showError: (e: Error) => void;
|
||||
```
|
|
@ -19,10 +19,11 @@
|
|||
| [IndexPatternSelect](./kibana-plugin-plugins-data-public.indexpatternselect.md) | |
|
||||
| [IndexPatternsService](./kibana-plugin-plugins-data-public.indexpatternsservice.md) | |
|
||||
| [OptionedParamType](./kibana-plugin-plugins-data-public.optionedparamtype.md) | |
|
||||
| [PainlessError](./kibana-plugin-plugins-data-public.painlesserror.md) | |
|
||||
| [Plugin](./kibana-plugin-plugins-data-public.plugin.md) | |
|
||||
| [RequestTimeoutError](./kibana-plugin-plugins-data-public.requesttimeouterror.md) | Class used to signify that a request timed out. Useful for applications to conditionally handle this type of error differently than other errors. |
|
||||
| [SearchInterceptor](./kibana-plugin-plugins-data-public.searchinterceptor.md) | |
|
||||
| [SearchSource](./kibana-plugin-plugins-data-public.searchsource.md) | \* |
|
||||
| [SearchTimeoutError](./kibana-plugin-plugins-data-public.searchtimeouterror.md) | Request Failure - When an entire multi request fails |
|
||||
| [TimeHistory](./kibana-plugin-plugins-data-public.timehistory.md) | |
|
||||
|
||||
## Enumerations
|
||||
|
@ -35,6 +36,7 @@
|
|||
| [METRIC\_TYPES](./kibana-plugin-plugins-data-public.metric_types.md) | |
|
||||
| [QuerySuggestionTypes](./kibana-plugin-plugins-data-public.querysuggestiontypes.md) | |
|
||||
| [SortDirection](./kibana-plugin-plugins-data-public.sortdirection.md) | |
|
||||
| [TimeoutErrorMode](./kibana-plugin-plugins-data-public.timeouterrormode.md) | |
|
||||
|
||||
## Functions
|
||||
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [PainlessError](./kibana-plugin-plugins-data-public.painlesserror.md) > [(constructor)](./kibana-plugin-plugins-data-public.painlesserror._constructor_.md)
|
||||
|
||||
## PainlessError.(constructor)
|
||||
|
||||
Constructs a new instance of the `PainlessError` class
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
constructor(err: EsError, request: IKibanaSearchRequest);
|
||||
```
|
||||
|
||||
## Parameters
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| err | <code>EsError</code> | |
|
||||
| request | <code>IKibanaSearchRequest</code> | |
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [PainlessError](./kibana-plugin-plugins-data-public.painlesserror.md) > [getErrorMessage](./kibana-plugin-plugins-data-public.painlesserror.geterrormessage.md)
|
||||
|
||||
## PainlessError.getErrorMessage() method
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
getErrorMessage(application: ApplicationStart): JSX.Element;
|
||||
```
|
||||
|
||||
## Parameters
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| application | <code>ApplicationStart</code> | |
|
||||
|
||||
<b>Returns:</b>
|
||||
|
||||
`JSX.Element`
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [PainlessError](./kibana-plugin-plugins-data-public.painlesserror.md)
|
||||
|
||||
## PainlessError class
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
export declare class PainlessError extends KbnError
|
||||
```
|
||||
|
||||
## Constructors
|
||||
|
||||
| Constructor | Modifiers | Description |
|
||||
| --- | --- | --- |
|
||||
| [(constructor)(err, request)](./kibana-plugin-plugins-data-public.painlesserror._constructor_.md) | | Constructs a new instance of the <code>PainlessError</code> class |
|
||||
|
||||
## Properties
|
||||
|
||||
| Property | Modifiers | Type | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| [painlessStack](./kibana-plugin-plugins-data-public.painlesserror.painlessstack.md) | | <code>string</code> | |
|
||||
|
||||
## Methods
|
||||
|
||||
| Method | Modifiers | Description |
|
||||
| --- | --- | --- |
|
||||
| [getErrorMessage(application)](./kibana-plugin-plugins-data-public.painlesserror.geterrormessage.md) | | |
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [PainlessError](./kibana-plugin-plugins-data-public.painlesserror.md) > [painlessStack](./kibana-plugin-plugins-data-public.painlesserror.painlessstack.md)
|
||||
|
||||
## PainlessError.painlessStack property
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
painlessStack?: string;
|
||||
```
|
|
@ -1,20 +0,0 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [RequestTimeoutError](./kibana-plugin-plugins-data-public.requesttimeouterror.md) > [(constructor)](./kibana-plugin-plugins-data-public.requesttimeouterror._constructor_.md)
|
||||
|
||||
## RequestTimeoutError.(constructor)
|
||||
|
||||
Constructs a new instance of the `RequestTimeoutError` class
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
constructor(message?: string);
|
||||
```
|
||||
|
||||
## Parameters
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| message | <code>string</code> | |
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [RequestTimeoutError](./kibana-plugin-plugins-data-public.requesttimeouterror.md)
|
||||
|
||||
## RequestTimeoutError class
|
||||
|
||||
Class used to signify that a request timed out. Useful for applications to conditionally handle this type of error differently than other errors.
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
export declare class RequestTimeoutError extends Error
|
||||
```
|
||||
|
||||
## Constructors
|
||||
|
||||
| Constructor | Modifiers | Description |
|
||||
| --- | --- | --- |
|
||||
| [(constructor)(message)](./kibana-plugin-plugins-data-public.requesttimeouterror._constructor_.md) | | Constructs a new instance of the <code>RequestTimeoutError</code> class |
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [SearchInterceptor](./kibana-plugin-plugins-data-public.searchinterceptor.md) > [getTimeoutMode](./kibana-plugin-plugins-data-public.searchinterceptor.gettimeoutmode.md)
|
||||
|
||||
## SearchInterceptor.getTimeoutMode() method
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
protected getTimeoutMode(): TimeoutErrorMode;
|
||||
```
|
||||
<b>Returns:</b>
|
||||
|
||||
`TimeoutErrorMode`
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [SearchInterceptor](./kibana-plugin-plugins-data-public.searchinterceptor.md) > [handleSearchError](./kibana-plugin-plugins-data-public.searchinterceptor.handlesearcherror.md)
|
||||
|
||||
## SearchInterceptor.handleSearchError() method
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
protected handleSearchError(e: any, request: IKibanaSearchRequest, timeoutSignal: AbortSignal, appAbortSignal?: AbortSignal): Error;
|
||||
```
|
||||
|
||||
## Parameters
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| e | <code>any</code> | |
|
||||
| request | <code>IKibanaSearchRequest</code> | |
|
||||
| timeoutSignal | <code>AbortSignal</code> | |
|
||||
| appAbortSignal | <code>AbortSignal</code> | |
|
||||
|
||||
<b>Returns:</b>
|
||||
|
||||
`Error`
|
||||
|
|
@ -21,11 +21,13 @@ export declare class SearchInterceptor
|
|||
| Property | Modifiers | Type | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| [deps](./kibana-plugin-plugins-data-public.searchinterceptor.deps.md) | | <code>SearchInterceptorDeps</code> | |
|
||||
| [showTimeoutError](./kibana-plugin-plugins-data-public.searchinterceptor.showtimeouterror.md) | | <code>((e: Error) => void) & import("lodash").Cancelable</code> | |
|
||||
|
||||
## Methods
|
||||
|
||||
| Method | Modifiers | Description |
|
||||
| --- | --- | --- |
|
||||
| [getTimeoutMode()](./kibana-plugin-plugins-data-public.searchinterceptor.gettimeoutmode.md) | | |
|
||||
| [handleSearchError(e, request, timeoutSignal, appAbortSignal)](./kibana-plugin-plugins-data-public.searchinterceptor.handlesearcherror.md) | | |
|
||||
| [search(request, options)](./kibana-plugin-plugins-data-public.searchinterceptor.search.md) | | Searches using the given <code>search</code> method. Overrides the <code>AbortSignal</code> with one that will abort either when <code>cancelPending</code> is called, when the request times out, or when the original <code>AbortSignal</code> is aborted. Updates <code>pendingCount$</code> when the request is started/finalized. |
|
||||
| [showError(e)](./kibana-plugin-plugins-data-public.searchinterceptor.showerror.md) | | |
|
||||
|
||||
|
|
|
@ -9,17 +9,19 @@ Searches using the given `search` method. Overrides the `AbortSignal` with one t
|
|||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
search(request: IEsSearchRequest, options?: ISearchOptions): Observable<IKibanaSearchResponse>;
|
||||
search(request: IKibanaSearchRequest, options?: ISearchOptions): Observable<IKibanaSearchResponse>;
|
||||
```
|
||||
|
||||
## Parameters
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| request | <code>IEsSearchRequest</code> | |
|
||||
| request | <code>IKibanaSearchRequest</code> | |
|
||||
| options | <code>ISearchOptions</code> | |
|
||||
|
||||
<b>Returns:</b>
|
||||
|
||||
`Observable<IKibanaSearchResponse>`
|
||||
|
||||
`Observalbe` emitting the search response or an error.
|
||||
|
||||
|
|
|
@ -1,11 +1,22 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [SearchInterceptor](./kibana-plugin-plugins-data-public.searchinterceptor.md) > [showTimeoutError](./kibana-plugin-plugins-data-public.searchinterceptor.showtimeouterror.md)
|
||||
[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [SearchInterceptor](./kibana-plugin-plugins-data-public.searchinterceptor.md) > [showError](./kibana-plugin-plugins-data-public.searchinterceptor.showerror.md)
|
||||
|
||||
## SearchInterceptor.showTimeoutError property
|
||||
## SearchInterceptor.showError() method
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
protected showTimeoutError: ((e: Error) => void) & import("lodash").Cancelable;
|
||||
showError(e: Error): void;
|
||||
```
|
||||
|
||||
## Parameters
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| e | <code>Error</code> | |
|
||||
|
||||
<b>Returns:</b>
|
||||
|
||||
`void`
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [SearchTimeoutError](./kibana-plugin-plugins-data-public.searchtimeouterror.md) > [(constructor)](./kibana-plugin-plugins-data-public.searchtimeouterror._constructor_.md)
|
||||
|
||||
## SearchTimeoutError.(constructor)
|
||||
|
||||
Constructs a new instance of the `SearchTimeoutError` class
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
constructor(err: Error, mode: TimeoutErrorMode);
|
||||
```
|
||||
|
||||
## Parameters
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| err | <code>Error</code> | |
|
||||
| mode | <code>TimeoutErrorMode</code> | |
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [SearchTimeoutError](./kibana-plugin-plugins-data-public.searchtimeouterror.md) > [getErrorMessage](./kibana-plugin-plugins-data-public.searchtimeouterror.geterrormessage.md)
|
||||
|
||||
## SearchTimeoutError.getErrorMessage() method
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
getErrorMessage(application: ApplicationStart): JSX.Element;
|
||||
```
|
||||
|
||||
## Parameters
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| application | <code>ApplicationStart</code> | |
|
||||
|
||||
<b>Returns:</b>
|
||||
|
||||
`JSX.Element`
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [SearchTimeoutError](./kibana-plugin-plugins-data-public.searchtimeouterror.md)
|
||||
|
||||
## SearchTimeoutError class
|
||||
|
||||
Request Failure - When an entire multi request fails
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
export declare class SearchTimeoutError extends KbnError
|
||||
```
|
||||
|
||||
## Constructors
|
||||
|
||||
| Constructor | Modifiers | Description |
|
||||
| --- | --- | --- |
|
||||
| [(constructor)(err, mode)](./kibana-plugin-plugins-data-public.searchtimeouterror._constructor_.md) | | Constructs a new instance of the <code>SearchTimeoutError</code> class |
|
||||
|
||||
## Properties
|
||||
|
||||
| Property | Modifiers | Type | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| [mode](./kibana-plugin-plugins-data-public.searchtimeouterror.mode.md) | | <code>TimeoutErrorMode</code> | |
|
||||
|
||||
## Methods
|
||||
|
||||
| Method | Modifiers | Description |
|
||||
| --- | --- | --- |
|
||||
| [getErrorMessage(application)](./kibana-plugin-plugins-data-public.searchtimeouterror.geterrormessage.md) | | |
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [SearchTimeoutError](./kibana-plugin-plugins-data-public.searchtimeouterror.md) > [mode](./kibana-plugin-plugins-data-public.searchtimeouterror.mode.md)
|
||||
|
||||
## SearchTimeoutError.mode property
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
mode: TimeoutErrorMode;
|
||||
```
|
|
@ -0,0 +1,20 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [TimeoutErrorMode](./kibana-plugin-plugins-data-public.timeouterrormode.md)
|
||||
|
||||
## TimeoutErrorMode enum
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
export declare enum TimeoutErrorMode
|
||||
```
|
||||
|
||||
## Enumeration Members
|
||||
|
||||
| Member | Value | Description |
|
||||
| --- | --- | --- |
|
||||
| CHANGE | <code>2</code> | |
|
||||
| CONTACT | <code>1</code> | |
|
||||
| UPGRADE | <code>0</code> | |
|
||||
|
|
@ -25,7 +25,7 @@ export const mySearchStrategyProvider = (
|
|||
): ISearchStrategy<IMyStrategyRequest, IMyStrategyResponse> => {
|
||||
const es = data.search.getSearchStrategy('es');
|
||||
return {
|
||||
search: async (context, request, options) => {
|
||||
search: async (context, request, options): Promise<IMyStrategyResponse> => {
|
||||
const esSearchRes = await es.search(context, request, options);
|
||||
return {
|
||||
...esSearchRes,
|
||||
|
|
|
@ -365,8 +365,6 @@ export {
|
|||
ISearchGeneric,
|
||||
ISearchSource,
|
||||
parseSearchSourceJSON,
|
||||
RequestTimeoutError,
|
||||
SearchError,
|
||||
SearchInterceptor,
|
||||
SearchInterceptorDeps,
|
||||
SearchRequest,
|
||||
|
@ -375,6 +373,11 @@ export {
|
|||
// expression functions and types
|
||||
EsdslExpressionFunctionDefinition,
|
||||
EsRawResponseExpressionTypeDefinition,
|
||||
// errors
|
||||
SearchError,
|
||||
SearchTimeoutError,
|
||||
TimeoutErrorMode,
|
||||
PainlessError,
|
||||
} from './search';
|
||||
|
||||
export type { SearchSource } from './search';
|
||||
|
|
|
@ -8,6 +8,7 @@ import { $Values } from '@kbn/utility-types';
|
|||
import _ from 'lodash';
|
||||
import { Action } from 'history';
|
||||
import { ApiResponse } from '@elastic/elasticsearch/lib/Transport';
|
||||
import { ApplicationStart } from 'kibana/public';
|
||||
import { Assign } from '@kbn/utility-types';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
import Boom from 'boom';
|
||||
|
@ -69,7 +70,6 @@ import { SavedObjectsClientContract } from 'src/core/public';
|
|||
import { Search } from '@elastic/elasticsearch/api/requestParams';
|
||||
import { SearchResponse } from 'elasticsearch';
|
||||
import { SerializedFieldFormat as SerializedFieldFormat_2 } from 'src/plugins/expressions/common';
|
||||
import { Subscription } from 'rxjs';
|
||||
import { ToastInputFields } from 'src/core/public/notifications';
|
||||
import { ToastsSetup } from 'kibana/public';
|
||||
import { TransportRequestOptions } from '@elastic/elasticsearch/lib/Transport';
|
||||
|
@ -1470,6 +1470,8 @@ export interface ISearchStart {
|
|||
aggs: AggsStart;
|
||||
search: ISearchGeneric;
|
||||
searchSource: ISearchStartSearchSource;
|
||||
// (undocumented)
|
||||
showError: (e: Error) => void;
|
||||
}
|
||||
|
||||
// @public
|
||||
|
@ -1636,6 +1638,19 @@ export interface OptionedValueProp {
|
|||
value: string;
|
||||
}
|
||||
|
||||
// Warning: (ae-forgotten-export) The symbol "KbnError" needs to be exported by the entry point index.d.ts
|
||||
// Warning: (ae-missing-release-tag) "PainlessError" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public (undocumented)
|
||||
export class PainlessError extends KbnError {
|
||||
// Warning: (ae-forgotten-export) The symbol "EsError" needs to be exported by the entry point index.d.ts
|
||||
constructor(err: EsError, request: IKibanaSearchRequest);
|
||||
// (undocumented)
|
||||
getErrorMessage(application: ApplicationStart): JSX.Element;
|
||||
// (undocumented)
|
||||
painlessStack?: string;
|
||||
}
|
||||
|
||||
// Warning: (ae-forgotten-export) The symbol "parseEsInterval" needs to be exported by the entry point index.d.ts
|
||||
// Warning: (ae-missing-release-tag) "ParsedInterval" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
|
@ -1901,13 +1916,6 @@ export interface RefreshInterval {
|
|||
value: number;
|
||||
}
|
||||
|
||||
// Warning: (ae-missing-release-tag) "RequestTimeoutError" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public
|
||||
export class RequestTimeoutError extends Error {
|
||||
constructor(message?: string);
|
||||
}
|
||||
|
||||
// Warning: (ae-missing-release-tag) "SavedQuery" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public (undocumented)
|
||||
|
@ -2031,24 +2039,27 @@ export class SearchInterceptor {
|
|||
protected application: CoreStart['application'];
|
||||
// (undocumented)
|
||||
protected readonly deps: SearchInterceptorDeps;
|
||||
// (undocumented)
|
||||
protected getTimeoutMode(): TimeoutErrorMode;
|
||||
// (undocumented)
|
||||
protected handleSearchError(e: any, request: IKibanaSearchRequest, timeoutSignal: AbortSignal, appAbortSignal?: AbortSignal): Error;
|
||||
// @internal
|
||||
protected pendingCount$: BehaviorSubject<number>;
|
||||
// @internal (undocumented)
|
||||
protected runSearch(request: IEsSearchRequest, signal: AbortSignal, strategy?: string): Observable<IKibanaSearchResponse>;
|
||||
search(request: IEsSearchRequest, options?: ISearchOptions): Observable<IKibanaSearchResponse>;
|
||||
protected runSearch(request: IKibanaSearchRequest, signal: AbortSignal, strategy?: string): Observable<IKibanaSearchResponse>;
|
||||
search(request: IKibanaSearchRequest, options?: ISearchOptions): Observable<IKibanaSearchResponse>;
|
||||
// @internal (undocumented)
|
||||
protected setupAbortSignal({ abortSignal, timeout, }: {
|
||||
abortSignal?: AbortSignal;
|
||||
timeout?: number;
|
||||
}): {
|
||||
combinedSignal: AbortSignal;
|
||||
timeoutSignal: AbortSignal;
|
||||
cleanup: () => void;
|
||||
};
|
||||
// (undocumented)
|
||||
protected showTimeoutError: ((e: Error) => void) & import("lodash").Cancelable;
|
||||
// @internal
|
||||
protected timeoutSubscriptions: Subscription;
|
||||
}
|
||||
showError(e: Error): void;
|
||||
}
|
||||
|
||||
// Warning: (ae-missing-release-tag) "SearchInterceptorDeps" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
|
@ -2161,6 +2172,17 @@ export interface SearchSourceFields {
|
|||
version?: boolean;
|
||||
}
|
||||
|
||||
// Warning: (ae-missing-release-tag) "SearchTimeoutError" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public
|
||||
export class SearchTimeoutError extends KbnError {
|
||||
constructor(err: Error, mode: TimeoutErrorMode);
|
||||
// (undocumented)
|
||||
getErrorMessage(application: ApplicationStart): JSX.Element;
|
||||
// (undocumented)
|
||||
mode: TimeoutErrorMode;
|
||||
}
|
||||
|
||||
// Warning: (ae-missing-release-tag) "SortDirection" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public (undocumented)
|
||||
|
@ -2233,6 +2255,18 @@ export class TimeHistory {
|
|||
// @public (undocumented)
|
||||
export type TimeHistoryContract = PublicMethodsOf<TimeHistory>;
|
||||
|
||||
// Warning: (ae-missing-release-tag) "TimeoutErrorMode" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public (undocumented)
|
||||
export enum TimeoutErrorMode {
|
||||
// (undocumented)
|
||||
CHANGE = 2,
|
||||
// (undocumented)
|
||||
CONTACT = 1,
|
||||
// (undocumented)
|
||||
UPGRADE = 0
|
||||
}
|
||||
|
||||
// Warning: (ae-missing-release-tag) "TimeRange" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public (undocumented)
|
||||
|
@ -2322,21 +2356,21 @@ export const UI_SETTINGS: {
|
|||
// src/plugins/data/public/index.ts:236:27 - (ae-forgotten-export) The symbol "getFromSavedObject" needs to be exported by the entry point index.d.ts
|
||||
// src/plugins/data/public/index.ts:236:27 - (ae-forgotten-export) The symbol "flattenHitWrapper" needs to be exported by the entry point index.d.ts
|
||||
// src/plugins/data/public/index.ts:236:27 - (ae-forgotten-export) The symbol "formatHitProvider" needs to be exported by the entry point index.d.ts
|
||||
// src/plugins/data/public/index.ts:385:20 - (ae-forgotten-export) The symbol "getRequestInspectorStats" needs to be exported by the entry point index.d.ts
|
||||
// src/plugins/data/public/index.ts:385:20 - (ae-forgotten-export) The symbol "getResponseInspectorStats" needs to be exported by the entry point index.d.ts
|
||||
// src/plugins/data/public/index.ts:385:20 - (ae-forgotten-export) The symbol "tabifyAggResponse" needs to be exported by the entry point index.d.ts
|
||||
// src/plugins/data/public/index.ts:385:20 - (ae-forgotten-export) The symbol "tabifyGetColumns" needs to be exported by the entry point index.d.ts
|
||||
// src/plugins/data/public/index.ts:387:1 - (ae-forgotten-export) The symbol "CidrMask" needs to be exported by the entry point index.d.ts
|
||||
// src/plugins/data/public/index.ts:388:1 - (ae-forgotten-export) The symbol "dateHistogramInterval" needs to be exported by the entry point index.d.ts
|
||||
// src/plugins/data/public/index.ts:397:1 - (ae-forgotten-export) The symbol "InvalidEsCalendarIntervalError" needs to be exported by the entry point index.d.ts
|
||||
// src/plugins/data/public/index.ts:398:1 - (ae-forgotten-export) The symbol "InvalidEsIntervalFormatError" needs to be exported by the entry point index.d.ts
|
||||
// src/plugins/data/public/index.ts:399:1 - (ae-forgotten-export) The symbol "Ipv4Address" needs to be exported by the entry point index.d.ts
|
||||
// src/plugins/data/public/index.ts:400:1 - (ae-forgotten-export) The symbol "isDateHistogramBucketAggConfig" needs to be exported by the entry point index.d.ts
|
||||
// src/plugins/data/public/index.ts:404:1 - (ae-forgotten-export) The symbol "isValidEsInterval" needs to be exported by the entry point index.d.ts
|
||||
// src/plugins/data/public/index.ts:405:1 - (ae-forgotten-export) The symbol "isValidInterval" needs to be exported by the entry point index.d.ts
|
||||
// src/plugins/data/public/index.ts:408:1 - (ae-forgotten-export) The symbol "parseInterval" needs to be exported by the entry point index.d.ts
|
||||
// src/plugins/data/public/index.ts:409:1 - (ae-forgotten-export) The symbol "propFilter" needs to be exported by the entry point index.d.ts
|
||||
// src/plugins/data/public/index.ts:412:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts
|
||||
// src/plugins/data/public/index.ts:388:20 - (ae-forgotten-export) The symbol "getRequestInspectorStats" needs to be exported by the entry point index.d.ts
|
||||
// src/plugins/data/public/index.ts:388:20 - (ae-forgotten-export) The symbol "getResponseInspectorStats" needs to be exported by the entry point index.d.ts
|
||||
// src/plugins/data/public/index.ts:388:20 - (ae-forgotten-export) The symbol "tabifyAggResponse" needs to be exported by the entry point index.d.ts
|
||||
// src/plugins/data/public/index.ts:388:20 - (ae-forgotten-export) The symbol "tabifyGetColumns" needs to be exported by the entry point index.d.ts
|
||||
// src/plugins/data/public/index.ts:390:1 - (ae-forgotten-export) The symbol "CidrMask" needs to be exported by the entry point index.d.ts
|
||||
// src/plugins/data/public/index.ts:391:1 - (ae-forgotten-export) The symbol "dateHistogramInterval" needs to be exported by the entry point index.d.ts
|
||||
// src/plugins/data/public/index.ts:400:1 - (ae-forgotten-export) The symbol "InvalidEsCalendarIntervalError" needs to be exported by the entry point index.d.ts
|
||||
// src/plugins/data/public/index.ts:401:1 - (ae-forgotten-export) The symbol "InvalidEsIntervalFormatError" needs to be exported by the entry point index.d.ts
|
||||
// src/plugins/data/public/index.ts:402:1 - (ae-forgotten-export) The symbol "Ipv4Address" needs to be exported by the entry point index.d.ts
|
||||
// src/plugins/data/public/index.ts:403:1 - (ae-forgotten-export) The symbol "isDateHistogramBucketAggConfig" needs to be exported by the entry point index.d.ts
|
||||
// src/plugins/data/public/index.ts:407:1 - (ae-forgotten-export) The symbol "isValidEsInterval" needs to be exported by the entry point index.d.ts
|
||||
// src/plugins/data/public/index.ts:408:1 - (ae-forgotten-export) The symbol "isValidInterval" needs to be exported by the entry point index.d.ts
|
||||
// src/plugins/data/public/index.ts:411:1 - (ae-forgotten-export) The symbol "parseInterval" needs to be exported by the entry point index.d.ts
|
||||
// src/plugins/data/public/index.ts:412:1 - (ae-forgotten-export) The symbol "propFilter" needs to be exported by the entry point index.d.ts
|
||||
// src/plugins/data/public/index.ts:415:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts
|
||||
// src/plugins/data/public/query/state_sync/connect_to_query_state.ts:45:5 - (ae-forgotten-export) The symbol "FilterStateStore" needs to be exported by the entry point index.d.ts
|
||||
|
||||
// (No @packageDocumentation comment for this package)
|
||||
|
|
|
@ -17,4 +17,5 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
import './fetch_error';
|
||||
export * from './painless_error';
|
||||
export * from './timeout_error';
|
89
src/plugins/data/public/search/errors/painless_error.tsx
Normal file
89
src/plugins/data/public/search/errors/painless_error.tsx
Normal file
|
@ -0,0 +1,89 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { EuiButton, EuiSpacer, EuiText, EuiCodeBlock } from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { ApplicationStart } from 'kibana/public';
|
||||
import { KbnError } from '../../../../kibana_utils/common';
|
||||
import { EsError, isEsError } from './types';
|
||||
import { IKibanaSearchRequest } from '..';
|
||||
|
||||
export class PainlessError extends KbnError {
|
||||
painlessStack?: string;
|
||||
constructor(err: EsError, request: IKibanaSearchRequest) {
|
||||
const rootCause = getRootCause(err as EsError);
|
||||
|
||||
super(
|
||||
i18n.translate('data.painlessError.painlessScriptedFieldErrorMessage', {
|
||||
defaultMessage: "Error executing Painless script: '{script}'.",
|
||||
values: { script: rootCause?.script },
|
||||
})
|
||||
);
|
||||
this.painlessStack = rootCause?.script_stack ? rootCause?.script_stack.join('\n') : undefined;
|
||||
}
|
||||
|
||||
public getErrorMessage(application: ApplicationStart) {
|
||||
function onClick() {
|
||||
application.navigateToApp('management', {
|
||||
path: `/kibana/indexPatterns`,
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{this.message}
|
||||
<EuiSpacer size="s" />
|
||||
<EuiSpacer size="s" />
|
||||
{this.painlessStack ? (
|
||||
<EuiCodeBlock data-test-subj="painlessStackTrace" isCopyable={true} paddingSize="s">
|
||||
{this.painlessStack}
|
||||
</EuiCodeBlock>
|
||||
) : null}
|
||||
<EuiText textAlign="right">
|
||||
<EuiButton color="danger" onClick={onClick} size="s">
|
||||
<FormattedMessage id="data.painlessError.buttonTxt" defaultMessage="Edit script" />
|
||||
</EuiButton>
|
||||
</EuiText>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function getFailedShards(err: EsError) {
|
||||
const failedShards =
|
||||
err.body?.attributes?.error?.failed_shards ||
|
||||
err.body?.attributes?.error?.caused_by?.failed_shards;
|
||||
return failedShards ? failedShards[0] : undefined;
|
||||
}
|
||||
|
||||
function getRootCause(err: EsError) {
|
||||
return getFailedShards(err)?.reason;
|
||||
}
|
||||
|
||||
export function isPainlessError(err: Error | EsError) {
|
||||
if (!isEsError(err)) return false;
|
||||
|
||||
const rootCause = getRootCause(err as EsError);
|
||||
if (!rootCause) return false;
|
||||
|
||||
const { lang } = rootCause;
|
||||
return lang === 'painless';
|
||||
}
|
62
src/plugins/data/public/search/errors/timeout_error.test.tsx
Normal file
62
src/plugins/data/public/search/errors/timeout_error.test.tsx
Normal file
|
@ -0,0 +1,62 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { SearchTimeoutError, TimeoutErrorMode } from './timeout_error';
|
||||
|
||||
import { coreMock } from '../../../../../core/public/mocks';
|
||||
const startMock = coreMock.createStart();
|
||||
|
||||
import { mount } from 'enzyme';
|
||||
import { AbortError } from 'src/plugins/data/common';
|
||||
|
||||
describe('SearchTimeoutError', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
startMock.application.navigateToApp.mockImplementation(jest.fn());
|
||||
});
|
||||
|
||||
it('Should navigate to upgrade', () => {
|
||||
const e = new SearchTimeoutError(new AbortError(), TimeoutErrorMode.UPGRADE);
|
||||
const component = mount(e.getErrorMessage(startMock.application));
|
||||
|
||||
expect(component.find('EuiButton').length).toBe(1);
|
||||
component.find('EuiButton').simulate('click');
|
||||
expect(startMock.application.navigateToApp).toHaveBeenCalledWith('management', {
|
||||
path: '/kibana/indexPatterns',
|
||||
});
|
||||
});
|
||||
|
||||
it('Should create contact admin message', () => {
|
||||
const e = new SearchTimeoutError(new AbortError(), TimeoutErrorMode.CONTACT);
|
||||
const component = mount(e.getErrorMessage(startMock.application));
|
||||
|
||||
expect(component.find('EuiButton').length).toBe(0);
|
||||
});
|
||||
|
||||
it('Should navigate to settings', () => {
|
||||
const e = new SearchTimeoutError(new AbortError(), TimeoutErrorMode.CHANGE);
|
||||
const component = mount(e.getErrorMessage(startMock.application));
|
||||
|
||||
expect(component.find('EuiButton').length).toBe(1);
|
||||
component.find('EuiButton').simulate('click');
|
||||
expect(startMock.application.navigateToApp).toHaveBeenCalledWith('management', {
|
||||
path: '/kibana/settings',
|
||||
});
|
||||
});
|
||||
});
|
111
src/plugins/data/public/search/errors/timeout_error.tsx
Normal file
111
src/plugins/data/public/search/errors/timeout_error.tsx
Normal file
|
@ -0,0 +1,111 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { EuiButton, EuiSpacer, EuiText } from '@elastic/eui';
|
||||
import { ApplicationStart } from 'kibana/public';
|
||||
import { KbnError } from '../../../../kibana_utils/common';
|
||||
|
||||
export enum TimeoutErrorMode {
|
||||
UPGRADE,
|
||||
CONTACT,
|
||||
CHANGE,
|
||||
}
|
||||
|
||||
/**
|
||||
* Request Failure - When an entire multi request fails
|
||||
* @param {Error} err - the Error that came back
|
||||
*/
|
||||
export class SearchTimeoutError extends KbnError {
|
||||
public mode: TimeoutErrorMode;
|
||||
constructor(err: Error, mode: TimeoutErrorMode) {
|
||||
super(`Request timeout: ${JSON.stringify(err?.message)}`);
|
||||
this.mode = mode;
|
||||
}
|
||||
|
||||
private getMessage() {
|
||||
switch (this.mode) {
|
||||
case TimeoutErrorMode.UPGRADE:
|
||||
return i18n.translate('data.search.upgradeLicense', {
|
||||
defaultMessage:
|
||||
'Your query has timed out. With our free Basic tier, your queries never time out.',
|
||||
});
|
||||
case TimeoutErrorMode.CONTACT:
|
||||
return i18n.translate('data.search.timeoutContactAdmin', {
|
||||
defaultMessage:
|
||||
'Your query has timed out. Contact your system administrator to increase the run time.',
|
||||
});
|
||||
case TimeoutErrorMode.CHANGE:
|
||||
return i18n.translate('data.search.timeoutIncreaseSetting', {
|
||||
defaultMessage:
|
||||
'Your query has timed out. Increase run time with the search timeout advanced setting.',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private getActionText() {
|
||||
switch (this.mode) {
|
||||
case TimeoutErrorMode.UPGRADE:
|
||||
return i18n.translate('data.search.upgradeLicenseActionText', {
|
||||
defaultMessage: 'Upgrade now',
|
||||
});
|
||||
break;
|
||||
case TimeoutErrorMode.CHANGE:
|
||||
return i18n.translate('data.search.timeoutIncreaseSettingActionText', {
|
||||
defaultMessage: 'Edit setting',
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private onClick(application: ApplicationStart) {
|
||||
switch (this.mode) {
|
||||
case TimeoutErrorMode.UPGRADE:
|
||||
application.navigateToApp('management', {
|
||||
path: `/kibana/indexPatterns`,
|
||||
});
|
||||
break;
|
||||
case TimeoutErrorMode.CHANGE:
|
||||
application.navigateToApp('management', {
|
||||
path: `/kibana/settings`,
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public getErrorMessage(application: ApplicationStart) {
|
||||
const actionText = this.getActionText();
|
||||
return (
|
||||
<>
|
||||
{this.getMessage()}
|
||||
{actionText && (
|
||||
<>
|
||||
<EuiSpacer size="s" />
|
||||
<EuiText textAlign="right">
|
||||
<EuiButton color="danger" onClick={() => this.onClick(application)} size="s">
|
||||
{actionText}
|
||||
</EuiButton>
|
||||
</EuiText>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -17,9 +17,7 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
interface FailedShards {
|
||||
interface FailedShard {
|
||||
shard: number;
|
||||
index: string;
|
||||
node: string;
|
||||
|
@ -41,7 +39,7 @@ interface FailedShards {
|
|||
};
|
||||
}
|
||||
|
||||
interface EsError {
|
||||
export interface EsError {
|
||||
body: {
|
||||
statusCode: number;
|
||||
error: string;
|
||||
|
@ -56,51 +54,20 @@ interface EsError {
|
|||
];
|
||||
type: string;
|
||||
reason: string;
|
||||
failed_shards: FailedShard[];
|
||||
caused_by: {
|
||||
type: string;
|
||||
reason: string;
|
||||
phase: string;
|
||||
grouped: boolean;
|
||||
failed_shards: FailedShards[];
|
||||
failed_shards: FailedShard[];
|
||||
script_stack: string[];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export function getCause(error: EsError) {
|
||||
const cause = error.body?.attributes?.error?.root_cause;
|
||||
if (cause) {
|
||||
return cause[0];
|
||||
}
|
||||
|
||||
const failedShards = error.body?.attributes?.error?.caused_by?.failed_shards;
|
||||
|
||||
if (failedShards && failedShards[0] && failedShards[0].reason) {
|
||||
return error.body?.attributes?.error?.caused_by?.failed_shards[0].reason;
|
||||
}
|
||||
}
|
||||
|
||||
export function getPainlessError(error: EsError) {
|
||||
const cause = getCause(error);
|
||||
|
||||
if (!cause) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { lang, script } = cause;
|
||||
|
||||
if (lang !== 'painless') {
|
||||
return;
|
||||
}
|
||||
|
||||
return {
|
||||
lang,
|
||||
script,
|
||||
message: i18n.translate('discover.painlessError.painlessScriptedFieldErrorMessage', {
|
||||
defaultMessage: "Error with Painless scripted field '{script}'.",
|
||||
values: { script },
|
||||
}),
|
||||
error: error.body?.message,
|
||||
};
|
||||
export function isEsError(e: any): e is EsError {
|
||||
return !!e.body?.attributes;
|
||||
}
|
|
@ -46,4 +46,4 @@ export {
|
|||
export { getEsPreference } from './es_search';
|
||||
|
||||
export { SearchInterceptor, SearchInterceptorDeps } from './search_interceptor';
|
||||
export { RequestTimeoutError } from './request_timeout_error';
|
||||
export * from './errors';
|
||||
|
|
|
@ -34,6 +34,7 @@ function createStartContract(): jest.Mocked<ISearchStart> {
|
|||
return {
|
||||
aggs: searchAggsStartMock(),
|
||||
search: jest.fn(),
|
||||
showError: jest.fn(),
|
||||
searchSource: searchSourceMock,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,30 +0,0 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Class used to signify that a request timed out. Useful for applications to conditionally handle
|
||||
* this type of error differently than other errors.
|
||||
*/
|
||||
export class RequestTimeoutError extends Error {
|
||||
constructor(message = 'Request timed out') {
|
||||
super(message);
|
||||
this.message = message;
|
||||
this.name = 'RequestTimeoutError';
|
||||
}
|
||||
}
|
|
@ -22,6 +22,7 @@ import { coreMock } from '../../../../core/public/mocks';
|
|||
import { IEsSearchRequest } from '../../common/search';
|
||||
import { SearchInterceptor } from './search_interceptor';
|
||||
import { AbortError } from '../../common';
|
||||
import { SearchTimeoutError, PainlessError } from './errors';
|
||||
|
||||
let searchInterceptor: SearchInterceptor;
|
||||
let mockCoreSetup: MockedKeys<CoreSetup>;
|
||||
|
@ -53,8 +54,8 @@ describe('SearchInterceptor', () => {
|
|||
expect(result).toBe(mockResponse);
|
||||
});
|
||||
|
||||
test('Observable should fail if fetch has an error', async () => {
|
||||
const mockResponse: any = { result: 500 };
|
||||
test('Observable should fail if fetch has an internal error', async () => {
|
||||
const mockResponse: any = { result: 500, message: 'Internal Error' };
|
||||
mockCoreSetup.http.fetch.mockRejectedValueOnce(mockResponse);
|
||||
const mockRequest: IEsSearchRequest = {
|
||||
params: {},
|
||||
|
@ -68,64 +69,83 @@ describe('SearchInterceptor', () => {
|
|||
}
|
||||
});
|
||||
|
||||
test('Observable should fail if fetch times out (test merged signal)', async () => {
|
||||
mockCoreSetup.http.fetch.mockImplementationOnce((options: any) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
options.signal.addEventListener('abort', () => {
|
||||
reject(new AbortError());
|
||||
});
|
||||
|
||||
setTimeout(resolve, 5000);
|
||||
});
|
||||
});
|
||||
test('Should throw SearchTimeoutError on server timeout AND show toast', async (done) => {
|
||||
const mockResponse: any = {
|
||||
result: 500,
|
||||
body: {
|
||||
message: 'Request timed out',
|
||||
},
|
||||
};
|
||||
mockCoreSetup.http.fetch.mockRejectedValueOnce(mockResponse);
|
||||
const mockRequest: IEsSearchRequest = {
|
||||
params: {},
|
||||
};
|
||||
const response = searchInterceptor.search(mockRequest);
|
||||
|
||||
const next = jest.fn();
|
||||
const error = (e: any) => {
|
||||
expect(next).not.toBeCalled();
|
||||
expect(e).toBeInstanceOf(AbortError);
|
||||
};
|
||||
response.subscribe({ next, error });
|
||||
|
||||
jest.advanceTimersByTime(5000);
|
||||
|
||||
await flushPromises();
|
||||
try {
|
||||
await response.toPromise();
|
||||
} catch (e) {
|
||||
expect(e).toBeInstanceOf(SearchTimeoutError);
|
||||
expect(mockCoreSetup.notifications.toasts.addDanger).toBeCalledTimes(1);
|
||||
done();
|
||||
}
|
||||
});
|
||||
|
||||
test('Should not timeout if requestTimeout is undefined', async () => {
|
||||
searchInterceptor = new SearchInterceptor({
|
||||
startServices: mockCoreSetup.getStartServices(),
|
||||
uiSettings: mockCoreSetup.uiSettings,
|
||||
http: mockCoreSetup.http,
|
||||
toasts: mockCoreSetup.notifications.toasts,
|
||||
});
|
||||
mockCoreSetup.http.fetch.mockImplementationOnce((options: any) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
options.signal.addEventListener('abort', () => {
|
||||
reject(new AbortError());
|
||||
});
|
||||
test('Search error should be debounced', async (done) => {
|
||||
const mockResponse: any = {
|
||||
result: 500,
|
||||
body: {
|
||||
message: 'Request timed out',
|
||||
},
|
||||
};
|
||||
mockCoreSetup.http.fetch.mockRejectedValue(mockResponse);
|
||||
const mockRequest: IEsSearchRequest = {
|
||||
params: {},
|
||||
};
|
||||
try {
|
||||
await searchInterceptor.search(mockRequest).toPromise();
|
||||
} catch (e) {
|
||||
expect(e).toBeInstanceOf(SearchTimeoutError);
|
||||
try {
|
||||
await searchInterceptor.search(mockRequest).toPromise();
|
||||
} catch (e2) {
|
||||
expect(mockCoreSetup.notifications.toasts.addDanger).toBeCalledTimes(1);
|
||||
done();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
setTimeout(resolve, 5000);
|
||||
});
|
||||
});
|
||||
test('Should throw Painless error on server error with OSS format', async (done) => {
|
||||
const mockResponse: any = {
|
||||
result: 500,
|
||||
body: {
|
||||
attributes: {
|
||||
error: {
|
||||
failed_shards: [
|
||||
{
|
||||
reason: {
|
||||
lang: 'painless',
|
||||
script_stack: ['a', 'b'],
|
||||
reason: 'banana',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
mockCoreSetup.http.fetch.mockRejectedValueOnce(mockResponse);
|
||||
const mockRequest: IEsSearchRequest = {
|
||||
params: {},
|
||||
};
|
||||
const response = searchInterceptor.search(mockRequest);
|
||||
|
||||
expect.assertions(1);
|
||||
const next = jest.fn();
|
||||
const complete = () => {
|
||||
expect(next).toBeCalled();
|
||||
};
|
||||
response.subscribe({ next, complete });
|
||||
|
||||
jest.advanceTimersByTime(5000);
|
||||
|
||||
await flushPromises();
|
||||
try {
|
||||
await response.toPromise();
|
||||
} catch (e) {
|
||||
expect(e).toBeInstanceOf(PainlessError);
|
||||
done();
|
||||
}
|
||||
});
|
||||
|
||||
test('Observable should fail if user aborts (test merged signal)', async () => {
|
||||
|
|
|
@ -17,29 +17,21 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
import { trimEnd, debounce } from 'lodash';
|
||||
import {
|
||||
BehaviorSubject,
|
||||
throwError,
|
||||
timer,
|
||||
Subscription,
|
||||
defer,
|
||||
from,
|
||||
Observable,
|
||||
NEVER,
|
||||
} from 'rxjs';
|
||||
import { get, trimEnd, debounce } from 'lodash';
|
||||
import { BehaviorSubject, throwError, timer, defer, from, Observable, NEVER } from 'rxjs';
|
||||
import { catchError, finalize } from 'rxjs/operators';
|
||||
import { CoreStart, CoreSetup, ToastsSetup } from 'kibana/public';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import {
|
||||
getCombinedSignal,
|
||||
AbortError,
|
||||
IEsSearchRequest,
|
||||
IKibanaSearchRequest,
|
||||
IKibanaSearchResponse,
|
||||
ISearchOptions,
|
||||
ES_SEARCH_STRATEGY,
|
||||
} from '../../common';
|
||||
import { SearchUsageCollector } from './collectors';
|
||||
import { SearchTimeoutError, PainlessError, isPainlessError, TimeoutErrorMode } from './errors';
|
||||
import { toMountPoint } from '../../../kibana_react/public';
|
||||
|
||||
export interface SearchInterceptorDeps {
|
||||
http: CoreSetup['http'];
|
||||
|
@ -62,12 +54,6 @@ export class SearchInterceptor {
|
|||
*/
|
||||
protected pendingCount$ = new BehaviorSubject(0);
|
||||
|
||||
/**
|
||||
* The subscriptions from scheduling the automatic timeout for each request.
|
||||
* @internal
|
||||
*/
|
||||
protected timeoutSubscriptions: Subscription = new Subscription();
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
|
@ -84,11 +70,46 @@ export class SearchInterceptor {
|
|||
});
|
||||
}
|
||||
|
||||
/*
|
||||
* @returns `TimeoutErrorMode` indicating what action should be taken in case of a request timeout based on license and permissions.
|
||||
* @internal
|
||||
*/
|
||||
protected getTimeoutMode() {
|
||||
return TimeoutErrorMode.UPGRADE;
|
||||
}
|
||||
|
||||
/*
|
||||
* @returns `Error` a search service specific error or the original error, if a specific error can't be recognized.
|
||||
* @internal
|
||||
*/
|
||||
protected handleSearchError(
|
||||
e: any,
|
||||
request: IKibanaSearchRequest,
|
||||
timeoutSignal: AbortSignal,
|
||||
appAbortSignal?: AbortSignal
|
||||
): Error {
|
||||
if (timeoutSignal.aborted || get(e, 'body.message') === 'Request timed out') {
|
||||
// Handle a client or a server side timeout
|
||||
const err = new SearchTimeoutError(e, this.getTimeoutMode());
|
||||
|
||||
// Show the timeout error here, so that it's shown regardless of how an application chooses to handle errors.
|
||||
this.showTimeoutError(err);
|
||||
return err;
|
||||
} else if (appAbortSignal?.aborted) {
|
||||
// In the case an application initiated abort, throw the existing AbortError.
|
||||
return e;
|
||||
} else if (isPainlessError(e)) {
|
||||
return new PainlessError(e, request);
|
||||
} else {
|
||||
return e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
protected runSearch(
|
||||
request: IEsSearchRequest,
|
||||
request: IKibanaSearchRequest,
|
||||
signal: AbortSignal,
|
||||
strategy?: string
|
||||
): Observable<IKibanaSearchResponse> {
|
||||
|
@ -105,41 +126,6 @@ export class SearchInterceptor {
|
|||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Searches using the given `search` method. Overrides the `AbortSignal` with one that will abort
|
||||
* either when `cancelPending` is called, when the request times out, or when the original
|
||||
* `AbortSignal` is aborted. Updates `pendingCount$` when the request is started/finalized.
|
||||
*/
|
||||
public search(
|
||||
request: IEsSearchRequest,
|
||||
options?: ISearchOptions
|
||||
): Observable<IKibanaSearchResponse> {
|
||||
// Defer the following logic until `subscribe` is actually called
|
||||
return defer(() => {
|
||||
if (options?.abortSignal?.aborted) {
|
||||
return throwError(new AbortError());
|
||||
}
|
||||
|
||||
const { combinedSignal, cleanup } = this.setupAbortSignal({
|
||||
abortSignal: options?.abortSignal,
|
||||
});
|
||||
this.pendingCount$.next(this.pendingCount$.getValue() + 1);
|
||||
|
||||
return this.runSearch(request, combinedSignal, options?.strategy).pipe(
|
||||
catchError((e: any) => {
|
||||
if (e.body?.attributes?.error === 'Request timed out') {
|
||||
this.showTimeoutError(e);
|
||||
}
|
||||
return throwError(e);
|
||||
}),
|
||||
finalize(() => {
|
||||
this.pendingCount$.next(this.pendingCount$.getValue() - 1);
|
||||
cleanup();
|
||||
})
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
|
@ -156,9 +142,7 @@ export class SearchInterceptor {
|
|||
const timeout$ = timeout ? timer(timeout) : NEVER;
|
||||
const subscription = timeout$.subscribe(() => {
|
||||
timeoutController.abort();
|
||||
this.showTimeoutError(new AbortError());
|
||||
});
|
||||
this.timeoutSubscriptions.add(subscription);
|
||||
|
||||
// Get a combined `AbortSignal` that will be aborted whenever the first of the following occurs:
|
||||
// 1. The user manually aborts (via `cancelPending`)
|
||||
|
@ -172,34 +156,95 @@ export class SearchInterceptor {
|
|||
|
||||
const combinedSignal = getCombinedSignal(signals);
|
||||
const cleanup = () => {
|
||||
this.timeoutSubscriptions.remove(subscription);
|
||||
subscription.unsubscribe();
|
||||
};
|
||||
|
||||
combinedSignal.addEventListener('abort', cleanup);
|
||||
|
||||
return {
|
||||
combinedSignal,
|
||||
timeoutSignal,
|
||||
cleanup,
|
||||
};
|
||||
}
|
||||
|
||||
// Right now we are debouncing but we will hook this up with background sessions to show only one
|
||||
// error notification per session.
|
||||
protected showTimeoutError = debounce(
|
||||
(e: Error) => {
|
||||
this.deps.toasts.addError(e, {
|
||||
/**
|
||||
* Right now we are throttling but we will hook this up with background sessions to show only one
|
||||
* error notification per session.
|
||||
* @internal
|
||||
*/
|
||||
private showTimeoutError = debounce(
|
||||
(e: SearchTimeoutError) => {
|
||||
this.deps.toasts.addDanger({
|
||||
title: 'Timed out',
|
||||
toastMessage: i18n.translate('data.search.upgradeLicense', {
|
||||
defaultMessage:
|
||||
'One or more queries timed out. With our free Basic tier, your queries never time out.',
|
||||
}),
|
||||
text: toMountPoint(e.getErrorMessage(this.application)),
|
||||
});
|
||||
},
|
||||
60000,
|
||||
{
|
||||
leading: true,
|
||||
}
|
||||
30000,
|
||||
{ leading: true, trailing: false }
|
||||
);
|
||||
|
||||
/**
|
||||
* Searches using the given `search` method. Overrides the `AbortSignal` with one that will abort
|
||||
* either when `cancelPending` is called, when the request times out, or when the original
|
||||
* `AbortSignal` is aborted. Updates `pendingCount$` when the request is started/finalized.
|
||||
*
|
||||
* @param request
|
||||
* @options
|
||||
* @returns `Observalbe` emitting the search response or an error.
|
||||
*/
|
||||
public search(
|
||||
request: IKibanaSearchRequest,
|
||||
options?: ISearchOptions
|
||||
): Observable<IKibanaSearchResponse> {
|
||||
// Defer the following logic until `subscribe` is actually called
|
||||
return defer(() => {
|
||||
if (options?.abortSignal?.aborted) {
|
||||
return throwError(new AbortError());
|
||||
}
|
||||
|
||||
const { timeoutSignal, combinedSignal, cleanup } = this.setupAbortSignal({
|
||||
abortSignal: options?.abortSignal,
|
||||
});
|
||||
this.pendingCount$.next(this.pendingCount$.getValue() + 1);
|
||||
|
||||
return this.runSearch(request, combinedSignal, options?.strategy).pipe(
|
||||
catchError((e: any) => {
|
||||
return throwError(
|
||||
this.handleSearchError(e, request, timeoutSignal, options?.abortSignal)
|
||||
);
|
||||
}),
|
||||
finalize(() => {
|
||||
this.pendingCount$.next(this.pendingCount$.getValue() - 1);
|
||||
cleanup();
|
||||
})
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/*
|
||||
*
|
||||
*/
|
||||
public showError(e: Error) {
|
||||
if (e instanceof AbortError) return;
|
||||
|
||||
if (e instanceof SearchTimeoutError) {
|
||||
// The SearchTimeoutError is shown by the interceptor in getSearchError (regardless of how the app chooses to handle errors)
|
||||
return;
|
||||
}
|
||||
|
||||
if (e instanceof PainlessError) {
|
||||
this.deps.toasts.addDanger({
|
||||
title: 'Search Error',
|
||||
text: toMountPoint(e.getErrorMessage(this.application)),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
this.deps.toasts.addError(e, {
|
||||
title: 'Search Error',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export type ISearchInterceptor = PublicMethodsOf<SearchInterceptor>;
|
||||
|
|
|
@ -111,6 +111,9 @@ export class SearchService implements Plugin<ISearchSetup, ISearchStart> {
|
|||
return {
|
||||
aggs: this.aggsService.start({ fieldFormats, uiSettings }),
|
||||
search,
|
||||
showError: (e: Error) => {
|
||||
this.searchInterceptor.showError(e);
|
||||
},
|
||||
searchSource: {
|
||||
/**
|
||||
* creates searchsource based on serialized search source fields
|
||||
|
|
|
@ -73,6 +73,8 @@ export interface ISearchStart {
|
|||
* {@link ISearchGeneric}
|
||||
*/
|
||||
search: ISearchGeneric;
|
||||
|
||||
showError: (e: Error) => void;
|
||||
/**
|
||||
* high level search
|
||||
* {@link ISearchStartSearchSource}
|
||||
|
|
|
@ -27,6 +27,12 @@ import { i18n } from '@kbn/i18n';
|
|||
import { getState, splitState } from './discover_state';
|
||||
|
||||
import { RequestAdapter } from '../../../../inspector/public';
|
||||
import {
|
||||
esFilters,
|
||||
indexPatterns as indexPatternsUtils,
|
||||
connectToQueryState,
|
||||
syncQueryStateWithUrl,
|
||||
} from '../../../../data/public';
|
||||
import { SavedObjectSaveModal, showSaveModal } from '../../../../saved_objects/public';
|
||||
import { getSortArray, getSortForSearchSource } from './doc_table';
|
||||
import { createFixedScroll } from './directives/fixed_scroll';
|
||||
|
@ -34,7 +40,6 @@ import * as columnActions from './doc_table/actions/columns';
|
|||
import indexTemplateLegacy from './discover_legacy.html';
|
||||
import { showOpenSearchPanel } from '../components/top_nav/show_open_search_panel';
|
||||
import { addHelpMenuToAppChrome } from '../components/help_menu/help_menu_util';
|
||||
import { getPainlessError } from './get_painless_error';
|
||||
import { discoverResponseHandler } from './response_handler';
|
||||
import {
|
||||
getRequestInspectorStats,
|
||||
|
@ -65,12 +70,7 @@ const {
|
|||
|
||||
import { getRootBreadcrumbs, getSavedSearchBreadcrumbs } from '../helpers/breadcrumbs';
|
||||
import { validateTimeRange } from '../helpers/validate_time_range';
|
||||
import {
|
||||
esFilters,
|
||||
indexPatterns as indexPatternsUtils,
|
||||
connectToQueryState,
|
||||
syncQueryStateWithUrl,
|
||||
} from '../../../../data/public';
|
||||
|
||||
import { getIndexPatternId } from '../helpers/get_index_pattern_id';
|
||||
import { addFatalError } from '../../../../kibana_legacy/public';
|
||||
import {
|
||||
|
@ -786,18 +786,10 @@ function discoverController($element, $route, $scope, $timeout, $window, Promise
|
|||
// If the request was aborted then no need to surface this error in the UI
|
||||
if (error instanceof Error && error.name === 'AbortError') return;
|
||||
|
||||
const fetchError = getPainlessError(error);
|
||||
$scope.fetchStatus = fetchStatuses.NO_RESULTS;
|
||||
$scope.rows = [];
|
||||
|
||||
if (fetchError) {
|
||||
$scope.fetchError = fetchError;
|
||||
} else {
|
||||
toastNotifications.addError(error, {
|
||||
title: i18n.translate('discover.errorLoadingData', {
|
||||
defaultMessage: 'Error loading data',
|
||||
}),
|
||||
toastMessage: error.shortMessage || error.body?.message,
|
||||
});
|
||||
}
|
||||
data.search.showError(error);
|
||||
});
|
||||
};
|
||||
|
||||
|
|
|
@ -31,7 +31,6 @@ import { DiscoverNoResults } from '../angular/directives/no_results';
|
|||
import { DiscoverUninitialized } from '../angular/directives/uninitialized';
|
||||
import { DiscoverHistogram } from '../angular/directives/histogram';
|
||||
import { LoadingSpinner } from './loading_spinner/loading_spinner';
|
||||
import { DiscoverFetchError, FetchError } from './fetch_error/fetch_error';
|
||||
import { DocTableLegacy } from '../angular/doc_table/create_doc_table_react';
|
||||
import { SkipBottomButton } from './skip_bottom_button';
|
||||
import {
|
||||
|
@ -54,7 +53,6 @@ export interface DiscoverLegacyProps {
|
|||
addColumn: (column: string) => void;
|
||||
fetch: () => void;
|
||||
fetchCounter: number;
|
||||
fetchError: FetchError;
|
||||
fieldCounts: Record<string, number>;
|
||||
histogramData: Chart;
|
||||
hits: number;
|
||||
|
@ -95,7 +93,6 @@ export function DiscoverLegacy({
|
|||
addColumn,
|
||||
fetch,
|
||||
fetchCounter,
|
||||
fetchError,
|
||||
fieldCounts,
|
||||
histogramData,
|
||||
hits,
|
||||
|
@ -216,8 +213,7 @@ export function DiscoverLegacy({
|
|||
{resultState === 'uninitialized' && <DiscoverUninitialized onRefresh={fetch} />}
|
||||
{/* @TODO: Solved in the Angular way to satisfy functional test - should be improved*/}
|
||||
<span style={{ display: resultState !== 'loading' ? 'none' : '' }}>
|
||||
{fetchError && <DiscoverFetchError fetchError={fetchError} />}
|
||||
<div className="dscOverlay" style={{ display: fetchError ? 'none' : '' }}>
|
||||
<div className="dscOverlay">
|
||||
<LoadingSpinner />
|
||||
</div>
|
||||
</span>
|
||||
|
|
|
@ -1,3 +0,0 @@
|
|||
.discoverFetchError {
|
||||
max-width: 1000px;
|
||||
}
|
|
@ -1,96 +0,0 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import './fetch_error.scss';
|
||||
import React, { Fragment } from 'react';
|
||||
import { FormattedMessage, I18nProvider } from '@kbn/i18n/react';
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiCallOut, EuiCodeBlock, EuiSpacer } from '@elastic/eui';
|
||||
import { getServices } from '../../../kibana_services';
|
||||
|
||||
export interface FetchError {
|
||||
lang: string;
|
||||
script: string;
|
||||
message: string;
|
||||
error: string;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
fetchError: FetchError;
|
||||
}
|
||||
|
||||
export const DiscoverFetchError = ({ fetchError }: Props) => {
|
||||
if (!fetchError) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let body;
|
||||
|
||||
if (fetchError.lang === 'painless') {
|
||||
const { chrome } = getServices();
|
||||
const mangagementUrlObj = chrome.navLinks.get('kibana:stack_management');
|
||||
const managementUrl = mangagementUrlObj ? mangagementUrlObj.url : '';
|
||||
const url = `${managementUrl}/kibana/indexPatterns`;
|
||||
|
||||
body = (
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="discover.fetchError.howToAddressErrorDescription"
|
||||
defaultMessage="You can address this error by editing the {fetchErrorScript} field
|
||||
in {managementLink}, under the {scriptedFields} tab."
|
||||
values={{
|
||||
fetchErrorScript: `'${fetchError.script}'`,
|
||||
scriptedFields: (
|
||||
<FormattedMessage
|
||||
id="discover.fetchError.scriptedFieldsText"
|
||||
defaultMessage="“Scripted fields”"
|
||||
/>
|
||||
),
|
||||
managementLink: (
|
||||
<a href={url}>
|
||||
<FormattedMessage
|
||||
id="discover.fetchError.managmentLinkText"
|
||||
defaultMessage="Management > Index Patterns"
|
||||
/>
|
||||
</a>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<I18nProvider>
|
||||
<Fragment>
|
||||
<EuiSpacer size="xl" />
|
||||
|
||||
<EuiFlexGroup justifyContent="center" data-test-subj="discoverFetchError">
|
||||
<EuiFlexItem grow={false} className="discoverFetchError">
|
||||
<EuiCallOut title={fetchError.message} color="danger" iconType="cross">
|
||||
{body}
|
||||
|
||||
<EuiCodeBlock>{fetchError.error}</EuiCodeBlock>
|
||||
</EuiCallOut>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
|
||||
<EuiSpacer size="xl" />
|
||||
</Fragment>
|
||||
</I18nProvider>
|
||||
);
|
||||
};
|
|
@ -30,6 +30,7 @@ export type ExpressionValueError = ExpressionValueBoxed<
|
|||
message: string;
|
||||
name?: string;
|
||||
stack?: string;
|
||||
original?: Error;
|
||||
};
|
||||
info?: unknown;
|
||||
}
|
||||
|
|
|
@ -21,7 +21,7 @@ import { ExpressionValueError } from '../../common';
|
|||
|
||||
type ErrorLike = Partial<Pick<Error, 'name' | 'message' | 'stack'>>;
|
||||
|
||||
export const createError = (err: string | ErrorLike): ExpressionValueError => ({
|
||||
export const createError = (err: string | Error | ErrorLike): ExpressionValueError => ({
|
||||
type: 'error',
|
||||
error: {
|
||||
stack:
|
||||
|
@ -32,5 +32,6 @@ export const createError = (err: string | ErrorLike): ExpressionValueError => ({
|
|||
: undefined,
|
||||
message: typeof err === 'string' ? err : String(err.message),
|
||||
name: typeof err === 'object' ? err.name || 'Error' : 'Error',
|
||||
original: err instanceof Error ? err : undefined,
|
||||
},
|
||||
});
|
||||
|
|
|
@ -50,6 +50,8 @@ describe('getVisualizationInstance', () => {
|
|||
};
|
||||
savedVisMock = {};
|
||||
// @ts-expect-error
|
||||
mockServices.data.search.showError.mockImplementation(() => {});
|
||||
// @ts-expect-error
|
||||
mockServices.savedVisualizations.get.mockImplementation(() => savedVisMock);
|
||||
// @ts-expect-error
|
||||
mockServices.visualizations.convertToSerializedVis.mockImplementation(() => serializedVisMock);
|
||||
|
@ -119,6 +121,6 @@ describe('getVisualizationInstance', () => {
|
|||
error: 'error',
|
||||
});
|
||||
|
||||
expect(mockServices.toastNotifications.addError).toHaveBeenCalled();
|
||||
expect(mockServices.data.search.showError).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -17,7 +17,6 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import {
|
||||
SerializedVis,
|
||||
Vis,
|
||||
|
@ -28,6 +27,7 @@ import {
|
|||
import { SearchSourceFields } from 'src/plugins/data/public';
|
||||
import { SavedObject } from 'src/plugins/saved_objects/public';
|
||||
import { cloneDeep } from 'lodash';
|
||||
import { ExpressionValueError } from 'src/plugins/expressions/public';
|
||||
import { createSavedSearchesLoader } from '../../../../discover/public';
|
||||
import { VisualizeServices } from '../types';
|
||||
|
||||
|
@ -35,14 +35,7 @@ const createVisualizeEmbeddableAndLinkSavedSearch = async (
|
|||
vis: Vis,
|
||||
visualizeServices: VisualizeServices
|
||||
) => {
|
||||
const {
|
||||
chrome,
|
||||
data,
|
||||
overlays,
|
||||
createVisEmbeddableFromObject,
|
||||
savedObjects,
|
||||
toastNotifications,
|
||||
} = visualizeServices;
|
||||
const { chrome, data, overlays, createVisEmbeddableFromObject, savedObjects } = visualizeServices;
|
||||
const embeddableHandler = (await createVisEmbeddableFromObject(vis, {
|
||||
timeRange: data.query.timefilter.timefilter.getTime(),
|
||||
filters: data.query.filterManager.getFilters(),
|
||||
|
@ -51,11 +44,9 @@ const createVisualizeEmbeddableAndLinkSavedSearch = async (
|
|||
|
||||
embeddableHandler.getOutput$().subscribe((output) => {
|
||||
if (output.error) {
|
||||
toastNotifications.addError(output.error, {
|
||||
title: i18n.translate('visualize.error.title', {
|
||||
defaultMessage: 'Visualization error',
|
||||
}),
|
||||
});
|
||||
data.search.showError(
|
||||
((output.error as unknown) as ExpressionValueError['error']).original || output.error
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -22,7 +22,7 @@ import { FtrProviderContext } from '../../ftr_provider_context';
|
|||
|
||||
export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
||||
const esArchiver = getService('esArchiver');
|
||||
const testSubjects = getService('testSubjects');
|
||||
const toasts = getService('toasts');
|
||||
const PageObjects = getPageObjects(['common', 'discover', 'timePicker']);
|
||||
|
||||
describe('errors', function describeIndexTests() {
|
||||
|
@ -39,8 +39,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
|
||||
describe('invalid scripted field error', () => {
|
||||
it('is rendered', async () => {
|
||||
const isFetchErrorVisible = await testSubjects.exists('discoverFetchError');
|
||||
expect(isFetchErrorVisible).to.be(true);
|
||||
const toast = await toasts.getToastElement(1);
|
||||
const painlessStackTrace = await toast.findByTestSubject('painlessStackTrace');
|
||||
expect(painlessStackTrace).not.to.be(undefined);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -63,7 +63,7 @@ export function ToastsProvider({ getService }: FtrProviderContext) {
|
|||
}
|
||||
}
|
||||
|
||||
private async getToastElement(index: number) {
|
||||
public async getToastElement(index: number) {
|
||||
const list = await this.getGlobalToastList();
|
||||
return await list.findByCssSelector(`.euiToast:nth-child(${index})`);
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@ import { coreMock } from '../../../../../src/core/public/mocks';
|
|||
import { EnhancedSearchInterceptor } from './search_interceptor';
|
||||
import { CoreSetup, CoreStart } from 'kibana/public';
|
||||
import { AbortError, UI_SETTINGS } from '../../../../../src/plugins/data/common';
|
||||
import { SearchTimeoutError } from 'src/plugins/data/public';
|
||||
|
||||
const timeTravel = (msToRun = 0) => {
|
||||
jest.advanceTimersByTime(msToRun);
|
||||
|
@ -265,7 +266,7 @@ describe('EnhancedSearchInterceptor', () => {
|
|||
await timeTravel(1000);
|
||||
|
||||
expect(error).toHaveBeenCalled();
|
||||
expect(error.mock.calls[0][0]).toBeInstanceOf(AbortError);
|
||||
expect(error.mock.calls[0][0]).toBeInstanceOf(SearchTimeoutError);
|
||||
expect(mockCoreSetup.http.fetch).toHaveBeenCalled();
|
||||
expect(mockCoreSetup.http.delete).not.toHaveBeenCalled();
|
||||
});
|
||||
|
@ -305,7 +306,7 @@ describe('EnhancedSearchInterceptor', () => {
|
|||
await timeTravel(1000);
|
||||
|
||||
expect(error).toHaveBeenCalled();
|
||||
expect(error.mock.calls[0][0]).toBeInstanceOf(AbortError);
|
||||
expect(error.mock.calls[0][0]).toBeInstanceOf(SearchTimeoutError);
|
||||
expect(mockCoreSetup.http.fetch).toHaveBeenCalledTimes(2);
|
||||
expect(mockCoreSetup.http.delete).toHaveBeenCalled();
|
||||
});
|
||||
|
|
|
@ -5,9 +5,7 @@
|
|||
*/
|
||||
|
||||
import { throwError, EMPTY, timer, from, Subscription } from 'rxjs';
|
||||
import { mergeMap, expand, takeUntil, finalize, tap } from 'rxjs/operators';
|
||||
import { debounce } from 'lodash';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { mergeMap, expand, takeUntil, finalize, catchError } from 'rxjs/operators';
|
||||
import {
|
||||
SearchInterceptor,
|
||||
SearchInterceptorDeps,
|
||||
|
@ -15,6 +13,7 @@ import {
|
|||
} from '../../../../../src/plugins/data/public';
|
||||
import { isErrorResponse, isCompleteResponse } from '../../../../../src/plugins/data/public';
|
||||
import { AbortError, toPromise } from '../../../../../src/plugins/data/common';
|
||||
import { TimeoutErrorMode } from '../../../../../src/plugins/data/public';
|
||||
import { IAsyncSearchOptions } from '.';
|
||||
import { IAsyncSearchRequest, ENHANCED_ES_SEARCH_STRATEGY } from '../../common';
|
||||
|
||||
|
@ -40,6 +39,12 @@ export class EnhancedSearchInterceptor extends SearchInterceptor {
|
|||
this.uiSettingsSub.unsubscribe();
|
||||
}
|
||||
|
||||
protected getTimeoutMode() {
|
||||
return this.application.capabilities.advancedSettings?.save
|
||||
? TimeoutErrorMode.CHANGE
|
||||
: TimeoutErrorMode.CONTACT;
|
||||
}
|
||||
|
||||
/**
|
||||
* Abort our `AbortController`, which in turn aborts any intercepted searches.
|
||||
*/
|
||||
|
@ -55,7 +60,7 @@ export class EnhancedSearchInterceptor extends SearchInterceptor {
|
|||
) {
|
||||
let { id } = request;
|
||||
|
||||
const { combinedSignal, cleanup } = this.setupAbortSignal({
|
||||
const { combinedSignal, timeoutSignal, cleanup } = this.setupAbortSignal({
|
||||
abortSignal: options.abortSignal,
|
||||
timeout: this.searchTimeout,
|
||||
});
|
||||
|
@ -86,15 +91,14 @@ export class EnhancedSearchInterceptor extends SearchInterceptor {
|
|||
);
|
||||
}),
|
||||
takeUntil(aborted$),
|
||||
tap({
|
||||
error: () => {
|
||||
// If we haven't received the response to the initial request, including the ID, then
|
||||
// we don't need to send a follow-up request to delete this search. Otherwise, we
|
||||
// send the follow-up request to delete this search, then throw an abort error.
|
||||
if (id !== undefined) {
|
||||
this.deps.http.delete(`/internal/search/${strategy}/${id}`);
|
||||
}
|
||||
},
|
||||
catchError((e: any) => {
|
||||
// If we haven't received the response to the initial request, including the ID, then
|
||||
// we don't need to send a follow-up request to delete this search. Otherwise, we
|
||||
// send the follow-up request to delete this search, then throw an abort error.
|
||||
if (id !== undefined) {
|
||||
this.deps.http.delete(`/internal/search/${strategy}/${id}`);
|
||||
}
|
||||
return throwError(this.handleSearchError(e, request, timeoutSignal, options?.abortSignal));
|
||||
}),
|
||||
finalize(() => {
|
||||
this.pendingCount$.next(this.pendingCount$.getValue() - 1);
|
||||
|
@ -102,28 +106,4 @@ export class EnhancedSearchInterceptor extends SearchInterceptor {
|
|||
})
|
||||
);
|
||||
}
|
||||
|
||||
// Right now we are debouncing but we will hook this up with background sessions to show only one
|
||||
// error notification per session.
|
||||
protected showTimeoutError = debounce(
|
||||
(e: Error) => {
|
||||
const message = this.application.capabilities.advancedSettings?.save
|
||||
? i18n.translate('xpack.data.search.timeoutIncreaseSetting', {
|
||||
defaultMessage:
|
||||
'One or more queries timed out. Increase run time with the search.timeout advanced setting.',
|
||||
})
|
||||
: i18n.translate('xpack.data.search.timeoutContactAdmin', {
|
||||
defaultMessage:
|
||||
'One or more queries timed out. Contact your system administrator to increase the run time.',
|
||||
});
|
||||
this.deps.toasts.addError(e, {
|
||||
title: 'Timed out',
|
||||
toastMessage: message,
|
||||
});
|
||||
},
|
||||
60000,
|
||||
{
|
||||
leading: true,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
|
|
@ -44,6 +44,7 @@ describe('alert actions', () => {
|
|||
updateTimelineIsLoading = jest.fn() as jest.Mocked<UpdateTimelineLoading>;
|
||||
searchStrategyClient = {
|
||||
aggs: {} as ISearchStart['aggs'],
|
||||
showError: jest.fn(),
|
||||
search: jest.fn().mockResolvedValue({ data: mockTimelineDetails }),
|
||||
searchSource: {} as ISearchStart['searchSource'],
|
||||
};
|
||||
|
|
|
@ -1370,10 +1370,6 @@
|
|||
"discover.embeddable.inspectorRequestDataTitle": "データ",
|
||||
"discover.embeddable.inspectorRequestDescription": "このリクエストはElasticsearchにクエリをかけ、検索データを取得します。",
|
||||
"discover.embeddable.search.displayName": "検索",
|
||||
"discover.errorLoadingData": "データの読み込み中にエラーが発生",
|
||||
"discover.fetchError.howToAddressErrorDescription": "このエラーは、{scriptedFields}タブにある {managementLink}の{fetchErrorScript}フィールドを編集することで解決できます。",
|
||||
"discover.fetchError.managmentLinkText": "管理>インデックスパターン",
|
||||
"discover.fetchError.scriptedFieldsText": "「スクリプトフィールド」",
|
||||
"discover.fieldChooser.detailViews.emptyStringText": "空の文字列",
|
||||
"discover.fieldChooser.detailViews.filterOutValueButtonAriaLabel": "{field}を除外:\"{value}\"",
|
||||
"discover.fieldChooser.detailViews.filterValueButtonAriaLabel": "{field}を除外:\"{value}\"",
|
||||
|
@ -1450,7 +1446,6 @@
|
|||
"discover.notifications.invalidTimeRangeTitle": "無効な時間範囲",
|
||||
"discover.notifications.notSavedSearchTitle": "検索「{savedSearchTitle}」は保存されませんでした。",
|
||||
"discover.notifications.savedSearchTitle": "検索「{savedSearchTitle}」が保存されました。",
|
||||
"discover.painlessError.painlessScriptedFieldErrorMessage": "Painlessスクリプトのフィールド「{script}」のエラー.",
|
||||
"discover.reloadSavedSearchButton": "検索をリセット",
|
||||
"discover.rootBreadcrumb": "発見",
|
||||
"discover.savedSearch.savedObjectName": "保存検索",
|
||||
|
@ -4385,7 +4380,6 @@
|
|||
"visualize.createVisualization.noIndexPatternOrSavedSearchIdErrorMessage": "indexPatternまたはsavedSearchIdが必要です",
|
||||
"visualize.createVisualization.noVisTypeErrorMessage": "有効なビジュアライゼーションタイプを指定してください",
|
||||
"visualize.editor.createBreadcrumb": "作成",
|
||||
"visualize.error.title": "ビジュアライゼーションエラー",
|
||||
"visualize.helpMenu.appName": "可視化",
|
||||
"visualize.linkedToSearch.unlinkSuccessNotificationText": "保存された検索「{searchTitle}」からリンクが解除されました",
|
||||
"visualize.listing.betaTitle": "ベータ",
|
||||
|
|
|
@ -1371,10 +1371,6 @@
|
|||
"discover.embeddable.inspectorRequestDataTitle": "数据",
|
||||
"discover.embeddable.inspectorRequestDescription": "此请求将查询 Elasticsearch 以获取搜索的数据。",
|
||||
"discover.embeddable.search.displayName": "搜索",
|
||||
"discover.errorLoadingData": "加载数据时出错",
|
||||
"discover.fetchError.howToAddressErrorDescription": "您可以通过编辑{managementLink}中{scriptedFields}选项卡下的“{fetchErrorScript}”字段来解决此错误。",
|
||||
"discover.fetchError.managmentLinkText": "“管理”>“索引模式”",
|
||||
"discover.fetchError.scriptedFieldsText": "“脚本字段”",
|
||||
"discover.fieldChooser.detailViews.emptyStringText": "空字符串",
|
||||
"discover.fieldChooser.detailViews.filterOutValueButtonAriaLabel": "筛除 {field}:“{value}”",
|
||||
"discover.fieldChooser.detailViews.filterValueButtonAriaLabel": "筛留 {field}:“{value}”",
|
||||
|
@ -1451,7 +1447,6 @@
|
|||
"discover.notifications.invalidTimeRangeTitle": "时间范围无效",
|
||||
"discover.notifications.notSavedSearchTitle": "搜索“{savedSearchTitle}”未保存。",
|
||||
"discover.notifications.savedSearchTitle": "搜索“{savedSearchTitle}”已保存",
|
||||
"discover.painlessError.painlessScriptedFieldErrorMessage": "Painless 脚本字段“{script}”有错误。",
|
||||
"discover.reloadSavedSearchButton": "重置搜索",
|
||||
"discover.rootBreadcrumb": "Discover",
|
||||
"discover.savedSearch.savedObjectName": "已保存搜索",
|
||||
|
@ -4386,7 +4381,6 @@
|
|||
"visualize.createVisualization.noIndexPatternOrSavedSearchIdErrorMessage": "必须提供 indexPattern 或 savedSearchId",
|
||||
"visualize.createVisualization.noVisTypeErrorMessage": "必须提供有效的可视化类型",
|
||||
"visualize.editor.createBreadcrumb": "创建",
|
||||
"visualize.error.title": "可视化错误",
|
||||
"visualize.helpMenu.appName": "Visualize",
|
||||
"visualize.linkedToSearch.unlinkSuccessNotificationText": "已取消与已保存搜索“{searchTitle}”的链接",
|
||||
"visualize.listing.betaTitle": "公测版",
|
||||
|
|
|
@ -9,7 +9,7 @@ import { FtrProviderContext } from '../../ftr_provider_context';
|
|||
|
||||
export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
||||
const esArchiver = getService('esArchiver');
|
||||
const testSubjects = getService('testSubjects');
|
||||
const toasts = getService('toasts');
|
||||
const PageObjects = getPageObjects(['common', 'discover', 'timePicker']);
|
||||
|
||||
describe('errors', function describeIndexTests() {
|
||||
|
@ -23,11 +23,13 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
after(async function () {
|
||||
await esArchiver.unload('invalid_scripted_field');
|
||||
});
|
||||
|
||||
// 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 isFetchErrorVisible = await testSubjects.exists('discoverFetchError');
|
||||
expect(isFetchErrorVisible).to.be(true);
|
||||
const toast = await toasts.getToastElement(1);
|
||||
const painlessStackTrace = await toast.findByTestSubject('painlessStackTrace');
|
||||
expect(painlessStackTrace).not.to.be(undefined);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue