refactor search source warnings to return a single warning for 'is_partial' results (#165512)

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

This PR replaces individual shard failure and timeout warnings with a
single "incomplete data" warning. This work is required for
https://github.com/elastic/kibana/issues/163381

<img width="500" alt="Screen Shot 2023-09-06 at 9 35 52 AM"
src="77e62792-c1f1-4780-b4f2-3aca24e4691b">

<img width="500" alt="Screen Shot 2023-09-06 at 9 36 00 AM"
src="56f37db1-2b4a-484b-9244-66b352d82dc1">

<img width="500" alt="Screen Shot 2023-09-06 at 9 36 07 AM"
src="4a777963-6e88-4736-9d63-99a2843ebdbb">

### Test instructions
* Install flights and web logs sample data
* Create data view kibana_sample_data*. **Set time field to timestamp**
* open discover and select kibana_sample_data* data view
* Add filter with custom DSL
    ```
    {
      "error_query": {
        "indices": [
          {
            "error_type": "exception",
            "message": "local shard failure message 123",
            "name": "kibana_sample_data_logs",
            "shard_ids": [
              0
            ]
          }
        ]
      }
    }
    ```

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: Julia Rechkunova <julia.rechkunova@gmail.com>
Co-authored-by: Marco Liberati <dej611@users.noreply.github.com>
This commit is contained in:
Nathan Reese 2023-09-14 08:55:53 -06:00 committed by GitHub
parent 5bd152369b
commit f3b280f6ee
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
82 changed files with 1234 additions and 1898 deletions

View file

@ -311,17 +311,15 @@ export const SearchExamplesApp = ({
const result = await lastValueFrom( const result = await lastValueFrom(
searchSource.fetch$({ searchSource.fetch$({
abortSignal: abortController.signal, abortSignal: abortController.signal,
disableShardFailureWarning: !showWarningToastNotifications, disableWarningToasts: !showWarningToastNotifications,
inspector, inspector,
}) })
); );
setRawResponse(result.rawResponse); setRawResponse(result.rawResponse);
/* Here is an example of using showWarnings on the search service, using an optional callback to /*
* intercept the warnings before notification warnings are shown. * Set disableWarningToasts to true to disable warning toasts and customize warning display.
* * Then use showWarnings to customize warning notification.
* Suppressing the shard failure warning notification from appearing by default requires setting
* { disableShardFailureWarning: true } in the SearchSourceSearchOptions passed to $fetch
*/ */
if (showWarningToastNotifications) { if (showWarningToastNotifications) {
setWarningContents([]); setWarningContents([]);
@ -498,7 +496,7 @@ export const SearchExamplesApp = ({
{' '} {' '}
<FormattedMessage <FormattedMessage
id="searchExamples.warningsObject" id="searchExamples.warningsObject"
defaultMessage="Timeout and shard failure warnings for high-level search may be handled in a callback to the showWarnings method on the search service." defaultMessage="Search warnings may optionally be handed with search service showWarnings method."
/>{' '} />{' '}
</EuiText>{' '} </EuiText>{' '}
<EuiProgress value={loaded} max={total} size="xs" data-test-subj="progressBar" />{' '} <EuiProgress value={loaded} max={total} size="xs" data-test-subj="progressBar" />{' '}

View file

@ -18,4 +18,5 @@ export type {
AggregationResultOfMap, AggregationResultOfMap,
ESFilter, ESFilter,
MaybeReadonlyArray, MaybeReadonlyArray,
ClusterDetails,
} from './src'; } from './src';

View file

@ -11,6 +11,7 @@ import {
AggregateOf as AggregationResultOf, AggregateOf as AggregationResultOf,
AggregateOfMap as AggregationResultOfMap, AggregateOfMap as AggregationResultOfMap,
SearchHit, SearchHit,
ClusterDetails,
} from './search'; } from './search';
export type ESFilter = estypes.QueryDslQueryContainer; export type ESFilter = estypes.QueryDslQueryContainer;
@ -34,4 +35,10 @@ export type ESSearchResponse<
TOptions extends { restTotalHitsAsInt: boolean } = { restTotalHitsAsInt: false } TOptions extends { restTotalHitsAsInt: boolean } = { restTotalHitsAsInt: false }
> = InferSearchResponseOf<TDocument, TSearchRequest, TOptions>; > = InferSearchResponseOf<TDocument, TSearchRequest, TOptions>;
export type { InferSearchResponseOf, AggregationResultOf, AggregationResultOfMap, SearchHit }; export type {
InferSearchResponseOf,
AggregationResultOf,
AggregationResultOfMap,
SearchHit,
ClusterDetails,
};

View file

@ -644,3 +644,12 @@ export type InferSearchResponseOf<
>; >;
}; };
}; };
export interface ClusterDetails {
status: 'running' | 'successful' | 'partial' | 'skipped' | 'failed';
indices: string;
took?: number;
timed_out: boolean;
_shards?: estypes.ShardStatistics;
failures?: estypes.ShardFailure[];
}

View file

@ -13,7 +13,5 @@ export {
type SearchResponseWarningsProps, type SearchResponseWarningsProps,
} from './src/components/search_response_warnings'; } from './src/components/search_response_warnings';
export { export { getSearchResponseInterceptedWarnings } from './src/utils/get_search_response_intercepted_warnings';
getSearchResponseInterceptedWarnings, export { hasUnsupportedDownsampledAggregationFailure } from './src/utils/has_unsupported_downsampled_aggregation_failure';
removeInterceptedWarningDuplicates,
} from './src/utils/get_search_response_intercepted_warnings';

View file

@ -8,43 +8,33 @@
import type { SearchResponseWarning } from '@kbn/data-plugin/public'; import type { SearchResponseWarning } from '@kbn/data-plugin/public';
export const searchResponseTimeoutWarningMock: SearchResponseWarning = { export const searchResponseIncompleteWarningLocalCluster: SearchResponseWarning = {
type: 'timed_out', type: 'incomplete',
message: 'Data might be incomplete because your request timed out', message: 'The data might be incomplete or wrong.',
reason: undefined, clusters: {
}; '(local)': {
status: 'partial',
export const searchResponseShardFailureWarningMock: SearchResponseWarning = { indices: '',
type: 'shard_failure', took: 25,
message: '3 of 4 shards failed', timed_out: false,
text: 'The data might be incomplete or wrong.', _shards: {
reason: { total: 4,
type: 'illegal_argument_exception', successful: 3,
reason: 'Field [__anonymous_] of type [boolean] does not support custom formats', skipped: 0,
}, failed: 1,
}; },
failures: [
export const searchResponseWarningsMock: SearchResponseWarning[] = [ {
searchResponseTimeoutWarningMock, shard: 0,
searchResponseShardFailureWarningMock, index: 'sample-01-rollup',
{ node: 'VFTFJxpHSdaoiGxJFLSExQ',
type: 'shard_failure', reason: {
message: '3 of 4 shards failed', type: 'illegal_argument_exception',
text: 'The data might be incomplete or wrong.', reason:
reason: { 'Field [kubernetes.container.memory.available.bytes] of type [aggregate_metric_double] is not supported for aggregation [percentiles]',
type: 'query_shard_exception', },
reason: },
'failed to create query: [.ds-kibana_sample_data_logs-2023.07.11-000001][0] Testing shard failures!', ],
}, },
}, },
{ };
type: 'shard_failure',
message: '1 of 4 shards failed',
text: 'The data might be incomplete or wrong.',
reason: {
type: 'query_shard_exception',
reason:
'failed to create query: [.ds-kibana_sample_data_logs-2023.07.11-000001][0] Testing shard failures!',
},
},
];

View file

