mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
[Partial Results] Move other bucket into Search Source (#96384)
* Move inspector adapter integration into search source * docs and ts * Move other bucket to search source * test ts + delete unused tabilfy function * hierarchical param in aggconfig. ts improvements more inspector tests * fix jest * separate inspect more tests * jest * inspector * Error handling and more tests * put the fun in functional tests * code review * Add functional test for other bucket in search example app * test * test * ts * test * test * ts Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
3b31d81196
commit
1a3e033c90
38 changed files with 964 additions and 423 deletions
|
@ -0,0 +1,11 @@
|
|||
<!-- 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) > [AggConfigs](./kibana-plugin-plugins-data-public.aggconfigs.md) > [hierarchical](./kibana-plugin-plugins-data-public.aggconfigs.hierarchical.md)
|
||||
|
||||
## AggConfigs.hierarchical property
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
hierarchical?: boolean;
|
||||
```
|
|
@ -22,6 +22,7 @@ export declare class AggConfigs
|
|||
| --- | --- | --- | --- |
|
||||
| [aggs](./kibana-plugin-plugins-data-public.aggconfigs.aggs.md) | | <code>IAggConfig[]</code> | |
|
||||
| [createAggConfig](./kibana-plugin-plugins-data-public.aggconfigs.createaggconfig.md) | | <code><T extends AggConfig = AggConfig>(params: CreateAggConfigParams, { addToAggConfigs }?: {</code><br/><code> addToAggConfigs?: boolean | undefined;</code><br/><code> }) => T</code> | |
|
||||
| [hierarchical](./kibana-plugin-plugins-data-public.aggconfigs.hierarchical.md) | | <code>boolean</code> | |
|
||||
| [indexPattern](./kibana-plugin-plugins-data-public.aggconfigs.indexpattern.md) | | <code>IndexPattern</code> | |
|
||||
| [timeFields](./kibana-plugin-plugins-data-public.aggconfigs.timefields.md) | | <code>string[]</code> | |
|
||||
| [timeRange](./kibana-plugin-plugins-data-public.aggconfigs.timerange.md) | | <code>TimeRange</code> | |
|
||||
|
@ -46,5 +47,5 @@ export declare class AggConfigs
|
|||
| [onSearchRequestStart(searchSource, options)](./kibana-plugin-plugins-data-public.aggconfigs.onsearchrequeststart.md) | | |
|
||||
| [setTimeFields(timeFields)](./kibana-plugin-plugins-data-public.aggconfigs.settimefields.md) | | |
|
||||
| [setTimeRange(timeRange)](./kibana-plugin-plugins-data-public.aggconfigs.settimerange.md) | | |
|
||||
| [toDsl(hierarchical)](./kibana-plugin-plugins-data-public.aggconfigs.todsl.md) | | |
|
||||
| [toDsl()](./kibana-plugin-plugins-data-public.aggconfigs.todsl.md) | | |
|
||||
|
||||
|
|
|
@ -7,15 +7,8 @@
|
|||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
toDsl(hierarchical?: boolean): Record<string, any>;
|
||||
toDsl(): Record<string, any>;
|
||||
```
|
||||
|
||||
## Parameters
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| hierarchical | <code>boolean</code> | |
|
||||
|
||||
<b>Returns:</b>
|
||||
|
||||
`Record<string, any>`
|
||||
|
|
|
@ -1,11 +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) > [ISearchOptions](./kibana-plugin-plugins-data-public.isearchoptions.md) > [requestResponder](./kibana-plugin-plugins-data-public.isearchoptions.requestresponder.md)
|
||||
[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [ISearchOptions](./kibana-plugin-plugins-data-public.isearchoptions.md) > [inspector](./kibana-plugin-plugins-data-public.isearchoptions.inspector.md)
|
||||
|
||||
## ISearchOptions.requestResponder property
|
||||
## ISearchOptions.inspector property
|
||||
|
||||
Inspector integration options
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
requestResponder?: RequestResponder;
|
||||
inspector?: IInspectorInfo;
|
||||
```
|
|
@ -16,10 +16,10 @@ export interface ISearchOptions
|
|||
| --- | --- | --- |
|
||||
| [abortSignal](./kibana-plugin-plugins-data-public.isearchoptions.abortsignal.md) | <code>AbortSignal</code> | An <code>AbortSignal</code> that allows the caller of <code>search</code> to abort a search request. |
|
||||
| [indexPattern](./kibana-plugin-plugins-data-public.isearchoptions.indexpattern.md) | <code>IndexPattern</code> | Index pattern reference is used for better error messages |
|
||||
| [inspector](./kibana-plugin-plugins-data-public.isearchoptions.inspector.md) | <code>IInspectorInfo</code> | Inspector integration options |
|
||||
| [isRestore](./kibana-plugin-plugins-data-public.isearchoptions.isrestore.md) | <code>boolean</code> | Whether the session is restored (i.e. search requests should re-use the stored search IDs, rather than starting from scratch) |
|
||||
| [isStored](./kibana-plugin-plugins-data-public.isearchoptions.isstored.md) | <code>boolean</code> | Whether the session is already saved (i.e. sent to background) |
|
||||
| [legacyHitsTotal](./kibana-plugin-plugins-data-public.isearchoptions.legacyhitstotal.md) | <code>boolean</code> | Request the legacy format for the total number of hits. If sending <code>rest_total_hits_as_int</code> to something other than <code>true</code>, this should be set to <code>false</code>. |
|
||||
| [requestResponder](./kibana-plugin-plugins-data-public.isearchoptions.requestresponder.md) | <code>RequestResponder</code> | |
|
||||
| [sessionId](./kibana-plugin-plugins-data-public.isearchoptions.sessionid.md) | <code>string</code> | A session ID, grouping multiple search requests into a single session. |
|
||||
| [strategy](./kibana-plugin-plugins-data-public.isearchoptions.strategy.md) | <code>string</code> | Use this option to force using a specific server side search strategy. Leave empty to use the default strategy. |
|
||||
|
||||
|
|
|
@ -14,7 +14,7 @@ Fetch this source and reject the returned Promise on error
|
|||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
fetch(options?: ISearchOptions): Promise<import("@elastic/elasticsearch/api/types").SearchResponse<any>>;
|
||||
fetch(options?: ISearchOptions): Promise<estypes.SearchResponse<any>>;
|
||||
```
|
||||
|
||||
## Parameters
|
||||
|
@ -25,5 +25,5 @@ fetch(options?: ISearchOptions): Promise<import("@elastic/elasticsearch/api/type
|
|||
|
||||
<b>Returns:</b>
|
||||
|
||||
`Promise<import("@elastic/elasticsearch/api/types").SearchResponse<any>>`
|
||||
`Promise<estypes.SearchResponse<any>>`
|
||||
|
||||
|
|
|
@ -9,7 +9,7 @@ Fetch this source from Elasticsearch, returning an observable over the response(
|
|||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
fetch$(options?: ISearchOptions): import("rxjs").Observable<import("@elastic/elasticsearch/api/types").SearchResponse<any>>;
|
||||
fetch$(options?: ISearchOptions): Observable<estypes.SearchResponse<any>>;
|
||||
```
|
||||
|
||||
## Parameters
|
||||
|
@ -20,5 +20,5 @@ fetch$(options?: ISearchOptions): import("rxjs").Observable<import("@elastic/ela
|
|||
|
||||
<b>Returns:</b>
|
||||
|
||||
`import("rxjs").Observable<import("@elastic/elasticsearch/api/types").SearchResponse<any>>`
|
||||
`Observable<estypes.SearchResponse<any>>`
|
||||
|
||||
|
|
|
@ -9,5 +9,5 @@
|
|||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
aggs?: any;
|
||||
aggs?: object | IAggConfigs | (() => object);
|
||||
```
|
||||
|
|
|
@ -16,7 +16,7 @@ export interface SearchSourceFields
|
|||
|
||||
| Property | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| [aggs](./kibana-plugin-plugins-data-public.searchsourcefields.aggs.md) | <code>any</code> | [AggConfigs](./kibana-plugin-plugins-data-public.aggconfigs.md) |
|
||||
| [aggs](./kibana-plugin-plugins-data-public.searchsourcefields.aggs.md) | <code>object | IAggConfigs | (() => object)</code> | [AggConfigs](./kibana-plugin-plugins-data-public.aggconfigs.md) |
|
||||
| [fields](./kibana-plugin-plugins-data-public.searchsourcefields.fields.md) | <code>SearchFieldValue[]</code> | Retrieve fields via the search Fields API |
|
||||
| [fieldsFromSource](./kibana-plugin-plugins-data-public.searchsourcefields.fieldsfromsource.md) | <code>NameList</code> | Retreive fields directly from \_source (legacy behavior) |
|
||||
| [filter](./kibana-plugin-plugins-data-public.searchsourcefields.filter.md) | <code>Filter[] | Filter | (() => Filter[] | Filter | undefined)</code> | [Filter](./kibana-plugin-plugins-data-public.filter.md) |
|
||||
|
|
|
@ -1,11 +1,13 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [ISearchOptions](./kibana-plugin-plugins-data-server.isearchoptions.md) > [requestResponder](./kibana-plugin-plugins-data-server.isearchoptions.requestresponder.md)
|
||||
[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [ISearchOptions](./kibana-plugin-plugins-data-server.isearchoptions.md) > [inspector](./kibana-plugin-plugins-data-server.isearchoptions.inspector.md)
|
||||
|
||||
## ISearchOptions.requestResponder property
|
||||
## ISearchOptions.inspector property
|
||||
|
||||
Inspector integration options
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
requestResponder?: RequestResponder;
|
||||
inspector?: IInspectorInfo;
|
||||
```
|
|
@ -16,10 +16,10 @@ export interface ISearchOptions
|
|||
| --- | --- | --- |
|
||||
| [abortSignal](./kibana-plugin-plugins-data-server.isearchoptions.abortsignal.md) | <code>AbortSignal</code> | An <code>AbortSignal</code> that allows the caller of <code>search</code> to abort a search request. |
|
||||
| [indexPattern](./kibana-plugin-plugins-data-server.isearchoptions.indexpattern.md) | <code>IndexPattern</code> | Index pattern reference is used for better error messages |
|
||||
| [inspector](./kibana-plugin-plugins-data-server.isearchoptions.inspector.md) | <code>IInspectorInfo</code> | Inspector integration options |
|
||||
| [isRestore](./kibana-plugin-plugins-data-server.isearchoptions.isrestore.md) | <code>boolean</code> | Whether the session is restored (i.e. search requests should re-use the stored search IDs, rather than starting from scratch) |
|
||||
| [isStored](./kibana-plugin-plugins-data-server.isearchoptions.isstored.md) | <code>boolean</code> | Whether the session is already saved (i.e. sent to background) |
|
||||
| [legacyHitsTotal](./kibana-plugin-plugins-data-server.isearchoptions.legacyhitstotal.md) | <code>boolean</code> | Request the legacy format for the total number of hits. If sending <code>rest_total_hits_as_int</code> to something other than <code>true</code>, this should be set to <code>false</code>. |
|
||||
| [requestResponder](./kibana-plugin-plugins-data-server.isearchoptions.requestresponder.md) | <code>RequestResponder</code> | |
|
||||
| [sessionId](./kibana-plugin-plugins-data-server.isearchoptions.sessionid.md) | <code>string</code> | A session ID, grouping multiple search requests into a single session. |
|
||||
| [strategy](./kibana-plugin-plugins-data-server.isearchoptions.strategy.md) | <code>string</code> | Use this option to force using a specific server side search strategy. Leave empty to use the default strategy. |
|
||||
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
@import '@elastic/eui/src/global_styling/variables/header';
|
||||
|
||||
.searchExampleStepDsc {
|
||||
padding-left: $euiSizeXL;
|
||||
font-style: italic;
|
||||
}
|
|
@ -20,13 +20,13 @@ import {
|
|||
EuiTitle,
|
||||
EuiText,
|
||||
EuiFlexGrid,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiCheckbox,
|
||||
EuiSpacer,
|
||||
EuiCode,
|
||||
EuiComboBox,
|
||||
EuiFormLabel,
|
||||
EuiTabbedContent,
|
||||
} from '@elastic/eui';
|
||||
|
||||
import { CoreStart } from '../../../../src/core/public';
|
||||
|
@ -60,6 +60,11 @@ function getNumeric(fields?: IndexPatternField[]) {
|
|||
return fields?.filter((f) => f.type === 'number' && f.aggregatable);
|
||||
}
|
||||
|
||||
function getAggregatableStrings(fields?: IndexPatternField[]) {
|
||||
if (!fields) return [];
|
||||
return fields?.filter((f) => f.type === 'string' && f.aggregatable);
|
||||
}
|
||||
|
||||
function formatFieldToComboBox(field?: IndexPatternField | null) {
|
||||
if (!field) return [];
|
||||
return formatFieldsToComboBox([field]);
|
||||
|
@ -90,6 +95,9 @@ export const SearchExamplesApp = ({
|
|||
const [selectedNumericField, setSelectedNumericField] = useState<
|
||||
IndexPatternField | null | undefined
|
||||
>();
|
||||
const [selectedBucketField, setSelectedBucketField] = useState<
|
||||
IndexPatternField | null | undefined
|
||||
>();
|
||||
const [request, setRequest] = useState<Record<string, any>>({});
|
||||
const [response, setResponse] = useState<Record<string, any>>({});
|
||||
|
||||
|
@ -108,6 +116,7 @@ export const SearchExamplesApp = ({
|
|||
setFields(indexPattern?.fields);
|
||||
}, [indexPattern]);
|
||||
useEffect(() => {
|
||||
setSelectedBucketField(fields?.length ? getAggregatableStrings(fields)[0] : null);
|
||||
setSelectedNumericField(fields?.length ? getNumeric(fields)[0] : null);
|
||||
}, [fields]);
|
||||
|
||||
|
@ -203,28 +212,40 @@ export const SearchExamplesApp = ({
|
|||
.setField('index', indexPattern)
|
||||
.setField('filter', filters)
|
||||
.setField('query', query)
|
||||
.setField('fields', selectedFields.length ? selectedFields.map((f) => f.name) : ['*'])
|
||||
.setField('fields', selectedFields.length ? selectedFields.map((f) => f.name) : [''])
|
||||
.setField('size', selectedFields.length ? 100 : 0)
|
||||
.setField('trackTotalHits', 100);
|
||||
|
||||
if (selectedNumericField) {
|
||||
searchSource.setField('aggs', () => {
|
||||
return data.search.aggs
|
||||
.createAggConfigs(indexPattern, [
|
||||
{ type: 'avg', params: { field: selectedNumericField.name } },
|
||||
])
|
||||
.toDsl();
|
||||
const aggDef = [];
|
||||
if (selectedBucketField) {
|
||||
aggDef.push({
|
||||
type: 'terms',
|
||||
schema: 'split',
|
||||
params: { field: selectedBucketField.name, size: 2, otherBucket: true },
|
||||
});
|
||||
}
|
||||
if (selectedNumericField) {
|
||||
aggDef.push({ type: 'avg', params: { field: selectedNumericField.name } });
|
||||
}
|
||||
if (aggDef.length > 0) {
|
||||
const ac = data.search.aggs.createAggConfigs(indexPattern, aggDef);
|
||||
searchSource.setField('aggs', ac);
|
||||
}
|
||||
|
||||
setRequest(searchSource.getSearchRequestBody());
|
||||
const res = await searchSource.fetch$().toPromise();
|
||||
setResponse(res);
|
||||
|
||||
const message = <EuiText>Searched {res.hits.total} documents.</EuiText>;
|
||||
notifications.toasts.addSuccess({
|
||||
title: 'Query result',
|
||||
text: mountReactNode(message),
|
||||
});
|
||||
notifications.toasts.addSuccess(
|
||||
{
|
||||
title: 'Query result',
|
||||
text: mountReactNode(message),
|
||||
},
|
||||
{
|
||||
toastLifeTimeMs: 300000,
|
||||
}
|
||||
);
|
||||
} catch (e) {
|
||||
setResponse(e.body);
|
||||
notifications.toasts.addWarning(`An error has occurred: ${e.message}`);
|
||||
|
@ -263,6 +284,55 @@ export const SearchExamplesApp = ({
|
|||
doSearchSourceSearch();
|
||||
};
|
||||
|
||||
const reqTabs = [
|
||||
{
|
||||
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>
|
||||
<EuiCodeBlock
|
||||
language="json"
|
||||
fontSize="s"
|
||||
paddingSize="s"
|
||||
overflowHeight={450}
|
||||
isCopyable
|
||||
data-test-subj="responseCodeBlock"
|
||||
>
|
||||
{JSON.stringify(response, null, 2)}
|
||||
</EuiCodeBlock>
|
||||
</>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<EuiPageBody>
|
||||
<EuiPageHeader>
|
||||
|
@ -284,59 +354,75 @@ export const SearchExamplesApp = ({
|
|||
useDefaultBehaviors={true}
|
||||
indexPatterns={indexPattern ? [indexPattern] : undefined}
|
||||
/>
|
||||
<EuiFlexGrid columns={3}>
|
||||
<EuiFlexGrid columns={4}>
|
||||
<EuiFlexItem>
|
||||
<EuiFormLabel>Index Pattern</EuiFormLabel>
|
||||
<IndexPatternSelect
|
||||
placeholder={i18n.translate('searchSessionExample.selectIndexPatternPlaceholder', {
|
||||
defaultMessage: 'Select index pattern',
|
||||
})}
|
||||
indexPatternId={indexPattern?.id || ''}
|
||||
onChange={async (newIndexPatternId: any) => {
|
||||
const newIndexPattern = await data.indexPatterns.get(newIndexPatternId);
|
||||
setIndexPattern(newIndexPattern);
|
||||
}}
|
||||
isClearable={false}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiFormLabel>Field (bucket)</EuiFormLabel>
|
||||
<EuiComboBox
|
||||
options={formatFieldsToComboBox(getAggregatableStrings(fields))}
|
||||
selectedOptions={formatFieldToComboBox(selectedBucketField)}
|
||||
singleSelection={true}
|
||||
onChange={(option) => {
|
||||
if (option.length) {
|
||||
const fld = indexPattern?.getFieldByName(option[0].label);
|
||||
setSelectedBucketField(fld || null);
|
||||
} else {
|
||||
setSelectedBucketField(null);
|
||||
}
|
||||
}}
|
||||
sortMatchesBy="startsWith"
|
||||
data-test-subj="searchBucketField"
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiFormLabel>Numeric Field (metric)</EuiFormLabel>
|
||||
<EuiComboBox
|
||||
options={formatFieldsToComboBox(getNumeric(fields))}
|
||||
selectedOptions={formatFieldToComboBox(selectedNumericField)}
|
||||
singleSelection={true}
|
||||
onChange={(option) => {
|
||||
if (option.length) {
|
||||
const fld = indexPattern?.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) => indexPattern?.getFieldByName(opt?.label))
|
||||
.filter((f) => f);
|
||||
setSelectedFields(flds.length ? (flds as IndexPatternField[]) : []);
|
||||
}}
|
||||
sortMatchesBy="startsWith"
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGrid>
|
||||
<EuiFlexGrid columns={2}>
|
||||
<EuiFlexItem style={{ width: '40%' }}>
|
||||
<EuiText>
|
||||
<EuiFlexGrid columns={2}>
|
||||
<EuiFlexItem>
|
||||
<EuiFormLabel>Index Pattern</EuiFormLabel>
|
||||
<IndexPatternSelect
|
||||
placeholder={i18n.translate(
|
||||
'searchSessionExample.selectIndexPatternPlaceholder',
|
||||
{
|
||||
defaultMessage: 'Select index pattern',
|
||||
}
|
||||
)}
|
||||
indexPatternId={indexPattern?.id || ''}
|
||||
onChange={async (newIndexPatternId: any) => {
|
||||
const newIndexPattern = await data.indexPatterns.get(newIndexPatternId);
|
||||
setIndexPattern(newIndexPattern);
|
||||
}}
|
||||
isClearable={false}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiFormLabel>Numeric Field to Aggregate</EuiFormLabel>
|
||||
<EuiComboBox
|
||||
options={formatFieldsToComboBox(getNumeric(fields))}
|
||||
selectedOptions={formatFieldToComboBox(selectedNumericField)}
|
||||
singleSelection={true}
|
||||
onChange={(option) => {
|
||||
const fld = indexPattern?.getFieldByName(option[0].label);
|
||||
setSelectedNumericField(fld || null);
|
||||
}}
|
||||
sortMatchesBy="startsWith"
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGrid>
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<EuiFormLabel>Fields to query (leave blank to include all fields)</EuiFormLabel>
|
||||
<EuiComboBox
|
||||
options={formatFieldsToComboBox(fields)}
|
||||
selectedOptions={formatFieldsToComboBox(selectedFields)}
|
||||
singleSelection={false}
|
||||
onChange={(option) => {
|
||||
const flds = option
|
||||
.map((opt) => indexPattern?.getFieldByName(opt?.label))
|
||||
.filter((f) => f);
|
||||
setSelectedFields(flds.length ? (flds as IndexPatternField[]) : []);
|
||||
}}
|
||||
sortMatchesBy="startsWith"
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiText>
|
||||
<EuiSpacer />
|
||||
<EuiTitle size="s">
|
||||
<h3>
|
||||
|
@ -352,15 +438,32 @@ export const SearchExamplesApp = ({
|
|||
<EuiButtonEmpty size="xs" onClick={onClickHandler} iconType="play">
|
||||
<FormattedMessage
|
||||
id="searchExamples.buttonText"
|
||||
defaultMessage="Request from low-level client (data.search.search)"
|
||||
defaultMessage="Request from low-level client (data.search.search)."
|
||||
/>
|
||||
</EuiButtonEmpty>
|
||||
<EuiButtonEmpty size="xs" onClick={onSearchSourceClickHandler} iconType="play">
|
||||
<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}
|
||||
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."
|
||||
/>
|
||||
</EuiText>
|
||||
</EuiText>
|
||||
<EuiSpacer />
|
||||
<EuiTitle size="s">
|
||||
|
@ -446,41 +549,8 @@ export const SearchExamplesApp = ({
|
|||
</EuiButtonEmpty>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem style={{ width: '30%' }}>
|
||||
<EuiTitle size="xs">
|
||||
<h4>Request</h4>
|
||||
</EuiTitle>
|
||||
<EuiText size="xs">Search body sent to ES</EuiText>
|
||||
<EuiCodeBlock
|
||||
language="json"
|
||||
fontSize="s"
|
||||
paddingSize="s"
|
||||
overflowHeight={450}
|
||||
isCopyable
|
||||
>
|
||||
{JSON.stringify(request, null, 2)}
|
||||
</EuiCodeBlock>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem style={{ width: '30%' }}>
|
||||
<EuiTitle size="xs">
|
||||
<h4>Response</h4>
|
||||
</EuiTitle>
|
||||
<EuiText size="xs">
|
||||
<FormattedMessage
|
||||
id="searchExamples.timestampText"
|
||||
defaultMessage="Took: {time} ms"
|
||||
values={{ time: timeTook ?? 'Unknown' }}
|
||||
/>
|
||||
</EuiText>
|
||||
<EuiCodeBlock
|
||||
language="json"
|
||||
fontSize="s"
|
||||
paddingSize="s"
|
||||
overflowHeight={450}
|
||||
isCopyable
|
||||
>
|
||||
{JSON.stringify(response, null, 2)}
|
||||
</EuiCodeBlock>
|
||||
<EuiFlexItem style={{ width: '60%' }}>
|
||||
<EuiTabbedContent tabs={reqTabs} />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGrid>
|
||||
</EuiPageContentBody>
|
||||
|
|
|
@ -342,8 +342,8 @@ describe('AggConfigs', () => {
|
|||
{ enabled: true, type: 'max', schema: 'metric', params: { field: 'bytes' } },
|
||||
];
|
||||
|
||||
const ac = new AggConfigs(indexPattern, configStates, { typesRegistry });
|
||||
const topLevelDsl = ac.toDsl(true);
|
||||
const ac = new AggConfigs(indexPattern, configStates, { typesRegistry, hierarchical: true });
|
||||
const topLevelDsl = ac.toDsl();
|
||||
const buckets = ac.bySchemaName('buckets');
|
||||
const metrics = ac.bySchemaName('metrics');
|
||||
|
||||
|
@ -412,8 +412,8 @@ describe('AggConfigs', () => {
|
|||
},
|
||||
];
|
||||
|
||||
const ac = new AggConfigs(indexPattern, configStates, { typesRegistry });
|
||||
const topLevelDsl = ac.toDsl(true)['2'];
|
||||
const ac = new AggConfigs(indexPattern, configStates, { typesRegistry, hierarchical: true });
|
||||
const topLevelDsl = ac.toDsl()['2'];
|
||||
|
||||
expect(Object.keys(topLevelDsl.aggs)).toContain('1');
|
||||
expect(Object.keys(topLevelDsl.aggs)).toContain('1-bucket');
|
||||
|
|
|
@ -43,6 +43,7 @@ function parseParentAggs(dslLvlCursor: any, dsl: any) {
|
|||
|
||||
export interface AggConfigsOptions {
|
||||
typesRegistry: AggTypesRegistryStart;
|
||||
hierarchical?: boolean;
|
||||
}
|
||||
|
||||
export type CreateAggConfigParams = Assign<AggConfigSerialized, { type: string | IAggType }>;
|
||||
|
@ -65,6 +66,8 @@ export class AggConfigs {
|
|||
public indexPattern: IndexPattern;
|
||||
public timeRange?: TimeRange;
|
||||
public timeFields?: string[];
|
||||
public hierarchical?: boolean = false;
|
||||
|
||||
private readonly typesRegistry: AggTypesRegistryStart;
|
||||
|
||||
aggs: IAggConfig[];
|
||||
|
@ -80,6 +83,7 @@ export class AggConfigs {
|
|||
|
||||
this.aggs = [];
|
||||
this.indexPattern = indexPattern;
|
||||
this.hierarchical = opts.hierarchical;
|
||||
|
||||
configStates.forEach((params: any) => this.createAggConfig(params));
|
||||
}
|
||||
|
@ -174,12 +178,12 @@ export class AggConfigs {
|
|||
return true;
|
||||
}
|
||||
|
||||
toDsl(hierarchical: boolean = false): Record<string, any> {
|
||||
toDsl(): Record<string, any> {
|
||||
const dslTopLvl = {};
|
||||
let dslLvlCursor: Record<string, any>;
|
||||
let nestedMetrics: Array<{ config: AggConfig; dsl: Record<string, any> }> | [];
|
||||
|
||||
if (hierarchical) {
|
||||
if (this.hierarchical) {
|
||||
// collect all metrics, and filter out the ones that we won't be copying
|
||||
nestedMetrics = this.aggs
|
||||
.filter(function (agg) {
|
||||
|
|
|
@ -13,12 +13,23 @@ import { ISearchSource } from 'src/plugins/data/public';
|
|||
import { DatatableColumnType, SerializedFieldFormat } from 'src/plugins/expressions/common';
|
||||
import type { RequestAdapter } from 'src/plugins/inspector/common';
|
||||
|
||||
import { estypes } from '@elastic/elasticsearch';
|
||||
import { initParams } from './agg_params';
|
||||
import { AggConfig } from './agg_config';
|
||||
import { IAggConfigs } from './agg_configs';
|
||||
import { BaseParamType } from './param_types/base';
|
||||
import { AggParamType } from './param_types/agg';
|
||||
|
||||
type PostFlightRequestFn<TAggConfig> = (
|
||||
resp: estypes.SearchResponse<any>,
|
||||
aggConfigs: IAggConfigs,
|
||||
aggConfig: TAggConfig,
|
||||
searchSource: ISearchSource,
|
||||
inspectorRequestAdapter?: RequestAdapter,
|
||||
abortSignal?: AbortSignal,
|
||||
searchSessionId?: string
|
||||
) => Promise<estypes.SearchResponse<any>>;
|
||||
|
||||
export interface AggTypeConfig<
|
||||
TAggConfig extends AggConfig = AggConfig,
|
||||
TParam extends AggParamType<TAggConfig> = AggParamType<TAggConfig>
|
||||
|
@ -40,15 +51,7 @@ export interface AggTypeConfig<
|
|||
customLabels?: boolean;
|
||||
json?: boolean;
|
||||
decorateAggConfig?: () => any;
|
||||
postFlightRequest?: (
|
||||
resp: any,
|
||||
aggConfigs: IAggConfigs,
|
||||
aggConfig: TAggConfig,
|
||||
searchSource: ISearchSource,
|
||||
inspectorRequestAdapter?: RequestAdapter,
|
||||
abortSignal?: AbortSignal,
|
||||
searchSessionId?: string
|
||||
) => Promise<any>;
|
||||
postFlightRequest?: PostFlightRequestFn<TAggConfig>;
|
||||
getSerializedFormat?: (agg: TAggConfig) => SerializedFieldFormat;
|
||||
getValue?: (agg: TAggConfig, bucket: any) => any;
|
||||
getKey?: (bucket: any, key: any, agg: TAggConfig) => any;
|
||||
|
@ -188,15 +191,7 @@ export class AggType<
|
|||
* @param searchSessionId - searchSessionId to be used for grouping requests into a single search session
|
||||
* @return {Promise}
|
||||
*/
|
||||
postFlightRequest: (
|
||||
resp: any,
|
||||
aggConfigs: IAggConfigs,
|
||||
aggConfig: TAggConfig,
|
||||
searchSource: ISearchSource,
|
||||
inspectorRequestAdapter?: RequestAdapter,
|
||||
abortSignal?: AbortSignal,
|
||||
searchSessionId?: string
|
||||
) => Promise<any>;
|
||||
postFlightRequest: PostFlightRequestFn<TAggConfig>;
|
||||
/**
|
||||
* Get the serialized format for the values produced by this agg type,
|
||||
* overridden by several metrics that always output a simple number.
|
||||
|
|
|
@ -433,7 +433,7 @@ describe('Terms Agg Other bucket helper', () => {
|
|||
aggConfigs.aggs[0] as IBucketAggConfig,
|
||||
otherAggConfig()
|
||||
);
|
||||
expect(mergedResponse.aggregations['1'].buckets[3].key).toEqual('__other__');
|
||||
expect((mergedResponse!.aggregations!['1'] as any).buckets[3].key).toEqual('__other__');
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -455,7 +455,7 @@ describe('Terms Agg Other bucket helper', () => {
|
|||
otherAggConfig()
|
||||
);
|
||||
|
||||
expect(mergedResponse.aggregations['1'].buckets[1]['2'].buckets[3].key).toEqual(
|
||||
expect((mergedResponse!.aggregations!['1'] as any).buckets[1]['2'].buckets[3].key).toEqual(
|
||||
'__other__'
|
||||
);
|
||||
}
|
||||
|
@ -471,7 +471,7 @@ describe('Terms Agg Other bucket helper', () => {
|
|||
aggConfigs.aggs[0] as IBucketAggConfig
|
||||
);
|
||||
expect(
|
||||
updatedResponse.aggregations['1'].buckets.find(
|
||||
(updatedResponse!.aggregations!['1'] as any).buckets.find(
|
||||
(bucket: Record<string, any>) => bucket.key === '__missing__'
|
||||
)
|
||||
).toBeDefined();
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
*/
|
||||
|
||||
import { isNumber, keys, values, find, each, cloneDeep, flatten } from 'lodash';
|
||||
import { estypes } from '@elastic/elasticsearch';
|
||||
import { buildExistsFilter, buildPhrasesFilter, buildQueryFromFilters } from '../../../../common';
|
||||
import { AggGroupNames } from '../agg_groups';
|
||||
import { IAggConfigs } from '../agg_configs';
|
||||
|
@ -42,7 +43,7 @@ const getNestedAggDSL = (aggNestedDsl: Record<string, any>, startFromAggId: stri
|
|||
*/
|
||||
const getAggResultBuckets = (
|
||||
aggConfigs: IAggConfigs,
|
||||
response: any,
|
||||
response: estypes.SearchResponse<any>['aggregations'],
|
||||
aggWithOtherBucket: IBucketAggConfig,
|
||||
key: string
|
||||
) => {
|
||||
|
@ -72,8 +73,8 @@ const getAggResultBuckets = (
|
|||
}
|
||||
}
|
||||
}
|
||||
if (responseAgg[aggWithOtherBucket.id]) {
|
||||
return responseAgg[aggWithOtherBucket.id].buckets;
|
||||
if (responseAgg?.[aggWithOtherBucket.id]) {
|
||||
return (responseAgg[aggWithOtherBucket.id] as any).buckets;
|
||||
}
|
||||
return [];
|
||||
};
|
||||
|
@ -235,11 +236,11 @@ export const buildOtherBucketAgg = (
|
|||
|
||||
export const mergeOtherBucketAggResponse = (
|
||||
aggsConfig: IAggConfigs,
|
||||
response: any,
|
||||
response: estypes.SearchResponse<any>,
|
||||
otherResponse: any,
|
||||
otherAgg: IBucketAggConfig,
|
||||
requestAgg: Record<string, any>
|
||||
) => {
|
||||
): estypes.SearchResponse<any> => {
|
||||
const updatedResponse = cloneDeep(response);
|
||||
each(otherResponse.aggregations['other-filter'].buckets, (bucket, key) => {
|
||||
if (!bucket.doc_count || key === undefined) return;
|
||||
|
@ -276,7 +277,7 @@ export const mergeOtherBucketAggResponse = (
|
|||
};
|
||||
|
||||
export const updateMissingBucket = (
|
||||
response: any,
|
||||
response: estypes.SearchResponse<any>,
|
||||
aggConfigs: IAggConfigs,
|
||||
agg: IBucketAggConfig
|
||||
) => {
|
||||
|
|
|
@ -101,25 +101,21 @@ export const getTermsBucketAgg = () =>
|
|||
|
||||
nestedSearchSource.setField('aggs', filterAgg);
|
||||
|
||||
const requestResponder = inspectorRequestAdapter?.start(
|
||||
i18n.translate('data.search.aggs.buckets.terms.otherBucketTitle', {
|
||||
defaultMessage: 'Other bucket',
|
||||
}),
|
||||
{
|
||||
description: i18n.translate('data.search.aggs.buckets.terms.otherBucketDescription', {
|
||||
defaultMessage:
|
||||
'This request counts the number of documents that fall ' +
|
||||
'outside the criterion of the data buckets.',
|
||||
}),
|
||||
searchSessionId,
|
||||
}
|
||||
);
|
||||
|
||||
const response = await nestedSearchSource
|
||||
.fetch$({
|
||||
abortSignal,
|
||||
sessionId: searchSessionId,
|
||||
requestResponder,
|
||||
inspector: {
|
||||
adapter: inspectorRequestAdapter,
|
||||
title: i18n.translate('data.search.aggs.buckets.terms.otherBucketTitle', {
|
||||
defaultMessage: 'Other bucket',
|
||||
}),
|
||||
description: i18n.translate('data.search.aggs.buckets.terms.otherBucketDescription', {
|
||||
defaultMessage:
|
||||
'This request counts the number of documents that fall ' +
|
||||
'outside the criterion of the data buckets.',
|
||||
}),
|
||||
},
|
||||
})
|
||||
.toPromise();
|
||||
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
import type { MockedKeys } from '@kbn/utility-types/jest';
|
||||
import type { Filter } from '../../../es_query';
|
||||
import type { IndexPattern } from '../../../index_patterns';
|
||||
import type { IAggConfig, IAggConfigs } from '../../aggs';
|
||||
import type { IAggConfigs } from '../../aggs';
|
||||
import type { ISearchSource } from '../../search_source';
|
||||
import { searchSourceCommonMock } from '../../search_source/mocks';
|
||||
|
||||
|
@ -38,7 +38,6 @@ describe('esaggs expression function - public', () => {
|
|||
filters: undefined,
|
||||
indexPattern: ({ id: 'logstash-*' } as unknown) as jest.Mocked<IndexPattern>,
|
||||
inspectorAdapters: {},
|
||||
metricsAtAllLevels: false,
|
||||
partialRows: false,
|
||||
query: undefined,
|
||||
searchSessionId: 'abc123',
|
||||
|
@ -76,21 +75,7 @@ describe('esaggs expression function - public', () => {
|
|||
|
||||
test('setField(aggs)', async () => {
|
||||
expect(searchSource.setField).toHaveBeenCalledTimes(5);
|
||||
expect(typeof (searchSource.setField as jest.Mock).mock.calls[2][1]).toBe('function');
|
||||
expect((searchSource.setField as jest.Mock).mock.calls[2][1]()).toEqual(
|
||||
mockParams.aggs.toDsl()
|
||||
);
|
||||
expect(mockParams.aggs.toDsl).toHaveBeenCalledWith(mockParams.metricsAtAllLevels);
|
||||
|
||||
// make sure param is passed through
|
||||
jest.clearAllMocks();
|
||||
await handleRequest({
|
||||
...mockParams,
|
||||
metricsAtAllLevels: true,
|
||||
});
|
||||
searchSource = await mockParams.searchSourceService.create();
|
||||
(searchSource.setField as jest.Mock).mock.calls[2][1]();
|
||||
expect(mockParams.aggs.toDsl).toHaveBeenCalledWith(true);
|
||||
expect((searchSource.setField as jest.Mock).mock.calls[2][1]).toEqual(mockParams.aggs);
|
||||
});
|
||||
|
||||
test('setField(filter)', async () => {
|
||||
|
@ -133,36 +118,24 @@ describe('esaggs expression function - public', () => {
|
|||
test('calls searchSource.fetch', async () => {
|
||||
await handleRequest(mockParams);
|
||||
const searchSource = await mockParams.searchSourceService.create();
|
||||
|
||||
expect(searchSource.fetch$).toHaveBeenCalledWith({
|
||||
abortSignal: mockParams.abortSignal,
|
||||
sessionId: mockParams.searchSessionId,
|
||||
inspector: {
|
||||
title: 'Data',
|
||||
description: 'This request queries Elasticsearch to fetch the data for the visualization.',
|
||||
adapter: undefined,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('calls agg.postFlightRequest if it exiests and agg is enabled', async () => {
|
||||
mockParams.aggs.aggs[0].enabled = true;
|
||||
await handleRequest(mockParams);
|
||||
expect(mockParams.aggs.aggs[0].type.postFlightRequest).toHaveBeenCalledTimes(1);
|
||||
|
||||
// ensure it works if the function doesn't exist
|
||||
jest.clearAllMocks();
|
||||
mockParams.aggs.aggs[0] = ({ type: { name: 'count' } } as unknown) as IAggConfig;
|
||||
expect(async () => await handleRequest(mockParams)).not.toThrowError();
|
||||
});
|
||||
|
||||
test('should skip agg.postFlightRequest call if the agg is disabled', async () => {
|
||||
mockParams.aggs.aggs[0].enabled = false;
|
||||
await handleRequest(mockParams);
|
||||
expect(mockParams.aggs.aggs[0].type.postFlightRequest).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
test('tabifies response data', async () => {
|
||||
await handleRequest(mockParams);
|
||||
expect(tabifyAggResponse).toHaveBeenCalledWith(
|
||||
mockParams.aggs,
|
||||
{},
|
||||
{
|
||||
metricsAtAllLevels: mockParams.metricsAtAllLevels,
|
||||
partialRows: mockParams.partialRows,
|
||||
timeRange: mockParams.timeRange,
|
||||
}
|
||||
|
|
|
@ -40,28 +40,12 @@ export interface RequestHandlerParams {
|
|||
getNow?: () => Date;
|
||||
}
|
||||
|
||||
function getRequestMainResponder(inspectorAdapters: Adapters, searchSessionId?: string) {
|
||||
return inspectorAdapters.requests?.start(
|
||||
i18n.translate('data.functions.esaggs.inspector.dataRequest.title', {
|
||||
defaultMessage: 'Data',
|
||||
}),
|
||||
{
|
||||
description: i18n.translate('data.functions.esaggs.inspector.dataRequest.description', {
|
||||
defaultMessage:
|
||||
'This request queries Elasticsearch to fetch the data for the visualization.',
|
||||
}),
|
||||
searchSessionId,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export const handleRequest = async ({
|
||||
abortSignal,
|
||||
aggs,
|
||||
filters,
|
||||
indexPattern,
|
||||
inspectorAdapters,
|
||||
metricsAtAllLevels,
|
||||
partialRows,
|
||||
query,
|
||||
searchSessionId,
|
||||
|
@ -100,9 +84,7 @@ export const handleRequest = async ({
|
|||
},
|
||||
});
|
||||
|
||||
requestSearchSource.setField('aggs', function () {
|
||||
return aggs.toDsl(metricsAtAllLevels);
|
||||
});
|
||||
requestSearchSource.setField('aggs', aggs);
|
||||
|
||||
requestSearchSource.onRequestStart((paramSearchSource, options) => {
|
||||
return aggs.onSearchRequestStart(paramSearchSource, options);
|
||||
|
@ -128,35 +110,27 @@ export const handleRequest = async ({
|
|||
requestSearchSource.setField('query', query);
|
||||
|
||||
inspectorAdapters.requests?.reset();
|
||||
const requestResponder = getRequestMainResponder(inspectorAdapters, searchSessionId);
|
||||
|
||||
const response$ = await requestSearchSource.fetch$({
|
||||
abortSignal,
|
||||
sessionId: searchSessionId,
|
||||
requestResponder,
|
||||
});
|
||||
|
||||
// Note that rawResponse is not deeply cloned here, so downstream applications using courier
|
||||
// must take care not to mutate it, or it could have unintended side effects, e.g. displaying
|
||||
// response data incorrectly in the inspector.
|
||||
let response = await response$.toPromise();
|
||||
for (const agg of aggs.aggs) {
|
||||
if (agg.enabled && typeof agg.type.postFlightRequest === 'function') {
|
||||
response = await agg.type.postFlightRequest(
|
||||
response,
|
||||
aggs,
|
||||
agg,
|
||||
requestSearchSource,
|
||||
inspectorAdapters.requests,
|
||||
abortSignal,
|
||||
searchSessionId
|
||||
);
|
||||
}
|
||||
}
|
||||
const response = await requestSearchSource
|
||||
.fetch$({
|
||||
abortSignal,
|
||||
sessionId: searchSessionId,
|
||||
inspector: {
|
||||
adapter: inspectorAdapters.requests,
|
||||
title: i18n.translate('data.functions.esaggs.inspector.dataRequest.title', {
|
||||
defaultMessage: 'Data',
|
||||
}),
|
||||
description: i18n.translate('data.functions.esaggs.inspector.dataRequest.description', {
|
||||
defaultMessage:
|
||||
'This request queries Elasticsearch to fetch the data for the visualization.',
|
||||
}),
|
||||
},
|
||||
})
|
||||
.toPromise();
|
||||
|
||||
const parsedTimeRange = timeRange ? calculateBounds(timeRange, { forceNow }) : null;
|
||||
const tabifyParams = {
|
||||
metricsAtAllLevels,
|
||||
metricsAtAllLevels: aggs.hierarchical,
|
||||
partialRows,
|
||||
timeRange: parsedTimeRange
|
||||
? { from: parsedTimeRange.min, to: parsedTimeRange.max, timeFields: allTimeFields }
|
||||
|
|
|
@ -50,7 +50,7 @@ export function getRequestInspectorStats(searchSource: ISearchSource) {
|
|||
|
||||
/** @public */
|
||||
export function getResponseInspectorStats(
|
||||
resp: estypes.SearchResponse<unknown>,
|
||||
resp?: estypes.SearchResponse<unknown>,
|
||||
searchSource?: ISearchSource
|
||||
) {
|
||||
const lastRequest =
|
||||
|
|
|
@ -11,6 +11,10 @@ import { IndexPattern } from '../../index_patterns';
|
|||
import { GetConfigFn } from '../../types';
|
||||
import { fetchSoon } from './legacy';
|
||||
import { SearchSource, SearchSourceDependencies, SortDirection } from './';
|
||||
import { AggConfigs, AggTypesRegistryStart } from '../../';
|
||||
import { mockAggTypesRegistry } from '../aggs/test_helpers';
|
||||
import { RequestResponder } from 'src/plugins/inspector/common';
|
||||
import { switchMap } from 'rxjs/operators';
|
||||
|
||||
jest.mock('./legacy', () => ({
|
||||
fetchSoon: jest.fn().mockResolvedValue({}),
|
||||
|
@ -39,6 +43,21 @@ const indexPattern2 = ({
|
|||
getSourceFiltering: () => mockSource2,
|
||||
} as unknown) as IndexPattern;
|
||||
|
||||
const fields3 = [{ name: 'foo-bar' }, { name: 'field1' }, { name: 'field2' }];
|
||||
const indexPattern3 = ({
|
||||
title: 'foo',
|
||||
fields: {
|
||||
getByName: (name: string) => {
|
||||
return fields3.find((field) => field.name === name);
|
||||
},
|
||||
filter: () => {
|
||||
return fields3;
|
||||
},
|
||||
},
|
||||
getComputedFields,
|
||||
getSourceFiltering: () => mockSource,
|
||||
} as unknown) as IndexPattern;
|
||||
|
||||
const runtimeFieldDef = {
|
||||
type: 'keyword',
|
||||
script: {
|
||||
|
@ -61,8 +80,8 @@ describe('SearchSource', () => {
|
|||
.fn()
|
||||
.mockReturnValue(
|
||||
of(
|
||||
{ rawResponse: { isPartial: true, isRunning: true } },
|
||||
{ rawResponse: { isPartial: false, isRunning: false } }
|
||||
{ rawResponse: { test: 1 }, isPartial: true, isRunning: true },
|
||||
{ rawResponse: { test: 2 }, isPartial: false, isRunning: false }
|
||||
)
|
||||
);
|
||||
|
||||
|
@ -81,17 +100,19 @@ describe('SearchSource', () => {
|
|||
|
||||
describe('#getField()', () => {
|
||||
test('gets the value for the property', () => {
|
||||
searchSource.setField('aggs', 5);
|
||||
expect(searchSource.getField('aggs')).toBe(5);
|
||||
searchSource.setField('aggs', { i: 5 });
|
||||
expect(searchSource.getField('aggs')).toStrictEqual({ i: 5 });
|
||||
});
|
||||
});
|
||||
|
||||
describe('#getFields()', () => {
|
||||
test('gets the value for the property', () => {
|
||||
searchSource.setField('aggs', 5);
|
||||
searchSource.setField('aggs', { i: 5 });
|
||||
expect(searchSource.getFields()).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"aggs": 5,
|
||||
"aggs": Object {
|
||||
"i": 5,
|
||||
},
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
@ -100,7 +121,7 @@ describe('SearchSource', () => {
|
|||
describe('#removeField()', () => {
|
||||
test('remove property', () => {
|
||||
searchSource = new SearchSource({}, searchSourceDependencies);
|
||||
searchSource.setField('aggs', 5);
|
||||
searchSource.setField('aggs', { i: 5 });
|
||||
searchSource.removeField('aggs');
|
||||
expect(searchSource.getField('aggs')).toBeFalsy();
|
||||
});
|
||||
|
@ -108,8 +129,20 @@ describe('SearchSource', () => {
|
|||
|
||||
describe('#setField() / #flatten', () => {
|
||||
test('sets the value for the property', () => {
|
||||
searchSource.setField('aggs', 5);
|
||||
expect(searchSource.getField('aggs')).toBe(5);
|
||||
searchSource.setField('aggs', { i: 5 });
|
||||
expect(searchSource.getField('aggs')).toStrictEqual({ i: 5 });
|
||||
});
|
||||
|
||||
test('sets the value for the property with AggConfigs', () => {
|
||||
const typesRegistry = mockAggTypesRegistry();
|
||||
|
||||
const ac = new AggConfigs(indexPattern3, [{ type: 'avg', params: { field: 'field1' } }], {
|
||||
typesRegistry,
|
||||
});
|
||||
|
||||
searchSource.setField('aggs', ac);
|
||||
const request = searchSource.getSearchRequestBody();
|
||||
expect(request.aggs).toStrictEqual({ '1': { avg: { field: 'field1' } } });
|
||||
});
|
||||
|
||||
describe('computed fields handling', () => {
|
||||
|
@ -631,7 +664,7 @@ describe('SearchSource', () => {
|
|||
const fn = jest.fn();
|
||||
searchSource.onRequestStart(fn);
|
||||
const options = {};
|
||||
await searchSource.fetch(options);
|
||||
await searchSource.fetch$(options).toPromise();
|
||||
expect(fn).toBeCalledWith(searchSource, options);
|
||||
});
|
||||
|
||||
|
@ -644,7 +677,7 @@ describe('SearchSource', () => {
|
|||
const parentFn = jest.fn();
|
||||
parent.onRequestStart(parentFn);
|
||||
const options = {};
|
||||
await searchSource.fetch(options);
|
||||
await searchSource.fetch$(options).toPromise();
|
||||
|
||||
expect(fn).toBeCalledWith(searchSource, options);
|
||||
expect(parentFn).not.toBeCalled();
|
||||
|
@ -664,69 +697,13 @@ describe('SearchSource', () => {
|
|||
const parentFn = jest.fn();
|
||||
parent.onRequestStart(parentFn);
|
||||
const options = {};
|
||||
await searchSource.fetch(options);
|
||||
await searchSource.fetch$(options).toPromise();
|
||||
|
||||
expect(fn).toBeCalledWith(searchSource, options);
|
||||
expect(parentFn).toBeCalledWith(searchSource, options);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#legacy fetch()', () => {
|
||||
beforeEach(() => {
|
||||
searchSourceDependencies = {
|
||||
...searchSourceDependencies,
|
||||
getConfig: jest.fn(() => {
|
||||
return true; // batchSearches = true
|
||||
}) as GetConfigFn,
|
||||
};
|
||||
});
|
||||
|
||||
test('should call msearch', async () => {
|
||||
searchSource = new SearchSource({ index: indexPattern }, searchSourceDependencies);
|
||||
const options = {};
|
||||
await searchSource.fetch(options);
|
||||
expect(fetchSoon).toBeCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#search service fetch()', () => {
|
||||
test('should call msearch', async () => {
|
||||
searchSource = new SearchSource({ index: indexPattern }, searchSourceDependencies);
|
||||
const options = {};
|
||||
|
||||
await searchSource.fetch(options);
|
||||
expect(mockSearchMethod).toBeCalledTimes(1);
|
||||
});
|
||||
|
||||
test('should return partial results', (done) => {
|
||||
searchSource = new SearchSource({ index: indexPattern }, searchSourceDependencies);
|
||||
const options = {};
|
||||
|
||||
const next = jest.fn();
|
||||
const complete = () => {
|
||||
expect(next).toBeCalledTimes(2);
|
||||
expect(next.mock.calls[0]).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"isPartial": true,
|
||||
"isRunning": true,
|
||||
},
|
||||
]
|
||||
`);
|
||||
expect(next.mock.calls[1]).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"isPartial": false,
|
||||
"isRunning": false,
|
||||
},
|
||||
]
|
||||
`);
|
||||
done();
|
||||
};
|
||||
searchSource.fetch$(options).subscribe({ next, complete });
|
||||
});
|
||||
});
|
||||
|
||||
describe('#serialize', () => {
|
||||
test('should reference index patterns', () => {
|
||||
const indexPattern123 = { id: '123' } as IndexPattern;
|
||||
|
@ -884,4 +861,373 @@ describe('SearchSource', () => {
|
|||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('fetch$', () => {
|
||||
describe('#legacy fetch()', () => {
|
||||
beforeEach(() => {
|
||||
searchSourceDependencies = {
|
||||
...searchSourceDependencies,
|
||||
getConfig: jest.fn(() => {
|
||||
return true; // batchSearches = true
|
||||
}) as GetConfigFn,
|
||||
};
|
||||
});
|
||||
|
||||
test('should call msearch', async () => {
|
||||
searchSource = new SearchSource({ index: indexPattern }, searchSourceDependencies);
|
||||
const options = {};
|
||||
await searchSource.fetch$(options).toPromise();
|
||||
expect(fetchSoon).toBeCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('responses', () => {
|
||||
test('should return partial results', async () => {
|
||||
searchSource = new SearchSource({ index: indexPattern }, searchSourceDependencies);
|
||||
const options = {};
|
||||
|
||||
const next = jest.fn();
|
||||
const complete = jest.fn();
|
||||
const res$ = searchSource.fetch$(options);
|
||||
res$.subscribe({ next, complete });
|
||||
await res$.toPromise();
|
||||
|
||||
expect(next).toBeCalledTimes(2);
|
||||
expect(complete).toBeCalledTimes(1);
|
||||
expect(next.mock.calls[0]).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"test": 1,
|
||||
},
|
||||
]
|
||||
`);
|
||||
expect(next.mock.calls[1]).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"test": 2,
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
test('shareReplays result', async () => {
|
||||
searchSource = new SearchSource({ index: indexPattern }, searchSourceDependencies);
|
||||
const options = {};
|
||||
|
||||
const next = jest.fn();
|
||||
const complete = jest.fn();
|
||||
const next2 = jest.fn();
|
||||
const complete2 = jest.fn();
|
||||
const res$ = searchSource.fetch$(options);
|
||||
res$.subscribe({ next, complete });
|
||||
res$.subscribe({ next: next2, complete: complete2 });
|
||||
await res$.toPromise();
|
||||
|
||||
expect(next).toBeCalledTimes(2);
|
||||
expect(next2).toBeCalledTimes(2);
|
||||
expect(complete).toBeCalledTimes(1);
|
||||
expect(complete2).toBeCalledTimes(1);
|
||||
expect(searchSourceDependencies.search).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('should emit error on empty response', async () => {
|
||||
searchSourceDependencies.search = mockSearchMethod = jest
|
||||
.fn()
|
||||
.mockReturnValue(
|
||||
of({ rawResponse: { test: 1 }, isPartial: true, isRunning: true }, undefined)
|
||||
);
|
||||
|
||||
searchSource = new SearchSource({ index: indexPattern }, searchSourceDependencies);
|
||||
const options = {};
|
||||
|
||||
const next = jest.fn();
|
||||
const error = jest.fn();
|
||||
const complete = jest.fn();
|
||||
const res$ = searchSource.fetch$(options);
|
||||
res$.subscribe({ next, error, complete });
|
||||
await res$.toPromise().catch((e) => {});
|
||||
|
||||
expect(next).toBeCalledTimes(1);
|
||||
expect(error).toBeCalledTimes(1);
|
||||
expect(complete).toBeCalledTimes(0);
|
||||
expect(next.mock.calls[0]).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"test": 1,
|
||||
},
|
||||
]
|
||||
`);
|
||||
expect(error.mock.calls[0][0]).toBe(undefined);
|
||||
});
|
||||
});
|
||||
|
||||
describe('inspector', () => {
|
||||
let requestResponder: RequestResponder;
|
||||
beforeEach(() => {
|
||||
requestResponder = ({
|
||||
stats: jest.fn(),
|
||||
ok: jest.fn(),
|
||||
error: jest.fn(),
|
||||
json: jest.fn(),
|
||||
} as unknown) as RequestResponder;
|
||||
});
|
||||
|
||||
test('calls inspector if provided', async () => {
|
||||
const options = {
|
||||
inspector: {
|
||||
title: 'a',
|
||||
adapter: {
|
||||
start: jest.fn().mockReturnValue(requestResponder),
|
||||
} as any,
|
||||
},
|
||||
};
|
||||
|
||||
searchSource = new SearchSource({}, searchSourceDependencies);
|
||||
searchSource.setField('index', indexPattern);
|
||||
await searchSource.fetch$(options).toPromise();
|
||||
|
||||
expect(options.inspector.adapter.start).toBeCalledTimes(1);
|
||||
expect(requestResponder.error).not.toBeCalled();
|
||||
expect(requestResponder.json).toBeCalledTimes(1);
|
||||
expect(requestResponder.ok).toBeCalledTimes(1);
|
||||
// First and last
|
||||
expect(requestResponder.stats).toBeCalledTimes(2);
|
||||
});
|
||||
|
||||
test('calls inspector only once, with multiple subs (shareReplay)', async () => {
|
||||
const options = {
|
||||
inspector: {
|
||||
title: 'a',
|
||||
adapter: {
|
||||
start: jest.fn().mockReturnValue(requestResponder),
|
||||
} as any,
|
||||
},
|
||||
};
|
||||
|
||||
searchSource = new SearchSource({}, searchSourceDependencies);
|
||||
searchSource.setField('index', indexPattern);
|
||||
const res$ = searchSource.fetch$(options);
|
||||
|
||||
const complete1 = jest.fn();
|
||||
const complete2 = jest.fn();
|
||||
|
||||
res$.subscribe({
|
||||
complete: complete1,
|
||||
});
|
||||
res$.subscribe({
|
||||
complete: complete2,
|
||||
});
|
||||
|
||||
await res$.toPromise();
|
||||
|
||||
expect(complete1).toBeCalledTimes(1);
|
||||
expect(complete2).toBeCalledTimes(1);
|
||||
expect(options.inspector.adapter.start).toBeCalledTimes(1);
|
||||
});
|
||||
|
||||
test('calls error on inspector', async () => {
|
||||
const options = {
|
||||
inspector: {
|
||||
title: 'a',
|
||||
adapter: {
|
||||
start: jest.fn().mockReturnValue(requestResponder),
|
||||
} as any,
|
||||
},
|
||||
};
|
||||
|
||||
searchSourceDependencies.search = jest.fn().mockReturnValue(of(Promise.reject('aaaaa')));
|
||||
|
||||
searchSource = new SearchSource({}, searchSourceDependencies);
|
||||
searchSource.setField('index', indexPattern);
|
||||
await searchSource
|
||||
.fetch$(options)
|
||||
.toPromise()
|
||||
.catch(() => {});
|
||||
|
||||
expect(options.inspector.adapter.start).toBeCalledTimes(1);
|
||||
expect(requestResponder.json).toBeCalledTimes(1);
|
||||
expect(requestResponder.error).toBeCalledTimes(1);
|
||||
expect(requestResponder.ok).toBeCalledTimes(0);
|
||||
expect(requestResponder.stats).toBeCalledTimes(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('postFlightRequest', () => {
|
||||
let fetchSub: any;
|
||||
|
||||
function getAggConfigs(typesRegistry: AggTypesRegistryStart, enabled: boolean) {
|
||||
return new AggConfigs(
|
||||
indexPattern3,
|
||||
[
|
||||
{
|
||||
type: 'avg',
|
||||
enabled,
|
||||
params: { field: 'field1' },
|
||||
},
|
||||
],
|
||||
{
|
||||
typesRegistry,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
fetchSub = {
|
||||
next: jest.fn(),
|
||||
complete: jest.fn(),
|
||||
error: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
test('doesnt call any post flight requests if disabled', async () => {
|
||||
const typesRegistry = mockAggTypesRegistry();
|
||||
typesRegistry.get('avg').postFlightRequest = jest.fn();
|
||||
const ac = getAggConfigs(typesRegistry, false);
|
||||
|
||||
searchSource = new SearchSource({}, searchSourceDependencies);
|
||||
searchSource.setField('index', indexPattern);
|
||||
searchSource.setField('aggs', ac);
|
||||
const fetch$ = searchSource.fetch$({});
|
||||
fetch$.subscribe(fetchSub);
|
||||
await fetch$.toPromise();
|
||||
|
||||
expect(fetchSub.next).toHaveBeenCalledTimes(2);
|
||||
expect(fetchSub.complete).toHaveBeenCalledTimes(1);
|
||||
expect(fetchSub.error).toHaveBeenCalledTimes(0);
|
||||
|
||||
expect(typesRegistry.get('avg').postFlightRequest).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
test('doesnt call any post flight if searchsource has error', async () => {
|
||||
const typesRegistry = mockAggTypesRegistry();
|
||||
typesRegistry.get('avg').postFlightRequest = jest.fn();
|
||||
const ac = getAggConfigs(typesRegistry, true);
|
||||
|
||||
searchSourceDependencies.search = jest.fn().mockImplementation(() =>
|
||||
of(1).pipe(
|
||||
switchMap((r) => {
|
||||
throw r;
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
searchSource = new SearchSource({}, searchSourceDependencies);
|
||||
searchSource.setField('index', indexPattern);
|
||||
searchSource.setField('aggs', ac);
|
||||
const fetch$ = searchSource.fetch$({});
|
||||
fetch$.subscribe(fetchSub);
|
||||
await fetch$.toPromise().catch((e) => {});
|
||||
|
||||
expect(fetchSub.next).toHaveBeenCalledTimes(0);
|
||||
expect(fetchSub.complete).toHaveBeenCalledTimes(0);
|
||||
expect(fetchSub.error).toHaveBeenNthCalledWith(1, 1);
|
||||
|
||||
expect(typesRegistry.get('avg').postFlightRequest).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
test('calls post flight requests, fires 1 extra response, returns last response', async () => {
|
||||
const typesRegistry = mockAggTypesRegistry();
|
||||
typesRegistry.get('avg').postFlightRequest = jest.fn().mockResolvedValue({
|
||||
other: 5,
|
||||
});
|
||||
|
||||
const allac = new AggConfigs(
|
||||
indexPattern3,
|
||||
[
|
||||
{
|
||||
type: 'avg',
|
||||
enabled: true,
|
||||
params: { field: 'field1' },
|
||||
},
|
||||
{
|
||||
type: 'avg',
|
||||
enabled: true,
|
||||
params: { field: 'field2' },
|
||||
},
|
||||
{
|
||||
type: 'avg',
|
||||
enabled: true,
|
||||
params: { field: 'foo-bar' },
|
||||
},
|
||||
],
|
||||
{
|
||||
typesRegistry,
|
||||
}
|
||||
);
|
||||
|
||||
searchSource = new SearchSource({}, searchSourceDependencies);
|
||||
searchSource.setField('index', indexPattern);
|
||||
searchSource.setField('aggs', allac);
|
||||
const fetch$ = searchSource.fetch$({});
|
||||
fetch$.subscribe(fetchSub);
|
||||
|
||||
const resp = await fetch$.toPromise();
|
||||
|
||||
expect(fetchSub.next).toHaveBeenCalledTimes(3);
|
||||
expect(fetchSub.complete).toHaveBeenCalledTimes(1);
|
||||
expect(fetchSub.error).toHaveBeenCalledTimes(0);
|
||||
expect(resp).toStrictEqual({ other: 5 });
|
||||
expect(typesRegistry.get('avg').postFlightRequest).toHaveBeenCalledTimes(3);
|
||||
});
|
||||
|
||||
test('calls post flight requests only once, with multiple subs (shareReplay)', async () => {
|
||||
const typesRegistry = mockAggTypesRegistry();
|
||||
typesRegistry.get('avg').postFlightRequest = jest.fn().mockResolvedValue({
|
||||
other: 5,
|
||||
});
|
||||
|
||||
const allac = new AggConfigs(
|
||||
indexPattern3,
|
||||
[
|
||||
{
|
||||
type: 'avg',
|
||||
enabled: true,
|
||||
params: { field: 'field1' },
|
||||
},
|
||||
],
|
||||
{
|
||||
typesRegistry,
|
||||
}
|
||||
);
|
||||
|
||||
searchSource = new SearchSource({}, searchSourceDependencies);
|
||||
searchSource.setField('index', indexPattern);
|
||||
searchSource.setField('aggs', allac);
|
||||
const fetch$ = searchSource.fetch$({});
|
||||
fetch$.subscribe(fetchSub);
|
||||
|
||||
const fetchSub2 = {
|
||||
next: jest.fn(),
|
||||
complete: jest.fn(),
|
||||
error: jest.fn(),
|
||||
};
|
||||
fetch$.subscribe(fetchSub2);
|
||||
|
||||
await fetch$.toPromise();
|
||||
|
||||
expect(fetchSub.next).toHaveBeenCalledTimes(3);
|
||||
expect(fetchSub.complete).toHaveBeenCalledTimes(1);
|
||||
expect(typesRegistry.get('avg').postFlightRequest).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('calls post flight requests, handles error', async () => {
|
||||
const typesRegistry = mockAggTypesRegistry();
|
||||
typesRegistry.get('avg').postFlightRequest = jest.fn().mockRejectedValue(undefined);
|
||||
const ac = getAggConfigs(typesRegistry, true);
|
||||
|
||||
searchSource = new SearchSource({}, searchSourceDependencies);
|
||||
searchSource.setField('index', indexPattern);
|
||||
searchSource.setField('aggs', ac);
|
||||
const fetch$ = searchSource.fetch$({});
|
||||
fetch$.subscribe(fetchSub);
|
||||
|
||||
await fetch$.toPromise().catch(() => {});
|
||||
|
||||
expect(fetchSub.next).toHaveBeenCalledTimes(2);
|
||||
expect(fetchSub.complete).toHaveBeenCalledTimes(0);
|
||||
expect(fetchSub.error).toHaveBeenCalledTimes(1);
|
||||
expect(typesRegistry.get('avg').postFlightRequest).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -60,12 +60,22 @@
|
|||
|
||||
import { setWith } from '@elastic/safer-lodash-set';
|
||||
import { uniqueId, keyBy, pick, difference, isFunction, isEqual, uniqWith, isObject } from 'lodash';
|
||||
import { catchError, finalize, map, switchMap, tap } from 'rxjs/operators';
|
||||
import { defer, from } from 'rxjs';
|
||||
import {
|
||||
catchError,
|
||||
finalize,
|
||||
first,
|
||||
last,
|
||||
map,
|
||||
shareReplay,
|
||||
switchMap,
|
||||
tap,
|
||||
} from 'rxjs/operators';
|
||||
import { defer, EMPTY, from, Observable } from 'rxjs';
|
||||
import { estypes } from '@elastic/elasticsearch';
|
||||
import { normalizeSortRequest } from './normalize_sort_request';
|
||||
import { fieldWildcardFilter } from '../../../../kibana_utils/common';
|
||||
import { IIndexPattern, IndexPattern, IndexPatternField } from '../../index_patterns';
|
||||
import { ISearchGeneric, ISearchOptions } from '../..';
|
||||
import { AggConfigs, ISearchGeneric, ISearchOptions } from '../..';
|
||||
import type {
|
||||
ISearchSource,
|
||||
SearchFieldValue,
|
||||
|
@ -75,7 +85,15 @@ import type {
|
|||
import { FetchHandlers, RequestFailure, getSearchParamsFromRequest, SearchRequest } from './fetch';
|
||||
import { getRequestInspectorStats, getResponseInspectorStats } from './inspect';
|
||||
|
||||
import { getEsQueryConfig, buildEsQuery, Filter, UI_SETTINGS } from '../../../common';
|
||||
import {
|
||||
getEsQueryConfig,
|
||||
buildEsQuery,
|
||||
Filter,
|
||||
UI_SETTINGS,
|
||||
isErrorResponse,
|
||||
isPartialResponse,
|
||||
IKibanaSearchResponse,
|
||||
} from '../../../common';
|
||||
import { getHighlightRequest } from '../../../common/field_formats';
|
||||
import { fetchSoon } from './legacy';
|
||||
import { extractReferences } from './extract_references';
|
||||
|
@ -256,10 +274,8 @@ export class SearchSource {
|
|||
*/
|
||||
fetch$(options: ISearchOptions = {}) {
|
||||
const { getConfig } = this.dependencies;
|
||||
return defer(() => this.requestIsStarting(options)).pipe(
|
||||
tap(() => {
|
||||
options.requestResponder?.stats(getRequestInspectorStats(this));
|
||||
}),
|
||||
|
||||
const s$ = defer(() => this.requestIsStarting(options)).pipe(
|
||||
switchMap(() => {
|
||||
const searchRequest = this.flatten();
|
||||
this.history = [searchRequest];
|
||||
|
@ -273,21 +289,14 @@ export class SearchSource {
|
|||
}),
|
||||
tap((response) => {
|
||||
// TODO: Remove casting when https://github.com/elastic/elasticsearch-js/issues/1287 is resolved
|
||||
if ((response as any).error) {
|
||||
if (!response || (response as any).error) {
|
||||
throw new RequestFailure(null, response);
|
||||
} else {
|
||||
options.requestResponder?.stats(getResponseInspectorStats(response, this));
|
||||
options.requestResponder?.ok({ json: response });
|
||||
}
|
||||
}),
|
||||
catchError((e) => {
|
||||
options.requestResponder?.error({ json: e });
|
||||
throw e;
|
||||
}),
|
||||
finalize(() => {
|
||||
options.requestResponder?.json(this.getSearchRequestBody());
|
||||
})
|
||||
shareReplay()
|
||||
);
|
||||
|
||||
return this.inspectSearch(s$, options);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -328,9 +337,96 @@ export class SearchSource {
|
|||
* PRIVATE APIS
|
||||
******/
|
||||
|
||||
private inspectSearch(s$: Observable<estypes.SearchResponse<any>>, options: ISearchOptions) {
|
||||
const { id, title, description, adapter } = options.inspector || { title: '' };
|
||||
|
||||
const requestResponder = adapter?.start(title, {
|
||||
id,
|
||||
description,
|
||||
searchSessionId: options.sessionId,
|
||||
});
|
||||
|
||||
const trackRequestBody = () => {
|
||||
try {
|
||||
requestResponder?.json(this.getSearchRequestBody());
|
||||
} catch (e) {} // eslint-disable-line no-empty
|
||||
};
|
||||
|
||||
// Track request stats on first emit, swallow errors
|
||||
const first$ = s$
|
||||
.pipe(
|
||||
first(undefined, null),
|
||||
tap(() => {
|
||||
requestResponder?.stats(getRequestInspectorStats(this));
|
||||
trackRequestBody();
|
||||
}),
|
||||
catchError(() => {
|
||||
trackRequestBody();
|
||||
return EMPTY;
|
||||
}),
|
||||
finalize(() => {
|
||||
first$.unsubscribe();
|
||||
})
|
||||
)
|
||||
.subscribe();
|
||||
|
||||
// Track response stats on last emit, as well as errors
|
||||
const last$ = s$
|
||||
.pipe(
|
||||
catchError((e) => {
|
||||
requestResponder?.error({ json: e });
|
||||
return EMPTY;
|
||||
}),
|
||||
last(undefined, null),
|
||||
tap((finalResponse) => {
|
||||
if (finalResponse) {
|
||||
requestResponder?.stats(getResponseInspectorStats(finalResponse, this));
|
||||
requestResponder?.ok({ json: finalResponse });
|
||||
}
|
||||
}),
|
||||
finalize(() => {
|
||||
last$.unsubscribe();
|
||||
})
|
||||
)
|
||||
.subscribe();
|
||||
|
||||
return s$;
|
||||
}
|
||||
|
||||
private hasPostFlightRequests() {
|
||||
const aggs = this.getField('aggs');
|
||||
if (aggs instanceof AggConfigs) {
|
||||
return aggs.aggs.some(
|
||||
(agg) => agg.enabled && typeof agg.type.postFlightRequest === 'function'
|
||||
);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private async fetchOthers(response: estypes.SearchResponse<any>, options: ISearchOptions) {
|
||||
const aggs = this.getField('aggs');
|
||||
if (aggs instanceof AggConfigs) {
|
||||
for (const agg of aggs.aggs) {
|
||||
if (agg.enabled && typeof agg.type.postFlightRequest === 'function') {
|
||||
response = await agg.type.postFlightRequest(
|
||||
response,
|
||||
aggs,
|
||||
agg,
|
||||
this,
|
||||
options.inspector?.adapter,
|
||||
options.abortSignal,
|
||||
options.sessionId
|
||||
);
|
||||
}
|
||||
}
|
||||
return response;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Run a search using the search service
|
||||
* @return {Promise<SearchResponse<unknown>>}
|
||||
* @return {Observable<SearchResponse<any>>}
|
||||
*/
|
||||
private fetchSearch$(searchRequest: SearchRequest, options: ISearchOptions) {
|
||||
const { search, getConfig, onResponse } = this.dependencies;
|
||||
|
@ -340,6 +436,43 @@ export class SearchSource {
|
|||
});
|
||||
|
||||
return search({ params, indexType: searchRequest.indexType }, options).pipe(
|
||||
switchMap((response) => {
|
||||
return new Observable<IKibanaSearchResponse<any>>((obs) => {
|
||||
if (isErrorResponse(response)) {
|
||||
obs.error(response);
|
||||
} else if (isPartialResponse(response)) {
|
||||
obs.next(response);
|
||||
} else {
|
||||
if (!this.hasPostFlightRequests()) {
|
||||
obs.next(response);
|
||||
obs.complete();
|
||||
} else {
|
||||
// Treat the complete response as partial, then run the postFlightRequests.
|
||||
obs.next({
|
||||
...response,
|
||||
isPartial: true,
|
||||
isRunning: true,
|
||||
});
|
||||
const sub = from(this.fetchOthers(response.rawResponse, options)).subscribe({
|
||||
next: (responseWithOther) => {
|
||||
obs.next({
|
||||
...response,
|
||||
rawResponse: responseWithOther,
|
||||
});
|
||||
},
|
||||
error: (e) => {
|
||||
obs.error(e);
|
||||
sub.unsubscribe();
|
||||
},
|
||||
complete: () => {
|
||||
obs.complete();
|
||||
sub.unsubscribe();
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}),
|
||||
map(({ rawResponse }) => onResponse(searchRequest, rawResponse))
|
||||
);
|
||||
}
|
||||
|
@ -452,6 +585,12 @@ export class SearchSource {
|
|||
getConfig(UI_SETTINGS.SORT_OPTIONS)
|
||||
);
|
||||
return addToBody(key, sort);
|
||||
case 'aggs':
|
||||
if ((val as any) instanceof AggConfigs) {
|
||||
return addToBody('aggs', val.toDsl());
|
||||
} else {
|
||||
return addToBody('aggs', val);
|
||||
}
|
||||
default:
|
||||
return addToBody(key, val);
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
*/
|
||||
|
||||
import { NameList } from 'elasticsearch';
|
||||
import { IAggConfigs } from 'src/plugins/data/public';
|
||||
import { Query } from '../..';
|
||||
import { Filter } from '../../es_query';
|
||||
import { IndexPattern } from '../../index_patterns';
|
||||
|
@ -78,7 +79,7 @@ export interface SearchSourceFields {
|
|||
/**
|
||||
* {@link AggConfigs}
|
||||
*/
|
||||
aggs?: any;
|
||||
aggs?: object | IAggConfigs | (() => object);
|
||||
from?: number;
|
||||
size?: number;
|
||||
source?: NameList;
|
||||
|
|
|
@ -6,27 +6,6 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { SearchResponse } from 'elasticsearch';
|
||||
import { SearchSource } from '../search_source';
|
||||
import { tabifyAggResponse } from './tabify';
|
||||
import { tabifyDocs, TabifyDocsOptions } from './tabify_docs';
|
||||
import { TabbedResponseWriterOptions } from './types';
|
||||
|
||||
export const tabify = (
|
||||
searchSource: SearchSource,
|
||||
esResponse: SearchResponse<unknown>,
|
||||
opts: Partial<TabbedResponseWriterOptions> | TabifyDocsOptions
|
||||
) => {
|
||||
return !esResponse.aggregations
|
||||
? tabifyDocs(esResponse, searchSource.getField('index'), opts as TabifyDocsOptions)
|
||||
: tabifyAggResponse(
|
||||
searchSource.getField('aggs'),
|
||||
esResponse,
|
||||
opts as Partial<TabbedResponseWriterOptions>
|
||||
);
|
||||
};
|
||||
|
||||
export { tabifyDocs };
|
||||
|
||||
export { tabifyDocs } from './tabify_docs';
|
||||
export { tabifyAggResponse } from './tabify';
|
||||
export { tabifyGetColumns } from './get_columns';
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
import { Observable } from 'rxjs';
|
||||
import { IEsSearchRequest, IEsSearchResponse } from './es_search';
|
||||
import { IndexPattern } from '..';
|
||||
import type { RequestResponder } from '../../../inspector/common';
|
||||
import type { RequestAdapter } from '../../../inspector/common';
|
||||
|
||||
export type ISearchGeneric = <
|
||||
SearchStrategyRequest extends IKibanaSearchRequest = IEsSearchRequest,
|
||||
|
@ -81,6 +81,13 @@ export interface IKibanaSearchRequest<Params = any> {
|
|||
params?: Params;
|
||||
}
|
||||
|
||||
export interface IInspectorInfo {
|
||||
adapter?: RequestAdapter;
|
||||
title: string;
|
||||
id?: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export interface ISearchOptions {
|
||||
/**
|
||||
* An `AbortSignal` that allows the caller of `search` to abort a search request.
|
||||
|
@ -117,10 +124,12 @@ export interface ISearchOptions {
|
|||
/**
|
||||
* Index pattern reference is used for better error messages
|
||||
*/
|
||||
|
||||
indexPattern?: IndexPattern;
|
||||
|
||||
requestResponder?: RequestResponder;
|
||||
/**
|
||||
* Inspector integration options
|
||||
*/
|
||||
inspector?: IInspectorInfo;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -46,6 +46,7 @@ import { FormatFactory as FormatFactory_2 } from 'src/plugins/data/common/field_
|
|||
import { History } from 'history';
|
||||
import { Href } from 'history';
|
||||
import { HttpSetup } from 'kibana/public';
|
||||
import { IAggConfigs as IAggConfigs_2 } from 'src/plugins/data/public';
|
||||
import { IconType } from '@elastic/eui';
|
||||
import { IncomingHttpHeaders } from 'http';
|
||||
import { InjectedIntl } from '@kbn/i18n/react';
|
||||
|
@ -254,6 +255,8 @@ export class AggConfigs {
|
|||
getResponseAggById(id: string): AggConfig | undefined;
|
||||
getResponseAggs(): AggConfig[];
|
||||
// (undocumented)
|
||||
hierarchical?: boolean;
|
||||
// (undocumented)
|
||||
indexPattern: IndexPattern;
|
||||
jsonDataEquals(aggConfigs: AggConfig[]): boolean;
|
||||
// (undocumented)
|
||||
|
@ -267,7 +270,7 @@ export class AggConfigs {
|
|||
// (undocumented)
|
||||
timeRange?: TimeRange;
|
||||
// (undocumented)
|
||||
toDsl(hierarchical?: boolean): Record<string, any>;
|
||||
toDsl(): Record<string, any>;
|
||||
}
|
||||
|
||||
// @internal (undocumented)
|
||||
|
@ -1672,13 +1675,11 @@ export type ISearchGeneric = <SearchStrategyRequest extends IKibanaSearchRequest
|
|||
export interface ISearchOptions {
|
||||
abortSignal?: AbortSignal;
|
||||
indexPattern?: IndexPattern;
|
||||
// Warning: (ae-forgotten-export) The symbol "IInspectorInfo" needs to be exported by the entry point index.d.ts
|
||||
inspector?: IInspectorInfo;
|
||||
isRestore?: boolean;
|
||||
isStored?: boolean;
|
||||
legacyHitsTotal?: boolean;
|
||||
// Warning: (ae-forgotten-export) The symbol "RequestResponder" needs to be exported by the entry point index.d.ts
|
||||
//
|
||||
// (undocumented)
|
||||
requestResponder?: RequestResponder;
|
||||
sessionId?: string;
|
||||
strategy?: string;
|
||||
}
|
||||
|
@ -2430,9 +2431,9 @@ export class SearchSource {
|
|||
createChild(options?: {}): SearchSource;
|
||||
createCopy(): SearchSource;
|
||||
destroy(): void;
|
||||
fetch$(options?: ISearchOptions): import("rxjs").Observable<import("@elastic/elasticsearch/api/types").SearchResponse<any>>;
|
||||
fetch$(options?: ISearchOptions): Observable<estypes.SearchResponse<any>>;
|
||||
// @deprecated
|
||||
fetch(options?: ISearchOptions): Promise<import("@elastic/elasticsearch/api/types").SearchResponse<any>>;
|
||||
fetch(options?: ISearchOptions): Promise<estypes.SearchResponse<any>>;
|
||||
getField<K extends keyof SearchSourceFields>(field: K, recurse?: boolean): SearchSourceFields[K];
|
||||
getFields(): SearchSourceFields;
|
||||
getId(): string;
|
||||
|
@ -2462,7 +2463,7 @@ export class SearchSource {
|
|||
// @public
|
||||
export interface SearchSourceFields {
|
||||
// (undocumented)
|
||||
aggs?: any;
|
||||
aggs?: object | IAggConfigs_2 | (() => object);
|
||||
// Warning: (ae-forgotten-export) The symbol "SearchFieldValue" needs to be exported by the entry point index.d.ts
|
||||
fields?: SearchFieldValue[];
|
||||
// @deprecated
|
||||
|
|
|
@ -100,17 +100,20 @@ describe('esaggs expression function - public', () => {
|
|||
|
||||
expect(handleEsaggsRequest).toHaveBeenCalledWith({
|
||||
abortSignal: mockHandlers.abortSignal,
|
||||
aggs: { foo: 'bar' },
|
||||
aggs: {
|
||||
foo: 'bar',
|
||||
hierarchical: true,
|
||||
},
|
||||
filters: undefined,
|
||||
indexPattern: {},
|
||||
inspectorAdapters: mockHandlers.inspectorAdapters,
|
||||
metricsAtAllLevels: args.metricsAtAllLevels,
|
||||
partialRows: args.partialRows,
|
||||
query: undefined,
|
||||
searchSessionId: 'abc123',
|
||||
searchSourceService: startDependencies.searchSource,
|
||||
timeFields: args.timeFields,
|
||||
timeRange: undefined,
|
||||
getNow: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -8,7 +8,6 @@
|
|||
|
||||
import { get } from 'lodash';
|
||||
import { StartServicesAccessor } from 'src/core/public';
|
||||
import { Adapters } from 'src/plugins/inspector/common';
|
||||
import {
|
||||
EsaggsExpressionFunctionDefinition,
|
||||
EsaggsStartDependencies,
|
||||
|
@ -44,14 +43,14 @@ export function getFunctionDefinition({
|
|||
indexPattern,
|
||||
args.aggs!.map((agg) => agg.value)
|
||||
);
|
||||
aggConfigs.hierarchical = args.metricsAtAllLevels;
|
||||
|
||||
return await handleEsaggsRequest({
|
||||
abortSignal: (abortSignal as unknown) as AbortSignal,
|
||||
abortSignal,
|
||||
aggs: aggConfigs,
|
||||
filters: get(input, 'filters', undefined),
|
||||
indexPattern,
|
||||
inspectorAdapters: inspectorAdapters as Adapters,
|
||||
metricsAtAllLevels: args.metricsAtAllLevels,
|
||||
inspectorAdapters,
|
||||
partialRows: args.partialRows,
|
||||
query: get(input, 'query', undefined) as any,
|
||||
searchSessionId: getSearchSessionId(),
|
||||
|
|
|
@ -108,11 +108,13 @@ describe('esaggs expression function - server', () => {
|
|||
|
||||
expect(handleEsaggsRequest).toHaveBeenCalledWith({
|
||||
abortSignal: mockHandlers.abortSignal,
|
||||
aggs: { foo: 'bar' },
|
||||
aggs: {
|
||||
foo: 'bar',
|
||||
hierarchical: args.metricsAtAllLevels,
|
||||
},
|
||||
filters: undefined,
|
||||
indexPattern: {},
|
||||
inspectorAdapters: mockHandlers.inspectorAdapters,
|
||||
metricsAtAllLevels: args.metricsAtAllLevels,
|
||||
partialRows: args.partialRows,
|
||||
query: undefined,
|
||||
searchSessionId: 'abc123',
|
||||
|
|
|
@ -9,7 +9,6 @@
|
|||
import { get } from 'lodash';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { KibanaRequest, StartServicesAccessor } from 'src/core/server';
|
||||
import { Adapters } from 'src/plugins/inspector/common';
|
||||
import {
|
||||
EsaggsExpressionFunctionDefinition,
|
||||
EsaggsStartDependencies,
|
||||
|
@ -61,13 +60,14 @@ export function getFunctionDefinition({
|
|||
args.aggs!.map((agg) => agg.value)
|
||||
);
|
||||
|
||||
aggConfigs.hierarchical = args.metricsAtAllLevels;
|
||||
|
||||
return await handleEsaggsRequest({
|
||||
abortSignal: (abortSignal as unknown) as AbortSignal,
|
||||
abortSignal,
|
||||
aggs: aggConfigs,
|
||||
filters: get(input, 'filters', undefined),
|
||||
indexPattern,
|
||||
inspectorAdapters: inspectorAdapters as Adapters,
|
||||
metricsAtAllLevels: args.metricsAtAllLevels,
|
||||
inspectorAdapters,
|
||||
partialRows: args.partialRows,
|
||||
query: get(input, 'query', undefined) as any,
|
||||
searchSessionId: getSearchSessionId(),
|
||||
|
|
|
@ -26,12 +26,14 @@ import { Ensure } from '@kbn/utility-types';
|
|||
import { EnvironmentMode } from '@kbn/config';
|
||||
import { ErrorToastOptions } from 'src/core/public/notifications';
|
||||
import { estypes } from '@elastic/elasticsearch';
|
||||
import { EventEmitter } from 'events';
|
||||
import { ExecutionContext } from 'src/plugins/expressions/common';
|
||||
import { ExpressionAstExpression } from 'src/plugins/expressions/common';
|
||||
import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common';
|
||||
import { ExpressionsServerSetup } from 'src/plugins/expressions/server';
|
||||
import { ExpressionValueBoxed } from 'src/plugins/expressions/common';
|
||||
import { FormatFactory as FormatFactory_2 } from 'src/plugins/data/common/field_formats/utils';
|
||||
import { IAggConfigs as IAggConfigs_2 } from 'src/plugins/data/public';
|
||||
import { ISavedObjectsRepository } from 'src/core/server';
|
||||
import { IScopedClusterClient } from 'src/core/server';
|
||||
import { ISearchOptions as ISearchOptions_2 } from 'src/plugins/data/public';
|
||||
|
@ -999,13 +1001,11 @@ export interface IScopedSearchClient extends ISearchClient {
|
|||
export interface ISearchOptions {
|
||||
abortSignal?: AbortSignal;
|
||||
indexPattern?: IndexPattern;
|
||||
// Warning: (ae-forgotten-export) The symbol "IInspectorInfo" needs to be exported by the entry point index.d.ts
|
||||
inspector?: IInspectorInfo;
|
||||
isRestore?: boolean;
|
||||
isStored?: boolean;
|
||||
legacyHitsTotal?: boolean;
|
||||
// Warning: (ae-forgotten-export) The symbol "RequestResponder" needs to be exported by the entry point index.d.ts
|
||||
//
|
||||
// (undocumented)
|
||||
requestResponder?: RequestResponder;
|
||||
sessionId?: string;
|
||||
strategy?: string;
|
||||
}
|
||||
|
|
|
@ -415,11 +415,20 @@ function discoverController($route, $scope) {
|
|||
$scope.fetchStatus = fetchStatuses.LOADING;
|
||||
$scope.resultState = getResultState($scope.fetchStatus, $scope.rows);
|
||||
|
||||
inspectorAdapters.requests.reset();
|
||||
return $scope.volatileSearchSource
|
||||
.fetch$({
|
||||
abortSignal: abortController.signal,
|
||||
sessionId: searchSessionId,
|
||||
requestResponder: getRequestResponder({ searchSessionId }),
|
||||
inspector: {
|
||||
adapter: inspectorAdapters.requests,
|
||||
title: i18n.translate('discover.inspectorRequestDataTitle', {
|
||||
defaultMessage: 'data',
|
||||
}),
|
||||
description: i18n.translate('discover.inspectorRequestDescription', {
|
||||
defaultMessage: 'This request queries Elasticsearch to fetch the data for the search.',
|
||||
}),
|
||||
},
|
||||
})
|
||||
.toPromise()
|
||||
.then(onResults)
|
||||
|
@ -465,17 +474,6 @@ function discoverController($route, $scope) {
|
|||
await refetch$.next();
|
||||
};
|
||||
|
||||
function getRequestResponder({ searchSessionId = null } = { searchSessionId: null }) {
|
||||
inspectorAdapters.requests.reset();
|
||||
const title = i18n.translate('discover.inspectorRequestDataTitle', {
|
||||
defaultMessage: 'data',
|
||||
});
|
||||
const description = i18n.translate('discover.inspectorRequestDescription', {
|
||||
defaultMessage: 'This request queries Elasticsearch to fetch the data for the search.',
|
||||
});
|
||||
return inspectorAdapters.requests.start(title, { description, searchSessionId });
|
||||
}
|
||||
|
||||
$scope.resetQuery = function () {
|
||||
history.push(
|
||||
$route.current.params.id ? `/view/${encodeURIComponent($route.current.params.id)}` : '/'
|
||||
|
|
|
@ -317,17 +317,6 @@ export class SearchEmbeddable
|
|||
|
||||
// Log request to inspector
|
||||
this.inspectorAdapters.requests!.reset();
|
||||
const title = i18n.translate('discover.embeddable.inspectorRequestDataTitle', {
|
||||
defaultMessage: 'Data',
|
||||
});
|
||||
const description = i18n.translate('discover.embeddable.inspectorRequestDescription', {
|
||||
defaultMessage: 'This request queries Elasticsearch to fetch the data for the search.',
|
||||
});
|
||||
|
||||
const requestResponder = this.inspectorAdapters.requests!.start(title, {
|
||||
description,
|
||||
searchSessionId,
|
||||
});
|
||||
|
||||
this.searchScope.$apply(() => {
|
||||
this.searchScope!.isLoading = true;
|
||||
|
@ -340,7 +329,16 @@ export class SearchEmbeddable
|
|||
.fetch$({
|
||||
abortSignal: this.abortController.signal,
|
||||
sessionId: searchSessionId,
|
||||
requestResponder,
|
||||
inspector: {
|
||||
adapter: this.inspectorAdapters.requests,
|
||||
title: i18n.translate('discover.embeddable.inspectorRequestDataTitle', {
|
||||
defaultMessage: 'Data',
|
||||
}),
|
||||
description: i18n.translate('discover.embeddable.inspectorRequestDescription', {
|
||||
defaultMessage:
|
||||
'This request queries Elasticsearch to fetch the data for the search.',
|
||||
}),
|
||||
},
|
||||
})
|
||||
.toPromise();
|
||||
this.updateOutput({ loading: false, error: undefined });
|
||||
|
|
|
@ -167,12 +167,6 @@ export class AbstractESSource extends AbstractVectorSource implements IESSource
|
|||
const abortController = new AbortController();
|
||||
registerCancelCallback(() => abortController.abort());
|
||||
|
||||
const requestResponder = this.getInspectorAdapters()?.requests?.start(requestName, {
|
||||
id: requestId,
|
||||
description: requestDescription,
|
||||
searchSessionId,
|
||||
});
|
||||
|
||||
let resp;
|
||||
try {
|
||||
resp = await searchSource
|
||||
|
@ -180,7 +174,12 @@ export class AbstractESSource extends AbstractVectorSource implements IESSource
|
|||
abortSignal: abortController.signal,
|
||||
sessionId: searchSessionId,
|
||||
legacyHitsTotal: false,
|
||||
requestResponder,
|
||||
inspector: {
|
||||
adapter: this.getInspectorAdapters()?.requests,
|
||||
id: requestId,
|
||||
title: requestName,
|
||||
description: requestDescription,
|
||||
},
|
||||
})
|
||||
.toPromise();
|
||||
} catch (error) {
|
||||
|
|
|
@ -23,7 +23,8 @@ export default function ({ getService, loadTestFile }: PluginFunctionalProviderC
|
|||
await esArchiver.unload('lens/basic');
|
||||
});
|
||||
|
||||
loadTestFile(require.resolve('./search_sessions_cache'));
|
||||
loadTestFile(require.resolve('./search_session_example'));
|
||||
loadTestFile(require.resolve('./search_example'));
|
||||
loadTestFile(require.resolve('./search_sessions_cache'));
|
||||
});
|
||||
}
|
||||
|
|
38
x-pack/test/examples/search_examples/search_example.ts
Normal file
38
x-pack/test/examples/search_examples/search_example.ts
Normal file
|
@ -0,0 +1,38 @@
|
|||
/*
|
||||
* 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; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { FtrProviderContext } from '../../functional/ftr_provider_context';
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
||||
const testSubjects = getService('testSubjects');
|
||||
const PageObjects = getPageObjects(['common', 'timePicker']);
|
||||
const retry = getService('retry');
|
||||
|
||||
describe.skip('Search session example', () => {
|
||||
const appId = 'searchExamples';
|
||||
|
||||
before(async function () {
|
||||
await PageObjects.common.navigateToApp(appId, { insertTimestamp: false });
|
||||
});
|
||||
|
||||
it('should have an other bucket', async () => {
|
||||
await PageObjects.timePicker.setAbsoluteRange(
|
||||
'Jan 1, 2014 @ 00:00:00.000',
|
||||
'Jan 1, 2016 @ 00:00:00.000'
|
||||
);
|
||||
await testSubjects.click('searchSourceWithOther');
|
||||
|
||||
await retry.waitFor('has other bucket', async () => {
|
||||
await testSubjects.click('responseTab');
|
||||
const codeBlock = await testSubjects.find('responseCodeBlock');
|
||||
const visibleText = await codeBlock.getVisibleText();
|
||||
return visibleText.indexOf('__other__') > -1;
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue