mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
[Discover] Add field types in-product help (#126657)
* added base popover * add field types popover * replace callout * fix anchor * use another euifilterbutton * clean up table and add more field types * [Discover] add pagination * [Discover] show field types which are present in current data view, sort them alphabetically * renaming * more progress * remove _source * fix mobile view * update icon * [Discover] improve unit tests * Scrolling instead of pagination * i18n feedback * cleanup * Update src/plugins/discover/public/application/main/components/sidebar/discover_sidebar.tsx Co-authored-by: Matthias Wilhelm <ankertal@gmail.com> * Update discover_field_search.tsx remove code that's unused * add missing links Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Dzmitry Tamashevich <diaamnj@mail.ru> Co-authored-by: Dmitry Tomashevich <39378793+Dmitriynj@users.noreply.github.com> Co-authored-by: cchaos <caroline.horn@elastic.co> Co-authored-by: Matthias Wilhelm <ankertal@gmail.com> Co-authored-by: Matthias Wilhelm <matthias.wilhelm@elastic.co>
This commit is contained in:
parent
9b7cde9e6c
commit
f032bbbf97
7 changed files with 309 additions and 22 deletions
|
@ -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: {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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(<DiscoverFieldSearch {...compProps} />);
|
||||
return mountWithIntl(
|
||||
<KibanaContextProvider
|
||||
services={{ docLinks: { links: { discover: { fieldTypeHelp: '' } } } }}
|
||||
>
|
||||
<DiscoverFieldSearch {...compProps} />
|
||||
</KibanaContextProvider>
|
||||
);
|
||||
}
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
|
||||
/**
|
||||
* the input value of the user
|
||||
*/
|
||||
value?: string;
|
||||
|
||||
/**
|
||||
* types for the type filter
|
||||
*/
|
||||
types: string[];
|
||||
/**
|
||||
* types presented in current data view
|
||||
*/
|
||||
presentFieldTypes: string[];
|
||||
/**
|
||||
* the input value of the user
|
||||
*/
|
||||
value?: 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<State>({
|
||||
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<EuiBasicTableColumn<FieldTypeTableItem>> = [
|
||||
{
|
||||
field: 'dataType',
|
||||
name: 'Data type',
|
||||
width: '110px',
|
||||
render: (name: string) => (
|
||||
<EuiFlexGroup alignItems="center" responsive={false} gutterSize="xs">
|
||||
<EuiFlexItem grow={false}>
|
||||
<FieldIcon type={name} />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>{name}</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
),
|
||||
},
|
||||
{
|
||||
field: 'description',
|
||||
name: 'Description',
|
||||
// eslint-disable-next-line react/no-danger
|
||||
render: (description: string) => <div dangerouslySetInnerHTML={{ __html: description }} />,
|
||||
},
|
||||
];
|
||||
|
||||
const filterBtnAriaLabel = isPopoverOpen
|
||||
? i18n.translate('discover.fieldChooser.toggleFieldFilterButtonHideAriaLabel', {
|
||||
defaultMessage: 'Hide field filter settings',
|
||||
|
@ -257,6 +311,26 @@ export function DiscoverFieldSearch({ onChange, value, types }: Props) {
|
|||
</div>
|
||||
);
|
||||
|
||||
const helpButton = (
|
||||
<EuiFilterButton
|
||||
grow={false}
|
||||
onClick={onHelpClick}
|
||||
data-test-subj="fieldTypesHelpButton"
|
||||
className="dscFieldTypesHelp__button"
|
||||
aria-label={i18n.translate('discover.fieldTypesPopover.buttonAriaLabel', {
|
||||
defaultMessage: 'Filter type help',
|
||||
})}
|
||||
>
|
||||
<EuiIcon
|
||||
type="iInCircle"
|
||||
color="primary"
|
||||
title={i18n.translate('discover.fieldTypesPopover.iconTitle', {
|
||||
defaultMessage: 'Filter type help',
|
||||
})}
|
||||
/>
|
||||
</EuiFilterButton>
|
||||
);
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<EuiFlexGroup responsive={false} gutterSize={'s'}>
|
||||
|
@ -272,8 +346,8 @@ export function DiscoverFieldSearch({ onChange, value, types }: Props) {
|
|||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiSpacer size="xs" />
|
||||
<EuiOutsideClickDetector onOutsideClick={() => {}} isDisabled={!isPopoverOpen}>
|
||||
<EuiFilterGroup className="dscFieldSearch__filterWrapper">
|
||||
<EuiFlexItem>
|
||||
<EuiFilterGroup fullWidth>
|
||||
<EuiPopover
|
||||
id="dataPanelTypeFilter"
|
||||
panelClassName="euiFilterGroup__popoverPanel"
|
||||
|
@ -294,8 +368,59 @@ export function DiscoverFieldSearch({ onChange, value, types }: Props) {
|
|||
{selectionPanel}
|
||||
{footer()}
|
||||
</EuiPopover>
|
||||
<EuiPopover
|
||||
anchorPosition="rightUp"
|
||||
display="block"
|
||||
button={helpButton}
|
||||
isOpen={isHelpOpen}
|
||||
panelPaddingSize="none"
|
||||
className="dscFieldTypesHelp__popover"
|
||||
panelClassName="dscFieldTypesHelp__panel"
|
||||
closePopover={closeHelp}
|
||||
initialFocus="#dscFieldTypesHelpBasicTableId"
|
||||
>
|
||||
<EuiPopoverTitle paddingSize="s">
|
||||
{i18n.translate('discover.fieldChooser.popoverTitle', {
|
||||
defaultMessage: 'Field types',
|
||||
})}
|
||||
</EuiPopoverTitle>
|
||||
<EuiPanel
|
||||
className="eui-yScroll"
|
||||
style={{ maxHeight: '50vh' }}
|
||||
color="transparent"
|
||||
paddingSize="s"
|
||||
>
|
||||
<EuiBasicTable
|
||||
id="dscFieldTypesHelpBasicTableId"
|
||||
tableCaption={i18n.translate('discover.fieldTypesPopover.tableTitle', {
|
||||
defaultMessage: 'Description of field types',
|
||||
})}
|
||||
items={items}
|
||||
compressed={true}
|
||||
rowHeader="firstName"
|
||||
columns={columnsSidebar}
|
||||
responsive={false}
|
||||
/>
|
||||
</EuiPanel>
|
||||
<EuiPanel color="transparent" paddingSize="s">
|
||||
<EuiText color="subdued" size="xs">
|
||||
<p>
|
||||
{i18n.translate('discover.fieldTypesPopover.learnMoreText', {
|
||||
defaultMessage: 'Learn more about',
|
||||
})}
|
||||
|
||||
<EuiLink href={docLinks.links.discover.fieldTypeHelp}>
|
||||
<FormattedMessage
|
||||
id="discover.fieldTypesPopover.learnMoreLink"
|
||||
defaultMessage="field types."
|
||||
/>
|
||||
</EuiLink>
|
||||
</p>
|
||||
</EuiText>
|
||||
</EuiPanel>
|
||||
</EuiPopover>
|
||||
</EuiFilterGroup>
|
||||
</EuiOutsideClickDetector>
|
||||
</EuiFlexItem>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -196,16 +196,31 @@ export function DiscoverSidebarComponent({
|
|||
}
|
||||
}, [paginate, scrollContainer, unpopularFields]);
|
||||
|
||||
const fieldTypes = useMemo(() => {
|
||||
const { fieldTypes, presentFieldTypes } = useMemo(() => {
|
||||
const result = ['any'];
|
||||
const dataViewFieldTypes = new Set<string>();
|
||||
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}
|
||||
>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiFlexGroup direction="row" alignItems="center" gutterSize="s">
|
||||
<EuiFlexGroup responsive={false} direction="row" alignItems="center" gutterSize="s">
|
||||
<EuiFlexItem grow={true} className="dscSidebar__indexPatternSwitcher">
|
||||
<DiscoverIndexPattern
|
||||
selectedIndexPattern={selectedIndexPattern}
|
||||
|
@ -351,6 +366,7 @@ export function DiscoverSidebarComponent({
|
|||
onChange={onChangeFieldSearch}
|
||||
value={fieldFilter.name}
|
||||
types={fieldTypes}
|
||||
presentFieldTypes={presentFieldTypes}
|
||||
/>
|
||||
</form>
|
||||
</EuiFlexItem>
|
||||
|
|
|
@ -51,6 +51,7 @@ const mockServices = {
|
|||
}
|
||||
},
|
||||
},
|
||||
docLinks: { links: { discover: { fieldTypeHelp: '' } } },
|
||||
} as unknown as DiscoverServices;
|
||||
|
||||
const mockfieldCounts: Record<string, number> = {};
|
||||
|
|
|
@ -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:
|
||||
`<a href=${docLinks.links.discover.dateFieldTypeDocs}
|
||||
target="_blank" rel="noopener">` +
|
||||
i18n.translate('discover.fieldNameDescription.dateRangeFieldLinkText', {
|
||||
defaultMessage: 'date',
|
||||
}) +
|
||||
'</a>',
|
||||
viewSupportedDateFormatsLink:
|
||||
`<a href=${docLinks.links.discover.dateFormatsDocs}
|
||||
target="_blank" rel="noopener">` +
|
||||
i18n.translate('discover.fieldNameDescription.viewSupportedDateFormatsLinkText', {
|
||||
defaultMessage: 'View supported date formats.',
|
||||
}) +
|
||||
'</a>',
|
||||
},
|
||||
});
|
||||
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:
|
||||
`<a href="https://semver.org/"
|
||||
target="_blank" rel="noopener">` +
|
||||
i18n.translate(
|
||||
'discover.advancedSettings.discover.fieldNameDescription.versionFieldLinkText',
|
||||
{
|
||||
defaultMessage: 'Semantic Versioning',
|
||||
}
|
||||
) +
|
||||
'</a>',
|
||||
},
|
||||
});
|
||||
default:
|
||||
return i18n.translate('discover.fieldNameDescription.unknownField', {
|
||||
defaultMessage: 'Unknown field',
|
||||
});
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue