mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
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:
parent
5bd152369b
commit
f3b280f6ee
82 changed files with 1234 additions and 1898 deletions
|
@ -311,17 +311,15 @@ export const SearchExamplesApp = ({
|
|||
const result = await lastValueFrom(
|
||||
searchSource.fetch$({
|
||||
abortSignal: abortController.signal,
|
||||
disableShardFailureWarning: !showWarningToastNotifications,
|
||||
disableWarningToasts: !showWarningToastNotifications,
|
||||
inspector,
|
||||
})
|
||||
);
|
||||
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.
|
||||
*
|
||||
* Suppressing the shard failure warning notification from appearing by default requires setting
|
||||
* { disableShardFailureWarning: true } in the SearchSourceSearchOptions passed to $fetch
|
||||
/*
|
||||
* Set disableWarningToasts to true to disable warning toasts and customize warning display.
|
||||
* Then use showWarnings to customize warning notification.
|
||||
*/
|
||||
if (showWarningToastNotifications) {
|
||||
setWarningContents([]);
|
||||
|
@ -498,7 +496,7 @@ export const SearchExamplesApp = ({
|
|||
{' '}
|
||||
<FormattedMessage
|
||||
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>{' '}
|
||||
<EuiProgress value={loaded} max={total} size="xs" data-test-subj="progressBar" />{' '}
|
||||
|
|
|
@ -18,4 +18,5 @@ export type {
|
|||
AggregationResultOfMap,
|
||||
ESFilter,
|
||||
MaybeReadonlyArray,
|
||||
ClusterDetails,
|
||||
} from './src';
|
||||
|
|
|
@ -11,6 +11,7 @@ import {
|
|||
AggregateOf as AggregationResultOf,
|
||||
AggregateOfMap as AggregationResultOfMap,
|
||||
SearchHit,
|
||||
ClusterDetails,
|
||||
} from './search';
|
||||
|
||||
export type ESFilter = estypes.QueryDslQueryContainer;
|
||||
|
@ -34,4 +35,10 @@ export type ESSearchResponse<
|
|||
TOptions extends { restTotalHitsAsInt: boolean } = { restTotalHitsAsInt: false }
|
||||
> = InferSearchResponseOf<TDocument, TSearchRequest, TOptions>;
|
||||
|
||||
export type { InferSearchResponseOf, AggregationResultOf, AggregationResultOfMap, SearchHit };
|
||||
export type {
|
||||
InferSearchResponseOf,
|
||||
AggregationResultOf,
|
||||
AggregationResultOfMap,
|
||||
SearchHit,
|
||||
ClusterDetails,
|
||||
};
|
||||
|
|
|
@ -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[];
|
||||
}
|
||||
|
|
|
@ -13,7 +13,5 @@ export {
|
|||
type SearchResponseWarningsProps,
|
||||
} from './src/components/search_response_warnings';
|
||||
|
||||
export {
|
||||
getSearchResponseInterceptedWarnings,
|
||||
removeInterceptedWarningDuplicates,
|
||||
} from './src/utils/get_search_response_intercepted_warnings';
|
||||
export { getSearchResponseInterceptedWarnings } from './src/utils/get_search_response_intercepted_warnings';
|
||||
export { hasUnsupportedDownsampledAggregationFailure } from './src/utils/has_unsupported_downsampled_aggregation_failure';
|
||||
|
|
|
@ -8,43 +8,33 @@
|
|||
|
||||
import type { SearchResponseWarning } from '@kbn/data-plugin/public';
|
||||
|
||||
export const searchResponseTimeoutWarningMock: SearchResponseWarning = {
|
||||
type: 'timed_out',
|
||||
message: 'Data might be incomplete because your request timed out',
|
||||
reason: undefined,
|
||||
};
|
||||
|
||||
export const searchResponseShardFailureWarningMock: SearchResponseWarning = {
|
||||
type: 'shard_failure',
|
||||
message: '3 of 4 shards failed',
|
||||
text: 'The data might be incomplete or wrong.',
|
||||
reason: {
|
||||
type: 'illegal_argument_exception',
|
||||
reason: 'Field [__anonymous_] of type [boolean] does not support custom formats',
|
||||
},
|
||||
};
|
||||
|
||||
export const searchResponseWarningsMock: SearchResponseWarning[] = [
|
||||
searchResponseTimeoutWarningMock,
|
||||
searchResponseShardFailureWarningMock,
|
||||
{
|
||||
type: 'shard_failure',
|
||||
message: '3 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!',
|
||||
export const searchResponseIncompleteWarningLocalCluster: SearchResponseWarning = {
|
||||
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]',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
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!',
|
||||
},
|
||||
},
|
||||
];
|
||||
};
|
||||
|
|
|
@ -13,7 +13,7 @@ exports[`SearchResponseWarnings renders "badge" correctly 1`] = `
|
|||
<button
|
||||
class="euiButton emotion-euiButtonDisplay-s-EuiButton"
|
||||
data-test-subj="test2_trigger"
|
||||
title="4 warnings"
|
||||
title="1 warning"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
|
@ -23,7 +23,7 @@ exports[`SearchResponseWarnings renders "badge" correctly 1`] = `
|
|||
class="emotion-EuiIcon"
|
||||
data-euiicon-type="warning"
|
||||
/>
|
||||
4
|
||||
1
|
||||
</span>
|
||||
</button>
|
||||
</span>
|
||||
|
@ -68,84 +68,14 @@ exports[`SearchResponseWarnings renders "callout" correctly 1`] = `
|
|||
class="euiText emotion-euiText-s"
|
||||
data-test-subj="test1_warningTitle"
|
||||
>
|
||||
Data might be incomplete because your request timed out
|
||||
</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>
|
||||
The data might be incomplete or wrong.
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="euiFlexItem emotion-euiFlexItem-growZero"
|
||||
>
|
||||
<button>
|
||||
test1
|
||||
test
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -155,161 +85,7 @@ exports[`SearchResponseWarnings renders "callout" correctly 1`] = `
|
|||
>
|
||||
<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
|
||||
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"
|
||||
class="euiButtonIcon emotion-euiButtonIcon-xs-empty-warning"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
|
@ -377,124 +153,14 @@ exports[`SearchResponseWarnings renders "empty_prompt" correctly 1`] = `
|
|||
class="euiText emotion-euiText-m"
|
||||
data-test-subj="test3_warningTitle"
|
||||
>
|
||||
Data might be incomplete because your request timed out
|
||||
</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>
|
||||
The data might be incomplete or wrong.
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="euiFlexItem emotion-euiFlexItem-growZero"
|
||||
>
|
||||
<button>
|
||||
test1
|
||||
</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
|
||||
test
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -9,12 +9,14 @@
|
|||
import React from 'react';
|
||||
import { mountWithIntl } from '@kbn/test-jest-helpers';
|
||||
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) => ({
|
||||
originalWarning,
|
||||
action: originalWarning.type === 'shard_failure' ? <button>{`test${index}`}</button> : undefined,
|
||||
}));
|
||||
const interceptedWarnings = [
|
||||
{
|
||||
originalWarning: searchResponseIncompleteWarningLocalCluster,
|
||||
action: <button>{`test`}</button>,
|
||||
},
|
||||
];
|
||||
|
||||
describe('SearchResponseWarnings', () => {
|
||||
it('renders "callout" correctly', () => {
|
||||
|
|
|
@ -270,22 +270,13 @@ function WarningContent({
|
|||
groupStyles?: Partial<EuiFlexGroupProps>;
|
||||
'data-test-subj': string;
|
||||
}) {
|
||||
const hasDescription = 'text' in originalWarning;
|
||||
|
||||
return (
|
||||
<EuiFlexGroup gutterSize="xs" {...groupStyles} wrap>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiText size={textSize} data-test-subj={`${dataTestSubj}_warningTitle`}>
|
||||
{hasDescription ? <strong>{originalWarning.message}</strong> : originalWarning.message}
|
||||
{originalWarning.message}
|
||||
</EuiText>
|
||||
</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}
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
|
@ -306,6 +297,7 @@ function CalloutTitleWrapper({
|
|||
onClick={onCloseCallout}
|
||||
type="button"
|
||||
iconType="cross"
|
||||
color="warning"
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
|
|
|
@ -9,11 +9,8 @@
|
|||
import { RequestAdapter } from '@kbn/inspector-plugin/common';
|
||||
import { dataPluginMock } from '@kbn/data-plugin/public/mocks';
|
||||
import { coreMock } from '@kbn/core/public/mocks';
|
||||
import {
|
||||
getSearchResponseInterceptedWarnings,
|
||||
removeInterceptedWarningDuplicates,
|
||||
} from './get_search_response_intercepted_warnings';
|
||||
import { searchResponseWarningsMock } from '../__mocks__/search_response_warnings';
|
||||
import { getSearchResponseInterceptedWarnings } from './get_search_response_intercepted_warnings';
|
||||
import { searchResponseIncompleteWarningLocalCluster } from '../__mocks__/search_response_warnings';
|
||||
|
||||
const servicesMock = {
|
||||
data: dataPluginMock.createStartContract(),
|
||||
|
@ -23,162 +20,66 @@ const servicesMock = {
|
|||
describe('getSearchResponseInterceptedWarnings', () => {
|
||||
const adapter = new RequestAdapter();
|
||||
|
||||
it('should catch warnings correctly', () => {
|
||||
it('should return intercepted incomplete data warnings', () => {
|
||||
const services = {
|
||||
...servicesMock,
|
||||
};
|
||||
services.data.search.showWarnings = jest.fn((_, callback) => {
|
||||
// @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], {});
|
||||
// @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], {});
|
||||
callback?.(searchResponseIncompleteWarningLocalCluster, {});
|
||||
});
|
||||
expect(
|
||||
getSearchResponseInterceptedWarnings({
|
||||
services,
|
||||
adapter,
|
||||
options: {
|
||||
disableShardFailureWarning: true,
|
||||
},
|
||||
})
|
||||
).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"action": undefined,
|
||||
"originalWarning": Object {
|
||||
"message": "Data might be incomplete because your request timed out",
|
||||
"reason": undefined,
|
||||
"type": "timed_out",
|
||||
},
|
||||
},
|
||||
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",
|
||||
const warnings = getSearchResponseInterceptedWarnings({
|
||||
services,
|
||||
adapter,
|
||||
});
|
||||
|
||||
expect(warnings.length).toBe(1);
|
||||
expect(warnings[0].originalWarning).toEqual(searchResponseIncompleteWarningLocalCluster);
|
||||
expect(warnings[0].action).toMatchInlineSnapshot(`
|
||||
<OpenIncompleteResultsModalButton
|
||||
color="primary"
|
||||
getRequestMeta={[Function]}
|
||||
isButtonEmpty={true}
|
||||
size="s"
|
||||
theme={
|
||||
Object {
|
||||
"theme$": Observable {
|
||||
"_subscribe": [Function],
|
||||
},
|
||||
"text": "The data might be incomplete or wrong.",
|
||||
"type": "shard_failure",
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"action": <ShardFailureOpenModalButton
|
||||
color="primary"
|
||||
getRequestMeta={[Function]}
|
||||
isButtonEmpty={true}
|
||||
size="s"
|
||||
theme={
|
||||
Object {
|
||||
"theme$": Observable {
|
||||
"_subscribe": [Function],
|
||||
}
|
||||
}
|
||||
warning={
|
||||
Object {
|
||||
"clusters": Object {
|
||||
"(local)": Object {
|
||||
"_shards": Object {
|
||||
"failed": 1,
|
||||
"skipped": 0,
|
||||
"successful": 3,
|
||||
"total": 4,
|
||||
},
|
||||
}
|
||||
}
|
||||
title="3 of 4 shards failed"
|
||||
/>,
|
||||
"originalWarning": Object {
|
||||
"message": "3 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",
|
||||
"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,
|
||||
},
|
||||
],
|
||||
"indices": "",
|
||||
"status": "partial",
|
||||
"timed_out": false,
|
||||
"took": 25,
|
||||
},
|
||||
},
|
||||
"text": "The data might be incomplete or wrong.",
|
||||
"type": "shard_failure",
|
||||
},
|
||||
},
|
||||
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",
|
||||
},
|
||||
},
|
||||
]
|
||||
"message": "The data might be incomplete or wrong.",
|
||||
"type": "incomplete",
|
||||
}
|
||||
}
|
||||
/>
|
||||
`);
|
||||
});
|
||||
|
||||
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();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -7,11 +7,9 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { uniqBy } from 'lodash';
|
||||
import {
|
||||
type DataPublicPluginStart,
|
||||
type ShardFailureRequest,
|
||||
ShardFailureOpenModalButton,
|
||||
OpenIncompleteResultsModalButton,
|
||||
} from '@kbn/data-plugin/public';
|
||||
import type { RequestAdapter } from '@kbn/inspector-plugin/common';
|
||||
import type { CoreStart } from '@kbn/core-lifecycle-browser';
|
||||
|
@ -26,21 +24,13 @@ import type { SearchResponseInterceptedWarning } from '../types';
|
|||
export const getSearchResponseInterceptedWarnings = ({
|
||||
services,
|
||||
adapter,
|
||||
options,
|
||||
}: {
|
||||
services: {
|
||||
data: DataPublicPluginStart;
|
||||
theme: CoreStart['theme'];
|
||||
};
|
||||
adapter: RequestAdapter;
|
||||
options?: {
|
||||
disableShardFailureWarning?: boolean;
|
||||
};
|
||||
}): SearchResponseInterceptedWarning[] | undefined => {
|
||||
if (!options?.disableShardFailureWarning) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
}): SearchResponseInterceptedWarning[] => {
|
||||
const interceptedWarnings: SearchResponseInterceptedWarning[] = [];
|
||||
|
||||
services.data.search.showWarnings(adapter, (warning, meta) => {
|
||||
|
@ -49,13 +39,13 @@ export const getSearchResponseInterceptedWarnings = ({
|
|||
interceptedWarnings.push({
|
||||
originalWarning: warning,
|
||||
action:
|
||||
warning.type === 'shard_failure' && warning.text && warning.message ? (
|
||||
<ShardFailureOpenModalButton
|
||||
warning.type === 'incomplete' ? (
|
||||
<OpenIncompleteResultsModalButton
|
||||
theme={services.theme}
|
||||
title={warning.message}
|
||||
warning={warning}
|
||||
size="s"
|
||||
getRequestMeta={() => ({
|
||||
request: request as ShardFailureRequest,
|
||||
request,
|
||||
response,
|
||||
})}
|
||||
color="primary"
|
||||
|
@ -66,23 +56,5 @@ export const getSearchResponseInterceptedWarnings = ({
|
|||
return true; // suppress the default behaviour
|
||||
});
|
||||
|
||||
return removeInterceptedWarningDuplicates(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;
|
||||
return interceptedWarnings;
|
||||
};
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -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;
|
||||
}
|
|
@ -30,7 +30,7 @@ type PostFlightRequestFn<TAggConfig> = (
|
|||
inspectorRequestAdapter?: RequestAdapter,
|
||||
abortSignal?: AbortSignal,
|
||||
searchSessionId?: string,
|
||||
disableShardFailureWarning?: boolean
|
||||
disableWarningToasts?: boolean
|
||||
) => Promise<estypes.SearchResponse<any>>;
|
||||
|
||||
export interface AggTypeConfig<
|
||||
|
|
|
@ -396,7 +396,7 @@ export const createOtherBucketPostFlightRequest = (
|
|||
inspectorRequestAdapter,
|
||||
abortSignal,
|
||||
searchSessionId,
|
||||
disableShardFailureWarning
|
||||
disableWarningToasts
|
||||
) => {
|
||||
if (!resp.aggregations) return resp;
|
||||
const nestedSearchSource = searchSource.createChild();
|
||||
|
@ -410,7 +410,7 @@ export const createOtherBucketPostFlightRequest = (
|
|||
nestedSearchSource.fetch$({
|
||||
abortSignal,
|
||||
sessionId: searchSessionId,
|
||||
disableShardFailureWarning,
|
||||
disableWarningToasts,
|
||||
inspector: {
|
||||
adapter: inspectorRequestAdapter,
|
||||
title: i18n.translate('data.search.aggs.buckets.terms.otherBucketTitle', {
|
||||
|
|
|
@ -53,7 +53,7 @@ describe('esaggs expression function - public', () => {
|
|||
query: undefined,
|
||||
searchSessionId: 'abc123',
|
||||
searchSourceService: searchSourceCommonMock,
|
||||
disableShardWarnings: false,
|
||||
disableWarningToasts: false,
|
||||
timeFields: ['@timestamp', 'utc_time'],
|
||||
timeRange: undefined,
|
||||
};
|
||||
|
@ -139,7 +139,7 @@ describe('esaggs expression function - public', () => {
|
|||
description: 'This request queries Elasticsearch to fetch the data for the visualization.',
|
||||
adapter: undefined,
|
||||
},
|
||||
disableShardFailureWarning: false,
|
||||
disableWarningToasts: false,
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -159,7 +159,7 @@ describe('esaggs expression function - public', () => {
|
|||
description: 'MyDescription',
|
||||
adapter: undefined,
|
||||
},
|
||||
disableShardFailureWarning: false,
|
||||
disableWarningToasts: false,
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -30,7 +30,7 @@ export interface RequestHandlerParams {
|
|||
searchSourceService: ISearchStartSearchSource;
|
||||
timeFields?: string[];
|
||||
timeRange?: TimeRange;
|
||||
disableShardWarnings?: boolean;
|
||||
disableWarningToasts?: boolean;
|
||||
getNow?: () => Date;
|
||||
executionContext?: KibanaExecutionContext;
|
||||
title?: string;
|
||||
|
@ -48,7 +48,7 @@ export const handleRequest = ({
|
|||
searchSourceService,
|
||||
timeFields,
|
||||
timeRange,
|
||||
disableShardWarnings,
|
||||
disableWarningToasts,
|
||||
getNow,
|
||||
executionContext,
|
||||
title,
|
||||
|
@ -110,7 +110,7 @@ export const handleRequest = ({
|
|||
requestSearchSource
|
||||
.fetch$({
|
||||
abortSignal,
|
||||
disableShardFailureWarning: disableShardWarnings,
|
||||
disableWarningToasts,
|
||||
sessionId: searchSessionId,
|
||||
inspector: {
|
||||
adapter: inspectorAdapters.requests,
|
||||
|
|
|
@ -15,7 +15,7 @@ export type ExecutionContextSearch = {
|
|||
filters?: Filter[];
|
||||
query?: Query | Query[];
|
||||
timeRange?: TimeRange;
|
||||
disableShardWarnings?: boolean;
|
||||
disableWarningToasts?: boolean;
|
||||
};
|
||||
|
||||
export type ExpressionValueSearchContext = ExpressionValueBoxed<
|
||||
|
|
|
@ -520,7 +520,7 @@ export class SearchSource {
|
|||
options.inspector?.adapter,
|
||||
options.abortSignal,
|
||||
options.sessionId,
|
||||
options.disableShardFailureWarning
|
||||
options.disableWarningToasts
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -249,7 +249,7 @@ export interface SearchSourceSearchOptions extends ISearchOptions {
|
|||
inspector?: IInspectorInfo;
|
||||
|
||||
/**
|
||||
* Disable default warnings of shard failures
|
||||
* Set to true to disable warning toasts and customize warning display
|
||||
*/
|
||||
disableShardFailureWarning?: boolean;
|
||||
disableWarningToasts?: boolean;
|
||||
}
|
||||
|
|
271
src/plugins/data/public/incomplete_results_modal/__snapshots__/incomplete_results_modal.test.tsx.snap
generated
Normal file
271
src/plugins/data/public/incomplete_results_modal/__snapshots__/incomplete_results_modal.test.tsx.snap
generated
Normal 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>
|
||||
`;
|
|
@ -1,5 +1,5 @@
|
|||
// set width and height to fixed values to prevent resizing when you switch tabs
|
||||
.shardFailureModal {
|
||||
.incompleteResultsModal {
|
||||
min-height: 75vh;
|
||||
width: 768px;
|
||||
|
||||
|
@ -12,10 +12,4 @@
|
|||
.euiModalHeader {
|
||||
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;
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -7,17 +7,13 @@
|
|||
*/
|
||||
|
||||
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 LazyShardFailureOpenModalButton = React.lazy(
|
||||
() => import('./shard_failure_open_modal_button')
|
||||
);
|
||||
export const ShardFailureOpenModalButton = (props: ShardFailureOpenModalButtonProps) => (
|
||||
const LazyOpenModalButton = React.lazy(() => import('./open_incomplete_results_modal_button'));
|
||||
export const OpenIncompleteResultsModalButton = (props: OpenIncompleteResultsModalButtonProps) => (
|
||||
<React.Suspense fallback={<Fallback />}>
|
||||
<LazyShardFailureOpenModalButton {...props} />
|
||||
<LazyOpenModalButton {...props} />
|
||||
</React.Suspense>
|
||||
);
|
||||
|
||||
export type { ShardFailureRequest } from './shard_failure_types';
|
|
@ -13,18 +13,19 @@ import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
|
|||
import type { ThemeServiceStart } from '@kbn/core/public';
|
||||
import { toMountPoint } from '@kbn/kibana-react-plugin/public';
|
||||
import { getOverlays } from '../services';
|
||||
import { ShardFailureModal } from './shard_failure_modal';
|
||||
import type { ShardFailureRequest } from './shard_failure_types';
|
||||
import './_shard_failure_modal.scss';
|
||||
import type { SearchRequest } from '..';
|
||||
import { IncompleteResultsModal } from './incomplete_results_modal';
|
||||
import type { SearchResponseIncompleteWarning } from '../search';
|
||||
import './_incomplete_results_modal.scss';
|
||||
|
||||
// @internal
|
||||
export interface ShardFailureOpenModalButtonProps {
|
||||
export interface OpenIncompleteResultsModalButtonProps {
|
||||
theme: ThemeServiceStart;
|
||||
title: string;
|
||||
warning: SearchResponseIncompleteWarning;
|
||||
size?: EuiButtonProps['size'];
|
||||
color?: EuiButtonProps['color'];
|
||||
getRequestMeta: () => {
|
||||
request: ShardFailureRequest;
|
||||
request: SearchRequest;
|
||||
response: estypes.SearchResponse<any>;
|
||||
};
|
||||
isButtonEmpty?: boolean;
|
||||
|
@ -32,31 +33,31 @@ export interface ShardFailureOpenModalButtonProps {
|
|||
|
||||
// Needed for React.lazy
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default function ShardFailureOpenModalButton({
|
||||
export default function OpenIncompleteResultsModalButton({
|
||||
getRequestMeta,
|
||||
theme,
|
||||
title,
|
||||
warning,
|
||||
size = 's',
|
||||
color = 'warning',
|
||||
isButtonEmpty = false,
|
||||
}: ShardFailureOpenModalButtonProps) {
|
||||
}: OpenIncompleteResultsModalButtonProps) {
|
||||
const onClick = useCallback(() => {
|
||||
const { request, response } = getRequestMeta();
|
||||
const modal = getOverlays().openModal(
|
||||
toMountPoint(
|
||||
<ShardFailureModal
|
||||
<IncompleteResultsModal
|
||||
request={request}
|
||||
response={response}
|
||||
title={title}
|
||||
warning={warning}
|
||||
onClose={() => modal.close()}
|
||||
/>,
|
||||
{ theme$: theme.theme$ }
|
||||
),
|
||||
{
|
||||
className: 'shardFailureModal',
|
||||
className: 'incompleteResultsModal',
|
||||
}
|
||||
);
|
||||
}, [getRequestMeta, theme.theme$, title]);
|
||||
}, [getRequestMeta, theme.theme$, warning]);
|
||||
|
||||
const Component = isButtonEmpty ? EuiLink : EuiButton;
|
||||
|
||||
|
@ -65,11 +66,11 @@ export default function ShardFailureOpenModalButton({
|
|||
color={color}
|
||||
size={size}
|
||||
onClick={onClick}
|
||||
data-test-subj="openShardFailureModalBtn"
|
||||
data-test-subj="openIncompleteResultsModalBtn"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="data.search.searchSource.fetch.shardsFailedModal.showDetails"
|
||||
defaultMessage="Show details"
|
||||
id="data.search.searchSource.fetch.incompleteResultsModal.viewDetails"
|
||||
defaultMessage="View details"
|
||||
description="Open the modal to show details"
|
||||
/>
|
||||
</Component>
|
|
@ -170,6 +170,7 @@ export type {
|
|||
Reason,
|
||||
WaitUntilNextSessionCompletesOptions,
|
||||
SearchResponseWarning,
|
||||
SearchResponseIncompleteWarning,
|
||||
} from './search';
|
||||
|
||||
export {
|
||||
|
@ -273,8 +274,7 @@ export type {
|
|||
} from './query';
|
||||
|
||||
// TODO: move to @kbn/search-response-warnings
|
||||
export type { ShardFailureRequest } from './shard_failure_modal';
|
||||
export { ShardFailureOpenModalButton } from './shard_failure_modal';
|
||||
export { OpenIncompleteResultsModalButton } from './incomplete_results_modal';
|
||||
|
||||
export type { AggsStart } from './search/aggs';
|
||||
|
||||
|
|
|
@ -126,7 +126,7 @@ describe('esaggs expression function - public', () => {
|
|||
searchSessionId: 'abc123',
|
||||
searchSourceService: startDependencies.searchSource,
|
||||
timeFields: args.timeFields,
|
||||
disableShardWarnings: false,
|
||||
disableWarningToasts: false,
|
||||
timeRange: undefined,
|
||||
getNow: undefined,
|
||||
});
|
||||
|
|
|
@ -61,7 +61,7 @@ export function getFunctionDefinition({
|
|||
return { aggConfigs, indexPattern, searchSource, getNow, handleEsaggsRequest };
|
||||
}).pipe(
|
||||
switchMap(({ aggConfigs, indexPattern, searchSource, getNow, handleEsaggsRequest }) => {
|
||||
const { disableShardWarnings } = getSearchContext();
|
||||
const { disableWarningToasts } = getSearchContext();
|
||||
|
||||
return handleEsaggsRequest({
|
||||
abortSignal,
|
||||
|
@ -74,7 +74,7 @@ export function getFunctionDefinition({
|
|||
searchSourceService: searchSource,
|
||||
timeFields: args.timeFields,
|
||||
timeRange: get(input, 'timeRange', undefined),
|
||||
disableShardWarnings: (disableShardWarnings || false) as boolean,
|
||||
disableWarningToasts: (disableWarningToasts || false) as boolean,
|
||||
getNow,
|
||||
executionContext: getExecutionContext(),
|
||||
});
|
||||
|
|
|
@ -10,121 +10,280 @@ import { estypes } from '@elastic/elasticsearch';
|
|||
import { extractWarnings } from './extract_warnings';
|
||||
|
||||
describe('extract search response warnings', () => {
|
||||
it('should extract warnings from response with shard failures', () => {
|
||||
const response = {
|
||||
took: 25,
|
||||
timed_out: false,
|
||||
_shards: {
|
||||
total: 4,
|
||||
successful: 2,
|
||||
skipped: 0,
|
||||
failed: 2,
|
||||
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]',
|
||||
describe('single cluster', () => {
|
||||
it('should extract incomplete warning from response with shard failures', () => {
|
||||
const response = {
|
||||
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]',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
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', () => {
|
||||
const warnings = {
|
||||
took: 999,
|
||||
timed_out: true,
|
||||
_shards: {} as estypes.ShardStatistics,
|
||||
hits: { hits: [] },
|
||||
};
|
||||
expect(extractWarnings(warnings)).toEqual([
|
||||
{
|
||||
type: 'timed_out',
|
||||
message: 'Data might be incomplete because your request timed out',
|
||||
},
|
||||
]);
|
||||
});
|
||||
describe('remote clusters', () => {
|
||||
it('should extract incomplete warning from response with shard failures', () => {
|
||||
const response = {
|
||||
took: 25,
|
||||
timed_out: false,
|
||||
_shards: {
|
||||
total: 4,
|
||||
successful: 3,
|
||||
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',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
_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', () => {
|
||||
const warnings = {
|
||||
_shards: {
|
||||
failed: 77,
|
||||
total: 79,
|
||||
},
|
||||
} 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.',
|
||||
},
|
||||
]);
|
||||
});
|
||||
expect(extractWarnings(response)).toEqual([
|
||||
{
|
||||
type: 'incomplete',
|
||||
message: 'The data might be incomplete or wrong.',
|
||||
clusters: response._clusters.details,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should extract shards failed warning failure reason type', () => {
|
||||
const warnings = extractWarnings({
|
||||
_shards: {
|
||||
failed: 77,
|
||||
total: 79,
|
||||
},
|
||||
} as estypes.SearchResponse);
|
||||
expect(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 incomplete warning from response with time out', () => {
|
||||
const response = {
|
||||
took: 999,
|
||||
timed_out: true,
|
||||
_shards: {
|
||||
total: 6,
|
||||
successful: 6,
|
||||
skipped: 0,
|
||||
failed: 0,
|
||||
},
|
||||
_clusters: {
|
||||
total: 2,
|
||||
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', () => {
|
||||
const warnings = extractWarnings({
|
||||
timed_out: true,
|
||||
_shards: {
|
||||
failed: 77,
|
||||
total: 79,
|
||||
},
|
||||
} as estypes.SearchResponse);
|
||||
const [shardFailures, timedOut] = [
|
||||
warnings.filter(({ type }) => type !== 'timed_out'),
|
||||
warnings.filter(({ type }) => type === 'timed_out'),
|
||||
];
|
||||
expect(shardFailures[0]!.message).toBeDefined();
|
||||
expect(timedOut[0]!.message).toBeDefined();
|
||||
});
|
||||
it('should not include warnings when there are none', () => {
|
||||
const warnings = extractWarnings({
|
||||
took: 10,
|
||||
timed_out: false,
|
||||
_shards: {
|
||||
total: 4,
|
||||
successful: 4,
|
||||
skipped: 0,
|
||||
failed: 0,
|
||||
},
|
||||
_clusters: {
|
||||
total: 2,
|
||||
successful: 2,
|
||||
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', () => {
|
||||
const warnings = extractWarnings({
|
||||
timed_out: false,
|
||||
_shards: {
|
||||
failed: 0,
|
||||
total: 9000,
|
||||
},
|
||||
} as estypes.SearchResponse);
|
||||
|
||||
expect(warnings).toEqual([]);
|
||||
expect(warnings).toEqual([]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
|
||||
import { estypes } from '@elastic/elasticsearch';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import type { ClusterDetails } from '@kbn/es-types';
|
||||
import { SearchResponseWarning } from '../types';
|
||||
|
||||
/**
|
||||
|
@ -16,53 +17,38 @@ import { SearchResponseWarning } from '../types';
|
|||
export function extractWarnings(rawResponse: estypes.SearchResponse): 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({
|
||||
type: 'timed_out',
|
||||
message: i18n.translate('data.search.searchSource.fetch.requestTimedOutNotificationMessage', {
|
||||
defaultMessage: 'Data might be incomplete because your request timed out',
|
||||
type: 'incomplete',
|
||||
message: i18n.translate('data.search.searchSource.fetch.incompleteResultsMessage', {
|
||||
defaultMessage: 'The data might be incomplete or wrong.',
|
||||
}),
|
||||
reason: undefined, // exists so that callers do not have to cast when working with shard warnings.
|
||||
});
|
||||
}
|
||||
|
||||
if (rawResponse._shards && rawResponse._shards.failed) {
|
||||
const message = i18n.translate(
|
||||
'data.search.searchSource.fetch.shardsFailedNotificationMessage',
|
||||
{
|
||||
defaultMessage: '{shardsFailed} of {shardsTotal} shards failed',
|
||||
values: {
|
||||
shardsFailed: rawResponse._shards.failed,
|
||||
shardsTotal: rawResponse._shards.total,
|
||||
},
|
||||
}
|
||||
);
|
||||
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,
|
||||
clusters: rawResponse._clusters
|
||||
? (
|
||||
rawResponse._clusters as estypes.ClusterStatistics & {
|
||||
details: Record<string, ClusterDetails>;
|
||||
}
|
||||
).details
|
||||
: {
|
||||
'(local)': {
|
||||
status: 'partial',
|
||||
indices: '',
|
||||
took: rawResponse.took,
|
||||
timed_out: rawResponse.timed_out,
|
||||
_shards: rawResponse._shards,
|
||||
failures: rawResponse._shards.failures,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
} else {
|
||||
// unknown type and reason
|
||||
warnings.push({
|
||||
type: 'shard_failure',
|
||||
message,
|
||||
text,
|
||||
reason: { type: 'generic_shard_warning' },
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return warnings;
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -7,49 +7,23 @@
|
|||
*/
|
||||
|
||||
import { estypes } from '@elastic/elasticsearch';
|
||||
import { debounce } from 'lodash';
|
||||
import { EuiSpacer, EuiTextAlign } from '@elastic/eui';
|
||||
import { EuiTextAlign } from '@elastic/eui';
|
||||
import { ThemeServiceStart } from '@kbn/core/public';
|
||||
import { toMountPoint } from '@kbn/kibana-react-plugin/public';
|
||||
import React from 'react';
|
||||
import type { MountPoint } from '@kbn/core/public';
|
||||
import { SearchRequest } from '..';
|
||||
import { getNotifications } from '../../services';
|
||||
import { ShardFailureOpenModalButton, ShardFailureRequest } from '../../shard_failure_modal';
|
||||
import { OpenIncompleteResultsModalButton } from '../../incomplete_results_modal';
|
||||
import {
|
||||
SearchResponseShardFailureWarning,
|
||||
SearchResponseIncompleteWarning,
|
||||
SearchResponseWarning,
|
||||
WarningHandlerCallback,
|
||||
} from '../types';
|
||||
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
|
||||
* All warnings are expected to come from the same response. Therefore all "text" properties, which contain the
|
||||
* response, will be the same.
|
||||
* All warnings are expected to come from the same response.
|
||||
*/
|
||||
export function handleWarnings({
|
||||
request,
|
||||
|
@ -78,47 +52,29 @@ export function handleWarnings({
|
|||
return;
|
||||
}
|
||||
|
||||
// timeout notification
|
||||
const [timeout] = internal.filter((w) => w.type === 'timed_out');
|
||||
if (timeout) {
|
||||
debouncedTimeoutWarning(sessionId + timeout.message, timeout.message);
|
||||
}
|
||||
|
||||
// shard warning failure notification
|
||||
const shardFailures = internal.filter((w) => w.type === 'shard_failure');
|
||||
if (shardFailures.length === 0) {
|
||||
// Incomplete data failure notification
|
||||
const incompleteWarnings = internal.filter((w) => w.type === 'incomplete');
|
||||
if (incompleteWarnings.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const [warning] = shardFailures as SearchResponseShardFailureWarning[];
|
||||
const title = warning.message;
|
||||
|
||||
// if warning message contains text (warning response), show in ShardFailureOpenModalButton
|
||||
if (warning.text) {
|
||||
const text = toMountPoint(
|
||||
<>
|
||||
{warning.text}
|
||||
<EuiSpacer size="s" />
|
||||
<EuiTextAlign textAlign="right">
|
||||
<ShardFailureOpenModalButton
|
||||
theme={theme}
|
||||
title={title}
|
||||
getRequestMeta={() => ({
|
||||
request: request as ShardFailureRequest,
|
||||
response,
|
||||
})}
|
||||
/>
|
||||
</EuiTextAlign>
|
||||
</>,
|
||||
const [incompleteWarning] = incompleteWarnings as SearchResponseIncompleteWarning[];
|
||||
getNotifications().toasts.addWarning({
|
||||
title: incompleteWarning.message,
|
||||
text: toMountPoint(
|
||||
<EuiTextAlign textAlign="right">
|
||||
<OpenIncompleteResultsModalButton
|
||||
theme={theme}
|
||||
getRequestMeta={() => ({
|
||||
request,
|
||||
response,
|
||||
})}
|
||||
warning={incompleteWarning}
|
||||
/>
|
||||
</EuiTextAlign>,
|
||||
{ theme$: theme.theme$ }
|
||||
);
|
||||
|
||||
debouncedWarning(sessionId + warning.text, title, text);
|
||||
return;
|
||||
}
|
||||
|
||||
// timeout warning, or shard warning with no failure reason
|
||||
debouncedWarningWithoutReason(sessionId + title, title);
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -10,6 +10,7 @@ export * from './expressions';
|
|||
|
||||
export type {
|
||||
SearchResponseWarning,
|
||||
SearchResponseIncompleteWarning,
|
||||
ISearchSetup,
|
||||
ISearchStart,
|
||||
ISearchStartSearchSource,
|
||||
|
|
|
@ -142,7 +142,7 @@ describe('Search service', () => {
|
|||
|
||||
expect(notifications.toasts.addWarning).toBeCalledTimes(1);
|
||||
expect(notifications.toasts.addWarning).toBeCalledWith({
|
||||
title: '2 of 4 shards failed',
|
||||
title: 'The data might be incomplete or wrong.',
|
||||
text: expect.any(Function),
|
||||
});
|
||||
});
|
||||
|
@ -155,90 +155,6 @@ describe('Search service', () => {
|
|||
|
||||
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',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -243,7 +243,7 @@ export class SearchService implements Plugin<ISearchSetup, ISearchStart> {
|
|||
getConfig: uiSettings.get.bind(uiSettings),
|
||||
search,
|
||||
onResponse: (request, response, options) => {
|
||||
if (!options.disableShardFailureWarning) {
|
||||
if (!options.disableWarningToasts) {
|
||||
const { rawResponse } = response;
|
||||
|
||||
handleWarnings({
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
*/
|
||||
|
||||
import { estypes } from '@elastic/elasticsearch';
|
||||
import type { ClusterDetails } from '@kbn/es-types';
|
||||
import type { PackageInfo } from '@kbn/core/server';
|
||||
import { DataViewsContract } from '@kbn/data-views-plugin/common';
|
||||
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
|
||||
*/
|
||||
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: 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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'];
|
||||
};
|
||||
clusters: Record<string, ClusterDetails>;
|
||||
}
|
||||
|
||||
/**
|
||||
* A warning object for a search response with warnings
|
||||
* @public
|
||||
*/
|
||||
export type SearchResponseWarning =
|
||||
| SearchResponseTimeoutWarning
|
||||
| SearchResponseShardFailureWarning;
|
||||
export type SearchResponseWarning = SearchResponseIncompleteWarning;
|
||||
|
||||
/**
|
||||
* A callback function which can intercept warnings when passed to {@link showWarnings}. Pass `true` from the
|
||||
|
|
|
@ -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;
|
|
@ -6,7 +6,7 @@
|
|||
* 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> = {
|
||||
_shards: {
|
||||
|
@ -33,4 +33,4 @@ export const shardFailureResponse: estypes.SearchResponse<any> = {
|
|||
},
|
||||
],
|
||||
},
|
||||
} as any;
|
||||
} as unknown as estypes.SearchResponse<any>;
|
||||
|
|
|
@ -10,7 +10,6 @@ exports[`ShardFailureDescription renders matching snapshot given valid propertie
|
|||
grow={false}
|
||||
>
|
||||
<EuiDescriptionList
|
||||
className="shardFailureModal__desc"
|
||||
columnWidths={
|
||||
Array [
|
||||
1,
|
||||
|
@ -82,7 +81,6 @@ exports[`ShardFailureDescription should show more details when button is pressed
|
|||
grow={false}
|
||||
>
|
||||
<EuiDescriptionList
|
||||
className="shardFailureModal__desc"
|
||||
columnWidths={
|
||||
Array [
|
||||
1,
|
||||
|
|
|
@ -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>
|
||||
`;
|
|
@ -14,13 +14,13 @@ import { shardFailureResponse } from './__mocks__/shard_failure_response';
|
|||
|
||||
describe('ShardFailureDescription', () => {
|
||||
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} />);
|
||||
expect(component).toMatchSnapshot();
|
||||
});
|
||||
|
||||
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} />);
|
||||
await component.find(EuiButtonEmpty).simulate('click');
|
||||
expect(component).toMatchSnapshot();
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
*/
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import { estypes } from '@elastic/elasticsearch';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { css } from '@emotion/react';
|
||||
import { getFlattenedObject } from '@kbn/std';
|
||||
|
@ -17,7 +18,6 @@ import {
|
|||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
} from '@elastic/eui';
|
||||
import { ShardFailure } from './shard_failure_types';
|
||||
|
||||
/**
|
||||
* 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 flattendReason = getFlattenedObject(props.reason);
|
||||
|
@ -70,7 +70,7 @@ export function ShardFailureDescription(props: ShardFailure) {
|
|||
title: i18n.translate('data.search.searchSource.fetch.shardsFailedModal.indexTitle', {
|
||||
defaultMessage: 'Index',
|
||||
}),
|
||||
description: props.index,
|
||||
description: props.index ?? '',
|
||||
},
|
||||
{
|
||||
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', {
|
||||
defaultMessage: 'Node',
|
||||
}),
|
||||
description: props.node,
|
||||
description: props.node ?? '',
|
||||
},
|
||||
...reasonItems,
|
||||
]
|
||||
|
@ -99,7 +99,6 @@ export function ShardFailureDescription(props: ShardFailure) {
|
|||
columnWidths={[1, 6]}
|
||||
listItems={items}
|
||||
compressed
|
||||
className="shardFailureModal__desc"
|
||||
titleProps={{ className: 'shardFailureModal__descTitle' }}
|
||||
descriptionProps={{ className: 'shardFailureModal__descValue' }}
|
||||
/>
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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);
|
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -10,12 +10,12 @@ import React from 'react';
|
|||
import { shallowWithIntl } from '@kbn/test-jest-helpers';
|
||||
import { ShardFailureTable } from './shard_failure_table';
|
||||
import { shardFailureResponse } from './__mocks__/shard_failure_response';
|
||||
import { ShardFailure } from './shard_failure_types';
|
||||
|
||||
describe('ShardFailureTable', () => {
|
||||
it('renders matching snapshot given valid properties', () => {
|
||||
const failures = (shardFailureResponse._shards as any).failures as ShardFailure[];
|
||||
const component = shallowWithIntl(<ShardFailureTable failures={failures} />);
|
||||
const component = shallowWithIntl(
|
||||
<ShardFailureTable failures={shardFailureResponse._shards.failures!} />
|
||||
);
|
||||
expect(component).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -7,13 +7,13 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { estypes } from '@elastic/elasticsearch';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { css } from '@emotion/react';
|
||||
import { EuiInMemoryTable, EuiInMemoryTableProps, euiScreenReaderOnly } from '@elastic/eui';
|
||||
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;
|
||||
}
|
||||
|
||||
|
@ -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 columns = [
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -131,7 +131,7 @@ describe('esaggs expression function - server', () => {
|
|||
query: undefined,
|
||||
searchSessionId: 'abc123',
|
||||
searchSourceService: startDependencies.searchSource,
|
||||
disableShardWarnings: false,
|
||||
disableWarningToasts: false,
|
||||
timeFields: args.timeFields,
|
||||
timeRange: undefined,
|
||||
});
|
||||
|
|
|
@ -72,7 +72,7 @@ export function getFunctionDefinition({
|
|||
query: get(input, 'query', undefined) as any,
|
||||
searchSessionId: getSearchSessionId(),
|
||||
searchSourceService: searchSource,
|
||||
disableShardWarnings: false,
|
||||
disableWarningToasts: false,
|
||||
timeFields: args.timeFields,
|
||||
timeRange: get(input, 'timeRange', undefined),
|
||||
})
|
||||
|
|
|
@ -49,7 +49,8 @@
|
|||
"@kbn/core-saved-objects-server",
|
||||
"@kbn/core-saved-objects-utils-server",
|
||||
"@kbn/data-service",
|
||||
"@kbn/react-kibana-context-render"
|
||||
"@kbn/react-kibana-context-render",
|
||||
"@kbn/es-types"
|
||||
],
|
||||
"exclude": [
|
||||
"target/**/*",
|
||||
|
|
|
@ -17,7 +17,6 @@ export enum VIEW_MODE {
|
|||
AGGREGATED_LEVEL = 'aggregated',
|
||||
}
|
||||
|
||||
export const DISABLE_SHARD_FAILURE_WARNING = true;
|
||||
export const getDefaultRowsPerPage = (uiSettings: IUiSettingsClient): number => {
|
||||
return parseInt(uiSettings.get(SAMPLE_ROWS_PER_PAGE_SETTING), 10) || DEFAULT_ROWS_PER_PAGE;
|
||||
};
|
||||
|
|
|
@ -18,7 +18,6 @@ import { useExecutionContext } from '@kbn/kibana-react-plugin/public';
|
|||
import { generateFilters } from '@kbn/data-plugin/public';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { reportPerformanceMetricEvent } from '@kbn/ebt-tools';
|
||||
import { removeInterceptedWarningDuplicates } from '@kbn/search-response-warnings';
|
||||
import {
|
||||
DOC_TABLE_LEGACY,
|
||||
SEARCH_FIELDS_FROM_SOURCE,
|
||||
|
@ -177,12 +176,11 @@ export const ContextApp = ({ dataView, anchorId, referrer }: ContextAppProps) =>
|
|||
);
|
||||
|
||||
const interceptedWarnings = useMemo(
|
||||
() =>
|
||||
removeInterceptedWarningDuplicates([
|
||||
...(fetchedState.predecessorsInterceptedWarnings || []),
|
||||
...(fetchedState.anchorInterceptedWarnings || []),
|
||||
...(fetchedState.successorsInterceptedWarnings || []),
|
||||
]),
|
||||
() => [
|
||||
...(fetchedState.predecessorsInterceptedWarnings || []),
|
||||
...(fetchedState.anchorInterceptedWarnings || []),
|
||||
...(fetchedState.successorsInterceptedWarnings || []),
|
||||
],
|
||||
[
|
||||
fetchedState.predecessorsInterceptedWarnings,
|
||||
fetchedState.anchorInterceptedWarnings,
|
||||
|
|
|
@ -19,15 +19,15 @@ import {
|
|||
mockSuccessorHits,
|
||||
} from '../__mocks__/use_context_app_fetch';
|
||||
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 { DataView } from '@kbn/data-views-plugin/public';
|
||||
import { themeServiceMock } from '@kbn/core/public/mocks';
|
||||
import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public';
|
||||
|
||||
const mockInterceptedWarnings = searchResponseWarningsMock.map((originalWarning) => ({
|
||||
originalWarning,
|
||||
}));
|
||||
const mockInterceptedWarning = {
|
||||
originalWarning: searchResponseIncompleteWarningLocalCluster,
|
||||
};
|
||||
|
||||
const mockFilterManager = createFilterManagerMock();
|
||||
|
||||
|
@ -44,9 +44,7 @@ jest.mock('../services/context', () => {
|
|||
}
|
||||
return {
|
||||
rows: type === 'predecessors' ? mockPredecessorHits : mockSuccessorHits,
|
||||
interceptedWarnings: mockOverrideInterceptedWarnings
|
||||
? [mockInterceptedWarnings[type === 'predecessors' ? 0 : 1]]
|
||||
: undefined,
|
||||
interceptedWarnings: mockOverrideInterceptedWarnings ? [mockInterceptedWarning] : undefined,
|
||||
};
|
||||
},
|
||||
};
|
||||
|
@ -59,9 +57,7 @@ jest.mock('../services/anchor', () => ({
|
|||
}
|
||||
return {
|
||||
anchorRow: mockAnchorHit,
|
||||
interceptedWarnings: mockOverrideInterceptedWarnings
|
||||
? [mockInterceptedWarnings[2]]
|
||||
: undefined,
|
||||
interceptedWarnings: mockOverrideInterceptedWarnings ? [mockInterceptedWarning] : undefined,
|
||||
};
|
||||
},
|
||||
}));
|
||||
|
@ -228,13 +224,11 @@ describe('test useContextAppFetch', () => {
|
|||
expect(result.current.fetchedState.predecessors).toEqual(mockPredecessorHits);
|
||||
expect(result.current.fetchedState.successors).toEqual(mockSuccessorHits);
|
||||
expect(result.current.fetchedState.predecessorsInterceptedWarnings).toEqual([
|
||||
mockInterceptedWarnings[0],
|
||||
mockInterceptedWarning,
|
||||
]);
|
||||
expect(result.current.fetchedState.successorsInterceptedWarnings).toEqual([
|
||||
mockInterceptedWarnings[1],
|
||||
]);
|
||||
expect(result.current.fetchedState.anchorInterceptedWarnings).toEqual([
|
||||
mockInterceptedWarnings[2],
|
||||
mockInterceptedWarning,
|
||||
]);
|
||||
expect(result.current.fetchedState.anchorInterceptedWarnings).toEqual([mockInterceptedWarning]);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -10,7 +10,7 @@ import { SortDirection } from '@kbn/data-plugin/public';
|
|||
import { createSearchSourceStub } from './_stubs';
|
||||
import { fetchAnchor, updateSearchSource } from './anchor';
|
||||
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 { discoverServiceMock } from '../../../__mocks__/services';
|
||||
|
||||
|
@ -206,7 +206,7 @@ describe('context app', function () {
|
|||
).then(({ anchorRow, interceptedWarnings }) => {
|
||||
expect(anchorRow).toHaveProperty('raw._id', '1');
|
||||
expect(anchorRow).toHaveProperty('isAnchor', true);
|
||||
expect(interceptedWarnings).toBeUndefined();
|
||||
expect(interceptedWarnings).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -216,20 +216,10 @@ describe('context app', function () {
|
|||
{ _id: '3', _index: 't' },
|
||||
]);
|
||||
|
||||
const mockWarnings = [
|
||||
{
|
||||
originalWarning: searchResponseTimeoutWarningMock,
|
||||
},
|
||||
];
|
||||
|
||||
const services = discoverServiceMock;
|
||||
services.data.search.showWarnings = jest.fn((adapter, callback) => {
|
||||
// @ts-expect-error for empty meta
|
||||
callback?.(mockWarnings[0].originalWarning, {});
|
||||
|
||||
// plus duplicates
|
||||
// @ts-expect-error for empty meta
|
||||
callback?.(mockWarnings[0].originalWarning, {});
|
||||
callback?.(searchResponseIncompleteWarningLocalCluster, {});
|
||||
});
|
||||
|
||||
return fetchAnchor(
|
||||
|
@ -242,7 +232,7 @@ describe('context app', function () {
|
|||
).then(({ anchorRow, interceptedWarnings }) => {
|
||||
expect(anchorRow).toHaveProperty('raw._id', '1');
|
||||
expect(anchorRow).toHaveProperty('isAnchor', true);
|
||||
expect(interceptedWarnings).toEqual(mockWarnings);
|
||||
expect(interceptedWarnings?.length).toBe(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -17,7 +17,6 @@ import {
|
|||
type SearchResponseInterceptedWarning,
|
||||
} from '@kbn/search-response-warnings';
|
||||
import type { DiscoverServices } from '../../../build_services';
|
||||
import { DISABLE_SHARD_FAILURE_WARNING } from '../../../../common/constants';
|
||||
|
||||
export async function fetchAnchor(
|
||||
anchorId: string,
|
||||
|
@ -35,7 +34,7 @@ export async function fetchAnchor(
|
|||
const adapter = new RequestAdapter();
|
||||
const { rawResponse } = await lastValueFrom(
|
||||
searchSource.fetch$({
|
||||
disableShardFailureWarning: DISABLE_SHARD_FAILURE_WARNING,
|
||||
disableWarningToasts: true,
|
||||
inspector: {
|
||||
adapter,
|
||||
title: 'anchor',
|
||||
|
@ -56,9 +55,6 @@ export async function fetchAnchor(
|
|||
interceptedWarnings: getSearchResponseInterceptedWarnings({
|
||||
services,
|
||||
adapter,
|
||||
options: {
|
||||
disableShardFailureWarning: DISABLE_SHARD_FAILURE_WARNING,
|
||||
},
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
|
|
@ -16,6 +16,7 @@ import { Query } from '@kbn/es-query';
|
|||
import { fetchSurroundingDocs, SurrDocType } from './context';
|
||||
import { buildDataTableRecord, buildDataTableRecordList } from '@kbn/discover-utils';
|
||||
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 ANCHOR_TIMESTAMP = new Date(MS_PER_DAY).toJSON();
|
||||
|
@ -257,28 +258,13 @@ describe('context successors', function () {
|
|||
const removeFieldsSpy = mockSearchSource.removeField.withArgs('fieldsFromSource');
|
||||
expect(removeFieldsSpy.calledOnce).toBe(true);
|
||||
expect(setFieldsSpy.calledOnce).toBe(true);
|
||||
expect(interceptedWarnings).toBeUndefined();
|
||||
expect(interceptedWarnings).toEqual([]);
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
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(() => {
|
||||
mockSearchSource = createContextSearchSourceStub('@timestamp');
|
||||
|
||||
|
@ -288,11 +274,7 @@ describe('context successors', function () {
|
|||
createEmpty: jest.fn().mockImplementation(() => mockSearchSource),
|
||||
},
|
||||
showWarnings: jest.fn((adapter, callback) => {
|
||||
callback(mockWarnings[0].originalWarning, {});
|
||||
callback(mockWarnings[1].originalWarning, {});
|
||||
// plus duplicates
|
||||
callback(mockWarnings[0].originalWarning, {});
|
||||
callback(mockWarnings[1].originalWarning, {});
|
||||
callback(searchResponseIncompleteWarningLocalCluster, {});
|
||||
}),
|
||||
},
|
||||
} as unknown as DataPublicPluginStart;
|
||||
|
@ -345,7 +327,7 @@ describe('context successors', function () {
|
|||
buildDataTableRecordList(mockSearchSource._stubHits.slice(-3), dataView)
|
||||
);
|
||||
expect(dataPluginMock.search.showWarnings).toHaveBeenCalledTimes(1);
|
||||
expect(interceptedWarnings).toEqual(mockWarnings);
|
||||
expect(interceptedWarnings?.length).toBe(1);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
|
|
@ -9,10 +9,7 @@ import type { Filter } from '@kbn/es-query';
|
|||
import { DataView } from '@kbn/data-views-plugin/public';
|
||||
import { DataPublicPluginStart, ISearchSource } from '@kbn/data-plugin/public';
|
||||
import type { DataTableRecord } from '@kbn/discover-utils/types';
|
||||
import {
|
||||
removeInterceptedWarningDuplicates,
|
||||
type SearchResponseInterceptedWarning,
|
||||
} from '@kbn/search-response-warnings';
|
||||
import type { SearchResponseInterceptedWarning } from '@kbn/search-response-warnings';
|
||||
import { reverseSortDir, SortDirection } from '../utils/sorting';
|
||||
import { convertIsoToMillis, extractNanos } from '../utils/date_conversion';
|
||||
import { fetchHitsInInterval } from '../utils/fetch_hits_in_interval';
|
||||
|
@ -126,7 +123,7 @@ export async function fetchSurroundingDocs(
|
|||
|
||||
return {
|
||||
rows,
|
||||
interceptedWarnings: removeInterceptedWarningDuplicates(interceptedWarnings),
|
||||
interceptedWarnings,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -17,7 +17,6 @@ import {
|
|||
import { RequestAdapter } from '@kbn/inspector-plugin/common';
|
||||
import { convertTimeValueToIso } from './date_conversion';
|
||||
import { IntervalValue } from './generate_intervals';
|
||||
import { DISABLE_SHARD_FAILURE_WARNING } from '../../../../common/constants';
|
||||
import type { SurrDocType } from '../services/context';
|
||||
import type { DiscoverServices } from '../../../build_services';
|
||||
|
||||
|
@ -91,7 +90,7 @@ export async function fetchHitsInInterval(
|
|||
.setField('sort', sort)
|
||||
.setField('version', true)
|
||||
.fetch$({
|
||||
disableShardFailureWarning: DISABLE_SHARD_FAILURE_WARNING,
|
||||
disableWarningToasts: true,
|
||||
inspector: {
|
||||
adapter,
|
||||
title: type,
|
||||
|
@ -107,9 +106,6 @@ export async function fetchHitsInInterval(
|
|||
interceptedWarnings: getSearchResponseInterceptedWarnings({
|
||||
services,
|
||||
adapter,
|
||||
options: {
|
||||
disableShardFailureWarning: DISABLE_SHARD_FAILURE_WARNING,
|
||||
},
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
|
|
@ -26,7 +26,7 @@ import {
|
|||
import { filter } from 'rxjs/operators';
|
||||
import { dataViewMock, esHitsMockWithSort } from '@kbn/discover-utils/src/__mocks__';
|
||||
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', () => {
|
||||
test('sendCompleteMsg', (done) => {
|
||||
|
@ -103,15 +103,13 @@ describe('test useSavedSearch message generators', () => {
|
|||
if (value.fetchStatus !== FetchStatus.LOADING_MORE) {
|
||||
expect(value.fetchStatus).toBe(FetchStatus.COMPLETE);
|
||||
expect(value.result).toStrictEqual([...initialRecords, ...moreRecords]);
|
||||
expect(value.interceptedWarnings).toHaveLength(searchResponseWarningsMock.length);
|
||||
expect(value.interceptedWarnings).toHaveLength(1);
|
||||
done();
|
||||
}
|
||||
});
|
||||
sendLoadingMoreFinishedMsg(documents$, {
|
||||
moreRecords,
|
||||
interceptedWarnings: searchResponseWarningsMock.map((warning) => ({
|
||||
originalWarning: warning,
|
||||
})),
|
||||
interceptedWarnings: [{ originalWarning: searchResponseIncompleteWarningLocalCluster }],
|
||||
});
|
||||
});
|
||||
test('sendLoadingMoreFinishedMsg after an exception', (done) => {
|
||||
|
@ -121,9 +119,7 @@ describe('test useSavedSearch message generators', () => {
|
|||
const documents$ = new BehaviorSubject<DataDocumentsMsg>({
|
||||
fetchStatus: FetchStatus.LOADING_MORE,
|
||||
result: initialRecords,
|
||||
interceptedWarnings: searchResponseWarningsMock.map((warning) => ({
|
||||
originalWarning: warning,
|
||||
})),
|
||||
interceptedWarnings: [{ originalWarning: searchResponseIncompleteWarningLocalCluster }],
|
||||
});
|
||||
documents$.subscribe((value) => {
|
||||
if (value.fetchStatus !== FetchStatus.LOADING_MORE) {
|
||||
|
|
|
@ -25,7 +25,7 @@ import { fetchDocuments } from './fetch_documents';
|
|||
import { fetchTextBased } from './fetch_text_based';
|
||||
import { buildDataTableRecord } from '@kbn/discover-utils';
|
||||
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', () => ({
|
||||
fetchDocuments: jest.fn().mockResolvedValue([]),
|
||||
|
@ -296,9 +296,11 @@ describe('test fetchAll', () => {
|
|||
const initialRecords = [records[0], records[1]];
|
||||
const moreRecords = [records[2], records[3]];
|
||||
|
||||
const interceptedWarnings = searchResponseWarningsMock.map((warning) => ({
|
||||
originalWarning: warning,
|
||||
}));
|
||||
const interceptedWarnings = [
|
||||
{
|
||||
originalWarning: searchResponseIncompleteWarningLocalCluster,
|
||||
},
|
||||
];
|
||||
|
||||
test('should add more records', async () => {
|
||||
const collectDocuments = subjectCollector(subjects.documents$);
|
||||
|
|
|
@ -37,17 +37,20 @@ describe('test fetchDocuments', () => {
|
|||
const documents = hits.map((hit) => buildDataTableRecord(hit, dataViewMock));
|
||||
savedSearchMock.searchSource.fetch$ = <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,
|
||||
});
|
||||
});
|
||||
|
||||
test('rejects on query failure', () => {
|
||||
test('rejects on query failure', async () => {
|
||||
savedSearchMock.searchSource.fetch$ = () => throwErrorRx(() => new Error('Oh noes!'));
|
||||
|
||||
expect(fetchDocuments(savedSearchMock.searchSource, getDeps())).rejects.toEqual(
|
||||
new Error('Oh noes!')
|
||||
);
|
||||
try {
|
||||
await fetchDocuments(savedSearchMock.searchSource, getDeps());
|
||||
} catch (e) {
|
||||
expect(e).toEqual(new Error('Oh noes!'));
|
||||
}
|
||||
});
|
||||
|
||||
test('passes a correct session id', async () => {
|
||||
|
@ -66,7 +69,8 @@ describe('test fetchDocuments', () => {
|
|||
|
||||
jest.spyOn(searchSourceRegular, 'fetch$');
|
||||
|
||||
expect(fetchDocuments(searchSourceRegular, deps)).resolves.toEqual({
|
||||
expect(await fetchDocuments(searchSourceRegular, deps)).toEqual({
|
||||
interceptedWarnings: [],
|
||||
records: documents,
|
||||
});
|
||||
|
||||
|
@ -84,7 +88,8 @@ describe('test fetchDocuments', () => {
|
|||
|
||||
jest.spyOn(searchSourceForLoadMore, 'fetch$');
|
||||
|
||||
expect(fetchDocuments(searchSourceForLoadMore, deps)).resolves.toEqual({
|
||||
expect(await fetchDocuments(searchSourceForLoadMore, deps)).toEqual({
|
||||
interceptedWarnings: [],
|
||||
records: documents,
|
||||
});
|
||||
|
||||
|
|
|
@ -13,7 +13,6 @@ import { SAMPLE_SIZE_SETTING, buildDataTableRecordList } from '@kbn/discover-uti
|
|||
import type { EsHitRecord } from '@kbn/discover-utils/types';
|
||||
import { getSearchResponseInterceptedWarnings } from '@kbn/search-response-warnings';
|
||||
import type { RecordsFetchResponse } from '../../types';
|
||||
import { DISABLE_SHARD_FAILURE_WARNING } from '../../../../common/constants';
|
||||
import { FetchDeps } from './fetch_all';
|
||||
|
||||
/**
|
||||
|
@ -60,7 +59,7 @@ export const fetchDocuments = (
|
|||
}),
|
||||
},
|
||||
executionContext,
|
||||
disableShardFailureWarning: DISABLE_SHARD_FAILURE_WARNING,
|
||||
disableWarningToasts: true,
|
||||
})
|
||||
.pipe(
|
||||
filter((res) => isCompleteResponse(res)),
|
||||
|
@ -75,9 +74,6 @@ export const fetchDocuments = (
|
|||
? getSearchResponseInterceptedWarnings({
|
||||
services,
|
||||
adapter,
|
||||
options: {
|
||||
disableShardFailureWarning: DISABLE_SHARD_FAILURE_WARNING,
|
||||
},
|
||||
})
|
||||
: [];
|
||||
|
||||
|
|
|
@ -41,7 +41,7 @@ export const ErrorCallout = ({
|
|||
const { euiTheme } = useEuiTheme();
|
||||
|
||||
const showErrorMessage = i18n.translate('discover.errorCalloutShowErrorMessage', {
|
||||
defaultMessage: 'Show details',
|
||||
defaultMessage: 'View details',
|
||||
});
|
||||
|
||||
const overrideDisplay = getSearchErrorOverrideDisplay({
|
||||
|
|
|
@ -62,11 +62,7 @@ import {
|
|||
import type { UnifiedDataTableProps } from '@kbn/unified-data-table';
|
||||
import type { UnifiedDataTableSettings } from '@kbn/unified-data-table';
|
||||
import { columnActions } from '@kbn/unified-data-table';
|
||||
import {
|
||||
VIEW_MODE,
|
||||
DISABLE_SHARD_FAILURE_WARNING,
|
||||
getDefaultRowsPerPage,
|
||||
} from '../../common/constants';
|
||||
import { VIEW_MODE, getDefaultRowsPerPage } from '../../common/constants';
|
||||
import type { ISearchEmbeddable, SearchInput, SearchOutput } from './types';
|
||||
import type { DiscoverServices } from '../build_services';
|
||||
import { getSortForEmbeddable, SortPair } from '../utils/sorting';
|
||||
|
@ -371,7 +367,7 @@ export class SavedSearchEmbeddable
|
|||
}),
|
||||
},
|
||||
executionContext,
|
||||
disableShardFailureWarning: DISABLE_SHARD_FAILURE_WARNING,
|
||||
disableWarningToasts: true,
|
||||
})
|
||||
);
|
||||
|
||||
|
@ -379,9 +375,6 @@ export class SavedSearchEmbeddable
|
|||
searchProps.interceptedWarnings = getSearchResponseInterceptedWarnings({
|
||||
services: this.services,
|
||||
adapter: this.inspectorAdapters.requests,
|
||||
options: {
|
||||
disableShardFailureWarning: DISABLE_SHARD_FAILURE_WARNING,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -206,7 +206,7 @@ const fetchTotalHitsSearchSource = async ({
|
|||
executionContext: {
|
||||
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(
|
||||
filter((res) => isCompleteResponse(res)),
|
||||
|
|
|
@ -19,6 +19,7 @@ import { KibanaThemeProvider } from '@kbn/kibana-react-plugin/public';
|
|||
import { TimefilterContract } from '@kbn/data-plugin/public';
|
||||
import type { DataView } from '@kbn/data-views-plugin/public';
|
||||
import { Warnings } from '@kbn/charts-plugin/public';
|
||||
import { hasUnsupportedDownsampledAggregationFailure } from '@kbn/search-response-warnings';
|
||||
import {
|
||||
Adapters,
|
||||
AttributeService,
|
||||
|
@ -351,11 +352,13 @@ export class VisualizeEmbeddable
|
|||
this.deps
|
||||
.start()
|
||||
.plugins.data.search.showWarnings(this.getInspectorAdapters()!.requests!, (warning) => {
|
||||
if (
|
||||
warning.type === 'shard_failure' &&
|
||||
warning.reason.type === 'unsupported_aggregation_on_downsampled_index'
|
||||
) {
|
||||
warnings.push(warning.reason.reason || warning.message);
|
||||
if (hasUnsupportedDownsampledAggregationFailure(warning)) {
|
||||
warnings.push(
|
||||
i18n.translate('visualizations.embeddable.tsdbRollupWarning', {
|
||||
defaultMessage:
|
||||
'Visualization uses a function that is unsupported by rolled up data. Select a different function or change the time range.',
|
||||
})
|
||||
);
|
||||
return true;
|
||||
}
|
||||
if (this.vis.type.suppressWarnings?.()) {
|
||||
|
@ -582,7 +585,7 @@ export class VisualizeEmbeddable
|
|||
timeRange: this.timeRange,
|
||||
query: this.input.query,
|
||||
filters: this.input.filters,
|
||||
disableShardWarnings: true,
|
||||
disableWarningToasts: true,
|
||||
},
|
||||
variables: {
|
||||
embeddableTitle: this.getTitle(),
|
||||
|
|
|
@ -63,7 +63,8 @@
|
|||
"@kbn/content-management-table-list-view",
|
||||
"@kbn/content-management-utils",
|
||||
"@kbn/serverless",
|
||||
"@kbn/no-data-page-plugin"
|
||||
"@kbn/no-data-page-plugin",
|
||||
"@kbn/search-response-warnings"
|
||||
],
|
||||
"exclude": [
|
||||
"target/**/*",
|
||||
|
|
|
@ -100,7 +100,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
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');
|
||||
|
||||
// wait for response - toasts appear before the response is rendered
|
||||
|
@ -113,30 +113,25 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
// toasts
|
||||
const toasts = await find.allByCssSelector(toastsSelector);
|
||||
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) => {
|
||||
expect(await t.getVisibleText()).to.eql(expects[index]);
|
||||
});
|
||||
|
||||
// click "see full error" button in the toast
|
||||
const [openShardModalButton] = await testSubjects.findAll('openShardFailureModalBtn');
|
||||
const [openShardModalButton] = await testSubjects.findAll('openIncompleteResultsModalBtn');
|
||||
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
|
||||
await testSubjects.click('shardFailuresModalRequestButton');
|
||||
const requestBlock = await testSubjects.find('shardsFailedModalRequestBlock');
|
||||
await testSubjects.click('showRequestButton');
|
||||
const requestBlock = await testSubjects.find('incompleteResultsModalRequestBlock');
|
||||
expect(await requestBlock.getVisibleText()).to.contain(testRollupField);
|
||||
// response
|
||||
await testSubjects.click('shardFailuresModalResponseButton');
|
||||
const responseBlock = await testSubjects.find('shardsFailedModalResponseBlock');
|
||||
await testSubjects.click('showResponseButton');
|
||||
const responseBlock = await testSubjects.find('incompleteResultsModalResponseBlock');
|
||||
expect(await responseBlock.getVisibleText()).to.contain(shardFailureReason);
|
||||
|
||||
await testSubjects.click('closeShardFailureModal');
|
||||
await testSubjects.click('closeIncompleteResultsModal');
|
||||
|
||||
// response tab
|
||||
assert(response && response._shards.failures);
|
||||
|
@ -154,7 +149,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
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');
|
||||
|
||||
// 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);
|
||||
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) => {
|
||||
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
|
||||
const warnings = await getTestJson('warningsTab', 'warningsCodeBlock');
|
||||
expect(warnings).to.eql([
|
||||
{
|
||||
type: 'shard_failure',
|
||||
message: '2 of 4 shards failed',
|
||||
reason: { reason: shardFailureReason, type: shardFailureType },
|
||||
text: 'The data might be incomplete or wrong.',
|
||||
},
|
||||
]);
|
||||
expect(warnings.length).to.be(1);
|
||||
expect(warnings[0].type).to.be('incomplete');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -64,7 +64,7 @@ import {
|
|||
|
||||
import {
|
||||
getFiltersInLayer,
|
||||
getShardFailuresWarningMessages,
|
||||
getSearchWarningMessages,
|
||||
getVisualDefaultsForLayer,
|
||||
isColumnInvalid,
|
||||
cloneLayer,
|
||||
|
@ -811,7 +811,7 @@ export function getFormBasedDatasource({
|
|||
},
|
||||
|
||||
getSearchWarningMessages: (state, warning, request, response) => {
|
||||
return [...getShardFailuresWarningMessages(state, warning, request, response, core.theme)];
|
||||
return [...getSearchWarningMessages(state, warning, request, response, core.theme)];
|
||||
},
|
||||
|
||||
checkIntegrity: (state, indexPatterns) => {
|
||||
|
|
|
@ -9,6 +9,7 @@ import React from 'react';
|
|||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import type { DocLinksStart, ThemeServiceStart } from '@kbn/core/public';
|
||||
import { hasUnsupportedDownsampledAggregationFailure } from '@kbn/search-response-warnings';
|
||||
import type { DatatableUtilitiesService } from '@kbn/data-plugin/common';
|
||||
import { TimeRange } from '@kbn/es-query';
|
||||
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 { SearchRequest } from '@kbn/data-plugin/common';
|
||||
|
||||
import {
|
||||
SearchResponseWarning,
|
||||
ShardFailureOpenModalButton,
|
||||
ShardFailureRequest,
|
||||
} from '@kbn/data-plugin/public';
|
||||
import { SearchResponseWarning, OpenIncompleteResultsModalButton } from '@kbn/data-plugin/public';
|
||||
|
||||
import { estypes } from '@elastic/elasticsearch';
|
||||
import { isQueryValid } from '@kbn/visualization-ui-components';
|
||||
|
@ -260,7 +257,7 @@ const accuracyModeEnabledWarning = (
|
|||
),
|
||||
});
|
||||
|
||||
export function getShardFailuresWarningMessages(
|
||||
export function getSearchWarningMessages(
|
||||
state: FormBasedPersistedState,
|
||||
warning: SearchResponseWarning,
|
||||
request: SearchRequest,
|
||||
|
@ -268,10 +265,9 @@ export function getShardFailuresWarningMessages(
|
|||
theme: ThemeServiceStart
|
||||
): UserMessage[] {
|
||||
if (state) {
|
||||
if (warning.type === 'shard_failure') {
|
||||
switch (warning.reason.type) {
|
||||
case 'unsupported_aggregation_on_downsampled_index':
|
||||
return Object.values(state.layers).flatMap((layer) =>
|
||||
if (warning.type === 'incomplete') {
|
||||
return hasUnsupportedDownsampledAggregationFailure(warning)
|
||||
? Object.values(state.layers).flatMap((layer) =>
|
||||
uniq(
|
||||
Object.values(layer.columns)
|
||||
.filter((col) =>
|
||||
|
@ -302,40 +298,33 @@ export function getShardFailuresWarningMessages(
|
|||
}),
|
||||
} as UserMessage)
|
||||
)
|
||||
);
|
||||
default:
|
||||
return [
|
||||
)
|
||||
: [
|
||||
{
|
||||
uniqueId: `shard_failure`,
|
||||
uniqueId: `incomplete`,
|
||||
severity: 'warning',
|
||||
fixableInEditor: true,
|
||||
displayLocations: [{ id: 'toolbar' }, { id: 'embeddableBadge' }],
|
||||
shortMessage: '',
|
||||
longMessage: (
|
||||
<>
|
||||
<EuiText size="s">
|
||||
<strong>{warning.message}</strong>
|
||||
<p>{warning.text}</p>
|
||||
</EuiText>
|
||||
<EuiText size="s">{warning.message}</EuiText>
|
||||
<EuiSpacer size="s" />
|
||||
{warning.text ? (
|
||||
<ShardFailureOpenModalButton
|
||||
theme={theme}
|
||||
title={warning.message}
|
||||
size="m"
|
||||
getRequestMeta={() => ({
|
||||
request: request as ShardFailureRequest,
|
||||
response,
|
||||
})}
|
||||
color="primary"
|
||||
isButtonEmpty={true}
|
||||
/>
|
||||
) : null}
|
||||
<OpenIncompleteResultsModalButton
|
||||
theme={theme}
|
||||
warning={warning}
|
||||
size="m"
|
||||
getRequestMeta={() => ({
|
||||
request,
|
||||
response,
|
||||
})}
|
||||
color="primary"
|
||||
isButtonEmpty={true}
|
||||
/>
|
||||
</>
|
||||
),
|
||||
} as UserMessage,
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
return [];
|
||||
|
|
|
@ -729,7 +729,7 @@ export const VisualizationWrapper = ({
|
|||
to: context.dateRange.toDate,
|
||||
},
|
||||
filters: context.filters,
|
||||
disableShardWarnings: true,
|
||||
disableWarningToasts: true,
|
||||
}),
|
||||
[context]
|
||||
);
|
||||
|
|
|
@ -1196,7 +1196,7 @@ export class Embeddable
|
|||
this.savedVis.state.filters,
|
||||
this.savedVis.references
|
||||
),
|
||||
disableShardWarnings: true,
|
||||
disableWarningToasts: true,
|
||||
};
|
||||
|
||||
if (input.query) {
|
||||
|
|
|
@ -62,7 +62,7 @@ export const selectExecutionContextSearch = createSelector(selectExecutionContex
|
|||
to: res.dateRange.toDate,
|
||||
},
|
||||
filters: res.filters,
|
||||
disableShardWarnings: true,
|
||||
disableWarningToasts: true,
|
||||
}));
|
||||
|
||||
const selectInjectedDependencies = (_state: LensState, dependencies: unknown) => dependencies;
|
||||
|
|
|
@ -349,29 +349,26 @@ export const getSearchWarningMessages = (
|
|||
searchService: ISearchStart;
|
||||
}
|
||||
): UserMessage[] => {
|
||||
const warningsMap: Map<string, UserMessage[]> = new Map();
|
||||
const userMessages: UserMessage[] = [];
|
||||
|
||||
deps.searchService.showWarnings(adapter, (warning, meta) => {
|
||||
const { request, response, requestId } = meta;
|
||||
const { request, response } = meta;
|
||||
|
||||
const warningMessages = datasource.getSearchWarningMessages?.(
|
||||
const userMessagesFromWarning = datasource.getSearchWarningMessages?.(
|
||||
state,
|
||||
warning,
|
||||
request,
|
||||
response
|
||||
);
|
||||
|
||||
if (warningMessages?.length) {
|
||||
const key = (requestId ?? '') + warning.type + warning.reason?.type ?? '';
|
||||
if (!warningsMap.has(key)) {
|
||||
warningsMap.set(key, warningMessages);
|
||||
}
|
||||
if (userMessagesFromWarning?.length) {
|
||||
userMessages.push(...userMessagesFromWarning);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
return [...warningsMap.values()].flat();
|
||||
return userMessages;
|
||||
};
|
||||
|
||||
function getSafeLabel(label: string) {
|
||||
|
|
|
@ -86,6 +86,7 @@
|
|||
"@kbn/content-management-utils",
|
||||
"@kbn/serverless",
|
||||
"@kbn/ebt-tools",
|
||||
"@kbn/search-response-warnings",
|
||||
],
|
||||
"exclude": [
|
||||
"target/**/*",
|
||||
|
|
|
@ -1317,7 +1317,6 @@
|
|||
"data.search.aggs.rareTerms.aggTypesLabel": "Termes rares de {fieldName}",
|
||||
"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.searchSource.fetch.shardsFailedNotificationMessage": "Échec de {shardsFailed} des {shardsTotal} partitions",
|
||||
"data.search.searchSource.indexPatternIdDescription": "ID dans l'index {kibanaIndexPattern}.",
|
||||
"data.search.searchSource.queryTimeValue": "{queryTime} 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.dataViewIdLabel": "ID de 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.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.hitsLabel": "Résultats",
|
||||
"data.search.searchSource.hitsTotalDescription": "Le nombre de documents correspondant à la requête.",
|
||||
|
|
|
@ -1331,7 +1331,6 @@
|
|||
"data.search.aggs.rareTerms.aggTypesLabel": "{fieldName}の希少な用語",
|
||||
"data.search.es_search.queryTimeValue": "{queryTime}ms",
|
||||
"data.search.functions.geoBoundingBox.arguments.error": "次のパラメーターのグループの1つ以上を指定する必要があります:{parameters}。",
|
||||
"data.search.searchSource.fetch.shardsFailedNotificationMessage": "{shardsTotal}件中{shardsFailed}件のシャードでエラーが発生しました",
|
||||
"data.search.searchSource.indexPatternIdDescription": "{kibanaIndexPattern}インデックス内のIDです。",
|
||||
"data.search.searchSource.queryTimeValue": "{queryTime}ms",
|
||||
"data.search.searchSource.requestTimeValue": "{requestTime}ms",
|
||||
|
@ -2078,15 +2077,7 @@
|
|||
"data.search.searchSource.dataViewDescription": "照会されたデータビュー。",
|
||||
"data.search.searchSource.dataViewIdLabel": "データビューID",
|
||||
"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.shardsFailedNotificationDescription": "データが不完全か誤りの可能性があります。",
|
||||
"data.search.searchSource.hitsDescription": "クエリにより返されたドキュメントの数です。",
|
||||
"data.search.searchSource.hitsLabel": "ヒット数",
|
||||
"data.search.searchSource.hitsTotalDescription": "クエリに一致するドキュメントの数です。",
|
||||
|
|
|
@ -1331,7 +1331,6 @@
|
|||
"data.search.aggs.rareTerms.aggTypesLabel": "{fieldName} 的稀有词",
|
||||
"data.search.es_search.queryTimeValue": "{queryTime}ms",
|
||||
"data.search.functions.geoBoundingBox.arguments.error": "必须至少提供一个以下参数组:{parameters}。",
|
||||
"data.search.searchSource.fetch.shardsFailedNotificationMessage": "{shardsTotal} 个分片有 {shardsFailed} 个失败",
|
||||
"data.search.searchSource.indexPatternIdDescription": "{kibanaIndexPattern} 索引中的 ID。",
|
||||
"data.search.searchSource.queryTimeValue": "{queryTime}ms",
|
||||
"data.search.searchSource.requestTimeValue": "{requestTime}ms",
|
||||
|
@ -2078,15 +2077,7 @@
|
|||
"data.search.searchSource.dataViewDescription": "被查询的数据视图。",
|
||||
"data.search.searchSource.dataViewIdLabel": "数据视图 ID",
|
||||
"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.shardsFailedNotificationDescription": "数据可能不完整或有错误。",
|
||||
"data.search.searchSource.hitsDescription": "查询返回的文档数目。",
|
||||
"data.search.searchSource.hitsLabel": "命中数",
|
||||
"data.search.searchSource.hitsTotalDescription": "与查询匹配的文档数目。",
|
||||
|
|
|
@ -27,7 +27,7 @@ export default function ({ getService, getPageObjects }) {
|
|||
const security = getService('security');
|
||||
const dashboardAddPanel = getService('dashboardAddPanel');
|
||||
|
||||
describe('async search with scripted fields', function () {
|
||||
describe('search with scripted fields', function () {
|
||||
this.tags(['skipFirefox']);
|
||||
|
||||
before(async function () {
|
||||
|
@ -51,7 +51,7 @@ export default function ({ getService, getPageObjects }) {
|
|||
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 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'
|
||||
);
|
||||
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([
|
||||
'test_logstash_reader',
|
||||
'global_discover_all',
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue