Skip to content

Commit

Permalink
[Security] New side navigation (#131437)
Browse files Browse the repository at this point in the history
* initial implementation

* feature flag and setting added

* Simplify mobile view to use EuiCollapsibleNavGroup

* PR suggestions

* Fix mobile title

* [CI] Auto-commit changed files from 'node scripts/eslint --no-cache --fix'

* style with theme vars, tests

* more theming

* remove emoji

* test fixes

* fix users page cypress test

* snapshot updated

* new nav tests

* test fixes

* refactor css to styled components

* remove duplicated deeplink

* nav panel test added and conflig cleaning

Co-authored-by: cchaos <caroline.horn@elastic.co>
Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
  • Loading branch information
3 people committed May 12, 2022
1 parent 11ae98d commit e267d2e
Show file tree
Hide file tree
Showing 35 changed files with 1,596 additions and 453 deletions.

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ $euiSideNavEmphasizedBackgroundColor: transparentize($euiColorLightShade, .7);
padding: $euiSizeL;
}

.kbnPageTemplateSolutionNavAvatar {
.kbnPageTemplateSolutionNav__avatar {
margin-right: $euiSize;
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,4 +69,12 @@ PureComponent.argTypes = {
options: ['logoKibana', 'logoObservability', 'logoSecurity'],
defaultValue: 'logoKibana',
},
children: {
control: 'text',
defaultValue: '',
},
};

PureComponent.parameters = {
layout: 'fullscreen',
};
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,15 @@ import React from 'react';
import { shallow } from 'enzyme';
import { KibanaPageTemplateSolutionNav, KibanaPageTemplateSolutionNavProps } from './solution_nav';

jest.mock('@elastic/eui', () => ({
useIsWithinBreakpoints: (args: string[]) => {
return args[0] === 'xs';
},
EuiSideNav: function Component() {
// no-op
},
}));
jest.mock('@elastic/eui', () => {
const original = jest.requireActual('@elastic/eui');
return {
...original,
useIsWithinBreakpoints: (args: string[]) => {
return args[0] === 'xs';
},
};
});

const items: KibanaPageTemplateSolutionNavProps['items'] = [
{
Expand Down Expand Up @@ -59,6 +60,19 @@ const items: KibanaPageTemplateSolutionNavProps['items'] = [
];

describe('KibanaPageTemplateSolutionNav', () => {
describe('heading', () => {
test('accepts more headingProps', () => {
const component = shallow(
<KibanaPageTemplateSolutionNav
name="Solution"
headingProps={{ id: 'testID', element: 'h3' }}
/>
);

expect(component).toMatchSnapshot();
});
});

test('renders', () => {
const component = shallow(<KibanaPageTemplateSolutionNav name="Solution" items={items} />);
expect(component).toMatchSnapshot();
Expand All @@ -71,6 +85,15 @@ describe('KibanaPageTemplateSolutionNav', () => {
expect(component).toMatchSnapshot();
});

test('renders with children', () => {
const component = shallow(
<KibanaPageTemplateSolutionNav name="Solution" data-test-subj="DTS">
<span id="dummy_component" />
</KibanaPageTemplateSolutionNav>
);
expect(component.find('#dummy_component').length > 0).toBeTruthy();
});

test('accepts EuiSideNavProps', () => {
const component = shallow(
<KibanaPageTemplateSolutionNav name="Solution" data-test-subj="DTS" items={items} />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,23 +7,31 @@
*/
import './solution_nav.scss';

import React, { FunctionComponent, useState } from 'react';
import React, { FunctionComponent, useState, useMemo } from 'react';
import classNames from 'classnames';
import {
EuiAvatarProps,
EuiCollapsibleNavGroup,
EuiFlyout,
EuiFlyoutProps,
EuiSideNav,
EuiSideNavItemType,
EuiSideNavProps,
EuiSpacer,
EuiTitle,
htmlIdGenerator,
useIsWithinBreakpoints,
} from '@elastic/eui';

import { FormattedMessage } from '@kbn/i18n-react';
import { i18n } from '@kbn/i18n';
import { KibanaSolutionAvatar } from '@kbn/shared-ux-avatar-solution';

import { KibanaPageTemplateSolutionNavCollapseButton } from './solution_nav_collapse_button';

export type KibanaPageTemplateSolutionNavProps = EuiSideNavProps<{}> & {
export type KibanaPageTemplateSolutionNavProps = Omit<
EuiSideNavProps<{}>,
'children' | 'items' | 'heading'
> & {
/**
* Name of the solution, i.e. "Observability"
*/
Expand All @@ -32,6 +40,19 @@ export type KibanaPageTemplateSolutionNavProps = EuiSideNavProps<{}> & {
* Solution logo, i.e. "logoObservability"
*/
icon?: EuiAvatarProps['iconType'];
/**
* An array of #EuiSideNavItem objects. Lists navigation menu items.
*/
items?: EuiSideNavProps<{}>['items'];
/**
* Renders the children instead of default EuiSideNav
*/
children?: React.ReactNode;
/**
* The position of the close button when the navigation flyout is open.
* Note that side navigation turns into a flyout only when the screen has medium size.
*/
closeFlyoutButtonPosition?: EuiFlyoutProps['closeButtonPosition'];
/**
* Control the collapsed state
*/
Expand All @@ -50,13 +71,26 @@ const setTabIndex = (items: Array<EuiSideNavItemType<{}>>, isHidden: boolean) =>
});
};

const generateId = htmlIdGenerator('KibanaPageTemplateSolutionNav');

/**
* A wrapper around EuiSideNav but also creates the appropriate title with optional solution logo
*/
export const KibanaPageTemplateSolutionNav: FunctionComponent<
KibanaPageTemplateSolutionNavProps
> = ({ name, icon, items, isOpenOnDesktop = false, onCollapse, ...rest }) => {
const isSmallerBreakpoint = useIsWithinBreakpoints(['xs', 's']);
> = ({
children,
headingProps,
icon,
isOpenOnDesktop = false,
items,
mobileBreakpoints = ['xs', 's'],
closeFlyoutButtonPosition = 'outside',
name,
onCollapse,
...rest
}) => {
const isSmallerBreakpoint = useIsWithinBreakpoints(mobileBreakpoints);
const isMediumBreakpoint = useIsWithinBreakpoints(['m']);
const isLargerBreakpoint = useIsWithinBreakpoints(['l', 'xl']);

Expand All @@ -67,68 +101,81 @@ export const KibanaPageTemplateSolutionNav: FunctionComponent<
};

const isHidden = isLargerBreakpoint && !isOpenOnDesktop;
const isCustomSideNav = !!children;

/**
* Create the avatar
*/
const solutionAvatar = icon ? (
<KibanaSolutionAvatar
className="kbnPageTemplateSolutionNavAvatar"
iconType={icon}
name={name}
/>
) : null;
const sideNavClasses = classNames('kbnPageTemplateSolutionNav', {
'kbnPageTemplateSolutionNav--hidden': isHidden,
});

/**
* Create the titles
* Create the avatar and titles
*/
const headingID = headingProps?.id || generateId('heading');
const HeadingElement = headingProps?.element || 'h2';
const titleText = (
<>
{solutionAvatar}
<strong>{name}</strong>
</>
);
const mobileTitleText = (
<FormattedMessage
id="sharedUXComponents.solutionNav.mobileTitleText"
defaultMessage="{solutionName} Menu"
values={{ solutionName: name || 'Navigation' }}
/>
<EuiTitle size="xs" id={headingID}>
<HeadingElement>
{icon && (
<KibanaSolutionAvatar
className="kbnPageTemplateSolutionNav__avatar"
iconType={icon}
name={name}
/>
)}
<strong>
<FormattedMessage
id="sharedUXComponents.solutionNav.mobileTitleText"
defaultMessage="{solutionName} {menuText}"
values={{
solutionName: name || 'Navigation',
menuText: isSmallerBreakpoint
? i18n.translate('sharedUXComponents.solutionNav.menuText', {
defaultMessage: 'menu',
})
: '',
}}
/>
</strong>
</HeadingElement>
</EuiTitle>
);

/**
* Create the side nav component
* Create the side nav content
*/

const sideNav = () => {
const sideNavContent = useMemo(() => {
if (isCustomSideNav) {
return children;
}
if (!items) {
return null;
}
const sideNavClasses = classNames('kbnPageTemplateSolutionNav', {
'kbnPageTemplateSolutionNav--hidden': isHidden,
});
return (
<EuiSideNav
aria-labelledby={headingID}
aria-hidden={isHidden}
className={sideNavClasses}
heading={titleText}
mobileTitle={
<>
{solutionAvatar}
{mobileTitleText}
</>
}
toggleOpenOnMobile={toggleOpenOnMobile}
isOpenOnMobile={isSideNavOpenOnMobile}
items={setTabIndex(items, isHidden)}
mobileBreakpoints={[]} // prevent EuiSideNav to apply mobile version, already implemented here
{...rest}
/>
);
};
}, [children, headingID, isCustomSideNav, isHidden, items, rest]);

return (
<>
{isSmallerBreakpoint && sideNav()}
{isSmallerBreakpoint && (
<EuiCollapsibleNavGroup
className={sideNavClasses}
paddingSize="m"
background="none"
title={titleText}
titleElement="span"
isCollapsible={true}
initialIsOpen={false}
>
{sideNavContent}
</EuiCollapsibleNavGroup>
)}
{isMediumBreakpoint && (
<>
{isSideNavOpenOnMobile && (
Expand All @@ -138,10 +185,14 @@ export const KibanaPageTemplateSolutionNav: FunctionComponent<
onClose={() => setIsSideNavOpenOnMobile(false)}
side="left"
size={FLYOUT_SIZE}
closeButtonPosition="outside"
closeButtonPosition={closeFlyoutButtonPosition}
className="kbnPageTemplateSolutionNav__flyout"
>
{sideNav()}
<div className={sideNavClasses}>
{titleText}
<EuiSpacer size="l" />
{sideNavContent}
</div>
</EuiFlyout>
)}
<KibanaPageTemplateSolutionNavCollapseButton
Expand All @@ -152,7 +203,11 @@ export const KibanaPageTemplateSolutionNav: FunctionComponent<
)}
{isLargerBreakpoint && (
<>
{sideNav()}
<div className={sideNavClasses}>
{titleText}
<EuiSpacer size="l" />
{sideNavContent}
</div>
<KibanaPageTemplateSolutionNavCollapseButton
isCollapsed={!isOpenOnDesktop}
onClick={onCollapse}
Expand Down
14 changes: 8 additions & 6 deletions x-pack/plugins/security_solution/common/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,14 +76,14 @@ export const DEFAULT_THREAT_MATCH_QUERY = '@timestamp >= "now-30d/d"' as const;
export enum SecurityPageName {
administration = 'administration',
alerts = 'alerts',
blocklist = 'blocklist',
/*
* Warning: Computed values are not permitted in an enum with string valued members
* The 3 following Cases page names must match `CasesDeepLinkId` in x-pack/plugins/cases/public/common/navigation.ts
* All Cases page names must match `CasesDeepLinkId` in x-pack/plugins/cases/public/common/navigation/deep_links.ts
*/
blocklist = 'blocklist',
case = 'cases',
caseConfigure = 'cases_configure',
caseCreate = 'cases_create',
case = 'cases', // must match `CasesDeepLinkId.cases`
caseConfigure = 'cases_configure', // must match `CasesDeepLinkId.casesConfigure`
caseCreate = 'cases_create', // must match `CasesDeepLinkId.casesCreate`
detections = 'detections',
detectionAndResponse = 'detection_response',
endpoints = 'endpoints',
Expand Down Expand Up @@ -185,10 +185,12 @@ export const INCLUDE_INDEX_PATTERN = [
'traces-apm*',
'winlogbeat-*',
];

/** The comma-delimited list of Elasticsearch indices from which the SIEM app collects events, and the exclude index pattern */
export const DEFAULT_INDEX_PATTERN = [...INCLUDE_INDEX_PATTERN, ...EXCLUDE_ELASTIC_CLOUD_INDICES];

/** This Kibana Advanced Setting enables the grouped navigation in Security Solution */
export const ENABLE_GROUPED_NAVIGATION = 'securitySolution:enableGroupedNav' as const;

/** This Kibana Advanced Setting enables the `Security news` feed widget */
export const ENABLE_NEWS_FEED_SETTING = 'securitySolution:enableNewsFeed' as const;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export const allowedExperimentalValues = Object.freeze({
riskyUsersEnabled: false,
pendingActionResponsesWithAck: true,
policyListEnabled: true,
groupedNavigation: true,

/**
* This is used for enabling the end to end tests for the security_solution telemetry.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,7 @@ describe('url state', () => {

it('Do not clears kql when navigating to a new page', () => {
visitWithoutDateRange(ABSOLUTE_DATE_RANGE.urlKqlHostsHosts);
kqlSearch('source.ip: "10.142.0.9"{enter}');
navigateFromHeaderTo(NETWORK);
cy.get(KQL_INPUT).should('have.text', 'source.ip: "10.142.0.9"');
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ import {
USERS_PATH,
THREAT_HUNTING_PATH,
DASHBOARDS_PATH,
MANAGE_PATH,
} from '../../../common/constants';
import { ExperimentalFeatures } from '../../../common/experimental_features';

Expand Down Expand Up @@ -433,7 +434,7 @@ export const securitySolutionsDeepLinks: SecuritySolutionDeepLink[] = [
{
id: SecurityPageName.administration,
title: MANAGE,
path: ENDPOINTS_PATH,
path: MANAGE_PATH,
navLinkStatus: AppNavLinkStatus.hidden,
features: [FEATURE.general],
keywords: [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
/* eslint-disable react/display-name */

import React from 'react';
import { KibanaPageTemplateProps } from '@kbn/kibana-react-plugin/public';
import { KibanaPageTemplateProps } from '@kbn/shared-ux-components';
import { AppLeaveHandler } from '@kbn/core/public';
import { useShowTimeline } from '../../../../common/utils/timeline/use_show_timeline';
import { useSourcererDataView } from '../../../../common/containers/sourcerer';
Expand Down
Loading

0 comments on commit e267d2e

Please sign in to comment.