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

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

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

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

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

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

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

---------

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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