@ -13,7 +13,7 @@ exports[`SearchResponseWarnings renders "badge" correctly 1`] = `
<button <button
class="euiButton emotion-euiButtonDisplay-s-EuiButton" class="euiButton emotion-euiButtonDisplay-s-EuiButton"
data-test-subj="test2_trigger" data-test-subj="test2_trigger"
title="4 warnings" title="1 warning"
type="button" type="button"
> >
<span <span
@ -23,7 +23,7 @@ exports[`SearchResponseWarnings renders "badge" correctly 1`] = `
class="emotion-EuiIcon" class="emotion-EuiIcon"
data-euiicon-type="warning" data-euiicon-type="warning"
/> />
4 1
</span> </span>
</button> </button>
</span> </span>
@ -68,84 +68,14 @@ exports[`SearchResponseWarnings renders "callout" correctly 1`] = `
class="euiText emotion-euiText-s" class="euiText emotion-euiText-s"
data-test-subj="test1_warningTitle" data-test-subj="test1_warningTitle"
> >
Data might be incomplete because your request timed out The data might be incomplete or wrong.
</div>
</div>
</div>
</div>
<div
class="euiFlexItem emotion-euiFlexItem-growZero"
>
<button
aria-label="Close"
class="euiButtonIcon emotion-euiButtonIcon-xs-empty-primary"
type="button"
>
<span
aria-hidden="true"
class="euiButtonIcon__icon"
color="inherit"
data-euiicon-type="cross"
/>
</button>
</div>
</div>
<p />
</div>
</li>
<li>
<div
class="euiPanel euiPanel--warning euiPanel--paddingSmall euiCallOut euiCallOut--warning emotion-euiPanel-none-s-warning"
data-test-subj="test1"
>
<p
class="euiTitle euiCallOutHeader__title emotion-euiTitle-xxs-euiCallOutHeader-warning"
>
<span
aria-hidden="true"
class="emotion-euiCallOut__icon"
color="inherit"
data-euiicon-type="warning"
/>
</p>
<div
class="euiFlexGroup emotion-euiFlexGroup-none-spaceBetween-stretch-row"
>
<div
class="euiFlexItem emotion-euiFlexItem-growZero"
>
<div
class="euiFlexGroup emotion-euiFlexGroup-responsive-wrap-xs-flexStart-center-row"
>
<div
class="euiFlexItem emotion-euiFlexItem-growZero"
>
<div
class="euiText emotion-euiText-s"
data-test-subj="test1_warningTitle"
>
<strong>
3 of 4 shards failed
</strong>
</div>
</div>
<div
class="euiFlexItem emotion-euiFlexItem-growZero"
>
<div
class="euiText emotion-euiText-s"
data-test-subj="test1_warningMessage"
>
<p>
The data might be incomplete or wrong.
</p>
</div> </div>
</div> </div>
<div <div
class="euiFlexItem emotion-euiFlexItem-growZero" class="euiFlexItem emotion-euiFlexItem-growZero"
> >
<button> <button>
test1 test
</button> </button>
</div> </div>
</div> </div>
@ -155,161 +85,7 @@ exports[`SearchResponseWarnings renders "callout" correctly 1`] = `
> >
<button <button
aria-label="Close" aria-label="Close"
class="euiButtonIcon emotion-euiButtonIcon-xs-empty-primary" class="euiButtonIcon emotion-euiButtonIcon-xs-empty-warning"
type="button"
>
<span
aria-hidden="true"
class="euiButtonIcon__icon"
color="inherit"
data-euiicon-type="cross"
/>
</button>
</div>
</div>
<p />
</div>
</li>
<li>
<div
class="euiPanel euiPanel--warning euiPanel--paddingSmall euiCallOut euiCallOut--warning emotion-euiPanel-none-s-warning"
data-test-subj="test1"
>
<p
class="euiTitle euiCallOutHeader__title emotion-euiTitle-xxs-euiCallOutHeader-warning"
>
<span
aria-hidden="true"
class="emotion-euiCallOut__icon"
color="inherit"
data-euiicon-type="warning"
/>
</p>
<div
class="euiFlexGroup emotion-euiFlexGroup-none-spaceBetween-stretch-row"
>
<div
class="euiFlexItem emotion-euiFlexItem-growZero"
>
<div
class="euiFlexGroup emotion-euiFlexGroup-responsive-wrap-xs-flexStart-center-row"
>
<div
class="euiFlexItem emotion-euiFlexItem-growZero"
>
<div
class="euiText emotion-euiText-s"
data-test-subj="test1_warningTitle"
>
<strong>
3 of 4 shards failed
</strong>
</div>
</div>
<div
class="euiFlexItem emotion-euiFlexItem-growZero"
>
<div
class="euiText emotion-euiText-s"
data-test-subj="test1_warningMessage"
>
<p>
The data might be incomplete or wrong.
</p>
</div>
</div>
<div
class="euiFlexItem emotion-euiFlexItem-growZero"
>
<button>
test2
</button>
</div>
</div>
</div>
<div
class="euiFlexItem emotion-euiFlexItem-growZero"
>
<button
aria-label="Close"
class="euiButtonIcon emotion-euiButtonIcon-xs-empty-primary"
type="button"
>
<span
aria-hidden="true"
class="euiButtonIcon__icon"
color="inherit"
data-euiicon-type="cross"
/>
</button>
</div>
</div>
<p />
</div>
</li>
<li>
<div
class="euiPanel euiPanel--warning euiPanel--paddingSmall euiCallOut euiCallOut--warning emotion-euiPanel-none-s-warning"
data-test-subj="test1"
>
<p
class="euiTitle euiCallOutHeader__title emotion-euiTitle-xxs-euiCallOutHeader-warning"
>
<span
aria-hidden="true"
class="emotion-euiCallOut__icon"
color="inherit"
data-euiicon-type="warning"
/>
</p>
<div
class="euiFlexGroup emotion-euiFlexGroup-none-spaceBetween-stretch-row"
>
<div
class="euiFlexItem emotion-euiFlexItem-growZero"
>
<div
class="euiFlexGroup emotion-euiFlexGroup-responsive-wrap-xs-flexStart-center-row"
>
<div
class="euiFlexItem emotion-euiFlexItem-growZero"
>
<div
class="euiText emotion-euiText-s"
data-test-subj="test1_warningTitle"
>
<strong>
1 of 4 shards failed
</strong>
</div>
</div>
<div
class="euiFlexItem emotion-euiFlexItem-growZero"
>
<div
class="euiText emotion-euiText-s"
data-test-subj="test1_warningMessage"
>
<p>
The data might be incomplete or wrong.
</p>
</div>
</div>
<div
class="euiFlexItem emotion-euiFlexItem-growZero"
>
<button>
test3
</button>
</div>
</div>
</div>
<div
class="euiFlexItem emotion-euiFlexItem-growZero"
>
<button
aria-label="Close"
class="euiButtonIcon emotion-euiButtonIcon-xs-empty-primary"
type="button" type="button"
> >
<span <span
@ -377,124 +153,14 @@ exports[`SearchResponseWarnings renders "empty_prompt" correctly 1`] = `
class="euiText emotion-euiText-m" class="euiText emotion-euiText-m"
data-test-subj="test3_warningTitle" data-test-subj="test3_warningTitle"
> >
Data might be incomplete because your request timed out The data might be incomplete or wrong.
</div>
</div>
</div>
</li>
<li
css="You have tried to stringify object returned from \`css\` function. It isn't supposed to be used directly (e.g. as value of the \`className\` prop), but rather handed to emotion so it can handle it (e.g. as value of \`css\` prop)."
>
<div
class="euiFlexGroup emotion-euiFlexGroup-wrap-xs-flexStart-stretch-column"
>
<div
class="euiFlexItem emotion-euiFlexItem-growZero"
>
<div
class="euiText emotion-euiText-m"
data-test-subj="test3_warningTitle"
>
<strong>
3 of 4 shards failed
</strong>
</div>
</div>
<div
class="euiFlexItem emotion-euiFlexItem-growZero"
>
<div
class="euiText emotion-euiText-m"
data-test-subj="test3_warningMessage"
>
<p>
The data might be incomplete or wrong.
</p>
</div> </div>
</div> </div>
<div <div
class="euiFlexItem emotion-euiFlexItem-growZero" class="euiFlexItem emotion-euiFlexItem-growZero"
> >
<button> <button>
test1 test
</button>
</div>
</div>
</li>
<li
css="You have tried to stringify object returned from \`css\` function. It isn't supposed to be used directly (e.g. as value of the \`className\` prop), but rather handed to emotion so it can handle it (e.g. as value of \`css\` prop)."
>
<div
class="euiFlexGroup emotion-euiFlexGroup-wrap-xs-flexStart-stretch-column"
>
<div
class="euiFlexItem emotion-euiFlexItem-growZero"
>
<div
class="euiText emotion-euiText-m"
data-test-subj="test3_warningTitle"
>
<strong>
3 of 4 shards failed
</strong>
</div>
</div>
<div
class="euiFlexItem emotion-euiFlexItem-growZero"
>
<div
class="euiText emotion-euiText-m"
data-test-subj="test3_warningMessage"
>
<p>
The data might be incomplete or wrong.
</p>
</div>
</div>
<div
class="euiFlexItem emotion-euiFlexItem-growZero"
>
<button>
test2
</button>
</div>
</div>
</li>
<li
css="You have tried to stringify object returned from \`css\` function. It isn't supposed to be used directly (e.g. as value of the \`className\` prop), but rather handed to emotion so it can handle it (e.g. as value of \`css\` prop)."
>
<div
class="euiFlexGroup emotion-euiFlexGroup-wrap-xs-flexStart-stretch-column"
>
<div
class="euiFlexItem emotion-euiFlexItem-growZero"
>
<div
class="euiText emotion-euiText-m"
data-test-subj="test3_warningTitle"
>
<strong>
1 of 4 shards failed
</strong>
</div>
</div>
<div
class="euiFlexItem emotion-euiFlexItem-growZero"
>
<div
class="euiText emotion-euiText-m"
data-test-subj="test3_warningMessage"
>
<p>
The data might be incomplete or wrong.
</p>
</div>
</div>
<div
class="euiFlexItem emotion-euiFlexItem-growZero"
>
<button>
test3
</button> </button>
</div> </div>
</div> </div>

View file

@ -9,12 +9,14 @@
import React from 'react'; import React from 'react';
import { mountWithIntl } from '@kbn/test-jest-helpers'; import { mountWithIntl } from '@kbn/test-jest-helpers';
import { SearchResponseWarnings } from './search_response_warnings'; import { SearchResponseWarnings } from './search_response_warnings';
import { searchResponseWarningsMock } from '../../__mocks__/search_response_warnings'; import { searchResponseIncompleteWarningLocalCluster } from '../../__mocks__/search_response_warnings';
const interceptedWarnings = searchResponseWarningsMock.map((originalWarning, index) => ({ const interceptedWarnings = [
originalWarning, {
action: originalWarning.type === 'shard_failure' ? <button>{`test${index}`}</button> : undefined, originalWarning: searchResponseIncompleteWarningLocalCluster,
})); action: <button>{`test`}</button>,
},
];
describe('SearchResponseWarnings', () => { describe('SearchResponseWarnings', () => {
it('renders "callout" correctly', () => { it('renders "callout" correctly', () => {

View file

@ -270,22 +270,13 @@ function WarningContent({
groupStyles?: Partial<EuiFlexGroupProps>; groupStyles?: Partial<EuiFlexGroupProps>;
'data-test-subj': string; 'data-test-subj': string;
}) { }) {
const hasDescription = 'text' in originalWarning;
return ( return (
<EuiFlexGroup gutterSize="xs" {...groupStyles} wrap> <EuiFlexGroup gutterSize="xs" {...groupStyles} wrap>
<EuiFlexItem grow={false}> <EuiFlexItem grow={false}>
<EuiText size={textSize} data-test-subj={`${dataTestSubj}_warningTitle`}> <EuiText size={textSize} data-test-subj={`${dataTestSubj}_warningTitle`}>
{hasDescription ? <strong>{originalWarning.message}</strong> : originalWarning.message} {originalWarning.message}
</EuiText> </EuiText>
</EuiFlexItem> </EuiFlexItem>
{hasDescription ? (
<EuiFlexItem grow={false}>
<EuiText size={textSize} data-test-subj={`${dataTestSubj}_warningMessage`}>
<p>{originalWarning.text}</p>
</EuiText>
</EuiFlexItem>
) : null}
{action ? <EuiFlexItem grow={false}>{action}</EuiFlexItem> : null} {action ? <EuiFlexItem grow={false}>{action}</EuiFlexItem> : null}
</EuiFlexGroup> </EuiFlexGroup>
); );
@ -306,6 +297,7 @@ function CalloutTitleWrapper({
onClick={onCloseCallout} onClick={onCloseCallout}
type="button" type="button"
iconType="cross" iconType="cross"
color="warning"
/> />
</EuiFlexItem> </EuiFlexItem>
</EuiFlexGroup> </EuiFlexGroup>

View file

@ -9,11 +9,8 @@
import { RequestAdapter } from '@kbn/inspector-plugin/common'; import { RequestAdapter } from '@kbn/inspector-plugin/common';
import { dataPluginMock } from '@kbn/data-plugin/public/mocks'; import { dataPluginMock } from '@kbn/data-plugin/public/mocks';
import { coreMock } from '@kbn/core/public/mocks'; import { coreMock } from '@kbn/core/public/mocks';
import { import { getSearchResponseInterceptedWarnings } from './get_search_response_intercepted_warnings';
getSearchResponseInterceptedWarnings, import { searchResponseIncompleteWarningLocalCluster } from '../__mocks__/search_response_warnings';
removeInterceptedWarningDuplicates,
} from './get_search_response_intercepted_warnings';
import { searchResponseWarningsMock } from '../__mocks__/search_response_warnings';
const servicesMock = { const servicesMock = {
data: dataPluginMock.createStartContract(), data: dataPluginMock.createStartContract(),
@ -23,162 +20,66 @@ const servicesMock = {
describe('getSearchResponseInterceptedWarnings', () => { describe('getSearchResponseInterceptedWarnings', () => {
const adapter = new RequestAdapter(); const adapter = new RequestAdapter();
it('should catch warnings correctly', () => { it('should return intercepted incomplete data warnings', () => {
const services = { const services = {
...servicesMock, ...servicesMock,
}; };
services.data.search.showWarnings = jest.fn((_, callback) => { services.data.search.showWarnings = jest.fn((_, callback) => {
// @ts-expect-error for empty meta // @ts-expect-error for empty meta
callback?.(searchResponseWarningsMock[0], {}); callback?.(searchResponseIncompleteWarningLocalCluster, {});
// @ts-expect-error for empty meta
callback?.(searchResponseWarningsMock[1], {});
// @ts-expect-error for empty meta
callback?.(searchResponseWarningsMock[2], {});
// @ts-expect-error for empty meta
callback?.(searchResponseWarningsMock[3], {});
// plus duplicates
// @ts-expect-error for empty meta
callback?.(searchResponseWarningsMock[0], {});
// @ts-expect-error for empty meta
callback?.(searchResponseWarningsMock[1], {});
// @ts-expect-error for empty meta
callback?.(searchResponseWarningsMock[2], {});
}); });
expect( const warnings = getSearchResponseInterceptedWarnings({
getSearchResponseInterceptedWarnings({ services,
services, adapter,
adapter, });
options: {
disableShardFailureWarning: true, expect(warnings.length).toBe(1);
}, expect(warnings[0].originalWarning).toEqual(searchResponseIncompleteWarningLocalCluster);
}) expect(warnings[0].action).toMatchInlineSnapshot(`
).toMatchInlineSnapshot(` <OpenIncompleteResultsModalButton
Array [ color="primary"
Object { getRequestMeta={[Function]}
"action": undefined, isButtonEmpty={true}
"originalWarning": Object { size="s"
"message": "Data might be incomplete because your request timed out", theme={
"reason": undefined, Object {
"type": "timed_out", "theme$": Observable {
}, "_subscribe": [Function],
},
Object {
"action": <ShardFailureOpenModalButton
color="primary"
getRequestMeta={[Function]}
isButtonEmpty={true}
size="s"
theme={
Object {
"theme$": Observable {
"_subscribe": [Function],
},
}
}
title="3 of 4 shards failed"
/>,
"originalWarning": Object {
"message": "3 of 4 shards failed",
"reason": Object {
"reason": "Field [__anonymous_] of type [boolean] does not support custom formats",
"type": "illegal_argument_exception",
}, },
"text": "The data might be incomplete or wrong.", }
"type": "shard_failure", }
}, warning={
}, Object {
Object { "clusters": Object {
"action": <ShardFailureOpenModalButton "(local)": Object {
color="primary" "_shards": Object {
getRequestMeta={[Function]} "failed": 1,
isButtonEmpty={true} "skipped": 0,
size="s" "successful": 3,
theme={ "total": 4,
Object {
"theme$": Observable {
"_subscribe": [Function],
}, },
} "failures": Array [
} Object {
title="3 of 4 shards failed" "index": "sample-01-rollup",
/>, "node": "VFTFJxpHSdaoiGxJFLSExQ",
"originalWarning": Object { "reason": Object {
"message": "3 of 4 shards failed", "reason": "Field [kubernetes.container.memory.available.bytes] of type [aggregate_metric_double] is not supported for aggregation [percentiles]",
"reason": Object { "type": "illegal_argument_exception",
"reason": "failed to create query: [.ds-kibana_sample_data_logs-2023.07.11-000001][0] Testing shard failures!", },
"type": "query_shard_exception", "shard": 0,
},
],
"indices": "",
"status": "partial",
"timed_out": false,
"took": 25,
},
}, },
"text": "The data might be incomplete or wrong.", "message": "The data might be incomplete or wrong.",
"type": "shard_failure", "type": "incomplete",
}, }
}, }
Object { />
"action": <ShardFailureOpenModalButton
color="primary"
getRequestMeta={[Function]}
isButtonEmpty={true}
size="s"
theme={
Object {
"theme$": Observable {
"_subscribe": [Function],
},
}
}
title="1 of 4 shards failed"
/>,
"originalWarning": Object {
"message": "1 of 4 shards failed",
"reason": Object {
"reason": "failed to create query: [.ds-kibana_sample_data_logs-2023.07.11-000001][0] Testing shard failures!",
"type": "query_shard_exception",
},
"text": "The data might be incomplete or wrong.",
"type": "shard_failure",
},
},
]
`); `);
}); });
it('should not catch any warnings if disableShardFailureWarning is false', () => {
const services = {
...servicesMock,
};
services.data.search.showWarnings = jest.fn((_, callback) => {
// @ts-expect-error for empty meta
callback?.(searchResponseWarningsMock[0], {});
});
expect(
getSearchResponseInterceptedWarnings({
services,
adapter,
options: {
disableShardFailureWarning: false,
},
})
).toBeUndefined();
});
});
describe('removeInterceptedWarningDuplicates', () => {
it('should remove duplicates successfully', () => {
const interceptedWarnings = searchResponseWarningsMock.map((originalWarning) => ({
originalWarning,
}));
expect(removeInterceptedWarningDuplicates([interceptedWarnings[0]])).toEqual([
interceptedWarnings[0],
]);
expect(removeInterceptedWarningDuplicates(interceptedWarnings)).toEqual(interceptedWarnings);
expect(
removeInterceptedWarningDuplicates([...interceptedWarnings, ...interceptedWarnings])
).toEqual(interceptedWarnings);
});
it('should return undefined if the list is empty', () => {
expect(removeInterceptedWarningDuplicates([])).toBeUndefined();
expect(removeInterceptedWarningDuplicates(undefined)).toBeUndefined();
});
}); });

View file

@ -7,11 +7,9 @@
*/ */
import React from 'react'; import React from 'react';
import { uniqBy } from 'lodash';
import { import {
type DataPublicPluginStart, type DataPublicPluginStart,
type ShardFailureRequest, OpenIncompleteResultsModalButton,
ShardFailureOpenModalButton,
} from '@kbn/data-plugin/public'; } from '@kbn/data-plugin/public';
import type { RequestAdapter } from '@kbn/inspector-plugin/common'; import type { RequestAdapter } from '@kbn/inspector-plugin/common';
import type { CoreStart } from '@kbn/core-lifecycle-browser'; import type { CoreStart } from '@kbn/core-lifecycle-browser';
@ -26,21 +24,13 @@ import type { SearchResponseInterceptedWarning } from '../types';
export const getSearchResponseInterceptedWarnings = ({ export const getSearchResponseInterceptedWarnings = ({
services, services,
adapter, adapter,
options,
}: { }: {
services: { services: {
data: DataPublicPluginStart; data: DataPublicPluginStart;
theme: CoreStart['theme']; theme: CoreStart['theme'];
}; };
adapter: RequestAdapter; adapter: RequestAdapter;
options?: { }): SearchResponseInterceptedWarning[] => {
disableShardFailureWarning?: boolean;
};
}): SearchResponseInterceptedWarning[] | undefined => {
if (!options?.disableShardFailureWarning) {
return undefined;
}
const interceptedWarnings: SearchResponseInterceptedWarning[] = []; const interceptedWarnings: SearchResponseInterceptedWarning[] = [];
services.data.search.showWarnings(adapter, (warning, meta) => { services.data.search.showWarnings(adapter, (warning, meta) => {
@ -49,13 +39,13 @@ export const getSearchResponseInterceptedWarnings = ({
interceptedWarnings.push({ interceptedWarnings.push({
originalWarning: warning, originalWarning: warning,
action: action:
warning.type === 'shard_failure' && warning.text && warning.message ? ( warning.type === 'incomplete' ? (
<ShardFailureOpenModalButton <OpenIncompleteResultsModalButton
theme={services.theme} theme={services.theme}
title={warning.message} warning={warning}
size="s" size="s"
getRequestMeta={() => ({ getRequestMeta={() => ({
request: request as ShardFailureRequest, request,
response, response,
})} })}
color="primary" color="primary"
@ -66,23 +56,5 @@ export const getSearchResponseInterceptedWarnings = ({
return true; // suppress the default behaviour return true; // suppress the default behaviour
}); });
return removeInterceptedWarningDuplicates(interceptedWarnings); return interceptedWarnings;
};
/**
* Removes duplicated warnings
* @param interceptedWarnings
*/
export const removeInterceptedWarningDuplicates = (
interceptedWarnings: SearchResponseInterceptedWarning[] | undefined
): SearchResponseInterceptedWarning[] | undefined => {
if (!interceptedWarnings?.length) {
return undefined;
}
const uniqInterceptedWarnings = uniqBy(interceptedWarnings, (interceptedWarning) =>
JSON.stringify(interceptedWarning.originalWarning)
);
return uniqInterceptedWarnings?.length ? uniqInterceptedWarnings : undefined;
}; };

View file

@ -0,0 +1,80 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { hasUnsupportedDownsampledAggregationFailure } from './has_unsupported_downsampled_aggregation_failure';
describe('hasUnsupportedDownsampledAggregationFailure', () => {
test('should return false when unsupported_aggregation_on_downsampled_index shard failure does not exist', () => {
expect(
hasUnsupportedDownsampledAggregationFailure({
type: 'incomplete',
message: 'The data might be incomplete or wrong.',
clusters: {
'(local)': {
status: 'partial',
indices: '',
took: 25,
timed_out: false,
_shards: {
total: 4,
successful: 3,
skipped: 0,
failed: 1,
},
failures: [
{
shard: 0,
index: 'sample-01-rollup',
node: 'VFTFJxpHSdaoiGxJFLSExQ',
reason: {
type: 'illegal_argument_exception',
reason:
'Field [kubernetes.container.memory.available.bytes] of type [aggregate_metric_double] is not supported for aggregation [percentiles]',
},
},
],
},
},
})
).toBe(false);
});
test('should return true when unsupported_aggregation_on_downsampled_index shard failure exists', () => {
expect(
hasUnsupportedDownsampledAggregationFailure({
type: 'incomplete',
message: 'The data might be incomplete or wrong.',
clusters: {
'(local)': {
status: 'partial',
indices: '',
took: 25,
timed_out: false,
_shards: {
total: 4,
successful: 3,
skipped: 0,
failed: 1,
},
failures: [
{
shard: 0,
index: 'sample-01-rollup',
node: 'VFTFJxpHSdaoiGxJFLSExQ',
reason: {
type: 'unsupported_aggregation_on_downsampled_index',
reason: 'blah blah blah timeseries data',
},
},
],
},
},
})
).toBe(true);
});
});

View file

@ -0,0 +1,26 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import type {
SearchResponseWarning,
SearchResponseIncompleteWarning,
} from '@kbn/data-plugin/public';
export function hasUnsupportedDownsampledAggregationFailure(warning: SearchResponseWarning) {
return warning.type === 'incomplete'
? Object.values((warning as SearchResponseIncompleteWarning).clusters).some(
(clusterDetails) => {
return clusterDetails.failures
? clusterDetails.failures.some((shardFailure) => {
return shardFailure.reason?.type === 'unsupported_aggregation_on_downsampled_index';
})
: false;
}
)
: false;
}

View file

@ -30,7 +30,7 @@ type PostFlightRequestFn<TAggConfig> = (
inspectorRequestAdapter?: RequestAdapter, inspectorRequestAdapter?: RequestAdapter,
abortSignal?: AbortSignal, abortSignal?: AbortSignal,
searchSessionId?: string, searchSessionId?: string,
disableShardFailureWarning?: boolean disableWarningToasts?: boolean
) => Promise<estypes.SearchResponse<any>>; ) => Promise<estypes.SearchResponse<any>>;
export interface AggTypeConfig< export interface AggTypeConfig<

View file

@ -396,7 +396,7 @@ export const createOtherBucketPostFlightRequest = (
inspectorRequestAdapter, inspectorRequestAdapter,
abortSignal, abortSignal,
searchSessionId, searchSessionId,
disableShardFailureWarning disableWarningToasts
) => { ) => {
if (!resp.aggregations) return resp; if (!resp.aggregations) return resp;
const nestedSearchSource = searchSource.createChild(); const nestedSearchSource = searchSource.createChild();
@ -410,7 +410,7 @@ export const createOtherBucketPostFlightRequest = (
nestedSearchSource.fetch$({ nestedSearchSource.fetch$({
abortSignal, abortSignal,
sessionId: searchSessionId, sessionId: searchSessionId,
disableShardFailureWarning, disableWarningToasts,
inspector: { inspector: {
adapter: inspectorRequestAdapter, adapter: inspectorRequestAdapter,
title: i18n.translate('data.search.aggs.buckets.terms.otherBucketTitle', { title: i18n.translate('data.search.aggs.buckets.terms.otherBucketTitle', {

View file

@ -53,7 +53,7 @@ describe('esaggs expression function - public', () => {
query: undefined, query: undefined,
searchSessionId: 'abc123', searchSessionId: 'abc123',
searchSourceService: searchSourceCommonMock, searchSourceService: searchSourceCommonMock,
disableShardWarnings: false, disableWarningToasts: false,
timeFields: ['@timestamp', 'utc_time'], timeFields: ['@timestamp', 'utc_time'],
timeRange: undefined, timeRange: undefined,
}; };
@ -139,7 +139,7 @@ describe('esaggs expression function - public', () => {
description: 'This request queries Elasticsearch to fetch the data for the visualization.', description: 'This request queries Elasticsearch to fetch the data for the visualization.',
adapter: undefined, adapter: undefined,
}, },
disableShardFailureWarning: false, disableWarningToasts: false,
}); });
}); });
@ -159,7 +159,7 @@ describe('esaggs expression function - public', () => {
description: 'MyDescription', description: 'MyDescription',
adapter: undefined, adapter: undefined,
}, },
disableShardFailureWarning: false, disableWarningToasts: false,
}); });
}); });

View file

@ -30,7 +30,7 @@ export interface RequestHandlerParams {
searchSourceService: ISearchStartSearchSource; searchSourceService: ISearchStartSearchSource;
timeFields?: string[]; timeFields?: string[];
timeRange?: TimeRange; timeRange?: TimeRange;
disableShardWarnings?: boolean; disableWarningToasts?: boolean;
getNow?: () => Date; getNow?: () => Date;
executionContext?: KibanaExecutionContext; executionContext?: KibanaExecutionContext;
title?: string; title?: string;
@ -48,7 +48,7 @@ export const handleRequest = ({
searchSourceService, searchSourceService,
timeFields, timeFields,
timeRange, timeRange,
disableShardWarnings, disableWarningToasts,
getNow, getNow,
executionContext, executionContext,
title, title,
@ -110,7 +110,7 @@ export const handleRequest = ({
requestSearchSource requestSearchSource
.fetch$({ .fetch$({
abortSignal, abortSignal,
disableShardFailureWarning: disableShardWarnings, disableWarningToasts,
sessionId: searchSessionId, sessionId: searchSessionId,
inspector: { inspector: {
adapter: inspectorAdapters.requests, adapter: inspectorAdapters.requests,

View file

@ -15,7 +15,7 @@ export type ExecutionContextSearch = {
filters?: Filter[]; filters?: Filter[];
query?: Query | Query[]; query?: Query | Query[];
timeRange?: TimeRange; timeRange?: TimeRange;
disableShardWarnings?: boolean; disableWarningToasts?: boolean;
}; };
export type ExpressionValueSearchContext = ExpressionValueBoxed< export type ExpressionValueSearchContext = ExpressionValueBoxed<

View file

@ -520,7 +520,7 @@ export class SearchSource {
options.inspector?.adapter, options.inspector?.adapter,
options.abortSignal, options.abortSignal,
options.sessionId, options.sessionId,
options.disableShardFailureWarning options.disableWarningToasts
); );
} }
} }

View file

@ -249,7 +249,7 @@ export interface SearchSourceSearchOptions extends ISearchOptions {
inspector?: IInspectorInfo; inspector?: IInspectorInfo;
/** /**
* Disable default warnings of shard failures * Set to true to disable warning toasts and customize warning display
*/ */
disableShardFailureWarning?: boolean; disableWarningToasts?: boolean;
} }

View file

@ -0,0 +1,271 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`IncompleteResultsModal should render shard failures 1`] = `
<Fragment>
<EuiModalHeader>
<EuiModalHeaderTitle
size="xs"
>
<FormattedMessage
defaultMessage="Response contains incomplete results"
id="data.search.searchSource.fetch.incompleteResultsModal.headerTitle"
values={Object {}}
/>
</EuiModalHeaderTitle>
</EuiModalHeader>
<EuiModalBody>
<EuiTabbedContent
autoFocus="selected"
initialSelectedTab={
Object {
"content": <React.Fragment>
<ShardFailureTable
failures={
Array [
Object {
"index": "sample-01-rollup",
"node": "VFTFJxpHSdaoiGxJFLSExQ",
"reason": Object {
"reason": "Field [kubernetes.container.memory.available.bytes] of type [aggregate_metric_double] is not supported for aggregation [percentiles]",
"type": "illegal_argument_exception",
},
"shard": 0,
},
]
}
/>
</React.Fragment>,
"data-test-subj": "showClusterDetailsButton",
"id": "table",
"name": "Cluster details",
}
}
tabs={
Array [
Object {
"content": <React.Fragment>
<ShardFailureTable
failures={
Array [
Object {
"index": "sample-01-rollup",
"node": "VFTFJxpHSdaoiGxJFLSExQ",
"reason": Object {
"reason": "Field [kubernetes.container.memory.available.bytes] of type [aggregate_metric_double] is not supported for aggregation [percentiles]",
"type": "illegal_argument_exception",
},
"shard": 0,
},
]
}
/>
</React.Fragment>,
"data-test-subj": "showClusterDetailsButton",
"id": "table",
"name": "Cluster details",
},
Object {
"content": <EuiCodeBlock
data-test-subj="incompleteResultsModalRequestBlock"
isCopyable={true}
language="json"
>
{}
</EuiCodeBlock>,
"data-test-subj": "showRequestButton",
"id": "json-request",
"name": "Request",
},
Object {
"content": <EuiCodeBlock
data-test-subj="incompleteResultsModalResponseBlock"
isCopyable={true}
language="json"
>
{
"_shards": {
"total": 4,
"successful": 3,
"skipped": 0,
"failed": 1,
"failures": [
{
"shard": 0,
"index": "sample-01-rollup",
"node": "VFTFJxpHSdaoiGxJFLSExQ",
"reason": {
"type": "illegal_argument_exception",
"reason": "Field [kubernetes.container.memory.available.bytes] of type [aggregate_metric_double] is not supported for aggregation [percentiles]"
}
}
]
}
}
</EuiCodeBlock>,
"data-test-subj": "showResponseButton",
"id": "json-response",
"name": "Response",
},
]
}
/>
</EuiModalBody>
<EuiModalFooter>
<EuiCopy
afterMessage="Copied"
textToCopy="{
\\"_shards\\": {
\\"total\\": 4,
\\"successful\\": 3,
\\"skipped\\": 0,
\\"failed\\": 1,
\\"failures\\": [
{
\\"shard\\": 0,
\\"index\\": \\"sample-01-rollup\\",
\\"node\\": \\"VFTFJxpHSdaoiGxJFLSExQ\\",
\\"reason\\": {
\\"type\\": \\"illegal_argument_exception\\",
\\"reason\\": \\"Field [kubernetes.container.memory.available.bytes] of type [aggregate_metric_double] is not supported for aggregation [percentiles]\\"
}
}
]
}
}"
>
<Component />
</EuiCopy>
<EuiButton
color="primary"
data-test-subj="closeIncompleteResultsModal"
fill={true}
onClick={[Function]}
size="m"
>
<FormattedMessage
defaultMessage="Close"
description="Closing the Modal"
id="data.search.searchSource.fetch.incompleteResultsModal.close"
values={Object {}}
/>
</EuiButton>
</EuiModalFooter>
</Fragment>
`;
exports[`IncompleteResultsModal should render time out 1`] = `
<Fragment>
<EuiModalHeader>
<EuiModalHeaderTitle
size="xs"
>
<FormattedMessage
defaultMessage="Response contains incomplete results"
id="data.search.searchSource.fetch.incompleteResultsModal.headerTitle"
values={Object {}}
/>
</EuiModalHeaderTitle>
</EuiModalHeader>
<EuiModalBody>
<EuiTabbedContent
autoFocus="selected"
initialSelectedTab={
Object {
"content": <React.Fragment>
<EuiCallOut
color="warning"
>
<p>
Request timed out
</p>
</EuiCallOut>
</React.Fragment>,
"data-test-subj": "showClusterDetailsButton",
"id": "table",
"name": "Cluster details",
}
}
tabs={
Array [
Object {
"content": <React.Fragment>
<EuiCallOut
color="warning"
>
<p>
Request timed out
</p>
</EuiCallOut>
</React.Fragment>,
"data-test-subj": "showClusterDetailsButton",
"id": "table",
"name": "Cluster details",
},
Object {
"content": <EuiCodeBlock
data-test-subj="incompleteResultsModalRequestBlock"
isCopyable={true}
language="json"
>
{}
</EuiCodeBlock>,
"data-test-subj": "showRequestButton",
"id": "json-request",
"name": "Request",
},
Object {
"content": <EuiCodeBlock
data-test-subj="incompleteResultsModalResponseBlock"
isCopyable={true}
language="json"
>
{
"timed_out": true,
"_shards": {
"total": 4,
"successful": 4,
"skipped": 0,
"failed": 0
}
}
</EuiCodeBlock>,
"data-test-subj": "showResponseButton",
"id": "json-response",
"name": "Response",
},
]
}
/>
</EuiModalBody>
<EuiModalFooter>
<EuiCopy
afterMessage="Copied"
textToCopy="{
\\"timed_out\\": true,
\\"_shards\\": {
\\"total\\": 4,
\\"successful\\": 4,
\\"skipped\\": 0,
\\"failed\\": 0
}
}"
>
<Component />
</EuiCopy>
<EuiButton
color="primary"
data-test-subj="closeIncompleteResultsModal"
fill={true}
onClick={[Function]}
size="m"
>
<FormattedMessage
defaultMessage="Close"
description="Closing the Modal"
id="data.search.searchSource.fetch.incompleteResultsModal.close"
values={Object {}}
/>
</EuiButton>
</EuiModalFooter>
</Fragment>
`;

View file

@ -1,5 +1,5 @@
// set width and height to fixed values to prevent resizing when you switch tabs // set width and height to fixed values to prevent resizing when you switch tabs
.shardFailureModal { .incompleteResultsModal {
min-height: 75vh; min-height: 75vh;
width: 768px; width: 768px;
@ -12,10 +12,4 @@
.euiModalHeader { .euiModalHeader {
padding-bottom: 0; padding-bottom: 0;
} }
} }
.shardFailureModal__desc {
// set for IE11, since without it depending on the content the width of the list
// could be much higher than the available screenspace
max-width: 686px;
}

View file

@ -0,0 +1,72 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import React from 'react';
import { shallow } from 'enzyme';
import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import type { SearchResponseIncompleteWarning } from '../search';
import { IncompleteResultsModal } from './incomplete_results_modal';
describe('IncompleteResultsModal', () => {
test('should render shard failures', () => {
const component = shallow(
<IncompleteResultsModal
warning={{} as unknown as SearchResponseIncompleteWarning}
request={{}}
response={
{
_shards: {
total: 4,
successful: 3,
skipped: 0,
failed: 1,
failures: [
{
shard: 0,
index: 'sample-01-rollup',
node: 'VFTFJxpHSdaoiGxJFLSExQ',
reason: {
type: 'illegal_argument_exception',
reason:
'Field [kubernetes.container.memory.available.bytes] of type [aggregate_metric_double] is not supported for aggregation [percentiles]',
},
},
],
},
} as unknown as estypes.SearchResponse<any>
}
onClose={jest.fn()}
/>
);
expect(component).toMatchSnapshot();
});
test('should render time out', () => {
const component = shallow(
<IncompleteResultsModal
warning={{} as unknown as SearchResponseIncompleteWarning}
request={{}}
response={
{
timed_out: true,
_shards: {
total: 4,
successful: 4,
skipped: 0,
failed: 0,
},
} as unknown as estypes.SearchResponse<any>
}
onClose={jest.fn()}
/>
);
expect(component).toMatchSnapshot();
});
});

View file

@ -0,0 +1,148 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import React from 'react';
import { FormattedMessage } from '@kbn/i18n-react';
import { i18n } from '@kbn/i18n';
import {
EuiCallOut,
EuiCodeBlock,
EuiTabbedContent,
EuiCopy,
EuiButton,
EuiModalBody,
EuiModalHeader,
EuiModalHeaderTitle,
EuiModalFooter,
EuiButtonEmpty,
} from '@elastic/eui';
import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import type { SearchRequest } from '..';
import type { SearchResponseIncompleteWarning } from '../search';
import { ShardFailureTable } from '../shard_failure_modal/shard_failure_table';
export interface Props {
onClose: () => void;
request: SearchRequest;
response: estypes.SearchResponse<any>;
warning: SearchResponseIncompleteWarning;
}
export function IncompleteResultsModal({ request, response, warning, onClose }: Props) {
const requestJSON = JSON.stringify(request, null, 2);
const responseJSON = JSON.stringify(response, null, 2);
const tabs = [
{
id: 'table',
name: i18n.translate(
'data.search.searchSource.fetch.incompleteResultsModal.tabHeaderClusterDetails',
{
defaultMessage: 'Cluster details',
description: 'Name of the tab displaying cluster details',
}
),
content: (
<>
{response.timed_out ? (
<EuiCallOut color="warning">
<p>
{i18n.translate(
'data.search.searchSource.fetch.incompleteResultsModal.requestTimedOutMessage',
{
defaultMessage: 'Request timed out',
}
)}
</p>
</EuiCallOut>
) : null}
{response._shards.failures?.length ? (
<ShardFailureTable failures={response._shards.failures ?? []} />
) : null}
</>
),
['data-test-subj']: 'showClusterDetailsButton',
},
{
id: 'json-request',
name: i18n.translate(
'data.search.searchSource.fetch.incompleteResultsModal.tabHeaderRequest',
{
defaultMessage: 'Request',
description: 'Name of the tab displaying the JSON request',
}
),
content: (
<EuiCodeBlock
language="json"
isCopyable
data-test-subj="incompleteResultsModalRequestBlock"
>
{requestJSON}
</EuiCodeBlock>
),
['data-test-subj']: 'showRequestButton',
},
{
id: 'json-response',
name: i18n.translate(
'data.search.searchSource.fetch.incompleteResultsModal.tabHeaderResponse',
{
defaultMessage: 'Response',
description: 'Name of the tab displaying the JSON response',
}
),
content: (
<EuiCodeBlock
language="json"
isCopyable
data-test-subj="incompleteResultsModalResponseBlock"
>
{responseJSON}
</EuiCodeBlock>
),
['data-test-subj']: 'showResponseButton',
},
];
return (
<React.Fragment>
<EuiModalHeader>
<EuiModalHeaderTitle size="xs">
<FormattedMessage
id="data.search.searchSource.fetch.incompleteResultsModal.headerTitle"
defaultMessage="Response contains incomplete results"
/>
</EuiModalHeaderTitle>
</EuiModalHeader>
<EuiModalBody>
<EuiTabbedContent tabs={tabs} initialSelectedTab={tabs[0]} autoFocus="selected" />
</EuiModalBody>
<EuiModalFooter>
<EuiCopy textToCopy={responseJSON}>
{(copy) => (
<EuiButtonEmpty onClick={copy}>
<FormattedMessage
id="data.search.searchSource.fetch.incompleteResultsModal.copyToClipboard"
defaultMessage="Copy response to clipboard"
/>
</EuiButtonEmpty>
)}
</EuiCopy>
<EuiButton onClick={() => onClose()} fill data-test-subj="closeIncompleteResultsModal">
<FormattedMessage
id="data.search.searchSource.fetch.incompleteResultsModal.close"
defaultMessage="Close"
description="Closing the Modal"
/>
</EuiButton>
</EuiModalFooter>
</React.Fragment>
);
}

View file

@ -7,17 +7,13 @@
*/ */
import React from 'react'; import React from 'react';
import type { ShardFailureOpenModalButtonProps } from './shard_failure_open_modal_button'; import type { OpenIncompleteResultsModalButtonProps } from './open_incomplete_results_modal_button';
const Fallback = () => <div />; const Fallback = () => <div />;
const LazyShardFailureOpenModalButton = React.lazy( const LazyOpenModalButton = React.lazy(() => import('./open_incomplete_results_modal_button'));
() => import('./shard_failure_open_modal_button') export const OpenIncompleteResultsModalButton = (props: OpenIncompleteResultsModalButtonProps) => (
);
export const ShardFailureOpenModalButton = (props: ShardFailureOpenModalButtonProps) => (
<React.Suspense fallback={<Fallback />}> <React.Suspense fallback={<Fallback />}>
<LazyShardFailureOpenModalButton {...props} /> <LazyOpenModalButton {...props} />
</React.Suspense> </React.Suspense>
); );
export type { ShardFailureRequest } from './shard_failure_types';

View file

@ -13,18 +13,19 @@ import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import type { ThemeServiceStart } from '@kbn/core/public'; import type { ThemeServiceStart } from '@kbn/core/public';
import { toMountPoint } from '@kbn/kibana-react-plugin/public'; import { toMountPoint } from '@kbn/kibana-react-plugin/public';
import { getOverlays } from '../services'; import { getOverlays } from '../services';
import { ShardFailureModal } from './shard_failure_modal'; import type { SearchRequest } from '..';
import type { ShardFailureRequest } from './shard_failure_types'; import { IncompleteResultsModal } from './incomplete_results_modal';
import './_shard_failure_modal.scss'; import type { SearchResponseIncompleteWarning } from '../search';
import './_incomplete_results_modal.scss';
// @internal // @internal
export interface ShardFailureOpenModalButtonProps { export interface OpenIncompleteResultsModalButtonProps {
theme: ThemeServiceStart; theme: ThemeServiceStart;
title: string; warning: SearchResponseIncompleteWarning;
size?: EuiButtonProps['size']; size?: EuiButtonProps['size'];
color?: EuiButtonProps['color']; color?: EuiButtonProps['color'];
getRequestMeta: () => { getRequestMeta: () => {
request: ShardFailureRequest; request: SearchRequest;
response: estypes.SearchResponse<any>; response: estypes.SearchResponse<any>;
}; };
isButtonEmpty?: boolean; isButtonEmpty?: boolean;
@ -32,31 +33,31 @@ export interface ShardFailureOpenModalButtonProps {
// Needed for React.lazy // Needed for React.lazy
// eslint-disable-next-line import/no-default-export // eslint-disable-next-line import/no-default-export
export default function ShardFailureOpenModalButton({ export default function OpenIncompleteResultsModalButton({
getRequestMeta, getRequestMeta,
theme, theme,
title, warning,
size = 's', size = 's',
color = 'warning', color = 'warning',
isButtonEmpty = false, isButtonEmpty = false,
}: ShardFailureOpenModalButtonProps) { }: OpenIncompleteResultsModalButtonProps) {
const onClick = useCallback(() => { const onClick = useCallback(() => {
const { request, response } = getRequestMeta(); const { request, response } = getRequestMeta();
const modal = getOverlays().openModal( const modal = getOverlays().openModal(
toMountPoint( toMountPoint(
<ShardFailureModal <IncompleteResultsModal
request={request} request={request}
response={response} response={response}
title={title} warning={warning}
onClose={() => modal.close()} onClose={() => modal.close()}
/>, />,
{ theme$: theme.theme$ } { theme$: theme.theme$ }
), ),
{ {
className: 'shardFailureModal', className: 'incompleteResultsModal',
} }
); );
}, [getRequestMeta, theme.theme$, title]); }, [getRequestMeta, theme.theme$, warning]);
const Component = isButtonEmpty ? EuiLink : EuiButton; const Component = isButtonEmpty ? EuiLink : EuiButton;
@ -65,11 +66,11 @@ export default function ShardFailureOpenModalButton({
color={color} color={color}
size={size} size={size}
onClick={onClick} onClick={onClick}
data-test-subj="openShardFailureModalBtn" data-test-subj="openIncompleteResultsModalBtn"
> >
<FormattedMessage <FormattedMessage
id="data.search.searchSource.fetch.shardsFailedModal.showDetails" id="data.search.searchSource.fetch.incompleteResultsModal.viewDetails"
defaultMessage="Show details" defaultMessage="View details"
description="Open the modal to show details" description="Open the modal to show details"
/> />
</Component> </Component>

View file

@ -170,6 +170,7 @@ export type {
Reason, Reason,
WaitUntilNextSessionCompletesOptions, WaitUntilNextSessionCompletesOptions,
SearchResponseWarning, SearchResponseWarning,
SearchResponseIncompleteWarning,
} from './search'; } from './search';
export { export {
@ -273,8 +274,7 @@ export type {
} from './query'; } from './query';
// TODO: move to @kbn/search-response-warnings // TODO: move to @kbn/search-response-warnings
export type { ShardFailureRequest } from './shard_failure_modal'; export { OpenIncompleteResultsModalButton } from './incomplete_results_modal';
export { ShardFailureOpenModalButton } from './shard_failure_modal';
export type { AggsStart } from './search/aggs'; export type { AggsStart } from './search/aggs';

View file

@ -126,7 +126,7 @@ describe('esaggs expression function - public', () => {
searchSessionId: 'abc123', searchSessionId: 'abc123',
searchSourceService: startDependencies.searchSource, searchSourceService: startDependencies.searchSource,
timeFields: args.timeFields, timeFields: args.timeFields,
disableShardWarnings: false, disableWarningToasts: false,
timeRange: undefined, timeRange: undefined,
getNow: undefined, getNow: undefined,
}); });

View file

@ -61,7 +61,7 @@ export function getFunctionDefinition({
return { aggConfigs, indexPattern, searchSource, getNow, handleEsaggsRequest }; return { aggConfigs, indexPattern, searchSource, getNow, handleEsaggsRequest };
}).pipe( }).pipe(
switchMap(({ aggConfigs, indexPattern, searchSource, getNow, handleEsaggsRequest }) => { switchMap(({ aggConfigs, indexPattern, searchSource, getNow, handleEsaggsRequest }) => {
const { disableShardWarnings } = getSearchContext(); const { disableWarningToasts } = getSearchContext();
return handleEsaggsRequest({ return handleEsaggsRequest({
abortSignal, abortSignal,
@ -74,7 +74,7 @@ export function getFunctionDefinition({
searchSourceService: searchSource, searchSourceService: searchSource,
timeFields: args.timeFields, timeFields: args.timeFields,
timeRange: get(input, 'timeRange', undefined), timeRange: get(input, 'timeRange', undefined),
disableShardWarnings: (disableShardWarnings || false) as boolean, disableWarningToasts: (disableWarningToasts || false) as boolean,
getNow, getNow,
executionContext: getExecutionContext(), executionContext: getExecutionContext(),
}); });

View file

@ -10,121 +10,280 @@ import { estypes } from '@elastic/elasticsearch';
import { extractWarnings } from './extract_warnings'; import { extractWarnings } from './extract_warnings';
describe('extract search response warnings', () => { describe('extract search response warnings', () => {
it('should extract warnings from response with shard failures', () => { describe('single cluster', () => {
const response = { it('should extract incomplete warning from response with shard failures', () => {
took: 25, const response = {
timed_out: false, took: 25,
_shards: { timed_out: false,
total: 4, _shards: {
successful: 2, total: 4,
skipped: 0, successful: 3,
failed: 2, skipped: 0,
failures: [ failed: 1,
{ failures: [
shard: 0, {
index: 'sample-01-rollup', shard: 0,
node: 'VFTFJxpHSdaoiGxJFLSExQ', index: 'sample-01-rollup',
reason: { node: 'VFTFJxpHSdaoiGxJFLSExQ',
type: 'illegal_argument_exception', reason: {
reason: type: 'illegal_argument_exception',
'Field [kubernetes.container.memory.available.bytes] of type [aggregate_metric_double] is not supported for aggregation [percentiles]', reason:
'Field [kubernetes.container.memory.available.bytes] of type [aggregate_metric_double] is not supported for aggregation [percentiles]',
},
},
],
},
hits: { total: 18239, max_score: null, hits: [] },
aggregations: {},
};
expect(extractWarnings(response)).toEqual([
{
type: 'incomplete',
message: 'The data might be incomplete or wrong.',
clusters: {
'(local)': {
status: 'partial',
indices: '',
took: 25,
timed_out: false,
_shards: response._shards,
failures: response._shards.failures,
}, },
}, },
],
},
hits: { total: 18239, max_score: null, hits: [] },
aggregations: {},
};
expect(extractWarnings(response)).toEqual([
{
type: 'shard_failure',
message: '2 of 4 shards failed',
reason: {
type: 'illegal_argument_exception',
reason:
'Field [kubernetes.container.memory.available.bytes] of type' +
' [aggregate_metric_double] is not supported for aggregation [percentiles]',
}, },
text: 'The data might be incomplete or wrong.', ]);
}, });
]);
it('should extract incomplete warning from response with time out', () => {
const response = {
took: 999,
timed_out: true,
_shards: {} as estypes.ShardStatistics,
hits: { hits: [] },
};
expect(extractWarnings(response)).toEqual([
{
type: 'incomplete',
message: 'The data might be incomplete or wrong.',
clusters: {
'(local)': {
status: 'partial',
indices: '',
took: 999,
timed_out: true,
_shards: response._shards,
failures: response._shards.failures,
},
},
},
]);
});
it('should not include warnings when there are none', () => {
const warnings = extractWarnings({
timed_out: false,
_shards: {
failed: 0,
total: 9000,
},
} as estypes.SearchResponse);
expect(warnings).toEqual([]);
});
}); });
it('should extract timeout warning', () => { describe('remote clusters', () => {
const warnings = { it('should extract incomplete warning from response with shard failures', () => {
took: 999, const response = {
timed_out: true, took: 25,
_shards: {} as estypes.ShardStatistics, timed_out: false,
hits: { hits: [] }, _shards: {
}; total: 4,
expect(extractWarnings(warnings)).toEqual([ successful: 3,
{ skipped: 0,
type: 'timed_out', failed: 1,
message: 'Data might be incomplete because your request timed out', failures: [
}, {
]); shard: 0,
}); index: 'remote1:.ds-kibana_sample_data_logs-2023.08.21-000001',
node: 'NVzFRd6SS4qT9o0k2vIzlg',
reason: {
type: 'query_shard_exception',
reason:
'failed to create query: [.ds-kibana_sample_data_logs-2023.08.21-000001][0] local shard failure message 123',
index_uuid: 'z1sPO8E4TdWcijNgsL_BxQ',
index: 'remote1:.ds-kibana_sample_data_logs-2023.08.21-000001',
caused_by: {
type: 'runtime_exception',
reason:
'runtime_exception: [.ds-kibana_sample_data_logs-2023.08.21-000001][0] local shard failure message 123',
},
},
},
],
},
_clusters: {
total: 2,
successful: 2,
skipped: 0,
details: {
'(local)': {
status: 'successful',
indices: 'kibana_sample_data_logs,kibana_sample_data_flights',
took: 1,
timed_out: false,
_shards: {
total: 2,
successful: 2,
skipped: 0,
failed: 0,
},
},
remote1: {
status: 'partial',
indices: 'kibana_sample_data_logs,kibana_sample_data_flights',
took: 5,
timed_out: false,
_shards: {
total: 2,
successful: 1,
skipped: 0,
failed: 1,
},
failures: [
{
shard: 0,
index: 'remote1:.ds-kibana_sample_data_logs-2023.08.21-000001',
node: 'NVzFRd6SS4qT9o0k2vIzlg',
reason: {
type: 'query_shard_exception',
reason:
'failed to create query: [.ds-kibana_sample_data_logs-2023.08.21-000001][0] local shard failure message 123',
index_uuid: 'z1sPO8E4TdWcijNgsL_BxQ',
index: 'remote1:.ds-kibana_sample_data_logs-2023.08.21-000001',
caused_by: {
type: 'runtime_exception',
reason:
'runtime_exception: [.ds-kibana_sample_data_logs-2023.08.21-000001][0] local shard failure message 123',
},
},
},
],
},
},
},
hits: { total: 18239, max_score: null, hits: [] },
aggregations: {},
};
it('should extract shards failed warnings', () => { expect(extractWarnings(response)).toEqual([
const warnings = { {
_shards: { type: 'incomplete',
failed: 77, message: 'The data might be incomplete or wrong.',
total: 79, clusters: response._clusters.details,
}, },
} as estypes.SearchResponse; ]);
expect(extractWarnings(warnings)).toEqual([ });
{
type: 'shard_failure',
message: '77 of 79 shards failed',
reason: { type: 'generic_shard_warning' },
text: 'The data might be incomplete or wrong.',
},
]);
});
it('should extract shards failed warning failure reason type', () => { it('should extract incomplete warning from response with time out', () => {
const warnings = extractWarnings({ const response = {
_shards: { took: 999,
failed: 77, timed_out: true,
total: 79, _shards: {
}, total: 6,
} as estypes.SearchResponse); successful: 6,
expect(warnings).toEqual([ skipped: 0,
{ failed: 0,
type: 'shard_failure', },
message: '77 of 79 shards failed', _clusters: {
reason: { type: 'generic_shard_warning' }, total: 2,
text: 'The data might be incomplete or wrong.', successful: 2,
}, skipped: 0,
]); details: {
}); '(local)': {
status: 'successful',
indices:
'kibana_sample_data_ecommerce,kibana_sample_data_logs,kibana_sample_data_flights',
took: 0,
timed_out: false,
_shards: {
total: 3,
successful: 3,
skipped: 0,
failed: 0,
},
},
remote1: {
status: 'partial',
indices: 'kibana_sample_data*',
took: 10005,
timed_out: true,
_shards: {
total: 3,
successful: 3,
skipped: 0,
failed: 0,
},
},
},
},
hits: { hits: [] },
};
expect(extractWarnings(response)).toEqual([
{
type: 'incomplete',
message: 'The data might be incomplete or wrong.',
clusters: response._clusters.details,
},
]);
});
it('extracts multiple warnings', () => { it('should not include warnings when there are none', () => {
const warnings = extractWarnings({ const warnings = extractWarnings({
timed_out: true, took: 10,
_shards: { timed_out: false,
failed: 77, _shards: {
total: 79, total: 4,
}, successful: 4,
} as estypes.SearchResponse); skipped: 0,
const [shardFailures, timedOut] = [ failed: 0,
warnings.filter(({ type }) => type !== 'timed_out'), },
warnings.filter(({ type }) => type === 'timed_out'), _clusters: {
]; total: 2,
expect(shardFailures[0]!.message).toBeDefined(); successful: 2,
expect(timedOut[0]!.message).toBeDefined(); skipped: 0,
}); details: {
'(local)': {
status: 'successful',
indices: 'kibana_sample_data_logs,kibana_sample_data_flights',
took: 0,
timed_out: false,
_shards: {
total: 2,
successful: 2,
skipped: 0,
failed: 0,
},
},
remote1: {
status: 'successful',
indices: 'kibana_sample_data_logs,kibana_sample_data_flights',
took: 1,
timed_out: false,
_shards: {
total: 2,
successful: 2,
skipped: 0,
failed: 0,
},
},
},
},
hits: { hits: [] },
} as estypes.SearchResponse);
it('should not include shardStats or types fields if there are no warnings', () => { expect(warnings).toEqual([]);
const warnings = extractWarnings({ });
timed_out: false,
_shards: {
failed: 0,
total: 9000,
},
} as estypes.SearchResponse);
expect(warnings).toEqual([]);
}); });
}); });

View file

@ -8,6 +8,7 @@
import { estypes } from '@elastic/elasticsearch'; import { estypes } from '@elastic/elasticsearch';
import { i18n } from '@kbn/i18n'; import { i18n } from '@kbn/i18n';
import type { ClusterDetails } from '@kbn/es-types';
import { SearchResponseWarning } from '../types'; import { SearchResponseWarning } from '../types';
/** /**
@ -16,53 +17,38 @@ import { SearchResponseWarning } from '../types';
export function extractWarnings(rawResponse: estypes.SearchResponse): SearchResponseWarning[] { export function extractWarnings(rawResponse: estypes.SearchResponse): SearchResponseWarning[] {
const warnings: SearchResponseWarning[] = []; const warnings: SearchResponseWarning[] = [];
if (rawResponse.timed_out === true) { const isPartial = rawResponse._clusters
? Object.values(
(
rawResponse._clusters as estypes.ClusterStatistics & {
details: Record<string, ClusterDetails>;
}
).details
).some((clusterDetails) => clusterDetails.status !== 'successful')
: rawResponse.timed_out || rawResponse._shards.failed > 0;
if (isPartial) {
warnings.push({ warnings.push({
type: 'timed_out', type: 'incomplete',
message: i18n.translate('data.search.searchSource.fetch.requestTimedOutNotificationMessage', { message: i18n.translate('data.search.searchSource.fetch.incompleteResultsMessage', {
defaultMessage: 'Data might be incomplete because your request timed out', defaultMessage: 'The data might be incomplete or wrong.',
}), }),
reason: undefined, // exists so that callers do not have to cast when working with shard warnings. clusters: rawResponse._clusters
}); ? (
} rawResponse._clusters as estypes.ClusterStatistics & {
details: Record<string, ClusterDetails>;
if (rawResponse._shards && rawResponse._shards.failed) { }
const message = i18n.translate( ).details
'data.search.searchSource.fetch.shardsFailedNotificationMessage', : {
{ '(local)': {
defaultMessage: '{shardsFailed} of {shardsTotal} shards failed', status: 'partial',
values: { indices: '',
shardsFailed: rawResponse._shards.failed, took: rawResponse.took,
shardsTotal: rawResponse._shards.total, timed_out: rawResponse.timed_out,
}, _shards: rawResponse._shards,
} failures: rawResponse._shards.failures,
); },
const text = i18n.translate(
'data.search.searchSource.fetch.shardsFailedNotificationDescription',
{ defaultMessage: 'The data might be incomplete or wrong.' }
);
if (rawResponse._shards.failures) {
rawResponse._shards.failures?.forEach((f) => {
warnings.push({
type: 'shard_failure',
message,
text,
reason: {
type: f.reason.type,
reason: f.reason.reason,
}, },
}); });
});
} else {
// unknown type and reason
warnings.push({
type: 'shard_failure',
message,
text,
reason: { type: 'generic_shard_warning' },
});
}
} }
return warnings; return warnings;

View file

@ -1,182 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { estypes } from '@elastic/elasticsearch';
import { notificationServiceMock } from '@kbn/core-notifications-browser-mocks';
import { themeServiceMock } from '@kbn/core/public/mocks';
import { setNotifications } from '../../services';
import { SearchResponseWarning } from '../types';
import { filterWarnings, handleWarnings } from './handle_warnings';
import * as extract from './extract_warnings';
import { SearchRequest } from '../../../common';
jest.mock('@kbn/i18n', () => {
return {
i18n: {
translate: (_id: string, { defaultMessage }: { defaultMessage: string }) => defaultMessage,
},
};
});
jest.mock('./extract_warnings', () => ({
extractWarnings: jest.fn(() => []),
}));
const theme = themeServiceMock.createStartContract();
const warnings: SearchResponseWarning[] = [
{
type: 'timed_out' as const,
message: 'Something timed out!',
reason: undefined,
},
{
type: 'shard_failure' as const,
message: 'Some shards failed!',
text: 'test text',
reason: { type: 'illegal_argument_exception', reason: 'Illegal argument! Go to jail!' },
},
{
type: 'shard_failure' as const,
message: 'Some shards failed!',
reason: { type: 'generic_shard_failure' },
},
];
const sessionId = 'abcd';
describe('Filtering and showing warnings', () => {
const notifications = notificationServiceMock.createStartContract();
jest.useFakeTimers();
describe('handleWarnings', () => {
const request = { body: {} };
beforeEach(() => {
jest.resetAllMocks();
jest.advanceTimersByTime(30000);
setNotifications(notifications);
(notifications.toasts.addWarning as jest.Mock).mockReset();
(extract.extractWarnings as jest.Mock).mockImplementation(() => warnings);
});
test('should notify if timed out', () => {
(extract.extractWarnings as jest.Mock).mockImplementation(() => [warnings[0]]);
const response = { rawResponse: { timed_out: true } } as unknown as estypes.SearchResponse;
handleWarnings({ request, response, theme });
// test debounce, addWarning should only be called once
handleWarnings({ request, response, theme });
expect(notifications.toasts.addWarning).toBeCalledTimes(1);
expect(notifications.toasts.addWarning).toBeCalledWith({ title: 'Something timed out!' });
// test debounce, call addWarning again due to sessionId
handleWarnings({ request, response, theme, sessionId });
expect(notifications.toasts.addWarning).toBeCalledTimes(2);
});
test('should notify if shards failed for unknown type/reason', () => {
(extract.extractWarnings as jest.Mock).mockImplementation(() => [warnings[2]]);
const response = {
rawResponse: { _shards: { failed: 1, total: 2, successful: 1, skipped: 1 } },
} as unknown as estypes.SearchResponse;
handleWarnings({ request, response, theme });
// test debounce, addWarning should only be called once
handleWarnings({ request, response, theme });
expect(notifications.toasts.addWarning).toBeCalledTimes(1);
expect(notifications.toasts.addWarning).toBeCalledWith({ title: 'Some shards failed!' });
// test debounce, call addWarning again due to sessionId
handleWarnings({ request, response, theme, sessionId });
expect(notifications.toasts.addWarning).toBeCalledTimes(2);
});
test('should add mount point for shard modal failure button if warning.text is provided', () => {
(extract.extractWarnings as jest.Mock).mockImplementation(() => [warnings[1]]);
const response = {
rawResponse: { _shards: { failed: 1, total: 2, successful: 1, skipped: 1 } },
} as unknown as estypes.SearchResponse;
handleWarnings({ request, response, theme });
// test debounce, addWarning should only be called once
handleWarnings({ request, response, theme });
expect(notifications.toasts.addWarning).toBeCalledTimes(1);
expect(notifications.toasts.addWarning).toBeCalledWith({
title: 'Some shards failed!',
text: expect.any(Function),
});
// test debounce, call addWarning again due to sessionId
handleWarnings({ request, response, theme, sessionId });
expect(notifications.toasts.addWarning).toBeCalledTimes(2);
});
test('should notify once if the response contains multiple failures', () => {
(extract.extractWarnings as jest.Mock).mockImplementation(() => [warnings[1], warnings[2]]);
const response = {
rawResponse: { _shards: { failed: 1, total: 2, successful: 1, skipped: 1 } },
} as unknown as estypes.SearchResponse;
handleWarnings({ request, response, theme });
expect(notifications.toasts.addWarning).toBeCalledTimes(1);
expect(notifications.toasts.addWarning).toBeCalledWith({
title: 'Some shards failed!',
text: expect.any(Function),
});
});
test('should notify once if the response contains some unfiltered failures', () => {
const callback = (warning: SearchResponseWarning) =>
warning.reason?.type !== 'generic_shard_failure';
const response = {
rawResponse: { _shards: { failed: 1, total: 2, successful: 1, skipped: 1 } },
} as unknown as estypes.SearchResponse;
handleWarnings({ request, response, theme, callback });
expect(notifications.toasts.addWarning).toBeCalledTimes(1);
expect(notifications.toasts.addWarning).toBeCalledWith({ title: 'Some shards failed!' });
});
test('should not notify if the response contains no unfiltered failures', () => {
const callback = () => true;
const response = {
rawResponse: { _shards: { failed: 1, total: 2, successful: 1, skipped: 1 } },
} as unknown as estypes.SearchResponse;
handleWarnings({ request, response, theme, callback });
expect(notifications.toasts.addWarning).toBeCalledTimes(0);
});
});
describe('filterWarnings', () => {
const callback = jest.fn();
const request = {} as SearchRequest;
const response = {} as estypes.SearchResponse;
beforeEach(() => {
callback.mockImplementation(() => {
throw new Error('not initialized');
});
});
it('filters out all', () => {
callback.mockImplementation(() => true);
expect(filterWarnings(warnings, callback, request, response, 'id')).toEqual([]);
});
it('filters out some', () => {
callback.mockImplementation(
(warning: SearchResponseWarning) => warning.reason?.type !== 'generic_shard_failure'
);
expect(filterWarnings(warnings, callback, request, response, 'id')).toEqual([warnings[2]]);
});
it('filters out none', () => {
callback.mockImplementation(() => false);
expect(filterWarnings(warnings, callback, request, response, 'id')).toEqual(warnings);
});
});
});

View file

@ -7,49 +7,23 @@
*/ */
import { estypes } from '@elastic/elasticsearch'; import { estypes } from '@elastic/elasticsearch';
import { debounce } from 'lodash'; import { EuiTextAlign } from '@elastic/eui';
import { EuiSpacer, EuiTextAlign } from '@elastic/eui';
import { ThemeServiceStart } from '@kbn/core/public'; import { ThemeServiceStart } from '@kbn/core/public';
import { toMountPoint } from '@kbn/kibana-react-plugin/public'; import { toMountPoint } from '@kbn/kibana-react-plugin/public';
import React from 'react'; import React from 'react';
import type { MountPoint } from '@kbn/core/public';
import { SearchRequest } from '..'; import { SearchRequest } from '..';
import { getNotifications } from '../../services'; import { getNotifications } from '../../services';
import { ShardFailureOpenModalButton, ShardFailureRequest } from '../../shard_failure_modal'; import { OpenIncompleteResultsModalButton } from '../../incomplete_results_modal';
import { import {
SearchResponseShardFailureWarning, SearchResponseIncompleteWarning,
SearchResponseWarning, SearchResponseWarning,
WarningHandlerCallback, WarningHandlerCallback,
} from '../types'; } from '../types';
import { extractWarnings } from './extract_warnings'; import { extractWarnings } from './extract_warnings';
const getDebouncedWarning = () => {
const addWarning = () => {
const { toasts } = getNotifications();
return debounce(toasts.addWarning.bind(toasts), 30000, {
leading: true,
});
};
const memory: Record<string, ReturnType<typeof addWarning>> = {};
return (
debounceKey: string,
title: string,
text?: string | MountPoint<HTMLElement> | undefined
) => {
memory[debounceKey] = memory[debounceKey] || addWarning();
return memory[debounceKey]({ title, text });
};
};
const debouncedWarningWithoutReason = getDebouncedWarning();
const debouncedTimeoutWarning = getDebouncedWarning();
const debouncedWarning = getDebouncedWarning();
/** /**
* @internal * @internal
* All warnings are expected to come from the same response. Therefore all "text" properties, which contain the * All warnings are expected to come from the same response.
* response, will be the same.
*/ */
export function handleWarnings({ export function handleWarnings({
request, request,
@ -78,47 +52,29 @@ export function handleWarnings({
return; return;
} }
// timeout notification // Incomplete data failure notification
const [timeout] = internal.filter((w) => w.type === 'timed_out'); const incompleteWarnings = internal.filter((w) => w.type === 'incomplete');
if (timeout) { if (incompleteWarnings.length === 0) {
debouncedTimeoutWarning(sessionId + timeout.message, timeout.message);
}
// shard warning failure notification
const shardFailures = internal.filter((w) => w.type === 'shard_failure');
if (shardFailures.length === 0) {
return; return;
} }
const [warning] = shardFailures as SearchResponseShardFailureWarning[]; const [incompleteWarning] = incompleteWarnings as SearchResponseIncompleteWarning[];
const title = warning.message; getNotifications().toasts.addWarning({
title: incompleteWarning.message,
// if warning message contains text (warning response), show in ShardFailureOpenModalButton text: toMountPoint(
if (warning.text) { <EuiTextAlign textAlign="right">
const text = toMountPoint( <OpenIncompleteResultsModalButton
<> theme={theme}
{warning.text} getRequestMeta={() => ({
<EuiSpacer size="s" /> request,
<EuiTextAlign textAlign="right"> response,
<ShardFailureOpenModalButton })}
theme={theme} warning={incompleteWarning}
title={title} />
getRequestMeta={() => ({ </EuiTextAlign>,
request: request as ShardFailureRequest,
response,
})}
/>
</EuiTextAlign>
</>,
{ theme$: theme.theme$ } { theme$: theme.theme$ }
); ),
});
debouncedWarning(sessionId + warning.text, title, text);
return;
}
// timeout warning, or shard warning with no failure reason
debouncedWarningWithoutReason(sessionId + title, title);
} }
/** /**

View file

@ -10,6 +10,7 @@ export * from './expressions';
export type { export type {
SearchResponseWarning, SearchResponseWarning,
SearchResponseIncompleteWarning,
ISearchSetup, ISearchSetup,
ISearchStart, ISearchStart,
ISearchStartSearchSource, ISearchStartSearchSource,

View file

@ -142,7 +142,7 @@ describe('Search service', () => {
expect(notifications.toasts.addWarning).toBeCalledTimes(1); expect(notifications.toasts.addWarning).toBeCalledTimes(1);
expect(notifications.toasts.addWarning).toBeCalledWith({ expect(notifications.toasts.addWarning).toBeCalledWith({
title: '2 of 4 shards failed', title: 'The data might be incomplete or wrong.',
text: expect.any(Function), text: expect.any(Function),
}); });
}); });
@ -155,90 +155,6 @@ describe('Search service', () => {
expect(notifications.toasts.addWarning).toBeCalledTimes(0); expect(notifications.toasts.addWarning).toBeCalledTimes(0);
}); });
it('will show single notification when some warnings are filtered', () => {
callback = (warning) => warning.reason?.type === 'illegal_argument_exception';
shards.failures = [
{
reason: {
type: 'illegal_argument_exception',
reason: 'reason of "illegal_argument_exception"',
},
},
{
reason: {
type: 'other_kind_of_exception',
reason: 'reason of other_kind_of_exception',
},
},
{ reason: { type: 'fatal_warning', reason: 'this is a fatal warning message' } },
] as unknown as estypes.ShardFailure[];
const responder = inspector.adapter.start('request1');
responder.ok(getMockResponseWithShards(shards));
data.showWarnings(inspector.adapter, callback);
expect(notifications.toasts.addWarning).toBeCalledTimes(1);
expect(notifications.toasts.addWarning).toBeCalledWith({
title: '2 of 4 shards failed',
text: expect.any(Function),
});
});
it('can show a timed_out warning', () => {
const responder = inspector.adapter.start('request1');
shards = { total: 4, successful: 4, skipped: 0, failed: 0 };
const response1 = getMockResponseWithShards(shards);
response1.json.rawResponse.timed_out = true;
responder.ok(response1);
data.showWarnings(inspector.adapter, callback);
expect(notifications.toasts.addWarning).toBeCalledTimes(1);
expect(notifications.toasts.addWarning).toBeCalledWith({
title: 'Data might be incomplete because your request timed out',
});
});
it('can show two warnings if response has shard failures and also timed_out', () => {
const responder = inspector.adapter.start('request1');
const response1 = getMockResponseWithShards(shards);
response1.json.rawResponse.timed_out = true;
responder.ok(response1);
data.showWarnings(inspector.adapter, callback);
expect(notifications.toasts.addWarning).toBeCalledTimes(2);
expect(notifications.toasts.addWarning).nthCalledWith(1, {
title: 'Data might be incomplete because your request timed out',
});
expect(notifications.toasts.addWarning).nthCalledWith(2, {
title: '2 of 4 shards failed',
text: expect.any(Function),
});
});
it('will show multiple warnings when multiple responses have shard failures', () => {
const responder1 = inspector.adapter.start('request1');
const responder2 = inspector.adapter.start('request2');
responder1.ok(getMockResponseWithShards(shards));
responder2.ok({
json: {
rawResponse: {
timed_out: true,
},
},
});
data.showWarnings(inspector.adapter, callback);
expect(notifications.toasts.addWarning).toBeCalledTimes(2);
expect(notifications.toasts.addWarning).nthCalledWith(1, {
title: '2 of 4 shards failed',
text: expect.any(Function),
});
expect(notifications.toasts.addWarning).nthCalledWith(2, {
title: 'Data might be incomplete because your request timed out',
});
});
}); });
}); });
}); });

View file

@ -243,7 +243,7 @@ export class SearchService implements Plugin<ISearchSetup, ISearchStart> {
getConfig: uiSettings.get.bind(uiSettings), getConfig: uiSettings.get.bind(uiSettings),
search, search,
onResponse: (request, response, options) => { onResponse: (request, response, options) => {
if (!options.disableShardFailureWarning) { if (!options.disableWarningToasts) {
const { rawResponse } = response; const { rawResponse } = response;
handleWarnings({ handleWarnings({

View file

@ -7,6 +7,7 @@
*/ */
import { estypes } from '@elastic/elasticsearch'; import { estypes } from '@elastic/elasticsearch';
import type { ClusterDetails } from '@kbn/es-types';
import type { PackageInfo } from '@kbn/core/server'; import type { PackageInfo } from '@kbn/core/server';
import { DataViewsContract } from '@kbn/data-views-plugin/common'; import { DataViewsContract } from '@kbn/data-views-plugin/common';
import { RequestAdapter } from '@kbn/inspector-plugin/public'; import { RequestAdapter } from '@kbn/inspector-plugin/public';
@ -96,63 +97,35 @@ export interface SearchServiceStartDependencies {
} }
/** /**
* A warning object for a search response with internal ES timeouts * A warning object for a search response with incomplete ES results
* ES returns incomplete results when:
* 1) Set timeout flag on search and the timeout expires on cluster
* 2) Some shard failures on a cluster
* 3) skipped remote(s) (skip_unavailable=true)
* a. all shards failed
* b. disconnected/not-connected
* @public * @public
*/ */
export interface SearchResponseTimeoutWarning { export interface SearchResponseIncompleteWarning {
/** /**
* type: for sorting out timeout warnings * type: for sorting out incomplete warnings
*/ */
type: 'timed_out'; type: 'incomplete';
/** /**
* message: human-friendly message * message: human-friendly message
*/ */
message: string; message: string;
/** /**
* reason: not given for timeout. This exists so that callers do not have to cast when working with shard failure warnings. * clusters: cluster details.
*/ */
reason: undefined; clusters: Record<string, ClusterDetails>;
}
/**
* A warning object for a search response with internal ES shard failures
* @public
*/
export interface SearchResponseShardFailureWarning {
/**
* type: for sorting out shard failure warnings
*/
type: 'shard_failure';
/**
* message: human-friendly message
*/
message: string;
/**
* text: text to show in ShardFailureModal (optional)
*/
text?: string;
/**
* reason: ShardFailureReason from es client
*/
reason: {
/**
* type: failure code from Elasticsearch
*/
type: 'generic_shard_warning' | estypes.ShardFailure['reason']['type'];
/**
* reason: failure reason from Elasticsearch
*/
reason?: estypes.ShardFailure['reason']['reason'];
};
} }
/** /**
* A warning object for a search response with warnings * A warning object for a search response with warnings
* @public * @public
*/ */
export type SearchResponseWarning = export type SearchResponseWarning = SearchResponseIncompleteWarning;
| SearchResponseTimeoutWarning
| SearchResponseShardFailureWarning;
/** /**
* A callback function which can intercept warnings when passed to {@link showWarnings}. Pass `true` from the * A callback function which can intercept warnings when passed to {@link showWarnings}. Pass `true` from the

View file

@ -1,22 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { ShardFailureRequest } from '../shard_failure_types';
export const shardFailureRequest = {
version: true,
size: 500,
sort: [],
_source: {
excludes: [],
},
stored_fields: ['*'],
script_fields: {},
docvalue_fields: [],
query: {},
highlight: {},
} as ShardFailureRequest;

View file

@ -6,7 +6,7 @@
* Side Public License, v 1. * Side Public License, v 1.
*/ */
import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { estypes } from '@elastic/elasticsearch';
export const shardFailureResponse: estypes.SearchResponse<any> = { export const shardFailureResponse: estypes.SearchResponse<any> = {
_shards: { _shards: {
@ -33,4 +33,4 @@ export const shardFailureResponse: estypes.SearchResponse<any> = {
}, },
], ],
}, },
} as any; } as unknown as estypes.SearchResponse<any>;

View file

@ -10,7 +10,6 @@ exports[`ShardFailureDescription renders matching snapshot given valid propertie
grow={false} grow={false}
> >
<EuiDescriptionList <EuiDescriptionList
className="shardFailureModal__desc"
columnWidths={ columnWidths={
Array [ Array [
1, 1,
@ -82,7 +81,6 @@ exports[`ShardFailureDescription should show more details when button is pressed
grow={false} grow={false}
> >
<EuiDescriptionList <EuiDescriptionList
className="shardFailureModal__desc"
columnWidths={ columnWidths={
Array [ Array [
1, 1,

View file

@ -1,201 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ShardFailureModal renders matching snapshot given valid properties 1`] = `
<Fragment>
<EuiModalHeader>
<EuiModalHeaderTitle
data-test-subj="shardFailureModalTitle"
size="xs"
>
test
</EuiModalHeaderTitle>
</EuiModalHeader>
<EuiModalBody>
<EuiTabbedContent
autoFocus="selected"
initialSelectedTab={
Object {
"content": <ShardFailureTable
failures={
Array [
Object {
"index": "repro2",
"node": "itsmeyournode",
"reason": Object {
"caused_by": Object {
"reason": "Gimme reason",
"type": "illegal_argument_exception",
},
"lang": "painless",
"reason": "runtime error",
"script": "return doc['targetfield'].value;",
"script_stack": Array [
"return doc['targetfield'].value;",
" ^---- HERE",
],
"type": "script_exception",
},
"shard": 0,
},
]
}
/>,
"data-test-subj": "shardFailuresModalShardButton",
"id": "table",
"name": "Shard failures",
}
}
tabs={
Array [
Object {
"content": <ShardFailureTable
failures={
Array [
Object {
"index": "repro2",
"node": "itsmeyournode",
"reason": Object {
"caused_by": Object {
"reason": "Gimme reason",
"type": "illegal_argument_exception",
},
"lang": "painless",
"reason": "runtime error",
"script": "return doc['targetfield'].value;",
"script_stack": Array [
"return doc['targetfield'].value;",
" ^---- HERE",
],
"type": "script_exception",
},
"shard": 0,
},
]
}
/>,
"data-test-subj": "shardFailuresModalShardButton",
"id": "table",
"name": "Shard failures",
},
Object {
"content": <EuiCodeBlock
data-test-subj="shardsFailedModalRequestBlock"
isCopyable={true}
language="json"
>
{
"version": true,
"size": 500,
"sort": [],
"_source": {
"excludes": []
},
"stored_fields": [
"*"
],
"script_fields": {},
"docvalue_fields": [],
"query": {},
"highlight": {}
}
</EuiCodeBlock>,
"data-test-subj": "shardFailuresModalRequestButton",
"id": "json-request",
"name": "Request",
},
Object {
"content": <EuiCodeBlock
data-test-subj="shardsFailedModalResponseBlock"
isCopyable={true}
language="json"
>
{
"_shards": {
"total": 2,
"successful": 1,
"skipped": 0,
"failed": 1,
"failures": [
{
"shard": 0,
"index": "repro2",
"node": "itsmeyournode",
"reason": {
"type": "script_exception",
"reason": "runtime error",
"script_stack": [
"return doc['targetfield'].value;",
" ^---- HERE"
],
"script": "return doc['targetfield'].value;",
"lang": "painless",
"caused_by": {
"type": "illegal_argument_exception",
"reason": "Gimme reason"
}
}
}
]
}
}
</EuiCodeBlock>,
"data-test-subj": "shardFailuresModalResponseButton",
"id": "json-response",
"name": "Response",
},
]
}
/>
</EuiModalBody>
<EuiModalFooter>
<EuiCopy
afterMessage="Copied"
textToCopy="{
\\"_shards\\": {
\\"total\\": 2,
\\"successful\\": 1,
\\"skipped\\": 0,
\\"failed\\": 1,
\\"failures\\": [
{
\\"shard\\": 0,
\\"index\\": \\"repro2\\",
\\"node\\": \\"itsmeyournode\\",
\\"reason\\": {
\\"type\\": \\"script_exception\\",
\\"reason\\": \\"runtime error\\",
\\"script_stack\\": [
\\"return doc['targetfield'].value;\\",
\\" ^---- HERE\\"
],
\\"script\\": \\"return doc['targetfield'].value;\\",
\\"lang\\": \\"painless\\",
\\"caused_by\\": {
\\"type\\": \\"illegal_argument_exception\\",
\\"reason\\": \\"Gimme reason\\"
}
}
}
]
}
}"
>
<Component />
</EuiCopy>
<EuiButton
color="primary"
data-test-subj="closeShardFailureModal"
fill={true}
onClick={[Function]}
size="m"
>
<FormattedMessage
defaultMessage="Close"
description="Closing the Modal"
id="data.search.searchSource.fetch.shardsFailedModal.close"
values={Object {}}
/>
</EuiButton>
</EuiModalFooter>
</Fragment>
`;

View file

@ -14,13 +14,13 @@ import { shardFailureResponse } from './__mocks__/shard_failure_response';
describe('ShardFailureDescription', () => { describe('ShardFailureDescription', () => {
it('renders matching snapshot given valid properties', () => { it('renders matching snapshot given valid properties', () => {
const failure = (shardFailureResponse._shards as any).failures[0]; const failure = shardFailureResponse._shards.failures![0];
const component = shallowWithIntl(<ShardFailureDescription {...failure} />); const component = shallowWithIntl(<ShardFailureDescription {...failure} />);
expect(component).toMatchSnapshot(); expect(component).toMatchSnapshot();
}); });
it('should show more details when button is pressed', async () => { it('should show more details when button is pressed', async () => {
const failure = (shardFailureResponse._shards as any).failures[0]; const failure = shardFailureResponse._shards.failures![0];
const component = shallowWithIntl(<ShardFailureDescription {...failure} />); const component = shallowWithIntl(<ShardFailureDescription {...failure} />);
await component.find(EuiButtonEmpty).simulate('click'); await component.find(EuiButtonEmpty).simulate('click');
expect(component).toMatchSnapshot(); expect(component).toMatchSnapshot();

View file

@ -7,6 +7,7 @@
*/ */
import React, { useState } from 'react'; import React, { useState } from 'react';
import { estypes } from '@elastic/elasticsearch';
import { i18n } from '@kbn/i18n'; import { i18n } from '@kbn/i18n';
import { css } from '@emotion/react'; import { css } from '@emotion/react';
import { getFlattenedObject } from '@kbn/std'; import { getFlattenedObject } from '@kbn/std';
@ -17,7 +18,6 @@ import {
EuiFlexGroup, EuiFlexGroup,
EuiFlexItem, EuiFlexItem,
} from '@elastic/eui'; } from '@elastic/eui';
import { ShardFailure } from './shard_failure_types';
/** /**
* Provides pretty formatting of a given key string * Provides pretty formatting of a given key string
@ -47,7 +47,7 @@ export function formatValueByKey(value: unknown, key: string): string | JSX.Elem
} }
} }
export function ShardFailureDescription(props: ShardFailure) { export function ShardFailureDescription(props: estypes.ShardFailure) {
const [showDetails, setShowDetails] = useState<boolean>(false); const [showDetails, setShowDetails] = useState<boolean>(false);
const flattendReason = getFlattenedObject(props.reason); const flattendReason = getFlattenedObject(props.reason);
@ -70,7 +70,7 @@ export function ShardFailureDescription(props: ShardFailure) {
title: i18n.translate('data.search.searchSource.fetch.shardsFailedModal.indexTitle', { title: i18n.translate('data.search.searchSource.fetch.shardsFailedModal.indexTitle', {
defaultMessage: 'Index', defaultMessage: 'Index',
}), }),
description: props.index, description: props.index ?? '',
}, },
{ {
title: i18n.translate('data.search.searchSource.fetch.shardsFailedModal.reasonTypeTitle', { title: i18n.translate('data.search.searchSource.fetch.shardsFailedModal.reasonTypeTitle', {
@ -84,7 +84,7 @@ export function ShardFailureDescription(props: ShardFailure) {
title: i18n.translate('data.search.searchSource.fetch.shardsFailedModal.nodeTitle', { title: i18n.translate('data.search.searchSource.fetch.shardsFailedModal.nodeTitle', {
defaultMessage: 'Node', defaultMessage: 'Node',
}), }),
description: props.node, description: props.node ?? '',
}, },
...reasonItems, ...reasonItems,
] ]
@ -99,7 +99,6 @@ export function ShardFailureDescription(props: ShardFailure) {
columnWidths={[1, 6]} columnWidths={[1, 6]}
listItems={items} listItems={items}
compressed compressed
className="shardFailureModal__desc"
titleProps={{ className: 'shardFailureModal__descTitle' }} titleProps={{ className: 'shardFailureModal__descTitle' }}
descriptionProps={{ className: 'shardFailureModal__descValue' }} descriptionProps={{ className: 'shardFailureModal__descValue' }}
/> />

View file

@ -1,28 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import React from 'react';
import { shallowWithIntl } from '@kbn/test-jest-helpers';
import { ShardFailureModal } from './shard_failure_modal';
import { shardFailureRequest } from './__mocks__/shard_failure_request';
import { shardFailureResponse } from './__mocks__/shard_failure_response';
describe('ShardFailureModal', () => {
it('renders matching snapshot given valid properties', () => {
const component = shallowWithIntl(
<ShardFailureModal
title="test"
request={shardFailureRequest}
response={shardFailureResponse}
onClose={jest.fn()}
/>
);
expect(component).toMatchSnapshot();
});
});

View file

@ -1,125 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import React from 'react';
import { FormattedMessage } from '@kbn/i18n-react';
import { i18n } from '@kbn/i18n';
import {
EuiCodeBlock,
EuiTabbedContent,
EuiCopy,
EuiButton,
EuiModalBody,
EuiModalHeader,
EuiModalHeaderTitle,
EuiModalFooter,
EuiButtonEmpty,
EuiCallOut,
} from '@elastic/eui';
import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import { ShardFailureTable } from './shard_failure_table';
import { ShardFailureRequest } from './shard_failure_types';
export interface Props {
onClose: () => void;
request: ShardFailureRequest;
response: estypes.SearchResponse<any>;
title: string;
}
export function ShardFailureModal({ request, response, title, onClose }: Props) {
if (
!response ||
!response._shards ||
!Array.isArray((response._shards as any).failures) ||
!request
) {
// this should never ever happen, but just in case
return (
<EuiCallOut title="Sorry, there was an error" color="danger" iconType="warning">
The ShardFailureModal component received invalid properties
</EuiCallOut>
);
}
const failures = (response._shards as any).failures;
const requestJSON = JSON.stringify(request, null, 2);
const responseJSON = JSON.stringify(response, null, 2);
const tabs = [
{
id: 'table',
name: i18n.translate(
'data.search.searchSource.fetch.shardsFailedModal.tabHeaderShardFailures',
{
defaultMessage: 'Shard failures',
description: 'Name of the tab displaying shard failures',
}
),
content: <ShardFailureTable failures={failures} />,
['data-test-subj']: 'shardFailuresModalShardButton',
},
{
id: 'json-request',
name: i18n.translate('data.search.searchSource.fetch.shardsFailedModal.tabHeaderRequest', {
defaultMessage: 'Request',
description: 'Name of the tab displaying the JSON request',
}),
content: (
<EuiCodeBlock language="json" isCopyable data-test-subj="shardsFailedModalRequestBlock">
{requestJSON}
</EuiCodeBlock>
),
['data-test-subj']: 'shardFailuresModalRequestButton',
},
{
id: 'json-response',
name: i18n.translate('data.search.searchSource.fetch.shardsFailedModal.tabHeaderResponse', {
defaultMessage: 'Response',
description: 'Name of the tab displaying the JSON response',
}),
content: (
<EuiCodeBlock language="json" isCopyable data-test-subj="shardsFailedModalResponseBlock">
{responseJSON}
</EuiCodeBlock>
),
['data-test-subj']: 'shardFailuresModalResponseButton',
},
];
return (
<React.Fragment>
<EuiModalHeader>
<EuiModalHeaderTitle data-test-subj="shardFailureModalTitle" size="xs">
{title}
</EuiModalHeaderTitle>
</EuiModalHeader>
<EuiModalBody>
<EuiTabbedContent tabs={tabs} initialSelectedTab={tabs[0]} autoFocus="selected" />
</EuiModalBody>
<EuiModalFooter>
<EuiCopy textToCopy={responseJSON}>
{(copy) => (
<EuiButtonEmpty onClick={copy}>
<FormattedMessage
id="data.search.searchSource.fetch.shardsFailedModal.copyToClipboard"
defaultMessage="Copy response to clipboard"
/>
</EuiButtonEmpty>
)}
</EuiCopy>
<EuiButton onClick={() => onClose()} fill data-test-subj="closeShardFailureModal">
<FormattedMessage
id="data.search.searchSource.fetch.shardsFailedModal.close"
defaultMessage="Close"
description="Closing the Modal"
/>
</EuiButton>
</EuiModalFooter>
</React.Fragment>
);
}

View file

@ -1,16 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { setOverlays } from '../services';
import { OverlayStart } from '@kbn/core/public';
export const openModal = jest.fn();
setOverlays({
openModal,
} as unknown as OverlayStart);

View file

@ -1,35 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { openModal } from './shard_failure_open_modal_button.test.mocks';
import React from 'react';
import { themeServiceMock } from '@kbn/core/public/mocks';
import { mountWithIntl } from '@kbn/test-jest-helpers';
import ShardFailureOpenModalButton from './shard_failure_open_modal_button';
import { shardFailureRequest } from './__mocks__/shard_failure_request';
import { shardFailureResponse } from './__mocks__/shard_failure_response';
import { findTestSubject } from '@elastic/eui/lib/test';
const theme = themeServiceMock.createStartContract();
describe('ShardFailureOpenModalButton', () => {
it('triggers the openModal function when "Show details" button is clicked', () => {
const component = mountWithIntl(
<ShardFailureOpenModalButton
getRequestMeta={() => ({
request: shardFailureRequest,
response: shardFailureResponse,
})}
theme={theme}
title="test"
/>
);
findTestSubject(component, 'openShardFailureModalBtn').simulate('click');
expect(openModal).toHaveBeenCalled();
});
});

View file

@ -10,12 +10,12 @@ import React from 'react';
import { shallowWithIntl } from '@kbn/test-jest-helpers'; import { shallowWithIntl } from '@kbn/test-jest-helpers';
import { ShardFailureTable } from './shard_failure_table'; import { ShardFailureTable } from './shard_failure_table';
import { shardFailureResponse } from './__mocks__/shard_failure_response'; import { shardFailureResponse } from './__mocks__/shard_failure_response';
import { ShardFailure } from './shard_failure_types';
describe('ShardFailureTable', () => { describe('ShardFailureTable', () => {
it('renders matching snapshot given valid properties', () => { it('renders matching snapshot given valid properties', () => {
const failures = (shardFailureResponse._shards as any).failures as ShardFailure[]; const component = shallowWithIntl(
const component = shallowWithIntl(<ShardFailureTable failures={failures} />); <ShardFailureTable failures={shardFailureResponse._shards.failures!} />
);
expect(component).toMatchSnapshot(); expect(component).toMatchSnapshot();
}); });
}); });

View file

@ -7,13 +7,13 @@
*/ */
import React from 'react'; import React from 'react';
import { estypes } from '@elastic/elasticsearch';
import { i18n } from '@kbn/i18n'; import { i18n } from '@kbn/i18n';
import { css } from '@emotion/react'; import { css } from '@emotion/react';
import { EuiInMemoryTable, EuiInMemoryTableProps, euiScreenReaderOnly } from '@elastic/eui'; import { EuiInMemoryTable, EuiInMemoryTableProps, euiScreenReaderOnly } from '@elastic/eui';
import { ShardFailureDescription } from './shard_failure_description'; import { ShardFailureDescription } from './shard_failure_description';
import { ShardFailure } from './shard_failure_types';
export interface ListItem extends ShardFailure { export interface ListItem extends estypes.ShardFailure {
id: string; id: string;
} }
@ -24,7 +24,7 @@ const SORTING: EuiInMemoryTableProps<ListItem>['sorting'] = {
}, },
}; };
export function ShardFailureTable({ failures }: { failures: ShardFailure[] }) { export function ShardFailureTable({ failures }: { failures: estypes.ShardFailure[] }) {
const itemList = failures.map((failure, idx) => ({ ...{ id: String(idx) }, ...failure })); const itemList = failures.map((failure, idx) => ({ ...{ id: String(idx) }, ...failure }));
const columns = [ const columns = [

View file

@ -1,33 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
export interface ShardFailureRequest {
docvalue_fields: string[];
_source: unknown;
query: unknown;
script_fields: unknown;
sort: unknown;
stored_fields: string[];
}
export interface ShardFailure {
index: string;
node: string;
reason: {
caused_by: {
reason: string;
type: string;
};
reason: string;
lang?: estypes.ScriptLanguage;
script?: string;
script_stack?: string[];
type: string;
};
shard: number;
}

View file

@ -131,7 +131,7 @@ describe('esaggs expression function - server', () => {
query: undefined, query: undefined,
searchSessionId: 'abc123', searchSessionId: 'abc123',
searchSourceService: startDependencies.searchSource, searchSourceService: startDependencies.searchSource,
disableShardWarnings: false, disableWarningToasts: false,
timeFields: args.timeFields, timeFields: args.timeFields,
timeRange: undefined, timeRange: undefined,
}); });

View file

@ -72,7 +72,7 @@ export function getFunctionDefinition({
query: get(input, 'query', undefined) as any, query: get(input, 'query', undefined) as any,
searchSessionId: getSearchSessionId(), searchSessionId: getSearchSessionId(),
searchSourceService: searchSource, searchSourceService: searchSource,
disableShardWarnings: false, disableWarningToasts: false,
timeFields: args.timeFields, timeFields: args.timeFields,
timeRange: get(input, 'timeRange', undefined), timeRange: get(input, 'timeRange', undefined),
}) })

View file

@ -49,7 +49,8 @@
"@kbn/core-saved-objects-server", "@kbn/core-saved-objects-server",
"@kbn/core-saved-objects-utils-server", "@kbn/core-saved-objects-utils-server",
"@kbn/data-service", "@kbn/data-service",
"@kbn/react-kibana-context-render" "@kbn/react-kibana-context-render",
"@kbn/es-types"
], ],
"exclude": [ "exclude": [
"target/**/*", "target/**/*",

View file

@ -17,7 +17,6 @@ export enum VIEW_MODE {
AGGREGATED_LEVEL = 'aggregated', AGGREGATED_LEVEL = 'aggregated',
} }
export const DISABLE_SHARD_FAILURE_WARNING = true;
export const getDefaultRowsPerPage = (uiSettings: IUiSettingsClient): number => { export const getDefaultRowsPerPage = (uiSettings: IUiSettingsClient): number => {
return parseInt(uiSettings.get(SAMPLE_ROWS_PER_PAGE_SETTING), 10) || DEFAULT_ROWS_PER_PAGE; return parseInt(uiSettings.get(SAMPLE_ROWS_PER_PAGE_SETTING), 10) || DEFAULT_ROWS_PER_PAGE;
}; };

View file

@ -18,7 +18,6 @@ import { useExecutionContext } from '@kbn/kibana-react-plugin/public';
import { generateFilters } from '@kbn/data-plugin/public'; import { generateFilters } from '@kbn/data-plugin/public';
import { i18n } from '@kbn/i18n'; import { i18n } from '@kbn/i18n';
import { reportPerformanceMetricEvent } from '@kbn/ebt-tools'; import { reportPerformanceMetricEvent } from '@kbn/ebt-tools';
import { removeInterceptedWarningDuplicates } from '@kbn/search-response-warnings';
import { import {
DOC_TABLE_LEGACY, DOC_TABLE_LEGACY,
SEARCH_FIELDS_FROM_SOURCE, SEARCH_FIELDS_FROM_SOURCE,
@ -177,12 +176,11 @@ export const ContextApp = ({ dataView, anchorId, referrer }: ContextAppProps) =>
); );
const interceptedWarnings = useMemo( const interceptedWarnings = useMemo(
() => () => [
removeInterceptedWarningDuplicates([ ...(fetchedState.predecessorsInterceptedWarnings || []),
...(fetchedState.predecessorsInterceptedWarnings || []), ...(fetchedState.anchorInterceptedWarnings || []),
...(fetchedState.anchorInterceptedWarnings || []), ...(fetchedState.successorsInterceptedWarnings || []),
...(fetchedState.successorsInterceptedWarnings || []), ],
]),
[ [
fetchedState.predecessorsInterceptedWarnings, fetchedState.predecessorsInterceptedWarnings,
fetchedState.anchorInterceptedWarnings, fetchedState.anchorInterceptedWarnings,

View file

@ -19,15 +19,15 @@ import {
mockSuccessorHits, mockSuccessorHits,
} from '../__mocks__/use_context_app_fetch'; } from '../__mocks__/use_context_app_fetch';
import { dataViewWithTimefieldMock } from '../../../__mocks__/data_view_with_timefield'; import { dataViewWithTimefieldMock } from '../../../__mocks__/data_view_with_timefield';
import { searchResponseWarningsMock } from '@kbn/search-response-warnings/src/__mocks__/search_response_warnings'; import { searchResponseIncompleteWarningLocalCluster } from '@kbn/search-response-warnings/src/__mocks__/search_response_warnings';
import { createContextSearchSourceStub } from '../services/_stubs'; import { createContextSearchSourceStub } from '../services/_stubs';
import { DataView } from '@kbn/data-views-plugin/public'; import { DataView } from '@kbn/data-views-plugin/public';
import { themeServiceMock } from '@kbn/core/public/mocks'; import { themeServiceMock } from '@kbn/core/public/mocks';
import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public';
const mockInterceptedWarnings = searchResponseWarningsMock.map((originalWarning) => ({ const mockInterceptedWarning = {
originalWarning, originalWarning: searchResponseIncompleteWarningLocalCluster,
})); };
const mockFilterManager = createFilterManagerMock(); const mockFilterManager = createFilterManagerMock();
@ -44,9 +44,7 @@ jest.mock('../services/context', () => {
} }
return { return {
rows: type === 'predecessors' ? mockPredecessorHits : mockSuccessorHits, rows: type === 'predecessors' ? mockPredecessorHits : mockSuccessorHits,
interceptedWarnings: mockOverrideInterceptedWarnings interceptedWarnings: mockOverrideInterceptedWarnings ? [mockInterceptedWarning] : undefined,
? [mockInterceptedWarnings[type === 'predecessors' ? 0 : 1]]
: undefined,
}; };
}, },
}; };
@ -59,9 +57,7 @@ jest.mock('../services/anchor', () => ({
} }
return { return {
anchorRow: mockAnchorHit, anchorRow: mockAnchorHit,
interceptedWarnings: mockOverrideInterceptedWarnings interceptedWarnings: mockOverrideInterceptedWarnings ? [mockInterceptedWarning] : undefined,
? [mockInterceptedWarnings[2]]
: undefined,
}; };
}, },
})); }));
@ -228,13 +224,11 @@ describe('test useContextAppFetch', () => {
expect(result.current.fetchedState.predecessors).toEqual(mockPredecessorHits); expect(result.current.fetchedState.predecessors).toEqual(mockPredecessorHits);
expect(result.current.fetchedState.successors).toEqual(mockSuccessorHits); expect(result.current.fetchedState.successors).toEqual(mockSuccessorHits);
expect(result.current.fetchedState.predecessorsInterceptedWarnings).toEqual([ expect(result.current.fetchedState.predecessorsInterceptedWarnings).toEqual([
mockInterceptedWarnings[0], mockInterceptedWarning,
]); ]);
expect(result.current.fetchedState.successorsInterceptedWarnings).toEqual([ expect(result.current.fetchedState.successorsInterceptedWarnings).toEqual([
mockInterceptedWarnings[1], mockInterceptedWarning,
]);
expect(result.current.fetchedState.anchorInterceptedWarnings).toEqual([
mockInterceptedWarnings[2],
]); ]);
expect(result.current.fetchedState.anchorInterceptedWarnings).toEqual([mockInterceptedWarning]);
}); });
}); });

