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:
parent
71ed148cfe
commit
0bf57a2447
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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 />
|
||||
|
|
|
@ -6,4 +6,5 @@
|
|||
*/
|
||||
|
||||
export { ResultFieldValue } from './result_field_value';
|
||||
export { ResultToken } from './result_token';
|
||||
export { Result } from './result';
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -14,6 +14,16 @@
|
|||
}
|
||||
}
|
||||
|
||||
&__key {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@include euiCodeFont;
|
||||
|
||||
.euiToken {
|
||||
margin-right: $euiSizeS;
|
||||
}
|
||||
}
|
||||
|
||||
&__value {
|
||||
padding-left: $euiSize;
|
||||
overflow: hidden;
|
||||
|
|
|
@ -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} />
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
.appSearchResultHeader {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: $euiSizeS;
|
||||
|
||||
@include euiBreakpoint('xs') {
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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" />
|
||||
|
||||
{href ? (
|
||||
<ReactRouterHelper to={href}>
|
||||
<a>
|
||||
<HeaderItemContent />
|
||||
</a>
|
||||
</ReactRouterHelper>
|
||||
) : (
|
||||
<HeaderItemContent />
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
});
|
|
@ -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]} />;
|
||||
};
|
|
@ -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;
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue