From abb63db6002766a2ea79f89fa7a7181da1c86dea Mon Sep 17 00:00:00 2001 From: Joe McElroy Date: Mon, 16 Sep 2024 17:03:01 +0100 Subject: [PATCH] [Onboarding] Connection details + Quick Stats (#192636) ## Summary Adding in the connection details and quickstats for the search_details page. ![Screenshot 2024-09-11 at 20 36 31](https://github.com/user-attachments/assets/5f030c06-4a98-4d9d-a465-c6719998ca56) ![Screenshot 2024-09-11 at 20 36 27](https://github.com/user-attachments/assets/d96be2f1-bcaa-42e5-9d32-1612e090b916) ![Screenshot 2024-09-11 at 20 36 09](https://github.com/user-attachments/assets/1f7995ae-5a0d-4810-acfb-3fafe33be451) ### Checklist Delete any items that are not applicable to this PR. --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Liam Thompson <32779855+leemthompo@users.noreply.github.com> (cherry picked from commit 4d488818dc5503c40617d01512cd89c05e1e0345) --- packages/kbn-doc-links/src/get_doc_links.ts | 1 + packages/kbn-doc-links/src/types.ts | 1 + .../search_indices/common/doc_links.ts | 2 + .../connection_details/connection_details.tsx | 73 ++++++++ .../components/indices/details_page.tsx | 32 +++- .../quick_stats/mappings_convertor.test.ts | 41 +++++ .../quick_stats/mappings_convertor.ts | 62 +++++++ .../components/quick_stats/quick_stat.tsx | 116 ++++++++++++ .../components/quick_stats/quick_stats.tsx | 167 ++++++++++++++++++ .../components/start/create_index_code.tsx | 10 +- .../public/hooks/api/use_index_mappings.ts | 22 +++ .../public/hooks/use_elasticsearch_url.ts | 18 ++ x-pack/plugins/search_indices/public/types.ts | 8 + .../svl_search_index_detail_page.ts | 34 ++++ .../test_suites/search/search_index_detail.ts | 25 +++ 15 files changed, 603 insertions(+), 9 deletions(-) create mode 100644 x-pack/plugins/search_indices/public/components/connection_details/connection_details.tsx create mode 100644 x-pack/plugins/search_indices/public/components/quick_stats/mappings_convertor.test.ts create mode 100644 x-pack/plugins/search_indices/public/components/quick_stats/mappings_convertor.ts create mode 100644 x-pack/plugins/search_indices/public/components/quick_stats/quick_stat.tsx create mode 100644 x-pack/plugins/search_indices/public/components/quick_stats/quick_stats.tsx create mode 100644 x-pack/plugins/search_indices/public/hooks/api/use_index_mappings.ts create mode 100644 x-pack/plugins/search_indices/public/hooks/use_elasticsearch_url.ts diff --git a/packages/kbn-doc-links/src/get_doc_links.ts b/packages/kbn-doc-links/src/get_doc_links.ts index d2ac9c6678797..3ad5d271bde47 100644 --- a/packages/kbn-doc-links/src/get_doc_links.ts +++ b/packages/kbn-doc-links/src/get_doc_links.ts @@ -219,6 +219,7 @@ export const getDocLinks = ({ kibanaBranch, buildFlavor }: GetDocLinkOptions): D searchApplicationsSearch: `${ELASTICSEARCH_DOCS}search-application-client.html`, searchLabs: `${SEARCH_LABS_URL}`, searchLabsRepo: `${SEARCH_LABS_REPO}`, + semanticSearch: `${ELASTICSEARCH_DOCS}semantic-search.html`, searchTemplates: `${ELASTICSEARCH_DOCS}search-template.html`, semanticTextField: `${ELASTICSEARCH_DOCS}semantic-text.html`, start: `${ENTERPRISE_SEARCH_DOCS}start.html`, diff --git a/packages/kbn-doc-links/src/types.ts b/packages/kbn-doc-links/src/types.ts index ae6e56a9ac385..cbf085623c3a6 100644 --- a/packages/kbn-doc-links/src/types.ts +++ b/packages/kbn-doc-links/src/types.ts @@ -183,6 +183,7 @@ export interface DocLinks { readonly searchApplicationsSearch: string; readonly searchLabs: string; readonly searchLabsRepo: string; + readonly semanticSearch: string; readonly searchTemplates: string; readonly semanticTextField: string; readonly start: string; diff --git a/x-pack/plugins/search_indices/common/doc_links.ts b/x-pack/plugins/search_indices/common/doc_links.ts index dbffa8f9f0f33..8cceb45041ab9 100644 --- a/x-pack/plugins/search_indices/common/doc_links.ts +++ b/x-pack/plugins/search_indices/common/doc_links.ts @@ -9,11 +9,13 @@ import { DocLinks } from '@kbn/doc-links'; class SearchIndicesDocLinks { public apiReference: string = ''; + public setupSemanticSearch: string = ''; constructor() {} setDocLinks(newDocLinks: DocLinks) { this.apiReference = newDocLinks.apiReference; + this.setupSemanticSearch = newDocLinks.enterpriseSearch.semanticSearch; } } export const docLinks = new SearchIndicesDocLinks(); diff --git a/x-pack/plugins/search_indices/public/components/connection_details/connection_details.tsx b/x-pack/plugins/search_indices/public/components/connection_details/connection_details.tsx new file mode 100644 index 0000000000000..d7ce8f308b683 --- /dev/null +++ b/x-pack/plugins/search_indices/public/components/connection_details/connection_details.tsx @@ -0,0 +1,73 @@ +/* + * 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 { + EuiButtonIcon, + EuiCopy, + EuiFlexGroup, + EuiFlexItem, + EuiTitle, + useEuiTheme, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +import { FormattedMessage } from '@kbn/i18n-react'; +import { useElasticsearchUrl } from '../../hooks/use_elasticsearch_url'; + +export const ConnectionDetails: React.FC = () => { + const { euiTheme } = useEuiTheme(); + const elasticsearchUrl = useElasticsearchUrl(); + + return ( + + + +

+ +

+
+
+ +

+ {elasticsearchUrl} +

+
+ + + {(copy) => ( + + )} + + +
+ ); +}; 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 afa798814d864..85021e79edbf2 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 @@ -27,13 +27,22 @@ import { i18n } from '@kbn/i18n'; import { SectionLoading } from '@kbn/es-ui-shared-plugin/public'; import { useIndex } from '../../hooks/api/use_index'; 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 { DeleteIndexModal } from './delete_index_modal'; import { IndexloadingError } from './details_page_loading_error'; export const SearchIndexDetailsPage = () => { const indexName = decodeURIComponent(useParams<{ indexName: string }>().indexName); const { console: consolePlugin, docLinks, application } = useKibana().services; - const { data: index, refetch, isSuccess, isInitialLoading } = useIndex(indexName); + + const { data: index, refetch, isError: isIndexError, isInitialLoading } = useIndex(indexName); + const { + data: mappings, + isError: isMappingsError, + isInitialLoading: isMappingsInitialLoading, + } = useIndexMapping(indexName); const embeddableConsole = useMemo( () => (consolePlugin?.EmbeddableConsole ? : null), @@ -87,7 +96,7 @@ export const SearchIndexDetailsPage = () => { /> ); - if (isInitialLoading) { + if (isInitialLoading || isMappingsInitialLoading) { return ( {i18n.translate('xpack.searchIndices.loadingDescription', { @@ -103,9 +112,10 @@ export const SearchIndexDetailsPage = () => { restrictWidth={false} data-test-subj="searchIndicesDetailsPage" grow={false} - bottomBorder={false} + panelled + bottomBorder > - {!isSuccess || !index ? ( + {isIndexError || isMappingsError || !index || !mappings ? ( { navigateToIndexListPage={navigateToIndexListPage} /> )} + + + + + + {/* TODO: API KEY */} + + + -
+ + + + )} {embeddableConsole} diff --git a/x-pack/plugins/search_indices/public/components/quick_stats/mappings_convertor.test.ts b/x-pack/plugins/search_indices/public/components/quick_stats/mappings_convertor.test.ts new file mode 100644 index 0000000000000..da182123ab4c1 --- /dev/null +++ b/x-pack/plugins/search_indices/public/components/quick_stats/mappings_convertor.test.ts @@ -0,0 +1,41 @@ +/* + * 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 { Mappings } from '../../types'; +import { countVectorBasedTypesFromMappings } from './mappings_convertor'; + +describe('mappings convertor', () => { + it('should count vector based types from mappings', () => { + const mappings = { + mappings: { + properties: { + field1: { + type: 'dense_vector', + }, + field2: { + type: 'dense_vector', + }, + field3: { + type: 'sparse_vector', + }, + field4: { + type: 'dense_vector', + }, + field5: { + type: 'semantic_text', + }, + }, + }, + }; + const result = countVectorBasedTypesFromMappings(mappings as unknown as Mappings); + expect(result).toEqual({ + dense_vector: 3, + sparse_vector: 1, + semantic_text: 1, + }); + }); +}); diff --git a/x-pack/plugins/search_indices/public/components/quick_stats/mappings_convertor.ts b/x-pack/plugins/search_indices/public/components/quick_stats/mappings_convertor.ts new file mode 100644 index 0000000000000..749fe05de1f54 --- /dev/null +++ b/x-pack/plugins/search_indices/public/components/quick_stats/mappings_convertor.ts @@ -0,0 +1,62 @@ +/* + * 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, + MappingPropertyBase, +} from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import type { Mappings } from '../../types'; + +interface VectorFieldTypes { + semantic_text: number; + dense_vector: number; + sparse_vector: number; +} + +export function countVectorBasedTypesFromMappings(mappings: Mappings): VectorFieldTypes { + const typeCounts: VectorFieldTypes = { + semantic_text: 0, + dense_vector: 0, + sparse_vector: 0, + }; + + const typeCountKeys = Object.keys(typeCounts); + + function recursiveCount(fields: MappingProperty | Mappings | MappingPropertyBase['fields']) { + if (!fields) { + return; + } + if ('mappings' in fields) { + recursiveCount(fields.mappings); + } + if ('properties' in fields && fields.properties) { + Object.keys(fields.properties).forEach((key) => { + const value = (fields.properties as Record)?.[key]; + + if (value && value.type) { + if (typeCountKeys.includes(value.type)) { + const type = value.type as keyof VectorFieldTypes; + typeCounts[type] = typeCounts[type] + 1; + } + + if ('fields' in value) { + recursiveCount(value.fields); + } + + if ('properties' in value) { + recursiveCount(value.properties); + } + } else if (value.properties || value.fields) { + recursiveCount(value); + } + }); + } + } + + recursiveCount(mappings); + return typeCounts; +} diff --git a/x-pack/plugins/search_indices/public/components/quick_stats/quick_stat.tsx b/x-pack/plugins/search_indices/public/components/quick_stats/quick_stat.tsx new file mode 100644 index 0000000000000..0d72835ad5779 --- /dev/null +++ b/x-pack/plugins/search_indices/public/components/quick_stats/quick_stat.tsx @@ -0,0 +1,116 @@ +/* + * 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 { + EuiAccordion, + EuiDescriptionList, + EuiFlexGroup, + EuiFlexItem, + EuiIcon, + EuiPanel, + EuiText, + useEuiTheme, + useGeneratedHtmlId, +} from '@elastic/eui'; + +interface BaseQuickStatProps { + icon: string; + iconColor: string; + title: string; + secondaryTitle: React.ReactNode; + open: boolean; + content?: React.ReactNode; + stats: Array<{ + title: string; + description: NonNullable; + }>; + setOpen: (open: boolean) => void; + first?: boolean; +} + +export const QuickStat: React.FC = ({ + icon, + title, + stats, + open, + setOpen, + first, + secondaryTitle, + iconColor, + content, + ...rest +}) => { + const { euiTheme } = useEuiTheme(); + + const id = useGeneratedHtmlId({ + prefix: 'formAccordion', + suffix: title, + }); + + return ( + setOpen(!open)} + paddingSize="none" + id={id} + buttonElement="div" + arrowDisplay="right" + {...rest} + css={{ + borderLeft: euiTheme.border.thin, + ...(first ? { borderLeftWidth: 0 } : {}), + '.euiAccordion__arrow': { + marginRight: euiTheme.size.s, + }, + '.euiAccordion__triggerWrapper': { + background: euiTheme.colors.ghost, + }, + '.euiAccordion__children': { + borderTop: euiTheme.border.thin, + padding: euiTheme.size.m, + }, + }} + buttonContent={ + + + + + + + +

{title}

+
+
+ + {secondaryTitle} + +
+
+ } + > + {content ? ( + content + ) : ( + + + + + + )} +
+ ); +}; diff --git a/x-pack/plugins/search_indices/public/components/quick_stats/quick_stats.tsx b/x-pack/plugins/search_indices/public/components/quick_stats/quick_stats.tsx new file mode 100644 index 0000000000000..cece2b1d39910 --- /dev/null +++ b/x-pack/plugins/search_indices/public/components/quick_stats/quick_stats.tsx @@ -0,0 +1,167 @@ +/* + * 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, { useMemo, useState } from 'react'; +import type { Index } from '@kbn/index-management-shared-types'; + +import { + EuiFlexGroup, + EuiFlexItem, + EuiI18nNumber, + EuiPanel, + EuiText, + useEuiTheme, + EuiButton, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { Mappings } from '../../types'; +import { countVectorBasedTypesFromMappings } from './mappings_convertor'; +import { QuickStat } from './quick_stat'; +import { useKibana } from '../../hooks/use_kibana'; + +export interface QuickStatsProps { + index: Index; + mappings: Mappings; +} + +export const SetupAISearchButton: React.FC = () => { + const { + services: { docLinks }, + } = useKibana(); + return ( + + + + +
+ {i18n.translate('xpack.searchIndices.quickStats.setup_ai_search_description', { + defaultMessage: 'Build AI-powered search experiences with Elastic', + })} +
+
+
+ + + {i18n.translate('xpack.searchIndices.quickStats.setup_ai_search_button', { + defaultMessage: 'Set up now', + })} + + +
+
+ ); +}; + +export const QuickStats: React.FC = ({ index, mappings }) => { + const [open, setOpen] = useState(false); + const { euiTheme } = useEuiTheme(); + const mappingStats = useMemo(() => countVectorBasedTypesFromMappings(mappings), [mappings]); + const vectorFieldCount = + mappingStats.sparse_vector + mappingStats.dense_vector + mappingStats.semantic_text; + + return ( + ({ + border: euiTheme.border.thin, + background: euiTheme.colors.lightestShade, + overflow: 'hidden', + })} + > + + + } + stats={[ + { + title: i18n.translate('xpack.searchIndices.quickStats.documents.totalTitle', { + defaultMessage: 'Total', + }), + description: , + }, + { + title: i18n.translate('xpack.searchIndices.quickStats.documents.indexSize', { + defaultMessage: 'Index Size', + }), + description: index.size ?? '0b', + }, + ]} + first + /> + + + 0 + ? i18n.translate('xpack.searchIndices.quickStats.total_count', { + defaultMessage: '{value, plural, one {# Field} other {# Fields}}', + values: { + value: vectorFieldCount, + }, + }) + : i18n.translate('xpack.searchIndices.quickStats.no_vector_fields', { + defaultMessage: 'Not configured', + }) + } + content={vectorFieldCount === 0 && } + stats={[ + { + title: i18n.translate('xpack.searchIndices.quickStats.sparse_vector', { + defaultMessage: 'Sparse Vector', + }), + description: i18n.translate('xpack.searchIndices.quickStats.sparse_vector_count', { + defaultMessage: '{value, plural, one {# Field} other {# Fields}}', + values: { value: mappingStats.sparse_vector }, + }), + }, + { + title: i18n.translate('xpack.searchIndices.quickStats.dense_vector', { + defaultMessage: 'Dense Vector', + }), + description: i18n.translate('xpack.searchIndices.quickStats.dense_vector_count', { + defaultMessage: '{value, plural, one {# Field} other {# Fields}}', + values: { value: mappingStats.dense_vector }, + }), + }, + { + title: i18n.translate('xpack.searchIndices.quickStats.semantic_text', { + defaultMessage: 'Semantic Text', + }), + description: i18n.translate('xpack.searchIndices.quickStats.semantic_text_count', { + defaultMessage: '{value, plural, one {# Field} other {# Fields}}', + values: { value: mappingStats.semantic_text }, + }), + }, + ]} + /> + + + + ); +}; 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 526f41fb9f870..b78137e7a3fdd 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 @@ -12,12 +12,12 @@ import { TryInConsoleButton } from '@kbn/try-in-console'; import { useKibana } from '../../hooks/use_kibana'; import { CodeSample } from './code_sample'; import { CreateIndexFormState } from './types'; -import { ELASTICSEARCH_URL_PLACEHOLDER } from '../../constants'; import { Languages, AvailableLanguages, LanguageOptions } from '../../code_examples'; import { DenseVectorSeverlessCodeExamples } from '../../code_examples/create_index'; import { LanguageSelector } from '../shared/language_selector'; +import { useElasticsearchUrl } from '../../hooks/use_elasticsearch_url'; export interface CreateIndexCodeViewProps { createIndexForm: CreateIndexFormState; @@ -27,15 +27,17 @@ export interface CreateIndexCodeViewProps { const SelectedCodeExamples = DenseVectorSeverlessCodeExamples; export const CreateIndexCodeView = ({ createIndexForm }: CreateIndexCodeViewProps) => { - const { application, cloud, share, console: consolePlugin } = useKibana().services; + const { application, share, console: consolePlugin } = useKibana().services; // TODO: initing this should be dynamic and possibly saved in the form state const [selectedLanguage, setSelectedLanguage] = useState('python'); + const elasticsearchUrl = useElasticsearchUrl(); + const codeParams = useMemo(() => { return { indexName: createIndexForm.indexName || undefined, - elasticsearchURL: cloud?.elasticsearchUrl ?? ELASTICSEARCH_URL_PLACEHOLDER, + elasticsearchURL: elasticsearchUrl, }; - }, [createIndexForm.indexName, cloud]); + }, [createIndexForm.indexName, elasticsearchUrl]); const selectedCodeExample = useMemo(() => { return SelectedCodeExamples[selectedLanguage]; }, [selectedLanguage]); diff --git a/x-pack/plugins/search_indices/public/hooks/api/use_index_mappings.ts b/x-pack/plugins/search_indices/public/hooks/api/use_index_mappings.ts new file mode 100644 index 0000000000000..a91198f70b4e8 --- /dev/null +++ b/x-pack/plugins/search_indices/public/hooks/api/use_index_mappings.ts @@ -0,0 +1,22 @@ +/* + * 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 { useQuery } from '@tanstack/react-query'; +import { useKibana } from '../use_kibana'; +import { Mappings } from '../../types'; + +export const useIndexMapping = (indexName: string) => { + const { http } = useKibana().services; + const queryKey = ['fetchMapping', indexName]; + const result = useQuery({ + queryKey, + refetchOnWindowFocus: 'always', + queryFn: () => + http.fetch(`/api/index_management/mapping/${encodeURIComponent(indexName)}`), + }); + return { queryKey, ...result }; +}; diff --git a/x-pack/plugins/search_indices/public/hooks/use_elasticsearch_url.ts b/x-pack/plugins/search_indices/public/hooks/use_elasticsearch_url.ts new file mode 100644 index 0000000000000..d07cc62b210de --- /dev/null +++ b/x-pack/plugins/search_indices/public/hooks/use_elasticsearch_url.ts @@ -0,0 +1,18 @@ +/* + * 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 { useKibana } from './use_kibana'; + +import { ELASTICSEARCH_URL_PLACEHOLDER } from '../constants'; + +export const useElasticsearchUrl = (): string => { + const { + services: { cloud }, + } = useKibana(); + + return cloud?.elasticsearchUrl ?? ELASTICSEARCH_URL_PLACEHOLDER; +}; diff --git a/x-pack/plugins/search_indices/public/types.ts b/x-pack/plugins/search_indices/public/types.ts index 8e7853543f76f..6e0192e34f87c 100644 --- a/x-pack/plugins/search_indices/public/types.ts +++ b/x-pack/plugins/search_indices/public/types.ts @@ -11,6 +11,7 @@ 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'; export interface SearchIndicesPluginSetup { enabled: boolean; @@ -44,11 +45,18 @@ export interface AppUsageTracker { load: (eventName: string | string[]) => void; } +export interface Mappings { + mappings: { + properties: MappingPropertyBase['properties']; + }; +} + export interface CodeSnippetParameters { indexName?: string; apiKey?: string; elasticsearchURL: string; } + export type CodeSnippetFunction = (params: CodeSnippetParameters) => string; export interface CodeLanguage { 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 5ac440ce6c4f4..09b69aaed5332 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 @@ -32,6 +32,40 @@ export function SvlSearchIndexDetailPageProvider({ getService }: FtrProviderCont async expectBackToIndicesButtonRedirectsToListPage() { await testSubjects.existOrFail('indicesList'); }, + async expectConnectionDetails() { + await testSubjects.existOrFail('connectionDetailsEndpoint', { timeout: 2000 }); + expect(await (await testSubjects.find('connectionDetailsEndpoint')).getVisibleText()).to.be( + 'https://fakeprojectid.es.fake-domain.cld.elstc.co:443' + ); + }, + async expectQuickStats() { + await testSubjects.existOrFail('quickStats', { timeout: 2000 }); + const quickStatsElem = await testSubjects.find('quickStats'); + const quickStatsDocumentElem = await quickStatsElem.findByTestSubject( + 'QuickStatsDocumentCount' + ); + expect(await quickStatsDocumentElem.getVisibleText()).to.contain('Document count\n0'); + expect(await quickStatsDocumentElem.getVisibleText()).not.to.contain('Index Size\n0b'); + await quickStatsDocumentElem.click(); + expect(await quickStatsDocumentElem.getVisibleText()).to.contain('Index Size\n0b'); + }, + async expectQuickStatsAIMappings() { + await testSubjects.existOrFail('quickStats', { timeout: 2000 }); + const quickStatsElem = await testSubjects.find('quickStats'); + const quickStatsAIMappingsElem = await quickStatsElem.findByTestSubject( + 'QuickStatsAIMappings' + ); + await quickStatsAIMappingsElem.click(); + await testSubjects.existOrFail('setupAISearchButton', { timeout: 2000 }); + }, + + async expectQuickStatsAIMappingsToHaveVectorFields() { + const quickStatsDocumentElem = await testSubjects.find('QuickStatsAIMappings'); + await quickStatsDocumentElem.click(); + expect(await quickStatsDocumentElem.getVisibleText()).to.contain('AI Search\n1 Field'); + await testSubjects.missingOrFail('setupAISearchButton', { 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 9450dca44df57..cd39079274d0a 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 @@ -26,6 +26,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { after(async () => { await esDeleteAllIndices(indexName); }); + describe('index details page overview', () => { before(async () => { await es.indices.create({ index: indexName }); @@ -41,11 +42,35 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { it('should have embedded dev console', async () => { await testHasEmbeddedConsole(pageObjects); }); + it('should have connection details', async () => { + await pageObjects.svlSearchIndexDetailPage.expectConnectionDetails(); + }); + + it('should have quick stats', async () => { + await pageObjects.svlSearchIndexDetailPage.expectQuickStats(); + await pageObjects.svlSearchIndexDetailPage.expectQuickStatsAIMappings(); + await es.indices.putMapping({ + index: indexName, + body: { + properties: { + my_field: { + type: 'dense_vector', + dims: 3, + }, + }, + }, + }); + await svlSearchNavigation.navigateToIndexDetailPage(indexName); + + await pageObjects.svlSearchIndexDetailPage.expectQuickStatsAIMappingsToHaveVectorFields(); + }); + it('back to indices button should redirect to list page', async () => { await pageObjects.svlSearchIndexDetailPage.expectBackToIndicesButtonExists(); await pageObjects.svlSearchIndexDetailPage.clickBackToIndicesButton(); await pageObjects.svlSearchIndexDetailPage.expectBackToIndicesButtonRedirectsToListPage(); }); + describe('page loading error', () => { before(async () => { await svlSearchNavigation.navigateToIndexDetailPage(indexName);