diff --git a/packages/kbn-search-index-documents/components/document_list.tsx b/packages/kbn-search-index-documents/components/document_list.tsx index 6c860a2f17b0..ec9efe7d6b1d 100644 --- a/packages/kbn-search-index-documents/components/document_list.tsx +++ b/packages/kbn-search-index-documents/components/document_list.tsx @@ -30,7 +30,7 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage, FormattedNumber } from '@kbn/i18n-react'; -import { resultMetaData } from './result/result_metadata'; +import { resultMetaData, resultToField } from './result/result_metadata'; import { Result } from '..'; interface DocumentListProps { @@ -55,20 +55,6 @@ export const DocumentList: React.FC = ({ setDocsPerPage, }) => { const [isPopoverOpen, setIsPopoverOpen] = useState(false); - const resultToField = (result: SearchHit) => { - if (mappings && 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', - fieldValue: JSON.stringify(value, null, 2), - }; - }); - } - } - return []; - }; const getIconType = (size: number) => { return size === docsPerPage ? 'check' : 'empty'; @@ -113,7 +99,7 @@ export const DocumentList: React.FC = ({ {docs.map((doc) => { return ( - + ); diff --git a/packages/kbn-search-index-documents/components/result/index.ts b/packages/kbn-search-index-documents/components/result/index.ts index e2be662bec51..a5e613fbd83e 100644 --- a/packages/kbn-search-index-documents/components/result/index.ts +++ b/packages/kbn-search-index-documents/components/result/index.ts @@ -8,3 +8,4 @@ */ export { Result } from './result'; +export { resultMetaData, resultToField } from './result_metadata'; 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 09a06878213a..783cd537b453 100644 --- a/packages/kbn-search-index-documents/components/result/result_metadata.ts +++ b/packages/kbn-search-index-documents/components/result/result_metadata.ts @@ -7,8 +7,8 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import { SearchHit } from '@elastic/elasticsearch/lib/api/types'; -import { MetaDataProps } from './result_types'; +import type { MappingProperty, SearchHit } from '@elastic/elasticsearch/lib/api/types'; +import type { MetaDataProps } from './result_types'; const TITLE_KEYS = ['title', 'name']; @@ -38,3 +38,18 @@ export const resultMetaData = (result: SearchHit): MetaDataProps => ({ id: result._id!, title: resultTitle(result), }); + +export const resultToField = (result: SearchHit, mappings?: Record) => { + if (mappings && 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', + fieldValue: JSON.stringify(value, null, 2), + }; + }); + } + } + return []; +}; diff --git a/x-pack/plugins/search_indices/public/components/index_documents/add_documents_code_example.tsx b/x-pack/plugins/search_indices/public/components/index_documents/add_documents_code_example.tsx new file mode 100644 index 000000000000..8d0b3af3f9d7 --- /dev/null +++ b/x-pack/plugins/search_indices/public/components/index_documents/add_documents_code_example.tsx @@ -0,0 +1,23 @@ +/* + * 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 { EuiPanel } from '@elastic/eui'; + +export const AddDocumentsCodeExample: React.FC = () => { + return ( + + TODO: WITHOUT DATA TICKET + + ); +}; diff --git a/x-pack/plugins/search_indices/public/components/index_documents/index_documents.tsx b/x-pack/plugins/search_indices/public/components/index_documents/index_documents.tsx new file mode 100644 index 000000000000..e431a62a3e53 --- /dev/null +++ b/x-pack/plugins/search_indices/public/components/index_documents/index_documents.tsx @@ -0,0 +1,114 @@ +/* + * 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 { Result, resultToField, resultMetaData } from '@kbn/search-index-documents'; + +import { i18n } from '@kbn/i18n'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiIcon, + EuiLink, + EuiPanel, + EuiProgress, + EuiSpacer, +} from '@elastic/eui'; +import { useIndexDocumentSearch } from '../../hooks/api/use_document_search'; +import { useIndexMapping } from '../../hooks/api/use_index_mappings'; +import { useKibana } from '../../hooks/use_kibana'; +import { AddDocumentsCodeExample } from './add_documents_code_example'; + +interface IndexDocumentsProps { + indexName: string; +} + +interface RecentDocsActionMessageProps { + indexName: string; +} + +const DEFAULT_PAGE_SIZE = 50; + +const RecentDocsActionMessage: React.FC = ({ indexName }) => { + const { + services: { share }, + } = useKibana(); + + const discoverLocator = share.url.locators.get('DISCOVER_APP_LOCATOR'); + + const onClick = async () => { + await discoverLocator?.navigate({ dataViewSpec: { title: indexName } }); + }; + + return ( + + + + + + +

+ {i18n.translate('xpack.searchIndices.indexDocuments.recentDocsActionMessage', { + defaultMessage: + 'You are viewing the {pageSize} most recently ingested documents in this index. To see all documents, view in', + values: { + pageSize: DEFAULT_PAGE_SIZE, + }, + })}{' '} + + {i18n.translate('xpack.searchIndices.indexDocuments.recentDocsActionMessageLink', { + defaultMessage: 'Discover.', + })} + +

+
+
+
+ ); +}; + +export const IndexDocuments: React.FC = ({ indexName }) => { + const { data: indexDocuments, isInitialLoading } = useIndexDocumentSearch(indexName, { + pageSize: DEFAULT_PAGE_SIZE, + pageIndex: 0, + }); + + const { data: mappingData } = useIndexMapping(indexName); + + const docs = indexDocuments?.results?.data ?? []; + const mappingProperties = mappingData?.mappings?.properties ?? {}; + + return ( + + + + + {isInitialLoading && } + {docs.length === 0 && } + {docs.length > 0 && ( + <> + + + {docs.map((doc) => { + return ( + + + + + ); + })} + + )} + + + + ); +}; diff --git a/x-pack/plugins/search_indices/public/components/indices/details_page.tsx b/x-pack/plugins/search_indices/public/components/indices/details_page.tsx index 85021e79edbf..ed498a777d71 100644 --- a/x-pack/plugins/search_indices/public/components/indices/details_page.tsx +++ b/x-pack/plugins/search_indices/public/components/indices/details_page.tsx @@ -7,10 +7,10 @@ import { EuiPageSection, - EuiSpacer, EuiButton, EuiPageTemplate, EuiFlexItem, + EuiTabbedContent, EuiFlexGroup, EuiPopover, EuiButtonIcon, @@ -30,6 +30,7 @@ import { useKibana } from '../../hooks/use_kibana'; import { ConnectionDetails } from '../connection_details/connection_details'; import { QuickStats } from '../quick_stats/quick_stats'; import { useIndexMapping } from '../../hooks/api/use_index_mappings'; +import { IndexDocuments } from '../index_documents/index_documents'; import { DeleteIndexModal } from './delete_index_modal'; import { IndexloadingError } from './details_page_loading_error'; @@ -157,31 +158,47 @@ export const SearchIndexDetailsPage = () => { , ]} /> - - - {isShowingDeleteModal && ( - setShowDeleteIndexModal(!isShowingDeleteModal)} - indexName={indexName} - navigateToIndexListPage={navigateToIndexListPage} - /> - )} - + - + + + + + {/* TODO: API KEY */} + + + + + + + + + + , + }, + ]} + /> + - {/* TODO: API KEY */} - - - - - - )} + {isShowingDeleteModal && ( + setShowDeleteIndexModal(!isShowingDeleteModal)} + indexName={indexName} + navigateToIndexListPage={navigateToIndexListPage} + /> + )} {embeddableConsole} ); diff --git a/x-pack/plugins/search_indices/public/hooks/api/use_document_search.ts b/x-pack/plugins/search_indices/public/hooks/api/use_document_search.ts new file mode 100644 index 000000000000..8a0570844b1f --- /dev/null +++ b/x-pack/plugins/search_indices/public/hooks/api/use_document_search.ts @@ -0,0 +1,53 @@ +/* + * 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 { Pagination } from '@elastic/eui'; +import { SearchHit } from '@kbn/es-types'; +import { pageToPagination, Paginate } from '@kbn/search-index-documents'; +import { useQuery } from '@tanstack/react-query'; +import { useKibana } from '../use_kibana'; + +interface IndexDocuments { + meta: Pagination; + results: Paginate; +} +const DEFAULT_PAGINATION = { + from: 0, + has_more_hits_than_total: false, + size: 10, + total: 0, +}; +const pollingInterval = 5 * 1000; +export const useIndexDocumentSearch = ( + indexName: string, + pagination: Omit, + searchQuery?: string +) => { + const { + services: { http }, + } = useKibana(); + const response = useQuery({ + queryKey: ['fetchIndexDocuments', pagination, searchQuery], + refetchInterval: pollingInterval, + refetchIntervalInBackground: true, + refetchOnWindowFocus: 'always', + queryFn: async () => + http.post(`/internal/serverless_search/indices/${indexName}/search`, { + body: JSON.stringify({ + searchQuery, + }), + query: { + page: pagination.pageIndex, + size: pagination.pageSize, + }, + }), + }); + return { + ...response, + meta: pageToPagination(response?.data?.results?._meta?.page ?? DEFAULT_PAGINATION), + }; +}; diff --git a/x-pack/plugins/search_indices/tsconfig.json b/x-pack/plugins/search_indices/tsconfig.json index c3623afcc0f2..56b67256e5b4 100644 --- a/x-pack/plugins/search_indices/tsconfig.json +++ b/x-pack/plugins/search_indices/tsconfig.json @@ -32,6 +32,8 @@ "@kbn/index-management-shared-types", "@kbn/try-in-console", "@kbn/cloud-plugin", + "@kbn/search-index-documents", + "@kbn/es-types", ], "exclude": [ "target/**/*", diff --git a/x-pack/test_serverless/functional/page_objects/svl_search_index_detail_page.ts b/x-pack/test_serverless/functional/page_objects/svl_search_index_detail_page.ts index 09b69aaed533..4c5a53a9a30e 100644 --- a/x-pack/test_serverless/functional/page_objects/svl_search_index_detail_page.ts +++ b/x-pack/test_serverless/functional/page_objects/svl_search_index_detail_page.ts @@ -66,6 +66,16 @@ export function SvlSearchIndexDetailPageProvider({ getService }: FtrProviderCont await testSubjects.missingOrFail('setupAISearchButton', { timeout: 2000 }); }, + async expectAddDocumentCodeExamples() { + await testSubjects.existOrFail('SearchIndicesAddDocumentsCode', { timeout: 2000 }); + }, + + async expectHasIndexDocuments() { + await retry.try(async () => { + await testSubjects.existOrFail('search-index-documents-result', { timeout: 2000 }); + }); + }, + async expectMoreOptionsActionButtonExists() { await testSubjects.existOrFail('moreOptionsActionButton'); }, diff --git a/x-pack/test_serverless/functional/test_suites/search/search_index_detail.ts b/x-pack/test_serverless/functional/test_suites/search/search_index_detail.ts index cd39079274d0..da707ca55ac8 100644 --- a/x-pack/test_serverless/functional/test_suites/search/search_index_detail.ts +++ b/x-pack/test_serverless/functional/test_suites/search/search_index_detail.ts @@ -61,10 +61,25 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { }, }); await svlSearchNavigation.navigateToIndexDetailPage(indexName); - await pageObjects.svlSearchIndexDetailPage.expectQuickStatsAIMappingsToHaveVectorFields(); }); + it('should show code examples for adding documents', async () => { + await pageObjects.svlSearchIndexDetailPage.expectAddDocumentCodeExamples(); + }); + + it('should have index documents', async () => { + await es.index({ + index: indexName, + body: { + my_field: [1, 0, 1], + }, + }); + + await svlSearchNavigation.navigateToIndexDetailPage(indexName); + await pageObjects.svlSearchIndexDetailPage.expectHasIndexDocuments(); + }); + it('back to indices button should redirect to list page', async () => { await pageObjects.svlSearchIndexDetailPage.expectBackToIndicesButtonExists(); await pageObjects.svlSearchIndexDetailPage.clickBackToIndicesButton();