diff --git a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.adapters.data.md b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.adapters.data.md new file mode 100644 index 0000000000000..0ddbcb3546d1e --- /dev/null +++ b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.adapters.data.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-embeddable-public](./kibana-plugin-plugins-embeddable-public.md) > [Adapters](./kibana-plugin-plugins-embeddable-public.adapters.md) > [data](./kibana-plugin-plugins-embeddable-public.adapters.data.md) + +## Adapters.data property + +Signature: + +```typescript +data?: DataAdapter; +``` diff --git a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.adapters.md b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.adapters.md index 9635b36cdf05a..47484dc79d88c 100644 --- a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.adapters.md +++ b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.adapters.md @@ -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) | DataAdapter | | +| [requests](./kibana-plugin-plugins-embeddable-public.adapters.requests.md) | RequestAdapter | | + diff --git a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.adapters.requests.md b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.adapters.requests.md new file mode 100644 index 0000000000000..2954ad86138ff --- /dev/null +++ b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.adapters.requests.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-embeddable-public](./kibana-plugin-plugins-embeddable-public.md) > [Adapters](./kibana-plugin-plugins-embeddable-public.adapters.md) > [requests](./kibana-plugin-plugins-embeddable-public.adapters.requests.md) + +## Adapters.requests property + +Signature: + +```typescript +requests?: RequestAdapter; +``` diff --git a/src/plugins/data/common/search/aggs/agg_type.ts b/src/plugins/data/common/search/aggs/agg_type.ts index 3ffac0c12eb22..4f4a593764b1e 100644 --- a/src/plugins/data/common/search/aggs/agg_type.ts +++ b/src/plugins/data/common/search/aggs/agg_type.ts @@ -54,7 +54,7 @@ export interface AggTypeConfig< aggConfigs: IAggConfigs, aggConfig: TAggConfig, searchSource: ISearchSource, - inspectorRequestAdapter: RequestAdapter, + inspectorRequestAdapter?: RequestAdapter, abortSignal?: AbortSignal ) => Promise; getSerializedFormat?: (agg: TAggConfig) => SerializedFieldFormat; @@ -189,7 +189,7 @@ export class AggType< aggConfigs: IAggConfigs, aggConfig: TAggConfig, searchSource: ISearchSource, - inspectorRequestAdapter: RequestAdapter, + inspectorRequestAdapter?: RequestAdapter, abortSignal?: AbortSignal ) => Promise; /** diff --git a/src/plugins/data/common/search/aggs/buckets/terms.ts b/src/plugins/data/common/search/aggs/buckets/terms.ts index 3d543e6c5f574..ac65e7fa813b3 100644 --- a/src/plugins/data/common/search/aggs/buckets/terms.ts +++ b/src/plugins/data/common/search/aggs/buckets/terms.ts @@ -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 | 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) { diff --git a/src/plugins/data/public/autocomplete/autocomplete_service.ts b/src/plugins/data/public/autocomplete/autocomplete_service.ts index 2136a405baad6..5e9aede0760fe 100644 --- a/src/plugins/data/public/autocomplete/autocomplete_service.ts +++ b/src/plugins/data/public/autocomplete/autocomplete_service.ts @@ -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 { diff --git a/src/plugins/data/public/autocomplete/providers/value_suggestion_provider.test.ts b/src/plugins/data/public/autocomplete/providers/value_suggestion_provider.test.ts index 0ef5b7db958e4..4e1745ffcabb2 100644 --- a/src/plugins/data/public/autocomplete/providers/value_suggestion_provider.test.ts +++ b/src/plugins/data/public/autocomplete/providers/value_suggestion_provider.test.ts @@ -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', () => { diff --git a/src/plugins/data/public/autocomplete/providers/value_suggestion_provider.ts b/src/plugins/data/public/autocomplete/providers/value_suggestion_provider.ts index fe9f939a0261d..ee92fce02dda5 100644 --- a/src/plugins/data/public/autocomplete/providers/value_suggestion_provider.ts +++ b/src/plugins/data/public/autocomplete/providers/value_suggestion_provider.ts @@ -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); diff --git a/src/plugins/data/public/index_patterns/index_pattern.stub.ts b/src/plugins/data/public/index_patterns/index_pattern.stub.ts index e5c6c008e3e28..804f0d7d89225 100644 --- a/src/plugins/data/public/index_patterns/index_pattern.stub.ts +++ b/src/plugins/data/public/index_patterns/index_pattern.stub.ts @@ -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, diff --git a/src/plugins/data/public/plugin.ts b/src/plugins/data/public/plugin.ts index 601b5f0ccda79..4334774300698 100644 --- a/src/plugins/data/public/plugin.ts +++ b/src/plugins/data/public/plugin.ts @@ -42,16 +42,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, @@ -112,8 +110,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; @@ -146,7 +158,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, @@ -163,7 +175,6 @@ export class DataPublicPlugin setUiSettings(uiSettings); const fieldFormats = this.fieldFormatsService.start(); - setFieldFormats(fieldFormats); const indexPatterns = new IndexPatternsService({ uiSettings: new UiSettingsPublicToCommon(uiSettings), @@ -191,7 +202,6 @@ export class DataPublicPlugin savedObjectsClient: savedObjects.client, uiSettings, }); - setQueryService(query); const search = this.searchService.start(core, { fieldFormats, indexPatterns }); setSearchService(search); diff --git a/src/plugins/data/public/public.api.md b/src/plugins/data/public/public.api.md index 029bb85b86c36..6c526ab613d76 100644 --- a/src/plugins/data/public/public.api.md +++ b/src/plugins/data/public/public.api.md @@ -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'; diff --git a/src/plugins/data/public/search/expressions/esaggs.ts b/src/plugins/data/public/search/expressions/esaggs.ts deleted file mode 100644 index 3932484801fa8..0000000000000 --- a/src/plugins/data/public/search/expressions/esaggs.ts +++ /dev/null @@ -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; - }, -}); diff --git a/src/plugins/data/public/search/expressions/build_tabular_inspector_data.ts b/src/plugins/data/public/search/expressions/esaggs/build_tabular_inspector_data.ts similarity index 78% rename from src/plugins/data/public/search/expressions/build_tabular_inspector_data.ts rename to src/plugins/data/public/search/expressions/esaggs/build_tabular_inspector_data.ts index 7eff6f25fd828..79dedf4131764 100644 --- a/src/plugins/data/public/search/expressions/build_tabular_inspector_data.ts +++ b/src/plugins/data/public/search/expressions/esaggs/build_tabular_inspector_data.ts @@ -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 { const aggConfigs = table.columns.map((column) => column.aggConfig); const rows = table.rows.map((row) => { return table.columns.reduce>((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); } }), }; diff --git a/src/plugins/data/public/search/expressions/esaggs/esaggs_fn.ts b/src/plugins/data/public/search/expressions/esaggs/esaggs_fn.ts new file mode 100644 index 0000000000000..ce3bd9bdaee76 --- /dev/null +++ b/src/plugins/data/public/search/expressions/esaggs/esaggs_fn.ts @@ -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; +}) { + 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; + }, + }); +} diff --git a/src/plugins/data/public/search/expressions/esaggs/index.ts b/src/plugins/data/public/search/expressions/esaggs/index.ts new file mode 100644 index 0000000000000..cbd3fb9cc5e91 --- /dev/null +++ b/src/plugins/data/public/search/expressions/esaggs/index.ts @@ -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'; diff --git a/src/plugins/data/public/search/expressions/esaggs/request_handler.ts b/src/plugins/data/public/search/expressions/esaggs/request_handler.ts new file mode 100644 index 0000000000000..93b5705b821c0 --- /dev/null +++ b/src/plugins/data/public/search/expressions/esaggs/request_handler.ts @@ -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; +}; diff --git a/src/plugins/data/public/services.ts b/src/plugins/data/public/services.ts index 032bce6d8d2aa..28fb4ff8b53ae 100644 --- a/src/plugins/data/public/services.ts +++ b/src/plugins/data/public/services.ts @@ -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( - 'FieldFormats' -); - export const [getOverlays, setOverlays] = createGetterSetter('Overlays'); export const [getIndexPatterns, setIndexPatterns] = createGetterSetter( 'IndexPatterns' ); -export const [getQueryService, setQueryService] = createGetterSetter< - DataPublicPluginStart['query'] ->('Query'); - export const [getSearchService, setSearchService] = createGetterSetter< DataPublicPluginStart['search'] >('Search'); diff --git a/src/plugins/discover/public/application/embeddable/search_embeddable.ts b/src/plugins/discover/public/application/embeddable/search_embeddable.ts index 170078076ec6f..980e90d0acf20 100644 --- a/src/plugins/discover/public/application/embeddable/search_embeddable.ts +++ b/src/plugins/discover/public/application/embeddable/search_embeddable.ts @@ -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, }); diff --git a/src/plugins/embeddable/public/public.api.md b/src/plugins/embeddable/public/public.api.md index 6a2565edf2f67..1bdfbe9d01a2f 100644 --- a/src/plugins/embeddable/public/public.api.md +++ b/src/plugins/embeddable/public/public.api.md @@ -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 diff --git a/src/plugins/inspector/common/adapters/data/data_adapter.ts b/src/plugins/inspector/common/adapters/data/data_adapter.ts index 34e6c278c693f..a21aa7db39145 100644 --- a/src/plugins/inspector/common/adapters/data/data_adapter.ts +++ b/src/plugins/inspector/common/adapters/data/data_adapter.ts @@ -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 }; diff --git a/src/plugins/inspector/common/adapters/data/data_adapters.test.ts b/src/plugins/inspector/common/adapters/data/data_adapters.test.ts index 287024ca1b59e..7cc52807548f0 100644 --- a/src/plugins/inspector/common/adapters/data/data_adapters.test.ts +++ b/src/plugins/inspector/common/adapters/data/data_adapters.test.ts @@ -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(); }); }); diff --git a/src/plugins/inspector/common/adapters/data/formatted_data.ts b/src/plugins/inspector/common/adapters/data/formatted_data.ts index c752e8670aca3..08c956f27d011 100644 --- a/src/plugins/inspector/common/adapters/data/formatted_data.ts +++ b/src/plugins/inspector/common/adapters/data/formatted_data.ts @@ -17,8 +17,6 @@ * under the License. */ -class FormattedData { +export class FormattedData { constructor(public readonly raw: any, public readonly formatted: any) {} } - -export { FormattedData }; diff --git a/src/plugins/inspector/common/adapters/data/index.ts b/src/plugins/inspector/common/adapters/data/index.ts index 920e298ab455f..a8b1abcd8cd7e 100644 --- a/src/plugins/inspector/common/adapters/data/index.ts +++ b/src/plugins/inspector/common/adapters/data/index.ts @@ -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'; diff --git a/src/plugins/inspector/common/adapters/data/types.ts b/src/plugins/inspector/common/adapters/data/types.ts index 1c7b17c143eca..040724f4ae36e 100644 --- a/src/plugins/inspector/common/adapters/data/types.ts +++ b/src/plugins/inspector/common/adapters/data/types.ts @@ -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; + +export interface TabularData { + columns: TabularDataColumn[]; + rows: TabularDataRow[]; +} + export type TabularCallback = () => TabularData | Promise; export interface TabularHolder { diff --git a/src/plugins/inspector/common/adapters/index.ts b/src/plugins/inspector/common/adapters/index.ts index 1e7a44a2c60b1..0c6319a2905a8 100644 --- a/src/plugins/inspector/common/adapters/index.ts +++ b/src/plugins/inspector/common/adapters/index.ts @@ -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'; diff --git a/src/plugins/inspector/common/adapters/request/request_adapter.ts b/src/plugins/inspector/common/adapters/request/request_adapter.ts index af10d1b77b16d..5f5728e1cf331 100644 --- a/src/plugins/inspector/common/adapters/request/request_adapter.ts +++ b/src/plugins/inspector/common/adapters/request/request_adapter.ts @@ -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; constructor() { @@ -78,5 +78,3 @@ class RequestAdapter extends EventEmitter { this.emit('change'); } } - -export { RequestAdapter }; diff --git a/src/plugins/inspector/common/adapters/types.ts b/src/plugins/inspector/common/adapters/types.ts index 362c69e299c9d..b51c3e56c749f 100644 --- a/src/plugins/inspector/common/adapters/types.ts +++ b/src/plugins/inspector/common/adapters/types.ts @@ -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; } diff --git a/src/plugins/inspector/common/index.ts b/src/plugins/inspector/common/index.ts index 06ab36a577d98..c5755b22095dc 100644 --- a/src/plugins/inspector/common/index.ts +++ b/src/plugins/inspector/common/index.ts @@ -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'; diff --git a/src/plugins/inspector/public/test/is_available.test.ts b/src/plugins/inspector/public/test/is_available.test.ts index 0604129a0734a..c38d9d7a3f825 100644 --- a/src/plugins/inspector/public/test/is_available.test.ts +++ b/src/plugins/inspector/public/test/is_available.test.ts @@ -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(); diff --git a/src/plugins/inspector/public/views/data/components/data_view.tsx b/src/plugins/inspector/public/views/data/components/data_view.tsx index 100fa7787321c..324094d8f93d0 100644 --- a/src/plugins/inspector/public/views/data/components/data_view.tsx +++ b/src/plugins/inspector/public/views/data/components/data_view.tsx @@ -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 | null; } interface DataViewComponentProps extends InspectorViewProps { @@ -73,7 +73,7 @@ class DataViewComponent extends Component 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; diff --git a/src/plugins/inspector/public/views/requests/components/requests_view.tsx b/src/plugins/inspector/public/views/requests/components/requests_view.tsx index 7762689daf4e6..e1879f7a6b6c8 100644 --- a/src/plugins/inspector/public/views/requests/components/requests_view.tsx +++ b/src/plugins/inspector/public/views/requests/components/requests_view.tsx @@ -31,7 +31,7 @@ import { RequestDetails } from './request_details'; interface RequestSelectorState { requests: Request[]; - request: Request; + request: Request | null; } export class RequestsViewComponent extends Component { @@ -43,9 +43,9 @@ export class RequestsViewComponent extends Component { - 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 - - + {this.state.request && ( + <> + + + + )} {this.state.request && this.state.request.description && ( diff --git a/src/plugins/ui_actions/public/public.api.md b/src/plugins/ui_actions/public/public.api.md index 3e40c94e116fb..3a14f49169e09 100644 --- a/src/plugins/ui_actions/public/public.api.md +++ b/src/plugins/ui_actions/public/public.api.md @@ -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'; diff --git a/x-pack/plugins/maps/public/classes/sources/es_source/es_source.ts b/x-pack/plugins/maps/public/classes/sources/es_source/es_source.ts index bef0b8c6ea7af..103fd11263330 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_source/es_source.ts +++ b/x-pack/plugins/maps/public/classes/sources/es_source/es_source.ts @@ -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,