[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:
Liza Katz 2021-04-18 12:50:02 +03:00 committed by GitHub
parent 3b31d81196
commit 1a3e033c90
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
38 changed files with 964 additions and 423 deletions

View file

@ -0,0 +1,11 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) &gt; [AggConfigs](./kibana-plugin-plugins-data-public.aggconfigs.md) &gt; [hierarchical](./kibana-plugin-plugins-data-public.aggconfigs.hierarchical.md)
## AggConfigs.hierarchical property
<b>Signature:</b>
```typescript
hierarchical?: boolean;
```

View file

@ -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>&lt;T extends AggConfig = AggConfig&gt;(params: CreateAggConfigParams, { addToAggConfigs }?: {</code><br/><code> addToAggConfigs?: boolean &#124; undefined;</code><br/><code> }) =&gt; 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) | | |

View file

@ -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>`

View file

@ -1,11 +1,13 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) &gt; [ISearchOptions](./kibana-plugin-plugins-data-public.isearchoptions.md) &gt; [requestResponder](./kibana-plugin-plugins-data-public.isearchoptions.requestresponder.md)
[Home](./index.md) &gt; [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) &gt; [ISearchOptions](./kibana-plugin-plugins-data-public.isearchoptions.md) &gt; [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;
```

View file

@ -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. |

View file

@ -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>>`

View file

@ -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>>`

View file

@ -9,5 +9,5 @@
<b>Signature:</b>
```typescript
aggs?: any;
aggs?: object | IAggConfigs | (() => object);
```

View file

@ -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 &#124; IAggConfigs &#124; (() =&gt; 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[] &#124; Filter &#124; (() =&gt; Filter[] &#124; Filter &#124; undefined)</code> | [Filter](./kibana-plugin-plugins-data-public.filter.md) |

View file

@ -1,11 +1,13 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) &gt; [ISearchOptions](./kibana-plugin-plugins-data-server.isearchoptions.md) &gt; [requestResponder](./kibana-plugin-plugins-data-server.isearchoptions.requestresponder.md)
[Home](./index.md) &gt; [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) &gt; [ISearchOptions](./kibana-plugin-plugins-data-server.isearchoptions.md) &gt; [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;
```

View file

@ -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. |

View file

@ -0,0 +1,6 @@
@import '@elastic/eui/src/global_styling/variables/header';
.searchExampleStepDsc {
padding-left: $euiSizeXL;
font-style: italic;
}

View file

@ -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>

View file

@ -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');

View file

@ -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) {

View file

@ -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.

View file

@ -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();

View file

@ -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
) => {

View file

@ -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();

View file

@ -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,
}

View file

@ -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 }

View file

@ -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 =

View file

@ -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);
});
});
});
});

View file

@ -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);
}

View file

@ -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;

View file

@ -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';

View file

@ -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;
}
/**

View file

@ -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

View file

@ -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,
});
});

View file

@ -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(),

View file

@ -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',

View file

@ -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(),

View file

@ -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;
}

View file

@ -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)}` : '/'

View file

@ -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 });

View file

@ -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) {

View file

@ -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'));
});
}

View 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;
});
});
});
}