From 91c2098303e6a47a5123ab805b157992a7ff892c Mon Sep 17 00:00:00 2001 From: Rodney Norris Date: Tue, 12 Nov 2024 22:11:19 +0000 Subject: [PATCH 1/9] search: scaffold navigation plugin --- .github/CODEOWNERS | 1 + docs/developer/plugin-list.asciidoc | 4 ++++ package.json | 1 + tsconfig.base.json | 2 ++ x-pack/.i18nrc.json | 1 + .../search_navigation/README.mdx | 3 +++ .../search_navigation/common/index.ts | 9 ++++++++ .../search_navigation/jest.config.js | 18 +++++++++++++++ .../search_navigation/kibana.jsonc | 19 +++++++++++++++ .../search_navigation/public/index.ts | 15 ++++++++++++ .../search_navigation/public/plugin.ts | 23 +++++++++++++++++++ .../search_navigation/public/types.ts | 21 +++++++++++++++++ .../search_navigation/tsconfig.json | 15 ++++++++++++ yarn.lock | 4 ++++ 14 files changed, 136 insertions(+) create mode 100644 x-pack/plugins/search_solution/search_navigation/README.mdx create mode 100644 x-pack/plugins/search_solution/search_navigation/common/index.ts create mode 100644 x-pack/plugins/search_solution/search_navigation/jest.config.js create mode 100644 x-pack/plugins/search_solution/search_navigation/kibana.jsonc create mode 100644 x-pack/plugins/search_solution/search_navigation/public/index.ts create mode 100644 x-pack/plugins/search_solution/search_navigation/public/plugin.ts create mode 100644 x-pack/plugins/search_solution/search_navigation/public/types.ts create mode 100644 x-pack/plugins/search_solution/search_navigation/tsconfig.json diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 99bd6d6b0757d..1d587e939f50e 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -956,6 +956,7 @@ x-pack/plugins/search_indices @elastic/search-kibana x-pack/plugins/search_inference_endpoints @elastic/search-kibana x-pack/plugins/search_notebooks @elastic/search-kibana x-pack/plugins/search_playground @elastic/search-kibana +x-pack/plugins/search_solution/search_navigation @elastic/search-kibana x-pack/plugins/searchprofiler @elastic/kibana-management x-pack/plugins/security @elastic/kibana-security x-pack/plugins/security_solution @elastic/security-solution diff --git a/docs/developer/plugin-list.asciidoc b/docs/developer/plugin-list.asciidoc index 71ab26400f496..9af852b1ce2e8 100644 --- a/docs/developer/plugin-list.asciidoc +++ b/docs/developer/plugin-list.asciidoc @@ -824,6 +824,10 @@ It uses Chromium and Puppeteer underneath to run the browser in headless mode. |The Inference Endpoints is a tool used to manage inference endpoints +|{kib-repo}blob/{branch}/x-pack/plugins/search_solution/search_navigation/README.mdx[searchNavigation] +|The Search Navigation plugin is used to handle navigation for search solution plugins across both stack and serverless. + + |{kib-repo}blob/{branch}/x-pack/plugins/search_notebooks/README.mdx[searchNotebooks] |This plugin contains endpoints and components for rendering search python notebooks in the persistent dev console. diff --git a/package.json b/package.json index acb1a118284ed..1d5c479b74a59 100644 --- a/package.json +++ b/package.json @@ -798,6 +798,7 @@ "@kbn/search-index-documents": "link:packages/kbn-search-index-documents", "@kbn/search-indices": "link:x-pack/plugins/search_indices", "@kbn/search-inference-endpoints": "link:x-pack/plugins/search_inference_endpoints", + "@kbn/search-navigation": "link:x-pack/plugins/search_solution/search_navigation", "@kbn/search-notebooks": "link:x-pack/plugins/search_notebooks", "@kbn/search-playground": "link:x-pack/plugins/search_playground", "@kbn/search-response-warnings": "link:packages/kbn-search-response-warnings", diff --git a/tsconfig.base.json b/tsconfig.base.json index 223b2d5a58ca2..756a9de5c4cac 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -1554,6 +1554,8 @@ "@kbn/search-indices/*": ["x-pack/plugins/search_indices/*"], "@kbn/search-inference-endpoints": ["x-pack/plugins/search_inference_endpoints"], "@kbn/search-inference-endpoints/*": ["x-pack/plugins/search_inference_endpoints/*"], + "@kbn/search-navigation": ["x-pack/plugins/search_solution/search_navigation"], + "@kbn/search-navigation/*": ["x-pack/plugins/search_solution/search_navigation/*"], "@kbn/search-notebooks": ["x-pack/plugins/search_notebooks"], "@kbn/search-notebooks/*": ["x-pack/plugins/search_notebooks/*"], "@kbn/search-playground": ["x-pack/plugins/search_playground"], diff --git a/x-pack/.i18nrc.json b/x-pack/.i18nrc.json index e1e8478aa0517..74737800b4f26 100644 --- a/x-pack/.i18nrc.json +++ b/x-pack/.i18nrc.json @@ -130,6 +130,7 @@ "xpack.searchSharedUI": "packages/search/shared_ui", "xpack.searchHomepage": "plugins/search_homepage", "xpack.searchIndices": "plugins/search_indices", + "xpack.searchNavigation": "plugins/search_solution/search_notebooks", "xpack.searchNotebooks": "plugins/search_notebooks", "xpack.searchPlayground": "plugins/search_playground", "xpack.searchInferenceEndpoints": "plugins/search_inference_endpoints", diff --git a/x-pack/plugins/search_solution/search_navigation/README.mdx b/x-pack/plugins/search_solution/search_navigation/README.mdx new file mode 100644 index 0000000000000..13ece425b680f --- /dev/null +++ b/x-pack/plugins/search_solution/search_navigation/README.mdx @@ -0,0 +1,3 @@ +# Search Navigation + +The Search Navigation plugin is used to handle navigation for search solution plugins across both stack and serverless. diff --git a/x-pack/plugins/search_solution/search_navigation/common/index.ts b/x-pack/plugins/search_solution/search_navigation/common/index.ts new file mode 100644 index 0000000000000..8a5bd50a5bc37 --- /dev/null +++ b/x-pack/plugins/search_solution/search_navigation/common/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export const PLUGIN_ID = 'searchNavigation'; +export const PLUGIN_NAME = 'searchNavigation'; diff --git a/x-pack/plugins/search_solution/search_navigation/jest.config.js b/x-pack/plugins/search_solution/search_navigation/jest.config.js new file mode 100644 index 0000000000000..cc1c38bb71ced --- /dev/null +++ b/x-pack/plugins/search_solution/search_navigation/jest.config.js @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['/x-pack/plugins/search_solution/search_navigation'], + coverageDirectory: + '/target/kibana-coverage/jest/x-pack/plugins/search_solution/search_navigation', + coverageReporters: ['text', 'html'], + collectCoverageFrom: [ + '/x-pack/plugins/search_solution/search_navigation/{public,server}/**/*.{ts,tsx}', + ], +}; diff --git a/x-pack/plugins/search_solution/search_navigation/kibana.jsonc b/x-pack/plugins/search_solution/search_navigation/kibana.jsonc new file mode 100644 index 0000000000000..c5bea9066350d --- /dev/null +++ b/x-pack/plugins/search_solution/search_navigation/kibana.jsonc @@ -0,0 +1,19 @@ +{ + "type": "plugin", + "id": "@kbn/search-navigation", + "owner": "@elastic/search-kibana", + "plugin": { + "id": "searchNavigation", + "server": false, + "browser": true, + "configPath": [ + "xpack", + "searchNavigation" + ], + "requiredPlugins": [], + "optionalPlugins": [ + "serverless" + ], + "requiredBundles": [] + } +} diff --git a/x-pack/plugins/search_solution/search_navigation/public/index.ts b/x-pack/plugins/search_solution/search_navigation/public/index.ts new file mode 100644 index 0000000000000..901e70da2ae84 --- /dev/null +++ b/x-pack/plugins/search_solution/search_navigation/public/index.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { SearchNavigationPlugin } from './plugin'; + +// This exports static code and TypeScript types, +// as well as, Kibana Platform `plugin()` initializer. +export function plugin() { + return new SearchNavigationPlugin(); +} +export type { SearchNavigationPluginSetup, SearchNavigationPluginStart } from './types'; diff --git a/x-pack/plugins/search_solution/search_navigation/public/plugin.ts b/x-pack/plugins/search_solution/search_navigation/public/plugin.ts new file mode 100644 index 0000000000000..0bc15607831ee --- /dev/null +++ b/x-pack/plugins/search_solution/search_navigation/public/plugin.ts @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { CoreSetup, CoreStart, Plugin } from '@kbn/core/public'; +import type { SearchNavigationPluginSetup, SearchNavigationPluginStart } from './types'; + +export class SearchNavigationPlugin + implements Plugin +{ + public setup(core: CoreSetup): SearchNavigationPluginSetup { + return {}; + } + + public start(core: CoreStart): SearchNavigationPluginStart { + return {}; + } + + public stop() {} +} diff --git a/x-pack/plugins/search_solution/search_navigation/public/types.ts b/x-pack/plugins/search_solution/search_navigation/public/types.ts new file mode 100644 index 0000000000000..0637835577e63 --- /dev/null +++ b/x-pack/plugins/search_solution/search_navigation/public/types.ts @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { ServerlessPluginSetup, ServerlessPluginStart } from '@kbn/serverless/public'; + +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface SearchNavigationPluginSetup {} +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface SearchNavigationPluginStart {} + +export interface AppPluginSetupDependencies { + serverless?: ServerlessPluginSetup; +} + +export interface AppPluginStartDependencies { + serverless?: ServerlessPluginStart; +} diff --git a/x-pack/plugins/search_solution/search_navigation/tsconfig.json b/x-pack/plugins/search_solution/search_navigation/tsconfig.json new file mode 100644 index 0000000000000..76fcc1b747caa --- /dev/null +++ b/x-pack/plugins/search_solution/search_navigation/tsconfig.json @@ -0,0 +1,15 @@ +{ + "extends": "../../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "target/types" + }, + "include": [ + "__mocks__/**/*", + "common/**/*", + "public/**/*", + "server/**/*", + "../../../../typings/**/*" + ], + "kbn_references": ["@kbn/core", "@kbn/navigation-plugin", "@kbn/config-schema"], + "exclude": ["target/**/*"] +} diff --git a/yarn.lock b/yarn.lock index b69c7755172e4..fb1e0e8e2952b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6362,6 +6362,10 @@ version "0.0.0" uid "" +"@kbn/search-navigation@link:x-pack/plugins/search_solution/search_navigation": + version "0.0.0" + uid "" + "@kbn/search-notebooks@link:x-pack/plugins/search_notebooks": version "0.0.0" uid "" From b991bf117061696f56a2bf5edb668660e7d6729b Mon Sep 17 00:00:00 2001 From: Rodney Norris Date: Thu, 14 Nov 2024 22:40:18 +0000 Subject: [PATCH 2/9] search_navigation: handle rendering solution nav props --- .../search_navigation/jest.config.js | 2 +- .../public/classic_navigation.test.ts | 201 ++++++++++++++++++ .../public/classic_navigation.ts | 124 +++++++++++ .../search_navigation/public/index.ts | 15 +- .../search_navigation/public/plugin.ts | 77 ++++++- .../search_navigation/public/types.ts | 34 ++- .../search_navigation/public/utils.ts | 19 ++ .../search_navigation/tsconfig.json | 10 +- 8 files changed, 469 insertions(+), 13 deletions(-) create mode 100644 x-pack/plugins/search_solution/search_navigation/public/classic_navigation.test.ts create mode 100644 x-pack/plugins/search_solution/search_navigation/public/classic_navigation.ts create mode 100644 x-pack/plugins/search_solution/search_navigation/public/utils.ts diff --git a/x-pack/plugins/search_solution/search_navigation/jest.config.js b/x-pack/plugins/search_solution/search_navigation/jest.config.js index cc1c38bb71ced..e86a30c143245 100644 --- a/x-pack/plugins/search_solution/search_navigation/jest.config.js +++ b/x-pack/plugins/search_solution/search_navigation/jest.config.js @@ -7,7 +7,7 @@ module.exports = { preset: '@kbn/test', - rootDir: '../../..', + rootDir: '../../../..', roots: ['/x-pack/plugins/search_solution/search_navigation'], coverageDirectory: '/target/kibana-coverage/jest/x-pack/plugins/search_solution/search_navigation', diff --git a/x-pack/plugins/search_solution/search_navigation/public/classic_navigation.test.ts b/x-pack/plugins/search_solution/search_navigation/public/classic_navigation.test.ts new file mode 100644 index 0000000000000..d352eb988a35f --- /dev/null +++ b/x-pack/plugins/search_solution/search_navigation/public/classic_navigation.test.ts @@ -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 type { CoreStart, ScopedHistory } from '@kbn/core/public'; +import type { ChromeNavLink } from '@kbn/core-chrome-browser'; + +import { classicNavigationFactory } from './classic_navigation'; +import { ClassicNavItem } from './types'; + +describe('classicNavigationFactory', function () { + const mockedNavLinks: Array> = [ + { + id: 'enterpriseSearch', + url: '/app/enterprise_search/overview', + title: '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', + }, + ]; + const mockedCoreStart = { + chrome: { + navLinks: { + getAll: () => mockedNavLinks, + }, + }, + }; + const core = mockedCoreStart as unknown as CoreStart; + const mockHistory = { + location: { + pathname: '/', + }, + createHref: jest.fn(), + }; + const history = mockHistory as unknown as ScopedHistory; + + beforeEach(() => { + jest.clearAllMocks(); + mockHistory.location.pathname = '/'; + mockHistory.createHref.mockReturnValue('/'); + }); + + it('can render top-level items', () => { + const items: ClassicNavItem[] = [ + { + id: 'unit-test', + deepLink: { + link: 'enterpriseSearch', + }, + }, + ]; + expect(classicNavigationFactory(items, core, history)).toEqual({ + icon: 'logoEnterpriseSearch', + items: [ + { + href: '/app/enterprise_search/overview', + id: 'unit-test', + isSelected: false, + name: 'Overview', + onClick: expect.any(Function), + }, + ], + name: 'Search', + }); + }); + + it('will set isSelected', () => { + mockHistory.location.pathname = '/overview'; + mockHistory.createHref.mockReturnValue('/app/enterprise_search/overview'); + + const items: ClassicNavItem[] = [ + { + id: 'unit-test', + deepLink: { + link: 'enterpriseSearch', + }, + }, + ]; + + const solutionNav = classicNavigationFactory(items, core, history); + expect(solutionNav!.items).toEqual([ + { + href: '/app/enterprise_search/overview', + id: 'unit-test', + isSelected: true, + name: 'Overview', + onClick: expect.any(Function), + }, + ]); + }); + it('can render items with children', () => { + const items: ClassicNavItem[] = [ + { + id: 'searchContent', + name: 'Content', + items: [ + { + id: 'searchIndices', + deepLink: { + link: 'enterpriseSearchContent:searchIndices', + }, + }, + { + id: 'searchConnectors', + deepLink: { + link: 'enterpriseSearchContent:connectors', + }, + }, + ], + }, + ]; + + const solutionNav = classicNavigationFactory(items, core, history); + expect(solutionNav!.items).toEqual([ + { + id: 'searchContent', + items: [ + { + href: '/app/enterprise_search/content/search_indices', + id: 'searchIndices', + isSelected: false, + name: 'Indices', + onClick: expect.any(Function), + }, + { + href: '/app/enterprise_search/content/connectors', + id: 'searchConnectors', + isSelected: false, + name: 'Connectors', + onClick: expect.any(Function), + }, + ], + name: 'Content', + }, + ]); + }); + it('returns name if provided over the deeplink title', () => { + const items: ClassicNavItem[] = [ + { + id: 'searchIndices', + deepLink: { + link: 'enterpriseSearchContent:searchIndices', + }, + name: 'Index Management', + }, + ]; + const solutionNav = classicNavigationFactory(items, core, history); + expect(solutionNav!.items).toEqual([ + { + href: '/app/enterprise_search/content/search_indices', + id: 'searchIndices', + isSelected: false, + name: 'Index Management', + onClick: expect.any(Function), + }, + ]); + }); + it('removes item if deeplink not defined', () => { + const items: ClassicNavItem[] = [ + { + id: 'unit-test', + deepLink: { + link: 'enterpriseSearch', + }, + }, + { + id: 'serverlessElasticsearch', + deepLink: { + link: 'serverlessElasticsearch', + }, + }, + ]; + + const solutionNav = classicNavigationFactory(items, core, history); + expect(solutionNav!.items).toEqual([ + { + href: '/app/enterprise_search/overview', + id: 'unit-test', + isSelected: false, + name: 'Overview', + onClick: expect.any(Function), + }, + ]); + }); +}); diff --git a/x-pack/plugins/search_solution/search_navigation/public/classic_navigation.ts b/x-pack/plugins/search_solution/search_navigation/public/classic_navigation.ts new file mode 100644 index 0000000000000..c3c99bb0a9393 --- /dev/null +++ b/x-pack/plugins/search_solution/search_navigation/public/classic_navigation.ts @@ -0,0 +1,124 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { type MouseEvent } from 'react'; +import { i18n } from '@kbn/i18n'; +import type { CoreStart, ScopedHistory } from '@kbn/core/public'; +import type { ChromeNavLink, EuiSideNavItemTypeEnhanced } from '@kbn/core-chrome-browser'; +import type { SolutionNavProps } from '@kbn/shared-ux-page-solution-nav'; + +import type { ClassicNavItem, ClassicNavItemDeepLink, ClassicNavigationFactoryFn } from './types'; +import { stripTrailingSlash } from './utils'; + +type DeepLinksMap = Record; +type SolutionNavItems = SolutionNavProps['items']; + +export const classicNavigationFactory: ClassicNavigationFactoryFn = ( + classicItems: ClassicNavItem[], + core: CoreStart, + history: ScopedHistory +): SolutionNavProps | undefined => { + const navLinks = core.chrome.navLinks.getAll(); + const deepLinks = navLinks.reduce((links: DeepLinksMap, link: ChromeNavLink) => { + links[link.id] = link; + return links; + }, {}); + + const currentPath = stripTrailingSlash(history.location.pathname); + const currentLocation = history.createHref({ pathname: currentPath }); + const items: SolutionNavItems = generateSideNavItems( + classicItems, + core, + deepLinks, + currentLocation + ); + + return { + items, + icon: 'logoEnterpriseSearch', + name: i18n.translate('xpack.searchNavigation.classicNav.name', { + defaultMessage: 'Search', + }), + }; +}; + +function generateSideNavItems( + classicItems: ClassicNavItem[], + core: CoreStart, + deepLinks: DeepLinksMap, + currentLocation: string +): SolutionNavItems { + const result: SolutionNavItems = []; + + for (const navItem of classicItems) { + let children: SolutionNavItems | undefined; + + const { deepLink, items, ...rest } = navItem; + if (items) { + children = generateSideNavItems(items, core, deepLinks, currentLocation); + } + + let item: EuiSideNavItemTypeEnhanced<{}> | undefined; + if (deepLink) { + const sideNavProps = getSideNavItemLinkProps(deepLink, deepLinks, core, currentLocation); + if (sideNavProps) { + const { name, ...linkProps } = sideNavProps; + item = { + ...rest, + ...linkProps, + name: navItem?.name ?? name, + }; + } + } else { + item = { + ...rest, + items: children, + name: navItem.name, + }; + } + + if (isValidSideNavItem(item)) { + result.push(item); + } + } + + return result; +} + +function isValidSideNavItem( + item: EuiSideNavItemTypeEnhanced | undefined +): item is EuiSideNavItemTypeEnhanced { + if (item === undefined) return false; + if (item.href || item.onClick) return true; + if (item?.items?.length ?? 0 > 0) return true; + + return false; +} + +function getSideNavItemLinkProps( + { link, shouldShowActiveForSubroutes }: ClassicNavItemDeepLink, + deepLinks: DeepLinksMap, + core: CoreStart, + currentLocation: string +) { + const deepLink = deepLinks[link]; + if (!deepLink || !deepLink.url) return undefined; + const isSelected = Boolean( + deepLink.url === currentLocation || + (shouldShowActiveForSubroutes && currentLocation.startsWith(deepLink.url)) + ); + + return { + onClick: (e: MouseEvent) => { + e.preventDefault(); + core.application.navigateToUrl(deepLink.url); + }, + href: deepLink.url, + name: deepLink.title, + isSelected, + }; +} diff --git a/x-pack/plugins/search_solution/search_navigation/public/index.ts b/x-pack/plugins/search_solution/search_navigation/public/index.ts index 901e70da2ae84..f5fc03e088a2c 100644 --- a/x-pack/plugins/search_solution/search_navigation/public/index.ts +++ b/x-pack/plugins/search_solution/search_navigation/public/index.ts @@ -5,11 +5,16 @@ * 2.0. */ +import type { PluginInitializerContext } from '@kbn/core-plugins-browser'; import { SearchNavigationPlugin } from './plugin'; -// This exports static code and TypeScript types, -// as well as, Kibana Platform `plugin()` initializer. -export function plugin() { - return new SearchNavigationPlugin(); +export function plugin(initializerContext: PluginInitializerContext) { + return new SearchNavigationPlugin(initializerContext); } -export type { SearchNavigationPluginSetup, SearchNavigationPluginStart } from './types'; + +export type { + SearchNavigationPluginSetup, + SearchNavigationPluginStart, + ClassicNavItem, + ClassicNavItemDeepLink, +} from './types'; diff --git a/x-pack/plugins/search_solution/search_navigation/public/plugin.ts b/x-pack/plugins/search_solution/search_navigation/public/plugin.ts index 0bc15607831ee..4b618a6245c40 100644 --- a/x-pack/plugins/search_solution/search_navigation/public/plugin.ts +++ b/x-pack/plugins/search_solution/search_navigation/public/plugin.ts @@ -5,19 +5,88 @@ * 2.0. */ -import type { CoreSetup, CoreStart, Plugin } from '@kbn/core/public'; -import type { SearchNavigationPluginSetup, SearchNavigationPluginStart } from './types'; +import type { + CoreSetup, + CoreStart, + Plugin, + PluginInitializerContext, + ScopedHistory, +} from '@kbn/core/public'; +import type { ChromeStyle } from '@kbn/core-chrome-browser'; +import type { Logger } from '@kbn/logging'; +import type { + SearchNavigationPluginSetup, + SearchNavigationPluginStart, + ClassicNavItem, + ClassicNavigationFactoryFn, +} from './types'; export class SearchNavigationPlugin implements Plugin { - public setup(core: CoreSetup): SearchNavigationPluginSetup { + private readonly logger: Logger; + private currentChromeStyle: ChromeStyle | undefined = undefined; + private baseClassicNavItemsFn: (() => ClassicNavItem[]) | undefined = undefined; + private coreStart: CoreStart | undefined = undefined; + private classicNavFactory: ClassicNavigationFactoryFn | undefined = undefined; + private onAppMountHandlers: Array<() => Promise> = []; + + constructor(private readonly initializerContext: PluginInitializerContext) { + this.logger = this.initializerContext.logger.get(); + } + + public setup(_core: CoreSetup): SearchNavigationPluginSetup { return {}; } public start(core: CoreStart): SearchNavigationPluginStart { - return {}; + this.coreStart = core; + core.chrome.getChromeStyle$().subscribe((value) => { + this.currentChromeStyle = value; + }); + + import('./classic_navigation').then(({ classicNavigationFactory }) => { + this.classicNavFactory = classicNavigationFactory; + }); + + return { + handleOnAppMount: this.handleOnAppMount.bind(this), + registerOnAppMountHandler: this.registerOnAppMountHandler.bind(this), + setGetBaseClassicNavItems: this.setGetBaseClassicNavItems.bind(this), + useClassicNavigation: this.useClassicNavigation.bind(this), + }; } public stop() {} + + private async handleOnAppMount() { + if (this.onAppMountHandlers.length === 0) return; + + try { + await Promise.all(this.onAppMountHandlers); + } catch (e) { + this.logger.warn('Error handling app mount functions for search navigation'); + this.logger.warn(e); + } + } + + private registerOnAppMountHandler(handler: () => Promise) { + this.onAppMountHandlers.push(handler); + } + + private setGetBaseClassicNavItems(classicNavItemsFn: () => ClassicNavItem[]) { + this.baseClassicNavItemsFn = classicNavItemsFn; + } + + private useClassicNavigation(history: ScopedHistory) { + if ( + this.baseClassicNavItemsFn === undefined || + this.classicNavFactory === undefined || + this.coreStart === undefined || + this.currentChromeStyle !== 'classic' + ) + return undefined; + + return this.classicNavFactory(this.baseClassicNavItemsFn(), this.coreStart, history); + } } diff --git a/x-pack/plugins/search_solution/search_navigation/public/types.ts b/x-pack/plugins/search_solution/search_navigation/public/types.ts index 0637835577e63..91e8cc73524e2 100644 --- a/x-pack/plugins/search_solution/search_navigation/public/types.ts +++ b/x-pack/plugins/search_solution/search_navigation/public/types.ts @@ -5,12 +5,22 @@ * 2.0. */ +import type { ReactNode } from 'react'; +import type { AppDeepLinkId } from '@kbn/core-chrome-browser'; +import type { CoreStart, ScopedHistory } from '@kbn/core/public'; import type { ServerlessPluginSetup, ServerlessPluginStart } from '@kbn/serverless/public'; +import type { SolutionNavProps } from '@kbn/shared-ux-page-solution-nav'; // eslint-disable-next-line @typescript-eslint/no-empty-interface export interface SearchNavigationPluginSetup {} -// eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface SearchNavigationPluginStart {} + +export interface SearchNavigationPluginStart { + registerOnAppMountHandler: (onAppMount: () => Promise) => void; + handleOnAppMount: () => Promise; + // This is temporary until we can migrate building the class nav item list out of `enterprise_search` plugin + setGetBaseClassicNavItems: (classicNavItemsFn: () => ClassicNavItem[]) => void; + useClassicNavigation: (history: ScopedHistory) => SolutionNavProps | undefined; +} export interface AppPluginSetupDependencies { serverless?: ServerlessPluginSetup; @@ -19,3 +29,23 @@ export interface AppPluginSetupDependencies { export interface AppPluginStartDependencies { serverless?: ServerlessPluginStart; } + +export interface ClassicNavItemDeepLink { + link: AppDeepLinkId; + shouldShowActiveForSubroutes?: boolean; +} + +export interface ClassicNavItem { + 'data-test-subj'?: string; + deepLink?: ClassicNavItemDeepLink; + iconToString?: string; + id: string; + items?: ClassicNavItem[]; + name?: ReactNode; +} + +export type ClassicNavigationFactoryFn = ( + items: ClassicNavItem[], + core: CoreStart, + history: ScopedHistory +) => SolutionNavProps | undefined; diff --git a/x-pack/plugins/search_solution/search_navigation/public/utils.ts b/x-pack/plugins/search_solution/search_navigation/public/utils.ts new file mode 100644 index 0000000000000..fb80778977b16 --- /dev/null +++ b/x-pack/plugins/search_solution/search_navigation/public/utils.ts @@ -0,0 +1,19 @@ +/* + * 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. + */ + +/** + * Helpers for stripping trailing or leading slashes from URLs or paths + * (usually ones that come in from React Router or API endpoints) + */ + +export const stripTrailingSlash = (url: string): string => { + return url && url.endsWith('/') ? url.slice(0, -1) : url; +}; + +export const stripLeadingSlash = (path: string): string => { + return path && path.startsWith('/') ? path.substring(1) : path; +}; diff --git a/x-pack/plugins/search_solution/search_navigation/tsconfig.json b/x-pack/plugins/search_solution/search_navigation/tsconfig.json index 76fcc1b747caa..54d37b6ff1caa 100644 --- a/x-pack/plugins/search_solution/search_navigation/tsconfig.json +++ b/x-pack/plugins/search_solution/search_navigation/tsconfig.json @@ -10,6 +10,14 @@ "server/**/*", "../../../../typings/**/*" ], - "kbn_references": ["@kbn/core", "@kbn/navigation-plugin", "@kbn/config-schema"], + "kbn_references": [ + "@kbn/core", + "@kbn/config-schema", + "@kbn/i18n", + "@kbn/core-chrome-browser", + "@kbn/shared-ux-page-solution-nav", + "@kbn/logging", + "@kbn/serverless", + ], "exclude": ["target/**/*"] } From 3671ee02599743f44a8b84f214303a175cfa96f8 Mon Sep 17 00:00:00 2001 From: Rodney Norris Date: Fri, 15 Nov 2024 21:41:09 +0000 Subject: [PATCH 3/9] search: configure search_navigation plugin in stack --- x-pack/plugins/enterprise_search/kibana.jsonc | 5 ++-- .../applications/shared/layout/base_nav.tsx | 3 ++- .../shared/layout/classic_nav_helpers.test.ts | 3 +-- .../shared/layout/classic_nav_helpers.ts | 7 ++---- .../public/applications/shared/types.ts | 11 -------- .../enterprise_search/public/plugin.ts | 25 ++++++++++++++++++- .../plugins/enterprise_search/tsconfig.json | 3 ++- 7 files changed, 34 insertions(+), 23 deletions(-) diff --git a/x-pack/plugins/enterprise_search/kibana.jsonc b/x-pack/plugins/enterprise_search/kibana.jsonc index e284ae1862144..65343904ba7fc 100644 --- a/x-pack/plugins/enterprise_search/kibana.jsonc +++ b/x-pack/plugins/enterprise_search/kibana.jsonc @@ -20,7 +20,7 @@ "logsShared", "logsDataAccess", "esUiShared", - "navigation" + "navigation", ], "optionalPlugins": [ "customIntegrations", @@ -34,8 +34,9 @@ "guidedOnboarding", "console", "searchConnectors", - "searchPlayground", "searchInferenceEndpoints", + "searchNavigation", + "searchPlayground", "embeddable", "discover", "charts", diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/layout/base_nav.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/layout/base_nav.tsx index b971ab6deff53..a8fff53d8a9b2 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/layout/base_nav.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/layout/base_nav.tsx @@ -17,10 +17,11 @@ import { SEARCH_AI_SEARCH, } from '@kbn/deeplinks-search'; import { i18n } from '@kbn/i18n'; +import type { ClassicNavItem } from '@kbn/search-navigation/public'; import { GETTING_STARTED_TITLE } from '../../../../common/constants'; -import { ClassicNavItem, BuildClassicNavParameters } from '../types'; +import { BuildClassicNavParameters } from '../types'; export const buildBaseClassicNavItems = ({ productAccess, diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/layout/classic_nav_helpers.test.ts b/x-pack/plugins/enterprise_search/public/applications/shared/layout/classic_nav_helpers.test.ts index 514072ba297aa..d43d14aba2235 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/layout/classic_nav_helpers.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/layout/classic_nav_helpers.test.ts @@ -8,6 +8,7 @@ import { mockKibanaValues } from '../../__mocks__/kea_logic'; import type { ChromeNavLink } from '@kbn/core-chrome-browser'; +import type { ClassicNavItem } from '@kbn/search-navigation/public'; import '../../__mocks__/react_router'; @@ -15,8 +16,6 @@ jest.mock('../react_router_helpers/link_events', () => ({ letBrowserHandleEvent: jest.fn(), })); -import { ClassicNavItem } from '../types'; - import { generateSideNavItems } from './classic_nav_helpers'; describe('generateSideNavItems', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/layout/classic_nav_helpers.ts b/x-pack/plugins/enterprise_search/public/applications/shared/layout/classic_nav_helpers.ts index 89f3c2ab5b59a..f42a6197f632a 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/layout/classic_nav_helpers.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/layout/classic_nav_helpers.ts @@ -6,12 +6,9 @@ */ import { ChromeNavLink, EuiSideNavItemTypeEnhanced } from '@kbn/core-chrome-browser'; +import type { ClassicNavItem } from '@kbn/search-navigation/public'; -import { - ClassicNavItem, - GenerateNavLinkFromDeepLinkParameters, - GenerateNavLinkParameters, -} from '../types'; +import { GenerateNavLinkFromDeepLinkParameters, GenerateNavLinkParameters } from '../types'; import { generateNavLink } from './nav_link_helpers'; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/types.ts b/x-pack/plugins/enterprise_search/public/applications/shared/types.ts index 095f1dddfcc4a..25fce6c62d05d 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/types.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/types.ts @@ -5,8 +5,6 @@ * 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'; @@ -87,12 +85,3 @@ export interface GenerateNavLinkFromDeepLinkParameters { export interface BuildClassicNavParameters { productAccess: ProductAccess; } - -export interface ClassicNavItem { - 'data-test-subj'?: string; - deepLink?: GenerateNavLinkFromDeepLinkParameters; - iconToString?: string; - id: string; - items?: ClassicNavItem[]; - name?: ReactNode; -} diff --git a/x-pack/plugins/enterprise_search/public/plugin.ts b/x-pack/plugins/enterprise_search/public/plugin.ts index 1eb3384d4f9e3..f5971b5cc9efd 100644 --- a/x-pack/plugins/enterprise_search/public/plugin.ts +++ b/x-pack/plugins/enterprise_search/public/plugin.ts @@ -35,6 +35,7 @@ import type { NavigationPublicPluginStart } from '@kbn/navigation-plugin/public' import { ELASTICSEARCH_URL_PLACEHOLDER } from '@kbn/search-api-panels/constants'; import { SearchConnectorsPluginStart } from '@kbn/search-connectors-plugin/public'; import { SearchInferenceEndpointsPluginStart } from '@kbn/search-inference-endpoints/public'; +import type { SearchNavigationPluginStart } from '@kbn/search-navigation/public'; import { SearchPlaygroundPluginStart } from '@kbn/search-playground/public'; import { SecurityPluginSetup, SecurityPluginStart } from '@kbn/security-plugin/public'; import { SharePluginSetup, SharePluginStart } from '@kbn/share-plugin/public'; @@ -55,7 +56,7 @@ import { SEARCH_RELEVANCE_PLUGIN, } from '../common/constants'; import { registerLocators } from '../common/locators'; -import { ClientConfigType, InitialAppData } from '../common/types'; +import { ClientConfigType, InitialAppData, ProductAccess } from '../common/types'; import { hasEnterpriseLicense } from '../common/utils/licensing'; import { ENGINES_PATH } from './applications/app_search/routes'; @@ -99,6 +100,7 @@ export interface PluginsStart { navigation: NavigationPublicPluginStart; searchConnectors?: SearchConnectorsPluginStart; searchInferenceEndpoints?: SearchInferenceEndpointsPluginStart; + searchNavigation?: SearchNavigationPluginStart; searchPlayground?: SearchPlaygroundPluginStart; security?: SecurityPluginStart; share?: SharePluginStart; @@ -619,6 +621,27 @@ export class EnterpriseSearchPlugin implements Plugin { }) ); }); + if (plugins.searchNavigation !== undefined) { + // while we have ent-search apps in the side nav, we need to provide access + // to the base set of classic side nav items to the search-navigation plugin. + import('./applications/shared/layout/base_nav').then(({ buildBaseClassicNavItems }) => { + plugins.searchNavigation?.setGetBaseClassicNavItems(() => { + const productAccess: ProductAccess = this.data?.access ?? { + hasAppSearchAccess: false, + hasWorkplaceSearchAccess: false, + }; + + return buildBaseClassicNavItems({ productAccess }); + }); + }); + + // This is needed so that we can fetch product access for plugins + // that need to share the classic nav. This can be removed when we + // remove product access and ent-search apps. + plugins.searchNavigation.registerOnAppMountHandler(async () => { + return this.getInitialData(core.http); + }); + } plugins.licensing?.license$.subscribe((license) => { if (hasEnterpriseLicense(license)) { diff --git a/x-pack/plugins/enterprise_search/tsconfig.json b/x-pack/plugins/enterprise_search/tsconfig.json index 7b7556729a76c..de98a647e0a94 100644 --- a/x-pack/plugins/enterprise_search/tsconfig.json +++ b/x-pack/plugins/enterprise_search/tsconfig.json @@ -83,6 +83,7 @@ "@kbn/security-plugin-types-common", "@kbn/core-security-server", "@kbn/core-security-server-mocks", - "@kbn/unsaved-changes-prompt" + "@kbn/unsaved-changes-prompt", + "@kbn/search-navigation", ] } From f5ac08c89299953e5b1e88382144aac9861bec24 Mon Sep 17 00:00:00 2001 From: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Date: Fri, 15 Nov 2024 22:02:58 +0000 Subject: [PATCH 4/9] [CI] Auto-commit changed files from 'node scripts/lint_ts_projects --fix' --- x-pack/plugins/search_solution/search_navigation/tsconfig.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/search_solution/search_navigation/tsconfig.json b/x-pack/plugins/search_solution/search_navigation/tsconfig.json index 54d37b6ff1caa..6d61fbb24ec89 100644 --- a/x-pack/plugins/search_solution/search_navigation/tsconfig.json +++ b/x-pack/plugins/search_solution/search_navigation/tsconfig.json @@ -12,12 +12,12 @@ ], "kbn_references": [ "@kbn/core", - "@kbn/config-schema", "@kbn/i18n", "@kbn/core-chrome-browser", "@kbn/shared-ux-page-solution-nav", "@kbn/logging", "@kbn/serverless", + "@kbn/core-plugins-browser", ], "exclude": ["target/**/*"] } From 7c43a4ce40d7fdd17fb36dc82cc04c19216460d8 Mon Sep 17 00:00:00 2001 From: Rodney Norris Date: Sat, 16 Nov 2024 04:24:27 +0000 Subject: [PATCH 5/9] search_navigation: add limits --- packages/kbn-optimizer/limits.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/kbn-optimizer/limits.yml b/packages/kbn-optimizer/limits.yml index 8e1cd3e8fb7a1..411be3ba517f5 100644 --- a/packages/kbn-optimizer/limits.yml +++ b/packages/kbn-optimizer/limits.yml @@ -142,6 +142,7 @@ pageLoadAssetSize: searchHomepage: 19831 searchIndices: 20519 searchInferenceEndpoints: 20470 + searchNavigation: 19233 searchNotebooks: 18942 searchPlayground: 19325 searchprofiler: 67080 From deb3dfef721c8e2e338f995cff7ecbb5dd510bdb Mon Sep 17 00:00:00 2001 From: Rodney Norris Date: Tue, 19 Nov 2024 20:08:49 +0000 Subject: [PATCH 6/9] fix(i18n): use correct path for search nav plugin --- x-pack/.i18nrc.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/.i18nrc.json b/x-pack/.i18nrc.json index 74737800b4f26..7c97dcf1b722f 100644 --- a/x-pack/.i18nrc.json +++ b/x-pack/.i18nrc.json @@ -130,7 +130,7 @@ "xpack.searchSharedUI": "packages/search/shared_ui", "xpack.searchHomepage": "plugins/search_homepage", "xpack.searchIndices": "plugins/search_indices", - "xpack.searchNavigation": "plugins/search_solution/search_notebooks", + "xpack.searchNavigation": "plugins/search_solution/search_navigation", "xpack.searchNotebooks": "plugins/search_notebooks", "xpack.searchPlayground": "plugins/search_playground", "xpack.searchInferenceEndpoints": "plugins/search_inference_endpoints", From 7d05b1f74755b62530218e504b8ee64791395109 Mon Sep 17 00:00:00 2001 From: Rodney Norris Date: Tue, 19 Nov 2024 20:09:04 +0000 Subject: [PATCH 7/9] fix: import just types --- .../public/applications/shared/layout/classic_nav_helpers.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/layout/classic_nav_helpers.ts b/x-pack/plugins/enterprise_search/public/applications/shared/layout/classic_nav_helpers.ts index f42a6197f632a..4609e01beb6f1 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/layout/classic_nav_helpers.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/layout/classic_nav_helpers.ts @@ -8,7 +8,7 @@ import { ChromeNavLink, EuiSideNavItemTypeEnhanced } from '@kbn/core-chrome-browser'; import type { ClassicNavItem } from '@kbn/search-navigation/public'; -import { GenerateNavLinkFromDeepLinkParameters, GenerateNavLinkParameters } from '../types'; +import type { GenerateNavLinkFromDeepLinkParameters, GenerateNavLinkParameters } from '../types'; import { generateNavLink } from './nav_link_helpers'; From b6835d78be2d8fe0db5509df37b541753d71a371 Mon Sep 17 00:00:00 2001 From: Rodney Norris Date: Tue, 19 Nov 2024 20:10:00 +0000 Subject: [PATCH 8/9] refactor: update classic nav to Elasticsearch to match stack --- .../search_navigation/public/classic_navigation.test.ts | 2 +- .../search_navigation/public/classic_navigation.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/search_solution/search_navigation/public/classic_navigation.test.ts b/x-pack/plugins/search_solution/search_navigation/public/classic_navigation.test.ts index d352eb988a35f..1b17296f54c73 100644 --- a/x-pack/plugins/search_solution/search_navigation/public/classic_navigation.test.ts +++ b/x-pack/plugins/search_solution/search_navigation/public/classic_navigation.test.ts @@ -76,7 +76,7 @@ describe('classicNavigationFactory', function () { onClick: expect.any(Function), }, ], - name: 'Search', + name: 'Elasticsearch', }); }); diff --git a/x-pack/plugins/search_solution/search_navigation/public/classic_navigation.ts b/x-pack/plugins/search_solution/search_navigation/public/classic_navigation.ts index c3c99bb0a9393..99a6b33c4bcce 100644 --- a/x-pack/plugins/search_solution/search_navigation/public/classic_navigation.ts +++ b/x-pack/plugins/search_solution/search_navigation/public/classic_navigation.ts @@ -41,7 +41,7 @@ export const classicNavigationFactory: ClassicNavigationFactoryFn = ( items, icon: 'logoEnterpriseSearch', name: i18n.translate('xpack.searchNavigation.classicNav.name', { - defaultMessage: 'Search', + defaultMessage: 'Elasticsearch', }), }; }; From e5f792a07147ba451045a031ca65cd832eae5450 Mon Sep 17 00:00:00 2001 From: Rodney Norris Date: Thu, 21 Nov 2024 14:29:05 +0000 Subject: [PATCH 9/9] search_navigation: add group and vis to plugin metadata --- x-pack/plugins/search_solution/search_navigation/kibana.jsonc | 2 ++ 1 file changed, 2 insertions(+) diff --git a/x-pack/plugins/search_solution/search_navigation/kibana.jsonc b/x-pack/plugins/search_solution/search_navigation/kibana.jsonc index c5bea9066350d..4b10b5f3a5a78 100644 --- a/x-pack/plugins/search_solution/search_navigation/kibana.jsonc +++ b/x-pack/plugins/search_solution/search_navigation/kibana.jsonc @@ -2,6 +2,8 @@ "type": "plugin", "id": "@kbn/search-navigation", "owner": "@elastic/search-kibana", + "group": "search", + "visibility": "private", "plugin": { "id": "searchNavigation", "server": false,