mirror of
https://github.com/elastic/kibana.git
synced 2025-06-27 18:51:07 -04:00
[Search] Add cancelation logic to search example (#118176)
This commit is contained in:
parent
6d20bf39fd
commit
b8a7370156
3 changed files with 136 additions and 42 deletions
|
@ -129,6 +129,12 @@ setTimeout(() => {
|
|||
}, 1000);
|
||||
```
|
||||
|
||||
<DocCallOut color="danger" title="Cancel your searches if results are no longer needed">
|
||||
Users might no longer be interested in search results. For example, they might start a new search
|
||||
or leave your app without waiting for the results. You should handle such cases by using
|
||||
`AbortController` with search API.
|
||||
</DocCallOut>
|
||||
|
||||
#### Search strategies
|
||||
|
||||
By default, the search service uses the DSL query and aggregation syntax and returns the response from Elasticsearch as is. It also provides several additional basic strategies, such as Async DSL (`x-pack` default) and EQL.
|
||||
|
|
|
@ -47,6 +47,7 @@ import {
|
|||
isErrorResponse,
|
||||
} from '../../../../src/plugins/data/public';
|
||||
import { IMyStrategyResponse } from '../../common/types';
|
||||
import { AbortError } from '../../../../src/plugins/kibana_utils/common';
|
||||
|
||||
interface SearchExamplesAppDeps {
|
||||
notifications: CoreStart['notifications'];
|
||||
|
@ -102,6 +103,8 @@ export const SearchExamplesApp = ({
|
|||
IndexPatternField | null | undefined
|
||||
>();
|
||||
const [request, setRequest] = useState<Record<string, any>>({});
|
||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||
const [currentAbortController, setAbortController] = useState<AbortController>();
|
||||
const [rawResponse, setRawResponse] = useState<Record<string, any>>({});
|
||||
const [selectedTab, setSelectedTab] = useState(0);
|
||||
|
||||
|
@ -187,16 +190,23 @@ export const SearchExamplesApp = ({
|
|||
...(strategy ? { get_cool: getCool } : {}),
|
||||
};
|
||||
|
||||
const abortController = new AbortController();
|
||||
setAbortController(abortController);
|
||||
|
||||
// Submit the search request using the `data.search` service.
|
||||
setRequest(req.params.body);
|
||||
const searchSubscription$ = data.search
|
||||
setIsLoading(true);
|
||||
|
||||
data.search
|
||||
.search(req, {
|
||||
strategy,
|
||||
sessionId,
|
||||
abortSignal: abortController.signal,
|
||||
})
|
||||
.subscribe({
|
||||
next: (res) => {
|
||||
if (isCompleteResponse(res)) {
|
||||
setIsLoading(false);
|
||||
setResponse(res);
|
||||
const avgResult: number | undefined = res.rawResponse.aggregations
|
||||
? // @ts-expect-error @elastic/elasticsearch no way to declare a type for aggregation in the search response
|
||||
|
@ -226,7 +236,6 @@ export const SearchExamplesApp = ({
|
|||
toastLifeTimeMs: 300000,
|
||||
}
|
||||
);
|
||||
searchSubscription$.unsubscribe();
|
||||
if (res.warning) {
|
||||
notifications.toasts.addWarning({
|
||||
title: 'Warning',
|
||||
|
@ -236,14 +245,20 @@ export const SearchExamplesApp = ({
|
|||
} else if (isErrorResponse(res)) {
|
||||
// TODO: Make response error status clearer
|
||||
notifications.toasts.addDanger('An error has occurred');
|
||||
searchSubscription$.unsubscribe();
|
||||
}
|
||||
},
|
||||
error: (e) => {
|
||||
notifications.toasts.addDanger({
|
||||
title: 'Failed to run search',
|
||||
text: e.message,
|
||||
});
|
||||
setIsLoading(false);
|
||||
if (e instanceof AbortError) {
|
||||
notifications.toasts.addWarning({
|
||||
title: e.message,
|
||||
});
|
||||
} else {
|
||||
notifications.toasts.addDanger({
|
||||
title: 'Failed to run search',
|
||||
text: e.message,
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
@ -286,7 +301,12 @@ export const SearchExamplesApp = ({
|
|||
}
|
||||
|
||||
setRequest(searchSource.getSearchRequestBody());
|
||||
const { rawResponse: res } = await searchSource.fetch$().toPromise();
|
||||
const abortController = new AbortController();
|
||||
setAbortController(abortController);
|
||||
setIsLoading(true);
|
||||
const { rawResponse: res } = await searchSource
|
||||
.fetch$({ abortSignal: abortController.signal })
|
||||
.toPromise();
|
||||
setRawResponse(res);
|
||||
|
||||
const message = <EuiText>Searched {res.hits.total} documents.</EuiText>;
|
||||
|
@ -301,7 +321,18 @@ export const SearchExamplesApp = ({
|
|||
);
|
||||
} catch (e) {
|
||||
setRawResponse(e.body);
|
||||
notifications.toasts.addWarning(`An error has occurred: ${e.message}`);
|
||||
if (e instanceof AbortError) {
|
||||
notifications.toasts.addWarning({
|
||||
title: e.message,
|
||||
});
|
||||
} else {
|
||||
notifications.toasts.addDanger({
|
||||
title: 'Failed to run search',
|
||||
text: e.message,
|
||||
});
|
||||
}
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -329,32 +360,44 @@ export const SearchExamplesApp = ({
|
|||
},
|
||||
};
|
||||
|
||||
const abortController = new AbortController();
|
||||
setAbortController(abortController);
|
||||
|
||||
// Submit the search request using the `data.search` service.
|
||||
setRequest(req.params);
|
||||
const searchSubscription$ = data.search
|
||||
setIsLoading(true);
|
||||
data.search
|
||||
.search(req, {
|
||||
strategy: 'fibonacciStrategy',
|
||||
abortSignal: abortController.signal,
|
||||
})
|
||||
.subscribe({
|
||||
next: (res) => {
|
||||
setResponse(res);
|
||||
if (isCompleteResponse(res)) {
|
||||
setIsLoading(false);
|
||||
notifications.toasts.addSuccess({
|
||||
title: 'Query result',
|
||||
text: 'Query finished',
|
||||
});
|
||||
searchSubscription$.unsubscribe();
|
||||
} else if (isErrorResponse(res)) {
|
||||
setIsLoading(false);
|
||||
// TODO: Make response error status clearer
|
||||
notifications.toasts.addWarning('An error has occurred');
|
||||
searchSubscription$.unsubscribe();
|
||||
}
|
||||
},
|
||||
error: (e) => {
|
||||
notifications.toasts.addDanger({
|
||||
title: 'Failed to run search',
|
||||
text: e.message,
|
||||
});
|
||||
setIsLoading(false);
|
||||
if (e instanceof AbortError) {
|
||||
notifications.toasts.addWarning({
|
||||
title: e.message,
|
||||
});
|
||||
} else {
|
||||
notifications.toasts.addDanger({
|
||||
title: 'Failed to run search',
|
||||
text: e.message,
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
@ -365,17 +408,32 @@ export const SearchExamplesApp = ({
|
|||
|
||||
const onServerClickHandler = async () => {
|
||||
if (!indexPattern || !selectedNumericField) return;
|
||||
const abortController = new AbortController();
|
||||
setAbortController(abortController);
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const res = await http.get(SERVER_SEARCH_ROUTE_PATH, {
|
||||
query: {
|
||||
index: indexPattern.title,
|
||||
field: selectedNumericField!.name,
|
||||
},
|
||||
signal: abortController.signal,
|
||||
});
|
||||
|
||||
notifications.toasts.addSuccess(`Server returned ${JSON.stringify(res)}`);
|
||||
} catch (e) {
|
||||
notifications.toasts.addDanger('Failed to run search');
|
||||
if (e?.name === 'AbortError') {
|
||||
notifications.toasts.addWarning({
|
||||
title: e.message,
|
||||
});
|
||||
} else {
|
||||
notifications.toasts.addDanger({
|
||||
title: 'Failed to run search',
|
||||
text: e.message,
|
||||
});
|
||||
}
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -721,6 +779,11 @@ export const SearchExamplesApp = ({
|
|||
strategy. This request does not take the configuration of{' '}
|
||||
<EuiCode>TopNavMenu</EuiCode> into account, but you could pass those down to the
|
||||
server as well.
|
||||
<br />
|
||||
When executing search on the server, make sure to cancel the search in case user
|
||||
cancels corresponding network request. This could happen in case user re-runs a
|
||||
query or leaves the page without waiting for the result. Cancellation API is similar
|
||||
on client and server and use `AbortController`.
|
||||
<EuiSpacer />
|
||||
<EuiButtonEmpty size="xs" onClick={onServerClickHandler} iconType="play">
|
||||
<FormattedMessage
|
||||
|
@ -737,6 +800,15 @@ export const SearchExamplesApp = ({
|
|||
selectedTab={reqTabs[selectedTab]}
|
||||
onTabClick={(tab) => setSelectedTab(reqTabs.indexOf(tab))}
|
||||
/>
|
||||
<EuiSpacer />
|
||||
{currentAbortController && isLoading && (
|
||||
<EuiButtonEmpty size="xs" onClick={() => currentAbortController?.abort()}>
|
||||
<FormattedMessage
|
||||
id="searchExamples.abortButtonText"
|
||||
defaultMessage="Abort request"
|
||||
/>
|
||||
</EuiButtonEmpty>
|
||||
)}
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGrid>
|
||||
</EuiPageContentBody>
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { Observable } from 'rxjs';
|
||||
import { IEsSearchRequest } from 'src/plugins/data/server';
|
||||
import { schema } from '@kbn/config-schema';
|
||||
import { IEsSearchResponse } from 'src/plugins/data/common';
|
||||
|
@ -26,36 +27,51 @@ export function registerServerSearchRoute(router: IRouter<DataRequestHandlerCont
|
|||
},
|
||||
async (context, request, response) => {
|
||||
const { index, field } = request.query;
|
||||
// Run a synchronous search server side, by enforcing a high keepalive and waiting for completion.
|
||||
// If you wish to run the search with polling (in basic+), you'd have to poll on the search API.
|
||||
// Please reach out to the @app-arch-team if you need this to be implemented.
|
||||
const res = await context
|
||||
.search!.search(
|
||||
{
|
||||
params: {
|
||||
index,
|
||||
body: {
|
||||
aggs: {
|
||||
'1': {
|
||||
avg: {
|
||||
field,
|
||||
|
||||
// User may abort the request without waiting for the results
|
||||
// we need to handle this scenario by aborting underlying server requests
|
||||
const abortSignal = getRequestAbortedSignal(request.events.aborted$);
|
||||
|
||||
try {
|
||||
const res = await context
|
||||
.search!.search(
|
||||
{
|
||||
params: {
|
||||
index,
|
||||
body: {
|
||||
aggs: {
|
||||
'1': {
|
||||
avg: {
|
||||
field,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
waitForCompletionTimeout: '5m',
|
||||
keepAlive: '5m',
|
||||
},
|
||||
} as IEsSearchRequest,
|
||||
{}
|
||||
)
|
||||
.toPromise();
|
||||
} as IEsSearchRequest,
|
||||
{ abortSignal }
|
||||
)
|
||||
.toPromise();
|
||||
|
||||
return response.ok({
|
||||
body: {
|
||||
aggs: (res as IEsSearchResponse).rawResponse.aggregations,
|
||||
},
|
||||
});
|
||||
return response.ok({
|
||||
body: {
|
||||
aggs: (res as IEsSearchResponse).rawResponse.aggregations,
|
||||
},
|
||||
});
|
||||
} catch (e) {
|
||||
return response.customError({
|
||||
statusCode: e.statusCode ?? 500,
|
||||
body: {
|
||||
message: e.message,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function getRequestAbortedSignal(aborted$: Observable<void>): AbortSignal {
|
||||
const controller = new AbortController();
|
||||
aborted$.subscribe(() => controller.abort());
|
||||
return controller.signal;
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue