kibana/examples/search_examples/public/search/app.tsx
Alejandro Fernández Haro 52ab19db2d
Upgrade ES client to 9.0.0-alpha.3 (#208776)
## Summary

Updating the ES client to 9.0. 

Resolves #116102

## What changes?

**Breaking change**: `body` has been removed.

Most of the changes are about bringing all the content inside the body
as a root attribute to the API params:

```diff
const response = await client.search({
  index: 'test',
-  body: {
    query: {
      match_all: {}
    }
-  }
})
```

For this reason, enabling the "Hide whitespace changes" option when
reviewing is recommended.

Some exceptions to this rule:

* Bulk APIs replace the `body` array with `operations` array (direct
replacement)
* Index Put Settings API replace `body` array with `settings` (direct
replacement)
* Msearch replaces the `body` array with `searches` array (direct
replacement)
* Document Index API replaces `body` with `document` (direct
replacement)
* Create Repository replaces `body` with `repository` (direct
replacement)

Because of a known issue in the client
(https://github.com/elastic/elasticsearch-js/issues/2584), there's still
an escape hatch to send data in the body in case the specific use case
requires it via `// @ts-expect-error elasticsearch@9.0.0
https://github.com/elastic/elasticsearch-js/issues/2584`, but it
shouldn't be abused because we lose types. In this PR we've used it in
those scenarios where we reuse the response of a GET as the body of a
PUT/POST.

### Other changes

* `estypes` can be imported from the root of the library as `import type
{ estypes } from '@elastic/elasticsearch';`
* `estypesWithBody` have been removed
* `requestTimeout`'s 30s default has been removed in the client. This PR
explicitly adds the setting in all client usages.


### Identify risks

- [x] The client places unknown properties as querystring, risking body
params leaking there, and causing 400 errors from ES => Solved by
forcing `body` usage there via `// @ts-expect-error elasticsearch@9.0.0
https://github.com/elastic/elasticsearch-js/issues/2584`. The next
version of the client will address this.
- [x] We need to run the MKI tests to make sure that we're not breaking
anything there =>
https://elastic.slack.com/archives/C04HT4P1YS3/p1739528112482629?thread_ts=1739480136.231439&cid=C04HT4P1YS3

---------

Co-authored-by: Gloria Hornero <gloria.hornero@elastic.co>
2025-02-25 14:37:23 +00:00

838 lines
29 KiB
TypeScript

/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import {
EuiButtonEmpty,
EuiCheckbox,
EuiCode,
EuiCodeBlock,
EuiComboBox,
EuiFieldNumber,
EuiFlexGrid,
EuiFlexItem,
EuiFormLabel,
EuiPageTemplate,
EuiProgress,
EuiSpacer,
EuiTabbedContent,
EuiTabbedContentTab,
EuiText,
EuiTitle,
} from '@elastic/eui';
import { CoreStart } from '@kbn/core/public';
import { IInspectorInfo } from '@kbn/data-plugin/common';
import { DataPublicPluginStart, isRunningResponse } from '@kbn/data-plugin/public';
import type { IKibanaSearchResponse } from '@kbn/search-types';
import type { SearchResponseWarning } from '@kbn/search-response-warnings';
import type { DataView, DataViewField } from '@kbn/data-views-plugin/public';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import { RequestAdapter } from '@kbn/inspector-plugin/common';
import { toMountPoint } from '@kbn/react-kibana-mount';
import { NavigationPublicPluginStart } from '@kbn/navigation-plugin/public';
import { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/public';
import React, { useEffect, useState } from 'react';
import { lastValueFrom } from 'rxjs';
import { PLUGIN_ID, PLUGIN_NAME, SERVER_SEARCH_ROUTE_PATH } from '../../common';
import { IMyStrategyResponse } from '../../common/types';
interface SearchExamplesAppDeps
extends Pick<
CoreStart,
'notifications' | 'http' | 'analytics' | 'i18n' | 'theme' | 'userProfile'
> {
navigation: NavigationPublicPluginStart;
data: DataPublicPluginStart;
unifiedSearch: UnifiedSearchPublicPluginStart;
}
function getNumeric(fields?: DataViewField[]) {
if (!fields) return [];
return fields?.filter((f) => f.type === 'number' && f.aggregatable);
}
function getAggregatableStrings(fields?: DataViewField[]) {
if (!fields) return [];
return fields?.filter((f) => f.type === 'string' && f.aggregatable);
}
function formatFieldToComboBox(field?: DataViewField | null) {
if (!field) return [];
return formatFieldsToComboBox([field]);
}
function formatFieldsToComboBox(fields?: DataViewField[]) {
if (!fields) return [];
return fields?.map((field) => {
return {
label: field.displayName || field.name,
};
});
}
const bucketAggType = 'terms';
const metricAggType = 'median';
export const SearchExamplesApp = ({
http,
notifications,
navigation,
data,
unifiedSearch,
...startServices
}: SearchExamplesAppDeps) => {
const { IndexPatternSelect } = unifiedSearch.ui;
const [getCool, setGetCool] = useState<boolean>(false);
const [fibonacciN, setFibonacciN] = useState<number>(10);
const [timeTook, setTimeTook] = useState<number | undefined>();
const [total, setTotal] = useState<number>(100);
const [loaded, setLoaded] = useState<number>(0);
const [dataView, setDataView] = useState<DataView | null>();
const [fields, setFields] = useState<DataViewField[]>();
const [selectedFields, setSelectedFields] = useState<DataViewField[]>([]);
const [selectedNumericField, setSelectedNumericField] = useState<
DataViewField | null | undefined
>();
const [selectedBucketField, setSelectedBucketField] = useState<
DataViewField | null | undefined
>();
const [isLoading, setIsLoading] = useState<boolean>(false);
const [currentAbortController, setAbortController] = useState<AbortController>();
const [request, setRequest] = useState<Record<string, any>>({});
const [rawResponse, setRawResponse] = useState<Record<string, any>>({});
const [warningContents, setWarningContents] = useState<SearchResponseWarning[]>([]);
const [selectedTab, setSelectedTab] = useState(0);
function setResponse(response: IKibanaSearchResponse) {
setWarningContents([]);
setRawResponse(response.rawResponse);
setLoaded(response.loaded!);
setTotal(response.total!);
setTimeTook(response.rawResponse.took);
}
// Fetch the default data view using the `data.dataViews` service, as the component is mounted.
useEffect(() => {
const setDefaultDataView = async () => {
const defaultDataView = await data.dataViews.getDefault();
setDataView(defaultDataView);
};
setDefaultDataView();
}, [data]);
// Update the fields list every time the data view is modified.
useEffect(() => {
setFields(dataView?.fields);
}, [dataView]);
useEffect(() => {
setSelectedBucketField(fields?.length ? getAggregatableStrings(fields)[0] : null);
setSelectedNumericField(fields?.length ? getNumeric(fields)[0] : null);
}, [fields]);
const doAsyncSearch = async (
strategy?: string,
sessionId?: string,
addWarning: boolean = false,
addError: boolean = false
) => {
if (!dataView || !selectedNumericField) return;
// Construct the query portion of the search request
const query = data.query.getEsQuery(dataView);
if (addWarning) {
query.bool.must.push({
// @ts-ignore
error_query: {
indices: [
{
name: dataView.title,
error_type: 'warning',
message: 'Watch out!',
},
],
},
});
}
if (addError) {
query.bool.must.push({
// @ts-ignore
error_query: {
indices: [
{
name: dataView.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: metricAggType, params: { field: selectedNumericField!.name } }];
const aggsDsl = data.search.aggs.createAggConfigs(dataView, aggs).toDsl();
const body = {
aggs: aggsDsl,
query,
};
const req = {
params: {
index: dataView.title,
...body,
},
// Add a custom request parameter to be consumed by `MyStrategy`.
...(strategy ? { get_cool: getCool } : {}),
};
const abortController = new AbortController();
setAbortController(abortController);
// Submit the search request using the `data.search` service.
setRequest(body);
setRawResponse({});
setWarningContents([]);
setIsLoading(true);
data.search
.search(req, {
strategy,
sessionId,
abortSignal: abortController.signal,
})
.subscribe({
next: (res) => {
if (!isRunningResponse(res)) {
setIsLoading(false);
setResponse(res);
const aggResult: number | undefined = res.rawResponse.aggregations
? // @ts-expect-error @elastic/elasticsearch no way to declare a type for aggregation in the search response
res.rawResponse.aggregations[1].value
: undefined;
const isCool = (res as IMyStrategyResponse).cool;
const executedAt = (res as IMyStrategyResponse).executed_at;
const message = (
<EuiText>
Searched {res.rawResponse.hits.total as number} documents. <br />
The ${metricAggType} of {selectedNumericField!.name} is{' '}
{aggResult ? Math.floor(aggResult) : 0}.
<br />
{isCool ? `Is it Cool? ${isCool}` : undefined}
<br />
<EuiText data-test-subj="requestExecutedAt">
{executedAt ? `Executed at? ${executedAt}` : undefined}
</EuiText>
</EuiText>
);
notifications.toasts.addSuccess(
{ title: 'Query result', text: toMountPoint(message, startServices) },
{ toastLifeTimeMs: 300000 }
);
if (res.warning) {
notifications.toasts.addWarning({
title: 'Warning',
text: toMountPoint(res.warning, startServices),
});
}
}
},
error: (e) => {
setIsLoading(false);
data.search.showError(e);
},
});
};
const doSearchSourceSearch = async (
otherBucket: boolean,
showWarningToastNotifications = true
) => {
if (!dataView) return;
const query = data.query.queryString.getQuery();
const filters = data.query.filterManager.getFilters();
const timefilter = data.query.timefilter.timefilter.createFilter(dataView);
if (timefilter) {
filters.push(timefilter);
}
try {
const searchSource = await data.search.searchSource.create();
searchSource
.setField('index', dataView)
.setField('filter', filters)
.setField('query', query)
.setField('fields', selectedFields.length ? selectedFields.map((f) => f.name) : [''])
.setField('size', selectedFields.length ? 100 : 0)
.setField('trackTotalHits', 100);
const aggDef = [];
if (selectedBucketField) {
aggDef.push({
type: bucketAggType,
schema: 'split',
params: { field: selectedBucketField.name, size: 2, otherBucket },
});
}
if (selectedNumericField) {
aggDef.push({ type: metricAggType, params: { field: selectedNumericField.name } });
}
if (aggDef.length > 0) {
const ac = data.search.aggs.createAggConfigs(dataView, aggDef);
searchSource.setField('aggs', ac);
}
setRequest(searchSource.getSearchRequestBody());
setRawResponse({});
setWarningContents([]);
const abortController = new AbortController();
const inspector: Required<IInspectorInfo> = {
adapter: new RequestAdapter(),
title: 'Example App Inspector!',
id: 'greatest-example-app-inspector',
description: 'Use the `description` field for more info about the inspector.',
};
setAbortController(abortController);
setIsLoading(true);
const result = await lastValueFrom(
searchSource.fetch$({
abortSignal: abortController.signal,
disableWarningToasts: !showWarningToastNotifications,
inspector,
})
);
setRawResponse(result.rawResponse);
/*
* Set disableWarningToasts to true to disable warning toasts and customize warning display.
* Then use showWarnings to customize warning notification.
*/
if (showWarningToastNotifications) {
setWarningContents([]);
} else {
const warnings: SearchResponseWarning[] = [];
data.search.showWarnings(inspector.adapter, (warning) => {
warnings.push(warning);
return false; // allow search service from showing this warning on its own
});
// click the warnings tab to see the warnings
setWarningContents(warnings);
}
const message = (
<EuiText>Searched {result.rawResponse.hits.total as number} documents.</EuiText>
);
notifications.toasts.addSuccess(
{
title: 'Query result',
text: toMountPoint(message, startServices),
},
{
toastLifeTimeMs: 300000,
}
);
} catch (e) {
setRawResponse(e.body);
data.search.showError(e);
} finally {
setIsLoading(false);
}
};
const onClickHandler = () => {
doAsyncSearch();
};
const onMyStrategyClickHandler = () => {
doAsyncSearch('myStrategy');
};
const onWarningSearchClickHandler = () => {
doAsyncSearch(undefined, undefined, true);
};
const onErrorSearchClickHandler = () => {
doAsyncSearch(undefined, undefined, false, true);
};
const onPartialResultsClickHandler = () => {
setSelectedTab(1);
const req = {
params: {
n: fibonacciN,
},
};
const abortController = new AbortController();
setAbortController(abortController);
// Submit the search request using the `data.search` service.
setRequest(req.params);
setIsLoading(true);
data.search
.search(req, {
strategy: 'fibonacciStrategy',
abortSignal: abortController.signal,
})
.subscribe({
next: (res) => {
setResponse(res);
if (!isRunningResponse(res)) {
setIsLoading(false);
notifications.toasts.addSuccess({
title: 'Query result',
text: 'Query finished',
});
}
},
error: (e) => {
setIsLoading(false);
data.search.showError(e);
},
});
};
const onClientSideSessionCacheClickHandler = () => {
doAsyncSearch('myStrategy', data.search.session.getSessionId());
};
const onServerClickHandler = async () => {
if (!dataView || !selectedNumericField) return;
const abortController = new AbortController();
setAbortController(abortController);
setIsLoading(true);
try {
const res = await http.get(SERVER_SEARCH_ROUTE_PATH, {
query: {
index: dataView.title,
field: selectedNumericField!.name,
},
signal: abortController.signal,
});
notifications.toasts.addSuccess(`Server returned ${JSON.stringify(res)}`);
} catch (e) {
data.search.showError(e);
} finally {
setIsLoading(false);
}
};
const onSearchSourceClickHandler = (
withOtherBucket: boolean,
showWarningToastNotifications: boolean
) => {
doSearchSourceSearch(withOtherBucket, showWarningToastNotifications);
};
const reqTabs: EuiTabbedContentTab[] = [
{
id: 'request',
name: <EuiText data-test-subj="requestTab">Request</EuiText>,
content: (
<>
<EuiSpacer />
<EuiText size="xs">Search body sent to ES</EuiText>
<EuiCodeBlock
language="json"
fontSize="s"
paddingSize="s"
overflowHeight={450}
isCopyable
data-test-subj="requestCodeBlock"
>
{JSON.stringify(request, null, 2)}
</EuiCodeBlock>
</>
),
},
{
id: 'response',
name: <EuiText data-test-subj="responseTab">Response</EuiText>,
content: (
<>
<EuiSpacer />
<EuiText size="xs">
<FormattedMessage
id="searchExamples.timestampText"
defaultMessage="Took: {time} ms"
values={{ time: timeTook ?? 'Unknown' }}
/>
</EuiText>
<EuiProgress value={loaded} max={total} size="xs" data-test-subj="progressBar" />
<EuiCodeBlock
language="json"
fontSize="s"
paddingSize="s"
overflowHeight={450}
isCopyable
data-test-subj="responseCodeBlock"
>
{JSON.stringify(rawResponse, null, 2)}
</EuiCodeBlock>
</>
),
},
{
id: 'warnings',
name: <EuiText data-test-subj="warningsTab">Warnings</EuiText>,
content: (
<>
{' '}
<EuiSpacer />{' '}
<EuiText size="xs">
{' '}
<FormattedMessage
id="searchExamples.warningsObject"
defaultMessage="Search warnings may optionally be handed with search service showWarnings method."
/>{' '}
</EuiText>{' '}
<EuiProgress value={loaded} max={total} size="xs" data-test-subj="progressBar" />{' '}
<EuiCodeBlock
language="json"
fontSize="s"
paddingSize="s"
overflowHeight={450}
isCopyable
data-test-subj="warningsCodeBlock"
>
{' '}
{JSON.stringify(warningContents, null, 2)}{' '}
</EuiCodeBlock>{' '}
</>
),
},
];
return (
<>
<EuiPageTemplate.Header
pageTitle={i18n.translate('searchExamples.helloWorldText', {
defaultMessage: '{name}',
values: { name: PLUGIN_NAME },
})}
/>
<EuiPageTemplate.Section grow={false}>
<navigation.ui.TopNavMenu
appName={PLUGIN_ID}
showSearchBar={true}
useDefaultBehaviors={true}
indexPatterns={dataView ? [dataView] : undefined}
/>
<EuiFlexGrid columns={4}>
<EuiFlexItem>
<EuiFormLabel>Data view</EuiFormLabel>
<IndexPatternSelect
placeholder={i18n.translate('searchSessionExample.selectDataViewPlaceholder', {
defaultMessage: 'Select data view',
})}
indexPatternId={dataView?.id || ''}
onChange={async (dataViewId?: string) => {
if (dataViewId) {
const newDataView = await data.dataViews.get(dataViewId);
setDataView(newDataView);
} else {
setDataView(undefined);
}
}}
isClearable={false}
data-test-subj="dataViewSelector"
/>
</EuiFlexItem>
<EuiFlexItem>
<EuiFormLabel>Field (using {bucketAggType} buckets)</EuiFormLabel>
<EuiComboBox
options={formatFieldsToComboBox(getAggregatableStrings(fields))}
selectedOptions={formatFieldToComboBox(selectedBucketField)}
singleSelection={true}
onChange={(option) => {
if (option.length) {
const fld = dataView?.getFieldByName(option[0].label);
setSelectedBucketField(fld || null);
} else {
setSelectedBucketField(null);
}
}}
sortMatchesBy="startsWith"
data-test-subj="searchBucketField"
/>
</EuiFlexItem>
<EuiFlexItem>
<EuiFormLabel>Numeric Field (using {metricAggType} metrics)</EuiFormLabel>
<EuiComboBox
options={formatFieldsToComboBox(getNumeric(fields))}
selectedOptions={formatFieldToComboBox(selectedNumericField)}
singleSelection={true}
onChange={(option) => {
if (option.length) {
const fld = dataView?.getFieldByName(option[0].label);
setSelectedNumericField(fld || null);
} else {
setSelectedNumericField(null);
}
}}
sortMatchesBy="startsWith"
data-test-subj="searchMetricField"
/>
</EuiFlexItem>
<EuiFlexItem>
<EuiFormLabel>Fields to queryString</EuiFormLabel>
<EuiComboBox
options={formatFieldsToComboBox(fields)}
selectedOptions={formatFieldsToComboBox(selectedFields)}
singleSelection={false}
onChange={(option) => {
const flds = option
.map((opt) => dataView?.getFieldByName(opt?.label))
.filter((f) => f);
setSelectedFields(flds.length ? (flds as DataViewField[]) : []);
}}
sortMatchesBy="startsWith"
/>
</EuiFlexItem>
</EuiFlexGrid>
<EuiSpacer size="xl" />
<EuiFlexGrid columns={2}>
<EuiFlexItem>
<EuiTitle size="s">
<h3>
Searching Elasticsearch using <EuiCode>data.search</EuiCode>
</h3>
</EuiTitle>
<EuiText>
If you want to fetch data from Elasticsearch, you can use the different services
provided by the <EuiCode>data</EuiCode> plugin. These help you get the data view and
search bar configuration, format them into a DSL query and send it to Elasticsearch.
<EuiSpacer />
<EuiButtonEmpty size="xs" onClick={onClickHandler} iconType="play">
<FormattedMessage
id="searchExamples.buttonText"
defaultMessage="Request from low-level client (data.search.search)."
/>
</EuiButtonEmpty>
<EuiText size="xs" color="subdued" className="searchExampleStepDsc">
<FormattedMessage
id="searchExamples.buttonText"
defaultMessage="Metrics aggregation with raw documents in response."
/>
</EuiText>
<EuiButtonEmpty
size="xs"
onClick={() => onSearchSourceClickHandler(true, true)}
iconType="play"
data-test-subj="searchSourceWithOther"
>
<FormattedMessage
id="searchExamples.searchSource.buttonText"
defaultMessage="Request from high-level client (data.search.searchSource)"
/>
</EuiButtonEmpty>
<EuiText size="xs" color="subdued" className="searchExampleStepDsc">
<FormattedMessage
id="searchExamples.buttonText"
defaultMessage="Bucket and metrics aggregations, with other bucket and default warnings."
/>
</EuiText>
<EuiButtonEmpty
size="xs"
onClick={() => onSearchSourceClickHandler(false, false)}
iconType="play"
data-test-subj="searchSourceWithoutOther"
>
<FormattedMessage
id="searchExamples.searchSource.buttonText"
defaultMessage="Request from high-level client (data.search.searchSource)"
/>
</EuiButtonEmpty>
<EuiText size="xs" color="subdued" className="searchExampleStepDsc">
<FormattedMessage
id="searchExamples.buttonText"
defaultMessage="Bucket and metrics aggregations, without other bucket and with custom logic to handle warnings."
/>
</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>
<EuiText>
The observable returned from <EuiCode>data.search</EuiCode> provides partial results
when the response is not yet complete. These can be handled to update a chart or
simply a progress bar:
<EuiSpacer />
<EuiCodeBlock language="html" fontSize="s" paddingSize="s" overflowHeight={450}>
&lt;EuiProgress value=&#123;response.loaded&#125; max=&#123;response.total&#125;
/&gt;
</EuiCodeBlock>
Below is an example showing a custom search strategy that emits partial Fibonacci
sequences up to the length provided, updates the response with each partial result,
and updates a progress bar (see the Response tab).
<EuiFieldNumber
id="FibonacciN"
placeholder="Number of Fibonacci numbers to generate"
value={fibonacciN}
onChange={(event) => setFibonacciN(parseInt(event.target.value, 10))}
/>
<EuiButtonEmpty
size="xs"
onClick={onPartialResultsClickHandler}
iconType="play"
data-test-subj="requestFibonacci"
>
Request Fibonacci sequence
</EuiButtonEmpty>
</EuiText>
<EuiSpacer />
<EuiTitle size="s">
<h3>Writing a custom search strategy</h3>
</EuiTitle>
<EuiText>
If you want to do some pre or post processing on the server, you might want to create
a custom search strategy. This example uses such a strategy, passing in custom input
and receiving custom output back.
<EuiSpacer />
<EuiCheckbox
id="GetCool"
label={
<FormattedMessage
id="searchExamples.getCoolCheckbox"
defaultMessage="Get cool parameter?"
/>
}
checked={getCool}
onChange={(event) => setGetCool(event.target.checked)}
/>
<EuiButtonEmpty size="xs" onClick={onMyStrategyClickHandler} iconType="play">
<FormattedMessage
id="searchExamples.myStrategyButtonText"
defaultMessage="Request from low-level client via My Strategy"
/>
</EuiButtonEmpty>
</EuiText>
<EuiSpacer />
<EuiTitle size="s">
<h3>Client side search session caching</h3>
</EuiTitle>
<EuiText>
<EuiButtonEmpty
size="xs"
onClick={() => data.search.session.start()}
iconType="warning"
data-test-subj="searchExamplesStartSession"
>
<FormattedMessage
id="searchExamples.startNewSession"
defaultMessage="Start a new session"
/>
</EuiButtonEmpty>
<EuiButtonEmpty
size="xs"
onClick={() => data.search.session.clear()}
iconType="warning"
data-test-subj="searchExamplesClearSession"
>
<FormattedMessage id="searchExamples.clearSession" defaultMessage="Clear session" />
</EuiButtonEmpty>
<EuiButtonEmpty
size="xs"
onClick={onClientSideSessionCacheClickHandler}
iconType="play"
data-test-subj="searchExamplesCacheSearch"
>
<FormattedMessage
id="searchExamples.myStrategyButtonText"
defaultMessage="Request from low-level client via My Strategy"
/>
</EuiButtonEmpty>
</EuiText>
<EuiSpacer />
<EuiTitle size="s">
<h3>Using search on the server</h3>
</EuiTitle>
<EuiText>
You can also run your search request from the server, without registering a search
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
id="searchExamples.myServerButtonText"
defaultMessage="Request from low-level client on the server"
/>
</EuiButtonEmpty>
</EuiText>
</EuiFlexItem>
<EuiFlexItem style={{ width: '60%' }}>
<EuiTabbedContent
tabs={reqTabs}
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>
</EuiPageTemplate.Section>
</>
);
};