[data.search] Handle warnings inside of headers (#103744)

* [data.search] Handle warnings inside of headers

* Update docs

* Add tests

* Remove isWarningResponse

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Lukas Olson 2021-08-16 21:06:19 -07:00 committed by GitHub
parent 82af747532
commit 6a39dc1880
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 151 additions and 10 deletions

View file

@ -21,4 +21,5 @@ export interface IKibanaSearchResponse<RawResponse = any>
| [loaded](./kibana-plugin-plugins-data-public.ikibanasearchresponse.loaded.md) | <code>number</code> | If relevant to the search strategy, return a loaded number that represents how progress is indicated. |
| [rawResponse](./kibana-plugin-plugins-data-public.ikibanasearchresponse.rawresponse.md) | <code>RawResponse</code> | The raw response returned by the internal search method (usually the raw ES response) |
| [total](./kibana-plugin-plugins-data-public.ikibanasearchresponse.total.md) | <code>number</code> | If relevant to the search strategy, return a total number that represents how progress is indicated. |
| [warning](./kibana-plugin-plugins-data-public.ikibanasearchresponse.warning.md) | <code>string</code> | Optional warnings that should be surfaced to the end user |

View file

@ -0,0 +1,13 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) &gt; [IKibanaSearchResponse](./kibana-plugin-plugins-data-public.ikibanasearchresponse.md) &gt; [warning](./kibana-plugin-plugins-data-public.ikibanasearchresponse.warning.md)
## IKibanaSearchResponse.warning property
Optional warnings that should be surfaced to the end user
<b>Signature:</b>
```typescript
warning?: string;
```

View file

