[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)

<!--- Backport version: 9.4.3 -->

### Questions ?
Please refer to the [Backport tool
documentation](https://github.com/sqren/backport)

<!--BACKPORT [{"author":{"name":"Rodney
Norris","email":"rodney.norris@elastic.co"},"sourceCommit":{"committedDate":"2024-10-30T14:39:42Z","message":"[Search]
Refactor: abstracting classic nav items (#196579)\n\n##
Summary\r\n\r\nMoved the base set of sidenav items from being statically
defined in\r\nuseEnterpriseSearchNav to using a function that can be
shared with the\r\nplugin. Additionally wrapped this generation in a
`useMemo` to improve\r\nperformance.\r\n\r\nThis will support the
ability to share the classic navigation items for\r\nSearch to other
plugins so that they can render their own UIs without\r\nsharing
components with enterprise_search just to have access to the\r\nside nav
defined by enterprise_search.\r\n\r\n### Checklist\r\n\r\n- [x] [Unit or
functional\r\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\r\nwere
updated or added to match the most common scenarios\r\n- [x] [Flaky
Test\r\nRunner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1)
was\r\nused on any tests changed\r\n\r\n---------\r\n\r\nCo-authored-by:
Elastic Machine
<elasticmachine@users.noreply.github.com>","sha":"c4301d080b9fd595b6cf2313d2053256b0fae89d","branchLabelMapping":{"^v9.0.0$":"main","^v8.17.0$":"8.x","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["release_note:skip","v9.0.0","Team:Search","backport:prev-minor","v8.17.0"],"title":"[Search]
Refactor: abstracting classic nav
items","number":196579,"url":"https://github.com/elastic/kibana/pull/196579","mergeCommit":{"message":"[Search]
Refactor: abstracting classic nav items (#196579)\n\n##
Summary\r\n\r\nMoved the base set of sidenav items from being statically
defined in\r\nuseEnterpriseSearchNav to using a function that can be
shared with the\r\nplugin. Additionally wrapped this generation in a
`useMemo` to improve\r\nperformance.\r\n\r\nThis will support the
ability to share the classic navigation items for\r\nSearch to other
plugins so that they can render their own UIs without\r\nsharing
components with enterprise_search just to have access to the\r\nside nav
defined by enterprise_search.\r\n\r\n### Checklist\r\n\r\n- [x] [Unit or
functional\r\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\r\nwere
updated or added to match the most common scenarios\r\n- [x] [Flaky
Test\r\nRunner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1)
was\r\nused on any tests changed\r\n\r\n---------\r\n\r\nCo-authored-by:
Elastic Machine
<elasticmachine@users.noreply.github.com>","sha":"c4301d080b9fd595b6cf2313d2053256b0fae89d"}},"sourceBranch":"main","suggestedTargetBranches":["8.x"],"targetPullRequestStates":[{"branch":"main","label":"v9.0.0","branchLabelMappingKey":"^v9.0.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/196579","number":196579,"mergeCommit":{"message":"[Search]
Refactor: abstracting classic nav items (#196579)\n\n##
Summary\r\n\r\nMoved the base set of sidenav items from being statically
defined in\r\nuseEnterpriseSearchNav to using a function that can be
shared with the\r\nplugin. Additionally wrapped this generation in a
`useMemo` to improve\r\nperformance.\r\n\r\nThis will support the
ability to share the classic navigation items for\r\nSearch to other
plugins so that they can render their own UIs without\r\nsharing
components with enterprise_search just to have access to the\r\nside nav
defined by enterprise_search.\r\n\r\n### Checklist\r\n\r\n- [x] [Unit or
functional\r\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\r\nwere
updated or added to match the most common scenarios\r\n- [x] [Flaky
Test\r\nRunner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1)
was\r\nused on any tests changed\r\n\r\n---------\r\n\r\nCo-authored-by:
Elastic Machine
<elasticmachine@users.noreply.github.com>","sha":"c4301d080b9fd595b6cf2313d2053256b0fae89d"}},{"branch":"8.x","label":"v8.17.0","branchLabelMappingKey":"^v8.17.0$","isSourceBranch":false,"state":"NOT_CREATED"}]}]
BACKPORT-->

Co-authored-by: Rodney Norris <rodney.norris@elastic.co>
This commit is contained in:
Kibana Machine 2024-10-31 03:22:13 +11:00 committed by GitHub
parent b5edaf6fca
commit 4ba21991f7
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();
});
});
}