[esaggs][inspector]: Refactor to prep for esaggs move to server. (#83199)

This commit is contained in:
Luke Elmers 2020-11-18 09:11:05 -07:00 committed by GitHub
parent b3eefb97da
commit 62e06aee9b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
34 changed files with 629 additions and 486 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-embeddable-public](./kibana-plugin-plugins-embeddable-public.md) &gt; [Adapters](./kibana-plugin-plugins-embeddable-public.adapters.md) &gt; [data](./kibana-plugin-plugins-embeddable-public.adapters.data.md)
## Adapters.data property
<b>Signature:</b>
```typescript
data?: DataAdapter;
```

View file

@ -11,3 +11,11 @@ The interface that the adapters used to open an inspector have to fullfill.
```typescript
export interface Adapters
```
## Properties
| Property | Type | Description |
| --- | --- | --- |
| [data](./kibana-plugin-plugins-embeddable-public.adapters.data.md) | <code>DataAdapter</code> | |
| [requests](./kibana-plugin-plugins-embeddable-public.adapters.requests.md) | <code>RequestAdapter</code> | |

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-embeddable-public](./kibana-plugin-plugins-embeddable-public.md) &gt; [Adapters](./kibana-plugin-plugins-embeddable-public.adapters.md) &gt; [requests](./kibana-plugin-plugins-embeddable-public.adapters.requests.md)
## Adapters.requests property
<b>Signature:</b>
```typescript
requests?: RequestAdapter;
```

View file

