From f7a766347939fb90ad2f918b2479fd6b979c8a7e Mon Sep 17 00:00:00 2001 From: Julia Rechkunova Date: Thu, 26 Jan 2023 10:40:13 +0100 Subject: [PATCH] [Discover] Integrate unified field list filters into Discover (#148547) Closes https://github.com/elastic/kibana/issues/145081 Part 1 of this integration was done in https://github.com/elastic/kibana/pull/147255 Screenshot 2023-01-16 at 16 58 59 ## Summary This PR integrates the unified field list filters and search into Discover. - [x] Integrate into Discover - [x] Clean up deprecated code ("searchable"/"aggregatable" filters were removed too) - [x] Refactor field icons, labels, desc to use the unified ones - [x] Field list in SQL view needs refactoring to use the received field types rather than data view field types - [x] Update tests ### Checklist - [x] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md) - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios - [x] This renders correctly on smaller devices using a responsive layout. (You can test this [in your browser](https://www.browserstack.com/guide/responsive-testing-on-local-server)) - [x] This was checked for [cross-browser compatibility](https://www.elastic.co/support/matrix#matrix_browsers) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Stratoula Kalafateli --- .../components/layout/discover_layout.scss | 4 - .../components/layout/discover_layout.tsx | 2 +- .../components/sidebar/discover_field.tsx | 11 +- .../sidebar/discover_field_search.scss | 15 - .../sidebar/discover_field_search.test.tsx | 152 ------- .../sidebar/discover_field_search.tsx | 426 ------------------ .../components/sidebar/discover_sidebar.scss | 10 +- .../sidebar/discover_sidebar.test.tsx | 8 +- .../components/sidebar/discover_sidebar.tsx | 167 +++---- .../discover_sidebar_responsive.test.tsx | 44 +- .../sidebar/discover_sidebar_responsive.tsx | 19 +- .../sidebar/lib/field_filter.test.ts | 84 ---- .../components/sidebar/lib/field_filter.ts | 59 --- ...a_view_field_list.ts => get_field_list.ts} | 60 ++- .../sidebar/lib/get_field_type_description.ts | 118 ----- .../sidebar/lib/group_fields.test.ts | 95 +++- .../components/sidebar/lib/group_fields.tsx | 20 +- .../sidebar/lib/sidebar_reducer.test.ts | 53 ++- .../components/sidebar/lib/sidebar_reducer.ts | 12 +- .../services/discover_data_state_container.ts | 2 + .../application/main/utils/fetch_all.test.ts | 16 +- .../application/main/utils/fetch_all.ts | 13 +- .../main/utils/fetch_documents.test.ts | 4 +- .../application/main/utils/fetch_documents.ts | 6 +- .../application/main/utils/fetch_sql.ts | 16 +- .../__snapshots__/field_name.test.tsx.snap | 20 +- .../components/field_name/field_name.tsx | 5 +- .../embeddable/saved_search_embeddable.tsx | 4 +- .../doc_viewer_table/legacy/table.tsx | 4 +- .../components/doc_viewer_table/table.tsx | 4 +- src/plugins/discover/public/types.ts | 6 + .../public/utils/get_field_type_name.test.ts | 35 -- .../public/utils/get_field_type_name.ts | 105 ----- .../utils/get_type_for_field_icon.test.ts | 48 -- .../public/utils/get_type_for_field_icon.ts | 22 - .../__snapshots__/field_icon.test.tsx.snap | 7 - .../components/field_icon/field_icon.test.tsx | 5 - .../components/field_icon/field_icon.tsx | 3 - .../field_type_filter.test.tsx | 8 +- .../field_list_grouped.test.tsx | 54 ++- .../public/hooks/use_grouped_fields.test.tsx | 45 +- .../public/hooks/use_grouped_fields.ts | 8 +- .../field_types/get_field_icon_type.test.ts | 2 +- .../utils/field_types/get_field_icon_type.ts | 4 +- .../apps/discover/group1/_sidebar.ts | 133 +++++- .../apps/discover/group2/_sql_view.ts | 13 +- test/functional/page_objects/discover_page.ts | 12 +- .../dimension_panel/dimension_editor.tsx | 10 +- .../translations/translations/fr-FR.json | 58 --- .../translations/translations/ja-JP.json | 58 --- .../translations/translations/zh-CN.json | 58 --- .../functional/apps/discover/reporting.ts | 2 +- 52 files changed, 591 insertions(+), 1558 deletions(-) delete mode 100644 src/plugins/discover/public/application/main/components/sidebar/discover_field_search.scss delete mode 100644 src/plugins/discover/public/application/main/components/sidebar/discover_field_search.test.tsx delete mode 100644 src/plugins/discover/public/application/main/components/sidebar/discover_field_search.tsx delete mode 100644 src/plugins/discover/public/application/main/components/sidebar/lib/field_filter.test.ts delete mode 100644 src/plugins/discover/public/application/main/components/sidebar/lib/field_filter.ts rename src/plugins/discover/public/application/main/components/sidebar/lib/{get_data_view_field_list.ts => get_field_list.ts} (59%) delete mode 100644 src/plugins/discover/public/application/main/components/sidebar/lib/get_field_type_description.ts delete mode 100644 src/plugins/discover/public/utils/get_field_type_name.test.ts delete mode 100644 src/plugins/discover/public/utils/get_field_type_name.ts delete mode 100644 src/plugins/discover/public/utils/get_type_for_field_icon.test.ts delete mode 100644 src/plugins/discover/public/utils/get_type_for_field_icon.ts diff --git a/src/plugins/discover/public/application/main/components/layout/discover_layout.scss b/src/plugins/discover/public/application/main/components/layout/discover_layout.scss index b2d9ab190e075..5cb1f54edfd2e 100644 --- a/src/plugins/discover/public/application/main/components/layout/discover_layout.scss +++ b/src/plugins/discover/public/application/main/components/layout/discover_layout.scss @@ -31,10 +31,6 @@ discover-app { overflow: hidden; } -.dscPageBody__sidebar { - position: relative; -} - .dscPageContent__wrapper { padding: $euiSizeS $euiSizeS $euiSizeS 0; overflow: hidden; // Ensures horizontal scroll of table diff --git a/src/plugins/discover/public/application/main/components/layout/discover_layout.tsx b/src/plugins/discover/public/application/main/components/layout/discover_layout.tsx index 8f1dcc4ba8dfb..ab8c5da67d04a 100644 --- a/src/plugins/discover/public/application/main/components/layout/discover_layout.tsx +++ b/src/plugins/discover/public/application/main/components/layout/discover_layout.tsx @@ -321,7 +321,7 @@ export function DiscoverLayout({ history={history} /> - + ( )); const DiscoverFieldTypeIcon: React.FC<{ field: DataViewField }> = memo(({ field }) => { - const typeForIcon = getTypeForFieldIcon(field); - return ( - - ); + return ; }); const FieldName: React.FC<{ field: DataViewField; highlight?: string }> = memo( diff --git a/src/plugins/discover/public/application/main/components/sidebar/discover_field_search.scss b/src/plugins/discover/public/application/main/components/sidebar/discover_field_search.scss deleted file mode 100644 index 59b6e49b0fe0e..0000000000000 --- a/src/plugins/discover/public/application/main/components/sidebar/discover_field_search.scss +++ /dev/null @@ -1,15 +0,0 @@ -.dscFieldSearch__formWrapper { - padding: $euiSizeM; -} - -.dscFieldTypesHelp__popover { - flex-grow: 0; - min-width: 0 !important; // Reduce width of icon-only button -} - -.dscFieldTypesHelp__panel { - width: $euiSize * 22; - @include euiBreakpoint('xs', 's') { - width: $euiSize * 20; - } -} \ No newline at end of file diff --git a/src/plugins/discover/public/application/main/components/sidebar/discover_field_search.test.tsx b/src/plugins/discover/public/application/main/components/sidebar/discover_field_search.test.tsx deleted file mode 100644 index eafe3fec1eeaf..0000000000000 --- a/src/plugins/discover/public/application/main/components/sidebar/discover_field_search.test.tsx +++ /dev/null @@ -1,152 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import React from 'react'; -import { act } from 'react-dom/test-utils'; -import { mountWithIntl } from '@kbn/test-jest-helpers'; -import { findTestSubject } from '@elastic/eui/lib/test'; -import { DiscoverFieldSearch, Props } from './discover_field_search'; -import { EuiButtonGroupProps, EuiPopover } from '@elastic/eui'; -import { ReactWrapper } from 'enzyme'; -import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; - -describe('DiscoverFieldSearch', () => { - const defaultProps = { - onChange: jest.fn(), - value: 'test', - types: ['any', 'string', '_source'], - presentFieldTypes: ['string', 'date', 'boolean', 'number'], - isPlainRecord: false, - }; - - function mountComponent(props?: Props) { - const compProps = props || defaultProps; - return mountWithIntl( - - - - ); - } - - function findButtonGroup(component: ReactWrapper, id: string) { - return component.find(`[data-test-subj="${id}ButtonGroup"]`).first(); - } - - test('enter value', () => { - const component = mountComponent(); - const input = findTestSubject(component, 'fieldFilterSearchInput'); - input.simulate('change', { target: { value: 'new filter' } }); - expect(defaultProps.onChange).toBeCalledTimes(1); - expect(defaultProps.onChange).toHaveBeenCalledWith('name', 'new filter'); - }); - - test('change in active filters should change facet selection and call onChange', () => { - const onChange = jest.fn(); - const component = mountComponent({ ...defaultProps, ...{ onChange } }); - const btn = findTestSubject(component, 'toggleFieldFilterButton'); - const badge = btn.find('.euiNotificationBadge').last(); - expect(badge.text()).toEqual('0'); - btn.simulate('click'); - const aggregatableButtonGroup = findButtonGroup(component, 'aggregatable'); - - act(() => { - // @ts-expect-error - (aggregatableButtonGroup.props() as EuiButtonGroupProps).onChange('aggregatable-true', null); - }); - component.update(); - expect(badge.text()).toEqual('1'); - expect(onChange).toBeCalledWith('aggregatable', true); - }); - - test('change in active filters should change filters count', () => { - const component = mountComponent(); - let btn = findTestSubject(component, 'toggleFieldFilterButton'); - btn.simulate('click'); - btn = findTestSubject(component, 'toggleFieldFilterButton'); - const badge = btn.find('.euiNotificationBadge').last(); - // no active filters - expect(badge.text()).toEqual('0'); - // change value of aggregatable select - const aggregatableButtonGroup = findButtonGroup(component, 'aggregatable'); - act(() => { - // @ts-expect-error - (aggregatableButtonGroup.props() as EuiButtonGroupProps).onChange('aggregatable-true', null); - }); - component.update(); - expect(badge.text()).toEqual('1'); - // change value of searchable select - const searchableButtonGroup = findButtonGroup(component, 'searchable'); - act(() => { - // @ts-expect-error - (searchableButtonGroup.props() as EuiButtonGroupProps).onChange('searchable-true', null); - }); - component.update(); - expect(badge.text()).toEqual('2'); - // change value of searchable select - act(() => { - // @ts-expect-error - (searchableButtonGroup.props() as EuiButtonGroupProps).onChange('searchable-any', null); - }); - component.update(); - expect(badge.text()).toEqual('1'); - }); - - test('change in filters triggers onChange', () => { - const onChange = jest.fn(); - const component = mountComponent({ ...defaultProps, ...{ onChange } }); - const btn = findTestSubject(component, 'toggleFieldFilterButton'); - btn.simulate('click'); - const aggregtableButtonGroup = findButtonGroup(component, 'aggregatable'); - act(() => { - // @ts-expect-error - (aggregtableButtonGroup.props() as EuiButtonGroupProps).onChange('aggregatable-true', null); - }); - expect(onChange).toBeCalledTimes(1); - }); - - test('change in type filters triggers onChange with appropriate value', () => { - const onChange = jest.fn(); - const component = mountComponent({ ...defaultProps, ...{ onChange } }); - const btn = findTestSubject(component, 'toggleFieldFilterButton'); - btn.simulate('click'); - const typeSelector = findTestSubject(component, 'typeSelect'); - typeSelector.simulate('change', { target: { value: 'string' } }); - expect(onChange).toBeCalledWith('type', 'string'); - typeSelector.simulate('change', { target: { value: 'any' } }); - expect(onChange).toBeCalledWith('type', 'any'); - }); - - test('click on filter button should open and close popover', () => { - const component = mountComponent(); - const btn = findTestSubject(component, 'toggleFieldFilterButton'); - btn.simulate('click'); - let popover = component.find(EuiPopover); - expect(popover.get(0).props.isOpen).toBe(true); - btn.simulate('click'); - popover = component.find(EuiPopover); - expect(popover.get(0).props.isOpen).toBe(false); - }); - - test('click help button should open popover with types of field docs', () => { - const component = mountComponent(); - - const btn = findTestSubject(component, 'fieldTypesHelpButton'); - btn.simulate('click'); - let popover = component.find(EuiPopover); - expect(popover.get(1).props.isOpen).toBe(true); - - const rows = component.find('.euiTableRow'); - expect(rows.length).toBe(4); - - btn.simulate('click'); - popover = component.find(EuiPopover); - expect(popover.get(1).props.isOpen).toBe(false); - }); -}); diff --git a/src/plugins/discover/public/application/main/components/sidebar/discover_field_search.tsx b/src/plugins/discover/public/application/main/components/sidebar/discover_field_search.tsx deleted file mode 100644 index 8d7103f70efe9..0000000000000 --- a/src/plugins/discover/public/application/main/components/sidebar/discover_field_search.tsx +++ /dev/null @@ -1,426 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import './discover_field_search.scss'; - -import React, { OptionHTMLAttributes, ReactNode, useMemo, useState } from 'react'; -import { i18n } from '@kbn/i18n'; -import { - EuiBasicTable, - EuiFieldSearch, - EuiFilterGroup, - EuiFlexGroup, - EuiFlexItem, - EuiPopover, - EuiPopoverTitle, - EuiSelect, - EuiForm, - EuiFormRow, - EuiButtonGroup, - EuiFilterButton, - EuiSpacer, - EuiIcon, - EuiBasicTableColumn, - EuiLink, - EuiText, - EuiPanel, -} from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n-react'; -import { FieldIcon } from '@kbn/react-field'; -import { getFieldTypeDescription } from './lib/get_field_type_description'; -import { KNOWN_FIELD_TYPES } from '../../../../../common/field_types'; -import { useDiscoverServices } from '../../../../hooks/use_discover_services'; - -export interface State { - searchable: string; - aggregatable: string; - type: string; - [index: string]: string | boolean; -} - -export interface Props { - /** - * triggered on input of user into search field - */ - onChange: (field: string, value: string | boolean | undefined) => void; - /** - * types for the type filter - */ - types: string[]; - /** - * types presented in current data view - */ - presentFieldTypes: string[]; - /** - * the input value of the user - */ - value?: string; - /** - * is text base lang mode - */ - isPlainRecord: boolean; - - /** - * For a11y - */ - fieldSearchDescriptionId?: string; -} - -interface FieldTypeTableItem { - id: number; - dataType: string; - description: string; -} - -/** - * Component is Discover's side bar to search of available fields - * Additionally there's a button displayed that allows the user to show/hide more filter fields - */ -export function DiscoverFieldSearch({ - onChange, - value, - types, - presentFieldTypes, - isPlainRecord, - fieldSearchDescriptionId, -}: Props) { - const searchPlaceholder = i18n.translate('discover.fieldChooser.searchPlaceHolder', { - defaultMessage: 'Search field names', - }); - const aggregatableLabel = i18n.translate('discover.fieldChooser.filter.aggregatableLabel', { - defaultMessage: 'Aggregatable', - }); - const searchableLabel = i18n.translate('discover.fieldChooser.filter.searchableLabel', { - defaultMessage: 'Searchable', - }); - const typeLabel = i18n.translate('discover.fieldChooser.filter.typeLabel', { - defaultMessage: 'Type', - }); - const typeOptions = types - ? types.map((type) => { - return { value: type, text: type }; - }) - : [{ value: 'any', text: 'any' }]; - - const [activeFiltersCount, setActiveFiltersCount] = useState(0); - const [isPopoverOpen, setPopoverOpen] = useState(false); - const [isHelpOpen, setIsHelpOpen] = useState(false); - const [values, setValues] = useState({ - searchable: 'any', - aggregatable: 'any', - type: 'any', - }); - - const { docLinks } = useDiscoverServices(); - - const items: FieldTypeTableItem[] = useMemo(() => { - const knownTypes = Object.values(KNOWN_FIELD_TYPES) as string[]; - return presentFieldTypes - .filter((element) => knownTypes.includes(element)) - .sort((one, another) => one.localeCompare(another)) - .map((element, index) => ({ - id: index, - dataType: element, - description: getFieldTypeDescription(element, docLinks), - })); - }, [presentFieldTypes, docLinks]); - - const onHelpClick = () => setIsHelpOpen((prevIsHelpOpen) => !prevIsHelpOpen); - const closeHelp = () => setIsHelpOpen(false); - - const columnsSidebar: Array> = [ - { - field: 'dataType', - name: i18n.translate('discover.fieldTypesPopover.dataTypeColumnTitle', { - defaultMessage: 'Data type', - }), - width: '110px', - render: (name: string) => ( - - - - - {name} - - ), - }, - { - field: 'description', - name: i18n.translate('discover.fieldTypesPopover.descriptionColumnTitle', { - defaultMessage: 'Description', - }), - // eslint-disable-next-line react/no-danger - render: (description: string) =>
, - }, - ]; - - const filterBtnAriaLabel = isPopoverOpen - ? i18n.translate('discover.fieldChooser.toggleFieldFilterButtonHideAriaLabel', { - defaultMessage: 'Hide field filter settings', - }) - : i18n.translate('discover.fieldChooser.toggleFieldFilterButtonShowAriaLabel', { - defaultMessage: 'Show field filter settings', - }); - - const handleFilterButtonClicked = () => { - setPopoverOpen(!isPopoverOpen); - }; - - const applyFilterValue = (id: string, filterValue: string | boolean) => { - switch (filterValue) { - case 'any': - if (id !== 'type') { - onChange(id, undefined); - } else { - onChange(id, filterValue); - } - break; - case 'true': - onChange(id, true); - break; - case 'false': - onChange(id, false); - break; - default: - onChange(id, filterValue); - } - }; - - const isFilterActive = (name: string, filterValue: string | boolean) => { - return filterValue !== 'any'; - }; - - const handleValueChange = (name: string, filterValue: string | boolean) => { - const previousValue = values[name]; - updateFilterCount(name, previousValue, filterValue); - const updatedValues = { ...values }; - updatedValues[name] = filterValue; - setValues(updatedValues); - applyFilterValue(name, filterValue); - }; - - const updateFilterCount = ( - name: string, - previousValue: string | boolean, - currentValue: string | boolean - ) => { - const previouslyFilterActive = isFilterActive(name, previousValue); - const filterActive = isFilterActive(name, currentValue); - const diff = Number(filterActive) - Number(previouslyFilterActive); - setActiveFiltersCount(activeFiltersCount + diff); - }; - - const buttonContent = ( - 0} - numFilters={0} - hasActiveFilters={activeFiltersCount > 0} - numActiveFilters={activeFiltersCount} - onClick={handleFilterButtonClicked} - > - - - ); - - const select = ( - id: string, - selectOptions: Array<{ text: ReactNode } & OptionHTMLAttributes>, - selectValue: string - ) => { - return ( - ) => - handleValueChange(id, e.target.value) - } - aria-label={i18n.translate('discover.fieldChooser.filter.fieldSelectorLabel', { - defaultMessage: 'Selection of {id} filter options', - values: { id }, - })} - data-test-subj={`${id}Select`} - compressed - /> - ); - }; - - const toggleButtons = (id: string) => { - return [ - { - id: `${id}-any`, - label: i18n.translate('discover.fieldChooser.filter.toggleButton.any', { - defaultMessage: 'any', - }), - }, - { - id: `${id}-true`, - label: i18n.translate('discover.fieldChooser.filter.toggleButton.yes', { - defaultMessage: 'yes', - }), - }, - { - id: `${id}-false`, - label: i18n.translate('discover.fieldChooser.filter.toggleButton.no', { - defaultMessage: 'no', - }), - }, - ]; - }; - - const buttonGroup = (id: string, legend: string) => { - return ( - handleValueChange(id, optionId.replace(`${id}-`, ''))} - buttonSize="compressed" - isFullWidth - data-test-subj={`${id}ButtonGroup`} - /> - ); - }; - - const selectionPanel = ( -
- - - {buttonGroup('aggregatable', aggregatableLabel)} - - - {buttonGroup('searchable', searchableLabel)} - - - {select('type', typeOptions, values.type)} - - -
- ); - - const helpButton = ( - - - - ); - - return ( - - - - onChange('name', event.target.value)} - placeholder={searchPlaceholder} - value={value} - /> - - - - {!isPlainRecord && ( - - - { - setPopoverOpen(false); - }} - button={buttonContent} - > - - {i18n.translate('discover.fieldChooser.filter.filterByTypeLabel', { - defaultMessage: 'Filter by type', - })} - - {selectionPanel} - - - - {i18n.translate('discover.fieldChooser.popoverTitle', { - defaultMessage: 'Field types', - })} - - - - - - -

- {i18n.translate('discover.fieldTypesPopover.learnMoreText', { - defaultMessage: 'Learn more about', - })} -   - - - -

-
-
-
-
-
- )} -
- ); -} diff --git a/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar.scss b/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar.scss index d7190b61e33f3..806893269da34 100644 --- a/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar.scss +++ b/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar.scss @@ -2,7 +2,7 @@ overflow: hidden; margin: 0 !important; flex-grow: 1; - padding: $euiSizeS 0 $euiSizeS $euiSizeS; + padding: 0; width: $euiSize * 19; height: 100%; @@ -13,6 +13,14 @@ } } +.dscSidebar__list { + padding: $euiSizeS 0 $euiSizeS $euiSizeS; + + @include euiBreakpoint('xs', 's') { + padding: $euiSizeS 0 0 0; + } +} + .dscSidebar__group { height: 100%; } diff --git a/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar.test.tsx b/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar.test.tsx index 86fda1b9a196d..28cb0b9bfde27 100644 --- a/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar.test.tsx +++ b/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar.test.tsx @@ -17,7 +17,6 @@ import { DiscoverSidebarProps, } from './discover_sidebar'; import type { AggregateQuery, Query } from '@kbn/es-query'; -import { getDefaultFieldFilter } from './lib/field_filter'; import { createDiscoverServicesMock } from '../../../../__mocks__/services'; import { stubLogstashDataView } from '@kbn/data-plugin/common/stubs'; import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; @@ -29,7 +28,7 @@ import { VIEW_MODE } from '../../../../../common/constants'; import { DiscoverMainProvider } from '../../services/discover_state_provider'; import * as ExistingFieldsHookApi from '@kbn/unified-field-list-plugin/public/hooks/use_existing_fields'; import { ExistenceFetchStatus } from '@kbn/unified-field-list-plugin/public'; -import { getDataViewFieldList } from './lib/get_data_view_field_list'; +import { getDataViewFieldList } from './lib/get_field_list'; const mockGetActions = jest.fn>>, [string, { fieldName: string }]>( () => Promise.resolve([]) @@ -66,7 +65,7 @@ function getCompProps(): DiscoverSidebarProps { } } - const allFields = getDataViewFieldList(dataView, fieldCounts, false); + const allFields = getDataViewFieldList(dataView, fieldCounts); (ExistingFieldsHookApi.useExistingFieldsReader as jest.Mock).mockClear(); (ExistingFieldsHookApi.useExistingFieldsReader as jest.Mock).mockImplementation(() => ({ @@ -100,8 +99,6 @@ function getCompProps(): DiscoverSidebarProps { onRemoveField: jest.fn(), selectedDataView: dataView, trackUiMetric: jest.fn(), - fieldFilter: getDefaultFieldFilter(), - setFieldFilter: jest.fn(), onFieldEdited: jest.fn(), editField: jest.fn(), viewMode: VIEW_MODE.DOCUMENT_LEVEL, @@ -112,6 +109,7 @@ function getCompProps(): DiscoverSidebarProps { useNewFieldsApi: true, showFieldList: true, isAffectedByGlobalFilter: false, + isProcessing: false, }; } diff --git a/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar.tsx b/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar.tsx index ae16233c1fa6b..fc41760cc731e 100644 --- a/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar.tsx +++ b/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar.tsx @@ -20,6 +20,8 @@ import { isOfAggregateQueryType } from '@kbn/es-query'; import { DataViewPicker } from '@kbn/unified-search-plugin/public'; import { type DataViewField, getFieldSubtypeMulti } from '@kbn/data-views-plugin/public'; import { + FieldList, + FieldListFilters, FieldListGrouped, FieldListGroupedProps, FieldsGroupNames, @@ -31,7 +33,6 @@ import { VIEW_MODE } from '../../../../../common/constants'; import { useAppStateSelector } from '../../services/discover_app_state_container'; import { useDiscoverServices } from '../../../../hooks/use_discover_services'; import { DiscoverField } from './discover_field'; -import { DiscoverFieldSearch } from './discover_field_search'; import { FIELDS_LIMIT_SETTING, PLUGIN_ID } from '../../../../../common'; import { getSelectedFields, @@ -39,7 +40,6 @@ import { type SelectedFieldsResult, INITIAL_SELECTED_FIELDS_RESULT, } from './lib/group_fields'; -import { doesFieldMatchFilters, FieldFilterState, setFieldFilterProp } from './lib/field_filter'; import { DiscoverSidebarResponsiveProps } from './discover_sidebar_responsive'; import { getUiActions } from '../../../../kibana_services'; import { getRawRecordType } from '../../utils/get_raw_record_type'; @@ -49,13 +49,9 @@ const fieldSearchDescriptionId = htmlIdGenerator()(); export interface DiscoverSidebarProps extends DiscoverSidebarResponsiveProps { /** - * Current state of the field filter, filtering fields by name, type, ... + * Show loading instead of the field list if processing */ - fieldFilter: FieldFilterState; - /** - * Change current state of fieldFilter - */ - setFieldFilter: (next: FieldFilterState) => void; + isProcessing: boolean; /** * Callback to close the flyout if sidebar is rendered in a flyout @@ -82,7 +78,7 @@ export interface DiscoverSidebarProps extends DiscoverSidebarResponsiveProps { createNewDataView?: () => void; /** - * All fields: fields from data view and unmapped fields + * All fields: fields from data view and unmapped fields or columns from text-based search */ allFields: DataViewField[] | null; @@ -108,16 +104,15 @@ export interface DiscoverSidebarProps extends DiscoverSidebarResponsiveProps { } export function DiscoverSidebarComponent({ + isProcessing, alwaysShowActionButtons = false, columns, - fieldFilter, documents$, allFields, onAddField, onAddFilter, onRemoveField, selectedDataView, - setFieldFilter, trackUiMetric, useNewFieldsApi = false, onFieldEdited, @@ -137,41 +132,6 @@ export function DiscoverSidebarComponent({ ); const query = useAppStateSelector((state) => state.query); - const onChangeFieldSearch = useCallback( - (filterName: string, value: string | boolean | undefined) => { - const newState = setFieldFilterProp(fieldFilter, filterName, value); - setFieldFilter(newState); - }, - [fieldFilter, setFieldFilter] - ); - - const { fieldTypes, presentFieldTypes } = useMemo(() => { - const result = ['any']; - const dataViewFieldTypes = new Set(); - if (Array.isArray(allFields)) { - for (const field of allFields) { - if (field.type !== '_source') { - // If it's a string type, we want to distinguish between keyword and text - // For this purpose we need the ES type - const type = - field.type === 'string' && - field.esTypes && - ['keyword', 'text'].includes(field.esTypes[0]) - ? field.esTypes?.[0] - : field.type; - // _id and _index would map to string, that's why we don't add the string type here - if (type && type !== 'string') { - dataViewFieldTypes.add(type); - } - if (result.indexOf(field.type) === -1) { - result.push(field.type); - } - } - } - } - return { fieldTypes: result, presentFieldTypes: Array.from(dataViewFieldTypes) }; - }, [allFields]); - const showFieldStats = useMemo(() => viewMode === VIEW_MODE.DOCUMENT_LEVEL, [viewMode]); const [selectedFieldsState, setSelectedFieldsState] = useState( INITIAL_SELECTED_FIELDS_RESULT @@ -181,9 +141,14 @@ export function DiscoverSidebarComponent({ >(undefined); useEffect(() => { - const result = getSelectedFields(selectedDataView, columns); + const result = getSelectedFields({ + dataView: selectedDataView, + columns, + allFields, + isPlainRecord, + }); setSelectedFieldsState(result); - }, [selectedDataView, columns, setSelectedFieldsState]); + }, [selectedDataView, columns, setSelectedFieldsState, allFields, isPlainRecord]); useEffect(() => { if (isPlainRecord || !useNewFieldsApi) { @@ -244,12 +209,6 @@ export function DiscoverSidebarComponent({ }, [columns, selectedDataView, query]); const popularFieldsLimit = useMemo(() => uiSettings.get(FIELDS_LIMIT_SETTING), [uiSettings]); - const onFilterField: GroupedFieldsParams['onFilterField'] = useCallback( - (field) => { - return doesFieldMatchFilters(field, fieldFilter); - }, - [fieldFilter] - ); const onSupportedFieldFilter: GroupedFieldsParams['onSupportedFieldFilter'] = useCallback( (field) => { @@ -267,7 +226,7 @@ export function DiscoverSidebarComponent({ }; } }, []); - const { fieldListGroupedProps } = useGroupedFields({ + const { fieldListFiltersProps, fieldListGroupedProps } = useGroupedFields({ dataViewId: (!isPlainRecord && selectedDataView?.id) || null, // passing `null` for text-based queries allFields, popularFieldsLimit: !isPlainRecord ? popularFieldsLimit : 0, @@ -277,18 +236,17 @@ export function DiscoverSidebarComponent({ dataViews, core, }, - onFilterField, onSupportedFieldFilter, onOverrideFieldGroupDetails, }); const renderFieldItem: FieldListGroupedProps['renderFieldItem'] = useCallback( - ({ field, groupName }) => ( + ({ field, groupName, fieldSearchHighlight }) => (
  • )} - -
    - - -
    - {showFieldList && ( - - )} + } + className="dscSidebar__list" + > + {showFieldList ? ( + + ) : ( + + )} + {!!editField && ( + + editField()} + size="s" + > + {i18n.translate('discover.fieldChooser.addField.label', { + defaultMessage: 'Add a field', + })} + + + )} + {isPlainRecord && ( + + + {i18n.translate('discover.textBasedLanguages.visualize.label', { + defaultMessage: 'Visualize in Lens', + })} + + + )} + - {!!editField && ( - - editField()} - size="s" - > - {i18n.translate('discover.fieldChooser.addField.label', { - defaultMessage: 'Add a field', - })} - - - )} - {isPlainRecord && ( - - - {i18n.translate('discover.textBasedLanguages.visualize.label', { - defaultMessage: 'Visualize in Lens', - })} - - - )} ); diff --git a/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar_responsive.test.tsx b/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar_responsive.test.tsx index e6370fb0713b0..26b2216eb0e04 100644 --- a/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar_responsive.test.tsx +++ b/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar_responsive.test.tsx @@ -396,7 +396,8 @@ describe('discover responsive sidebar', function () { findTestSubject(comp, 'discoverFieldListPanelAddExistFilter-extension').simulate('click'); expect(props.onAddFilter).toHaveBeenCalledWith('_exists_', 'extension', '+'); }); - it('should allow filtering by string, and calcFieldCount should just be executed once', async function () { + + it('should allow searching by string, and calcFieldCount should just be executed once', async function () { const comp = await mountComponent(props); expect(findTestSubject(comp, 'fieldListGroupedAvailableFields-count').text()).toBe('3'); @@ -405,7 +406,7 @@ describe('discover responsive sidebar', function () { ); await act(async () => { - await findTestSubject(comp, 'fieldFilterSearchInput').simulate('change', { + await findTestSubject(comp, 'fieldListFiltersFieldSearch').simulate('change', { target: { value: 'bytes' }, }); }); @@ -417,6 +418,34 @@ describe('discover responsive sidebar', function () { expect(mockCalcFieldCounts.mock.calls.length).toBe(1); }); + it('should allow filtering by field type', async function () { + const comp = await mountComponent(props); + + expect(findTestSubject(comp, 'fieldListGroupedAvailableFields-count').text()).toBe('3'); + expect(findTestSubject(comp, 'fieldListGrouped__ariaDescription').text()).toBe( + '1 selected field. 4 popular fields. 3 available fields. 20 empty fields. 2 meta fields.' + ); + + await act(async () => { + await findTestSubject(comp, 'fieldListFiltersFieldTypeFilterToggle').simulate('click'); + }); + + await comp.update(); + + await act(async () => { + await findTestSubject(comp, 'typeFilter-number').simulate('click'); + }); + + await comp.update(); + + expect(findTestSubject(comp, 'fieldListGroupedAvailableFields-count').text()).toBe('2'); + expect(findTestSubject(comp, 'fieldListGrouped__ariaDescription').text()).toBe( + '1 popular field. 2 available fields. 1 empty field. 0 meta fields.' + ); + + expect(mockCalcFieldCounts.mock.calls.length).toBe(1); + }, 10000); + it('should show "Add a field" button to create a runtime field', async () => { const services = createMockServices(); const comp = await mountComponent(props, {}, services); @@ -433,6 +462,11 @@ describe('discover responsive sidebar', function () { fetchStatus: FetchStatus.COMPLETE, recordRawType: RecordRawType.PLAIN, result: getDataTableRecords(stubLogstashDataView), + textBasedQueryColumns: [ + { id: '1', name: 'extension', meta: { type: 'text' } }, + { id: '1', name: 'bytes', meta: { type: 'number' } }, + { id: '1', name: '@timestamp', meta: { type: 'date' } }, + ], }) as DataDocuments$, }; const compInViewerMode = await mountComponent(propsWithTextBasedMode, { @@ -461,15 +495,15 @@ describe('discover responsive sidebar', function () { expect(selectedFieldsCount.text()).toBe('2'); expect(popularFieldsCount.exists()).toBe(false); - expect(availableFieldsCount.text()).toBe('4'); + expect(availableFieldsCount.text()).toBe('3'); expect(emptyFieldsCount.exists()).toBe(false); expect(metaFieldsCount.exists()).toBe(false); expect(unmappedFieldsCount.exists()).toBe(false); - expect(mockCalcFieldCounts.mock.calls.length).toBe(1); + expect(mockCalcFieldCounts.mock.calls.length).toBe(0); expect(findTestSubject(compInViewerMode, 'fieldListGrouped__ariaDescription').text()).toBe( - '2 selected fields. 4 available fields.' + '2 selected fields. 3 available fields.' ); }); diff --git a/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar_responsive.tsx b/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar_responsive.tsx index 7d31fff6e5a2f..b2b2715067d8c 100644 --- a/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar_responsive.tsx +++ b/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar_responsive.tsx @@ -19,7 +19,6 @@ import { EuiIcon, EuiLink, EuiPortal, - EuiProgress, EuiShowFor, EuiTitle, } from '@elastic/eui'; @@ -30,7 +29,6 @@ import { } from '@kbn/unified-field-list-plugin/public'; import { VIEW_MODE } from '../../../../../common/constants'; import { useDiscoverServices } from '../../../../hooks/use_discover_services'; -import { getDefaultFieldFilter } from './lib/field_filter'; import { DiscoverSidebar } from './discover_sidebar'; import { AvailableFields$, @@ -49,6 +47,8 @@ import { DiscoverSidebarReducerStatus, } from './lib/sidebar_reducer'; +const EMPTY_FIELD_COUNTS = {}; + export interface DiscoverSidebarResponsiveProps { /** * Determines whether add/remove buttons are displayed non only when focused @@ -127,7 +127,6 @@ export function DiscoverSidebarResponsive(props: DiscoverSidebarResponsiveProps) (state) => getRawRecordType(state.query) === RecordRawType.PLAIN ); const { selectedDataView, onFieldEdited, onDataViewCreated } = props; - const [fieldFilter, setFieldFilter] = useState(getDefaultFieldFilter()); const [isFlyoutVisible, setIsFlyoutVisible] = useState(false); const [sidebarState, dispatchSidebarStateAction] = useReducer( discoverSidebarReducer, @@ -163,7 +162,10 @@ export function DiscoverSidebarResponsive(props: DiscoverSidebarResponsiveProps) type: DiscoverSidebarReducerActionType.DOCUMENTS_LOADED, payload: { dataView: selectedDataViewRef.current, - fieldCounts: calcFieldCounts(documentState.result), + fieldCounts: isPlainRecordType + ? EMPTY_FIELD_COUNTS + : calcFieldCounts(documentState.result), + textBasedQueryColumns: documentState.textBasedQueryColumns, isPlainRecord: isPlainRecordType, }, }); @@ -173,7 +175,7 @@ export function DiscoverSidebarResponsive(props: DiscoverSidebarResponsiveProps) type: DiscoverSidebarReducerActionType.DOCUMENTS_LOADED, payload: { dataView: selectedDataViewRef.current, - fieldCounts: {}, + fieldCounts: EMPTY_FIELD_COUNTS, isPlainRecord: isPlainRecordType, }, }); @@ -324,13 +326,11 @@ export function DiscoverSidebarResponsive(props: DiscoverSidebarResponsiveProps) <> {!props.isClosed && ( - {isProcessing && } { - return setFieldFilterProp(acc, kv[0], kv[1]); - }, state); - expect(actualState).toMatchInlineSnapshot(` - Object { - "aggregatable": true, - "name": "test", - "searchable": true, - "type": "string", - } - `); - }); - it('filters a given list', () => { - const defaultState = getDefaultFieldFilter(); - const fieldList = [ - { - name: 'bytes', - displayName: 'Bye,bye,Bytes', - type: 'number', - esTypes: ['long'], - count: 10, - scripted: false, - searchable: false, - aggregatable: false, - }, - { - name: 'extension', - displayName: 'Extension', - type: 'string', - esTypes: ['text'], - count: 10, - scripted: true, - searchable: true, - aggregatable: true, - }, - ] as DataViewField[]; - - [ - { filter: {}, result: ['bytes', 'extension'] }, - { filter: { name: 'by' }, result: ['bytes'] }, - { filter: { name: 'Ext' }, result: ['extension'] }, - { filter: { name: 'Bytes' }, result: ['bytes'] }, - { filter: { aggregatable: true }, result: ['extension'] }, - { filter: { aggregatable: true, searchable: false }, result: [] }, - { filter: { type: 'string' }, result: ['extension'] }, - ].forEach((test) => { - const filtered = fieldList - .filter((field) => doesFieldMatchFilters(field, { ...defaultState, ...test.filter })) - .map((field) => field.name); - - expect(filtered).toEqual(test.result); - }); - }); -}); diff --git a/src/plugins/discover/public/application/main/components/sidebar/lib/field_filter.ts b/src/plugins/discover/public/application/main/components/sidebar/lib/field_filter.ts deleted file mode 100644 index 1f2ab0b9b64cd..0000000000000 --- a/src/plugins/discover/public/application/main/components/sidebar/lib/field_filter.ts +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { DataViewField } from '@kbn/data-views-plugin/public'; - -export interface FieldFilterState { - type: string; - name: string; - aggregatable: null | boolean; - searchable: null | boolean; -} - -export function getDefaultFieldFilter(): FieldFilterState { - return { - type: 'any', - name: '', - aggregatable: null, - searchable: null, - }; -} - -export function setFieldFilterProp( - state: FieldFilterState, - name: string, - value: string | boolean | null | undefined -): FieldFilterState { - const newState = { ...state }; - if (name === 'aggregatable') { - newState.aggregatable = typeof value !== 'boolean' ? null : value; - } else if (name === 'searchable') { - newState.searchable = typeof value !== 'boolean' ? null : value; - } else if (name === 'name') { - newState.name = String(value); - } else if (name === 'type') { - newState.type = String(value); - } - return newState; -} - -export function doesFieldMatchFilters( - field: DataViewField, - filterState: FieldFilterState -): boolean { - const matchFilter = filterState.type === 'any' || field.type === filterState.type; - const isAggregatable = - filterState.aggregatable === null || field.aggregatable === filterState.aggregatable; - const isSearchable = - filterState.searchable === null || field.searchable === filterState.searchable; - const needle = filterState.name ? filterState.name.toLowerCase() : ''; - const haystack = `${field.name}${field.displayName || ''}`.toLowerCase(); - const matchName = !filterState.name || haystack.indexOf(needle) !== -1; - - return matchFilter && isAggregatable && isSearchable && matchName; -} diff --git a/src/plugins/discover/public/application/main/components/sidebar/lib/get_data_view_field_list.ts b/src/plugins/discover/public/application/main/components/sidebar/lib/get_field_list.ts similarity index 59% rename from src/plugins/discover/public/application/main/components/sidebar/lib/get_data_view_field_list.ts rename to src/plugins/discover/public/application/main/components/sidebar/lib/get_field_list.ts index 5d055d94184ed..78823bb0f468f 100644 --- a/src/plugins/discover/public/application/main/components/sidebar/lib/get_data_view_field_list.ts +++ b/src/plugins/discover/public/application/main/components/sidebar/lib/get_field_list.ts @@ -7,16 +7,16 @@ */ import { difference } from 'lodash'; -import type { DataView, DataViewField } from '@kbn/data-views-plugin/public'; +import { type DataView, DataViewField } from '@kbn/data-views-plugin/public'; +import type { DatatableColumn } from '@kbn/expressions-plugin/common'; import { fieldWildcardFilter } from '@kbn/kibana-utils-plugin/public'; import { isNestedFieldParent } from '../../../utils/nested_fields'; export function getDataViewFieldList( dataView: DataView | undefined | null, - fieldCounts: Record | undefined | null, - isPlainRecord: boolean + fieldCounts: Record | undefined | null ): DataViewField[] | null { - if (isPlainRecord && !fieldCounts) { + if (!fieldCounts) { // still loading data return null; } @@ -28,7 +28,7 @@ export function getDataViewFieldList( if (sourceFiltersValues) { const filter = fieldWildcardFilter(sourceFiltersValues, dataView.metaFields); dataViewFields = dataViewFields.filter((field) => { - return filter(field.name) || currentFieldCounts[field.name]; // don't filter out a field which was present in hits (ex. for text-based queries, selected fields) + return filter(field.name) || currentFieldCounts[field.name]; // don't filter out a field which was present in hits (ex. for selected fields) }); } @@ -38,24 +38,42 @@ export function getDataViewFieldList( difference(fieldNamesInDocs, fieldNamesInDataView).forEach((unknownFieldName) => { if (dataView && isNestedFieldParent(unknownFieldName, dataView)) { - unknownFields.push({ - displayName: String(unknownFieldName), - name: String(unknownFieldName), - type: 'nested', - } as DataViewField); + unknownFields.push( + new DataViewField({ + name: String(unknownFieldName), + type: 'nested', + searchable: false, + aggregatable: false, + }) + ); } else { - unknownFields.push({ - displayName: String(unknownFieldName), - name: String(unknownFieldName), - type: 'unknown', - } as DataViewField); + unknownFields.push( + new DataViewField({ + name: String(unknownFieldName), + type: 'unknown', + searchable: false, + aggregatable: false, + }) + ); } }); - return [ - ...(isPlainRecord - ? dataViewFields.filter((field) => currentFieldCounts[field.name]) - : dataViewFields), - ...unknownFields, - ]; + return [...dataViewFields, ...unknownFields]; +} + +export function getTextBasedQueryFieldList( + textBasedQueryColumns?: DatatableColumn[] +): DataViewField[] { + if (!textBasedQueryColumns) { + return []; + } + return textBasedQueryColumns.map( + (column) => + new DataViewField({ + name: column.name, + type: column.meta?.type ?? 'unknown', + searchable: false, + aggregatable: false, + }) + ); } diff --git a/src/plugins/discover/public/application/main/components/sidebar/lib/get_field_type_description.ts b/src/plugins/discover/public/application/main/components/sidebar/lib/get_field_type_description.ts deleted file mode 100644 index 3b5b6aaa016ce..0000000000000 --- a/src/plugins/discover/public/application/main/components/sidebar/lib/get_field_type_description.ts +++ /dev/null @@ -1,118 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import type { DocLinksStart } from '@kbn/core/public'; -import { i18n } from '@kbn/i18n'; -import { KNOWN_FIELD_TYPES } from '../../../../../../common/field_types'; - -const UNKNOWN_FIELD_TYPE_DESC = i18n.translate('discover.fieldNameDescription.unknownField', { - defaultMessage: 'Unknown field', -}); - -export function getFieldTypeDescription(type: string, docLinks: DocLinksStart) { - const knownType: KNOWN_FIELD_TYPES = type as KNOWN_FIELD_TYPES; - switch (knownType) { - case KNOWN_FIELD_TYPES.BOOLEAN: - return i18n.translate('discover.fieldNameDescription.booleanField', { - defaultMessage: 'True and false values.', - }); - case KNOWN_FIELD_TYPES.CONFLICT: - return i18n.translate('discover.fieldNameDescription.conflictField', { - defaultMessage: 'Field has values of different types. Resolve in Management > Data Views.', - }); - case KNOWN_FIELD_TYPES.DATE: - return i18n.translate('discover.fieldNameDescription.dateField', { - defaultMessage: 'A date string or the number of seconds or milliseconds since 1/1/1970.', - }); - case KNOWN_FIELD_TYPES.DATE_RANGE: - return i18n.translate('discover.fieldNameDescription.dateRangeField', { - defaultMessage: 'Range of {dateFieldTypeLink} values. {viewSupportedDateFormatsLink}', - values: { - dateFieldTypeLink: - `` + - i18n.translate('discover.fieldNameDescription.dateRangeFieldLinkText', { - defaultMessage: 'date', - }) + - '', - viewSupportedDateFormatsLink: - `` + - i18n.translate('discover.fieldNameDescription.viewSupportedDateFormatsLinkText', { - defaultMessage: 'View supported date formats.', - }) + - '', - }, - }); - case KNOWN_FIELD_TYPES.GEO_POINT: - return i18n.translate('discover.fieldNameDescription.geoPointField', { - defaultMessage: 'Latitude and longitude points.', - }); - case KNOWN_FIELD_TYPES.GEO_SHAPE: - return i18n.translate('discover.fieldNameDescription.geoShapeField', { - defaultMessage: 'Complex shapes, such as polygons.', - }); - case KNOWN_FIELD_TYPES.HISTOGRAM: - return i18n.translate('discover.fieldNameDescription.histogramField', { - defaultMessage: 'Pre-aggregated numerical values in the form of a histogram.', - }); - case KNOWN_FIELD_TYPES.IP: - return i18n.translate('discover.fieldNameDescription.ipAddressField', { - defaultMessage: 'IPv4 and IPv6 addresses.', - }); - case KNOWN_FIELD_TYPES.IP_RANGE: - return i18n.translate('discover.fieldNameDescription.ipAddressRangeField', { - defaultMessage: 'Range of ip values supporting either IPv4 or IPv6 (or mixed) addresses.', - }); - case KNOWN_FIELD_TYPES.MURMUR3: - return i18n.translate('discover.fieldNameDescription.murmur3Field', { - defaultMessage: 'Field that computes and stores hashes of values.', - }); - case KNOWN_FIELD_TYPES.NUMBER: - return i18n.translate('discover.fieldNameDescription.numberField', { - defaultMessage: 'Long, integer, short, byte, double, and float values.', - }); - case KNOWN_FIELD_TYPES.STRING: - return i18n.translate('discover.fieldNameDescription.stringField', { - defaultMessage: 'Full text such as the body of an email or a product description.', - }); - case KNOWN_FIELD_TYPES.TEXT: - return i18n.translate('discover.fieldNameDescription.textField', { - defaultMessage: 'Full text such as the body of an email or a product description.', - }); - case KNOWN_FIELD_TYPES.KEYWORD: - return i18n.translate('discover.fieldNameDescription.keywordField', { - defaultMessage: - 'Structured content such as an ID, email address, hostname, status code, or tag.', - }); - case KNOWN_FIELD_TYPES.NESTED: - return i18n.translate('discover.fieldNameDescription.nestedField', { - defaultMessage: 'JSON object that preserves the relationship between its subfields.', - }); - case KNOWN_FIELD_TYPES.VERSION: - return i18n.translate('discover.fieldNameDescription.versionField', { - defaultMessage: 'Software versions. Supports {SemanticVersioningLink} precedence rules.', - values: { - SemanticVersioningLink: - `` + - i18n.translate( - 'discover.advancedSettings.discover.fieldNameDescription.versionFieldLinkText', - { - defaultMessage: 'Semantic Versioning', - } - ) + - '', - }, - }); - default: - // If you see a typescript error here, that's a sign that there are missing switch cases ^^ - const _exhaustiveCheck: never = knownType; - return UNKNOWN_FIELD_TYPE_DESC || _exhaustiveCheck; - } -} diff --git a/src/plugins/discover/public/application/main/components/sidebar/lib/group_fields.test.ts b/src/plugins/discover/public/application/main/components/sidebar/lib/group_fields.test.ts index 7dee06ec512bc..53d0bc96d3ca8 100644 --- a/src/plugins/discover/public/application/main/components/sidebar/lib/group_fields.test.ts +++ b/src/plugins/discover/public/application/main/components/sidebar/lib/group_fields.test.ts @@ -12,7 +12,12 @@ import { getSelectedFields, shouldShowField, INITIAL_SELECTED_FIELDS_RESULT } fr describe('group_fields', function () { it('should pick fields as unknown_selected if they are unknown', function () { - const actual = getSelectedFields(dataView, ['currency']); + const actual = getSelectedFields({ + dataView, + columns: ['currency'], + allFields: dataView.fields, + isPlainRecord: false, + }); expect(actual).toMatchInlineSnapshot(` Object { "selectedFields": Array [ @@ -29,13 +34,47 @@ describe('group_fields', function () { `); }); + it('should pick fields as nested for a nested field root', function () { + const actual = getSelectedFields({ + dataView, + columns: ['nested1', 'bytes'], + allFields: [ + { + name: 'nested1', + type: 'nested', + }, + ] as DataViewField[], + isPlainRecord: false, + }); + expect(actual.selectedFieldsMap).toMatchInlineSnapshot(` + Object { + "bytes": true, + "nested1": true, + } + `); + }); + it('should work correctly if no columns selected', function () { - expect(getSelectedFields(dataView, [])).toBe(INITIAL_SELECTED_FIELDS_RESULT); - expect(getSelectedFields(dataView, ['_source'])).toBe(INITIAL_SELECTED_FIELDS_RESULT); + expect( + getSelectedFields({ dataView, columns: [], allFields: dataView.fields, isPlainRecord: false }) + ).toBe(INITIAL_SELECTED_FIELDS_RESULT); + expect( + getSelectedFields({ + dataView, + columns: ['_source'], + allFields: dataView.fields, + isPlainRecord: false, + }) + ).toBe(INITIAL_SELECTED_FIELDS_RESULT); }); it('should pick fields into selected group', function () { - const actual = getSelectedFields(dataView, ['bytes', '@timestamp']); + const actual = getSelectedFields({ + dataView, + columns: ['bytes', '@timestamp'], + allFields: dataView.fields, + isPlainRecord: false, + }); expect(actual.selectedFields.map((field) => field.name)).toEqual(['bytes', '@timestamp']); expect(actual.selectedFieldsMap).toStrictEqual({ bytes: true, @@ -44,7 +83,12 @@ describe('group_fields', function () { }); it('should pick fields into selected group if they contain multifields', function () { - const actual = getSelectedFields(dataView, ['machine.os', 'machine.os.raw']); + const actual = getSelectedFields({ + dataView, + columns: ['machine.os', 'machine.os.raw'], + allFields: dataView.fields, + isPlainRecord: false, + }); expect(actual.selectedFields.map((field) => field.name)).toEqual([ 'machine.os', 'machine.os.raw', @@ -56,7 +100,12 @@ describe('group_fields', function () { }); it('should sort selected fields by columns order', function () { - const actual1 = getSelectedFields(dataView, ['bytes', 'extension.keyword', 'unknown']); + const actual1 = getSelectedFields({ + dataView, + columns: ['bytes', 'extension.keyword', 'unknown'], + allFields: dataView.fields, + isPlainRecord: false, + }); expect(actual1.selectedFields.map((field) => field.name)).toEqual([ 'bytes', 'extension.keyword', @@ -68,7 +117,12 @@ describe('group_fields', function () { unknown: true, }); - const actual2 = getSelectedFields(dataView, ['extension', 'bytes', 'unknown']); + const actual2 = getSelectedFields({ + dataView, + columns: ['extension', 'bytes', 'unknown'], + allFields: dataView.fields, + isPlainRecord: false, + }); expect(actual2.selectedFields.map((field) => field.name)).toEqual([ 'extension', 'bytes', @@ -81,6 +135,33 @@ describe('group_fields', function () { }); }); + it('should pick fields only from allFields instead of data view fields for a text based query', function () { + const actual = getSelectedFields({ + dataView, + columns: ['bytes'], + allFields: [ + { + name: 'bytes', + type: 'text', + }, + ] as DataViewField[], + isPlainRecord: true, + }); + expect(actual).toMatchInlineSnapshot(` + Object { + "selectedFields": Array [ + Object { + "name": "bytes", + "type": "text", + }, + ], + "selectedFieldsMap": Object { + "bytes": true, + }, + } + `); + }); + it('should show any fields if for text-based searches', function () { expect(shouldShowField(dataView.getFieldByName('bytes'), true)).toBe(true); expect(shouldShowField({ type: 'unknown', name: 'unknown' } as DataViewField, true)).toBe(true); diff --git a/src/plugins/discover/public/application/main/components/sidebar/lib/group_fields.tsx b/src/plugins/discover/public/application/main/components/sidebar/lib/group_fields.tsx index eaae1c90d3833..11bbd285f4b7e 100644 --- a/src/plugins/discover/public/application/main/components/sidebar/lib/group_fields.tsx +++ b/src/plugins/discover/public/application/main/components/sidebar/lib/group_fields.tsx @@ -36,22 +36,30 @@ export interface SelectedFieldsResult { selectedFieldsMap: Record; } -export function getSelectedFields( - dataView: DataView | undefined, - columns: string[] -): SelectedFieldsResult { +export function getSelectedFields({ + dataView, + columns, + allFields, + isPlainRecord, +}: { + dataView: DataView | undefined; + columns: string[]; + allFields: DataViewField[] | null; + isPlainRecord: boolean; +}): SelectedFieldsResult { const result: SelectedFieldsResult = { selectedFields: [], selectedFieldsMap: {}, }; - if (!Array.isArray(columns) || !columns.length) { + if (!Array.isArray(columns) || !columns.length || !allFields) { return INITIAL_SELECTED_FIELDS_RESULT; } // add selected columns, that are not part of the data view, to be removable for (const column of columns) { const selectedField = - dataView?.getFieldByName?.(column) || + (!isPlainRecord && dataView?.getFieldByName?.(column)) || + allFields.find((field) => field.name === column) || // for example to pick a `nested` root field or find a selected field in text-based response ({ name: column, displayName: column, diff --git a/src/plugins/discover/public/application/main/components/sidebar/lib/sidebar_reducer.test.ts b/src/plugins/discover/public/application/main/components/sidebar/lib/sidebar_reducer.test.ts index 131f9c358317f..c5ea2878f800e 100644 --- a/src/plugins/discover/public/application/main/components/sidebar/lib/sidebar_reducer.test.ts +++ b/src/plugins/discover/public/application/main/components/sidebar/lib/sidebar_reducer.test.ts @@ -18,6 +18,7 @@ import { getInitialState, } from './sidebar_reducer'; import { DataViewField } from '@kbn/data-views-plugin/common'; +import type { DatatableColumn } from '@kbn/expressions-plugin/common'; describe('sidebar reducer', function () { it('should set an initial state', function () { @@ -84,11 +85,12 @@ describe('sidebar reducer', function () { allFields: [ ...stubDataViewWithoutTimeField.fields, // merging in unmapped fields - { - displayName: unmappedFieldName, + new DataViewField({ name: unmappedFieldName, type: 'unknown', - } as DataViewField, + aggregatable: false, + searchable: false, + }), ], fieldCounts, status: DiscoverSidebarReducerStatus.COMPLETED, @@ -99,33 +101,54 @@ describe('sidebar reducer', function () { payload: { isPlainRecord: true, dataView: stubDataViewWithoutTimeField, - fieldCounts, + fieldCounts: {}, + textBasedQueryColumns: [ + { + id: '1', + name: 'text1', + meta: { + type: 'number', + }, + }, + { + id: '2', + name: 'text2', + meta: { + type: 'keyword', + }, + }, + ] as DatatableColumn[], }, }); expect(resultForTextBasedQuery).toStrictEqual({ dataView: stubDataViewWithoutTimeField, allFields: [ - stubDataViewWithoutTimeField.fields.find((field) => field.name === dataViewFieldName), - // merging in unmapped fields - { - displayName: 'field1', - name: 'field1', - type: 'unknown', - } as DataViewField, + new DataViewField({ + name: 'text1', + type: 'number', + aggregatable: false, + searchable: false, + }), + new DataViewField({ + name: 'text2', + type: 'keyword', + aggregatable: false, + searchable: false, + }), ], - fieldCounts, + fieldCounts: {}, status: DiscoverSidebarReducerStatus.COMPLETED, }); - const resultForTextBasedQueryWhileLoading = discoverSidebarReducer(state, { + const resultWhileLoading = discoverSidebarReducer(state, { type: DiscoverSidebarReducerActionType.DOCUMENTS_LOADED, payload: { - isPlainRecord: true, + isPlainRecord: false, dataView: stubDataViewWithoutTimeField, fieldCounts: null, }, }); - expect(resultForTextBasedQueryWhileLoading).toStrictEqual({ + expect(resultWhileLoading).toStrictEqual({ dataView: stubDataViewWithoutTimeField, allFields: null, fieldCounts: null, diff --git a/src/plugins/discover/public/application/main/components/sidebar/lib/sidebar_reducer.ts b/src/plugins/discover/public/application/main/components/sidebar/lib/sidebar_reducer.ts index 0c579275029b1..54e9a2c95ce12 100644 --- a/src/plugins/discover/public/application/main/components/sidebar/lib/sidebar_reducer.ts +++ b/src/plugins/discover/public/application/main/components/sidebar/lib/sidebar_reducer.ts @@ -7,7 +7,8 @@ */ import { type DataView, type DataViewField } from '@kbn/data-views-plugin/common'; -import { getDataViewFieldList } from './get_data_view_field_list'; +import type { DatatableColumn } from '@kbn/expressions-plugin/common'; +import { getDataViewFieldList, getTextBasedQueryFieldList } from './get_field_list'; export enum DiscoverSidebarReducerActionType { RESET = 'RESET', @@ -39,6 +40,7 @@ type DiscoverSidebarReducerAction = type: DiscoverSidebarReducerActionType.DOCUMENTS_LOADED; payload: { fieldCounts: DiscoverSidebarReducerState['fieldCounts']; + textBasedQueryColumns?: DatatableColumn[]; // from text-based searches isPlainRecord: boolean; dataView: DataView | null | undefined; }; @@ -94,11 +96,9 @@ export function discoverSidebarReducer( status: DiscoverSidebarReducerStatus.PROCESSING, }; case DiscoverSidebarReducerActionType.DOCUMENTS_LOADED: - const mappedAndUnmappedFields = getDataViewFieldList( - action.payload.dataView, - action.payload.fieldCounts, - action.payload.isPlainRecord - ); + const mappedAndUnmappedFields = action.payload.isPlainRecord + ? getTextBasedQueryFieldList(action.payload.textBasedQueryColumns) + : getDataViewFieldList(action.payload.dataView, action.payload.fieldCounts); return { ...state, dataView: action.payload.dataView, diff --git a/src/plugins/discover/public/application/main/services/discover_data_state_container.ts b/src/plugins/discover/public/application/main/services/discover_data_state_container.ts index 5cadb099c483e..0a845dc92315c 100644 --- a/src/plugins/discover/public/application/main/services/discover_data_state_container.ts +++ b/src/plugins/discover/public/application/main/services/discover_data_state_container.ts @@ -7,6 +7,7 @@ */ import { BehaviorSubject, filter, map, Observable, share, Subject, tap } from 'rxjs'; import { AutoRefreshDoneFn } from '@kbn/data-plugin/public'; +import type { DatatableColumn } from '@kbn/expressions-plugin/common'; import { RequestAdapter } from '@kbn/inspector-plugin/common'; import { SavedSearch } from '@kbn/saved-search-plugin/public'; import { AggregateQuery, Query } from '@kbn/es-query'; @@ -68,6 +69,7 @@ export interface DataMainMsg extends DataMsg { export interface DataDocumentsMsg extends DataMsg { result?: DataTableRecord[]; + textBasedQueryColumns?: DatatableColumn[]; // columns from text-based request } export interface DataTotalHitsMsg extends DataMsg { diff --git a/src/plugins/discover/public/application/main/utils/fetch_all.test.ts b/src/plugins/discover/public/application/main/utils/fetch_all.test.ts index 95b1cd7618b4d..40aeb04fb8e95 100644 --- a/src/plugins/discover/public/application/main/utils/fetch_all.test.ts +++ b/src/plugins/discover/public/application/main/utils/fetch_all.test.ts @@ -82,8 +82,8 @@ describe('test fetchAll', () => { }; searchSource = savedSearchMock.searchSource.createChild(); - mockFetchDocuments.mockReset().mockResolvedValue([]); - mockFetchSQL.mockReset().mockResolvedValue([]); + mockFetchDocuments.mockReset().mockResolvedValue({ records: [] }); + mockFetchSQL.mockReset().mockResolvedValue({ records: [] }); }); test('changes of fetchStatus when starting with FetchStatus.UNINITIALIZED', async () => { @@ -108,7 +108,7 @@ describe('test fetchAll', () => { { _id: '2', _index: 'logs' }, ]; const documents = hits.map((hit) => buildDataTableRecord(hit, dataViewMock)); - mockFetchDocuments.mockResolvedValue(documents); + mockFetchDocuments.mockResolvedValue({ records: documents }); fetchAll(subjects, searchSource, false, deps); await waitForNextTick(); expect(await collect()).toEqual([ @@ -130,7 +130,7 @@ describe('test fetchAll', () => { ]; searchSource.getField('index')!.isTimeBased = () => false; const documents = hits.map((hit) => buildDataTableRecord(hit, dataViewMock)); - mockFetchDocuments.mockResolvedValue(documents); + mockFetchDocuments.mockResolvedValue({ records: documents }); subjects.totalHits$.next({ fetchStatus: FetchStatus.LOADING, @@ -181,7 +181,7 @@ describe('test fetchAll', () => { searchSource.getField('index')!.isTimeBased = () => false; const hits = [{ _id: '1', _index: 'logs' }]; const documents = hits.map((hit) => buildDataTableRecord(hit, dataViewMock)); - mockFetchDocuments.mockResolvedValue(documents); + mockFetchDocuments.mockResolvedValue({ records: documents }); subjects.totalHits$.next({ fetchStatus: FetchStatus.LOADING, recordRawType: RecordRawType.DOCUMENT, @@ -248,7 +248,10 @@ describe('test fetchAll', () => { { _id: '2', _index: 'logs' }, ]; const documents = hits.map((hit) => buildDataTableRecord(hit, dataViewMock)); - mockFetchSQL.mockResolvedValue(documents); + mockFetchSQL.mockResolvedValue({ + records: documents, + textBasedQueryColumns: [{ id: '1', name: 'test1', meta: { type: 'number' } }], + }); const query = { sql: 'SELECT * from foo' }; deps = { appStateContainer: { @@ -275,6 +278,7 @@ describe('test fetchAll', () => { fetchStatus: FetchStatus.COMPLETE, recordRawType: 'plain', result: documents, + textBasedQueryColumns: [{ id: '1', name: 'test1', meta: { type: 'number' } }], query, }, ]); diff --git a/src/plugins/discover/public/application/main/utils/fetch_all.ts b/src/plugins/discover/public/application/main/utils/fetch_all.ts index c2a3c0856af06..656957d85f5d6 100644 --- a/src/plugins/discover/public/application/main/utils/fetch_all.ts +++ b/src/plugins/discover/public/application/main/utils/fetch_all.ts @@ -86,32 +86,33 @@ export function fetchAll( sendLoadingMsg(dataSubjects.totalHits$, { recordRawType }); // Start fetching all required requests - const documents = + const response = useSql && query ? fetchSql(query, dataView, data, services.expressions, inspectorAdapters) : fetchDocuments(searchSource.createCopy(), fetchDeps); // Handle results of the individual queries and forward the results to the corresponding dataSubjects - documents - .then((docs) => { + response + .then(({ records, textBasedQueryColumns }) => { // If the total hits (or chart) query is still loading, emit a partial // hit count that's at least our retrieved document count if (dataSubjects.totalHits$.getValue().fetchStatus === FetchStatus.LOADING) { dataSubjects.totalHits$.next({ fetchStatus: FetchStatus.PARTIAL, - result: docs.length, + result: records.length, recordRawType, }); } dataSubjects.documents$.next({ fetchStatus: FetchStatus.COMPLETE, - result: docs, + result: records, + textBasedQueryColumns, recordRawType, query, }); - checkHitCount(dataSubjects.main$, docs.length); + checkHitCount(dataSubjects.main$, records.length); }) // Only the document query should send its errors to main$, to cause the full Discover app // to get into an error state. The other queries will not cause all of Discover to error out diff --git a/src/plugins/discover/public/application/main/utils/fetch_documents.test.ts b/src/plugins/discover/public/application/main/utils/fetch_documents.test.ts index 28738cdc522c9..22415aa782194 100644 --- a/src/plugins/discover/public/application/main/utils/fetch_documents.test.ts +++ b/src/plugins/discover/public/application/main/utils/fetch_documents.test.ts @@ -36,7 +36,9 @@ describe('test fetchDocuments', () => { const documents = hits.map((hit) => buildDataTableRecord(hit, dataViewMock)); savedSearchMock.searchSource.fetch$ = () => of({ rawResponse: { hits: { hits } } } as IKibanaSearchResponse>); - expect(fetchDocuments(savedSearchMock.searchSource, getDeps())).resolves.toEqual(documents); + expect(fetchDocuments(savedSearchMock.searchSource, getDeps())).resolves.toEqual({ + records: documents, + }); }); test('rejects on query failure', () => { diff --git a/src/plugins/discover/public/application/main/utils/fetch_documents.ts b/src/plugins/discover/public/application/main/utils/fetch_documents.ts index 9a2913aea6d0b..db092cb449057 100644 --- a/src/plugins/discover/public/application/main/utils/fetch_documents.ts +++ b/src/plugins/discover/public/application/main/utils/fetch_documents.ts @@ -9,7 +9,7 @@ import { i18n } from '@kbn/i18n'; import { filter, map } from 'rxjs/operators'; import { lastValueFrom } from 'rxjs'; import { isCompleteResponse, ISearchSource } from '@kbn/data-plugin/public'; -import { EsHitRecord } from '../../../types'; +import type { RecordsFetchResponse, EsHitRecord } from '../../../types'; import { buildDataTableRecordList } from '../../../utils/build_data_record'; import { SAMPLE_SIZE_SETTING } from '../../../../common'; import { FetchDeps } from './fetch_all'; @@ -21,7 +21,7 @@ import { FetchDeps } from './fetch_all'; export const fetchDocuments = ( searchSource: ISearchSource, { abortController, inspectorAdapters, searchSessionId, services }: FetchDeps -) => { +): Promise => { searchSource.setField('size', services.uiSettings.get(SAMPLE_SIZE_SETTING)); searchSource.setField('trackTotalHits', false); searchSource.setField('highlightAll', true); @@ -61,5 +61,5 @@ export const fetchDocuments = ( }) ); - return lastValueFrom(fetch$); + return lastValueFrom(fetch$).then((records) => ({ records })); }; diff --git a/src/plugins/discover/public/application/main/utils/fetch_sql.ts b/src/plugins/discover/public/application/main/utils/fetch_sql.ts index b61d5334e4453..60a0831e0d6c9 100644 --- a/src/plugins/discover/public/application/main/utils/fetch_sql.ts +++ b/src/plugins/discover/public/application/main/utils/fetch_sql.ts @@ -14,7 +14,7 @@ import type { ExpressionsStart } from '@kbn/expressions-plugin/public'; import type { Datatable } from '@kbn/expressions-plugin/public'; import type { DataView } from '@kbn/data-views-plugin/common'; import { textBasedQueryStateToAstWithValidation } from '@kbn/data-plugin/common'; -import { DataTableRecord } from '../../../types'; +import type { RecordsFetchResponse, DataTableRecord } from '../../../types'; interface SQLErrorResponse { error: { @@ -31,7 +31,7 @@ export function fetchSql( inspectorAdapters: Adapters, filters?: Filter[], inputQuery?: Query -) { +): Promise { const timeRange = data.query.timefilter.timefilter.getTime(); return textBasedQueryStateToAstWithValidation({ filters, @@ -46,6 +46,7 @@ export function fetchSql( inspectorAdapters, }); let finalData: DataTableRecord[] = []; + let textBasedQueryColumns: Datatable['columns'] | undefined; let error: string | undefined; execution.pipe(pluck('result')).subscribe((resp) => { const response = resp as Datatable | SQLErrorResponse; @@ -54,6 +55,7 @@ export function fetchSql( } else { const table = response as Datatable; const rows = table?.rows ?? []; + textBasedQueryColumns = table?.columns ?? undefined; finalData = rows.map( (row: Record, idx: number) => ({ @@ -68,11 +70,17 @@ export function fetchSql( if (error) { throw new Error(error); } else { - return finalData || []; + return { + records: finalData || [], + textBasedQueryColumns, + }; } }); } - return []; + return { + records: [] as DataTableRecord[], + textBasedQueryColumns: [], + }; }) .catch((err) => { throw new Error(err.message); diff --git a/src/plugins/discover/public/components/field_name/__snapshots__/field_name.test.tsx.snap b/src/plugins/discover/public/components/field_name/__snapshots__/field_name.test.tsx.snap index 21777f772c9a5..e7cf7eddd0b94 100644 --- a/src/plugins/discover/public/components/field_name/__snapshots__/field_name.test.tsx.snap +++ b/src/plugins/discover/public/components/field_name/__snapshots__/field_name.test.tsx.snap @@ -10,9 +10,9 @@ Array [ > - Geo point field + Geo point
  • , @@ -44,9 +44,9 @@ Array [ > - Number field + Number , @@ -78,9 +78,9 @@ Array [ > - String field + String , @@ -146,9 +146,9 @@ Array [ > - Number field + Number , @@ -180,9 +180,9 @@ Array [ > - Number field + Number , diff --git a/src/plugins/discover/public/components/field_name/field_name.tsx b/src/plugins/discover/public/components/field_name/field_name.tsx index ca386d344e42f..488e412eea107 100644 --- a/src/plugins/discover/public/components/field_name/field_name.tsx +++ b/src/plugins/discover/public/components/field_name/field_name.tsx @@ -12,9 +12,8 @@ import { EuiBadge, EuiFlexGroup, EuiFlexItem, EuiToolTip, EuiHighlight } from '@ import { FormattedMessage } from '@kbn/i18n-react'; import { i18n } from '@kbn/i18n'; import { FieldIcon, FieldIconProps } from '@kbn/react-field'; -import { getFieldSubtypeMulti } from '@kbn/data-views-plugin/public'; -import type { DataViewField } from '@kbn/data-views-plugin/public'; -import { getFieldTypeName } from '../../utils/get_field_type_name'; +import { type DataViewField, getFieldSubtypeMulti } from '@kbn/data-views-plugin/public'; +import { getFieldTypeName } from '@kbn/unified-field-list-plugin/public'; interface Props { fieldName: string; diff --git a/src/plugins/discover/public/embeddable/saved_search_embeddable.tsx b/src/plugins/discover/public/embeddable/saved_search_embeddable.tsx index 69a8f4115a45e..a6d8f6cf3edaf 100644 --- a/src/plugins/discover/public/embeddable/saved_search_embeddable.tsx +++ b/src/plugins/discover/public/embeddable/saved_search_embeddable.tsx @@ -240,8 +240,8 @@ export class SavedSearchEmbeddable loading: false, }); - this.searchProps!.rows = result; - this.searchProps!.totalHitCount = result.length; + this.searchProps!.rows = result.records; + this.searchProps!.totalHitCount = result.records.length; this.searchProps!.isLoading = false; this.searchProps!.isPlainRecord = true; this.searchProps!.showTimeCol = false; diff --git a/src/plugins/discover/public/services/doc_views/components/doc_viewer_table/legacy/table.tsx b/src/plugins/discover/public/services/doc_views/components/doc_viewer_table/legacy/table.tsx index 91e3bbc73fdf1..1a63ccf69477e 100644 --- a/src/plugins/discover/public/services/doc_views/components/doc_viewer_table/legacy/table.tsx +++ b/src/plugins/discover/public/services/doc_views/components/doc_viewer_table/legacy/table.tsx @@ -9,7 +9,7 @@ import '../table.scss'; import React, { useCallback, useMemo } from 'react'; import { EuiInMemoryTable } from '@elastic/eui'; -import { getTypeForFieldIcon } from '../../../../../utils/get_type_for_field_icon'; +import { getFieldIconType } from '@kbn/unified-field-list-plugin/public'; import { useDiscoverServices } from '../../../../../hooks/use_discover_services'; import { SHOW_MULTIFIELDS } from '../../../../../../common'; import { DocViewRenderProps, FieldRecordLegacy } from '../../../doc_views_types'; @@ -76,7 +76,7 @@ export const DocViewerLegacyTable = ({ const fieldType = isNestedFieldParent(field, dataView) ? 'nested' : fieldMapping - ? getTypeForFieldIcon(fieldMapping) + ? getFieldIconType(fieldMapping) : undefined; const ignored = getIgnoredReason(fieldMapping ?? field, hit.raw._ignored); return { diff --git a/src/plugins/discover/public/services/doc_views/components/doc_viewer_table/table.tsx b/src/plugins/discover/public/services/doc_views/components/doc_viewer_table/table.tsx index 0eab3a68c8218..56232a31f9a0f 100644 --- a/src/plugins/discover/public/services/doc_views/components/doc_viewer_table/table.tsx +++ b/src/plugins/discover/public/services/doc_views/components/doc_viewer_table/table.tsx @@ -29,7 +29,7 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; import { debounce } from 'lodash'; import { Storage } from '@kbn/kibana-utils-plugin/public'; -import { getTypeForFieldIcon } from '../../../../utils/get_type_for_field_icon'; +import { getFieldIconType } from '@kbn/unified-field-list-plugin/public'; import { useDiscoverServices } from '../../../../hooks/use_discover_services'; import { usePager } from '../../../../hooks/use_pager'; import { FieldName } from '../../../../components/field_name/field_name'; @@ -166,7 +166,7 @@ export const DocViewerTable = ({ const fieldType = isNestedFieldParent(field, dataView) ? 'nested' : fieldMapping - ? getTypeForFieldIcon(fieldMapping) + ? getFieldIconType(fieldMapping) : undefined; const ignored = getIgnoredReason(fieldMapping ?? field, hit.raw._ignored); diff --git a/src/plugins/discover/public/types.ts b/src/plugins/discover/public/types.ts index f96edccb5f9bf..2419f15b8a429 100644 --- a/src/plugins/discover/public/types.ts +++ b/src/plugins/discover/public/types.ts @@ -7,6 +7,7 @@ */ import * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import { type DatatableColumn } from '@kbn/expressions-plugin/common'; export type ValueToStringConverter = ( rowIndex: number, @@ -38,3 +39,8 @@ export interface DataTableRecord { */ isAnchor?: boolean; } + +export interface RecordsFetchResponse { + records: DataTableRecord[]; + textBasedQueryColumns?: DatatableColumn[]; +} diff --git a/src/plugins/discover/public/utils/get_field_type_name.test.ts b/src/plugins/discover/public/utils/get_field_type_name.test.ts deleted file mode 100644 index bada07e8ad9f7..0000000000000 --- a/src/plugins/discover/public/utils/get_field_type_name.test.ts +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { getFieldTypeName, UNKNOWN_FIELD_TYPE_MESSAGE } from './get_field_type_name'; -import { KNOWN_FIELD_TYPES } from '../../common/field_types'; - -describe('getFieldTypeName', () => { - describe('known field types should be recognized', () => { - it.each(Object.values(KNOWN_FIELD_TYPES))( - `'%s' should return a string that does not match '${UNKNOWN_FIELD_TYPE_MESSAGE}'`, - (field) => { - const fieldTypeName = getFieldTypeName(field); - expect(typeof fieldTypeName).toBe('string'); - expect(fieldTypeName).not.toBe(UNKNOWN_FIELD_TYPE_MESSAGE); - } - ); - }); - - it(`should return '${UNKNOWN_FIELD_TYPE_MESSAGE}' when passed undefined`, () => { - expect(getFieldTypeName(undefined)).toBe(UNKNOWN_FIELD_TYPE_MESSAGE); - }); - - it(`should return '${UNKNOWN_FIELD_TYPE_MESSAGE}' when passed 'unknown'`, () => { - expect(getFieldTypeName('unknown')).toBe(UNKNOWN_FIELD_TYPE_MESSAGE); - }); - - it('should return the original type string back when passed an unknown field type', () => { - expect(getFieldTypeName('unknown_field_type')).toBe('unknown_field_type'); - }); -}); diff --git a/src/plugins/discover/public/utils/get_field_type_name.ts b/src/plugins/discover/public/utils/get_field_type_name.ts deleted file mode 100644 index 81a4346f63902..0000000000000 --- a/src/plugins/discover/public/utils/get_field_type_name.ts +++ /dev/null @@ -1,105 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { i18n } from '@kbn/i18n'; -import { KBN_FIELD_TYPES } from '@kbn/data-plugin/public'; -import { KNOWN_FIELD_TYPES } from '../../common/field_types'; - -export const UNKNOWN_FIELD_TYPE_MESSAGE = i18n.translate( - 'discover.fieldNameIcons.unknownFieldAriaLabel', - { - defaultMessage: 'Unknown field', - } -); - -export function getFieldTypeName(type?: string) { - if (!type || type === KBN_FIELD_TYPES.UNKNOWN) { - return UNKNOWN_FIELD_TYPE_MESSAGE; - } - - if (type === 'source') { - // TODO: check if we can remove this logic as outdated - - // Note that this type is currently not provided, type for _source is undefined - return i18n.translate('discover.fieldNameIcons.sourceFieldAriaLabel', { - defaultMessage: 'Source field', - }); - } - - const knownType: KNOWN_FIELD_TYPES = type as KNOWN_FIELD_TYPES; - switch (knownType) { - case KNOWN_FIELD_TYPES.BOOLEAN: - return i18n.translate('discover.fieldNameIcons.booleanAriaLabel', { - defaultMessage: 'Boolean field', - }); - case KNOWN_FIELD_TYPES.CONFLICT: - return i18n.translate('discover.fieldNameIcons.conflictFieldAriaLabel', { - defaultMessage: 'Conflicting field', - }); - case KNOWN_FIELD_TYPES.DATE: - return i18n.translate('discover.fieldNameIcons.dateFieldAriaLabel', { - defaultMessage: 'Date field', - }); - case KNOWN_FIELD_TYPES.DATE_RANGE: - return i18n.translate('discover.fieldNameIcons.dateRangeFieldAriaLabel', { - defaultMessage: 'Date range field', - }); - case KNOWN_FIELD_TYPES.GEO_POINT: - return i18n.translate('discover.fieldNameIcons.geoPointFieldAriaLabel', { - defaultMessage: 'Geo point field', - }); - case KNOWN_FIELD_TYPES.GEO_SHAPE: - return i18n.translate('discover.fieldNameIcons.geoShapeFieldAriaLabel', { - defaultMessage: 'Geo shape field', - }); - case KNOWN_FIELD_TYPES.HISTOGRAM: - return i18n.translate('discover.fieldNameIcons.histogramFieldAriaLabel', { - defaultMessage: 'Histogram field', - }); - case KNOWN_FIELD_TYPES.IP: - return i18n.translate('discover.fieldNameIcons.ipAddressFieldAriaLabel', { - defaultMessage: 'IP address field', - }); - case KNOWN_FIELD_TYPES.IP_RANGE: - return i18n.translate('discover.fieldNameIcons.ipRangeFieldAriaLabel', { - defaultMessage: 'IP range field', - }); - case KNOWN_FIELD_TYPES.MURMUR3: - return i18n.translate('discover.fieldNameIcons.murmur3FieldAriaLabel', { - defaultMessage: 'Murmur3 field', - }); - case KNOWN_FIELD_TYPES.NUMBER: - return i18n.translate('discover.fieldNameIcons.numberFieldAriaLabel', { - defaultMessage: 'Number field', - }); - case KNOWN_FIELD_TYPES.STRING: - return i18n.translate('discover.fieldNameIcons.stringFieldAriaLabel', { - defaultMessage: 'String field', - }); - case KNOWN_FIELD_TYPES.TEXT: - return i18n.translate('discover.fieldNameIcons.textFieldAriaLabel', { - defaultMessage: 'Text field', - }); - case KNOWN_FIELD_TYPES.KEYWORD: - return i18n.translate('discover.fieldNameIcons.keywordFieldAriaLabel', { - defaultMessage: 'Keyword field', - }); - case KNOWN_FIELD_TYPES.NESTED: - return i18n.translate('discover.fieldNameIcons.nestedFieldAriaLabel', { - defaultMessage: 'Nested field', - }); - case KNOWN_FIELD_TYPES.VERSION: - return i18n.translate('discover.fieldNameIcons.versionFieldAriaLabel', { - defaultMessage: 'Version field', - }); - default: - // If you see a typescript error here, that's a sign that there are missing switch cases ^^ - const _exhaustiveCheck: never = knownType; - return knownType || _exhaustiveCheck; - } -} diff --git a/src/plugins/discover/public/utils/get_type_for_field_icon.test.ts b/src/plugins/discover/public/utils/get_type_for_field_icon.test.ts deleted file mode 100644 index 33decc463d013..0000000000000 --- a/src/plugins/discover/public/utils/get_type_for_field_icon.test.ts +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { DataViewField } from '@kbn/data-views-plugin/common'; -import { getTypeForFieldIcon } from './get_type_for_field_icon'; - -describe('getTypeForFieldIcon', () => { - it('extracts type for non-string types', () => { - expect( - getTypeForFieldIcon({ - type: 'not-string', - esTypes: ['bar'], - } as DataViewField) - ).toBe('not-string'); - }); - - it('extracts type when type is string but esTypes is unavailable', () => { - expect( - getTypeForFieldIcon({ - type: 'string', - esTypes: undefined, - } as DataViewField) - ).toBe('string'); - }); - - it('extracts esType when type is string and esTypes is available', () => { - expect( - getTypeForFieldIcon({ - type: 'string', - esTypes: ['version'], - } as DataViewField) - ).toBe('version'); - }); - - it('extracts type for meta fields', () => { - expect( - getTypeForFieldIcon({ - type: 'string', - esTypes: ['_id'], - } as DataViewField) - ).toBe('string'); - }); -}); diff --git a/src/plugins/discover/public/utils/get_type_for_field_icon.ts b/src/plugins/discover/public/utils/get_type_for_field_icon.ts deleted file mode 100644 index 3d05e8365e59c..0000000000000 --- a/src/plugins/discover/public/utils/get_type_for_field_icon.ts +++ /dev/null @@ -1,22 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { DataViewField } from '@kbn/data-views-plugin/common'; - -/** - * Extracts the type from a data view field that will match the right icon. - * - * We define custom logic for Discover in order to distinguish between various "string" types. - */ -export const getTypeForFieldIcon = (field: DataViewField) => { - const esType = field.esTypes?.[0] || null; - if (esType && ['_id', '_index'].includes(esType)) { - return field.type; - } - return field.type === 'string' && esType ? esType : field.type; -}; diff --git a/src/plugins/unified_field_list/public/components/field_icon/__snapshots__/field_icon.test.tsx.snap b/src/plugins/unified_field_list/public/components/field_icon/__snapshots__/field_icon.test.tsx.snap index 89bde8769a0c7..66a9236679abc 100644 --- a/src/plugins/unified_field_list/public/components/field_icon/__snapshots__/field_icon.test.tsx.snap +++ b/src/plugins/unified_field_list/public/components/field_icon/__snapshots__/field_icon.test.tsx.snap @@ -16,13 +16,6 @@ exports[`UnifiedFieldList renders Document type properly 1`] = ` /> `; -exports[`UnifiedFieldList renders Histogram type properly 1`] = ` - -`; - exports[`UnifiedFieldList renders properly 1`] = ` ', () => { const component = shallow(); expect(component).toMatchSnapshot(); }); - - test('renders Histogram type properly', () => { - const component = shallow(); - expect(component).toMatchSnapshot(); - }); }); diff --git a/src/plugins/unified_field_list/public/components/field_icon/field_icon.tsx b/src/plugins/unified_field_list/public/components/field_icon/field_icon.tsx index 1485ebe1c8d02..070716911706c 100644 --- a/src/plugins/unified_field_list/public/components/field_icon/field_icon.tsx +++ b/src/plugins/unified_field_list/public/components/field_icon/field_icon.tsx @@ -24,8 +24,5 @@ const FieldIcon = React.memo(InnerFieldIcon) as GenericFieldIcon; export default FieldIcon; function normalizeFieldType(type: string) { - if (type === 'histogram') { - return 'number'; - } return type === 'document' ? 'number' : type; } diff --git a/src/plugins/unified_field_list/public/components/field_list_filters/field_type_filter.test.tsx b/src/plugins/unified_field_list/public/components/field_list_filters/field_type_filter.test.tsx index 01bc1c4147f15..7a9701f31a9d1 100644 --- a/src/plugins/unified_field_list/public/components/field_list_filters/field_type_filter.test.tsx +++ b/src/plugins/unified_field_list/public/components/field_list_filters/field_type_filter.test.tsx @@ -59,7 +59,7 @@ describe('UnifiedFieldList ', () => { await openPopover(wrapper, props); - expect(wrapper.find(EuiContextMenuItem)?.length).toBe(11); + expect(wrapper.find(EuiContextMenuItem)?.length).toBe(10); expect( wrapper .find(EuiContextMenuItem) @@ -67,7 +67,7 @@ describe('UnifiedFieldList ', () => { .join(', ') ).toBe( // format:type_icon type_name help_icon count - 'BooleanBooleanInfo1, ConflictConflictInfo1, DateDateInfo4, Geo pointGeo pointInfo2, Geo shapeGeo shapeInfo1, IP addressIP addressInfo1, KeywordKeywordInfo4, Murmur3Murmur3Info2, NumberNumberInfo3, StringStringInfo1, TextTextInfo5' + 'BooleanBooleanInfo1, ConflictConflictInfo1, DateDateInfo4, Geo pointGeo pointInfo2, Geo shapeGeo shapeInfo1, IP addressIP addressInfo1, KeywordKeywordInfo5, Murmur3Murmur3Info2, NumberNumberInfo3, TextTextInfo5' ); expect(props.getCustomFieldType).toHaveBeenCalledTimes(props.allFields?.length ?? 0); expect(props.onChange).not.toBeCalled(); @@ -111,7 +111,7 @@ describe('UnifiedFieldList ', () => { await openPopover(wrapper, props); const clearAllButton = findClearAllButton(wrapper, props)?.first(); - expect(wrapper.find(EuiContextMenuItem)?.length).toBe(11); + expect(wrapper.find(EuiContextMenuItem)?.length).toBe(10); expect(clearAllButton?.length).toBe(1); expect( wrapper @@ -120,7 +120,7 @@ describe('UnifiedFieldList ', () => { .join(', ') ).toBe( // format:selection_icon type_icon type_name help_icon count - 'empty-BooleanBooleanInfo1, empty-ConflictConflictInfo1, check-DateDateInfo4, empty-Geo pointGeo pointInfo2, empty-Geo shapeGeo shapeInfo1, empty-IP addressIP addressInfo1, empty-KeywordKeywordInfo4, empty-Murmur3Murmur3Info2, check-NumberNumberInfo3, empty-StringStringInfo1, empty-TextTextInfo5' + 'empty-BooleanBooleanInfo1, empty-ConflictConflictInfo1, check-DateDateInfo4, empty-Geo pointGeo pointInfo2, empty-Geo shapeGeo shapeInfo1, empty-IP addressIP addressInfo1, empty-KeywordKeywordInfo5, empty-Murmur3Murmur3Info2, check-NumberNumberInfo3, empty-TextTextInfo5' ); await toggleType(wrapper, 'boolean'); diff --git a/src/plugins/unified_field_list/public/components/field_list_grouped/field_list_grouped.test.tsx b/src/plugins/unified_field_list/public/components/field_list_grouped/field_list_grouped.test.tsx index 9190c6de2859e..7cabd28bc171d 100644 --- a/src/plugins/unified_field_list/public/components/field_list_grouped/field_list_grouped.test.tsx +++ b/src/plugins/unified_field_list/public/components/field_list_grouped/field_list_grouped.test.tsx @@ -16,6 +16,7 @@ import { ReactWrapper } from 'enzyme'; import { dataViewPluginMocks } from '@kbn/data-views-plugin/public/mocks'; import { coreMock } from '@kbn/core/public/mocks'; import FieldListGrouped, { type FieldListGroupedProps } from './field_list_grouped'; +import { FieldListFilters } from '../field_list_filters'; import { ExistenceFetchStatus } from '../../types'; import { FieldsAccordion } from './fields_accordion'; import { NoFieldsCallout } from './no_fields_callout'; @@ -68,13 +69,19 @@ describe('UnifiedFieldList + useGroupedFields()', () => { async function mountGroupedList({ listProps, hookParams }: WrapperProps): Promise { const Wrapper: React.FC = (props) => { const { + fieldListFiltersProps, fieldListGroupedProps: { fieldGroups }, } = useGroupedFields({ ...props.hookParams, services: mockedServices, }); - return ; + return ( + <> + + + + ); }; let wrapper: ReactWrapper; @@ -301,7 +308,7 @@ describe('UnifiedFieldList + useGroupedFields()', () => { ).toStrictEqual([25, 88, 0, 0]); }); - it('renders correctly when filtered', async () => { + it('renders correctly when fields are searched and filtered', async () => { const hookParams = { dataViewId: dataView.id!, allFields: manyFields, @@ -319,12 +326,12 @@ describe('UnifiedFieldList + useGroupedFields()', () => { ); await act(async () => { - await wrapper.setProps({ - hookParams: { - ...hookParams, - onFilterField: (field: DataViewField) => field.name.startsWith('@'), - }, - }); + await wrapper + .find('[data-test-subj="fieldListFiltersFieldSearch"]') + .last() + .simulate('change', { + target: { value: '@' }, + }); await wrapper.update(); }); @@ -333,17 +340,34 @@ describe('UnifiedFieldList + useGroupedFields()', () => { ); await act(async () => { - await wrapper.setProps({ - hookParams: { - ...hookParams, - onFilterField: (field: DataViewField) => field.name.startsWith('_'), - }, - }); + await wrapper + .find('[data-test-subj="fieldListFiltersFieldSearch"]') + .last() + .simulate('change', { + target: { value: '_' }, + }); + await wrapper.update(); + }); + + expect(wrapper.find(`#${defaultProps.screenReaderDescriptionId}`).first().text()).toBe( + '3 available fields. 24 unmapped fields. 0 empty fields. 3 meta fields.' + ); + + await act(async () => { + await wrapper + .find('[data-test-subj="fieldListFiltersFieldTypeFilterToggle"]') + .last() + .simulate('click'); + await wrapper.update(); + }); + + await act(async () => { + await wrapper.find('[data-test-subj="typeFilter-date"]').first().simulate('click'); await wrapper.update(); }); expect(wrapper.find(`#${defaultProps.screenReaderDescriptionId}`).first().text()).toBe( - '0 available fields. 12 unmapped fields. 0 empty fields. 3 meta fields.' + '1 available field. 4 unmapped fields. 0 empty fields. 0 meta fields.' ); }); diff --git a/src/plugins/unified_field_list/public/hooks/use_grouped_fields.test.tsx b/src/plugins/unified_field_list/public/hooks/use_grouped_fields.test.tsx index 5fa7344955b52..053e7d912d375 100644 --- a/src/plugins/unified_field_list/public/hooks/use_grouped_fields.test.tsx +++ b/src/plugins/unified_field_list/public/hooks/use_grouped_fields.test.tsx @@ -6,8 +6,7 @@ * Side Public License, v 1. */ -import { renderHook } from '@testing-library/react-hooks'; -import { act } from 'react-test-renderer'; +import { renderHook, act } from '@testing-library/react-hooks'; import { stubDataViewWithoutTimeField, stubLogstashDataView as dataView, @@ -168,6 +167,7 @@ describe('UnifiedFieldList useGroupedFields()', () => { let fieldListGroupedProps = result.current.fieldListGroupedProps; const fieldGroups = fieldListGroupedProps.fieldGroups; + const scrollToTopResetCounter1 = fieldListGroupedProps.scrollToTopResetCounter; expect( Object.keys(fieldGroups!).map( @@ -195,25 +195,26 @@ describe('UnifiedFieldList useGroupedFields()', () => { fieldListGroupedProps = result.current.fieldListGroupedProps; expect(fieldListGroupedProps.fieldsExistenceStatus).toBe(ExistenceFetchStatus.succeeded); expect(fieldListGroupedProps.fieldsExistInIndex).toBe(true); + expect(result.current.fieldListGroupedProps.scrollToTopResetCounter).not.toBe( + scrollToTopResetCounter1 + ); (ExistenceApi.useExistingFieldsReader as jest.Mock).mockRestore(); }); - it('should work correctly when filtered', async () => { + it('should work correctly when searched and filtered', async () => { const props: GroupedFieldsParams = { dataViewId: dataView.id!, allFields: allFieldsIncludingUnmapped, services: mockedServices, }; - const { result, waitForNextUpdate, rerender } = renderHook(useGroupedFields, { + const { result, waitForNextUpdate } = renderHook(useGroupedFields, { initialProps: props, }); await waitForNextUpdate(); - const fieldListGroupedProps = result.current.fieldListGroupedProps; - let fieldGroups = fieldListGroupedProps.fieldGroups; - const scrollToTopResetCounter1 = fieldListGroupedProps.scrollToTopResetCounter; + let fieldGroups = result.current.fieldListGroupedProps.fieldGroups; expect( Object.keys(fieldGroups!).map( @@ -232,9 +233,8 @@ describe('UnifiedFieldList useGroupedFields()', () => { 'MetaFields-3-3', ]); - rerender({ - ...props, - onFilterField: (field: DataViewField) => field.name.startsWith('@'), + act(() => { + result.current.fieldListFiltersProps.onChangeNameFilter('@'); }); fieldGroups = result.current.fieldListGroupedProps.fieldGroups; @@ -256,9 +256,28 @@ describe('UnifiedFieldList useGroupedFields()', () => { 'MetaFields-0-3', ]); - expect(result.current.fieldListGroupedProps.scrollToTopResetCounter).not.toBe( - scrollToTopResetCounter1 - ); + act(() => { + result.current.fieldListFiltersProps.onChangeFieldTypes(['date']); + }); + + fieldGroups = result.current.fieldListGroupedProps.fieldGroups; + + expect( + Object.keys(fieldGroups!).map( + (key) => + `${key}-${fieldGroups![key as FieldsGroupNames]?.fields.length}-${ + fieldGroups![key as FieldsGroupNames]?.fieldCount + }` + ) + ).toStrictEqual([ + 'SpecialFields-0-0', + 'SelectedFields-0-0', + 'PopularFields-0-0', + 'AvailableFields-1-25', + 'UnmappedFields-1-28', + 'EmptyFields-0-0', + 'MetaFields-0-3', + ]); }); it('should not change the scroll position if fields list is extended', async () => { diff --git a/src/plugins/unified_field_list/public/hooks/use_grouped_fields.ts b/src/plugins/unified_field_list/public/hooks/use_grouped_fields.ts index 9ac24aaa86063..1209ed64f1eb5 100644 --- a/src/plugins/unified_field_list/public/hooks/use_grouped_fields.ts +++ b/src/plugins/unified_field_list/public/hooks/use_grouped_fields.ts @@ -43,7 +43,6 @@ export interface GroupedFieldsParams { ) => Partial | undefined | null; onSupportedFieldFilter?: (field: T) => boolean; onSelectedFieldFilter?: (field: T) => boolean; - onFilterField?: (field: T) => boolean; // TODO: deprecate after integrating the unified field search and field filters into Discover } export interface GroupedFieldsResult { @@ -68,7 +67,6 @@ export function useGroupedFields({ onOverrideFieldGroupDetails, onSupportedFieldFilter, onSelectedFieldFilter, - onFilterField, }: GroupedFieldsParams): GroupedFieldsResult { const fieldsExistenceReader = useExistingFieldsReader(); const fieldListFilters = useFieldFilters({ @@ -77,7 +75,7 @@ export function useGroupedFields({ getCustomFieldType, onSupportedFieldFilter, }); - const onFilterFieldList = onFilterField ?? fieldListFilters.onFilterField; + const onFilterFieldList = fieldListFilters.onFilterField; const [dataView, setDataView] = useState(null); const isAffectedByTimeFilter = Boolean(dataView?.timeFieldName); const fieldsExistenceInfoUnavailable: boolean = dataViewId @@ -132,6 +130,10 @@ export function useGroupedFields({ if (dataView?.metaFields?.includes(field.name)) { return 'metaFields'; } + // `nested` root fields are not a part of data view fields list, so we need to check them separately + if (field.type === 'nested') { + return 'availableFields'; + } if (dataView?.getFieldByName && !dataView.getFieldByName(field.name)) { return 'unmappedFields'; } diff --git a/src/plugins/unified_field_list/public/utils/field_types/get_field_icon_type.test.ts b/src/plugins/unified_field_list/public/utils/field_types/get_field_icon_type.test.ts index 82da142c03d3b..70f54ba84685e 100644 --- a/src/plugins/unified_field_list/public/utils/field_types/get_field_icon_type.test.ts +++ b/src/plugins/unified_field_list/public/utils/field_types/get_field_icon_type.test.ts @@ -43,6 +43,6 @@ describe('UnifiedFieldList getFieldIconType()', () => { type: 'string', esTypes: ['_id'], } as DataViewField) - ).toBe('string'); + ).toBe('keyword'); }); }); diff --git a/src/plugins/unified_field_list/public/utils/field_types/get_field_icon_type.ts b/src/plugins/unified_field_list/public/utils/field_types/get_field_icon_type.ts index ef843012e0c48..e548d4bf1744b 100644 --- a/src/plugins/unified_field_list/public/utils/field_types/get_field_icon_type.ts +++ b/src/plugins/unified_field_list/public/utils/field_types/get_field_icon_type.ts @@ -22,8 +22,8 @@ export function getFieldIconType( ): string { const type = getCustomFieldType ? getCustomFieldType(field) : getFieldType(field); const esType = field.esTypes?.[0] || null; - if (esType && ['_id', '_index'].includes(esType)) { - return type; + if (esType && ['_id', '_index'].includes(esType) && type === 'string') { + return 'keyword'; } return type === 'string' && esType ? esType : type; } diff --git a/test/functional/apps/discover/group1/_sidebar.ts b/test/functional/apps/discover/group1/_sidebar.ts index a62a379c20224..9d0878b107073 100644 --- a/test/functional/apps/discover/group1/_sidebar.ts +++ b/test/functional/apps/discover/group1/_sidebar.ts @@ -25,6 +25,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const monacoEditor = getService('monacoEditor'); const filterBar = getService('filterBar'); const fieldEditor = getService('fieldEditor'); + const retry = getService('retry'); + const INITIAL_FIELD_LIST_SUMMARY = '53 available fields. 0 empty fields. 3 meta fields.'; describe('discover sidebar', function describeIndexTests() { before(async function () { @@ -53,6 +55,109 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.discover.openSidebarFieldFilter(); await PageObjects.discover.closeSidebarFieldFilter(); }); + + it('should filter by field type', async function () { + await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.discover.waitUntilSidebarHasLoaded(); + await PageObjects.discover.openSidebarFieldFilter(); + + expect(await PageObjects.discover.getSidebarAriaDescription()).to.be( + INITIAL_FIELD_LIST_SUMMARY + ); + + await testSubjects.click('typeFilter-keyword'); + + await retry.waitFor('first updates', async () => { + return ( + (await PageObjects.discover.getSidebarAriaDescription()) === + '7 available fields. 0 empty fields. 2 meta fields.' + ); + }); + + await testSubjects.click('typeFilter-number'); + + await retry.waitFor('second updates', async () => { + return ( + (await PageObjects.discover.getSidebarAriaDescription()) === + '13 available fields. 0 empty fields. 3 meta fields.' + ); + }); + + await testSubjects.click('fieldListFiltersFieldTypeFilterClearAll'); + + await retry.waitFor('reset', async () => { + return ( + (await PageObjects.discover.getSidebarAriaDescription()) === INITIAL_FIELD_LIST_SUMMARY + ); + }); + }); + + it('should show filters by type in text-based view', async function () { + await kibanaServer.uiSettings.update({ 'discover:enableSql': true }); + await browser.refresh(); + + await PageObjects.discover.waitUntilSidebarHasLoaded(); + await PageObjects.discover.openSidebarFieldFilter(); + let options = await find.allByCssSelector('[data-test-subj*="typeFilter"]'); + expect(options).to.have.length(6); + await PageObjects.discover.closeSidebarFieldFilter(); + + await PageObjects.discover.selectTextBaseLang('SQL'); + + await PageObjects.discover.waitUntilSidebarHasLoaded(); + await PageObjects.discover.openSidebarFieldFilter(); + options = await find.allByCssSelector('[data-test-subj*="typeFilter"]'); + expect(options).to.have.length(3); + + expect(await PageObjects.discover.getSidebarAriaDescription()).to.be( + '50 selected fields. 51 available fields.' + ); + + await testSubjects.click('typeFilter-number'); + + await retry.waitFor('updates', async () => { + return ( + (await PageObjects.discover.getSidebarAriaDescription()) === + '6 selected fields. 6 available fields.' + ); + }); + }); + + it('should be able to search by string', async function () { + await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.discover.waitUntilSidebarHasLoaded(); + + expect(await PageObjects.discover.getSidebarAriaDescription()).to.be( + INITIAL_FIELD_LIST_SUMMARY + ); + + await PageObjects.discover.findFieldByName('i'); + + await retry.waitFor('first updates', async () => { + return ( + (await PageObjects.discover.getSidebarAriaDescription()) === + '30 available fields. 0 empty fields. 2 meta fields.' + ); + }); + + await PageObjects.discover.findFieldByName('p'); + + await retry.waitFor('second updates', async () => { + return ( + (await PageObjects.discover.getSidebarAriaDescription()) === + '4 available fields. 0 empty fields. 0 meta fields.' + ); + }); + + const fieldSearch = await testSubjects.find('clearSearchButton'); + await fieldSearch.click(); + + await retry.waitFor('reset', async () => { + return ( + (await PageObjects.discover.getSidebarAriaDescription()) === INITIAL_FIELD_LIST_SUMMARY + ); + }); + }); }); describe('field stats', function () { @@ -154,7 +259,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { ); expect(await PageObjects.discover.getSidebarAriaDescription()).to.be( - '53 available fields. 0 empty fields. 3 meta fields.' + INITIAL_FIELD_LIST_SUMMARY ); }); @@ -253,7 +358,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.discover.waitUntilSidebarHasLoaded(); expect(await PageObjects.discover.getSidebarAriaDescription()).to.be( - '53 available fields. 0 empty fields. 3 meta fields.' + INITIAL_FIELD_LIST_SUMMARY ); await PageObjects.discover.selectTextBaseLang('SQL'); @@ -309,7 +414,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.discover.waitUntilSidebarHasLoaded(); expect(await PageObjects.discover.getSidebarAriaDescription()).to.be( - '53 available fields. 0 empty fields. 3 meta fields.' + INITIAL_FIELD_LIST_SUMMARY ); await PageObjects.discover.selectIndexPattern('with-timefield'); @@ -335,7 +440,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.discover.waitUntilSidebarHasLoaded(); expect(await PageObjects.discover.getSidebarAriaDescription()).to.be( - '53 available fields. 0 empty fields. 3 meta fields.' + INITIAL_FIELD_LIST_SUMMARY ); await kibanaServer.importExport.unload( 'test/functional/fixtures/kbn_archiver/index_pattern_without_timefield' @@ -354,7 +459,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.discover.waitUntilSidebarHasLoaded(); expect(await PageObjects.discover.getSidebarAriaDescription()).to.be( - '53 available fields. 0 empty fields. 3 meta fields.' + INITIAL_FIELD_LIST_SUMMARY ); await PageObjects.discover.selectIndexPattern('without-timefield'); @@ -386,7 +491,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.discover.waitUntilSidebarHasLoaded(); expect(await PageObjects.discover.getSidebarAriaDescription()).to.be( - '53 available fields. 0 empty fields. 3 meta fields.' + INITIAL_FIELD_LIST_SUMMARY ); await kibanaServer.importExport.unload( @@ -402,7 +507,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.header.waitUntilLoadingHasFinished(); expect(await PageObjects.discover.getSidebarAriaDescription()).to.be( - '53 available fields. 0 empty fields. 3 meta fields.' + INITIAL_FIELD_LIST_SUMMARY ); await PageObjects.discover.clickFieldListItem('extension'); @@ -415,7 +520,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.discover.waitUntilSidebarHasLoaded(); expect(await PageObjects.discover.getSidebarAriaDescription()).to.be( - '53 available fields. 0 empty fields. 3 meta fields.' + INITIAL_FIELD_LIST_SUMMARY ); // check that the filter was passed down to the sidebar @@ -433,7 +538,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.discover.waitUntilSidebarHasLoaded(); expect(await PageObjects.discover.getSidebarAriaDescription()).to.be( - '53 available fields. 0 empty fields. 3 meta fields.' + INITIAL_FIELD_LIST_SUMMARY ); await PageObjects.discover.selectIndexPattern('indices-stats*'); @@ -451,7 +556,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.discover.waitUntilSidebarHasLoaded(); expect(await PageObjects.discover.getSidebarAriaDescription()).to.be( - '53 available fields. 0 empty fields. 3 meta fields.' + INITIAL_FIELD_LIST_SUMMARY ); await kibanaServer.importExport.unload( @@ -465,7 +570,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.header.waitUntilLoadingHasFinished(); expect(await PageObjects.discover.getSidebarAriaDescription()).to.be( - '53 available fields. 0 empty fields. 3 meta fields.' + INITIAL_FIELD_LIST_SUMMARY ); await PageObjects.discover.addRuntimeField( @@ -503,7 +608,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.discover.waitUntilSidebarHasLoaded(); expect(await PageObjects.discover.getSidebarAriaDescription()).to.be( - '53 available fields. 0 empty fields. 3 meta fields.' + INITIAL_FIELD_LIST_SUMMARY ); allFields = await PageObjects.discover.getAllFieldNames(); @@ -517,7 +622,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await testSubjects.missingOrFail('discoverNoResultsError'); expect(await PageObjects.discover.getSidebarAriaDescription()).to.be( - '53 available fields. 0 empty fields. 3 meta fields.' + INITIAL_FIELD_LIST_SUMMARY ); await PageObjects.discover.addRuntimeField('_invalid-runtimefield', `emit(‘’);`); @@ -564,7 +669,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.discover.waitUntilSidebarHasLoaded(); expect(await PageObjects.discover.getSidebarAriaDescription()).to.be( - '53 available fields. 0 empty fields. 3 meta fields.' + INITIAL_FIELD_LIST_SUMMARY ); await PageObjects.discover.selectIndexPattern('with-timefield'); diff --git a/test/functional/apps/discover/group2/_sql_view.ts b/test/functional/apps/discover/group2/_sql_view.ts index 4ae63f0e6b1f1..1f567ec67ddff 100644 --- a/test/functional/apps/discover/group2/_sql_view.ts +++ b/test/functional/apps/discover/group2/_sql_view.ts @@ -39,6 +39,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { describe('test', () => { it('should render sql view correctly', async function () { + await PageObjects.discover.waitUntilSidebarHasLoaded(); + expect(await testSubjects.exists('showQueryBarMenu')).to.be(true); expect(await testSubjects.exists('superDatePickerToggleQuickMenuButton')).to.be(true); expect(await testSubjects.exists('addFilter')).to.be(true); @@ -49,15 +51,15 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { expect(await testSubjects.exists('shareTopNavButton')).to.be(true); expect(await testSubjects.exists('docTableExpandToggleColumn')).to.be(true); expect(await testSubjects.exists('dataGridColumnSortingButton')).to.be(true); - expect(await testSubjects.exists('fieldFilterSearchInput')).to.be(true); - expect(await testSubjects.exists('toggleFieldFilterButton')).to.be(true); - expect(await testSubjects.exists('fieldTypesHelpButton')).to.be(true); + expect(await testSubjects.exists('fieldListFiltersFieldSearch')).to.be(true); + expect(await testSubjects.exists('fieldListFiltersFieldTypeFilterToggle')).to.be(true); await testSubjects.click('field-@message-showDetails'); expect(await testSubjects.exists('discoverFieldListPanelEdit-@message')).to.be(true); await PageObjects.discover.selectTextBaseLang('SQL'); + await PageObjects.discover.waitUntilSidebarHasLoaded(); - expect(await testSubjects.exists('fieldFilterSearchInput')).to.be(true); + expect(await testSubjects.exists('fieldListFiltersFieldSearch')).to.be(true); expect(await testSubjects.exists('unifiedTextLangEditor')).to.be(true); expect(await testSubjects.exists('superDatePickerToggleQuickMenuButton')).to.be(true); @@ -70,8 +72,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { expect(await testSubjects.exists('shareTopNavButton')).to.be(false); expect(await testSubjects.exists('docTableExpandToggleColumn')).to.be(false); expect(await testSubjects.exists('dataGridColumnSortingButton')).to.be(false); - expect(await testSubjects.exists('toggleFieldFilterButton')).to.be(false); - expect(await testSubjects.exists('fieldTypesHelpButton')).to.be(false); + expect(await testSubjects.exists('fieldListFiltersFieldTypeFilterToggle')).to.be(true); await testSubjects.click('field-@message-showDetails'); expect(await testSubjects.exists('discoverFieldListPanelEditItem')).to.be(false); }); diff --git a/test/functional/page_objects/discover_page.ts b/test/functional/page_objects/discover_page.ts index 7db95f8063c12..41f4dd09d6c7e 100644 --- a/test/functional/page_objects/discover_page.ts +++ b/test/functional/page_objects/discover_page.ts @@ -46,12 +46,12 @@ export class DiscoverPageObject extends FtrService { } public async findFieldByName(name: string) { - const fieldSearch = await this.testSubjects.find('fieldFilterSearchInput'); + const fieldSearch = await this.testSubjects.find('fieldListFiltersFieldSearch'); await fieldSearch.type(name); } public async clearFieldSearchInput() { - const fieldSearch = await this.testSubjects.find('fieldFilterSearchInput'); + const fieldSearch = await this.testSubjects.find('fieldListFiltersFieldSearch'); await fieldSearch.clearValue(); } @@ -665,15 +665,15 @@ export class DiscoverPageObject extends FtrService { } public async openSidebarFieldFilter() { - await this.testSubjects.click('toggleFieldFilterButton'); - await this.testSubjects.existOrFail('filterSelectionPanel'); + await this.testSubjects.click('fieldListFiltersFieldTypeFilterToggle'); + await this.testSubjects.existOrFail('fieldListFiltersFieldTypeFilterOptions'); } public async closeSidebarFieldFilter() { - await this.testSubjects.click('toggleFieldFilterButton'); + await this.testSubjects.click('fieldListFiltersFieldTypeFilterToggle'); await this.retry.waitFor('sidebar filter closed', async () => { - return !(await this.testSubjects.exists('filterSelectionPanel')); + return !(await this.testSubjects.exists('fieldListFiltersFieldTypeFilterOptions')); }); } diff --git a/x-pack/plugins/lens/public/datasources/form_based/dimension_panel/dimension_editor.tsx b/x-pack/plugins/lens/public/datasources/form_based/dimension_panel/dimension_editor.tsx index f93ee60bae9e6..c51d909ba6766 100644 --- a/x-pack/plugins/lens/public/datasources/form_based/dimension_panel/dimension_editor.tsx +++ b/x-pack/plugins/lens/public/datasources/form_based/dimension_panel/dimension_editor.tsx @@ -397,7 +397,7 @@ export function DimensionEditor(props: DimensionEditorProps) { ); } else if (!compatibleWithCurrentField) { label = ( - + {label} @@ -652,7 +652,7 @@ export function DimensionEditor(props: DimensionEditorProps) { <> + {i18n.translate('xpack.lens.indexPattern.functionsLabel', { defaultMessage: 'Functions', @@ -665,10 +665,8 @@ export function DimensionEditor(props: DimensionEditorProps) { isOpen={isHelpOpen} display="inlineBlock" panelPaddingSize="none" - className="dscFieldTypesHelp__popover" - panelClassName="dscFieldTypesHelp__panel" closePopover={closeHelp} - initialFocus="#dscFieldTypesHelpBasicTableId" + initialFocus="#functionsHelpBasicTableId" > {i18n.translate('xpack.lens.indexPattern.quickFunctions.popoverTitle', { @@ -682,7 +680,7 @@ export function DimensionEditor(props: DimensionEditorProps) { paddingSize="s" > Vues de données.", - "discover.fieldNameDescription.dateField": "Chaîne de date ou nombre de secondes ou de millisecondes depuis 1/1/1970.", - "discover.fieldNameDescription.dateRangeFieldLinkText": "date", - "discover.fieldNameDescription.geoPointField": "Points de latitude et de longitude.", - "discover.fieldNameDescription.geoShapeField": "Formes complexes, telles que des polygones.", - "discover.fieldNameDescription.histogramField": "Valeurs numériques pré-agrégées sous forme d'histogramme.", - "discover.fieldNameDescription.ipAddressField": "Adresses IPv4 et IPv6.", - "discover.fieldNameDescription.ipAddressRangeField": "Plage de valeurs IP prenant en charge les adresses IPv4 ou IPv6 (ou les 2).", - "discover.fieldNameDescription.keywordField": "Contenu structuré tel qu'un ID, une adresse e-mail, un nom d'hôte, un code de statut, ou une balise.", - "discover.fieldNameDescription.murmur3Field": "Champ qui calcule et stocke les hachages de valeurs.", - "discover.fieldNameDescription.nestedField": "Objet JSON qui conserve la relation entre ses sous-champs.", - "discover.fieldNameDescription.numberField": "Valeurs Long, Entier, Court, Octet, Double et Élément flottant.", - "discover.fieldNameDescription.stringField": "Texte intégral tel que le corps d'un e-mail ou la description d'un produit.", - "discover.fieldNameDescription.textField": "Texte intégral tel que le corps d'un e-mail ou la description d'un produit.", - "discover.fieldNameDescription.unknownField": "Champ inconnu", - "discover.fieldNameDescription.viewSupportedDateFormatsLinkText": "Affichez les formats de date pris en charge.", - "discover.fieldNameIcons.booleanAriaLabel": "Champ booléen", - "discover.fieldNameIcons.conflictFieldAriaLabel": "Champ conflictuel", - "discover.fieldNameIcons.dateFieldAriaLabel": "Champ de date", - "discover.fieldNameIcons.dateRangeFieldAriaLabel": "Champ de plage de dates", - "discover.fieldNameIcons.geoPointFieldAriaLabel": "Champ de point géographique", - "discover.fieldNameIcons.geoShapeFieldAriaLabel": "Champ de forme géométrique", - "discover.fieldNameIcons.histogramFieldAriaLabel": "Champ d'histogramme", - "discover.fieldNameIcons.ipAddressFieldAriaLabel": "Champ d'adresse IP", - "discover.fieldNameIcons.ipRangeFieldAriaLabel": "Champ de plage d’IP", - "discover.fieldNameIcons.keywordFieldAriaLabel": "Champ de mot-clé", - "discover.fieldNameIcons.murmur3FieldAriaLabel": "Champ Murmur3", - "discover.fieldNameIcons.nestedFieldAriaLabel": "Champ imbriqué", - "discover.fieldNameIcons.numberFieldAriaLabel": "Champ numérique", - "discover.fieldNameIcons.sourceFieldAriaLabel": "Champ source", - "discover.fieldNameIcons.stringFieldAriaLabel": "Champ de chaîne", - "discover.fieldNameIcons.textFieldAriaLabel": "Champ de texte", - "discover.fieldNameIcons.unknownFieldAriaLabel": "Champ inconnu", - "discover.fieldNameIcons.versionFieldAriaLabel": "Champ de version", - "discover.fieldTypesPopover.buttonAriaLabel": "Aide sur le type de filtre", - "discover.fieldTypesPopover.dataTypeColumnTitle": "Type de données", - "discover.fieldTypesPopover.descriptionColumnTitle": "Description", - "discover.fieldTypesPopover.fieldTypesDocLinkLabel": "types de champ", - "discover.fieldTypesPopover.iconTitle": "Aide sur le type de filtre", - "discover.fieldTypesPopover.learnMoreText": "Découvrez", - "discover.fieldTypesPopover.tableTitle": "Description des types de champ", "discover.grid.closePopover": "Fermer la fenêtre contextuelle", "discover.grid.copyClipboardButton": "Copier dans le presse-papiers", "discover.grid.copyColumnNameToClipboard.toastTitle": "Copié dans le presse-papiers", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index bae36e12e0353..eb9e2bb17df15 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -2070,9 +2070,6 @@ "discover.fieldChooser.discoverField.addButtonAriaLabel": "{field}を表に追加", "discover.fieldChooser.discoverField.removeButtonAriaLabel": "{field}を表から削除", "discover.fieldChooser.fieldCalculator.fieldIsNotPresentInDocumentsErrorMessage": "このフィールドはElasticsearchマッピングに表示されますが、ドキュメントテーブルの{hitsLength}件のドキュメントには含まれません。可視化や検索は可能な場合があります。", - "discover.fieldChooser.filter.fieldSelectorLabel": "{id}フィルターオプションの選択", - "discover.fieldNameDescription.dateRangeField": "{dateFieldTypeLink}値の範囲。{viewSupportedDateFormatsLink}", - "discover.fieldNameDescription.versionField": "ソフトウェアバージョン。{SemanticVersioningLink}優先度ルールをサポートします。", "discover.grid.copyClipboardButtonTitle": "{column}の値をコピー", "discover.grid.copyColumnValuesToClipboard.toastTitle": "\"{column}\"列の値がクリップボードにコピーされました", "discover.grid.filterForAria": "この{value}でフィルターを適用", @@ -2102,7 +2099,6 @@ "discover.advancedSettings.defaultColumnsText": "デフォルトでDiscoverアプリに表示される列。空の場合、ドキュメントの概要が表示されます。", "discover.advancedSettings.defaultColumnsTitle": "デフォルトの列", "discover.advancedSettings.disableDocumentExplorer": "ドキュメントエクスプローラーまたはクラシックビュー", - "discover.advancedSettings.discover.fieldNameDescription.versionFieldLinkText": "セマンティックバージョニング", "discover.advancedSettings.discover.fieldStatisticsLinkText": "フィールド統計情報ビュー", "discover.advancedSettings.discover.modifyColumnsOnSwitchText": "新しいデータビューで使用できない列を削除します。", "discover.advancedSettings.discover.modifyColumnsOnSwitchTitle": "データビューを変更するときに列を修正", @@ -2269,64 +2265,10 @@ "discover.fieldChooser.discoverField.value": "値", "discover.fieldChooser.fieldCalculator.analysisIsNotAvailableForGeoFieldsErrorMessage": "ジオフィールドは分析できません。", "discover.fieldChooser.fieldCalculator.analysisIsNotAvailableForObjectFieldsErrorMessage": "オブジェクトフィールドは分析できません。", - "discover.fieldChooser.fieldFilterButtonLabel": "タイプでフィルタリング", "discover.fieldChooser.fieldsMobileButtonLabel": "フィールド", - "discover.fieldChooser.filter.aggregatableLabel": "集約可能", - "discover.fieldChooser.filter.filterByTypeLabel": "タイプでフィルタリング", "discover.fieldChooser.filter.indexAndFieldsSectionAriaLabel": "インデックスとフィールド", - "discover.fieldChooser.filter.searchableLabel": "検索可能", - "discover.fieldChooser.filter.toggleButton.any": "すべて", - "discover.fieldChooser.filter.toggleButton.no": "いいえ", - "discover.fieldChooser.filter.toggleButton.yes": "はい", - "discover.fieldChooser.filter.typeLabel": "型", - "discover.fieldChooser.popoverTitle": "フィールド型", - "discover.fieldChooser.searchPlaceHolder": "検索フィールド名", - "discover.fieldChooser.toggleFieldFilterButtonHideAriaLabel": "フィールド設定を非表示", - "discover.fieldChooser.toggleFieldFilterButtonShowAriaLabel": "フィールド設定を表示", "discover.fieldList.flyoutBackIcon": "戻る", "discover.fieldList.flyoutHeading": "フィールドリスト", - "discover.fieldNameDescription.booleanField": "True および False 値。", - "discover.fieldNameDescription.conflictField": "フィールドには異なる型の値があります。[管理 > データビュー]で解決してください。", - "discover.fieldNameDescription.dateField": "日付文字列、または1/1/1970以降の秒またはミリ秒の数値。", - "discover.fieldNameDescription.dateRangeFieldLinkText": "日付", - "discover.fieldNameDescription.geoPointField": "緯度および経度点。", - "discover.fieldNameDescription.geoShapeField": "多角形などの複雑な図形。", - "discover.fieldNameDescription.histogramField": "ヒストグラムの形式の集計された数値。", - "discover.fieldNameDescription.ipAddressField": "IPv4およびIPv6アドレス。", - "discover.fieldNameDescription.ipAddressRangeField": "IPv4またはIPv6(または混合)のアドレスをサポートするIP値の範囲。", - "discover.fieldNameDescription.keywordField": "ID、電子メールアドレス、ホスト名、ステータスコード、タグなどの構造化されたコンテンツ。", - "discover.fieldNameDescription.murmur3Field": "値のハッシュタグを計算して格納するフィールド。", - "discover.fieldNameDescription.nestedField": "サブフィールド間の関係を保持するJSONオブジェクト。", - "discover.fieldNameDescription.numberField": "長整数、整数、短整数、バイト、倍精度浮動小数点数、浮動小数点数の値。", - "discover.fieldNameDescription.stringField": "電子メール本文や製品説明などの全文テキスト。", - "discover.fieldNameDescription.textField": "電子メール本文や製品説明などの全文テキスト。", - "discover.fieldNameDescription.unknownField": "不明なフィールド", - "discover.fieldNameDescription.viewSupportedDateFormatsLinkText": "サポートされている日付形式を表示します。", - "discover.fieldNameIcons.booleanAriaLabel": "ブールフィールド", - "discover.fieldNameIcons.conflictFieldAriaLabel": "矛盾フィールド", - "discover.fieldNameIcons.dateFieldAriaLabel": "日付フィールド", - "discover.fieldNameIcons.dateRangeFieldAriaLabel": "日付範囲フィールド", - "discover.fieldNameIcons.geoPointFieldAriaLabel": "地理ポイントフィールド", - "discover.fieldNameIcons.geoShapeFieldAriaLabel": "地理情報シェイプフィールド", - "discover.fieldNameIcons.histogramFieldAriaLabel": "ヒストグラムフィールド", - "discover.fieldNameIcons.ipAddressFieldAriaLabel": "IPアドレスフィールド", - "discover.fieldNameIcons.ipRangeFieldAriaLabel": "IP範囲フィールド", - "discover.fieldNameIcons.keywordFieldAriaLabel": "キーワードフィールド", - "discover.fieldNameIcons.murmur3FieldAriaLabel": "Murmur3フィールド", - "discover.fieldNameIcons.nestedFieldAriaLabel": "入れ子フィールド", - "discover.fieldNameIcons.numberFieldAriaLabel": "数値フィールド", - "discover.fieldNameIcons.sourceFieldAriaLabel": "ソースフィールド", - "discover.fieldNameIcons.stringFieldAriaLabel": "文字列フィールド", - "discover.fieldNameIcons.textFieldAriaLabel": "テキストフィールド", - "discover.fieldNameIcons.unknownFieldAriaLabel": "不明なフィールド", - "discover.fieldNameIcons.versionFieldAriaLabel": "バージョンフィールド", - "discover.fieldTypesPopover.buttonAriaLabel": "フィルタータイプのヘルプ", - "discover.fieldTypesPopover.dataTypeColumnTitle": "データ型", - "discover.fieldTypesPopover.descriptionColumnTitle": "説明", - "discover.fieldTypesPopover.fieldTypesDocLinkLabel": "フィールド型", - "discover.fieldTypesPopover.iconTitle": "フィルタータイプのヘルプ", - "discover.fieldTypesPopover.learnMoreText": "詳細", - "discover.fieldTypesPopover.tableTitle": "フィールド型の説明", "discover.grid.closePopover": "ポップオーバーを閉じる", "discover.grid.copyClipboardButton": "クリップボードにコピー", "discover.grid.copyColumnNameToClipboard.toastTitle": "クリップボードにコピーされました", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index d09dfb11d9841..5468d9820cd69 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -2074,9 +2074,6 @@ "discover.fieldChooser.discoverField.addButtonAriaLabel": "将 {field} 添加到表中", "discover.fieldChooser.discoverField.removeButtonAriaLabel": "从表中移除 {field}", "discover.fieldChooser.fieldCalculator.fieldIsNotPresentInDocumentsErrorMessage": "此字段在您的 Elasticsearch 映射中,但不在文档表中显示的 {hitsLength} 个文档中。您可能仍能够基于它可视化或搜索。", - "discover.fieldChooser.filter.fieldSelectorLabel": "{id} 筛选选项的选择", - "discover.fieldNameDescription.dateRangeField": "{dateFieldTypeLink} 值的范围。{viewSupportedDateFormatsLink}", - "discover.fieldNameDescription.versionField": "软件版本。支持 {SemanticVersioningLink} 优先规则。", "discover.grid.copyClipboardButtonTitle": "复制 {column} 的值", "discover.grid.copyColumnValuesToClipboard.toastTitle": "“{column}”列的值已复制到剪贴板", "discover.grid.filterForAria": "筛留此 {value}", @@ -2106,7 +2103,6 @@ "discover.advancedSettings.defaultColumnsText": "Discover 应用中默认显示的列。如果为空,将显示文档摘要。", "discover.advancedSettings.defaultColumnsTitle": "默认列", "discover.advancedSettings.disableDocumentExplorer": "Document Explorer 或经典视图", - "discover.advancedSettings.discover.fieldNameDescription.versionFieldLinkText": "语义版本控制", "discover.advancedSettings.discover.fieldStatisticsLinkText": "字段统计信息视图", "discover.advancedSettings.discover.modifyColumnsOnSwitchText": "移除新数据视图中不存在的列。", "discover.advancedSettings.discover.modifyColumnsOnSwitchTitle": "在更改数据视图时修改列", @@ -2273,64 +2269,10 @@ "discover.fieldChooser.discoverField.value": "值", "discover.fieldChooser.fieldCalculator.analysisIsNotAvailableForGeoFieldsErrorMessage": "分析不适用于地理字段。", "discover.fieldChooser.fieldCalculator.analysisIsNotAvailableForObjectFieldsErrorMessage": "分析不适用于对象字段。", - "discover.fieldChooser.fieldFilterButtonLabel": "按类型筛选", "discover.fieldChooser.fieldsMobileButtonLabel": "字段", - "discover.fieldChooser.filter.aggregatableLabel": "可聚合", - "discover.fieldChooser.filter.filterByTypeLabel": "按类型筛选", "discover.fieldChooser.filter.indexAndFieldsSectionAriaLabel": "索引和字段", - "discover.fieldChooser.filter.searchableLabel": "可搜索", - "discover.fieldChooser.filter.toggleButton.any": "任意", - "discover.fieldChooser.filter.toggleButton.no": "否", - "discover.fieldChooser.filter.toggleButton.yes": "是", - "discover.fieldChooser.filter.typeLabel": "类型", - "discover.fieldChooser.popoverTitle": "字段类型", - "discover.fieldChooser.searchPlaceHolder": "搜索字段名称", - "discover.fieldChooser.toggleFieldFilterButtonHideAriaLabel": "隐藏字段筛选设置", - "discover.fieldChooser.toggleFieldFilterButtonShowAriaLabel": "显示字段筛选设置", "discover.fieldList.flyoutBackIcon": "返回", "discover.fieldList.flyoutHeading": "字段列表", - "discover.fieldNameDescription.booleanField": "True 和 False 值。", - "discover.fieldNameDescription.conflictField": "字体具有不同类型的值。在“管理”>“数据视图”中解析。", - "discover.fieldNameDescription.dateField": "日期字符串或 1/1/1970 以来的秒数或毫秒数。", - "discover.fieldNameDescription.dateRangeFieldLinkText": "日期", - "discover.fieldNameDescription.geoPointField": "纬度和经度点。", - "discover.fieldNameDescription.geoShapeField": "复杂形状,如多边形。", - "discover.fieldNameDescription.histogramField": "直方图形式的预聚合数字值。", - "discover.fieldNameDescription.ipAddressField": "IPv4 和 IPv6 地址。", - "discover.fieldNameDescription.ipAddressRangeField": "支持 IPv4 或 IPv6(或混合)地址的 IP 值的范围。", - "discover.fieldNameDescription.keywordField": "结构化内容,如 ID、电子邮件地址、主机名、状态代码或标签。", - "discover.fieldNameDescription.murmur3Field": "计算和存储值哈希的字段。", - "discover.fieldNameDescription.nestedField": "保留其子字段之间关系的 JSON 对象。", - "discover.fieldNameDescription.numberField": "长整型、整数、短整型、字节、双精度和浮点值。", - "discover.fieldNameDescription.stringField": "全文本,如电子邮件正文或产品描述。", - "discover.fieldNameDescription.textField": "全文本,如电子邮件正文或产品描述。", - "discover.fieldNameDescription.unknownField": "未知字段", - "discover.fieldNameDescription.viewSupportedDateFormatsLinkText": "查看支持的日期格式。", - "discover.fieldNameIcons.booleanAriaLabel": "布尔值字段", - "discover.fieldNameIcons.conflictFieldAriaLabel": "冲突字段", - "discover.fieldNameIcons.dateFieldAriaLabel": "日期字段", - "discover.fieldNameIcons.dateRangeFieldAriaLabel": "日期范围字段", - "discover.fieldNameIcons.geoPointFieldAriaLabel": "地理点字段", - "discover.fieldNameIcons.geoShapeFieldAriaLabel": "几何形状字段", - "discover.fieldNameIcons.histogramFieldAriaLabel": "直方图字段", - "discover.fieldNameIcons.ipAddressFieldAriaLabel": "IP 地址字段", - "discover.fieldNameIcons.ipRangeFieldAriaLabel": "IP 范围字段", - "discover.fieldNameIcons.keywordFieldAriaLabel": "关键字字段", - "discover.fieldNameIcons.murmur3FieldAriaLabel": "Murmur3 字段", - "discover.fieldNameIcons.nestedFieldAriaLabel": "嵌套字段", - "discover.fieldNameIcons.numberFieldAriaLabel": "数字字段", - "discover.fieldNameIcons.sourceFieldAriaLabel": "源字段", - "discover.fieldNameIcons.stringFieldAriaLabel": "字符串字段", - "discover.fieldNameIcons.textFieldAriaLabel": "文本字段", - "discover.fieldNameIcons.unknownFieldAriaLabel": "未知字段", - "discover.fieldNameIcons.versionFieldAriaLabel": "版本字段", - "discover.fieldTypesPopover.buttonAriaLabel": "筛选类型帮助", - "discover.fieldTypesPopover.dataTypeColumnTitle": "数据类型", - "discover.fieldTypesPopover.descriptionColumnTitle": "描述", - "discover.fieldTypesPopover.fieldTypesDocLinkLabel": "字段类型", - "discover.fieldTypesPopover.iconTitle": "筛选类型帮助", - "discover.fieldTypesPopover.learnMoreText": "详细了解", - "discover.fieldTypesPopover.tableTitle": "字段类型描述", "discover.grid.closePopover": "关闭弹出框", "discover.grid.copyClipboardButton": "复制到剪贴板", "discover.grid.copyColumnNameToClipboard.toastTitle": "已复制到剪贴板", diff --git a/x-pack/test/functional/apps/discover/reporting.ts b/x-pack/test/functional/apps/discover/reporting.ts index ddb5d774765bb..026129e7f0778 100644 --- a/x-pack/test/functional/apps/discover/reporting.ts +++ b/x-pack/test/functional/apps/discover/reporting.ts @@ -98,7 +98,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { // get clipboard value using field search input, since // 'browser.getClipboardValue()' doesn't work, due to permissions - const textInput = await testSubjects.find('fieldFilterSearchInput'); + const textInput = await testSubjects.find('fieldListFiltersFieldSearch'); await textInput.click(); await browser.getActions().keyDown(Key.CONTROL).perform(); await browser.getActions().keyDown('v').perform();