@ -131,12 +131,46 @@ export const SearchExamplesApp = ({
setSelectedNumericField(fields?.length ? getNumeric(fields)[0] : null);
}, [fields]);
const doAsyncSearch = async (strategy?: string, sessionId?: string) => {
const doAsyncSearch = async (
strategy?: string,
sessionId?: string,
addWarning: boolean = false,
addError: boolean = false
) => {
if (!indexPattern || !selectedNumericField) return;
// Construct the query portion of the search request
const query = data.query.getEsQuery(indexPattern);
if (addWarning) {
query.bool.must.push({
// @ts-ignore
error_query: {
indices: [
{
name: indexPattern.title,
error_type: 'warning',
message: 'Watch out!',
},
],
},
});
}
if (addError) {
query.bool.must.push({
// @ts-ignore
error_query: {
indices: [
{
name: indexPattern.title,
error_type: 'exception',
message: 'Watch out!',
},
],
},
});
}
// Construct the aggregations portion of the search request by using the `data.search.aggs` service.
const aggs = [{ type: 'avg', params: { field: selectedNumericField!.name } }];
const aggsDsl = data.search.aggs.createAggConfigs(indexPattern, aggs).toDsl();
@ -193,14 +227,23 @@ export const SearchExamplesApp = ({
}
);
searchSubscription$.unsubscribe();
if (res.warning) {
notifications.toasts.addWarning({
title: 'Warning',
text: mountReactNode(res.warning),
});
}
} else if (isErrorResponse(res)) {
// TODO: Make response error status clearer
notifications.toasts.addWarning('An error has occurred');
notifications.toasts.addDanger('An error has occurred');
searchSubscription$.unsubscribe();
}
},
error: () => {
notifications.toasts.addDanger('Failed to run search');
error: (e) => {
notifications.toasts.addDanger({
title: 'Failed to run search',
text: e.message,
});
},
});
};
@ -270,6 +313,14 @@ export const SearchExamplesApp = ({
doAsyncSearch('myStrategy');
};
const onWarningSearchClickHandler = () => {
doAsyncSearch(undefined, undefined, true);
};
const onErrorSearchClickHandler = () => {
doAsyncSearch(undefined, undefined, false, true);
};
const onPartialResultsClickHandler = () => {
setSelectedTab(1);
const req = {
@ -299,8 +350,11 @@ export const SearchExamplesApp = ({
searchSubscription$.unsubscribe();
}
},
error: () => {
notifications.toasts.addDanger('Failed to run search');
error: (e) => {
notifications.toasts.addDanger({
title: 'Failed to run search',
text: e.message,
});
},
});
};
@ -530,6 +584,38 @@ export const SearchExamplesApp = ({
</EuiText>
</EuiText>
<EuiSpacer />
<EuiTitle size="xs">
<h3>Handling errors & warnings</h3>
</EuiTitle>
<EuiText>
When fetching data from Elasticsearch, there are several different ways warnings and
errors may be returned. In general, it is recommended to surface these in the UX.
<EuiSpacer />
<EuiButtonEmpty
size="xs"
onClick={onWarningSearchClickHandler}
iconType="play"
data-test-subj="searchWithWarning"
>
<FormattedMessage
id="searchExamples.searchWithWarningButtonText"
defaultMessage="Request with a warning in response"
/>
</EuiButtonEmpty>
<EuiText />
<EuiButtonEmpty
size="xs"
onClick={onErrorSearchClickHandler}
iconType="play"
data-test-subj="searchWithError"
>
<FormattedMessage
id="searchExamples.searchWithErrorButtonText"
defaultMessage="Request with an error in response"
/>
</EuiButtonEmpty>
</EuiText>
<EuiSpacer />
<EuiTitle size="xs">
<h3>Handling partial results</h3>
</EuiTitle>

View file

@ -70,6 +70,11 @@ export interface IKibanaSearchResponse<RawResponse = any> {
*/
isRestored?: boolean;
/**
* Optional warnings that should be surfaced to the end user
*/
warning?: string;
/**
* The raw response returned by the internal search method (usually the raw ES response)
*/

View file

@ -1147,6 +1147,7 @@ export interface IKibanaSearchResponse<RawResponse = any> {
loaded?: number;
rawResponse: RawResponse;
total?: number;
warning?: string;
}
// Warning: (ae-forgotten-export) The symbol "MetricAggType" needs to be exported by the entry point index.d.ts

View file

@ -16,7 +16,18 @@ import { getNotifications } from '../../services';
import { SearchRequest } from '..';
export function handleResponse(request: SearchRequest, response: IKibanaSearchResponse) {
const { rawResponse } = response;
const { rawResponse, warning } = response;
if (warning) {
getNotifications().toasts.addWarning({
title: i18n.translate('data.search.searchSource.fetch.warningMessage', {
defaultMessage: 'Warning: {warning}',
values: {
warning,
},
}),
});
}
if (rawResponse.timed_out) {
getNotifications().toasts.addWarning({
title: i18n.translate('data.search.searchSource.fetch.requestTimedOutNotificationMessage', {

View file

@ -71,12 +71,14 @@ export const enhancedEsSearchStrategyProvider = (
const promise = id
? client.asyncSearch.get({ ...params, id })
: client.asyncSearch.submit(params);
const { body } = await shimAbortSignal(promise, options.abortSignal);
const { body, headers } = await shimAbortSignal(promise, options.abortSignal);
const response = shimHitsTotal(body.response, options);
return toAsyncKibanaSearchResponse(
// @ts-expect-error @elastic/elasticsearch start_time_in_millis expected to be number
{ ...body, response }
{ ...body, response },
headers?.warning
);
};

View file

@ -12,12 +12,13 @@ import { getTotalLoaded } from '../es_search';
/**
* Get the Kibana representation of an async search response (see `IKibanaSearchResponse`).
*/
export function toAsyncKibanaSearchResponse(response: AsyncSearchResponse) {
export function toAsyncKibanaSearchResponse(response: AsyncSearchResponse, warning?: string) {
return {
id: response.id,
rawResponse: response.response,
isPartial: response.is_partial,
isRunning: response.is_running,
...(warning ? { warning } : {}),
...getTotalLoaded(response.response),
};
}

View file

@ -5,6 +5,7 @@
* 2.0.
*/
import expect from '@kbn/expect';
import { FtrProviderContext } from '../../functional/ftr_provider_context';
// eslint-disable-next-line import/no-default-export
@ -13,6 +14,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
const PageObjects = getPageObjects(['common', 'timePicker']);
const retry = getService('retry');
const comboBox = getService('comboBox');
const toasts = getService('toasts');
describe('Search session example', () => {
const appId = 'searchExamples';
@ -28,6 +30,13 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
);
});
beforeEach(async () => {
await toasts.dismissAllToasts();
await retry.waitFor('toasts gone', async () => {
return (await toasts.getToastCount()) === 0;
});
});
it('should have an other bucket', async () => {
await testSubjects.click('searchSourceWithOther');
await testSubjects.click('responseTab');
@ -53,5 +62,17 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
return buckets.length === 2;
});
});
it('should handle warnings', async () => {
await testSubjects.click('searchWithWarning');
await retry.waitFor('', async () => {
const toastCount = await toasts.getToastCount();
return toastCount > 1;
});
const warningToast = await toasts.getToastElement(2);
const textEl = await warningToast.findByClassName('euiToastBody');
const text: string = await textEl.getVisibleText();
expect(text).to.contain('Watch out!');
});
});
}