[Search] Refactor: abstracting classic nav items (#196579)

## Summary

Moved the base set of sidenav items from being statically defined in
useEnterpriseSearchNav to using a function that can be shared with the
plugin. Additionally wrapped this generation in a `useMemo` to improve
performance.

This will support the ability to share the classic navigation items for
Search to other plugins so that they can render their own UIs without
sharing components with enterprise_search just to have access to the
side nav defined by enterprise_search.

### Checklist

- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios
- [x] [Flaky Test
Runner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1) was
used on any tests changed

---------

Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
Rodney Norris 2024-10-30 09:39:42 -05:00 committed by GitHub
parent b9a5d6a46d
commit c4301d080b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
37 changed files with 1180 additions and 358 deletions

View file

@ -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';

View file

@ -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;

View file

@ -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 {

View file

@ -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',
}
);

View file

@ -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,

View file

@ -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<SearchPlaygroundPageTemplateProps> = ({
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 (
<EnterpriseSearchPageTemplateWrapper
{...pageTemplateProps}
solutionNav={{
items: chromeStyle === 'classic' ? navItems : undefined,
name: SEARCH_PRODUCT_NAME,
}}
restrictWidth={restrictWidth}
setPageChrome={pageChrome && <SetSearchPlaygroundChrome trail={pageChrome} />}
useEndpointHeaderActions={false}
>
{pageViewTelemetry && (
<SendEnterpriseSearchTelemetry action="viewed" metric={pageViewTelemetry} />
)}
{children}
</EnterpriseSearchPageTemplateWrapper>
);
};

View file

@ -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 (
<searchPlayground.PlaygroundProvider>
<EnterpriseSearchApplicationsPageTemplate
<SearchPlaygroundPageTemplate
pageChrome={[
i18n.translate('xpack.enterpriseSearch.content.playground.breadcrumb', {
defaultMessage: 'Playground',
@ -33,10 +34,9 @@ export const Playground: React.FC = () => {
panelled={false}
customPageSections
bottomBorder="extended"
docLink="playground"
>
<searchPlayground.Playground />
</EnterpriseSearchApplicationsPageTemplate>
</SearchPlaygroundPageTemplate>
</searchPlayground.PlaygroundProvider>
);
};

View file

@ -40,7 +40,7 @@ export const ElasticsearchGuide = () => {
}, []);
return (
<EnterpriseSearchElasticsearchPageTemplate>
<EnterpriseSearchElasticsearchPageTemplate pageChrome={[]}>
{isFlyoutOpen && <CreateApiKeyFlyout onClose={() => setIsFlyoutOpen(false)} />}
<EuiTitle size="l" data-test-subj="elasticsearchGuide">
<h1>

View file

@ -19,13 +19,14 @@ export const EnterpriseSearchElasticsearchPageTemplate: React.FC<PageTemplatePro
pageViewTelemetry,
...pageTemplateProps
}) => {
const navItems = useEnterpriseSearchNav();
return (
<EnterpriseSearchPageTemplateWrapper
{...pageTemplateProps}
restrictWidth
solutionNav={{
name: SEARCH_PRODUCT_NAME,
items: useEnterpriseSearchNav(),
items: navItems,
}}
setPageChrome={pageChrome && <SetElasticsearchChrome trail={pageChrome} />}
>

View file

@ -114,6 +114,7 @@ export const renderApp = (
data: plugins.data,
esConfig,
getChromeStyle$: chrome.getChromeStyle$,
getNavLinks: chrome.navLinks.getAll,
guidedOnboarding,
history,
indexMappingComponent,

View file

@ -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<IndexMappingProps>;
@ -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<IndexMappingProps> | null;
@ -126,6 +128,7 @@ export const KibanaLogic = kea<MakeLogicType<KibanaValues>>({
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, {}],

View file

@ -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,
]);

View file

@ -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',
}),
]);

View file

@ -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<SetChromeProps> = ({ trail = [
return null;
};
export const SetSearchPlaygroundChrome: React.FC<SetChromeProps> = ({ 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<SetChromeProps> = ({ 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(() => {

View file

@ -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: (
<EuiText size="s">
{i18n.translate('xpack.enterpriseSearch.nav.homeTitle', {
defaultMessage: 'Home',
})}
</EuiText>
),
});
// 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;
};

View file

@ -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<string, ChromeNavLink | undefined>;
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',
},
]);
});
});

