Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Security Solution][Serverless] Left navigation #156600

Merged
merged 16 commits into from
May 7, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is OK for now, but later we will need to split code ownership by the components, subfolders etc. to reduce PR noise across Security Solution teams.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yup, It makes sense, let's define what team owns what sub-directories, and then I'll update it

semd marked this conversation as resolved.
Show resolved Hide resolved
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) =>
sebelga marked this conversation as resolved.
Show resolved Hide resolved
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