mirror of
https://github.com/elastic/kibana.git
synced 2025-06-27 18:51:07 -04:00
[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
9 changed files with 151 additions and 10 deletions
|
@ -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. |
|
| [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) |
|
| [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. |
|
| [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);
|
setSelectedNumericField(fields?.length ? getNumeric(fields)[0] : null);
|
||||||
}, [fields]);
|
}, [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;
|
if (!indexPattern || !selectedNumericField) return;
|
||||||
|
|
||||||
// Construct the query portion of the search request
|
// Construct the query portion of the search request
|
||||||
const query = data.query.getEsQuery(indexPattern);
|
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.
|
// 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: 'avg', params: { field: selectedNumericField!.name } }];
|
||||||
const aggsDsl = data.search.aggs.createAggConfigs(indexPattern, aggs).toDsl();
|
const aggsDsl = data.search.aggs.createAggConfigs(indexPattern, aggs).toDsl();
|
||||||
|
@ -193,14 +227,23 @@ export const SearchExamplesApp = ({
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
searchSubscription$.unsubscribe();
|
searchSubscription$.unsubscribe();
|
||||||
|
if (res.warning) {
|
||||||
|
notifications.toasts.addWarning({
|
||||||
|
title: 'Warning',
|
||||||
|
text: mountReactNode(res.warning),
|
||||||
|
});
|
||||||
|
}
|
||||||
} else if (isErrorResponse(res)) {
|
} else if (isErrorResponse(res)) {
|
||||||
// TODO: Make response error status clearer
|
// TODO: Make response error status clearer
|
||||||
notifications.toasts.addWarning('An error has occurred');
|
notifications.toasts.addDanger('An error has occurred');
|
||||||
searchSubscription$.unsubscribe();
|
searchSubscription$.unsubscribe();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
error: () => {
|
error: (e) => {
|
||||||
notifications.toasts.addDanger('Failed to run search');
|
notifications.toasts.addDanger({
|
||||||
|
title: 'Failed to run search',
|
||||||
|
text: e.message,
|
||||||
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -270,6 +313,14 @@ export const SearchExamplesApp = ({
|
||||||
doAsyncSearch('myStrategy');
|
doAsyncSearch('myStrategy');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const onWarningSearchClickHandler = () => {
|
||||||
|
doAsyncSearch(undefined, undefined, true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onErrorSearchClickHandler = () => {
|
||||||
|
doAsyncSearch(undefined, undefined, false, true);
|
||||||
|
};
|
||||||
|
|
||||||
const onPartialResultsClickHandler = () => {
|
const onPartialResultsClickHandler = () => {
|
||||||
setSelectedTab(1);
|
setSelectedTab(1);
|
||||||
const req = {
|
const req = {
|
||||||
|
@ -299,8 +350,11 @@ export const SearchExamplesApp = ({
|
||||||
searchSubscription$.unsubscribe();
|
searchSubscription$.unsubscribe();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
error: () => {
|
error: (e) => {
|
||||||
notifications.toasts.addDanger('Failed to run search');
|
notifications.toasts.addDanger({
|
||||||
|
title: 'Failed to run search',
|
||||||
|
text: e.message,
|
||||||
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -530,6 +584,38 @@ export const SearchExamplesApp = ({
|
||||||
</EuiText>
|
</EuiText>
|
||||||
</EuiText>
|
</EuiText>
|
||||||
<EuiSpacer />
|
<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">
|
<EuiTitle size="xs">
|
||||||
<h3>Handling partial results</h3>
|
<h3>Handling partial results</h3>
|
||||||
</EuiTitle>
|
</EuiTitle>
|
||||||
|
|
|
@ -70,6 +70,11 @@ export interface IKibanaSearchResponse<RawResponse = any> {
|
||||||
*/
|
*/
|
||||||
isRestored?: boolean;
|
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)
|
* 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;
|
loaded?: number;
|
||||||
rawResponse: RawResponse;
|
rawResponse: RawResponse;
|
||||||
total?: number;
|
total?: number;
|
||||||
|
warning?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Warning: (ae-forgotten-export) The symbol "MetricAggType" needs to be exported by the entry point index.d.ts
|
// 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 '..';
|
import { SearchRequest } from '..';
|
||||||
|
|
||||||
export function handleResponse(request: SearchRequest, response: IKibanaSearchResponse) {
|
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) {
|
if (rawResponse.timed_out) {
|
||||||
getNotifications().toasts.addWarning({
|
getNotifications().toasts.addWarning({
|
||||||
title: i18n.translate('data.search.searchSource.fetch.requestTimedOutNotificationMessage', {
|
title: i18n.translate('data.search.searchSource.fetch.requestTimedOutNotificationMessage', {
|
||||||
|
|
|
@ -71,12 +71,14 @@ export const enhancedEsSearchStrategyProvider = (
|
||||||
const promise = id
|
const promise = id
|
||||||
? client.asyncSearch.get({ ...params, id })
|
? client.asyncSearch.get({ ...params, id })
|
||||||
: client.asyncSearch.submit(params);
|
: 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);
|
const response = shimHitsTotal(body.response, options);
|
||||||
|
|
||||||
return toAsyncKibanaSearchResponse(
|
return toAsyncKibanaSearchResponse(
|
||||||
// @ts-expect-error @elastic/elasticsearch start_time_in_millis expected to be number
|
// @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`).
|
* Get the Kibana representation of an async search response (see `IKibanaSearchResponse`).
|
||||||
*/
|
*/
|
||||||
export function toAsyncKibanaSearchResponse(response: AsyncSearchResponse) {
|
export function toAsyncKibanaSearchResponse(response: AsyncSearchResponse, warning?: string) {
|
||||||
return {
|
return {
|
||||||
id: response.id,
|
id: response.id,
|
||||||
rawResponse: response.response,
|
rawResponse: response.response,
|
||||||
isPartial: response.is_partial,
|
isPartial: response.is_partial,
|
||||||
isRunning: response.is_running,
|
isRunning: response.is_running,
|
||||||
|
...(warning ? { warning } : {}),
|
||||||
...getTotalLoaded(response.response),
|
...getTotalLoaded(response.response),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,7 @@
|
||||||
* 2.0.
|
* 2.0.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import expect from '@kbn/expect';
|
||||||
import { FtrProviderContext } from '../../functional/ftr_provider_context';
|
import { FtrProviderContext } from '../../functional/ftr_provider_context';
|
||||||
|
|
||||||
// eslint-disable-next-line import/no-default-export
|
// eslint-disable-next-line import/no-default-export
|
||||||
|
@ -13,6 +14,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
||||||
const PageObjects = getPageObjects(['common', 'timePicker']);
|
const PageObjects = getPageObjects(['common', 'timePicker']);
|
||||||
const retry = getService('retry');
|
const retry = getService('retry');
|
||||||
const comboBox = getService('comboBox');
|
const comboBox = getService('comboBox');
|
||||||
|
const toasts = getService('toasts');
|
||||||
|
|
||||||
describe('Search session example', () => {
|
describe('Search session example', () => {
|
||||||
const appId = 'searchExamples';
|
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 () => {
|
it('should have an other bucket', async () => {
|
||||||
await testSubjects.click('searchSourceWithOther');
|
await testSubjects.click('searchSourceWithOther');
|
||||||
await testSubjects.click('responseTab');
|
await testSubjects.click('responseTab');
|
||||||
|
@ -53,5 +62,17 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
||||||
return buckets.length === 2;
|
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…
Add table
Add a link
Reference in a new issue