[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:
parent
82af747532
commit
6a39dc1880
|
@ -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 |
|
||||
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IKibanaSearchResponse](./kibana-plugin-plugins-data-public.ikibanasearchresponse.md) > [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;
|
||||
```
|
|
@ -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>
|
||||
|
|
|
@ -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)
|
||||
*/
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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', {
|
||||
|
|
|
@ -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
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -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),
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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!');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue