Skip to content

Commit

Permalink
[Security Solution][Serverless] Left navigation (elastic#156600)
Browse files Browse the repository at this point in the history
## Summary

closes: elastic#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>
  • Loading branch information
semd and kibanamachine committed May 7, 2023
1 parent 1abd32c commit c4d233d
Show file tree
Hide file tree
Showing 27 changed files with 1,103 additions and 9 deletions.
3 changes: 2 additions & 1 deletion .github/CODEOWNERS
Validating CODEOWNERS rules …
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down
23 changes: 23 additions & 0 deletions x-pack/plugins/security_solution/public/mocks.ts
Original file line number Diff line number Diff line change
@@ -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<NavigationLink[]>([])),
setIsSidebarEnabled: jest.fn(),
});

export const securitySolutionMock = {
createSetup: setupMock,
createStart: startMock,
};
2 changes: 1 addition & 1 deletion x-pack/plugins/security_solution/public/plugin.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -310,7 +310,7 @@ export class Plugin implements IPlugin<PluginSetup, PluginStart, SetupPlugins, S
this.registerAppLinks(core, plugins);

return {
navLinks$,
getNavLinks$: () => navLinks$,
setIsSidebarEnabled: (isSidebarEnabled: boolean) =>
this.isSidebarEnabled$.next(isSidebarEnabled),
};
Expand Down
2 changes: 1 addition & 1 deletion x-pack/plugins/security_solution/public/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ export interface PluginSetup {
}

export interface PluginStart {
navLinks$: Observable<NavigationLink[]>;
getNavLinks$: () => Observable<NavigationLink[]>;
setIsSidebarEnabled: (isSidebarEnabled: boolean) => void;
}

Expand Down
16 changes: 16 additions & 0 deletions x-pack/plugins/serverless/public/mocks.ts
Original file line number Diff line number Diff line change
@@ -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,
};
5 changes: 4 additions & 1 deletion x-pack/plugins/serverless/public/plugin.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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() {}
Expand Down
6 changes: 4 additions & 2 deletions x-pack/plugins/serverless/public/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
1 change: 1 addition & 0 deletions x-pack/plugins/serverless/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,6 @@
"@kbn/serverless-project-switcher",
"@kbn/serverless-types",
"@kbn/utils",
"@kbn/core-chrome-browser",
]
}
12 changes: 12 additions & 0 deletions x-pack/plugins/serverless_security/jest.config.dev.js
Original file line number Diff line number Diff line change
@@ -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: ['<rootDir>/x-pack/plugins/serverless_security/public/*/jest.config.js'],
};
2 changes: 1 addition & 1 deletion x-pack/plugins/serverless_security/kibana.jsonc
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
Original file line number Diff line number Diff line change
@@ -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: ['<rootDir>/x-pack/plugins/serverless_security/public/components'],
testMatch: [
'<rootDir>/x-pack/plugins/serverless_security/public/components/**/*.test.{js,mjs,ts,tsx}',
],
coverageDirectory:
'<rootDir>/target/kibana-coverage/jest/x-pack/plugins/serverless_security/public/components',
coverageReporters: ['text', 'html'],
collectCoverageFrom: [
'<rootDir>/x-pack/plugins/serverless_security/public/components/**/*.{ts,tsx}',
'!<rootDir>/x-pack/plugins/serverless_security/public/components/*.test.{ts,tsx}',
'!<rootDir>/x-pack/plugins/serverless_security/public/components/{__test__,__snapshots__,__examples__,*mock*,tests,test_helpers,integration_tests,types}/**/*',
'!<rootDir>/x-pack/plugins/serverless_security/public/components/*mock*.{ts,tsx}',
'!<rootDir>/x-pack/plugins/serverless_security/public/components/*.test.{ts,tsx}',
'!<rootDir>/x-pack/plugins/serverless_security/public/components/*.d.ts',
'!<rootDir>/x-pack/plugins/serverless_security/public/components/*.config.ts',
'!<rootDir>/x-pack/plugins/serverless_security/public/components/index.{js,ts,tsx}',
],
};
Original file line number Diff line number Diff line change
@@ -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) => (
<KibanaServicesProvider core={core} pluginsStart={pluginsStart}>
<SecuritySideNavigation />
</KibanaServicesProvider>
);
};
Original file line number Diff line number Diff line change
@@ -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) => <div data-test-subj="solutionSideNav" />);
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(<SecuritySideNavigation />, { wrapper: KibanaServicesProvider });
expect(component.queryByTestId('sideNavLoader')).toBeInTheDocument();
});

it('should not render loading when items received', () => {
const component = render(<SecuritySideNavigation />, { wrapper: KibanaServicesProvider });
expect(component.queryByTestId('sideNavLoader')).not.toBeInTheDocument();
});

it('should render the SideNav when items received', () => {
const component = render(<SecuritySideNavigation />, { wrapper: KibanaServicesProvider });
expect(component.queryByTestId('solutionSideNav')).toBeInTheDocument();
});

it('should pass item props to the SolutionSideNav component', () => {
render(<SecuritySideNavigation />, { wrapper: KibanaServicesProvider });

expect(mockSolutionSideNav).toHaveBeenCalledWith(
expect.objectContaining({
items: sideNavItems,
})
);
});

it('should pass footerItems props to the SolutionSideNav component', () => {
mockUseSideNavItems.mockReturnValueOnce(sideNavFooterItems);
render(<SecuritySideNavigation />, { wrapper: KibanaServicesProvider });

expect(mockSolutionSideNav).toHaveBeenCalledWith(
expect.objectContaining({
footerItems: sideNavFooterItems,
})
);
});

it('should selectedId the SolutionSideNav component', () => {
render(<SecuritySideNavigation />, { wrapper: KibanaServicesProvider });

expect(mockSolutionSideNav).toHaveBeenCalledWith(
expect.objectContaining({
selectedId: SecurityPageName.alerts,
})
);
});
});
Original file line number Diff line number Diff line change
@@ -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 ? (
<EuiLoadingSpinner size="m" data-test-subj="sideNavLoader" />
) : (
<SolutionNav
canBeCollapsed={false}
name={'Security'}
icon={'logoSecurity'}
children={
<SolutionSideNav
items={items}
footerItems={footerItems}
selectedId={selectedId}
panelTopOffset={`calc(${euiTheme.size.l} * 2)`}
/>
}
closeFlyoutButtonPosition={'inside'}
/>
);
};
Original file line number Diff line number Diff line change
@@ -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);
Original file line number Diff line number Diff line change
@@ -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 => '');
Loading

0 comments on commit c4d233d

Please sign in to comment.