View file

@ -10,7 +10,7 @@ import { SortDirection } from '@kbn/data-plugin/public';
import { createSearchSourceStub } from './_stubs'; import { createSearchSourceStub } from './_stubs';
import { fetchAnchor, updateSearchSource } from './anchor'; import { fetchAnchor, updateSearchSource } from './anchor';
import { dataViewMock } from '@kbn/discover-utils/src/__mocks__'; import { dataViewMock } from '@kbn/discover-utils/src/__mocks__';
import { searchResponseTimeoutWarningMock } from '@kbn/search-response-warnings/src/__mocks__/search_response_warnings'; import { searchResponseIncompleteWarningLocalCluster } from '@kbn/search-response-warnings/src/__mocks__/search_response_warnings';
import { savedSearchMock } from '../../../__mocks__/saved_search'; import { savedSearchMock } from '../../../__mocks__/saved_search';
import { discoverServiceMock } from '../../../__mocks__/services'; import { discoverServiceMock } from '../../../__mocks__/services';
@ -206,7 +206,7 @@ describe('context app', function () {
).then(({ anchorRow, interceptedWarnings }) => { ).then(({ anchorRow, interceptedWarnings }) => {
expect(anchorRow).toHaveProperty('raw._id', '1'); expect(anchorRow).toHaveProperty('raw._id', '1');
expect(anchorRow).toHaveProperty('isAnchor', true); expect(anchorRow).toHaveProperty('isAnchor', true);
expect(interceptedWarnings).toBeUndefined(); expect(interceptedWarnings).toEqual([]);
}); });
}); });
@ -216,20 +216,10 @@ describe('context app', function () {
{ _id: '3', _index: 't' }, { _id: '3', _index: 't' },
]); ]);
const mockWarnings = [
{
originalWarning: searchResponseTimeoutWarningMock,
},
];
const services = discoverServiceMock; const services = discoverServiceMock;
services.data.search.showWarnings = jest.fn((adapter, callback) => { services.data.search.showWarnings = jest.fn((adapter, callback) => {
// @ts-expect-error for empty meta // @ts-expect-error for empty meta
callback?.(mockWarnings[0].originalWarning, {}); callback?.(searchResponseIncompleteWarningLocalCluster, {});
// plus duplicates
// @ts-expect-error for empty meta
callback?.(mockWarnings[0].originalWarning, {});
}); });
return fetchAnchor( return fetchAnchor(
@ -242,7 +232,7 @@ describe('context app', function () {
).then(({ anchorRow, interceptedWarnings }) => { ).then(({ anchorRow, interceptedWarnings }) => {
expect(anchorRow).toHaveProperty('raw._id', '1'); expect(anchorRow).toHaveProperty('raw._id', '1');
expect(anchorRow).toHaveProperty('isAnchor', true); expect(anchorRow).toHaveProperty('isAnchor', true);
expect(interceptedWarnings).toEqual(mockWarnings); expect(interceptedWarnings?.length).toBe(1);
}); });
}); });
}); });

