mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[esaggs][inspector]: Refactor to prep for esaggs move to server. (#83199)
This commit is contained in:
parent
b3eefb97da
commit
62e06aee9b
34 changed files with 629 additions and 486 deletions
|
@ -0,0 +1,11 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[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
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
data?: DataAdapter;
|
||||
```
|
|
@ -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> | |
|
||||
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[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
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
requests?: RequestAdapter;
|
||||
```
|
|
@ -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>;
|
||||
/**
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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;
|
||||
},
|
||||
});
|
|
@ -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);
|
||||
}
|
||||
}),
|
||||
};
|
155
src/plugins/data/public/search/expressions/esaggs/esaggs_fn.ts
Normal file
155
src/plugins/data/public/search/expressions/esaggs/esaggs_fn.ts
Normal 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;
|
||||
},
|
||||
});
|
||||
}
|
20
src/plugins/data/public/search/expressions/esaggs/index.ts
Normal file
20
src/plugins/data/public/search/expressions/esaggs/index.ts
Normal 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';
|
|
@ -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;
|
||||
};
|
|
@ -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');
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 };
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -17,8 +17,6 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
class FormattedData {
|
||||
export class FormattedData {
|
||||
constructor(public readonly raw: any, public readonly formatted: any) {}
|
||||
}
|
||||
|
||||
export { FormattedData };
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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 };
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue