[Discover] Migrate field_calculator.js to typescript (#148187)

## Summary

Closes #138114

This PR replaces `field_calculator.js` with `field_calculator.ts`
This commit is contained in:
Dmitry Tomashevich 2023-01-03 18:41:31 +03:00 committed by GitHub
parent 5f2c0b3c8a
commit cf5280de5c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 109 additions and 89 deletions

View file

@ -12,8 +12,8 @@ import { EuiText, EuiSpacer, EuiLink, EuiTitle } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import { DataViewField, DataView } from '@kbn/data-views-plugin/public';
import { DiscoverFieldBucket } from './discover_field_bucket';
import { Bucket, FieldDetails } from './types';
import { getDetails } from './get_details';
import { Bucket } from './types';
import { getDetails, isValidFieldDetails } from './get_details';
import { DataDocuments$ } from '../../../hooks/use_saved_search';
import { FetchStatus } from '../../../../types';
@ -33,13 +33,13 @@ export function DiscoverFieldDetails({
dataView,
onAddFilter,
}: DiscoverFieldDetailsProps) {
const details: FieldDetails = useMemo(() => {
const details = useMemo(() => {
const data = documents$.getValue();
const documents = data.fetchStatus === FetchStatus.COMPLETE ? data.result : undefined;
return getDetails(field, documents, dataView);
}, [field, documents$, dataView]);
if (!details?.error && !details?.buckets) {
if (!details) {
return null;
}
@ -52,8 +52,8 @@ export function DiscoverFieldDetails({
})}
</h5>
</EuiTitle>
{details.error && <EuiText size="xs">{details.error}</EuiText>}
{!details.error && (
{!isValidFieldDetails(details) && <EuiText size="xs">{details.error}</EuiText>}
{isValidFieldDetails(details) && (
<>
<div style={{ marginTop: '4px' }}>
{details.buckets.map((bucket: Bucket, idx: number) => (

View file

@ -9,10 +9,21 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { keys, clone, uniq, filter, map } from 'lodash';
import { getDataTableRecords } from '../../../../../__fixtures__/real_hits';
import type { DataView } from '@kbn/data-views-plugin/public';
// @ts-expect-error
import { fieldCalculator } from './field_calculator';
import { fieldCalculator, FieldCountsParams } from './field_calculator';
import { stubLogstashDataView as dataView } from '@kbn/data-views-plugin/common/data_view.stub';
import { FieldDetails, ValidFieldDetails } from './types';
import { isValidFieldDetails } from './get_details';
const validateResults = (
extensions: FieldDetails,
validate: (extensions: ValidFieldDetails) => void
) => {
if (isValidFieldDetails(extensions)) {
validate(extensions);
} else {
throw new Error('extensions is not valid');
}
};
describe('fieldCalculator', function () {
it('should have a _countMissing that counts nulls & undefineds in an array', function () {
@ -122,72 +133,67 @@ describe('fieldCalculator', function () {
it('Should return an array of values for _source fields', function () {
const extensions = fieldCalculator.getFieldValues(
hits,
dataView.fields.getByName('extension'),
dataView
dataView.fields.getByName('extension')!
);
expect(extensions).toBeInstanceOf(Array);
expect(
filter(extensions, function (v) {
return v === 'html';
}).length
).toBe(8);
expect(filter(extensions, (v) => v === 'html').length).toBe(8);
expect(uniq(clone(extensions)).sort()).toEqual(['gif', 'html', 'php', 'png']);
});
it('Should return an array of values for core meta fields', function () {
const types = fieldCalculator.getFieldValues(
hits,
dataView.fields.getByName('_id'),
dataView
);
const types = fieldCalculator.getFieldValues(hits, dataView.fields.getByName('_id')!);
expect(types).toBeInstanceOf(Array);
expect(types.length).toBe(20);
});
});
describe('getFieldValueCounts', function () {
let params: { hits: any; field: any; count: number; dataView: DataView };
let params: FieldCountsParams;
beforeEach(function () {
params = {
hits: getDataTableRecords(dataView),
field: dataView.fields.getByName('extension'),
field: dataView.fields.getByName('extension')!,
count: 3,
dataView,
};
});
it('counts the top 3 values', function () {
const extensions = fieldCalculator.getFieldValueCounts(params);
expect(extensions).toBeInstanceOf(Object);
expect(extensions.buckets).toBeInstanceOf(Array);
expect(extensions.buckets.length).toBe(3);
expect(map(extensions.buckets, 'value')).toEqual(['html', 'php', 'gif']);
expect(extensions.error).toBe(undefined);
validateResults(fieldCalculator.getFieldValueCounts(params), (extensions) => {
expect(extensions).toBeInstanceOf(Object);
expect(extensions.buckets).toBeInstanceOf(Array);
expect(extensions.buckets.length).toBe(3);
expect(map(extensions.buckets, 'value')).toEqual(['html', 'php', 'gif']);
});
});
it('fails to analyze geo and attachment types', function () {
params.field = dataView.fields.getByName('point');
expect(fieldCalculator.getFieldValueCounts(params).error).not.toBe(undefined);
params.field = dataView.fields.getByName('point')!;
expect(isValidFieldDetails(fieldCalculator.getFieldValueCounts(params))).toBeFalsy();
params.field = dataView.fields.getByName('area');
expect(fieldCalculator.getFieldValueCounts(params).error).not.toBe(undefined);
params.field = dataView.fields.getByName('area')!;
expect(isValidFieldDetails(fieldCalculator.getFieldValueCounts(params))).toBeFalsy();
params.field = dataView.fields.getByName('request_body');
expect(fieldCalculator.getFieldValueCounts(params).error).not.toBe(undefined);
params.field = dataView.fields.getByName('request_body')!;
expect(isValidFieldDetails(fieldCalculator.getFieldValueCounts(params))).toBeFalsy();
});
it('fails to analyze fields that are in the mapping, but not the hits', function () {
params.field = dataView.fields.getByName('ip');
expect(fieldCalculator.getFieldValueCounts(params).error).not.toBe(undefined);
params.field = dataView.fields.getByName('ip')!;
expect(isValidFieldDetails(fieldCalculator.getFieldValueCounts(params))).toBeFalsy();
});
it('counts the total hits', function () {
expect(fieldCalculator.getFieldValueCounts(params).total).toBe(params.hits.length);
validateResults(fieldCalculator.getFieldValueCounts(params), (extensions) => {
expect(extensions.total).toBe(params.hits.length);
});
});
it('counts the hits the field exists in', function () {
params.field = dataView.fields.getByName('phpmemory');
expect(fieldCalculator.getFieldValueCounts(params).exists).toBe(5);
params.field = dataView.fields.getByName('phpmemory')!;
validateResults(fieldCalculator.getFieldValueCounts(params), (extensions) => {
expect(extensions.exists).toBe(5);
});
});
});
});

View file

@ -8,15 +8,27 @@
import { map, sortBy, without, each, defaults, isObject } from 'lodash';
import { i18n } from '@kbn/i18n';
import type { DataViewField, DataView } from '@kbn/data-views-plugin/common';
import type { DataTableRecord } from '../../../../../types';
import { Bucket, FieldDetails } from './types';
function getFieldValues(hits, field) {
const name = field.name;
return map(hits, function (hit) {
return hit.flattened[name];
});
export interface FieldCountsParams {
hits: DataTableRecord[];
field: DataViewField;
dataView: DataView;
count?: number;
grouped?: boolean;
}
function getFieldValueCounts(params) {
interface FieldCountsBucket {
count: number;
value: string;
}
const getFieldValues = (hits: DataTableRecord[], field: DataViewField): unknown[] =>
map(hits, (hit) => hit.flattened[field.name]);
const getFieldValueCounts = (params: FieldCountsParams): FieldDetails => {
params = defaults(params, {
count: 5,
grouped: false,
@ -38,18 +50,19 @@ function getFieldValueCounts(params) {
}
const allValues = getFieldValues(params.hits, params.field);
let counts;
const missing = _countMissing(allValues);
try {
const groups = _groupValues(allValues, params);
counts = map(sortBy(groups, 'count').reverse().slice(0, params.count), function (bucket) {
return {
const counts: Bucket[] = sortBy(groups, 'count')
.reverse()
.slice(0, params.count)
.map((bucket: FieldCountsBucket) => ({
value: bucket.value,
count: bucket.count,
percent: ((bucket.count / (params.hits.length - missing)) * 100).toFixed(1),
};
});
count: bucket.count as number,
percent: Number(((bucket.count / (params.hits.length - missing)) * 100).toFixed(1)),
display: params.dataView.getFormatterForField(params.field).convert(bucket.value),
}));
if (params.hits.length - missing === 0) {
return {
@ -69,24 +82,22 @@ function getFieldValueCounts(params) {
return {
total: params.hits.length,
exists: params.hits.length - missing,
missing: missing,
missing,
buckets: counts,
};
} catch (e) {
return { error: e.message };
}
}
};
// returns a count of fields in the array that are undefined or null
function _countMissing(array) {
return array.length - without(array, undefined, null).length;
}
const _countMissing = (array: unknown[]) => array.length - without(array, undefined, null).length;
function _groupValues(allValues, params) {
const groups = {};
const _groupValues = (allValues: unknown[], params: FieldCountsParams) => {
const groups: Record<string, FieldCountsBucket> = {};
let k;
allValues.forEach(function (value) {
allValues.forEach((value: unknown) => {
if (isObject(value) && !Array.isArray(value)) {
throw new Error(
i18n.translate(
@ -104,12 +115,12 @@ function _groupValues(allValues, params) {
k = value == null ? undefined : [value];
}
each(k, function (key) {
each(k, (key: string) => {
if (groups.hasOwnProperty(key)) {
groups[key].count++;
(groups[key] as FieldCountsBucket).count++;
} else {
groups[key] = {
value: params.grouped ? value : key,
value: params.grouped ? (value as string) : key,
count: 1,
};
}
@ -117,11 +128,11 @@ function _groupValues(allValues, params) {
});
return groups;
}
};
export const fieldCalculator = {
_groupValues: _groupValues,
_countMissing: _countMissing,
getFieldValues: getFieldValues,
getFieldValueCounts: getFieldValueCounts,
_groupValues,
_countMissing,
getFieldValues,
getFieldValueCounts,
};

View file

@ -7,30 +7,27 @@
*/
import { DataView, DataViewField } from '@kbn/data-views-plugin/public';
// @ts-expect-error
import { fieldCalculator } from './field_calculator';
import { DataTableRecord } from '../../../../../types';
import { ErrorFieldDetails, FieldDetails, ValidFieldDetails } from './types';
export const isValidFieldDetails = (details: FieldDetails): details is ValidFieldDetails =>
!(details as ErrorFieldDetails).error;
export function getDetails(
field: DataViewField,
hits: DataTableRecord[] | undefined,
dataView?: DataView
dataView: DataView
) {
if (!dataView || !hits) {
return {};
if (!hits) {
return undefined;
}
const details = {
...fieldCalculator.getFieldValueCounts({
hits,
field,
count: 5,
grouped: false,
}),
};
if (details.buckets) {
for (const bucket of details.buckets) {
bucket.display = dataView.getFormatterForField(field).convert(bucket.value);
}
}
return details;
return fieldCalculator.getFieldValueCounts({
hits,
field,
count: 5,
grouped: false,
dataView,
});
}

View file

@ -6,13 +6,19 @@
* Side Public License, v 1.
*/
export interface FieldDetails {
error: string;
export interface ValidFieldDetails {
exists: number;
total: number;
missing: number;
buckets: Bucket[];
}
export interface ErrorFieldDetails {
error: string;
}
export type FieldDetails = ValidFieldDetails | ErrorFieldDetails;
export interface Bucket {
display: string;
value: string;