mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
[Search] Introduce search navigation plugin (#200314)
This commit is contained in:
parent
ae31ce1ea6
commit
a84122c4ca
25 changed files with 629 additions and 23 deletions
1
.github/CODEOWNERS
vendored
1
.github/CODEOWNERS
vendored
|
@ -961,6 +961,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
|
||||
|
|
|
@ -832,6 +832,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.
|
||||
|
||||
|
|
|
@ -802,6 +802,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",
|
||||
|
|
|
@ -143,6 +143,7 @@ pageLoadAssetSize:
|
|||
searchHomepage: 19831
|
||||
searchIndices: 20519
|
||||
searchInferenceEndpoints: 20470
|
||||
searchNavigation: 19233
|
||||
searchNotebooks: 18942
|
||||
searchPlayground: 19325
|
||||
searchprofiler: 67080
|
||||
|
|
|
@ -1564,6 +1564,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"],
|
||||
|
|
|
@ -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_navigation",
|
||||
"xpack.searchNotebooks": "plugins/search_notebooks",
|
||||
"xpack.searchPlayground": "plugins/search_playground",
|
||||
"xpack.searchInferenceEndpoints": "plugins/search_inference_endpoints",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
|
@ -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 type { GenerateNavLinkFromDeepLinkParameters, GenerateNavLinkParameters } from '../types';
|
||||
|
||||
import { generateNavLink } from './nav_link_helpers';
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
@ -618,6 +620,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)) {
|
||||
|
|
|
@ -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",
|
||||
]
|
||||
}
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
# Search Navigation
|
||||
|
||||
The Search Navigation plugin is used to handle navigation for search solution plugins across both stack and serverless.
|
|
@ -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';
|
|
@ -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: ['<rootDir>/x-pack/plugins/search_solution/search_navigation'],
|
||||
coverageDirectory:
|
||||
'<rootDir>/target/kibana-coverage/jest/x-pack/plugins/search_solution/search_navigation',
|
||||
coverageReporters: ['text', 'html'],
|
||||
collectCoverageFrom: [
|
||||
'<rootDir>/x-pack/plugins/search_solution/search_navigation/{public,server}/**/*.{ts,tsx}',
|
||||
],
|
||||
};
|
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"type": "plugin",
|
||||
"id": "@kbn/search-navigation",
|
||||
"owner": "@elastic/search-kibana",
|
||||
"group": "search",
|
||||
"visibility": "private",
|
||||
"plugin": {
|
||||
"id": "searchNavigation",
|
||||
"server": false,
|
||||
"browser": true,
|
||||
"configPath": [
|
||||
"xpack",
|
||||
"searchNavigation"
|
||||
],
|
||||
"requiredPlugins": [],
|
||||
"optionalPlugins": [
|
||||
"serverless"
|
||||
],
|
||||
"requiredBundles": []
|
||||
}
|
||||
}
|
|
@ -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<Partial<ChromeNavLink>> = [
|
||||
{
|
||||
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: 'Elasticsearch',
|
||||
});
|
||||
});
|
||||
|
||||
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),
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
|
@ -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<string, ChromeNavLink | undefined>;
|
||||
type SolutionNavItems = SolutionNavProps['items'];
|
||||
|
||||
export const classicNavigationFactory: ClassicNavigationFactoryFn = (
|
||||
classicItems: ClassicNavItem[],
|
||||
core: CoreStart,
|
||||
history: ScopedHistory<unknown>
|
||||
): 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: 'Elasticsearch',
|
||||
}),
|
||||
};
|
||||
};
|
||||
|
||||
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<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;
|
||||
}
|
||||
|
||||
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,
|
||||
};
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
/*
|
||||
* 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 { PluginInitializerContext } from '@kbn/core-plugins-browser';
|
||||
import { SearchNavigationPlugin } from './plugin';
|
||||
|
||||
export function plugin(initializerContext: PluginInitializerContext) {
|
||||
return new SearchNavigationPlugin(initializerContext);
|
||||
}
|
||||
|
||||
export type {
|
||||
SearchNavigationPluginSetup,
|
||||
SearchNavigationPluginStart,
|
||||
ClassicNavItem,
|
||||
ClassicNavItemDeepLink,
|
||||
} from './types';
|
|
@ -0,0 +1,92 @@
|
|||
/*
|
||||
* 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,
|
||||
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<SearchNavigationPluginSetup, SearchNavigationPluginStart>
|
||||
{
|
||||
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<void>> = [];
|
||||
|
||||
constructor(private readonly initializerContext: PluginInitializerContext) {
|
||||
this.logger = this.initializerContext.logger.get();
|
||||
}
|
||||
|
||||
public setup(_core: CoreSetup): SearchNavigationPluginSetup {
|
||||
return {};
|
||||
}
|
||||
|
||||
public start(core: CoreStart): SearchNavigationPluginStart {
|
||||
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<void>) {
|
||||
this.onAppMountHandlers.push(handler);
|
||||
}
|
||||
|
||||
private setGetBaseClassicNavItems(classicNavItemsFn: () => ClassicNavItem[]) {
|
||||
this.baseClassicNavItemsFn = classicNavItemsFn;
|
||||
}
|
||||
|
||||
private useClassicNavigation(history: ScopedHistory<unknown>) {
|
||||
if (
|
||||
this.baseClassicNavItemsFn === undefined ||
|
||||
this.classicNavFactory === undefined ||
|
||||
this.coreStart === undefined ||
|
||||
this.currentChromeStyle !== 'classic'
|
||||
)
|
||||
return undefined;
|
||||
|
||||
return this.classicNavFactory(this.baseClassicNavItemsFn(), this.coreStart, history);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,51 @@
|
|||
/*
|
||||
* 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 { 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 {}
|
||||
|
||||
export interface SearchNavigationPluginStart {
|
||||
registerOnAppMountHandler: (onAppMount: () => Promise<void>) => void;
|
||||
handleOnAppMount: () => Promise<void>;
|
||||
// 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<unknown>) => SolutionNavProps | undefined;
|
||||
}
|
||||
|
||||
export interface AppPluginSetupDependencies {
|
||||
serverless?: ServerlessPluginSetup;
|
||||
}
|
||||
|
||||
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<unknown>
|
||||
) => SolutionNavProps | undefined;
|
|
@ -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;
|
||||
};
|
|
@ -0,0 +1,23 @@
|
|||
{
|
||||
"extends": "../../../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "target/types"
|
||||
},
|
||||
"include": [
|
||||
"__mocks__/**/*",
|
||||
"common/**/*",
|
||||
"public/**/*",
|
||||
"server/**/*",
|
||||
"../../../../typings/**/*"
|
||||
],
|
||||
"kbn_references": [
|
||||
"@kbn/core",
|
||||
"@kbn/i18n",
|
||||
"@kbn/core-chrome-browser",
|
||||
"@kbn/shared-ux-page-solution-nav",
|
||||
"@kbn/logging",
|
||||
"@kbn/serverless",
|
||||
"@kbn/core-plugins-browser",
|
||||
],
|
||||
"exclude": ["target/**/*"]
|
||||
}
|
|
@ -6914,6 +6914,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 ""
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue