From 4ba21991f732be926038661dac68f51153ca662d Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Thu, 31 Oct 2024 03:22:13 +1100 Subject: [PATCH] [8.x] [Search] Refactor: abstracting classic nav items (#196579) (#198369) # Backport This will backport the following commits from `main` to `8.x`: - [[Search] Refactor: abstracting classic nav items (#196579)](https://github.com/elastic/kibana/pull/196579) ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sqren/backport) Co-authored-by: Rodney Norris --- packages/deeplinks/search/constants.ts | 4 + packages/deeplinks/search/deep_links.ts | 14 +- packages/deeplinks/search/index.ts | 4 + .../enterprise_search/common/constants.ts | 23 +- .../__mocks__/kea_logic/kibana_logic.mock.ts | 1 + .../components/playground/page_template.tsx | 71 +++++ .../components/playground/playground.tsx | 8 +- .../elasticsearch_guide.tsx | 2 +- .../components/layout/page_template.tsx | 3 +- .../public/applications/index.tsx | 1 + .../shared/kibana/kibana_logic.ts | 3 + .../kibana_chrome/generate_breadcrumbs.ts | 28 +- .../shared/kibana_chrome/generate_title.ts | 17 +- .../shared/kibana_chrome/set_chrome.tsx | 25 +- .../applications/shared/layout/base_nav.tsx | 201 ++++++++++++++ .../shared/layout/classic_nav_helpers.test.ts | 189 +++++++++++++ .../shared/layout/classic_nav_helpers.ts | 102 +++++++ .../applications/shared/layout/nav.test.tsx | 190 ++++++++++--- .../public/applications/shared/layout/nav.tsx | 256 ++---------------- .../shared/layout/nav_link_helpers.test.ts | 1 + .../shared/layout/nav_link_helpers.ts | 22 +- .../react_router_helpers/create_href.ts | 11 +- .../react_router_helpers/eui_components.tsx | 4 +- .../generate_react_router_props.test.ts | 2 + .../generate_react_router_props.ts | 14 +- .../shared/react_router_helpers/index.ts | 1 - .../public/applications/shared/types.ts | 39 +++ .../test_helpers/test_utils.test_helper.tsx | 1 + .../translations/translations/fr-FR.json | 12 - .../translations/translations/ja-JP.json | 12 - .../translations/translations/zh-CN.json | 12 - x-pack/test/functional/config.base.js | 3 + x-pack/test/functional/page_objects/index.ts | 2 + .../page_objects/search_classic_navigation.ts | 118 ++++++++ x-pack/test/functional_search/config.ts | 10 + x-pack/test/functional_search/index.ts | 1 + .../tests/classic_navigation.ts | 131 +++++++++ 37 files changed, 1180 insertions(+), 358 deletions(-) create mode 100644 x-pack/plugins/enterprise_search/public/applications/applications/components/playground/page_template.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/layout/base_nav.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/layout/classic_nav_helpers.test.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/layout/classic_nav_helpers.ts create mode 100644 x-pack/test/functional/page_objects/search_classic_navigation.ts create mode 100644 x-pack/test/functional_search/tests/classic_navigation.ts diff --git a/packages/deeplinks/search/constants.ts b/packages/deeplinks/search/constants.ts index a2a17b20efba8..9848bb0c3d42e 100644 --- a/packages/deeplinks/search/constants.ts +++ b/packages/deeplinks/search/constants.ts @@ -21,3 +21,7 @@ export const SERVERLESS_ES_SEARCH_INFERENCE_ENDPOINTS_ID = 'searchInferenceEndpo export const SEARCH_HOMEPAGE = 'searchHomepage'; export const SEARCH_INDICES_START = 'elasticsearchStart'; export const SEARCH_INDICES = 'elasticsearchIndices'; +export const SEARCH_ELASTICSEARCH = 'enterpriseSearchElasticsearch'; +export const SEARCH_VECTOR_SEARCH = 'enterpriseSearchVectorSearch'; +export const SEARCH_SEMANTIC_SEARCH = 'enterpriseSearchSemanticSearch'; +export const SEARCH_AI_SEARCH = 'enterpriseSearchAISearch'; diff --git a/packages/deeplinks/search/deep_links.ts b/packages/deeplinks/search/deep_links.ts index 98703f18ac3fb..22dfb91bdff33 100644 --- a/packages/deeplinks/search/deep_links.ts +++ b/packages/deeplinks/search/deep_links.ts @@ -22,6 +22,10 @@ import { SEARCH_HOMEPAGE, SEARCH_INDICES_START, SEARCH_INDICES, + SEARCH_ELASTICSEARCH, + SEARCH_VECTOR_SEARCH, + SEARCH_SEMANTIC_SEARCH, + SEARCH_AI_SEARCH, } from './constants'; export type EnterpriseSearchApp = typeof ENTERPRISE_SEARCH_APP_ID; @@ -38,6 +42,10 @@ export type SearchInferenceEndpointsId = typeof SERVERLESS_ES_SEARCH_INFERENCE_E export type SearchHomepage = typeof SEARCH_HOMEPAGE; export type SearchStart = typeof SEARCH_INDICES_START; export type SearchIndices = typeof SEARCH_INDICES; +export type SearchElasticsearch = typeof SEARCH_ELASTICSEARCH; +export type SearchVectorSearch = typeof SEARCH_VECTOR_SEARCH; +export type SearchSemanticSearch = typeof SEARCH_SEMANTIC_SEARCH; +export type SearchAISearch = typeof SEARCH_AI_SEARCH; export type ContentLinkId = 'searchIndices' | 'connectors' | 'webCrawlers'; @@ -65,4 +73,8 @@ export type DeepLinkId = | `${EnterpriseSearchAppsearchApp}:${AppsearchLinkId}` | `${EnterpriseSearchRelevanceApp}:${RelevanceLinkId}` | SearchStart - | SearchIndices; + | SearchIndices + | SearchElasticsearch + | SearchVectorSearch + | SearchSemanticSearch + | SearchAISearch; diff --git a/packages/deeplinks/search/index.ts b/packages/deeplinks/search/index.ts index 250dfeed299e6..7c78d64081133 100644 --- a/packages/deeplinks/search/index.ts +++ b/packages/deeplinks/search/index.ts @@ -17,6 +17,10 @@ export { ENTERPRISE_SEARCH_WORKPLACESEARCH_APP_ID, SERVERLESS_ES_APP_ID, SERVERLESS_ES_CONNECTORS_ID, + SEARCH_ELASTICSEARCH, + SEARCH_VECTOR_SEARCH, + SEARCH_SEMANTIC_SEARCH, + SEARCH_AI_SEARCH, } from './constants'; export type { diff --git a/x-pack/plugins/enterprise_search/common/constants.ts b/x-pack/plugins/enterprise_search/common/constants.ts index 67dfa03dc3705..797f94fa29e51 100644 --- a/x-pack/plugins/enterprise_search/common/constants.ts +++ b/x-pack/plugins/enterprise_search/common/constants.ts @@ -15,6 +15,10 @@ import { ENTERPRISE_SEARCH_ANALYTICS_APP_ID, ENTERPRISE_SEARCH_APPSEARCH_APP_ID, ENTERPRISE_SEARCH_WORKPLACESEARCH_APP_ID, + SEARCH_ELASTICSEARCH, + SEARCH_VECTOR_SEARCH, + SEARCH_SEMANTIC_SEARCH, + SEARCH_AI_SEARCH, } from '@kbn/deeplinks-search'; import { i18n } from '@kbn/i18n'; @@ -58,7 +62,7 @@ export const ENTERPRISE_SEARCH_CONTENT_PLUGIN = { }; export const AI_SEARCH_PLUGIN = { - ID: 'enterpriseSearchAISearch', + ID: SEARCH_AI_SEARCH, NAME: i18n.translate('xpack.enterpriseSearch.aiSearch.productName', { defaultMessage: 'AI Search', }), @@ -91,7 +95,7 @@ export const ANALYTICS_PLUGIN = { }; export const ELASTICSEARCH_PLUGIN = { - ID: 'enterpriseSearchElasticsearch', + ID: SEARCH_ELASTICSEARCH, NAME: i18n.translate('xpack.enterpriseSearch.elasticsearch.productName', { defaultMessage: 'Elasticsearch', }), @@ -167,7 +171,7 @@ export const VECTOR_SEARCH_PLUGIN = { defaultMessage: 'Elasticsearch can be used as a vector database, which enables vector search and semantic search use cases.', }), - ID: 'enterpriseSearchVectorSearch', + ID: SEARCH_VECTOR_SEARCH, LOGO: 'logoEnterpriseSearch', NAME: i18n.translate('xpack.enterpriseSearch.vectorSearch.productName', { defaultMessage: 'Vector Search', @@ -184,7 +188,7 @@ export const SEMANTIC_SEARCH_PLUGIN = { defaultMessage: 'Easily add semantic search to Elasticsearch with inference endpoints and the semantic_text field type, to boost search relevance.', }), - ID: 'enterpriseSearchSemanticSearch', + ID: SEARCH_SEMANTIC_SEARCH, LOGO: 'logoEnterpriseSearch', NAME: i18n.translate('xpack.enterpriseSearch.SemanticSearch.productName', { defaultMessage: 'Semantic Search', @@ -297,3 +301,14 @@ export const CRAWLER = { // TODO remove this once the connector service types are no longer in "example" state export const EXAMPLE_CONNECTOR_SERVICE_TYPES = ['opentext_documentum']; + +export const GETTING_STARTED_TITLE = i18n.translate('xpack.enterpriseSearch.gettingStarted.title', { + defaultMessage: 'Getting started', +}); + +export const SEARCH_APPS_BREADCRUMB = i18n.translate( + 'xpack.enterpriseSearch.searchApplications.breadcrumb', + { + defaultMessage: 'Search Applications', + } +); diff --git a/x-pack/plugins/enterprise_search/public/applications/__mocks__/kea_logic/kibana_logic.mock.ts b/x-pack/plugins/enterprise_search/public/applications/__mocks__/kea_logic/kibana_logic.mock.ts index cca5523ded681..9b37c661d923a 100644 --- a/x-pack/plugins/enterprise_search/public/applications/__mocks__/kea_logic/kibana_logic.mock.ts +++ b/x-pack/plugins/enterprise_search/public/applications/__mocks__/kea_logic/kibana_logic.mock.ts @@ -41,6 +41,7 @@ export const mockKibanaValues = { data: dataPluginMock.createStartContract(), esConfig: { elasticsearch_host: 'https://your_deployment_url' }, getChromeStyle$: jest.fn().mockReturnValue(of('classic')), + getNavLinks: jest.fn().mockReturnValue([]), guidedOnboarding: {}, history: mockHistory, indexMappingComponent: null, diff --git a/x-pack/plugins/enterprise_search/public/applications/applications/components/playground/page_template.tsx b/x-pack/plugins/enterprise_search/public/applications/applications/components/playground/page_template.tsx new file mode 100644 index 0000000000000..40698b273730b --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/applications/components/playground/page_template.tsx @@ -0,0 +1,71 @@ +/* + * 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, { useLayoutEffect } from 'react'; + +import { useValues } from 'kea'; + +import useObservable from 'react-use/lib/useObservable'; + +import { SEARCH_PRODUCT_NAME } from '../../../../../common/constants'; +import { KibanaLogic } from '../../../shared/kibana'; +import { SetSearchPlaygroundChrome } from '../../../shared/kibana_chrome/set_chrome'; +import { EnterpriseSearchPageTemplateWrapper, PageTemplateProps } from '../../../shared/layout'; +import { useEnterpriseSearchNav } from '../../../shared/layout'; +import { SendEnterpriseSearchTelemetry } from '../../../shared/telemetry'; + +import { PlaygroundHeaderDocsAction } from './header_docs_action'; + +export type SearchPlaygroundPageTemplateProps = Omit< + PageTemplateProps, + 'useEndpointHeaderActions' +> & { + hasSchemaConflicts?: boolean; + restrictWidth?: boolean; + searchApplicationName?: string; +}; + +export const SearchPlaygroundPageTemplate: React.FC = ({ + children, + pageChrome, + pageViewTelemetry, + searchApplicationName, + hasSchemaConflicts, + restrictWidth = true, + ...pageTemplateProps +}) => { + const navItems = useEnterpriseSearchNav(); + + const { renderHeaderActions, getChromeStyle$ } = useValues(KibanaLogic); + const chromeStyle = useObservable(getChromeStyle$(), 'classic'); + + useLayoutEffect(() => { + renderHeaderActions(PlaygroundHeaderDocsAction); + + return () => { + renderHeaderActions(); + }; + }, []); + + return ( + } + useEndpointHeaderActions={false} + > + {pageViewTelemetry && ( + + )} + {children} + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/applications/components/playground/playground.tsx b/x-pack/plugins/enterprise_search/public/applications/applications/components/playground/playground.tsx index b117518d3a6e0..e8e72e5dfb37a 100644 --- a/x-pack/plugins/enterprise_search/public/applications/applications/components/playground/playground.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/applications/components/playground/playground.tsx @@ -12,7 +12,8 @@ import { useValues } from 'kea'; import { i18n } from '@kbn/i18n'; import { KibanaLogic } from '../../../shared/kibana'; -import { EnterpriseSearchApplicationsPageTemplate } from '../layout/page_template'; + +import { SearchPlaygroundPageTemplate } from './page_template'; export const Playground: React.FC = () => { const { searchPlayground } = useValues(KibanaLogic); @@ -22,7 +23,7 @@ export const Playground: React.FC = () => { } return ( - { panelled={false} customPageSections bottomBorder="extended" - docLink="playground" > - + ); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/elasticsearch/components/elasticsearch_guide/elasticsearch_guide.tsx b/x-pack/plugins/enterprise_search/public/applications/elasticsearch/components/elasticsearch_guide/elasticsearch_guide.tsx index 80a8de9acdc21..be470577cd519 100644 --- a/x-pack/plugins/enterprise_search/public/applications/elasticsearch/components/elasticsearch_guide/elasticsearch_guide.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/elasticsearch/components/elasticsearch_guide/elasticsearch_guide.tsx @@ -40,7 +40,7 @@ export const ElasticsearchGuide = () => { }, []); return ( - + {isFlyoutOpen && setIsFlyoutOpen(false)} />}

diff --git a/x-pack/plugins/enterprise_search/public/applications/elasticsearch/components/layout/page_template.tsx b/x-pack/plugins/enterprise_search/public/applications/elasticsearch/components/layout/page_template.tsx index 7f2eded8a6565..c5c777cb74773 100644 --- a/x-pack/plugins/enterprise_search/public/applications/elasticsearch/components/layout/page_template.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/elasticsearch/components/layout/page_template.tsx @@ -19,13 +19,14 @@ export const EnterpriseSearchElasticsearchPageTemplate: React.FC { + const navItems = useEnterpriseSearchNav(); return ( } > diff --git a/x-pack/plugins/enterprise_search/public/applications/index.tsx b/x-pack/plugins/enterprise_search/public/applications/index.tsx index eafa8827869d8..717379d433dd1 100644 --- a/x-pack/plugins/enterprise_search/public/applications/index.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/index.tsx @@ -114,6 +114,7 @@ export const renderApp = ( data: plugins.data, esConfig, getChromeStyle$: chrome.getChromeStyle$, + getNavLinks: chrome.navLinks.getAll, guidedOnboarding, history, indexMappingComponent, diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/kibana/kibana_logic.ts b/x-pack/plugins/enterprise_search/public/applications/shared/kibana/kibana_logic.ts index f74345a1c75c1..6cd6e5410ef11 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/kibana/kibana_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/kibana/kibana_logic.ts @@ -55,6 +55,7 @@ export interface KibanaLogicProps { data?: DataPublicPluginStart; esConfig: ESConfig; getChromeStyle$: ChromeStart['getChromeStyle$']; + getNavLinks: ChromeStart['navLinks']['getAll']; guidedOnboarding?: GuidedOnboardingPluginStart; history: ScopedHistory; indexMappingComponent?: React.FC; @@ -87,6 +88,7 @@ export interface KibanaValues { data: DataPublicPluginStart | null; esConfig: ESConfig; getChromeStyle$: ChromeStart['getChromeStyle$']; + getNavLinks: ChromeStart['navLinks']['getAll']; guidedOnboarding: GuidedOnboardingPluginStart | null; history: ScopedHistory; indexMappingComponent: React.FC | null; @@ -126,6 +128,7 @@ export const KibanaLogic = kea>({ data: [props.data || null, {}], esConfig: [props.esConfig || { elasticsearch_host: ELASTICSEARCH_URL_PLACEHOLDER }, {}], getChromeStyle$: [props.getChromeStyle$, {}], + getNavLinks: [props.getNavLinks, {}], guidedOnboarding: [props.guidedOnboarding || null, {}], history: [props.history, {}], indexMappingComponent: [props.indexMappingComponent || null, {}], diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/kibana_chrome/generate_breadcrumbs.ts b/x-pack/plugins/enterprise_search/public/applications/shared/kibana_chrome/generate_breadcrumbs.ts index ea6bda26be450..189ca53e362e1 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/kibana_chrome/generate_breadcrumbs.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/kibana_chrome/generate_breadcrumbs.ts @@ -22,6 +22,8 @@ import { VECTOR_SEARCH_PLUGIN, WORKPLACE_SEARCH_PLUGIN, SEMANTIC_SEARCH_PLUGIN, + APPLICATIONS_PLUGIN, + GETTING_STARTED_TITLE, } from '../../../../common/constants'; import { stripLeadingSlash } from '../../../../common/strip_slashes'; @@ -126,7 +128,11 @@ export const useEnterpriseSearchBreadcrumbs = (breadcrumbs: Breadcrumbs = []) => ]); export const useAnalyticsBreadcrumbs = (breadcrumbs: Breadcrumbs = []) => - useSearchBreadcrumbs([{ text: ANALYTICS_PLUGIN.NAME, path: '/' }, ...breadcrumbs]); + useSearchBreadcrumbs([ + { text: APPLICATIONS_PLUGIN.NAV_TITLE }, + { text: ANALYTICS_PLUGIN.NAME, path: '/' }, + ...breadcrumbs, + ]); export const useElasticsearchBreadcrumbs = (breadcrumbs: Breadcrumbs = []) => useSearchBreadcrumbs([ @@ -161,13 +167,25 @@ export const useSearchExperiencesBreadcrumbs = (breadcrumbs: Breadcrumbs = []) = useSearchBreadcrumbs([{ text: SEARCH_EXPERIENCES_PLUGIN.NAV_TITLE, path: '/' }, ...breadcrumbs]); export const useEnterpriseSearchApplicationsBreadcrumbs = (breadcrumbs: Breadcrumbs = []) => - useSearchBreadcrumbs(breadcrumbs); + useSearchBreadcrumbs([{ text: APPLICATIONS_PLUGIN.NAV_TITLE }, ...breadcrumbs]); export const useAiSearchBreadcrumbs = (breadcrumbs: Breadcrumbs = []) => - useSearchBreadcrumbs([{ text: AI_SEARCH_PLUGIN.NAME, path: '/' }, ...breadcrumbs]); + useSearchBreadcrumbs([ + { text: GETTING_STARTED_TITLE }, + { text: AI_SEARCH_PLUGIN.NAME, path: '/' }, + ...breadcrumbs, + ]); export const useVectorSearchBreadcrumbs = (breadcrumbs: Breadcrumbs = []) => - useSearchBreadcrumbs([{ text: VECTOR_SEARCH_PLUGIN.NAV_TITLE, path: '/' }, ...breadcrumbs]); + useSearchBreadcrumbs([ + { text: GETTING_STARTED_TITLE }, + { text: VECTOR_SEARCH_PLUGIN.NAV_TITLE, path: '/' }, + ...breadcrumbs, + ]); export const useSemanticSearchBreadcrumbs = (breadcrumbs: Breadcrumbs = []) => - useSearchBreadcrumbs([{ text: SEMANTIC_SEARCH_PLUGIN.NAME, path: '/' }, ...breadcrumbs]); + useSearchBreadcrumbs([ + { text: GETTING_STARTED_TITLE }, + { text: SEMANTIC_SEARCH_PLUGIN.NAME, path: '/' }, + ...breadcrumbs, + ]); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/kibana_chrome/generate_title.ts b/x-pack/plugins/enterprise_search/public/applications/shared/kibana_chrome/generate_title.ts index eaeb30f1540d0..df7d16cddc4d4 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/kibana_chrome/generate_title.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/kibana_chrome/generate_title.ts @@ -5,6 +5,8 @@ * 2.0. */ +import { i18n } from '@kbn/i18n'; + import { AI_SEARCH_PLUGIN, ANALYTICS_PLUGIN, @@ -40,7 +42,12 @@ export const searchTitle = (page: Title = []) => generateTitle([...page, SEARCH_ export const analyticsTitle = (page: Title = []) => generateTitle([...page, ANALYTICS_PLUGIN.NAME]); export const elasticsearchTitle = (page: Title = []) => - generateTitle([...page, 'Getting started with Elasticsearch']); + generateTitle([ + ...page, + i18n.translate('xpack.enterpriseSearch.titles.elasticsearch', { + defaultMessage: 'Getting started with Elasticsearch', + }), + ]); export const appSearchTitle = (page: Title = []) => generateTitle([...page, APP_SEARCH_PLUGIN.NAME]); @@ -61,3 +68,11 @@ export const semanticSearchTitle = (page: Title = []) => export const enterpriseSearchContentTitle = (page: Title = []) => generateTitle([...page, ENTERPRISE_SEARCH_CONTENT_PLUGIN.NAME]); + +export const searchApplicationsTitle = (page: Title = []) => + generateTitle([ + ...page, + i18n.translate('xpack.enterpriseSearch.titles.searchApplications', { + defaultMessage: 'Search Applications', + }), + ]); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/kibana_chrome/set_chrome.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/kibana_chrome/set_chrome.tsx index 8f7c71d1309c0..0c05cb0c02ca0 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/kibana_chrome/set_chrome.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/kibana_chrome/set_chrome.tsx @@ -9,8 +9,7 @@ import React, { useEffect } from 'react'; import { useValues } from 'kea'; -import { APPLICATIONS_PLUGIN } from '../../../../common/constants'; - +import { SEARCH_APPS_BREADCRUMB } from '../../../../common/constants'; import { KibanaLogic } from '../kibana'; import { @@ -35,6 +34,8 @@ import { appSearchTitle, elasticsearchTitle, enterpriseSearchContentTitle, + generateTitle, + searchApplicationsTitle, searchExperiencesTitle, searchTitle, semanticSearchTitle, @@ -210,14 +211,30 @@ export const SetSearchExperiencesChrome: React.FC = ({ trail = [ return null; }; +export const SetSearchPlaygroundChrome: React.FC = ({ trail = [] }) => { + const { setBreadcrumbs, setDocTitle } = useValues(KibanaLogic); + + const title = reverseArray(trail); + const docTitle = generateTitle(title); + + const breadcrumbs = useEnterpriseSearchApplicationsBreadcrumbs(useGenerateBreadcrumbs(trail)); + + useEffect(() => { + setBreadcrumbs(breadcrumbs); + setDocTitle(docTitle); + }, [trail]); + + return null; +}; + export const SetEnterpriseSearchApplicationsChrome: React.FC = ({ trail = [] }) => { const { setBreadcrumbs, setDocTitle } = useValues(KibanaLogic); const title = reverseArray(trail); - const docTitle = appSearchTitle(title); + const docTitle = searchApplicationsTitle(title); const breadcrumbs = useEnterpriseSearchApplicationsBreadcrumbs( - useGenerateBreadcrumbs([APPLICATIONS_PLUGIN.NAV_TITLE, ...trail]) + useGenerateBreadcrumbs([SEARCH_APPS_BREADCRUMB, ...trail]) ); useEffect(() => { diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/layout/base_nav.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/layout/base_nav.tsx new file mode 100644 index 0000000000000..b971ab6deff53 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/layout/base_nav.tsx @@ -0,0 +1,201 @@ +/* + * 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 { EuiText } from '@elastic/eui'; +import { + ENTERPRISE_SEARCH_APP_ID, + ENTERPRISE_SEARCH_ANALYTICS_APP_ID, + SEARCH_ELASTICSEARCH, + SEARCH_VECTOR_SEARCH, + SEARCH_SEMANTIC_SEARCH, + SEARCH_AI_SEARCH, +} from '@kbn/deeplinks-search'; +import { i18n } from '@kbn/i18n'; + +import { GETTING_STARTED_TITLE } from '../../../../common/constants'; + +import { ClassicNavItem, BuildClassicNavParameters } from '../types'; + +export const buildBaseClassicNavItems = ({ + productAccess, +}: BuildClassicNavParameters): ClassicNavItem[] => { + const navItems: ClassicNavItem[] = []; + + // Home + navItems.push({ + 'data-test-subj': 'searchSideNav-Home', + deepLink: { + link: ENTERPRISE_SEARCH_APP_ID, + shouldShowActiveForSubroutes: true, + }, + id: 'home', + name: ( + + {i18n.translate('xpack.enterpriseSearch.nav.homeTitle', { + defaultMessage: 'Home', + })} + + ), + }); + + // Content + navItems.push({ + 'data-test-subj': 'searchSideNav-Content', + id: 'content', + items: [ + { + 'data-test-subj': 'searchSideNav-Indices', + deepLink: { + link: 'enterpriseSearchContent:searchIndices', + shouldShowActiveForSubroutes: true, + }, + id: 'search_indices', + }, + { + 'data-test-subj': 'searchSideNav-Connectors', + deepLink: { + link: 'enterpriseSearchContent:connectors', + shouldShowActiveForSubroutes: true, + }, + id: 'connectors', + }, + { + 'data-test-subj': 'searchSideNav-Crawlers', + deepLink: { + link: 'enterpriseSearchContent:webCrawlers', + shouldShowActiveForSubroutes: true, + }, + id: 'crawlers', + }, + ], + name: i18n.translate('xpack.enterpriseSearch.nav.contentTitle', { + defaultMessage: 'Content', + }), + }); + + // Build + navItems.push({ + 'data-test-subj': 'searchSideNav-Build', + id: 'build', + items: [ + { + 'data-test-subj': 'searchSideNav-Playground', + deepLink: { + link: 'enterpriseSearchApplications:playground', + shouldShowActiveForSubroutes: true, + }, + id: 'playground', + }, + { + 'data-test-subj': 'searchSideNav-SearchApplications', + deepLink: { + link: 'enterpriseSearchApplications:searchApplications', + }, + id: 'searchApplications', + }, + { + 'data-test-subj': 'searchSideNav-BehavioralAnalytics', + deepLink: { + link: ENTERPRISE_SEARCH_ANALYTICS_APP_ID, + }, + id: 'analyticsCollections', + }, + ], + name: i18n.translate('xpack.enterpriseSearch.nav.applicationsTitle', { + defaultMessage: 'Build', + }), + }); + + navItems.push({ + 'data-test-subj': 'searchSideNav-Relevance', + id: 'relevance', + items: [ + { + 'data-test-subj': 'searchSideNav-InferenceEndpoints', + deepLink: { + link: 'searchInferenceEndpoints:inferenceEndpoints', + shouldShowActiveForSubroutes: true, + }, + id: 'inference_endpoints', + }, + ], + name: i18n.translate('xpack.enterpriseSearch.nav.relevanceTitle', { + defaultMessage: 'Relevance', + }), + }); + + // Getting Started + navItems.push({ + 'data-test-subj': 'searchSideNav-GettingStarted', + id: 'es_getting_started', + items: [ + { + 'data-test-subj': 'searchSideNav-Elasticsearch', + deepLink: { + link: SEARCH_ELASTICSEARCH, + }, + id: 'elasticsearch', + }, + { + 'data-test-subj': 'searchSideNav-VectorSearch', + deepLink: { + link: SEARCH_VECTOR_SEARCH, + }, + id: 'vectorSearch', + }, + { + 'data-test-subj': 'searchSideNav-SemanticSearch', + deepLink: { + link: SEARCH_SEMANTIC_SEARCH, + }, + id: 'semanticSearch', + }, + { + 'data-test-subj': 'searchSideNav-AISearch', + deepLink: { + link: SEARCH_AI_SEARCH, + }, + id: 'aiSearch', + }, + ], + name: GETTING_STARTED_TITLE, + }); + + if (productAccess.hasAppSearchAccess || productAccess.hasWorkplaceSearchAccess) { + const entSearchItems: ClassicNavItem[] = []; + if (productAccess.hasAppSearchAccess) { + entSearchItems.push({ + 'data-test-subj': 'searchSideNav-AppSearch', + deepLink: { + link: 'appSearch:engines', + }, + id: 'app_search', + }); + } + if (productAccess.hasWorkplaceSearchAccess) { + entSearchItems.push({ + 'data-test-subj': 'searchSideNav-WorkplaceSearch', + deepLink: { + link: 'workplaceSearch', + }, + id: 'workplace_search', + }); + } + navItems.push({ + 'data-test-subj': 'searchSideNav-EnterpriseSearch', + id: 'enterpriseSearch', + items: entSearchItems, + name: i18n.translate('xpack.enterpriseSearch.nav.title', { + defaultMessage: 'Enterprise Search', + }), + }); + } + + return navItems; +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/layout/classic_nav_helpers.test.ts b/x-pack/plugins/enterprise_search/public/applications/shared/layout/classic_nav_helpers.test.ts new file mode 100644 index 0000000000000..514072ba297aa --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/layout/classic_nav_helpers.test.ts @@ -0,0 +1,189 @@ +/* + * 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 { mockKibanaValues } from '../../__mocks__/kea_logic'; + +import type { ChromeNavLink } from '@kbn/core-chrome-browser'; + +import '../../__mocks__/react_router'; + +jest.mock('../react_router_helpers/link_events', () => ({ + letBrowserHandleEvent: jest.fn(), +})); + +import { ClassicNavItem } from '../types'; + +import { generateSideNavItems } from './classic_nav_helpers'; + +describe('generateSideNavItems', () => { + const deepLinksMap = { + enterpriseSearch: { + id: 'enterpriseSearch', + url: '/app/enterprise_search/overview', + title: 'Overview', + }, + 'enterpriseSearchContent:searchIndices': { + id: 'enterpriseSearchContent:searchIndices', + title: 'Indices', + url: '/app/enterprise_search/content/search_indices', + }, + 'enterpriseSearchContent:connectors': { + id: 'enterpriseSearchContent:connectors', + title: 'Connectors', + url: '/app/enterprise_search/content/connectors', + }, + 'enterpriseSearchContent:webCrawlers': { + id: 'enterpriseSearchContent:webCrawlers', + title: 'Web crawlers', + url: '/app/enterprise_search/content/crawlers', + }, + } as unknown as Record; + beforeEach(() => { + jest.clearAllMocks(); + mockKibanaValues.history.location.pathname = '/'; + }); + + it('renders top-level items', () => { + const classicNavItems: ClassicNavItem[] = [ + { + id: 'unit-test', + deepLink: { + link: 'enterpriseSearch', + }, + }, + ]; + + expect(generateSideNavItems(classicNavItems, deepLinksMap)).toEqual([ + { + href: '/app/enterprise_search/overview', + id: 'unit-test', + isSelected: false, + name: 'Overview', + onClick: expect.any(Function), + }, + ]); + }); + + it('renders items with children', () => { + const classicNavItems: ClassicNavItem[] = [ + { + id: 'parent', + name: 'Parent', + items: [ + { + id: 'unit-test', + deepLink: { + link: 'enterpriseSearch', + }, + }, + ], + }, + ]; + + expect(generateSideNavItems(classicNavItems, deepLinksMap)).toEqual([ + { + id: 'parent', + items: [ + { + href: '/app/enterprise_search/overview', + id: 'unit-test', + isSelected: false, + name: 'Overview', + onClick: expect.any(Function), + }, + ], + name: 'Parent', + }, + ]); + }); + + it('renders classic nav name over deep link title if provided', () => { + const classicNavItems: ClassicNavItem[] = [ + { + deepLink: { + link: 'enterpriseSearch', + }, + id: 'unit-test', + name: 'Home', + }, + ]; + + expect(generateSideNavItems(classicNavItems, deepLinksMap)).toEqual([ + { + href: '/app/enterprise_search/overview', + id: 'unit-test', + isSelected: false, + name: 'Home', + onClick: expect.any(Function), + }, + ]); + }); + + it('removes item if deep link is not defined', () => { + const classicNavItems: ClassicNavItem[] = [ + { + deepLink: { + link: 'enterpriseSearch', + }, + id: 'unit-test', + name: 'Home', + }, + { + deepLink: { + link: 'enterpriseSearchApplications:playground', + }, + id: 'unit-test-missing', + }, + ]; + + expect(generateSideNavItems(classicNavItems, deepLinksMap)).toEqual([ + { + href: '/app/enterprise_search/overview', + id: 'unit-test', + isSelected: false, + name: 'Home', + onClick: expect.any(Function), + }, + ]); + }); + + it('adds pre-rendered child items provided', () => { + const classicNavItems: ClassicNavItem[] = [ + { + id: 'unit-test', + name: 'Indices', + }, + ]; + const subItems = { + 'unit-test': [ + { + href: '/app/unit-test', + id: 'child', + isSelected: true, + name: 'Index', + onClick: jest.fn(), + }, + ], + }; + + expect(generateSideNavItems(classicNavItems, deepLinksMap, subItems)).toEqual([ + { + id: 'unit-test', + items: [ + { + href: '/app/unit-test', + id: 'child', + isSelected: true, + name: 'Index', + onClick: expect.any(Function), + }, + ], + name: 'Indices', + }, + ]); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/layout/classic_nav_helpers.ts b/x-pack/plugins/enterprise_search/public/applications/shared/layout/classic_nav_helpers.ts new file mode 100644 index 0000000000000..89f3c2ab5b59a --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/layout/classic_nav_helpers.ts @@ -0,0 +1,102 @@ +/* + * 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 { ChromeNavLink, EuiSideNavItemTypeEnhanced } from '@kbn/core-chrome-browser'; + +import { + ClassicNavItem, + GenerateNavLinkFromDeepLinkParameters, + GenerateNavLinkParameters, +} from '../types'; + +import { generateNavLink } from './nav_link_helpers'; + +export const generateSideNavItems = ( + navItems: ClassicNavItem[], + deepLinks: Record, + subItemsMap: Record> | undefined> = {} +): Array> => { + const sideNavItems: Array> = []; + + for (const navItem of navItems) { + let sideNavChildItems: Array> | undefined; + + const { deepLink, items, ...rest } = navItem; + const subItems = subItemsMap?.[navItem.id]; + + if (items || subItems) { + sideNavChildItems = []; + if (items) { + sideNavChildItems.push(...generateSideNavItems(items, deepLinks, subItemsMap)); + } + if (subItems) { + sideNavChildItems.push(...subItems); + } + } + + let sideNavItem: EuiSideNavItemTypeEnhanced | undefined; + if (deepLink) { + const navLinkParams = getNavLinkParameters(deepLink, deepLinks); + if (navLinkParams !== undefined) { + const name = navItem.name ?? getDeepLinkTitle(deepLink.link, deepLinks); + sideNavItem = { + ...rest, + name, + ...generateNavLink({ + ...navLinkParams, + items: sideNavChildItems, + }), + }; + } + } else { + sideNavItem = { + ...rest, + items: sideNavChildItems, + name: navItem.name, + }; + } + + if (isValidSideNavItem(sideNavItem)) { + sideNavItems.push(sideNavItem); + } + } + + return sideNavItems; +}; + +const getNavLinkParameters = ( + navLink: GenerateNavLinkFromDeepLinkParameters, + deepLinks: Record +): GenerateNavLinkParameters | undefined => { + const { link, ...navLinkProps } = navLink; + const deepLink = deepLinks[link]; + if (!deepLink || !deepLink.url) return undefined; + return { + ...navLinkProps, + shouldNotCreateHref: true, + shouldNotPrepend: true, + to: deepLink.url, + }; +}; +const getDeepLinkTitle = ( + link: string, + deepLinks: Record +): string | undefined => { + const deepLink = deepLinks[link]; + if (!deepLink || !deepLink.url) return undefined; + return deepLink.title; +}; + +function isValidSideNavItem( + item: EuiSideNavItemTypeEnhanced | undefined +): item is EuiSideNavItemTypeEnhanced { + if (item === undefined) return false; + if (item.href || item.onClick) return true; + if (item?.items?.length ?? 0 > 0) return true; + + return false; +} diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/layout/nav.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/layout/nav.test.tsx index b2c31ff4868bc..3305e92dd8d9e 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/layout/nav.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/layout/nav.test.tsx @@ -15,6 +15,8 @@ jest.mock('../../enterprise_search_content/components/search_index/indices/indic import { setMockValues, mockKibanaValues } from '../../__mocks__/kea_logic'; +import { renderHook } from '@testing-library/react-hooks'; + import { EuiSideNavItemType } from '@elastic/eui'; import { DEFAULT_PRODUCT_FEATURES } from '../../../../common/constants'; @@ -32,26 +34,31 @@ const DEFAULT_PRODUCT_ACCESS: ProductAccess = { }; const baseNavItems = [ expect.objectContaining({ + 'data-test-subj': 'searchSideNav-Home', href: '/app/enterprise_search/overview', id: 'home', items: undefined, }), { + 'data-test-subj': 'searchSideNav-Content', id: 'content', items: [ { + 'data-test-subj': 'searchSideNav-Indices', href: '/app/enterprise_search/content/search_indices', id: 'search_indices', items: [], name: 'Indices', }, { + 'data-test-subj': 'searchSideNav-Connectors', href: '/app/enterprise_search/content/connectors', id: 'connectors', items: undefined, name: 'Connectors', }, { + 'data-test-subj': 'searchSideNav-Crawlers', href: '/app/enterprise_search/content/crawlers', id: 'crawlers', items: undefined, @@ -61,21 +68,25 @@ const baseNavItems = [ name: 'Content', }, { + 'data-test-subj': 'searchSideNav-Build', id: 'build', items: [ { + 'data-test-subj': 'searchSideNav-Playground', href: '/app/enterprise_search/applications/playground', id: 'playground', items: undefined, name: 'Playground', }, { + 'data-test-subj': 'searchSideNav-SearchApplications', href: '/app/enterprise_search/applications/search_applications', id: 'searchApplications', items: undefined, name: 'Search Applications', }, { + 'data-test-subj': 'searchSideNav-BehavioralAnalytics', href: '/app/enterprise_search/analytics', id: 'analyticsCollections', items: undefined, @@ -85,9 +96,11 @@ const baseNavItems = [ name: 'Build', }, { + 'data-test-subj': 'searchSideNav-Relevance', id: 'relevance', items: [ { + 'data-test-subj': 'searchSideNav-InferenceEndpoints', href: '/app/enterprise_search/relevance/inference_endpoints', id: 'inference_endpoints', items: undefined, @@ -97,27 +110,32 @@ const baseNavItems = [ name: 'Relevance', }, { + 'data-test-subj': 'searchSideNav-GettingStarted', id: 'es_getting_started', items: [ { + 'data-test-subj': 'searchSideNav-Elasticsearch', href: '/app/enterprise_search/elasticsearch', id: 'elasticsearch', items: undefined, name: 'Elasticsearch', }, { + 'data-test-subj': 'searchSideNav-VectorSearch', href: '/app/enterprise_search/vector_search', id: 'vectorSearch', items: undefined, name: 'Vector Search', }, { + 'data-test-subj': 'searchSideNav-SemanticSearch', href: '/app/enterprise_search/semantic_search', id: 'semanticSearch', items: undefined, name: 'Semantic Search', }, { + 'data-test-subj': 'searchSideNav-AISearch', href: '/app/enterprise_search/ai_search', id: 'aiSearch', items: undefined, @@ -127,15 +145,18 @@ const baseNavItems = [ name: 'Getting started', }, { + 'data-test-subj': 'searchSideNav-EnterpriseSearch', id: 'enterpriseSearch', items: [ { + 'data-test-subj': 'searchSideNav-AppSearch', href: '/app/enterprise_search/app_search', id: 'app_search', items: undefined, name: 'App Search', }, { + 'data-test-subj': 'searchSideNav-WorkplaceSearch', href: '/app/enterprise_search/workplace_search', id: 'workplace_search', items: undefined, @@ -146,21 +167,102 @@ const baseNavItems = [ }, ]; +const mockNavLinks = [ + { + id: 'enterpriseSearch', + url: '/app/enterprise_search/overview', + }, + { + id: 'enterpriseSearchContent:searchIndices', + title: 'Indices', + url: '/app/enterprise_search/content/search_indices', + }, + { + id: 'enterpriseSearchContent:connectors', + title: 'Connectors', + url: '/app/enterprise_search/content/connectors', + }, + { + id: 'enterpriseSearchContent:webCrawlers', + title: 'Web crawlers', + url: '/app/enterprise_search/content/crawlers', + }, + { + id: 'enterpriseSearchApplications:playground', + title: 'Playground', + url: '/app/enterprise_search/applications/playground', + }, + { + id: 'enterpriseSearchApplications:searchApplications', + title: 'Search Applications', + url: '/app/enterprise_search/applications/search_applications', + }, + { + id: 'enterpriseSearchAnalytics', + title: 'Behavioral Analytics', + url: '/app/enterprise_search/analytics', + }, + { + id: 'searchInferenceEndpoints:inferenceEndpoints', + title: 'Inference Endpoints', + url: '/app/enterprise_search/relevance/inference_endpoints', + }, + { + id: 'appSearch:engines', + title: 'App Search', + url: '/app/enterprise_search/app_search', + }, + { + id: 'workplaceSearch', + title: 'Workplace Search', + url: '/app/enterprise_search/workplace_search', + }, + { + id: 'enterpriseSearchElasticsearch', + title: 'Elasticsearch', + url: '/app/enterprise_search/elasticsearch', + }, + { + id: 'enterpriseSearchVectorSearch', + title: 'Vector Search', + url: '/app/enterprise_search/vector_search', + }, + { + id: 'enterpriseSearchSemanticSearch', + title: 'Semantic Search', + url: '/app/enterprise_search/semantic_search', + }, + { + id: 'enterpriseSearchAISearch', + title: 'AI Search', + url: '/app/enterprise_search/ai_search', + }, +]; + +const defaultMockValues = { + hasEnterpriseLicense: true, + isSidebarEnabled: true, + productAccess: DEFAULT_PRODUCT_ACCESS, + productFeatures: DEFAULT_PRODUCT_FEATURES, +}; + describe('useEnterpriseSearchContentNav', () => { beforeEach(() => { jest.clearAllMocks(); mockKibanaValues.uiSettings.get.mockReturnValue(false); + mockKibanaValues.getNavLinks.mockReturnValue(mockNavLinks); }); it('returns an array of top-level Enterprise Search nav items', () => { const fullProductAccess: ProductAccess = DEFAULT_PRODUCT_ACCESS; setMockValues({ - isSidebarEnabled: true, + ...defaultMockValues, productAccess: fullProductAccess, - productFeatures: DEFAULT_PRODUCT_FEATURES, }); - expect(useEnterpriseSearchNav()).toEqual(baseNavItems); + const { result } = renderHook(() => useEnterpriseSearchNav()); + + expect(result.current).toEqual(baseNavItems); }); it('excludes legacy products when the user has no access to them', () => { @@ -171,13 +273,13 @@ describe('useEnterpriseSearchContentNav', () => { }; setMockValues({ - isSidebarEnabled: true, + ...defaultMockValues, productAccess: noProductAccess, - productFeatures: DEFAULT_PRODUCT_FEATURES, }); mockKibanaValues.uiSettings.get.mockReturnValue(false); - const esNav = useEnterpriseSearchNav(); + const { result } = renderHook(() => useEnterpriseSearchNav()); + const esNav = result.current; const legacyESNav = esNav?.find((item) => item.id === 'enterpriseSearch'); expect(legacyESNav).toBeUndefined(); }); @@ -190,18 +292,20 @@ describe('useEnterpriseSearchContentNav', () => { }; setMockValues({ - isSidebarEnabled: true, + ...defaultMockValues, productAccess: workplaceSearchProductAccess, - productFeatures: DEFAULT_PRODUCT_FEATURES, }); - const esNav = useEnterpriseSearchNav(); + const { result } = renderHook(() => useEnterpriseSearchNav()); + const esNav = result.current; const legacyESNav = esNav?.find((item) => item.id === 'enterpriseSearch'); expect(legacyESNav).not.toBeUndefined(); expect(legacyESNav).toEqual({ + 'data-test-subj': 'searchSideNav-EnterpriseSearch', id: 'enterpriseSearch', items: [ { + 'data-test-subj': 'searchSideNav-WorkplaceSearch', href: '/app/enterprise_search/workplace_search', id: 'workplace_search', name: 'Workplace Search', @@ -218,18 +322,20 @@ describe('useEnterpriseSearchContentNav', () => { }; setMockValues({ - isSidebarEnabled: true, + ...defaultMockValues, productAccess: appSearchProductAccess, - productFeatures: DEFAULT_PRODUCT_FEATURES, }); - const esNav = useEnterpriseSearchNav(); + const { result } = renderHook(() => useEnterpriseSearchNav()); + const esNav = result.current; const legacyESNav = esNav?.find((item) => item.id === 'enterpriseSearch'); expect(legacyESNav).not.toBeUndefined(); expect(legacyESNav).toEqual({ + 'data-test-subj': 'searchSideNav-EnterpriseSearch', id: 'enterpriseSearch', items: [ { + 'data-test-subj': 'searchSideNav-AppSearch', href: '/app/enterprise_search/app_search', id: 'app_search', name: 'App Search', @@ -243,21 +349,21 @@ describe('useEnterpriseSearchContentNav', () => { describe('useEnterpriseSearchApplicationNav', () => { beforeEach(() => { jest.clearAllMocks(); + mockKibanaValues.getNavLinks.mockReturnValue(mockNavLinks); mockKibanaValues.uiSettings.get.mockReturnValue(true); - setMockValues({ - isSidebarEnabled: true, - productAccess: DEFAULT_PRODUCT_ACCESS, - productFeatures: DEFAULT_PRODUCT_FEATURES, - }); + setMockValues(defaultMockValues); }); it('returns an array of top-level Enterprise Search nav items', () => { - expect(useEnterpriseSearchApplicationNav()).toEqual(baseNavItems); + const { result } = renderHook(() => useEnterpriseSearchApplicationNav()); + expect(result.current).toEqual(baseNavItems); }); it('returns selected engine sub nav items', () => { const engineName = 'my-test-engine'; - const navItems = useEnterpriseSearchApplicationNav(engineName); + const { + result: { current: navItems }, + } = renderHook(() => useEnterpriseSearchApplicationNav(engineName)); expect(navItems![0].id).toEqual('home'); expect(navItems?.slice(1).map((ni) => ni.name)).toEqual([ 'Content', @@ -317,7 +423,9 @@ describe('useEnterpriseSearchApplicationNav', () => { it('returns selected engine without tabs when isEmpty', () => { const engineName = 'my-test-engine'; - const navItems = useEnterpriseSearchApplicationNav(engineName, true); + const { + result: { current: navItems }, + } = renderHook(() => useEnterpriseSearchApplicationNav(engineName, true)); expect(navItems![0].id).toEqual('home'); expect(navItems?.slice(1).map((ni) => ni.name)).toEqual([ 'Content', @@ -348,7 +456,9 @@ describe('useEnterpriseSearchApplicationNav', () => { it('returns selected engine with conflict warning when hasSchemaConflicts', () => { const engineName = 'my-test-engine'; - const navItems = useEnterpriseSearchApplicationNav(engineName, false, true); + const { + result: { current: navItems }, + } = renderHook(() => useEnterpriseSearchApplicationNav(engineName, false, true)); // @ts-ignore const engineItem = navItems @@ -383,27 +493,20 @@ describe('useEnterpriseSearchApplicationNav', () => { describe('useEnterpriseSearchAnalyticsNav', () => { beforeEach(() => { jest.clearAllMocks(); - setMockValues({ - isSidebarEnabled: true, - }); + setMockValues(defaultMockValues); + mockKibanaValues.getNavLinks.mockReturnValue(mockNavLinks); }); it('returns basic nav all params are empty', () => { - const navItems = useEnterpriseSearchAnalyticsNav(); - expect(navItems).toEqual( - baseNavItems.map((item) => - item.id === 'content' - ? { - ...item, - items: item.items, - } - : item - ) - ); + const { result } = renderHook(() => useEnterpriseSearchAnalyticsNav()); + + expect(result.current).toEqual(baseNavItems); }); it('returns basic nav if only name provided', () => { - const navItems = useEnterpriseSearchAnalyticsNav('my-test-collection'); + const { + result: { current: navItems }, + } = renderHook(() => useEnterpriseSearchAnalyticsNav('my-test-collection')); expect(navItems).toEqual( baseNavItems.map((item) => item.id === 'content' @@ -417,16 +520,21 @@ describe('useEnterpriseSearchAnalyticsNav', () => { }); it('returns nav with sub items when name and paths provided', () => { - const navItems = useEnterpriseSearchAnalyticsNav('my-test-collection', { - explorer: '/explorer-path', - integration: '/integration-path', - overview: '/overview-path', - }); + const { + result: { current: navItems }, + } = renderHook(() => + useEnterpriseSearchAnalyticsNav('my-test-collection', { + explorer: '/explorer-path', + integration: '/integration-path', + overview: '/overview-path', + }) + ); const applicationsNav = navItems?.find((item) => item.id === 'build'); expect(applicationsNav).not.toBeUndefined(); const analyticsNav = applicationsNav?.items?.[2]; expect(analyticsNav).not.toBeUndefined(); expect(analyticsNav).toEqual({ + 'data-test-subj': 'searchSideNav-BehavioralAnalytics', href: '/app/enterprise_search/analytics', id: 'analyticsCollections', items: [ diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/layout/nav.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/layout/nav.tsx index 3b3960a7a92ba..8f83b6c73402e 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/layout/nav.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/layout/nav.tsx @@ -5,44 +5,22 @@ * 2.0. */ -import React from 'react'; +import React, { useMemo } from 'react'; import { useValues } from 'kea'; -import { EuiFlexGroup, EuiIcon, EuiText } from '@elastic/eui'; -import type { EuiSideNavItemTypeEnhanced } from '@kbn/core-chrome-browser'; +import { EuiFlexGroup, EuiIcon } from '@elastic/eui'; +import type { ChromeNavLink, EuiSideNavItemTypeEnhanced } from '@kbn/core-chrome-browser'; import { i18n } from '@kbn/i18n'; -import { - ANALYTICS_PLUGIN, - APPLICATIONS_PLUGIN, - APP_SEARCH_PLUGIN, - ELASTICSEARCH_PLUGIN, - ENTERPRISE_SEARCH_CONTENT_PLUGIN, - ENTERPRISE_SEARCH_OVERVIEW_PLUGIN, - AI_SEARCH_PLUGIN, - VECTOR_SEARCH_PLUGIN, - WORKPLACE_SEARCH_PLUGIN, - SEARCH_RELEVANCE_PLUGIN, - SEMANTIC_SEARCH_PLUGIN, -} from '../../../../common/constants'; -import { - SEARCH_APPLICATIONS_PATH, - SearchApplicationViewTabs, - PLAYGROUND_PATH, -} from '../../applications/routes'; +import { ANALYTICS_PLUGIN, APPLICATIONS_PLUGIN } from '../../../../common/constants'; +import { SEARCH_APPLICATIONS_PATH, SearchApplicationViewTabs } from '../../applications/routes'; import { useIndicesNav } from '../../enterprise_search_content/components/search_index/indices/indices_nav'; -import { - CONNECTORS_PATH, - CRAWLERS_PATH, - SEARCH_INDICES_PATH, -} from '../../enterprise_search_content/routes'; -import { INFERENCE_ENDPOINTS_PATH } from '../../enterprise_search_relevance/routes'; import { KibanaLogic } from '../kibana'; -import { LicensingLogic } from '../licensing'; - +import { buildBaseClassicNavItems } from './base_nav'; +import { generateSideNavItems } from './classic_nav_helpers'; import { generateNavLink } from './nav_link_helpers'; /** @@ -52,219 +30,21 @@ import { generateNavLink } from './nav_link_helpers'; * @returns The Enterprise Search navigation items */ export const useEnterpriseSearchNav = (alwaysReturn = false) => { - const { isSidebarEnabled, productAccess } = useValues(KibanaLogic); - - const { hasEnterpriseLicense } = useValues(LicensingLogic); + const { isSidebarEnabled, productAccess, getNavLinks } = useValues(KibanaLogic); const indicesNavItems = useIndicesNav(); - if (!isSidebarEnabled && !alwaysReturn) return undefined; + const navItems: Array> = useMemo(() => { + const baseNavItems = buildBaseClassicNavItems({ productAccess }); + const deepLinks = getNavLinks().reduce((links, link) => { + links[link.id] = link; + return links; + }, {} as Record); - const navItems: Array> = [ - { - id: 'home', - name: ( - - {i18n.translate('xpack.enterpriseSearch.nav.homeTitle', { - defaultMessage: 'Home', - })} - - ), - ...generateNavLink({ - shouldNotCreateHref: true, - shouldShowActiveForSubroutes: true, - to: ENTERPRISE_SEARCH_OVERVIEW_PLUGIN.URL, - }), - }, - { - id: 'content', - items: [ - { - id: 'search_indices', - name: i18n.translate('xpack.enterpriseSearch.nav.searchIndicesTitle', { - defaultMessage: 'Indices', - }), - ...generateNavLink({ - items: indicesNavItems, - shouldNotCreateHref: true, - shouldShowActiveForSubroutes: true, - to: ENTERPRISE_SEARCH_CONTENT_PLUGIN.URL + SEARCH_INDICES_PATH, - }), - }, - { - id: 'connectors', - name: i18n.translate('xpack.enterpriseSearch.nav.connectorsTitle', { - defaultMessage: 'Connectors', - }), - ...generateNavLink({ - shouldNotCreateHref: true, - shouldShowActiveForSubroutes: true, - to: ENTERPRISE_SEARCH_CONTENT_PLUGIN.URL + CONNECTORS_PATH, - }), - }, - { - id: 'crawlers', - name: i18n.translate('xpack.enterpriseSearch.nav.crawlersTitle', { - defaultMessage: 'Web crawlers', - }), - ...generateNavLink({ - shouldNotCreateHref: true, - shouldShowActiveForSubroutes: true, - to: ENTERPRISE_SEARCH_CONTENT_PLUGIN.URL + CRAWLERS_PATH, - }), - }, - ], - name: i18n.translate('xpack.enterpriseSearch.nav.contentTitle', { - defaultMessage: 'Content', - }), - }, - { - id: 'build', - items: [ - { - id: 'playground', - name: i18n.translate('xpack.enterpriseSearch.nav.PlaygroundTitle', { - defaultMessage: 'Playground', - }), - ...generateNavLink({ - shouldNotCreateHref: true, - shouldShowActiveForSubroutes: true, - to: APPLICATIONS_PLUGIN.URL + PLAYGROUND_PATH, - }), - }, - { - id: 'searchApplications', - name: i18n.translate('xpack.enterpriseSearch.nav.searchApplicationsTitle', { - defaultMessage: 'Search Applications', - }), - ...generateNavLink({ - shouldNotCreateHref: true, - to: APPLICATIONS_PLUGIN.URL + SEARCH_APPLICATIONS_PATH, - }), - }, - { - id: 'analyticsCollections', - name: i18n.translate('xpack.enterpriseSearch.nav.analyticsTitle', { - defaultMessage: 'Behavioral Analytics', - }), - ...generateNavLink({ - shouldNotCreateHref: true, - to: ANALYTICS_PLUGIN.URL, - }), - }, - ], - name: i18n.translate('xpack.enterpriseSearch.nav.applicationsTitle', { - defaultMessage: 'Build', - }), - }, - ...(hasEnterpriseLicense - ? [ - { - id: 'relevance', - items: [ - { - id: 'inference_endpoints', - name: i18n.translate('xpack.enterpriseSearch.nav.inferenceEndpointsTitle', { - defaultMessage: 'Inference Endpoints', - }), - ...generateNavLink({ - shouldNotCreateHref: true, - shouldShowActiveForSubroutes: true, - to: SEARCH_RELEVANCE_PLUGIN.URL + INFERENCE_ENDPOINTS_PATH, - }), - }, - ], - name: i18n.translate('xpack.enterpriseSearch.nav.relevanceTitle', { - defaultMessage: 'Relevance', - }), - }, - ] - : []), - { - id: 'es_getting_started', - items: [ - { - id: 'elasticsearch', - name: i18n.translate('xpack.enterpriseSearch.nav.elasticsearchTitle', { - defaultMessage: 'Elasticsearch', - }), - ...generateNavLink({ - shouldNotCreateHref: true, - to: ELASTICSEARCH_PLUGIN.URL, - }), - }, - { - id: 'vectorSearch', - name: VECTOR_SEARCH_PLUGIN.NAME, - ...generateNavLink({ - shouldNotCreateHref: true, - to: VECTOR_SEARCH_PLUGIN.URL, - }), - }, - { - id: 'semanticSearch', - name: SEMANTIC_SEARCH_PLUGIN.NAME, - ...generateNavLink({ - shouldNotCreateHref: true, - to: SEMANTIC_SEARCH_PLUGIN.URL, - }), - }, - { - id: 'aiSearch', - name: i18n.translate('xpack.enterpriseSearch.nav.aiSearchTitle', { - defaultMessage: 'AI Search', - }), - ...generateNavLink({ - shouldNotCreateHref: true, - to: AI_SEARCH_PLUGIN.URL, - }), - }, - ], - name: i18n.translate('xpack.enterpriseSearch.nav.enterpriseSearchOverviewTitle', { - defaultMessage: 'Getting started', - }), - }, - ...(productAccess.hasAppSearchAccess || productAccess.hasWorkplaceSearchAccess - ? [ - { - id: 'enterpriseSearch', - items: [ - ...(productAccess.hasAppSearchAccess - ? [ - { - id: 'app_search', - name: i18n.translate('xpack.enterpriseSearch.nav.appSearchTitle', { - defaultMessage: 'App Search', - }), - ...generateNavLink({ - shouldNotCreateHref: true, - to: APP_SEARCH_PLUGIN.URL, - }), - }, - ] - : []), - ...(productAccess.hasWorkplaceSearchAccess - ? [ - { - id: 'workplace_search', - name: i18n.translate('xpack.enterpriseSearch.nav.workplaceSearchTitle', { - defaultMessage: 'Workplace Search', - }), - ...generateNavLink({ - shouldNotCreateHref: true, - to: WORKPLACE_SEARCH_PLUGIN.URL, - }), - }, - ] - : []), - ], - name: i18n.translate('xpack.enterpriseSearch.nav.title', { - defaultMessage: 'Enterprise Search', - }), - }, - ] - : []), - ]; + return generateSideNavItems(baseNavItems, deepLinks, { search_indices: indicesNavItems }); + }, [productAccess, indicesNavItems]); + + if (!isSidebarEnabled && !alwaysReturn) return undefined; return navItems; }; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/layout/nav_link_helpers.test.ts b/x-pack/plugins/enterprise_search/public/applications/shared/layout/nav_link_helpers.test.ts index fff28345bb1bb..50c85a268e366 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/layout/nav_link_helpers.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/layout/nav_link_helpers.test.ts @@ -36,6 +36,7 @@ describe('generateNavLink', () => { navItem.onClick({ preventDefault: jest.fn() } as any); expect(mockKibanaValues.navigateToUrl).toHaveBeenCalledWith('/test', { shouldNotCreateHref: false, + shouldNotPrepend: false, }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/layout/nav_link_helpers.ts b/x-pack/plugins/enterprise_search/public/applications/shared/layout/nav_link_helpers.ts index f086433c9fc0e..36000307adcc3 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/layout/nav_link_helpers.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/layout/nav_link_helpers.ts @@ -5,27 +5,32 @@ * 2.0. */ -import { EuiSideNavItemType } from '@elastic/eui'; +import { EuiSideNavItemTypeEnhanced } from '@kbn/core-chrome-browser'; import { stripTrailingSlash } from '../../../../common/strip_slashes'; import { KibanaLogic } from '../kibana'; -import { generateReactRouterProps, ReactRouterProps } from '../react_router_helpers'; -import { GeneratedReactRouterProps } from '../react_router_helpers/generate_react_router_props'; +import { + type GeneratedReactRouterProps, + generateReactRouterProps, +} from '../react_router_helpers/generate_react_router_props'; +import { ReactRouterProps } from '../types'; interface Params { - items?: Array>; // Primarily passed if using `items` to determine isSelected - if not, you can just set `items` outside of this helper + items?: Array>; // Primarily passed if using `items` to determine isSelected - if not, you can just set `items` outside of this helper shouldShowActiveForSubroutes?: boolean; to: string; } type NavLinkProps = GeneratedReactRouterProps & - Pick, 'isSelected' | 'items'>; + Pick, 'isSelected' | 'items'>; + +export type GenerateNavLinkParameters = Params & ReactRouterProps; export const generateNavLink = ({ items, ...rest -}: Params & ReactRouterProps): NavLinkProps => { +}: GenerateNavLinkParameters): NavLinkProps => { const linkProps = { ...generateReactRouterProps({ ...rest }), isSelected: getNavLinkActive({ items, ...rest }), @@ -38,14 +43,15 @@ export const getNavLinkActive = ({ shouldShowActiveForSubroutes = false, items = [], shouldNotCreateHref = false, -}: Params & ReactRouterProps): boolean => { + shouldNotPrepend = false, +}: GenerateNavLinkParameters): boolean => { const { pathname } = KibanaLogic.values.history.location; const currentPath = stripTrailingSlash(pathname); const { href: currentPathHref } = generateReactRouterProps({ shouldNotCreateHref: false, to: currentPath, }); - const { href: toHref } = generateReactRouterProps({ shouldNotCreateHref, to }); + const { href: toHref } = generateReactRouterProps({ shouldNotCreateHref, shouldNotPrepend, to }); if (currentPathHref === toHref) return true; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/create_href.ts b/x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/create_href.ts index a399d632140b6..cf02c3ed74f71 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/create_href.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/create_href.ts @@ -30,12 +30,19 @@ interface CreateHrefDeps { } export interface CreateHrefOptions { shouldNotCreateHref?: boolean; + shouldNotPrepend?: boolean; } export const createHref = ( path: string, { history, http }: CreateHrefDeps, - { shouldNotCreateHref }: CreateHrefOptions = {} + { shouldNotCreateHref, shouldNotPrepend }: CreateHrefOptions = {} ): string => { - return shouldNotCreateHref ? http.basePath.prepend(path) : history.createHref({ pathname: path }); + if (shouldNotCreateHref) { + if (shouldNotPrepend) { + return path; + } + return http.basePath.prepend(path); + } + return history.createHref({ pathname: path }); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/eui_components.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/eui_components.tsx index 8271f49f9f39a..708cc597e582d 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/eui_components.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/eui_components.tsx @@ -26,7 +26,9 @@ import { } from '@elastic/eui'; import { EuiPanelProps } from '@elastic/eui/src/components/panel/panel'; -import { generateReactRouterProps, ReactRouterProps } from '.'; +import { ReactRouterProps } from '../types'; + +import { generateReactRouterProps } from '.'; /** * Correctly typed component helpers with React-Router-friendly `href` and `onClick` props diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/generate_react_router_props.test.ts b/x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/generate_react_router_props.test.ts index 309f94fcf55b4..de2a80ee5eaf4 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/generate_react_router_props.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/generate_react_router_props.test.ts @@ -44,6 +44,7 @@ describe('generateReactRouterProps', () => { expect(mockEvent.preventDefault).toHaveBeenCalled(); expect(mockKibanaValues.navigateToUrl).toHaveBeenCalledWith('/test', { shouldNotCreateHref: false, + shouldNotPrepend: false, }); }); @@ -63,6 +64,7 @@ describe('generateReactRouterProps', () => { expect(mockEvent.preventDefault).toHaveBeenCalled(); expect(mockKibanaValues.navigateToUrl).toHaveBeenCalledWith('/app/enterprise_search/test', { shouldNotCreateHref: true, + shouldNotPrepend: false, }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/generate_react_router_props.ts b/x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/generate_react_router_props.ts index 2ef7f556eb2d1..89219362e5be4 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/generate_react_router_props.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/generate_react_router_props.ts @@ -11,6 +11,7 @@ import { EuiSideNavItemType } from '@elastic/eui'; import { HttpLogic } from '../http'; import { KibanaLogic } from '../kibana'; +import { ReactRouterProps } from '../types'; import { letBrowserHandleEvent, createHref } from '.'; @@ -23,14 +24,6 @@ import { letBrowserHandleEvent, createHref } from '.'; * but separated out from EuiLink portion as we use this for multiple EUI components */ -export interface ReactRouterProps { - to: string; - onClick?(): void; - // Used to navigate outside of the React Router plugin basename but still within Kibana, - // e.g. if we need to go from Enterprise Search to App Search - shouldNotCreateHref?: boolean; -} - export type GeneratedReactRouterProps = Required< Pick, 'href' | 'onClick'> >; @@ -39,12 +32,13 @@ export const generateReactRouterProps = ({ to, onClick, shouldNotCreateHref = false, + shouldNotPrepend = false, }: ReactRouterProps): GeneratedReactRouterProps => { const { navigateToUrl, history } = KibanaLogic.values; const { http } = HttpLogic.values; // Generate the correct link href (with basename etc. accounted for) - const href = createHref(to, { history, http }, { shouldNotCreateHref }); + const href = createHref(to, { history, http }, { shouldNotCreateHref, shouldNotPrepend }); const reactRouterLinkClick = (event: React.MouseEvent) => { if (onClick) onClick(); // Run any passed click events (e.g. telemetry) @@ -54,7 +48,7 @@ export const generateReactRouterProps = ({ event.preventDefault(); // Perform SPA navigation. - navigateToUrl(to, { shouldNotCreateHref }); + navigateToUrl(to, { shouldNotCreateHref, shouldNotPrepend }); }; return { href, onClick: reactRouterLinkClick }; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/index.ts b/x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/index.ts index ded9310fe361a..237e0d342ed1f 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/index.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/index.ts @@ -8,7 +8,6 @@ export { letBrowserHandleEvent } from './link_events'; export type { CreateHrefOptions } from './create_href'; export { createHref } from './create_href'; -export type { ReactRouterProps } from './generate_react_router_props'; export { generateReactRouterProps } from './generate_react_router_props'; export { EuiLinkTo, diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/types.ts b/x-pack/plugins/enterprise_search/public/applications/shared/types.ts index 51a83cb15cca5..095f1dddfcc4a 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/types.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/types.ts @@ -5,7 +5,12 @@ * 2.0. */ +import type { ReactNode } from 'react'; + +import type { AppDeepLinkId, EuiSideNavItemTypeEnhanced } from '@kbn/core-chrome-browser'; + import { APP_SEARCH_PLUGIN, WORKPLACE_SEARCH_PLUGIN } from '../../../common/constants'; +import type { ProductAccess } from '../../../common/types'; import { ADD, UPDATE } from './constants/operations'; @@ -57,3 +62,37 @@ export interface SingleUserRoleMapping { roleMapping: T; hasEnterpriseSearchRole?: boolean; } + +export interface ReactRouterProps { + to: string; + onClick?(): void; + // Used to navigate outside of the React Router plugin basename but still within Kibana, + // e.g. if we need to go from Enterprise Search to App Search + shouldNotCreateHref?: boolean; + // Used if to is already a fully qualified URL that doesn't need basePath prepended + shouldNotPrepend?: boolean; +} + +export type GenerateNavLinkParameters = { + items?: Array>; // Primarily passed if using `items` to determine isSelected - if not, you can just set `items` outside of this helper + shouldShowActiveForSubroutes?: boolean; + to: string; +} & ReactRouterProps; + +export interface GenerateNavLinkFromDeepLinkParameters { + link: AppDeepLinkId; + shouldShowActiveForSubroutes?: boolean; +} + +export interface BuildClassicNavParameters { + productAccess: ProductAccess; +} + +export interface ClassicNavItem { + 'data-test-subj'?: string; + deepLink?: GenerateNavLinkFromDeepLinkParameters; + iconToString?: string; + id: string; + items?: ClassicNavItem[]; + name?: ReactNode; +} diff --git a/x-pack/plugins/enterprise_search/public/applications/test_helpers/test_utils.test_helper.tsx b/x-pack/plugins/enterprise_search/public/applications/test_helpers/test_utils.test_helper.tsx index d1729a50909ed..da30e6e93fadb 100644 --- a/x-pack/plugins/enterprise_search/public/applications/test_helpers/test_utils.test_helper.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/test_helpers/test_utils.test_helper.tsx @@ -58,6 +58,7 @@ export const mockKibanaProps: KibanaLogicProps = { elasticsearch_host: 'https://your_deployment_url', }, getChromeStyle$: jest.fn().mockReturnValue(of('classic')), + getNavLinks: jest.fn().mockReturnValue([]), guidedOnboarding: {}, history: mockHistory, indexMappingComponent: () => { diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index 54f56d8eb8658..2ae87164a3dec 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -17359,27 +17359,16 @@ "xpack.enterpriseSearch.modelCard.elserPlaceholder.description": "ELSER est le modèle NLP d'Elastic pour la recherche sémantique en anglais, utilisant des vecteurs creux. Il donne la priorité à l'intention et à la signification contextuelle plutôt qu'à la correspondance littérale des termes. Il est optimisé spécifiquement pour les documents et les recherches en anglais sur la plateforme Elastic.", "xpack.enterpriseSearch.nameLabel": "Nom", "xpack.enterpriseSearch.nativeLabel": "Natif", - "xpack.enterpriseSearch.nav.aiSearchTitle": "Recherche propulsée par l'intelligence artificielle", "xpack.enterpriseSearch.nav.analyticsCollections.explorerTitle": "Explorer", "xpack.enterpriseSearch.nav.analyticsCollections.integrationTitle": "Intégration", "xpack.enterpriseSearch.nav.analyticsCollections.overviewTitle": "Aperçu", - "xpack.enterpriseSearch.nav.analyticsTitle": "Behavioral Analytics", "xpack.enterpriseSearch.nav.applications.searchApplications.connectTitle": "Connecter", "xpack.enterpriseSearch.nav.applicationsTitle": "Développer", - "xpack.enterpriseSearch.nav.appSearchTitle": "App Search", - "xpack.enterpriseSearch.nav.connectorsTitle": "Connecteurs", "xpack.enterpriseSearch.nav.contentTitle": "Contenu", - "xpack.enterpriseSearch.nav.crawlersTitle": "Robots d'indexation", - "xpack.enterpriseSearch.nav.elasticsearchTitle": "Elasticsearch", - "xpack.enterpriseSearch.nav.enterpriseSearchOverviewTitle": "Premiers pas", "xpack.enterpriseSearch.nav.homeTitle": "Accueil", - "xpack.enterpriseSearch.nav.inferenceEndpointsTitle": "Points de terminaison d'inférence", - "xpack.enterpriseSearch.nav.PlaygroundTitle": "Playground", "xpack.enterpriseSearch.nav.relevanceTitle": "Pertinence", "xpack.enterpriseSearch.nav.searchApplication.contentTitle": "Contenu", "xpack.enterpriseSearch.nav.searchApplication.docsExplorerTitle": "Explorateur de documents", - "xpack.enterpriseSearch.nav.searchApplicationsTitle": "Applications de recherche", - "xpack.enterpriseSearch.nav.searchIndicesTitle": "Index", "xpack.enterpriseSearch.nav.searchIndicesTitle.nav.connectorsConfigurationLabel": "Configuration", "xpack.enterpriseSearch.nav.searchIndicesTitle.nav.crawlerConfigurationLabel": "Configuration", "xpack.enterpriseSearch.nav.searchIndicesTitle.nav.crawlerSchedulingLabel": "Planification", @@ -17391,7 +17380,6 @@ "xpack.enterpriseSearch.nav.searchIndicesTitle.nav.schedulingTitle": "Planification", "xpack.enterpriseSearch.nav.searchIndicesTitle.nav.syncRulesLabel": "Règles de synchronisation", "xpack.enterpriseSearch.nav.title": "Enterprise Search", - "xpack.enterpriseSearch.nav.workplaceSearchTitle": "Workplace Search", "xpack.enterpriseSearch.navigation.applicationsSearchApplicationsLinkLabel": "Applications de recherche", "xpack.enterpriseSearch.navigation.appSearchEnginesLinkLabel": "Moteurs", "xpack.enterpriseSearch.navigation.contentConnectorsLinkLabel": "Connecteurs", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index f5d8c0179094b..b1a8859d20830 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -17105,27 +17105,16 @@ "xpack.enterpriseSearch.modelCard.elserPlaceholder.description": "ELSERは、疎ベクトルを利用した英語のセマンティック検索のためのElasticのNLPモデルです。Elasticプラットフォームの英語ドキュメントやクエリー向けに特別に最適化されており、文字通りの用語一致よりも意図や文脈上の意味を優先します。", "xpack.enterpriseSearch.nameLabel": "名前", "xpack.enterpriseSearch.nativeLabel": "ネイティブ", - "xpack.enterpriseSearch.nav.aiSearchTitle": "AI検索", "xpack.enterpriseSearch.nav.analyticsCollections.explorerTitle": "エクスプローラー", "xpack.enterpriseSearch.nav.analyticsCollections.integrationTitle": "統合", "xpack.enterpriseSearch.nav.analyticsCollections.overviewTitle": "概要", - "xpack.enterpriseSearch.nav.analyticsTitle": "Behavioral Analytics", "xpack.enterpriseSearch.nav.applications.searchApplications.connectTitle": "接続", "xpack.enterpriseSearch.nav.applicationsTitle": "ビルド", - "xpack.enterpriseSearch.nav.appSearchTitle": "App Search", - "xpack.enterpriseSearch.nav.connectorsTitle": "コネクター", "xpack.enterpriseSearch.nav.contentTitle": "コンテンツ", - "xpack.enterpriseSearch.nav.crawlersTitle": "Webクローラー", - "xpack.enterpriseSearch.nav.elasticsearchTitle": "Elasticsearch", - "xpack.enterpriseSearch.nav.enterpriseSearchOverviewTitle": "はじめて使う", "xpack.enterpriseSearch.nav.homeTitle": "ホーム", - "xpack.enterpriseSearch.nav.inferenceEndpointsTitle": "推論エンドポイント", - "xpack.enterpriseSearch.nav.PlaygroundTitle": "Playground", "xpack.enterpriseSearch.nav.relevanceTitle": "関連性", "xpack.enterpriseSearch.nav.searchApplication.contentTitle": "コンテンツ", "xpack.enterpriseSearch.nav.searchApplication.docsExplorerTitle": "ドキュメントエクスプローラー", - "xpack.enterpriseSearch.nav.searchApplicationsTitle": "検索アプリケーション", - "xpack.enterpriseSearch.nav.searchIndicesTitle": "インデックス", "xpack.enterpriseSearch.nav.searchIndicesTitle.nav.connectorsConfigurationLabel": "構成", "xpack.enterpriseSearch.nav.searchIndicesTitle.nav.crawlerConfigurationLabel": "構成", "xpack.enterpriseSearch.nav.searchIndicesTitle.nav.crawlerSchedulingLabel": "スケジュール", @@ -17137,7 +17126,6 @@ "xpack.enterpriseSearch.nav.searchIndicesTitle.nav.schedulingTitle": "スケジュール", "xpack.enterpriseSearch.nav.searchIndicesTitle.nav.syncRulesLabel": "同期ルール", "xpack.enterpriseSearch.nav.title": "エンタープライズ サーチ", - "xpack.enterpriseSearch.nav.workplaceSearchTitle": "Workplace Search", "xpack.enterpriseSearch.navigation.applicationsSearchApplicationsLinkLabel": "検索アプリケーション", "xpack.enterpriseSearch.navigation.appSearchEnginesLinkLabel": "エンジン", "xpack.enterpriseSearch.navigation.contentConnectorsLinkLabel": "コネクター", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index fe092b94f4df8..b040c7ab587df 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -17134,27 +17134,16 @@ "xpack.enterpriseSearch.modelCard.elserPlaceholder.description": "ELSER 是 Elastic 的利用稀疏向量执行英语语义搜索的 NLP 模型。与字面值匹配相比,它优先处理意图和上下文含义,对 Elastic 平台上的英语文档和查询专门进行了优化。", "xpack.enterpriseSearch.nameLabel": "名称", "xpack.enterpriseSearch.nativeLabel": "原生", - "xpack.enterpriseSearch.nav.aiSearchTitle": "AI 搜索", "xpack.enterpriseSearch.nav.analyticsCollections.explorerTitle": "浏览器", "xpack.enterpriseSearch.nav.analyticsCollections.integrationTitle": "集成", "xpack.enterpriseSearch.nav.analyticsCollections.overviewTitle": "概览", - "xpack.enterpriseSearch.nav.analyticsTitle": "行为分析", "xpack.enterpriseSearch.nav.applications.searchApplications.connectTitle": "连接", "xpack.enterpriseSearch.nav.applicationsTitle": "构建", - "xpack.enterpriseSearch.nav.appSearchTitle": "App Search", - "xpack.enterpriseSearch.nav.connectorsTitle": "连接器", "xpack.enterpriseSearch.nav.contentTitle": "内容", - "xpack.enterpriseSearch.nav.crawlersTitle": "网络爬虫", - "xpack.enterpriseSearch.nav.elasticsearchTitle": "Elasticsearch", - "xpack.enterpriseSearch.nav.enterpriseSearchOverviewTitle": "入门", "xpack.enterpriseSearch.nav.homeTitle": "主页", - "xpack.enterpriseSearch.nav.inferenceEndpointsTitle": "推理终端", - "xpack.enterpriseSearch.nav.PlaygroundTitle": "Playground", "xpack.enterpriseSearch.nav.relevanceTitle": "相关性", "xpack.enterpriseSearch.nav.searchApplication.contentTitle": "内容", "xpack.enterpriseSearch.nav.searchApplication.docsExplorerTitle": "文档浏览器", - "xpack.enterpriseSearch.nav.searchApplicationsTitle": "搜索应用程序", - "xpack.enterpriseSearch.nav.searchIndicesTitle": "索引", "xpack.enterpriseSearch.nav.searchIndicesTitle.nav.connectorsConfigurationLabel": "配置", "xpack.enterpriseSearch.nav.searchIndicesTitle.nav.crawlerConfigurationLabel": "配置", "xpack.enterpriseSearch.nav.searchIndicesTitle.nav.crawlerSchedulingLabel": "正在计划", @@ -17166,7 +17155,6 @@ "xpack.enterpriseSearch.nav.searchIndicesTitle.nav.schedulingTitle": "正在计划", "xpack.enterpriseSearch.nav.searchIndicesTitle.nav.syncRulesLabel": "同步规则", "xpack.enterpriseSearch.nav.title": "Enterprise Search", - "xpack.enterpriseSearch.nav.workplaceSearchTitle": "Workplace Search", "xpack.enterpriseSearch.navigation.applicationsSearchApplicationsLinkLabel": "搜索应用程序", "xpack.enterpriseSearch.navigation.appSearchEnginesLinkLabel": "引擎", "xpack.enterpriseSearch.navigation.contentConnectorsLinkLabel": "连接器", diff --git a/x-pack/test/functional/config.base.js b/x-pack/test/functional/config.base.js index b35d1f6b6673c..d5c6d77785b85 100644 --- a/x-pack/test/functional/config.base.js +++ b/x-pack/test/functional/config.base.js @@ -196,6 +196,9 @@ export default async function ({ readConfigFile }) { obsAIAssistantManagement: { pathname: '/app/management/kibana/observabilityAiAssistantManagement', }, + enterpriseSearch: { + pathname: '/app/enterprise_search/overview', + }, }, suiteTags: { diff --git a/x-pack/test/functional/page_objects/index.ts b/x-pack/test/functional/page_objects/index.ts index e0d12bdd7459e..0d270661a05df 100644 --- a/x-pack/test/functional/page_objects/index.ts +++ b/x-pack/test/functional/page_objects/index.ts @@ -54,6 +54,7 @@ import { UserProfilePageProvider } from './user_profile_page'; import { WatcherPageObject } from './watcher_page'; import { SearchProfilerPageProvider } from './search_profiler_page'; import { SearchPlaygroundPageProvider } from './search_playground_page'; +import { SearchClassicNavigationProvider } from './search_classic_navigation'; // just like services, PageObjects are defined as a map of // names to Providers. Merge in Kibana's or pick specific ones @@ -93,6 +94,7 @@ export const pageObjects = { reporting: ReportingPageObject, roleMappings: RoleMappingsPageProvider, rollup: RollupPageObject, + searchClassicNavigation: SearchClassicNavigationProvider, searchProfiler: SearchProfilerPageProvider, searchPlayground: SearchPlaygroundPageProvider, searchSessionsManagement: SearchSessionsPageProvider, diff --git a/x-pack/test/functional/page_objects/search_classic_navigation.ts b/x-pack/test/functional/page_objects/search_classic_navigation.ts new file mode 100644 index 0000000000000..90ec1c6c46007 --- /dev/null +++ b/x-pack/test/functional/page_objects/search_classic_navigation.ts @@ -0,0 +1,118 @@ +/* + * 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 expect from '@kbn/expect'; +import type { WebElementWrapper } from '@kbn/ftr-common-functional-ui-services'; + +import { FtrProviderContext } from '../ftr_provider_context'; + +const TIMEOUT_CHECK = 3000; + +export function SearchClassicNavigationProvider({ getService }: FtrProviderContext) { + const testSubjects = getService('testSubjects'); + const browser = getService('browser'); + const retry = getService('retry'); + + async function getByVisibleText( + selector: string | (() => Promise), + text: string + ) { + const subjects = + typeof selector === 'string' ? await testSubjects.findAll(selector) : await selector(); + let found: WebElementWrapper | null = null; + for (const subject of subjects) { + const visibleText = await subject.getVisibleText(); + if (visibleText === text) { + found = subject; + break; + } + } + return found; + } + const sideNavTestSubj = (id: string) => `searchSideNav-${id}`; + + return { + async expectAllNavItems(items: Array<{ id: string; label: string }>) { + for (const navItem of items) { + await testSubjects.existOrFail(sideNavTestSubj(navItem.id)); + const itemElement = await testSubjects.find(sideNavTestSubj(navItem.id)); + const itemLabel = await itemElement.getVisibleText(); + expect(itemLabel).to.equal(navItem.label); + } + const allSideNavItems = await testSubjects.findAll('*searchSideNav-'); + expect(allSideNavItems.length).to.equal(items.length); + }, + + async expectNavItemExists(id: string) { + await testSubjects.existOrFail(sideNavTestSubj(id)); + }, + + async expectNavItemMissing(id: string) { + await testSubjects.missingOrFail(sideNavTestSubj(id)); + }, + + async clickNavItem(id: string) { + await testSubjects.existOrFail(sideNavTestSubj(id)); + await testSubjects.click(sideNavTestSubj(id)); + }, + + async expectNavItemActive(id: string) { + await testSubjects.existOrFail(sideNavTestSubj(id)); + const item = await testSubjects.find(sideNavTestSubj(id)); + expect(await item.elementHasClass('euiSideNavItemButton-isSelected')).to.be(true); + }, + + breadcrumbs: { + async expectExists() { + await testSubjects.existOrFail('breadcrumbs', { timeout: TIMEOUT_CHECK }); + }, + async clickBreadcrumb(text: string) { + await (await getByVisibleText('~breadcrumb', text))?.click(); + }, + async getBreadcrumb(text: string) { + return getByVisibleText('~breadcrumb', text); + }, + async expectBreadcrumbExists(text: string) { + await retry.try(async () => { + expect(await getByVisibleText('~breadcrumb', text)).not.be(null); + }); + }, + async expectBreadcrumbMissing(text: string) { + await retry.try(async () => { + expect(await getByVisibleText('~breadcrumb', text)).be(null); + }); + }, + }, + + // helper to assert that the page did not reload + async createNoPageReloadCheck() { + const trackReloadTs = Date.now(); + await browser.execute( + ({ ts }) => { + // @ts-ignore + window.__testTrackReload__ = ts; + }, + { + ts: trackReloadTs, + } + ); + + return async () => { + const noReload = await browser.execute( + ({ ts }) => { + // @ts-ignore + return window.__testTrackReload__ && window.__testTrackReload__ === ts; + }, + { + ts: trackReloadTs, + } + ); + expect(noReload).to.be(true); + }; + }, + }; +} diff --git a/x-pack/test/functional_search/config.ts b/x-pack/test/functional_search/config.ts index f997aaea7c5e2..c7708363766b0 100644 --- a/x-pack/test/functional_search/config.ts +++ b/x-pack/test/functional_search/config.ts @@ -17,7 +17,17 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { return { ...functionalConfig.getAll(), + junit: { + reportName: 'Search Solution UI Functional Tests', + }, testFiles: [require.resolve('.')], + esTestCluster: { + ...functionalConfig.get('esTestCluster'), + serverArgs: [ + ...functionalConfig.get('esTestCluster.serverArgs'), + 'xpack.security.enabled=true', + ], + }, kbnTestServer: { ...functionalConfig.get('kbnTestServer'), serverArgs: [ diff --git a/x-pack/test/functional_search/index.ts b/x-pack/test/functional_search/index.ts index 149b3dbcf7eca..d48bd1d695d16 100644 --- a/x-pack/test/functional_search/index.ts +++ b/x-pack/test/functional_search/index.ts @@ -10,6 +10,7 @@ import { FtrProviderContext } from './ftr_provider_context'; export default ({ loadTestFile }: FtrProviderContext): void => { describe('Search solution tests', function () { + loadTestFile(require.resolve('./tests/classic_navigation')); loadTestFile(require.resolve('./tests/solution_navigation')); }); }; diff --git a/x-pack/test/functional_search/tests/classic_navigation.ts b/x-pack/test/functional_search/tests/classic_navigation.ts new file mode 100644 index 0000000000000..7ec78394dae74 --- /dev/null +++ b/x-pack/test/functional_search/tests/classic_navigation.ts @@ -0,0 +1,131 @@ +/* + * 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 { FtrProviderContext } from '../ftr_provider_context'; + +export default function searchSolutionNavigation({ + getPageObjects, + getService, +}: FtrProviderContext) { + const { common, searchClassicNavigation } = getPageObjects(['common', 'searchClassicNavigation']); + const spaces = getService('spaces'); + const browser = getService('browser'); + + describe('Search Classic Navigation', () => { + let cleanUp: () => Promise; + let spaceCreated: { id: string } = { id: '' }; + + before(async () => { + // Navigate to the spaces management page which will log us in Kibana + await common.navigateToUrl('management', 'kibana/spaces', { + shouldUseHashForSubUrl: false, + }); + + // Create a space with the search solution and navigate to its home page + ({ cleanUp, space: spaceCreated } = await spaces.create({ solution: 'classic' })); + await browser.navigateTo(spaces.getRootUrl(spaceCreated.id)); + await common.navigateToApp('enterpriseSearch'); + }); + + after(async () => { + // Clean up space created + await cleanUp(); + }); + + it('renders expected navigation items', async () => { + await searchClassicNavigation.expectAllNavItems([ + { id: 'Home', label: 'Home' }, + { id: 'Content', label: 'Content' }, + { id: 'Indices', label: 'Indices' }, + { id: 'Connectors', label: 'Connectors' }, + { id: 'Crawlers', label: 'Web crawlers' }, + { id: 'Build', label: 'Build' }, + { id: 'Playground', label: 'Playground' }, + { id: 'SearchApplications', label: 'Search Applications' }, + { id: 'BehavioralAnalytics', label: 'Behavioral Analytics' }, + { id: 'Relevance', label: 'Relevance' }, + { id: 'InferenceEndpoints', label: 'Inference Endpoints' }, + { id: 'GettingStarted', label: 'Getting started' }, + { id: 'Elasticsearch', label: 'Elasticsearch' }, + { id: 'VectorSearch', label: 'Vector Search' }, + { id: 'SemanticSearch', label: 'Semantic Search' }, + { id: 'AISearch', label: 'AI Search' }, + ]); + }); + it('has expected navigation', async () => { + const expectNoPageReload = await searchClassicNavigation.createNoPageReloadCheck(); + + await searchClassicNavigation.expectNavItemExists('Home'); + + // Check Content + // > Indices + await searchClassicNavigation.clickNavItem('Indices'); + await searchClassicNavigation.expectNavItemActive('Indices'); + await searchClassicNavigation.breadcrumbs.expectBreadcrumbExists('Content'); + await searchClassicNavigation.breadcrumbs.expectBreadcrumbExists('Elasticsearch indices'); + // > Connectors + await searchClassicNavigation.clickNavItem('Connectors'); + await searchClassicNavigation.expectNavItemActive('Connectors'); + await searchClassicNavigation.breadcrumbs.expectBreadcrumbExists('Content'); + await searchClassicNavigation.breadcrumbs.expectBreadcrumbExists('Connectors'); + // > Crawlers + await searchClassicNavigation.clickNavItem('Crawlers'); + await searchClassicNavigation.expectNavItemActive('Crawlers'); + await searchClassicNavigation.breadcrumbs.expectBreadcrumbExists('Content'); + await searchClassicNavigation.breadcrumbs.expectBreadcrumbExists('Web crawlers'); + + // Check Build + // > Playground + await searchClassicNavigation.clickNavItem('Playground'); + await searchClassicNavigation.expectNavItemActive('Playground'); + await searchClassicNavigation.breadcrumbs.expectBreadcrumbExists('Build'); + await searchClassicNavigation.breadcrumbs.expectBreadcrumbExists('Playground'); + // > SearchApplications + await searchClassicNavigation.clickNavItem('SearchApplications'); + await searchClassicNavigation.expectNavItemActive('SearchApplications'); + await searchClassicNavigation.breadcrumbs.expectBreadcrumbExists('Build'); + await searchClassicNavigation.breadcrumbs.expectBreadcrumbExists('Search Applications'); + // > BehavioralAnalytics + await searchClassicNavigation.clickNavItem('BehavioralAnalytics'); + await searchClassicNavigation.expectNavItemActive('BehavioralAnalytics'); + await searchClassicNavigation.breadcrumbs.expectBreadcrumbExists('Build'); + await searchClassicNavigation.breadcrumbs.expectBreadcrumbExists('Behavioral Analytics'); + + // Check Relevance + // > InferenceEndpoints + await searchClassicNavigation.clickNavItem('InferenceEndpoints'); + await searchClassicNavigation.expectNavItemActive('InferenceEndpoints'); + await searchClassicNavigation.breadcrumbs.expectBreadcrumbExists('Relevance'); + await searchClassicNavigation.breadcrumbs.expectBreadcrumbExists('Inference Endpoints'); + + // Check Getting started + // > Elasticsearch + await searchClassicNavigation.clickNavItem('Elasticsearch'); + await searchClassicNavigation.expectNavItemActive('Elasticsearch'); + await searchClassicNavigation.breadcrumbs.expectBreadcrumbExists( + 'Getting started with Elasticsearch' + ); + // > VectorSearch + await searchClassicNavigation.clickNavItem('VectorSearch'); + await searchClassicNavigation.expectNavItemActive('VectorSearch'); + await searchClassicNavigation.breadcrumbs.expectBreadcrumbExists('Getting started'); + await searchClassicNavigation.breadcrumbs.expectBreadcrumbExists('Vector Search'); + // > SemanticSearch + await searchClassicNavigation.clickNavItem('SemanticSearch'); + await searchClassicNavigation.expectNavItemActive('SemanticSearch'); + await searchClassicNavigation.breadcrumbs.expectBreadcrumbExists('Getting started'); + await searchClassicNavigation.breadcrumbs.expectBreadcrumbExists('Semantic Search'); + // > AISearch + await searchClassicNavigation.clickNavItem('AISearch'); + await searchClassicNavigation.expectNavItemActive('AISearch'); + await searchClassicNavigation.breadcrumbs.expectBreadcrumbExists('Getting started'); + await searchClassicNavigation.breadcrumbs.expectBreadcrumbExists('AI Search'); + + await expectNoPageReload(); + }); + }); +}