View file

@ -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<string, ChromeNavLink | undefined>,
subItemsMap: Record<string, Array<EuiSideNavItemTypeEnhanced<unknown>> | undefined> = {}
): Array<EuiSideNavItemTypeEnhanced<unknown>> => {
const sideNavItems: Array<EuiSideNavItemTypeEnhanced<unknown>> = [];
for (const navItem of navItems) {
let sideNavChildItems: Array<EuiSideNavItemTypeEnhanced<unknown>> | 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<unknown> | 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<string, ChromeNavLink | undefined>
): 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, ChromeNavLink | undefined>
): string | undefined => {
const deepLink = deepLinks[link];
if (!deepLink || !deepLink.url) return undefined;
return deepLink.title;
};
function isValidSideNavItem(
item: EuiSideNavItemTypeEnhanced<unknown> | undefined
): item is EuiSideNavItemTypeEnhanced<unknown> {
if (item === undefined) return false;
if (item.href || item.onClick) return true;
if (item?.items?.length ?? 0 > 0) return true;
return false;
}

View file

@ -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: [

View file

@ -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<EuiSideNavItemTypeEnhanced<unknown>> = useMemo(() => {
const baseNavItems = buildBaseClassicNavItems({ productAccess });
const deepLinks = getNavLinks().reduce((links, link) => {
links[link.id] = link;
return links;
}, {} as Record<string, ChromeNavLink | undefined>);
const navItems: Array<EuiSideNavItemTypeEnhanced<unknown>> = [
{
id: 'home',
name: (
<EuiText size="s">
{i18n.translate('xpack.enterpriseSearch.nav.homeTitle', {
defaultMessage: 'Home',
})}
</EuiText>
),
...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;
};

View file

@ -36,6 +36,7 @@ describe('generateNavLink', () => {
navItem.onClick({ preventDefault: jest.fn() } as any);
expect(mockKibanaValues.navigateToUrl).toHaveBeenCalledWith('/test', {
shouldNotCreateHref: false,
shouldNotPrepend: false,
});
});

View file

@ -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<EuiSideNavItemType<unknown>>; // Primarily passed if using `items` to determine isSelected - if not, you can just set `items` outside of this helper
items?: Array<EuiSideNavItemTypeEnhanced<unknown>>; // 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<T> = GeneratedReactRouterProps<T> &
Pick<EuiSideNavItemType<T>, 'isSelected' | 'items'>;
Pick<EuiSideNavItemTypeEnhanced<T>, 'isSelected' | 'items'>;
export type GenerateNavLinkParameters = Params & ReactRouterProps;
export const generateNavLink = ({
items,
...rest
}: Params & ReactRouterProps): NavLinkProps<unknown> => {
}: GenerateNavLinkParameters): NavLinkProps<unknown> => {
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;

View file

@ -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 });
};

View file

@ -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

View file

@ -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,
});
});

View file

@ -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<T> = Required<
Pick<EuiSideNavItemType<T>, 'href' | 'onClick'>
>;
@ -39,12 +32,13 @@ export const generateReactRouterProps = ({
to,
onClick,
shouldNotCreateHref = false,
shouldNotPrepend = false,
}: ReactRouterProps): GeneratedReactRouterProps<unknown> => {
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 };

View file

@ -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,

View file

@ -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<T> {
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<EuiSideNavItemTypeEnhanced<unknown>>; // 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;
}

View file

@ -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: () => {

View file

@ -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",

View file

@ -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": "<b>関連性</b>",
"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": "コネクター",

View file

@ -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": "连接器",

View file

@ -196,6 +196,9 @@ export default async function ({ readConfigFile }) {
obsAIAssistantManagement: {
pathname: '/app/management/kibana/observabilityAiAssistantManagement',
},
enterpriseSearch: {
pathname: '/app/enterprise_search/overview',
},
},
suiteTags: {

View file

@ -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,

View file

@ -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<WebElementWrapper[]>),
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);
};
},
};
}

View file

@ -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: [

View file

@ -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'));
});
};

View file

@ -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<unknown>;
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();
});
});
}