mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
[Security Solution] SideNav changes according to new Unified IA (ESS) (#159185)
This commit is contained in:
parent
6bbb49706c
commit
aad68003a6
69 changed files with 1385 additions and 961 deletions
|
@ -120,7 +120,7 @@ pageLoadAssetSize:
|
|||
serverless: 16573
|
||||
serverlessObservability: 68747
|
||||
serverlessSearch: 71995
|
||||
serverlessSecurity: 70441
|
||||
serverlessSecurity: 40000
|
||||
sessionView: 77750
|
||||
share: 71239
|
||||
snapshotRestore: 79032
|
||||
|
|
|
@ -6,4 +6,13 @@
|
|||
*/
|
||||
|
||||
export { SolutionSideNav, type SolutionSideNavProps } from './src';
|
||||
export type { SolutionSideNavItem, LinkCategory, LinkCategories, Tracker } from './src/types';
|
||||
export { LinkCategoryType, SolutionSideNavItemPosition } from './src/types';
|
||||
export type {
|
||||
SolutionSideNavItem,
|
||||
LinkCategory,
|
||||
TitleLinkCategory,
|
||||
AccordionLinkCategory,
|
||||
SeparatorLinkCategory,
|
||||
LinkCategories,
|
||||
Tracker,
|
||||
} from './src/types';
|
||||
|
|
|
@ -12,6 +12,8 @@ import {
|
|||
SolutionSideNav as SolutionSideNavComponent,
|
||||
type SolutionSideNavProps,
|
||||
type SolutionSideNavItem,
|
||||
SolutionSideNavItemPosition,
|
||||
LinkCategoryType,
|
||||
} from '..';
|
||||
|
||||
const items: SolutionSideNavItem[] = [
|
||||
|
@ -34,7 +36,8 @@ const items: SolutionSideNavItem[] = [
|
|||
},
|
||||
{
|
||||
id: 'panelLink2',
|
||||
label: 'I am the second nested',
|
||||
label: 'I have an icon',
|
||||
iconType: 'logoVulnerabilityManagement',
|
||||
href: '#',
|
||||
description: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.',
|
||||
},
|
||||
|
@ -92,26 +95,41 @@ const items: SolutionSideNavItem[] = [
|
|||
},
|
||||
],
|
||||
},
|
||||
{ id: 'linkTruncated', href: '#', label: 'I have truncated text because I am too long' },
|
||||
{ id: 'linkSmall', href: '#', label: 'I am smaller', labelSize: 'xs' },
|
||||
];
|
||||
const footerItems: SolutionSideNavItem[] = [
|
||||
{ id: 'footerLink', href: '#', label: 'I am a footer link' },
|
||||
{ id: 'linkWrapped', href: '#', label: 'I have wrapped text because I am too long' },
|
||||
{
|
||||
id: 'footerLinkPanel',
|
||||
id: 'bottomLink',
|
||||
href: '#',
|
||||
label: 'I am a bottom link',
|
||||
position: SolutionSideNavItemPosition.bottom,
|
||||
},
|
||||
{
|
||||
id: 'bottomLinkPanel',
|
||||
href: '#',
|
||||
label: 'I also have panel',
|
||||
position: SolutionSideNavItemPosition.bottom,
|
||||
items: [
|
||||
{
|
||||
id: 'footerLinkPanel1',
|
||||
label: 'I am a footer nested link',
|
||||
id: 'bottomLinkPanel1',
|
||||
label: 'I am a bottom nested link',
|
||||
href: '#',
|
||||
description: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.',
|
||||
},
|
||||
],
|
||||
},
|
||||
{ id: 'footerLinkSeparator', href: '#', label: 'I have a separator', appendSeparator: true },
|
||||
{ id: 'footerLinkIcon', href: '#', label: 'I have an icon', iconType: 'heart' },
|
||||
{
|
||||
id: 'bottomLinkSeparator',
|
||||
href: '#',
|
||||
label: 'I have a separator',
|
||||
appendSeparator: true,
|
||||
position: SolutionSideNavItemPosition.bottom,
|
||||
},
|
||||
{
|
||||
id: 'bottomLinkIcon',
|
||||
href: '#',
|
||||
label: 'I have an icon',
|
||||
iconType: 'heart',
|
||||
position: SolutionSideNavItemPosition.bottom,
|
||||
},
|
||||
];
|
||||
|
||||
export default {
|
||||
|
@ -136,9 +154,7 @@ export default {
|
|||
],
|
||||
};
|
||||
|
||||
type Params = Pick<SolutionSideNavProps, 'selectedId' | 'panelTopOffset' | 'panelBottomOffset'>;
|
||||
|
||||
export const SolutionSideNav = (params: Params) => (
|
||||
export const SolutionSideNav = (params: SolutionSideNavProps) => (
|
||||
<>
|
||||
<SolutionNav
|
||||
name={'Security'}
|
||||
|
@ -148,9 +164,9 @@ export const SolutionSideNav = (params: Params) => (
|
|||
// eslint-disable-next-line react/no-children-prop
|
||||
children={
|
||||
<SolutionSideNavComponent
|
||||
items={items}
|
||||
footerItems={footerItems}
|
||||
items={params.items}
|
||||
selectedId={params.selectedId}
|
||||
categories={params.categories}
|
||||
panelBottomOffset={params.panelBottomOffset || undefined}
|
||||
panelTopOffset={params.panelTopOffset || undefined}
|
||||
/>
|
||||
|
@ -168,9 +184,30 @@ export const SolutionSideNav = (params: Params) => (
|
|||
SolutionSideNav.argTypes = {
|
||||
selectedId: {
|
||||
control: { type: 'radio' },
|
||||
options: [...items, ...footerItems].map(({ id }) => id),
|
||||
options: items.map(({ id }) => id),
|
||||
defaultValue: 'simpleLink',
|
||||
},
|
||||
items: {
|
||||
control: 'object',
|
||||
defaultValue: items,
|
||||
},
|
||||
categories: {
|
||||
control: 'object',
|
||||
defaultValue: [
|
||||
{
|
||||
type: LinkCategoryType.separator,
|
||||
linkIds: ['simpleLink', 'panelLink', 'categoriesPanelLink'],
|
||||
},
|
||||
{
|
||||
type: LinkCategoryType.separator,
|
||||
linkIds: ['linkWrapped'],
|
||||
},
|
||||
{
|
||||
type: LinkCategoryType.separator,
|
||||
linkIds: ['bottomLink', 'bottomLinkPanel', 'bottomLinkSeparator', 'bottomLinkIcon'],
|
||||
},
|
||||
],
|
||||
},
|
||||
panelTopOffset: {
|
||||
control: 'text',
|
||||
defaultValue: '0px',
|
||||
|
|
|
@ -5,24 +5,22 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { transparentize, type EuiThemeComputed } from '@elastic/eui';
|
||||
import { tint, type EuiThemeComputed } from '@elastic/eui';
|
||||
import { css } from '@emotion/css';
|
||||
|
||||
export const SolutionSideNavItemStyles = (euiTheme: EuiThemeComputed<{}>) => css`
|
||||
font-weight: ${euiTheme.font.weight.regular};
|
||||
&.solutionSideNavItem--isPrimary * {
|
||||
font-weight: ${euiTheme.font.weight.bold};
|
||||
* {
|
||||
// EuiListGroupItem changes the links font-weight, we need to override it
|
||||
font-weight: ${euiTheme.font.weight.regular};
|
||||
}
|
||||
&:focus,
|
||||
&:focus-within,
|
||||
&:hover,
|
||||
&.solutionSideNavItem--isActive {
|
||||
background-color: ${transparentize(euiTheme.colors.primary, 0.1)};
|
||||
&.solutionSideNavItem--isSelected {
|
||||
background-color: ${tint(euiTheme.colors.lightShade, 0.5)};
|
||||
& * {
|
||||
font-weight: ${euiTheme.font.weight.medium};
|
||||
}
|
||||
}
|
||||
.solutionSideNavItemButton:focus,
|
||||
.solutionSideNavItemButton:focus-within,
|
||||
.solutionSideNavItemButton:hover {
|
||||
transform: none; /* prevent translationY transform that causes misalignment within the list item */
|
||||
background-color: ${transparentize(euiTheme.colors.primary, 0.2)};
|
||||
// Needed to place the icon on the right side
|
||||
span.euiListGroupItem__label {
|
||||
width: 100%;
|
||||
}
|
||||
`;
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { render, waitFor } from '@testing-library/react';
|
||||
import { render } from '@testing-library/react';
|
||||
import { SolutionSideNav, type SolutionSideNavProps } from './solution_side_nav';
|
||||
import type { SolutionSideNavItem } from './types';
|
||||
import { METRIC_TYPE } from '@kbn/analytics';
|
||||
|
@ -134,7 +134,8 @@ describe('SolutionSideNav', () => {
|
|||
|
||||
result.getByTestId(`solutionSideNavItemButton-${'dashboardsLanding'}`).click();
|
||||
|
||||
waitFor(() => {
|
||||
// add check at the end of the event loop to ensure the panel is removed
|
||||
setTimeout(() => {
|
||||
expect(result.queryByTestId('solutionSideNavPanel')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -5,64 +5,59 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
/* eslint-disable @elastic/eui/href-or-on-click */
|
||||
|
||||
import React, { useCallback, useMemo, useRef, useState } from 'react';
|
||||
import {
|
||||
EuiListGroup,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiLink,
|
||||
useIsWithinBreakpoints,
|
||||
useEuiTheme,
|
||||
EuiListGroupItem,
|
||||
EuiHorizontalRule,
|
||||
EuiSpacer,
|
||||
EuiButtonIcon,
|
||||
EuiIcon,
|
||||
} from '@elastic/eui';
|
||||
|
||||
import partition from 'lodash/fp/partition';
|
||||
import classNames from 'classnames';
|
||||
import { METRIC_TYPE } from '@kbn/analytics';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { SolutionSideNavPanel } from './solution_side_nav_panel';
|
||||
import type { LinkCategories, SolutionSideNavItem, Tracker } from './types';
|
||||
import { LinkCategories, SeparatorLinkCategory, SolutionSideNavItemPosition } from './types';
|
||||
import type { SolutionSideNavItem, Tracker } from './types';
|
||||
import { TELEMETRY_EVENT } from './telemetry/const';
|
||||
import { TelemetryContextProvider, useTelemetryContext } from './telemetry/telemetry_context';
|
||||
import { SolutionSideNavItemStyles } from './solution_side_nav.styles';
|
||||
|
||||
export const TOGGLE_PANEL_LABEL = i18n.translate('securitySolutionPackages.sideNav.togglePanel', {
|
||||
defaultMessage: 'Toggle panel nav',
|
||||
});
|
||||
|
||||
export interface SolutionSideNavProps {
|
||||
/** All the items to display in the side navigation */
|
||||
items: SolutionSideNavItem[];
|
||||
/** The id of the selected item to highlight. It only affects the top level items rendered in the main panel */
|
||||
selectedId: string;
|
||||
footerItems?: SolutionSideNavItem[];
|
||||
/** The categories to group and separate the main items. Ignores `position: 'bottom'` items */
|
||||
categories?: SeparatorLinkCategory[];
|
||||
/** Css value for the bottom offset of the secondary panel. defaults to 0 */
|
||||
panelBottomOffset?: string;
|
||||
/** Css value for the top offset of the secondary panel. defaults to the generic kibana header height */
|
||||
panelTopOffset?: string;
|
||||
// This enables Telemetry tracking inside side navigation, this has to be bound with the plugin appId
|
||||
// e.g.: usageCollection?.reportUiCounter?.bind(null, appId)
|
||||
/**
|
||||
* The tracker function to enable navigation Telemetry, this has to be bound with the plugin `appId`
|
||||
* e.g.: usageCollection?.reportUiCounter?.bind(null, appId)
|
||||
* */
|
||||
tracker?: Tracker;
|
||||
}
|
||||
export interface SolutionSideNavItemsProps {
|
||||
items: SolutionSideNavItem[];
|
||||
selectedId: string;
|
||||
activePanelNavId: ActivePanelNav;
|
||||
isMobileSize: boolean;
|
||||
navItemsById: NavItemsById;
|
||||
onOpenPanelNav: (id: string) => void;
|
||||
}
|
||||
export interface SolutionSideNavItemProps {
|
||||
item: SolutionSideNavItem;
|
||||
isSelected: boolean;
|
||||
isActive: boolean;
|
||||
hasPanelNav: boolean;
|
||||
onOpenPanelNav: (id: string) => void;
|
||||
}
|
||||
|
||||
type ActivePanelNav = string | null;
|
||||
type NavItemsById = Record<
|
||||
string,
|
||||
{ title: string; panelItems: SolutionSideNavItem[]; categories?: LinkCategories }
|
||||
>;
|
||||
|
||||
/**
|
||||
* The Solution side navigation main component
|
||||
*/
|
||||
export const SolutionSideNav: React.FC<SolutionSideNavProps> = React.memo(function SolutionSideNav({
|
||||
items,
|
||||
categories,
|
||||
selectedId,
|
||||
footerItems = [],
|
||||
panelBottomOffset,
|
||||
panelTopOffset,
|
||||
tracker,
|
||||
|
@ -95,46 +90,16 @@ export const SolutionSideNav: React.FC<SolutionSideNavProps> = React.memo(functi
|
|||
});
|
||||
}, [onClosePanelNav]);
|
||||
|
||||
const navItemsById = useMemo<NavItemsById>(
|
||||
const [topItems, bottomItems] = useMemo(
|
||||
() =>
|
||||
[...items, ...footerItems].reduce<NavItemsById>((acc, navItem) => {
|
||||
if (navItem.items?.length) {
|
||||
acc[navItem.id] = {
|
||||
title: navItem.label,
|
||||
panelItems: navItem.items,
|
||||
categories: navItem.categories,
|
||||
};
|
||||
}
|
||||
return acc;
|
||||
}, {}),
|
||||
[items, footerItems]
|
||||
partition(
|
||||
({ position = SolutionSideNavItemPosition.top }) =>
|
||||
position === SolutionSideNavItemPosition.top,
|
||||
items
|
||||
),
|
||||
[items]
|
||||
);
|
||||
|
||||
const panelNav = useMemo(() => {
|
||||
if (activePanelNavId == null || !navItemsById[activePanelNavId]) {
|
||||
return null;
|
||||
}
|
||||
const { panelItems, title, categories } = navItemsById[activePanelNavId];
|
||||
return (
|
||||
<SolutionSideNavPanel
|
||||
onClose={onClosePanelNav}
|
||||
onOutsideClick={onOutsidePanelClick}
|
||||
items={panelItems}
|
||||
title={title}
|
||||
categories={categories}
|
||||
bottomOffset={panelBottomOffset}
|
||||
topOffset={panelTopOffset}
|
||||
/>
|
||||
);
|
||||
}, [
|
||||
activePanelNavId,
|
||||
navItemsById,
|
||||
onClosePanelNav,
|
||||
onOutsidePanelClick,
|
||||
panelBottomOffset,
|
||||
panelTopOffset,
|
||||
]);
|
||||
|
||||
return (
|
||||
<TelemetryContextProvider tracker={tracker}>
|
||||
<EuiFlexGroup gutterSize="none" direction="column">
|
||||
|
@ -143,11 +108,11 @@ export const SolutionSideNav: React.FC<SolutionSideNavProps> = React.memo(functi
|
|||
<EuiFlexItem>
|
||||
<EuiListGroup gutterSize="none">
|
||||
<SolutionSideNavItems
|
||||
items={items}
|
||||
items={topItems}
|
||||
categories={categories}
|
||||
selectedId={selectedId}
|
||||
activePanelNavId={activePanelNavId}
|
||||
isMobileSize={isMobileSize}
|
||||
navItemsById={navItemsById}
|
||||
onOpenPanelNav={openPanelNav}
|
||||
/>
|
||||
</EuiListGroup>
|
||||
|
@ -155,11 +120,10 @@ export const SolutionSideNav: React.FC<SolutionSideNavProps> = React.memo(functi
|
|||
<EuiFlexItem grow={false}>
|
||||
<EuiListGroup gutterSize="none">
|
||||
<SolutionSideNavItems
|
||||
items={footerItems}
|
||||
items={bottomItems}
|
||||
selectedId={selectedId}
|
||||
activePanelNavId={activePanelNavId}
|
||||
isMobileSize={isMobileSize}
|
||||
navItemsById={navItemsById}
|
||||
onOpenPanelNav={openPanelNav}
|
||||
/>
|
||||
</EuiListGroup>
|
||||
|
@ -168,102 +132,233 @@ export const SolutionSideNav: React.FC<SolutionSideNavProps> = React.memo(functi
|
|||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
|
||||
{panelNav}
|
||||
<SolutionSideNavPanels
|
||||
items={items}
|
||||
activePanelNavId={activePanelNavId}
|
||||
onClose={onClosePanelNav}
|
||||
onOutsideClick={onOutsidePanelClick}
|
||||
bottomOffset={panelBottomOffset}
|
||||
topOffset={panelTopOffset}
|
||||
/>
|
||||
</TelemetryContextProvider>
|
||||
);
|
||||
});
|
||||
|
||||
const SolutionSideNavItems: React.FC<SolutionSideNavItemsProps> = ({
|
||||
items,
|
||||
selectedId,
|
||||
activePanelNavId,
|
||||
isMobileSize,
|
||||
navItemsById,
|
||||
onOpenPanelNav,
|
||||
}) => (
|
||||
<>
|
||||
{items.map((item) => (
|
||||
<SolutionSideNavItem
|
||||
key={item.id}
|
||||
item={item}
|
||||
isSelected={selectedId === item.id}
|
||||
isActive={activePanelNavId === item.id}
|
||||
hasPanelNav={!isMobileSize && item.id in navItemsById}
|
||||
onOpenPanelNav={onOpenPanelNav}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
interface SolutionSideNavItemsProps {
|
||||
items: SolutionSideNavItem[];
|
||||
selectedId: string;
|
||||
activePanelNavId: ActivePanelNav;
|
||||
isMobileSize: boolean;
|
||||
onOpenPanelNav: (id: string) => void;
|
||||
categories?: LinkCategories;
|
||||
}
|
||||
/**
|
||||
* The Solution side navigation items component.
|
||||
* Renders either the top or bottom panel items, considering the categories if present.
|
||||
* When `categories` is received all links that do not belong to any category are ignored.
|
||||
*/
|
||||
const SolutionSideNavItems: React.FC<SolutionSideNavItemsProps> = React.memo(
|
||||
function SolutionSideNavItems({
|
||||
items,
|
||||
categories,
|
||||
selectedId,
|
||||
activePanelNavId,
|
||||
isMobileSize,
|
||||
onOpenPanelNav,
|
||||
}) {
|
||||
if (!categories?.length) {
|
||||
return (
|
||||
<>
|
||||
{items.map((item) => (
|
||||
<SolutionSideNavItem
|
||||
key={item.id}
|
||||
item={item}
|
||||
isSelected={selectedId === item.id}
|
||||
isActive={activePanelNavId === item.id}
|
||||
isMobileSize={isMobileSize}
|
||||
onOpenPanelNav={onOpenPanelNav}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{categories?.map((category, categoryIndex) => {
|
||||
const categoryItems = category.linkIds.reduce<SolutionSideNavItem[]>((acc, linkId) => {
|
||||
const link = items.find((item) => item.id === linkId);
|
||||
if (link) {
|
||||
acc.push(link);
|
||||
}
|
||||
return acc;
|
||||
}, []);
|
||||
|
||||
if (!categoryItems.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{categoryIndex !== 0 && <EuiSpacer size="s" />}
|
||||
{categoryItems.map((item) => (
|
||||
<SolutionSideNavItem
|
||||
key={item.id}
|
||||
item={item}
|
||||
isSelected={selectedId === item.id}
|
||||
isActive={activePanelNavId === item.id}
|
||||
isMobileSize={isMobileSize}
|
||||
onOpenPanelNav={onOpenPanelNav}
|
||||
/>
|
||||
))}
|
||||
<EuiSpacer size="s" />
|
||||
</>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
interface SolutionSideNavItemProps {
|
||||
item: SolutionSideNavItem;
|
||||
isSelected: boolean;
|
||||
isActive: boolean;
|
||||
onOpenPanelNav: (id: string) => void;
|
||||
isMobileSize: boolean;
|
||||
}
|
||||
/**
|
||||
* The Solution side navigation item component.
|
||||
* Renders a single item for the main side navigation panel,
|
||||
* and it adds a button to open the item secondary panel if needed.
|
||||
*/
|
||||
const SolutionSideNavItem: React.FC<SolutionSideNavItemProps> = React.memo(
|
||||
function SolutionSideNavItem({ item, isSelected, isActive, hasPanelNav, onOpenPanelNav }) {
|
||||
function SolutionSideNavItem({ item, isSelected, isActive, isMobileSize, onOpenPanelNav }) {
|
||||
const { euiTheme } = useEuiTheme();
|
||||
const { tracker } = useTelemetryContext();
|
||||
|
||||
const { id, href, label, onClick, labelSize, iconType, appendSeparator } = item;
|
||||
|
||||
const onLinkClicked: React.MouseEventHandler = (ev) => {
|
||||
tracker?.(METRIC_TYPE.CLICK, `${TELEMETRY_EVENT.NAVIGATION}${id}`);
|
||||
onClick?.(ev);
|
||||
};
|
||||
const { id, href, label, items, onClick, iconType, appendSeparator } = item;
|
||||
|
||||
const solutionSideNavItemStyles = SolutionSideNavItemStyles(euiTheme);
|
||||
const itemClassNames = classNames(
|
||||
'solutionSideNavItem',
|
||||
{
|
||||
'solutionSideNavItem--isActive': isActive,
|
||||
'solutionSideNavItem--isPrimary': isSelected,
|
||||
},
|
||||
{ 'solutionSideNavItem--isSelected': isSelected },
|
||||
solutionSideNavItemStyles
|
||||
);
|
||||
const buttonClassNames = classNames('solutionSideNavItemButton');
|
||||
|
||||
const onButtonClick: React.MouseEventHandler = (ev) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
const hasPanelNav = useMemo(
|
||||
() => !isMobileSize && items != null && items.length > 0,
|
||||
[items, isMobileSize]
|
||||
);
|
||||
|
||||
const onLinkClicked: React.MouseEventHandler = useCallback(
|
||||
(ev) => {
|
||||
tracker?.(METRIC_TYPE.CLICK, `${TELEMETRY_EVENT.NAVIGATION}${id}`);
|
||||
onClick?.(ev);
|
||||
},
|
||||
[id, onClick, tracker]
|
||||
);
|
||||
|
||||
const onButtonClick: React.MouseEventHandler = useCallback(() => {
|
||||
tracker?.(METRIC_TYPE.CLICK, `${TELEMETRY_EVENT.PANEL_NAVIGATION_TOGGLE}${id}`);
|
||||
onOpenPanelNav(id);
|
||||
};
|
||||
}, [id, onOpenPanelNav, tracker]);
|
||||
|
||||
const itemLabel = useMemo(() => {
|
||||
if (iconType == null) {
|
||||
return label;
|
||||
}
|
||||
return (
|
||||
<EuiFlexGroup alignItems="center" gutterSize="none">
|
||||
<EuiFlexItem>{label}</EuiFlexItem>
|
||||
<EuiFlexItem grow={0}>
|
||||
<EuiIcon type={iconType} color="text" />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
}, [label, iconType]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiLink
|
||||
key={id}
|
||||
href={href}
|
||||
onClick={onLinkClicked}
|
||||
color={isSelected ? 'primary' : 'text'}
|
||||
data-test-subj={`solutionSideNavItemLink-${id}`}
|
||||
>
|
||||
<EuiListGroupItem
|
||||
className={itemClassNames}
|
||||
color={isSelected ? 'primary' : 'text'}
|
||||
label={label}
|
||||
size={labelSize ?? 's'}
|
||||
{...(iconType && {
|
||||
iconType,
|
||||
iconProps: {
|
||||
color: isSelected ? 'primary' : 'text',
|
||||
},
|
||||
})}
|
||||
{...(hasPanelNav && {
|
||||
extraAction: {
|
||||
className: buttonClassNames,
|
||||
color: isActive ? 'primary' : 'text',
|
||||
onClick: onButtonClick,
|
||||
iconType: 'spaces',
|
||||
iconSize: 'm',
|
||||
'aria-label': 'Toggle panel nav',
|
||||
'data-test-subj': `solutionSideNavItemButton-${id}`,
|
||||
alwaysShow: true,
|
||||
},
|
||||
})}
|
||||
/>
|
||||
</EuiLink>
|
||||
{appendSeparator && <EuiHorizontalRule margin="xs" />}
|
||||
<EuiFlexGroup alignItems="center" gutterSize="xs">
|
||||
<EuiFlexItem>
|
||||
<EuiListGroupItem
|
||||
label={itemLabel}
|
||||
href={href}
|
||||
wrapText
|
||||
onClick={onLinkClicked}
|
||||
className={itemClassNames}
|
||||
color="text"
|
||||
size="s"
|
||||
data-test-subj={`solutionSideNavItemLink-${id}`}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
{hasPanelNav && (
|
||||
<EuiFlexItem grow={0}>
|
||||
<EuiButtonIcon
|
||||
className={buttonClassNames}
|
||||
display={isActive ? 'base' : 'empty'}
|
||||
size="s"
|
||||
color="text"
|
||||
onClick={onButtonClick}
|
||||
iconType="spaces"
|
||||
iconSize="m"
|
||||
aria-label={TOGGLE_PANEL_LABEL}
|
||||
data-test-subj={`solutionSideNavItemButton-${id}`}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
</EuiFlexGroup>
|
||||
{appendSeparator ? <EuiHorizontalRule margin="xs" /> : <EuiSpacer size="xs" />}
|
||||
</>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
interface SolutionSideNavPanelsProps {
|
||||
items: SolutionSideNavItem[];
|
||||
activePanelNavId: ActivePanelNav;
|
||||
onClose: () => void;
|
||||
onOutsideClick: () => void;
|
||||
bottomOffset?: string;
|
||||
topOffset?: string;
|
||||
}
|
||||
/**
|
||||
* The Solution side navigation panels component.
|
||||
* Renders the secondary panel according to the `activePanelNavId` received.
|
||||
*/
|
||||
const SolutionSideNavPanels: React.FC<SolutionSideNavPanelsProps> = React.memo(
|
||||
function SolutionSideNavPanels({
|
||||
items,
|
||||
activePanelNavId,
|
||||
onClose,
|
||||
onOutsideClick,
|
||||
bottomOffset,
|
||||
topOffset,
|
||||
}) {
|
||||
const activePanelNavItem = useMemo<SolutionSideNavItem | undefined>(
|
||||
() => items.find(({ id }) => id === activePanelNavId),
|
||||
[items, activePanelNavId]
|
||||
);
|
||||
|
||||
if (activePanelNavItem == null || !activePanelNavItem.items?.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<SolutionSideNavPanel
|
||||
onClose={onClose}
|
||||
onOutsideClick={onOutsideClick}
|
||||
items={activePanelNavItem.items}
|
||||
title={activePanelNavItem.label}
|
||||
categories={activePanelNavItem.categories}
|
||||
bottomOffset={bottomOffset}
|
||||
topOffset={topOffset}
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default SolutionSideNav;
|
||||
|
|
|
@ -5,37 +5,31 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { transparentize, type EuiThemeComputed } from '@elastic/eui';
|
||||
import { css, injectGlobal } from '@emotion/css';
|
||||
import { transparentize, type EuiThemeComputed, euiFontSize, type UseEuiTheme } from '@elastic/eui';
|
||||
import { css } from '@emotion/css';
|
||||
|
||||
const EUI_HEADER_HEIGHT = '93px';
|
||||
const PANEL_LEFT_OFFSET = '248px';
|
||||
const PANEL_WIDTH = '340px';
|
||||
const EUI_HEADER_HEIGHT = '96px';
|
||||
const PANEL_LEFT_OFFSET = '249px';
|
||||
const PANEL_WIDTH = '270px';
|
||||
|
||||
export const panelClass = 'solutionSideNavPanel';
|
||||
|
||||
export const SolutionSideNavPanelStyles = (
|
||||
euiTheme: EuiThemeComputed<{}>,
|
||||
{ $bottomOffset, $topOffset }: { $bottomOffset?: string; $topOffset?: string } = {}
|
||||
) => {
|
||||
// We need to add the banner height to the top space when the header banner is present
|
||||
injectGlobal(`
|
||||
body.kbnBody--hasHeaderBanner .${panelClass} {
|
||||
top: calc(${EUI_HEADER_HEIGHT} + ${euiTheme.size.xl});
|
||||
}
|
||||
`);
|
||||
) => css`
|
||||
position: fixed;
|
||||
top: ${$topOffset ?? EUI_HEADER_HEIGHT};
|
||||
left: ${PANEL_LEFT_OFFSET};
|
||||
bottom: 0;
|
||||
width: ${PANEL_WIDTH};
|
||||
height: inherit;
|
||||
z-index: 999;
|
||||
background-color: ${euiTheme.colors.body};
|
||||
|
||||
return css`
|
||||
position: fixed;
|
||||
top: ${$topOffset ?? EUI_HEADER_HEIGHT};
|
||||
left: ${PANEL_LEFT_OFFSET};
|
||||
bottom: 0;
|
||||
width: ${PANEL_WIDTH};
|
||||
height: inherit;
|
||||
|
||||
// If the bottom bar is visible add padding to the navigation
|
||||
${$bottomOffset != null &&
|
||||
`
|
||||
// If the bottom bar is visible add padding to the navigation
|
||||
${$bottomOffset != null &&
|
||||
`
|
||||
height: inherit;
|
||||
bottom: ${$bottomOffset};
|
||||
box-shadow:
|
||||
|
@ -47,27 +41,40 @@ export const SolutionSideNavPanelStyles = (
|
|||
inset 0 -6px ${euiTheme.size.xs} -${euiTheme.size.xs} rgb(0 0 0 / 15%);
|
||||
`}
|
||||
|
||||
.solutionSideNavPanelLink {
|
||||
.solutionSideNavPanelLinkItem {
|
||||
background-color: transparent; /* originally white, it prevents panel to remove the bottom inset box shadow */
|
||||
&:hover {
|
||||
background-color: ${transparentize(euiTheme.colors.primary, 0.1)};
|
||||
}
|
||||
dt {
|
||||
color: ${euiTheme.colors.primaryText};
|
||||
}
|
||||
dd {
|
||||
color: ${euiTheme.colors.darkestShade};
|
||||
}
|
||||
.solutionSideNavPanelLink {
|
||||
&:focus-within {
|
||||
background-color: transparent;
|
||||
a {
|
||||
text-decoration: auto;
|
||||
}
|
||||
}
|
||||
`;
|
||||
};
|
||||
|
||||
export const SolutionSideNavTitleStyles = (
|
||||
euiTheme: EuiThemeComputed<{}>,
|
||||
{ $paddingTop = false }: { $paddingTop?: boolean } = {}
|
||||
) => css`
|
||||
padding-left: ${euiTheme.size.s};
|
||||
${$paddingTop && `padding-top: ${euiTheme.size.s};`}
|
||||
&:hover {
|
||||
background-color: ${transparentize(euiTheme.colors.primary, 0.1)};
|
||||
a {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const SolutionSideNavTitleStyles = (euiTheme: EuiThemeComputed<{}>) => css`
|
||||
padding-top: ${euiTheme.size.s};
|
||||
`;
|
||||
|
||||
export const SolutionSideNavCategoryTitleStyles = (euiTheme: EuiThemeComputed<{}>) => css`
|
||||
text-transform: uppercase;
|
||||
color: ${euiTheme.colors.darkShade};
|
||||
padding-left: ${euiTheme.size.s};
|
||||
padding-bottom: ${euiTheme.size.s};
|
||||
${euiFontSize({ euiTheme } as UseEuiTheme<{}>, 'xxs')}
|
||||
font-weight: ${euiTheme.font.weight.medium};
|
||||
`;
|
||||
|
||||
export const SolutionSideNavPanelLinksGroupStyles = (euiTheme: EuiThemeComputed<{}>) => css`
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
`;
|
||||
|
||||
export const SolutionSideNavCategoryAccordionStyles = (euiTheme: EuiThemeComputed<{}>) => css`
|
||||
margin-bottom: ${euiTheme.size.s};
|
||||
`;
|
||||
|
|
|
@ -12,7 +12,7 @@ import { BETA_LABEL } from './beta_badge';
|
|||
import { TELEMETRY_EVENT } from './telemetry/const';
|
||||
import { METRIC_TYPE } from '@kbn/analytics';
|
||||
import { TelemetryContextProvider } from './telemetry/telemetry_context';
|
||||
import type { SolutionSideNavItem, LinkCategories } from './types';
|
||||
import { SolutionSideNavItem, LinkCategories, LinkCategoryType } from './types';
|
||||
|
||||
const mockUseIsWithinMinBreakpoint = jest.fn(() => true);
|
||||
jest.mock('@elastic/eui', () => {
|
||||
|
@ -52,12 +52,16 @@ const betaMockItemsCount = mockItems.filter((item) => item.isBeta).length;
|
|||
const mockCategories: LinkCategories = [
|
||||
{
|
||||
label: 'HOSTS CATEGORY',
|
||||
linkIds: ['hosts'],
|
||||
linkIds: ['hosts', 'network'],
|
||||
},
|
||||
{
|
||||
label: 'Empty category',
|
||||
linkIds: [],
|
||||
},
|
||||
{
|
||||
type: LinkCategoryType.separator,
|
||||
linkIds: ['kubernetes'],
|
||||
},
|
||||
];
|
||||
|
||||
const bottomNavOffset = '10px';
|
||||
|
@ -92,9 +96,6 @@ describe('SolutionSideNavPanel', () => {
|
|||
|
||||
mockItems.forEach((item) => {
|
||||
expect(result.getByText(item.label)).toBeInTheDocument();
|
||||
if (item.description) {
|
||||
expect(result.getByText(item.description)).toBeInTheDocument();
|
||||
}
|
||||
});
|
||||
expect(result.queryAllByText(BETA_LABEL).length).toBe(betaMockItemsCount);
|
||||
});
|
||||
|
@ -103,6 +104,7 @@ describe('SolutionSideNavPanel', () => {
|
|||
const result = renderNavPanel({ categories: mockCategories });
|
||||
|
||||
mockCategories.forEach((mockCategory) => {
|
||||
if (!mockCategory.label) return; // omit separator categories
|
||||
if (mockCategory.linkIds.length) {
|
||||
expect(result.getByText(mockCategory.label)).toBeInTheDocument();
|
||||
} else {
|
||||
|
@ -111,6 +113,16 @@ describe('SolutionSideNavPanel', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('should render separator categories with items', () => {
|
||||
const result = renderNavPanel({ categories: mockCategories });
|
||||
mockCategories.forEach((mockCategory) => {
|
||||
if (mockCategory.type !== LinkCategoryType.separator) return; // omit non-separator categories
|
||||
mockCategory.linkIds.forEach((linkId) => {
|
||||
expect(result.queryByTestId(`solutionSideNavPanelLink-${linkId}`)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('links', () => {
|
||||
it('should contain correct href in links', () => {
|
||||
const result = renderNavPanel();
|
||||
|
|
|
@ -5,15 +5,14 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { Fragment, useCallback } from 'react';
|
||||
import React, { useCallback } from 'react';
|
||||
import {
|
||||
EuiDescriptionList,
|
||||
EuiDescriptionListDescription,
|
||||
EuiDescriptionListTitle,
|
||||
EuiAccordion,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiFocusTrap,
|
||||
EuiHorizontalRule,
|
||||
EuiListGroup,
|
||||
EuiListGroupItem,
|
||||
EuiOutsideClickDetector,
|
||||
EuiPanel,
|
||||
EuiPortal,
|
||||
|
@ -26,14 +25,23 @@ import {
|
|||
} from '@elastic/eui';
|
||||
import classNames from 'classnames';
|
||||
import { METRIC_TYPE } from '@kbn/analytics';
|
||||
import type { SolutionSideNavItem, LinkCategories } from './types';
|
||||
import {
|
||||
type SolutionSideNavItem,
|
||||
type LinkCategories,
|
||||
isAccordionLinkCategory,
|
||||
isTitleLinkCategory,
|
||||
isSeparatorLinkCategory,
|
||||
} from './types';
|
||||
import { BetaBadge } from './beta_badge';
|
||||
import { TELEMETRY_EVENT } from './telemetry/const';
|
||||
import { useTelemetryContext } from './telemetry/telemetry_context';
|
||||
import {
|
||||
SolutionSideNavPanelStyles,
|
||||
panelClass,
|
||||
SolutionSideNavCategoryTitleStyles,
|
||||
SolutionSideNavTitleStyles,
|
||||
SolutionSideNavCategoryAccordionStyles,
|
||||
SolutionSideNavPanelLinksGroupStyles,
|
||||
} from './solution_side_nav_panel.styles';
|
||||
|
||||
export interface SolutionSideNavPanelProps {
|
||||
|
@ -45,18 +53,8 @@ export interface SolutionSideNavPanelProps {
|
|||
bottomOffset?: string;
|
||||
topOffset?: string;
|
||||
}
|
||||
export interface SolutionSideNavPanelCategoriesProps {
|
||||
categories: LinkCategories;
|
||||
items: SolutionSideNavItem[];
|
||||
onClose: () => void;
|
||||
}
|
||||
export interface SolutionSideNavPanelItemsProps {
|
||||
items: SolutionSideNavItem[];
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the side navigation panel for secondary links
|
||||
* Renders the secondary navigation panel for the nested link items
|
||||
*/
|
||||
export const SolutionSideNavPanel: React.FC<SolutionSideNavPanelProps> = React.memo(
|
||||
function SolutionSideNavPanel({
|
||||
|
@ -80,9 +78,8 @@ export const SolutionSideNavPanel: React.FC<SolutionSideNavPanelProps> = React.m
|
|||
$bottomOffset,
|
||||
$topOffset,
|
||||
});
|
||||
const solutionSideNavTitleStyles = SolutionSideNavTitleStyles(euiTheme, { $paddingTop: true });
|
||||
const panelClasses = classNames(panelClass, 'eui-yScroll', solutionSideNavPanelStyles);
|
||||
const titleClasses = classNames(solutionSideNavTitleStyles);
|
||||
const titleClasses = classNames(SolutionSideNavTitleStyles(euiTheme));
|
||||
|
||||
// ESC key closes PanelNav
|
||||
const onKeyDown = useCallback(
|
||||
|
@ -103,30 +100,26 @@ export const SolutionSideNavPanel: React.FC<SolutionSideNavPanelProps> = React.m
|
|||
<EuiPanel
|
||||
className={panelClasses}
|
||||
hasShadow={hasShadow}
|
||||
// $bottomOffset={bottomOffsetLargerBreakpoint}
|
||||
borderRadius="none"
|
||||
paddingSize="m"
|
||||
data-test-subj="solutionSideNavPanel"
|
||||
>
|
||||
<EuiFlexGroup direction="column" gutterSize="l" alignItems="flexStart">
|
||||
<EuiFlexGroup direction="column" gutterSize="m" alignItems="flexStart">
|
||||
<EuiFlexItem>
|
||||
<EuiTitle size="xs" className={titleClasses}>
|
||||
<strong>{title}</strong>
|
||||
</EuiTitle>
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem>
|
||||
<EuiDescriptionList>
|
||||
{categories ? (
|
||||
<SolutionSideNavPanelCategories
|
||||
categories={categories}
|
||||
items={items}
|
||||
onClose={onClose}
|
||||
/>
|
||||
) : (
|
||||
<SolutionSideNavPanelItems items={items} onClose={onClose} />
|
||||
)}
|
||||
</EuiDescriptionList>
|
||||
<EuiFlexItem style={{ width: '100%' }}>
|
||||
{categories ? (
|
||||
<SolutionSideNavPanelCategories
|
||||
categories={categories}
|
||||
items={items}
|
||||
onClose={onClose}
|
||||
/>
|
||||
) : (
|
||||
<SolutionSideNavPanelItems items={items} onClose={onClose} />
|
||||
)}
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiPanel>
|
||||
|
@ -138,16 +131,21 @@ export const SolutionSideNavPanel: React.FC<SolutionSideNavPanelProps> = React.m
|
|||
}
|
||||
);
|
||||
|
||||
interface SolutionSideNavPanelCategoriesProps {
|
||||
categories: LinkCategories;
|
||||
items: SolutionSideNavItem[];
|
||||
onClose: () => void;
|
||||
}
|
||||
/**
|
||||
* Renders all the categories for the secondary navigation panel.
|
||||
* Links that do not belong to any category are ignored
|
||||
*/
|
||||
const SolutionSideNavPanelCategories: React.FC<SolutionSideNavPanelCategoriesProps> = React.memo(
|
||||
function SolutionSideNavPanelCategories({ categories, items, onClose }) {
|
||||
const { euiTheme } = useEuiTheme();
|
||||
const sideNavTitleStyles = SolutionSideNavTitleStyles(euiTheme);
|
||||
const titleClasses = classNames(sideNavTitleStyles);
|
||||
|
||||
return (
|
||||
<>
|
||||
{categories.map(({ label, linkIds }) => {
|
||||
const links = linkIds.reduce<SolutionSideNavItem[]>((acc, linkId) => {
|
||||
{categories.map((category, index) => {
|
||||
const categoryItems = category.linkIds.reduce<SolutionSideNavItem[]>((acc, linkId) => {
|
||||
const link = items.find((item) => item.id === linkId);
|
||||
if (link) {
|
||||
acc.push(link);
|
||||
|
@ -155,55 +153,150 @@ const SolutionSideNavPanelCategories: React.FC<SolutionSideNavPanelCategoriesPro
|
|||
return acc;
|
||||
}, []);
|
||||
|
||||
if (!links.length) {
|
||||
if (!categoryItems.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Fragment key={label}>
|
||||
<EuiTitle size="xxxs" className={titleClasses}>
|
||||
<h2>{label}</h2>
|
||||
</EuiTitle>
|
||||
<EuiHorizontalRule margin="xs" />
|
||||
<SolutionSideNavPanelItems items={links} onClose={onClose} />
|
||||
<EuiSpacer size="l" />
|
||||
</Fragment>
|
||||
);
|
||||
if (isTitleLinkCategory(category)) {
|
||||
return (
|
||||
<SolutionSideNavPanelTitleCategory
|
||||
label={category.label}
|
||||
items={categoryItems}
|
||||
onClose={onClose}
|
||||
key={category.label}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (isAccordionLinkCategory(category)) {
|
||||
return (
|
||||
<SolutionSideNavPanelAccordionCategory
|
||||
label={category.label}
|
||||
items={categoryItems}
|
||||
onClose={onClose}
|
||||
key={category.label}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (isSeparatorLinkCategory(category)) {
|
||||
return (
|
||||
<SolutionSideNavPanelSeparatorCategory
|
||||
items={categoryItems}
|
||||
onClose={onClose}
|
||||
key={index}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
})}
|
||||
</>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
const SolutionSideNavPanelItems: React.FC<SolutionSideNavPanelItemsProps> = React.memo(
|
||||
function SolutionSideNavPanelItems({ items, onClose }) {
|
||||
const panelLinkClassNames = classNames('solutionSideNavPanelLink');
|
||||
const panelLinkItemClassNames = classNames('solutionSideNavPanelLinkItem');
|
||||
const { tracker } = useTelemetryContext();
|
||||
interface SolutionSideNavPanelTitleCategoryProps {
|
||||
label: string;
|
||||
items: SolutionSideNavItem[];
|
||||
onClose: () => void;
|
||||
}
|
||||
/**
|
||||
* Renders a title category for the secondary navigation panel.
|
||||
*/
|
||||
const SolutionSideNavPanelTitleCategory: React.FC<SolutionSideNavPanelTitleCategoryProps> =
|
||||
React.memo(function SolutionSideNavPanelTitleCategory({ label, onClose, items }) {
|
||||
const { euiTheme } = useEuiTheme();
|
||||
const titleClasses = classNames(SolutionSideNavCategoryTitleStyles(euiTheme));
|
||||
return (
|
||||
<>
|
||||
{items.map(({ id, href, onClick, label, description, isBeta, betaOptions }) => (
|
||||
<a
|
||||
key={id}
|
||||
className={panelLinkClassNames}
|
||||
data-test-subj={`solutionSideNavPanelLink-${id}`}
|
||||
href={href}
|
||||
onClick={(ev) => {
|
||||
tracker?.(METRIC_TYPE.CLICK, `${TELEMETRY_EVENT.PANEL_NAVIGATION}${id}`);
|
||||
onClose();
|
||||
onClick?.(ev);
|
||||
}}
|
||||
>
|
||||
<EuiPanel hasShadow={false} className={panelLinkItemClassNames} paddingSize="s">
|
||||
<EuiDescriptionListTitle>
|
||||
{label}
|
||||
{isBeta && <BetaBadge text={betaOptions?.text} />}
|
||||
</EuiDescriptionListTitle>
|
||||
<EuiDescriptionListDescription>{description}</EuiDescriptionListDescription>
|
||||
</EuiPanel>
|
||||
</a>
|
||||
))}
|
||||
<EuiSpacer size="m" />
|
||||
<EuiTitle size="xxxs" className={titleClasses}>
|
||||
<h2>{label}</h2>
|
||||
</EuiTitle>
|
||||
<SolutionSideNavPanelItems items={items} onClose={onClose} />
|
||||
<EuiSpacer size="s" />
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
interface SolutionSideNavPanelAccordionCategoryProps {
|
||||
label: string;
|
||||
items: SolutionSideNavItem[];
|
||||
onClose: () => void;
|
||||
}
|
||||
/**
|
||||
* Renders an accordion category for the secondary navigation panel.
|
||||
*/
|
||||
const SolutionSideNavPanelAccordionCategory: React.FC<SolutionSideNavPanelAccordionCategoryProps> =
|
||||
React.memo(function SolutionSideNavPanelAccordionCategory({ label, onClose, items }) {
|
||||
const { euiTheme } = useEuiTheme();
|
||||
const accordionClasses = classNames(SolutionSideNavCategoryAccordionStyles(euiTheme));
|
||||
return (
|
||||
<EuiAccordion id={label} buttonContent={label} className={accordionClasses}>
|
||||
<SolutionSideNavPanelItems items={items} onClose={onClose} />
|
||||
</EuiAccordion>
|
||||
);
|
||||
});
|
||||
|
||||
interface SolutionSideNavPanelSeparatorCategoryProps {
|
||||
items: SolutionSideNavItem[];
|
||||
onClose: () => void;
|
||||
}
|
||||
/**
|
||||
* Renders a separator category for the secondary navigation panel.
|
||||
*/
|
||||
const SolutionSideNavPanelSeparatorCategory: React.FC<SolutionSideNavPanelSeparatorCategoryProps> =
|
||||
React.memo(function SolutionSideNavPanelSeparatorCategory({ onClose, items }) {
|
||||
return (
|
||||
<>
|
||||
<EuiSpacer size="m" />
|
||||
<SolutionSideNavPanelItems items={items} onClose={onClose} />
|
||||
<EuiSpacer size="s" />
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
interface SolutionSideNavPanelItemsProps {
|
||||
items: SolutionSideNavItem[];
|
||||
onClose: () => void;
|
||||
}
|
||||
/**
|
||||
* Renders the items for the secondary navigation panel.
|
||||
*/
|
||||
const SolutionSideNavPanelItems: React.FC<SolutionSideNavPanelItemsProps> = React.memo(
|
||||
function SolutionSideNavPanelItems({ items, onClose }) {
|
||||
const { euiTheme } = useEuiTheme();
|
||||
const panelLinksGroupClassNames = classNames(SolutionSideNavPanelLinksGroupStyles(euiTheme));
|
||||
const panelLinkClassNames = classNames('solutionSideNavPanelLink');
|
||||
const { tracker } = useTelemetryContext();
|
||||
return (
|
||||
<EuiListGroup className={panelLinksGroupClassNames}>
|
||||
{items.map(({ id, href, onClick, label, iconType, isBeta, betaOptions }) => {
|
||||
const itemLabel = !isBeta ? (
|
||||
label
|
||||
) : (
|
||||
<>
|
||||
{label} <BetaBadge text={betaOptions?.text} />
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<EuiListGroupItem
|
||||
key={id}
|
||||
label={itemLabel}
|
||||
wrapText
|
||||
className={panelLinkClassNames}
|
||||
size="s"
|
||||
data-test-subj={`solutionSideNavPanelLink-${id}`}
|
||||
href={href}
|
||||
iconType={iconType}
|
||||
onClick={(ev) => {
|
||||
tracker?.(METRIC_TYPE.CLICK, `${TELEMETRY_EVENT.PANEL_NAVIGATION}${id}`);
|
||||
onClose();
|
||||
onClick?.(ev);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</EuiListGroup>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
|
|
@ -7,7 +7,12 @@
|
|||
|
||||
import type React from 'react';
|
||||
import type { UiCounterMetricType } from '@kbn/analytics';
|
||||
import type { EuiListGroupItemProps, IconType } from '@elastic/eui';
|
||||
import type { IconType } from '@elastic/eui';
|
||||
|
||||
export enum SolutionSideNavItemPosition {
|
||||
top = 'top',
|
||||
bottom = 'bottom',
|
||||
}
|
||||
|
||||
export interface SolutionSideNavItem<T extends string = string> {
|
||||
id: T;
|
||||
|
@ -18,19 +23,50 @@ export interface SolutionSideNavItem<T extends string = string> {
|
|||
items?: Array<SolutionSideNavItem<T>>;
|
||||
categories?: LinkCategories<T>;
|
||||
iconType?: IconType;
|
||||
labelSize?: EuiListGroupItemProps['size'];
|
||||
appendSeparator?: boolean;
|
||||
position?: SolutionSideNavItemPosition;
|
||||
isBeta?: boolean;
|
||||
betaOptions?: {
|
||||
text: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface LinkCategory<T extends string = string> {
|
||||
label: string;
|
||||
linkIds: readonly T[];
|
||||
export enum LinkCategoryType {
|
||||
title = 'title',
|
||||
collapsibleTitle = 'collapsibleTitle',
|
||||
accordion = 'accordion',
|
||||
separator = 'separator',
|
||||
}
|
||||
|
||||
export interface LinkCategory<T extends string = string> {
|
||||
linkIds: readonly T[];
|
||||
label?: string;
|
||||
type?: LinkCategoryType;
|
||||
}
|
||||
|
||||
export interface TitleLinkCategory<T extends string = string> extends LinkCategory<T> {
|
||||
type?: LinkCategoryType.title;
|
||||
label: string;
|
||||
}
|
||||
export const isTitleLinkCategory = (category: LinkCategory): category is TitleLinkCategory =>
|
||||
(category.type == null || category.type === LinkCategoryType.title) && category.label != null;
|
||||
|
||||
export interface AccordionLinkCategory<T extends string = string> extends LinkCategory<T> {
|
||||
type: LinkCategoryType.accordion;
|
||||
label: string;
|
||||
}
|
||||
export const isAccordionLinkCategory = (
|
||||
category: LinkCategory
|
||||
): category is AccordionLinkCategory =>
|
||||
category.type === LinkCategoryType.accordion && category.label != null;
|
||||
|
||||
export interface SeparatorLinkCategory<T extends string = string> extends LinkCategory<T> {
|
||||
type: LinkCategoryType.separator;
|
||||
}
|
||||
export const isSeparatorLinkCategory = (
|
||||
category: LinkCategory
|
||||
): category is SeparatorLinkCategory => category.type === LinkCategoryType.separator;
|
||||
|
||||
export type LinkCategories<T extends string = string> = Readonly<Array<LinkCategory<T>>>;
|
||||
|
||||
export type Tracker = (
|
||||
|
|
|
@ -130,6 +130,7 @@ export enum SecurityPageName {
|
|||
rules = 'rules',
|
||||
rulesAdd = 'rules-add',
|
||||
rulesCreate = 'rules-create',
|
||||
rulesLanding = 'rules-landing',
|
||||
sessions = 'sessions',
|
||||
/*
|
||||
* Warning: Computed values are not permitted in an enum with string valued members
|
||||
|
@ -161,6 +162,7 @@ export const DETECTIONS_PATH = '/detections' as const;
|
|||
export const ALERTS_PATH = '/alerts' as const;
|
||||
export const ALERT_DETAILS_REDIRECT_PATH = `${ALERTS_PATH}/redirect` as const;
|
||||
export const RULES_PATH = '/rules' as const;
|
||||
export const RULES_LANDING_PATH = `${RULES_PATH}/landing` as const;
|
||||
export const RULES_ADD_PATH = `${RULES_PATH}/add_rules` as const;
|
||||
export const RULES_CREATE_PATH = `${RULES_PATH}/create` as const;
|
||||
export const EXCEPTIONS_PATH = '/exceptions' as const;
|
||||
|
|
|
@ -28,7 +28,7 @@ import {
|
|||
CSP_FINDINGS,
|
||||
POLICIES,
|
||||
EXPLORE,
|
||||
MANAGE,
|
||||
SETTINGS,
|
||||
ENTITY_ANALYTICS,
|
||||
} from '../../screens/security_header';
|
||||
|
||||
|
@ -169,7 +169,7 @@ describe('top-level navigation common to all pages in the Security app', () => {
|
|||
});
|
||||
|
||||
it('navigates to the Manage landing page', () => {
|
||||
navigateFromHeaderTo(MANAGE);
|
||||
navigateFromHeaderTo(SETTINGS);
|
||||
cy.url().should('include', MANAGE_URL);
|
||||
});
|
||||
|
||||
|
|
|
@ -16,7 +16,7 @@ import { HOSTS_NAMES } from '../../screens/hosts/all_hosts';
|
|||
import { ANOMALIES_TAB } from '../../screens/hosts/main';
|
||||
import {
|
||||
BREADCRUMBS,
|
||||
EXPLORE,
|
||||
EXPLORE_PANEL_BTN,
|
||||
HOSTS,
|
||||
KQL_INPUT,
|
||||
NETWORK,
|
||||
|
@ -222,7 +222,7 @@ describe('url state', () => {
|
|||
kqlSearch('source.ip: "10.142.0.9" {enter}');
|
||||
navigateFromHeaderTo(HOSTS);
|
||||
|
||||
openNavigationPanel(EXPLORE);
|
||||
openNavigationPanel(EXPLORE_PANEL_BTN);
|
||||
cy.get(NETWORK).should(
|
||||
'have.attr',
|
||||
'href',
|
||||
|
@ -236,7 +236,7 @@ describe('url state', () => {
|
|||
openAllHosts();
|
||||
waitForAllHostsToBeLoaded();
|
||||
|
||||
openNavigationPanel(EXPLORE);
|
||||
openNavigationPanel(EXPLORE_PANEL_BTN);
|
||||
cy.get(HOSTS).should(
|
||||
'have.attr',
|
||||
'href',
|
||||
|
|
|
@ -26,7 +26,7 @@ export const THREAT_INTELLIGENCE_PAGE =
|
|||
'[data-test-subj="collapsibleNavGroup-securitySolution"] [title="Intelligence"]';
|
||||
|
||||
export const MANAGE_PAGE =
|
||||
'[data-test-subj="collapsibleNavGroup-securitySolution"] [title="Manage"]';
|
||||
'[data-test-subj="collapsibleNavGroup-securitySolution"] [title="Settings"]';
|
||||
|
||||
export const KIBANA_NAVIGATION_TOGGLE = '[data-test-subj="toggleNavButton"]';
|
||||
|
||||
|
|
|
@ -140,7 +140,7 @@ export const THREAT_TECHNIQUE = '[data-test-subj="threatTechniqueLink"]';
|
|||
|
||||
export const THREAT_SUBTECHNIQUE = '[data-test-subj="threatSubtechniqueLink"]';
|
||||
|
||||
export const BACK_TO_RULES_TABLE = '[data-test-subj="breadcrumb"][title="Rules"]';
|
||||
export const BACK_TO_RULES_TABLE = '[data-test-subj="breadcrumb"][title="SIEM Rules"]';
|
||||
|
||||
export const HIGHLIGHTED_ROWS_IN_TABLE =
|
||||
'[data-test-subj="euiDataGridBody"] .alertsTableHighlightedRow';
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
// main links
|
||||
export const DASHBOARDS = '[data-test-subj="solutionSideNavItemLink-dashboards"]';
|
||||
export const DASHBOARDS_PANEL_BTN = '[data-test-subj="solutionSideNavItemButton-dashboards"]';
|
||||
|
||||
export const ALERTS = '[data-test-subj="solutionSideNavItemLink-alerts"]';
|
||||
|
||||
|
@ -18,8 +19,13 @@ export const CASES = '[data-test-subj="solutionSideNavItemLink-cases"]';
|
|||
export const TIMELINES = '[data-test-subj="solutionSideNavItemLink-timelines"]';
|
||||
|
||||
export const EXPLORE = '[data-test-subj="solutionSideNavItemLink-explore"]';
|
||||
export const EXPLORE_PANEL_BTN = '[data-test-subj="solutionSideNavItemButton-explore"]';
|
||||
|
||||
export const MANAGE = '[data-test-subj="solutionSideNavItemLink-administration"]';
|
||||
export const RULES_LANDING = '[data-test-subj="solutionSideNavPanelLink-rules-landing"]';
|
||||
export const RULES_PANEL_BTN = '[data-test-subj="solutionSideNavItemButton-rules-landing"]';
|
||||
|
||||
export const SETTINGS = '[data-test-subj="solutionSideNavItemLink-administration"]';
|
||||
export const SETTINGS_PANEL_BTN = '[data-test-subj="solutionSideNavItemButton-administration"]';
|
||||
|
||||
// nested links
|
||||
export const OVERVIEW = '[data-test-subj="solutionSideNavPanelLink-overview"]';
|
||||
|
@ -80,24 +86,27 @@ export const openNavigationPanelFor = (page: string) => {
|
|||
case KUBERNETES:
|
||||
case ENTITY_ANALYTICS:
|
||||
case CSP_DASHBOARD: {
|
||||
panel = DASHBOARDS;
|
||||
panel = DASHBOARDS_PANEL_BTN;
|
||||
break;
|
||||
}
|
||||
case HOSTS:
|
||||
case NETWORK:
|
||||
case USERS: {
|
||||
panel = EXPLORE;
|
||||
panel = EXPLORE_PANEL_BTN;
|
||||
break;
|
||||
}
|
||||
case RULES:
|
||||
case EXCEPTIONS:
|
||||
case CSP_BENCHMARKS: {
|
||||
panel = RULES_PANEL_BTN;
|
||||
break;
|
||||
}
|
||||
case ENDPOINTS:
|
||||
case TRUSTED_APPS:
|
||||
case EVENT_FILTERS:
|
||||
case RULES:
|
||||
case POLICIES:
|
||||
case EXCEPTIONS:
|
||||
case BLOCKLIST:
|
||||
case CSP_BENCHMARKS: {
|
||||
panel = MANAGE;
|
||||
case BLOCKLIST: {
|
||||
panel = SETTINGS_PANEL_BTN;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
@ -108,5 +117,5 @@ export const openNavigationPanelFor = (page: string) => {
|
|||
|
||||
// opens the navigation panel of a main link
|
||||
export const openNavigationPanel = (page: string) => {
|
||||
cy.get(`${page} button.solutionSideNavItemButton`).click({ force: true });
|
||||
cy.get(page).click();
|
||||
};
|
||||
|
|
|
@ -36,6 +36,10 @@ const StyledKibanaPageTemplate = styled(KibanaPageTemplate)<
|
|||
$addBottomPadding?: boolean;
|
||||
}
|
||||
>`
|
||||
.kbnSolutionNav {
|
||||
background-color: ${({ theme }) => theme.eui.euiColorEmptyShade};
|
||||
}
|
||||
|
||||
.${BOTTOM_BAR_CLASSNAME} {
|
||||
animation: 'none !important'; // disable the default bottom bar slide animation
|
||||
background: ${({ theme }) =>
|
||||
|
|
|
@ -64,6 +64,10 @@ export const RULES = i18n.translate('xpack.securitySolution.navigation.rules', {
|
|||
defaultMessage: 'Rules',
|
||||
});
|
||||
|
||||
export const SIEM_RULES = i18n.translate('xpack.securitySolution.navigation.siemRules', {
|
||||
defaultMessage: 'SIEM Rules',
|
||||
});
|
||||
|
||||
export const ADD_RULES = i18n.translate('xpack.securitySolution.navigation.addRules', {
|
||||
defaultMessage: 'Add Rules',
|
||||
});
|
||||
|
@ -127,8 +131,8 @@ export const EXPLORE = i18n.translate('xpack.securitySolution.navigation.explore
|
|||
export const INVESTIGATE = i18n.translate('xpack.securitySolution.navigation.investigate', {
|
||||
defaultMessage: 'Investigate',
|
||||
});
|
||||
export const MANAGE = i18n.translate('xpack.securitySolution.navigation.manage', {
|
||||
defaultMessage: 'Manage',
|
||||
export const SETTINGS = i18n.translate('xpack.securitySolution.navigation.settings', {
|
||||
defaultMessage: 'Settings',
|
||||
});
|
||||
|
||||
export const BLOCKLIST = i18n.translate('xpack.securitySolution.navigation.blocklist', {
|
||||
|
|
|
@ -14,29 +14,29 @@ import { getCasesDeepLinks } from '@kbn/cases-plugin/public';
|
|||
import { CASES_FEATURE_ID, CASES_PATH, SecurityPageName } from '../../common/constants';
|
||||
import type { LinkItem } from '../common/links/types';
|
||||
|
||||
export const getCasesLinkItems = (): LinkItem => {
|
||||
const casesLinks = getCasesDeepLinks<LinkItem>({
|
||||
basePath: CASES_PATH,
|
||||
extend: {
|
||||
[SecurityPageName.case]: {
|
||||
globalNavPosition: 5,
|
||||
capabilities: [`${CASES_FEATURE_ID}.${READ_CASES_CAPABILITY}`],
|
||||
},
|
||||
[SecurityPageName.caseConfigure]: {
|
||||
capabilities: [`${CASES_FEATURE_ID}.${UPDATE_CASES_CAPABILITY}`],
|
||||
licenseType: 'gold',
|
||||
sideNavDisabled: true,
|
||||
},
|
||||
[SecurityPageName.caseCreate]: {
|
||||
capabilities: [`${CASES_FEATURE_ID}.${CREATE_CASES_CAPABILITY}`],
|
||||
sideNavDisabled: true,
|
||||
},
|
||||
const casesLinks = getCasesDeepLinks<LinkItem>({
|
||||
basePath: CASES_PATH,
|
||||
extend: {
|
||||
[SecurityPageName.case]: {
|
||||
globalNavPosition: 5,
|
||||
capabilities: [`${CASES_FEATURE_ID}.${READ_CASES_CAPABILITY}`],
|
||||
},
|
||||
});
|
||||
const { id, deepLinks, ...rest } = casesLinks;
|
||||
return {
|
||||
...rest,
|
||||
id: SecurityPageName.case,
|
||||
links: deepLinks as LinkItem[],
|
||||
};
|
||||
[SecurityPageName.caseConfigure]: {
|
||||
capabilities: [`${CASES_FEATURE_ID}.${UPDATE_CASES_CAPABILITY}`],
|
||||
licenseType: 'gold',
|
||||
sideNavDisabled: true,
|
||||
},
|
||||
[SecurityPageName.caseCreate]: {
|
||||
capabilities: [`${CASES_FEATURE_ID}.${CREATE_CASES_CAPABILITY}`],
|
||||
sideNavDisabled: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const { id, deepLinks, ...rest } = casesLinks;
|
||||
|
||||
export const links = {
|
||||
...rest,
|
||||
id: SecurityPageName.case,
|
||||
links: deepLinks as LinkItem[],
|
||||
};
|
||||
|
|
|
@ -9,7 +9,7 @@ import { i18n } from '@kbn/i18n';
|
|||
import type { SecurityPageName } from '../../common/constants';
|
||||
import { SERVER_APP_ID } from '../../common/constants';
|
||||
import type { LinkItem } from '../common/links/types';
|
||||
import { IconCloudDefend } from '../management/icons/cloud_defend';
|
||||
import { IconCloudDefend } from '../common/icons/cloud_defend';
|
||||
|
||||
const commonLinkProperties: Partial<LinkItem> = {
|
||||
isBeta: true,
|
||||
|
@ -17,7 +17,7 @@ const commonLinkProperties: Partial<LinkItem> = {
|
|||
capabilities: [`${SERVER_APP_ID}.show`],
|
||||
};
|
||||
|
||||
export const manageLinks: LinkItem = {
|
||||
export const cloudDefendLink: LinkItem = {
|
||||
...getSecuritySolutionLink<SecurityPageName>('policies'),
|
||||
description: i18n.translate('xpack.securitySolution.appLinks.cloudDefendPoliciesDescription', {
|
||||
defaultMessage:
|
||||
|
|
|
@ -6,24 +6,25 @@
|
|||
*/
|
||||
import { getSecuritySolutionLink } from '@kbn/cloud-security-posture-plugin/public';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { SecurityPageName, SERVER_APP_ID } from '../../common/constants';
|
||||
import type { SecurityPageName } from '../../common/constants';
|
||||
import { SERVER_APP_ID } from '../../common/constants';
|
||||
import cloudSecurityPostureDashboardImage from '../common/images/cloud_security_posture_dashboard_page.png';
|
||||
import cloudNativeVulnerabilityManagementDashboardImage from '../common/images/cloud_native_vulnerability_management_dashboard_page.png';
|
||||
import type { LinkCategories, LinkItem } from '../common/links/types';
|
||||
import { IconExceptionLists } from '../management/icons/exception_lists';
|
||||
import type { LinkItem } from '../common/links/types';
|
||||
import { IconEndpoints } from '../common/icons/endpoints';
|
||||
|
||||
const commonLinkProperties: Partial<LinkItem> = {
|
||||
hideTimeline: true,
|
||||
capabilities: [`${SERVER_APP_ID}.show`],
|
||||
};
|
||||
|
||||
export const rootLinks: LinkItem = {
|
||||
export const findingsLinks: LinkItem = {
|
||||
...getSecuritySolutionLink<SecurityPageName>('findings'),
|
||||
globalNavPosition: 3,
|
||||
...commonLinkProperties,
|
||||
};
|
||||
|
||||
export const dashboardLinks: LinkItem = {
|
||||
export const cspDashboardLink: LinkItem = {
|
||||
...getSecuritySolutionLink<SecurityPageName>('dashboard'),
|
||||
description: i18n.translate(
|
||||
'xpack.securitySolution.appLinks.cloudSecurityPostureDashboardDescription',
|
||||
|
@ -46,7 +47,7 @@ export const vulnerabilityDashboardLink: LinkItem = {
|
|||
...commonLinkProperties,
|
||||
};
|
||||
|
||||
export const manageLinks: LinkItem = {
|
||||
export const benchmarksLink: LinkItem = {
|
||||
...getSecuritySolutionLink<SecurityPageName>('benchmarks'),
|
||||
description: i18n.translate(
|
||||
'xpack.securitySolution.appLinks.cloudSecurityPostureBenchmarksDescription',
|
||||
|
@ -54,18 +55,6 @@ export const manageLinks: LinkItem = {
|
|||
defaultMessage: 'View benchmark rules.',
|
||||
}
|
||||
),
|
||||
landingIcon: IconExceptionLists,
|
||||
landingIcon: IconEndpoints,
|
||||
...commonLinkProperties,
|
||||
};
|
||||
|
||||
export const manageCategories: LinkCategories = [
|
||||
{
|
||||
label: i18n.translate('xpack.securitySolution.appLinks.category.cloudSecurityPosture', {
|
||||
defaultMessage: 'CLOUD SECURITY',
|
||||
}),
|
||||
linkIds: [
|
||||
SecurityPageName.cloudSecurityPostureBenchmarks,
|
||||
SecurityPageName.cloudDefendPolicies,
|
||||
],
|
||||
},
|
||||
];
|
||||
|
|
|
@ -17,7 +17,7 @@ const DEFAULT_NAV_ITEM: NavigationLink = {
|
|||
id: SecurityPageName.overview,
|
||||
title: 'TEST LABEL',
|
||||
description: 'TEST DESCRIPTION',
|
||||
icon: 'myTestIcon',
|
||||
landingIcon: 'myTestIcon',
|
||||
};
|
||||
const spyTrack = jest.spyOn(telemetry, 'track');
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import { EuiFlexGrid, EuiFlexGroup, EuiFlexItem, EuiIcon, EuiText, EuiTitle } from '@elastic/eui';
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiText, EuiTitle } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
|
@ -17,25 +17,26 @@ interface LandingLinksImagesProps {
|
|||
items: NavigationLink[];
|
||||
}
|
||||
|
||||
const LandingItem = styled(EuiFlexItem)`
|
||||
min-width: 22em;
|
||||
`;
|
||||
const Title = styled(EuiTitle)`
|
||||
margin-top: ${({ theme }) => theme.eui.euiSizeM};
|
||||
margin-bottom: ${({ theme }) => theme.eui.euiSizeXS};
|
||||
`;
|
||||
const Description = styled(EuiFlexItem)`
|
||||
max-width: 22em;
|
||||
`;
|
||||
const Link = styled.a`
|
||||
color: inherit;
|
||||
`;
|
||||
|
||||
const SecuritySolutionLink = withSecuritySolutionLink(Link);
|
||||
|
||||
const Description = styled(EuiFlexItem)`
|
||||
max-width: 22em;
|
||||
`;
|
||||
|
||||
const StyledEuiTitle = styled(EuiTitle)`
|
||||
margin-top: ${({ theme }) => theme.eui.euiSizeM};
|
||||
margin-bottom: ${({ theme }) => theme.eui.euiSizeXS};
|
||||
`;
|
||||
|
||||
export const LandingLinksIcons: React.FC<LandingLinksImagesProps> = ({ items }) => (
|
||||
<EuiFlexGrid columns={3} gutterSize="xl">
|
||||
{items.map(({ title, description, id, icon, isBeta, betaOptions }) => (
|
||||
<EuiFlexItem key={id} data-test-subj="LandingItem">
|
||||
<EuiFlexGroup gutterSize="xl" wrap>
|
||||
{items.map(({ title, description, id, landingIcon, isBeta, betaOptions }) => (
|
||||
<LandingItem key={id} data-test-subj="LandingItem" grow={false}>
|
||||
<EuiFlexGroup
|
||||
direction="column"
|
||||
alignItems="flexStart"
|
||||
|
@ -44,11 +45,11 @@ export const LandingLinksIcons: React.FC<LandingLinksImagesProps> = ({ items })
|
|||
>
|
||||
<EuiFlexItem grow={false}>
|
||||
<SecuritySolutionLink tabIndex={-1} deepLinkId={id}>
|
||||
<EuiIcon aria-hidden="true" size="xl" type={icon ?? ''} role="presentation" />
|
||||
<EuiIcon aria-hidden="true" size="xl" type={landingIcon ?? ''} role="presentation" />
|
||||
</SecuritySolutionLink>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<StyledEuiTitle size="xxs">
|
||||
<Title size="xxs">
|
||||
<EuiFlexGroup gutterSize="none">
|
||||
<EuiFlexItem grow={false}>
|
||||
<SecuritySolutionLinkAnchor
|
||||
|
@ -66,7 +67,7 @@ export const LandingLinksIcons: React.FC<LandingLinksImagesProps> = ({ items })
|
|||
</EuiFlexItem>
|
||||
)}
|
||||
</EuiFlexGroup>
|
||||
</StyledEuiTitle>
|
||||
</Title>
|
||||
</EuiFlexItem>
|
||||
<Description>
|
||||
<EuiText size="s" color="text">
|
||||
|
@ -74,7 +75,7 @@ export const LandingLinksIcons: React.FC<LandingLinksImagesProps> = ({ items })
|
|||
</EuiText>
|
||||
</Description>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
</LandingItem>
|
||||
))}
|
||||
</EuiFlexGrid>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
|
|
|
@ -7,10 +7,10 @@
|
|||
|
||||
import { render } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import { SecurityPageName } from '../../app/types';
|
||||
import { TestProviders } from '../../common/mock';
|
||||
import { ManagementCategories } from './landing';
|
||||
import type { NavigationLink } from '../../common/links';
|
||||
import { SecurityPageName } from '../../../../common';
|
||||
import { TestProviders } from '../../mock';
|
||||
import { LandingLinksIconsCategories } from './landing_links_icons_categories';
|
||||
import type { NavigationLink } from '../../links';
|
||||
|
||||
const RULES_ITEM_LABEL = 'elastic rules!';
|
||||
const EXCEPTIONS_ITEM_LABEL = 'exceptional!';
|
||||
|
@ -35,27 +35,27 @@ const defaultAppManageLink: NavigationLink = {
|
|||
id: SecurityPageName.rules,
|
||||
title: RULES_ITEM_LABEL,
|
||||
description: '',
|
||||
icon: 'testIcon1',
|
||||
landingIcon: 'testIcon1',
|
||||
},
|
||||
{
|
||||
id: SecurityPageName.exceptions,
|
||||
title: EXCEPTIONS_ITEM_LABEL,
|
||||
description: '',
|
||||
icon: 'testIcon2',
|
||||
landingIcon: 'testIcon2',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const mockAppManageLink = jest.fn(() => defaultAppManageLink);
|
||||
jest.mock('../../common/links/nav_links', () => ({
|
||||
jest.mock('../../links/nav_links', () => ({
|
||||
useRootNavLink: () => mockAppManageLink(),
|
||||
}));
|
||||
|
||||
describe('ManagementCategories', () => {
|
||||
describe('LandingLinksIconsCategories', () => {
|
||||
it('should render items', () => {
|
||||
const { queryByText } = render(
|
||||
<TestProviders>
|
||||
<ManagementCategories />
|
||||
<LandingLinksIconsCategories pageName={defaultAppManageLink.id} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
|
@ -75,7 +75,7 @@ describe('ManagementCategories', () => {
|
|||
});
|
||||
const { queryAllByTestId } = render(
|
||||
<TestProviders>
|
||||
<ManagementCategories />
|
||||
<LandingLinksIconsCategories pageName={defaultAppManageLink.id} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
|
@ -99,13 +99,13 @@ describe('ManagementCategories', () => {
|
|||
id: SecurityPageName.rules,
|
||||
title: RULES_ITEM_LABEL,
|
||||
description: '',
|
||||
icon: 'testIcon1',
|
||||
landingIcon: 'testIcon1',
|
||||
},
|
||||
],
|
||||
});
|
||||
const { queryAllByTestId } = render(
|
||||
<TestProviders>
|
||||
<ManagementCategories />
|
||||
<LandingLinksIconsCategories pageName={defaultAppManageLink.id} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
|
@ -122,7 +122,7 @@ describe('ManagementCategories', () => {
|
|||
});
|
||||
const { queryByText } = render(
|
||||
<TestProviders>
|
||||
<ManagementCategories />
|
||||
<LandingLinksIconsCategories pageName={defaultAppManageLink.id} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
/*
|
||||
* 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 { css } from '@emotion/react';
|
||||
import { EuiHorizontalRule, EuiSpacer, EuiTitle, useEuiTheme } from '@elastic/eui';
|
||||
import type { SecurityPageName } from '../../../../common';
|
||||
import type { NavigationLink } from '../../links';
|
||||
import { useRootNavLink } from '../../links/nav_links';
|
||||
import { LandingLinksIcons } from './landing_links_icons';
|
||||
|
||||
type CategoriesLinks = Array<{ label?: string; links: NavigationLink[] }>;
|
||||
|
||||
const useCategories = ({ pageName }: { pageName: SecurityPageName }): CategoriesLinks => {
|
||||
const { links = [], categories = [] } = useRootNavLink(pageName) ?? {};
|
||||
const linksById = Object.fromEntries(links.map((link) => [link.id, link]));
|
||||
|
||||
return categories.reduce<CategoriesLinks>((acc, { label, linkIds }) => {
|
||||
const linksItem = linkIds.reduce<NavigationLink[]>((linksAcc, linkId) => {
|
||||
if (linksById[linkId]) {
|
||||
linksAcc.push(linksById[linkId]);
|
||||
}
|
||||
return linksAcc;
|
||||
}, []);
|
||||
if (linksItem.length > 0) {
|
||||
acc.push({ label, links: linksItem });
|
||||
}
|
||||
return acc;
|
||||
}, []);
|
||||
};
|
||||
|
||||
export const LandingLinksIconsCategories = React.memo(function LandingLinksIconsCategories({
|
||||
pageName,
|
||||
}: {
|
||||
pageName: SecurityPageName;
|
||||
}) {
|
||||
const { euiTheme } = useEuiTheme();
|
||||
const categories = useCategories({ pageName });
|
||||
return (
|
||||
<>
|
||||
{categories.map(({ label, links }, index) => (
|
||||
<div key={`${index}_${label}`}>
|
||||
{index > 0 && (
|
||||
<>
|
||||
<EuiSpacer key="first" size="xl" />
|
||||
<EuiSpacer key="second" size="xl" />
|
||||
</>
|
||||
)}
|
||||
<EuiTitle size="xxxs">
|
||||
<h2>{label}</h2>
|
||||
</EuiTitle>
|
||||
<EuiHorizontalRule
|
||||
css={css`
|
||||
margin-top: ${euiTheme.size.m};
|
||||
margin-bottom: ${euiTheme.size.l};
|
||||
`}
|
||||
/>
|
||||
<LandingLinksIcons items={links} />
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
});
|
|
@ -18,14 +18,14 @@ const DEFAULT_NAV_ITEM: NavigationLink = {
|
|||
id: SecurityPageName.overview,
|
||||
title: 'TEST LABEL',
|
||||
description: 'TEST DESCRIPTION',
|
||||
image: 'TEST_IMAGE.png',
|
||||
landingImage: 'TEST_IMAGE.png',
|
||||
};
|
||||
|
||||
const BETA_NAV_ITEM: NavigationLink = {
|
||||
id: SecurityPageName.kubernetes,
|
||||
title: 'TEST LABEL',
|
||||
description: 'TEST DESCRIPTION',
|
||||
image: 'TEST_IMAGE.png',
|
||||
landingImage: 'TEST_IMAGE.png',
|
||||
isBeta: true,
|
||||
};
|
||||
|
||||
|
@ -58,17 +58,17 @@ describe('LandingLinksImages', () => {
|
|||
expect(queryByText(title)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders image', () => {
|
||||
const image = 'test_image.jpeg';
|
||||
it('renders landingImage', () => {
|
||||
const landingImage = 'test_image.jpeg';
|
||||
const title = 'TEST_LABEL';
|
||||
|
||||
const { getByTestId } = render(
|
||||
<TestProviders>
|
||||
<LandingLinksImages items={[{ ...DEFAULT_NAV_ITEM, image, title }]} />
|
||||
<LandingLinksImages items={[{ ...DEFAULT_NAV_ITEM, landingImage, title }]} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
expect(getByTestId('LandingLinksImage')).toHaveAttribute('src', image);
|
||||
expect(getByTestId('LandingLinksImage')).toHaveAttribute('src', landingImage);
|
||||
});
|
||||
|
||||
it('renders beta tag when isBeta is true', () => {
|
||||
|
@ -105,26 +105,26 @@ describe('LandingImageCards', () => {
|
|||
expect(queryByText(title)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders image', () => {
|
||||
const image = 'test_image.jpeg';
|
||||
it('renders landingImage', () => {
|
||||
const landingImage = 'test_image.jpeg';
|
||||
const title = 'TEST_LABEL';
|
||||
|
||||
const { getByTestId } = render(
|
||||
<TestProviders>
|
||||
<LandingImageCards items={[{ ...DEFAULT_NAV_ITEM, image, title }]} />
|
||||
<LandingImageCards items={[{ ...DEFAULT_NAV_ITEM, landingImage, title }]} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
expect(getByTestId('LandingImageCard-image')).toHaveAttribute('src', image);
|
||||
expect(getByTestId('LandingImageCard-image')).toHaveAttribute('src', landingImage);
|
||||
});
|
||||
|
||||
it('sends telemetry', () => {
|
||||
const image = 'test_image.jpeg';
|
||||
const landingImage = 'test_image.jpeg';
|
||||
const title = 'TEST LABEL';
|
||||
|
||||
const { getByText } = render(
|
||||
<TestProviders>
|
||||
<LandingImageCards items={[{ ...DEFAULT_NAV_ITEM, image, title }]} />
|
||||
<LandingImageCards items={[{ ...DEFAULT_NAV_ITEM, landingImage, title }]} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
|
|
|
@ -59,7 +59,7 @@ const SecuritySolutionLink = withSecuritySolutionLink(Link);
|
|||
|
||||
export const LandingLinksImages: React.FC<LandingImagesProps> = ({ items }) => (
|
||||
<EuiFlexGroup direction="column">
|
||||
{items.map(({ title, description, image, id, isBeta, betaOptions }) => (
|
||||
{items.map(({ title, description, landingImage, id, isBeta, betaOptions }) => (
|
||||
<EuiFlexItem key={id} data-test-subj="LandingItem">
|
||||
<SecuritySolutionLink
|
||||
deepLinkId={id}
|
||||
|
@ -72,13 +72,13 @@ export const LandingLinksImages: React.FC<LandingImagesProps> = ({ items }) => (
|
|||
<EuiPanel hasBorder hasShadow={false} paddingSize="m" onClick={() => {}}>
|
||||
<EuiFlexGroup>
|
||||
<StyledFlexItem grow={false}>
|
||||
{image && (
|
||||
{landingImage && (
|
||||
<EuiImage
|
||||
data-test-subj="LandingLinksImage"
|
||||
size="l"
|
||||
role="presentation"
|
||||
alt=""
|
||||
src={image}
|
||||
src={landingImage}
|
||||
/>
|
||||
)}
|
||||
</StyledFlexItem>
|
||||
|
@ -122,7 +122,7 @@ const SecuritySolutionCard = withSecuritySolutionLink(PrimaryTitleCard);
|
|||
|
||||
export const LandingImageCards: React.FC<LandingImagesProps> = React.memo(({ items }) => (
|
||||
<EuiFlexGroup direction="row" wrap>
|
||||
{items.map(({ id, image, title, description, isBeta, betaOptions }) => (
|
||||
{items.map(({ id, landingImage, title, description, isBeta, betaOptions }) => (
|
||||
<LandingImageCardItem key={id} data-test-subj="LandingImageCard-item" grow={false}>
|
||||
<SecuritySolutionCard
|
||||
deepLinkId={id}
|
||||
|
@ -130,13 +130,13 @@ export const LandingImageCards: React.FC<LandingImagesProps> = React.memo(({ ite
|
|||
textAlign="left"
|
||||
paddingSize="m"
|
||||
image={
|
||||
image && (
|
||||
landingImage && (
|
||||
<EuiImage
|
||||
data-test-subj="LandingImageCard-image"
|
||||
role="presentation"
|
||||
size={CARD_WIDTH}
|
||||
alt={title}
|
||||
src={image}
|
||||
src={landingImage}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -587,7 +587,7 @@ interface LinkProps {
|
|||
href: string;
|
||||
}
|
||||
|
||||
type GetSecuritySolutionProps = (
|
||||
export type GetSecuritySolutionProps = (
|
||||
params: SecuritySolutionLinkProps & { onClick?: MouseEventHandler }
|
||||
) => LinkProps;
|
||||
|
||||
|
|
|
@ -123,12 +123,12 @@ const ipv4 = '192.0.2.255';
|
|||
const ipv6 = '2001:db8:ffff:ffff:ffff:ffff:ffff:ffff';
|
||||
const ipv6Encoded = encodeIpv6(ipv6);
|
||||
|
||||
const securityBreadCrumb = {
|
||||
const securityBreadcrumb = {
|
||||
href: 'securitySolutionUI/get_started',
|
||||
text: 'Security',
|
||||
};
|
||||
|
||||
const hostsBreadcrumbs = {
|
||||
const hostsBreadcrumb = {
|
||||
href: 'securitySolutionUI/hosts',
|
||||
text: 'Hosts',
|
||||
};
|
||||
|
@ -138,23 +138,28 @@ const networkBreadcrumb = {
|
|||
href: 'securitySolutionUI/network',
|
||||
};
|
||||
|
||||
const exploreBreadcrumbs = {
|
||||
const exploreBreadcrumb = {
|
||||
href: 'securitySolutionUI/explore',
|
||||
text: 'Explore',
|
||||
};
|
||||
|
||||
const rulesBReadcrumb = {
|
||||
const rulesLandingBreadcrumb = {
|
||||
text: 'Rules',
|
||||
href: 'securitySolutionUI/rules-landing',
|
||||
};
|
||||
|
||||
const rulesBreadcrumb = {
|
||||
text: 'SIEM Rules',
|
||||
href: 'securitySolutionUI/rules',
|
||||
};
|
||||
|
||||
const exceptionsBReadcrumb = {
|
||||
const exceptionsBreadcrumb = {
|
||||
text: 'Shared Exception Lists',
|
||||
href: 'securitySolutionUI/exceptions',
|
||||
};
|
||||
|
||||
const manageBreadcrumbs = {
|
||||
text: 'Manage',
|
||||
const settingsBreadcrumb = {
|
||||
text: 'Settings',
|
||||
href: 'securitySolutionUI/administration',
|
||||
};
|
||||
|
||||
|
@ -187,7 +192,7 @@ describe('Navigation Breadcrumbs', () => {
|
|||
getSecuritySolutionUrl
|
||||
);
|
||||
expect(breadcrumbs).toEqual([
|
||||
securityBreadCrumb,
|
||||
securityBreadcrumb,
|
||||
{
|
||||
href: 'securitySolutionUI/dashboards',
|
||||
text: 'Dashboards',
|
||||
|
@ -205,9 +210,9 @@ describe('Navigation Breadcrumbs', () => {
|
|||
getSecuritySolutionUrl
|
||||
);
|
||||
expect(breadcrumbs).toEqual([
|
||||
securityBreadCrumb,
|
||||
exploreBreadcrumbs,
|
||||
hostsBreadcrumbs,
|
||||
securityBreadcrumb,
|
||||
exploreBreadcrumb,
|
||||
hostsBreadcrumb,
|
||||
{
|
||||
href: '',
|
||||
text: 'Authentications',
|
||||
|
@ -221,8 +226,8 @@ describe('Navigation Breadcrumbs', () => {
|
|||
getSecuritySolutionUrl
|
||||
);
|
||||
expect(breadcrumbs).toEqual([
|
||||
securityBreadCrumb,
|
||||
exploreBreadcrumbs,
|
||||
securityBreadcrumb,
|
||||
exploreBreadcrumb,
|
||||
networkBreadcrumb,
|
||||
{
|
||||
text: 'Flows',
|
||||
|
@ -237,7 +242,7 @@ describe('Navigation Breadcrumbs', () => {
|
|||
getSecuritySolutionUrl
|
||||
);
|
||||
expect(breadcrumbs).toEqual([
|
||||
securityBreadCrumb,
|
||||
securityBreadcrumb,
|
||||
{
|
||||
text: 'Timelines',
|
||||
href: '',
|
||||
|
@ -251,9 +256,9 @@ describe('Navigation Breadcrumbs', () => {
|
|||
getSecuritySolutionUrl
|
||||
);
|
||||
expect(breadcrumbs).toEqual([
|
||||
securityBreadCrumb,
|
||||
exploreBreadcrumbs,
|
||||
hostsBreadcrumbs,
|
||||
securityBreadcrumb,
|
||||
exploreBreadcrumb,
|
||||
hostsBreadcrumb,
|
||||
{
|
||||
text: 'siem-kibana',
|
||||
href: 'securitySolutionUI/hosts/name/siem-kibana',
|
||||
|
@ -268,8 +273,8 @@ describe('Navigation Breadcrumbs', () => {
|
|||
getSecuritySolutionUrl
|
||||
);
|
||||
expect(breadcrumbs).toEqual([
|
||||
securityBreadCrumb,
|
||||
exploreBreadcrumbs,
|
||||
securityBreadcrumb,
|
||||
exploreBreadcrumb,
|
||||
networkBreadcrumb,
|
||||
{
|
||||
text: ipv4,
|
||||
|
@ -285,8 +290,8 @@ describe('Navigation Breadcrumbs', () => {
|
|||
getSecuritySolutionUrl
|
||||
);
|
||||
expect(breadcrumbs).toEqual([
|
||||
securityBreadCrumb,
|
||||
exploreBreadcrumbs,
|
||||
securityBreadcrumb,
|
||||
exploreBreadcrumb,
|
||||
networkBreadcrumb,
|
||||
{
|
||||
text: ipv6,
|
||||
|
@ -302,7 +307,7 @@ describe('Navigation Breadcrumbs', () => {
|
|||
getSecuritySolutionUrl
|
||||
);
|
||||
expect(breadcrumbs).toEqual([
|
||||
securityBreadCrumb,
|
||||
securityBreadcrumb,
|
||||
{
|
||||
text: 'Alerts',
|
||||
href: 'securitySolutionUI/alerts',
|
||||
|
@ -320,8 +325,8 @@ describe('Navigation Breadcrumbs', () => {
|
|||
getSecuritySolutionUrl
|
||||
);
|
||||
expect(breadcrumbs).toEqual([
|
||||
securityBreadCrumb,
|
||||
manageBreadcrumbs,
|
||||
securityBreadcrumb,
|
||||
rulesLandingBreadcrumb,
|
||||
{
|
||||
text: 'Shared Exception Lists',
|
||||
href: '',
|
||||
|
@ -335,10 +340,10 @@ describe('Navigation Breadcrumbs', () => {
|
|||
getSecuritySolutionUrl
|
||||
);
|
||||
expect(breadcrumbs).toEqual([
|
||||
securityBreadCrumb,
|
||||
manageBreadcrumbs,
|
||||
securityBreadcrumb,
|
||||
rulesLandingBreadcrumb,
|
||||
{
|
||||
text: 'Rules',
|
||||
...rulesBreadcrumb,
|
||||
href: '',
|
||||
},
|
||||
]);
|
||||
|
@ -350,9 +355,9 @@ describe('Navigation Breadcrumbs', () => {
|
|||
getSecuritySolutionUrl
|
||||
);
|
||||
expect(breadcrumbs).toEqual([
|
||||
securityBreadCrumb,
|
||||
manageBreadcrumbs,
|
||||
rulesBReadcrumb,
|
||||
securityBreadcrumb,
|
||||
rulesLandingBreadcrumb,
|
||||
rulesBreadcrumb,
|
||||
{
|
||||
text: 'Create',
|
||||
href: '',
|
||||
|
@ -374,9 +379,9 @@ describe('Navigation Breadcrumbs', () => {
|
|||
getSecuritySolutionUrl
|
||||
);
|
||||
expect(breadcrumbs).toEqual([
|
||||
securityBreadCrumb,
|
||||
manageBreadcrumbs,
|
||||
rulesBReadcrumb,
|
||||
securityBreadcrumb,
|
||||
rulesLandingBreadcrumb,
|
||||
rulesBreadcrumb,
|
||||
{
|
||||
text: mockRuleName,
|
||||
href: `securitySolutionUI/rules/id/${mockDetailName}`,
|
||||
|
@ -402,9 +407,9 @@ describe('Navigation Breadcrumbs', () => {
|
|||
getSecuritySolutionUrl
|
||||
);
|
||||
expect(breadcrumbs).toEqual([
|
||||
securityBreadCrumb,
|
||||
manageBreadcrumbs,
|
||||
rulesBReadcrumb,
|
||||
securityBreadcrumb,
|
||||
rulesLandingBreadcrumb,
|
||||
rulesBreadcrumb,
|
||||
{
|
||||
text: 'ALERT_RULE_NAME',
|
||||
href: `securitySolutionUI/rules/id/${mockDetailName}`,
|
||||
|
@ -446,8 +451,8 @@ describe('Navigation Breadcrumbs', () => {
|
|||
);
|
||||
|
||||
expect(breadcrumbs).toEqual([
|
||||
securityBreadCrumb,
|
||||
manageBreadcrumbs,
|
||||
securityBreadcrumb,
|
||||
settingsBreadcrumb,
|
||||
{
|
||||
text: 'Endpoints',
|
||||
href: '',
|
||||
|
@ -462,9 +467,9 @@ describe('Navigation Breadcrumbs', () => {
|
|||
);
|
||||
|
||||
expect(breadcrumbs).toEqual([
|
||||
securityBreadCrumb,
|
||||
securityBreadcrumb,
|
||||
{
|
||||
text: 'Manage',
|
||||
...settingsBreadcrumb,
|
||||
href: '',
|
||||
},
|
||||
]);
|
||||
|
@ -485,9 +490,9 @@ describe('Navigation Breadcrumbs', () => {
|
|||
getSecuritySolutionUrl
|
||||
);
|
||||
expect(breadcrumbs).toEqual([
|
||||
securityBreadCrumb,
|
||||
manageBreadcrumbs,
|
||||
exceptionsBReadcrumb,
|
||||
securityBreadcrumb,
|
||||
rulesLandingBreadcrumb,
|
||||
exceptionsBreadcrumb,
|
||||
{
|
||||
text: mockListName,
|
||||
href: ``,
|
||||
|
|
|
@ -0,0 +1,36 @@
|
|||
/*
|
||||
* 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 { LinkCategoryType, type SeparatorLinkCategory } from '@kbn/security-solution-side-nav';
|
||||
import { SecurityPageName } from '../../../../../common';
|
||||
|
||||
export const CATEGORIES: SeparatorLinkCategory[] = [
|
||||
{
|
||||
type: LinkCategoryType.separator,
|
||||
linkIds: [SecurityPageName.dashboards],
|
||||
},
|
||||
{
|
||||
type: LinkCategoryType.separator,
|
||||
linkIds: [
|
||||
SecurityPageName.alerts,
|
||||
SecurityPageName.cloudSecurityPostureFindings,
|
||||
SecurityPageName.case,
|
||||
],
|
||||
},
|
||||
{
|
||||
type: LinkCategoryType.separator,
|
||||
linkIds: [
|
||||
SecurityPageName.timelines,
|
||||
SecurityPageName.threatIntelligenceIndicators,
|
||||
SecurityPageName.exploreLanding,
|
||||
],
|
||||
},
|
||||
{
|
||||
type: LinkCategoryType.separator,
|
||||
linkIds: [SecurityPageName.rulesLanding],
|
||||
},
|
||||
];
|
|
@ -15,11 +15,12 @@ import type { SolutionSideNavProps } from '@kbn/security-solution-side-nav';
|
|||
import type { NavigationLink } from '../../../links/types';
|
||||
import { track } from '../../../lib/telemetry';
|
||||
import { useKibana } from '../../../lib/kibana';
|
||||
import { CATEGORIES } from './categories';
|
||||
|
||||
const manageNavLink: NavigationLink = {
|
||||
const settingsNavLink: NavigationLink = {
|
||||
id: SecurityPageName.administration,
|
||||
title: 'manage',
|
||||
description: 'manage description',
|
||||
title: 'Settings',
|
||||
description: 'Settings description',
|
||||
categories: [{ label: 'test category', linkIds: [SecurityPageName.endpoints] }],
|
||||
links: [
|
||||
{
|
||||
|
@ -38,6 +39,7 @@ const alertsNavLink: NavigationLink = {
|
|||
|
||||
const mockSolutionSideNav = jest.fn((_: SolutionSideNavProps) => <></>);
|
||||
jest.mock('@kbn/security-solution-side-nav', () => ({
|
||||
...jest.requireActual('@kbn/security-solution-side-nav'),
|
||||
SolutionSideNav: (props: SolutionSideNavProps) => mockSolutionSideNav(props),
|
||||
}));
|
||||
jest.mock('../../../lib/kibana');
|
||||
|
@ -80,7 +82,7 @@ const renderNav = () =>
|
|||
describe('SecuritySideNav', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
mockUseNavLinks.mockReturnValue([alertsNavLink, manageNavLink]);
|
||||
mockUseNavLinks.mockReturnValue([alertsNavLink, settingsNavLink]);
|
||||
useKibana().services.chrome.hasHeaderBanner$ = jest.fn(() =>
|
||||
new BehaviorSubject(false).asObservable()
|
||||
);
|
||||
|
@ -96,9 +98,10 @@ describe('SecuritySideNav', () => {
|
|||
id: SecurityPageName.alerts,
|
||||
label: 'alerts',
|
||||
href: '/alerts',
|
||||
position: 'top',
|
||||
},
|
||||
],
|
||||
footerItems: [],
|
||||
categories: CATEGORIES,
|
||||
tracker: track,
|
||||
});
|
||||
});
|
||||
|
@ -121,22 +124,21 @@ describe('SecuritySideNav', () => {
|
|||
});
|
||||
|
||||
it('should render footer items', () => {
|
||||
mockUseNavLinks.mockReturnValue([manageNavLink]);
|
||||
mockUseNavLinks.mockReturnValue([settingsNavLink]);
|
||||
renderNav();
|
||||
expect(mockSolutionSideNav).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
items: [],
|
||||
footerItems: [
|
||||
items: [
|
||||
{
|
||||
id: SecurityPageName.administration,
|
||||
label: 'manage',
|
||||
label: 'Settings',
|
||||
href: '/administration',
|
||||
categories: manageNavLink.categories,
|
||||
categories: settingsNavLink.categories,
|
||||
position: 'bottom',
|
||||
items: [
|
||||
{
|
||||
id: SecurityPageName.endpoints,
|
||||
label: 'title 2',
|
||||
description: 'description 2',
|
||||
href: '/endpoints',
|
||||
isBeta: true,
|
||||
},
|
||||
|
@ -148,12 +150,11 @@ describe('SecuritySideNav', () => {
|
|||
});
|
||||
|
||||
it('should not render disabled items', () => {
|
||||
mockUseNavLinks.mockReturnValue([{ ...alertsNavLink, disabled: true }, manageNavLink]);
|
||||
mockUseNavLinks.mockReturnValue([{ ...alertsNavLink, disabled: true }, settingsNavLink]);
|
||||
renderNav();
|
||||
expect(mockSolutionSideNav).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
items: [],
|
||||
footerItems: [
|
||||
items: [
|
||||
expect.objectContaining({
|
||||
id: SecurityPageName.administration,
|
||||
}),
|
||||
|
@ -162,17 +163,18 @@ describe('SecuritySideNav', () => {
|
|||
);
|
||||
});
|
||||
|
||||
it('should render custom item', () => {
|
||||
mockUseNavLinks.mockReturnValue([{ id: SecurityPageName.landing, title: 'get started' }]);
|
||||
it('should render get started item', () => {
|
||||
mockUseNavLinks.mockReturnValue([
|
||||
{ id: SecurityPageName.landing, title: 'Get started', sideNavIcon: 'launch' },
|
||||
]);
|
||||
renderNav();
|
||||
expect(mockSolutionSideNav).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
items: [],
|
||||
footerItems: [
|
||||
items: [
|
||||
expect.objectContaining({
|
||||
id: SecurityPageName.landing,
|
||||
label: 'GET STARTED',
|
||||
labelSize: 'xs',
|
||||
label: 'Get started',
|
||||
position: 'bottom',
|
||||
iconType: 'launch',
|
||||
appendSeparator: true,
|
||||
}),
|
||||
|
|
|
@ -7,25 +7,79 @@
|
|||
|
||||
import React, { useMemo } from 'react';
|
||||
import { EuiLoadingSpinner, useEuiTheme } from '@elastic/eui';
|
||||
import { SolutionSideNav, type SolutionSideNavItem } from '@kbn/security-solution-side-nav';
|
||||
import {
|
||||
SolutionSideNav,
|
||||
SolutionSideNavItemPosition,
|
||||
type SolutionSideNavItem,
|
||||
} from '@kbn/security-solution-side-nav';
|
||||
import useObservable from 'react-use/lib/useObservable';
|
||||
import { SecurityPageName } from '../../../../app/types';
|
||||
import type { NavigationLink } from '../../../links';
|
||||
import { getAncestorLinksInfo } from '../../../links';
|
||||
import { useRouteSpy } from '../../../utils/route/use_route_spy';
|
||||
import { useGetSecuritySolutionLinkProps } from '../../links';
|
||||
import { useGetSecuritySolutionLinkProps, type GetSecuritySolutionProps } from '../../links';
|
||||
import { useNavLinks } from '../../../links/nav_links';
|
||||
import { useShowTimeline } from '../../../utils/timeline/use_show_timeline';
|
||||
import { useIsPolicySettingsBarVisible } from '../../../../management/pages/policy/view/policy_hooks';
|
||||
import { track } from '../../../lib/telemetry';
|
||||
import { useKibana } from '../../../lib/kibana';
|
||||
import { CATEGORIES } from './categories';
|
||||
|
||||
export const EUI_HEADER_HEIGHT = '93px';
|
||||
export const BOTTOM_BAR_HEIGHT = '50px';
|
||||
|
||||
const isFooterNavItem = (id: SecurityPageName) =>
|
||||
id === SecurityPageName.landing || id === SecurityPageName.administration;
|
||||
const getNavItemPosition = (id: SecurityPageName): SolutionSideNavItemPosition =>
|
||||
id === SecurityPageName.landing || id === SecurityPageName.administration
|
||||
? SolutionSideNavItemPosition.bottom
|
||||
: SolutionSideNavItemPosition.top;
|
||||
|
||||
const isGetStartedNavItem = (id: SecurityPageName) => id === SecurityPageName.landing;
|
||||
|
||||
/**
|
||||
* Formats generic navigation links into the shape expected by the `SolutionSideNav`
|
||||
*/
|
||||
const formatLink = (
|
||||
navLink: NavigationLink,
|
||||
getSecuritySolutionLinkProps: GetSecuritySolutionProps
|
||||
): SolutionSideNavItem => ({
|
||||
id: navLink.id,
|
||||
label: navLink.title,
|
||||
position: getNavItemPosition(navLink.id),
|
||||
...getSecuritySolutionLinkProps({ deepLinkId: navLink.id }),
|
||||
...(navLink.sideNavIcon && { iconType: navLink.sideNavIcon }),
|
||||
...(navLink.categories?.length && { categories: navLink.categories }),
|
||||
...(navLink.links?.length && {
|
||||
items: navLink.links.reduce<SolutionSideNavItem[]>((acc, current) => {
|
||||
if (!current.disabled) {
|
||||
acc.push({
|
||||
id: current.id,
|
||||
label: current.title,
|
||||
iconType: current.sideNavIcon,
|
||||
isBeta: current.isBeta,
|
||||
betaOptions: current.betaOptions,
|
||||
...getSecuritySolutionLinkProps({ deepLinkId: current.id }),
|
||||
});
|
||||
}
|
||||
return acc;
|
||||
}, []),
|
||||
}),
|
||||
});
|
||||
|
||||
/**
|
||||
* Formats the get started navigation links into the shape expected by the `SolutionSideNav`
|
||||
*/
|
||||
const formatGetStartedLink = (
|
||||
navLink: NavigationLink,
|
||||
getSecuritySolutionLinkProps: GetSecuritySolutionProps
|
||||
): SolutionSideNavItem => ({
|
||||
id: navLink.id,
|
||||
label: navLink.title,
|
||||
iconType: navLink.sideNavIcon,
|
||||
position: SolutionSideNavItemPosition.bottom,
|
||||
appendSeparator: true,
|
||||
...getSecuritySolutionLinkProps({ deepLinkId: navLink.id }),
|
||||
});
|
||||
|
||||
/**
|
||||
* Returns the formatted `items` and `footerItems` to be rendered in the navigation
|
||||
*/
|
||||
|
@ -34,60 +88,21 @@ const useSolutionSideNavItems = () => {
|
|||
const getSecuritySolutionLinkProps = useGetSecuritySolutionLinkProps(); // adds href and onClick props
|
||||
|
||||
const sideNavItems = useMemo(() => {
|
||||
const mainNavItems: SolutionSideNavItem[] = [];
|
||||
const footerNavItems: SolutionSideNavItem[] = [];
|
||||
navLinks.forEach((navLink) => {
|
||||
if (!navLinks?.length) {
|
||||
return undefined;
|
||||
}
|
||||
return navLinks.reduce<SolutionSideNavItem[]>((navItems, navLink) => {
|
||||
if (navLink.disabled) {
|
||||
return;
|
||||
return navItems;
|
||||
}
|
||||
|
||||
let sideNavItem: SolutionSideNavItem;
|
||||
if (isGetStartedNavItem(navLink.id)) {
|
||||
sideNavItem = {
|
||||
id: navLink.id,
|
||||
label: navLink.title.toUpperCase(),
|
||||
labelSize: 'xs',
|
||||
iconType: 'launch',
|
||||
...getSecuritySolutionLinkProps({
|
||||
deepLinkId: navLink.id,
|
||||
}),
|
||||
appendSeparator: true,
|
||||
};
|
||||
navItems.push(formatGetStartedLink(navLink, getSecuritySolutionLinkProps));
|
||||
} else {
|
||||
// generic links
|
||||
sideNavItem = {
|
||||
id: navLink.id,
|
||||
label: navLink.title,
|
||||
...getSecuritySolutionLinkProps({
|
||||
deepLinkId: navLink.id,
|
||||
}),
|
||||
...(navLink.categories?.length && { categories: navLink.categories }),
|
||||
...(navLink.links?.length && {
|
||||
items: navLink.links.reduce<SolutionSideNavItem[]>((acc, current) => {
|
||||
if (!current.disabled) {
|
||||
acc.push({
|
||||
id: current.id,
|
||||
label: current.title,
|
||||
description: current.description,
|
||||
isBeta: current.isBeta,
|
||||
betaOptions: current.betaOptions,
|
||||
...getSecuritySolutionLinkProps({ deepLinkId: current.id }),
|
||||
});
|
||||
}
|
||||
return acc;
|
||||
}, []),
|
||||
}),
|
||||
};
|
||||
navItems.push(formatLink(navLink, getSecuritySolutionLinkProps));
|
||||
}
|
||||
|
||||
if (isFooterNavItem(navLink.id)) {
|
||||
footerNavItems.push(sideNavItem);
|
||||
} else {
|
||||
mainNavItems.push(sideNavItem);
|
||||
}
|
||||
});
|
||||
|
||||
return [mainNavItems, footerNavItems];
|
||||
return navItems;
|
||||
}, []);
|
||||
}, [navLinks, getSecuritySolutionLinkProps]);
|
||||
|
||||
return sideNavItems;
|
||||
|
@ -123,19 +138,19 @@ const usePanelBottomOffset = (): string | undefined => {
|
|||
* It takes the links to render from the generic application `links` configs.
|
||||
*/
|
||||
export const SecuritySideNav: React.FC = () => {
|
||||
const [items, footerItems] = useSolutionSideNavItems();
|
||||
const items = useSolutionSideNavItems();
|
||||
const selectedId = useSelectedId();
|
||||
const panelTopOffset = usePanelTopOffset();
|
||||
const panelBottomOffset = usePanelBottomOffset();
|
||||
|
||||
if (items.length === 0 && footerItems.length === 0) {
|
||||
if (!items) {
|
||||
return <EuiLoadingSpinner size="m" data-test-subj="sideNavLoader" />;
|
||||
}
|
||||
|
||||
return (
|
||||
<SolutionSideNav
|
||||
items={items}
|
||||
footerItems={footerItems}
|
||||
categories={CATEGORIES}
|
||||
selectedId={selectedId}
|
||||
panelTopOffset={panelTopOffset}
|
||||
panelBottomOffset={panelBottomOffset}
|
||||
|
|
|
@ -7,7 +7,14 @@
|
|||
import type { SVGProps } from 'react';
|
||||
import React from 'react';
|
||||
export const IconCloudDefend: React.FC<SVGProps<SVGSVGElement>> = ({ ...props }) => (
|
||||
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<svg
|
||||
width="32"
|
||||
height="32"
|
||||
viewBox="0 0 32 32"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...props}
|
||||
>
|
||||
<g clipPath="url(#clip0_327_292367)">
|
||||
<path
|
||||
fillRule="evenodd"
|
|
@ -6,7 +6,7 @@
|
|||
*/
|
||||
import type { SVGProps } from 'react';
|
||||
import React from 'react';
|
||||
export const IconActionHistory: React.FC<SVGProps<SVGSVGElement>> = ({ ...props }) => (
|
||||
export const IconConsole: React.FC<SVGProps<SVGSVGElement>> = ({ ...props }) => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={16}
|
|
@ -6,7 +6,7 @@
|
|||
*/
|
||||
import type { SVGProps } from 'react';
|
||||
import React from 'react';
|
||||
export const IconExceptionLists: React.FC<SVGProps<SVGSVGElement>> = ({ ...props }) => (
|
||||
export const IconConsoleCloud: React.FC<SVGProps<SVGSVGElement>> = ({ ...props }) => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={16}
|
|
@ -6,7 +6,7 @@
|
|||
*/
|
||||
import type { SVGProps } from 'react';
|
||||
import React from 'react';
|
||||
export const IconTrustedApplications: React.FC<SVGProps<SVGSVGElement>> = ({ ...props }) => (
|
||||
export const IconDashboards: React.FC<SVGProps<SVGSVGElement>> = ({ ...props }) => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={16}
|
|
@ -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 type { SVGProps } from 'react';
|
||||
import React from 'react';
|
||||
export const IconPipeline: React.FC<SVGProps<SVGSVGElement>> = ({ ...props }) => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={16}
|
||||
height={16}
|
||||
fill="none"
|
||||
viewBox="0 0 32 32"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M30 23H29C28.449 23 28 22.552 28 22V14C28 13.448 28.449 13 29 13H30V23ZM20 20V22C20 22.552 19.551 23 19 23H13C12.449 23 12 22.552 12 22V20H6V16H12V14C12 13.448 12.449 13 13 13H19C19.551 13 20 13.448 20 14V16H26V20H20ZM3 23H2V13H3C3.551 13 4 13.448 4 14V22C4 22.552 3.551 23 3 23ZM29 11C27.346 11 26 12.346 26 14H22C22 12.346 20.654 11 19 11H13C11.346 11 10 12.346 10 14H6C6 12.346 4.654 11 3 11H0V25H3C4.654 25 6 23.654 6 22H10C10 23.654 11.346 25 13 25H19C20.654 25 22 23.654 22 22H26C26 23.654 27.346 25 29 25H32V11H29Z"
|
||||
fill="#535766"
|
||||
/>
|
||||
<path fillRule="evenodd" clipRule="evenodd" d="M22 5H10V7H15V9H17V7H22V5Z" fill="#00BFB3" />
|
||||
</svg>
|
||||
);
|
|
@ -6,7 +6,7 @@
|
|||
*/
|
||||
import type { SVGProps } from 'react';
|
||||
import React from 'react';
|
||||
export const IconSiemRules: React.FC<SVGProps<SVGSVGElement>> = ({ ...props }) => (
|
||||
export const IconRollup: React.FC<SVGProps<SVGSVGElement>> = ({ ...props }) => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={16}
|
|
@ -6,7 +6,7 @@
|
|||
*/
|
||||
import type { SVGProps } from 'react';
|
||||
import React from 'react';
|
||||
export const IconHostIsolation: React.FC<SVGProps<SVGSVGElement>> = ({ ...props }) => (
|
||||
export const IconSavedObject: React.FC<SVGProps<SVGSVGElement>> = ({ ...props }) => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={16}
|
|
@ -6,7 +6,7 @@
|
|||
*/
|
||||
import type { SVGProps } from 'react';
|
||||
import React from 'react';
|
||||
export const IconBlocklist: React.FC<SVGProps<SVGSVGElement>> = ({ ...props }) => (
|
||||
export const IconShield: React.FC<SVGProps<SVGSVGElement>> = ({ ...props }) => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={16}
|
|
@ -6,7 +6,7 @@
|
|||
*/
|
||||
import type { SVGProps } from 'react';
|
||||
import React from 'react';
|
||||
export const IconEndpointPolicies: React.FC<SVGProps<SVGSVGElement>> = ({ ...props }) => (
|
||||
export const IconTool: React.FC<SVGProps<SVGSVGElement>> = ({ ...props }) => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={16}
|
||||
|
@ -18,8 +18,9 @@ export const IconEndpointPolicies: React.FC<SVGProps<SVGSVGElement>> = ({ ...pro
|
|||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M16 28.0001H14V19.5081L14.6 19.2461C17.88 17.8121 20 14.5751 20 11.0001C20 7.96306 18.471 5.17106 16 3.52106V11.0001H6V3.52106C3.529 5.17106 2 7.96306 2 11.0001C2 14.5751 4.12 17.8121 7.4 19.2461L8 19.5081V28.0001H6V20.7951C2.334 18.9241 0 15.1481 0 11.0001C0 6.63006 2.591 2.67406 6.6 0.922059L8 0.310059V9.00006H14V0.310059L15.4 0.922059C19.409 2.67406 22 6.63006 22 11.0001C22 15.1481 19.666 18.9241 16 20.7951V28.0001Z"
|
||||
d="M16 28H14V19.508L14.6 19.246C17.88 17.812 20 14.575 20 11C20 7.963 18.471 5.171 16 3.521V11H6V3.521C3.529 5.171 2 7.963 2 11C2 14.575 4.12 17.812 7.4 19.246L8 19.508V28H6V20.795C2.334 18.924 0 15.148 0 11C0 6.63 2.591 2.674 6.6 0.921998L8 0.309998V9H14V0.309998L15.4 0.921998C19.409 2.674 22 6.63 22 11C22 15.148 19.666 18.924 16 20.795V28Z"
|
||||
fill="#535766"
|
||||
/>
|
||||
<path fillRule="evenodd" clipRule="evenodd" d="M6 32H16V30H6V32Z" fill="#00BFB3" />
|
||||
</svg>
|
||||
);
|
|
@ -7,26 +7,26 @@
|
|||
import type { CoreStart } from '@kbn/core/public';
|
||||
import type { AppLinkItems } from './types';
|
||||
import { indicatorsLinks } from '../../threat_intelligence/links';
|
||||
import { links as detectionLinks } from '../../detections/links';
|
||||
import { links as alertsLinks } from '../../detections/links';
|
||||
import { links as rulesLinks } from '../../rules/links';
|
||||
import { links as timelinesLinks } from '../../timelines/links';
|
||||
import { getCasesLinkItems } from '../../cases/links';
|
||||
import { links as casesLinks } from '../../cases/links';
|
||||
import { links as managementLinks, getManagementFilteredLinks } from '../../management/links';
|
||||
import { exploreLinks } from '../../explore/links';
|
||||
import { gettingStartedLinks } from '../../overview/links';
|
||||
import { rootLinks as cloudSecurityPostureRootLinks } from '../../cloud_security_posture/links';
|
||||
import { findingsLinks } from '../../cloud_security_posture/links';
|
||||
import type { StartPlugins } from '../../types';
|
||||
import { dashboardsLandingLinks } from '../../dashboards/links';
|
||||
import { dashboardsLinks } from '../../dashboards/links';
|
||||
|
||||
const casesLinks = getCasesLinkItems();
|
||||
|
||||
export const links = Object.freeze([
|
||||
dashboardsLandingLinks,
|
||||
detectionLinks,
|
||||
cloudSecurityPostureRootLinks,
|
||||
timelinesLinks,
|
||||
export const links: AppLinkItems = Object.freeze([
|
||||
dashboardsLinks,
|
||||
alertsLinks,
|
||||
findingsLinks,
|
||||
casesLinks,
|
||||
exploreLinks,
|
||||
timelinesLinks,
|
||||
indicatorsLinks,
|
||||
exploreLinks,
|
||||
rulesLinks,
|
||||
gettingStartedLinks,
|
||||
managementLinks,
|
||||
]);
|
||||
|
@ -38,13 +38,14 @@ export const getFilteredLinks = async (
|
|||
const managementFilteredLinks = await getManagementFilteredLinks(core, plugins);
|
||||
|
||||
return Object.freeze([
|
||||
dashboardsLandingLinks,
|
||||
detectionLinks,
|
||||
cloudSecurityPostureRootLinks,
|
||||
timelinesLinks,
|
||||
dashboardsLinks,
|
||||
alertsLinks,
|
||||
findingsLinks,
|
||||
casesLinks,
|
||||
exploreLinks,
|
||||
timelinesLinks,
|
||||
indicatorsLinks,
|
||||
exploreLinks,
|
||||
rulesLinks,
|
||||
gettingStartedLinks,
|
||||
managementFilteredLinks,
|
||||
]);
|
||||
|
|
|
@ -42,9 +42,9 @@ describe('formatNavigationLinks', () => {
|
|||
Object {
|
||||
"description": "description 2",
|
||||
"disabled": true,
|
||||
"icon": "someicon",
|
||||
"id": "endpoints",
|
||||
"image": "someimage",
|
||||
"landingIcon": "someicon",
|
||||
"landingImage": "someimage",
|
||||
"skipUrlState": true,
|
||||
"title": "title 2",
|
||||
},
|
||||
|
|
|
@ -12,14 +12,15 @@ import type { SecurityPageName } from '../../app/types';
|
|||
import type { AppLinkItems, NavigationLink } from './types';
|
||||
|
||||
export const formatNavigationLinks = (appLinks: AppLinkItems): NavigationLink[] =>
|
||||
appLinks.map((link) => ({
|
||||
appLinks.map<NavigationLink>((link) => ({
|
||||
id: link.id,
|
||||
title: link.title,
|
||||
...(link.categories != null ? { categories: link.categories } : {}),
|
||||
...(link.description != null ? { description: link.description } : {}),
|
||||
...(link.sideNavDisabled === true ? { disabled: true } : {}),
|
||||
...(link.landingIcon != null ? { icon: link.landingIcon } : {}),
|
||||
...(link.landingImage != null ? { image: link.landingImage } : {}),
|
||||
...(link.landingIcon != null ? { landingIcon: link.landingIcon } : {}),
|
||||
...(link.landingImage != null ? { landingImage: link.landingImage } : {}),
|
||||
...(link.sideNavIcon != null ? { sideNavIcon: link.sideNavIcon } : {}),
|
||||
...(link.skipUrlState != null ? { skipUrlState: link.skipUrlState } : {}),
|
||||
...(link.isBeta != null ? { isBeta: link.isBeta } : {}),
|
||||
...(link.betaOptions != null ? { betaOptions: link.betaOptions } : {}),
|
||||
|
|
|
@ -8,6 +8,10 @@
|
|||
import type { Capabilities } from '@kbn/core/types';
|
||||
import type { ILicense, LicenseType } from '@kbn/licensing-plugin/common/types';
|
||||
import type { IconType } from '@elastic/eui';
|
||||
import type {
|
||||
LinkCategory as BaseLinkCategory,
|
||||
LinkCategories as BaseLinkCategories,
|
||||
} from '@kbn/security-solution-side-nav';
|
||||
import type { ExperimentalFeatures } from '../../../common/experimental_features';
|
||||
import type { SecurityPageName } from '../../../common/constants';
|
||||
import type { UpsellingService } from '../lib/upsellings';
|
||||
|
@ -23,12 +27,8 @@ export interface LinksPermissions {
|
|||
license?: ILicense;
|
||||
}
|
||||
|
||||
export interface LinkCategory {
|
||||
label: string;
|
||||
linkIds: readonly SecurityPageName[];
|
||||
}
|
||||
|
||||
export type LinkCategories = Readonly<LinkCategory[]>;
|
||||
export type LinkCategory = BaseLinkCategory<SecurityPageName>;
|
||||
export type LinkCategories = BaseLinkCategories<SecurityPageName>;
|
||||
|
||||
export interface LinkItem {
|
||||
/**
|
||||
|
@ -119,6 +119,10 @@ export interface LinkItem {
|
|||
* Disables link in the side navigation. Defaults to false.
|
||||
*/
|
||||
sideNavDisabled?: boolean;
|
||||
/**
|
||||
* Icon that is displayed on the side navigation menu.
|
||||
*/
|
||||
sideNavIcon?: IconType;
|
||||
/**
|
||||
* Disables the state query string in the URL. Defaults to false.
|
||||
*/
|
||||
|
@ -143,11 +147,12 @@ export interface NavigationLink {
|
|||
categories?: LinkCategories;
|
||||
description?: string;
|
||||
disabled?: boolean;
|
||||
icon?: IconType;
|
||||
id: SecurityPageName;
|
||||
landingIcon?: IconType;
|
||||
landingImage?: string;
|
||||
links?: NavigationLink[];
|
||||
image?: string;
|
||||
title: string;
|
||||
sideNavIcon?: IconType;
|
||||
skipUrlState?: boolean;
|
||||
unauthorized?: boolean;
|
||||
isBeta?: boolean;
|
||||
|
|
|
@ -9,18 +9,26 @@ import { DASHBOARDS_PATH, SecurityPageName, SERVER_APP_ID } from '../../common/c
|
|||
import { DASHBOARDS } from '../app/translations';
|
||||
import type { LinkItem } from '../common/links/types';
|
||||
import { links as kubernetesLinks } from '../kubernetes/links';
|
||||
import {
|
||||
dashboardLinks as cloudSecurityPostureLinks,
|
||||
vulnerabilityDashboardLink,
|
||||
} from '../cloud_security_posture/links';
|
||||
import { cspDashboardLink, vulnerabilityDashboardLink } from '../cloud_security_posture/links';
|
||||
import {
|
||||
ecsDataQualityDashboardLinks,
|
||||
detectionResponseLinks,
|
||||
entityAnalyticsLinks,
|
||||
overviewLinks,
|
||||
} from '../overview/links';
|
||||
import { IconDashboards } from '../common/icons/dashboards';
|
||||
|
||||
export const dashboardsLandingLinks: LinkItem = {
|
||||
const subLinks: LinkItem[] = [
|
||||
overviewLinks,
|
||||
detectionResponseLinks,
|
||||
kubernetesLinks,
|
||||
cspDashboardLink,
|
||||
vulnerabilityDashboardLink,
|
||||
entityAnalyticsLinks,
|
||||
ecsDataQualityDashboardLinks,
|
||||
].map((link) => ({ ...link, sideNavIcon: IconDashboards }));
|
||||
|
||||
export const dashboardsLinks: LinkItem = {
|
||||
id: SecurityPageName.dashboards,
|
||||
title: DASHBOARDS,
|
||||
path: DASHBOARDS_PATH,
|
||||
|
@ -31,14 +39,6 @@ export const dashboardsLandingLinks: LinkItem = {
|
|||
defaultMessage: 'Dashboards',
|
||||
}),
|
||||
],
|
||||
links: [
|
||||
overviewLinks,
|
||||
detectionResponseLinks,
|
||||
kubernetesLinks,
|
||||
cloudSecurityPostureLinks,
|
||||
vulnerabilityDashboardLink,
|
||||
entityAnalyticsLinks,
|
||||
ecsDataQualityDashboardLinks,
|
||||
],
|
||||
links: subLinks,
|
||||
skipUrlState: false,
|
||||
};
|
||||
|
|
|
@ -36,13 +36,13 @@ const APP_DASHBOARD_LINKS: NavigationLink = {
|
|||
id: SecurityPageName.overview,
|
||||
title: OVERVIEW_ITEM_LABEL,
|
||||
description: '',
|
||||
icon: 'testIcon1',
|
||||
landingIcon: 'testIcon1',
|
||||
},
|
||||
{
|
||||
id: SecurityPageName.detectionAndResponse,
|
||||
title: DETECTION_RESPONSE_ITEM_LABEL,
|
||||
description: '',
|
||||
icon: 'testIcon2',
|
||||
landingIcon: 'testIcon2',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
|
|
@ -188,5 +188,6 @@ export const exploreLinks: LinkItem = {
|
|||
}),
|
||||
],
|
||||
links: [hostsLinks, networkLinks, usersLinks],
|
||||
hideTimeline: true,
|
||||
skipUrlState: true,
|
||||
};
|
||||
|
|
|
@ -1,28 +0,0 @@
|
|||
/*
|
||||
* 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 { SVGProps } from 'react';
|
||||
import React from 'react';
|
||||
export const IconEventFilters: React.FC<SVGProps<SVGSVGElement>> = ({ ...props }) => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={16}
|
||||
height={16}
|
||||
fill="none"
|
||||
viewBox="0 0 32 32"
|
||||
{...props}
|
||||
>
|
||||
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M30 23H29C28.449 23 28 22.552 28 22V14C28 13.448 28.449 13 29 13H30V23ZM20 20V22C20 22.552 19.551 23 19 23H13C12.449 23 12 22.552 12 22V20H6V16H12V14C12 13.448 12.449 13 13 13H19C19.551 13 20 13.448 20 14V16H26V20H20ZM3 23H2V13H3C3.551 13 4 13.448 4 14V22C4 22.552 3.551 23 3 23ZM29 11C27.346 11 26 12.346 26 14H22C22 12.346 20.654 11 19 11H13C11.346 11 10 12.346 10 14H6C6 12.346 4.654 11 3 11H0V25H3C4.654 25 6 23.654 6 22H10C10 23.654 11.346 25 13 25H19C20.654 25 22 23.654 22 22H26C26 23.654 27.346 25 29 25H32V11H29Z"
|
||||
fill="#535766"
|
||||
/>
|
||||
<path fillRule="evenodd" clipRule="evenodd" d="M22 5H10V7H15V9H17V7H22V5Z" fill="#00BFB3" />
|
||||
</svg>
|
||||
</svg>
|
||||
);
|
|
@ -17,61 +17,41 @@ import {
|
|||
BLOCKLIST_PATH,
|
||||
ENDPOINTS_PATH,
|
||||
EVENT_FILTERS_PATH,
|
||||
EXCEPTIONS_PATH,
|
||||
HOST_ISOLATION_EXCEPTIONS_PATH,
|
||||
MANAGE_PATH,
|
||||
POLICIES_PATH,
|
||||
RESPONSE_ACTIONS_HISTORY_PATH,
|
||||
RULES_ADD_PATH,
|
||||
RULES_CREATE_PATH,
|
||||
RULES_PATH,
|
||||
SecurityPageName,
|
||||
SERVER_APP_ID,
|
||||
TRUSTED_APPS_PATH,
|
||||
} from '../../common/constants';
|
||||
import {
|
||||
ADD_RULES,
|
||||
BLOCKLIST,
|
||||
CREATE_NEW_RULE,
|
||||
ENDPOINTS,
|
||||
EVENT_FILTERS,
|
||||
EXCEPTIONS,
|
||||
HOST_ISOLATION_EXCEPTIONS,
|
||||
MANAGE,
|
||||
SETTINGS,
|
||||
POLICIES,
|
||||
RESPONSE_ACTIONS_HISTORY,
|
||||
RULES,
|
||||
TRUSTED_APPLICATIONS,
|
||||
} from '../app/translations';
|
||||
import { licenseService } from '../common/hooks/use_license';
|
||||
import type { LinkItem } from '../common/links/types';
|
||||
import type { StartPlugins } from '../types';
|
||||
import {
|
||||
manageCategories as cloudSecurityPostureCategories,
|
||||
manageLinks as cloudSecurityPostureLinks,
|
||||
} from '../cloud_security_posture/links';
|
||||
import { manageLinks as cloudDefendLinks } from '../cloud_defend/links';
|
||||
import { IconActionHistory } from './icons/action_history';
|
||||
import { IconBlocklist } from './icons/blocklist';
|
||||
import { IconEndpoints } from './icons/endpoints';
|
||||
import { IconEndpointPolicies } from './icons/endpoint_policies';
|
||||
import { IconEventFilters } from './icons/event_filters';
|
||||
import { IconExceptionLists } from './icons/exception_lists';
|
||||
import { IconHostIsolation } from './icons/host_isolation';
|
||||
import { IconSiemRules } from './icons/siem_rules';
|
||||
import { IconTrustedApplications } from './icons/trusted_applications';
|
||||
import { cloudDefendLink } from '../cloud_defend/links';
|
||||
import { IconConsole } from '../common/icons/console';
|
||||
import { IconShield } from '../common/icons/shield';
|
||||
import { IconEndpoints } from '../common/icons/endpoints';
|
||||
import { IconTool } from '../common/icons/tool';
|
||||
import { IconPipeline } from '../common/icons/pipeline';
|
||||
import { IconSavedObject } from '../common/icons/saved_object';
|
||||
import { IconDashboards } from '../common/icons/dashboards';
|
||||
import { HostIsolationExceptionsApiClient } from './pages/host_isolation_exceptions/host_isolation_exceptions_api_client';
|
||||
|
||||
const categories = [
|
||||
{
|
||||
label: i18n.translate('xpack.securitySolution.appLinks.category.siem', {
|
||||
defaultMessage: 'SIEM',
|
||||
}),
|
||||
linkIds: [SecurityPageName.rules, SecurityPageName.exceptions],
|
||||
},
|
||||
{
|
||||
label: i18n.translate('xpack.securitySolution.appLinks.category.endpoints', {
|
||||
defaultMessage: 'ENDPOINTS',
|
||||
defaultMessage: 'Endpoints',
|
||||
}),
|
||||
linkIds: [
|
||||
SecurityPageName.endpoints,
|
||||
|
@ -83,73 +63,29 @@ const categories = [
|
|||
SecurityPageName.responseActionsHistory,
|
||||
],
|
||||
},
|
||||
...cloudSecurityPostureCategories,
|
||||
{
|
||||
label: i18n.translate('xpack.securitySolution.appLinks.category.cloudSecurity', {
|
||||
defaultMessage: 'Cloud Security',
|
||||
}),
|
||||
linkIds: [cloudDefendLink.id],
|
||||
},
|
||||
];
|
||||
|
||||
export const links: LinkItem = {
|
||||
id: SecurityPageName.administration,
|
||||
title: MANAGE,
|
||||
title: SETTINGS,
|
||||
path: MANAGE_PATH,
|
||||
skipUrlState: true,
|
||||
hideTimeline: true,
|
||||
globalNavPosition: 8,
|
||||
capabilities: [`${SERVER_APP_ID}.show`],
|
||||
globalSearchKeywords: [
|
||||
i18n.translate('xpack.securitySolution.appLinks.manage', {
|
||||
defaultMessage: 'Manage',
|
||||
i18n.translate('xpack.securitySolution.appLinks.settings', {
|
||||
defaultMessage: 'Settings',
|
||||
}),
|
||||
],
|
||||
categories,
|
||||
links: [
|
||||
{
|
||||
id: SecurityPageName.rules,
|
||||
title: RULES,
|
||||
description: i18n.translate('xpack.securitySolution.appLinks.rulesDescription', {
|
||||
defaultMessage:
|
||||
"Create and manage rules to check for suspicious source events, and create alerts when a rule's conditions are met.",
|
||||
}),
|
||||
|
||||
landingIcon: IconSiemRules,
|
||||
path: RULES_PATH,
|
||||
globalSearchKeywords: [
|
||||
i18n.translate('xpack.securitySolution.appLinks.rules', {
|
||||
defaultMessage: 'Rules',
|
||||
}),
|
||||
],
|
||||
links: [
|
||||
{
|
||||
id: SecurityPageName.rulesAdd,
|
||||
title: ADD_RULES,
|
||||
path: RULES_ADD_PATH,
|
||||
skipUrlState: true,
|
||||
hideTimeline: true,
|
||||
},
|
||||
{
|
||||
id: SecurityPageName.rulesCreate,
|
||||
title: CREATE_NEW_RULE,
|
||||
path: RULES_CREATE_PATH,
|
||||
skipUrlState: true,
|
||||
hideTimeline: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: SecurityPageName.exceptions,
|
||||
title: EXCEPTIONS,
|
||||
description: i18n.translate('xpack.securitySolution.appLinks.exceptionsDescription', {
|
||||
defaultMessage:
|
||||
'Create and manage shared exception lists to prevent the creation of unwanted alerts.',
|
||||
}),
|
||||
landingIcon: IconExceptionLists,
|
||||
path: EXCEPTIONS_PATH,
|
||||
skipUrlState: true,
|
||||
hideTimeline: true,
|
||||
globalSearchKeywords: [
|
||||
i18n.translate('xpack.securitySolution.appLinks.exceptions', {
|
||||
defaultMessage: 'Exception lists',
|
||||
}),
|
||||
],
|
||||
},
|
||||
{
|
||||
id: SecurityPageName.endpoints,
|
||||
description: i18n.translate('xpack.securitySolution.appLinks.endpointsDescription', {
|
||||
|
@ -168,7 +104,7 @@ export const links: LinkItem = {
|
|||
defaultMessage:
|
||||
'Use policies to customize endpoint and cloud workload protections and other configurations.',
|
||||
}),
|
||||
landingIcon: IconEndpointPolicies,
|
||||
landingIcon: IconTool,
|
||||
path: POLICIES_PATH,
|
||||
skipUrlState: true,
|
||||
hideTimeline: true,
|
||||
|
@ -183,7 +119,7 @@ export const links: LinkItem = {
|
|||
'Improve performance or alleviate conflicts with other applications running on your hosts.',
|
||||
}
|
||||
),
|
||||
landingIcon: IconTrustedApplications,
|
||||
landingIcon: IconDashboards,
|
||||
path: TRUSTED_APPS_PATH,
|
||||
skipUrlState: true,
|
||||
hideTimeline: true,
|
||||
|
@ -194,7 +130,7 @@ export const links: LinkItem = {
|
|||
description: i18n.translate('xpack.securitySolution.appLinks.eventFiltersDescription', {
|
||||
defaultMessage: 'Exclude high volume or unwanted events being written into Elasticsearch.',
|
||||
}),
|
||||
landingIcon: IconEventFilters,
|
||||
landingIcon: IconPipeline,
|
||||
path: EVENT_FILTERS_PATH,
|
||||
skipUrlState: true,
|
||||
hideTimeline: true,
|
||||
|
@ -205,7 +141,7 @@ export const links: LinkItem = {
|
|||
description: i18n.translate('xpack.securitySolution.appLinks.hostIsolationDescription', {
|
||||
defaultMessage: 'Allow isolated hosts to communicate with specific IPs.',
|
||||
}),
|
||||
landingIcon: IconHostIsolation,
|
||||
landingIcon: IconSavedObject,
|
||||
path: HOST_ISOLATION_EXCEPTIONS_PATH,
|
||||
skipUrlState: true,
|
||||
hideTimeline: true,
|
||||
|
@ -216,7 +152,7 @@ export const links: LinkItem = {
|
|||
description: i18n.translate('xpack.securitySolution.appLinks.blocklistDescription', {
|
||||
defaultMessage: 'Exclude unwanted applications from running on your hosts.',
|
||||
}),
|
||||
landingIcon: IconBlocklist,
|
||||
landingIcon: IconShield,
|
||||
path: BLOCKLIST_PATH,
|
||||
skipUrlState: true,
|
||||
hideTimeline: true,
|
||||
|
@ -227,13 +163,12 @@ export const links: LinkItem = {
|
|||
description: i18n.translate('xpack.securitySolution.appLinks.actionHistoryDescription', {
|
||||
defaultMessage: 'View the history of response actions performed on hosts.',
|
||||
}),
|
||||
landingIcon: IconActionHistory,
|
||||
landingIcon: IconConsole,
|
||||
path: RESPONSE_ACTIONS_HISTORY_PATH,
|
||||
skipUrlState: true,
|
||||
hideTimeline: true,
|
||||
},
|
||||
cloudSecurityPostureLinks,
|
||||
cloudDefendLinks,
|
||||
cloudDefendLink,
|
||||
],
|
||||
};
|
||||
|
||||
|
|
|
@ -5,77 +5,22 @@
|
|||
* 2.0.
|
||||
*/
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { EuiHorizontalRule, EuiSpacer, EuiTitle } from '@elastic/eui';
|
||||
|
||||
import { SecurityPageName } from '../../app/types';
|
||||
import { HeaderPage } from '../../common/components/header_page';
|
||||
import { useRootNavLink } from '../../common/links/nav_links';
|
||||
import type { NavigationLink } from '../../common/links';
|
||||
import { SecuritySolutionPageWrapper } from '../../common/components/page_wrapper';
|
||||
import { SpyRoute } from '../../common/utils/route/spy_routes';
|
||||
import { LandingLinksIcons } from '../../common/components/landing_links/landing_links_icons';
|
||||
import { LandingLinksIconsCategories } from '../../common/components/landing_links/landing_links_icons_categories';
|
||||
|
||||
const MANAGE_PAGE_TITLE = i18n.translate('xpack.securitySolution.management.landing.pageTitle', {
|
||||
defaultMessage: 'Manage',
|
||||
const PAGE_TITLE = i18n.translate('xpack.securitySolution.management.landing.settingsTitle', {
|
||||
defaultMessage: 'Settings',
|
||||
});
|
||||
|
||||
export const ManageLandingPage = () => (
|
||||
<SecuritySolutionPageWrapper>
|
||||
<HeaderPage title={MANAGE_PAGE_TITLE} />
|
||||
<ManagementCategories />
|
||||
<HeaderPage title={PAGE_TITLE} />
|
||||
<LandingLinksIconsCategories pageName={SecurityPageName.administration} />
|
||||
<SpyRoute pageName={SecurityPageName.administration} />
|
||||
</SecuritySolutionPageWrapper>
|
||||
);
|
||||
|
||||
const StyledEuiHorizontalRule = styled(EuiHorizontalRule)`
|
||||
margin-top: ${({ theme }) => theme.eui.euiSizeM};
|
||||
margin-bottom: ${({ theme }) => theme.eui.euiSizeL};
|
||||
`;
|
||||
|
||||
type ManagementCategories = Array<{ label: string; links: NavigationLink[] }>;
|
||||
const useManagementCategories = (): ManagementCategories => {
|
||||
const { links = [], categories = [] } = useRootNavLink(SecurityPageName.administration) ?? {};
|
||||
|
||||
const manageLinksById = Object.fromEntries(links.map((link) => [link.id, link]));
|
||||
|
||||
return categories.reduce<ManagementCategories>((acc, { label, linkIds }) => {
|
||||
const linksItem = linkIds.reduce<NavigationLink[]>((linksAcc, linkId) => {
|
||||
if (manageLinksById[linkId]) {
|
||||
linksAcc.push(manageLinksById[linkId]);
|
||||
}
|
||||
return linksAcc;
|
||||
}, []);
|
||||
if (linksItem.length > 0) {
|
||||
acc.push({ label, links: linksItem });
|
||||
}
|
||||
return acc;
|
||||
}, []);
|
||||
};
|
||||
|
||||
export const ManagementCategories = () => {
|
||||
const managementCategories = useManagementCategories();
|
||||
|
||||
return (
|
||||
<>
|
||||
{managementCategories.map(({ label, links }, index) => (
|
||||
<div key={label}>
|
||||
{index > 0 && (
|
||||
<>
|
||||
<EuiSpacer key="first" size="xl" />
|
||||
<EuiSpacer key="second" size="xl" />
|
||||
</>
|
||||
)}
|
||||
<EuiTitle size="xxxs">
|
||||
<h2>{label}</h2>
|
||||
</EuiTitle>
|
||||
<StyledEuiHorizontalRule />
|
||||
<LandingLinksIcons items={links} />
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
ManagementCategories.displayName = 'ManagementCategories';
|
||||
|
|
|
@ -55,6 +55,7 @@ export const gettingStartedLinks: LinkItem = {
|
|||
defaultMessage: 'Getting started',
|
||||
}),
|
||||
],
|
||||
sideNavIcon: 'launch',
|
||||
skipUrlState: true,
|
||||
hideTimeline: true,
|
||||
};
|
||||
|
|
51
x-pack/plugins/security_solution/public/rules/landing.tsx
Normal file
51
x-pack/plugins/security_solution/public/rules/landing.tsx
Normal file
|
@ -0,0 +1,51 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import React from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { TrackApplicationView } from '@kbn/usage-collection-plugin/public';
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui';
|
||||
import { SecurityPageName } from '../../common';
|
||||
import { PluginTemplateWrapper } from '../common/components/plugin_template_wrapper';
|
||||
import { SecuritySolutionPageWrapper } from '../common/components/page_wrapper';
|
||||
import { SpyRoute } from '../common/utils/route/spy_routes';
|
||||
import { LandingLinksIconsCategories } from '../common/components/landing_links/landing_links_icons_categories';
|
||||
import { Title } from '../common/components/header_page/title';
|
||||
import { SecuritySolutionLinkButton } from '../common/components/links';
|
||||
|
||||
const RULES_PAGE_TITLE = i18n.translate('xpack.securitySolution.rules.landing.pageTitle', {
|
||||
defaultMessage: 'Rules',
|
||||
});
|
||||
|
||||
const CREATE_RULE_BUTTON = i18n.translate('xpack.securitySolution.rules.landing.createRule', {
|
||||
defaultMessage: 'Create rule',
|
||||
});
|
||||
|
||||
const RulesLandingHeader: React.FC = () => (
|
||||
<EuiFlexGroup gutterSize="none" direction="row">
|
||||
<EuiFlexItem>
|
||||
<Title title={RULES_PAGE_TITLE} />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<SecuritySolutionLinkButton deepLinkId={SecurityPageName.rulesCreate} iconType="plusInCircle">
|
||||
{CREATE_RULE_BUTTON}
|
||||
</SecuritySolutionLinkButton>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
|
||||
export const RulesLandingPage = () => (
|
||||
<PluginTemplateWrapper>
|
||||
<TrackApplicationView viewId={SecurityPageName.rulesLanding}>
|
||||
<SecuritySolutionPageWrapper>
|
||||
<RulesLandingHeader />
|
||||
<EuiSpacer size="xl" />
|
||||
<LandingLinksIconsCategories pageName={SecurityPageName.rulesLanding} />
|
||||
<SpyRoute pageName={SecurityPageName.rulesLanding} />
|
||||
</SecuritySolutionPageWrapper>
|
||||
</TrackApplicationView>
|
||||
</PluginTemplateWrapper>
|
||||
);
|
93
x-pack/plugins/security_solution/public/rules/links.ts
Normal file
93
x-pack/plugins/security_solution/public/rules/links.ts
Normal file
|
@ -0,0 +1,93 @@
|
|||
/*
|
||||
* 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 { i18n } from '@kbn/i18n';
|
||||
import {
|
||||
RULES_PATH,
|
||||
RULES_CREATE_PATH,
|
||||
EXCEPTIONS_PATH,
|
||||
RULES_LANDING_PATH,
|
||||
RULES_ADD_PATH,
|
||||
} from '../../common/constants';
|
||||
import { ADD_RULES, CREATE_NEW_RULE, EXCEPTIONS, RULES, SIEM_RULES } from '../app/translations';
|
||||
import { SecurityPageName } from '../app/types';
|
||||
import { benchmarksLink } from '../cloud_security_posture/links';
|
||||
import type { LinkItem } from '../common/links';
|
||||
import { IconConsoleCloud } from '../common/icons/console_cloud';
|
||||
import { IconRollup } from '../common/icons/rollup';
|
||||
|
||||
export const links: LinkItem = {
|
||||
id: SecurityPageName.rulesLanding,
|
||||
title: RULES,
|
||||
path: RULES_LANDING_PATH,
|
||||
hideTimeline: true,
|
||||
links: [
|
||||
{
|
||||
id: SecurityPageName.rules,
|
||||
title: SIEM_RULES,
|
||||
description: i18n.translate('xpack.securitySolution.appLinks.rulesDescription', {
|
||||
defaultMessage:
|
||||
"Create and manage rules to check for suspicious source events, and create alerts when a rule's conditions are met.",
|
||||
}),
|
||||
landingIcon: IconRollup,
|
||||
path: RULES_PATH,
|
||||
globalSearchKeywords: [
|
||||
i18n.translate('xpack.securitySolution.appLinks.rules', {
|
||||
defaultMessage: 'Rules',
|
||||
}),
|
||||
],
|
||||
links: [
|
||||
{
|
||||
id: SecurityPageName.rulesAdd,
|
||||
title: ADD_RULES,
|
||||
path: RULES_ADD_PATH,
|
||||
skipUrlState: true,
|
||||
hideTimeline: true,
|
||||
},
|
||||
{
|
||||
id: SecurityPageName.rulesCreate,
|
||||
title: CREATE_NEW_RULE,
|
||||
path: RULES_CREATE_PATH,
|
||||
skipUrlState: true,
|
||||
hideTimeline: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: SecurityPageName.exceptions,
|
||||
title: EXCEPTIONS,
|
||||
description: i18n.translate('xpack.securitySolution.appLinks.exceptionsDescription', {
|
||||
defaultMessage:
|
||||
'Create and manage shared exception lists to prevent the creation of unwanted alerts.',
|
||||
}),
|
||||
landingIcon: IconConsoleCloud,
|
||||
path: EXCEPTIONS_PATH,
|
||||
skipUrlState: true,
|
||||
hideTimeline: true,
|
||||
globalSearchKeywords: [
|
||||
i18n.translate('xpack.securitySolution.appLinks.exceptions', {
|
||||
defaultMessage: 'Exception lists',
|
||||
}),
|
||||
],
|
||||
},
|
||||
benchmarksLink,
|
||||
],
|
||||
categories: [
|
||||
{
|
||||
label: i18n.translate('xpack.securitySolution.appLinks.category.siemRules', {
|
||||
defaultMessage: 'Security Detection Rules',
|
||||
}),
|
||||
linkIds: [SecurityPageName.rules, SecurityPageName.exceptions],
|
||||
},
|
||||
{
|
||||
label: i18n.translate('xpack.securitySolution.appLinks.category.cspRules', {
|
||||
defaultMessage: 'Cloud Security Rules',
|
||||
}),
|
||||
linkIds: [SecurityPageName.cloudSecurityPostureBenchmarks],
|
||||
},
|
||||
],
|
||||
};
|
|
@ -10,7 +10,7 @@ import { Routes, Route } from '@kbn/shared-ux-router';
|
|||
|
||||
import { TrackApplicationView } from '@kbn/usage-collection-plugin/public';
|
||||
import * as i18n from './translations';
|
||||
import { RULES_PATH, SecurityPageName } from '../../common/constants';
|
||||
import { RULES_LANDING_PATH, RULES_PATH, SecurityPageName } from '../../common/constants';
|
||||
import { NotFoundPage } from '../app/404';
|
||||
import { RulesPage } from '../detection_engine/rule_management_ui/pages/rule_management';
|
||||
import { CreateRulePage } from '../detection_engine/rule_creation_ui/pages/rule_creation';
|
||||
|
@ -24,6 +24,8 @@ import { PluginTemplateWrapper } from '../common/components/plugin_template_wrap
|
|||
import { SpyRoute } from '../common/utils/route/spy_routes';
|
||||
import { AllRulesTabs } from '../detection_engine/rule_management_ui/components/rules_table/rules_table_toolbar';
|
||||
import { AddRulesPage } from '../detection_engine/rule_management_ui/pages/add_rules';
|
||||
import type { SecuritySubPluginRoutes } from '../app/types';
|
||||
import { RulesLandingPage } from './landing';
|
||||
|
||||
const RulesSubRoutes = [
|
||||
{
|
||||
|
@ -100,11 +102,13 @@ const RulesContainerComponent: React.FC = () => {
|
|||
|
||||
const Rules = React.memo(RulesContainerComponent);
|
||||
|
||||
const renderRulesRoutes = () => <Rules />;
|
||||
|
||||
export const routes = [
|
||||
export const routes: SecuritySubPluginRoutes = [
|
||||
{
|
||||
path: RULES_LANDING_PATH,
|
||||
component: RulesLandingPage,
|
||||
},
|
||||
{
|
||||
path: RULES_PATH,
|
||||
render: renderRulesRoutes,
|
||||
component: Rules,
|
||||
},
|
||||
];
|
||||
|
|
|
@ -74,3 +74,6 @@ export const GetStartedComponent: React.FC = () => {
|
|||
|
||||
GetStartedComponent.displayName = 'GetStartedComponent';
|
||||
export const GetStarted = React.memo(GetStartedComponent);
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default GetStarted;
|
||||
|
|
|
@ -10,7 +10,7 @@ import React from 'react';
|
|||
import { CoreStart } from '@kbn/core/public';
|
||||
|
||||
import type { GetStartedComponent } from './types';
|
||||
import { GetStarted } from './get_started';
|
||||
import { GetStarted } from './lazy';
|
||||
import { KibanaServicesProvider } from '../../services';
|
||||
import { ServerlessSecurityPluginStartDependencies } from '../../types';
|
||||
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import React, { lazy, Suspense } from 'react';
|
||||
import { EuiLoadingLogo } from '@elastic/eui';
|
||||
|
||||
const GetStartedLazy = lazy(() => import('./get_started'));
|
||||
|
||||
const centerLogoStyle = { display: 'flex', margin: 'auto' };
|
||||
|
||||
export const GetStarted = () => (
|
||||
<Suspense fallback={<EuiLoadingLogo logo="logoSecurity" size="xl" style={centerLogoStyle} />}>
|
||||
<GetStartedLazy />
|
||||
</Suspense>
|
||||
);
|
|
@ -0,0 +1,36 @@
|
|||
/*
|
||||
* 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 { LinkCategoryType, type SeparatorLinkCategory } from '@kbn/security-solution-side-nav';
|
||||
import { SecurityPageName } from '@kbn/security-solution-plugin/common';
|
||||
|
||||
export const CATEGORIES: SeparatorLinkCategory[] = [
|
||||
{
|
||||
type: LinkCategoryType.separator,
|
||||
linkIds: [SecurityPageName.dashboards],
|
||||
},
|
||||
{
|
||||
type: LinkCategoryType.separator,
|
||||
linkIds: [
|
||||
SecurityPageName.alerts,
|
||||
SecurityPageName.cloudSecurityPostureFindings,
|
||||
SecurityPageName.case,
|
||||
],
|
||||
},
|
||||
{
|
||||
type: LinkCategoryType.separator,
|
||||
linkIds: [
|
||||
SecurityPageName.timelines,
|
||||
SecurityPageName.threatIntelligenceIndicators,
|
||||
SecurityPageName.exploreLanding,
|
||||
],
|
||||
},
|
||||
{
|
||||
type: LinkCategoryType.separator,
|
||||
linkIds: [SecurityPageName.rulesLanding],
|
||||
},
|
||||
];
|
|
@ -12,7 +12,7 @@ import type {
|
|||
SideNavCompProps,
|
||||
} from '@kbn/core-chrome-browser/src/project_navigation';
|
||||
import { ServerlessSecurityPluginStartDependencies } from '../../types';
|
||||
import { SecuritySideNavigation } from './side_navigation';
|
||||
import { SecuritySideNavigation } from './lazy';
|
||||
import { KibanaServicesProvider } from '../../services';
|
||||
|
||||
export const getSecuritySideNavComponent = (
|
||||
|
|
|
@ -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 React, { lazy, Suspense } from 'react';
|
||||
import { EuiLoadingSpinner } from '@elastic/eui';
|
||||
|
||||
const SecuritySideNavigationLazy = lazy(() => import('./side_navigation'));
|
||||
|
||||
export const SecuritySideNavigation = () => (
|
||||
<Suspense fallback={<EuiLoadingSpinner size="m" />}>
|
||||
<SecuritySideNavigationLazy />
|
||||
</Suspense>
|
||||
);
|
|
@ -18,6 +18,7 @@ const mockUseSideNavSelectedId = useSideNavSelectedId as jest.Mock;
|
|||
|
||||
const mockSolutionSideNav = jest.fn((_props: unknown) => <div data-test-subj="solutionSideNav" />);
|
||||
jest.mock('@kbn/security-solution-side-nav', () => ({
|
||||
...jest.requireActual('@kbn/security-solution-side-nav'),
|
||||
SolutionSideNav: (props: unknown) => mockSolutionSideNav(props),
|
||||
}));
|
||||
|
||||
|
@ -34,12 +35,11 @@ const sideNavItems = [
|
|||
href: '/alerts',
|
||||
onClick: jest.fn(),
|
||||
},
|
||||
];
|
||||
const sideNavFooterItems = [
|
||||
{
|
||||
id: SecurityPageName.administration,
|
||||
label: 'Manage',
|
||||
href: '/administration',
|
||||
position: 'bottom',
|
||||
onClick: jest.fn(),
|
||||
},
|
||||
];
|
||||
|
@ -78,17 +78,6 @@ describe('SecuritySideNavigation', () => {
|
|||
);
|
||||
});
|
||||
|
||||
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 });
|
||||
|
||||
|
|
|
@ -9,19 +9,15 @@ 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';
|
||||
import { useSideNavItems, useSideNavSelectedId } from '../../hooks/use_side_nav_items';
|
||||
import { CATEGORIES } from './categories';
|
||||
|
||||
export const SecuritySideNavigation: React.FC = () => {
|
||||
const { euiTheme } = useEuiTheme();
|
||||
const sideNavItems = useSideNavItems();
|
||||
const selectedId = useSideNavSelectedId(sideNavItems);
|
||||
const [items, footerItems] = usePartitionFooterNavItems(sideNavItems);
|
||||
const items = useSideNavItems();
|
||||
const selectedId = useSideNavSelectedId(items);
|
||||
|
||||
const isLoading = items.length === 0 && footerItems.length === 0;
|
||||
const isLoading = items.length === 0;
|
||||
|
||||
return isLoading ? (
|
||||
<EuiLoadingSpinner size="m" data-test-subj="sideNavLoader" />
|
||||
|
@ -33,7 +29,7 @@ export const SecuritySideNavigation: React.FC = () => {
|
|||
children={
|
||||
<SolutionSideNav
|
||||
items={items}
|
||||
footerItems={footerItems}
|
||||
categories={CATEGORIES}
|
||||
selectedId={selectedId}
|
||||
panelTopOffset={`calc(${euiTheme.size.l} * 2)`}
|
||||
/>
|
||||
|
@ -45,3 +41,6 @@ export const SecuritySideNavigation: React.FC = () => {
|
|||
/>
|
||||
);
|
||||
};
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default SecuritySideNavigation;
|
||||
|
|
|
@ -6,11 +6,7 @@
|
|||
*/
|
||||
|
||||
import { renderHook } from '@testing-library/react-hooks';
|
||||
import {
|
||||
usePartitionFooterNavItems,
|
||||
useSideNavItems,
|
||||
useSideNavSelectedId,
|
||||
} from './use_side_nav_items';
|
||||
import { 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';
|
||||
|
@ -55,12 +51,14 @@ describe('useSideNavItems', () => {
|
|||
{
|
||||
id: SecurityPageName.alerts,
|
||||
label: 'Alerts',
|
||||
position: 'top',
|
||||
href: expect.any(String),
|
||||
onClick: expect.any(Function),
|
||||
},
|
||||
{
|
||||
id: SecurityPageName.case,
|
||||
label: 'Cases',
|
||||
position: 'top',
|
||||
href: expect.any(String),
|
||||
onClick: expect.any(Function),
|
||||
},
|
||||
|
@ -82,6 +80,7 @@ describe('useSideNavItems', () => {
|
|||
{
|
||||
id: SecurityPageName.dashboards,
|
||||
label: 'Dashboards',
|
||||
position: 'top',
|
||||
href: expect.any(String),
|
||||
onClick: expect.any(Function),
|
||||
items: [
|
||||
|
@ -101,6 +100,7 @@ describe('useSideNavItems', () => {
|
|||
{
|
||||
id: SecurityPageName.landing,
|
||||
title: 'Get Started',
|
||||
sideNavIcon: 'launch',
|
||||
},
|
||||
]);
|
||||
const { result } = renderHook(useSideNavItems, { wrapper: KibanaServicesProvider });
|
||||
|
@ -110,10 +110,10 @@ describe('useSideNavItems', () => {
|
|||
expect(items).toEqual([
|
||||
{
|
||||
id: SecurityPageName.landing,
|
||||
label: 'GET STARTED',
|
||||
label: 'Get Started',
|
||||
position: 'bottom',
|
||||
href: expect.any(String),
|
||||
onClick: expect.any(Function),
|
||||
labelSize: 'xs',
|
||||
iconType: 'launch',
|
||||
appendSeparator: true,
|
||||
},
|
||||
|
@ -121,101 +121,6 @@ describe('useSideNavItems', () => {
|
|||
});
|
||||
});
|
||||
|
||||
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();
|
||||
|
|
|
@ -7,18 +7,62 @@
|
|||
|
||||
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 { SolutionSideNavItem, SolutionSideNavItemPosition } from '@kbn/security-solution-side-nav';
|
||||
import { useKibana } from '../services';
|
||||
import { useGetLinkProps } from './use_link_props';
|
||||
import { type GetLinkProps, useGetLinkProps } from './use_link_props';
|
||||
import { useNavLinks } from './use_nav_links';
|
||||
|
||||
const isFooterNavItem = (id: string) =>
|
||||
id === SecurityPageName.landing || id === SecurityPageName.administration;
|
||||
type NavigationLink = ReturnType<typeof useNavLinks>[number];
|
||||
|
||||
const isBottomNavItem = (id: string) =>
|
||||
id === SecurityPageName.landing || id === SecurityPageName.administration;
|
||||
const isGetStartedNavItem = (id: string) => id === SecurityPageName.landing;
|
||||
|
||||
/**
|
||||
* Formats generic navigation links into the shape expected by the `SolutionSideNav`
|
||||
*/
|
||||
const formatLink = (navLink: NavigationLink, getLinkProps: GetLinkProps): SolutionSideNavItem => ({
|
||||
id: navLink.id,
|
||||
label: navLink.title,
|
||||
iconType: navLink.sideNavIcon,
|
||||
position: isBottomNavItem(navLink.id)
|
||||
? SolutionSideNavItemPosition.bottom
|
||||
: SolutionSideNavItemPosition.top,
|
||||
...getLinkProps({ deepLinkId: navLink.id }),
|
||||
...(navLink.categories?.length && { categories: navLink.categories }),
|
||||
...(navLink.links?.length && {
|
||||
items: navLink.links.reduce<SolutionSideNavItem[]>((acc, current) => {
|
||||
if (!current.disabled) {
|
||||
acc.push({
|
||||
id: current.id,
|
||||
label: current.title,
|
||||
iconType: current.sideNavIcon,
|
||||
isBeta: current.isBeta,
|
||||
betaOptions: current.betaOptions,
|
||||
...getLinkProps({ deepLinkId: current.id }),
|
||||
});
|
||||
}
|
||||
return acc;
|
||||
}, []),
|
||||
}),
|
||||
});
|
||||
|
||||
/**
|
||||
* Formats the get started navigation links into the shape expected by the `SolutionSideNav`
|
||||
*/
|
||||
const formatGetStartedLink = (
|
||||
navLink: NavigationLink,
|
||||
getLinkProps: GetLinkProps
|
||||
): SolutionSideNavItem => ({
|
||||
id: navLink.id,
|
||||
label: navLink.title,
|
||||
iconType: navLink.sideNavIcon,
|
||||
position: SolutionSideNavItemPosition.bottom,
|
||||
...getLinkProps({ deepLinkId: navLink.id }),
|
||||
appendSeparator: true,
|
||||
});
|
||||
|
||||
// DFS for the sideNavItem matching the current `pathname`, returns all item hierarchy when found
|
||||
const findItemsByPath = (
|
||||
sideNavItems: SolutionSideNavItem[],
|
||||
|
@ -53,37 +97,9 @@ export const useSideNavItems = (): SolutionSideNavItem[] => {
|
|||
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,
|
||||
});
|
||||
items.push(formatGetStartedLink(navLink, getLinkProps));
|
||||
} 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<SolutionSideNavItem[]>((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;
|
||||
}, []),
|
||||
}),
|
||||
});
|
||||
items.push(formatLink(navLink, getLinkProps));
|
||||
}
|
||||
return items;
|
||||
}, []),
|
||||
|
@ -117,16 +133,6 @@ const useAddExternalSideNavItems = (securitySideNavItems: SolutionSideNavItem[])
|
|||
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
|
||||
*/
|
||||
|
|
|
@ -29432,9 +29432,7 @@
|
|||
"xpack.securitySolution.appLinks.actionHistoryDescription": "Affichez l'historique des actions de réponse effectuées sur les hôtes.",
|
||||
"xpack.securitySolution.appLinks.alerts": "Alertes",
|
||||
"xpack.securitySolution.appLinks.blocklistDescription": "Excluez les applications non souhaitées de l'exécution sur vos hôtes.",
|
||||
"xpack.securitySolution.appLinks.category.cloudSecurityPosture": "SÉCURITÉ DU CLOUD",
|
||||
"xpack.securitySolution.appLinks.category.endpoints": "POINTS DE TERMINAISON",
|
||||
"xpack.securitySolution.appLinks.category.siem": "SIEM",
|
||||
"xpack.securitySolution.appLinks.cloudDefendPoliciesDescription": "Sécurisez les charges de travail conteneurisées dans Kubernetes contre les attaques et les dérives grâce à des politiques d'exécution granulaires et flexibles.",
|
||||
"xpack.securitySolution.appLinks.cloudSecurityPostureBenchmarksDescription": "Affichez les règles de benchmark.",
|
||||
"xpack.securitySolution.appLinks.cloudSecurityPostureDashboardDescription": "Un aperçu des résultats de toutes les intégrations CSP.",
|
||||
|
@ -29458,7 +29456,6 @@
|
|||
"xpack.securitySolution.appLinks.hosts.sessions": "Sessions",
|
||||
"xpack.securitySolution.appLinks.hosts.uncommonProcesses": "Processus inhabituels",
|
||||
"xpack.securitySolution.appLinks.kubernetesDescription": "Fournit des visualisations interactives de votre charge de travail et de vos données de session Kubernetes.",
|
||||
"xpack.securitySolution.appLinks.manage": "Gérer",
|
||||
"xpack.securitySolution.appLinks.network": "Réseau",
|
||||
"xpack.securitySolution.appLinks.network.description": "Fournit des indicateurs d'activités clés sur une carte interactive ainsi que des tableaux d'événements qui permettent l'interaction avec la chronologie.",
|
||||
"xpack.securitySolution.appLinks.network.dns": "DNS",
|
||||
|
@ -32636,7 +32633,6 @@
|
|||
"xpack.securitySolution.navigation.investigate": "Examiner",
|
||||
"xpack.securitySolution.navigation.kubernetes": "Kubernetes",
|
||||
"xpack.securitySolution.navigation.mainLabel": "Sécurité",
|
||||
"xpack.securitySolution.navigation.manage": "Gérer",
|
||||
"xpack.securitySolution.navigation.network": "Réseau",
|
||||
"xpack.securitySolution.navigation.newRuleTitle": "Créer une nouvelle règle",
|
||||
"xpack.securitySolution.navigation.overview": "Aperçu",
|
||||
|
|
|
@ -29413,9 +29413,7 @@
|
|||
"xpack.securitySolution.appLinks.actionHistoryDescription": "ホストで実行された対応アクションの履歴を表示します。",
|
||||
"xpack.securitySolution.appLinks.alerts": "アラート",
|
||||
"xpack.securitySolution.appLinks.blocklistDescription": "不要なアプリケーションがホストで実行されないようにします。",
|
||||
"xpack.securitySolution.appLinks.category.cloudSecurityPosture": "クラウドセキュリティ",
|
||||
"xpack.securitySolution.appLinks.category.endpoints": "エンドポイント",
|
||||
"xpack.securitySolution.appLinks.category.siem": "SIEM",
|
||||
"xpack.securitySolution.appLinks.cloudDefendPoliciesDescription": "粒度の高い柔軟なランタイムポリシーによって、Kubernetesのコンテナーワークロードを攻撃とドリフトから保護します。",
|
||||
"xpack.securitySolution.appLinks.cloudSecurityPostureBenchmarksDescription": "ベンチマークルールを表示します。",
|
||||
"xpack.securitySolution.appLinks.cloudSecurityPostureDashboardDescription": "すべてのCSP統合の結果の概要。",
|
||||
|
@ -29439,7 +29437,6 @@
|
|||
"xpack.securitySolution.appLinks.hosts.sessions": "セッション",
|
||||
"xpack.securitySolution.appLinks.hosts.uncommonProcesses": "非共通プロセス",
|
||||
"xpack.securitySolution.appLinks.kubernetesDescription": "Kubernetesワークロードおよびセッションデータのインタラクティブビジュアライゼーションを提供します。",
|
||||
"xpack.securitySolution.appLinks.manage": "管理",
|
||||
"xpack.securitySolution.appLinks.network": "ネットワーク",
|
||||
"xpack.securitySolution.appLinks.network.description": "インタラクティブなマップで主要なアクティビティメトリックと、タイムラインと連携できるイベントテーブルを提供します。",
|
||||
"xpack.securitySolution.appLinks.network.dns": "DNS",
|
||||
|
@ -32617,7 +32614,6 @@
|
|||
"xpack.securitySolution.navigation.investigate": "調査",
|
||||
"xpack.securitySolution.navigation.kubernetes": "Kubernetes",
|
||||
"xpack.securitySolution.navigation.mainLabel": "セキュリティ",
|
||||
"xpack.securitySolution.navigation.manage": "管理",
|
||||
"xpack.securitySolution.navigation.network": "ネットワーク",
|
||||
"xpack.securitySolution.navigation.newRuleTitle": "新規ルールを作成",
|
||||
"xpack.securitySolution.navigation.overview": "概要",
|
||||
|
|
|
@ -29409,9 +29409,7 @@
|
|||
"xpack.securitySolution.appLinks.actionHistoryDescription": "查看在主机上执行的响应操作的历史记录。",
|
||||
"xpack.securitySolution.appLinks.alerts": "告警",
|
||||
"xpack.securitySolution.appLinks.blocklistDescription": "阻止不需要的应用程序在您的主机上运行。",
|
||||
"xpack.securitySolution.appLinks.category.cloudSecurityPosture": "云安全",
|
||||
"xpack.securitySolution.appLinks.category.endpoints": "终端",
|
||||
"xpack.securitySolution.appLinks.category.siem": "SIEM",
|
||||
"xpack.securitySolution.appLinks.cloudDefendPoliciesDescription": "通过细粒度、灵活的运行时策略保护 Kubernetes 中的容器工作负载,使其免于受到攻击和出现漂移。",
|
||||
"xpack.securitySolution.appLinks.cloudSecurityPostureBenchmarksDescription": "查看基准规则。",
|
||||
"xpack.securitySolution.appLinks.cloudSecurityPostureDashboardDescription": "所有 CSP 集成中的结果概述。",
|
||||
|
@ -29435,7 +29433,6 @@
|
|||
"xpack.securitySolution.appLinks.hosts.sessions": "会话",
|
||||
"xpack.securitySolution.appLinks.hosts.uncommonProcesses": "不常见进程",
|
||||
"xpack.securitySolution.appLinks.kubernetesDescription": "提供 Kubernetes 工作负载和会话数据的交互式可视化。",
|
||||
"xpack.securitySolution.appLinks.manage": "管理",
|
||||
"xpack.securitySolution.appLinks.network": "网络",
|
||||
"xpack.securitySolution.appLinks.network.description": "在交互式地图中提供关键活动指标以及启用与时间线进行交互的事件表。",
|
||||
"xpack.securitySolution.appLinks.network.dns": "DNS",
|
||||
|
@ -32613,7 +32610,6 @@
|
|||
"xpack.securitySolution.navigation.investigate": "调查",
|
||||
"xpack.securitySolution.navigation.kubernetes": "Kubernetes",
|
||||
"xpack.securitySolution.navigation.mainLabel": "安全",
|
||||
"xpack.securitySolution.navigation.manage": "管理",
|
||||
"xpack.securitySolution.navigation.network": "网络",
|
||||
"xpack.securitySolution.navigation.newRuleTitle": "创建新规则",
|
||||
"xpack.securitySolution.navigation.overview": "概览",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue