App Search: Result Component Updates (#96184)

* Starting work on result component

* Write tests

* Fix types

* Cleanup

* Fix type errors

Co-authored-by: Jason Stoltzfus <jastoltz24@gmail.com>
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Davey Holler 2021-04-08 15:51:39 -07:00 committed by GitHub
parent 71ed148cfe
commit 0bf57a2447
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 320 additions and 194 deletions

View file

@ -5,6 +5,7 @@
* 2.0.
*/
import { EuiButtonIconColor } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
export const CURATIONS_TITLE = i18n.translate(
@ -49,26 +50,26 @@ export const PROMOTE_DOCUMENT_ACTION = {
defaultMessage: 'Promote this result',
}),
iconType: 'starPlusEmpty',
iconColor: 'primary',
iconColor: 'primary' as EuiButtonIconColor,
};
export const DEMOTE_DOCUMENT_ACTION = {
title: i18n.translate('xpack.enterpriseSearch.appSearch.engine.curations.demoteButtonLabel', {
defaultMessage: 'Demote this result',
}),
iconType: 'starMinusFilled',
iconColor: 'primary',
iconColor: 'primary' as EuiButtonIconColor,
};
export const HIDE_DOCUMENT_ACTION = {
title: i18n.translate('xpack.enterpriseSearch.appSearch.engine.curations.hideButtonLabel', {
defaultMessage: 'Hide this result',
}),
iconType: 'eyeClosed',
iconColor: 'danger',
iconColor: 'danger' as EuiButtonIconColor,
};
export const SHOW_DOCUMENT_ACTION = {
title: i18n.translate('xpack.enterpriseSearch.appSearch.engine.curations.showButtonLabel', {
defaultMessage: 'Show this result',
}),
iconType: 'eye',
iconColor: 'primary',
iconColor: 'primary' as EuiButtonIconColor,
};

View file