View file

@ -17,7 +17,6 @@ import {
type SearchResponseInterceptedWarning, type SearchResponseInterceptedWarning,
} from '@kbn/search-response-warnings'; } from '@kbn/search-response-warnings';
import type { DiscoverServices } from '../../../build_services'; import type { DiscoverServices } from '../../../build_services';
import { DISABLE_SHARD_FAILURE_WARNING } from '../../../../common/constants';
export async function fetchAnchor( export async function fetchAnchor(
anchorId: string, anchorId: string,
@ -35,7 +34,7 @@ export async function fetchAnchor(
const adapter = new RequestAdapter(); const adapter = new RequestAdapter();
const { rawResponse } = await lastValueFrom( const { rawResponse } = await lastValueFrom(
searchSource.fetch$({ searchSource.fetch$({
disableShardFailureWarning: DISABLE_SHARD_FAILURE_WARNING, disableWarningToasts: true,
inspector: { inspector: {
adapter, adapter,
title: 'anchor', title: 'anchor',
@ -56,9 +55,6 @@ export async function fetchAnchor(
interceptedWarnings: getSearchResponseInterceptedWarnings({ interceptedWarnings: getSearchResponseInterceptedWarnings({
services, services,
adapter, adapter,
options: {
disableShardFailureWarning: DISABLE_SHARD_FAILURE_WARNING,
},
}), }),
}; };
} }

View file

@ -16,6 +16,7 @@ import { Query } from '@kbn/es-query';
import { fetchSurroundingDocs, SurrDocType } from './context'; import { fetchSurroundingDocs, SurrDocType } from './context';
import { buildDataTableRecord, buildDataTableRecordList } from '@kbn/discover-utils'; import { buildDataTableRecord, buildDataTableRecordList } from '@kbn/discover-utils';
import { discoverServiceMock } from '../../../__mocks__/services'; import { discoverServiceMock } from '../../../__mocks__/services';
import { searchResponseIncompleteWarningLocalCluster } from '@kbn/search-response-warnings/src/__mocks__/search_response_warnings';
const MS_PER_DAY = 24 * 60 * 60 * 1000; const MS_PER_DAY = 24 * 60 * 60 * 1000;
const ANCHOR_TIMESTAMP = new Date(MS_PER_DAY).toJSON(); const ANCHOR_TIMESTAMP = new Date(MS_PER_DAY).toJSON();
@ -257,28 +258,13 @@ describe('context successors', function () {
const removeFieldsSpy = mockSearchSource.removeField.withArgs('fieldsFromSource'); const removeFieldsSpy = mockSearchSource.removeField.withArgs('fieldsFromSource');
expect(removeFieldsSpy.calledOnce).toBe(true); expect(removeFieldsSpy.calledOnce).toBe(true);
expect(setFieldsSpy.calledOnce).toBe(true); expect(setFieldsSpy.calledOnce).toBe(true);
expect(interceptedWarnings).toBeUndefined(); expect(interceptedWarnings).toEqual([]);
} }
); );
}); });
}); });
describe('function fetchSuccessors with shard failures', function () { describe('function fetchSuccessors with shard failures', function () {
const mockWarnings = [
{
originalWarning: {
message: 'Data might be incomplete because your request timed out 1',
type: 'timed_out',
},
},
{
originalWarning: {
message: 'Data might be incomplete because your request timed out 2',
type: 'timed_out',
},
},
];
beforeEach(() => { beforeEach(() => {
mockSearchSource = createContextSearchSourceStub('@timestamp'); mockSearchSource = createContextSearchSourceStub('@timestamp');
@ -288,11 +274,7 @@ describe('context successors', function () {
createEmpty: jest.fn().mockImplementation(() => mockSearchSource), createEmpty: jest.fn().mockImplementation(() => mockSearchSource),
}, },
showWarnings: jest.fn((adapter, callback) => { showWarnings: jest.fn((adapter, callback) => {
callback(mockWarnings[0].originalWarning, {}); callback(searchResponseIncompleteWarningLocalCluster, {});
callback(mockWarnings[1].originalWarning, {});
// plus duplicates
callback(mockWarnings[0].originalWarning, {});
callback(mockWarnings[1].originalWarning, {});
}), }),
}, },
} as unknown as DataPublicPluginStart; } as unknown as DataPublicPluginStart;
@ -345,7 +327,7 @@ describe('context successors', function () {
buildDataTableRecordList(mockSearchSource._stubHits.slice(-3), dataView) buildDataTableRecordList(mockSearchSource._stubHits.slice(-3), dataView)
); );
expect(dataPluginMock.search.showWarnings).toHaveBeenCalledTimes(1); expect(dataPluginMock.search.showWarnings).toHaveBeenCalledTimes(1);
expect(interceptedWarnings).toEqual(mockWarnings); expect(interceptedWarnings?.length).toBe(1);
} }
); );
}); });

View file

@ -9,10 +9,7 @@ import type { Filter } from '@kbn/es-query';
import { DataView } from '@kbn/data-views-plugin/public'; import { DataView } from '@kbn/data-views-plugin/public';
import { DataPublicPluginStart, ISearchSource } from '@kbn/data-plugin/public'; import { DataPublicPluginStart, ISearchSource } from '@kbn/data-plugin/public';
import type { DataTableRecord } from '@kbn/discover-utils/types'; import type { DataTableRecord } from '@kbn/discover-utils/types';
import { import type { SearchResponseInterceptedWarning } from '@kbn/search-response-warnings';
removeInterceptedWarningDuplicates,
type SearchResponseInterceptedWarning,
} from '@kbn/search-response-warnings';
import { reverseSortDir, SortDirection } from '../utils/sorting'; import { reverseSortDir, SortDirection } from '../utils/sorting';
import { convertIsoToMillis, extractNanos } from '../utils/date_conversion'; import { convertIsoToMillis, extractNanos } from '../utils/date_conversion';
import { fetchHitsInInterval } from '../utils/fetch_hits_in_interval'; import { fetchHitsInInterval } from '../utils/fetch_hits_in_interval';
@ -126,7 +123,7 @@ export async function fetchSurroundingDocs(
return { return {
rows, rows,
interceptedWarnings: removeInterceptedWarningDuplicates(interceptedWarnings), interceptedWarnings,
}; };
} }

View file

@ -17,7 +17,6 @@ import {
import { RequestAdapter } from '@kbn/inspector-plugin/common'; import { RequestAdapter } from '@kbn/inspector-plugin/common';
import { convertTimeValueToIso } from './date_conversion'; import { convertTimeValueToIso } from './date_conversion';
import { IntervalValue } from './generate_intervals'; import { IntervalValue } from './generate_intervals';
import { DISABLE_SHARD_FAILURE_WARNING } from '../../../../common/constants';
import type { SurrDocType } from '../services/context'; import type { SurrDocType } from '../services/context';
import type { DiscoverServices } from '../../../build_services'; import type { DiscoverServices } from '../../../build_services';
@ -91,7 +90,7 @@ export async function fetchHitsInInterval(
.setField('sort', sort) .setField('sort', sort)
.setField('version', true) .setField('version', true)
.fetch$({ .fetch$({
disableShardFailureWarning: DISABLE_SHARD_FAILURE_WARNING, disableWarningToasts: true,
inspector: { inspector: {
adapter, adapter,
title: type, title: type,
@ -107,9 +106,6 @@ export async function fetchHitsInInterval(
interceptedWarnings: getSearchResponseInterceptedWarnings({ interceptedWarnings: getSearchResponseInterceptedWarnings({
services, services,
adapter, adapter,
options: {
disableShardFailureWarning: DISABLE_SHARD_FAILURE_WARNING,
},
}), }),
}; };
} }

View file

@ -26,7 +26,7 @@ import {
import { filter } from 'rxjs/operators'; import { filter } from 'rxjs/operators';
import { dataViewMock, esHitsMockWithSort } from '@kbn/discover-utils/src/__mocks__'; import { dataViewMock, esHitsMockWithSort } from '@kbn/discover-utils/src/__mocks__';
import { buildDataTableRecord } from '@kbn/discover-utils'; import { buildDataTableRecord } from '@kbn/discover-utils';
import { searchResponseWarningsMock } from '@kbn/search-response-warnings/src/__mocks__/search_response_warnings'; import { searchResponseIncompleteWarningLocalCluster } from '@kbn/search-response-warnings/src/__mocks__/search_response_warnings';
describe('test useSavedSearch message generators', () => { describe('test useSavedSearch message generators', () => {
test('sendCompleteMsg', (done) => { test('sendCompleteMsg', (done) => {
@ -103,15 +103,13 @@ describe('test useSavedSearch message generators', () => {
if (value.fetchStatus !== FetchStatus.LOADING_MORE) { if (value.fetchStatus !== FetchStatus.LOADING_MORE) {
expect(value.fetchStatus).toBe(FetchStatus.COMPLETE); expect(value.fetchStatus).toBe(FetchStatus.COMPLETE);
expect(value.result).toStrictEqual([...initialRecords, ...moreRecords]); expect(value.result).toStrictEqual([...initialRecords, ...moreRecords]);
expect(value.interceptedWarnings).toHaveLength(searchResponseWarningsMock.length); expect(value.interceptedWarnings).toHaveLength(1);
done(); done();
} }
}); });
sendLoadingMoreFinishedMsg(documents$, { sendLoadingMoreFinishedMsg(documents$, {
moreRecords, moreRecords,
interceptedWarnings: searchResponseWarningsMock.map((warning) => ({ interceptedWarnings: [{ originalWarning: searchResponseIncompleteWarningLocalCluster }],
originalWarning: warning,
})),
}); });
}); });
test('sendLoadingMoreFinishedMsg after an exception', (done) => { test('sendLoadingMoreFinishedMsg after an exception', (done) => {
@ -121,9 +119,7 @@ describe('test useSavedSearch message generators', () => {
const documents$ = new BehaviorSubject<DataDocumentsMsg>({ const documents$ = new BehaviorSubject<DataDocumentsMsg>({
fetchStatus: FetchStatus.LOADING_MORE, fetchStatus: FetchStatus.LOADING_MORE,
result: initialRecords, result: initialRecords,
interceptedWarnings: searchResponseWarningsMock.map((warning) => ({ interceptedWarnings: [{ originalWarning: searchResponseIncompleteWarningLocalCluster }],
originalWarning: warning,
})),
}); });
documents$.subscribe((value) => { documents$.subscribe((value) => {
if (value.fetchStatus !== FetchStatus.LOADING_MORE) { if (value.fetchStatus !== FetchStatus.LOADING_MORE) {

View file

@ -25,7 +25,7 @@ import { fetchDocuments } from './fetch_documents';
import { fetchTextBased } from './fetch_text_based'; import { fetchTextBased } from './fetch_text_based';
import { buildDataTableRecord } from '@kbn/discover-utils'; import { buildDataTableRecord } from '@kbn/discover-utils';
import { dataViewMock, esHitsMockWithSort } from '@kbn/discover-utils/src/__mocks__'; import { dataViewMock, esHitsMockWithSort } from '@kbn/discover-utils/src/__mocks__';
import { searchResponseWarningsMock } from '@kbn/search-response-warnings/src/__mocks__/search_response_warnings'; import { searchResponseIncompleteWarningLocalCluster } from '@kbn/search-response-warnings/src/__mocks__/search_response_warnings';
jest.mock('./fetch_documents', () => ({ jest.mock('./fetch_documents', () => ({
fetchDocuments: jest.fn().mockResolvedValue([]), fetchDocuments: jest.fn().mockResolvedValue([]),
@ -296,9 +296,11 @@ describe('test fetchAll', () => {
const initialRecords = [records[0], records[1]]; const initialRecords = [records[0], records[1]];
const moreRecords = [records[2], records[3]]; const moreRecords = [records[2], records[3]];
const interceptedWarnings = searchResponseWarningsMock.map((warning) => ({ const interceptedWarnings = [
originalWarning: warning, {
})); originalWarning: searchResponseIncompleteWarningLocalCluster,
},
];
test('should add more records', async () => { test('should add more records', async () => {
const collectDocuments = subjectCollector(subjects.documents$); const collectDocuments = subjectCollector(subjects.documents$);

View file

@ -37,17 +37,20 @@ describe('test fetchDocuments', () => {
const documents = hits.map((hit) => buildDataTableRecord(hit, dataViewMock)); const documents = hits.map((hit) => buildDataTableRecord(hit, dataViewMock));
savedSearchMock.searchSource.fetch$ = <T>() => savedSearchMock.searchSource.fetch$ = <T>() =>
of({ rawResponse: { hits: { hits } } } as IKibanaSearchResponse<SearchResponse<T>>); of({ rawResponse: { hits: { hits } } } as IKibanaSearchResponse<SearchResponse<T>>);
expect(fetchDocuments(savedSearchMock.searchSource, getDeps())).resolves.toEqual({ expect(await fetchDocuments(savedSearchMock.searchSource, getDeps())).toEqual({
interceptedWarnings: [],
records: documents, records: documents,
}); });
}); });
test('rejects on query failure', () => { test('rejects on query failure', async () => {
savedSearchMock.searchSource.fetch$ = () => throwErrorRx(() => new Error('Oh noes!')); savedSearchMock.searchSource.fetch$ = () => throwErrorRx(() => new Error('Oh noes!'));
expect(fetchDocuments(savedSearchMock.searchSource, getDeps())).rejects.toEqual( try {
new Error('Oh noes!') await fetchDocuments(savedSearchMock.searchSource, getDeps());
); } catch (e) {
expect(e).toEqual(new Error('Oh noes!'));
}
}); });
test('passes a correct session id', async () => { test('passes a correct session id', async () => {
@ -66,7 +69,8 @@ describe('test fetchDocuments', () => {
jest.spyOn(searchSourceRegular, 'fetch$'); jest.spyOn(searchSourceRegular, 'fetch$');
expect(fetchDocuments(searchSourceRegular, deps)).resolves.toEqual({ expect(await fetchDocuments(searchSourceRegular, deps)).toEqual({
interceptedWarnings: [],
records: documents, records: documents,
}); });
@ -84,7 +88,8 @@ describe('test fetchDocuments', () => {
jest.spyOn(searchSourceForLoadMore, 'fetch$'); jest.spyOn(searchSourceForLoadMore, 'fetch$');
expect(fetchDocuments(searchSourceForLoadMore, deps)).resolves.toEqual({ expect(await fetchDocuments(searchSourceForLoadMore, deps)).toEqual({
interceptedWarnings: [],
records: documents, records: documents,
}); });

View file

@ -13,7 +13,6 @@ import { SAMPLE_SIZE_SETTING, buildDataTableRecordList } from '@kbn/discover-uti
import type { EsHitRecord } from '@kbn/discover-utils/types'; import type { EsHitRecord } from '@kbn/discover-utils/types';
import { getSearchResponseInterceptedWarnings } from '@kbn/search-response-warnings'; import { getSearchResponseInterceptedWarnings } from '@kbn/search-response-warnings';
import type { RecordsFetchResponse } from '../../types'; import type { RecordsFetchResponse } from '../../types';
import { DISABLE_SHARD_FAILURE_WARNING } from '../../../../common/constants';
import { FetchDeps } from './fetch_all'; import { FetchDeps } from './fetch_all';
/** /**
@ -60,7 +59,7 @@ export const fetchDocuments = (
}), }),
}, },
executionContext, executionContext,
disableShardFailureWarning: DISABLE_SHARD_FAILURE_WARNING, disableWarningToasts: true,
}) })
.pipe( .pipe(
filter((res) => isCompleteResponse(res)), filter((res) => isCompleteResponse(res)),
@ -75,9 +74,6 @@ export const fetchDocuments = (
? getSearchResponseInterceptedWarnings({ ? getSearchResponseInterceptedWarnings({
services, services,
adapter, adapter,
options: {
disableShardFailureWarning: DISABLE_SHARD_FAILURE_WARNING,
},
}) })
: []; : [];

View file

@ -41,7 +41,7 @@ export const ErrorCallout = ({
const { euiTheme } = useEuiTheme(); const { euiTheme } = useEuiTheme();
const showErrorMessage = i18n.translate('discover.errorCalloutShowErrorMessage', { const showErrorMessage = i18n.translate('discover.errorCalloutShowErrorMessage', {
defaultMessage: 'Show details', defaultMessage: 'View details',
}); });
const overrideDisplay = getSearchErrorOverrideDisplay({ const overrideDisplay = getSearchErrorOverrideDisplay({

View file

@ -62,11 +62,7 @@ import {
import type { UnifiedDataTableProps } from '@kbn/unified-data-table'; import type { UnifiedDataTableProps } from '@kbn/unified-data-table';
import type { UnifiedDataTableSettings } from '@kbn/unified-data-table'; import type { UnifiedDataTableSettings } from '@kbn/unified-data-table';
import { columnActions } from '@kbn/unified-data-table'; import { columnActions } from '@kbn/unified-data-table';
import { import { VIEW_MODE, getDefaultRowsPerPage } from '../../common/constants';
VIEW_MODE,
DISABLE_SHARD_FAILURE_WARNING,
getDefaultRowsPerPage,
} from '../../common/constants';
import type { ISearchEmbeddable, SearchInput, SearchOutput } from './types'; import type { ISearchEmbeddable, SearchInput, SearchOutput } from './types';
import type { DiscoverServices } from '../build_services'; import type { DiscoverServices } from '../build_services';
import { getSortForEmbeddable, SortPair } from '../utils/sorting'; import { getSortForEmbeddable, SortPair } from '../utils/sorting';
@ -371,7 +367,7 @@ export class SavedSearchEmbeddable
}), }),
}, },
executionContext, executionContext,
disableShardFailureWarning: DISABLE_SHARD_FAILURE_WARNING, disableWarningToasts: true,
}) })
); );
@ -379,9 +375,6 @@ export class SavedSearchEmbeddable
searchProps.interceptedWarnings = getSearchResponseInterceptedWarnings({ searchProps.interceptedWarnings = getSearchResponseInterceptedWarnings({
services: this.services, services: this.services,
adapter: this.inspectorAdapters.requests, adapter: this.inspectorAdapters.requests,
options: {
disableShardFailureWarning: DISABLE_SHARD_FAILURE_WARNING,
},
}); });
} }

View file

@ -206,7 +206,7 @@ const fetchTotalHitsSearchSource = async ({
executionContext: { executionContext: {
description: 'fetch total hits', description: 'fetch total hits',
}, },
disableShardFailureWarning: true, // TODO: show warnings as a badge next to total hits number disableWarningToasts: true, // TODO: show warnings as a badge next to total hits number
}) })
.pipe( .pipe(
filter((res) => isCompleteResponse(res)), filter((res) => isCompleteResponse(res)),

View file

@ -19,6 +19,7 @@ import { KibanaThemeProvider } from '@kbn/kibana-react-plugin/public';
import { TimefilterContract } from '@kbn/data-plugin/public'; import { TimefilterContract } from '@kbn/data-plugin/public';
import type { DataView } from '@kbn/data-views-plugin/public'; import type { DataView } from '@kbn/data-views-plugin/public';
import { Warnings } from '@kbn/charts-plugin/public'; import { Warnings } from '@kbn/charts-plugin/public';
import { hasUnsupportedDownsampledAggregationFailure } from '@kbn/search-response-warnings';
import { import {
Adapters, Adapters,
AttributeService, AttributeService,
@ -351,11 +352,13 @@ export class VisualizeEmbeddable
this.deps this.deps
.start() .start()
.plugins.data.search.showWarnings(this.getInspectorAdapters()!.requests!, (warning) => { .plugins.data.search.showWarnings(this.getInspectorAdapters()!.requests!, (warning) => {
if ( if (hasUnsupportedDownsampledAggregationFailure(warning)) {
warning.type === 'shard_failure' && warnings.push(
warning.reason.type === 'unsupported_aggregation_on_downsampled_index' i18n.translate('visualizations.embeddable.tsdbRollupWarning', {
) { defaultMessage:
warnings.push(warning.reason.reason || warning.message); 'Visualization uses a function that is unsupported by rolled up data. Select a different function or change the time range.',
})
);
return true; return true;
} }
if (this.vis.type.suppressWarnings?.()) { if (this.vis.type.suppressWarnings?.()) {
@ -582,7 +585,7 @@ export class VisualizeEmbeddable
timeRange: this.timeRange, timeRange: this.timeRange,
query: this.input.query, query: this.input.query,
filters: this.input.filters, filters: this.input.filters,
disableShardWarnings: true, disableWarningToasts: true,
}, },
variables: { variables: {
embeddableTitle: this.getTitle(), embeddableTitle: this.getTitle(),

View file

@ -63,7 +63,8 @@
"@kbn/content-management-table-list-view", "@kbn/content-management-table-list-view",
"@kbn/content-management-utils", "@kbn/content-management-utils",
"@kbn/serverless", "@kbn/serverless",
"@kbn/no-data-page-plugin" "@kbn/no-data-page-plugin",
"@kbn/search-response-warnings"
], ],
"exclude": [ "exclude": [
"target/**/*", "target/**/*",

View file

@ -100,7 +100,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await PageObjects.common.clearAllToasts(); await PageObjects.common.clearAllToasts();
}); });
it('shows shard failure warning notifications by default', async () => { it('should show search warnings as toasts', async () => {
await testSubjects.click('searchSourceWithOther'); await testSubjects.click('searchSourceWithOther');
// wait for response - toasts appear before the response is rendered // wait for response - toasts appear before the response is rendered
@ -113,30 +113,25 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
// toasts // toasts
const toasts = await find.allByCssSelector(toastsSelector); const toasts = await find.allByCssSelector(toastsSelector);
expect(toasts.length).to.be(2); expect(toasts.length).to.be(2);
const expects = ['2 of 4 shards failed', 'Query result']; const expects = ['The data might be incomplete or wrong.', 'Query result'];
await asyncForEach(toasts, async (t, index) => { await asyncForEach(toasts, async (t, index) => {
expect(await t.getVisibleText()).to.eql(expects[index]); expect(await t.getVisibleText()).to.eql(expects[index]);
}); });
// click "see full error" button in the toast // click "see full error" button in the toast
const [openShardModalButton] = await testSubjects.findAll('openShardFailureModalBtn'); const [openShardModalButton] = await testSubjects.findAll('openIncompleteResultsModalBtn');
await openShardModalButton.click(); await openShardModalButton.click();
await retry.waitFor('modal title visible', async () => {
const modalHeader = await testSubjects.find('shardFailureModalTitle');
return (await modalHeader.getVisibleText()) === '2 of 4 shards failed';
});
// request // request
await testSubjects.click('shardFailuresModalRequestButton'); await testSubjects.click('showRequestButton');
const requestBlock = await testSubjects.find('shardsFailedModalRequestBlock'); const requestBlock = await testSubjects.find('incompleteResultsModalRequestBlock');
expect(await requestBlock.getVisibleText()).to.contain(testRollupField); expect(await requestBlock.getVisibleText()).to.contain(testRollupField);
// response // response
await testSubjects.click('shardFailuresModalResponseButton'); await testSubjects.click('showResponseButton');
const responseBlock = await testSubjects.find('shardsFailedModalResponseBlock'); const responseBlock = await testSubjects.find('incompleteResultsModalResponseBlock');
expect(await responseBlock.getVisibleText()).to.contain(shardFailureReason); expect(await responseBlock.getVisibleText()).to.contain(shardFailureReason);
await testSubjects.click('closeShardFailureModal'); await testSubjects.click('closeIncompleteResultsModal');
// response tab // response tab
assert(response && response._shards.failures); assert(response && response._shards.failures);
@ -154,7 +149,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
expect(warnings).to.eql([]); expect(warnings).to.eql([]);
}); });
it('able to handle shard failure warnings and prevent default notifications', async () => { it('should show search warnings in results tab', async () => {
await testSubjects.click('searchSourceWithoutOther'); await testSubjects.click('searchSourceWithoutOther');
// wait for toasts - toasts appear after the response is rendered // wait for toasts - toasts appear after the response is rendered
@ -163,53 +158,15 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
toasts = await find.allByCssSelector(toastsSelector); toasts = await find.allByCssSelector(toastsSelector);
expect(toasts.length).to.be(2); expect(toasts.length).to.be(2);
}); });
const expects = ['Query result', '2 of 4 shards failed']; const expects = ['The data might be incomplete or wrong.', 'Query result'];
await asyncForEach(toasts, async (t, index) => { await asyncForEach(toasts, async (t, index) => {
expect(await t.getVisibleText()).to.eql(expects[index]); expect(await t.getVisibleText()).to.eql(expects[index]);
}); });
// click "see full error" button in the toast
const [openShardModalButton] = await testSubjects.findAll('openShardFailureModalBtn');
await openShardModalButton.click();
await testSubjects.exists('shardFailureModalTitle');
await retry.waitFor('modal title visible', async () => {
const modalHeader = await testSubjects.find('shardFailureModalTitle');
return (await modalHeader.getVisibleText()) === '2 of 4 shards failed';
});
// request
await testSubjects.click('shardFailuresModalRequestButton');
const requestBlock = await testSubjects.find('shardsFailedModalRequestBlock');
expect(await requestBlock.getVisibleText()).to.contain(testRollupField);
// response
await testSubjects.click('shardFailuresModalResponseButton');
const responseBlock = await testSubjects.find('shardsFailedModalResponseBlock');
expect(await responseBlock.getVisibleText()).to.contain(shardFailureReason);
await testSubjects.click('closeShardFailureModal');
// response tab
const response = await getTestJson('responseTab', 'responseCodeBlock');
expect(response._shards.total).to.be(4);
expect(response._shards.successful).to.be(2);
expect(response._shards.skipped).to.be(0);
expect(response._shards.failed).to.be(2);
expect(response._shards.failures.length).to.equal(1);
expect(response._shards.failures[0].index).to.equal(testRollupIndex);
expect(response._shards.failures[0].reason.type).to.equal(shardFailureType);
expect(response._shards.failures[0].reason.reason).to.equal(shardFailureReason);
// warnings tab // warnings tab
const warnings = await getTestJson('warningsTab', 'warningsCodeBlock'); const warnings = await getTestJson('warningsTab', 'warningsCodeBlock');
expect(warnings).to.eql([ expect(warnings.length).to.be(1);
{ expect(warnings[0].type).to.be('incomplete');
type: 'shard_failure',
message: '2 of 4 shards failed',
reason: { reason: shardFailureReason, type: shardFailureType },
text: 'The data might be incomplete or wrong.',
},
]);
}); });
}); });
} }

View file

@ -64,7 +64,7 @@ import {
import { import {
getFiltersInLayer, getFiltersInLayer,
getShardFailuresWarningMessages, getSearchWarningMessages,
getVisualDefaultsForLayer, getVisualDefaultsForLayer,
isColumnInvalid, isColumnInvalid,
cloneLayer, cloneLayer,
@ -811,7 +811,7 @@ export function getFormBasedDatasource({
}, },
getSearchWarningMessages: (state, warning, request, response) => { getSearchWarningMessages: (state, warning, request, response) => {
return [...getShardFailuresWarningMessages(state, warning, request, response, core.theme)]; return [...getSearchWarningMessages(state, warning, request, response, core.theme)];
}, },
checkIntegrity: (state, indexPatterns) => { checkIntegrity: (state, indexPatterns) => {

View file

@ -9,6 +9,7 @@ import React from 'react';
import { i18n } from '@kbn/i18n'; import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react'; import { FormattedMessage } from '@kbn/i18n-react';
import type { DocLinksStart, ThemeServiceStart } from '@kbn/core/public'; import type { DocLinksStart, ThemeServiceStart } from '@kbn/core/public';
import { hasUnsupportedDownsampledAggregationFailure } from '@kbn/search-response-warnings';
import type { DatatableUtilitiesService } from '@kbn/data-plugin/common'; import type { DatatableUtilitiesService } from '@kbn/data-plugin/common';
import { TimeRange } from '@kbn/es-query'; import { TimeRange } from '@kbn/es-query';
import { EuiLink, EuiSpacer, EuiText } from '@elastic/eui'; import { EuiLink, EuiSpacer, EuiText } from '@elastic/eui';
@ -18,11 +19,7 @@ import { groupBy, escape, uniq, uniqBy } from 'lodash';
import type { Query } from '@kbn/data-plugin/common'; import type { Query } from '@kbn/data-plugin/common';
import { SearchRequest } from '@kbn/data-plugin/common'; import { SearchRequest } from '@kbn/data-plugin/common';
import { import { SearchResponseWarning, OpenIncompleteResultsModalButton } from '@kbn/data-plugin/public';
SearchResponseWarning,
ShardFailureOpenModalButton,
ShardFailureRequest,
} from '@kbn/data-plugin/public';
import { estypes } from '@elastic/elasticsearch'; import { estypes } from '@elastic/elasticsearch';
import { isQueryValid } from '@kbn/visualization-ui-components'; import { isQueryValid } from '@kbn/visualization-ui-components';
@ -260,7 +257,7 @@ const accuracyModeEnabledWarning = (
), ),
}); });
export function getShardFailuresWarningMessages( export function getSearchWarningMessages(
state: FormBasedPersistedState, state: FormBasedPersistedState,
warning: SearchResponseWarning, warning: SearchResponseWarning,
request: SearchRequest, request: SearchRequest,
@ -268,10 +265,9 @@ export function getShardFailuresWarningMessages(
theme: ThemeServiceStart theme: ThemeServiceStart
): UserMessage[] { ): UserMessage[] {
if (state) { if (state) {
if (warning.type === 'shard_failure') { if (warning.type === 'incomplete') {
switch (warning.reason.type) { return hasUnsupportedDownsampledAggregationFailure(warning)
case 'unsupported_aggregation_on_downsampled_index': ? Object.values(state.layers).flatMap((layer) =>
return Object.values(state.layers).flatMap((layer) =>
uniq( uniq(
Object.values(layer.columns) Object.values(layer.columns)
.filter((col) => .filter((col) =>
@ -302,40 +298,33 @@ export function getShardFailuresWarningMessages(
}), }),
} as UserMessage) } as UserMessage)
) )
); )
default: : [
return [
{ {
uniqueId: `shard_failure`, uniqueId: `incomplete`,
severity: 'warning', severity: 'warning',
fixableInEditor: true, fixableInEditor: true,
displayLocations: [{ id: 'toolbar' }, { id: 'embeddableBadge' }], displayLocations: [{ id: 'toolbar' }, { id: 'embeddableBadge' }],
shortMessage: '', shortMessage: '',
longMessage: ( longMessage: (
<> <>
<EuiText size="s"> <EuiText size="s">{warning.message}</EuiText>
<strong>{warning.message}</strong>
<p>{warning.text}</p>
</EuiText>
<EuiSpacer size="s" /> <EuiSpacer size="s" />
{warning.text ? ( <OpenIncompleteResultsModalButton
<ShardFailureOpenModalButton theme={theme}
theme={theme} warning={warning}
title={warning.message} size="m"
size="m" getRequestMeta={() => ({
getRequestMeta={() => ({ request,
request: request as ShardFailureRequest, response,
response, })}
})} color="primary"
color="primary" isButtonEmpty={true}
isButtonEmpty={true} />
/>
) : null}
</> </>
), ),
} as UserMessage, } as UserMessage,
]; ];
}
} }
} }
return []; return [];

View file

@ -729,7 +729,7 @@ export const VisualizationWrapper = ({
to: context.dateRange.toDate, to: context.dateRange.toDate,
}, },
filters: context.filters, filters: context.filters,
disableShardWarnings: true, disableWarningToasts: true,
}), }),
[context] [context]
); );

View file

@ -1196,7 +1196,7 @@ export class Embeddable
this.savedVis.state.filters, this.savedVis.state.filters,
this.savedVis.references this.savedVis.references
), ),
disableShardWarnings: true, disableWarningToasts: true,
}; };
if (input.query) { if (input.query) {

View file

@ -62,7 +62,7 @@ export const selectExecutionContextSearch = createSelector(selectExecutionContex
to: res.dateRange.toDate, to: res.dateRange.toDate,
}, },
filters: res.filters, filters: res.filters,
disableShardWarnings: true, disableWarningToasts: true,
})); }));
const selectInjectedDependencies = (_state: LensState, dependencies: unknown) => dependencies; const selectInjectedDependencies = (_state: LensState, dependencies: unknown) => dependencies;

View file

@ -349,29 +349,26 @@ export const getSearchWarningMessages = (
searchService: ISearchStart; searchService: ISearchStart;
} }
): UserMessage[] => { ): UserMessage[] => {
const warningsMap: Map<string, UserMessage[]> = new Map(); const userMessages: UserMessage[] = [];
deps.searchService.showWarnings(adapter, (warning, meta) => { deps.searchService.showWarnings(adapter, (warning, meta) => {
const { request, response, requestId } = meta; const { request, response } = meta;
const warningMessages = datasource.getSearchWarningMessages?.( const userMessagesFromWarning = datasource.getSearchWarningMessages?.(
state, state,
warning, warning,
request, request,
response response
); );
if (warningMessages?.length) { if (userMessagesFromWarning?.length) {
const key = (requestId ?? '') + warning.type + warning.reason?.type ?? ''; userMessages.push(...userMessagesFromWarning);
if (!warningsMap.has(key)) {
warningsMap.set(key, warningMessages);
}
return true; return true;
} }
return false; return false;
}); });
return [...warningsMap.values()].flat(); return userMessages;
}; };
function getSafeLabel(label: string) { function getSafeLabel(label: string) {

View file

@ -86,6 +86,7 @@
"@kbn/content-management-utils", "@kbn/content-management-utils",
"@kbn/serverless", "@kbn/serverless",
"@kbn/ebt-tools", "@kbn/ebt-tools",
"@kbn/search-response-warnings",
], ],
"exclude": [ "exclude": [
"target/**/*", "target/**/*",

View file

@ -1317,7 +1317,6 @@
"data.search.aggs.rareTerms.aggTypesLabel": "Termes rares de {fieldName}", "data.search.aggs.rareTerms.aggTypesLabel": "Termes rares de {fieldName}",
"data.search.es_search.queryTimeValue": "{queryTime} ms", "data.search.es_search.queryTimeValue": "{queryTime} ms",
"data.search.functions.geoBoundingBox.arguments.error": "Au moins un des groupes de paramètres suivants doit être fourni : {parameters}.", "data.search.functions.geoBoundingBox.arguments.error": "Au moins un des groupes de paramètres suivants doit être fourni : {parameters}.",
"data.search.searchSource.fetch.shardsFailedNotificationMessage": "Échec de {shardsFailed} des {shardsTotal} partitions",
"data.search.searchSource.indexPatternIdDescription": "ID dans l'index {kibanaIndexPattern}.", "data.search.searchSource.indexPatternIdDescription": "ID dans l'index {kibanaIndexPattern}.",
"data.search.searchSource.queryTimeValue": "{queryTime} ms", "data.search.searchSource.queryTimeValue": "{queryTime} ms",
"data.search.searchSource.requestTimeValue": "{requestTime} ms", "data.search.searchSource.requestTimeValue": "{requestTime} ms",
@ -2064,15 +2063,7 @@
"data.search.searchSource.dataViewDescription": "La vue de données qui a été interrogée.", "data.search.searchSource.dataViewDescription": "La vue de données qui a été interrogée.",
"data.search.searchSource.dataViewIdLabel": "ID de vue de données", "data.search.searchSource.dataViewIdLabel": "ID de vue de données",
"data.search.searchSource.dataViewLabel": "Vue de données", "data.search.searchSource.dataViewLabel": "Vue de données",
"data.search.searchSource.fetch.requestTimedOutNotificationMessage": "Les données peuvent être incomplètes parce que votre requête est arrivée à échéance.",
"data.search.searchSource.fetch.shardsFailedModal.close": "Fermer",
"data.search.searchSource.fetch.shardsFailedModal.copyToClipboard": "Copier la réponse dans le presse-papiers",
"data.search.searchSource.fetch.shardsFailedModal.showDetails": "Afficher les détails",
"data.search.searchSource.fetch.shardsFailedModal.tabHeaderRequest": "Requête",
"data.search.searchSource.fetch.shardsFailedModal.tabHeaderResponse": "Réponse",
"data.search.searchSource.fetch.shardsFailedModal.tabHeaderShardFailures": "Échecs de partition",
"data.search.searchSource.fetch.shardsFailedModal.tableColReason": "Raison", "data.search.searchSource.fetch.shardsFailedModal.tableColReason": "Raison",
"data.search.searchSource.fetch.shardsFailedNotificationDescription": "Les données peuvent être incomplètes ou erronées.",
"data.search.searchSource.hitsDescription": "Le nombre de documents renvoyés par la requête.", "data.search.searchSource.hitsDescription": "Le nombre de documents renvoyés par la requête.",
"data.search.searchSource.hitsLabel": "Résultats", "data.search.searchSource.hitsLabel": "Résultats",
"data.search.searchSource.hitsTotalDescription": "Le nombre de documents correspondant à la requête.", "data.search.searchSource.hitsTotalDescription": "Le nombre de documents correspondant à la requête.",

View file

@ -1331,7 +1331,6 @@
"data.search.aggs.rareTerms.aggTypesLabel": "{fieldName}の希少な用語", "data.search.aggs.rareTerms.aggTypesLabel": "{fieldName}の希少な用語",
"data.search.es_search.queryTimeValue": "{queryTime}ms", "data.search.es_search.queryTimeValue": "{queryTime}ms",
"data.search.functions.geoBoundingBox.arguments.error": "次のパラメーターのグループの1つ以上を指定する必要があります{parameters}。", "data.search.functions.geoBoundingBox.arguments.error": "次のパラメーターのグループの1つ以上を指定する必要があります{parameters}。",
"data.search.searchSource.fetch.shardsFailedNotificationMessage": "{shardsTotal}件中{shardsFailed}件のシャードでエラーが発生しました",
"data.search.searchSource.indexPatternIdDescription": "{kibanaIndexPattern}インデックス内のIDです。", "data.search.searchSource.indexPatternIdDescription": "{kibanaIndexPattern}インデックス内のIDです。",
"data.search.searchSource.queryTimeValue": "{queryTime}ms", "data.search.searchSource.queryTimeValue": "{queryTime}ms",
"data.search.searchSource.requestTimeValue": "{requestTime}ms", "data.search.searchSource.requestTimeValue": "{requestTime}ms",
@ -2078,15 +2077,7 @@
"data.search.searchSource.dataViewDescription": "照会されたデータビュー。", "data.search.searchSource.dataViewDescription": "照会されたデータビュー。",
"data.search.searchSource.dataViewIdLabel": "データビューID", "data.search.searchSource.dataViewIdLabel": "データビューID",
"data.search.searchSource.dataViewLabel": "データビュー", "data.search.searchSource.dataViewLabel": "データビュー",
"data.search.searchSource.fetch.requestTimedOutNotificationMessage": "リクエストがタイムアウトしたため、データが不完全な可能性があります",
"data.search.searchSource.fetch.shardsFailedModal.close": "閉じる",
"data.search.searchSource.fetch.shardsFailedModal.copyToClipboard": "応答をクリップボードにコピー",
"data.search.searchSource.fetch.shardsFailedModal.showDetails": "詳細を表示",
"data.search.searchSource.fetch.shardsFailedModal.tabHeaderRequest": "リクエスト",
"data.search.searchSource.fetch.shardsFailedModal.tabHeaderResponse": "応答",
"data.search.searchSource.fetch.shardsFailedModal.tabHeaderShardFailures": "シャードエラー",
"data.search.searchSource.fetch.shardsFailedModal.tableColReason": "理由", "data.search.searchSource.fetch.shardsFailedModal.tableColReason": "理由",
"data.search.searchSource.fetch.shardsFailedNotificationDescription": "データが不完全か誤りの可能性があります。",
"data.search.searchSource.hitsDescription": "クエリにより返されたドキュメントの数です。", "data.search.searchSource.hitsDescription": "クエリにより返されたドキュメントの数です。",
"data.search.searchSource.hitsLabel": "ヒット数", "data.search.searchSource.hitsLabel": "ヒット数",
"data.search.searchSource.hitsTotalDescription": "クエリに一致するドキュメントの数です。", "data.search.searchSource.hitsTotalDescription": "クエリに一致するドキュメントの数です。",

View file

@ -1331,7 +1331,6 @@
"data.search.aggs.rareTerms.aggTypesLabel": "{fieldName} 的稀有词", "data.search.aggs.rareTerms.aggTypesLabel": "{fieldName} 的稀有词",
"data.search.es_search.queryTimeValue": "{queryTime}ms", "data.search.es_search.queryTimeValue": "{queryTime}ms",
"data.search.functions.geoBoundingBox.arguments.error": "必须至少提供一个以下参数组:{parameters}。", "data.search.functions.geoBoundingBox.arguments.error": "必须至少提供一个以下参数组:{parameters}。",
"data.search.searchSource.fetch.shardsFailedNotificationMessage": "{shardsTotal} 个分片有 {shardsFailed} 个失败",
"data.search.searchSource.indexPatternIdDescription": "{kibanaIndexPattern} 索引中的 ID。", "data.search.searchSource.indexPatternIdDescription": "{kibanaIndexPattern} 索引中的 ID。",
"data.search.searchSource.queryTimeValue": "{queryTime}ms", "data.search.searchSource.queryTimeValue": "{queryTime}ms",
"data.search.searchSource.requestTimeValue": "{requestTime}ms", "data.search.searchSource.requestTimeValue": "{requestTime}ms",
@ -2078,15 +2077,7 @@
"data.search.searchSource.dataViewDescription": "被查询的数据视图。", "data.search.searchSource.dataViewDescription": "被查询的数据视图。",
"data.search.searchSource.dataViewIdLabel": "数据视图 ID", "data.search.searchSource.dataViewIdLabel": "数据视图 ID",
"data.search.searchSource.dataViewLabel": "数据视图", "data.search.searchSource.dataViewLabel": "数据视图",
"data.search.searchSource.fetch.requestTimedOutNotificationMessage": "由于您的请求超时,数据可能不完整",
"data.search.searchSource.fetch.shardsFailedModal.close": "关闭",
"data.search.searchSource.fetch.shardsFailedModal.copyToClipboard": "将响应复制到剪贴板",
"data.search.searchSource.fetch.shardsFailedModal.showDetails": "显示详情",
"data.search.searchSource.fetch.shardsFailedModal.tabHeaderRequest": "请求",
"data.search.searchSource.fetch.shardsFailedModal.tabHeaderResponse": "响应",
"data.search.searchSource.fetch.shardsFailedModal.tabHeaderShardFailures": "分片错误",
"data.search.searchSource.fetch.shardsFailedModal.tableColReason": "原因", "data.search.searchSource.fetch.shardsFailedModal.tableColReason": "原因",
"data.search.searchSource.fetch.shardsFailedNotificationDescription": "数据可能不完整或有错误。",
"data.search.searchSource.hitsDescription": "查询返回的文档数目。", "data.search.searchSource.hitsDescription": "查询返回的文档数目。",
"data.search.searchSource.hitsLabel": "命中数", "data.search.searchSource.hitsLabel": "命中数",
"data.search.searchSource.hitsTotalDescription": "与查询匹配的文档数目。", "data.search.searchSource.hitsTotalDescription": "与查询匹配的文档数目。",

View file

@ -27,7 +27,7 @@ export default function ({ getService, getPageObjects }) {
const security = getService('security'); const security = getService('security');
const dashboardAddPanel = getService('dashboardAddPanel'); const dashboardAddPanel = getService('dashboardAddPanel');
describe('async search with scripted fields', function () { describe('search with scripted fields', function () {
this.tags(['skipFirefox']); this.tags(['skipFirefox']);
before(async function () { before(async function () {
@ -51,7 +51,7 @@ export default function ({ getService, getPageObjects }) {
await security.testUser.restoreDefaults(); await security.testUser.restoreDefaults();
}); });
it('query should show failed shards callout', async function () { it('query should show incomplete results callout', async function () {
if (false) { if (false) {
/* If you had to modify the scripted fields, you could un-comment all this, run it, use es_archiver to update 'kibana_scripted_fields_on_logstash' /* If you had to modify the scripted fields, you could un-comment all this, run it, use es_archiver to update 'kibana_scripted_fields_on_logstash'
*/ */
@ -81,11 +81,11 @@ export default function ({ getService, getPageObjects }) {
'dscNoResultsInterceptedWarningsCallout_warningTitle' 'dscNoResultsInterceptedWarningsCallout_warningTitle'
); );
log.debug(shardMessage); log.debug(shardMessage);
expect(shardMessage).to.be('1 of 3 shards failed'); expect(shardMessage).to.be('The data might be incomplete or wrong.');
}); });
}); });
it('query should show failed shards badge on dashboard', async function () { it('query should show incomplete results badge on dashboard', async function () {
await security.testUser.setRoles([ await security.testUser.setRoles([
'test_logstash_reader', 'test_logstash_reader',
'global_discover_all', 'global_discover_all',