diff --git a/CHANGELOG.md b/CHANGELOG.md index e53c6b54e15..00093d45382 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -58,6 +58,7 @@ Inspired from [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) ### 🪛 Refactoring * [MD] Refactor data source error handling ([#2661](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2661)) +- Refactor and improve Discover field summaries ([#2391](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2391)) ### 🔩 Tests * [Multi DataSource] Add unit test coverage for Update Data source management stack ([#2567](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2567)) diff --git a/src/core/public/mocks.ts b/src/core/public/mocks.ts index 163e34b4cf1..e863d627c80 100644 --- a/src/core/public/mocks.ts +++ b/src/core/public/mocks.ts @@ -83,6 +83,7 @@ function createCoreSetupMock({ uiSettings: uiSettingsServiceMock.createSetupContract(), injectedMetadata: { getInjectedVar: injectedMetadataServiceMock.createSetupContract().getInjectedVar, + getBranding: injectedMetadataServiceMock.createSetupContract().getBranding, }, }; diff --git a/src/plugins/discover/public/application/components/sidebar/discover_field.test.tsx b/src/plugins/discover/public/application/components/sidebar/discover_field.test.tsx index 39c5c0abed0..1b384a4b555 100644 --- a/src/plugins/discover/public/application/components/sidebar/discover_field.test.tsx +++ b/src/plugins/discover/public/application/components/sidebar/discover_field.test.tsx @@ -29,6 +29,7 @@ */ import React from 'react'; +// @ts-ignore import { findTestSubject } from '@elastic/eui/lib/test'; // @ts-ignore import stubbedLogstashFields from 'fixtures/logstash_fields'; @@ -99,8 +100,9 @@ function getComponent({ const props = { indexPattern, + columns: [], field: finalField, - getDetails: jest.fn(() => ({ buckets: [], error: '', exists: 1, total: true, columns: [] })), + getDetails: jest.fn(() => ({ buckets: [], error: '', exists: 1, total: 1 })), onAddFilter: jest.fn(), onAddField: jest.fn(), onRemoveField: jest.fn(), diff --git a/src/plugins/discover/public/application/components/sidebar/discover_field.tsx b/src/plugins/discover/public/application/components/sidebar/discover_field.tsx index 157cb88e782..e807267435e 100644 --- a/src/plugins/discover/public/application/components/sidebar/discover_field.tsx +++ b/src/plugins/discover/public/application/components/sidebar/discover_field.tsx @@ -40,6 +40,10 @@ import { getFieldTypeName } from './lib/get_field_type_name'; import './discover_field.scss'; export interface DiscoverFieldProps { + /** + * the selected columns displayed in the doc table in discover + */ + columns: string[]; /** * The displayed field */ @@ -76,6 +80,7 @@ export interface DiscoverFieldProps { } export function DiscoverField({ + columns, field, indexPattern, onAddField, @@ -228,9 +233,10 @@ export function DiscoverField({ {infoIsOpen && ( )} diff --git a/src/plugins/discover/public/application/components/sidebar/discover_field_bucket.tsx b/src/plugins/discover/public/application/components/sidebar/discover_field_bucket.tsx index 1f1af8e9133..6a4dbe295e5 100644 --- a/src/plugins/discover/public/application/components/sidebar/discover_field_bucket.tsx +++ b/src/plugins/discover/public/application/components/sidebar/discover_field_bucket.tsx @@ -68,7 +68,7 @@ export function DiscoverFieldBucket({ field, bucket, onAddFilter }: Props) { title={ bucket.display === '' ? emptyTxt - : `${bucket.display}: ${bucket.count} (${bucket.percent}%)` + : `${bucket.display}: ${bucket.count} (${bucket.percent.toFixed(1)}%)` } size="xs" className="eui-textTruncate" @@ -78,7 +78,7 @@ export function DiscoverFieldBucket({ field, bucket, onAddFilter }: Props) { - {bucket.percent}% + {bucket.percent.toFixed(1)}% diff --git a/src/plugins/discover/public/application/components/sidebar/discover_field_details.test.tsx b/src/plugins/discover/public/application/components/sidebar/discover_field_details.test.tsx index c57300f3032..63d5c7ace30 100644 --- a/src/plugins/discover/public/application/components/sidebar/discover_field_details.test.tsx +++ b/src/plugins/discover/public/application/components/sidebar/discover_field_details.test.tsx @@ -29,15 +29,26 @@ */ import React from 'react'; +// @ts-ignore import { findTestSubject } from '@elastic/eui/lib/test'; +import { act } from '@testing-library/react'; // @ts-ignore import stubbedLogstashFields from 'fixtures/logstash_fields'; -import { mountWithIntl } from 'test_utils/enzyme_helpers'; +import { mountWithIntl, nextTick } from 'test_utils/enzyme_helpers'; import { DiscoverFieldDetails } from './discover_field_details'; import { coreMock } from '../../../../../../core/public/mocks'; import { IndexPatternField } from '../../../../../data/public'; import { getStubIndexPattern } from '../../../../../data/public/test_utils'; +const mockGetHref = jest.fn(); +const mockGetTriggerCompatibleActions = jest.fn(); + +jest.mock('../../../opensearch_dashboards_services', () => ({ + getUiActions: () => ({ + getTriggerCompatibleActions: mockGetTriggerCompatibleActions, + }), +})); + const indexPattern = getStubIndexPattern( 'logstash-*', (cfg: any) => cfg, @@ -48,17 +59,187 @@ const indexPattern = getStubIndexPattern( describe('discover sidebar field details', function () { const defaultProps = { + columns: [], + details: { buckets: [], error: '', exists: 1, total: 1 }, indexPattern, - details: { buckets: [], error: '', exists: 1, total: true, columns: [] }, onAddFilter: jest.fn(), }; - function mountComponent(field: IndexPatternField) { - const compProps = { ...defaultProps, field }; + beforeEach(() => { + mockGetHref.mockReturnValue('/foo/bar'); + mockGetTriggerCompatibleActions.mockReturnValue([ + { + getHref: mockGetHref, + }, + ]); + }); + + function mountComponent(field: IndexPatternField, props?: Record) { + const compProps = { ...defaultProps, ...props, field }; return mountWithIntl(); } - it('should enable the visualize link for a number field', function () { + it('should render buckets if they exist', async function () { + const visualizableField = new IndexPatternField( + { + name: 'bytes', + type: 'number', + esTypes: ['long'], + count: 10, + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + 'bytes' + ); + const buckets = [1, 2, 3].map((n) => ({ + display: `display-${n}`, + value: `value-${n}`, + percent: 25, + count: 100, + })); + const comp = mountComponent(visualizableField, { + details: { ...defaultProps.details, buckets }, + }); + expect(findTestSubject(comp, 'fieldVisualizeError').length).toBe(0); + expect(findTestSubject(comp, 'fieldVisualizeBucketContainer').length).toBe(1); + expect(findTestSubject(comp, 'fieldVisualizeBucketContainer').children().length).toBe( + buckets.length + ); + // Visualize link should not be rendered until async hook update + expect(findTestSubject(comp, 'fieldVisualizeLink').length).toBe(0); + expect(findTestSubject(comp, 'fieldVisualize-bytes').length).toBe(0); + + // Complete async hook + await act(async () => { + await nextTick(); + comp.update(); + }); + expect(findTestSubject(comp, 'fieldVisualizeError').length).toBe(0); + expect(findTestSubject(comp, 'fieldVisualizeBucketContainer').length).toBe(1); + expect(findTestSubject(comp, 'fieldVisualizeBucketContainer').children().length).toBe( + buckets.length + ); + expect(findTestSubject(comp, 'fieldVisualizeLink').length).toBe(1); + expect(findTestSubject(comp, 'fieldVisualize-bytes').length).toBe(1); + }); + + it('should only render buckets if they exist', async function () { + const visualizableField = new IndexPatternField( + { + name: 'bytes', + type: 'number', + esTypes: ['long'], + count: 10, + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + 'bytes' + ); + const comp = mountComponent(visualizableField); + expect(findTestSubject(comp, 'fieldVisualizeContainer').length).toBe(1); + expect(findTestSubject(comp, 'fieldVisualizeError').length).toBe(0); + expect(findTestSubject(comp, 'fieldVisualizeBucketContainer').length).toBe(0); + expect(findTestSubject(comp, 'fieldVisualizeLink').length).toBe(0); + expect(findTestSubject(comp, 'fieldVisualize-bytes').length).toBe(0); + + await act(async () => { + await nextTick(); + comp.update(); + }); + + expect(findTestSubject(comp, 'fieldVisualizeContainer').length).toBe(1); + expect(findTestSubject(comp, 'fieldVisualizeError').length).toBe(0); + expect(findTestSubject(comp, 'fieldVisualizeBucketContainer').length).toBe(0); + expect(findTestSubject(comp, 'fieldVisualizeLink').length).toBe(1); + expect(findTestSubject(comp, 'fieldVisualize-bytes').length).toBe(1); + }); + + it('should render a details error', async function () { + const visualizableField = new IndexPatternField( + { + name: 'bytes', + type: 'number', + esTypes: ['long'], + count: 10, + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + 'bytes' + ); + const errText = 'Some error'; + const comp = mountComponent(visualizableField, { + details: { ...defaultProps.details, error: errText }, + }); + expect(findTestSubject(comp, 'fieldVisualizeContainer').length).toBe(1); + expect(findTestSubject(comp, 'fieldVisualizeBucketContainer').length).toBe(0); + expect(findTestSubject(comp, 'fieldVisualizeError').length).toBe(1); + expect(findTestSubject(comp, 'fieldVisualizeError').text()).toBe(errText); + + await act(async () => { + await nextTick(); + comp.update(); + }); + expect(findTestSubject(comp, 'fieldVisualizeLink').length).toBe(1); + expect(findTestSubject(comp, 'fieldVisualize-bytes').length).toBe(1); + }); + + it('should handle promise rejection from isFieldVisualizable', async function () { + mockGetTriggerCompatibleActions.mockRejectedValue(new Error('Async error')); + const visualizableField = new IndexPatternField( + { + name: 'bytes', + type: 'number', + esTypes: ['long'], + count: 10, + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + 'bytes' + ); + const comp = mountComponent(visualizableField); + + await act(async () => { + await nextTick(); + comp.update(); + }); + expect(findTestSubject(comp, 'fieldVisualizeLink').length).toBe(0); + expect(findTestSubject(comp, 'fieldVisualize-bytes').length).toBe(0); + }); + + it('should handle promise rejection from getVisualizeHref', async function () { + mockGetHref.mockRejectedValue(new Error('Async error')); + const visualizableField = new IndexPatternField( + { + name: 'bytes', + type: 'number', + esTypes: ['long'], + count: 10, + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + 'bytes' + ); + const comp = mountComponent(visualizableField); + + await act(async () => { + await nextTick(); + comp.update(); + }); + expect(findTestSubject(comp, 'fieldVisualizeLink').length).toBe(0); + expect(findTestSubject(comp, 'fieldVisualize-bytes').length).toBe(0); + }); + + it('should enable the visualize link for a number field', async function () { const visualizableField = new IndexPatternField( { name: 'bytes', @@ -73,10 +254,17 @@ describe('discover sidebar field details', function () { 'bytes' ); const comp = mountComponent(visualizableField); - expect(findTestSubject(comp, 'fieldVisualize-bytes')).toBeTruthy(); + + await act(async () => { + await nextTick(); + comp.update(); + }); + expect(findTestSubject(comp, 'fieldVisualizeLink').length).toBe(1); + expect(findTestSubject(comp, 'fieldVisualize-bytes').length).toBe(1); }); - it('should disable the visualize link for an _id field', function () { + it('should disable the visualize link for an _id field', async function () { + expect.assertions(1); const conflictField = new IndexPatternField( { name: '_id', @@ -91,10 +279,15 @@ describe('discover sidebar field details', function () { 'test' ); const comp = mountComponent(conflictField); - expect(findTestSubject(comp, 'fieldVisualize-_id')).toEqual({}); + + await act(async () => { + await nextTick(); + comp.update(); + }); + expect(findTestSubject(comp, 'fieldVisualize-_id').length).toBe(0); }); - it('should disable the visualize link for an unknown field', function () { + it('should disable the visualize link for an unknown field', async function () { const unknownField = new IndexPatternField( { name: 'test', @@ -109,6 +302,11 @@ describe('discover sidebar field details', function () { 'test' ); const comp = mountComponent(unknownField); - expect(findTestSubject(comp, 'fieldVisualize-test')).toEqual({}); + + await act(async () => { + await nextTick(); + comp.update(); + }); + expect(findTestSubject(comp, 'fieldVisualize-test').length).toBe(0); }); }); diff --git a/src/plugins/discover/public/application/components/sidebar/discover_field_details.tsx b/src/plugins/discover/public/application/components/sidebar/discover_field_details.tsx index 1fbcebbc0c8..906c173ed07 100644 --- a/src/plugins/discover/public/application/components/sidebar/discover_field_details.tsx +++ b/src/plugins/discover/public/application/components/sidebar/discover_field_details.tsx @@ -43,16 +43,18 @@ import { IndexPatternField, IndexPattern } from '../../../../../data/public'; import './discover_field_details.scss'; interface DiscoverFieldDetailsProps { + columns: string[]; + details: FieldDetails; field: IndexPatternField; indexPattern: IndexPattern; - details: FieldDetails; onAddFilter: (field: IndexPatternField | string, value: string, type: '+' | '-') => void; } export function DiscoverFieldDetails({ + columns, + details, field, indexPattern, - details, onAddFilter, }: DiscoverFieldDetailsProps) { const warnings = getWarnings(field); @@ -60,37 +62,37 @@ export function DiscoverFieldDetails({ const [visualizeLink, setVisualizeLink] = useState(''); useEffect(() => { - isFieldVisualizable(field, indexPattern.id, details.columns).then( - (flag) => { - setShowVisualizeLink(flag); - // get href only if Visualize button is enabled - getVisualizeHref(field, indexPattern.id, details.columns).then( - (uri) => { - if (uri) setVisualizeLink(uri); - }, - () => { - setVisualizeLink(''); - } - ); - }, - () => { - setShowVisualizeLink(false); + const checkIfVisualizable = async () => { + const visualizable = await isFieldVisualizable(field, indexPattern.id, columns).catch( + () => false + ); + + setShowVisualizeLink(visualizable); + if (visualizable) { + const href = await getVisualizeHref(field, indexPattern.id, columns).catch(() => ''); + setVisualizeLink(href || ''); } - ); - }, [field, indexPattern.id, details.columns]); + }; + checkIfVisualizable(); + }, [field, indexPattern.id, columns]); const handleVisualizeLinkClick = (event: React.MouseEvent) => { // regular link click. let the uiActions code handle the navigation and show popup if needed event.preventDefault(); - triggerVisualizeActions(field, indexPattern.id, details.columns); + triggerVisualizeActions(field, indexPattern.id, columns); }; return ( <> -
- {details.error && {details.error}} - {!details.error && ( -
+
+ {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 f957b93a4cc..865aff59028 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 00000000000..54f8832fa1f --- /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 fb8f22e202c..823cbde9ba7 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 b254057b0de..a43120b28e9 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 ac379b373a4..51f0dc44d77 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();