mirror of
https://github.com/elastic/kibana.git
synced 2025-06-27 10:40:07 -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(
|
const result = await lastValueFrom(
|
||||||
searchSource.fetch$({
|
searchSource.fetch$({
|
||||||
abortSignal: abortController.signal,
|
abortSignal: abortController.signal,
|
||||||
disableShardFailureWarning: !showWarningToastNotifications,
|
disableWarningToasts: !showWarningToastNotifications,
|
||||||
inspector,
|
inspector,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
setRawResponse(result.rawResponse);
|
setRawResponse(result.rawResponse);
|
||||||
|
|
||||||
/* Here is an example of using showWarnings on the search service, using an optional callback to
|
/*
|
||||||
* intercept the warnings before notification warnings are shown.
|
* Set disableWarningToasts to true to disable warning toasts and customize warning display.
|
||||||
*
|
* Then use showWarnings to customize warning notification.
|
||||||
* Suppressing the shard failure warning notification from appearing by default requires setting
|
|
||||||
* { disableShardFailureWarning: true } in the SearchSourceSearchOptions passed to $fetch
|
|
||||||
*/
|
*/
|
||||||
if (showWarningToastNotifications) {
|
if (showWarningToastNotifications) {
|
||||||
setWarningContents([]);
|
setWarningContents([]);
|
||||||
|
@ -498,7 +496,7 @@ export const SearchExamplesApp = ({
|
||||||
{' '}
|
{' '}
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id="searchExamples.warningsObject"
|
id="searchExamples.warningsObject"
|
||||||
defaultMessage="Timeout and shard failure warnings for high-level search may be handled in a callback to the showWarnings method on the search service."
|
defaultMessage="Search warnings may optionally be handed with search service showWarnings method."
|
||||||
/>{' '}
|
/>{' '}
|
||||||
</EuiText>{' '}
|
</EuiText>{' '}
|
||||||
<EuiProgress value={loaded} max={total} size="xs" data-test-subj="progressBar" />{' '}
|
<EuiProgress value={loaded} max={total} size="xs" data-test-subj="progressBar" />{' '}
|
||||||
|
|
|
@ -18,4 +18,5 @@ export type {
|
||||||
AggregationResultOfMap,
|
AggregationResultOfMap,
|
||||||
ESFilter,
|
ESFilter,
|
||||||
MaybeReadonlyArray,
|
MaybeReadonlyArray,
|
||||||
|
ClusterDetails,
|
||||||
} from './src';
|
} from './src';
|
||||||
|
|
|
@ -11,6 +11,7 @@ import {
|
||||||
AggregateOf as AggregationResultOf,
|
AggregateOf as AggregationResultOf,
|
||||||
AggregateOfMap as AggregationResultOfMap,
|
AggregateOfMap as AggregationResultOfMap,
|
||||||
SearchHit,
|
SearchHit,
|
||||||
|
ClusterDetails,
|
||||||
} from './search';
|
} from './search';
|
||||||
|
|
||||||
export type ESFilter = estypes.QueryDslQueryContainer;
|
export type ESFilter = estypes.QueryDslQueryContainer;
|
||||||
|
@ -34,4 +35,10 @@ export type ESSearchResponse<
|
||||||
TOptions extends { restTotalHitsAsInt: boolean } = { restTotalHitsAsInt: false }
|
TOptions extends { restTotalHitsAsInt: boolean } = { restTotalHitsAsInt: false }
|
||||||
> = InferSearchResponseOf<TDocument, TSearchRequest, TOptions>;
|
> = InferSearchResponseOf<TDocument, TSearchRequest, TOptions>;
|
||||||
|
|
||||||
export type { InferSearchResponseOf, AggregationResultOf, AggregationResultOfMap, SearchHit };
|
export type {
|
||||||
|
InferSearchResponseOf,
|
||||||
|
AggregationResultOf,
|
||||||
|
AggregationResultOfMap,
|
||||||
|
SearchHit,
|
||||||
|
ClusterDetails,
|
||||||
|
};
|
||||||
|
|
|
@ -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,
|
type SearchResponseWarningsProps,
|
||||||
} from './src/components/search_response_warnings';
|
} from './src/components/search_response_warnings';
|
||||||
|
|
||||||
export {
|
export { getSearchResponseInterceptedWarnings } from './src/utils/get_search_response_intercepted_warnings';
|
||||||
getSearchResponseInterceptedWarnings,
|
export { hasUnsupportedDownsampledAggregationFailure } from './src/utils/has_unsupported_downsampled_aggregation_failure';
|
||||||
removeInterceptedWarningDuplicates,
|
|
||||||
} from './src/utils/get_search_response_intercepted_warnings';
|
|
||||||
|
|
|
@ -8,43 +8,33 @@
|
||||||
|
|
||||||
import type { SearchResponseWarning } from '@kbn/data-plugin/public';
|
import type { SearchResponseWarning } from '@kbn/data-plugin/public';
|
||||||
|
|
||||||
export const searchResponseTimeoutWarningMock: SearchResponseWarning = {
|
export const searchResponseIncompleteWarningLocalCluster: SearchResponseWarning = {
|
||||||
type: 'timed_out',
|
type: 'incomplete',
|
||||||
message: 'Data might be incomplete because your request timed out',
|
message: 'The data might be incomplete or wrong.',
|
||||||
reason: undefined,
|
clusters: {
|
||||||
};
|
'(local)': {
|
||||||
|
status: 'partial',
|
||||||
export const searchResponseShardFailureWarningMock: SearchResponseWarning = {
|
indices: '',
|
||||||
type: 'shard_failure',
|
took: 25,
|
||||||
message: '3 of 4 shards failed',
|
timed_out: false,
|
||||||
text: 'The data might be incomplete or wrong.',
|
_shards: {
|
||||||
reason: {
|
total: 4,
|
||||||
type: 'illegal_argument_exception',
|
successful: 3,
|
||||||
reason: 'Field [__anonymous_] of type [boolean] does not support custom formats',
|
skipped: 0,
|
||||||
},
|
failed: 1,
|
||||||
};
|
},
|
||||||
|
failures: [
|
||||||
export const searchResponseWarningsMock: SearchResponseWarning[] = [
|
{
|
||||||
searchResponseTimeoutWarningMock,
|
shard: 0,
|
||||||
searchResponseShardFailureWarningMock,
|
index: 'sample-01-rollup',
|
||||||
{
|
node: 'VFTFJxpHSdaoiGxJFLSExQ',
|
||||||
type: 'shard_failure',
|
reason: {
|
||||||
message: '3 of 4 shards failed',
|
type: 'illegal_argument_exception',
|
||||||
text: 'The data might be incomplete or wrong.',
|
reason:
|
||||||
reason: {
|
'Field [kubernetes.container.memory.available.bytes] of type [aggregate_metric_double] is not supported for aggregation [percentiles]',
|
||||||
type: 'query_shard_exception',
|
},
|
||||||
reason:
|
},
|
||||||
'failed to create query: [.ds-kibana_sample_data_logs-2023.07.11-000001][0] Testing shard failures!',
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
};
|
||||||
type: 'shard_failure',
|
|
||||||
message: '1 of 4 shards failed',
|
|
||||||
text: 'The data might be incomplete or wrong.',
|
|
||||||
reason: {
|
|
||||||
type: 'query_shard_exception',
|
|
||||||
reason:
|
|
||||||
'failed to create query: [.ds-kibana_sample_data_logs-2023.07.11-000001][0] Testing shard failures!',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
|
@ -13,7 +13,7 @@ exports[`SearchResponseWarnings renders "badge" correctly 1`] = `
|
||||||
<button
|
<button
|
||||||
class="euiButton emotion-euiButtonDisplay-s-EuiButton"
|
class="euiButton emotion-euiButtonDisplay-s-EuiButton"
|
||||||
data-test-subj="test2_trigger"
|
data-test-subj="test2_trigger"
|
||||||
title="4 warnings"
|
title="1 warning"
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
|
@ -23,7 +23,7 @@ exports[`SearchResponseWarnings renders "badge" correctly 1`] = `
|
||||||
class="emotion-EuiIcon"
|
class="emotion-EuiIcon"
|
||||||
data-euiicon-type="warning"
|
data-euiicon-type="warning"
|
||||||
/>
|
/>
|
||||||
4
|
1
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
</span>
|
</span>
|
||||||
|
@ -68,84 +68,14 @@ exports[`SearchResponseWarnings renders "callout" correctly 1`] = `
|
||||||
class="euiText emotion-euiText-s"
|
class="euiText emotion-euiText-s"
|
||||||
data-test-subj="test1_warningTitle"
|
data-test-subj="test1_warningTitle"
|
||||||
>
|
>
|
||||||
Data might be incomplete because your request timed out
|
The data might be incomplete or wrong.
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="euiFlexItem emotion-euiFlexItem-growZero"
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
aria-label="Close"
|
|
||||||
class="euiButtonIcon emotion-euiButtonIcon-xs-empty-primary"
|
|
||||||
type="button"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
aria-hidden="true"
|
|
||||||
class="euiButtonIcon__icon"
|
|
||||||
color="inherit"
|
|
||||||
data-euiicon-type="cross"
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<p />
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<div
|
|
||||||
class="euiPanel euiPanel--warning euiPanel--paddingSmall euiCallOut euiCallOut--warning emotion-euiPanel-none-s-warning"
|
|
||||||
data-test-subj="test1"
|
|
||||||
>
|
|
||||||
<p
|
|
||||||
class="euiTitle euiCallOutHeader__title emotion-euiTitle-xxs-euiCallOutHeader-warning"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
aria-hidden="true"
|
|
||||||
class="emotion-euiCallOut__icon"
|
|
||||||
color="inherit"
|
|
||||||
data-euiicon-type="warning"
|
|
||||||
/>
|
|
||||||
</p>
|
|
||||||
<div
|
|
||||||
class="euiFlexGroup emotion-euiFlexGroup-none-spaceBetween-stretch-row"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="euiFlexItem emotion-euiFlexItem-growZero"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="euiFlexGroup emotion-euiFlexGroup-responsive-wrap-xs-flexStart-center-row"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="euiFlexItem emotion-euiFlexItem-growZero"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="euiText emotion-euiText-s"
|
|
||||||
data-test-subj="test1_warningTitle"
|
|
||||||
>
|
|
||||||
<strong>
|
|
||||||
3 of 4 shards failed
|
|
||||||
</strong>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="euiFlexItem emotion-euiFlexItem-growZero"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="euiText emotion-euiText-s"
|
|
||||||
data-test-subj="test1_warningMessage"
|
|
||||||
>
|
|
||||||
<p>
|
|
||||||
The data might be incomplete or wrong.
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="euiFlexItem emotion-euiFlexItem-growZero"
|
class="euiFlexItem emotion-euiFlexItem-growZero"
|
||||||
>
|
>
|
||||||
<button>
|
<button>
|
||||||
test1
|
test
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -155,161 +85,7 @@ exports[`SearchResponseWarnings renders "callout" correctly 1`] = `
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
aria-label="Close"
|
aria-label="Close"
|
||||||
class="euiButtonIcon emotion-euiButtonIcon-xs-empty-primary"
|
class="euiButtonIcon emotion-euiButtonIcon-xs-empty-warning"
|
||||||
type="button"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
aria-hidden="true"
|
|
||||||
class="euiButtonIcon__icon"
|
|
||||||
color="inherit"
|
|
||||||
data-euiicon-type="cross"
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<p />
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<div
|
|
||||||
class="euiPanel euiPanel--warning euiPanel--paddingSmall euiCallOut euiCallOut--warning emotion-euiPanel-none-s-warning"
|
|
||||||
data-test-subj="test1"
|
|
||||||
>
|
|
||||||
<p
|
|
||||||
class="euiTitle euiCallOutHeader__title emotion-euiTitle-xxs-euiCallOutHeader-warning"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
aria-hidden="true"
|
|
||||||
class="emotion-euiCallOut__icon"
|
|
||||||
color="inherit"
|
|
||||||
data-euiicon-type="warning"
|
|
||||||
/>
|
|
||||||
</p>
|
|
||||||
<div
|
|
||||||
class="euiFlexGroup emotion-euiFlexGroup-none-spaceBetween-stretch-row"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="euiFlexItem emotion-euiFlexItem-growZero"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="euiFlexGroup emotion-euiFlexGroup-responsive-wrap-xs-flexStart-center-row"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="euiFlexItem emotion-euiFlexItem-growZero"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="euiText emotion-euiText-s"
|
|
||||||
data-test-subj="test1_warningTitle"
|
|
||||||
>
|
|
||||||
<strong>
|
|
||||||
3 of 4 shards failed
|
|
||||||
</strong>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="euiFlexItem emotion-euiFlexItem-growZero"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="euiText emotion-euiText-s"
|
|
||||||
data-test-subj="test1_warningMessage"
|
|
||||||
>
|
|
||||||
<p>
|
|
||||||
The data might be incomplete or wrong.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="euiFlexItem emotion-euiFlexItem-growZero"
|
|
||||||
>
|
|
||||||
<button>
|
|
||||||
test2
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="euiFlexItem emotion-euiFlexItem-growZero"
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
aria-label="Close"
|
|
||||||
class="euiButtonIcon emotion-euiButtonIcon-xs-empty-primary"
|
|
||||||
type="button"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
aria-hidden="true"
|
|
||||||
class="euiButtonIcon__icon"
|
|
||||||
color="inherit"
|
|
||||||
data-euiicon-type="cross"
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<p />
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<div
|
|
||||||
class="euiPanel euiPanel--warning euiPanel--paddingSmall euiCallOut euiCallOut--warning emotion-euiPanel-none-s-warning"
|
|
||||||
data-test-subj="test1"
|
|
||||||
>
|
|
||||||
<p
|
|
||||||
class="euiTitle euiCallOutHeader__title emotion-euiTitle-xxs-euiCallOutHeader-warning"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
aria-hidden="true"
|
|
||||||
class="emotion-euiCallOut__icon"
|
|
||||||
color="inherit"
|
|
||||||
data-euiicon-type="warning"
|
|
||||||
/>
|
|
||||||
</p>
|
|
||||||
<div
|
|
||||||
class="euiFlexGroup emotion-euiFlexGroup-none-spaceBetween-stretch-row"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="euiFlexItem emotion-euiFlexItem-growZero"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="euiFlexGroup emotion-euiFlexGroup-responsive-wrap-xs-flexStart-center-row"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="euiFlexItem emotion-euiFlexItem-growZero"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="euiText emotion-euiText-s"
|
|
||||||
data-test-subj="test1_warningTitle"
|
|
||||||
>
|
|
||||||
<strong>
|
|
||||||
1 of 4 shards failed
|
|
||||||
</strong>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="euiFlexItem emotion-euiFlexItem-growZero"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="euiText emotion-euiText-s"
|
|
||||||
data-test-subj="test1_warningMessage"
|
|
||||||
>
|
|
||||||
<p>
|
|
||||||
The data might be incomplete or wrong.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="euiFlexItem emotion-euiFlexItem-growZero"
|
|
||||||
>
|
|
||||||
<button>
|
|
||||||
test3
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="euiFlexItem emotion-euiFlexItem-growZero"
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
aria-label="Close"
|
|
||||||
class="euiButtonIcon emotion-euiButtonIcon-xs-empty-primary"
|
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
|
@ -377,124 +153,14 @@ exports[`SearchResponseWarnings renders "empty_prompt" correctly 1`] = `
|
||||||
class="euiText emotion-euiText-m"
|
class="euiText emotion-euiText-m"
|
||||||
data-test-subj="test3_warningTitle"
|
data-test-subj="test3_warningTitle"
|
||||||
>
|
>
|
||||||
Data might be incomplete because your request timed out
|
The data might be incomplete or wrong.
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
<li
|
|
||||||
css="You have tried to stringify object returned from \`css\` function. It isn't supposed to be used directly (e.g. as value of the \`className\` prop), but rather handed to emotion so it can handle it (e.g. as value of \`css\` prop)."
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="euiFlexGroup emotion-euiFlexGroup-wrap-xs-flexStart-stretch-column"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="euiFlexItem emotion-euiFlexItem-growZero"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="euiText emotion-euiText-m"
|
|
||||||
data-test-subj="test3_warningTitle"
|
|
||||||
>
|
|
||||||
<strong>
|
|
||||||
3 of 4 shards failed
|
|
||||||
</strong>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="euiFlexItem emotion-euiFlexItem-growZero"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="euiText emotion-euiText-m"
|
|
||||||
data-test-subj="test3_warningMessage"
|
|
||||||
>
|
|
||||||
<p>
|
|
||||||
The data might be incomplete or wrong.
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="euiFlexItem emotion-euiFlexItem-growZero"
|
class="euiFlexItem emotion-euiFlexItem-growZero"
|
||||||
>
|
>
|
||||||
<button>
|
<button>
|
||||||
test1
|
test
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
<li
|
|
||||||
css="You have tried to stringify object returned from \`css\` function. It isn't supposed to be used directly (e.g. as value of the \`className\` prop), but rather handed to emotion so it can handle it (e.g. as value of \`css\` prop)."
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="euiFlexGroup emotion-euiFlexGroup-wrap-xs-flexStart-stretch-column"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="euiFlexItem emotion-euiFlexItem-growZero"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="euiText emotion-euiText-m"
|
|
||||||
data-test-subj="test3_warningTitle"
|
|
||||||
>
|
|
||||||
<strong>
|
|
||||||
3 of 4 shards failed
|
|
||||||
</strong>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="euiFlexItem emotion-euiFlexItem-growZero"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="euiText emotion-euiText-m"
|
|
||||||
data-test-subj="test3_warningMessage"
|
|
||||||
>
|
|
||||||
<p>
|
|
||||||
The data might be incomplete or wrong.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="euiFlexItem emotion-euiFlexItem-growZero"
|
|
||||||
>
|
|
||||||
<button>
|
|
||||||
test2
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
<li
|
|
||||||
css="You have tried to stringify object returned from \`css\` function. It isn't supposed to be used directly (e.g. as value of the \`className\` prop), but rather handed to emotion so it can handle it (e.g. as value of \`css\` prop)."
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="euiFlexGroup emotion-euiFlexGroup-wrap-xs-flexStart-stretch-column"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="euiFlexItem emotion-euiFlexItem-growZero"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="euiText emotion-euiText-m"
|
|
||||||
data-test-subj="test3_warningTitle"
|
|
||||||
>
|
|
||||||
<strong>
|
|
||||||
1 of 4 shards failed
|
|
||||||
</strong>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="euiFlexItem emotion-euiFlexItem-growZero"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="euiText emotion-euiText-m"
|
|
||||||
data-test-subj="test3_warningMessage"
|
|
||||||
>
|
|
||||||
<p>
|
|
||||||
The data might be incomplete or wrong.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="euiFlexItem emotion-euiFlexItem-growZero"
|
|
||||||
>
|
|
||||||
<button>
|
|
||||||
test3
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -9,12 +9,14 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { mountWithIntl } from '@kbn/test-jest-helpers';
|
import { mountWithIntl } from '@kbn/test-jest-helpers';
|
||||||
import { SearchResponseWarnings } from './search_response_warnings';
|
import { SearchResponseWarnings } from './search_response_warnings';
|
||||||
import { searchResponseWarningsMock } from '../../__mocks__/search_response_warnings';
|
import { searchResponseIncompleteWarningLocalCluster } from '../../__mocks__/search_response_warnings';
|
||||||
|
|
||||||
const interceptedWarnings = searchResponseWarningsMock.map((originalWarning, index) => ({
|
const interceptedWarnings = [
|
||||||
originalWarning,
|
{
|
||||||
action: originalWarning.type === 'shard_failure' ? <button>{`test${index}`}</button> : undefined,
|
originalWarning: searchResponseIncompleteWarningLocalCluster,
|
||||||
}));
|
action: <button>{`test`}</button>,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
describe('SearchResponseWarnings', () => {
|
describe('SearchResponseWarnings', () => {
|
||||||
it('renders "callout" correctly', () => {
|
it('renders "callout" correctly', () => {
|
||||||
|
|
|
@ -270,22 +270,13 @@ function WarningContent({
|
||||||
groupStyles?: Partial<EuiFlexGroupProps>;
|
groupStyles?: Partial<EuiFlexGroupProps>;
|
||||||
'data-test-subj': string;
|
'data-test-subj': string;
|
||||||
}) {
|
}) {
|
||||||
const hasDescription = 'text' in originalWarning;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<EuiFlexGroup gutterSize="xs" {...groupStyles} wrap>
|
<EuiFlexGroup gutterSize="xs" {...groupStyles} wrap>
|
||||||
<EuiFlexItem grow={false}>
|
<EuiFlexItem grow={false}>
|
||||||
<EuiText size={textSize} data-test-subj={`${dataTestSubj}_warningTitle`}>
|
<EuiText size={textSize} data-test-subj={`${dataTestSubj}_warningTitle`}>
|
||||||
{hasDescription ? <strong>{originalWarning.message}</strong> : originalWarning.message}
|
{originalWarning.message}
|
||||||
</EuiText>
|
</EuiText>
|
||||||
</EuiFlexItem>
|
</EuiFlexItem>
|
||||||
{hasDescription ? (
|
|
||||||
<EuiFlexItem grow={false}>
|
|
||||||
<EuiText size={textSize} data-test-subj={`${dataTestSubj}_warningMessage`}>
|
|
||||||
<p>{originalWarning.text}</p>
|
|
||||||
</EuiText>
|
|
||||||
</EuiFlexItem>
|
|
||||||
) : null}
|
|
||||||
{action ? <EuiFlexItem grow={false}>{action}</EuiFlexItem> : null}
|
{action ? <EuiFlexItem grow={false}>{action}</EuiFlexItem> : null}
|
||||||
</EuiFlexGroup>
|
</EuiFlexGroup>
|
||||||
);
|
);
|
||||||
|
@ -306,6 +297,7 @@ function CalloutTitleWrapper({
|
||||||
onClick={onCloseCallout}
|
onClick={onCloseCallout}
|
||||||
type="button"
|
type="button"
|
||||||
iconType="cross"
|
iconType="cross"
|
||||||
|
color="warning"
|
||||||
/>
|
/>
|
||||||
</EuiFlexItem>
|
</EuiFlexItem>
|
||||||
</EuiFlexGroup>
|
</EuiFlexGroup>
|
||||||
|
|
|
@ -9,11 +9,8 @@
|
||||||
import { RequestAdapter } from '@kbn/inspector-plugin/common';
|
import { RequestAdapter } from '@kbn/inspector-plugin/common';
|
||||||
import { dataPluginMock } from '@kbn/data-plugin/public/mocks';
|
import { dataPluginMock } from '@kbn/data-plugin/public/mocks';
|
||||||
import { coreMock } from '@kbn/core/public/mocks';
|
import { coreMock } from '@kbn/core/public/mocks';
|
||||||
import {
|
import { getSearchResponseInterceptedWarnings } from './get_search_response_intercepted_warnings';
|
||||||
getSearchResponseInterceptedWarnings,
|
import { searchResponseIncompleteWarningLocalCluster } from '../__mocks__/search_response_warnings';
|
||||||
removeInterceptedWarningDuplicates,
|
|
||||||
} from './get_search_response_intercepted_warnings';
|
|
||||||
import { searchResponseWarningsMock } from '../__mocks__/search_response_warnings';
|
|
||||||
|
|
||||||
const servicesMock = {
|
const servicesMock = {
|
||||||
data: dataPluginMock.createStartContract(),
|
data: dataPluginMock.createStartContract(),
|
||||||
|
@ -23,162 +20,66 @@ const servicesMock = {
|
||||||
describe('getSearchResponseInterceptedWarnings', () => {
|
describe('getSearchResponseInterceptedWarnings', () => {
|
||||||
const adapter = new RequestAdapter();
|
const adapter = new RequestAdapter();
|
||||||
|
|
||||||
it('should catch warnings correctly', () => {
|
it('should return intercepted incomplete data warnings', () => {
|
||||||
const services = {
|
const services = {
|
||||||
...servicesMock,
|
...servicesMock,
|
||||||
};
|
};
|
||||||
services.data.search.showWarnings = jest.fn((_, callback) => {
|
services.data.search.showWarnings = jest.fn((_, callback) => {
|
||||||
// @ts-expect-error for empty meta
|
// @ts-expect-error for empty meta
|
||||||
callback?.(searchResponseWarningsMock[0], {});
|
callback?.(searchResponseIncompleteWarningLocalCluster, {});
|
||||||
// @ts-expect-error for empty meta
|
|
||||||
callback?.(searchResponseWarningsMock[1], {});
|
|
||||||
// @ts-expect-error for empty meta
|
|
||||||
callback?.(searchResponseWarningsMock[2], {});
|
|
||||||
// @ts-expect-error for empty meta
|
|
||||||
callback?.(searchResponseWarningsMock[3], {});
|
|
||||||
|
|
||||||
// plus duplicates
|
|
||||||
// @ts-expect-error for empty meta
|
|
||||||
callback?.(searchResponseWarningsMock[0], {});
|
|
||||||
// @ts-expect-error for empty meta
|
|
||||||
callback?.(searchResponseWarningsMock[1], {});
|
|
||||||
// @ts-expect-error for empty meta
|
|
||||||
callback?.(searchResponseWarningsMock[2], {});
|
|
||||||
});
|
});
|
||||||
expect(
|
const warnings = getSearchResponseInterceptedWarnings({
|
||||||
getSearchResponseInterceptedWarnings({
|
services,
|
||||||
services,
|
adapter,
|
||||||
adapter,
|
});
|
||||||
options: {
|
|
||||||
disableShardFailureWarning: true,
|
expect(warnings.length).toBe(1);
|
||||||
},
|
expect(warnings[0].originalWarning).toEqual(searchResponseIncompleteWarningLocalCluster);
|
||||||
})
|
expect(warnings[0].action).toMatchInlineSnapshot(`
|
||||||
).toMatchInlineSnapshot(`
|
<OpenIncompleteResultsModalButton
|
||||||
Array [
|
color="primary"
|
||||||
Object {
|
getRequestMeta={[Function]}
|
||||||
"action": undefined,
|
isButtonEmpty={true}
|
||||||
"originalWarning": Object {
|
size="s"
|
||||||
"message": "Data might be incomplete because your request timed out",
|
theme={
|
||||||
"reason": undefined,
|
Object {
|
||||||
"type": "timed_out",
|
"theme$": Observable {
|
||||||
},
|
"_subscribe": [Function],
|
||||||
},
|
|
||||||
Object {
|
|
||||||
"action": <ShardFailureOpenModalButton
|
|
||||||
color="primary"
|
|
||||||
getRequestMeta={[Function]}
|
|
||||||
isButtonEmpty={true}
|
|
||||||
size="s"
|
|
||||||
theme={
|
|
||||||
Object {
|
|
||||||
"theme$": Observable {
|
|
||||||
"_subscribe": [Function],
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
title="3 of 4 shards failed"
|
|
||||||
/>,
|
|
||||||
"originalWarning": Object {
|
|
||||||
"message": "3 of 4 shards failed",
|
|
||||||
"reason": Object {
|
|
||||||
"reason": "Field [__anonymous_] of type [boolean] does not support custom formats",
|
|
||||||
"type": "illegal_argument_exception",
|
|
||||||
},
|
},
|
||||||
"text": "The data might be incomplete or wrong.",
|
}
|
||||||
"type": "shard_failure",
|
}
|
||||||
},
|
warning={
|
||||||
},
|
Object {
|
||||||
Object {
|
"clusters": Object {
|
||||||
"action": <ShardFailureOpenModalButton
|
"(local)": Object {
|
||||||
color="primary"
|
"_shards": Object {
|
||||||
getRequestMeta={[Function]}
|
"failed": 1,
|
||||||
isButtonEmpty={true}
|
"skipped": 0,
|
||||||
size="s"
|
"successful": 3,
|
||||||
theme={
|
"total": 4,
|
||||||
Object {
|
|
||||||
"theme$": Observable {
|
|
||||||
"_subscribe": [Function],
|
|
||||||
},
|
},
|
||||||
}
|
"failures": Array [
|
||||||
}
|
Object {
|
||||||
title="3 of 4 shards failed"
|
"index": "sample-01-rollup",
|
||||||
/>,
|
"node": "VFTFJxpHSdaoiGxJFLSExQ",
|
||||||
"originalWarning": Object {
|
"reason": Object {
|
||||||
"message": "3 of 4 shards failed",
|
"reason": "Field [kubernetes.container.memory.available.bytes] of type [aggregate_metric_double] is not supported for aggregation [percentiles]",
|
||||||
"reason": Object {
|
"type": "illegal_argument_exception",
|
||||||
"reason": "failed to create query: [.ds-kibana_sample_data_logs-2023.07.11-000001][0] Testing shard failures!",
|
},
|
||||||
"type": "query_shard_exception",
|
"shard": 0,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"indices": "",
|
||||||
|
"status": "partial",
|
||||||
|
"timed_out": false,
|
||||||
|
"took": 25,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
"text": "The data might be incomplete or wrong.",
|
"message": "The data might be incomplete or wrong.",
|
||||||
"type": "shard_failure",
|
"type": "incomplete",
|
||||||
},
|
}
|
||||||
},
|
}
|
||||||
Object {
|
/>
|
||||||
"action": <ShardFailureOpenModalButton
|
|
||||||
color="primary"
|
|
||||||
getRequestMeta={[Function]}
|
|
||||||
isButtonEmpty={true}
|
|
||||||
size="s"
|
|
||||||
theme={
|
|
||||||
Object {
|
|
||||||
"theme$": Observable {
|
|
||||||
"_subscribe": [Function],
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
title="1 of 4 shards failed"
|
|
||||||
/>,
|
|
||||||
"originalWarning": Object {
|
|
||||||
"message": "1 of 4 shards failed",
|
|
||||||
"reason": Object {
|
|
||||||
"reason": "failed to create query: [.ds-kibana_sample_data_logs-2023.07.11-000001][0] Testing shard failures!",
|
|
||||||
"type": "query_shard_exception",
|
|
||||||
},
|
|
||||||
"text": "The data might be incomplete or wrong.",
|
|
||||||
"type": "shard_failure",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]
|
|
||||||
`);
|
`);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not catch any warnings if disableShardFailureWarning is false', () => {
|
|
||||||
const services = {
|
|
||||||
...servicesMock,
|
|
||||||
};
|
|
||||||
services.data.search.showWarnings = jest.fn((_, callback) => {
|
|
||||||
// @ts-expect-error for empty meta
|
|
||||||
callback?.(searchResponseWarningsMock[0], {});
|
|
||||||
});
|
|
||||||
expect(
|
|
||||||
getSearchResponseInterceptedWarnings({
|
|
||||||
services,
|
|
||||||
adapter,
|
|
||||||
options: {
|
|
||||||
disableShardFailureWarning: false,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
).toBeUndefined();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('removeInterceptedWarningDuplicates', () => {
|
|
||||||
it('should remove duplicates successfully', () => {
|
|
||||||
const interceptedWarnings = searchResponseWarningsMock.map((originalWarning) => ({
|
|
||||||
originalWarning,
|
|
||||||
}));
|
|
||||||
|
|
||||||
expect(removeInterceptedWarningDuplicates([interceptedWarnings[0]])).toEqual([
|
|
||||||
interceptedWarnings[0],
|
|
||||||
]);
|
|
||||||
expect(removeInterceptedWarningDuplicates(interceptedWarnings)).toEqual(interceptedWarnings);
|
|
||||||
expect(
|
|
||||||
removeInterceptedWarningDuplicates([...interceptedWarnings, ...interceptedWarnings])
|
|
||||||
).toEqual(interceptedWarnings);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return undefined if the list is empty', () => {
|
|
||||||
expect(removeInterceptedWarningDuplicates([])).toBeUndefined();
|
|
||||||
expect(removeInterceptedWarningDuplicates(undefined)).toBeUndefined();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
|
@ -7,11 +7,9 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { uniqBy } from 'lodash';
|
|
||||||
import {
|
import {
|
||||||
type DataPublicPluginStart,
|
type DataPublicPluginStart,
|
||||||
type ShardFailureRequest,
|
OpenIncompleteResultsModalButton,
|
||||||
ShardFailureOpenModalButton,
|
|
||||||
} from '@kbn/data-plugin/public';
|
} from '@kbn/data-plugin/public';
|
||||||
import type { RequestAdapter } from '@kbn/inspector-plugin/common';
|
import type { RequestAdapter } from '@kbn/inspector-plugin/common';
|
||||||
import type { CoreStart } from '@kbn/core-lifecycle-browser';
|
import type { CoreStart } from '@kbn/core-lifecycle-browser';
|
||||||
|
@ -26,21 +24,13 @@ import type { SearchResponseInterceptedWarning } from '../types';
|
||||||
export const getSearchResponseInterceptedWarnings = ({
|
export const getSearchResponseInterceptedWarnings = ({
|
||||||
services,
|
services,
|
||||||
adapter,
|
adapter,
|
||||||
options,
|
|
||||||
}: {
|
}: {
|
||||||
services: {
|
services: {
|
||||||
data: DataPublicPluginStart;
|
data: DataPublicPluginStart;
|
||||||
theme: CoreStart['theme'];
|
theme: CoreStart['theme'];
|
||||||
};
|
};
|
||||||
adapter: RequestAdapter;
|
adapter: RequestAdapter;
|
||||||
options?: {
|
}): SearchResponseInterceptedWarning[] => {
|
||||||
disableShardFailureWarning?: boolean;
|
|
||||||
};
|
|
||||||
}): SearchResponseInterceptedWarning[] | undefined => {
|
|
||||||
if (!options?.disableShardFailureWarning) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
const interceptedWarnings: SearchResponseInterceptedWarning[] = [];
|
const interceptedWarnings: SearchResponseInterceptedWarning[] = [];
|
||||||
|
|
||||||
services.data.search.showWarnings(adapter, (warning, meta) => {
|
services.data.search.showWarnings(adapter, (warning, meta) => {
|
||||||
|
@ -49,13 +39,13 @@ export const getSearchResponseInterceptedWarnings = ({
|
||||||
interceptedWarnings.push({
|
interceptedWarnings.push({
|
||||||
originalWarning: warning,
|
originalWarning: warning,
|
||||||
action:
|
action:
|
||||||
warning.type === 'shard_failure' && warning.text && warning.message ? (
|
warning.type === 'incomplete' ? (
|
||||||
<ShardFailureOpenModalButton
|
<OpenIncompleteResultsModalButton
|
||||||
theme={services.theme}
|
theme={services.theme}
|
||||||
title={warning.message}
|
warning={warning}
|
||||||
size="s"
|
size="s"
|
||||||
getRequestMeta={() => ({
|
getRequestMeta={() => ({
|
||||||
request: request as ShardFailureRequest,
|
request,
|
||||||
response,
|
response,
|
||||||
})}
|
})}
|
||||||
color="primary"
|
color="primary"
|
||||||
|
@ -66,23 +56,5 @@ export const getSearchResponseInterceptedWarnings = ({
|
||||||
return true; // suppress the default behaviour
|
return true; // suppress the default behaviour
|
||||||
});
|
});
|
||||||
|
|
||||||
return removeInterceptedWarningDuplicates(interceptedWarnings);
|
return interceptedWarnings;
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Removes duplicated warnings
|
|
||||||
* @param interceptedWarnings
|
|
||||||
*/
|
|
||||||
export const removeInterceptedWarningDuplicates = (
|
|
||||||
interceptedWarnings: SearchResponseInterceptedWarning[] | undefined
|
|
||||||
): SearchResponseInterceptedWarning[] | undefined => {
|
|
||||||
if (!interceptedWarnings?.length) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
const uniqInterceptedWarnings = uniqBy(interceptedWarnings, (interceptedWarning) =>
|
|
||||||
JSON.stringify(interceptedWarning.originalWarning)
|
|
||||||
);
|
|
||||||
|
|
||||||
return uniqInterceptedWarnings?.length ? uniqInterceptedWarnings : undefined;
|
|
||||||
};
|
};
|
||||||
|
|
|
@ -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,
|
inspectorRequestAdapter?: RequestAdapter,
|
||||||
abortSignal?: AbortSignal,
|
abortSignal?: AbortSignal,
|
||||||
searchSessionId?: string,
|
searchSessionId?: string,
|
||||||
disableShardFailureWarning?: boolean
|
disableWarningToasts?: boolean
|
||||||
) => Promise<estypes.SearchResponse<any>>;
|
) => Promise<estypes.SearchResponse<any>>;
|
||||||
|
|
||||||
export interface AggTypeConfig<
|
export interface AggTypeConfig<
|
||||||
|
|
|
@ -396,7 +396,7 @@ export const createOtherBucketPostFlightRequest = (
|
||||||
inspectorRequestAdapter,
|
inspectorRequestAdapter,
|
||||||
abortSignal,
|
abortSignal,
|
||||||
searchSessionId,
|
searchSessionId,
|
||||||
disableShardFailureWarning
|
disableWarningToasts
|
||||||
) => {
|
) => {
|
||||||
if (!resp.aggregations) return resp;
|
if (!resp.aggregations) return resp;
|
||||||
const nestedSearchSource = searchSource.createChild();
|
const nestedSearchSource = searchSource.createChild();
|
||||||
|
@ -410,7 +410,7 @@ export const createOtherBucketPostFlightRequest = (
|
||||||
nestedSearchSource.fetch$({
|
nestedSearchSource.fetch$({
|
||||||
abortSignal,
|
abortSignal,
|
||||||
sessionId: searchSessionId,
|
sessionId: searchSessionId,
|
||||||
disableShardFailureWarning,
|
disableWarningToasts,
|
||||||
inspector: {
|
inspector: {
|
||||||
adapter: inspectorRequestAdapter,
|
adapter: inspectorRequestAdapter,
|
||||||
title: i18n.translate('data.search.aggs.buckets.terms.otherBucketTitle', {
|
title: i18n.translate('data.search.aggs.buckets.terms.otherBucketTitle', {
|
||||||
|
|
|
@ -53,7 +53,7 @@ describe('esaggs expression function - public', () => {
|
||||||
query: undefined,
|
query: undefined,
|
||||||
searchSessionId: 'abc123',
|
searchSessionId: 'abc123',
|
||||||
searchSourceService: searchSourceCommonMock,
|
searchSourceService: searchSourceCommonMock,
|
||||||
disableShardWarnings: false,
|
disableWarningToasts: false,
|
||||||
timeFields: ['@timestamp', 'utc_time'],
|
timeFields: ['@timestamp', 'utc_time'],
|
||||||
timeRange: undefined,
|
timeRange: undefined,
|
||||||
};
|
};
|
||||||
|
@ -139,7 +139,7 @@ describe('esaggs expression function - public', () => {
|
||||||
description: 'This request queries Elasticsearch to fetch the data for the visualization.',
|
description: 'This request queries Elasticsearch to fetch the data for the visualization.',
|
||||||
adapter: undefined,
|
adapter: undefined,
|
||||||
},
|
},
|
||||||
disableShardFailureWarning: false,
|
disableWarningToasts: false,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -159,7 +159,7 @@ describe('esaggs expression function - public', () => {
|
||||||
description: 'MyDescription',
|
description: 'MyDescription',
|
||||||
adapter: undefined,
|
adapter: undefined,
|
||||||
},
|
},
|
||||||
disableShardFailureWarning: false,
|
disableWarningToasts: false,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -30,7 +30,7 @@ export interface RequestHandlerParams {
|
||||||
searchSourceService: ISearchStartSearchSource;
|
searchSourceService: ISearchStartSearchSource;
|
||||||
timeFields?: string[];
|
timeFields?: string[];
|
||||||
timeRange?: TimeRange;
|
timeRange?: TimeRange;
|
||||||
disableShardWarnings?: boolean;
|
disableWarningToasts?: boolean;
|
||||||
getNow?: () => Date;
|
getNow?: () => Date;
|
||||||
executionContext?: KibanaExecutionContext;
|
executionContext?: KibanaExecutionContext;
|
||||||
title?: string;
|
title?: string;
|
||||||
|
@ -48,7 +48,7 @@ export const handleRequest = ({
|
||||||
searchSourceService,
|
searchSourceService,
|
||||||
timeFields,
|
timeFields,
|
||||||
timeRange,
|
timeRange,
|
||||||
disableShardWarnings,
|
disableWarningToasts,
|
||||||
getNow,
|
getNow,
|
||||||
executionContext,
|
executionContext,
|
||||||
title,
|
title,
|
||||||
|
@ -110,7 +110,7 @@ export const handleRequest = ({
|
||||||
requestSearchSource
|
requestSearchSource
|
||||||
.fetch$({
|
.fetch$({
|
||||||
abortSignal,
|
abortSignal,
|
||||||
disableShardFailureWarning: disableShardWarnings,
|
disableWarningToasts,
|
||||||
sessionId: searchSessionId,
|
sessionId: searchSessionId,
|
||||||
inspector: {
|
inspector: {
|
||||||
adapter: inspectorAdapters.requests,
|
adapter: inspectorAdapters.requests,
|
||||||
|
|
|
@ -15,7 +15,7 @@ export type ExecutionContextSearch = {
|
||||||
filters?: Filter[];
|
filters?: Filter[];
|
||||||
query?: Query | Query[];
|
query?: Query | Query[];
|
||||||
timeRange?: TimeRange;
|
timeRange?: TimeRange;
|
||||||
disableShardWarnings?: boolean;
|
disableWarningToasts?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ExpressionValueSearchContext = ExpressionValueBoxed<
|
export type ExpressionValueSearchContext = ExpressionValueBoxed<
|
||||||
|
|
|
@ -520,7 +520,7 @@ export class SearchSource {
|
||||||
options.inspector?.adapter,
|
options.inspector?.adapter,
|
||||||
options.abortSignal,
|
options.abortSignal,
|
||||||
options.sessionId,
|
options.sessionId,
|
||||||
options.disableShardFailureWarning
|
options.disableWarningToasts
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -249,7 +249,7 @@ export interface SearchSourceSearchOptions extends ISearchOptions {
|
||||||
inspector?: IInspectorInfo;
|
inspector?: IInspectorInfo;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Disable default warnings of shard failures
|
* Set to true to disable warning toasts and customize warning display
|
||||||
*/
|
*/
|
||||||
disableShardFailureWarning?: boolean;
|
disableWarningToasts?: boolean;
|
||||||
}
|
}
|
||||||
|
|
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
|
// set width and height to fixed values to prevent resizing when you switch tabs
|
||||||
.shardFailureModal {
|
.incompleteResultsModal {
|
||||||
min-height: 75vh;
|
min-height: 75vh;
|
||||||
width: 768px;
|
width: 768px;
|
||||||
|
|
||||||
|
@ -12,10 +12,4 @@
|
||||||
.euiModalHeader {
|
.euiModalHeader {
|
||||||
padding-bottom: 0;
|
padding-bottom: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.shardFailureModal__desc {
|
|
||||||
// set for IE11, since without it depending on the content the width of the list
|
|
||||||
// could be much higher than the available screenspace
|
|
||||||
max-width: 686px;
|
|
||||||
}
|
|
|
@ -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 React from 'react';
|
||||||
import type { ShardFailureOpenModalButtonProps } from './shard_failure_open_modal_button';
|
import type { OpenIncompleteResultsModalButtonProps } from './open_incomplete_results_modal_button';
|
||||||
|
|
||||||
const Fallback = () => <div />;
|
const Fallback = () => <div />;
|
||||||
|
|
||||||
const LazyShardFailureOpenModalButton = React.lazy(
|
const LazyOpenModalButton = React.lazy(() => import('./open_incomplete_results_modal_button'));
|
||||||
() => import('./shard_failure_open_modal_button')
|
export const OpenIncompleteResultsModalButton = (props: OpenIncompleteResultsModalButtonProps) => (
|
||||||
);
|
|
||||||
export const ShardFailureOpenModalButton = (props: ShardFailureOpenModalButtonProps) => (
|
|
||||||
<React.Suspense fallback={<Fallback />}>
|
<React.Suspense fallback={<Fallback />}>
|
||||||
<LazyShardFailureOpenModalButton {...props} />
|
<LazyOpenModalButton {...props} />
|
||||||
</React.Suspense>
|
</React.Suspense>
|
||||||
);
|
);
|
||||||
|
|
||||||
export type { ShardFailureRequest } from './shard_failure_types';
|
|
|
@ -13,18 +13,19 @@ import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
|
||||||
import type { ThemeServiceStart } from '@kbn/core/public';
|
import type { ThemeServiceStart } from '@kbn/core/public';
|
||||||
import { toMountPoint } from '@kbn/kibana-react-plugin/public';
|
import { toMountPoint } from '@kbn/kibana-react-plugin/public';
|
||||||
import { getOverlays } from '../services';
|
import { getOverlays } from '../services';
|
||||||
import { ShardFailureModal } from './shard_failure_modal';
|
import type { SearchRequest } from '..';
|
||||||
import type { ShardFailureRequest } from './shard_failure_types';
|
import { IncompleteResultsModal } from './incomplete_results_modal';
|
||||||
import './_shard_failure_modal.scss';
|
import type { SearchResponseIncompleteWarning } from '../search';
|
||||||
|
import './_incomplete_results_modal.scss';
|
||||||
|
|
||||||
// @internal
|
// @internal
|
||||||
export interface ShardFailureOpenModalButtonProps {
|
export interface OpenIncompleteResultsModalButtonProps {
|
||||||
theme: ThemeServiceStart;
|
theme: ThemeServiceStart;
|
||||||
title: string;
|
warning: SearchResponseIncompleteWarning;
|
||||||
size?: EuiButtonProps['size'];
|
size?: EuiButtonProps['size'];
|
||||||
color?: EuiButtonProps['color'];
|
color?: EuiButtonProps['color'];
|
||||||
getRequestMeta: () => {
|
getRequestMeta: () => {
|
||||||
request: ShardFailureRequest;
|
request: SearchRequest;
|
||||||
response: estypes.SearchResponse<any>;
|
response: estypes.SearchResponse<any>;
|
||||||
};
|
};
|
||||||
isButtonEmpty?: boolean;
|
isButtonEmpty?: boolean;
|
||||||
|
@ -32,31 +33,31 @@ export interface ShardFailureOpenModalButtonProps {
|
||||||
|
|
||||||
// Needed for React.lazy
|
// Needed for React.lazy
|
||||||
// eslint-disable-next-line import/no-default-export
|
// eslint-disable-next-line import/no-default-export
|
||||||
export default function ShardFailureOpenModalButton({
|
export default function OpenIncompleteResultsModalButton({
|
||||||
getRequestMeta,
|
getRequestMeta,
|
||||||
theme,
|
theme,
|
||||||
title,
|
warning,
|
||||||
size = 's',
|
size = 's',
|
||||||
color = 'warning',
|
color = 'warning',
|
||||||
isButtonEmpty = false,
|
isButtonEmpty = false,
|
||||||
}: ShardFailureOpenModalButtonProps) {
|
}: OpenIncompleteResultsModalButtonProps) {
|
||||||
const onClick = useCallback(() => {
|
const onClick = useCallback(() => {
|
||||||
const { request, response } = getRequestMeta();
|
const { request, response } = getRequestMeta();
|
||||||
const modal = getOverlays().openModal(
|
const modal = getOverlays().openModal(
|
||||||
toMountPoint(
|
toMountPoint(
|
||||||
<ShardFailureModal
|
<IncompleteResultsModal
|
||||||
request={request}
|
request={request}
|
||||||
response={response}
|
response={response}
|
||||||
title={title}
|
warning={warning}
|
||||||
onClose={() => modal.close()}
|
onClose={() => modal.close()}
|
||||||
/>,
|
/>,
|
||||||
{ theme$: theme.theme$ }
|
{ theme$: theme.theme$ }
|
||||||
),
|
),
|
||||||
{
|
{
|
||||||
className: 'shardFailureModal',
|
className: 'incompleteResultsModal',
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}, [getRequestMeta, theme.theme$, title]);
|
}, [getRequestMeta, theme.theme$, warning]);
|
||||||
|
|
||||||
const Component = isButtonEmpty ? EuiLink : EuiButton;
|
const Component = isButtonEmpty ? EuiLink : EuiButton;
|
||||||
|
|
||||||
|
@ -65,11 +66,11 @@ export default function ShardFailureOpenModalButton({
|
||||||
color={color}
|
color={color}
|
||||||
size={size}
|
size={size}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
data-test-subj="openShardFailureModalBtn"
|
data-test-subj="openIncompleteResultsModalBtn"
|
||||||
>
|
>
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id="data.search.searchSource.fetch.shardsFailedModal.showDetails"
|
id="data.search.searchSource.fetch.incompleteResultsModal.viewDetails"
|
||||||
defaultMessage="Show details"
|
defaultMessage="View details"
|
||||||
description="Open the modal to show details"
|
description="Open the modal to show details"
|
||||||
/>
|
/>
|
||||||
</Component>
|
</Component>
|
|
@ -170,6 +170,7 @@ export type {
|
||||||
Reason,
|
Reason,
|
||||||
WaitUntilNextSessionCompletesOptions,
|
WaitUntilNextSessionCompletesOptions,
|
||||||
SearchResponseWarning,
|
SearchResponseWarning,
|
||||||
|
SearchResponseIncompleteWarning,
|
||||||
} from './search';
|
} from './search';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
@ -273,8 +274,7 @@ export type {
|
||||||
} from './query';
|
} from './query';
|
||||||
|
|
||||||
// TODO: move to @kbn/search-response-warnings
|
// TODO: move to @kbn/search-response-warnings
|
||||||
export type { ShardFailureRequest } from './shard_failure_modal';
|
export { OpenIncompleteResultsModalButton } from './incomplete_results_modal';
|
||||||
export { ShardFailureOpenModalButton } from './shard_failure_modal';
|
|
||||||
|
|
||||||
export type { AggsStart } from './search/aggs';
|
export type { AggsStart } from './search/aggs';
|
||||||
|
|
||||||
|
|
|
@ -126,7 +126,7 @@ describe('esaggs expression function - public', () => {
|
||||||
searchSessionId: 'abc123',
|
searchSessionId: 'abc123',
|
||||||
searchSourceService: startDependencies.searchSource,
|
searchSourceService: startDependencies.searchSource,
|
||||||
timeFields: args.timeFields,
|
timeFields: args.timeFields,
|
||||||
disableShardWarnings: false,
|
disableWarningToasts: false,
|
||||||
timeRange: undefined,
|
timeRange: undefined,
|
||||||
getNow: undefined,
|
getNow: undefined,
|
||||||
});
|
});
|
||||||
|
|
|
@ -61,7 +61,7 @@ export function getFunctionDefinition({
|
||||||
return { aggConfigs, indexPattern, searchSource, getNow, handleEsaggsRequest };
|
return { aggConfigs, indexPattern, searchSource, getNow, handleEsaggsRequest };
|
||||||
}).pipe(
|
}).pipe(
|
||||||
switchMap(({ aggConfigs, indexPattern, searchSource, getNow, handleEsaggsRequest }) => {
|
switchMap(({ aggConfigs, indexPattern, searchSource, getNow, handleEsaggsRequest }) => {
|
||||||
const { disableShardWarnings } = getSearchContext();
|
const { disableWarningToasts } = getSearchContext();
|
||||||
|
|
||||||
return handleEsaggsRequest({
|
return handleEsaggsRequest({
|
||||||
abortSignal,
|
abortSignal,
|
||||||
|
@ -74,7 +74,7 @@ export function getFunctionDefinition({
|
||||||
searchSourceService: searchSource,
|
searchSourceService: searchSource,
|
||||||
timeFields: args.timeFields,
|
timeFields: args.timeFields,
|
||||||
timeRange: get(input, 'timeRange', undefined),
|
timeRange: get(input, 'timeRange', undefined),
|
||||||
disableShardWarnings: (disableShardWarnings || false) as boolean,
|
disableWarningToasts: (disableWarningToasts || false) as boolean,
|
||||||
getNow,
|
getNow,
|
||||||
executionContext: getExecutionContext(),
|
executionContext: getExecutionContext(),
|
||||||
});
|
});
|
||||||
|
|
|
@ -10,121 +10,280 @@ import { estypes } from '@elastic/elasticsearch';
|
||||||
import { extractWarnings } from './extract_warnings';
|
import { extractWarnings } from './extract_warnings';
|
||||||
|
|
||||||
describe('extract search response warnings', () => {
|
describe('extract search response warnings', () => {
|
||||||
it('should extract warnings from response with shard failures', () => {
|
describe('single cluster', () => {
|
||||||
const response = {
|
it('should extract incomplete warning from response with shard failures', () => {
|
||||||
took: 25,
|
const response = {
|
||||||
timed_out: false,
|
took: 25,
|
||||||
_shards: {
|
timed_out: false,
|
||||||
total: 4,
|
_shards: {
|
||||||
successful: 2,
|
total: 4,
|
||||||
skipped: 0,
|
successful: 3,
|
||||||
failed: 2,
|
skipped: 0,
|
||||||
failures: [
|
failed: 1,
|
||||||
{
|
failures: [
|
||||||
shard: 0,
|
{
|
||||||
index: 'sample-01-rollup',
|
shard: 0,
|
||||||
node: 'VFTFJxpHSdaoiGxJFLSExQ',
|
index: 'sample-01-rollup',
|
||||||
reason: {
|
node: 'VFTFJxpHSdaoiGxJFLSExQ',
|
||||||
type: 'illegal_argument_exception',
|
reason: {
|
||||||
reason:
|
type: 'illegal_argument_exception',
|
||||||
'Field [kubernetes.container.memory.available.bytes] of type [aggregate_metric_double] is not supported for aggregation [percentiles]',
|
reason:
|
||||||
|
'Field [kubernetes.container.memory.available.bytes] of type [aggregate_metric_double] is not supported for aggregation [percentiles]',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
hits: { total: 18239, max_score: null, hits: [] },
|
||||||
|
aggregations: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(extractWarnings(response)).toEqual([
|
||||||
|
{
|
||||||
|
type: 'incomplete',
|
||||||
|
message: 'The data might be incomplete or wrong.',
|
||||||
|
clusters: {
|
||||||
|
'(local)': {
|
||||||
|
status: 'partial',
|
||||||
|
indices: '',
|
||||||
|
took: 25,
|
||||||
|
timed_out: false,
|
||||||
|
_shards: response._shards,
|
||||||
|
failures: response._shards.failures,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
|
||||||
},
|
|
||||||
hits: { total: 18239, max_score: null, hits: [] },
|
|
||||||
aggregations: {},
|
|
||||||
};
|
|
||||||
|
|
||||||
expect(extractWarnings(response)).toEqual([
|
|
||||||
{
|
|
||||||
type: 'shard_failure',
|
|
||||||
message: '2 of 4 shards failed',
|
|
||||||
reason: {
|
|
||||||
type: 'illegal_argument_exception',
|
|
||||||
reason:
|
|
||||||
'Field [kubernetes.container.memory.available.bytes] of type' +
|
|
||||||
' [aggregate_metric_double] is not supported for aggregation [percentiles]',
|
|
||||||
},
|
},
|
||||||
text: 'The data might be incomplete or wrong.',
|
]);
|
||||||
},
|
});
|
||||||
]);
|
|
||||||
|
it('should extract incomplete warning from response with time out', () => {
|
||||||
|
const response = {
|
||||||
|
took: 999,
|
||||||
|
timed_out: true,
|
||||||
|
_shards: {} as estypes.ShardStatistics,
|
||||||
|
hits: { hits: [] },
|
||||||
|
};
|
||||||
|
expect(extractWarnings(response)).toEqual([
|
||||||
|
{
|
||||||
|
type: 'incomplete',
|
||||||
|
message: 'The data might be incomplete or wrong.',
|
||||||
|
clusters: {
|
||||||
|
'(local)': {
|
||||||
|
status: 'partial',
|
||||||
|
indices: '',
|
||||||
|
took: 999,
|
||||||
|
timed_out: true,
|
||||||
|
_shards: response._shards,
|
||||||
|
failures: response._shards.failures,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not include warnings when there are none', () => {
|
||||||
|
const warnings = extractWarnings({
|
||||||
|
timed_out: false,
|
||||||
|
_shards: {
|
||||||
|
failed: 0,
|
||||||
|
total: 9000,
|
||||||
|
},
|
||||||
|
} as estypes.SearchResponse);
|
||||||
|
|
||||||
|
expect(warnings).toEqual([]);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should extract timeout warning', () => {
|
describe('remote clusters', () => {
|
||||||
const warnings = {
|
it('should extract incomplete warning from response with shard failures', () => {
|
||||||
took: 999,
|
const response = {
|
||||||
timed_out: true,
|
took: 25,
|
||||||
_shards: {} as estypes.ShardStatistics,
|
timed_out: false,
|
||||||
hits: { hits: [] },
|
_shards: {
|
||||||
};
|
total: 4,
|
||||||
expect(extractWarnings(warnings)).toEqual([
|
successful: 3,
|
||||||
{
|
skipped: 0,
|
||||||
type: 'timed_out',
|
failed: 1,
|
||||||
message: 'Data might be incomplete because your request timed out',
|
failures: [
|
||||||
},
|
{
|
||||||
]);
|
shard: 0,
|
||||||
});
|
index: 'remote1:.ds-kibana_sample_data_logs-2023.08.21-000001',
|
||||||
|
node: 'NVzFRd6SS4qT9o0k2vIzlg',
|
||||||
|
reason: {
|
||||||
|
type: 'query_shard_exception',
|
||||||
|
reason:
|
||||||
|
'failed to create query: [.ds-kibana_sample_data_logs-2023.08.21-000001][0] local shard failure message 123',
|
||||||
|
index_uuid: 'z1sPO8E4TdWcijNgsL_BxQ',
|
||||||
|
index: 'remote1:.ds-kibana_sample_data_logs-2023.08.21-000001',
|
||||||
|
caused_by: {
|
||||||
|
type: 'runtime_exception',
|
||||||
|
reason:
|
||||||
|
'runtime_exception: [.ds-kibana_sample_data_logs-2023.08.21-000001][0] local shard failure message 123',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
_clusters: {
|
||||||
|
total: 2,
|
||||||
|
successful: 2,
|
||||||
|
skipped: 0,
|
||||||
|
details: {
|
||||||
|
'(local)': {
|
||||||
|
status: 'successful',
|
||||||
|
indices: 'kibana_sample_data_logs,kibana_sample_data_flights',
|
||||||
|
took: 1,
|
||||||
|
timed_out: false,
|
||||||
|
_shards: {
|
||||||
|
total: 2,
|
||||||
|
successful: 2,
|
||||||
|
skipped: 0,
|
||||||
|
failed: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
remote1: {
|
||||||
|
status: 'partial',
|
||||||
|
indices: 'kibana_sample_data_logs,kibana_sample_data_flights',
|
||||||
|
took: 5,
|
||||||
|
timed_out: false,
|
||||||
|
_shards: {
|
||||||
|
total: 2,
|
||||||
|
successful: 1,
|
||||||
|
skipped: 0,
|
||||||
|
failed: 1,
|
||||||
|
},
|
||||||
|
failures: [
|
||||||
|
{
|
||||||
|
shard: 0,
|
||||||
|
index: 'remote1:.ds-kibana_sample_data_logs-2023.08.21-000001',
|
||||||
|
node: 'NVzFRd6SS4qT9o0k2vIzlg',
|
||||||
|
reason: {
|
||||||
|
type: 'query_shard_exception',
|
||||||
|
reason:
|
||||||
|
'failed to create query: [.ds-kibana_sample_data_logs-2023.08.21-000001][0] local shard failure message 123',
|
||||||
|
index_uuid: 'z1sPO8E4TdWcijNgsL_BxQ',
|
||||||
|
index: 'remote1:.ds-kibana_sample_data_logs-2023.08.21-000001',
|
||||||
|
caused_by: {
|
||||||
|
type: 'runtime_exception',
|
||||||
|
reason:
|
||||||
|
'runtime_exception: [.ds-kibana_sample_data_logs-2023.08.21-000001][0] local shard failure message 123',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
hits: { total: 18239, max_score: null, hits: [] },
|
||||||
|
aggregations: {},
|
||||||
|
};
|
||||||
|
|
||||||
it('should extract shards failed warnings', () => {
|
expect(extractWarnings(response)).toEqual([
|
||||||
const warnings = {
|
{
|
||||||
_shards: {
|
type: 'incomplete',
|
||||||
failed: 77,
|
message: 'The data might be incomplete or wrong.',
|
||||||
total: 79,
|
clusters: response._clusters.details,
|
||||||
},
|
},
|
||||||
} as estypes.SearchResponse;
|
]);
|
||||||
expect(extractWarnings(warnings)).toEqual([
|
});
|
||||||
{
|
|
||||||
type: 'shard_failure',
|
|
||||||
message: '77 of 79 shards failed',
|
|
||||||
reason: { type: 'generic_shard_warning' },
|
|
||||||
text: 'The data might be incomplete or wrong.',
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should extract shards failed warning failure reason type', () => {
|
it('should extract incomplete warning from response with time out', () => {
|
||||||
const warnings = extractWarnings({
|
const response = {
|
||||||
_shards: {
|
took: 999,
|
||||||
failed: 77,
|
timed_out: true,
|
||||||
total: 79,
|
_shards: {
|
||||||
},
|
total: 6,
|
||||||
} as estypes.SearchResponse);
|
successful: 6,
|
||||||
expect(warnings).toEqual([
|
skipped: 0,
|
||||||
{
|
failed: 0,
|
||||||
type: 'shard_failure',
|
},
|
||||||
message: '77 of 79 shards failed',
|
_clusters: {
|
||||||
reason: { type: 'generic_shard_warning' },
|
total: 2,
|
||||||
text: 'The data might be incomplete or wrong.',
|
successful: 2,
|
||||||
},
|
skipped: 0,
|
||||||
]);
|
details: {
|
||||||
});
|
'(local)': {
|
||||||
|
status: 'successful',
|
||||||
|
indices:
|
||||||
|
'kibana_sample_data_ecommerce,kibana_sample_data_logs,kibana_sample_data_flights',
|
||||||
|
took: 0,
|
||||||
|
timed_out: false,
|
||||||
|
_shards: {
|
||||||
|
total: 3,
|
||||||
|
successful: 3,
|
||||||
|
skipped: 0,
|
||||||
|
failed: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
remote1: {
|
||||||
|
status: 'partial',
|
||||||
|
indices: 'kibana_sample_data*',
|
||||||
|
took: 10005,
|
||||||
|
timed_out: true,
|
||||||
|
_shards: {
|
||||||
|
total: 3,
|
||||||
|
successful: 3,
|
||||||
|
skipped: 0,
|
||||||
|
failed: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
hits: { hits: [] },
|
||||||
|
};
|
||||||
|
expect(extractWarnings(response)).toEqual([
|
||||||
|
{
|
||||||
|
type: 'incomplete',
|
||||||
|
message: 'The data might be incomplete or wrong.',
|
||||||
|
clusters: response._clusters.details,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
it('extracts multiple warnings', () => {
|
it('should not include warnings when there are none', () => {
|
||||||
const warnings = extractWarnings({
|
const warnings = extractWarnings({
|
||||||
timed_out: true,
|
took: 10,
|
||||||
_shards: {
|
timed_out: false,
|
||||||
failed: 77,
|
_shards: {
|
||||||
total: 79,
|
total: 4,
|
||||||
},
|
successful: 4,
|
||||||
} as estypes.SearchResponse);
|
skipped: 0,
|
||||||
const [shardFailures, timedOut] = [
|
failed: 0,
|
||||||
warnings.filter(({ type }) => type !== 'timed_out'),
|
},
|
||||||
warnings.filter(({ type }) => type === 'timed_out'),
|
_clusters: {
|
||||||
];
|
total: 2,
|
||||||
expect(shardFailures[0]!.message).toBeDefined();
|
successful: 2,
|
||||||
expect(timedOut[0]!.message).toBeDefined();
|
skipped: 0,
|
||||||
});
|
details: {
|
||||||
|
'(local)': {
|
||||||
|
status: 'successful',
|
||||||
|
indices: 'kibana_sample_data_logs,kibana_sample_data_flights',
|
||||||
|
took: 0,
|
||||||
|
timed_out: false,
|
||||||
|
_shards: {
|
||||||
|
total: 2,
|
||||||
|
successful: 2,
|
||||||
|
skipped: 0,
|
||||||
|
failed: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
remote1: {
|
||||||
|
status: 'successful',
|
||||||
|
indices: 'kibana_sample_data_logs,kibana_sample_data_flights',
|
||||||
|
took: 1,
|
||||||
|
timed_out: false,
|
||||||
|
_shards: {
|
||||||
|
total: 2,
|
||||||
|
successful: 2,
|
||||||
|
skipped: 0,
|
||||||
|
failed: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
hits: { hits: [] },
|
||||||
|
} as estypes.SearchResponse);
|
||||||
|
|
||||||
it('should not include shardStats or types fields if there are no warnings', () => {
|
expect(warnings).toEqual([]);
|
||||||
const warnings = extractWarnings({
|
});
|
||||||
timed_out: false,
|
|
||||||
_shards: {
|
|
||||||
failed: 0,
|
|
||||||
total: 9000,
|
|
||||||
},
|
|
||||||
} as estypes.SearchResponse);
|
|
||||||
|
|
||||||
expect(warnings).toEqual([]);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -8,6 +8,7 @@
|
||||||
|
|
||||||
import { estypes } from '@elastic/elasticsearch';
|
import { estypes } from '@elastic/elasticsearch';
|
||||||
import { i18n } from '@kbn/i18n';
|
import { i18n } from '@kbn/i18n';
|
||||||
|
import type { ClusterDetails } from '@kbn/es-types';
|
||||||
import { SearchResponseWarning } from '../types';
|
import { SearchResponseWarning } from '../types';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -16,53 +17,38 @@ import { SearchResponseWarning } from '../types';
|
||||||
export function extractWarnings(rawResponse: estypes.SearchResponse): SearchResponseWarning[] {
|
export function extractWarnings(rawResponse: estypes.SearchResponse): SearchResponseWarning[] {
|
||||||
const warnings: SearchResponseWarning[] = [];
|
const warnings: SearchResponseWarning[] = [];
|
||||||
|
|
||||||
if (rawResponse.timed_out === true) {
|
const isPartial = rawResponse._clusters
|
||||||
|
? Object.values(
|
||||||
|
(
|
||||||
|
rawResponse._clusters as estypes.ClusterStatistics & {
|
||||||
|
details: Record<string, ClusterDetails>;
|
||||||
|
}
|
||||||
|
).details
|
||||||
|
).some((clusterDetails) => clusterDetails.status !== 'successful')
|
||||||
|
: rawResponse.timed_out || rawResponse._shards.failed > 0;
|
||||||
|
if (isPartial) {
|
||||||
warnings.push({
|
warnings.push({
|
||||||
type: 'timed_out',
|
type: 'incomplete',
|
||||||
message: i18n.translate('data.search.searchSource.fetch.requestTimedOutNotificationMessage', {
|
message: i18n.translate('data.search.searchSource.fetch.incompleteResultsMessage', {
|
||||||
defaultMessage: 'Data might be incomplete because your request timed out',
|
defaultMessage: 'The data might be incomplete or wrong.',
|
||||||
}),
|
}),
|
||||||
reason: undefined, // exists so that callers do not have to cast when working with shard warnings.
|
clusters: rawResponse._clusters
|
||||||
});
|
? (
|
||||||
}
|
rawResponse._clusters as estypes.ClusterStatistics & {
|
||||||
|
details: Record<string, ClusterDetails>;
|
||||||
if (rawResponse._shards && rawResponse._shards.failed) {
|
}
|
||||||
const message = i18n.translate(
|
).details
|
||||||
'data.search.searchSource.fetch.shardsFailedNotificationMessage',
|
: {
|
||||||
{
|
'(local)': {
|
||||||
defaultMessage: '{shardsFailed} of {shardsTotal} shards failed',
|
status: 'partial',
|
||||||
values: {
|
indices: '',
|
||||||
shardsFailed: rawResponse._shards.failed,
|
took: rawResponse.took,
|
||||||
shardsTotal: rawResponse._shards.total,
|
timed_out: rawResponse.timed_out,
|
||||||
},
|
_shards: rawResponse._shards,
|
||||||
}
|
failures: rawResponse._shards.failures,
|
||||||
);
|
},
|
||||||
const text = i18n.translate(
|
|
||||||
'data.search.searchSource.fetch.shardsFailedNotificationDescription',
|
|
||||||
{ defaultMessage: 'The data might be incomplete or wrong.' }
|
|
||||||
);
|
|
||||||
|
|
||||||
if (rawResponse._shards.failures) {
|
|
||||||
rawResponse._shards.failures?.forEach((f) => {
|
|
||||||
warnings.push({
|
|
||||||
type: 'shard_failure',
|
|
||||||
message,
|
|
||||||
text,
|
|
||||||
reason: {
|
|
||||||
type: f.reason.type,
|
|
||||||
reason: f.reason.reason,
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// unknown type and reason
|
|
||||||
warnings.push({
|
|
||||||
type: 'shard_failure',
|
|
||||||
message,
|
|
||||||
text,
|
|
||||||
reason: { type: 'generic_shard_warning' },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return warnings;
|
return warnings;
|
||||||
|
|
|
@ -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 { estypes } from '@elastic/elasticsearch';
|
||||||
import { debounce } from 'lodash';
|
import { EuiTextAlign } from '@elastic/eui';
|
||||||
import { EuiSpacer, EuiTextAlign } from '@elastic/eui';
|
|
||||||
import { ThemeServiceStart } from '@kbn/core/public';
|
import { ThemeServiceStart } from '@kbn/core/public';
|
||||||
import { toMountPoint } from '@kbn/kibana-react-plugin/public';
|
import { toMountPoint } from '@kbn/kibana-react-plugin/public';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import type { MountPoint } from '@kbn/core/public';
|
|
||||||
import { SearchRequest } from '..';
|
import { SearchRequest } from '..';
|
||||||
import { getNotifications } from '../../services';
|
import { getNotifications } from '../../services';
|
||||||
import { ShardFailureOpenModalButton, ShardFailureRequest } from '../../shard_failure_modal';
|
import { OpenIncompleteResultsModalButton } from '../../incomplete_results_modal';
|
||||||
import {
|
import {
|
||||||
SearchResponseShardFailureWarning,
|
SearchResponseIncompleteWarning,
|
||||||
SearchResponseWarning,
|
SearchResponseWarning,
|
||||||
WarningHandlerCallback,
|
WarningHandlerCallback,
|
||||||
} from '../types';
|
} from '../types';
|
||||||
import { extractWarnings } from './extract_warnings';
|
import { extractWarnings } from './extract_warnings';
|
||||||
|
|
||||||
const getDebouncedWarning = () => {
|
|
||||||
const addWarning = () => {
|
|
||||||
const { toasts } = getNotifications();
|
|
||||||
return debounce(toasts.addWarning.bind(toasts), 30000, {
|
|
||||||
leading: true,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
const memory: Record<string, ReturnType<typeof addWarning>> = {};
|
|
||||||
|
|
||||||
return (
|
|
||||||
debounceKey: string,
|
|
||||||
title: string,
|
|
||||||
text?: string | MountPoint<HTMLElement> | undefined
|
|
||||||
) => {
|
|
||||||
memory[debounceKey] = memory[debounceKey] || addWarning();
|
|
||||||
return memory[debounceKey]({ title, text });
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const debouncedWarningWithoutReason = getDebouncedWarning();
|
|
||||||
const debouncedTimeoutWarning = getDebouncedWarning();
|
|
||||||
const debouncedWarning = getDebouncedWarning();
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @internal
|
* @internal
|
||||||
* All warnings are expected to come from the same response. Therefore all "text" properties, which contain the
|
* All warnings are expected to come from the same response.
|
||||||
* response, will be the same.
|
|
||||||
*/
|
*/
|
||||||
export function handleWarnings({
|
export function handleWarnings({
|
||||||
request,
|
request,
|
||||||
|
@ -78,47 +52,29 @@ export function handleWarnings({
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// timeout notification
|
// Incomplete data failure notification
|
||||||
const [timeout] = internal.filter((w) => w.type === 'timed_out');
|
const incompleteWarnings = internal.filter((w) => w.type === 'incomplete');
|
||||||
if (timeout) {
|
if (incompleteWarnings.length === 0) {
|
||||||
debouncedTimeoutWarning(sessionId + timeout.message, timeout.message);
|
|
||||||
}
|
|
||||||
|
|
||||||
// shard warning failure notification
|
|
||||||
const shardFailures = internal.filter((w) => w.type === 'shard_failure');
|
|
||||||
if (shardFailures.length === 0) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const [warning] = shardFailures as SearchResponseShardFailureWarning[];
|
const [incompleteWarning] = incompleteWarnings as SearchResponseIncompleteWarning[];
|
||||||
const title = warning.message;
|
getNotifications().toasts.addWarning({
|
||||||
|
title: incompleteWarning.message,
|
||||||
// if warning message contains text (warning response), show in ShardFailureOpenModalButton
|
text: toMountPoint(
|
||||||
if (warning.text) {
|
<EuiTextAlign textAlign="right">
|
||||||
const text = toMountPoint(
|
<OpenIncompleteResultsModalButton
|
||||||
<>
|
theme={theme}
|
||||||
{warning.text}
|
getRequestMeta={() => ({
|
||||||
<EuiSpacer size="s" />
|
request,
|
||||||
<EuiTextAlign textAlign="right">
|
response,
|
||||||
<ShardFailureOpenModalButton
|
})}
|
||||||
theme={theme}
|
warning={incompleteWarning}
|
||||||
title={title}
|
/>
|
||||||
getRequestMeta={() => ({
|
</EuiTextAlign>,
|
||||||
request: request as ShardFailureRequest,
|
|
||||||
response,
|
|
||||||
})}
|
|
||||||
/>
|
|
||||||
</EuiTextAlign>
|
|
||||||
</>,
|
|
||||||
{ theme$: theme.theme$ }
|
{ theme$: theme.theme$ }
|
||||||
);
|
),
|
||||||
|
});
|
||||||
debouncedWarning(sessionId + warning.text, title, text);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// timeout warning, or shard warning with no failure reason
|
|
||||||
debouncedWarningWithoutReason(sessionId + title, title);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -10,6 +10,7 @@ export * from './expressions';
|
||||||
|
|
||||||
export type {
|
export type {
|
||||||
SearchResponseWarning,
|
SearchResponseWarning,
|
||||||
|
SearchResponseIncompleteWarning,
|
||||||
ISearchSetup,
|
ISearchSetup,
|
||||||
ISearchStart,
|
ISearchStart,
|
||||||
ISearchStartSearchSource,
|
ISearchStartSearchSource,
|
||||||
|
|
|
@ -142,7 +142,7 @@ describe('Search service', () => {
|
||||||
|
|
||||||
expect(notifications.toasts.addWarning).toBeCalledTimes(1);
|
expect(notifications.toasts.addWarning).toBeCalledTimes(1);
|
||||||
expect(notifications.toasts.addWarning).toBeCalledWith({
|
expect(notifications.toasts.addWarning).toBeCalledWith({
|
||||||
title: '2 of 4 shards failed',
|
title: 'The data might be incomplete or wrong.',
|
||||||
text: expect.any(Function),
|
text: expect.any(Function),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -155,90 +155,6 @@ describe('Search service', () => {
|
||||||
|
|
||||||
expect(notifications.toasts.addWarning).toBeCalledTimes(0);
|
expect(notifications.toasts.addWarning).toBeCalledTimes(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('will show single notification when some warnings are filtered', () => {
|
|
||||||
callback = (warning) => warning.reason?.type === 'illegal_argument_exception';
|
|
||||||
shards.failures = [
|
|
||||||
{
|
|
||||||
reason: {
|
|
||||||
type: 'illegal_argument_exception',
|
|
||||||
reason: 'reason of "illegal_argument_exception"',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
reason: {
|
|
||||||
type: 'other_kind_of_exception',
|
|
||||||
reason: 'reason of other_kind_of_exception',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{ reason: { type: 'fatal_warning', reason: 'this is a fatal warning message' } },
|
|
||||||
] as unknown as estypes.ShardFailure[];
|
|
||||||
|
|
||||||
const responder = inspector.adapter.start('request1');
|
|
||||||
responder.ok(getMockResponseWithShards(shards));
|
|
||||||
data.showWarnings(inspector.adapter, callback);
|
|
||||||
|
|
||||||
expect(notifications.toasts.addWarning).toBeCalledTimes(1);
|
|
||||||
expect(notifications.toasts.addWarning).toBeCalledWith({
|
|
||||||
title: '2 of 4 shards failed',
|
|
||||||
text: expect.any(Function),
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('can show a timed_out warning', () => {
|
|
||||||
const responder = inspector.adapter.start('request1');
|
|
||||||
shards = { total: 4, successful: 4, skipped: 0, failed: 0 };
|
|
||||||
const response1 = getMockResponseWithShards(shards);
|
|
||||||
response1.json.rawResponse.timed_out = true;
|
|
||||||
responder.ok(response1);
|
|
||||||
data.showWarnings(inspector.adapter, callback);
|
|
||||||
|
|
||||||
expect(notifications.toasts.addWarning).toBeCalledTimes(1);
|
|
||||||
expect(notifications.toasts.addWarning).toBeCalledWith({
|
|
||||||
title: 'Data might be incomplete because your request timed out',
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('can show two warnings if response has shard failures and also timed_out', () => {
|
|
||||||
const responder = inspector.adapter.start('request1');
|
|
||||||
const response1 = getMockResponseWithShards(shards);
|
|
||||||
response1.json.rawResponse.timed_out = true;
|
|
||||||
responder.ok(response1);
|
|
||||||
data.showWarnings(inspector.adapter, callback);
|
|
||||||
|
|
||||||
expect(notifications.toasts.addWarning).toBeCalledTimes(2);
|
|
||||||
expect(notifications.toasts.addWarning).nthCalledWith(1, {
|
|
||||||
title: 'Data might be incomplete because your request timed out',
|
|
||||||
});
|
|
||||||
expect(notifications.toasts.addWarning).nthCalledWith(2, {
|
|
||||||
title: '2 of 4 shards failed',
|
|
||||||
text: expect.any(Function),
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('will show multiple warnings when multiple responses have shard failures', () => {
|
|
||||||
const responder1 = inspector.adapter.start('request1');
|
|
||||||
const responder2 = inspector.adapter.start('request2');
|
|
||||||
responder1.ok(getMockResponseWithShards(shards));
|
|
||||||
responder2.ok({
|
|
||||||
json: {
|
|
||||||
rawResponse: {
|
|
||||||
timed_out: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
data.showWarnings(inspector.adapter, callback);
|
|
||||||
|
|
||||||
expect(notifications.toasts.addWarning).toBeCalledTimes(2);
|
|
||||||
expect(notifications.toasts.addWarning).nthCalledWith(1, {
|
|
||||||
title: '2 of 4 shards failed',
|
|
||||||
text: expect.any(Function),
|
|
||||||
});
|
|
||||||
expect(notifications.toasts.addWarning).nthCalledWith(2, {
|
|
||||||
title: 'Data might be incomplete because your request timed out',
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -243,7 +243,7 @@ export class SearchService implements Plugin<ISearchSetup, ISearchStart> {
|
||||||
getConfig: uiSettings.get.bind(uiSettings),
|
getConfig: uiSettings.get.bind(uiSettings),
|
||||||
search,
|
search,
|
||||||
onResponse: (request, response, options) => {
|
onResponse: (request, response, options) => {
|
||||||
if (!options.disableShardFailureWarning) {
|
if (!options.disableWarningToasts) {
|
||||||
const { rawResponse } = response;
|
const { rawResponse } = response;
|
||||||
|
|
||||||
handleWarnings({
|
handleWarnings({
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { estypes } from '@elastic/elasticsearch';
|
import { estypes } from '@elastic/elasticsearch';
|
||||||
|
import type { ClusterDetails } from '@kbn/es-types';
|
||||||
import type { PackageInfo } from '@kbn/core/server';
|
import type { PackageInfo } from '@kbn/core/server';
|
||||||
import { DataViewsContract } from '@kbn/data-views-plugin/common';
|
import { DataViewsContract } from '@kbn/data-views-plugin/common';
|
||||||
import { RequestAdapter } from '@kbn/inspector-plugin/public';
|
import { RequestAdapter } from '@kbn/inspector-plugin/public';
|
||||||
|
@ -96,63 +97,35 @@ export interface SearchServiceStartDependencies {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A warning object for a search response with internal ES timeouts
|
* A warning object for a search response with incomplete ES results
|
||||||
|
* ES returns incomplete results when:
|
||||||
|
* 1) Set timeout flag on search and the timeout expires on cluster
|
||||||
|
* 2) Some shard failures on a cluster
|
||||||
|
* 3) skipped remote(s) (skip_unavailable=true)
|
||||||
|
* a. all shards failed
|
||||||
|
* b. disconnected/not-connected
|
||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
export interface SearchResponseTimeoutWarning {
|
export interface SearchResponseIncompleteWarning {
|
||||||
/**
|
/**
|
||||||
* type: for sorting out timeout warnings
|
* type: for sorting out incomplete warnings
|
||||||
*/
|
*/
|
||||||
type: 'timed_out';
|
type: 'incomplete';
|
||||||
/**
|
/**
|
||||||
* message: human-friendly message
|
* message: human-friendly message
|
||||||
*/
|
*/
|
||||||
message: string;
|
message: string;
|
||||||
/**
|
/**
|
||||||
* reason: not given for timeout. This exists so that callers do not have to cast when working with shard failure warnings.
|
* clusters: cluster details.
|
||||||
*/
|
*/
|
||||||
reason: undefined;
|
clusters: Record<string, ClusterDetails>;
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A warning object for a search response with internal ES shard failures
|
|
||||||
* @public
|
|
||||||
*/
|
|
||||||
export interface SearchResponseShardFailureWarning {
|
|
||||||
/**
|
|
||||||
* type: for sorting out shard failure warnings
|
|
||||||
*/
|
|
||||||
type: 'shard_failure';
|
|
||||||
/**
|
|
||||||
* message: human-friendly message
|
|
||||||
*/
|
|
||||||
message: string;
|
|
||||||
/**
|
|
||||||
* text: text to show in ShardFailureModal (optional)
|
|
||||||
*/
|
|
||||||
text?: string;
|
|
||||||
/**
|
|
||||||
* reason: ShardFailureReason from es client
|
|
||||||
*/
|
|
||||||
reason: {
|
|
||||||
/**
|
|
||||||
* type: failure code from Elasticsearch
|
|
||||||
*/
|
|
||||||
type: 'generic_shard_warning' | estypes.ShardFailure['reason']['type'];
|
|
||||||
/**
|
|
||||||
* reason: failure reason from Elasticsearch
|
|
||||||
*/
|
|
||||||
reason?: estypes.ShardFailure['reason']['reason'];
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A warning object for a search response with warnings
|
* A warning object for a search response with warnings
|
||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
export type SearchResponseWarning =
|
export type SearchResponseWarning = SearchResponseIncompleteWarning;
|
||||||
| SearchResponseTimeoutWarning
|
|
||||||
| SearchResponseShardFailureWarning;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A callback function which can intercept warnings when passed to {@link showWarnings}. Pass `true` from the
|
* A callback function which can intercept warnings when passed to {@link showWarnings}. Pass `true` from the
|
||||||
|
|
|
@ -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.
|
* Side Public License, v 1.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
|
import { estypes } from '@elastic/elasticsearch';
|
||||||
|
|
||||||
export const shardFailureResponse: estypes.SearchResponse<any> = {
|
export const shardFailureResponse: estypes.SearchResponse<any> = {
|
||||||
_shards: {
|
_shards: {
|
||||||
|
@ -33,4 +33,4 @@ export const shardFailureResponse: estypes.SearchResponse<any> = {
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
} as any;
|
} as unknown as estypes.SearchResponse<any>;
|
||||||
|
|
|
@ -10,7 +10,6 @@ exports[`ShardFailureDescription renders matching snapshot given valid propertie
|
||||||
grow={false}
|
grow={false}
|
||||||
>
|
>
|
||||||
<EuiDescriptionList
|
<EuiDescriptionList
|
||||||
className="shardFailureModal__desc"
|
|
||||||
columnWidths={
|
columnWidths={
|
||||||
Array [
|
Array [
|
||||||
1,
|
1,
|
||||||
|
@ -82,7 +81,6 @@ exports[`ShardFailureDescription should show more details when button is pressed
|
||||||
grow={false}
|
grow={false}
|
||||||
>
|
>
|
||||||
<EuiDescriptionList
|
<EuiDescriptionList
|
||||||
className="shardFailureModal__desc"
|
|
||||||
columnWidths={
|
columnWidths={
|
||||||
Array [
|
Array [
|
||||||
1,
|
1,
|
||||||
|
|
|
@ -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', () => {
|
describe('ShardFailureDescription', () => {
|
||||||
it('renders matching snapshot given valid properties', () => {
|
it('renders matching snapshot given valid properties', () => {
|
||||||
const failure = (shardFailureResponse._shards as any).failures[0];
|
const failure = shardFailureResponse._shards.failures![0];
|
||||||
const component = shallowWithIntl(<ShardFailureDescription {...failure} />);
|
const component = shallowWithIntl(<ShardFailureDescription {...failure} />);
|
||||||
expect(component).toMatchSnapshot();
|
expect(component).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should show more details when button is pressed', async () => {
|
it('should show more details when button is pressed', async () => {
|
||||||
const failure = (shardFailureResponse._shards as any).failures[0];
|
const failure = shardFailureResponse._shards.failures![0];
|
||||||
const component = shallowWithIntl(<ShardFailureDescription {...failure} />);
|
const component = shallowWithIntl(<ShardFailureDescription {...failure} />);
|
||||||
await component.find(EuiButtonEmpty).simulate('click');
|
await component.find(EuiButtonEmpty).simulate('click');
|
||||||
expect(component).toMatchSnapshot();
|
expect(component).toMatchSnapshot();
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
|
import { estypes } from '@elastic/elasticsearch';
|
||||||
import { i18n } from '@kbn/i18n';
|
import { i18n } from '@kbn/i18n';
|
||||||
import { css } from '@emotion/react';
|
import { css } from '@emotion/react';
|
||||||
import { getFlattenedObject } from '@kbn/std';
|
import { getFlattenedObject } from '@kbn/std';
|
||||||
|
@ -17,7 +18,6 @@ import {
|
||||||
EuiFlexGroup,
|
EuiFlexGroup,
|
||||||
EuiFlexItem,
|
EuiFlexItem,
|
||||||
} from '@elastic/eui';
|
} from '@elastic/eui';
|
||||||
import { ShardFailure } from './shard_failure_types';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Provides pretty formatting of a given key string
|
* Provides pretty formatting of a given key string
|
||||||
|
@ -47,7 +47,7 @@ export function formatValueByKey(value: unknown, key: string): string | JSX.Elem
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ShardFailureDescription(props: ShardFailure) {
|
export function ShardFailureDescription(props: estypes.ShardFailure) {
|
||||||
const [showDetails, setShowDetails] = useState<boolean>(false);
|
const [showDetails, setShowDetails] = useState<boolean>(false);
|
||||||
|
|
||||||
const flattendReason = getFlattenedObject(props.reason);
|
const flattendReason = getFlattenedObject(props.reason);
|
||||||
|
@ -70,7 +70,7 @@ export function ShardFailureDescription(props: ShardFailure) {
|
||||||
title: i18n.translate('data.search.searchSource.fetch.shardsFailedModal.indexTitle', {
|
title: i18n.translate('data.search.searchSource.fetch.shardsFailedModal.indexTitle', {
|
||||||
defaultMessage: 'Index',
|
defaultMessage: 'Index',
|
||||||
}),
|
}),
|
||||||
description: props.index,
|
description: props.index ?? '',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: i18n.translate('data.search.searchSource.fetch.shardsFailedModal.reasonTypeTitle', {
|
title: i18n.translate('data.search.searchSource.fetch.shardsFailedModal.reasonTypeTitle', {
|
||||||
|
@ -84,7 +84,7 @@ export function ShardFailureDescription(props: ShardFailure) {
|
||||||
title: i18n.translate('data.search.searchSource.fetch.shardsFailedModal.nodeTitle', {
|
title: i18n.translate('data.search.searchSource.fetch.shardsFailedModal.nodeTitle', {
|
||||||
defaultMessage: 'Node',
|
defaultMessage: 'Node',
|
||||||
}),
|
}),
|
||||||
description: props.node,
|
description: props.node ?? '',
|
||||||
},
|
},
|
||||||
...reasonItems,
|
...reasonItems,
|
||||||
]
|
]
|
||||||
|
@ -99,7 +99,6 @@ export function ShardFailureDescription(props: ShardFailure) {
|
||||||
columnWidths={[1, 6]}
|
columnWidths={[1, 6]}
|
||||||
listItems={items}
|
listItems={items}
|
||||||
compressed
|
compressed
|
||||||
className="shardFailureModal__desc"
|
|
||||||
titleProps={{ className: 'shardFailureModal__descTitle' }}
|
titleProps={{ className: 'shardFailureModal__descTitle' }}
|
||||||
descriptionProps={{ className: 'shardFailureModal__descValue' }}
|
descriptionProps={{ className: 'shardFailureModal__descValue' }}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -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 { shallowWithIntl } from '@kbn/test-jest-helpers';
|
||||||
import { ShardFailureTable } from './shard_failure_table';
|
import { ShardFailureTable } from './shard_failure_table';
|
||||||
import { shardFailureResponse } from './__mocks__/shard_failure_response';
|
import { shardFailureResponse } from './__mocks__/shard_failure_response';
|
||||||
import { ShardFailure } from './shard_failure_types';
|
|
||||||
|
|
||||||
describe('ShardFailureTable', () => {
|
describe('ShardFailureTable', () => {
|
||||||
it('renders matching snapshot given valid properties', () => {
|
it('renders matching snapshot given valid properties', () => {
|
||||||
const failures = (shardFailureResponse._shards as any).failures as ShardFailure[];
|
const component = shallowWithIntl(
|
||||||
const component = shallowWithIntl(<ShardFailureTable failures={failures} />);
|
<ShardFailureTable failures={shardFailureResponse._shards.failures!} />
|
||||||
|
);
|
||||||
expect(component).toMatchSnapshot();
|
expect(component).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -7,13 +7,13 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { estypes } from '@elastic/elasticsearch';
|
||||||
import { i18n } from '@kbn/i18n';
|
import { i18n } from '@kbn/i18n';
|
||||||
import { css } from '@emotion/react';
|
import { css } from '@emotion/react';
|
||||||
import { EuiInMemoryTable, EuiInMemoryTableProps, euiScreenReaderOnly } from '@elastic/eui';
|
import { EuiInMemoryTable, EuiInMemoryTableProps, euiScreenReaderOnly } from '@elastic/eui';
|
||||||
import { ShardFailureDescription } from './shard_failure_description';
|
import { ShardFailureDescription } from './shard_failure_description';
|
||||||
import { ShardFailure } from './shard_failure_types';
|
|
||||||
|
|
||||||
export interface ListItem extends ShardFailure {
|
export interface ListItem extends estypes.ShardFailure {
|
||||||
id: string;
|
id: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -24,7 +24,7 @@ const SORTING: EuiInMemoryTableProps<ListItem>['sorting'] = {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export function ShardFailureTable({ failures }: { failures: ShardFailure[] }) {
|
export function ShardFailureTable({ failures }: { failures: estypes.ShardFailure[] }) {
|
||||||
const itemList = failures.map((failure, idx) => ({ ...{ id: String(idx) }, ...failure }));
|
const itemList = failures.map((failure, idx) => ({ ...{ id: String(idx) }, ...failure }));
|
||||||
|
|
||||||
const columns = [
|
const columns = [
|
||||||
|
|
|
@ -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,
|
query: undefined,
|
||||||
searchSessionId: 'abc123',
|
searchSessionId: 'abc123',
|
||||||
searchSourceService: startDependencies.searchSource,
|
searchSourceService: startDependencies.searchSource,
|
||||||
disableShardWarnings: false,
|
disableWarningToasts: false,
|
||||||
timeFields: args.timeFields,
|
timeFields: args.timeFields,
|
||||||
timeRange: undefined,
|
timeRange: undefined,
|
||||||
});
|
});
|
||||||
|
|
|
@ -72,7 +72,7 @@ export function getFunctionDefinition({
|
||||||
query: get(input, 'query', undefined) as any,
|
query: get(input, 'query', undefined) as any,
|
||||||
searchSessionId: getSearchSessionId(),
|
searchSessionId: getSearchSessionId(),
|
||||||
searchSourceService: searchSource,
|
searchSourceService: searchSource,
|
||||||
disableShardWarnings: false,
|
disableWarningToasts: false,
|
||||||
timeFields: args.timeFields,
|
timeFields: args.timeFields,
|
||||||
timeRange: get(input, 'timeRange', undefined),
|
timeRange: get(input, 'timeRange', undefined),
|
||||||
})
|
})
|
||||||
|
|
|
@ -49,7 +49,8 @@
|
||||||
"@kbn/core-saved-objects-server",
|
"@kbn/core-saved-objects-server",
|
||||||
"@kbn/core-saved-objects-utils-server",
|
"@kbn/core-saved-objects-utils-server",
|
||||||
"@kbn/data-service",
|
"@kbn/data-service",
|
||||||
"@kbn/react-kibana-context-render"
|
"@kbn/react-kibana-context-render",
|
||||||
|
"@kbn/es-types"
|
||||||
],
|
],
|
||||||
"exclude": [
|
"exclude": [
|
||||||
"target/**/*",
|
"target/**/*",
|
||||||
|
|
|
@ -17,7 +17,6 @@ export enum VIEW_MODE {
|
||||||
AGGREGATED_LEVEL = 'aggregated',
|
AGGREGATED_LEVEL = 'aggregated',
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DISABLE_SHARD_FAILURE_WARNING = true;
|
|
||||||
export const getDefaultRowsPerPage = (uiSettings: IUiSettingsClient): number => {
|
export const getDefaultRowsPerPage = (uiSettings: IUiSettingsClient): number => {
|
||||||
return parseInt(uiSettings.get(SAMPLE_ROWS_PER_PAGE_SETTING), 10) || DEFAULT_ROWS_PER_PAGE;
|
return parseInt(uiSettings.get(SAMPLE_ROWS_PER_PAGE_SETTING), 10) || DEFAULT_ROWS_PER_PAGE;
|
||||||
};
|
};
|
||||||
|
|
|
@ -18,7 +18,6 @@ import { useExecutionContext } from '@kbn/kibana-react-plugin/public';
|
||||||
import { generateFilters } from '@kbn/data-plugin/public';
|
import { generateFilters } from '@kbn/data-plugin/public';
|
||||||
import { i18n } from '@kbn/i18n';
|
import { i18n } from '@kbn/i18n';
|
||||||
import { reportPerformanceMetricEvent } from '@kbn/ebt-tools';
|
import { reportPerformanceMetricEvent } from '@kbn/ebt-tools';
|
||||||
import { removeInterceptedWarningDuplicates } from '@kbn/search-response-warnings';
|
|
||||||
import {
|
import {
|
||||||
DOC_TABLE_LEGACY,
|
DOC_TABLE_LEGACY,
|
||||||
SEARCH_FIELDS_FROM_SOURCE,
|
SEARCH_FIELDS_FROM_SOURCE,
|
||||||
|
@ -177,12 +176,11 @@ export const ContextApp = ({ dataView, anchorId, referrer }: ContextAppProps) =>
|
||||||
);
|
);
|
||||||
|
|
||||||
const interceptedWarnings = useMemo(
|
const interceptedWarnings = useMemo(
|
||||||
() =>
|
() => [
|
||||||
removeInterceptedWarningDuplicates([
|
...(fetchedState.predecessorsInterceptedWarnings || []),
|
||||||
...(fetchedState.predecessorsInterceptedWarnings || []),
|
...(fetchedState.anchorInterceptedWarnings || []),
|
||||||
...(fetchedState.anchorInterceptedWarnings || []),
|
...(fetchedState.successorsInterceptedWarnings || []),
|
||||||
...(fetchedState.successorsInterceptedWarnings || []),
|
],
|
||||||
]),
|
|
||||||
[
|
[
|
||||||
fetchedState.predecessorsInterceptedWarnings,
|
fetchedState.predecessorsInterceptedWarnings,
|
||||||
fetchedState.anchorInterceptedWarnings,
|
fetchedState.anchorInterceptedWarnings,
|
||||||
|
|
|
@ -19,15 +19,15 @@ import {
|
||||||
mockSuccessorHits,
|
mockSuccessorHits,
|
||||||
} from '../__mocks__/use_context_app_fetch';
|
} from '../__mocks__/use_context_app_fetch';
|
||||||
import { dataViewWithTimefieldMock } from '../../../__mocks__/data_view_with_timefield';
|
import { dataViewWithTimefieldMock } from '../../../__mocks__/data_view_with_timefield';
|
||||||
import { searchResponseWarningsMock } from '@kbn/search-response-warnings/src/__mocks__/search_response_warnings';
|
import { searchResponseIncompleteWarningLocalCluster } from '@kbn/search-response-warnings/src/__mocks__/search_response_warnings';
|
||||||
import { createContextSearchSourceStub } from '../services/_stubs';
|
import { createContextSearchSourceStub } from '../services/_stubs';
|
||||||
import { DataView } from '@kbn/data-views-plugin/public';
|
import { DataView } from '@kbn/data-views-plugin/public';
|
||||||
import { themeServiceMock } from '@kbn/core/public/mocks';
|
import { themeServiceMock } from '@kbn/core/public/mocks';
|
||||||
import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public';
|
import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public';
|
||||||
|
|
||||||
const mockInterceptedWarnings = searchResponseWarningsMock.map((originalWarning) => ({
|
const mockInterceptedWarning = {
|
||||||
originalWarning,
|
originalWarning: searchResponseIncompleteWarningLocalCluster,
|
||||||
}));
|
};
|
||||||
|
|
||||||
const mockFilterManager = createFilterManagerMock();
|
const mockFilterManager = createFilterManagerMock();
|
||||||
|
|
||||||
|
@ -44,9 +44,7 @@ jest.mock('../services/context', () => {
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
rows: type === 'predecessors' ? mockPredecessorHits : mockSuccessorHits,
|
rows: type === 'predecessors' ? mockPredecessorHits : mockSuccessorHits,
|
||||||
interceptedWarnings: mockOverrideInterceptedWarnings
|
interceptedWarnings: mockOverrideInterceptedWarnings ? [mockInterceptedWarning] : undefined,
|
||||||
? [mockInterceptedWarnings[type === 'predecessors' ? 0 : 1]]
|
|
||||||
: undefined,
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -59,9 +57,7 @@ jest.mock('../services/anchor', () => ({
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
anchorRow: mockAnchorHit,
|
anchorRow: mockAnchorHit,
|
||||||
interceptedWarnings: mockOverrideInterceptedWarnings
|
interceptedWarnings: mockOverrideInterceptedWarnings ? [mockInterceptedWarning] : undefined,
|
||||||
? [mockInterceptedWarnings[2]]
|
|
||||||
: undefined,
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
@ -228,13 +224,11 @@ describe('test useContextAppFetch', () => {
|
||||||
expect(result.current.fetchedState.predecessors).toEqual(mockPredecessorHits);
|
expect(result.current.fetchedState.predecessors).toEqual(mockPredecessorHits);
|
||||||
expect(result.current.fetchedState.successors).toEqual(mockSuccessorHits);
|
expect(result.current.fetchedState.successors).toEqual(mockSuccessorHits);
|
||||||
expect(result.current.fetchedState.predecessorsInterceptedWarnings).toEqual([
|
expect(result.current.fetchedState.predecessorsInterceptedWarnings).toEqual([
|
||||||
mockInterceptedWarnings[0],
|
mockInterceptedWarning,
|
||||||
]);
|
]);
|
||||||
expect(result.current.fetchedState.successorsInterceptedWarnings).toEqual([
|
expect(result.current.fetchedState.successorsInterceptedWarnings).toEqual([
|
||||||
mockInterceptedWarnings[1],
|
mockInterceptedWarning,
|
||||||
]);
|
|
||||||
expect(result.current.fetchedState.anchorInterceptedWarnings).toEqual([
|
|
||||||
mockInterceptedWarnings[2],
|
|
||||||
]);
|
]);
|
||||||
|
expect(result.current.fetchedState.anchorInterceptedWarnings).toEqual([mockInterceptedWarning]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -10,7 +10,7 @@ import { SortDirection } from '@kbn/data-plugin/public';
|
||||||
import { createSearchSourceStub } from './_stubs';
|
import { createSearchSourceStub } from './_stubs';
|
||||||
import { fetchAnchor, updateSearchSource } from './anchor';
|
import { fetchAnchor, updateSearchSource } from './anchor';
|
||||||
import { dataViewMock } from '@kbn/discover-utils/src/__mocks__';
|
import { dataViewMock } from '@kbn/discover-utils/src/__mocks__';
|
||||||
import { searchResponseTimeoutWarningMock } from '@kbn/search-response-warnings/src/__mocks__/search_response_warnings';
|
import { searchResponseIncompleteWarningLocalCluster } from '@kbn/search-response-warnings/src/__mocks__/search_response_warnings';
|
||||||
import { savedSearchMock } from '../../../__mocks__/saved_search';
|
import { savedSearchMock } from '../../../__mocks__/saved_search';
|
||||||
import { discoverServiceMock } from '../../../__mocks__/services';
|
import { discoverServiceMock } from '../../../__mocks__/services';
|
||||||
|
|
||||||
|
@ -206,7 +206,7 @@ describe('context app', function () {
|
||||||
).then(({ anchorRow, interceptedWarnings }) => {
|
).then(({ anchorRow, interceptedWarnings }) => {
|
||||||
expect(anchorRow).toHaveProperty('raw._id', '1');
|
expect(anchorRow).toHaveProperty('raw._id', '1');
|
||||||
expect(anchorRow).toHaveProperty('isAnchor', true);
|
expect(anchorRow).toHaveProperty('isAnchor', true);
|
||||||
expect(interceptedWarnings).toBeUndefined();
|
expect(interceptedWarnings).toEqual([]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -216,20 +216,10 @@ describe('context app', function () {
|
||||||
{ _id: '3', _index: 't' },
|
{ _id: '3', _index: 't' },
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const mockWarnings = [
|
|
||||||
{
|
|
||||||
originalWarning: searchResponseTimeoutWarningMock,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const services = discoverServiceMock;
|
const services = discoverServiceMock;
|
||||||
services.data.search.showWarnings = jest.fn((adapter, callback) => {
|
services.data.search.showWarnings = jest.fn((adapter, callback) => {
|
||||||
// @ts-expect-error for empty meta
|
// @ts-expect-error for empty meta
|
||||||
callback?.(mockWarnings[0].originalWarning, {});
|
callback?.(searchResponseIncompleteWarningLocalCluster, {});
|
||||||
|
|
||||||
// plus duplicates
|
|
||||||
// @ts-expect-error for empty meta
|
|
||||||
callback?.(mockWarnings[0].originalWarning, {});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return fetchAnchor(
|
return fetchAnchor(
|
||||||
|
@ -242,7 +232,7 @@ describe('context app', function () {
|
||||||
).then(({ anchorRow, interceptedWarnings }) => {
|
).then(({ anchorRow, interceptedWarnings }) => {
|
||||||
expect(anchorRow).toHaveProperty('raw._id', '1');
|
expect(anchorRow).toHaveProperty('raw._id', '1');
|
||||||
expect(anchorRow).toHaveProperty('isAnchor', true);
|
expect(anchorRow).toHaveProperty('isAnchor', true);
|
||||||
expect(interceptedWarnings).toEqual(mockWarnings);
|
expect(interceptedWarnings?.length).toBe(1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -17,7 +17,6 @@ import {
|
||||||
type SearchResponseInterceptedWarning,
|
type SearchResponseInterceptedWarning,
|
||||||
} from '@kbn/search-response-warnings';
|
} from '@kbn/search-response-warnings';
|
||||||
import type { DiscoverServices } from '../../../build_services';
|
import type { DiscoverServices } from '../../../build_services';
|
||||||
import { DISABLE_SHARD_FAILURE_WARNING } from '../../../../common/constants';
|
|
||||||
|
|
||||||
export async function fetchAnchor(
|
export async function fetchAnchor(
|
||||||
anchorId: string,
|
anchorId: string,
|
||||||
|
@ -35,7 +34,7 @@ export async function fetchAnchor(
|
||||||
const adapter = new RequestAdapter();
|
const adapter = new RequestAdapter();
|
||||||
const { rawResponse } = await lastValueFrom(
|
const { rawResponse } = await lastValueFrom(
|
||||||
searchSource.fetch$({
|
searchSource.fetch$({
|
||||||
disableShardFailureWarning: DISABLE_SHARD_FAILURE_WARNING,
|
disableWarningToasts: true,
|
||||||
inspector: {
|
inspector: {
|
||||||
adapter,
|
adapter,
|
||||||
title: 'anchor',
|
title: 'anchor',
|
||||||
|
@ -56,9 +55,6 @@ export async function fetchAnchor(
|
||||||
interceptedWarnings: getSearchResponseInterceptedWarnings({
|
interceptedWarnings: getSearchResponseInterceptedWarnings({
|
||||||
services,
|
services,
|
||||||
adapter,
|
adapter,
|
||||||
options: {
|
|
||||||
disableShardFailureWarning: DISABLE_SHARD_FAILURE_WARNING,
|
|
||||||
},
|
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,6 +16,7 @@ import { Query } from '@kbn/es-query';
|
||||||
import { fetchSurroundingDocs, SurrDocType } from './context';
|
import { fetchSurroundingDocs, SurrDocType } from './context';
|
||||||
import { buildDataTableRecord, buildDataTableRecordList } from '@kbn/discover-utils';
|
import { buildDataTableRecord, buildDataTableRecordList } from '@kbn/discover-utils';
|
||||||
import { discoverServiceMock } from '../../../__mocks__/services';
|
import { discoverServiceMock } from '../../../__mocks__/services';
|
||||||
|
import { searchResponseIncompleteWarningLocalCluster } from '@kbn/search-response-warnings/src/__mocks__/search_response_warnings';
|
||||||
|
|
||||||
const MS_PER_DAY = 24 * 60 * 60 * 1000;
|
const MS_PER_DAY = 24 * 60 * 60 * 1000;
|
||||||
const ANCHOR_TIMESTAMP = new Date(MS_PER_DAY).toJSON();
|
const ANCHOR_TIMESTAMP = new Date(MS_PER_DAY).toJSON();
|
||||||
|
@ -257,28 +258,13 @@ describe('context successors', function () {
|
||||||
const removeFieldsSpy = mockSearchSource.removeField.withArgs('fieldsFromSource');
|
const removeFieldsSpy = mockSearchSource.removeField.withArgs('fieldsFromSource');
|
||||||
expect(removeFieldsSpy.calledOnce).toBe(true);
|
expect(removeFieldsSpy.calledOnce).toBe(true);
|
||||||
expect(setFieldsSpy.calledOnce).toBe(true);
|
expect(setFieldsSpy.calledOnce).toBe(true);
|
||||||
expect(interceptedWarnings).toBeUndefined();
|
expect(interceptedWarnings).toEqual([]);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('function fetchSuccessors with shard failures', function () {
|
describe('function fetchSuccessors with shard failures', function () {
|
||||||
const mockWarnings = [
|
|
||||||
{
|
|
||||||
originalWarning: {
|
|
||||||
message: 'Data might be incomplete because your request timed out 1',
|
|
||||||
type: 'timed_out',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
originalWarning: {
|
|
||||||
message: 'Data might be incomplete because your request timed out 2',
|
|
||||||
type: 'timed_out',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
mockSearchSource = createContextSearchSourceStub('@timestamp');
|
mockSearchSource = createContextSearchSourceStub('@timestamp');
|
||||||
|
|
||||||
|
@ -288,11 +274,7 @@ describe('context successors', function () {
|
||||||
createEmpty: jest.fn().mockImplementation(() => mockSearchSource),
|
createEmpty: jest.fn().mockImplementation(() => mockSearchSource),
|
||||||
},
|
},
|
||||||
showWarnings: jest.fn((adapter, callback) => {
|
showWarnings: jest.fn((adapter, callback) => {
|
||||||
callback(mockWarnings[0].originalWarning, {});
|
callback(searchResponseIncompleteWarningLocalCluster, {});
|
||||||
callback(mockWarnings[1].originalWarning, {});
|
|
||||||
// plus duplicates
|
|
||||||
callback(mockWarnings[0].originalWarning, {});
|
|
||||||
callback(mockWarnings[1].originalWarning, {});
|
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
} as unknown as DataPublicPluginStart;
|
} as unknown as DataPublicPluginStart;
|
||||||
|
@ -345,7 +327,7 @@ describe('context successors', function () {
|
||||||
buildDataTableRecordList(mockSearchSource._stubHits.slice(-3), dataView)
|
buildDataTableRecordList(mockSearchSource._stubHits.slice(-3), dataView)
|
||||||
);
|
);
|
||||||
expect(dataPluginMock.search.showWarnings).toHaveBeenCalledTimes(1);
|
expect(dataPluginMock.search.showWarnings).toHaveBeenCalledTimes(1);
|
||||||
expect(interceptedWarnings).toEqual(mockWarnings);
|
expect(interceptedWarnings?.length).toBe(1);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
@ -9,10 +9,7 @@ import type { Filter } from '@kbn/es-query';
|
||||||
import { DataView } from '@kbn/data-views-plugin/public';
|
import { DataView } from '@kbn/data-views-plugin/public';
|
||||||
import { DataPublicPluginStart, ISearchSource } from '@kbn/data-plugin/public';
|
import { DataPublicPluginStart, ISearchSource } from '@kbn/data-plugin/public';
|
||||||
import type { DataTableRecord } from '@kbn/discover-utils/types';
|
import type { DataTableRecord } from '@kbn/discover-utils/types';
|
||||||
import {
|
import type { SearchResponseInterceptedWarning } from '@kbn/search-response-warnings';
|
||||||
removeInterceptedWarningDuplicates,
|
|
||||||
type SearchResponseInterceptedWarning,
|
|
||||||
} from '@kbn/search-response-warnings';
|
|
||||||
import { reverseSortDir, SortDirection } from '../utils/sorting';
|
import { reverseSortDir, SortDirection } from '../utils/sorting';
|
||||||
import { convertIsoToMillis, extractNanos } from '../utils/date_conversion';
|
import { convertIsoToMillis, extractNanos } from '../utils/date_conversion';
|
||||||
import { fetchHitsInInterval } from '../utils/fetch_hits_in_interval';
|
import { fetchHitsInInterval } from '../utils/fetch_hits_in_interval';
|
||||||
|
@ -126,7 +123,7 @@ export async function fetchSurroundingDocs(
|
||||||
|
|
||||||
return {
|
return {
|
||||||
rows,
|
rows,
|
||||||
interceptedWarnings: removeInterceptedWarningDuplicates(interceptedWarnings),
|
interceptedWarnings,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -17,7 +17,6 @@ import {
|
||||||
import { RequestAdapter } from '@kbn/inspector-plugin/common';
|
import { RequestAdapter } from '@kbn/inspector-plugin/common';
|
||||||
import { convertTimeValueToIso } from './date_conversion';
|
import { convertTimeValueToIso } from './date_conversion';
|
||||||
import { IntervalValue } from './generate_intervals';
|
import { IntervalValue } from './generate_intervals';
|
||||||
import { DISABLE_SHARD_FAILURE_WARNING } from '../../../../common/constants';
|
|
||||||
import type { SurrDocType } from '../services/context';
|
import type { SurrDocType } from '../services/context';
|
||||||
import type { DiscoverServices } from '../../../build_services';
|
import type { DiscoverServices } from '../../../build_services';
|
||||||
|
|
||||||
|
@ -91,7 +90,7 @@ export async function fetchHitsInInterval(
|
||||||
.setField('sort', sort)
|
.setField('sort', sort)
|
||||||
.setField('version', true)
|
.setField('version', true)
|
||||||
.fetch$({
|
.fetch$({
|
||||||
disableShardFailureWarning: DISABLE_SHARD_FAILURE_WARNING,
|
disableWarningToasts: true,
|
||||||
inspector: {
|
inspector: {
|
||||||
adapter,
|
adapter,
|
||||||
title: type,
|
title: type,
|
||||||
|
@ -107,9 +106,6 @@ export async function fetchHitsInInterval(
|
||||||
interceptedWarnings: getSearchResponseInterceptedWarnings({
|
interceptedWarnings: getSearchResponseInterceptedWarnings({
|
||||||
services,
|
services,
|
||||||
adapter,
|
adapter,
|
||||||
options: {
|
|
||||||
disableShardFailureWarning: DISABLE_SHARD_FAILURE_WARNING,
|
|
||||||
},
|
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,7 +26,7 @@ import {
|
||||||
import { filter } from 'rxjs/operators';
|
import { filter } from 'rxjs/operators';
|
||||||
import { dataViewMock, esHitsMockWithSort } from '@kbn/discover-utils/src/__mocks__';
|
import { dataViewMock, esHitsMockWithSort } from '@kbn/discover-utils/src/__mocks__';
|
||||||
import { buildDataTableRecord } from '@kbn/discover-utils';
|
import { buildDataTableRecord } from '@kbn/discover-utils';
|
||||||
import { searchResponseWarningsMock } from '@kbn/search-response-warnings/src/__mocks__/search_response_warnings';
|
import { searchResponseIncompleteWarningLocalCluster } from '@kbn/search-response-warnings/src/__mocks__/search_response_warnings';
|
||||||
|
|
||||||
describe('test useSavedSearch message generators', () => {
|
describe('test useSavedSearch message generators', () => {
|
||||||
test('sendCompleteMsg', (done) => {
|
test('sendCompleteMsg', (done) => {
|
||||||
|
@ -103,15 +103,13 @@ describe('test useSavedSearch message generators', () => {
|
||||||
if (value.fetchStatus !== FetchStatus.LOADING_MORE) {
|
if (value.fetchStatus !== FetchStatus.LOADING_MORE) {
|
||||||
expect(value.fetchStatus).toBe(FetchStatus.COMPLETE);
|
expect(value.fetchStatus).toBe(FetchStatus.COMPLETE);
|
||||||
expect(value.result).toStrictEqual([...initialRecords, ...moreRecords]);
|
expect(value.result).toStrictEqual([...initialRecords, ...moreRecords]);
|
||||||
expect(value.interceptedWarnings).toHaveLength(searchResponseWarningsMock.length);
|
expect(value.interceptedWarnings).toHaveLength(1);
|
||||||
done();
|
done();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
sendLoadingMoreFinishedMsg(documents$, {
|
sendLoadingMoreFinishedMsg(documents$, {
|
||||||
moreRecords,
|
moreRecords,
|
||||||
interceptedWarnings: searchResponseWarningsMock.map((warning) => ({
|
interceptedWarnings: [{ originalWarning: searchResponseIncompleteWarningLocalCluster }],
|
||||||
originalWarning: warning,
|
|
||||||
})),
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
test('sendLoadingMoreFinishedMsg after an exception', (done) => {
|
test('sendLoadingMoreFinishedMsg after an exception', (done) => {
|
||||||
|
@ -121,9 +119,7 @@ describe('test useSavedSearch message generators', () => {
|
||||||
const documents$ = new BehaviorSubject<DataDocumentsMsg>({
|
const documents$ = new BehaviorSubject<DataDocumentsMsg>({
|
||||||
fetchStatus: FetchStatus.LOADING_MORE,
|
fetchStatus: FetchStatus.LOADING_MORE,
|
||||||
result: initialRecords,
|
result: initialRecords,
|
||||||
interceptedWarnings: searchResponseWarningsMock.map((warning) => ({
|
interceptedWarnings: [{ originalWarning: searchResponseIncompleteWarningLocalCluster }],
|
||||||
originalWarning: warning,
|
|
||||||
})),
|
|
||||||
});
|
});
|
||||||
documents$.subscribe((value) => {
|
documents$.subscribe((value) => {
|
||||||
if (value.fetchStatus !== FetchStatus.LOADING_MORE) {
|
if (value.fetchStatus !== FetchStatus.LOADING_MORE) {
|
||||||
|
|
|
@ -25,7 +25,7 @@ import { fetchDocuments } from './fetch_documents';
|
||||||
import { fetchTextBased } from './fetch_text_based';
|
import { fetchTextBased } from './fetch_text_based';
|
||||||
import { buildDataTableRecord } from '@kbn/discover-utils';
|
import { buildDataTableRecord } from '@kbn/discover-utils';
|
||||||
import { dataViewMock, esHitsMockWithSort } from '@kbn/discover-utils/src/__mocks__';
|
import { dataViewMock, esHitsMockWithSort } from '@kbn/discover-utils/src/__mocks__';
|
||||||
import { searchResponseWarningsMock } from '@kbn/search-response-warnings/src/__mocks__/search_response_warnings';
|
import { searchResponseIncompleteWarningLocalCluster } from '@kbn/search-response-warnings/src/__mocks__/search_response_warnings';
|
||||||
|
|
||||||
jest.mock('./fetch_documents', () => ({
|
jest.mock('./fetch_documents', () => ({
|
||||||
fetchDocuments: jest.fn().mockResolvedValue([]),
|
fetchDocuments: jest.fn().mockResolvedValue([]),
|
||||||
|
@ -296,9 +296,11 @@ describe('test fetchAll', () => {
|
||||||
const initialRecords = [records[0], records[1]];
|
const initialRecords = [records[0], records[1]];
|
||||||
const moreRecords = [records[2], records[3]];
|
const moreRecords = [records[2], records[3]];
|
||||||
|
|
||||||
const interceptedWarnings = searchResponseWarningsMock.map((warning) => ({
|
const interceptedWarnings = [
|
||||||
originalWarning: warning,
|
{
|
||||||
}));
|
originalWarning: searchResponseIncompleteWarningLocalCluster,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
test('should add more records', async () => {
|
test('should add more records', async () => {
|
||||||
const collectDocuments = subjectCollector(subjects.documents$);
|
const collectDocuments = subjectCollector(subjects.documents$);
|
||||||
|
|
|
@ -37,17 +37,20 @@ describe('test fetchDocuments', () => {
|
||||||
const documents = hits.map((hit) => buildDataTableRecord(hit, dataViewMock));
|
const documents = hits.map((hit) => buildDataTableRecord(hit, dataViewMock));
|
||||||
savedSearchMock.searchSource.fetch$ = <T>() =>
|
savedSearchMock.searchSource.fetch$ = <T>() =>
|
||||||
of({ rawResponse: { hits: { hits } } } as IKibanaSearchResponse<SearchResponse<T>>);
|
of({ rawResponse: { hits: { hits } } } as IKibanaSearchResponse<SearchResponse<T>>);
|
||||||
expect(fetchDocuments(savedSearchMock.searchSource, getDeps())).resolves.toEqual({
|
expect(await fetchDocuments(savedSearchMock.searchSource, getDeps())).toEqual({
|
||||||
|
interceptedWarnings: [],
|
||||||
records: documents,
|
records: documents,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test('rejects on query failure', () => {
|
test('rejects on query failure', async () => {
|
||||||
savedSearchMock.searchSource.fetch$ = () => throwErrorRx(() => new Error('Oh noes!'));
|
savedSearchMock.searchSource.fetch$ = () => throwErrorRx(() => new Error('Oh noes!'));
|
||||||
|
|
||||||
expect(fetchDocuments(savedSearchMock.searchSource, getDeps())).rejects.toEqual(
|
try {
|
||||||
new Error('Oh noes!')
|
await fetchDocuments(savedSearchMock.searchSource, getDeps());
|
||||||
);
|
} catch (e) {
|
||||||
|
expect(e).toEqual(new Error('Oh noes!'));
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
test('passes a correct session id', async () => {
|
test('passes a correct session id', async () => {
|
||||||
|
@ -66,7 +69,8 @@ describe('test fetchDocuments', () => {
|
||||||
|
|
||||||
jest.spyOn(searchSourceRegular, 'fetch$');
|
jest.spyOn(searchSourceRegular, 'fetch$');
|
||||||
|
|
||||||
expect(fetchDocuments(searchSourceRegular, deps)).resolves.toEqual({
|
expect(await fetchDocuments(searchSourceRegular, deps)).toEqual({
|
||||||
|
interceptedWarnings: [],
|
||||||
records: documents,
|
records: documents,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -84,7 +88,8 @@ describe('test fetchDocuments', () => {
|
||||||
|
|
||||||
jest.spyOn(searchSourceForLoadMore, 'fetch$');
|
jest.spyOn(searchSourceForLoadMore, 'fetch$');
|
||||||
|
|
||||||
expect(fetchDocuments(searchSourceForLoadMore, deps)).resolves.toEqual({
|
expect(await fetchDocuments(searchSourceForLoadMore, deps)).toEqual({
|
||||||
|
interceptedWarnings: [],
|
||||||
records: documents,
|
records: documents,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -13,7 +13,6 @@ import { SAMPLE_SIZE_SETTING, buildDataTableRecordList } from '@kbn/discover-uti
|
||||||
import type { EsHitRecord } from '@kbn/discover-utils/types';
|
import type { EsHitRecord } from '@kbn/discover-utils/types';
|
||||||
import { getSearchResponseInterceptedWarnings } from '@kbn/search-response-warnings';
|
import { getSearchResponseInterceptedWarnings } from '@kbn/search-response-warnings';
|
||||||
import type { RecordsFetchResponse } from '../../types';
|
import type { RecordsFetchResponse } from '../../types';
|
||||||
import { DISABLE_SHARD_FAILURE_WARNING } from '../../../../common/constants';
|
|
||||||
import { FetchDeps } from './fetch_all';
|
import { FetchDeps } from './fetch_all';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -60,7 +59,7 @@ export const fetchDocuments = (
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
executionContext,
|
executionContext,
|
||||||
disableShardFailureWarning: DISABLE_SHARD_FAILURE_WARNING,
|
disableWarningToasts: true,
|
||||||
})
|
})
|
||||||
.pipe(
|
.pipe(
|
||||||
filter((res) => isCompleteResponse(res)),
|
filter((res) => isCompleteResponse(res)),
|
||||||
|
@ -75,9 +74,6 @@ export const fetchDocuments = (
|
||||||
? getSearchResponseInterceptedWarnings({
|
? getSearchResponseInterceptedWarnings({
|
||||||
services,
|
services,
|
||||||
adapter,
|
adapter,
|
||||||
options: {
|
|
||||||
disableShardFailureWarning: DISABLE_SHARD_FAILURE_WARNING,
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
|
|
|
@ -41,7 +41,7 @@ export const ErrorCallout = ({
|
||||||
const { euiTheme } = useEuiTheme();
|
const { euiTheme } = useEuiTheme();
|
||||||
|
|
||||||
const showErrorMessage = i18n.translate('discover.errorCalloutShowErrorMessage', {
|
const showErrorMessage = i18n.translate('discover.errorCalloutShowErrorMessage', {
|
||||||
defaultMessage: 'Show details',
|
defaultMessage: 'View details',
|
||||||
});
|
});
|
||||||
|
|
||||||
const overrideDisplay = getSearchErrorOverrideDisplay({
|
const overrideDisplay = getSearchErrorOverrideDisplay({
|
||||||
|
|
|
@ -62,11 +62,7 @@ import {
|
||||||
import type { UnifiedDataTableProps } from '@kbn/unified-data-table';
|
import type { UnifiedDataTableProps } from '@kbn/unified-data-table';
|
||||||
import type { UnifiedDataTableSettings } from '@kbn/unified-data-table';
|
import type { UnifiedDataTableSettings } from '@kbn/unified-data-table';
|
||||||
import { columnActions } from '@kbn/unified-data-table';
|
import { columnActions } from '@kbn/unified-data-table';
|
||||||
import {
|
import { VIEW_MODE, getDefaultRowsPerPage } from '../../common/constants';
|
||||||
VIEW_MODE,
|
|
||||||
DISABLE_SHARD_FAILURE_WARNING,
|
|
||||||
getDefaultRowsPerPage,
|
|
||||||
} from '../../common/constants';
|
|
||||||
import type { ISearchEmbeddable, SearchInput, SearchOutput } from './types';
|
import type { ISearchEmbeddable, SearchInput, SearchOutput } from './types';
|
||||||
import type { DiscoverServices } from '../build_services';
|
import type { DiscoverServices } from '../build_services';
|
||||||
import { getSortForEmbeddable, SortPair } from '../utils/sorting';
|
import { getSortForEmbeddable, SortPair } from '../utils/sorting';
|
||||||
|
@ -371,7 +367,7 @@ export class SavedSearchEmbeddable
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
executionContext,
|
executionContext,
|
||||||
disableShardFailureWarning: DISABLE_SHARD_FAILURE_WARNING,
|
disableWarningToasts: true,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -379,9 +375,6 @@ export class SavedSearchEmbeddable
|
||||||
searchProps.interceptedWarnings = getSearchResponseInterceptedWarnings({
|
searchProps.interceptedWarnings = getSearchResponseInterceptedWarnings({
|
||||||
services: this.services,
|
services: this.services,
|
||||||
adapter: this.inspectorAdapters.requests,
|
adapter: this.inspectorAdapters.requests,
|
||||||
options: {
|
|
||||||
disableShardFailureWarning: DISABLE_SHARD_FAILURE_WARNING,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -206,7 +206,7 @@ const fetchTotalHitsSearchSource = async ({
|
||||||
executionContext: {
|
executionContext: {
|
||||||
description: 'fetch total hits',
|
description: 'fetch total hits',
|
||||||
},
|
},
|
||||||
disableShardFailureWarning: true, // TODO: show warnings as a badge next to total hits number
|
disableWarningToasts: true, // TODO: show warnings as a badge next to total hits number
|
||||||
})
|
})
|
||||||
.pipe(
|
.pipe(
|
||||||
filter((res) => isCompleteResponse(res)),
|
filter((res) => isCompleteResponse(res)),
|
||||||
|
|
|
@ -19,6 +19,7 @@ import { KibanaThemeProvider } from '@kbn/kibana-react-plugin/public';
|
||||||
import { TimefilterContract } from '@kbn/data-plugin/public';
|
import { TimefilterContract } from '@kbn/data-plugin/public';
|
||||||
import type { DataView } from '@kbn/data-views-plugin/public';
|
import type { DataView } from '@kbn/data-views-plugin/public';
|
||||||
import { Warnings } from '@kbn/charts-plugin/public';
|
import { Warnings } from '@kbn/charts-plugin/public';
|
||||||
|
import { hasUnsupportedDownsampledAggregationFailure } from '@kbn/search-response-warnings';
|
||||||
import {
|
import {
|
||||||
Adapters,
|
Adapters,
|
||||||
AttributeService,
|
AttributeService,
|
||||||
|
@ -351,11 +352,13 @@ export class VisualizeEmbeddable
|
||||||
this.deps
|
this.deps
|
||||||
.start()
|
.start()
|
||||||
.plugins.data.search.showWarnings(this.getInspectorAdapters()!.requests!, (warning) => {
|
.plugins.data.search.showWarnings(this.getInspectorAdapters()!.requests!, (warning) => {
|
||||||
if (
|
if (hasUnsupportedDownsampledAggregationFailure(warning)) {
|
||||||
warning.type === 'shard_failure' &&
|
warnings.push(
|
||||||
warning.reason.type === 'unsupported_aggregation_on_downsampled_index'
|
i18n.translate('visualizations.embeddable.tsdbRollupWarning', {
|
||||||
) {
|
defaultMessage:
|
||||||
warnings.push(warning.reason.reason || warning.message);
|
'Visualization uses a function that is unsupported by rolled up data. Select a different function or change the time range.',
|
||||||
|
})
|
||||||
|
);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (this.vis.type.suppressWarnings?.()) {
|
if (this.vis.type.suppressWarnings?.()) {
|
||||||
|
@ -582,7 +585,7 @@ export class VisualizeEmbeddable
|
||||||
timeRange: this.timeRange,
|
timeRange: this.timeRange,
|
||||||
query: this.input.query,
|
query: this.input.query,
|
||||||
filters: this.input.filters,
|
filters: this.input.filters,
|
||||||
disableShardWarnings: true,
|
disableWarningToasts: true,
|
||||||
},
|
},
|
||||||
variables: {
|
variables: {
|
||||||
embeddableTitle: this.getTitle(),
|
embeddableTitle: this.getTitle(),
|
||||||
|
|
|
@ -63,7 +63,8 @@
|
||||||
"@kbn/content-management-table-list-view",
|
"@kbn/content-management-table-list-view",
|
||||||
"@kbn/content-management-utils",
|
"@kbn/content-management-utils",
|
||||||
"@kbn/serverless",
|
"@kbn/serverless",
|
||||||
"@kbn/no-data-page-plugin"
|
"@kbn/no-data-page-plugin",
|
||||||
|
"@kbn/search-response-warnings"
|
||||||
],
|
],
|
||||||
"exclude": [
|
"exclude": [
|
||||||
"target/**/*",
|
"target/**/*",
|
||||||
|
|
|
@ -100,7 +100,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
||||||
await PageObjects.common.clearAllToasts();
|
await PageObjects.common.clearAllToasts();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('shows shard failure warning notifications by default', async () => {
|
it('should show search warnings as toasts', async () => {
|
||||||
await testSubjects.click('searchSourceWithOther');
|
await testSubjects.click('searchSourceWithOther');
|
||||||
|
|
||||||
// wait for response - toasts appear before the response is rendered
|
// wait for response - toasts appear before the response is rendered
|
||||||
|
@ -113,30 +113,25 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
||||||
// toasts
|
// toasts
|
||||||
const toasts = await find.allByCssSelector(toastsSelector);
|
const toasts = await find.allByCssSelector(toastsSelector);
|
||||||
expect(toasts.length).to.be(2);
|
expect(toasts.length).to.be(2);
|
||||||
const expects = ['2 of 4 shards failed', 'Query result'];
|
const expects = ['The data might be incomplete or wrong.', 'Query result'];
|
||||||
await asyncForEach(toasts, async (t, index) => {
|
await asyncForEach(toasts, async (t, index) => {
|
||||||
expect(await t.getVisibleText()).to.eql(expects[index]);
|
expect(await t.getVisibleText()).to.eql(expects[index]);
|
||||||
});
|
});
|
||||||
|
|
||||||
// click "see full error" button in the toast
|
// click "see full error" button in the toast
|
||||||
const [openShardModalButton] = await testSubjects.findAll('openShardFailureModalBtn');
|
const [openShardModalButton] = await testSubjects.findAll('openIncompleteResultsModalBtn');
|
||||||
await openShardModalButton.click();
|
await openShardModalButton.click();
|
||||||
|
|
||||||
await retry.waitFor('modal title visible', async () => {
|
|
||||||
const modalHeader = await testSubjects.find('shardFailureModalTitle');
|
|
||||||
return (await modalHeader.getVisibleText()) === '2 of 4 shards failed';
|
|
||||||
});
|
|
||||||
|
|
||||||
// request
|
// request
|
||||||
await testSubjects.click('shardFailuresModalRequestButton');
|
await testSubjects.click('showRequestButton');
|
||||||
const requestBlock = await testSubjects.find('shardsFailedModalRequestBlock');
|
const requestBlock = await testSubjects.find('incompleteResultsModalRequestBlock');
|
||||||
expect(await requestBlock.getVisibleText()).to.contain(testRollupField);
|
expect(await requestBlock.getVisibleText()).to.contain(testRollupField);
|
||||||
// response
|
// response
|
||||||
await testSubjects.click('shardFailuresModalResponseButton');
|
await testSubjects.click('showResponseButton');
|
||||||
const responseBlock = await testSubjects.find('shardsFailedModalResponseBlock');
|
const responseBlock = await testSubjects.find('incompleteResultsModalResponseBlock');
|
||||||
expect(await responseBlock.getVisibleText()).to.contain(shardFailureReason);
|
expect(await responseBlock.getVisibleText()).to.contain(shardFailureReason);
|
||||||
|
|
||||||
await testSubjects.click('closeShardFailureModal');
|
await testSubjects.click('closeIncompleteResultsModal');
|
||||||
|
|
||||||
// response tab
|
// response tab
|
||||||
assert(response && response._shards.failures);
|
assert(response && response._shards.failures);
|
||||||
|
@ -154,7 +149,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
||||||
expect(warnings).to.eql([]);
|
expect(warnings).to.eql([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('able to handle shard failure warnings and prevent default notifications', async () => {
|
it('should show search warnings in results tab', async () => {
|
||||||
await testSubjects.click('searchSourceWithoutOther');
|
await testSubjects.click('searchSourceWithoutOther');
|
||||||
|
|
||||||
// wait for toasts - toasts appear after the response is rendered
|
// wait for toasts - toasts appear after the response is rendered
|
||||||
|
@ -163,53 +158,15 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
||||||
toasts = await find.allByCssSelector(toastsSelector);
|
toasts = await find.allByCssSelector(toastsSelector);
|
||||||
expect(toasts.length).to.be(2);
|
expect(toasts.length).to.be(2);
|
||||||
});
|
});
|
||||||
const expects = ['Query result', '2 of 4 shards failed'];
|
const expects = ['The data might be incomplete or wrong.', 'Query result'];
|
||||||
await asyncForEach(toasts, async (t, index) => {
|
await asyncForEach(toasts, async (t, index) => {
|
||||||
expect(await t.getVisibleText()).to.eql(expects[index]);
|
expect(await t.getVisibleText()).to.eql(expects[index]);
|
||||||
});
|
});
|
||||||
|
|
||||||
// click "see full error" button in the toast
|
|
||||||
const [openShardModalButton] = await testSubjects.findAll('openShardFailureModalBtn');
|
|
||||||
await openShardModalButton.click();
|
|
||||||
await testSubjects.exists('shardFailureModalTitle');
|
|
||||||
|
|
||||||
await retry.waitFor('modal title visible', async () => {
|
|
||||||
const modalHeader = await testSubjects.find('shardFailureModalTitle');
|
|
||||||
return (await modalHeader.getVisibleText()) === '2 of 4 shards failed';
|
|
||||||
});
|
|
||||||
|
|
||||||
// request
|
|
||||||
await testSubjects.click('shardFailuresModalRequestButton');
|
|
||||||
const requestBlock = await testSubjects.find('shardsFailedModalRequestBlock');
|
|
||||||
expect(await requestBlock.getVisibleText()).to.contain(testRollupField);
|
|
||||||
// response
|
|
||||||
await testSubjects.click('shardFailuresModalResponseButton');
|
|
||||||
const responseBlock = await testSubjects.find('shardsFailedModalResponseBlock');
|
|
||||||
expect(await responseBlock.getVisibleText()).to.contain(shardFailureReason);
|
|
||||||
|
|
||||||
await testSubjects.click('closeShardFailureModal');
|
|
||||||
|
|
||||||
// response tab
|
|
||||||
const response = await getTestJson('responseTab', 'responseCodeBlock');
|
|
||||||
expect(response._shards.total).to.be(4);
|
|
||||||
expect(response._shards.successful).to.be(2);
|
|
||||||
expect(response._shards.skipped).to.be(0);
|
|
||||||
expect(response._shards.failed).to.be(2);
|
|
||||||
expect(response._shards.failures.length).to.equal(1);
|
|
||||||
expect(response._shards.failures[0].index).to.equal(testRollupIndex);
|
|
||||||
expect(response._shards.failures[0].reason.type).to.equal(shardFailureType);
|
|
||||||
expect(response._shards.failures[0].reason.reason).to.equal(shardFailureReason);
|
|
||||||
|
|
||||||
// warnings tab
|
// warnings tab
|
||||||
const warnings = await getTestJson('warningsTab', 'warningsCodeBlock');
|
const warnings = await getTestJson('warningsTab', 'warningsCodeBlock');
|
||||||
expect(warnings).to.eql([
|
expect(warnings.length).to.be(1);
|
||||||
{
|
expect(warnings[0].type).to.be('incomplete');
|
||||||
type: 'shard_failure',
|
|
||||||
message: '2 of 4 shards failed',
|
|
||||||
reason: { reason: shardFailureReason, type: shardFailureType },
|
|
||||||
text: 'The data might be incomplete or wrong.',
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -64,7 +64,7 @@ import {
|
||||||
|
|
||||||
import {
|
import {
|
||||||
getFiltersInLayer,
|
getFiltersInLayer,
|
||||||
getShardFailuresWarningMessages,
|
getSearchWarningMessages,
|
||||||
getVisualDefaultsForLayer,
|
getVisualDefaultsForLayer,
|
||||||
isColumnInvalid,
|
isColumnInvalid,
|
||||||
cloneLayer,
|
cloneLayer,
|
||||||
|
@ -811,7 +811,7 @@ export function getFormBasedDatasource({
|
||||||
},
|
},
|
||||||
|
|
||||||
getSearchWarningMessages: (state, warning, request, response) => {
|
getSearchWarningMessages: (state, warning, request, response) => {
|
||||||
return [...getShardFailuresWarningMessages(state, warning, request, response, core.theme)];
|
return [...getSearchWarningMessages(state, warning, request, response, core.theme)];
|
||||||
},
|
},
|
||||||
|
|
||||||
checkIntegrity: (state, indexPatterns) => {
|
checkIntegrity: (state, indexPatterns) => {
|
||||||
|
|
|
@ -9,6 +9,7 @@ import React from 'react';
|
||||||
import { i18n } from '@kbn/i18n';
|
import { i18n } from '@kbn/i18n';
|
||||||
import { FormattedMessage } from '@kbn/i18n-react';
|
import { FormattedMessage } from '@kbn/i18n-react';
|
||||||
import type { DocLinksStart, ThemeServiceStart } from '@kbn/core/public';
|
import type { DocLinksStart, ThemeServiceStart } from '@kbn/core/public';
|
||||||
|
import { hasUnsupportedDownsampledAggregationFailure } from '@kbn/search-response-warnings';
|
||||||
import type { DatatableUtilitiesService } from '@kbn/data-plugin/common';
|
import type { DatatableUtilitiesService } from '@kbn/data-plugin/common';
|
||||||
import { TimeRange } from '@kbn/es-query';
|
import { TimeRange } from '@kbn/es-query';
|
||||||
import { EuiLink, EuiSpacer, EuiText } from '@elastic/eui';
|
import { EuiLink, EuiSpacer, EuiText } from '@elastic/eui';
|
||||||
|
@ -18,11 +19,7 @@ import { groupBy, escape, uniq, uniqBy } from 'lodash';
|
||||||
import type { Query } from '@kbn/data-plugin/common';
|
import type { Query } from '@kbn/data-plugin/common';
|
||||||
import { SearchRequest } from '@kbn/data-plugin/common';
|
import { SearchRequest } from '@kbn/data-plugin/common';
|
||||||
|
|
||||||
import {
|
import { SearchResponseWarning, OpenIncompleteResultsModalButton } from '@kbn/data-plugin/public';
|
||||||
SearchResponseWarning,
|
|
||||||
ShardFailureOpenModalButton,
|
|
||||||
ShardFailureRequest,
|
|
||||||
} from '@kbn/data-plugin/public';
|
|
||||||
|
|
||||||
import { estypes } from '@elastic/elasticsearch';
|
import { estypes } from '@elastic/elasticsearch';
|
||||||
import { isQueryValid } from '@kbn/visualization-ui-components';
|
import { isQueryValid } from '@kbn/visualization-ui-components';
|
||||||
|
@ -260,7 +257,7 @@ const accuracyModeEnabledWarning = (
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
export function getShardFailuresWarningMessages(
|
export function getSearchWarningMessages(
|
||||||
state: FormBasedPersistedState,
|
state: FormBasedPersistedState,
|
||||||
warning: SearchResponseWarning,
|
warning: SearchResponseWarning,
|
||||||
request: SearchRequest,
|
request: SearchRequest,
|
||||||
|
@ -268,10 +265,9 @@ export function getShardFailuresWarningMessages(
|
||||||
theme: ThemeServiceStart
|
theme: ThemeServiceStart
|
||||||
): UserMessage[] {
|
): UserMessage[] {
|
||||||
if (state) {
|
if (state) {
|
||||||
if (warning.type === 'shard_failure') {
|
if (warning.type === 'incomplete') {
|
||||||
switch (warning.reason.type) {
|
return hasUnsupportedDownsampledAggregationFailure(warning)
|
||||||
case 'unsupported_aggregation_on_downsampled_index':
|
? Object.values(state.layers).flatMap((layer) =>
|
||||||
return Object.values(state.layers).flatMap((layer) =>
|
|
||||||
uniq(
|
uniq(
|
||||||
Object.values(layer.columns)
|
Object.values(layer.columns)
|
||||||
.filter((col) =>
|
.filter((col) =>
|
||||||
|
@ -302,40 +298,33 @@ export function getShardFailuresWarningMessages(
|
||||||
}),
|
}),
|
||||||
} as UserMessage)
|
} as UserMessage)
|
||||||
)
|
)
|
||||||
);
|
)
|
||||||
default:
|
: [
|
||||||
return [
|
|
||||||
{
|
{
|
||||||
uniqueId: `shard_failure`,
|
uniqueId: `incomplete`,
|
||||||
severity: 'warning',
|
severity: 'warning',
|
||||||
fixableInEditor: true,
|
fixableInEditor: true,
|
||||||
displayLocations: [{ id: 'toolbar' }, { id: 'embeddableBadge' }],
|
displayLocations: [{ id: 'toolbar' }, { id: 'embeddableBadge' }],
|
||||||
shortMessage: '',
|
shortMessage: '',
|
||||||
longMessage: (
|
longMessage: (
|
||||||
<>
|
<>
|
||||||
<EuiText size="s">
|
<EuiText size="s">{warning.message}</EuiText>
|
||||||
<strong>{warning.message}</strong>
|
|
||||||
<p>{warning.text}</p>
|
|
||||||
</EuiText>
|
|
||||||
<EuiSpacer size="s" />
|
<EuiSpacer size="s" />
|
||||||
{warning.text ? (
|
<OpenIncompleteResultsModalButton
|
||||||
<ShardFailureOpenModalButton
|
theme={theme}
|
||||||
theme={theme}
|
warning={warning}
|
||||||
title={warning.message}
|
size="m"
|
||||||
size="m"
|
getRequestMeta={() => ({
|
||||||
getRequestMeta={() => ({
|
request,
|
||||||
request: request as ShardFailureRequest,
|
response,
|
||||||
response,
|
})}
|
||||||
})}
|
color="primary"
|
||||||
color="primary"
|
isButtonEmpty={true}
|
||||||
isButtonEmpty={true}
|
/>
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
} as UserMessage,
|
} as UserMessage,
|
||||||
];
|
];
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return [];
|
return [];
|
||||||
|
|
|
@ -729,7 +729,7 @@ export const VisualizationWrapper = ({
|
||||||
to: context.dateRange.toDate,
|
to: context.dateRange.toDate,
|
||||||
},
|
},
|
||||||
filters: context.filters,
|
filters: context.filters,
|
||||||
disableShardWarnings: true,
|
disableWarningToasts: true,
|
||||||
}),
|
}),
|
||||||
[context]
|
[context]
|
||||||
);
|
);
|
||||||
|
|
|
@ -1196,7 +1196,7 @@ export class Embeddable
|
||||||
this.savedVis.state.filters,
|
this.savedVis.state.filters,
|
||||||
this.savedVis.references
|
this.savedVis.references
|
||||||
),
|
),
|
||||||
disableShardWarnings: true,
|
disableWarningToasts: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (input.query) {
|
if (input.query) {
|
||||||
|
|
|
@ -62,7 +62,7 @@ export const selectExecutionContextSearch = createSelector(selectExecutionContex
|
||||||
to: res.dateRange.toDate,
|
to: res.dateRange.toDate,
|
||||||
},
|
},
|
||||||
filters: res.filters,
|
filters: res.filters,
|
||||||
disableShardWarnings: true,
|
disableWarningToasts: true,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const selectInjectedDependencies = (_state: LensState, dependencies: unknown) => dependencies;
|
const selectInjectedDependencies = (_state: LensState, dependencies: unknown) => dependencies;
|
||||||
|
|
|
@ -349,29 +349,26 @@ export const getSearchWarningMessages = (
|
||||||
searchService: ISearchStart;
|
searchService: ISearchStart;
|
||||||
}
|
}
|
||||||
): UserMessage[] => {
|
): UserMessage[] => {
|
||||||
const warningsMap: Map<string, UserMessage[]> = new Map();
|
const userMessages: UserMessage[] = [];
|
||||||
|
|
||||||
deps.searchService.showWarnings(adapter, (warning, meta) => {
|
deps.searchService.showWarnings(adapter, (warning, meta) => {
|
||||||
const { request, response, requestId } = meta;
|
const { request, response } = meta;
|
||||||
|
|
||||||
const warningMessages = datasource.getSearchWarningMessages?.(
|
const userMessagesFromWarning = datasource.getSearchWarningMessages?.(
|
||||||
state,
|
state,
|
||||||
warning,
|
warning,
|
||||||
request,
|
request,
|
||||||
response
|
response
|
||||||
);
|
);
|
||||||
|
|
||||||
if (warningMessages?.length) {
|
if (userMessagesFromWarning?.length) {
|
||||||
const key = (requestId ?? '') + warning.type + warning.reason?.type ?? '';
|
userMessages.push(...userMessagesFromWarning);
|
||||||
if (!warningsMap.has(key)) {
|
|
||||||
warningsMap.set(key, warningMessages);
|
|
||||||
}
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
|
|
||||||
return [...warningsMap.values()].flat();
|
return userMessages;
|
||||||
};
|
};
|
||||||
|
|
||||||
function getSafeLabel(label: string) {
|
function getSafeLabel(label: string) {
|
||||||
|
|
|
@ -86,6 +86,7 @@
|
||||||
"@kbn/content-management-utils",
|
"@kbn/content-management-utils",
|
||||||
"@kbn/serverless",
|
"@kbn/serverless",
|
||||||
"@kbn/ebt-tools",
|
"@kbn/ebt-tools",
|
||||||
|
"@kbn/search-response-warnings",
|
||||||
],
|
],
|
||||||
"exclude": [
|
"exclude": [
|
||||||
"target/**/*",
|
"target/**/*",
|
||||||
|
|
|
@ -1317,7 +1317,6 @@
|
||||||
"data.search.aggs.rareTerms.aggTypesLabel": "Termes rares de {fieldName}",
|
"data.search.aggs.rareTerms.aggTypesLabel": "Termes rares de {fieldName}",
|
||||||
"data.search.es_search.queryTimeValue": "{queryTime} ms",
|
"data.search.es_search.queryTimeValue": "{queryTime} ms",
|
||||||
"data.search.functions.geoBoundingBox.arguments.error": "Au moins un des groupes de paramètres suivants doit être fourni : {parameters}.",
|
"data.search.functions.geoBoundingBox.arguments.error": "Au moins un des groupes de paramètres suivants doit être fourni : {parameters}.",
|
||||||
"data.search.searchSource.fetch.shardsFailedNotificationMessage": "Échec de {shardsFailed} des {shardsTotal} partitions",
|
|
||||||
"data.search.searchSource.indexPatternIdDescription": "ID dans l'index {kibanaIndexPattern}.",
|
"data.search.searchSource.indexPatternIdDescription": "ID dans l'index {kibanaIndexPattern}.",
|
||||||
"data.search.searchSource.queryTimeValue": "{queryTime} ms",
|
"data.search.searchSource.queryTimeValue": "{queryTime} ms",
|
||||||
"data.search.searchSource.requestTimeValue": "{requestTime} ms",
|
"data.search.searchSource.requestTimeValue": "{requestTime} ms",
|
||||||
|
@ -2064,15 +2063,7 @@
|
||||||
"data.search.searchSource.dataViewDescription": "La vue de données qui a été interrogée.",
|
"data.search.searchSource.dataViewDescription": "La vue de données qui a été interrogée.",
|
||||||
"data.search.searchSource.dataViewIdLabel": "ID de vue de données",
|
"data.search.searchSource.dataViewIdLabel": "ID de vue de données",
|
||||||
"data.search.searchSource.dataViewLabel": "Vue de données",
|
"data.search.searchSource.dataViewLabel": "Vue de données",
|
||||||
"data.search.searchSource.fetch.requestTimedOutNotificationMessage": "Les données peuvent être incomplètes parce que votre requête est arrivée à échéance.",
|
|
||||||
"data.search.searchSource.fetch.shardsFailedModal.close": "Fermer",
|
|
||||||
"data.search.searchSource.fetch.shardsFailedModal.copyToClipboard": "Copier la réponse dans le presse-papiers",
|
|
||||||
"data.search.searchSource.fetch.shardsFailedModal.showDetails": "Afficher les détails",
|
|
||||||
"data.search.searchSource.fetch.shardsFailedModal.tabHeaderRequest": "Requête",
|
|
||||||
"data.search.searchSource.fetch.shardsFailedModal.tabHeaderResponse": "Réponse",
|
|
||||||
"data.search.searchSource.fetch.shardsFailedModal.tabHeaderShardFailures": "Échecs de partition",
|
|
||||||
"data.search.searchSource.fetch.shardsFailedModal.tableColReason": "Raison",
|
"data.search.searchSource.fetch.shardsFailedModal.tableColReason": "Raison",
|
||||||
"data.search.searchSource.fetch.shardsFailedNotificationDescription": "Les données peuvent être incomplètes ou erronées.",
|
|
||||||
"data.search.searchSource.hitsDescription": "Le nombre de documents renvoyés par la requête.",
|
"data.search.searchSource.hitsDescription": "Le nombre de documents renvoyés par la requête.",
|
||||||
"data.search.searchSource.hitsLabel": "Résultats",
|
"data.search.searchSource.hitsLabel": "Résultats",
|
||||||
"data.search.searchSource.hitsTotalDescription": "Le nombre de documents correspondant à la requête.",
|
"data.search.searchSource.hitsTotalDescription": "Le nombre de documents correspondant à la requête.",
|
||||||
|
|
|
@ -1331,7 +1331,6 @@
|
||||||
"data.search.aggs.rareTerms.aggTypesLabel": "{fieldName}の希少な用語",
|
"data.search.aggs.rareTerms.aggTypesLabel": "{fieldName}の希少な用語",
|
||||||
"data.search.es_search.queryTimeValue": "{queryTime}ms",
|
"data.search.es_search.queryTimeValue": "{queryTime}ms",
|
||||||
"data.search.functions.geoBoundingBox.arguments.error": "次のパラメーターのグループの1つ以上を指定する必要があります:{parameters}。",
|
"data.search.functions.geoBoundingBox.arguments.error": "次のパラメーターのグループの1つ以上を指定する必要があります:{parameters}。",
|
||||||
"data.search.searchSource.fetch.shardsFailedNotificationMessage": "{shardsTotal}件中{shardsFailed}件のシャードでエラーが発生しました",
|
|
||||||
"data.search.searchSource.indexPatternIdDescription": "{kibanaIndexPattern}インデックス内のIDです。",
|
"data.search.searchSource.indexPatternIdDescription": "{kibanaIndexPattern}インデックス内のIDです。",
|
||||||
"data.search.searchSource.queryTimeValue": "{queryTime}ms",
|
"data.search.searchSource.queryTimeValue": "{queryTime}ms",
|
||||||
"data.search.searchSource.requestTimeValue": "{requestTime}ms",
|
"data.search.searchSource.requestTimeValue": "{requestTime}ms",
|
||||||
|
@ -2078,15 +2077,7 @@
|
||||||
"data.search.searchSource.dataViewDescription": "照会されたデータビュー。",
|
"data.search.searchSource.dataViewDescription": "照会されたデータビュー。",
|
||||||
"data.search.searchSource.dataViewIdLabel": "データビューID",
|
"data.search.searchSource.dataViewIdLabel": "データビューID",
|
||||||
"data.search.searchSource.dataViewLabel": "データビュー",
|
"data.search.searchSource.dataViewLabel": "データビュー",
|
||||||
"data.search.searchSource.fetch.requestTimedOutNotificationMessage": "リクエストがタイムアウトしたため、データが不完全な可能性があります",
|
|
||||||
"data.search.searchSource.fetch.shardsFailedModal.close": "閉じる",
|
|
||||||
"data.search.searchSource.fetch.shardsFailedModal.copyToClipboard": "応答をクリップボードにコピー",
|
|
||||||
"data.search.searchSource.fetch.shardsFailedModal.showDetails": "詳細を表示",
|
|
||||||
"data.search.searchSource.fetch.shardsFailedModal.tabHeaderRequest": "リクエスト",
|
|
||||||
"data.search.searchSource.fetch.shardsFailedModal.tabHeaderResponse": "応答",
|
|
||||||
"data.search.searchSource.fetch.shardsFailedModal.tabHeaderShardFailures": "シャードエラー",
|
|
||||||
"data.search.searchSource.fetch.shardsFailedModal.tableColReason": "理由",
|
"data.search.searchSource.fetch.shardsFailedModal.tableColReason": "理由",
|
||||||
"data.search.searchSource.fetch.shardsFailedNotificationDescription": "データが不完全か誤りの可能性があります。",
|
|
||||||
"data.search.searchSource.hitsDescription": "クエリにより返されたドキュメントの数です。",
|
"data.search.searchSource.hitsDescription": "クエリにより返されたドキュメントの数です。",
|
||||||
"data.search.searchSource.hitsLabel": "ヒット数",
|
"data.search.searchSource.hitsLabel": "ヒット数",
|
||||||
"data.search.searchSource.hitsTotalDescription": "クエリに一致するドキュメントの数です。",
|
"data.search.searchSource.hitsTotalDescription": "クエリに一致するドキュメントの数です。",
|
||||||
|
|
|
@ -1331,7 +1331,6 @@
|
||||||
"data.search.aggs.rareTerms.aggTypesLabel": "{fieldName} 的稀有词",
|
"data.search.aggs.rareTerms.aggTypesLabel": "{fieldName} 的稀有词",
|
||||||
"data.search.es_search.queryTimeValue": "{queryTime}ms",
|
"data.search.es_search.queryTimeValue": "{queryTime}ms",
|
||||||
"data.search.functions.geoBoundingBox.arguments.error": "必须至少提供一个以下参数组:{parameters}。",
|
"data.search.functions.geoBoundingBox.arguments.error": "必须至少提供一个以下参数组:{parameters}。",
|
||||||
"data.search.searchSource.fetch.shardsFailedNotificationMessage": "{shardsTotal} 个分片有 {shardsFailed} 个失败",
|
|
||||||
"data.search.searchSource.indexPatternIdDescription": "{kibanaIndexPattern} 索引中的 ID。",
|
"data.search.searchSource.indexPatternIdDescription": "{kibanaIndexPattern} 索引中的 ID。",
|
||||||
"data.search.searchSource.queryTimeValue": "{queryTime}ms",
|
"data.search.searchSource.queryTimeValue": "{queryTime}ms",
|
||||||
"data.search.searchSource.requestTimeValue": "{requestTime}ms",
|
"data.search.searchSource.requestTimeValue": "{requestTime}ms",
|
||||||
|
@ -2078,15 +2077,7 @@
|
||||||
"data.search.searchSource.dataViewDescription": "被查询的数据视图。",
|
"data.search.searchSource.dataViewDescription": "被查询的数据视图。",
|
||||||
"data.search.searchSource.dataViewIdLabel": "数据视图 ID",
|
"data.search.searchSource.dataViewIdLabel": "数据视图 ID",
|
||||||
"data.search.searchSource.dataViewLabel": "数据视图",
|
"data.search.searchSource.dataViewLabel": "数据视图",
|
||||||
"data.search.searchSource.fetch.requestTimedOutNotificationMessage": "由于您的请求超时,数据可能不完整",
|
|
||||||
"data.search.searchSource.fetch.shardsFailedModal.close": "关闭",
|
|
||||||
"data.search.searchSource.fetch.shardsFailedModal.copyToClipboard": "将响应复制到剪贴板",
|
|
||||||
"data.search.searchSource.fetch.shardsFailedModal.showDetails": "显示详情",
|
|
||||||
"data.search.searchSource.fetch.shardsFailedModal.tabHeaderRequest": "请求",
|
|
||||||
"data.search.searchSource.fetch.shardsFailedModal.tabHeaderResponse": "响应",
|
|
||||||
"data.search.searchSource.fetch.shardsFailedModal.tabHeaderShardFailures": "分片错误",
|
|
||||||
"data.search.searchSource.fetch.shardsFailedModal.tableColReason": "原因",
|
"data.search.searchSource.fetch.shardsFailedModal.tableColReason": "原因",
|
||||||
"data.search.searchSource.fetch.shardsFailedNotificationDescription": "数据可能不完整或有错误。",
|
|
||||||
"data.search.searchSource.hitsDescription": "查询返回的文档数目。",
|
"data.search.searchSource.hitsDescription": "查询返回的文档数目。",
|
||||||
"data.search.searchSource.hitsLabel": "命中数",
|
"data.search.searchSource.hitsLabel": "命中数",
|
||||||
"data.search.searchSource.hitsTotalDescription": "与查询匹配的文档数目。",
|
"data.search.searchSource.hitsTotalDescription": "与查询匹配的文档数目。",
|
||||||
|
|
|
@ -27,7 +27,7 @@ export default function ({ getService, getPageObjects }) {
|
||||||
const security = getService('security');
|
const security = getService('security');
|
||||||
const dashboardAddPanel = getService('dashboardAddPanel');
|
const dashboardAddPanel = getService('dashboardAddPanel');
|
||||||
|
|
||||||
describe('async search with scripted fields', function () {
|
describe('search with scripted fields', function () {
|
||||||
this.tags(['skipFirefox']);
|
this.tags(['skipFirefox']);
|
||||||
|
|
||||||
before(async function () {
|
before(async function () {
|
||||||
|
@ -51,7 +51,7 @@ export default function ({ getService, getPageObjects }) {
|
||||||
await security.testUser.restoreDefaults();
|
await security.testUser.restoreDefaults();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('query should show failed shards callout', async function () {
|
it('query should show incomplete results callout', async function () {
|
||||||
if (false) {
|
if (false) {
|
||||||
/* If you had to modify the scripted fields, you could un-comment all this, run it, use es_archiver to update 'kibana_scripted_fields_on_logstash'
|
/* If you had to modify the scripted fields, you could un-comment all this, run it, use es_archiver to update 'kibana_scripted_fields_on_logstash'
|
||||||
*/
|
*/
|
||||||
|
@ -81,11 +81,11 @@ export default function ({ getService, getPageObjects }) {
|
||||||
'dscNoResultsInterceptedWarningsCallout_warningTitle'
|
'dscNoResultsInterceptedWarningsCallout_warningTitle'
|
||||||
);
|
);
|
||||||
log.debug(shardMessage);
|
log.debug(shardMessage);
|
||||||
expect(shardMessage).to.be('1 of 3 shards failed');
|
expect(shardMessage).to.be('The data might be incomplete or wrong.');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('query should show failed shards badge on dashboard', async function () {
|
it('query should show incomplete results badge on dashboard', async function () {
|
||||||
await security.testUser.setRoles([
|
await security.testUser.setRoles([
|
||||||
'test_logstash_reader',
|
'test_logstash_reader',
|
||||||
'global_discover_all',
|
'global_discover_all',
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue