Skip to content

Commit

Permalink
[data.search] Handle warnings inside of headers (#103744) (#109298)
Browse files Browse the repository at this point in the history
* [data.search] Handle warnings inside of headers

* Update docs

* Add tests

* Remove isWarningResponse

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
  • Loading branch information
lukasolson and kibanamachine authored Aug 20, 2021
1 parent fb8af4d commit f087c75
Show file tree
Hide file tree
Showing 9 changed files with 151 additions and 10 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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 |

Original file line number Diff line number Diff line change
@@ -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;
```
98 changes: 92 additions & 6 deletions examples/search_examples/public/search/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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,
});
},
});
};
Expand Down Expand Up @@ -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 = {
Expand Down Expand Up @@ -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,
});
},
});
};
Expand Down Expand Up @@ -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>
Expand Down
5 changes: 5 additions & 0 deletions src/plugins/data/common/search/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
*/
Expand Down
1 change: 1 addition & 0 deletions src/plugins/data/public/public.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -1149,6 +1149,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
Expand Down
13 changes: 12 additions & 1 deletion src/plugins/data/public/search/fetch/handle_response.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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', {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
);
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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),
};
}
21 changes: 21 additions & 0 deletions x-pack/test/examples/search_examples/search_example.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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';
Expand All @@ -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');
Expand All @@ -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!');
});
});
}

0 comments on commit f087c75

Please sign in to comment.