[8.x] [Onboarding] Search detail document list tweaks (#194804) (#195982)

# Backport

This will backport the following commits from `main` to `8.x`:
- [[Onboarding] Search detail document list tweaks
(#194804)](https://github.com/elastic/kibana/pull/194804)

<!--- Backport version: 9.4.3 -->

### Questions ?
Please refer to the [Backport tool
documentation](https://github.com/sqren/backport)

<!--BACKPORT [{"author":{"name":"Joe
McElroy","email":"joseph.mcelroy@elastic.co"},"sourceCommit":{"committedDate":"2024-10-11T19:09:11Z","message":"[Onboarding]
Search detail document list tweaks (#194804)\n\n## Summary\r\n\r\n-
Implemented priority ordering of fields. Semantic_text,
dense_vector,\r\nsparse_vector comes first + special field names.\r\n-
give extra information + copy action for dense_vector fields\r\n-
updated the design for the non compact view\r\n\r\n\r\n![Screenshot
2024-10-10 at 16
12\r\n58](https://github.com/user-attachments/assets/34b89455-6184-4a41-b9e3-8d67a1926f95)\r\n\r\n
\r\n\r\n### Checklist\r\n\r\nDelete any items that are not applicable to
this PR.\r\n\r\n- [ ] Any text added follows [EUI's
writing\r\nguidelines](https://elastic.github.io/eui/#/guidelines/writing),
uses\r\nsentence case text and includes
[i18n\r\nsupport](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)\r\n-
[
]\r\n[Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html)\r\nwas
added for features that require explanation or tutorials\r\n- [ ] [Unit
or
functional\r\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\r\nwere
updated or added to match the most common scenarios\r\n- [ ] [Flaky
Test\r\nRunner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1)
was\r\nused on any tests changed\r\n- [ ] Any UI touched in this PR is
usable by keyboard only (learn more\r\nabout [keyboard
accessibility](https://webaim.org/techniques/keyboard/))\r\n- [ ] Any UI
touched in this PR does not create any new axe failures\r\n(run axe in
browser:\r\n[FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/),\r\n[Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US))\r\n-
[ ] If a plugin configuration key changed, check if it needs to
be\r\nallowlisted in the cloud and added to the
[docker\r\nlist](https://github.com/elastic/kibana/blob/main/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker)\r\n-
[ ] This renders correctly on smaller devices using a
responsive\r\nlayout. (You can test this [in
your\r\nbrowser](https://www.browserstack.com/guide/responsive-testing-on-local-server))\r\n-
[ ] This was checked for
[cross-browser\r\ncompatibility](https://www.elastic.co/support/matrix#matrix_browsers)\r\n\r\n\r\n###
Risk Matrix\r\n\r\nDelete this section if it is not applicable to this
PR.\r\n\r\nBefore closing this PR, invite QA, stakeholders, and other
developers to\r\nidentify risks that should be tested prior to the
change/feature\r\nrelease.\r\n\r\nWhen forming the risk matrix, consider
some of the following examples\r\nand how they may potentially impact
the change:\r\n\r\n| Risk | Probability | Severity | Mitigation/Notes
|\r\n\r\n|---------------------------|-------------|----------|-------------------------|\r\n|
Multiple Spaces&mdash;unexpected behavior in non-default Kibana
Space.\r\n| Low | High | Integration tests will verify that all features
are still\r\nsupported in non-default Kibana Space and when user
switches between\r\nspaces. |\r\n| Multiple nodes&mdash;Elasticsearch
polling might have race conditions\r\nwhen multiple Kibana nodes are
polling for the same tasks. | High | Low\r\n| Tasks are idempotent, so
executing them multiple times will not result\r\nin logical error, but
will degrade performance. To test for this case we\r\nadd plenty of unit
tests around this logic and document manual testing\r\nprocedure. |\r\n|
Code should gracefully handle cases when feature X or plugin Y
are\r\ndisabled. | Medium | High | Unit tests will verify that any
feature flag\r\nor plugin combination still results in our service
operational. |\r\n| [See more potential
risk\r\nexamples](https://github.com/elastic/kibana/blob/main/RISK_MATRIX.mdx)
|\r\n\r\n\r\n### For maintainers\r\n\r\n- [ ] This was checked for
breaking API changes and was
[labeled\r\nappropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)\r\n\r\n---------\r\n\r\nCo-authored-by:
Julian Rosado
<julian.rosado@elastic.co>","sha":"2f54dc8afda5f3881e9d20b06c3b534cdf89a141","branchLabelMapping":{"^v9.0.0$":"main","^v8.16.0$":"8.x","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["release_note:skip","v9.0.0","backport:prev-minor"],"title":"[Onboarding]
Search detail document list
tweaks","number":194804,"url":"https://github.com/elastic/kibana/pull/194804","mergeCommit":{"message":"[Onboarding]
Search detail document list tweaks (#194804)\n\n## Summary\r\n\r\n-
Implemented priority ordering of fields. Semantic_text,
dense_vector,\r\nsparse_vector comes first + special field names.\r\n-
give extra information + copy action for dense_vector fields\r\n-
updated the design for the non compact view\r\n\r\n\r\n![Screenshot
2024-10-10 at 16
12\r\n58](https://github.com/user-attachments/assets/34b89455-6184-4a41-b9e3-8d67a1926f95)\r\n\r\n
\r\n\r\n### Checklist\r\n\r\nDelete any items that are not applicable to
this PR.\r\n\r\n- [ ] Any text added follows [EUI's
writing\r\nguidelines](https://elastic.github.io/eui/#/guidelines/writing),
uses\r\nsentence case text and includes
[i18n\r\nsupport](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)\r\n-
[
]\r\n[Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html)\r\nwas
added for features that require explanation or tutorials\r\n- [ ] [Unit
or
functional\r\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\r\nwere
updated or added to match the most common scenarios\r\n- [ ] [Flaky
Test\r\nRunner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1)
was\r\nused on any tests changed\r\n- [ ] Any UI touched in this PR is
usable by keyboard only (learn more\r\nabout [keyboard
accessibility](https://webaim.org/techniques/keyboard/))\r\n- [ ] Any UI
touched in this PR does not create any new axe failures\r\n(run axe in
browser:\r\n[FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/),\r\n[Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US))\r\n-
[ ] If a plugin configuration key changed, check if it needs to
be\r\nallowlisted in the cloud and added to the
[docker\r\nlist](https://github.com/elastic/kibana/blob/main/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker)\r\n-
[ ] This renders correctly on smaller devices using a
responsive\r\nlayout. (You can test this [in
your\r\nbrowser](https://www.browserstack.com/guide/responsive-testing-on-local-server))\r\n-
[ ] This was checked for
[cross-browser\r\ncompatibility](https://www.elastic.co/support/matrix#matrix_browsers)\r\n\r\n\r\n###
Risk Matrix\r\n\r\nDelete this section if it is not applicable to this
PR.\r\n\r\nBefore closing this PR, invite QA, stakeholders, and other
developers to\r\nidentify risks that should be tested prior to the
change/feature\r\nrelease.\r\n\r\nWhen forming the risk matrix, consider
some of the following examples\r\nand how they may potentially impact
the change:\r\n\r\n| Risk | Probability | Severity | Mitigation/Notes
|\r\n\r\n|---------------------------|-------------|----------|-------------------------|\r\n|
Multiple Spaces&mdash;unexpected behavior in non-default Kibana
Space.\r\n| Low | High | Integration tests will verify that all features
are still\r\nsupported in non-default Kibana Space and when user
switches between\r\nspaces. |\r\n| Multiple nodes&mdash;Elasticsearch
polling might have race conditions\r\nwhen multiple Kibana nodes are
polling for the same tasks. | High | Low\r\n| Tasks are idempotent, so
executing them multiple times will not result\r\nin logical error, but
will degrade performance. To test for this case we\r\nadd plenty of unit
tests around this logic and document manual testing\r\nprocedure. |\r\n|
Code should gracefully handle cases when feature X or plugin Y
are\r\ndisabled. | Medium | High | Unit tests will verify that any
feature flag\r\nor plugin combination still results in our service
operational. |\r\n| [See more potential
risk\r\nexamples](https://github.com/elastic/kibana/blob/main/RISK_MATRIX.mdx)
|\r\n\r\n\r\n### For maintainers\r\n\r\n- [ ] This was checked for
breaking API changes and was
[labeled\r\nappropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)\r\n\r\n---------\r\n\r\nCo-authored-by:
Julian Rosado
<julian.rosado@elastic.co>","sha":"2f54dc8afda5f3881e9d20b06c3b534cdf89a141"}},"sourceBranch":"main","suggestedTargetBranches":[],"targetPullRequestStates":[{"branch":"main","label":"v9.0.0","branchLabelMappingKey":"^v9.0.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/194804","number":194804,"mergeCommit":{"message":"[Onboarding]
Search detail document list tweaks (#194804)\n\n## Summary\r\n\r\n-
Implemented priority ordering of fields. Semantic_text,
dense_vector,\r\nsparse_vector comes first + special field names.\r\n-
give extra information + copy action for dense_vector fields\r\n-
updated the design for the non compact view\r\n\r\n\r\n![Screenshot
2024-10-10 at 16
12\r\n58](https://github.com/user-attachments/assets/34b89455-6184-4a41-b9e3-8d67a1926f95)\r\n\r\n
\r\n\r\n### Checklist\r\n\r\nDelete any items that are not applicable to
this PR.\r\n\r\n- [ ] Any text added follows [EUI's
writing\r\nguidelines](https://elastic.github.io/eui/#/guidelines/writing),
uses\r\nsentence case text and includes
[i18n\r\nsupport](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)\r\n-
[
]\r\n[Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html)\r\nwas
added for features that require explanation or tutorials\r\n- [ ] [Unit
or
functional\r\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\r\nwere
updated or added to match the most common scenarios\r\n- [ ] [Flaky
Test\r\nRunner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1)
was\r\nused on any tests changed\r\n- [ ] Any UI touched in this PR is
usable by keyboard only (learn more\r\nabout [keyboard
accessibility](https://webaim.org/techniques/keyboard/))\r\n- [ ] Any UI
touched in this PR does not create any new axe failures\r\n(run axe in
browser:\r\n[FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/),\r\n[Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US))\r\n-
[ ] If a plugin configuration key changed, check if it needs to
be\r\nallowlisted in the cloud and added to the
[docker\r\nlist](https://github.com/elastic/kibana/blob/main/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker)\r\n-
[ ] This renders correctly on smaller devices using a
responsive\r\nlayout. (You can test this [in
your\r\nbrowser](https://www.browserstack.com/guide/responsive-testing-on-local-server))\r\n-
[ ] This was checked for
[cross-browser\r\ncompatibility](https://www.elastic.co/support/matrix#matrix_browsers)\r\n\r\n\r\n###
Risk Matrix\r\n\r\nDelete this section if it is not applicable to this
PR.\r\n\r\nBefore closing this PR, invite QA, stakeholders, and other
developers to\r\nidentify risks that should be tested prior to the
change/feature\r\nrelease.\r\n\r\nWhen forming the risk matrix, consider
some of the following examples\r\nand how they may potentially impact
the change:\r\n\r\n| Risk | Probability | Severity | Mitigation/Notes
|\r\n\r\n|---------------------------|-------------|----------|-------------------------|\r\n|
Multiple Spaces&mdash;unexpected behavior in non-default Kibana
Space.\r\n| Low | High | Integration tests will verify that all features
are still\r\nsupported in non-default Kibana Space and when user
switches between\r\nspaces. |\r\n| Multiple nodes&mdash;Elasticsearch
polling might have race conditions\r\nwhen multiple Kibana nodes are
polling for the same tasks. | High | Low\r\n| Tasks are idempotent, so
executing them multiple times will not result\r\nin logical error, but
will degrade performance. To test for this case we\r\nadd plenty of unit
tests around this logic and document manual testing\r\nprocedure. |\r\n|
Code should gracefully handle cases when feature X or plugin Y
are\r\ndisabled. | Medium | High | Unit tests will verify that any
feature flag\r\nor plugin combination still results in our service
operational. |\r\n| [See more potential
risk\r\nexamples](https://github.com/elastic/kibana/blob/main/RISK_MATRIX.mdx)
|\r\n\r\n\r\n### For maintainers\r\n\r\n- [ ] This was checked for
breaking API changes and was
[labeled\r\nappropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)\r\n\r\n---------\r\n\r\nCo-authored-by:
Julian Rosado
<julian.rosado@elastic.co>","sha":"2f54dc8afda5f3881e9d20b06c3b534cdf89a141"}}]}]
BACKPORT-->

Co-authored-by: Joe McElroy <joseph.mcelroy@elastic.co>
This commit is contained in:
Kibana Machine 2024-10-12 08:04:30 +11:00 committed by GitHub
parent 5eafc27e2a
commit 18d03f264d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 310 additions and 156 deletions

View file

@ -0,0 +1,10 @@
/*
* 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
export const PERMANENTLY_TRUNCATED_FIELDS = ['dense_vector', 'sparse_vector'];

View file

@ -12,4 +12,5 @@ export {
resultMetaData, resultMetaData,
resultToFieldFromMappingResponse, resultToFieldFromMappingResponse,
resultToFieldFromMappings as resultToField, resultToFieldFromMappings as resultToField,
reorderFieldsInImportance,
} from './result_metadata'; } from './result_metadata';

View file

@ -1,20 +1,36 @@
.resultField:nth-child(odd) {
background-color: $euiColorLightestShade;
}
.resultField { .resultField {
padding: $euiSizeXS $euiSizeS; padding: 0;
border-bottom: 1px solid $euiColorLightShade;
position: relative;
> .euiTableRowCell { &:last-child {
border-bottom: none;
}
>.euiTableRow:hover {
background-color: $euiColorEmptyShade;
}
>.euiTableRowCell {
border-top: none; border-top: none;
border-bottom: none; border-bottom: none;
> .euiTableCellContent { >.euiTableCellContent {
padding: $euiSizeXS; padding: $euiSizeS;
font-family: $euiCodeFontFamily;
color: $euiColorMediumShade;
} }
} }
} }
.denseVectorFieldValue {
position: absolute;
right: 0;
top: $euiSizeS;
background-color: $euiColorEmptyShade;
padding: 0 $euiSizeS;
}
.resultExpandColumn { .resultExpandColumn {
border-left: $euiBorderThin; border-left: $euiBorderThin;
align-items: flex-start; align-items: flex-start;
@ -31,4 +47,4 @@
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: flex-start; justify-content: flex-start;
} }

View file

@ -13,8 +13,8 @@ import {
EuiButtonIcon, EuiButtonIcon,
EuiFlexGroup, EuiFlexGroup,
EuiFlexItem, EuiFlexItem,
EuiPanel, EuiHorizontalRule,
EuiSpacer, EuiSplitPanel,
EuiToolTip, EuiToolTip,
} from '@elastic/eui'; } from '@elastic/eui';
@ -66,99 +66,65 @@ export const Result: React.FC<ResultProps> = ({
const toolTipContent = <>{tooltipText}</>; const toolTipContent = <>{tooltipText}</>;
return ( return (
<EuiPanel <EuiSplitPanel.Outer hasBorder={true} data-test-subj="search-index-documents-result">
hasBorder <EuiSplitPanel.Inner paddingSize="m" color="plain" className="resultHeaderContainer">
data-test-subj="search-index-documents-result" <EuiFlexGroup gutterSize="none" alignItems="center">
paddingSize={compactCard ? 's' : 'l'} <EuiFlexItem>
> {compactCard && (
<EuiFlexGroup gutterSize="none"> <ResultHeader
<EuiFlexItem> title={
<EuiFlexGroup metaData.title ??
direction="column" i18n.translate('searchIndexDocuments.result.title.id', {
gutterSize="none" defaultMessage: 'Document id: {id}',
responsive={false} values: { id: metaData.id },
justifyContent="spaceAround" })
> }
<EuiFlexItem grow={false}> metaData={metaData}
{compactCard && (
<ResultHeader
title={
metaData.title ??
i18n.translate('searchIndexDocuments.result.title.id', {
defaultMessage: 'Document id: {id}',
values: { id: metaData.id },
})
}
metaData={{
...metaData,
onDocumentDelete,
}}
/>
)}
{!compactCard && (
<RichResultHeader
showScore={showScore}
title={
metaData.title ??
i18n.translate('searchIndexDocuments.result.title.id', {
defaultMessage: 'Document id: {id}',
values: { id: metaData.id },
})
}
onTitleClick={onDocumentClick}
metaData={{
...metaData,
onDocumentDelete,
}}
rightSideActions={
<EuiFlexItem grow={false}>
<EuiToolTip position="left" content={toolTipContent}>
<EuiButtonIcon
iconType={isExpanded ? 'fold' : 'unfold'}
color={isExpanded ? 'danger' : 'primary'}
onClick={(e: React.MouseEvent<HTMLElement>) => {
e.stopPropagation();
setIsExpanded(!isExpanded);
}}
aria-label={tooltipText}
/>
</EuiToolTip>
</EuiFlexItem>
}
/>
)}
</EuiFlexItem>
<EuiFlexItem>
{!compactCard &&
((isExpanded && fields.length > 0) ||
(!isExpanded && fields.slice(0, defaultVisibleFields).length > 0)) && (
<EuiSpacer size="l" />
)}
<ResultFields
isExpanded={isExpanded}
fields={isExpanded ? fields : fields.slice(0, defaultVisibleFields)}
/> />
</EuiFlexItem> )}
</EuiFlexGroup>
</EuiFlexItem> {!compactCard && (
{compactCard && ( <RichResultHeader
<EuiFlexItem grow={false}> showScore={showScore}
<div className="resultExpandColumn"> title={
<EuiToolTip position="left" content={toolTipContent}> metaData.title ??
<EuiButtonIcon i18n.translate('searchIndexDocuments.result.title.id', {
iconType={isExpanded ? 'fold' : 'unfold'} defaultMessage: 'Document id: {id}',
color="text" values: { id: metaData.id },
onClick={(e: React.MouseEvent<HTMLElement>) => { })
e.stopPropagation(); }
setIsExpanded(!isExpanded); onTitleClick={onDocumentClick}
}} metaData={{
aria-label={tooltipText} ...metaData,
/> onDocumentDelete,
</EuiToolTip> }}
</div> />
)}
</EuiFlexItem> </EuiFlexItem>
)} <EuiFlexItem grow={false}>
</EuiFlexGroup> <EuiToolTip position="left" content={toolTipContent}>
</EuiPanel> <EuiButtonIcon
size="xs"
iconType={isExpanded ? 'fold' : 'unfold'}
color={isExpanded ? 'danger' : 'primary'}
data-test-subj={isExpanded ? 'documentShowLessFields' : 'documentShowMoreFields'}
onClick={(e: React.MouseEvent<HTMLElement>) => {
e.stopPropagation();
setIsExpanded(!isExpanded);
}}
aria-label={tooltipText}
/>
</EuiToolTip>
</EuiFlexItem>
</EuiFlexGroup>
</EuiSplitPanel.Inner>
<EuiHorizontalRule margin="none" />
<EuiSplitPanel.Inner paddingSize="m">
<ResultFields
isExpanded={isExpanded}
fields={isExpanded ? fields : fields.slice(0, defaultVisibleFields)}
/>
</EuiSplitPanel.Inner>
</EuiSplitPanel.Outer>
); );
}; };

View file

@ -9,17 +9,12 @@
import React from 'react'; import React from 'react';
import { import { EuiTableRow, EuiTableRowCell, EuiText, EuiToken } from '@elastic/eui';
EuiCodeBlock,
EuiIcon,
EuiTableRow,
EuiTableRowCell,
EuiText,
EuiToken,
} from '@elastic/eui';
import { euiThemeVars } from '@kbn/ui-theme'; import { euiThemeVars } from '@kbn/ui-theme';
import { ResultFieldProps } from './result_types'; import { ResultFieldProps } from './result_types';
import { PERMANENTLY_TRUNCATED_FIELDS } from './constants';
import { ResultFieldValue } from './result_field_value';
const iconMap: Record<string, string> = { const iconMap: Record<string, string> = {
boolean: 'tokenBoolean', boolean: 'tokenBoolean',
@ -64,9 +59,11 @@ export const ResultField: React.FC<ResultFieldProps> = ({
iconType, iconType,
fieldName, fieldName,
fieldValue, fieldValue,
fieldType, fieldType = 'object',
isExpanded, isExpanded,
}) => { }) => {
const shouldTruncate = !isExpanded || PERMANENTLY_TRUNCATED_FIELDS.includes(fieldType);
return ( return (
<EuiTableRow className="resultField"> <EuiTableRow className="resultField">
<EuiTableRowCell className="resultFieldRowCell" width={euiThemeVars.euiSizeL} valign="middle"> <EuiTableRowCell className="resultFieldRowCell" width={euiThemeVars.euiSizeL} valign="middle">
@ -79,31 +76,16 @@ export const ResultField: React.FC<ResultFieldProps> = ({
</EuiTableRowCell> </EuiTableRowCell>
<EuiTableRowCell <EuiTableRowCell
className="resultFieldRowCell" className="resultFieldRowCell"
width="25%" width="20%"
truncateText={!isExpanded} truncateText={!isExpanded}
valign="middle" valign="middle"
> >
<EuiText size="xs">{fieldName}</EuiText> <EuiText size="s" color="default">
{fieldName}
</EuiText>
</EuiTableRowCell> </EuiTableRowCell>
<EuiTableRowCell <EuiTableRowCell className="resultFieldRowCell" truncateText={shouldTruncate} valign="middle">
className="resultFieldRowCell" <ResultFieldValue fieldValue={fieldValue} fieldType={fieldType} isExpanded={isExpanded} />
width={euiThemeVars.euiSizeXXL}
valign="middle"
>
<EuiIcon type="sortRight" color="subdued" />
</EuiTableRowCell>
<EuiTableRowCell className="resultFieldRowCell" truncateText={!isExpanded} valign="middle">
{(fieldType === 'object' ||
fieldType === 'array' ||
fieldType === 'nested' ||
Array.isArray(fieldValue)) &&
isExpanded ? (
<EuiCodeBlock language="json" overflowHeight="250" transparentBackground>
{fieldValue}
</EuiCodeBlock>
) : (
<EuiText size="xs">{fieldValue}</EuiText>
)}
</EuiTableRowCell> </EuiTableRowCell>
</EuiTableRow> </EuiTableRow>
); );

View file

@ -0,0 +1,94 @@
/*
* 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import React from 'react';
import {
EuiBadge,
EuiCodeBlock,
EuiCopy,
EuiFlexGroup,
EuiFlexItem,
EuiIcon,
EuiText,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { PERMANENTLY_TRUNCATED_FIELDS } from './constants';
interface ResultFieldValueProps {
fieldValue: string;
fieldType: string;
isExpanded?: boolean;
}
export const ResultFieldValue: React.FC<ResultFieldValueProps> = ({
fieldValue,
fieldType,
isExpanded = false,
}) => {
if (
isExpanded &&
fieldType &&
(['object', 'array', 'nested'].includes(fieldType) || Array.isArray(fieldValue))
) {
return (
<EuiCodeBlock language="json" transparentBackground fontSize="s">
{fieldValue}
</EuiCodeBlock>
);
} else if (PERMANENTLY_TRUNCATED_FIELDS.includes(fieldType)) {
return (
<>
<EuiText size="s" color="default">
{fieldValue}
</EuiText>
{fieldType === 'dense_vector' && (
<div className={'denseVectorFieldValue'}>
<EuiFlexGroup justifyContent="center" alignItems="center" gutterSize="s">
<EuiFlexItem>
<EuiBadge color="hollow">
{i18n.translate('searchIndexDocuments.result.value.denseVector.dimLabel', {
defaultMessage: '{value} dims',
values: {
value: JSON.parse(fieldValue).length,
},
})}
</EuiBadge>
</EuiFlexItem>
<EuiFlexItem>
<EuiCopy textToCopy={fieldValue}>
{(copy) => (
<EuiIcon
type="copyClipboard"
onClick={copy}
data-test-subj="copyDenseVector"
aria-label={i18n.translate(
'searchIndexDocuments.result.value.denseVector.copy',
{
defaultMessage: 'Copy vector',
}
)}
/>
)}
</EuiCopy>
</EuiFlexItem>
</EuiFlexGroup>
</div>
)}
</>
);
} else {
return (
<EuiText size="s" color="default">
{fieldValue}
</EuiText>
);
}
};

View file

@ -17,8 +17,8 @@ import {
EuiPopover, EuiPopover,
EuiPopoverFooter, EuiPopoverFooter,
EuiPopoverTitle, EuiPopoverTitle,
EuiText,
EuiTextColor, EuiTextColor,
EuiTitle,
} from '@elastic/eui'; } from '@elastic/eui';
import { i18n } from '@kbn/i18n'; import { i18n } from '@kbn/i18n';
@ -95,18 +95,18 @@ const MetadataPopover: React.FC<MetaDataProps> = ({ id, onDocumentDelete }) => {
export const ResultHeader: React.FC<Props> = ({ title, metaData }) => { export const ResultHeader: React.FC<Props> = ({ title, metaData }) => {
return ( return (
<div className="resultHeader"> <div className="resultHeader">
<EuiText size="s"> <EuiFlexGroup alignItems="center" justifyContent="spaceBetween" gutterSize="s">
<EuiFlexGroup alignItems="center" gutterSize="s"> <EuiFlexItem>
<EuiFlexItem> <EuiTitle size="xs">
<strong>{title}</strong> <h4>{title}</h4>
</EuiTitle>
</EuiFlexItem>
{!!metaData && (
<EuiFlexItem grow={false}>
<MetadataPopover {...metaData} />
</EuiFlexItem> </EuiFlexItem>
{!!metaData && ( )}
<EuiFlexItem grow={false}> </EuiFlexGroup>
<MetadataPopover {...metaData} />
</EuiFlexItem>
)}
</EuiFlexGroup>
</EuiText>
</div> </div>
); );
}; };

View file

@ -9,7 +9,8 @@
import { SearchHit } from '@elastic/elasticsearch/lib/api/types'; import { SearchHit } from '@elastic/elasticsearch/lib/api/types';
import { resultTitle } from './result_metadata'; import { reorderFieldsInImportance, resultTitle } from './result_metadata';
import { FieldProps } from './result_types';
const makeSearchHit = (source: undefined | unknown): SearchHit => const makeSearchHit = (source: undefined | unknown): SearchHit =>
({ ({
@ -27,3 +28,45 @@ describe('resultTitle', () => {
expect(resultTitle(makeSearchHit(undefined))).toEqual(undefined); expect(resultTitle(makeSearchHit(undefined))).toEqual(undefined);
}); });
}); });
describe('reorderFieldsInImportance', () => {
it('sorts fields by type and name', () => {
const fields: FieldProps[] = [
{ fieldName: 'field6', fieldType: 'sparse_vector', fieldValue: 'value6' },
{ fieldName: 'field1', fieldType: 'semantic_text', fieldValue: 'value1' },
{ fieldName: 'field2', fieldType: 'dense_vector', fieldValue: 'value2' },
{ fieldName: 'field3', fieldType: 'sparse_vector', fieldValue: 'value3' },
{ fieldName: 'field4', fieldType: 'semantic_text', fieldValue: 'value4' },
{ fieldName: 'field5', fieldType: 'dense_vector', fieldValue: 'value5' },
];
const sortedFields = [
{ fieldName: 'field1', fieldType: 'semantic_text', fieldValue: 'value1' },
{ fieldName: 'field4', fieldType: 'semantic_text', fieldValue: 'value4' },
{ fieldName: 'field2', fieldType: 'dense_vector', fieldValue: 'value2' },
{ fieldName: 'field5', fieldType: 'dense_vector', fieldValue: 'value5' },
{ fieldName: 'field3', fieldType: 'sparse_vector', fieldValue: 'value3' },
{ fieldName: 'field6', fieldType: 'sparse_vector', fieldValue: 'value6' },
];
expect(reorderFieldsInImportance(fields)).toEqual(sortedFields);
});
it('sorts fields if they are special fields', () => {
const fields: FieldProps[] = [
{ fieldName: 'field2', fieldType: 'dense_vector', fieldValue: 'value2' },
{ fieldName: 'body_content', fieldType: 'sparse_vector', fieldValue: 'value3' },
{ fieldName: 'headings', fieldType: 'text', fieldValue: 'value1' },
{ fieldName: 'field4', fieldType: 'semantic_text', fieldValue: 'value4' },
{ fieldName: 'field5', fieldType: 'dense_vector', fieldValue: 'value5' },
{ fieldName: 'field6', fieldType: 'sparse_vector', fieldValue: 'value6' },
];
const sortedFields = [
{ fieldName: 'headings', fieldType: 'text', fieldValue: 'value1' },
{ fieldName: 'field4', fieldType: 'semantic_text', fieldValue: 'value4' },
{ fieldName: 'field2', fieldType: 'dense_vector', fieldValue: 'value2' },
{ fieldName: 'field5', fieldType: 'dense_vector', fieldValue: 'value5' },
{ fieldName: 'body_content', fieldType: 'sparse_vector', fieldValue: 'value3' },
{ fieldName: 'field6', fieldType: 'sparse_vector', fieldValue: 'value6' },
];
expect(reorderFieldsInImportance(fields)).toEqual(sortedFields);
});
});

View file

@ -12,7 +12,7 @@ import type {
MappingProperty, MappingProperty,
SearchHit, SearchHit,
} from '@elastic/elasticsearch/lib/api/types'; } from '@elastic/elasticsearch/lib/api/types';
import type { MetaDataProps } from './result_types'; import type { MetaDataProps, FieldProps } from './result_types';
const TITLE_KEYS = ['title', 'name']; const TITLE_KEYS = ['title', 'name'];
@ -44,10 +44,45 @@ export const resultMetaData = (result: SearchHit): MetaDataProps => ({
score: result._score, score: result._score,
}); });
const MAPPING_TYPE_ORDER = ['semantic_text', 'dense_vector', 'sparse_vector'];
const SPECIAL_NAME_FIELDS = ['headings'];
/**
* Reorders an array of fields based on their importance.
*
* The function sorts the fields by checking if their names are in the `SPECIAL_NAME_FIELDS` array first and then by
* their mapping type (semantic_text, dense_vector, sparse_vector) if they are not in the `SPECIAL_NAME_FIELDS` array.
*
* @param fields - An array of field properties to be reordered.
* @returns The reordered array of field properties.
*/
export const reorderFieldsInImportance = (fields: FieldProps[]) => {
return fields.sort((a, b) => {
const specialA = SPECIAL_NAME_FIELDS.indexOf(a.fieldName);
const specialB = SPECIAL_NAME_FIELDS.indexOf(b.fieldName);
if (specialA !== -1 || specialB !== -1) {
if (specialA === -1) return 1;
if (specialB === -1) return -1;
return specialA - specialB;
}
const typeA = MAPPING_TYPE_ORDER.indexOf(a.fieldType);
const typeB = MAPPING_TYPE_ORDER.indexOf(b.fieldType);
const orderA = typeA === -1 ? MAPPING_TYPE_ORDER.length : typeA;
const orderB = typeB === -1 ? MAPPING_TYPE_ORDER.length : typeB;
if (orderA === orderB) {
return a.fieldName.localeCompare(b.fieldName);
}
return orderA - orderB;
});
};
export const resultToFieldFromMappingResponse = ( export const resultToFieldFromMappingResponse = (
result: SearchHit, result: SearchHit,
mappings?: IndicesGetMappingResponse mappings?: IndicesGetMappingResponse
) => { ): FieldProps[] => {
if (mappings && mappings[result._index] && result._source && !Array.isArray(result._source)) { if (mappings && mappings[result._index] && result._source && !Array.isArray(result._source)) {
if (typeof result._source === 'object') { if (typeof result._source === 'object') {
return Object.entries(result._source).map(([key, value]) => { return Object.entries(result._source).map(([key, value]) => {
@ -65,7 +100,7 @@ export const resultToFieldFromMappingResponse = (
export const resultToFieldFromMappings = ( export const resultToFieldFromMappings = (
result: SearchHit, result: SearchHit,
mappings?: Record<string, MappingProperty> mappings?: Record<string, MappingProperty>
) => { ): FieldProps[] => {
if (mappings && result._source && !Array.isArray(result._source)) { if (mappings && result._source && !Array.isArray(result._source)) {
return Object.entries(result._source).map(([key, value]) => { return Object.entries(result._source).map(([key, value]) => {
return { return {

View file

@ -7,12 +7,12 @@
* License v3.0 only", or the "Server Side Public License, v 1". * License v3.0 only", or the "Server Side Public License, v 1".
*/ */
import { SearchHit } from '@elastic/elasticsearch/lib/api/types'; import { MappingProperty, SearchHit } from '@elastic/elasticsearch/lib/api/types';
import { IconType } from '@elastic/eui'; import { IconType } from '@elastic/eui';
export interface ResultFieldProps { export interface ResultFieldProps {
fieldName: string; fieldName: string;
fieldType?: string; fieldType: string;
fieldValue: string; fieldValue: string;
iconType?: IconType; iconType?: IconType;
isExpanded?: boolean; isExpanded?: boolean;
@ -24,3 +24,9 @@ export interface MetaDataProps {
score?: SearchHit['_score']; score?: SearchHit['_score'];
showScore?: boolean; showScore?: boolean;
} }
export interface FieldProps {
fieldName: string;
fieldType: Exclude<MappingProperty['type'], undefined>;
fieldValue: string;
}

View file

@ -67,7 +67,7 @@ const MetadataPopover: React.FC<MetaDataProps> = ({
const metaDataIcon = ( const metaDataIcon = (
<EuiButtonIcon <EuiButtonIcon
display="empty" display="empty"
size="xs" size="s"
iconType="iInCircle" iconType="iInCircle"
color="primary" color="primary"
data-test-subj="documentMetadataButton" data-test-subj="documentMetadataButton"
@ -186,7 +186,7 @@ export const RichResultHeader: React.FC<Props> = ({
<EuiFlexItem <EuiFlexItem
grow grow
css={css` css={css`
min-height: ${euiTheme.base * 3}px; min-height: ${euiTheme.base * 1}px;
max-height: ${euiTheme.base * 8}px; max-height: ${euiTheme.base * 8}px;
`} `}
> >
@ -204,12 +204,12 @@ export const RichResultHeader: React.FC<Props> = ({
<EuiFlexItem> <EuiFlexItem>
{onTitleClick ? ( {onTitleClick ? (
<EuiLink onClick={onTitleClick} color="text"> <EuiLink onClick={onTitleClick} color="text">
<EuiTitle size="xs"> <EuiTitle size="s">
<h4>{title}</h4> <h4>{title}</h4>
</EuiTitle> </EuiTitle>
</EuiLink> </EuiLink>
) : ( ) : (
<EuiTitle size="xs"> <EuiTitle size="s">
<h4>{title}</h4> <h4>{title}</h4>
</EuiTitle> </EuiTitle>
)} )}

View file

@ -12,6 +12,7 @@ import { Result, resultMetaData, resultToField } from '@kbn/search-index-documen
import { EuiSpacer } from '@elastic/eui'; import { EuiSpacer } from '@elastic/eui';
import { reorderFieldsInImportance } from '@kbn/search-index-documents';
import { RecentDocsActionMessage } from './recent_docs_action_message'; import { RecentDocsActionMessage } from './recent_docs_action_message';
import { useDeleteDocument } from '../../hooks/api/use_delete_document'; import { useDeleteDocument } from '../../hooks/api/use_delete_document';
@ -32,7 +33,7 @@ export const DocumentList = ({ indexName, docs, mappingProperties }: DocumentLis
return ( return (
<React.Fragment key={doc._id}> <React.Fragment key={doc._id}>
<Result <Result
fields={resultToField(doc, mappingProperties)} fields={reorderFieldsInImportance(resultToField(doc, mappingProperties))}
metaData={resultMetaData(doc)} metaData={resultMetaData(doc)}
onDocumentDelete={() => { onDocumentDelete={() => {
mutate({ id: doc._id! }); mutate({ id: doc._id! });