+ {details.error && (
+
+ {details.error}
+
+ )}
+
+ {!details.error && details.buckets.length > 0 && (
+
{details.buckets.map((bucket: Bucket, idx: number) => (
)}
- {showVisualizeLink && (
- <>
+ {showVisualizeLink && visualizeLink && (
+
{/* eslint-disable-next-line @elastic/eui/href-or-on-click */}
0 && (
)}
- >
+
)}
{!details.error && (
diff --git a/src/plugins/discover/public/application/components/sidebar/discover_sidebar.tsx b/src/plugins/discover/public/application/components/sidebar/discover_sidebar.tsx
index f957b93a4cc4..865aff590286 100644
--- a/src/plugins/discover/public/application/components/sidebar/discover_sidebar.tsx
+++ b/src/plugins/discover/public/application/components/sidebar/discover_sidebar.tsx
@@ -117,8 +117,8 @@ export function DiscoverSidebar({
);
const getDetailsByField = useCallback(
- (ipField: IndexPatternField) => getDetails(ipField, hits, columns, selectedIndexPattern),
- [hits, columns, selectedIndexPattern]
+ (ipField: IndexPatternField) => getDetails(ipField, hits, selectedIndexPattern),
+ [hits, selectedIndexPattern]
);
const popularLimit = services.uiSettings.get(FIELDS_LIMIT_SETTING);
@@ -199,6 +199,7 @@ export function DiscoverSidebar({
className="dscSidebar__item"
>
;
- let params: any;
- let values: any;
+ let grouped: boolean;
+ let values: any[];
beforeEach(function () {
values = [
['foo', 'bar'],
@@ -88,30 +76,28 @@ describe('fieldCalculator', function () {
'foo',
undefined,
];
- params = {};
- groups = fieldCalculator._groupValues(values, params);
+ groups = groupValues(values, grouped);
});
- it('should have a _groupValues that counts values', function () {
+ it('should return an object values', function () {
expect(groups).toBeInstanceOf(Object);
});
it('should throw an error if any value is a plain object', function () {
expect(function () {
- fieldCalculator._groupValues([{}, true, false], params);
+ groupValues([{}, true, false], grouped);
}).toThrowError();
});
it('should handle values with dots in them', function () {
values = ['0', '0.........', '0.......,.....'];
- params = {};
- groups = fieldCalculator._groupValues(values, params);
+ groups = groupValues(values, grouped);
expect(groups[values[0]].count).toBe(1);
expect(groups[values[1]].count).toBe(1);
expect(groups[values[2]].count).toBe(1);
});
- it('should have a a key for value in the array when not grouping array terms', function () {
+ it('should have a key for value in the array when not grouping array terms', function () {
expect(_.keys(groups).length).toBe(3);
expect(groups.foo).toBeInstanceOf(Object);
expect(groups.bar).toBeInstanceOf(Object);
@@ -119,7 +105,7 @@ describe('fieldCalculator', function () {
});
it('should count array terms independently', function () {
- expect(groups['foo,bar']).toBe(undefined);
+ expect(groups['foo,bar']).toBeUndefined();
expect(groups.foo.count).toBe(5);
expect(groups.bar.count).toBe(3);
expect(groups.baz.count).toBe(1);
@@ -127,11 +113,11 @@ describe('fieldCalculator', function () {
describe('grouped array terms', function () {
beforeEach(function () {
- params.grouped = true;
- groups = fieldCalculator._groupValues(values, params);
+ grouped = true;
+ groups = groupValues(values, grouped);
});
- it('should group array terms when passed params.grouped', function () {
+ it('should group array terms when grouped is true', function () {
expect(_.keys(groups).length).toBe(4);
expect(groups['foo,bar']).toBeInstanceOf(Object);
});
@@ -155,12 +141,12 @@ describe('fieldCalculator', function () {
hits = _.each(_.cloneDeep(realHits), (hit) => indexPattern.flattenHit(hit));
});
- it('Should return an array of values for _source fields', function () {
- const extensions = fieldCalculator.getFieldValues(
+ it('should return an array of values for _source fields', function () {
+ const extensions = getFieldValues({
hits,
- indexPattern.fields.getByName('extension'),
- indexPattern
- );
+ field: indexPattern.fields.getByName('extension') as IndexPatternField,
+ indexPattern,
+ });
expect(extensions).toBeInstanceOf(Array);
expect(
_.filter(extensions, function (v) {
@@ -170,12 +156,12 @@ describe('fieldCalculator', function () {
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(
+ it('should return an array of values for core meta fields', function () {
+ const types = getFieldValues({
hits,
- indexPattern.fields.getByName('_type'),
- indexPattern
- );
+ field: indexPattern.fields.getByName('_type') as IndexPatternField,
+ indexPattern,
+ });
expect(types).toBeInstanceOf(Array);
expect(
_.filter(types, function (v) {
@@ -187,48 +173,96 @@ describe('fieldCalculator', function () {
});
describe('getFieldValueCounts', function () {
- let params: { hits: any; field: any; count: number; indexPattern: IndexPattern };
+ let params: FieldValueCountsParams;
beforeEach(function () {
params = {
hits: _.cloneDeep(realHits),
- field: indexPattern.fields.getByName('extension'),
+ field: indexPattern.fields.getByName('extension') as IndexPatternField,
count: 3,
indexPattern,
};
});
+ it('counts the top 5 values by default', function () {
+ params.hits = params.hits.map((hit: Record, i) => ({
+ ...hit,
+ _source: {
+ extension: `${hit._source.extension}-${i}`,
+ },
+ }));
+ params.count = undefined;
+ const extensions = getFieldValueCounts(params);
+ expect(extensions).toBeInstanceOf(Object);
+ expect(extensions.buckets).toBeInstanceOf(Array);
+ const buckets = extensions.buckets as Bucket[];
+ expect(buckets.length).toBe(5);
+ expect(extensions.error).toBeUndefined();
+ });
+
+ it('counts only distinct values if less than default', function () {
+ params.count = undefined;
+ const extensions = getFieldValueCounts(params);
+ expect(extensions).toBeInstanceOf(Object);
+ expect(extensions.buckets).toBeInstanceOf(Array);
+ const buckets = extensions.buckets as Bucket[];
+ expect(buckets.length).toBe(4);
+ expect(extensions.error).toBeUndefined();
+ });
+
+ it('counts only distinct values if less than specified count', function () {
+ params.count = 10;
+ const extensions = getFieldValueCounts(params);
+ expect(extensions).toBeInstanceOf(Object);
+ expect(extensions.buckets).toBeInstanceOf(Array);
+ const buckets = extensions.buckets as Bucket[];
+ expect(buckets.length).toBe(4);
+ expect(extensions.error).toBeUndefined();
+ });
+
it('counts the top 3 values', function () {
- const extensions = fieldCalculator.getFieldValueCounts(params);
+ const extensions = 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);
+ const buckets = extensions.buckets as Bucket[];
+ expect(buckets.length).toBe(3);
+ expect(_.map(buckets, 'value')).toEqual(['html', 'gif', 'php']);
+ expect(extensions.error).toBeUndefined();
});
it('fails to analyze geo and attachment types', function () {
- params.field = indexPattern.fields.getByName('point');
- expect(fieldCalculator.getFieldValueCounts(params).error).not.toBe(undefined);
+ params.field = indexPattern.fields.getByName('point') as IndexPatternField;
+ expect(getFieldValueCounts(params).error).not.toBeUndefined();
- params.field = indexPattern.fields.getByName('area');
- expect(fieldCalculator.getFieldValueCounts(params).error).not.toBe(undefined);
+ params.field = indexPattern.fields.getByName('area') as IndexPatternField;
+ expect(getFieldValueCounts(params).error).not.toBeUndefined();
- params.field = indexPattern.fields.getByName('request_body');
- expect(fieldCalculator.getFieldValueCounts(params).error).not.toBe(undefined);
+ params.field = indexPattern.fields.getByName('request_body') as IndexPatternField;
+ expect(getFieldValueCounts(params).error).not.toBeUndefined();
});
it('fails to analyze fields that are in the mapping, but not the hits', function () {
- params.field = indexPattern.fields.getByName('ip');
- expect(fieldCalculator.getFieldValueCounts(params).error).not.toBe(undefined);
+ params.field = indexPattern.fields.getByName('ip') as IndexPatternField;
+ expect(getFieldValueCounts(params).error).not.toBeUndefined();
});
it('counts the total hits', function () {
- expect(fieldCalculator.getFieldValueCounts(params).total).toBe(params.hits.length);
+ expect(getFieldValueCounts(params).total).toBe(params.hits.length);
});
it('counts the hits the field exists in', function () {
- params.field = indexPattern.fields.getByName('phpmemory');
- expect(fieldCalculator.getFieldValueCounts(params).exists).toBe(5);
+ params.field = indexPattern.fields.getByName('phpmemory') as IndexPatternField;
+ expect(getFieldValueCounts(params).exists).toBe(5);
+ });
+
+ it('catches and returns errors', function () {
+ params.hits = params.hits.map((hit: Record) => ({
+ ...hit,
+ _source: {
+ extension: { foo: hit._source.extension },
+ },
+ }));
+ params.grouped = true;
+ expect(typeof getFieldValueCounts(params).error).toBe('string');
});
});
});
diff --git a/src/plugins/discover/public/application/components/sidebar/lib/field_calculator.ts b/src/plugins/discover/public/application/components/sidebar/lib/field_calculator.ts
new file mode 100644
index 000000000000..54f8832fa1fc
--- /dev/null
+++ b/src/plugins/discover/public/application/components/sidebar/lib/field_calculator.ts
@@ -0,0 +1,148 @@
+/*
+ * SPDX-License-Identifier: Apache-2.0
+ *
+ * The OpenSearch Contributors require contributions made to
+ * this file be licensed under the Apache-2.0 license or a
+ * compatible open source license.
+ *
+ * Any modifications Copyright OpenSearch Contributors. See
+ * GitHub history for details.
+ */
+
+/*
+ * 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 '@osd/i18n';
+import { IndexPattern, IndexPatternField } from 'src/plugins/data/public';
+import { FieldValueCounts } from '../types';
+
+const NO_ANALYSIS_TYPES = ['geo_point', 'geo_shape', 'attachment'];
+
+interface FieldValuesParams {
+ hits: Array>;
+ field: IndexPatternField;
+ indexPattern: IndexPattern;
+}
+
+interface FieldValueCountsParams extends FieldValuesParams {
+ count?: number;
+ grouped?: boolean;
+}
+
+const getFieldValues = ({ hits, field, indexPattern }: FieldValuesParams) => {
+ const name = field.name;
+ const flattenHit = indexPattern.flattenHit;
+ return hits.map((hit) => flattenHit(hit)[name]);
+};
+
+const getFieldValueCounts = (params: FieldValueCountsParams): FieldValueCounts => {
+ const { hits, field, indexPattern, count = 5, grouped = false } = params;
+ const { type: fieldType } = field;
+
+ if (NO_ANALYSIS_TYPES.includes(fieldType)) {
+ return {
+ error: i18n.translate(
+ 'discover.fieldChooser.fieldCalculator.analysisIsNotAvailableForGeoFieldsErrorMessage',
+ {
+ defaultMessage: 'Analysis is not available for {fieldType} fields.',
+ values: {
+ fieldType,
+ },
+ }
+ ),
+ };
+ }
+
+ const allValues = getFieldValues({ hits, field, indexPattern });
+ const missing = allValues.filter((v) => v === undefined || v === null).length;
+
+ try {
+ const groups = groupValues(allValues, grouped);
+ const counts = Object.keys(groups)
+ .sort((a, b) => groups[b].count - groups[a].count)
+ .slice(0, count)
+ .map((key) => ({
+ value: groups[key].value,
+ count: groups[key].count,
+ percent: (groups[key].count / (hits.length - missing)) * 100,
+ display: indexPattern.getFormatterForField(field).convert(groups[key].value),
+ }));
+
+ if (hits.length === missing) {
+ return {
+ error: i18n.translate(
+ 'discover.fieldChooser.fieldCalculator.fieldIsNotPresentInDocumentsErrorMessage',
+ {
+ defaultMessage:
+ 'This field is present in your OpenSearch mapping but not in the {hitsLength} documents shown in the doc table. You may still be able to visualize or search on it.',
+ values: {
+ hitsLength: hits.length,
+ },
+ }
+ ),
+ };
+ }
+
+ return {
+ total: hits.length,
+ exists: hits.length - missing,
+ missing,
+ buckets: counts,
+ };
+ } catch (e) {
+ return {
+ error: e instanceof Error ? e.message : String(e),
+ };
+ }
+};
+
+const groupValues = (
+ allValues: any[],
+ grouped?: boolean
+): Record => {
+ const values = grouped ? allValues : allValues.flat();
+
+ return values
+ .filter((v) => {
+ if (v instanceof Object && !Array.isArray(v)) {
+ throw new Error(
+ i18n.translate(
+ 'discover.fieldChooser.fieldCalculator.analysisIsNotAvailableForObjectFieldsErrorMessage',
+ {
+ defaultMessage: 'Analysis is not available for object fields.',
+ }
+ )
+ );
+ }
+ return v !== undefined && v !== null;
+ })
+ .reduce((groups, value) => {
+ if (groups.hasOwnProperty(value)) {
+ groups[value].count++;
+ } else {
+ groups[value] = {
+ value,
+ count: 1,
+ };
+ }
+ return groups;
+ }, {});
+};
+
+export { FieldValueCountsParams, groupValues, getFieldValues, getFieldValueCounts };
diff --git a/src/plugins/discover/public/application/components/sidebar/lib/get_details.ts b/src/plugins/discover/public/application/components/sidebar/lib/get_details.ts
index fb8f22e202cd..823cbde9ba72 100644
--- a/src/plugins/discover/public/application/components/sidebar/lib/get_details.ts
+++ b/src/plugins/discover/public/application/components/sidebar/lib/get_details.ts
@@ -29,27 +29,38 @@
*/
// @ts-ignore
-import { fieldCalculator } from './field_calculator';
+import { i18n } from '@osd/i18n';
+import { getFieldValueCounts } from './field_calculator';
import { IndexPattern, IndexPatternField } from '../../../../../../data/public';
export function getDetails(
field: IndexPatternField,
hits: Array>,
- columns: string[],
indexPattern?: IndexPattern
) {
+ const defaultDetails = {
+ error: '',
+ exists: 0,
+ total: 0,
+ buckets: [],
+ };
if (!indexPattern) {
- return {};
+ return {
+ ...defaultDetails,
+ error: i18n.translate('discover.fieldChooser.noIndexPatternSelectedErrorMessage', {
+ defaultMessage: 'Index pattern not specified.',
+ }),
+ };
}
const details = {
- ...fieldCalculator.getFieldValueCounts({
+ ...defaultDetails,
+ ...getFieldValueCounts({
hits,
field,
indexPattern,
count: 5,
grouped: false,
}),
- columns,
};
if (details.buckets) {
for (const bucket of details.buckets) {
diff --git a/src/plugins/discover/public/application/components/sidebar/types.ts b/src/plugins/discover/public/application/components/sidebar/types.ts
index b254057b0de0..a43120b28e96 100644
--- a/src/plugins/discover/public/application/components/sidebar/types.ts
+++ b/src/plugins/discover/public/application/components/sidebar/types.ts
@@ -36,9 +36,12 @@ export interface IndexPatternRef {
export interface FieldDetails {
error: string;
exists: number;
- total: boolean;
+ total: number;
buckets: Bucket[];
- columns: string[];
+}
+
+export interface FieldValueCounts extends Partial {
+ missing?: number;
}
export interface Bucket {
diff --git a/test/functional/apps/management/_scripted_fields.js b/test/functional/apps/management/_scripted_fields.js
index cbd1169e7a33..9ce2a57436e1 100644
--- a/test/functional/apps/management/_scripted_fields.js
+++ b/test/functional/apps/management/_scripted_fields.js
@@ -509,10 +509,10 @@ export default function ({ getService, getPageObjects }) {
it('should filter by scripted field value in Discover', async function () {
await PageObjects.discover.clickFieldListItem(scriptedPainlessFieldName2);
- await log.debug('filter by "Sep 17, 2015 @ 23:00" in the expanded scripted field list');
+ await log.debug('filter by "Sep 18, 2015 @ 7:52" in the expanded scripted field list');
await PageObjects.discover.clickFieldListPlusFilter(
scriptedPainlessFieldName2,
- '1442531297065'
+ '1442562775953'
);
await PageObjects.header.waitUntilLoadingHasFinished();