From 3ba6e73e1972cf0188519395df320f7e09bb5042 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Thu, 26 Sep 2024 02:34:57 +1000 Subject: [PATCH] [8.x] [Search][Onboarding] Index Details - No Data view (#193637) (#194023) # Backport This will backport the following commits from `main` to `8.x`: - [[Search][Onboarding] Index Details - No Data view (#193637)](https://github.com/elastic/kibana/pull/193637) ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sqren/backport) Co-authored-by: Rodney Norris --- .../public/analytics/constants.ts | 3 + .../public/code_examples/constants.ts | 15 ++ .../public/code_examples/create_index.ts | 24 +-- .../public/code_examples/curl.ts | 29 +++- .../public/code_examples/ingest_data.ts | 35 ++++ .../public/code_examples/javascript.ts | 60 ++++++- .../public/code_examples/python.ts | 62 +++++++- .../public/code_examples/sense.ts | 14 +- .../public/code_examples/types.ts | 17 ++ .../add_documents_code_example.tsx | 149 +++++++++++++++++- .../components/index_documents/constants.ts | 8 + .../index_documents/document_list.tsx | 38 +++++ .../hooks/use_ingest_code_examples.tsx | 13 ++ .../index_documents/index_documents.tsx | 81 +--------- .../recent_docs_action_message.tsx | 56 +++++++ .../{start => shared}/code_sample.tsx | 6 +- .../components/shared/language_selector.tsx | 1 + .../components/start/create_index_code.tsx | 7 +- x-pack/plugins/search_indices/public/types.ts | 31 +++- .../public/utils/document_generation.test.ts | 143 +++++++++++++++++ .../public/utils/document_generation.ts | 107 +++++++++++++ .../search_indices/public/utils/language.ts | 26 +++ .../svl_search_index_detail_page.ts | 24 +++ .../test_suites/search/search_index_detail.ts | 14 ++ 24 files changed, 855 insertions(+), 108 deletions(-) create mode 100644 x-pack/plugins/search_indices/public/code_examples/constants.ts create mode 100644 x-pack/plugins/search_indices/public/code_examples/ingest_data.ts create mode 100644 x-pack/plugins/search_indices/public/code_examples/types.ts create mode 100644 x-pack/plugins/search_indices/public/components/index_documents/constants.ts create mode 100644 x-pack/plugins/search_indices/public/components/index_documents/document_list.tsx create mode 100644 x-pack/plugins/search_indices/public/components/index_documents/hooks/use_ingest_code_examples.tsx create mode 100644 x-pack/plugins/search_indices/public/components/index_documents/recent_docs_action_message.tsx rename x-pack/plugins/search_indices/public/components/{start => shared}/code_sample.tsx (89%) create mode 100644 x-pack/plugins/search_indices/public/utils/document_generation.test.ts create mode 100644 x-pack/plugins/search_indices/public/utils/document_generation.ts create mode 100644 x-pack/plugins/search_indices/public/utils/language.ts diff --git a/x-pack/plugins/search_indices/public/analytics/constants.ts b/x-pack/plugins/search_indices/public/analytics/constants.ts index 48ecec88ff0536..563e5b62382c06 100644 --- a/x-pack/plugins/search_indices/public/analytics/constants.ts +++ b/x-pack/plugins/search_indices/public/analytics/constants.ts @@ -13,4 +13,7 @@ export enum AnalyticsEvents { startCreateIndexLanguageSelect = 'start_code_lang_select', startCreateIndexCodeCopyInstall = 'start_code_copy_install', startCreateIndexCodeCopy = 'start_code_copy', + indexDetailsInstallCodeCopy = 'index_details_code_copy_install', + indexDetailsAddMappingsCodeCopy = 'index_details_add_mappings_code_copy', + indexDetailsIngestDocumentsCodeCopy = 'index_details_ingest_documents_code_copy', } diff --git a/x-pack/plugins/search_indices/public/code_examples/constants.ts b/x-pack/plugins/search_indices/public/code_examples/constants.ts new file mode 100644 index 00000000000000..31850ce7fbbe07 --- /dev/null +++ b/x-pack/plugins/search_indices/public/code_examples/constants.ts @@ -0,0 +1,15 @@ +/* + * 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 { i18n } from '@kbn/i18n'; + +export const UPLOAD_VECTORS_TITLE = i18n.translate( + 'xpack.searchIndices.codeExamples.ingest.denseVector.title', + { + defaultMessage: 'Upload vectors', + } +); diff --git a/x-pack/plugins/search_indices/public/code_examples/create_index.ts b/x-pack/plugins/search_indices/public/code_examples/create_index.ts index ec11283b2185f0..627329b37d0be5 100644 --- a/x-pack/plugins/search_indices/public/code_examples/create_index.ts +++ b/x-pack/plugins/search_indices/public/code_examples/create_index.ts @@ -7,21 +7,21 @@ import { CreateIndexCodeExamples } from '../types'; -import { CurlExamples } from './curl'; -import { JavascriptServerlessExamples } from './javascript'; -import { PythonServerlessExamples } from './python'; -import { ConsoleExamples } from './sense'; +import { CurlCreateIndexExamples } from './curl'; +import { JavascriptServerlessCreateIndexExamples } from './javascript'; +import { PythonServerlessCreateIndexExamples } from './python'; +import { ConsoleCreateIndexExamples } from './sense'; export const DefaultServerlessCodeExamples: CreateIndexCodeExamples = { - sense: ConsoleExamples.default, - curl: CurlExamples.default, - python: PythonServerlessExamples.default, - javascript: JavascriptServerlessExamples.default, + sense: ConsoleCreateIndexExamples.default, + curl: CurlCreateIndexExamples.default, + python: PythonServerlessCreateIndexExamples.default, + javascript: JavascriptServerlessCreateIndexExamples.default, }; export const DenseVectorSeverlessCodeExamples: CreateIndexCodeExamples = { - sense: ConsoleExamples.dense_vector, - curl: CurlExamples.dense_vector, - python: PythonServerlessExamples.dense_vector, - javascript: JavascriptServerlessExamples.dense_vector, + sense: ConsoleCreateIndexExamples.dense_vector, + curl: CurlCreateIndexExamples.dense_vector, + python: PythonServerlessCreateIndexExamples.dense_vector, + javascript: JavascriptServerlessCreateIndexExamples.dense_vector, }; diff --git a/x-pack/plugins/search_indices/public/code_examples/curl.ts b/x-pack/plugins/search_indices/public/code_examples/curl.ts index c5ee1d581250c7..a451f7cf089671 100644 --- a/x-pack/plugins/search_indices/public/code_examples/curl.ts +++ b/x-pack/plugins/search_indices/public/code_examples/curl.ts @@ -7,7 +7,8 @@ import { i18n } from '@kbn/i18n'; import { API_KEY_PLACEHOLDER, INDEX_PLACEHOLDER } from '../constants'; -import { CodeLanguage, CreateIndexLanguageExamples } from '../types'; +import { CodeLanguage, IngestDataCodeDefinition } from '../types'; +import { CreateIndexLanguageExamples } from './types'; export const CURL_INFO: CodeLanguage = { id: 'curl', @@ -16,7 +17,7 @@ export const CURL_INFO: CodeLanguage = { codeBlockLanguage: 'shell', }; -export const CurlExamples: CreateIndexLanguageExamples = { +export const CurlCreateIndexExamples: CreateIndexLanguageExamples = { default: { createIndex: ({ elasticsearchURL, apiKey, indexName }) => `curl PUT '${elasticsearchURL}/${ indexName ?? INDEX_PLACEHOLDER @@ -45,3 +46,27 @@ export const CurlExamples: CreateIndexLanguageExamples = { }'`, }, }; + +export const CurlVectorsIngestDataExample: IngestDataCodeDefinition = { + ingestCommand: ({ + elasticsearchURL, + apiKey, + indexName, + sampleDocument, + }) => `curl -X POST "${elasticsearchURL}/_bulk?pretty" \ +--header 'Authorization: ApiKey ${apiKey ?? API_KEY_PLACEHOLDER}' \ +--header 'Content-Type: application/json' \ +-d' +{ "index": { "_index": "${indexName}" } } +${JSON.stringify(sampleDocument)} +`, + updateMappingsCommand: ({ + elasticsearchURL, + apiKey, + indexName, + mappingProperties, + }) => `curl -X PUT "${elasticsearchURL}/${indexName}/_mapping" \ +--header 'Authorization: ApiKey ${apiKey ?? API_KEY_PLACEHOLDER}' \ +--header 'Content-Type: application/json' \ +--data-raw '${JSON.stringify({ properties: mappingProperties })}'`, +}; diff --git a/x-pack/plugins/search_indices/public/code_examples/ingest_data.ts b/x-pack/plugins/search_indices/public/code_examples/ingest_data.ts new file mode 100644 index 00000000000000..885a3832953388 --- /dev/null +++ b/x-pack/plugins/search_indices/public/code_examples/ingest_data.ts @@ -0,0 +1,35 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import { IngestDataCodeExamples } from '../types'; +import { UPLOAD_VECTORS_TITLE } from './constants'; + +import { JSServerlessIngestVectorDataExample } from './javascript'; +import { PythonServerlessVectorsIngestDataExample } from './python'; +import { ConsoleVectorsIngestDataExample } from './sense'; +import { CurlVectorsIngestDataExample } from './curl'; + +export const DenseVectorServerlessCodeExamples: IngestDataCodeExamples = { + title: UPLOAD_VECTORS_TITLE, + ingestTitle: UPLOAD_VECTORS_TITLE, + description: i18n.translate( + 'xpack.searchIndices.codeExamples.serverless.denseVector.description', + { + defaultMessage: + 'The following example connects to your Elasticsearch endpoint and uploads vectors to the index.', + } + ), + defaultMapping: { + vector: { type: 'dense_vector', dims: 3 }, + text: { type: 'text' }, + }, + sense: ConsoleVectorsIngestDataExample, + curl: CurlVectorsIngestDataExample, + python: PythonServerlessVectorsIngestDataExample, + javascript: JSServerlessIngestVectorDataExample, +}; diff --git a/x-pack/plugins/search_indices/public/code_examples/javascript.ts b/x-pack/plugins/search_indices/public/code_examples/javascript.ts index 26b7b5e8111ac3..3e91cb99301a71 100644 --- a/x-pack/plugins/search_indices/public/code_examples/javascript.ts +++ b/x-pack/plugins/search_indices/public/code_examples/javascript.ts @@ -7,7 +7,8 @@ import { i18n } from '@kbn/i18n'; import { API_KEY_PLACEHOLDER, INDEX_PLACEHOLDER } from '../constants'; -import { CodeLanguage, CreateIndexLanguageExamples } from '../types'; +import { CodeLanguage, IngestDataCodeDefinition } from '../types'; +import { CreateIndexLanguageExamples } from './types'; export const JAVASCRIPT_INFO: CodeLanguage = { id: 'javascript', @@ -20,7 +21,7 @@ export const JAVASCRIPT_INFO: CodeLanguage = { const SERVERLESS_INSTALL_CMD = `npm install @elastic/elasticsearch-serverless`; -export const JavascriptServerlessExamples: CreateIndexLanguageExamples = { +export const JavascriptServerlessCreateIndexExamples: CreateIndexLanguageExamples = { default: { installCommand: SERVERLESS_INSTALL_CMD, createIndex: ({ @@ -66,3 +67,58 @@ client.indices.create({ });`, }, }; + +export const JSServerlessIngestVectorDataExample: IngestDataCodeDefinition = { + installCommand: SERVERLESS_INSTALL_CMD, + ingestCommand: ({ + apiKey, + elasticsearchURL, + sampleDocument, + indexName, + }) => `import { Client } from "@elastic/elasticsearch"; + +const client = new Client({ + node: '${elasticsearchURL}', + auth: { + apiKey: "${apiKey ?? API_KEY_PLACEHOLDER}" + }, +}); + +const index = "${indexName}"; +const docs = [ +${JSON.stringify(sampleDocument, null, 2)}, +] + +const bulkIngestResponse = await client.helpers.bulk({ + index, + datasource: docs, + onDocument() { + return { + index: {}, + }; + } +}); +console.log(bulkIngestResponse);`, + updateMappingsCommand: ({ + apiKey, + elasticsearchURL, + indexName, + mappingProperties, + }) => `import { Client } from "@elastic/elasticsearch"; + +const client = new Client({ +node: '${elasticsearchURL}', +auth: { + apiKey: "${apiKey ?? API_KEY_PLACEHOLDER}" +} +}); + +const index = "${indexName}"; +const mapping = ${JSON.stringify(mappingProperties, null, 2)}; + +const updateMappingResponse = await client.indices.putMapping({ + index, + properties: mapping, +}); +console.log(updateMappingResponse);`, +}; diff --git a/x-pack/plugins/search_indices/public/code_examples/python.ts b/x-pack/plugins/search_indices/public/code_examples/python.ts index 0d9e778ca1060a..e41e542456e72a 100644 --- a/x-pack/plugins/search_indices/public/code_examples/python.ts +++ b/x-pack/plugins/search_indices/public/code_examples/python.ts @@ -7,7 +7,14 @@ import { i18n } from '@kbn/i18n'; import { API_KEY_PLACEHOLDER, INDEX_PLACEHOLDER } from '../constants'; -import { CodeLanguage, CodeSnippetParameters, CreateIndexLanguageExamples } from '../types'; +import { + CodeLanguage, + CodeSnippetParameters, + IngestCodeSnippetFunction, + IngestDataCodeDefinition, +} from '../types'; + +import { CreateIndexLanguageExamples } from './types'; export const PYTHON_INFO: CodeLanguage = { id: 'python', @@ -18,7 +25,7 @@ export const PYTHON_INFO: CodeLanguage = { const SERVERLESS_PYTHON_INSTALL_CMD = 'pip install elasticsearch-serverless'; -export const PythonServerlessExamples: CreateIndexLanguageExamples = { +export const PythonServerlessCreateIndexExamples: CreateIndexLanguageExamples = { default: { installCommand: SERVERLESS_PYTHON_INSTALL_CMD, createIndex: ({ @@ -60,3 +67,54 @@ client.indices.create( )`, }, }; +const serverlessIngestionCommand: IngestCodeSnippetFunction = ({ + elasticsearchURL, + apiKey, + indexName, + sampleDocument, +}) => `from elasticsearch-serverless import Elasticsearch, helpers + +client = Elasticsearch( + "${elasticsearchURL}", + api_key="${apiKey ?? API_KEY_PLACEHOLDER}" +) + +index_name = "${indexName}" + +docs = [ +${JSON.stringify(sampleDocument, null, 4)}, +] + +bulk_response = helpers.bulk(client, docs, index=index_name) +print(bulk_response)`; + +const serverlessUpdateMappingsCommand: IngestCodeSnippetFunction = ({ + elasticsearchURL, + apiKey, + indexName, + mappingProperties, +}) => `from elasticsearch-serverless import Elasticsearch + +client = Elasticsearch( +"${elasticsearchURL}", +api_key="${apiKey ?? API_KEY_PLACEHOLDER}" +) + +index_name = "${indexName}" + +mappings = ${JSON.stringify({ properties: mappingProperties }, null, 4)} + +update_mapping_response = client.indices.put_mapping(index=index_name, body=mappings) + +# Print the response +print(update_mapping_response) + +# Verify the mapping +mapping = client.indices.get_mapping(index=index_name) +print(mapping)`; + +export const PythonServerlessVectorsIngestDataExample: IngestDataCodeDefinition = { + installCommand: SERVERLESS_PYTHON_INSTALL_CMD, + ingestCommand: serverlessIngestionCommand, + updateMappingsCommand: serverlessUpdateMappingsCommand, +}; diff --git a/x-pack/plugins/search_indices/public/code_examples/sense.ts b/x-pack/plugins/search_indices/public/code_examples/sense.ts index ad7b5834c9d260..c1864287ab1695 100644 --- a/x-pack/plugins/search_indices/public/code_examples/sense.ts +++ b/x-pack/plugins/search_indices/public/code_examples/sense.ts @@ -6,9 +6,10 @@ */ import { INDEX_PLACEHOLDER } from '../constants'; -import { CreateIndexLanguageExamples } from '../types'; +import { IngestDataCodeDefinition } from '../types'; +import { CreateIndexLanguageExamples } from './types'; -export const ConsoleExamples: CreateIndexLanguageExamples = { +export const ConsoleCreateIndexExamples: CreateIndexLanguageExamples = { default: { createIndex: ({ indexName }) => `PUT /${indexName ?? INDEX_PLACEHOLDER}`, }, @@ -29,3 +30,12 @@ export const ConsoleExamples: CreateIndexLanguageExamples = { }`, }, }; + +export const ConsoleVectorsIngestDataExample: IngestDataCodeDefinition = { + ingestCommand: ({ indexName, sampleDocument }) => `POST /_bulk?pretty +{ "index": { "_index": "${indexName}" } } +${JSON.stringify(sampleDocument)} +`, + updateMappingsCommand: ({ indexName, mappingProperties }) => `PUT /${indexName}/_mapping +${JSON.stringify({ properties: mappingProperties }, null, 2)}`, +}; diff --git a/x-pack/plugins/search_indices/public/code_examples/types.ts b/x-pack/plugins/search_indices/public/code_examples/types.ts new file mode 100644 index 00000000000000..dc8f877f565d5e --- /dev/null +++ b/x-pack/plugins/search_indices/public/code_examples/types.ts @@ -0,0 +1,17 @@ +/* + * 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 { CreateIndexCodeDefinition, IngestDataCodeDefinition } from '../types'; + +export interface CreateIndexLanguageExamples { + default: CreateIndexCodeDefinition; + dense_vector: CreateIndexCodeDefinition; +} + +export interface IngestDataLanguageExamples { + dense_vector: IngestDataCodeDefinition; +} 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 index 8d0b3af3f9d72c..c629903b384449 100644 --- 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 @@ -5,19 +5,156 @@ * 2.0. */ -import React from 'react'; +import React, { useCallback, useMemo, useState } from 'react'; +import { MappingProperty } from '@elastic/elasticsearch/lib/api/types'; +import { EuiFlexGroup, EuiFlexItem, EuiPanel, EuiText } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { TryInConsoleButton } from '@kbn/try-in-console'; -import { EuiPanel } from '@elastic/eui'; +import { useKibana } from '../../hooks/use_kibana'; +import { IngestCodeSnippetParameters } from '../../types'; +import { LanguageSelector } from '../shared/language_selector'; +import { useIngestCodeExamples } from './hooks/use_ingest_code_examples'; +import { useElasticsearchUrl } from '../../hooks/use_elasticsearch_url'; +import { useUsageTracker } from '../../contexts/usage_tracker_context'; +import { AvailableLanguages, LanguageOptions, Languages } from '../../code_examples'; +import { AnalyticsEvents } from '../../analytics/constants'; +import { CodeSample } from '../shared/code_sample'; +import { generateSampleDocument } from '../../utils/document_generation'; +import { getDefaultCodingLanguage } from '../../utils/language'; + +export interface AddDocumentsCodeExampleProps { + indexName: string; + mappingProperties: Record; +} + +export const AddDocumentsCodeExample = ({ + indexName, + mappingProperties, +}: AddDocumentsCodeExampleProps) => { + const { application, share, console: consolePlugin } = useKibana().services; + const ingestCodeExamples = useIngestCodeExamples(); + const elasticsearchUrl = useElasticsearchUrl(); + const usageTracker = useUsageTracker(); + const indexHasMappings = Object.keys(mappingProperties).length > 0; + + const [selectedLanguage, setSelectedLanguage] = + useState(getDefaultCodingLanguage); + const selectedCodeExamples = ingestCodeExamples[selectedLanguage]; + const codeSampleMappings = indexHasMappings + ? mappingProperties + : ingestCodeExamples.defaultMapping; + const onSelectLanguage = useCallback( + (value: AvailableLanguages) => { + setSelectedLanguage(value); + usageTracker.count([ + AnalyticsEvents.startCreateIndexLanguageSelect, + `${AnalyticsEvents.startCreateIndexLanguageSelect}_${value}`, + ]); + }, + [usageTracker] + ); + const sampleDocument = useMemo(() => { + // TODO: implement smart document generation + return generateSampleDocument(codeSampleMappings); + }, [codeSampleMappings]); + const codeParams: IngestCodeSnippetParameters = useMemo(() => { + return { + indexName, + elasticsearchURL: elasticsearchUrl, + sampleDocument, + indexHasMappings, + mappingProperties: codeSampleMappings, + }; + }, [indexName, elasticsearchUrl, sampleDocument, codeSampleMappings, indexHasMappings]); -export const AddDocumentsCodeExample: React.FC = () => { return ( - TODO: WITHOUT DATA TICKET + + + + + + {selectedLanguage === 'curl' && ( + + + + )} + + +

{ingestCodeExamples.description}

+
+ {selectedCodeExamples.installCommand && ( + + { + usageTracker.click([ + AnalyticsEvents.indexDetailsInstallCodeCopy, + `${AnalyticsEvents.indexDetailsInstallCodeCopy}_${selectedLanguage}`, + ]); + }} + /> + + )} + {!indexHasMappings && ( + + { + usageTracker.click([ + AnalyticsEvents.indexDetailsAddMappingsCodeCopy, + `${AnalyticsEvents.indexDetailsAddMappingsCodeCopy}_${selectedLanguage}`, + ]); + }} + /> + + )} + + { + usageTracker.click([ + AnalyticsEvents.indexDetailsIngestDocumentsCodeCopy, + `${AnalyticsEvents.indexDetailsIngestDocumentsCodeCopy}_${selectedLanguage}`, + ]); + }} + /> + +
); }; diff --git a/x-pack/plugins/search_indices/public/components/index_documents/constants.ts b/x-pack/plugins/search_indices/public/components/index_documents/constants.ts new file mode 100644 index 00000000000000..944a7f54507a1b --- /dev/null +++ b/x-pack/plugins/search_indices/public/components/index_documents/constants.ts @@ -0,0 +1,8 @@ +/* + * 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. + */ + +export const DEFAULT_PAGE_SIZE = 50; 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 new file mode 100644 index 00000000000000..ddf4f27122ef99 --- /dev/null +++ b/x-pack/plugins/search_indices/public/components/index_documents/document_list.tsx @@ -0,0 +1,38 @@ +/* + * 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 { MappingProperty, SearchHit } from '@elastic/elasticsearch/lib/api/types'; + +import { Result, resultToField, resultMetaData } from '@kbn/search-index-documents'; + +import { EuiSpacer } from '@elastic/eui'; + +import { RecentDocsActionMessage } from './recent_docs_action_message'; + +export interface DocumentListProps { + indexName: string; + docs: SearchHit[]; + mappingProperties: Record; +} + +export const DocumentList = ({ indexName, docs, mappingProperties }: DocumentListProps) => { + return ( + <> + + + {docs.map((doc) => { + return ( + + + + + ); + })} + + ); +}; diff --git a/x-pack/plugins/search_indices/public/components/index_documents/hooks/use_ingest_code_examples.tsx b/x-pack/plugins/search_indices/public/components/index_documents/hooks/use_ingest_code_examples.tsx new file mode 100644 index 00000000000000..7eb84e6f629338 --- /dev/null +++ b/x-pack/plugins/search_indices/public/components/index_documents/hooks/use_ingest_code_examples.tsx @@ -0,0 +1,13 @@ +/* + * 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 * as IngestCodeExamples from '../../../code_examples/ingest_data'; + +export const useIngestCodeExamples = () => { + // TODO: Choose code examples based on onboarding token, stack vs es3, or project type + return IngestCodeExamples.DenseVectorServerlessCodeExamples; +}; 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 index e431a62a3e53b8..9b8852eb596bc4 100644 --- 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 @@ -7,71 +7,18 @@ 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 { EuiFlexGroup, EuiFlexItem, 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; -} +import { DEFAULT_PAGE_SIZE } from './constants'; +import { DocumentList } from './document_list'; -interface RecentDocsActionMessageProps { +interface IndexDocumentsProps { 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, @@ -89,23 +36,11 @@ export const IndexDocuments: React.FC = ({ indexName }) => {isInitialLoading && } - {docs.length === 0 && } + {!isInitialLoading && docs.length === 0 && ( + + )} {docs.length > 0 && ( - <> - - - {docs.map((doc) => { - return ( - - - - - ); - })} - + )} diff --git a/x-pack/plugins/search_indices/public/components/index_documents/recent_docs_action_message.tsx b/x-pack/plugins/search_indices/public/components/index_documents/recent_docs_action_message.tsx new file mode 100644 index 00000000000000..a3039e8e68602f --- /dev/null +++ b/x-pack/plugins/search_indices/public/components/index_documents/recent_docs_action_message.tsx @@ -0,0 +1,56 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiLink, EuiPanel } from '@elastic/eui'; +import { useKibana } from '../../hooks/use_kibana'; + +import { DEFAULT_PAGE_SIZE } from './constants'; + +export interface RecentDocsActionMessageProps { + indexName: string; +} + +export 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.', + })} + +

+
+
+
+ ); +}; diff --git a/x-pack/plugins/search_indices/public/components/start/code_sample.tsx b/x-pack/plugins/search_indices/public/components/shared/code_sample.tsx similarity index 89% rename from x-pack/plugins/search_indices/public/components/start/code_sample.tsx rename to x-pack/plugins/search_indices/public/components/shared/code_sample.tsx index fbc1c7be8af746..4ddce94d685b07 100644 --- a/x-pack/plugins/search_indices/public/components/start/code_sample.tsx +++ b/x-pack/plugins/search_indices/public/components/shared/code_sample.tsx @@ -18,13 +18,14 @@ import { } from '@elastic/eui'; export interface CodeSampleProps { + id?: string; title: string; language: string; code: string; onCodeCopyClick?: React.MouseEventHandler; } -export const CodeSample = ({ title, language, code, onCodeCopyClick }: CodeSampleProps) => { +export const CodeSample = ({ id, title, language, code, onCodeCopyClick }: CodeSampleProps) => { const onCodeClick = React.useCallback( (e: React.MouseEvent) => { if (onCodeCopyClick === undefined) return; @@ -38,7 +39,7 @@ export const CodeSample = ({ title, language, code, onCodeCopyClick }: CodeSampl ); return ( - + {title} @@ -47,6 +48,7 @@ export const CodeSample = ({ title, language, code, onCodeCopyClick }: CodeSampl
onSelectLanguage(value)} + data-test-subj="codeExampleLanguageSelect" /> ); }; diff --git a/x-pack/plugins/search_indices/public/components/start/create_index_code.tsx b/x-pack/plugins/search_indices/public/components/start/create_index_code.tsx index 8c0f1973378b50..6fc2fb3b50e2ff 100644 --- a/x-pack/plugins/search_indices/public/components/start/create_index_code.tsx +++ b/x-pack/plugins/search_indices/public/components/start/create_index_code.tsx @@ -15,10 +15,11 @@ import { DenseVectorSeverlessCodeExamples } from '../../code_examples/create_ind import { useUsageTracker } from '../../hooks/use_usage_tracker'; import { useKibana } from '../../hooks/use_kibana'; import { useElasticsearchUrl } from '../../hooks/use_elasticsearch_url'; +import { getDefaultCodingLanguage } from '../../utils/language'; +import { CodeSample } from '../shared/code_sample'; import { LanguageSelector } from '../shared/language_selector'; -import { CodeSample } from './code_sample'; import { CreateIndexFormState } from './types'; export interface CreateIndexCodeViewProps { @@ -32,8 +33,8 @@ export const CreateIndexCodeView = ({ createIndexForm }: CreateIndexCodeViewProp const { application, share, console: consolePlugin } = useKibana().services; const usageTracker = useUsageTracker(); - // TODO: initing this should be dynamic and possibly saved in the form state - const [selectedLanguage, setSelectedLanguage] = useState('python'); + const [selectedLanguage, setSelectedLanguage] = + useState(getDefaultCodingLanguage); const onSelectLanguage = useCallback( (value: AvailableLanguages) => { setSelectedLanguage(value); diff --git a/x-pack/plugins/search_indices/public/types.ts b/x-pack/plugins/search_indices/public/types.ts index 0f68863fe72f83..95f5eb2883d2ee 100644 --- a/x-pack/plugins/search_indices/public/types.ts +++ b/x-pack/plugins/search_indices/public/types.ts @@ -11,7 +11,10 @@ import type { AppMountParameters, CoreStart } from '@kbn/core/public'; import type { NavigationPublicPluginStart } from '@kbn/navigation-plugin/public'; import type { SharePluginStart } from '@kbn/share-plugin/public'; import type { UsageCollectionStart } from '@kbn/usage-collection-plugin/public'; -import type { MappingPropertyBase } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import type { + MappingProperty, + MappingPropertyBase, +} from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { IndexManagementPluginStart } from '@kbn/index-management-shared-types'; export interface SearchIndicesPluginSetup { @@ -80,7 +83,27 @@ export interface CreateIndexCodeExamples { javascript: CreateIndexCodeDefinition; } -export interface CreateIndexLanguageExamples { - default: CreateIndexCodeDefinition; - dense_vector: CreateIndexCodeDefinition; +export interface IngestCodeSnippetParameters extends CodeSnippetParameters { + indexName: string; + sampleDocument: object; + mappingProperties: Record; +} + +export type IngestCodeSnippetFunction = (params: IngestCodeSnippetParameters) => string; + +export interface IngestDataCodeDefinition { + installCommand?: string; + ingestCommand: IngestCodeSnippetFunction; + updateMappingsCommand: IngestCodeSnippetFunction; +} + +export interface IngestDataCodeExamples { + title: string; + ingestTitle: string; + description: string; + defaultMapping: Record; + sense: IngestDataCodeDefinition; + curl: IngestDataCodeDefinition; + python: IngestDataCodeDefinition; + javascript: IngestDataCodeDefinition; } diff --git a/x-pack/plugins/search_indices/public/utils/document_generation.test.ts b/x-pack/plugins/search_indices/public/utils/document_generation.test.ts new file mode 100644 index 00000000000000..67ae2e81f3c144 --- /dev/null +++ b/x-pack/plugins/search_indices/public/utils/document_generation.test.ts @@ -0,0 +1,143 @@ +/* + * 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 type { MappingProperty } from '@elastic/elasticsearch/lib/api/types'; + +import { generateSampleDocument } from './document_generation'; + +describe('document generation util', () => { + it('should generate a sample document for text and keyword fields', () => { + const mapping: Record = { + body: { type: 'semantic_text', inference_id: '.elser_model_2' }, + title: { type: 'text' }, + tags: { type: 'keyword' }, + }; + + const result = generateSampleDocument(mapping); + + expect(result).toEqual({ + title: 'Sample text for title', + tags: 'sample-keyword-tags', + body: 'Hello World', + }); + }); + + it('should generate a sample document for integer and float fields', () => { + const mapping: Record = { + age: { type: 'integer' }, + rating: { type: 'float' }, + price: { type: 'double' }, + }; + + const result = generateSampleDocument(mapping); + + expect(Number.isInteger(result.age)).toBe(true); + expect(typeof result.rating).toBe('number'); + expect(typeof result.price).toBe('number'); + }); + + it('should generate a sample document for boolean and date fields', () => { + const mapping: Record = { + isActive: { type: 'boolean' }, + createdAt: { type: 'date' }, + }; + + const result = generateSampleDocument(mapping); + + expect(typeof result.isActive).toBe('boolean'); + expect(new Date(result.createdAt as string).toISOString()).toBe(result.createdAt); + }); + + it('should generate a sample document for geo_point fields', () => { + const mapping: Record = { + location: { type: 'geo_point' }, + }; + + const result = generateSampleDocument(mapping); + + expect(result.location).toEqual({ + lat: 40.7128, + lon: -74.006, + }); + }); + + it('should generate a sample document for nested fields', () => { + const mapping: Record = { + user: { + type: 'nested', + properties: { + name: { type: 'text' }, + age: { type: 'integer' }, + }, + }, + }; + + const result = generateSampleDocument(mapping); + + expect(result.user).toEqual([ + { + name: 'Sample text for name', + age: expect.any(Number), + }, + ]); + }); + + it('should generate a sample document for object fields', () => { + const mapping: Record = { + address: { + type: 'object', + properties: { + city: { type: 'text' }, + postalCode: { type: 'integer' }, + }, + }, + }; + + const result = generateSampleDocument(mapping); + + expect(result.address).toEqual({ + city: 'Sample text for city', + postalCode: expect.any(Number), + }); + }); + + it('should generate a sample document for dense_vector fields', () => { + const mapping: Record = { + embedding: { type: 'dense_vector', dims: 512 }, + }; + + const result = generateSampleDocument(mapping); + + expect(Array.isArray(result.embedding)).toBe(true); + expect((result.embedding as number[]).length!).toBe(21); + expect((result.embedding as number[])[20]).toBe('...'); + }); + + it('should generate a sample document for sparse_vector fields', () => { + const mapping: Record = { + vector: { type: 'sparse_vector' }, + }; + + const result = generateSampleDocument(mapping); + + expect(result.vector).toBeDefined(); + for (const [key, value] of Object.entries(result.vector!)) { + expect(key).toEqual(expect.any(String)); + expect(value).toEqual(expect.any(Number)); + } + }); + + it('should handle unknown mapping types by setting null', () => { + const mapping: Record = { + unknownField: { type: 'unknown' as any }, + }; + + const result = generateSampleDocument(mapping); + + expect(result.unknownField).toBeNull(); + }); +}); diff --git a/x-pack/plugins/search_indices/public/utils/document_generation.ts b/x-pack/plugins/search_indices/public/utils/document_generation.ts new file mode 100644 index 00000000000000..42f4dc88ee5b26 --- /dev/null +++ b/x-pack/plugins/search_indices/public/utils/document_generation.ts @@ -0,0 +1,107 @@ +/* + * 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 type { + MappingDenseVectorProperty, + MappingProperty, +} from '@elastic/elasticsearch/lib/api/types'; + +export function generateSampleDocument( + mappingProperties: Record +): Record { + const sampleDocument: Record = {}; + + Object.entries(mappingProperties).forEach(([field, mapping]) => { + if ('type' in mapping) { + switch (mapping.type) { + case 'text': + sampleDocument[field] = `Sample text for ${field}`; + break; + case 'keyword': + sampleDocument[field] = `sample-keyword-${field}`; + break; + case 'semantic_text': + sampleDocument[field] = 'Hello World'; + break; + case 'integer': + case 'long': + sampleDocument[field] = Math.floor(Math.random() * 100); + break; + case 'float': + case 'double': + sampleDocument[field] = Math.random() * 100; + break; + case 'boolean': + sampleDocument[field] = Math.random() < 0.5; + break; + case 'date': + sampleDocument[field] = new Date().toISOString(); + break; + case 'geo_point': + sampleDocument[field] = { + lat: 40.7128, + lon: -74.006, + }; + break; + case 'nested': + if (mapping.properties) { + sampleDocument[field] = [generateSampleDocument(mapping.properties)]; + } + break; + case 'object': + if (mapping.properties) { + sampleDocument[field] = generateSampleDocument(mapping.properties); + } + break; + case 'dense_vector': + sampleDocument[field] = generateDenseVector(mapping); + break; + case 'sparse_vector': + sampleDocument[field] = generateSparseVector(); + break; + default: + // Default to null for unhandled types + sampleDocument[field] = null; + break; + } + } + }); + + return sampleDocument; +} + +function generateDenseVector(mapping: MappingDenseVectorProperty, maxDisplayDims = 20) { + // Limit the dimensions for better UI display + const dimension = Math.min(mapping?.dims ?? 20, maxDisplayDims); + + // Generate an array of random floating-point numbers + const denseVector: Array = Array.from({ length: dimension }, () => + parseFloat((Math.random() * 10).toFixed(3)) + ); + if (dimension < (mapping?.dims || 0)) { + denseVector.push('...'); + } + + return denseVector; +} + +function generateSparseVector(numElements: number = 5, vectorSize: number = 100) { + const sparseVector: Record = {}; + const usedIndices = new Set(); + + while (usedIndices.size < numElements) { + // Generate a random index for the sparse vector + const index = Math.floor(Math.random() * vectorSize); + + if (!usedIndices.has(index)) { + sparseVector[index] = parseFloat((Math.random() * 10).toFixed(3)); + usedIndices.add(index); + } + } + + return sparseVector; +} diff --git a/x-pack/plugins/search_indices/public/utils/language.ts b/x-pack/plugins/search_indices/public/utils/language.ts new file mode 100644 index 00000000000000..240434256b8f00 --- /dev/null +++ b/x-pack/plugins/search_indices/public/utils/language.ts @@ -0,0 +1,26 @@ +/* + * 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 { AvailableLanguages, Languages } from '../code_examples'; + +export function getDefaultCodingLanguage(): AvailableLanguages { + const defaultLang = readConsoleDefaultLanguage() ?? 'python'; + return defaultLang; +} + +const validLanguages = Object.keys(Languages); + +const CONSOLE_DEFAULT_LANGUAGE_KEY = 'sense:defaultLanguage'; +function readConsoleDefaultLanguage() { + const consoleLanguageValue = localStorage + .getItem(CONSOLE_DEFAULT_LANGUAGE_KEY) + ?.replaceAll('"', ''); // Console is storing the value wrapped in "", so we want to remove them + if (consoleLanguageValue && validLanguages.includes(consoleLanguageValue)) { + return consoleLanguageValue as AvailableLanguages; + } + return undefined; +} 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 f19a713eece5b4..fe54f75dbdba94 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 @@ -127,5 +127,29 @@ export function SvlSearchIndexDetailPageProvider({ getService }: FtrProviderCont async expectSettingsComponentIsVisible() { await testSubjects.existOrFail('indexDetailsSettingsEditModeSwitch', { timeout: 2000 }); }, + async expectSelectedLanguage(language: string) { + await testSubjects.existOrFail('codeExampleLanguageSelect'); + expect( + (await testSubjects.getVisibleText('codeExampleLanguageSelect')).toLowerCase() + ).contain(language); + }, + async selectCodingLanguage(language: string) { + await testSubjects.existOrFail('codeExampleLanguageSelect'); + await testSubjects.click('codeExampleLanguageSelect'); + await testSubjects.existOrFail(`lang-option-${language}`); + await testSubjects.click(`lang-option-${language}`); + expect( + (await testSubjects.getVisibleText('codeExampleLanguageSelect')).toLowerCase() + ).contain(language); + }, + async codeSampleContainsValue(subject: string, value: string) { + const tstSubjId = `${subject}-code-block`; + await testSubjects.existOrFail(tstSubjId); + expect(await testSubjects.getVisibleText(tstSubjId)).contain(value); + }, + async openConsoleCodeExample() { + await testSubjects.existOrFail('tryInConsoleButton'); + await testSubjects.click('tryInConsoleButton'); + }, }; } 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 a4d58d32c751b3..8ba8e8d0300d00 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 @@ -66,6 +66,20 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { it('should show code examples for adding documents', async () => { await pageObjects.svlSearchIndexDetailPage.expectAddDocumentCodeExamples(); + await pageObjects.svlSearchIndexDetailPage.expectSelectedLanguage('python'); + await pageObjects.svlSearchIndexDetailPage.codeSampleContainsValue( + 'installCodeExample', + 'pip install' + ); + await pageObjects.svlSearchIndexDetailPage.selectCodingLanguage('javascript'); + await pageObjects.svlSearchIndexDetailPage.codeSampleContainsValue( + 'installCodeExample', + 'npm install' + ); + await pageObjects.svlSearchIndexDetailPage.selectCodingLanguage('curl'); + await pageObjects.svlSearchIndexDetailPage.openConsoleCodeExample(); + await pageObjects.embeddedConsole.expectEmbeddedConsoleToBeOpen(); + await pageObjects.embeddedConsole.clickEmbeddedConsoleControlBar(); }); it('back to indices button should redirect to list page', async () => {