Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 5 commits into from
Aug 20, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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!');
});
});
}