mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
[search/public] expose showWarnings(inspector) method on search service (#138342)
* add showWarning to search service * add comments * add unit tests * test foo * cleanup * add s to property name in test * comments for api items * use the warnings when calling showWarnings * change showWarning to just show a single warning * put handleWarnings on the request adapter * comment * simplify 1 * fix lens unit test * remove underscoring for unused variables * revert inspector changes, extract the response warnings in the search service * fix bug * remove console.log * re-apply typescript fixes to app test code * declutter * add test, improve comments * fix some unexported public api items * include rawResponse in the warning structure * fix lint * tweak clean up example app * SearchResponseWarnings and SearchResponseWarningNotification * fix export bug * not include shardStats if there are no warnings * Update src/plugins/data/common/search/types.ts * simplify SearchResponseWarnings interface * remove array copying * [CI] Auto-commit changed files from 'node scripts/eslint --no-cache --fix' * comments for api_docs * simplify per feedback * Pass callback to handleResponse in showWarnings * export more public types * update example to make possible to show shard failure * pr cleanup * eslint fix * allow example app to not show default warnings * move extractWarning and related types to inspector plugin * wip functional test of example app * fix test references * finish functional test * relocate extractWarnings back to search/fetch * fix test * remove need for isTimeout, isShardFailure * ts fix * improve test * Change showWarnings to accept the RequestAdapter * use showWarnings in vis_types/timeseries * more tests * use handle_warning name * fix ts * add reason field to SearchResponseWarning * fix component snapshot * update comments * test cleanup * fix test * ensure notification appears only once * fix and cleanup * fix ts * fix response.json bug * use top-level type, and lower-level reason.type * cleanup * fix shard failure warning in tsvb per feedback cc @flash1293 Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Joe Reuter <johannes.reuter@elastic.co>
This commit is contained in:
parent
28ab14cbe1
commit
160058a8c1
29 changed files with 1291 additions and 367 deletions
|
@ -9,7 +9,7 @@
|
|||
"description": "Example plugin of how to use data plugin search services",
|
||||
"server": true,
|
||||
"ui": true,
|
||||
"requiredPlugins": ["navigation", "data", "developerExamples", "kibanaUtils", "share", "unifiedSearch"],
|
||||
"requiredPlugins": ["navigation", "data", "developerExamples", "inspector", "kibanaUtils", "share", "unifiedSearch"],
|
||||
"optionalPlugins": [],
|
||||
"requiredBundles": ["kibanaReact"],
|
||||
"owner": {
|
||||
|
|
|
@ -6,48 +6,48 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
|
||||
import {
|
||||
EuiButtonEmpty,
|
||||
EuiCheckbox,
|
||||
EuiCode,
|
||||
EuiCodeBlock,
|
||||
EuiComboBox,
|
||||
EuiFieldNumber,
|
||||
EuiFlexGrid,
|
||||
EuiFlexItem,
|
||||
EuiFormLabel,
|
||||
EuiHorizontalRule,
|
||||
EuiPageBody,
|
||||
EuiPageContent,
|
||||
EuiPageContentBody,
|
||||
EuiPageHeader,
|
||||
EuiTitle,
|
||||
EuiText,
|
||||
EuiFlexGrid,
|
||||
EuiFlexItem,
|
||||
EuiCheckbox,
|
||||
EuiSpacer,
|
||||
EuiCode,
|
||||
EuiComboBox,
|
||||
EuiFormLabel,
|
||||
EuiFieldNumber,
|
||||
EuiProgress,
|
||||
EuiSpacer,
|
||||
EuiTabbedContent,
|
||||
EuiTabbedContentTab,
|
||||
EuiText,
|
||||
EuiTitle,
|
||||
} from '@elastic/eui';
|
||||
|
||||
import { lastValueFrom } from 'rxjs';
|
||||
import { CoreStart } from '@kbn/core/public';
|
||||
import { toMountPoint } from '@kbn/kibana-react-plugin/public';
|
||||
import { NavigationPublicPluginStart } from '@kbn/navigation-plugin/public';
|
||||
|
||||
import { IInspectorInfo } from '@kbn/data-plugin/common';
|
||||
import {
|
||||
DataPublicPluginStart,
|
||||
IKibanaSearchResponse,
|
||||
isCompleteResponse,
|
||||
isErrorResponse,
|
||||
} from '@kbn/data-plugin/public';
|
||||
import { SearchResponseWarning } from '@kbn/data-plugin/public/search/types';
|
||||
import type { DataView, DataViewField } from '@kbn/data-views-plugin/public';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { RequestAdapter } from '@kbn/inspector-plugin/common';
|
||||
import { toMountPoint } from '@kbn/kibana-react-plugin/public';
|
||||
import { NavigationPublicPluginStart } from '@kbn/navigation-plugin/public';
|
||||
import { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/public';
|
||||
import type { DataViewField, DataView } from '@kbn/data-views-plugin/public';
|
||||
import { AbortError } from '@kbn/kibana-utils-plugin/common';
|
||||
import { IMyStrategyResponse } from '../../common/types';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { lastValueFrom } from 'rxjs';
|
||||
import { PLUGIN_ID, PLUGIN_NAME, SERVER_SEARCH_ROUTE_PATH } from '../../common';
|
||||
import { IMyStrategyResponse } from '../../common/types';
|
||||
|
||||
interface SearchExamplesAppDeps {
|
||||
notifications: CoreStart['notifications'];
|
||||
|
@ -82,6 +82,9 @@ function formatFieldsToComboBox(fields?: DataViewField[]) {
|
|||
});
|
||||
}
|
||||
|
||||
const bucketAggType = 'terms';
|
||||
const metricAggType = 'median';
|
||||
|
||||
export const SearchExamplesApp = ({
|
||||
http,
|
||||
notifications,
|
||||
|
@ -108,9 +111,11 @@ export const SearchExamplesApp = ({
|
|||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||
const [currentAbortController, setAbortController] = useState<AbortController>();
|
||||
const [rawResponse, setRawResponse] = useState<Record<string, any>>({});
|
||||
const [warningContents, setWarningContents] = useState<SearchResponseWarning[]>([]);
|
||||
const [selectedTab, setSelectedTab] = useState(0);
|
||||
|
||||
function setResponse(response: IKibanaSearchResponse) {
|
||||
setWarningContents([]);
|
||||
setRawResponse(response.rawResponse);
|
||||
setLoaded(response.loaded!);
|
||||
setTotal(response.total!);
|
||||
|
@ -177,7 +182,7 @@ export const SearchExamplesApp = ({
|
|||
}
|
||||
|
||||
// Construct the aggregations portion of the search request by using the `data.search.aggs` service.
|
||||
const aggs = [{ type: 'avg', params: { field: selectedNumericField!.name } }];
|
||||
const aggs = [{ type: metricAggType, params: { field: selectedNumericField!.name } }];
|
||||
const aggsDsl = data.search.aggs.createAggConfigs(dataView, aggs).toDsl();
|
||||
|
||||
const req = {
|
||||
|
@ -210,7 +215,7 @@ export const SearchExamplesApp = ({
|
|||
if (isCompleteResponse(res)) {
|
||||
setIsLoading(false);
|
||||
setResponse(res);
|
||||
const avgResult: number | undefined = res.rawResponse.aggregations
|
||||
const aggResult: number | undefined = res.rawResponse.aggregations
|
||||
? // @ts-expect-error @elastic/elasticsearch no way to declare a type for aggregation in the search response
|
||||
res.rawResponse.aggregations[1].value
|
||||
: undefined;
|
||||
|
@ -219,8 +224,8 @@ export const SearchExamplesApp = ({
|
|||
const message = (
|
||||
<EuiText>
|
||||
Searched {res.rawResponse.hits.total} documents. <br />
|
||||
The average of {selectedNumericField!.name} is{' '}
|
||||
{avgResult ? Math.floor(avgResult) : 0}.
|
||||
The ${metricAggType} of {selectedNumericField!.name} is{' '}
|
||||
{aggResult ? Math.floor(aggResult) : 0}.
|
||||
<br />
|
||||
{isCool ? `Is it Cool? ${isCool}` : undefined}
|
||||
<br />
|
||||
|
@ -251,21 +256,15 @@ export const SearchExamplesApp = ({
|
|||
},
|
||||
error: (e) => {
|
||||
setIsLoading(false);
|
||||
if (e instanceof AbortError) {
|
||||
notifications.toasts.addWarning({
|
||||
title: e.message,
|
||||
});
|
||||
} else {
|
||||
notifications.toasts.addDanger({
|
||||
title: 'Failed to run search',
|
||||
text: e.message,
|
||||
});
|
||||
}
|
||||
data.search.showError(e);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const doSearchSourceSearch = async (otherBucket: boolean) => {
|
||||
const doSearchSourceSearch = async (
|
||||
otherBucket: boolean,
|
||||
showWarningToastNotifications = true
|
||||
) => {
|
||||
if (!dataView) return;
|
||||
|
||||
const query = data.query.queryString.getQuery();
|
||||
|
@ -289,29 +288,58 @@ export const SearchExamplesApp = ({
|
|||
const aggDef = [];
|
||||
if (selectedBucketField) {
|
||||
aggDef.push({
|
||||
type: 'terms',
|
||||
type: bucketAggType,
|
||||
schema: 'split',
|
||||
params: { field: selectedBucketField.name, size: 2, otherBucket },
|
||||
});
|
||||
}
|
||||
if (selectedNumericField) {
|
||||
aggDef.push({ type: 'avg', params: { field: selectedNumericField.name } });
|
||||
aggDef.push({ type: metricAggType, params: { field: selectedNumericField.name } });
|
||||
}
|
||||
if (aggDef.length > 0) {
|
||||
const ac = data.search.aggs.createAggConfigs(dataView, aggDef);
|
||||
searchSource.setField('aggs', ac);
|
||||
}
|
||||
|
||||
setRequest(searchSource.getSearchRequestBody());
|
||||
const abortController = new AbortController();
|
||||
|
||||
const inspector: Required<IInspectorInfo> = {
|
||||
adapter: new RequestAdapter(),
|
||||
title: 'Example App Inspector!',
|
||||
id: 'greatest-example-app-inspector',
|
||||
description: 'Use the `description` field for more info about the inspector.',
|
||||
};
|
||||
|
||||
setAbortController(abortController);
|
||||
setIsLoading(true);
|
||||
const { rawResponse: res } = await lastValueFrom(
|
||||
searchSource.fetch$({ abortSignal: abortController.signal })
|
||||
const result = await lastValueFrom(
|
||||
searchSource.fetch$({
|
||||
abortSignal: abortController.signal,
|
||||
disableShardFailureWarning: !showWarningToastNotifications,
|
||||
inspector,
|
||||
})
|
||||
);
|
||||
setRawResponse(res);
|
||||
setRawResponse(result.rawResponse);
|
||||
|
||||
const message = <EuiText>Searched {res.hits.total} documents.</EuiText>;
|
||||
/* 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
|
||||
*/
|
||||
if (showWarningToastNotifications) {
|
||||
setWarningContents([]);
|
||||
} else {
|
||||
const warnings: SearchResponseWarning[] = [];
|
||||
data.search.showWarnings(inspector.adapter, (warning) => {
|
||||
warnings.push(warning);
|
||||
return false; // allow search service from showing this warning on its own
|
||||
});
|
||||
// click the warnings tab to see the warnings
|
||||
setWarningContents(warnings);
|
||||
}
|
||||
|
||||
const message = <EuiText>Searched {result.rawResponse.hits.total} documents.</EuiText>;
|
||||
notifications.toasts.addSuccess(
|
||||
{
|
||||
title: 'Query result',
|
||||
|
@ -323,16 +351,7 @@ export const SearchExamplesApp = ({
|
|||
);
|
||||
} catch (e) {
|
||||
setRawResponse(e.body);
|
||||
if (e instanceof AbortError) {
|
||||
notifications.toasts.addWarning({
|
||||
title: e.message,
|
||||
});
|
||||
} else {
|
||||
notifications.toasts.addDanger({
|
||||
title: 'Failed to run search',
|
||||
text: e.message,
|
||||
});
|
||||
}
|
||||
data.search.showError(e);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
|
@ -390,16 +409,7 @@ export const SearchExamplesApp = ({
|
|||
},
|
||||
error: (e) => {
|
||||
setIsLoading(false);
|
||||
if (e instanceof AbortError) {
|
||||
notifications.toasts.addWarning({
|
||||
title: e.message,
|
||||
});
|
||||
} else {
|
||||
notifications.toasts.addDanger({
|
||||
title: 'Failed to run search',
|
||||
text: e.message,
|
||||
});
|
||||
}
|
||||
data.search.showError(e);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
@ -424,23 +434,17 @@ export const SearchExamplesApp = ({
|
|||
|
||||
notifications.toasts.addSuccess(`Server returned ${JSON.stringify(res)}`);
|
||||
} catch (e) {
|
||||
if (e?.name === 'AbortError') {
|
||||
notifications.toasts.addWarning({
|
||||
title: e.message,
|
||||
});
|
||||
} else {
|
||||
notifications.toasts.addDanger({
|
||||
title: 'Failed to run search',
|
||||
text: e.message,
|
||||
});
|
||||
}
|
||||
data.search.showError(e);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const onSearchSourceClickHandler = (withOtherBucket: boolean) => {
|
||||
doSearchSourceSearch(withOtherBucket);
|
||||
const onSearchSourceClickHandler = (
|
||||
withOtherBucket: boolean,
|
||||
showWarningToastNotifications: boolean
|
||||
) => {
|
||||
doSearchSourceSearch(withOtherBucket, showWarningToastNotifications);
|
||||
};
|
||||
|
||||
const reqTabs: EuiTabbedContentTab[] = [
|
||||
|
@ -491,6 +495,35 @@ export const SearchExamplesApp = ({
|
|||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'warnings',
|
||||
name: <EuiText data-test-subj="warningsTab">Warnings</EuiText>,
|
||||
content: (
|
||||
<>
|
||||
{' '}
|
||||
<EuiSpacer />{' '}
|
||||
<EuiText size="xs">
|
||||
{' '}
|
||||
<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."
|
||||
/>{' '}
|
||||
</EuiText>{' '}
|
||||
<EuiProgress value={loaded} max={total} size="xs" data-test-subj="progressBar" />{' '}
|
||||
<EuiCodeBlock
|
||||
language="json"
|
||||
fontSize="s"
|
||||
paddingSize="s"
|
||||
overflowHeight={450}
|
||||
isCopyable
|
||||
data-test-subj="warningsCodeBlock"
|
||||
>
|
||||
{' '}
|
||||
{JSON.stringify(warningContents, null, 2)}{' '}
|
||||
</EuiCodeBlock>{' '}
|
||||
</>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
|
@ -535,7 +568,7 @@ export const SearchExamplesApp = ({
|
|||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiFormLabel>Field (bucket)</EuiFormLabel>
|
||||
<EuiFormLabel>Field (using {bucketAggType} buckets)</EuiFormLabel>
|
||||
<EuiComboBox
|
||||
options={formatFieldsToComboBox(getAggregatableStrings(fields))}
|
||||
selectedOptions={formatFieldToComboBox(selectedBucketField)}
|
||||
|
@ -553,7 +586,7 @@ export const SearchExamplesApp = ({
|
|||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiFormLabel>Numeric Field (metric)</EuiFormLabel>
|
||||
<EuiFormLabel>Numeric Field (using {metricAggType} metrics)</EuiFormLabel>
|
||||
<EuiComboBox
|
||||
options={formatFieldsToComboBox(getNumeric(fields))}
|
||||
selectedOptions={formatFieldToComboBox(selectedNumericField)}
|
||||
|
@ -586,6 +619,9 @@ export const SearchExamplesApp = ({
|
|||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGrid>
|
||||
|
||||
<EuiHorizontalRule />
|
||||
|
||||
<EuiFlexGrid columns={2}>
|
||||
<EuiFlexItem style={{ width: '40%' }}>
|
||||
<EuiSpacer />
|
||||
|
@ -613,7 +649,7 @@ export const SearchExamplesApp = ({
|
|||
</EuiText>
|
||||
<EuiButtonEmpty
|
||||
size="xs"
|
||||
onClick={() => onSearchSourceClickHandler(true)}
|
||||
onClick={() => onSearchSourceClickHandler(true, true)}
|
||||
iconType="play"
|
||||
data-test-subj="searchSourceWithOther"
|
||||
>
|
||||
|
@ -625,12 +661,12 @@ export const SearchExamplesApp = ({
|
|||
<EuiText size="xs" color="subdued" className="searchExampleStepDsc">
|
||||
<FormattedMessage
|
||||
id="searchExamples.buttonText"
|
||||
defaultMessage="Bucket and metrics aggregations with other bucket."
|
||||
defaultMessage="Bucket and metrics aggregations, with other bucket and default warnings."
|
||||
/>
|
||||
</EuiText>
|
||||
<EuiButtonEmpty
|
||||
size="xs"
|
||||
onClick={() => onSearchSourceClickHandler(false)}
|
||||
onClick={() => onSearchSourceClickHandler(false, false)}
|
||||
iconType="play"
|
||||
data-test-subj="searchSourceWithoutOther"
|
||||
>
|
||||
|
@ -642,7 +678,7 @@ export const SearchExamplesApp = ({
|
|||
<EuiText size="xs" color="subdued" className="searchExampleStepDsc">
|
||||
<FormattedMessage
|
||||
id="searchExamples.buttonText"
|
||||
defaultMessage="Bucket and metrics aggregations without other bucket."
|
||||
defaultMessage="Bucket and metrics aggregations, without other bucket and with custom logic to handle warnings."
|
||||
/>
|
||||
</EuiText>
|
||||
</EuiText>
|
||||
|
|
|
@ -16,6 +16,7 @@
|
|||
{ "path": "../../src/core/tsconfig.json" },
|
||||
{ "path": "../../src/plugins/data/tsconfig.json" },
|
||||
{ "path": "../../src/plugins/data_views/tsconfig.json" },
|
||||
{ "path": "../../src/plugins/inspector/tsconfig.json" },
|
||||
{ "path": "../../src/plugins/kibana_utils/tsconfig.json" },
|
||||
{ "path": "../../src/plugins/kibana_react/tsconfig.json" },
|
||||
{ "path": "../../src/plugins/navigation/tsconfig.json" },
|
||||
|
|
|
@ -16,6 +16,7 @@ import { IKibanaSearchResponse } from '../../types';
|
|||
* This type is used when flattenning a SearchSource and passing it down to legacy search.
|
||||
* Once legacy search is removed, this type should become internal to `SearchSource`,
|
||||
* where `ISearchRequestParams` is used externally instead.
|
||||
* FIXME: replace with estypes.SearchRequest?
|
||||
*/
|
||||
export type SearchRequest = Record<string, any>;
|
||||
|
||||
|
|
|
@ -181,7 +181,6 @@ export {
|
|||
SearchSource,
|
||||
SearchSessionState,
|
||||
SortDirection,
|
||||
handleResponse,
|
||||
} from './search';
|
||||
|
||||
export type {
|
||||
|
|
130
src/plugins/data/public/search/fetch/extract_warnings.test.ts
Normal file
130
src/plugins/data/public/search/fetch/extract_warnings.test.ts
Normal file
|
@ -0,0 +1,130 @@
|
|||
/*
|
||||
* 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 { 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]',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
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 you are seeing might be incomplete or wrong.',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
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',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
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 you are seeing might be incomplete or wrong.',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
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 you are seeing might be incomplete or wrong.',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
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 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([]);
|
||||
});
|
||||
});
|
69
src/plugins/data/public/search/fetch/extract_warnings.ts
Normal file
69
src/plugins/data/public/search/fetch/extract_warnings.ts
Normal file
|
@ -0,0 +1,69 @@
|
|||
/*
|
||||
* 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 { i18n } from '@kbn/i18n';
|
||||
import { SearchResponseWarning } from '../types';
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export function extractWarnings(rawResponse: estypes.SearchResponse): SearchResponseWarning[] {
|
||||
const warnings: SearchResponseWarning[] = [];
|
||||
|
||||
if (rawResponse.timed_out === true) {
|
||||
warnings.push({
|
||||
type: 'timed_out',
|
||||
message: i18n.translate('data.search.searchSource.fetch.requestTimedOutNotificationMessage', {
|
||||
defaultMessage: 'Data might be incomplete because your request timed out',
|
||||
}),
|
||||
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 you are seeing might be incomplete or wrong.' }
|
||||
);
|
||||
|
||||
if (rawResponse._shards.failures) {
|
||||
rawResponse._shards.failures?.forEach((f) => {
|
||||
warnings.push({
|
||||
type: 'shard_failure',
|
||||
message,
|
||||
text,
|
||||
reason: {
|
||||
type: f.reason.type,
|
||||
reason: f.reason.reason,
|
||||
},
|
||||
});
|
||||
});
|
||||
} else {
|
||||
// unknown type and reason
|
||||
warnings.push({
|
||||
type: 'shard_failure',
|
||||
message,
|
||||
text,
|
||||
reason: { type: 'generic_shard_warning' },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return warnings;
|
||||
}
|
|
@ -1,99 +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 { handleResponse } from './handle_response';
|
||||
|
||||
// Temporary disable eslint, will be removed after moving to new platform folder
|
||||
import { notificationServiceMock } from '@kbn/core-notifications-browser-mocks';
|
||||
import { setNotifications } from '../../services';
|
||||
import { SearchSourceSearchOptions } from '../../../common';
|
||||
import { themeServiceMock } from '@kbn/core/public/mocks';
|
||||
|
||||
jest.mock('@kbn/i18n', () => {
|
||||
return {
|
||||
i18n: {
|
||||
translate: (_id: string, { defaultMessage }: { defaultMessage: string }) => defaultMessage,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const theme = themeServiceMock.createStartContract();
|
||||
|
||||
describe('handleResponse', () => {
|
||||
const notifications = notificationServiceMock.createStartContract();
|
||||
let options: SearchSourceSearchOptions;
|
||||
|
||||
beforeEach(() => {
|
||||
setNotifications(notifications);
|
||||
(notifications.toasts.addWarning as jest.Mock).mockReset();
|
||||
options = { disableShardFailureWarning: false };
|
||||
});
|
||||
|
||||
test('should notify if timed out', () => {
|
||||
const request = { body: {} };
|
||||
const response = {
|
||||
rawResponse: {
|
||||
timed_out: true,
|
||||
},
|
||||
};
|
||||
const result = handleResponse(request, response, options, theme);
|
||||
expect(result).toBe(response);
|
||||
expect(notifications.toasts.addWarning).toBeCalled();
|
||||
expect((notifications.toasts.addWarning as jest.Mock).mock.calls[0][0].title).toMatch(
|
||||
'request timed out'
|
||||
);
|
||||
});
|
||||
|
||||
test('should notify if shards failed', () => {
|
||||
const request = { body: {} };
|
||||
const response = {
|
||||
rawResponse: {
|
||||
_shards: {
|
||||
failed: 1,
|
||||
total: 2,
|
||||
successful: 1,
|
||||
skipped: 1,
|
||||
},
|
||||
},
|
||||
};
|
||||
const result = handleResponse(request, response, options, theme);
|
||||
expect(result).toBe(response);
|
||||
expect(notifications.toasts.addWarning).toBeCalled();
|
||||
expect((notifications.toasts.addWarning as jest.Mock).mock.calls[0][0].title).toMatch(
|
||||
'shards failed'
|
||||
);
|
||||
});
|
||||
|
||||
test('should not notify of shards failed if disableShardFailureWarning is true', () => {
|
||||
options.disableShardFailureWarning = true;
|
||||
|
||||
const request = { body: {} };
|
||||
const response = {
|
||||
rawResponse: {
|
||||
_shards: {
|
||||
failed: 1,
|
||||
total: 2,
|
||||
successful: 1,
|
||||
skipped: 1,
|
||||
},
|
||||
},
|
||||
};
|
||||
const result = handleResponse(request, response, options, theme);
|
||||
expect(result).toBe(response);
|
||||
expect(notifications.toasts.addWarning).not.toBeCalled();
|
||||
});
|
||||
|
||||
test('returns the response', () => {
|
||||
const request = {};
|
||||
const response = {
|
||||
rawResponse: {},
|
||||
};
|
||||
const result = handleResponse(request, response, options, theme);
|
||||
expect(result).toBe(response);
|
||||
});
|
||||
});
|
|
@ -1,68 +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 { i18n } from '@kbn/i18n';
|
||||
import { EuiSpacer } from '@elastic/eui';
|
||||
import { ThemeServiceStart } from '@kbn/core/public';
|
||||
import { toMountPoint } from '@kbn/kibana-react-plugin/public';
|
||||
import { IKibanaSearchResponse, SearchSourceSearchOptions } from '../../../common';
|
||||
import { ShardFailureOpenModalButton } from '../../shard_failure_modal';
|
||||
import { getNotifications } from '../../services';
|
||||
import type { SearchRequest } from '..';
|
||||
|
||||
export function handleResponse(
|
||||
request: SearchRequest,
|
||||
response: IKibanaSearchResponse,
|
||||
{ disableShardFailureWarning }: SearchSourceSearchOptions,
|
||||
theme: ThemeServiceStart
|
||||
) {
|
||||
const { rawResponse } = response;
|
||||
|
||||
if (rawResponse.timed_out) {
|
||||
getNotifications().toasts.addWarning({
|
||||
title: i18n.translate('data.search.searchSource.fetch.requestTimedOutNotificationMessage', {
|
||||
defaultMessage: 'Data might be incomplete because your request timed out',
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
if (rawResponse._shards && rawResponse._shards.failed && !disableShardFailureWarning) {
|
||||
const title = i18n.translate('data.search.searchSource.fetch.shardsFailedNotificationMessage', {
|
||||
defaultMessage: '{shardsFailed} of {shardsTotal} shards failed',
|
||||
values: {
|
||||
shardsFailed: rawResponse._shards.failed,
|
||||
shardsTotal: rawResponse._shards.total,
|
||||
},
|
||||
});
|
||||
const description = i18n.translate(
|
||||
'data.search.searchSource.fetch.shardsFailedNotificationDescription',
|
||||
{
|
||||
defaultMessage: 'The data you are seeing might be incomplete or wrong.',
|
||||
}
|
||||
);
|
||||
|
||||
const text = toMountPoint(
|
||||
<>
|
||||
{description}
|
||||
<EuiSpacer size="s" />
|
||||
<ShardFailureOpenModalButton
|
||||
request={request.body}
|
||||
response={rawResponse}
|
||||
theme={theme}
|
||||
title={title}
|
||||
/>
|
||||
</>,
|
||||
{ theme$: theme.theme$ }
|
||||
);
|
||||
|
||||
getNotifications().toasts.addWarning({ title, text });
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
157
src/plugins/data/public/search/fetch/handle_warnings.test.ts
Normal file
157
src/plugins/data/public/search/fetch/handle_warnings.test.ts
Normal file
|
@ -0,0 +1,157 @@
|
|||
/*
|
||||
* 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';
|
||||
|
||||
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' },
|
||||
},
|
||||
];
|
||||
|
||||
describe('Filtering and showing warnings', () => {
|
||||
const notifications = notificationServiceMock.createStartContract();
|
||||
|
||||
describe('handleWarnings', () => {
|
||||
const request = { body: {} };
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
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);
|
||||
|
||||
expect(notifications.toasts.addWarning).toBeCalledTimes(1);
|
||||
expect(notifications.toasts.addWarning).toBeCalledWith({ title: 'Something timed out!' });
|
||||
});
|
||||
|
||||
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);
|
||||
|
||||
expect(notifications.toasts.addWarning).toBeCalledTimes(1);
|
||||
expect(notifications.toasts.addWarning).toBeCalledWith({ title: 'Some shards failed!' });
|
||||
});
|
||||
|
||||
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);
|
||||
|
||||
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 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();
|
||||
|
||||
beforeEach(() => {
|
||||
callback.mockImplementation(() => {
|
||||
throw new Error('not initialized');
|
||||
});
|
||||
});
|
||||
|
||||
it('filters out all', () => {
|
||||
callback.mockImplementation(() => true);
|
||||
expect(filterWarnings(warnings, callback)).toEqual([]);
|
||||
});
|
||||
|
||||
it('filters out some', () => {
|
||||
callback.mockImplementation(
|
||||
(warning: SearchResponseWarning) => warning.reason?.type !== 'generic_shard_failure'
|
||||
);
|
||||
expect(filterWarnings(warnings, callback)).toEqual([warnings[2]]);
|
||||
});
|
||||
|
||||
it('filters out none', () => {
|
||||
callback.mockImplementation(() => false);
|
||||
expect(filterWarnings(warnings, callback)).toEqual(warnings);
|
||||
});
|
||||
});
|
||||
});
|
101
src/plugins/data/public/search/fetch/handle_warnings.tsx
Normal file
101
src/plugins/data/public/search/fetch/handle_warnings.tsx
Normal file
|
@ -0,0 +1,101 @@
|
|||
/*
|
||||
* 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 { EuiSpacer } from '@elastic/eui';
|
||||
import { ThemeServiceStart } from '@kbn/core/public';
|
||||
import { toMountPoint } from '@kbn/kibana-react-plugin/public';
|
||||
import React from 'react';
|
||||
import { SearchRequest } from '..';
|
||||
import { getNotifications } from '../../services';
|
||||
import { ShardFailureOpenModalButton, ShardFailureRequest } from '../../shard_failure_modal';
|
||||
import {
|
||||
SearchResponseShardFailureWarning,
|
||||
SearchResponseWarning,
|
||||
WarningHandlerCallback,
|
||||
} from '../types';
|
||||
import { extractWarnings } from './extract_warnings';
|
||||
|
||||
/**
|
||||
* @internal
|
||||
* All warnings are expected to come from the same response. Therefore all "text" properties, which contain the
|
||||
* response, will be the same.
|
||||
*/
|
||||
export function handleWarnings(
|
||||
request: SearchRequest,
|
||||
response: estypes.SearchResponse,
|
||||
theme: ThemeServiceStart,
|
||||
cb?: WarningHandlerCallback
|
||||
) {
|
||||
const warnings = extractWarnings(response);
|
||||
if (warnings.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const internal = cb ? filterWarnings(warnings, cb) : warnings;
|
||||
if (internal.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// timeout notification
|
||||
const [timeout] = internal.filter((w) => w.type === 'timed_out');
|
||||
if (timeout) {
|
||||
getNotifications().toasts.addWarning({
|
||||
title: timeout.message,
|
||||
});
|
||||
}
|
||||
|
||||
// shard warning failure notification
|
||||
const shardFailures = internal.filter((w) => w.type === 'shard_failure');
|
||||
if (shardFailures.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" />
|
||||
<ShardFailureOpenModalButton
|
||||
request={request as ShardFailureRequest}
|
||||
response={response}
|
||||
theme={theme}
|
||||
title={title}
|
||||
/>
|
||||
</>,
|
||||
{ theme$: theme.theme$ }
|
||||
);
|
||||
|
||||
getNotifications().toasts.addWarning({ title, text });
|
||||
return;
|
||||
}
|
||||
|
||||
// timeout warning, or shard warning with no failure reason
|
||||
getNotifications().toasts.addWarning({ title });
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export function filterWarnings(warnings: SearchResponseWarning[], cb: WarningHandlerCallback) {
|
||||
const unfiltered: SearchResponseWarning[] = [];
|
||||
|
||||
// use the consumer's callback as a filter to receive warnings to handle on our side
|
||||
warnings.forEach((warning) => {
|
||||
const consumerHandled = cb?.(warning);
|
||||
if (!consumerHandled) {
|
||||
unfiltered.push(warning);
|
||||
}
|
||||
});
|
||||
|
||||
return unfiltered;
|
||||
}
|
|
@ -6,4 +6,4 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
export { handleResponse } from './handle_response';
|
||||
export { handleWarnings } from './handle_warnings';
|
||||
|
|
|
@ -53,7 +53,6 @@ export {
|
|||
waitUntilNextSessionCompletes$,
|
||||
} from './session';
|
||||
export { getEsPreference } from './es_search';
|
||||
export { handleResponse } from './fetch';
|
||||
|
||||
export type { SearchInterceptorDeps } from './search_interceptor';
|
||||
export { SearchInterceptor } from './search_interceptor';
|
||||
|
|
|
@ -26,6 +26,7 @@ function createStartContract(): jest.Mocked<ISearchStart> {
|
|||
aggs: searchAggsStartMock(),
|
||||
search: jest.fn(),
|
||||
showError: jest.fn(),
|
||||
showWarnings: jest.fn(),
|
||||
session: getSessionServiceMock(),
|
||||
sessionsClient: getSessionsClientMock(),
|
||||
searchSource: searchSourceMock.createStartContract(),
|
||||
|
|
|
@ -6,16 +6,21 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import type { MockedKeys } from '@kbn/utility-types-jest';
|
||||
import { coreMock } from '@kbn/core/public/mocks';
|
||||
import { CoreSetup, CoreStart } from '@kbn/core/public';
|
||||
|
||||
import { SearchService, SearchServiceSetupDependencies } from './search_service';
|
||||
import { estypes } from '@elastic/elasticsearch';
|
||||
import { bfetchPluginMock } from '@kbn/bfetch-plugin/public/mocks';
|
||||
import { managementPluginMock } from '@kbn/management-plugin/public/mocks';
|
||||
import { screenshotModePluginMock } from '@kbn/screenshot-mode-plugin/public/mocks';
|
||||
import { notificationServiceMock } from '@kbn/core-notifications-browser-mocks';
|
||||
import { CoreSetup, CoreStart } from '@kbn/core/public';
|
||||
import { coreMock } from '@kbn/core/public/mocks';
|
||||
import { DataViewsContract } from '@kbn/data-views-plugin/common';
|
||||
import { FieldFormatsStart } from '@kbn/field-formats-plugin/public';
|
||||
import { RequestAdapter } from '@kbn/inspector-plugin/public';
|
||||
import { managementPluginMock } from '@kbn/management-plugin/public/mocks';
|
||||
import { screenshotModePluginMock } from '@kbn/screenshot-mode-plugin/public/mocks';
|
||||
import type { MockedKeys } from '@kbn/utility-types-jest';
|
||||
import { IInspectorInfo } from '../../common/search/search_source';
|
||||
import { setNotifications } from '../services';
|
||||
import { SearchService, SearchServiceSetupDependencies } from './search_service';
|
||||
import { ISearchStart, WarningHandlerCallback } from './types';
|
||||
|
||||
describe('Search service', () => {
|
||||
let searchService: SearchService;
|
||||
|
@ -49,7 +54,8 @@ describe('Search service', () => {
|
|||
});
|
||||
|
||||
describe('start()', () => {
|
||||
it('exposes proper contract', async () => {
|
||||
let data: ISearchStart;
|
||||
beforeEach(() => {
|
||||
const bfetch = bfetchPluginMock.createSetupContract();
|
||||
searchService.setup(mockCoreSetup, {
|
||||
packageInfo: { version: '8' },
|
||||
|
@ -57,18 +63,174 @@ describe('Search service', () => {
|
|||
expressions: { registerFunction: jest.fn(), registerType: jest.fn() },
|
||||
management: managementPluginMock.createSetupContract(),
|
||||
} as unknown as SearchServiceSetupDependencies);
|
||||
|
||||
const start = searchService.start(mockCoreStart, {
|
||||
data = searchService.start(mockCoreStart, {
|
||||
fieldFormats: {} as FieldFormatsStart,
|
||||
indexPatterns: {} as DataViewsContract,
|
||||
screenshotMode: screenshotModePluginMock.createStartContract(),
|
||||
});
|
||||
expect(start).toHaveProperty('aggs');
|
||||
expect(start).toHaveProperty('search');
|
||||
expect(start).toHaveProperty('showError');
|
||||
expect(start).toHaveProperty('searchSource');
|
||||
expect(start).toHaveProperty('sessionsClient');
|
||||
expect(start).toHaveProperty('session');
|
||||
});
|
||||
|
||||
it('exposes proper contract', async () => {
|
||||
expect(data).toHaveProperty('aggs');
|
||||
expect(data).toHaveProperty('search');
|
||||
expect(data).toHaveProperty('showError');
|
||||
expect(data).toHaveProperty('searchSource');
|
||||
expect(data).toHaveProperty('sessionsClient');
|
||||
expect(data).toHaveProperty('session');
|
||||
});
|
||||
|
||||
describe('showWarnings', () => {
|
||||
const notifications = notificationServiceMock.createStartContract();
|
||||
const hits = { total: 0, max_score: null, hits: [] };
|
||||
let failures: estypes.ShardFailure[] = [];
|
||||
let shards: estypes.ShardStatistics;
|
||||
let inspector: Required<IInspectorInfo>;
|
||||
let callback: WarningHandlerCallback;
|
||||
|
||||
const getMockInspector = (base: Partial<IInspectorInfo>): Required<IInspectorInfo> =>
|
||||
({
|
||||
title: 'test inspector',
|
||||
id: 'test-inspector-123',
|
||||
description: '',
|
||||
...base,
|
||||
} as Required<IInspectorInfo>);
|
||||
|
||||
const getMockResponseWithShards = (mockShards: estypes.ShardStatistics) => ({
|
||||
json: {
|
||||
rawResponse: { took: 25, timed_out: false, _shards: mockShards, hits, aggregations: {} },
|
||||
},
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
setNotifications(notifications);
|
||||
notifications.toasts.addWarning.mockClear();
|
||||
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]',
|
||||
},
|
||||
},
|
||||
];
|
||||
shards = { total: 4, successful: 2, skipped: 0, failed: 2, failures };
|
||||
const adapter = new RequestAdapter();
|
||||
inspector = getMockInspector({ adapter });
|
||||
callback = jest.fn(() => false);
|
||||
});
|
||||
|
||||
it('can show no notifications', () => {
|
||||
const responder = inspector.adapter.start('request1');
|
||||
shards = { total: 4, successful: 4, skipped: 0, failed: 0 };
|
||||
responder.ok(getMockResponseWithShards(shards));
|
||||
data.showWarnings(inspector.adapter, callback);
|
||||
|
||||
expect(notifications.toasts.addWarning).toBeCalledTimes(0);
|
||||
});
|
||||
|
||||
it('can show notifications if no callback is provided', () => {
|
||||
const responder = inspector.adapter.start('request1');
|
||||
responder.ok(getMockResponseWithShards(shards));
|
||||
data.showWarnings(inspector.adapter);
|
||||
|
||||
expect(notifications.toasts.addWarning).toBeCalledTimes(1);
|
||||
expect(notifications.toasts.addWarning).toBeCalledWith({
|
||||
title: '2 of 4 shards failed',
|
||||
text: expect.any(Function),
|
||||
});
|
||||
});
|
||||
|
||||
it("won't show notifications when all warnings are filtered out", () => {
|
||||
callback = () => true;
|
||||
const responder = inspector.adapter.start('request1');
|
||||
responder.ok(getMockResponseWithShards(shards));
|
||||
data.showWarnings(inspector.adapter, callback);
|
||||
|
||||
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(getMockResponseWithShards(shards));
|
||||
|
||||
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: '2 of 4 shards failed',
|
||||
text: expect.any(Function),
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -6,6 +6,8 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { estypes } from '@elastic/elasticsearch';
|
||||
import { BfetchPublicSetup } from '@kbn/bfetch-plugin/public';
|
||||
import {
|
||||
CoreSetup,
|
||||
CoreStart,
|
||||
|
@ -13,24 +15,21 @@ import {
|
|||
PluginInitializerContext,
|
||||
StartServicesAccessor,
|
||||
} from '@kbn/core/public';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
import React from 'react';
|
||||
import moment from 'moment';
|
||||
import { BfetchPublicSetup } from '@kbn/bfetch-plugin/public';
|
||||
import { FieldFormatsStart } from '@kbn/field-formats-plugin/public';
|
||||
import { UsageCollectionSetup } from '@kbn/usage-collection-plugin/public';
|
||||
import { DataViewsContract } from '@kbn/data-views-plugin/common';
|
||||
import { ExpressionsSetup } from '@kbn/expressions-plugin/public';
|
||||
import { FieldFormatsStart } from '@kbn/field-formats-plugin/public';
|
||||
import { toMountPoint } from '@kbn/kibana-react-plugin/public';
|
||||
import { Storage } from '@kbn/kibana-utils-plugin/public';
|
||||
import { ScreenshotModePluginStart } from '@kbn/screenshot-mode-plugin/public';
|
||||
import { ManagementSetup } from '@kbn/management-plugin/public';
|
||||
import { DataViewsContract } from '@kbn/data-views-plugin/common';
|
||||
import type { ISearchSetup, ISearchStart } from './types';
|
||||
|
||||
import { handleResponse } from './fetch';
|
||||
import { ScreenshotModePluginStart } from '@kbn/screenshot-mode-plugin/public';
|
||||
import { UsageCollectionSetup } from '@kbn/usage-collection-plugin/public';
|
||||
import moment from 'moment';
|
||||
import React from 'react';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
import {
|
||||
cidrFunction,
|
||||
dateRangeFunction,
|
||||
eqlRawResponse,
|
||||
esRawResponse,
|
||||
existsFilterFunction,
|
||||
extendedBoundsFunction,
|
||||
|
@ -51,30 +50,29 @@ import {
|
|||
rangeFilterFunction,
|
||||
rangeFunction,
|
||||
removeFilterFunction,
|
||||
SearchRequest,
|
||||
SearchSourceDependencies,
|
||||
SearchSourceService,
|
||||
selectFilterFunction,
|
||||
eqlRawResponse,
|
||||
SearchSourceSearchOptions,
|
||||
} from '../../common/search';
|
||||
import { AggsService } from './aggs';
|
||||
import { IKibanaSearchResponse, SearchRequest } from '..';
|
||||
import { ISearchInterceptor, SearchInterceptor } from './search_interceptor';
|
||||
import { createUsageCollector, SearchUsageCollector } from './collectors';
|
||||
import { getEsaggs, getEsdsl, getEssql, getEql } from './expressions';
|
||||
import { ISessionsClient, ISessionService, SessionsClient, SessionService } from './session';
|
||||
import { ConfigSchema } from '../../config';
|
||||
import {
|
||||
getShardDelayBucketAgg,
|
||||
SHARD_DELAY_AGG_NAME,
|
||||
} from '../../common/search/aggs/buckets/shard_delay';
|
||||
import { aggShardDelay } from '../../common/search/aggs/buckets/shard_delay_fn';
|
||||
import { DataPublicPluginStart, DataStartDependencies } from '../types';
|
||||
import { ConfigSchema } from '../../config';
|
||||
import { NowProviderInternalContract } from '../now_provider';
|
||||
import { DataPublicPluginStart, DataStartDependencies } from '../types';
|
||||
import { AggsService } from './aggs';
|
||||
import { createUsageCollector, SearchUsageCollector } from './collectors';
|
||||
import { getEql, getEsaggs, getEsdsl, getEssql } from './expressions';
|
||||
import { getKibanaContext } from './expressions/kibana_context';
|
||||
import { createConnectedSearchSessionIndicator } from './session/session_indicator';
|
||||
|
||||
import { handleWarnings } from './fetch/handle_warnings';
|
||||
import { ISearchInterceptor, SearchInterceptor } from './search_interceptor';
|
||||
import { ISessionsClient, ISessionService, SessionsClient, SessionService } from './session';
|
||||
import { registerSearchSessionsMgmt } from './session/sessions_mgmt';
|
||||
import { createConnectedSearchSessionIndicator } from './session/session_indicator';
|
||||
import { ISearchSetup, ISearchStart } from './types';
|
||||
|
||||
/** @internal */
|
||||
export interface SearchServiceSetupDependencies {
|
||||
|
@ -239,8 +237,13 @@ export class SearchService implements Plugin<ISearchSetup, ISearchStart> {
|
|||
aggs,
|
||||
getConfig: uiSettings.get.bind(uiSettings),
|
||||
search,
|
||||
onResponse: (...args: [SearchRequest, IKibanaSearchResponse, SearchSourceSearchOptions]) =>
|
||||
handleResponse(...args, theme),
|
||||
onResponse: (request, response, options) => {
|
||||
if (!options.disableShardFailureWarning) {
|
||||
const { rawResponse } = response;
|
||||
handleWarnings(request.body, rawResponse, theme);
|
||||
}
|
||||
return response;
|
||||
},
|
||||
};
|
||||
|
||||
const config = this.initializerContext.config.get();
|
||||
|
@ -268,9 +271,22 @@ export class SearchService implements Plugin<ISearchSetup, ISearchStart> {
|
|||
return {
|
||||
aggs,
|
||||
search,
|
||||
showError: (e: Error) => {
|
||||
showError: (e) => {
|
||||
this.searchInterceptor.showError(e);
|
||||
},
|
||||
showWarnings: (adapter, cb) => {
|
||||
adapter?.getRequests().forEach((request) => {
|
||||
const rawResponse = (
|
||||
request.response?.json as { rawResponse: estypes.SearchResponse | undefined }
|
||||
)?.rawResponse;
|
||||
|
||||
if (!rawResponse) {
|
||||
return;
|
||||
}
|
||||
|
||||
handleWarnings(request.json as SearchRequest, rawResponse, theme, cb);
|
||||
});
|
||||
},
|
||||
session: this.sessionService,
|
||||
sessionsClient: this.sessionsClient,
|
||||
searchSource: this.searchSourceService.start(indexPatterns, searchSourceDependencies),
|
||||
|
|
|
@ -6,14 +6,17 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { estypes } from '@elastic/elasticsearch';
|
||||
import type { PackageInfo } from '@kbn/core/server';
|
||||
import { UsageCollectionSetup } from '@kbn/usage-collection-plugin/public';
|
||||
import { DataViewsContract } from '@kbn/data-views-plugin/common';
|
||||
import { SearchUsageCollector } from './collectors';
|
||||
import { AggsSetup, AggsSetupDependencies, AggsStartDependencies, AggsStart } from './aggs';
|
||||
import { RequestAdapter } from '@kbn/inspector-plugin/public';
|
||||
import { UsageCollectionSetup } from '@kbn/usage-collection-plugin/public';
|
||||
import { ISearchGeneric, ISearchStartSearchSource } from '../../common/search';
|
||||
import { AggsSetup, AggsSetupDependencies, AggsStart, AggsStartDependencies } from './aggs';
|
||||
import { SearchUsageCollector } from './collectors';
|
||||
import { ISessionsClient, ISessionService } from './session';
|
||||
|
||||
export { SEARCH_EVENT_TYPE } from './collectors';
|
||||
export type { ISearchStartSearchSource, SearchUsageCollector };
|
||||
|
||||
/**
|
||||
|
@ -51,8 +54,17 @@ export interface ISearchStart {
|
|||
* {@link ISearchGeneric}
|
||||
*/
|
||||
search: ISearchGeneric;
|
||||
|
||||
/**
|
||||
* Show toast for caught error
|
||||
* @param e Error
|
||||
*/
|
||||
showError: (e: Error) => void;
|
||||
/**
|
||||
* Show warnings, or customize how they're shown
|
||||
* @param inspector IInspectorInfo - an inspector object with requests internally collected
|
||||
* @param cb WarningHandlerCallback - optional callback to intercept warnings
|
||||
*/
|
||||
showWarnings: (adapter: RequestAdapter, cb?: WarningHandlerCallback) => void;
|
||||
/**
|
||||
* high level search
|
||||
* {@link ISearchStartSearchSource}
|
||||
|
@ -70,8 +82,6 @@ export interface ISearchStart {
|
|||
sessionsClient: ISessionsClient;
|
||||
}
|
||||
|
||||
export { SEARCH_EVENT_TYPE } from './collectors';
|
||||
|
||||
/** @internal */
|
||||
export interface SearchServiceSetupDependencies {
|
||||
packageInfo: PackageInfo;
|
||||
|
@ -84,3 +94,69 @@ export interface SearchServiceStartDependencies {
|
|||
fieldFormats: AggsStartDependencies['fieldFormats'];
|
||||
indexPatterns: DataViewsContract;
|
||||
}
|
||||
|
||||
/**
|
||||
* A warning object for a search response with internal ES timeouts
|
||||
* @public
|
||||
*/
|
||||
export interface SearchResponseTimeoutWarning {
|
||||
/**
|
||||
* type: for sorting out timeout warnings
|
||||
*/
|
||||
type: 'timed_out';
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
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'];
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* A warning object for a search response with warnings
|
||||
* @public
|
||||
*/
|
||||
export type SearchResponseWarning =
|
||||
| SearchResponseTimeoutWarning
|
||||
| SearchResponseShardFailureWarning;
|
||||
|
||||
/**
|
||||
* A callback function which can intercept warnings when passed to {@link showWarnings}. Pass `true` from the
|
||||
* function to prevent the search service from showing warning notifications by default.
|
||||
* @public
|
||||
*/
|
||||
export type WarningHandlerCallback = (warnings: SearchResponseWarning) => boolean | undefined;
|
||||
|
|
|
@ -3,7 +3,9 @@
|
|||
exports[`ShardFailureModal renders matching snapshot given valid properties 1`] = `
|
||||
<Fragment>
|
||||
<EuiModalHeader>
|
||||
<EuiModalHeaderTitle>
|
||||
<EuiModalHeaderTitle
|
||||
data-test-subj="shardFailureModalTitle"
|
||||
>
|
||||
test
|
||||
</EuiModalHeaderTitle>
|
||||
</EuiModalHeader>
|
||||
|
@ -37,6 +39,7 @@ exports[`ShardFailureModal renders matching snapshot given valid properties 1`]
|
|||
]
|
||||
}
|
||||
/>,
|
||||
"data-test-subj": "shardFailuresModalShardButton",
|
||||
"id": "table",
|
||||
"name": "Shard failures",
|
||||
}
|
||||
|
@ -69,11 +72,13 @@ exports[`ShardFailureModal renders matching snapshot given valid properties 1`]
|
|||
]
|
||||
}
|
||||
/>,
|
||||
"data-test-subj": "shardFailuresModalShardButton",
|
||||
"id": "table",
|
||||
"name": "Shard failures",
|
||||
},
|
||||
Object {
|
||||
"content": <EuiCodeBlock
|
||||
data-test-subj="shardsFailedModalRequestBlock"
|
||||
isCopyable={true}
|
||||
language="json"
|
||||
>
|
||||
|
@ -93,11 +98,13 @@ exports[`ShardFailureModal renders matching snapshot given valid properties 1`]
|
|||
"highlight": {}
|
||||
}
|
||||
</EuiCodeBlock>,
|
||||
"data-test-subj": "shardFailuresModalRequestButton",
|
||||
"id": "json-request",
|
||||
"name": "Request",
|
||||
},
|
||||
Object {
|
||||
"content": <EuiCodeBlock
|
||||
data-test-subj="shardsFailedModalResponseBlock"
|
||||
isCopyable={true}
|
||||
language="json"
|
||||
>
|
||||
|
@ -131,6 +138,7 @@ exports[`ShardFailureModal renders matching snapshot given valid properties 1`]
|
|||
}
|
||||
}
|
||||
</EuiCodeBlock>,
|
||||
"data-test-subj": "shardFailuresModalResponseButton",
|
||||
"id": "json-response",
|
||||
"name": "Response",
|
||||
},
|
||||
|
@ -174,7 +182,7 @@ exports[`ShardFailureModal renders matching snapshot given valid properties 1`]
|
|||
<Component />
|
||||
</EuiCopy>
|
||||
<EuiButton
|
||||
data-test-sub="closeShardFailureModal"
|
||||
data-test-subj="closeShardFailureModal"
|
||||
fill={true}
|
||||
onClick={[Function]}
|
||||
>
|
||||
|
|
|
@ -61,6 +61,7 @@ export function ShardFailureModal({ request, response, title, onClose }: Props)
|
|||
}
|
||||
),
|
||||
content: <ShardFailureTable failures={failures} />,
|
||||
['data-test-subj']: 'shardFailuresModalShardButton',
|
||||
},
|
||||
{
|
||||
id: 'json-request',
|
||||
|
@ -69,10 +70,11 @@ export function ShardFailureModal({ request, response, title, onClose }: Props)
|
|||
description: 'Name of the tab displaying the JSON request',
|
||||
}),
|
||||
content: (
|
||||
<EuiCodeBlock language="json" isCopyable>
|
||||
<EuiCodeBlock language="json" isCopyable data-test-subj="shardsFailedModalRequestBlock">
|
||||
{requestJSON}
|
||||
</EuiCodeBlock>
|
||||
),
|
||||
['data-test-subj']: 'shardFailuresModalRequestButton',
|
||||
},
|
||||
{
|
||||
id: 'json-response',
|
||||
|
@ -81,17 +83,18 @@ export function ShardFailureModal({ request, response, title, onClose }: Props)
|
|||
description: 'Name of the tab displaying the JSON response',
|
||||
}),
|
||||
content: (
|
||||
<EuiCodeBlock language="json" isCopyable>
|
||||
<EuiCodeBlock language="json" isCopyable data-test-subj="shardsFailedModalResponseBlock">
|
||||
{responseJSON}
|
||||
</EuiCodeBlock>
|
||||
),
|
||||
['data-test-subj']: 'shardFailuresModalResponseButton',
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<EuiModalHeader>
|
||||
<EuiModalHeaderTitle>{title}</EuiModalHeaderTitle>
|
||||
<EuiModalHeaderTitle data-test-subj="shardFailureModalTitle">{title}</EuiModalHeaderTitle>
|
||||
</EuiModalHeader>
|
||||
<EuiModalBody>
|
||||
<EuiTabbedContent tabs={tabs} initialSelectedTab={tabs[0]} autoFocus="selected" />
|
||||
|
@ -107,7 +110,7 @@ export function ShardFailureModal({ request, response, title, onClose }: Props)
|
|||
</EuiButtonEmpty>
|
||||
)}
|
||||
</EuiCopy>
|
||||
<EuiButton onClick={() => onClose()} fill data-test-sub="closeShardFailureModal">
|
||||
<EuiButton onClick={() => onClose()} fill data-test-subj="closeShardFailureModal">
|
||||
<FormattedMessage
|
||||
id="data.search.searchSource.fetch.shardsFailedModal.close"
|
||||
defaultMessage="Close"
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
*/
|
||||
import type { KibanaExecutionContext } from '@kbn/core/public';
|
||||
import type { Adapters } from '@kbn/inspector-plugin/common';
|
||||
import { KibanaContext, handleResponse } from '@kbn/data-plugin/public';
|
||||
import { KibanaContext } from '@kbn/data-plugin/public';
|
||||
import { getTimezone } from './application/lib/get_timezone';
|
||||
import { getUISettings, getDataStart, getCoreStart } from './services';
|
||||
import { ROUTES, UI_SETTINGS } from '../common/constants';
|
||||
|
@ -37,7 +37,6 @@ export const metricsRequestHandler = async ({
|
|||
if (!expressionAbortSignal.aborted) {
|
||||
const config = getUISettings();
|
||||
const data = getDataStart();
|
||||
const theme = getCoreStart().theme;
|
||||
const abortController = new AbortController();
|
||||
const expressionAbortHandler = function () {
|
||||
abortController.abort();
|
||||
|
@ -84,10 +83,14 @@ export const metricsRequestHandler = async ({
|
|||
inspectorAdapters?.requests
|
||||
?.start(query.label ?? key, { searchSessionId })
|
||||
.json(query.body)
|
||||
.ok({ time: query.time });
|
||||
.ok({ time: query.time, json: { rawResponse: query.response } });
|
||||
|
||||
if (query.response && config.get(UI_SETTINGS.ALLOW_CHECKING_FOR_FAILED_SHARDS)) {
|
||||
handleResponse({ body: query.body }, { rawResponse: query.response }, {}, theme);
|
||||
if (
|
||||
query.response &&
|
||||
inspectorAdapters?.requests &&
|
||||
config.get(UI_SETTINGS.ALLOW_CHECKING_FOR_FAILED_SHARDS)
|
||||
) {
|
||||
data.search.showWarnings(inspectorAdapters.requests);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -17,7 +17,7 @@ export class IndexPatternsService extends FtrService {
|
|||
*/
|
||||
async create(
|
||||
indexPattern: { title: string; timeFieldName?: string },
|
||||
{ override = false }: { override: boolean },
|
||||
{ override } = { override: false },
|
||||
spaceId = ''
|
||||
): Promise<DataViewSpec> {
|
||||
const response = await this.kibanaServer.request<{
|
||||
|
|
|
@ -33,6 +33,7 @@ export default async function ({ readConfigFile }) {
|
|||
require.resolve('./data_view_field_editor_example'),
|
||||
require.resolve('./field_formats'),
|
||||
require.resolve('./partial_results'),
|
||||
require.resolve('./search'),
|
||||
],
|
||||
services: {
|
||||
...functionalConfig.get('services'),
|
||||
|
|
16
test/examples/search/index.ts
Normal file
16
test/examples/search/index.ts
Normal file
|
@ -0,0 +1,16 @@
|
|||
/*
|
||||
* 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 { FtrProviderContext } from '../../functional/ftr_provider_context';
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default function ({ loadTestFile }: FtrProviderContext) {
|
||||
describe('Search examples', () => {
|
||||
loadTestFile(require.resolve('./warnings'));
|
||||
});
|
||||
}
|
193
test/examples/search/warnings.ts
Normal file
193
test/examples/search/warnings.ts
Normal file
|
@ -0,0 +1,193 @@
|
|||
/*
|
||||
* 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 expect from '@kbn/expect';
|
||||
import { asyncForEach } from '@kbn/std';
|
||||
import { FtrProviderContext } from '../../functional/ftr_provider_context';
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
||||
const PageObjects = getPageObjects(['common', 'timePicker']);
|
||||
const testSubjects = getService('testSubjects');
|
||||
const find = getService('find');
|
||||
const es = getService('es');
|
||||
const log = getService('log');
|
||||
const indexPatterns = getService('indexPatterns');
|
||||
const comboBox = getService('comboBox');
|
||||
const kibanaServer = getService('kibanaServer');
|
||||
const esArchiver = getService('esArchiver');
|
||||
|
||||
describe('handling warnings with search source fetch', function () {
|
||||
const dataViewTitle = 'sample-01,sample-01-rollup';
|
||||
const fromTime = 'Jun 17, 2022 @ 00:00:00.000';
|
||||
const toTime = 'Jun 23, 2022 @ 00:00:00.000';
|
||||
const testArchive = 'test/functional/fixtures/es_archiver/search/downsampled';
|
||||
const testIndex = 'sample-01';
|
||||
const testRollupIndex = 'sample-01-rollup';
|
||||
const testRollupField = 'kubernetes.container.memory.usage.bytes';
|
||||
const toastsSelector = '[data-test-subj=globalToastList] [data-test-subj=euiToastHeader]';
|
||||
const shardFailureType = 'unsupported_aggregation_on_rollup_index';
|
||||
const shardFailureReason = `Field [${testRollupField}] of type [aggregate_metric_double] is not supported for aggregation [percentiles]`;
|
||||
|
||||
const getTestJson = async (tabTestSubj: string, codeTestSubj: string) => {
|
||||
log.info(`switch to ${tabTestSubj} tab...`);
|
||||
await testSubjects.click(tabTestSubj);
|
||||
const block = await testSubjects.find(codeTestSubj);
|
||||
const testText = (await block.getVisibleText()).trim();
|
||||
return testText && JSON.parse(testText);
|
||||
};
|
||||
|
||||
before(async () => {
|
||||
// create rollup data
|
||||
log.info(`loading ${testIndex} index...`);
|
||||
await esArchiver.loadIfNeeded(testArchive);
|
||||
log.info(`add write block to ${testIndex} index...`);
|
||||
await es.indices.addBlock({ index: testIndex, block: 'write' });
|
||||
try {
|
||||
log.info(`rolling up ${testIndex} index...`);
|
||||
await es.rollup.rollup({
|
||||
index: testIndex,
|
||||
rollup_index: testRollupIndex,
|
||||
config: { fixed_interval: '1h' },
|
||||
});
|
||||
} catch (err) {
|
||||
log.info(`ignoring resource_already_exists_exception...`);
|
||||
if (!err.message.match(/resource_already_exists_exception/)) {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
log.info(`creating ${dataViewTitle} data view...`);
|
||||
await indexPatterns.create(
|
||||
{
|
||||
title: dataViewTitle,
|
||||
timeFieldName: '@timestamp',
|
||||
},
|
||||
{ override: true }
|
||||
);
|
||||
await kibanaServer.uiSettings.update({
|
||||
'dateFormat:tz': 'UTC',
|
||||
defaultIndex: '0ae0bc7a-e4ca-405c-ab67-f2b5913f2a51',
|
||||
'timepicker:timeDefaults': '{ "from": "now-1y", "to": "now" }',
|
||||
});
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await es.indices.delete({ index: [testIndex, testRollupIndex] });
|
||||
await kibanaServer.savedObjects.cleanStandardList();
|
||||
await kibanaServer.uiSettings.replace({});
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
// reload the page to clear toasts from previous test
|
||||
|
||||
await PageObjects.common.navigateToApp('searchExamples');
|
||||
|
||||
await comboBox.setCustom('dataViewSelector', dataViewTitle);
|
||||
await comboBox.set('searchMetricField', testRollupField);
|
||||
await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime);
|
||||
});
|
||||
|
||||
it('shows shard failure warning notifications by default', async () => {
|
||||
await testSubjects.click('searchSourceWithOther');
|
||||
|
||||
// toasts
|
||||
const toasts = await find.allByCssSelector(toastsSelector);
|
||||
expect(toasts.length).to.be(3);
|
||||
const expects = ['2 of 4 shards failed', '2 of 4 shards failed', 'Query result']; // BUG: there are 2 shards failed toast notifications
|
||||
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();
|
||||
const modalHeader = await testSubjects.find('shardFailureModalTitle');
|
||||
expect(await modalHeader.getVisibleText()).to.be('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);
|
||||
|
||||
// close things
|
||||
await testSubjects.click('closeShardFailureModal');
|
||||
await PageObjects.common.clearAllToasts();
|
||||
|
||||
// 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([]);
|
||||
});
|
||||
|
||||
it('able to handle shard failure warnings and prevent default notifications', async () => {
|
||||
await testSubjects.click('searchSourceWithoutOther');
|
||||
|
||||
// toasts
|
||||
const toasts = await find.allByCssSelector(toastsSelector);
|
||||
expect(toasts.length).to.be(2);
|
||||
const expects = ['2 of 4 shards failed', '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();
|
||||
const modalHeader = await testSubjects.find('shardFailureModalTitle');
|
||||
expect(await modalHeader.getVisibleText()).to.be('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);
|
||||
|
||||
// close things
|
||||
await testSubjects.click('closeShardFailureModal');
|
||||
await PageObjects.common.clearAllToasts();
|
||||
|
||||
// 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 you are seeing might be incomplete or wrong.',
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
}
|
Binary file not shown.
|
@ -0,0 +1,155 @@
|
|||
{
|
||||
"type": "index",
|
||||
"value": {
|
||||
"aliases": {
|
||||
},
|
||||
"index": "sample-01",
|
||||
"mappings": {
|
||||
"_data_stream_timestamp": {
|
||||
"enabled": true
|
||||
},
|
||||
"properties": {
|
||||
"@timestamp": {
|
||||
"type": "date"
|
||||
},
|
||||
"kubernetes": {
|
||||
"properties": {
|
||||
"container": {
|
||||
"properties": {
|
||||
"cpu": {
|
||||
"properties": {
|
||||
"usage": {
|
||||
"properties": {
|
||||
"core": {
|
||||
"properties": {
|
||||
"ns": {
|
||||
"type": "long"
|
||||
}
|
||||
}
|
||||
},
|
||||
"limit": {
|
||||
"properties": {
|
||||
"pct": {
|
||||
"type": "float"
|
||||
}
|
||||
}
|
||||
},
|
||||
"nanocores": {
|
||||
"time_series_metric": "gauge",
|
||||
"type": "long"
|
||||
},
|
||||
"node": {
|
||||
"properties": {
|
||||
"pct": {
|
||||
"type": "float"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"memory": {
|
||||
"properties": {
|
||||
"available": {
|
||||
"properties": {
|
||||
"bytes": {
|
||||
"time_series_metric": "gauge",
|
||||
"type": "long"
|
||||
}
|
||||
}
|
||||
},
|
||||
"majorpagefaults": {
|
||||
"type": "long"
|
||||
},
|
||||
"pagefaults": {
|
||||
"time_series_metric": "gauge",
|
||||
"type": "long"
|
||||
},
|
||||
"rss": {
|
||||
"properties": {
|
||||
"bytes": {
|
||||
"time_series_metric": "gauge",
|
||||
"type": "long"
|
||||
}
|
||||
}
|
||||
},
|
||||
"usage": {
|
||||
"properties": {
|
||||
"bytes": {
|
||||
"time_series_metric": "gauge",
|
||||
"type": "long"
|
||||
},
|
||||
"limit": {
|
||||
"properties": {
|
||||
"pct": {
|
||||
"type": "float"
|
||||
}
|
||||
}
|
||||
},
|
||||
"node": {
|
||||
"properties": {
|
||||
"pct": {
|
||||
"type": "float"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"workingset": {
|
||||
"properties": {
|
||||
"bytes": {
|
||||
"time_series_metric": "gauge",
|
||||
"type": "long"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"name": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"start_time": {
|
||||
"type": "date"
|
||||
}
|
||||
}
|
||||
},
|
||||
"host": {
|
||||
"time_series_dimension": true,
|
||||
"type": "keyword"
|
||||
},
|
||||
"namespace": {
|
||||
"time_series_dimension": true,
|
||||
"type": "keyword"
|
||||
},
|
||||
"node": {
|
||||
"time_series_dimension": true,
|
||||
"type": "keyword"
|
||||
},
|
||||
"pod": {
|
||||
"time_series_dimension": true,
|
||||
"type": "keyword"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"index": {
|
||||
"mode": "time_series",
|
||||
"number_of_replicas": "0",
|
||||
"number_of_shards": "2",
|
||||
"routing_path": [
|
||||
"kubernetes.namespace",
|
||||
"kubernetes.host",
|
||||
"kubernetes.node",
|
||||
"kubernetes.pod"
|
||||
],
|
||||
"time_series": {
|
||||
"end_time": "2022-06-30T23:59:59Z",
|
||||
"start_time": "2022-06-10T00:00:00Z"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,48 +0,0 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Lens App renders the editor frame 1`] = `
|
||||
Array [
|
||||
Array [
|
||||
Object {
|
||||
"indexPatternService": Object {
|
||||
"ensureIndexPattern": [Function],
|
||||
"getDefaultIndex": [Function],
|
||||
"loadIndexPatternRefs": [Function],
|
||||
"loadIndexPatterns": [Function],
|
||||
"refreshExistingFields": [Function],
|
||||
"replaceDataViewId": [Function],
|
||||
"updateDataViewsState": [Function],
|
||||
},
|
||||
"lensInspector": Object {
|
||||
"adapters": Object {
|
||||
"expression": ExpressionsInspectorAdapter {
|
||||
"_ast": Object {},
|
||||
"_events": Object {},
|
||||
"_eventsCount": 0,
|
||||
"_maxListeners": undefined,
|
||||
Symbol(kCapture): false,
|
||||
},
|
||||
"requests": RequestAdapter {
|
||||
"_events": Object {},
|
||||
"_eventsCount": 0,
|
||||
"_maxListeners": undefined,
|
||||
"requests": Map {},
|
||||
Symbol(kCapture): false,
|
||||
},
|
||||
"tables": TablesAdapter {
|
||||
"_events": Object {},
|
||||
"_eventsCount": 0,
|
||||
"_maxListeners": undefined,
|
||||
"_tables": Object {},
|
||||
Symbol(kCapture): false,
|
||||
},
|
||||
},
|
||||
"close": [MockFunction],
|
||||
"inspect": [MockFunction],
|
||||
},
|
||||
"showNoDataPopover": [Function],
|
||||
},
|
||||
Object {},
|
||||
],
|
||||
]
|
||||
`;
|
|
@ -141,7 +141,22 @@ describe('Lens App', () => {
|
|||
|
||||
it('renders the editor frame', async () => {
|
||||
const { frame } = await mountWith({});
|
||||
expect(frame.EditorFrameContainer.mock.calls).toMatchSnapshot();
|
||||
expect(frame.EditorFrameContainer).toHaveBeenLastCalledWith(
|
||||
{
|
||||
indexPatternService: expect.any(Object),
|
||||
lensInspector: {
|
||||
adapters: {
|
||||
expression: expect.any(Object),
|
||||
requests: expect.any(Object),
|
||||
tables: expect.any(Object),
|
||||
},
|
||||
close: expect.any(Function),
|
||||
inspect: expect.any(Function),
|
||||
},
|
||||
showNoDataPopover: expect.any(Function),
|
||||
},
|
||||
{}
|
||||
);
|
||||
});
|
||||
|
||||
it('updates global filters with store state', async () => {
|
||||
|
|
|
@ -29,7 +29,7 @@ import {
|
|||
TimelineTabs,
|
||||
} from '../../../../common/types/timeline';
|
||||
import type { ISearchStart } from '@kbn/data-plugin/public';
|
||||
import { dataPluginMock } from '@kbn/data-plugin/public/mocks';
|
||||
import { searchServiceMock } from '@kbn/data-plugin/public/search/mocks';
|
||||
import { getTimelineTemplate } from '../../../timelines/containers/api';
|
||||
import { defaultHeaders } from '../../../timelines/components/timeline/body/column_headers/default_headers';
|
||||
import { KibanaServices } from '../../../common/lib/kibana';
|
||||
|
@ -239,11 +239,8 @@ describe('alert actions', () => {
|
|||
});
|
||||
|
||||
searchStrategyClient = {
|
||||
...dataPluginMock.createStartContract().search,
|
||||
aggs: {} as ISearchStart['aggs'],
|
||||
showError: jest.fn(),
|
||||
...searchServiceMock.createStartContract(),
|
||||
search: jest.fn().mockImplementation(() => of({ data: mockTimelineDetails })),
|
||||
searchSource: {} as ISearchStart['searchSource'],
|
||||
};
|
||||
|
||||
(getTimelineTemplate as jest.Mock).mockResolvedValue(mockTimelineResult);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue