mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
[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:
parent
5f2c0b3c8a
commit
cf5280de5c
5 changed files with 109 additions and 89 deletions
|
@ -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) => (
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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,
|
||||
};
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue