diff --git a/packages/kbn-doc-links/src/get_doc_links.ts b/packages/kbn-doc-links/src/get_doc_links.ts index 122486a1d0e9d..690765f3d0c4d 100644 --- a/packages/kbn-doc-links/src/get_doc_links.ts +++ b/packages/kbn-doc-links/src/get_doc_links.ts @@ -67,6 +67,9 @@ export const getDocLinks = ({ kibanaBranch }: GetDocLinkOptions): DocLinks => { discover: { guide: `${KIBANA_DOCS}discover.html`, fieldStatistics: `${KIBANA_DOCS}show-field-statistics.html`, + fieldTypeHelp: `${ELASTICSEARCH_DOCS}mapping-types.html`, + dateFieldTypeDocs: `${ELASTICSEARCH_DOCS}date.html`, + dateFormatsDocs: `${ELASTICSEARCH_DOCS}mapping-date-format.html`, documentExplorer: `${KIBANA_DOCS}document-explorer.html`, }, filebeat: { 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 index 4b620f2073771..59b6e49b0fe0e 100644 --- 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 @@ -2,6 +2,14 @@ padding: $euiSizeM; } -.dscFieldSearch__filterWrapper { - width: 100%; +.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 index 40474f3e4fc1d..454f8620d7b08 100644 --- 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 @@ -13,17 +13,25 @@ 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 '../../../../../../kibana_react/public'; describe('DiscoverFieldSearch', () => { const defaultProps = { onChange: jest.fn(), value: 'test', types: ['any', 'string', '_source'], + presentFieldTypes: ['string', 'date', 'boolean', 'number'], }; function mountComponent(props?: Props) { const compProps = props || defaultProps; - return mountWithIntl(); + return mountWithIntl( + + + + ); } function findButtonGroup(component: ReactWrapper, id: string) { @@ -131,9 +139,25 @@ describe('DiscoverFieldSearch', () => { const btn = findTestSubject(component, 'toggleFieldFilterButton'); btn.simulate('click'); let popover = component.find(EuiPopover); - expect(popover.prop('isOpen')).toBe(true); + expect(popover.get(0).props.isOpen).toBe(true); btn.simulate('click'); popover = component.find(EuiPopover); - expect(popover.prop('isOpen')).toBe(false); + 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 index 67282ea2667ed..f79e45059e87e 100644 --- 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 @@ -8,9 +8,10 @@ import './discover_field_search.scss'; -import React, { OptionHTMLAttributes, ReactNode, useState } from 'react'; +import React, { OptionHTMLAttributes, ReactNode, useMemo, useState } from 'react'; import { i18n } from '@kbn/i18n'; import { + EuiBasicTable, EuiFieldSearch, EuiFilterGroup, EuiFlexGroup, @@ -24,11 +25,18 @@ import { EuiForm, EuiFormRow, EuiButtonGroup, - EuiOutsideClickDetector, 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 { useDiscoverServices } from '../../../../utils/use_discover_services'; export interface State { searchable: string; @@ -43,23 +51,31 @@ 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; +} - /** - * types for the type filter - */ - types: 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 }: Props) { +export function DiscoverFieldSearch({ onChange, value, types, presentFieldTypes }: Props) { const searchPlaceholder = i18n.translate('discover.fieldChooser.searchPlaceHolder', { defaultMessage: 'Search field names', }); @@ -80,6 +96,7 @@ export function DiscoverFieldSearch({ onChange, value, types }: Props) { const [activeFiltersCount, setActiveFiltersCount] = useState(0); const [isPopoverOpen, setPopoverOpen] = useState(false); + const [isHelpOpen, setIsHelpOpen] = useState(false); const [values, setValues] = useState({ searchable: 'any', aggregatable: 'any', @@ -87,6 +104,43 @@ export function DiscoverFieldSearch({ onChange, value, types }: Props) { missing: true, }); + const { docLinks } = useDiscoverServices(); + + const items: FieldTypeTableItem[] = useMemo(() => { + return presentFieldTypes + .sort((one, another) => one.localeCompare(another)) + .map((element, index) => ({ + id: index, + dataType: element, + description: GetFieldTypeDescription(element), + })); + }, [presentFieldTypes]); + + const onHelpClick = () => setIsHelpOpen((prevIsHelpOpen) => !prevIsHelpOpen); + const closeHelp = () => setIsHelpOpen(false); + + const columnsSidebar: Array> = [ + { + field: 'dataType', + name: 'Data type', + width: '110px', + render: (name: string) => ( + + + + + {name} + + ), + }, + { + field: 'description', + name: 'Description', + // eslint-disable-next-line react/no-danger + render: (description: string) =>
, + }, + ]; + const filterBtnAriaLabel = isPopoverOpen ? i18n.translate('discover.fieldChooser.toggleFieldFilterButtonHideAriaLabel', { defaultMessage: 'Hide field filter settings', @@ -257,6 +311,26 @@ export function DiscoverFieldSearch({ onChange, value, types }: Props) {
); + const helpButton = ( + + + + ); + return ( @@ -272,8 +346,8 @@ export function DiscoverFieldSearch({ onChange, value, types }: Props) { - {}} isDisabled={!isPopoverOpen}> - + + + + + {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.tsx b/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar.tsx index 5b9f9a6c452d6..f3a3f121e36ad 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 @@ -196,16 +196,31 @@ export function DiscoverSidebarComponent({ } }, [paginate, scrollContainer, unpopularFields]); - const fieldTypes = useMemo(() => { + const { fieldTypes, presentFieldTypes } = useMemo(() => { const result = ['any']; + const dataViewFieldTypes = new Set(); if (Array.isArray(fields)) { for (const field of fields) { - if (result.indexOf(field.type) === -1) { - result.push(field.type); + 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 result; + return { fieldTypes: result, presentFieldTypes: Array.from(dataViewFieldTypes) }; }, [fields]); const showFieldStats = useMemo(() => viewMode === VIEW_MODE.DOCUMENT_LEVEL, [viewMode]); @@ -327,7 +342,7 @@ export function DiscoverSidebarComponent({ responsive={false} > - + 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 ef8fcd145c908..2adb1e5607a3b 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 @@ -51,6 +51,7 @@ const mockServices = { } }, }, + docLinks: { links: { discover: { fieldTypeHelp: '' } } }, } as unknown as DiscoverServices; const mockfieldCounts: Record = {}; 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 new file mode 100644 index 0000000000000..69728787e7667 --- /dev/null +++ b/src/plugins/discover/public/application/main/components/sidebar/lib/get_field_type_description.ts @@ -0,0 +1,110 @@ +/* + * 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 { useDiscoverServices } from '../../../../../utils/use_discover_services'; + +export function GetFieldTypeDescription(type: string) { + const { docLinks } = useDiscoverServices(); + switch (type) { + case 'boolean': + return i18n.translate('discover.fieldNameDescription.booleanField', { + defaultMessage: 'True and false values.', + }); + case 'conflict': + return i18n.translate('discover.fieldNameDescription.conflictField', { + defaultMessage: 'Field has values of different types. Resolve in Management > Data Views.', + }); + case 'date': + return i18n.translate('discover.fieldNameDescription.dateField', { + defaultMessage: 'A date string or the number of seconds or milliseconds since 1/1/1970.', + }); + case '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 'geo_point': + return i18n.translate('discover.fieldNameDescription.geoPointField', { + defaultMessage: 'Latitude and longitude points.', + }); + case 'geo_shape': + return i18n.translate('discover.fieldNameDescription.geoShapeField', { + defaultMessage: 'Complex shapes, such as polygons.', + }); + case 'ip': + return i18n.translate('discover.fieldNameDescription.ipAddressField', { + defaultMessage: 'IPv4 and IPv6 addresses.', + }); + case 'ip_range': + return i18n.translate('discover.fieldNameDescription.ipAddressRangeField', { + defaultMessage: 'Range of ip values supporting either IPv4 or IPv6 (or mixed) addresses.', + }); + case 'murmur3': + return i18n.translate('discover.fieldNameDescription.murmur3Field', { + defaultMessage: 'Field that computes and stores hashes of values.', + }); + case 'number': + return i18n.translate('discover.fieldNameDescription.numberField', { + defaultMessage: 'Long, integer, short, byte, double, and float values.', + }); + case 'string': + return i18n.translate('discover.fieldNameDescription.stringField', { + defaultMessage: 'Full text such as the body of an email or a product description.', + }); + case 'text': + return i18n.translate('discover.fieldNameDescription.textField', { + defaultMessage: 'Full text such as the body of an email or a product description.', + }); + case 'keyword': + return i18n.translate('discover.fieldNameDescription.keywordField', { + defaultMessage: + 'Structured content such as an ID, email address, hostname, status code, or tag.', + }); + + case 'nested': + return i18n.translate('discover.fieldNameDescription.nestedField', { + defaultMessage: 'JSON object that preserves the relationship between its subfields.', + }); + case '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: + return i18n.translate('discover.fieldNameDescription.unknownField', { + defaultMessage: 'Unknown field', + }); + } +}