@ -54,7 +54,7 @@ export interface AggTypeConfig<
aggConfigs: IAggConfigs,
aggConfig: TAggConfig,
searchSource: ISearchSource,
inspectorRequestAdapter: RequestAdapter,
inspectorRequestAdapter?: RequestAdapter,
abortSignal?: AbortSignal
) => Promise<any>;
getSerializedFormat?: (agg: TAggConfig) => SerializedFieldFormat;
@ -189,7 +189,7 @@ export class AggType<
aggConfigs: IAggConfigs,
aggConfig: TAggConfig,
searchSource: ISearchSource,
inspectorRequestAdapter: RequestAdapter,
inspectorRequestAdapter?: RequestAdapter,
abortSignal?: AbortSignal
) => Promise<any>;
/**

View file

@ -19,6 +19,7 @@
import { noop } from 'lodash';
import { i18n } from '@kbn/i18n';
import type { RequestAdapter } from 'src/plugins/inspector/common';
import { BucketAggType, IBucketAggConfig } from './bucket_agg_type';
import { BUCKET_TYPES } from './bucket_agg_types';
@ -111,27 +112,32 @@ export const getTermsBucketAgg = () =>
nestedSearchSource.setField('aggs', filterAgg);
const request = 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.',
let request: ReturnType<RequestAdapter['start']> | undefined;
if (inspectorRequestAdapter) {
request = inspectorRequestAdapter.start(
i18n.translate('data.search.aggs.buckets.terms.otherBucketTitle', {
defaultMessage: 'Other bucket',
}),
}
);
nestedSearchSource.getSearchRequestBody().then((body) => {
request.json(body);
});
request.stats(getRequestInspectorStats(nestedSearchSource));
{
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.',
}),
}
);
nestedSearchSource.getSearchRequestBody().then((body) => {
request!.json(body);
});
request.stats(getRequestInspectorStats(nestedSearchSource));
}
const response = await nestedSearchSource.fetch({ abortSignal });
request
.stats(getResponseInspectorStats(response, nestedSearchSource))
.ok({ json: response });
if (request) {
request
.stats(getResponseInspectorStats(response, nestedSearchSource))
.ok({ json: response });
}
resp = mergeOtherBucketAggResponse(aggConfigs, resp, response, aggConfig, filterAgg());
}
if (aggConfig.params.missingBucket) {

View file

@ -18,6 +18,7 @@
*/
import { CoreSetup, PluginInitializerContext } from 'src/core/public';
import { TimefilterSetup } from '../query';
import { QuerySuggestionGetFn } from './providers/query_suggestion_provider';
import {
getEmptyValueSuggestions,
@ -57,9 +58,9 @@ export class AutocompleteService {
private hasQuerySuggestions = (language: string) => this.querySuggestionProviders.has(language);
/** @public **/
public setup(core: CoreSetup) {
public setup(core: CoreSetup, { timefilter }: { timefilter: TimefilterSetup }) {
this.getValueSuggestions = this.autocompleteConfig.valueSuggestions.enabled
? setupValueSuggestionProvider(core)
? setupValueSuggestionProvider(core, { timefilter })
: getEmptyValueSuggestions;
return {

View file

@ -18,29 +18,10 @@
*/
import { stubIndexPattern, stubFields } from '../../stubs';
import { TimefilterSetup } from '../../query';
import { setupValueSuggestionProvider, ValueSuggestionsGetFn } from './value_suggestion_provider';
import { IUiSettingsClient, CoreSetup } from 'kibana/public';
jest.mock('../../services', () => ({
getQueryService: () => ({
timefilter: {
timefilter: {
createFilter: () => {
return {
time: 'fake',
};
},
getTime: () => {
return {
to: 'now',
from: 'now-15m',
};
},
},
},
}),
}));
describe('FieldSuggestions', () => {
let getValueSuggestions: ValueSuggestionsGetFn;
let http: any;
@ -50,7 +31,23 @@ describe('FieldSuggestions', () => {
const uiSettings = { get: (key: string) => shouldSuggestValues } as IUiSettingsClient;
http = { fetch: jest.fn() };
getValueSuggestions = setupValueSuggestionProvider({ http, uiSettings } as CoreSetup);
getValueSuggestions = setupValueSuggestionProvider({ http, uiSettings } as CoreSetup, {
timefilter: ({
timefilter: {
createFilter: () => {
return {
time: 'fake',
};
},
getTime: () => {
return {
to: 'now',
from: 'now-15m',
};
},
},
} as unknown) as TimefilterSetup,
});
});
describe('with value suggestions disabled', () => {

View file

@ -21,7 +21,7 @@ import dateMath from '@elastic/datemath';
import { memoize } from 'lodash';
import { CoreSetup } from 'src/core/public';
import { IIndexPattern, IFieldType, UI_SETTINGS, buildQueryFromFilters } from '../../../common';
import { getQueryService } from '../../services';
import { TimefilterSetup } from '../../query';
function resolver(title: string, field: IFieldType, query: string, filters: any[]) {
// Only cache results for a minute
@ -40,8 +40,10 @@ interface ValueSuggestionsGetFnArgs {
signal?: AbortSignal;
}
const getAutocompleteTimefilter = (indexPattern: IIndexPattern) => {
const { timefilter } = getQueryService().timefilter;
const getAutocompleteTimefilter = (
{ timefilter }: TimefilterSetup,
indexPattern: IIndexPattern
) => {
const timeRange = timefilter.getTime();
// Use a rounded timerange so that memoizing works properly
@ -54,7 +56,10 @@ const getAutocompleteTimefilter = (indexPattern: IIndexPattern) => {
export const getEmptyValueSuggestions = (() => Promise.resolve([])) as ValueSuggestionsGetFn;
export const setupValueSuggestionProvider = (core: CoreSetup): ValueSuggestionsGetFn => {
export const setupValueSuggestionProvider = (
core: CoreSetup,
{ timefilter }: { timefilter: TimefilterSetup }
): ValueSuggestionsGetFn => {
const requestSuggestions = memoize(
(index: string, field: IFieldType, query: string, filters: any = [], signal?: AbortSignal) =>
core.http.fetch(`/api/kibana/suggestions/values/${index}`, {
@ -86,7 +91,9 @@ export const setupValueSuggestionProvider = (core: CoreSetup): ValueSuggestionsG
return [];
}
const timeFilter = useTimeRange ? getAutocompleteTimefilter(indexPattern) : undefined;
const timeFilter = useTimeRange
? getAutocompleteTimefilter(timefilter, indexPattern)
: undefined;
const filterQuery = timeFilter ? buildQueryFromFilters([timeFilter], indexPattern).filter : [];
const filters = [...(boolFilter ? boolFilter : []), ...filterQuery];
return await requestSuggestions(title, field, query, filters, signal);

View file

@ -20,20 +20,9 @@
import sinon from 'sinon';
import { CoreSetup } from 'src/core/public';
import { FieldFormat as FieldFormatImpl } from '../../common/field_formats';
import { IFieldType, FieldSpec } from '../../common/index_patterns';
import { FieldFormatsStart } from '../field_formats';
import { IndexPattern, indexPatterns, KBN_FIELD_TYPES, fieldList } from '../';
import { getFieldFormatsRegistry } from '../test_utils';
import { setFieldFormats } from '../services';
setFieldFormats(({
getDefaultInstance: () =>
({
getConverterFor: () => (value: any) => value,
convert: (value: any) => JSON.stringify(value),
} as FieldFormatImpl),
} as unknown) as FieldFormatsStart);
export function getStubIndexPattern(
pattern: string,

View file

@ -41,16 +41,14 @@ import {
UiSettingsPublicToCommon,
} from './index_patterns';
import {
setFieldFormats,
setIndexPatterns,
setNotifications,
setOverlays,
setQueryService,
setSearchService,
setUiSettings,
} from './services';
import { createSearchBar } from './ui/search_bar/create_search_bar';
import { esaggs } from './search/expressions';
import { getEsaggs } from './search/expressions';
import {
SELECT_RANGE_TRIGGER,
VALUE_CLICK_TRIGGER,
@ -111,8 +109,22 @@ export class DataPublicPlugin
): DataPublicPluginSetup {
const startServices = createStartServicesGetter(core.getStartServices);
expressions.registerFunction(esaggs);
expressions.registerFunction(indexPatternLoad);
expressions.registerFunction(
getEsaggs({
getStartDependencies: async () => {
const [, , self] = await core.getStartServices();
const { fieldFormats, indexPatterns, query, search } = self;
return {
addFilters: query.filterManager.addFilters.bind(query.filterManager),
aggs: search.aggs,
deserializeFieldFormat: fieldFormats.deserialize.bind(fieldFormats),
indexPatterns,
searchSource: search.searchSource,
};
},
})
);
this.usageCollection = usageCollection;
@ -145,7 +157,7 @@ export class DataPublicPlugin
});
return {
autocomplete: this.autocomplete.setup(core),
autocomplete: this.autocomplete.setup(core, { timefilter: queryService.timefilter }),
search: searchService,
fieldFormats: this.fieldFormatsService.setup(core),
query: queryService,
@ -162,7 +174,6 @@ export class DataPublicPlugin
setUiSettings(uiSettings);
const fieldFormats = this.fieldFormatsService.start();
setFieldFormats(fieldFormats);
const indexPatterns = new IndexPatternsService({
uiSettings: new UiSettingsPublicToCommon(uiSettings),
@ -186,7 +197,6 @@ export class DataPublicPlugin
savedObjectsClient: savedObjects.client,
uiSettings,
});
setQueryService(query);
const search = this.searchService.start(core, { fieldFormats, indexPatterns });
setSearchService(search);

View file

@ -27,6 +27,7 @@ import { EuiButtonEmptyProps } from '@elastic/eui';
import { EuiComboBoxProps } from '@elastic/eui';
import { EuiConfirmModalProps } from '@elastic/eui';
import { EuiGlobalToastListToast } from '@elastic/eui';
import { EventEmitter } from 'events';
import { ExclusiveUnion } from '@elastic/eui';
import { ExecutionContext } from 'src/plugins/expressions/common';
import { ExpressionAstFunction } from 'src/plugins/expressions/common';
@ -66,7 +67,7 @@ import * as React_2 from 'react';
import { RecursiveReadonly } from '@kbn/utility-types';
import { Reporter } from '@kbn/analytics';
import { RequestAdapter } from 'src/plugins/inspector/common';
import { RequestStatistics } from 'src/plugins/inspector/common';
import { RequestStatistics as RequestStatistics_2 } from 'src/plugins/inspector/common';
import { Required } from '@kbn/utility-types';
import * as Rx from 'rxjs';
import { SavedObject } from 'src/core/server';

View file

@ -1,323 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { get, hasIn } from 'lodash';
import { i18n } from '@kbn/i18n';
import { Datatable, DatatableColumn } from 'src/plugins/expressions/public';
import { PersistedState } from '../../../../../plugins/visualizations/public';
import { Adapters } from '../../../../../plugins/inspector/public';
import {
calculateBounds,
EsaggsExpressionFunctionDefinition,
Filter,
getTime,
IIndexPattern,
isRangeFilter,
Query,
TimeRange,
} from '../../../common';
import {
getRequestInspectorStats,
getResponseInspectorStats,
IAggConfigs,
ISearchSource,
tabifyAggResponse,
} from '../../../common/search';
import { FilterManager } from '../../query';
import {
getFieldFormats,
getIndexPatterns,
getQueryService,
getSearchService,
} from '../../services';
import { buildTabularInspectorData } from './build_tabular_inspector_data';
export interface RequestHandlerParams {
searchSource: ISearchSource;
aggs: IAggConfigs;
timeRange?: TimeRange;
timeFields?: string[];
indexPattern?: IIndexPattern;
query?: Query;
filters?: Filter[];
filterManager: FilterManager;
uiState?: PersistedState;
partialRows?: boolean;
inspectorAdapters: Adapters;
metricsAtAllLevels?: boolean;
visParams?: any;
abortSignal?: AbortSignal;
searchSessionId?: string;
}
const name = 'esaggs';
const handleCourierRequest = async ({
searchSource,
aggs,
timeRange,
timeFields,
indexPattern,
query,
filters,
partialRows,
metricsAtAllLevels,
inspectorAdapters,
filterManager,
abortSignal,
searchSessionId,
}: RequestHandlerParams) => {
// Create a new search source that inherits the original search source
// but has the appropriate timeRange applied via a filter.
// This is a temporary solution until we properly pass down all required
// information for the request to the request handler (https://github.com/elastic/kibana/issues/16641).
// Using callParentStartHandlers: true we make sure, that the parent searchSource
// onSearchRequestStart will be called properly even though we use an inherited
// search source.
const timeFilterSearchSource = searchSource.createChild({ callParentStartHandlers: true });
const requestSearchSource = timeFilterSearchSource.createChild({ callParentStartHandlers: true });
aggs.setTimeRange(timeRange as TimeRange);
// For now we need to mirror the history of the passed search source, since
// the request inspector wouldn't work otherwise.
Object.defineProperty(requestSearchSource, 'history', {
get() {
return searchSource.history;
},
set(history) {
return (searchSource.history = history);
},
});
requestSearchSource.setField('aggs', function () {
return aggs.toDsl(metricsAtAllLevels);
});
requestSearchSource.onRequestStart((paramSearchSource, options) => {
return aggs.onSearchRequestStart(paramSearchSource, options);
});
// If timeFields have been specified, use the specified ones, otherwise use primary time field of index
// pattern if it's available.
const defaultTimeField = indexPattern?.getTimeField?.();
const defaultTimeFields = defaultTimeField ? [defaultTimeField.name] : [];
const allTimeFields = timeFields && timeFields.length > 0 ? timeFields : defaultTimeFields;
// If a timeRange has been specified and we had at least one timeField available, create range
// filters for that those time fields
if (timeRange && allTimeFields.length > 0) {
timeFilterSearchSource.setField('filter', () => {
return allTimeFields
.map((fieldName) => getTime(indexPattern, timeRange, { fieldName }))
.filter(isRangeFilter);
});
}
requestSearchSource.setField('filter', filters);
requestSearchSource.setField('query', query);
inspectorAdapters.requests.reset();
const request = 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,
}
);
request.stats(getRequestInspectorStats(requestSearchSource));
try {
const response = await requestSearchSource.fetch({
abortSignal,
sessionId: searchSessionId,
});
request.stats(getResponseInspectorStats(response, searchSource)).ok({ json: response });
(searchSource as any).rawResponse = response;
} catch (e) {
// Log any error during request to the inspector
request.error({ json: e });
throw e;
} finally {
// Add the request body no matter if things went fine or not
requestSearchSource.getSearchRequestBody().then((req: unknown) => {
request.json(req);
});
}
// 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 resp = (searchSource as any).rawResponse;
for (const agg of aggs.aggs) {
if (hasIn(agg, 'type.postFlightRequest')) {
resp = await agg.type.postFlightRequest(
resp,
aggs,
agg,
requestSearchSource,
inspectorAdapters.requests,
abortSignal
);
}
}
(searchSource as any).finalResponse = resp;
const parsedTimeRange = timeRange ? calculateBounds(timeRange) : null;
const tabifyParams = {
metricsAtAllLevels,
partialRows,
timeRange: parsedTimeRange
? { from: parsedTimeRange.min, to: parsedTimeRange.max, timeFields: allTimeFields }
: undefined,
};
const response = tabifyAggResponse(aggs, (searchSource as any).finalResponse, tabifyParams);
(searchSource as any).tabifiedResponse = response;
inspectorAdapters.data.setTabularLoader(
() =>
buildTabularInspectorData((searchSource as any).tabifiedResponse, {
queryFilter: filterManager,
deserializeFieldFormat: getFieldFormats().deserialize,
}),
{ returnsFormattedValues: true }
);
return response;
};
export const esaggs = (): EsaggsExpressionFunctionDefinition => ({
name,
type: 'datatable',
inputTypes: ['kibana_context', 'null'],
help: i18n.translate('data.functions.esaggs.help', {
defaultMessage: 'Run AggConfig aggregation',
}),
args: {
index: {
types: ['string'],
help: '',
},
metricsAtAllLevels: {
types: ['boolean'],
default: false,
help: '',
},
partialRows: {
types: ['boolean'],
default: false,
help: '',
},
includeFormatHints: {
types: ['boolean'],
default: false,
help: '',
},
aggConfigs: {
types: ['string'],
default: '""',
help: '',
},
timeFields: {
types: ['string'],
help: '',
multi: true,
},
},
async fn(input, args, { inspectorAdapters, abortSignal, getSearchSessionId }) {
const indexPatterns = getIndexPatterns();
const { filterManager } = getQueryService();
const searchService = getSearchService();
const aggConfigsState = JSON.parse(args.aggConfigs);
const indexPattern = await indexPatterns.get(args.index);
const aggs = searchService.aggs.createAggConfigs(indexPattern, aggConfigsState);
// we should move searchSource creation inside courier request handler
const searchSource = await searchService.searchSource.create();
searchSource.setField('index', indexPattern);
searchSource.setField('size', 0);
const resolvedTimeRange = input?.timeRange && calculateBounds(input.timeRange);
const response = await handleCourierRequest({
searchSource,
aggs,
indexPattern,
timeRange: get(input, 'timeRange', undefined),
query: get(input, 'query', undefined) as any,
filters: get(input, 'filters', undefined),
timeFields: args.timeFields,
metricsAtAllLevels: args.metricsAtAllLevels,
partialRows: args.partialRows,
inspectorAdapters: inspectorAdapters as Adapters,
filterManager,
abortSignal: (abortSignal as unknown) as AbortSignal,
searchSessionId: getSearchSessionId(),
});
const table: Datatable = {
type: 'datatable',
rows: response.rows,
columns: response.columns.map((column) => {
const cleanedColumn: DatatableColumn = {
id: column.id,
name: column.name,
meta: {
type: column.aggConfig.params.field?.type || 'number',
field: column.aggConfig.params.field?.name,
index: indexPattern.title,
params: column.aggConfig.toSerializedFieldFormat(),
source: 'esaggs',
sourceParams: {
indexPatternId: indexPattern.id,
appliedTimeRange:
column.aggConfig.params.field?.name &&
input?.timeRange &&
args.timeFields &&
args.timeFields.includes(column.aggConfig.params.field?.name)
? {
from: resolvedTimeRange?.min?.toISOString(),
to: resolvedTimeRange?.max?.toISOString(),
}
: undefined,
...column.aggConfig.serialize(),
},
},
};
return cleanedColumn;
}),
};
return table;
},
});

View file

@ -18,35 +18,41 @@
*/
import { set } from '@elastic/safer-lodash-set';
import { FormattedData } from '../../../../../plugins/inspector/public';
import { TabbedTable } from '../../../common';
import { FormatFactory } from '../../../common/field_formats/utils';
import { createFilter } from './create_filter';
import {
FormattedData,
TabularData,
TabularDataValue,
} from '../../../../../../plugins/inspector/common';
import { Filter, TabbedTable } from '../../../../common';
import { FormatFactory } from '../../../../common/field_formats/utils';
import { createFilter } from '../create_filter';
/**
* @deprecated
* Type borrowed from the client-side FilterManager['addFilters'].
*
* Do not use this function.
*
* @todo This function is used only by Courier. Courier will
* soon be removed, and this function will be deleted, too. If Courier is not removed,
* move this function inside Courier.
*
* ---
* We need to use a custom type to make this isomorphic since FilterManager
* doesn't exist on the server.
*
* @internal
*/
export type AddFilters = (filters: Filter[] | Filter, pinFilterStatus?: boolean) => void;
/**
* This function builds tabular data from the response and attaches it to the
* inspector. It will only be called when the data view in the inspector is opened.
*
* @internal
*/
export async function buildTabularInspectorData(
table: TabbedTable,
{
queryFilter,
addFilters,
deserializeFieldFormat,
}: {
queryFilter: { addFilters: (filter: any) => void };
addFilters?: AddFilters;
deserializeFieldFormat: FormatFactory;
}
) {
): Promise<TabularData> {
const aggConfigs = table.columns.map((column) => column.aggConfig);
const rows = table.rows.map((row) => {
return table.columns.reduce<Record<string, FormattedData>>((prev, cur, colIndex) => {
@ -74,20 +80,22 @@ export async function buildTabularInspectorData(
name: col.name,
field: `col-${colIndex}-${col.aggConfig.id}`,
filter:
addFilters &&
isCellContentFilterable &&
((value: { raw: unknown }) => {
((value: TabularDataValue) => {
const rowIndex = rows.findIndex(
(row) => row[`col-${colIndex}-${col.aggConfig.id}`].raw === value.raw
);
const filter = createFilter(aggConfigs, table, colIndex, rowIndex, value.raw);
if (filter) {
queryFilter.addFilters(filter);
addFilters(filter);
}
}),
filterOut:
addFilters &&
isCellContentFilterable &&
((value: { raw: unknown }) => {
((value: TabularDataValue) => {
const rowIndex = rows.findIndex(
(row) => row[`col-${colIndex}-${col.aggConfig.id}`].raw === value.raw
);
@ -101,7 +109,7 @@ export async function buildTabularInspectorData(
} else {
set(filter, 'meta.negate', notOther && notMissing);
}
queryFilter.addFilters(filter);
addFilters(filter);
}
}),
};

View file

@ -0,0 +1,155 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { get } from 'lodash';
import { i18n } from '@kbn/i18n';
import { Datatable, DatatableColumn } from 'src/plugins/expressions/common';
import { Adapters } from 'src/plugins/inspector/common';
import { calculateBounds, EsaggsExpressionFunctionDefinition } from '../../../../common';
import { FormatFactory } from '../../../../common/field_formats/utils';
import { IndexPatternsContract } from '../../../../common/index_patterns/index_patterns';
import { ISearchStartSearchSource, AggsStart } from '../../../../common/search';
import { AddFilters } from './build_tabular_inspector_data';
import { handleRequest } from './request_handler';
const name = 'esaggs';
interface StartDependencies {
addFilters: AddFilters;
aggs: AggsStart;
deserializeFieldFormat: FormatFactory;
indexPatterns: IndexPatternsContract;
searchSource: ISearchStartSearchSource;
}
export function getEsaggs({
getStartDependencies,
}: {
getStartDependencies: () => Promise<StartDependencies>;
}) {
return (): EsaggsExpressionFunctionDefinition => ({
name,
type: 'datatable',
inputTypes: ['kibana_context', 'null'],
help: i18n.translate('data.functions.esaggs.help', {
defaultMessage: 'Run AggConfig aggregation',
}),
args: {
index: {
types: ['string'],
help: '',
},
metricsAtAllLevels: {
types: ['boolean'],
default: false,
help: '',
},
partialRows: {
types: ['boolean'],
default: false,
help: '',
},
includeFormatHints: {
types: ['boolean'],
default: false,
help: '',
},
aggConfigs: {
types: ['string'],
default: '""',
help: '',
},
timeFields: {
types: ['string'],
help: '',
multi: true,
},
},
async fn(input, args, { inspectorAdapters, abortSignal, getSearchSessionId }) {
const {
addFilters,
aggs,
deserializeFieldFormat,
indexPatterns,
searchSource,
} = await getStartDependencies();
const aggConfigsState = JSON.parse(args.aggConfigs);
const indexPattern = await indexPatterns.get(args.index);
const aggConfigs = aggs.createAggConfigs(indexPattern, aggConfigsState);
const resolvedTimeRange = input?.timeRange && calculateBounds(input.timeRange);
const response = await handleRequest({
abortSignal: (abortSignal as unknown) as AbortSignal,
addFilters,
aggs: aggConfigs,
deserializeFieldFormat,
filters: get(input, 'filters', undefined),
indexPattern,
inspectorAdapters: inspectorAdapters as Adapters,
metricsAtAllLevels: args.metricsAtAllLevels,
partialRows: args.partialRows,
query: get(input, 'query', undefined) as any,
searchSessionId: getSearchSessionId(),
searchSourceService: searchSource,
timeFields: args.timeFields,
timeRange: get(input, 'timeRange', undefined),
});
const table: Datatable = {
type: 'datatable',
rows: response.rows,
columns: response.columns.map((column) => {
const cleanedColumn: DatatableColumn = {
id: column.id,
name: column.name,
meta: {
type: column.aggConfig.params.field?.type || 'number',
field: column.aggConfig.params.field?.name,
index: indexPattern.title,
params: column.aggConfig.toSerializedFieldFormat(),
source: name,
sourceParams: {
indexPatternId: indexPattern.id,
appliedTimeRange:
column.aggConfig.params.field?.name &&
input?.timeRange &&
args.timeFields &&
args.timeFields.includes(column.aggConfig.params.field?.name)
? {
from: resolvedTimeRange?.min?.toISOString(),
to: resolvedTimeRange?.max?.toISOString(),
}
: undefined,
...column.aggConfig.serialize(),
},
},
};
return cleanedColumn;
}),
};
return table;
},
});
}

View file

@ -0,0 +1,20 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
export * from './esaggs_fn';

View file

@ -0,0 +1,213 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { i18n } from '@kbn/i18n';
import { Adapters } from 'src/plugins/inspector/common';
import {
calculateBounds,
Filter,
getTime,
IndexPattern,
isRangeFilter,
Query,
TimeRange,
} from '../../../../common';
import {
getRequestInspectorStats,
getResponseInspectorStats,
IAggConfigs,
ISearchStartSearchSource,
tabifyAggResponse,
} from '../../../../common/search';
import { FormatFactory } from '../../../../common/field_formats/utils';
import { AddFilters, buildTabularInspectorData } from './build_tabular_inspector_data';
interface RequestHandlerParams {
abortSignal?: AbortSignal;
addFilters?: AddFilters;
aggs: IAggConfigs;
deserializeFieldFormat: FormatFactory;
filters?: Filter[];
indexPattern?: IndexPattern;
inspectorAdapters: Adapters;
metricsAtAllLevels?: boolean;
partialRows?: boolean;
query?: Query;
searchSessionId?: string;
searchSourceService: ISearchStartSearchSource;
timeFields?: string[];
timeRange?: TimeRange;
}
export const handleRequest = async ({
abortSignal,
addFilters,
aggs,
deserializeFieldFormat,
filters,
indexPattern,
inspectorAdapters,
metricsAtAllLevels,
partialRows,
query,
searchSessionId,
searchSourceService,
timeFields,
timeRange,
}: RequestHandlerParams) => {
const searchSource = await searchSourceService.create();
searchSource.setField('index', indexPattern);
searchSource.setField('size', 0);
// Create a new search source that inherits the original search source
// but has the appropriate timeRange applied via a filter.
// This is a temporary solution until we properly pass down all required
// information for the request to the request handler (https://github.com/elastic/kibana/issues/16641).
// Using callParentStartHandlers: true we make sure, that the parent searchSource
// onSearchRequestStart will be called properly even though we use an inherited
// search source.
const timeFilterSearchSource = searchSource.createChild({ callParentStartHandlers: true });
const requestSearchSource = timeFilterSearchSource.createChild({ callParentStartHandlers: true });
aggs.setTimeRange(timeRange as TimeRange);
// For now we need to mirror the history of the passed search source, since
// the request inspector wouldn't work otherwise.
Object.defineProperty(requestSearchSource, 'history', {
get() {
return searchSource.history;
},
set(history) {
return (searchSource.history = history);
},
});
requestSearchSource.setField('aggs', function () {
return aggs.toDsl(metricsAtAllLevels);
});
requestSearchSource.onRequestStart((paramSearchSource, options) => {
return aggs.onSearchRequestStart(paramSearchSource, options);
});
// If timeFields have been specified, use the specified ones, otherwise use primary time field of index
// pattern if it's available.
const defaultTimeField = indexPattern?.getTimeField?.();
const defaultTimeFields = defaultTimeField ? [defaultTimeField.name] : [];
const allTimeFields = timeFields && timeFields.length > 0 ? timeFields : defaultTimeFields;
// If a timeRange has been specified and we had at least one timeField available, create range
// filters for that those time fields
if (timeRange && allTimeFields.length > 0) {
timeFilterSearchSource.setField('filter', () => {
return allTimeFields
.map((fieldName) => getTime(indexPattern, timeRange, { fieldName }))
.filter(isRangeFilter);
});
}
requestSearchSource.setField('filter', filters);
requestSearchSource.setField('query', query);
let request;
if (inspectorAdapters.requests) {
inspectorAdapters.requests.reset();
request = 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,
}
);
request.stats(getRequestInspectorStats(requestSearchSource));
}
try {
const response = await requestSearchSource.fetch({
abortSignal,
sessionId: searchSessionId,
});
if (request) {
request.stats(getResponseInspectorStats(response, searchSource)).ok({ json: response });
}
(searchSource as any).rawResponse = response;
} catch (e) {
// Log any error during request to the inspector
if (request) {
request.error({ json: e });
}
throw e;
} finally {
// Add the request body no matter if things went fine or not
if (request) {
request.json(await requestSearchSource.getSearchRequestBody());
}
}
// 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 = (searchSource as any).rawResponse;
for (const agg of aggs.aggs) {
if (typeof agg.type.postFlightRequest === 'function') {
response = await agg.type.postFlightRequest(
response,
aggs,
agg,
requestSearchSource,
inspectorAdapters.requests,
abortSignal
);
}
}
const parsedTimeRange = timeRange ? calculateBounds(timeRange) : null;
const tabifyParams = {
metricsAtAllLevels,
partialRows,
timeRange: parsedTimeRange
? { from: parsedTimeRange.min, to: parsedTimeRange.max, timeFields: allTimeFields }
: undefined,
};
const tabifiedResponse = tabifyAggResponse(aggs, response, tabifyParams);
if (inspectorAdapters.data) {
inspectorAdapters.data.setTabularLoader(
() =>
buildTabularInspectorData(tabifiedResponse, {
addFilters,
deserializeFieldFormat,
}),
{ returnsFormattedValues: true }
);
}
return tabifiedResponse;
};

View file

@ -18,7 +18,6 @@
*/
import { NotificationsStart, CoreStart } from 'src/core/public';
import { FieldFormatsStart } from './field_formats';
import { createGetterSetter } from '../../kibana_utils/public';
import { IndexPatternsContract } from './index_patterns';
import { DataPublicPluginStart } from './types';
@ -31,20 +30,12 @@ export const [getUiSettings, setUiSettings] = createGetterSetter<CoreStart['uiSe
'UiSettings'
);
export const [getFieldFormats, setFieldFormats] = createGetterSetter<FieldFormatsStart>(
'FieldFormats'
);
export const [getOverlays, setOverlays] = createGetterSetter<CoreStart['overlays']>('Overlays');
export const [getIndexPatterns, setIndexPatterns] = createGetterSetter<IndexPatternsContract>(
'IndexPatterns'
);
export const [getQueryService, setQueryService] = createGetterSetter<
DataPublicPluginStart['query']
>('Query');
export const [getSearchService, setSearchService] = createGetterSetter<
DataPublicPluginStart['search']
>('Search');

View file

@ -84,7 +84,7 @@ export class SearchEmbeddable
private readonly savedSearch: SavedSearch;
private $rootScope: ng.IRootScopeService;
private $compile: ng.ICompileService;
private inspectorAdaptors: Adapters;
private inspectorAdapters: Adapters;
private searchScope?: SearchScope;
private panelTitle: string = '';
private filtersSearchSource?: ISearchSource;
@ -131,7 +131,7 @@ export class SearchEmbeddable
this.savedSearch = savedSearch;
this.$rootScope = $rootScope;
this.$compile = $compile;
this.inspectorAdaptors = {
this.inspectorAdapters = {
requests: new RequestAdapter(),
};
this.initializeSearchScope();
@ -150,7 +150,7 @@ export class SearchEmbeddable
}
public getInspectorAdapters() {
return this.inspectorAdaptors;
return this.inspectorAdapters;
}
public getSavedSearch() {
@ -195,7 +195,7 @@ export class SearchEmbeddable
const searchScope: SearchScope = (this.searchScope = this.$rootScope.$new());
searchScope.description = this.savedSearch.description;
searchScope.inspectorAdapters = this.inspectorAdaptors;
searchScope.inspectorAdapters = this.inspectorAdapters;
const { searchSource } = this.savedSearch;
const indexPattern = (searchScope.indexPattern = searchSource.getField('index'))!;
@ -287,7 +287,7 @@ export class SearchEmbeddable
);
// Log request to inspector
this.inspectorAdaptors.requests.reset();
this.inspectorAdapters.requests!.reset();
const title = i18n.translate('discover.embeddable.inspectorRequestDataTitle', {
defaultMessage: 'Data',
});
@ -295,7 +295,7 @@ export class SearchEmbeddable
defaultMessage: 'This request queries Elasticsearch to fetch the data for the search.',
});
const inspectorRequest = this.inspectorAdaptors.requests.start(title, {
const inspectorRequest = this.inspectorAdapters.requests!.start(title, {
description,
searchSessionId,
});

View file

@ -28,6 +28,7 @@ import { EuiComboBoxProps } from '@elastic/eui';
import { EuiConfirmModalProps } from '@elastic/eui';
import { EuiContextMenuPanelDescriptor } from '@elastic/eui';
import { EuiGlobalToastListToast } from '@elastic/eui';
import { EventEmitter } from 'events';
import { ExclusiveUnion } from '@elastic/eui';
import { ExpressionAstFunction } from 'src/plugins/expressions/common';
import { History } from 'history';
@ -59,7 +60,7 @@ import { PublicMethodsOf } from '@kbn/utility-types';
import { PublicUiSettingsParams } from 'src/core/server/types';
import React from 'react';
import { RecursiveReadonly } from '@kbn/utility-types';
import { RequestAdapter } from 'src/plugins/inspector/common';
import { RequestAdapter as RequestAdapter_2 } from 'src/plugins/inspector/common';
import { Required } from '@kbn/utility-types';
import * as Rx from 'rxjs';
import { SavedObject as SavedObject_2 } from 'src/core/server';
@ -100,6 +101,14 @@ export const ACTION_EDIT_PANEL = "editPanel";
export interface Adapters {
// (undocumented)
[key: string]: any;
// Warning: (ae-forgotten-export) The symbol "DataAdapter" needs to be exported by the entry point index.d.ts
//
// (undocumented)
data?: DataAdapter;
// Warning: (ae-forgotten-export) The symbol "RequestAdapter" needs to be exported by the entry point index.d.ts
//
// (undocumented)
requests?: RequestAdapter;
}
// Warning: (ae-forgotten-export) The symbol "ActionContext" needs to be exported by the entry point index.d.ts

View file

@ -20,7 +20,7 @@
import { EventEmitter } from 'events';
import { TabularCallback, TabularHolder, TabularLoaderOptions } from './types';
class DataAdapter extends EventEmitter {
export class DataAdapter extends EventEmitter {
private tabular?: TabularCallback;
private tabularOptions?: TabularLoaderOptions;
@ -38,5 +38,3 @@ class DataAdapter extends EventEmitter {
return Promise.resolve(this.tabular()).then((data) => ({ data, options }));
}
}
export { DataAdapter };

View file

@ -35,33 +35,37 @@ describe('DataAdapter', () => {
});
it('should call the provided callback and resolve with its value', async () => {
const spy = jest.fn(() => 'foo');
const data = { columns: [], rows: [] };
const spy = jest.fn(() => data);
adapter.setTabularLoader(spy);
expect(spy).not.toBeCalled();
const result = await adapter.getTabular();
expect(spy).toBeCalled();
expect(result.data).toBe('foo');
expect(result.data).toBe(data);
});
it('should pass through options specified via setTabularLoader', async () => {
adapter.setTabularLoader(() => 'foo', { returnsFormattedValues: true });
const data = { columns: [], rows: [] };
adapter.setTabularLoader(() => data, { returnsFormattedValues: true });
const result = await adapter.getTabular();
expect(result.options).toEqual({ returnsFormattedValues: true });
});
it('should return options set when starting loading data', async () => {
adapter.setTabularLoader(() => 'foo', { returnsFormattedValues: true });
const data = { columns: [], rows: [] };
adapter.setTabularLoader(() => data, { returnsFormattedValues: true });
const waitForResult = adapter.getTabular();
adapter.setTabularLoader(() => 'bar', { returnsFormattedValues: false });
adapter.setTabularLoader(() => data, { returnsFormattedValues: false });
const result = await waitForResult;
expect(result.options).toEqual({ returnsFormattedValues: true });
});
});
it('should emit a "tabular" event when a new tabular loader is specified', () => {
const data = { columns: [], rows: [] };
const spy = jest.fn();
adapter.once('change', spy);
adapter.setTabularLoader(() => 42);
adapter.setTabularLoader(() => data);
expect(spy).toBeCalled();
});
});

View file

@ -17,8 +17,6 @@
* under the License.
*/
class FormattedData {
export class FormattedData {
constructor(public readonly raw: any, public readonly formatted: any) {}
}
export { FormattedData };

View file

@ -17,5 +17,6 @@
* under the License.
*/
export { FormattedData } from './formatted_data';
export { DataAdapter } from './data_adapter';
export * from './data_adapter';
export * from './formatted_data';
export * from './types';

View file

@ -17,8 +17,25 @@
* under the License.
*/
// TODO: add a more specific TabularData type.
export type TabularData = any;
export interface TabularDataValue {
formatted: string;
raw: unknown;
}
export interface TabularDataColumn {
name: string;
field: string;
filter?: (value: TabularDataValue) => void;
filterOut?: (value: TabularDataValue) => void;
}
export type TabularDataRow = Record<TabularDataColumn['field'], TabularDataValue>;
export interface TabularData {
columns: TabularDataColumn[];
rows: TabularDataRow[];
}
export type TabularCallback = () => TabularData | Promise<TabularData>;
export interface TabularHolder {

View file

@ -17,12 +17,6 @@
* under the License.
*/
export { Adapters } from './types';
export { DataAdapter, FormattedData } from './data';
export {
RequestAdapter,
RequestStatistic,
RequestStatistics,
RequestStatus,
RequestResponder,
} from './request';
export * from './data';
export * from './request';
export * from './types';

View file

@ -29,7 +29,7 @@ import { Request, RequestParams, RequestStatus } from './types';
* instead it offers a generic API to log requests of any kind.
* @extends EventEmitter
*/
class RequestAdapter extends EventEmitter {
export class RequestAdapter extends EventEmitter {
private requests: Map<string, Request>;
constructor() {
@ -78,5 +78,3 @@ class RequestAdapter extends EventEmitter {
this.emit('change');
}
}
export { RequestAdapter };

View file

@ -17,9 +17,14 @@
* under the License.
*/
import type { DataAdapter } from './data';
import type { RequestAdapter } from './request';
/**
* The interface that the adapters used to open an inspector have to fullfill.
*/
export interface Adapters {
data?: DataAdapter;
requests?: RequestAdapter;
[key: string]: any;
}

View file

@ -17,4 +17,17 @@
* under the License.
*/
export * from './adapters';
export {
Adapters,
DataAdapter,
FormattedData,
RequestAdapter,
RequestStatistic,
RequestStatistics,
RequestStatus,
RequestResponder,
TabularData,
TabularDataColumn,
TabularDataRow,
TabularDataValue,
} from './adapters';

View file

@ -18,8 +18,7 @@
*/
import { inspectorPluginMock } from '../mocks';
import { DataAdapter } from '../../common/adapters/data/data_adapter';
import { RequestAdapter } from '../../common/adapters/request/request_adapter';
import { DataAdapter, RequestAdapter } from '../../common/adapters';
const adapter1 = new DataAdapter();
const adapter2 = new RequestAdapter();

View file

@ -35,7 +35,7 @@ import { Adapters } from '../../../../common';
import {
TabularLoaderOptions,
TabularData,
TabularCallback,
TabularHolder,
} from '../../../../common/adapters/data/types';
import { IUiSettingsClient } from '../../../../../../core/public';
import { withKibana, KibanaReactContextValue } from '../../../../../kibana_react/public';
@ -44,7 +44,7 @@ interface DataViewComponentState {
tabularData: TabularData | null;
tabularOptions: TabularLoaderOptions;
adapters: Adapters;
tabularPromise: TabularCallback | null;
tabularPromise: Promise<TabularHolder> | null;
}
interface DataViewComponentProps extends InspectorViewProps {
@ -73,7 +73,7 @@ class DataViewComponent extends Component<DataViewComponentProps, DataViewCompon
adapters: nextProps.adapters,
tabularData: null,
tabularOptions: {},
tabularPromise: nextProps.adapters.data.getTabular(),
tabularPromise: nextProps.adapters.data!.getTabular(),
};
}
@ -82,7 +82,7 @@ class DataViewComponent extends Component<DataViewComponentProps, DataViewCompon
this.setState({
tabularData: null,
tabularOptions: {},
tabularPromise: this.props.adapters.data.getTabular(),
tabularPromise: this.props.adapters.data!.getTabular(),
});
}
};
@ -91,7 +91,7 @@ class DataViewComponent extends Component<DataViewComponentProps, DataViewCompon
const { tabularPromise } = this.state;
if (tabularPromise) {
const tabularData: TabularData = await tabularPromise;
const tabularData: TabularHolder = await tabularPromise;
if (this._isMounted) {
this.setState({
@ -105,13 +105,13 @@ class DataViewComponent extends Component<DataViewComponentProps, DataViewCompon
componentDidMount() {
this._isMounted = true;
this.props.adapters.data.on('change', this.onUpdateData);
this.props.adapters.data!.on('change', this.onUpdateData);
this.finishLoadingData();
}
componentWillUnmount() {
this._isMounted = false;
this.props.adapters.data.removeListener('change', this.onUpdateData);
this.props.adapters.data!.removeListener('change', this.onUpdateData);
}
componentDidUpdate() {

View file

@ -17,18 +17,15 @@
* under the License.
*/
import { TabularDataRow } from '../../../common/adapters';
type DataViewColumnRender = (value: string, _item: TabularDataRow) => string;
export interface DataViewColumn {
name: string;
field: string;
sortable: (item: DataViewRow) => string | number;
sortable: (item: TabularDataRow) => string | number;
render: DataViewColumnRender;
}
type DataViewColumnRender = (value: string, _item: DataViewRow) => string;
export interface DataViewRow {
[fields: string]: {
formatted: string;
raw: any;
};
}
export type DataViewRow = TabularDataRow;

View file

@ -31,7 +31,7 @@ import { RequestDetails } from './request_details';
interface RequestSelectorState {
requests: Request[];
request: Request;
request: Request | null;
}
export class RequestsViewComponent extends Component<InspectorViewProps, RequestSelectorState> {
@ -43,9 +43,9 @@ export class RequestsViewComponent extends Component<InspectorViewProps, Request
constructor(props: InspectorViewProps) {
super(props);
props.adapters.requests.on('change', this._onRequestsChange);
props.adapters.requests!.on('change', this._onRequestsChange);
const requests = props.adapters.requests.getRequests();
const requests = props.adapters.requests!.getRequests();
this.state = {
requests,
request: requests.length ? requests[0] : null,
@ -53,10 +53,10 @@ export class RequestsViewComponent extends Component<InspectorViewProps, Request
}
_onRequestsChange = () => {
const requests = this.props.adapters.requests.getRequests();
const requests = this.props.adapters.requests!.getRequests();
const newState = { requests } as RequestSelectorState;
if (!requests.includes(this.state.request)) {
if (!this.state.request || !requests.includes(this.state.request)) {
newState.request = requests.length ? requests[0] : null;
}
this.setState(newState);
@ -69,7 +69,7 @@ export class RequestsViewComponent extends Component<InspectorViewProps, Request
};
componentWillUnmount() {
this.props.adapters.requests.removeListener('change', this._onRequestsChange);
this.props.adapters.requests!.removeListener('change', this._onRequestsChange);
}
static renderEmptyRequests() {
@ -140,12 +140,16 @@ export class RequestsViewComponent extends Component<InspectorViewProps, Request
</p>
</EuiText>
<EuiSpacer size="xs" />
<RequestSelector
requests={this.state.requests}
selectedRequest={this.state.request}
onRequestChanged={this.selectRequest}
/>
<EuiSpacer size="xs" />
{this.state.request && (
<>
<RequestSelector
requests={this.state.requests}
selectedRequest={this.state.request}
onRequestChanged={this.selectRequest}
/>
<EuiSpacer size="xs" />
</>
)}
{this.state.request && this.state.request.description && (
<EuiText size="xs">

View file

@ -8,6 +8,7 @@ import { CoreSetup } from 'src/core/public';
import { CoreStart } from 'src/core/public';
import { EnvironmentMode } from '@kbn/config';
import { EuiContextMenuPanelDescriptor } from '@elastic/eui';
import { EventEmitter } from 'events';
import { Observable } from 'rxjs';
import { PackageInfo } from '@kbn/config';
import { Plugin } from 'src/core/public';

View file

@ -134,7 +134,7 @@ export class AbstractESSource extends AbstractVectorSource implements IESSource
destroy() {
const inspectorAdapters = this.getInspectorAdapters();
if (inspectorAdapters) {
if (inspectorAdapters?.requests) {
inspectorAdapters.requests.resetRequest(this.getId());
}
}
@ -164,7 +164,7 @@ export class AbstractESSource extends AbstractVectorSource implements IESSource
const inspectorAdapters = this.getInspectorAdapters();
let inspectorRequest: RequestResponder | undefined;
if (inspectorAdapters) {
if (inspectorAdapters?.requests) {
inspectorRequest = inspectorAdapters.requests.start(requestName, {
id: requestId,
description: requestDescription,