diff --git a/src/plugins/vis_builder/public/application/components/data_tab/field.tsx b/src/plugins/vis_builder/public/application/components/data_tab/field.tsx index daea4139653b..7ab3659ec187 100644 --- a/src/plugins/vis_builder/public/application/components/data_tab/field.tsx +++ b/src/plugins/vis_builder/public/application/components/data_tab/field.tsx @@ -28,15 +28,10 @@ * under the License. */ -import React, { useCallback, useState } from 'react'; +import React, { useState } from 'react'; import { EuiPopover } from '@elastic/eui'; -import { - FilterManager, - IndexPattern, - IndexPatternField, - opensearchFilters, -} from '../../../../../data/public'; +import { IndexPatternField } from '../../../../../data/public'; import { FieldButton, FieldButtonProps, @@ -50,31 +45,13 @@ import './field.scss'; export interface FieldProps { field: IndexPatternField; - filterManager: FilterManager; - indexPattern?: IndexPattern; getDetails: (field) => FieldDetails; } // TODO: Add field sections (Available fields, popular fields from src/plugins/discover/public/application/components/sidebar/discover_sidebar.tsx) -export const Field = ({ field, filterManager, indexPattern, getDetails }: FieldProps) => { - const { id: indexPatternId = '', metaFields = [] } = indexPattern ?? {}; - const isMetaField = metaFields.includes(field.name); +export const Field = ({ field, getDetails }: FieldProps) => { const [infoIsOpen, setOpen] = useState(false); - const onAddFilter = useCallback( - (fieldToFilter, value, operation) => { - const newFilters = opensearchFilters.generateFilters( - filterManager, - fieldToFilter, - value, - operation, - indexPatternId - ); - return filterManager.addFilters(newFilters); - }, - [filterManager, indexPatternId] - ); - function togglePopover() { setOpen(!infoIsOpen); } @@ -91,14 +68,7 @@ export const Field = ({ field, filterManager, indexPattern, getDetails }: FieldP repositionOnScroll data-test-subj="field-popover" > - {infoIsOpen && ( - - )} + {infoIsOpen && } ); }; diff --git a/src/plugins/vis_builder/public/application/components/data_tab/field_bucket.tsx b/src/plugins/vis_builder/public/application/components/data_tab/field_bucket.tsx index 5371a65b684d..1a45857a6550 100644 --- a/src/plugins/vis_builder/public/application/components/data_tab/field_bucket.tsx +++ b/src/plugins/vis_builder/public/application/components/data_tab/field_bucket.tsx @@ -18,17 +18,19 @@ import { IndexPatternField } from '../../../../../data/public'; import { Bucket } from './types'; import './field_bucket.scss'; +import { useOnAddFilter } from '../../utils/use'; interface FieldBucketProps { bucket: Bucket; field: IndexPatternField; - onAddFilter: (field: IndexPatternField | string, value: string, type: '+' | '-') => void; } -export function FieldBucket({ bucket, field, onAddFilter }: FieldBucketProps) { +export function FieldBucket({ bucket, field }: FieldBucketProps) { const { count, display, percent, value } = bucket; const { filterable: isFilterableField, name: fieldName } = field; + const onAddFilter = useOnAddFilter(); + const emptyText = i18n.translate('visBuilder.fieldSelector.detailsView.emptyStringText', { // We need this to communicate to users when a top value is actually an empty string defaultMessage: 'Empty string', diff --git a/src/plugins/vis_builder/public/application/components/data_tab/field_details.test.tsx b/src/plugins/vis_builder/public/application/components/data_tab/field_details.test.tsx index 313673312269..83a148b2f77b 100644 --- a/src/plugins/vis_builder/public/application/components/data_tab/field_details.test.tsx +++ b/src/plugins/vis_builder/public/application/components/data_tab/field_details.test.tsx @@ -40,17 +40,17 @@ import { IndexPatternField } from '../../../../../data/public'; import { FieldDetailsView } from './field_details'; -const mockOnAddFilter = jest.fn(); +const mockUseIndexPatterns = jest.fn(() => ({ selected: 'mockIndexPattern' })); +const mockUseOnAddFilter = jest.fn(); +jest.mock('../../utils/use', () => ({ + useIndexPatterns: jest.fn(() => mockUseIndexPatterns), + useOnAddFilter: jest.fn(() => mockUseOnAddFilter), +})); describe('visBuilder field details', function () { - const defaultProps = { - isMetaField: false, - details: { buckets: [], error: '', exists: 1, total: 1 }, - onAddFilter: mockOnAddFilter, - }; - + const defaultDetails = { buckets: [], error: '', exists: 1, total: 1 }; function mountComponent(field: IndexPatternField, props?: Record) { - const compProps = { ...defaultProps, ...props, field }; + const compProps = { details: defaultDetails, ...props, field }; return mountWithIntl(); } @@ -75,7 +75,7 @@ describe('visBuilder field details', function () { count: 100, })); const comp = mountComponent(field, { - details: { ...defaultProps.details, buckets }, + details: { ...defaultDetails, buckets }, }); expect(findTestSubject(comp, 'fieldDetailsContainer').length).toBe(1); expect(findTestSubject(comp, 'fieldDetailsError').length).toBe(0); @@ -124,7 +124,7 @@ describe('visBuilder field details', function () { ); const errText = 'Some error'; const comp = mountComponent(field, { - details: { ...defaultProps.details, error: errText }, + details: { ...defaultDetails, error: errText }, }); expect(findTestSubject(comp, 'fieldDetailsContainer').length).toBe(1); expect(findTestSubject(comp, 'fieldDetailsBucketsContainer').children().length).toBe(0); @@ -152,27 +152,4 @@ describe('visBuilder field details', function () { expect(findTestSubject(comp, 'fieldDetailsError').length).toBe(0); expect(findTestSubject(comp, 'fieldDetailsExistsLink').length).toBe(0); }); - - it('should not render an exists filter link for meta fields', async function () { - const field = new IndexPatternField( - { - name: 'bytes', - type: 'number', - esTypes: ['long'], - count: 10, - scripted: true, - searchable: true, - aggregatable: true, - readFromDocValues: true, - }, - 'bytes' - ); - const comp = mountComponent(field, { - ...defaultProps, - isMetaField: true, - }); - expect(findTestSubject(comp, 'fieldDetailsContainer').length).toBe(1); - expect(findTestSubject(comp, 'fieldDetailsError').length).toBe(0); - expect(findTestSubject(comp, 'fieldDetailsExistsLink').length).toBe(0); - }); }); diff --git a/src/plugins/vis_builder/public/application/components/data_tab/field_details.tsx b/src/plugins/vis_builder/public/application/components/data_tab/field_details.tsx index 4f1e07a88643..cf6f4974bb18 100644 --- a/src/plugins/vis_builder/public/application/components/data_tab/field_details.tsx +++ b/src/plugins/vis_builder/public/application/components/data_tab/field_details.tsx @@ -9,19 +9,25 @@ import { i18n } from '@osd/i18n'; import { IndexPatternField } from '../../../../../data/public'; +import { useIndexPatterns, useOnAddFilter } from '../../utils/use'; import { FieldBucket } from './field_bucket'; import { Bucket, FieldDetails } from './types'; interface FieldDetailsProps { field: IndexPatternField; - isMetaField: boolean; details: FieldDetails; - onAddFilter: (field: IndexPatternField | string, value: string, type: '+' | '-') => void; } -export function FieldDetailsView({ field, isMetaField, details, onAddFilter }: FieldDetailsProps) { +export function FieldDetailsView({ field, details }: FieldDetailsProps) { const { buckets, error, exists, total } = details; + const onAddFilter = useOnAddFilter(); + const indexPattern = useIndexPatterns().selected; + + const { metaFields = [] } = indexPattern ?? {}; + const isMetaField = metaFields.includes(field.name); + const shouldAllowExistsFilter = !isMetaField && !field.scripted; + const bucketsTitle = buckets.length > 1 ? i18n.translate('visBuilder.fieldSelector.detailsView.fieldTopValuesLabel', { @@ -45,8 +51,6 @@ export function FieldDetailsView({ field, isMetaField, details, onAddFilter }: F const title = buckets.length ? bucketsTitle : errorTitle; - const shouldAllowExistsFilter = !isMetaField && !field.scripted; - return ( <> {title} @@ -61,12 +65,7 @@ export function FieldDetailsView({ field, isMetaField, details, onAddFilter }: F data-test-subj="fieldDetailsBucketsContainer" > {buckets.map((bucket: Bucket, idx: number) => ( - + ))} )} diff --git a/src/plugins/vis_builder/public/application/components/data_tab/field_selector.test.tsx b/src/plugins/vis_builder/public/application/components/data_tab/field_selector.test.tsx index be3a47072e85..980cfb50c666 100644 --- a/src/plugins/vis_builder/public/application/components/data_tab/field_selector.test.tsx +++ b/src/plugins/vis_builder/public/application/components/data_tab/field_selector.test.tsx @@ -8,7 +8,14 @@ import { fireEvent, render, screen } from '@testing-library/react'; import { FilterManager, IndexPatternField } from '../../../../../data/public'; import { FieldGroup } from './field_selector'; -const mockGetDetails = jest.fn(() => ({ +const mockUseIndexPatterns = jest.fn(() => ({ selected: 'mockIndexPattern' })); +const mockUseOnAddFilter = jest.fn(); +jest.mock('../../utils/use', () => ({ + useIndexPatterns: jest.fn(() => mockUseIndexPatterns), + useOnAddFilter: jest.fn(() => mockUseOnAddFilter), +})); + +const mockGetDetailsByField = jest.fn(() => ({ buckets: [1, 2, 3].map((n) => ({ display: `display-${n}`, value: `value-${n}`, @@ -39,7 +46,7 @@ const getFields = (name) => { describe('visBuilder sidebar field selector', function () { const defaultProps = { filterManager: {} as FilterManager, - getDetails: mockGetDetails, + getDetailsByField: mockGetDetailsByField, header: 'mockHeader', id: 'mockID', }; @@ -53,7 +60,7 @@ describe('visBuilder sidebar field selector', function () { await fireEvent.click(screen.getByText(defaultProps.header)); - expect(mockGetDetails).not.toHaveBeenCalled(); + expect(mockGetDetailsByField).not.toHaveBeenCalled(); }); it('renders an accordion with Fields if fields provided', async () => { @@ -69,7 +76,7 @@ describe('visBuilder sidebar field selector', function () { await fireEvent.click(screen.getByText('memory')); - expect(mockGetDetails).toHaveBeenCalledTimes(1); + expect(mockGetDetailsByField).toHaveBeenCalledTimes(1); }); }); }); diff --git a/src/plugins/vis_builder/public/application/components/data_tab/field_selector.tsx b/src/plugins/vis_builder/public/application/components/data_tab/field_selector.tsx index c315e6088162..e0fad14a000b 100644 --- a/src/plugins/vis_builder/public/application/components/data_tab/field_selector.tsx +++ b/src/plugins/vis_builder/public/application/components/data_tab/field_selector.tsx @@ -3,24 +3,14 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React, { useState, useEffect, useCallback, useMemo, useLayoutEffect } from 'react'; +import React, { useState, useEffect, useCallback, useMemo } from 'react'; import { EuiFlexItem, EuiAccordion, EuiNotificationBadge, EuiTitle } from '@elastic/eui'; -import { - FilterManager, - IndexPattern, - IndexPatternField, - OPENSEARCH_FIELD_TYPES, - OSD_FIELD_TYPES, - SortDirection, -} from '../../../../../data/public'; -import { IExpressionLoaderParams } from '../../../../../expressions/public'; -import { useOpenSearchDashboards } from '../../../../../opensearch_dashboards_react/public'; +import { IndexPattern, IndexPatternField, OSD_FIELD_TYPES } from '../../../../../data/public'; -import { VisBuilderServices } from '../../../types'; import { COUNT_FIELD } from '../../utils/drag_drop'; import { useTypedSelector } from '../../utils/state_management'; -import { useIndexPatterns } from '../../utils/use'; +import { useIndexPatterns, useSampleHits } from '../../utils/use'; import { FieldSearch } from './field_search'; import { Field, DraggableFieldButton } from './field'; import { FieldDetails } from './types'; @@ -33,36 +23,11 @@ interface IFieldCategories { meta: IndexPatternField[]; } -const META_FIELDS: string[] = [ - OPENSEARCH_FIELD_TYPES._ID, - OPENSEARCH_FIELD_TYPES._INDEX, - OPENSEARCH_FIELD_TYPES._SOURCE, - OPENSEARCH_FIELD_TYPES._TYPE, -]; - export const FieldSelector = () => { - const { - services: { - data: { - query: { - filterManager, - queryString, - state$, - timefilter: { timefilter }, - }, - search: { searchSource }, - }, - uiSettings: config, - }, - } = useOpenSearchDashboards(); const indexPattern = useIndexPatterns().selected; const fieldSearchValue = useTypedSelector((state) => state.visualization.searchField); + const hits = useSampleHits(); const [filteredFields, setFilteredFields] = useState([]); - const [hits, setHits] = useState>>([]); - const [searchContext, setSearchContext] = useState({ - query: queryString.getQuery(), - filters: filterManager.getFilters(), - }); useEffect(() => { const indexFields = indexPattern?.fields.getAll() ?? []; @@ -79,7 +44,7 @@ export const FieldSelector = () => { () => filteredFields?.reduce( (fieldGroups, currentField) => { - const category = getFieldCategory(currentField); + const category = getFieldCategory(currentField, indexPattern); fieldGroups[category].push(currentField); return fieldGroups; @@ -90,53 +55,9 @@ export const FieldSelector = () => { meta: [], } ), - [filteredFields] + [filteredFields, indexPattern] ); - useEffect(() => { - async function getData() { - if (indexPattern && searchContext) { - const newSearchSource = await searchSource.create(); - const timeRangeFilter = timefilter.createFilter(indexPattern); - - newSearchSource - .setField('index', indexPattern) - .setField('size', config.get('discover:sampleSize') ?? 500) - .setField('sort', [{ [indexPattern.timeFieldName || '_score']: 'desc' as SortDirection }]) - .setField('filter', [ - ...(searchContext.filters ?? []), - ...(timeRangeFilter ? [timeRangeFilter] : []), - ]); - - if (searchContext.query) { - const contextQuery = - searchContext.query instanceof Array ? searchContext.query[0] : searchContext.query; - - newSearchSource.setField('query', contextQuery); - } - - const searchResponse = await newSearchSource.fetch(); - - setHits(searchResponse.hits.hits); - } - } - - getData(); - }, [config, searchContext, searchSource, indexPattern, timefilter]); - - useLayoutEffect(() => { - const subscription = state$.subscribe(({ state }) => { - setSearchContext({ - query: state.query, - filters: state.filters, - }); - }); - - return () => { - subscription.unsubscribe(); - }; - }, [state$]); - const getDetailsByField = useCallback( (ipField: IndexPatternField) => { return getDetails(ipField, hits, indexPattern); @@ -144,12 +65,6 @@ export const FieldSelector = () => { [hits, indexPattern] ); - const commonFieldGroupProps = { - filterManager, - indexPattern, - getDetails: getDetailsByField, - }; - return (
@@ -167,19 +82,19 @@ export const FieldSelector = () => { id="categoricalFields" header="Categorical Fields" fields={fields?.categorical} - {...commonFieldGroupProps} + getDetailsByField={getDetailsByField} />
@@ -188,14 +103,12 @@ export const FieldSelector = () => { interface FieldGroupProps { fields?: IndexPatternField[]; - filterManager: FilterManager; - getDetails: (ipField: IndexPatternField) => FieldDetails; + getDetailsByField: (ipField: IndexPatternField) => FieldDetails; header: string; id: string; - indexPattern?: IndexPattern; } -export const FieldGroup = ({ fields, header, id, ...rest }: FieldGroupProps) => { +export const FieldGroup = ({ fields, header, id, getDetailsByField }: FieldGroupProps) => { return ( > {fields?.map((field, i) => ( - + ))} ); }; -export const getFieldCategory = ({ name, type }: IndexPatternField): keyof IFieldCategories => { - if (META_FIELDS.includes(name)) return 'meta'; +export const getFieldCategory = ( + { name, type }: IndexPatternField, + indexPattern: IndexPattern | undefined +): keyof IFieldCategories => { + const { metaFields = [] } = indexPattern ?? {}; + if (metaFields.includes(name)) return 'meta'; if (type === OSD_FIELD_TYPES.NUMBER) return 'numerical'; return 'categorical'; diff --git a/src/plugins/vis_builder/public/application/utils/use/index.ts b/src/plugins/vis_builder/public/application/utils/use/index.ts index 3ba3ca359072..1cc0b28dc89a 100644 --- a/src/plugins/vis_builder/public/application/utils/use/index.ts +++ b/src/plugins/vis_builder/public/application/utils/use/index.ts @@ -4,6 +4,8 @@ */ export { useAggs } from './use_aggs'; -export { useVisualizationType } from './use_visualization_type'; export { useIndexPatterns } from './use_index_pattern'; +export { useOnAddFilter } from './use_on_add_filter'; +export { useSampleHits } from './use_sample_hits'; export { useSavedVisBuilderVis } from './use_saved_vis_builder_vis'; +export { useVisualizationType } from './use_visualization_type'; diff --git a/src/plugins/vis_builder/public/application/utils/use/use_on_add_filter.ts b/src/plugins/vis_builder/public/application/utils/use/use_on_add_filter.ts new file mode 100644 index 000000000000..791521fccad5 --- /dev/null +++ b/src/plugins/vis_builder/public/application/utils/use/use_on_add_filter.ts @@ -0,0 +1,35 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { useCallback } from 'react'; +import { IndexPatternField, opensearchFilters } from '../../../../../data/public'; +import { useOpenSearchDashboards } from '../../../../../opensearch_dashboards_react/public'; +import { VisBuilderServices } from '../../../types'; +import { useIndexPatterns } from './use_index_pattern'; + +export const useOnAddFilter = () => { + const { + services: { + data: { + query: { filterManager }, + }, + }, + } = useOpenSearchDashboards(); + const indexPattern = useIndexPatterns().selected; + const { id = '' } = indexPattern ?? {}; + return useCallback( + (fieldToFilter: IndexPatternField | string, value: string, operation: '+' | '-') => { + const newFilters = opensearchFilters.generateFilters( + filterManager, + fieldToFilter, + value, + operation, + id + ); + return filterManager.addFilters(newFilters); + }, + [filterManager, id] + ); +}; diff --git a/src/plugins/vis_builder/public/application/utils/use/use_sample_hits.ts b/src/plugins/vis_builder/public/application/utils/use/use_sample_hits.ts new file mode 100644 index 000000000000..f3ed75a4dd6a --- /dev/null +++ b/src/plugins/vis_builder/public/application/utils/use/use_sample_hits.ts @@ -0,0 +1,80 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { useEffect, useLayoutEffect, useState } from 'react'; +import { SortDirection } from '../../../../../data/public'; +import { IExpressionLoaderParams } from '../../../../../expressions/public'; +import { useOpenSearchDashboards } from '../../../../../opensearch_dashboards_react/public'; +import { VisBuilderServices } from '../../../types'; +import { useIndexPatterns } from './use_index_pattern'; + +export const useSampleHits = () => { + const { + services: { + data: { + query: { + filterManager, + queryString, + state$, + timefilter: { timefilter }, + }, + search: { searchSource }, + }, + uiSettings: config, + }, + } = useOpenSearchDashboards(); + const indexPattern = useIndexPatterns().selected; + const [hits, setHits] = useState>>([]); + const [searchContext, setSearchContext] = useState({ + query: queryString.getQuery(), + filters: filterManager.getFilters(), + }); + + useEffect(() => { + async function getData() { + if (indexPattern && searchContext) { + const newSearchSource = await searchSource.create(); + const timeRangeFilter = timefilter.createFilter(indexPattern); + + newSearchSource + .setField('index', indexPattern) + .setField('size', config.get('discover:sampleSize') ?? 500) + .setField('sort', [{ [indexPattern.timeFieldName || '_score']: 'desc' as SortDirection }]) + .setField('filter', [ + ...(searchContext.filters ?? []), + ...(timeRangeFilter ? [timeRangeFilter] : []), + ]); + + if (searchContext.query) { + const contextQuery = + searchContext.query instanceof Array ? searchContext.query[0] : searchContext.query; + + newSearchSource.setField('query', contextQuery); + } + + const searchResponse = await newSearchSource.fetch(); + + setHits(searchResponse.hits.hits); + } + } + + getData(); + }, [config, searchContext, searchSource, indexPattern, timefilter]); + + useLayoutEffect(() => { + const subscription = state$.subscribe(({ state }) => { + setSearchContext({ + query: state.query, + filters: state.filters, + }); + }); + + return () => { + subscription.unsubscribe(); + }; + }, [state$]); + + return hits; +};