From 272f3bb07fe7c5671afaaa439dd198e49a6cb631 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Wed, 2 Oct 2024 23:57:21 +1000 Subject: [PATCH] [8.x] Update Search Index Document Card design. (#194061) (#194555) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Backport This will backport the following commits from `main` to `8.x`: - [Update Search Index Document Card design. (#194061)](https://github.com/elastic/kibana/pull/194061) ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sqren/backport) Co-authored-by: Efe Gürkan YALAMAN Co-authored-by: Elastic Machine Co-authored-by: Alex Szabo --- .../components/document_list.tsx | 163 +++++++------ .../components/documents_list.test.tsx | 10 +- .../components/result/index.ts | 6 +- .../components/result/result.tsx | 135 ++++++++--- .../components/result/result_metadata.ts | 32 ++- .../components/result/result_types.ts | 3 + .../components/result/rich_result_header.tsx | 228 ++++++++++++++++++ .../components/search_index/documents.tsx | 2 +- .../index_documents/document_list.tsx | 2 +- .../plugins/search_playground/common/types.ts | 1 + .../public/components/app.tsx | 6 +- .../components/search_mode/empty_results.tsx | 34 --- .../components/search_mode/result_list.tsx | 122 +++------- .../components/search_mode/search_mode.tsx | 6 +- .../public/hooks/use_index_mappings.ts | 45 ++++ .../public/hooks/use_search_preview.ts | 8 +- .../public/utils/pagination_helper.ts | 7 - .../search_playground/server/routes.ts | 43 ++++ .../plugins/search_playground/tsconfig.json | 3 +- .../components/index_documents/documents.tsx | 2 +- 20 files changed, 611 insertions(+), 247 deletions(-) create mode 100644 packages/kbn-search-index-documents/components/result/rich_result_header.tsx delete mode 100644 x-pack/plugins/search_playground/public/components/search_mode/empty_results.tsx create mode 100644 x-pack/plugins/search_playground/public/hooks/use_index_mappings.ts diff --git a/packages/kbn-search-index-documents/components/document_list.tsx b/packages/kbn-search-index-documents/components/document_list.tsx index ec9efe7d6b1d7..5491d228e7d07 100644 --- a/packages/kbn-search-index-documents/components/document_list.tsx +++ b/packages/kbn-search-index-documents/components/document_list.tsx @@ -9,7 +9,7 @@ import React, { useState } from 'react'; -import { MappingProperty, SearchHit } from '@elastic/elasticsearch/lib/api/types'; +import type { IndicesGetMappingResponse, SearchHit } from '@elastic/elasticsearch/lib/api/types'; import { EuiButtonEmpty, @@ -30,18 +30,22 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage, FormattedNumber } from '@kbn/i18n-react'; -import { resultMetaData, resultToField } from './result/result_metadata'; +import { resultMetaData, resultToFieldFromMappingResponse } from './result/result_metadata'; import { Result } from '..'; +import { type ResultProps } from './result/result'; + interface DocumentListProps { dataTelemetryIdPrefix: string; docs: SearchHit[]; docsPerPage: number; isLoading: boolean; - mappings: Record | undefined; + mappings: IndicesGetMappingResponse | undefined; meta: Pagination; onPaginate: (newPageIndex: number) => void; - setDocsPerPage: (docsPerPage: number) => void; + setDocsPerPage?: (docsPerPage: number) => void; + onDocumentClick?: (doc: SearchHit) => void; + resultProps?: Partial; } export const DocumentList: React.FC = ({ @@ -53,6 +57,8 @@ export const DocumentList: React.FC = ({ meta, onPaginate, setDocsPerPage, + onDocumentClick, + resultProps = {}, }) => { const [isPopoverOpen, setIsPopoverOpen] = useState(false); @@ -99,7 +105,12 @@ export const DocumentList: React.FC = ({ {docs.map((doc) => { return ( - + onDocumentClick(doc) : undefined} + {...resultProps} + /> ); @@ -116,81 +127,83 @@ export const DocumentList: React.FC = ({ onPageClick={onPaginate} /> - - { - setIsPopoverOpen(true); - }} - > - {i18n.translate('searchIndexDocuments.documentList.pagination.itemsPerPage', { - defaultMessage: 'Documents per page: {docPerPage}', - values: { docPerPage: docsPerPage }, - })} - - } - isOpen={isPopoverOpen} - closePopover={() => { - setIsPopoverOpen(false); - }} - panelPaddingSize="none" - anchorPosition="downLeft" - > - + { - setIsPopoverOpen(false); - setDocsPerPage(10); + setIsPopoverOpen(true); }} > - {i18n.translate('searchIndexDocuments.documentList.paginationOptions.option', { - defaultMessage: '{docCount} documents', - values: { docCount: 10 }, + {i18n.translate('searchIndexDocuments.documentList.pagination.itemsPerPage', { + defaultMessage: 'Documents per page: {docPerPage}', + values: { docPerPage: docsPerPage }, })} - , + + } + isOpen={isPopoverOpen} + closePopover={() => { + setIsPopoverOpen(false); + }} + panelPaddingSize="none" + anchorPosition="downLeft" + > + { + setIsPopoverOpen(false); + setDocsPerPage(10); + }} + > + {i18n.translate('searchIndexDocuments.documentList.paginationOptions.option', { + defaultMessage: '{docCount} documents', + values: { docCount: 10 }, + })} + , - { - setIsPopoverOpen(false); - setDocsPerPage(25); - }} - > - {i18n.translate('searchIndexDocuments.documentList.paginationOptions.option', { - defaultMessage: '{docCount} documents', - values: { docCount: 25 }, - })} - , - { - setIsPopoverOpen(false); - setDocsPerPage(50); - }} - > - {i18n.translate('searchIndexDocuments.documentList.paginationOptions.option', { - defaultMessage: '{docCount} documents', - values: { docCount: 50 }, - })} - , - ]} - /> - - + { + setIsPopoverOpen(false); + setDocsPerPage(25); + }} + > + {i18n.translate('searchIndexDocuments.documentList.paginationOptions.option', { + defaultMessage: '{docCount} documents', + values: { docCount: 25 }, + })} + , + { + setIsPopoverOpen(false); + setDocsPerPage(50); + }} + > + {i18n.translate('searchIndexDocuments.documentList.paginationOptions.option', { + defaultMessage: '{docCount} documents', + values: { docCount: 50 }, + })} + , + ]} + /> + + + )} diff --git a/packages/kbn-search-index-documents/components/documents_list.test.tsx b/packages/kbn-search-index-documents/components/documents_list.test.tsx index b445c5fa48711..b97b36989ad62 100644 --- a/packages/kbn-search-index-documents/components/documents_list.test.tsx +++ b/packages/kbn-search-index-documents/components/documents_list.test.tsx @@ -58,8 +58,14 @@ describe('DocumentList', () => { }, ], mappings: { - AvgTicketPrice: { - type: 'float' as const, + kibana_sample_data_flights: { + mappings: { + properties: { + AvgTicketPrice: { + type: 'float' as const, + }, + }, + }, }, }, }; diff --git a/packages/kbn-search-index-documents/components/result/index.ts b/packages/kbn-search-index-documents/components/result/index.ts index a5e613fbd83ec..8c894f7e2ca3b 100644 --- a/packages/kbn-search-index-documents/components/result/index.ts +++ b/packages/kbn-search-index-documents/components/result/index.ts @@ -8,4 +8,8 @@ */ export { Result } from './result'; -export { resultMetaData, resultToField } from './result_metadata'; +export { + resultMetaData, + resultToFieldFromMappingResponse, + resultToFieldFromMappings as resultToField, +} from './result_metadata'; diff --git a/packages/kbn-search-index-documents/components/result/result.tsx b/packages/kbn-search-index-documents/components/result/result.tsx index 4e6f0ed8c6eb8..5e1c4db104116 100644 --- a/packages/kbn-search-index-documents/components/result/result.tsx +++ b/packages/kbn-search-index-documents/components/result/result.tsx @@ -9,76 +9,147 @@ import React, { useState } from 'react'; -import { EuiButtonIcon, EuiFlexGroup, EuiFlexItem, EuiPanel, EuiToolTip } from '@elastic/eui'; +import { + EuiButtonIcon, + EuiFlexGroup, + EuiFlexItem, + EuiPanel, + EuiSpacer, + EuiToolTip, +} from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { ResultFields } from './results_fields'; -import { ResultHeader } from './result_header'; import './result.scss'; import { MetaDataProps, ResultFieldProps } from './result_types'; +import { RichResultHeader } from './rich_result_header'; +import { ResultHeader } from './result_header'; + +export const DEFAULT_VISIBLE_FIELDS = 3; -interface ResultProps { +export interface ResultProps { fields: ResultFieldProps[]; metaData: MetaDataProps; + defaultVisibleFields?: number; + showScore?: boolean; + compactCard?: boolean; + onDocumentClick?: () => void; } -export const Result: React.FC = ({ metaData, fields }) => { +export const Result: React.FC = ({ + metaData, + fields, + defaultVisibleFields = DEFAULT_VISIBLE_FIELDS, + compactCard = true, + showScore = false, + onDocumentClick, +}) => { const [isExpanded, setIsExpanded] = useState(false); const tooltipText = - fields.length <= 3 + fields.length <= defaultVisibleFields ? i18n.translate('searchIndexDocuments.result.expandTooltip.allVisible', { defaultMessage: 'All fields are visible', }) : isExpanded ? i18n.translate('searchIndexDocuments.result.expandTooltip.showFewer', { defaultMessage: 'Show {amount} fewer fields', - values: { amount: fields.length - 3 }, + values: { amount: fields.length - defaultVisibleFields }, }) : i18n.translate('searchIndexDocuments.result.expandTooltip.showMore', { defaultMessage: 'Show {amount} more fields', - values: { amount: fields.length - 3 }, + values: { amount: fields.length - defaultVisibleFields }, }); const toolTipContent = <>{tooltipText}; return ( - + - + - + {compactCard && ( + + )} + {!compactCard && ( + + + ) => { + e.stopPropagation(); + setIsExpanded(!isExpanded); + }} + aria-label={tooltipText} + /> + + + } + /> + )} + {!compactCard && + ((isExpanded && fields.length > 0) || + (!isExpanded && fields.slice(0, defaultVisibleFields).length > 0)) && ( + + )} - -
- - setIsExpanded(!isExpanded)} - aria-label={tooltipText} - /> - -
-
+ {compactCard && ( + +
+ + ) => { + e.stopPropagation(); + setIsExpanded(!isExpanded); + }} + aria-label={tooltipText} + /> + +
+
+ )}
); diff --git a/packages/kbn-search-index-documents/components/result/result_metadata.ts b/packages/kbn-search-index-documents/components/result/result_metadata.ts index 783cd537b4535..ba50644cafc59 100644 --- a/packages/kbn-search-index-documents/components/result/result_metadata.ts +++ b/packages/kbn-search-index-documents/components/result/result_metadata.ts @@ -7,7 +7,11 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import type { MappingProperty, SearchHit } from '@elastic/elasticsearch/lib/api/types'; +import type { + IndicesGetMappingResponse, + MappingProperty, + SearchHit, +} from '@elastic/elasticsearch/lib/api/types'; import type { MetaDataProps } from './result_types'; const TITLE_KEYS = ['title', 'name']; @@ -37,15 +41,19 @@ export const resultTitle = (result: SearchHit): string | undefined => { export const resultMetaData = (result: SearchHit): MetaDataProps => ({ id: result._id!, title: resultTitle(result), + score: result._score, }); -export const resultToField = (result: SearchHit, mappings?: Record) => { - if (mappings && result._source && !Array.isArray(result._source)) { +export const resultToFieldFromMappingResponse = ( + result: SearchHit, + mappings?: IndicesGetMappingResponse +) => { + if (mappings && mappings[result._index] && result._source && !Array.isArray(result._source)) { if (typeof result._source === 'object') { return Object.entries(result._source).map(([key, value]) => { return { fieldName: key, - fieldType: mappings[key]?.type ?? 'object', + fieldType: mappings[result._index]?.mappings?.properties?.[key]?.type ?? 'object', fieldValue: JSON.stringify(value, null, 2), }; }); @@ -53,3 +61,19 @@ export const resultToField = (result: SearchHit, mappings?: Record +) => { + if (mappings && result._source && !Array.isArray(result._source)) { + return Object.entries(result._source).map(([key, value]) => { + return { + fieldName: key, + fieldType: mappings[key]?.type ?? 'object', + fieldValue: JSON.stringify(value, null, 2), + }; + }); + } + return []; +}; diff --git a/packages/kbn-search-index-documents/components/result/result_types.ts b/packages/kbn-search-index-documents/components/result/result_types.ts index e04ccb1091c8e..c7899874b27ee 100644 --- a/packages/kbn-search-index-documents/components/result/result_types.ts +++ b/packages/kbn-search-index-documents/components/result/result_types.ts @@ -7,6 +7,7 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ +import { SearchHit } from '@elastic/elasticsearch/lib/api/types'; import { IconType } from '@elastic/eui'; export interface ResultFieldProps { @@ -20,4 +21,6 @@ export interface MetaDataProps { id: string; onDocumentDelete?: Function; title?: string; + score?: SearchHit['_score']; + showScore?: boolean; } diff --git a/packages/kbn-search-index-documents/components/result/rich_result_header.tsx b/packages/kbn-search-index-documents/components/result/rich_result_header.tsx new file mode 100644 index 0000000000000..7caff8514871f --- /dev/null +++ b/packages/kbn-search-index-documents/components/result/rich_result_header.tsx @@ -0,0 +1,228 @@ +/* + * 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, { useState } from 'react'; + +import { + EuiButton, + EuiButtonIcon, + EuiFlexGroup, + EuiFlexItem, + EuiIcon, + EuiLink, + EuiPanel, + EuiPopover, + EuiPopoverFooter, + EuiPopoverTitle, + EuiText, + EuiTextColor, + EuiTitle, + useEuiTheme, +} from '@elastic/eui'; + +import { i18n } from '@kbn/i18n'; +import { css } from '@emotion/react'; +import { MetaDataProps } from './result_types'; + +interface Props { + metaData: MetaDataProps; + title: string; + rightSideActions?: React.ReactNode; + showScore?: boolean; + onTitleClick?: () => void; +} + +interface TermDef { + label: string | number; +} + +const Term: React.FC = ({ label }) => ( + + + {label}: + + +); + +const Definition: React.FC = ({ label }) => ( + + {label} + +); +const MetadataPopover: React.FC = ({ + id, + onDocumentDelete, + score, + showScore = false, +}) => { + const [popoverIsOpen, setPopoverIsOpen] = useState(false); + const closePopover = () => setPopoverIsOpen(false); + + const metaDataIcon = ( + ) => { + e.stopPropagation(); + setPopoverIsOpen(!popoverIsOpen); + }} + aria-label={i18n.translate('searchIndexDocuments.result.header.metadata.icon.ariaLabel', { + defaultMessage: 'Metadata for document: {id}', + values: { id }, + })} + /> + ); + + return ( + + + {i18n.translate('searchIndexDocuments.result.header.metadata.title', { + defaultMessage: 'Document metadata', + })} + + + + + + + + + + {score && showScore && ( + + + + + + + )} + + {onDocumentDelete && ( + + ) => { + e.stopPropagation(); + closePopover(); + }} + fullWidth + > + {i18n.translate('searchIndexDocuments.result.header.metadata.deleteDocument', { + defaultMessage: 'Delete document', + })} + + + )} + + ); +}; + +const Score: React.FC<{ score: MetaDataProps['score'] }> = ({ score }) => { + return ( + + + + + + + + + {score ? score.toString().substring(0, 5) : '-'} + + + + + + ); +}; + +export const RichResultHeader: React.FC = ({ + title, + metaData, + rightSideActions = null, + showScore = false, + onTitleClick, +}) => { + const { euiTheme } = useEuiTheme(); + return ( + + + {showScore && ( + + + + )} + + + + + + + {onTitleClick ? ( + + +

{title}

+
+
+ ) : ( + +

{title}

+
+ )} +
+ {!!metaData && ( + + + + )} +
+
+
+
+
+ {rightSideActions} +
+
+ ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/documents.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/documents.tsx index 353fc84546a9c..592da5d044f2a 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/documents.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/documents.tsx @@ -122,7 +122,7 @@ export const SearchIndexDocuments: React.FC = () => { docs={docs} docsPerPage={pagination.pageSize ?? 10} isLoading={status !== Status.SUCCESS && mappingStatus !== Status.SUCCESS} - mappings={mappingData?.mappings?.properties ?? {}} + mappings={mappingData ? { [indexName]: mappingData } : undefined} meta={data?.meta ?? DEFAULT_PAGINATION} onPaginate={(pageIndex) => setPagination({ ...pagination, pageIndex })} setDocsPerPage={(pageSize) => setPagination({ ...pagination, pageSize })} diff --git a/x-pack/plugins/search_indices/public/components/index_documents/document_list.tsx b/x-pack/plugins/search_indices/public/components/index_documents/document_list.tsx index ddf4f27122ef9..22735cc86c05f 100644 --- a/x-pack/plugins/search_indices/public/components/index_documents/document_list.tsx +++ b/x-pack/plugins/search_indices/public/components/index_documents/document_list.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { MappingProperty, SearchHit } from '@elastic/elasticsearch/lib/api/types'; -import { Result, resultToField, resultMetaData } from '@kbn/search-index-documents'; +import { Result, resultMetaData, resultToField } from '@kbn/search-index-documents'; import { EuiSpacer } from '@elastic/eui'; diff --git a/x-pack/plugins/search_playground/common/types.ts b/x-pack/plugins/search_playground/common/types.ts index c0e3300fe7dff..c239858b5b459 100644 --- a/x-pack/plugins/search_playground/common/types.ts +++ b/x-pack/plugins/search_playground/common/types.ts @@ -51,6 +51,7 @@ export enum APIRoutes { POST_QUERY_SOURCE_FIELDS = '/internal/search_playground/query_source_fields', GET_INDICES = '/internal/search_playground/indices', POST_SEARCH_QUERY = '/internal/search_playground/search', + GET_INDEX_MAPPINGS = '/internal/search_playground/mappings', } export enum LLMs { diff --git a/x-pack/plugins/search_playground/public/components/app.tsx b/x-pack/plugins/search_playground/public/components/app.tsx index 34b89433ea705..4f371ea5d15bb 100644 --- a/x-pack/plugins/search_playground/public/components/app.tsx +++ b/x-pack/plugins/search_playground/public/components/app.tsx @@ -106,7 +106,11 @@ export const App: React.FC = ({ css={{ position: 'relative', }} - contentProps={{ css: { display: 'flex', flexGrow: 1, position: 'absolute', inset: 0 } }} + contentProps={ + selectedPageMode === PlaygroundPageMode.search && selectedMode === 'chat' + ? undefined + : { css: { display: 'flex', flexGrow: 1, position: 'absolute', inset: 0 } } + } paddingSize={paddingSize} className="eui-fullHeight" > diff --git a/x-pack/plugins/search_playground/public/components/search_mode/empty_results.tsx b/x-pack/plugins/search_playground/public/components/search_mode/empty_results.tsx deleted file mode 100644 index ab5779e85ddd5..0000000000000 --- a/x-pack/plugins/search_playground/public/components/search_mode/empty_results.tsx +++ /dev/null @@ -1,34 +0,0 @@ -/* - * 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 { EuiEmptyPrompt } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; - -export interface EmptyResultsArgs { - query?: string; -} - -export const EmptyResults: React.FC = ({ query }) => { - return ( - - {query - ? i18n.translate('xpack.searchPlayground.resultList.emptyWithQuery.text', { - defaultMessage: 'No result found for: {query}', - values: { query }, - }) - : i18n.translate('xpack.searchPlayground.resultList.empty.text', { - defaultMessage: 'No results found', - })} -

- } - /> - ); -}; diff --git a/x-pack/plugins/search_playground/public/components/search_mode/result_list.tsx b/x-pack/plugins/search_playground/public/components/search_mode/result_list.tsx index 02e1193e22332..87c7060c29151 100644 --- a/x-pack/plugins/search_playground/public/components/search_mode/result_list.tsx +++ b/x-pack/plugins/search_playground/public/components/search_mode/result_list.tsx @@ -7,40 +7,29 @@ import React, { useEffect, useState } from 'react'; -import { - EuiFlexGroup, - EuiFlexItem, - EuiHorizontalRule, - EuiPagination, - EuiPanel, - EuiText, - EuiTitle, -} from '@elastic/eui'; - -import { UnifiedDocViewerFlyout } from '@kbn/unified-doc-viewer-plugin/public'; +import { DocumentList, pageToPagination } from '@kbn/search-index-documents'; import type { DataView } from '@kbn/data-views-plugin/public'; import type { EsHitRecord } from '@kbn/discover-utils/types'; -import type { SearchHit } from '@elastic/elasticsearch/lib/api/types'; +import type { IndicesGetMappingResponse, SearchHit } from '@elastic/elasticsearch/lib/api/types'; import { buildDataTableRecord } from '@kbn/discover-utils'; -import { i18n } from '@kbn/i18n'; -import { Pagination } from '../../types'; -import { getPageCounts } from '../../utils/pagination_helper'; -import { EmptyResults } from './empty_results'; +import { UnifiedDocViewerFlyout } from '@kbn/unified-doc-viewer-plugin/public'; +import { Pagination as PaginationTypeEui } from '@elastic/eui'; import { useKibana } from '../../hooks/use_kibana'; +import { Pagination } from '../../types'; export interface ResultListArgs { searchResults: SearchHit[]; + mappings?: IndicesGetMappingResponse; pagination: Pagination; onPaginationChange: (nextPage: number) => void; - searchQuery?: string; } export const ResultList: React.FC = ({ searchResults, + mappings, pagination, onPaginationChange, - searchQuery = '', }) => { const { services: { data }, @@ -50,73 +39,42 @@ export const ResultList: React.FC = ({ data.dataViews.getDefaultDataView().then((d) => setDataView(d)); }, [data]); const [flyoutDocId, setFlyoutDocId] = useState(undefined); - const { totalPage, page } = getPageCounts(pagination); + const documentMeta: PaginationTypeEui = pageToPagination(pagination); const hit = flyoutDocId && buildDataTableRecord(searchResults.find((item) => item._id === flyoutDocId) as EsHitRecord); return ( - - - {searchResults.length === 0 && ( - - - - )} - {searchResults.length !== 0 && - searchResults.map((item, index) => { - return ( - <> - setFlyoutDocId(item._id)} - grow - > - - - -

ID:{item._id}

-
-
- - -

- {i18n.translate('xpack.searchPlayground.resultList.result.score', { - defaultMessage: 'Document score: {score}', - values: { score: item._score }, - })} -

-
-
-
-
- {index !== searchResults.length - 1 && } - - ); - })} - {searchResults.length !== 0 && ( - - - - )} - {flyoutDocId && dataView && hit && ( - setFlyoutDocId(undefined)} - isEsqlQuery={false} - columns={[]} - hit={hit} - dataView={dataView} - onAddColumn={() => {}} - onRemoveColumn={() => {}} - setExpandedDoc={() => {}} - flyoutType="overlay" - /> - )} -
-
+ <> + setFlyoutDocId(searchHit._id)} + resultProps={{ + showScore: true, + compactCard: false, + defaultVisibleFields: 0, + }} + /> + + {flyoutDocId && dataView && hit && ( + setFlyoutDocId(undefined)} + isEsqlQuery={false} + columns={[]} + hit={hit} + dataView={dataView} + onAddColumn={() => {}} + onRemoveColumn={() => {}} + setExpandedDoc={() => {}} + flyoutType="overlay" + /> + )} + ); }; diff --git a/x-pack/plugins/search_playground/public/components/search_mode/search_mode.tsx b/x-pack/plugins/search_playground/public/components/search_mode/search_mode.tsx index 967c5786eed63..94e6337f1a03c 100644 --- a/x-pack/plugins/search_playground/public/components/search_mode/search_mode.tsx +++ b/x-pack/plugins/search_playground/public/components/search_mode/search_mode.tsx @@ -23,6 +23,7 @@ import { ResultList } from './result_list'; import { ChatForm, ChatFormFields, Pagination } from '../../types'; import { useSearchPreview } from '../../hooks/use_search_preview'; import { getPaginationFromPage } from '../../utils/pagination_helper'; +import { useIndexMappings } from '../../hooks/use_index_mappings'; export const SearchMode: React.FC = () => { const { euiTheme } = useEuiTheme(); @@ -40,6 +41,7 @@ export const SearchMode: React.FC = () => { }>({ query: searchBarValue, pagination: DEFAULT_PAGINATION }); const { results, pagination } = useSearchPreview(searchQuery); + const { data: mappingData } = useIndexMappings(); const queryClient = useQueryClient(); const handleSearch = async (query = searchBarValue, paginationParam = DEFAULT_PAGINATION) => { @@ -81,15 +83,15 @@ export const SearchMode: React.FC = () => { /> - + {searchQuery.query ? ( ) : ( { + const mappings = await http.post<{ + mappings: IndicesGetMappingResponse; + }>(APIRoutes.GET_INDEX_MAPPINGS, { + body: JSON.stringify({ + indices, + }), + }); + return mappings; +}; +export const useIndexMappings = () => { + const { + services: { http }, + } = useKibana(); + const { getValues } = useFormContext(); + const indices = getValues(ChatFormFields.indices); + const { data } = useQuery({ + queryKey: ['search-playground-index-mappings'], + queryFn: () => fetchIndexMappings({ indices, http }), + refetchOnWindowFocus: false, + refetchOnReconnect: false, + refetchOnMount: false, + }); + + return { data: data?.mappings }; +}; diff --git a/x-pack/plugins/search_playground/public/hooks/use_search_preview.ts b/x-pack/plugins/search_playground/public/hooks/use_search_preview.ts index 54566563fcee5..f66f81b37cd2e 100644 --- a/x-pack/plugins/search_playground/public/hooks/use_search_preview.ts +++ b/x-pack/plugins/search_playground/public/hooks/use_search_preview.ts @@ -8,6 +8,7 @@ import { SearchHit } from '@elastic/elasticsearch/lib/api/types'; import { useQuery } from '@tanstack/react-query'; import { useFormContext } from 'react-hook-form'; +import type { HttpSetup } from '@kbn/core-http-browser'; import { APIRoutes, ChatForm, ChatFormFields, Pagination } from '../types'; import { useKibana } from './use_kibana'; import { DEFAULT_PAGINATION } from '../../common'; @@ -17,7 +18,7 @@ export interface FetchSearchResultsArgs { pagination: Pagination; indices: ChatForm[ChatFormFields.indices]; elasticsearchQuery: ChatForm[ChatFormFields.elasticsearchQuery]; - http: ReturnType['services']['http']; + http: HttpSetup; } interface UseSearchPreviewData { @@ -64,9 +65,10 @@ export const useSearchPreview = ({ query: string; pagination: Pagination; }) => { - const { services } = useKibana(); + const { + services: { http }, + } = useKibana(); const { getValues } = useFormContext(); - const { http } = services; const indices = getValues(ChatFormFields.indices); const elasticsearchQuery = getValues(ChatFormFields.elasticsearchQuery); diff --git a/x-pack/plugins/search_playground/public/utils/pagination_helper.ts b/x-pack/plugins/search_playground/public/utils/pagination_helper.ts index 1379bbc257bd4..2602327b8c968 100644 --- a/x-pack/plugins/search_playground/public/utils/pagination_helper.ts +++ b/x-pack/plugins/search_playground/public/utils/pagination_helper.ts @@ -7,13 +7,6 @@ import { Pagination } from '../../common/types'; -export const getPageCounts = (pagination: Pagination) => { - const { total, from, size } = pagination; - const totalPage = Math.ceil(total / size); - const page = Math.floor(from / size); - return { totalPage, total, page, size }; -}; - export const getPaginationFromPage = (page: number, size: number, previousValue: Pagination) => { const from = page < 0 ? 0 : page * size; return { ...previousValue, from, size, page }; diff --git a/x-pack/plugins/search_playground/server/routes.ts b/x-pack/plugins/search_playground/server/routes.ts index d30904214d8df..cf6a139b1344b 100644 --- a/x-pack/plugins/search_playground/server/routes.ts +++ b/x-pack/plugins/search_playground/server/routes.ts @@ -305,4 +305,47 @@ export function defineRoutes({ } }) ); + router.post( + { + path: APIRoutes.GET_INDEX_MAPPINGS, + validate: { + body: schema.object({ + indices: schema.arrayOf(schema.string()), + }), + }, + }, + errorHandler(logger)(async (context, request, response) => { + const { client } = (await context.core).elasticsearch; + const { indices } = request.body; + + try { + if (indices.length === 0) { + return response.badRequest({ + body: { + message: 'Indices cannot be empty', + }, + }); + } + + const mappings = await client.asCurrentUser.indices.getMapping({ + index: indices, + }); + return response.ok({ + body: { + mappings, + }, + }); + } catch (e) { + logger.error('Failed to get index mappings', e); + if (typeof e === 'object' && e.message) { + return response.badRequest({ + body: { + message: e.message, + }, + }); + } + throw e; + } + }) + ); } diff --git a/x-pack/plugins/search_playground/tsconfig.json b/x-pack/plugins/search_playground/tsconfig.json index eebfd0df9a7b3..29c144ff4bac8 100644 --- a/x-pack/plugins/search_playground/tsconfig.json +++ b/x-pack/plugins/search_playground/tsconfig.json @@ -44,7 +44,8 @@ "@kbn/unified-doc-viewer-plugin", "@kbn/data-views-plugin", "@kbn/discover-utils", - "@kbn/data-plugin" + "@kbn/data-plugin", + "@kbn/search-index-documents" ], "exclude": [ "target/**/*", diff --git a/x-pack/plugins/serverless_search/public/application/components/index_documents/documents.tsx b/x-pack/plugins/serverless_search/public/application/components/index_documents/documents.tsx index d74c3a479a68a..0dc6e74d63ba6 100644 --- a/x-pack/plugins/serverless_search/public/application/components/index_documents/documents.tsx +++ b/x-pack/plugins/serverless_search/public/application/components/index_documents/documents.tsx @@ -63,7 +63,7 @@ export const IndexDocuments: React.FC = ({ indexName }) => docs={docs} docsPerPage={pagination.pageSize ?? 10} isLoading={false} - mappings={mappingData?.mappings?.properties ?? {}} + mappings={mappingData ? { [indexName]: mappingData } : undefined} meta={documentsMeta ?? DEFAULT_PAGINATION} onPaginate={(pageIndex) => setPagination({ ...pagination, pageIndex })} setDocsPerPage={(pageSize) => setPagination({ ...pagination, pageSize })}