Skip to content

Commit

Permalink
[Lens] Move field existence from Lens to UnifiedFieldList plugin (#13…
Browse files Browse the repository at this point in the history
…9453)

* [Lens] move field existence from to unified field list plugin

* [Lens] update readme, move integration tests

* [Lens] update wording paths

* [CI] Auto-commit changed files from 'node scripts/eslint --no-cache --fix'

* [Discover] fix loader tests, clean up code

* [Discover] update datapanel tests, clean up code

* [Discover] remove comments

* [Discover] fix problem with filters

* [Lens] apply suggestions

* [Discover] remove spread

* [Discover] fix type checks

Co-authored-by: Joe Reuter <[email protected]>
  • Loading branch information
dimaanj and flash1293 authored Sep 8, 2022
1 parent 5c9a11a commit e11bea9
Show file tree
Hide file tree
Showing 36 changed files with 779 additions and 615 deletions.
40 changes: 21 additions & 19 deletions src/plugins/data_views/public/data_views/data_views_api_client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,18 +27,17 @@ export class DataViewsApiClient implements IDataViewsApiClient {
this.http = http;
}

private _request<T = unknown>(url: string, query?: {}): Promise<T | undefined> {
return this.http
.fetch<T>(url, {
query,
})
.catch((resp) => {
if (resp.body.statusCode === 404 && resp.body.attributes?.code === 'no_matching_indices') {
throw new DataViewMissingIndices(resp.body.message);
}
private _request<T = unknown>(url: string, query?: {}, body?: string): Promise<T | undefined> {
const request = body
? this.http.post<T>(url, { query, body })
: this.http.fetch<T>(url, { query });
return request.catch((resp) => {
if (resp.body.statusCode === 404 && resp.body.attributes?.code === 'no_matching_indices') {
throw new DataViewMissingIndices(resp.body.message);
}

throw new Error(resp.body.message || resp.body.error || `${resp.body.statusCode} Response`);
});
throw new Error(resp.body.message || resp.body.error || `${resp.body.statusCode} Response`);
});
}

private _getUrl(path: string[]) {
Expand All @@ -51,14 +50,17 @@ export class DataViewsApiClient implements IDataViewsApiClient {
*/
getFieldsForWildcard(options: GetFieldsOptions) {
const { pattern, metaFields, type, rollupIndex, allowNoIndex, filter } = options;
return this._request<FieldsForWildcardResponse>(this._getUrl(['_fields_for_wildcard']), {
pattern,
meta_fields: metaFields,
type,
rollup_index: rollupIndex,
allow_no_index: allowNoIndex,
filter,
}).then((response) => {
return this._request<FieldsForWildcardResponse>(
this._getUrl(['_fields_for_wildcard']),
{
pattern,
meta_fields: metaFields,
type,
rollup_index: rollupIndex,
allow_no_index: allowNoIndex,
},
filter ? JSON.stringify({ index_filter: filter }) : undefined
).then((response) => {
return response || { fields: [], indices: [] };
});
}
Expand Down
1 change: 1 addition & 0 deletions src/plugins/data_views/public/mocks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ const createStartContract = (): Start => {
clearCache: jest.fn(),
getCanSaveSync: jest.fn(),
getIdsWithTitle: jest.fn(),
getFieldsForIndexPattern: jest.fn(),
} as unknown as jest.Mocked<DataViewsContract>;
};

Expand Down
1 change: 1 addition & 0 deletions src/plugins/data_views/server/routes/fields_for.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,5 +118,6 @@ export const registerFieldForWildcard = (
>
) => {
router.put({ path, validate }, handler);
router.post({ path, validate }, handler);
router.get({ path, validate }, handler);
};
4 changes: 4 additions & 0 deletions src/plugins/unified_field_list/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,14 @@ This Kibana plugin contains components and services for field list UI (as in fie

* `loadStats(...)` - returns the loaded field stats (can also work with Ad-hoc data views)

* `loadFieldExisting(...)` - returns the loaded existing fields (can also work with Ad-hoc data views)

## Server APIs

* `/api/unified_field_list/field_stats` - returns the loaded field stats (except for Ad-hoc data views)

* `/api/unified_field_list/existing_fields/{dataViewId}` - returns the loaded existing fields (except for Ad-hoc data views)

## Development

See the [kibana contributing guide](https://github.com/elastic/kibana/blob/main/CONTRIBUTING.md) for instructions setting up your development environment.
1 change: 1 addition & 0 deletions src/plugins/unified_field_list/common/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@

export const BASE_API_PATH = '/api/unified_field_list';
export const FIELD_STATS_API_PATH = `${BASE_API_PATH}/field_stats`;
export const FIELD_EXISTING_API_PATH = `${BASE_API_PATH}/existing_fields/{dataViewId}`;
1 change: 1 addition & 0 deletions src/plugins/unified_field_list/common/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@
*/

export const PLUGIN_ID = 'unifiedFieldList';
export const FIELD_EXISTENCE_SETTING = 'lens:useFieldExistenceSampling';
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/

import { DataView } from '@kbn/data-views-plugin/common';
import { legacyExistingFields, existingFields, Field, buildFieldList } from './existing_fields';
import {
legacyExistingFields,
existingFields,
Field,
buildFieldList,
} from './field_existing_utils';

describe('existingFields', () => {
it('should remove missing fields by matching names', () => {
Expand Down
266 changes: 266 additions & 0 deletions src/plugins/unified_field_list/common/utils/field_existing_utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,266 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/

import Boom from '@hapi/boom';
import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import { RuntimeField } from '@kbn/data-views-plugin/common';
import type { DataViewsContract, DataView, FieldSpec } from '@kbn/data-views-plugin/common';
import { IKibanaSearchRequest } from '@kbn/data-plugin/common';

export type SearchHandler = (
params: IKibanaSearchRequest['params']
) => Promise<estypes.SearchResponse<Array<estypes.SearchHit<unknown>>>>;

/**
* The number of docs to sample to determine field empty status.
*/
const SAMPLE_SIZE = 500;

export interface Field {
name: string;
isScript: boolean;
isMeta: boolean;
lang?: estypes.ScriptLanguage;
script?: string;
runtimeField?: RuntimeField;
}

export async function fetchFieldExistence({
search,
dataViewsService,
dataView,
dslQuery = { match_all: {} },
fromDate,
toDate,
timeFieldName,
includeFrozen,
metaFields,
useSampling,
}: {
search: SearchHandler;
dataView: DataView;
dslQuery: object;
fromDate?: string;
toDate?: string;
timeFieldName?: string;
includeFrozen: boolean;
useSampling: boolean;
metaFields: string[];
dataViewsService: DataViewsContract;
}) {
if (useSampling) {
return legacyFetchFieldExistenceSampling({
search,
metaFields,
dataView,
dataViewsService,
dslQuery,
fromDate,
toDate,
timeFieldName,
includeFrozen,
});
}

const allFields = buildFieldList(dataView, metaFields);
const existingFieldList = await dataViewsService.getFieldsForIndexPattern(dataView, {
// filled in by data views service
pattern: '',
filter: toQuery(timeFieldName, fromDate, toDate, dslQuery),
});
return {
indexPatternTitle: dataView.title,
existingFieldNames: existingFields(existingFieldList, allFields),
};
}

async function legacyFetchFieldExistenceSampling({
search,
metaFields,
dataView,
dslQuery,
fromDate,
toDate,
timeFieldName,
includeFrozen,
}: {
search: SearchHandler;
metaFields: string[];
dataView: DataView;
dataViewsService: DataViewsContract;
dslQuery: object;
fromDate?: string;
toDate?: string;
timeFieldName?: string;
includeFrozen: boolean;
}) {
const fields = buildFieldList(dataView, metaFields);
const runtimeMappings = dataView.getRuntimeMappings();

const docs = await fetchDataViewStats({
search,
fromDate,
toDate,
dslQuery,
index: dataView.title,
timeFieldName: timeFieldName || dataView.timeFieldName,
fields,
runtimeMappings,
includeFrozen,
});

return {
indexPatternTitle: dataView.title,
existingFieldNames: legacyExistingFields(docs, fields),
};
}

/**
* Exported only for unit tests.
*/
export function buildFieldList(indexPattern: DataView, metaFields: string[]): Field[] {
return indexPattern.fields.map((field) => {
return {
name: field.name,
isScript: !!field.scripted,
lang: field.lang,
script: field.script,
// id is a special case - it doesn't show up in the meta field list,
// but as it's not part of source, it has to be handled separately.
isMeta: metaFields.includes(field.name) || field.name === '_id',
runtimeField: !field.isMapped ? field.runtimeField : undefined,
};
});
}

async function fetchDataViewStats({
search,
index,
dslQuery,
timeFieldName,
fromDate,
toDate,
fields,
runtimeMappings,
includeFrozen,
}: {
search: SearchHandler;
index: string;
dslQuery: object;
timeFieldName?: string;
fromDate?: string;
toDate?: string;
fields: Field[];
runtimeMappings: estypes.MappingRuntimeFields;
includeFrozen: boolean;
}) {
const query = toQuery(timeFieldName, fromDate, toDate, dslQuery);

const scriptedFields = fields.filter((f) => f.isScript);
const response = await search({
index,
...(includeFrozen ? { ignore_throttled: false } : {}),
body: {
size: SAMPLE_SIZE,
query,
// Sorted queries are usually able to skip entire shards that don't match
sort: timeFieldName && fromDate && toDate ? [{ [timeFieldName]: 'desc' }] : [],
fields: ['*'],
_source: false,
runtime_mappings: runtimeMappings,
script_fields: scriptedFields.reduce((acc, field) => {
acc[field.name] = {
script: {
lang: field.lang!,
source: field.script!,
},
};
return acc;
}, {} as Record<string, estypes.ScriptField>),
// Small improvement because there is overhead in counting
track_total_hits: false,
// Per-shard timeout, must be lower than overall. Shards return partial results on timeout
timeout: '4500ms',
},
});

return response?.hits.hits;
}

function toQuery(
timeFieldName: string | undefined,
fromDate: string | undefined,
toDate: string | undefined,
dslQuery: object
) {
const filter =
timeFieldName && fromDate && toDate
? [
{
range: {
[timeFieldName]: {
format: 'strict_date_optional_time',
gte: fromDate,
lte: toDate,
},
},
},
dslQuery,
]
: [dslQuery];

const query = {
bool: {
filter,
},
};
return query;
}

/**
* Exported only for unit tests.
*/
export function existingFields(filteredFields: FieldSpec[], allFields: Field[]): string[] {
const filteredFieldsSet = new Set(filteredFields.map((f) => f.name));

return allFields
.filter((field) => field.isScript || field.runtimeField || filteredFieldsSet.has(field.name))
.map((f) => f.name);
}

/**
* Exported only for unit tests.
*/
export function legacyExistingFields(docs: estypes.SearchHit[], fields: Field[]): string[] {
const missingFields = new Set(fields);

for (const doc of docs) {
if (missingFields.size === 0) {
break;
}

missingFields.forEach((field) => {
let fieldStore = doc.fields!;
if (field.isMeta) {
fieldStore = doc;
}
const value = fieldStore[field.name];
if (Array.isArray(value) && value.length) {
missingFields.delete(field);
} else if (!Array.isArray(value) && value) {
missingFields.delete(field);
}
});
}

return fields.filter((field) => !missingFields.has(field)).map((f) => f.name);
}

export function isBoomError(error: { isBoom?: boolean }): error is Boom.Boom {
return error.isBoom === true;
}
1 change: 1 addition & 0 deletions src/plugins/unified_field_list/public/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export type {
export type { FieldStatsProps, FieldStatsServices } from './components/field_stats';
export { FieldStats } from './components/field_stats';
export { loadFieldStats } from './services/field_stats';
export { loadFieldExisting } from './services/field_existing';

// This exports static code and TypeScript types,
// as well as, Kibana Platform `plugin()` initializer.
Expand Down
Loading

0 comments on commit e11bea9

Please sign in to comment.