@ -16,6 +16,7 @@ import {
EuiDragDropContext,
EuiDroppable,
EuiDraggable,
EuiButtonIconColor,
} from '@elastic/eui';
import { SetAppSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome';
@ -78,7 +79,7 @@ export const Library: React.FC = () => {
title: 'Fill this action button',
onClick: () => setIsActionButtonFilled(!isActionButtonFilled),
iconType: isActionButtonFilled ? 'starFilled' : 'starEmpty',
iconColor: 'primary',
iconColor: 'primary' as EuiButtonIconColor,
},
];
@ -221,7 +222,7 @@ export const Library: React.FC = () => {
<h3>With custom actions and a link</h3>
</EuiTitle>
<EuiSpacer />
<Result {...props} actions={actions} shouldLinkToDetailPage />
<Result {...props} actions={actions} shouldLinkToDetailPage showScore isMetaEngine />
<EuiSpacer />
<EuiSpacer />

View file

@ -6,4 +6,5 @@
*/
export { ResultFieldValue } from './result_field_value';
export { ResultToken } from './result_token';
export { Result } from './result';

View file

@ -6,6 +6,7 @@
'drag content actions'
'drag toggle actions';
overflow: hidden; // Prevents child background-colors from clipping outside of panel border-radius
border: $euiBorderThin; // TODO: Remove after EUI version is bumped beyond 31.8.0
&__content {
grid-area: content;
@ -44,9 +45,13 @@
display: flex;
justify-content: center;
align-items: center;
width: $euiSizeL * 2;
width: $euiSize * 2;
border-left: $euiBorderThin;
&:first-child {
border-left: none;
}
&:hover,
&:focus {
background-color: $euiPageBackgroundColor;
@ -62,22 +67,3 @@
border-right: $euiBorderThin;
}
}
/**
* CSS for hover specific logic
* It's mildly horrific, so I pulled it out to its own section here
*/
.appSearchResult--link {
&:hover,
&:focus {
@include euiSlightShadowHover;
}
}
.appSearchResult__content--link:hover {
cursor: pointer;
& ~ .appSearchResult__actionButtons .appSearchResult__actionButton--link {
background-color: $euiPageBackgroundColor;
}
}

View file

@ -10,9 +10,8 @@ import { DraggableProvidedDragHandleProps } from 'react-beautiful-dnd';
import { shallow, ShallowWrapper } from 'enzyme';
import { EuiPanel } from '@elastic/eui';
import { EuiButtonIcon, EuiPanel, EuiButtonIconColor } from '@elastic/eui';
import { ReactRouterHelper } from '../../../shared/react_router_helpers/eui_components';
import { SchemaTypes } from '../../../shared/types';
import { Result } from './result';
@ -64,37 +63,18 @@ describe('Result', () => {
]);
});
it('passes showScore, resultMeta, and isMetaEngine to ResultHeader', () => {
it('renders a header', () => {
const wrapper = shallow(<Result {...props} showScore isMetaEngine />);
expect(wrapper.find(ResultHeader).props()).toEqual({
isMetaEngine: true,
showScore: true,
resultMeta: {
id: '1',
score: 100,
engine: 'my-engine',
},
});
});
describe('document detail link', () => {
it('will render a link if shouldLinkToDetailPage is true', () => {
const wrapper = shallow(<Result {...props} shouldLinkToDetailPage />);
wrapper.find(ReactRouterHelper).forEach((link) => {
expect(link.prop('to')).toEqual('/engines/my-engine/documents/1');
});
expect(wrapper.hasClass('appSearchResult--link')).toBe(true);
expect(wrapper.find('.appSearchResult__content--link').exists()).toBe(true);
expect(wrapper.find('.appSearchResult__actionButton--link').exists()).toBe(true);
});
it('will not render a link if shouldLinkToDetailPage is not set', () => {
const wrapper = shallow(<Result {...props} />);
expect(wrapper.find(ReactRouterHelper).exists()).toBe(false);
expect(wrapper.hasClass('appSearchResult--link')).toBe(false);
expect(wrapper.find('.appSearchResult__content--link').exists()).toBe(false);
expect(wrapper.find('.appSearchResult__actionButton--link').exists()).toBe(false);
});
const header = wrapper.find(ResultHeader);
expect(header.exists()).toBe(true);
expect(header.prop('isMetaEngine')).toBe(true); // passed through from props
expect(header.prop('showScore')).toBe(true); // passed through from props
expect(header.prop('shouldLinkToDetailPage')).toBe(false); // passed through from props
expect(header.prop('resultMeta')).toEqual({
id: '1',
score: 100,
engine: 'my-engine',
}); // passed through from meta in result prop
});
describe('actions', () => {
@ -103,30 +83,53 @@ describe('Result', () => {
title: 'Hide',
onClick: jest.fn(),
iconType: 'eyeClosed',
iconColor: 'danger',
iconColor: 'danger' as EuiButtonIconColor,
},
{
title: 'Bookmark',
onClick: jest.fn(),
iconType: 'starFilled',
iconColor: 'primary',
iconColor: undefined,
},
];
it('will render an action button for each action passed', () => {
it('will render an action button in the header for each action passed', () => {
const wrapper = shallow(<Result {...props} actions={actions} />);
expect(wrapper.find('.appSearchResult__actionButton')).toHaveLength(2);
const header = wrapper.find(ResultHeader);
const renderedActions = shallow(header.prop('actions') as any);
const buttons = renderedActions.find(EuiButtonIcon);
expect(buttons).toHaveLength(2);
wrapper.find('.appSearchResult__actionButton').first().simulate('click');
expect(buttons.first().prop('iconType')).toEqual('eyeClosed');
expect(buttons.first().prop('color')).toEqual('danger');
buttons.first().simulate('click');
expect(actions[0].onClick).toHaveBeenCalled();
wrapper.find('.appSearchResult__actionButton').last().simulate('click');
expect(buttons.last().prop('iconType')).toEqual('starFilled');
// Note that no iconColor was passed so it was defaulted to primary
expect(buttons.last().prop('color')).toEqual('primary');
buttons.last().simulate('click');
expect(actions[1].onClick).toHaveBeenCalled();
});
it('will render custom actions seamlessly next to the document detail link', () => {
it('will render a document detail link as the first action if shouldLinkToDetailPage is passed', () => {
const wrapper = shallow(<Result {...props} actions={actions} shouldLinkToDetailPage />);
expect(wrapper.find('.appSearchResult__actionButton')).toHaveLength(3);
const header = wrapper.find(ResultHeader);
const renderedActions = shallow(header.prop('actions') as any);
const buttons = renderedActions.find(EuiButtonIcon);
// In addition to the 2 actions passed, we also have a link action
expect(buttons).toHaveLength(3);
expect(buttons.first().prop('data-test-subj')).toEqual('DocumentDetailLink');
});
it('will not render anything if no actions are passed and shouldLinkToDetailPage is false', () => {
const wrapper = shallow(<Result {...props} actions={undefined} />);
const header = wrapper.find(ResultHeader);
const renderedActions = shallow(header.prop('actions') as any);
const buttons = renderedActions.find(EuiButtonIcon);
expect(buttons).toHaveLength(0);
});
});

View file

@ -8,16 +8,16 @@
import React, { useState, useMemo } from 'react';
import { DraggableProvidedDragHandleProps } from 'react-beautiful-dnd';
import classNames from 'classnames';
import './result.scss';
import { EuiPanel, EuiIcon } from '@elastic/eui';
import { EuiButtonIcon, EuiPanel, EuiFlexGroup, EuiFlexItem, EuiIcon } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { ReactRouterHelper } from '../../../shared/react_router_helpers/eui_components';
import { Schema } from '../../../shared/types';
import { ENGINE_DOCUMENT_DETAIL_PATH } from '../../routes';
import { generateEncodedPath } from '../../utils/encode_path_params';
@ -56,34 +56,54 @@ export const Result: React.FC<Props> = ({
[result]
);
const numResults = resultFields.length;
const typeForField = (fieldName: string) => {
if (schemaForTypeHighlights) return schemaForTypeHighlights[fieldName];
};
const documentLink = generateEncodedPath(ENGINE_DOCUMENT_DETAIL_PATH, {
engineName: resultMeta.engine,
documentId: resultMeta.id,
});
const conditionallyLinkedArticle = (children: React.ReactNode) => {
return shouldLinkToDetailPage ? (
<ReactRouterHelper to={documentLink}>
<article className="appSearchResult__content appSearchResult__content--link">
{children}
</article>
</ReactRouterHelper>
) : (
<article className="appSearchResult__content">{children}</article>
);
const typeForField = (fieldName: string) => {
if (schemaForTypeHighlights) return schemaForTypeHighlights[fieldName];
};
const classes = classNames('appSearchResult', {
'appSearchResult--link': shouldLinkToDetailPage,
});
const ResultActions = () => {
if (!shouldLinkToDetailPage && !actions.length) return null;
return (
<EuiFlexItem grow={false}>
<EuiFlexGroup gutterSize="s">
{shouldLinkToDetailPage && (
<ReactRouterHelper to={documentLink}>
<EuiFlexItem grow={false}>
<EuiButtonIcon
iconType="eye"
data-test-subj="DocumentDetailLink"
aria-label={i18n.translate(
'xpack.enterpriseSearch.appSearch.result.documentDetailLink',
{ defaultMessage: 'Visit document details' }
)}
/>
</EuiFlexItem>
</ReactRouterHelper>
)}
{actions.map(({ onClick, title, iconType, iconColor }) => (
<EuiFlexItem key={title}>
<EuiButtonIcon
iconType={iconType}
onClick={onClick}
color={iconColor ? iconColor : 'primary'}
aria-label={title}
/>
</EuiFlexItem>
))}
</EuiFlexGroup>
</EuiFlexItem>
);
};
return (
<EuiPanel
paddingSize="none"
className={classes}
hasShadow={false}
className="appSearchResult"
data-test-subj="AppSearchResult"
title={i18n.translate('xpack.enterpriseSearch.appSearch.result.title', {
defaultMessage: 'Document {id}',
@ -95,26 +115,26 @@ export const Result: React.FC<Props> = ({
<EuiIcon type="grab" />
</div>
)}
{conditionallyLinkedArticle(
<>
<ResultHeader
resultMeta={resultMeta}
showScore={!!showScore}
isMetaEngine={isMetaEngine}
/>
{resultFields
.slice(0, isOpen ? resultFields.length : RESULT_CUTOFF)
.map(([field, value]: [string, FieldValue]) => (
<ResultField
key={field}
field={field}
raw={value.raw}
snippet={value.snippet}
type={typeForField(field)}
/>
))}
</>
)}
<article className="appSearchResult__content">
<ResultHeader
resultMeta={resultMeta}
showScore={!!showScore}
isMetaEngine={isMetaEngine}
shouldLinkToDetailPage={shouldLinkToDetailPage}
actions={<ResultActions />}
/>
{resultFields
.slice(0, isOpen ? resultFields.length : RESULT_CUTOFF)
.map(([field, value]: [string, FieldValue]) => (
<ResultField
key={field}
field={field}
raw={value.raw}
snippet={value.snippet}
type={typeForField(field)}
/>
))}
</article>
{numResults > RESULT_CUTOFF && (
<button
type="button"
@ -138,33 +158,6 @@ export const Result: React.FC<Props> = ({
/>
</button>
)}
<div className="appSearchResult__actionButtons">
{shouldLinkToDetailPage && (
<ReactRouterHelper to={documentLink}>
<a
className="appSearchResult__actionButton appSearchResult__actionButton--link"
aria-label={i18n.translate(
'xpack.enterpriseSearch.appSearch.result.documentDetailLink',
{ defaultMessage: 'Visit document details' }
)}
>
<EuiIcon type="eye" />
</a>
</ReactRouterHelper>
)}
{actions.map(({ onClick, title, iconType, iconColor }) => (
<button
key={title}
aria-label={title}
title={title}
onClick={onClick}
className="appSearchResult__actionButton"
type="button"
>
<EuiIcon type={iconType} color={iconColor} />
</button>
))}
</div>
</EuiPanel>
);
};

View file

@ -14,6 +14,16 @@
}
}
&__key {
display: flex;
align-items: center;
@include euiCodeFont;
.euiToken {
margin-right: $euiSizeS;
}
}
&__value {
padding-left: $euiSize;
overflow: hidden;

View file

@ -9,7 +9,7 @@ import React from 'react';
import { FieldType, Raw, Snippet } from './types';
import { ResultFieldValue } from '.';
import { ResultFieldValue, ResultToken } from '.';
import './result_field.scss';
@ -23,7 +23,10 @@ interface Props {
export const ResultField: React.FC<Props> = ({ field, raw, snippet, type }) => {
return (
<div className="appSearchResultField">
<div className="appSearchResultField__key eui-textTruncate">{field}</div>
<div className="appSearchResultField__key eui-textTruncate">
{type && <ResultToken fieldType={type} />}
{field}
</div>
<div className="appSearchResultField__separator" aria-hidden />
<div className="appSearchResultField__value">
<ResultFieldValue className="eui-textTruncate" raw={raw} snippet={snippet} type={type} />

View file

@ -1,7 +1,5 @@
.appSearchResultHeader {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: $euiSizeS;
@include euiBreakpoint('xs') {

View file

@ -30,6 +30,22 @@ describe('ResultHeader', () => {
<ResultHeader showScore={false} resultMeta={resultMeta} isMetaEngine={false} />
);
expect(wrapper.find('[data-test-subj="ResultId"]').prop('value')).toEqual('1');
expect(wrapper.find('[data-test-subj="ResultId"]').prop('href')).toBeUndefined();
});
it('renders id as a link if shouldLinkToDetailPage is true', () => {
const wrapper = shallow(
<ResultHeader
showScore={false}
resultMeta={resultMeta}
isMetaEngine={false}
shouldLinkToDetailPage
/>
);
expect(wrapper.find('[data-test-subj="ResultId"]').prop('value')).toEqual('1');
expect(wrapper.find('[data-test-subj="ResultId"]').prop('href')).toEqual(
'/engines/my-engine/documents/1'
);
});
describe('score', () => {

View file

@ -7,6 +7,11 @@
import React from 'react';
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { ENGINE_DOCUMENT_DETAIL_PATH } from '../../routes';
import { generateEncodedPath } from '../../utils/encode_path_params';
import { ResultHeaderItem } from './result_header_item';
import { ResultMeta } from './types';
@ -16,33 +21,56 @@ interface Props {
showScore: boolean;
isMetaEngine: boolean;
resultMeta: ResultMeta;
actions?: React.ReactNode;
shouldLinkToDetailPage?: boolean;
}
export const ResultHeader: React.FC<Props> = ({ showScore, resultMeta, isMetaEngine }) => {
return (
<header className="appSearchResultHeader">
{showScore && (
<div className="appSearchResultHeader__column">
<ResultHeaderItem
data-test-subj="ResultScore"
field="score"
value={resultMeta.score}
type="score"
/>
</div>
)}
export const ResultHeader: React.FC<Props> = ({
showScore,
resultMeta,
isMetaEngine,
actions,
shouldLinkToDetailPage = false,
}) => {
const documentLink = generateEncodedPath(ENGINE_DOCUMENT_DETAIL_PATH, {
engineName: resultMeta.engine,
documentId: resultMeta.id,
});
<div className="appSearchResultHeader__column">
{isMetaEngine && (
return (
<header style={{ margin: '0 0 .75rem 0' }}>
<EuiFlexGroup alignItems="center" gutterSize="s" justifyContent="spaceBetween">
<EuiFlexItem grow>
<ResultHeaderItem
data-test-subj="ResultEngine"
field="engine"
value={resultMeta.engine}
type="string"
href={shouldLinkToDetailPage ? documentLink : undefined}
data-test-subj="ResultId"
field="ID"
value={resultMeta.id}
type="id"
/>
</EuiFlexItem>
{showScore && (
<EuiFlexItem grow={false}>
<ResultHeaderItem
data-test-subj="ResultScore"
field="Score"
value={resultMeta.score}
type="score"
/>
</EuiFlexItem>
)}
<ResultHeaderItem data-test-subj="ResultId" field="id" value={resultMeta.id} type="id" />
</div>
{isMetaEngine && (
<EuiFlexItem grow={false}>
<ResultHeaderItem
data-test-subj="ResultEngine"
field="Engine"
value={resultMeta.engine}
type="string"
/>
</EuiFlexItem>
)}
{actions}
</EuiFlexGroup>
</header>
);
};

View file

@ -1,16 +1,12 @@
.euiFlexItem:not(:first-child):not(:last-child) .appSearchResultHeaderItem {
padding-right: .75rem;
box-shadow: inset -1px 0 0 0 $euiBorderColor;
}
.appSearchResultHeaderItem {
display: flex;
@include euiCodeFont;
&__key,
&__value {
line-height: $euiLineHeight;
font-size: $euiFontSizeXS;
}
&__key {
text-transform: uppercase;
font-weight: $euiFontWeightLight;
color: $euiColorDarkShade;
margin-right: $euiSizeXS;
&__score {
color: $euiColorSuccessText;
}
}

View file

@ -7,15 +7,14 @@
import React from 'react';
import { mount } from 'enzyme';
import { shallow, mount } from 'enzyme';
import { ResultHeaderItem } from './result_header_item';
describe('ResultHeaderItem', () => {
it('renders', () => {
const wrapper = mount(<ResultHeaderItem field="id" value="001" type="id" />);
expect(wrapper.find('.appSearchResultHeaderItem__key').text()).toEqual('id');
expect(wrapper.find('.appSearchResultHeaderItem__value').text()).toEqual('001');
expect(wrapper.find('.appSearchResultHeaderItem').text()).toContain('001');
});
it('will truncate long field names', () => {
@ -26,7 +25,7 @@ describe('ResultHeaderItem', () => {
type="string"
/>
);
expect(wrapper.find('.appSearchResultHeaderItem__key').text()).toEqual(
expect(wrapper.find('.appSearchResultHeaderItem').text()).toContain(
'a-really-really-really-really-…'
);
});
@ -35,7 +34,7 @@ describe('ResultHeaderItem', () => {
const wrapper = mount(
<ResultHeaderItem field="foo" value="a-really-really-really-really-value" type="string" />
);
expect(wrapper.find('.appSearchResultHeaderItem__value').text()).toEqual(
expect(wrapper.find('.appSearchResultHeaderItem').text()).toContain(
'a-really-really-really-really-…'
);
});
@ -44,18 +43,33 @@ describe('ResultHeaderItem', () => {
const wrapper = mount(
<ResultHeaderItem field="foo" value="a-really-really-really-really-value" type="id" />
);
expect(wrapper.find('.appSearchResultHeaderItem__value').text()).toEqual(
expect(wrapper.find('.appSearchResultHeaderItem').text()).toContain(
'…lly-really-really-really-value'
);
});
it('will round any numeric values that are passed in to 2 decimals, regardless of the explicit "type" passed', () => {
const wrapper = mount(<ResultHeaderItem field="foo" value={5.19383718193} type="string" />);
expect(wrapper.find('.appSearchResultHeaderItem__value').text()).toEqual('5.19');
expect(wrapper.find('.appSearchResultHeaderItem').text()).toContain('5.19');
});
it('if the value passed in is undefined, it will render "-"', () => {
const wrapper = mount(<ResultHeaderItem field="foo" type="string" />);
expect(wrapper.find('.appSearchResultHeaderItem__value').text()).toEqual('-');
expect(wrapper.find('.appSearchResultHeaderItem').text()).toContain('-');
});
it('it will add a "score" class if the "type" passed is "score"', () => {
const wrapper = shallow(<ResultHeaderItem field="foo" type="score" />);
expect(
wrapper.find('.appSearchResultHeaderItem').hasClass('appSearchResultHeaderItem__score')
).toBe(true);
});
it('it will render as a link if an href is passed', () => {
const wrapper = shallow(
<ResultHeaderItem field="foo" type="score" href="http://www.example.com" />
);
expect(wrapper.find('ReactRouterHelper').exists()).toBe(true);
expect(wrapper.find('ReactRouterHelper').prop('to')).toBe('http://www.example.com');
});
});

View file

@ -9,17 +9,20 @@ import React from 'react';
import './result_header_item.scss';
import { ReactRouterHelper } from '../../../shared/react_router_helpers/eui_components';
import { TruncatedContent } from '../../../shared/truncate';
interface Props {
field: string;
value?: string | number;
type: 'id' | 'score' | 'string';
href?: string;
}
const MAX_CHARACTER_LENGTH = 30;
export const ResultHeaderItem: React.FC<Props> = ({ field, type, value }) => {
export const ResultHeaderItem: React.FC<Props> = ({ field, type, value, href }) => {
let formattedValue = '-';
if (typeof value === 'string') {
formattedValue = value;
@ -27,19 +30,32 @@ export const ResultHeaderItem: React.FC<Props> = ({ field, type, value }) => {
formattedValue = parseFloat((value as number).toFixed(2)).toString();
}
const HeaderItemContent = () => (
<TruncatedContent
content={formattedValue}
length={MAX_CHARACTER_LENGTH}
tooltipType="title"
beginning={type === 'id'}
/>
);
return (
<div className="appSearchResultHeaderItem">
<div className="appSearchResultHeaderItem__key">
<TruncatedContent content={field} length={MAX_CHARACTER_LENGTH} tooltipType="title" />
</div>
<div className="appSearchResultHeaderItem__value">
<TruncatedContent
content={formattedValue}
length={MAX_CHARACTER_LENGTH}
tooltipType="title"
beginning={type === 'id'}
/>
</div>
</div>
<span
className={`appSearchResultHeaderItem ${
type === 'score' && 'appSearchResultHeaderItem__score'
}`}
>
<TruncatedContent content={`${field}:`} length={MAX_CHARACTER_LENGTH} tooltipType="title" />
&nbsp;
{href ? (
<ReactRouterHelper to={href}>
<a>
<HeaderItemContent />
</a>
</ReactRouterHelper>
) : (
<HeaderItemContent />
)}
</span>
);
};

View file

@ -0,0 +1,28 @@
/*
* 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; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React from 'react';
import { shallow } from 'enzyme';
import { EuiToken } from '@elastic/eui';
import { ResultToken } from './result_token';
describe('ResultToken', () => {
beforeEach(() => {
jest.clearAllMocks();
});
it('render a token icon based on the provided field type', () => {
expect(
shallow(<ResultToken fieldType="text" />)
.find(EuiToken)
.prop('iconType')
).toBe('tokenString');
});
});

View file

@ -0,0 +1,30 @@
/*
* 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; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React from 'react';
import { EuiToken } from '@elastic/eui';
import { FieldType } from './types';
interface Props {
fieldType: FieldType;
}
const fieldTypeToTokenMap = {
text: 'tokenString',
string: 'tokenString',
number: 'tokenNumber',
float: 'tokenNumber',
location: 'tokenGeo',
geolocation: 'tokenGeo',
date: 'tokenDate',
};
export const ResultToken: React.FC<Props> = ({ fieldType }) => {
return <EuiToken iconType={fieldTypeToTokenMap[fieldType]} />;
};

View file

@ -5,6 +5,8 @@
* 2.0.
*/
import { EuiButtonIconColor } from '@elastic/eui';
import { InternalSchemaTypes, SchemaTypes } from '../../../shared/types';
export type FieldType = InternalSchemaTypes | SchemaTypes;
@ -38,5 +40,5 @@ export interface ResultAction {
onClick(): void;
title: string;
iconType: string;
iconColor?: string;
iconColor?: EuiButtonIconColor;
}