From c4d233d29a26ab8198a306ea9e02f31b466ca434 Mon Sep 17 00:00:00 2001 From: Sergi Massaneda Date: Sun, 7 May 2023 19:04:37 +0200 Subject: [PATCH] [Security Solution][Serverless] Left navigation (#156600) ## Summary closes: https://github.com/elastic/kibana/issues/156414 Adds the basic navigation to the Security Solution project. - Renders the current navigation hierarchy as in the classic Security. - Uses the basic styles defined by Core (dark sideNav has been dropped). - Reuses the Security SideNav package. - Adds the `setSideNavComponent` API to the Serverless plugin. ![screenshot](https://user-images.githubusercontent.com/17747913/236006911-1d1ccc9b-8a00-41eb-be54-8d6b85be3cb5.png) ### Run project `yarn serverless-security` ## Next steps - Add the new features needed in the navigation package to align with the new Security IA design. - Update the configuration of the links to display the hierarchy defined by the new Security IA. - Add Serverless specific styles to the sideNav in the ServerlesSecurity plugin. --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- .github/CODEOWNERS | 3 +- .../plugins/security_solution/public/mocks.ts | 23 ++ .../security_solution/public/plugin.tsx | 2 +- .../plugins/security_solution/public/types.ts | 2 +- x-pack/plugins/serverless/public/mocks.ts | 16 + x-pack/plugins/serverless/public/plugin.tsx | 5 +- x-pack/plugins/serverless/public/types.ts | 6 +- x-pack/plugins/serverless/tsconfig.json | 1 + .../serverless_security/jest.config.dev.js | 12 + .../plugins/serverless_security/kibana.jsonc | 2 +- .../public/components/jest.config.js | 28 ++ .../components/side_navigation/index.tsx | 26 ++ .../side_navigation/side_navigation.test.tsx | 101 ++++++ .../side_navigation/side_navigation.tsx | 44 +++ .../public/hooks/__mocks__/use_link_props.ts | 16 + .../hooks/__mocks__/use_side_nav_items.ts | 21 ++ .../public/hooks/jest.config.js | 28 ++ .../public/hooks/use_link_props.test.tsx | 156 +++++++++ .../public/hooks/use_link_props.ts | 65 ++++ .../public/hooks/use_nav_links.ts | 17 + .../public/hooks/use_side_nav_items.test.tsx | 302 ++++++++++++++++++ .../public/hooks/use_side_nav_items.ts | 143 +++++++++ .../serverless_security/public/jest.config.js | 27 ++ .../serverless_security/public/plugin.ts | 9 +- .../public/services.mock.tsx | 26 ++ .../serverless_security/public/services.tsx | 26 ++ .../plugins/serverless_security/tsconfig.json | 5 + 27 files changed, 1103 insertions(+), 9 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/mocks.ts create mode 100644 x-pack/plugins/serverless/public/mocks.ts create mode 100644 x-pack/plugins/serverless_security/jest.config.dev.js create mode 100644 x-pack/plugins/serverless_security/public/components/jest.config.js create mode 100644 x-pack/plugins/serverless_security/public/components/side_navigation/index.tsx create mode 100644 x-pack/plugins/serverless_security/public/components/side_navigation/side_navigation.test.tsx create mode 100644 x-pack/plugins/serverless_security/public/components/side_navigation/side_navigation.tsx create mode 100644 x-pack/plugins/serverless_security/public/hooks/__mocks__/use_link_props.ts create mode 100644 x-pack/plugins/serverless_security/public/hooks/__mocks__/use_side_nav_items.ts create mode 100644 x-pack/plugins/serverless_security/public/hooks/jest.config.js create mode 100644 x-pack/plugins/serverless_security/public/hooks/use_link_props.test.tsx create mode 100644 x-pack/plugins/serverless_security/public/hooks/use_link_props.ts create mode 100644 x-pack/plugins/serverless_security/public/hooks/use_nav_links.ts create mode 100644 x-pack/plugins/serverless_security/public/hooks/use_side_nav_items.test.tsx create mode 100644 x-pack/plugins/serverless_security/public/hooks/use_side_nav_items.ts create mode 100644 x-pack/plugins/serverless_security/public/jest.config.js create mode 100644 x-pack/plugins/serverless_security/public/services.mock.tsx create mode 100644 x-pack/plugins/serverless_security/public/services.tsx diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index cb9607ecbb51a7..fa2a580b4ae83e 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -579,7 +579,7 @@ x-pack/plugins/serverless @elastic/appex-sharedux x-pack/plugins/serverless_observability @elastic/appex-sharedux packages/serverless/project_switcher @elastic/appex-sharedux x-pack/plugins/serverless_search @elastic/appex-sharedux -x-pack/plugins/serverless_security @elastic/appex-sharedux +x-pack/plugins/serverless_security @elastic/security-solution packages/serverless/storybook/config @elastic/appex-sharedux packages/serverless/types @elastic/appex-sharedux test/plugin_functional/plugins/session_notifications @elastic/kibana-core @@ -789,6 +789,7 @@ packages/kbn-yarn-lock-validator @elastic/kibana-operations #CC# /src/plugins/home/server/services/ @elastic/appex-sharedux #CC# /src/plugins/home/ @elastic/appex-sharedux #CC# /x-pack/plugins/reporting/ @elastic/appex-sharedux +#CC# /x-pack/plugins/serverless_security/ @elastic/appex-sharedux ### Observability Plugins diff --git a/x-pack/plugins/security_solution/public/mocks.ts b/x-pack/plugins/security_solution/public/mocks.ts new file mode 100644 index 00000000000000..f16e81636846cc --- /dev/null +++ b/x-pack/plugins/security_solution/public/mocks.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 { BehaviorSubject } from 'rxjs'; +import type { NavigationLink } from './common/links/types'; + +const setupMock = () => ({ + resolver: jest.fn(), +}); + +const startMock = () => ({ + getNavLinks$: jest.fn(() => new BehaviorSubject([])), + setIsSidebarEnabled: jest.fn(), +}); + +export const securitySolutionMock = { + createSetup: setupMock, + createStart: startMock, +}; diff --git a/x-pack/plugins/security_solution/public/plugin.tsx b/x-pack/plugins/security_solution/public/plugin.tsx index 14b886920613b4..7e46c170ca133c 100644 --- a/x-pack/plugins/security_solution/public/plugin.tsx +++ b/x-pack/plugins/security_solution/public/plugin.tsx @@ -310,7 +310,7 @@ export class Plugin implements IPlugin navLinks$, setIsSidebarEnabled: (isSidebarEnabled: boolean) => this.isSidebarEnabled$.next(isSidebarEnabled), }; diff --git a/x-pack/plugins/security_solution/public/types.ts b/x-pack/plugins/security_solution/public/types.ts index ba5bd460ba376f..76351fde26135b 100644 --- a/x-pack/plugins/security_solution/public/types.ts +++ b/x-pack/plugins/security_solution/public/types.ts @@ -141,7 +141,7 @@ export interface PluginSetup { } export interface PluginStart { - navLinks$: Observable; + getNavLinks$: () => Observable; setIsSidebarEnabled: (isSidebarEnabled: boolean) => void; } diff --git a/x-pack/plugins/serverless/public/mocks.ts b/x-pack/plugins/serverless/public/mocks.ts new file mode 100644 index 00000000000000..d83018425260b0 --- /dev/null +++ b/x-pack/plugins/serverless/public/mocks.ts @@ -0,0 +1,16 @@ +/* + * 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 { ServerlessPluginStart } from './types'; + +const startMock = (): ServerlessPluginStart => ({ + setSideNavComponent: jest.fn(), +}); + +export const serverlessMock = { + createStart: startMock, +}; diff --git a/x-pack/plugins/serverless/public/plugin.tsx b/x-pack/plugins/serverless/public/plugin.tsx index 20f4634fb7fa09..1d18ed27cc8f0f 100644 --- a/x-pack/plugins/serverless/public/plugin.tsx +++ b/x-pack/plugins/serverless/public/plugin.tsx @@ -62,7 +62,10 @@ export class ServerlessPlugin core.chrome.setChromeStyle('project'); management.setIsSidebarEnabled(false); - return {}; + return { + setSideNavComponent: (sideNavigationComponent) => + core.chrome.project.setSideNavComponent(sideNavigationComponent), + }; } public stop() {} diff --git a/x-pack/plugins/serverless/public/types.ts b/x-pack/plugins/serverless/public/types.ts index 9e8d9df34cb570..23109f6fb5db54 100644 --- a/x-pack/plugins/serverless/public/types.ts +++ b/x-pack/plugins/serverless/public/types.ts @@ -6,12 +6,14 @@ */ import type { ManagementSetup, ManagementStart } from '@kbn/management-plugin/public'; +import { SideNavComponent } from '@kbn/core-chrome-browser/src/project_navigation'; // eslint-disable-next-line @typescript-eslint/no-empty-interface export interface ServerlessPluginSetup {} -// eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface ServerlessPluginStart {} +export interface ServerlessPluginStart { + setSideNavComponent: (navigation: SideNavComponent) => void; +} export interface ServerlessPluginSetupDependencies { management: ManagementSetup; diff --git a/x-pack/plugins/serverless/tsconfig.json b/x-pack/plugins/serverless/tsconfig.json index 75c3b77afc9bb3..490449a4ee5a42 100644 --- a/x-pack/plugins/serverless/tsconfig.json +++ b/x-pack/plugins/serverless/tsconfig.json @@ -21,5 +21,6 @@ "@kbn/serverless-project-switcher", "@kbn/serverless-types", "@kbn/utils", + "@kbn/core-chrome-browser", ] } diff --git a/x-pack/plugins/serverless_security/jest.config.dev.js b/x-pack/plugins/serverless_security/jest.config.dev.js new file mode 100644 index 00000000000000..f759355f0e0e5a --- /dev/null +++ b/x-pack/plugins/serverless_security/jest.config.dev.js @@ -0,0 +1,12 @@ +/* + * 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: '../../../', + projects: ['/x-pack/plugins/serverless_security/public/*/jest.config.js'], +}; diff --git a/x-pack/plugins/serverless_security/kibana.jsonc b/x-pack/plugins/serverless_security/kibana.jsonc index 99def198e6b466..dec92842f389c1 100644 --- a/x-pack/plugins/serverless_security/kibana.jsonc +++ b/x-pack/plugins/serverless_security/kibana.jsonc @@ -1,7 +1,7 @@ { "type": "plugin", "id": "@kbn/serverless-security", - "owner": "@elastic/appex-sharedux", + "owner": "@elastic/security-solution", "description": "Serverless customizations for security.", "plugin": { "id": "serverlessSecurity", diff --git a/x-pack/plugins/serverless_security/public/components/jest.config.js b/x-pack/plugins/serverless_security/public/components/jest.config.js new file mode 100644 index 00000000000000..2bfc5a72a09ce3 --- /dev/null +++ b/x-pack/plugins/serverless_security/public/components/jest.config.js @@ -0,0 +1,28 @@ +/* + * 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/serverless_security/public/components'], + testMatch: [ + '/x-pack/plugins/serverless_security/public/components/**/*.test.{js,mjs,ts,tsx}', + ], + coverageDirectory: + '/target/kibana-coverage/jest/x-pack/plugins/serverless_security/public/components', + coverageReporters: ['text', 'html'], + collectCoverageFrom: [ + '/x-pack/plugins/serverless_security/public/components/**/*.{ts,tsx}', + '!/x-pack/plugins/serverless_security/public/components/*.test.{ts,tsx}', + '!/x-pack/plugins/serverless_security/public/components/{__test__,__snapshots__,__examples__,*mock*,tests,test_helpers,integration_tests,types}/**/*', + '!/x-pack/plugins/serverless_security/public/components/*mock*.{ts,tsx}', + '!/x-pack/plugins/serverless_security/public/components/*.test.{ts,tsx}', + '!/x-pack/plugins/serverless_security/public/components/*.d.ts', + '!/x-pack/plugins/serverless_security/public/components/*.config.ts', + '!/x-pack/plugins/serverless_security/public/components/index.{js,ts,tsx}', + ], +}; diff --git a/x-pack/plugins/serverless_security/public/components/side_navigation/index.tsx b/x-pack/plugins/serverless_security/public/components/side_navigation/index.tsx new file mode 100644 index 00000000000000..75ee42419fce6e --- /dev/null +++ b/x-pack/plugins/serverless_security/public/components/side_navigation/index.tsx @@ -0,0 +1,26 @@ +/* + * 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 { CoreStart } from '@kbn/core/public'; +import type { + SideNavComponent, + SideNavCompProps, +} from '@kbn/core-chrome-browser/src/project_navigation'; +import { ServerlessSecurityPluginStartDependencies } from '../../types'; +import { SecuritySideNavigation } from './side_navigation'; +import { KibanaServicesProvider } from '../../services'; + +export const getSecuritySideNavComponent = ( + core: CoreStart, + pluginsStart: ServerlessSecurityPluginStartDependencies +): SideNavComponent => { + return (_props: SideNavCompProps) => ( + + + + ); +}; diff --git a/x-pack/plugins/serverless_security/public/components/side_navigation/side_navigation.test.tsx b/x-pack/plugins/serverless_security/public/components/side_navigation/side_navigation.test.tsx new file mode 100644 index 00000000000000..7b67b4714da677 --- /dev/null +++ b/x-pack/plugins/serverless_security/public/components/side_navigation/side_navigation.test.tsx @@ -0,0 +1,101 @@ +/* + * 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 { render } from '@testing-library/react'; +import { SecuritySideNavigation } from './side_navigation'; +import { useSideNavItems, useSideNavSelectedId } from '../../hooks/use_side_nav_items'; +import { SecurityPageName } from '@kbn/security-solution-plugin/common'; +import { KibanaServicesProvider } from '../../services.mock'; + +jest.mock('../../hooks/use_side_nav_items'); +const mockUseSideNavItems = useSideNavItems as jest.Mock; +const mockUseSideNavSelectedId = useSideNavSelectedId as jest.Mock; + +const mockSolutionSideNav = jest.fn((_props: unknown) =>
); +jest.mock('@kbn/security-solution-side-nav', () => ({ + SolutionSideNav: (props: unknown) => mockSolutionSideNav(props), +})); + +const sideNavItems = [ + { + id: SecurityPageName.dashboards, + label: 'Dashboards', + href: '/dashboards', + onClick: jest.fn(), + }, + { + id: SecurityPageName.alerts, + label: 'Alerts', + href: '/alerts', + onClick: jest.fn(), + }, +]; +const sideNavFooterItems = [ + { + id: SecurityPageName.administration, + label: 'Manage', + href: '/administration', + onClick: jest.fn(), + }, +]; + +mockUseSideNavItems.mockReturnValue(sideNavItems); +mockUseSideNavSelectedId.mockReturnValue(SecurityPageName.alerts); + +describe('SecuritySideNavigation', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should render loading when not items received', () => { + mockUseSideNavItems.mockReturnValueOnce([]); + const component = render(, { wrapper: KibanaServicesProvider }); + expect(component.queryByTestId('sideNavLoader')).toBeInTheDocument(); + }); + + it('should not render loading when items received', () => { + const component = render(, { wrapper: KibanaServicesProvider }); + expect(component.queryByTestId('sideNavLoader')).not.toBeInTheDocument(); + }); + + it('should render the SideNav when items received', () => { + const component = render(, { wrapper: KibanaServicesProvider }); + expect(component.queryByTestId('solutionSideNav')).toBeInTheDocument(); + }); + + it('should pass item props to the SolutionSideNav component', () => { + render(, { wrapper: KibanaServicesProvider }); + + expect(mockSolutionSideNav).toHaveBeenCalledWith( + expect.objectContaining({ + items: sideNavItems, + }) + ); + }); + + it('should pass footerItems props to the SolutionSideNav component', () => { + mockUseSideNavItems.mockReturnValueOnce(sideNavFooterItems); + render(, { wrapper: KibanaServicesProvider }); + + expect(mockSolutionSideNav).toHaveBeenCalledWith( + expect.objectContaining({ + footerItems: sideNavFooterItems, + }) + ); + }); + + it('should selectedId the SolutionSideNav component', () => { + render(, { wrapper: KibanaServicesProvider }); + + expect(mockSolutionSideNav).toHaveBeenCalledWith( + expect.objectContaining({ + selectedId: SecurityPageName.alerts, + }) + ); + }); +}); diff --git a/x-pack/plugins/serverless_security/public/components/side_navigation/side_navigation.tsx b/x-pack/plugins/serverless_security/public/components/side_navigation/side_navigation.tsx new file mode 100644 index 00000000000000..1f7ca7821d2916 --- /dev/null +++ b/x-pack/plugins/serverless_security/public/components/side_navigation/side_navigation.tsx @@ -0,0 +1,44 @@ +/* + * 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 { EuiLoadingSpinner, useEuiTheme } from '@elastic/eui'; +import { SolutionNav } from '@kbn/shared-ux-page-solution-nav'; +import { SolutionSideNav } from '@kbn/security-solution-side-nav'; +import { + usePartitionFooterNavItems, + useSideNavItems, + useSideNavSelectedId, +} from '../../hooks/use_side_nav_items'; + +export const SecuritySideNavigation: React.FC = () => { + const { euiTheme } = useEuiTheme(); + const sideNavItems = useSideNavItems(); + const selectedId = useSideNavSelectedId(sideNavItems); + const [items, footerItems] = usePartitionFooterNavItems(sideNavItems); + + const isLoading = items.length === 0 && footerItems.length === 0; + + return isLoading ? ( + + ) : ( + + } + closeFlyoutButtonPosition={'inside'} + /> + ); +}; diff --git a/x-pack/plugins/serverless_security/public/hooks/__mocks__/use_link_props.ts b/x-pack/plugins/serverless_security/public/hooks/__mocks__/use_link_props.ts new file mode 100644 index 00000000000000..4941619cf3f99f --- /dev/null +++ b/x-pack/plugins/serverless_security/public/hooks/__mocks__/use_link_props.ts @@ -0,0 +1,16 @@ +/* + * 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 { GetLinkProps } from '../use_link_props'; + +export const getLinkProps = jest.fn(() => ({ + href: '/test-href', + onClick: jest.fn(), +})); + +export const useLinkProps: GetLinkProps = getLinkProps; +export const useGetLinkProps: () => GetLinkProps = jest.fn(() => getLinkProps); diff --git a/x-pack/plugins/serverless_security/public/hooks/__mocks__/use_side_nav_items.ts b/x-pack/plugins/serverless_security/public/hooks/__mocks__/use_side_nav_items.ts new file mode 100644 index 00000000000000..a01612190d8f90 --- /dev/null +++ b/x-pack/plugins/serverless_security/public/hooks/__mocks__/use_side_nav_items.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 { SolutionSideNavItem } from '@kbn/security-solution-side-nav'; + +const { usePartitionFooterNavItems: originalUsePartitionFooterNavItems } = + jest.requireActual('../use_side_nav_items'); + +export const useSideNavItems = jest.fn((): SolutionSideNavItem[] => []); + +export const usePartitionFooterNavItems = jest.fn( + (sideNavItems: SolutionSideNavItem[]): [SolutionSideNavItem[], SolutionSideNavItem[]] => + // Same implementation as original for convenience. Can be overridden in tests if needed + originalUsePartitionFooterNavItems(sideNavItems) +); + +export const useSideNavSelectedId = jest.fn((_sideNavItems: SolutionSideNavItem[]): string => ''); diff --git a/x-pack/plugins/serverless_security/public/hooks/jest.config.js b/x-pack/plugins/serverless_security/public/hooks/jest.config.js new file mode 100644 index 00000000000000..e384b19ed580b2 --- /dev/null +++ b/x-pack/plugins/serverless_security/public/hooks/jest.config.js @@ -0,0 +1,28 @@ +/* + * 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/serverless_security/public/hooks'], + testMatch: [ + '/x-pack/plugins/serverless_security/public/hooks/**/*.test.{js,mjs,ts,tsx}', + ], + coverageDirectory: + '/target/kibana-coverage/jest/x-pack/plugins/serverless_security/public/hooks', + coverageReporters: ['text', 'html'], + collectCoverageFrom: [ + '/x-pack/plugins/serverless_security/public/hooks/**/*.{ts,tsx}', + '!/x-pack/plugins/serverless_security/public/hooks/*.test.{ts,tsx}', + '!/x-pack/plugins/serverless_security/public/hooks/{__test__,__snapshots__,__examples__,*mock*,tests,test_helpers,integration_tests,types}/**/*', + '!/x-pack/plugins/serverless_security/public/hooks/*mock*.{ts,tsx}', + '!/x-pack/plugins/serverless_security/public/hooks/*.test.{ts,tsx}', + '!/x-pack/plugins/serverless_security/public/hooks/*.d.ts', + '!/x-pack/plugins/serverless_security/public/hooks/*.config.ts', + '!/x-pack/plugins/serverless_security/public/hooks/index.{js,ts,tsx}', + ], +}; diff --git a/x-pack/plugins/serverless_security/public/hooks/use_link_props.test.tsx b/x-pack/plugins/serverless_security/public/hooks/use_link_props.test.tsx new file mode 100644 index 00000000000000..b04f19be75b8ff --- /dev/null +++ b/x-pack/plugins/serverless_security/public/hooks/use_link_props.test.tsx @@ -0,0 +1,156 @@ +/* + * 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 { MouseEvent } from 'react'; +import { renderHook } from '@testing-library/react-hooks'; +import { APP_UI_ID, SecurityPageName } from '@kbn/security-solution-plugin/common'; +import { KibanaServicesProvider, servicesMocks } from '../services.mock'; +import { useGetLinkProps, useLinkProps } from './use_link_props'; + +const { getUrlForApp: mockGetUrlForApp, navigateToUrl: mockNavigateToUrl } = + servicesMocks.application; + +const href = '/app/security/test'; +mockGetUrlForApp.mockReturnValue(href); + +describe('useLinkProps', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should return link props', async () => { + const { result } = renderHook(useLinkProps, { wrapper: KibanaServicesProvider }); + + const linkProps = result.current; + + expect(linkProps).toEqual({ href, onClick: expect.any(Function) }); + expect(mockGetUrlForApp).toHaveBeenCalledTimes(1); + expect(mockGetUrlForApp).toHaveBeenCalledWith(APP_UI_ID, { + deepLinkId: undefined, + path: undefined, + }); + }); + + it('should call navigate when clicked normally', async () => { + const ev = { preventDefault: jest.fn() } as unknown as MouseEvent; + const { result } = renderHook(useLinkProps, { wrapper: KibanaServicesProvider }); + + const { onClick } = result.current; + onClick(ev); + + expect(mockNavigateToUrl).toHaveBeenCalledTimes(1); + expect(mockNavigateToUrl).toHaveBeenCalledWith(href); + }); + + it('should not call navigate when clicked with modifiers', async () => { + const ev = { preventDefault: jest.fn(), ctrlKey: true } as unknown as MouseEvent; + const { result } = renderHook(useLinkProps, { wrapper: KibanaServicesProvider }); + + const { onClick } = result.current; + onClick(ev); + + expect(mockNavigateToUrl).not.toHaveBeenCalled(); + }); + + it('should return link props passing deepLink', async () => { + const { result } = renderHook(useLinkProps, { + wrapper: KibanaServicesProvider, + initialProps: { deepLinkId: SecurityPageName.alerts }, + }); + + const linkProps = result.current; + + expect(linkProps).toEqual({ href, onClick: expect.any(Function) }); + expect(mockGetUrlForApp).toHaveBeenCalledTimes(1); + expect(mockGetUrlForApp).toHaveBeenCalledWith(APP_UI_ID, { + deepLinkId: SecurityPageName.alerts, + path: undefined, + }); + }); + + it('should return link props passing deepLink and path', async () => { + const { result } = renderHook(useLinkProps, { + wrapper: KibanaServicesProvider, + initialProps: { deepLinkId: SecurityPageName.alerts, path: '/test' }, + }); + + const linkProps = result.current; + + expect(linkProps).toEqual({ href, onClick: expect.any(Function) }); + expect(mockGetUrlForApp).toHaveBeenCalledTimes(1); + expect(mockGetUrlForApp).toHaveBeenCalledWith(APP_UI_ID, { + deepLinkId: SecurityPageName.alerts, + path: '/test', + }); + }); +}); + +describe('useGetLinkProps', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should return link props', async () => { + const { result } = renderHook(useGetLinkProps, { wrapper: KibanaServicesProvider }); + + const linkProps = result.current({}); + + expect(linkProps).toEqual({ href, onClick: expect.any(Function) }); + expect(mockGetUrlForApp).toHaveBeenCalledTimes(1); + expect(mockGetUrlForApp).toHaveBeenCalledWith(APP_UI_ID, { + deepLinkId: undefined, + path: undefined, + }); + }); + + it('should call navigate when clicked normally', async () => { + const ev = { preventDefault: jest.fn() } as unknown as MouseEvent; + const { result } = renderHook(useGetLinkProps, { wrapper: KibanaServicesProvider }); + + const { onClick } = result.current({}); + onClick(ev); + + expect(mockNavigateToUrl).toHaveBeenCalledTimes(1); + expect(mockNavigateToUrl).toHaveBeenCalledWith(href); + }); + + it('should not call navigate when clicked with modifiers', async () => { + const ev = { preventDefault: jest.fn(), ctrlKey: true } as unknown as MouseEvent; + const { result } = renderHook(useGetLinkProps, { wrapper: KibanaServicesProvider }); + + const { onClick } = result.current({}); + onClick(ev); + + expect(mockNavigateToUrl).not.toHaveBeenCalled(); + }); + + it('should return link props passing deepLink', async () => { + const { result } = renderHook(useGetLinkProps, { wrapper: KibanaServicesProvider }); + + const linkProps = result.current({ deepLinkId: SecurityPageName.alerts }); + + expect(linkProps).toEqual({ href, onClick: expect.any(Function) }); + expect(mockGetUrlForApp).toHaveBeenCalledTimes(1); + expect(mockGetUrlForApp).toHaveBeenCalledWith(APP_UI_ID, { + deepLinkId: SecurityPageName.alerts, + path: undefined, + }); + }); + + it('should return link props passing deepLink and path', async () => { + const { result } = renderHook(useGetLinkProps, { wrapper: KibanaServicesProvider }); + + const linkProps = result.current({ deepLinkId: SecurityPageName.alerts, path: '/test' }); + + expect(linkProps).toEqual({ href, onClick: expect.any(Function) }); + expect(mockGetUrlForApp).toHaveBeenCalledTimes(1); + expect(mockGetUrlForApp).toHaveBeenCalledWith(APP_UI_ID, { + deepLinkId: SecurityPageName.alerts, + path: '/test', + }); + }); +}); diff --git a/x-pack/plugins/serverless_security/public/hooks/use_link_props.ts b/x-pack/plugins/serverless_security/public/hooks/use_link_props.ts new file mode 100644 index 00000000000000..3a1989dbdc79a0 --- /dev/null +++ b/x-pack/plugins/serverless_security/public/hooks/use_link_props.ts @@ -0,0 +1,65 @@ +/* + * 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 { APP_UI_ID, type SecurityPageName } from '@kbn/security-solution-plugin/common'; +import { useMemo, useCallback, type MouseEventHandler, type MouseEvent } from 'react'; +import { useKibana, type Services } from '../services'; + +interface LinkProps { + onClick: MouseEventHandler; + href: string; +} + +interface GetLinkPropsParams { + deepLinkId?: SecurityPageName; + path?: string; + appId?: string; + onClick?: MouseEventHandler; +} + +export type GetLinkProps = (params: GetLinkPropsParams) => LinkProps; + +export const useLinkProps: GetLinkProps = (props) => { + const { application } = useKibana().services; + return useMemo(() => getLinkProps({ ...props, application }), [application, props]); +}; + +export const useGetLinkProps: () => GetLinkProps = () => { + const { application } = useKibana().services; + return useCallback( + (props) => getLinkProps({ ...props, application }), + [application] + ); +}; + +const getLinkProps = ({ + deepLinkId, + path, + onClick: onClickProps, + appId = APP_UI_ID, + application, +}: GetLinkPropsParams & { application: Services['application'] }): LinkProps => { + const { getUrlForApp, navigateToUrl } = application; + const url = getUrlForApp(appId, { deepLinkId, path }); + return { + href: url, + onClick: (ev) => { + if (isModifiedEvent(ev)) { + return; + } + + ev.preventDefault(); + navigateToUrl(url); + if (onClickProps) { + onClickProps(ev); + } + }, + }; +}; + +const isModifiedEvent = (event: MouseEvent) => + event.metaKey || event.altKey || event.ctrlKey || event.shiftKey; diff --git a/x-pack/plugins/serverless_security/public/hooks/use_nav_links.ts b/x-pack/plugins/serverless_security/public/hooks/use_nav_links.ts new file mode 100644 index 00000000000000..7d2b16f6cc6e84 --- /dev/null +++ b/x-pack/plugins/serverless_security/public/hooks/use_nav_links.ts @@ -0,0 +1,17 @@ +/* + * 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 { useMemo } from 'react'; +import useObservable from 'react-use/lib/useObservable'; +import { useKibana } from '../services'; + +export const useNavLinks = () => { + const { securitySolution } = useKibana().services; + const { getNavLinks$ } = securitySolution; + const navLinks$ = useMemo(() => getNavLinks$(), [getNavLinks$]); + return useObservable(navLinks$, []); +}; diff --git a/x-pack/plugins/serverless_security/public/hooks/use_side_nav_items.test.tsx b/x-pack/plugins/serverless_security/public/hooks/use_side_nav_items.test.tsx new file mode 100644 index 00000000000000..064b2bafd82a4a --- /dev/null +++ b/x-pack/plugins/serverless_security/public/hooks/use_side_nav_items.test.tsx @@ -0,0 +1,302 @@ +/* + * 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 { renderHook } from '@testing-library/react-hooks'; +import { + usePartitionFooterNavItems, + useSideNavItems, + useSideNavSelectedId, +} from './use_side_nav_items'; +import { BehaviorSubject } from 'rxjs'; +import type { NavigationLink } from '@kbn/security-solution-plugin/public/common/links/types'; +import { SecurityPageName } from '@kbn/security-solution-plugin/common'; +import { KibanaServicesProvider, servicesMocks } from '../services.mock'; + +jest.mock('./use_link_props'); + +const mockNavLinks = jest.fn((): NavigationLink[] => []); +servicesMocks.securitySolution.getNavLinks$.mockImplementation( + () => new BehaviorSubject(mockNavLinks()) +); + +const mockUseLocation = jest.fn(() => ({ pathname: '/' })); +jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), + useLocation: () => mockUseLocation(), +})); + +describe('useSideNavItems', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should return empty items', async () => { + const { result } = renderHook(useSideNavItems, { wrapper: KibanaServicesProvider }); + + const items = result.current; + + expect(items).toEqual([]); + expect(servicesMocks.securitySolution.getNavLinks$).toHaveBeenCalledTimes(1); + }); + + it('should return main items', async () => { + mockNavLinks.mockReturnValueOnce([ + { id: SecurityPageName.alerts, title: 'Alerts' }, + { id: SecurityPageName.case, title: 'Cases' }, + ]); + const { result } = renderHook(useSideNavItems, { wrapper: KibanaServicesProvider }); + + const items = result.current; + expect(items).toEqual([ + { + id: SecurityPageName.alerts, + label: 'Alerts', + href: expect.any(String), + onClick: expect.any(Function), + }, + { + id: SecurityPageName.case, + label: 'Cases', + href: expect.any(String), + onClick: expect.any(Function), + }, + ]); + }); + + it('should return secondary items', async () => { + mockNavLinks.mockReturnValueOnce([ + { + id: SecurityPageName.dashboards, + title: 'Dashboards', + links: [{ id: SecurityPageName.detectionAndResponse, title: 'Detection & Response' }], + }, + ]); + const { result } = renderHook(useSideNavItems, { wrapper: KibanaServicesProvider }); + + const items = result.current; + expect(items).toEqual([ + { + id: SecurityPageName.dashboards, + label: 'Dashboards', + href: expect.any(String), + onClick: expect.any(Function), + items: [ + { + id: SecurityPageName.detectionAndResponse, + label: 'Detection & Response', + href: expect.any(String), + onClick: expect.any(Function), + }, + ], + }, + ]); + }); + + it('should return get started link', async () => { + mockNavLinks.mockReturnValueOnce([ + { + id: SecurityPageName.landing, + title: 'Get Started', + }, + ]); + const { result } = renderHook(useSideNavItems, { wrapper: KibanaServicesProvider }); + + const items = result.current; + + expect(items).toEqual([ + { + id: SecurityPageName.landing, + label: 'GET STARTED', + href: expect.any(String), + onClick: expect.any(Function), + labelSize: 'xs', + iconType: 'launch', + appendSeparator: true, + }, + ]); + }); +}); + +describe('usePartitionFooterNavItems', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should partition main items only', async () => { + const mainInputItems = [ + { + id: SecurityPageName.dashboards, + label: 'Dashboards', + href: '', + onClick: jest.fn(), + }, + { + id: SecurityPageName.alerts, + label: 'Alerts', + href: '', + onClick: jest.fn(), + }, + ]; + const { result } = renderHook(usePartitionFooterNavItems, { + initialProps: mainInputItems, + }); + + const [items, footerItems] = result.current; + + expect(items).toEqual(mainInputItems); + expect(footerItems).toEqual([]); + }); + + it('should partition footer items only', async () => { + const footerInputItems = [ + { + id: SecurityPageName.landing, + label: 'GET STARTED', + href: '', + onClick: jest.fn(), + }, + { + id: SecurityPageName.administration, + label: 'Manage', + href: '', + onClick: jest.fn(), + }, + ]; + const { result } = renderHook(usePartitionFooterNavItems, { + initialProps: footerInputItems, + }); + + const [items, footerItems] = result.current; + + expect(items).toEqual([]); + expect(footerItems).toEqual(footerInputItems); + }); + + it('should partition main and footer items', async () => { + const mainInputItems = [ + { + id: SecurityPageName.dashboards, + label: 'Dashboards', + href: '', + onClick: jest.fn(), + }, + { + id: SecurityPageName.alerts, + label: 'Alerts', + href: '', + onClick: jest.fn(), + }, + ]; + const footerInputItems = [ + { + id: SecurityPageName.landing, + label: 'GET STARTED', + href: '', + onClick: jest.fn(), + }, + { + id: SecurityPageName.administration, + label: 'Manage', + href: '', + onClick: jest.fn(), + }, + ]; + const { result } = renderHook(usePartitionFooterNavItems, { + initialProps: [...mainInputItems, ...footerInputItems], + }); + + const [items, footerItems] = result.current; + + expect(items).toEqual(mainInputItems); + expect(footerItems).toEqual(footerInputItems); + }); +}); + +describe('useSideNavSelectedId', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should return empty string when no item selected', async () => { + const items = [ + { + id: SecurityPageName.dashboards, + label: 'Dashboards', + href: '/app/security/dashboards', + onClick: jest.fn(), + }, + { + id: SecurityPageName.alerts, + label: 'Alerts', + href: '/app/security/alerts', + onClick: jest.fn(), + }, + ]; + + const { result } = renderHook(useSideNavSelectedId, { + wrapper: KibanaServicesProvider, + initialProps: items, + }); + + const selectedId = result.current; + expect(selectedId).toEqual(''); + }); + + it('should return the item with path selected', async () => { + mockUseLocation.mockReturnValueOnce({ pathname: '/app/security/alerts' }); + const items = [ + { + id: SecurityPageName.dashboards, + label: 'Dashboards', + href: '/app/security/dashboards', + onClick: jest.fn(), + }, + { + id: SecurityPageName.alerts, + label: 'Alerts', + href: '/app/security/alerts', + onClick: jest.fn(), + }, + ]; + + const { result } = renderHook(useSideNavSelectedId, { + wrapper: KibanaServicesProvider, + initialProps: items, + }); + + const selectedId = result.current; + expect(selectedId).toEqual(SecurityPageName.alerts); + }); + + it('should return the main item when nested path selected', async () => { + mockUseLocation.mockReturnValueOnce({ pathname: '/app/security/detection_response' }); + const items = [ + { + id: SecurityPageName.dashboards, + label: 'Dashboards', + href: '/app/security/dashboards', + onClick: jest.fn(), + items: [ + { + id: SecurityPageName.detectionAndResponse, + label: 'Detection & Response', + href: '/app/security/detection_response', + onClick: jest.fn(), + }, + ], + }, + ]; + + const { result } = renderHook(useSideNavSelectedId, { + wrapper: KibanaServicesProvider, + initialProps: items, + }); + + const selectedId = result.current; + expect(selectedId).toEqual(SecurityPageName.dashboards); + }); +}); diff --git a/x-pack/plugins/serverless_security/public/hooks/use_side_nav_items.ts b/x-pack/plugins/serverless_security/public/hooks/use_side_nav_items.ts new file mode 100644 index 00000000000000..3f6069be444e0d --- /dev/null +++ b/x-pack/plugins/serverless_security/public/hooks/use_side_nav_items.ts @@ -0,0 +1,143 @@ +/* + * 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 { useMemo } from 'react'; +import { matchPath, useLocation } from 'react-router-dom'; +import { partition } from 'lodash/fp'; +import { SecurityPageName } from '@kbn/security-solution-plugin/common'; +import type { SolutionSideNavItem } from '@kbn/security-solution-side-nav'; +import { useKibana } from '../services'; +import { useGetLinkProps } from './use_link_props'; +import { useNavLinks } from './use_nav_links'; + +const isFooterNavItem = (id: string) => + id === SecurityPageName.landing || id === SecurityPageName.administration; + +const isGetStartedNavItem = (id: string) => id === SecurityPageName.landing; + +// DFS for the sideNavItem matching the current `pathname`, returns all item hierarchy when found +const findItemsByPath = ( + sideNavItems: SolutionSideNavItem[], + pathname: string +): SolutionSideNavItem[] => { + for (const sideNavItem of sideNavItems) { + if (sideNavItem.items?.length) { + const found = findItemsByPath(sideNavItem.items, pathname); + if (found.length) { + found.unshift(sideNavItem); + return found; + } + } + if (matchPath(pathname, { path: sideNavItem.href })) { + return [sideNavItem]; + } + } + return []; +}; + +/** + * Returns all the formatted SideNavItems, including external links + */ +export const useSideNavItems = (): SolutionSideNavItem[] => { + const navLinks = useNavLinks(); + const getLinkProps = useGetLinkProps(); + + const securitySideNavItems = useMemo( + () => + navLinks.reduce((items, navLink) => { + if (navLink.disabled) { + return items; + } + if (isGetStartedNavItem(navLink.id)) { + items.push({ + id: navLink.id, + label: navLink.title.toUpperCase(), + ...getLinkProps({ deepLinkId: navLink.id }), + labelSize: 'xs', + iconType: 'launch', + appendSeparator: true, + }); + } else { + // default sideNavItem formatting + items.push({ + id: navLink.id, + label: navLink.title, + ...getLinkProps({ deepLinkId: navLink.id }), + ...(navLink.categories?.length && { categories: navLink.categories }), + ...(navLink.links?.length && { + items: navLink.links.reduce((acc, current) => { + if (!current.disabled) { + acc.push({ + id: current.id, + label: current.title, + description: current.description, + isBeta: current.isBeta, + betaOptions: current.betaOptions, + ...getLinkProps({ deepLinkId: current.id }), + }); + } + return acc; + }, []), + }), + }); + } + return items; + }, []), + [getLinkProps, navLinks] + ); + + const sideNavItems = useAddExternalSideNavItems(securitySideNavItems); + + return sideNavItems; +}; + +/** + * @param securitySideNavItems the sideNavItems for Security pages + * @returns sideNavItems with Security and external links + */ +const useAddExternalSideNavItems = (securitySideNavItems: SolutionSideNavItem[]) => { + const sideNavItemsWithExternals = useMemo( + () => [ + ...securitySideNavItems, + // TODO: add external links. e.g.: + // { + // id: 'ml', + // label: 'Machine Learning Jobs', + // ...getLinkProps({ appId: 'ml', path: '/jobs' }), + // links: [...] + // }, + ], + [securitySideNavItems] + ); + + return sideNavItemsWithExternals; +}; + +/** + * Partitions the sideNavItems into main and footer SideNavItems + * @param sideNavItems array for all SideNavItems + * @returns `[items, footerItems]` to be used in the side navigation component + */ +export const usePartitionFooterNavItems = ( + sideNavItems: SolutionSideNavItem[] +): [SolutionSideNavItem[], SolutionSideNavItem[]] => + useMemo(() => partition((item) => !isFooterNavItem(item.id), sideNavItems), [sideNavItems]); + +/** + * Returns the selected item id, which is the root item in the links hierarchy + */ +export const useSideNavSelectedId = (sideNavItems: SolutionSideNavItem[]): string => { + const { http } = useKibana().services; + const { pathname } = useLocation(); + + const selectedId: string = useMemo(() => { + const [rootNavItem] = findItemsByPath(sideNavItems, http.basePath.prepend(pathname)); + return rootNavItem?.id ?? ''; + }, [sideNavItems, pathname, http]); + + return selectedId; +}; diff --git a/x-pack/plugins/serverless_security/public/jest.config.js b/x-pack/plugins/serverless_security/public/jest.config.js new file mode 100644 index 00000000000000..8cbea4b0a4e671 --- /dev/null +++ b/x-pack/plugins/serverless_security/public/jest.config.js @@ -0,0 +1,27 @@ +/* + * 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: '../../../..', + /** all nested directories have their own Jest config file */ + testMatch: ['/x-pack/plugins/serverless_security/public/*.test.{js,mjs,ts,tsx}'], + roots: ['/x-pack/plugins/serverless_security/public'], + coverageDirectory: + '/target/kibana-coverage/jest/x-pack/plugins/serverless_security/public', + coverageReporters: ['text', 'html'], + collectCoverageFrom: [ + '/x-pack/plugins/serverless_security/public/**/*.{ts,tsx}', + '!/x-pack/plugins/serverless_security/public/*.test.{ts,tsx}', + '!/x-pack/plugins/serverless_security/public/{__test__,__snapshots__,__examples__,*mock*,tests,test_helpers,integration_tests,types}/**/*', + '!/x-pack/plugins/serverless_security/public/*mock*.{ts,tsx}', + '!/x-pack/plugins/serverless_security/public/*.test.{ts,tsx}', + '!/x-pack/plugins/serverless_security/public/*.d.ts', + '!/x-pack/plugins/serverless_security/public/*.config.ts', + '!/x-pack/plugins/serverless_security/public/index.{js,ts,tsx}', + ], +}; diff --git a/x-pack/plugins/serverless_security/public/plugin.ts b/x-pack/plugins/serverless_security/public/plugin.ts index 4e7fa19a0945b3..bc867cbcc9a04e 100644 --- a/x-pack/plugins/serverless_security/public/plugin.ts +++ b/x-pack/plugins/serverless_security/public/plugin.ts @@ -6,6 +6,7 @@ */ import { CoreSetup, CoreStart, Plugin } from '@kbn/core/public'; +import { getSecuritySideNavComponent } from './components/side_navigation'; import { ServerlessSecurityPluginSetup, ServerlessSecurityPluginStart, @@ -30,10 +31,14 @@ export class ServerlessSecurityPlugin } public start( - _core: CoreStart, - { securitySolution }: ServerlessSecurityPluginStartDependencies + core: CoreStart, + startDeps: ServerlessSecurityPluginStartDependencies ): ServerlessSecurityPluginStart { + const { securitySolution, serverless } = startDeps; + securitySolution.setIsSidebarEnabled(false); + serverless.setSideNavComponent(getSecuritySideNavComponent(core, startDeps)); + return {}; } diff --git a/x-pack/plugins/serverless_security/public/services.mock.tsx b/x-pack/plugins/serverless_security/public/services.mock.tsx new file mode 100644 index 00000000000000..142ebb2152e632 --- /dev/null +++ b/x-pack/plugins/serverless_security/public/services.mock.tsx @@ -0,0 +1,26 @@ +/* + * 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 { I18nProvider } from '@kbn/i18n-react'; +import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; +import { coreMock } from '@kbn/core/public/mocks'; +import { serverlessMock } from '@kbn/serverless/public/mocks'; +import { securityMock } from '@kbn/security-plugin/public/mocks'; +import { securitySolutionMock } from '@kbn/security-solution-plugin/public/mocks'; + +export const servicesMocks = { + ...coreMock.createStart(), + serverless: serverlessMock.createStart(), + security: securityMock.createStart(), + securitySolution: securitySolutionMock.createStart(), +}; + +export const KibanaServicesProvider = React.memo(({ children }) => ( + + {children} + +)); diff --git a/x-pack/plugins/serverless_security/public/services.tsx b/x-pack/plugins/serverless_security/public/services.tsx new file mode 100644 index 00000000000000..3fb7ffbda18cfe --- /dev/null +++ b/x-pack/plugins/serverless_security/public/services.tsx @@ -0,0 +1,26 @@ +/* + * 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 { CoreStart } from '@kbn/core/public'; +import React from 'react'; +import { + KibanaContextProvider, + useKibana as useKibanaReact, +} from '@kbn/kibana-react-plugin/public'; +import type { ServerlessSecurityPluginStartDependencies } from './types'; + +export type Services = CoreStart & ServerlessSecurityPluginStartDependencies; + +export const KibanaServicesProvider: React.FC<{ + core: CoreStart; + pluginsStart: ServerlessSecurityPluginStartDependencies; +}> = ({ core, pluginsStart, children }) => { + const services: Services = { ...core, ...pluginsStart }; + return {children}; +}; + +export const useKibana = () => useKibanaReact(); diff --git a/x-pack/plugins/serverless_security/tsconfig.json b/x-pack/plugins/serverless_security/tsconfig.json index ddf77b288e6287..75252c18a0cfa3 100644 --- a/x-pack/plugins/serverless_security/tsconfig.json +++ b/x-pack/plugins/serverless_security/tsconfig.json @@ -20,5 +20,10 @@ "@kbn/security-plugin", "@kbn/security-solution-plugin", "@kbn/serverless", + "@kbn/shared-ux-page-solution-nav", + "@kbn/security-solution-side-nav", + "@kbn/kibana-react-plugin", + "@kbn/core-chrome-browser", + "@kbn/i18n-react", ] }