mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 01:13:23 -04:00
[Security Solution][Serverless] Implements panelContentProvider on the DefaultNavigation (#169270)
## Summary
Implements the `panelContentProvider` for the `DefaultNavigation`
component, so the content of the panels when open is provided by the
Security Solution plugin.
In order to test it, the experimental flag needs to be enabled.
In `config/serverless.security.yml` add:
```
xpack.securitySolutionServerless.enableExperimental: ['platformNavEnabled']
```
## Screenshot
<img width="1718" alt="Captura de pantalla 2023-10-18 a les 18 38 04"
src="5022a7d9
-c619-4dbb-87cf-ee3ed0090853">
(The vertical separation of the main nav links is still not implemented
by the DefaultNavigation)
---------
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
17c78db794
commit
a8a22c39b3
18 changed files with 455 additions and 305 deletions
|
@ -89,6 +89,7 @@ const createStartContractMock = () => {
|
|||
startContract.getIsNavDrawerLocked$.mockReturnValue(new BehaviorSubject(false));
|
||||
startContract.getBodyClasses$.mockReturnValue(new BehaviorSubject([]));
|
||||
startContract.hasHeaderBanner$.mockReturnValue(new BehaviorSubject(false));
|
||||
startContract.getIsSideNavCollapsed$.mockReturnValue(new BehaviorSubject(false));
|
||||
return startContract;
|
||||
};
|
||||
|
||||
|
|
|
@ -5,4 +5,4 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
export { SolutionSideNavPanel } from './src/solution_side_nav_panel';
|
||||
export { SolutionSideNavPanelContent } from './src/solution_side_nav_panel';
|
||||
|
|
|
@ -50,12 +50,14 @@ import {
|
|||
accordionButtonClassName,
|
||||
} from './solution_side_nav_panel.styles';
|
||||
|
||||
export interface SolutionSideNavPanelProps {
|
||||
onClose: () => void;
|
||||
onOutsideClick: () => void;
|
||||
export interface SolutionSideNavPanelContentProps {
|
||||
title: string;
|
||||
onClose: () => void;
|
||||
items: SolutionSideNavItem[];
|
||||
categories?: LinkCategories;
|
||||
}
|
||||
export interface SolutionSideNavPanelProps extends SolutionSideNavPanelContentProps {
|
||||
onOutsideClick: () => void;
|
||||
bottomOffset?: string;
|
||||
topOffset?: string;
|
||||
}
|
||||
|
@ -85,7 +87,6 @@ export const SolutionSideNavPanel: React.FC<SolutionSideNavPanelProps> = React.m
|
|||
$topOffset,
|
||||
});
|
||||
const panelClasses = classNames(panelClassName, 'eui-yScroll', solutionSideNavPanelStyles);
|
||||
const titleClasses = classNames(SolutionSideNavTitleStyles(euiTheme));
|
||||
|
||||
// ESC key closes PanelNav
|
||||
const onKeyDown = useCallback(
|
||||
|
@ -110,24 +111,12 @@ export const SolutionSideNavPanel: React.FC<SolutionSideNavPanelProps> = React.m
|
|||
paddingSize="m"
|
||||
data-test-subj="solutionSideNavPanel"
|
||||
>
|
||||
<EuiFlexGroup direction="column" gutterSize="m" alignItems="flexStart">
|
||||
<EuiFlexItem>
|
||||
<EuiTitle size="xs" className={titleClasses}>
|
||||
<strong>{title}</strong>
|
||||
</EuiTitle>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem style={{ width: '100%' }}>
|
||||
{categories ? (
|
||||
<SolutionSideNavPanelCategories
|
||||
categories={categories}
|
||||
items={items}
|
||||
onClose={onClose}
|
||||
/>
|
||||
) : (
|
||||
<SolutionSideNavPanelItems items={items} onClose={onClose} />
|
||||
)}
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<SolutionSideNavPanelContent
|
||||
title={title}
|
||||
categories={categories}
|
||||
items={items}
|
||||
onClose={onClose}
|
||||
/>
|
||||
</EuiPanel>
|
||||
</EuiOutsideClickDetector>
|
||||
</EuiFocusTrap>
|
||||
|
@ -137,6 +126,33 @@ export const SolutionSideNavPanel: React.FC<SolutionSideNavPanelProps> = React.m
|
|||
}
|
||||
);
|
||||
|
||||
export const SolutionSideNavPanelContent: React.FC<SolutionSideNavPanelContentProps> = React.memo(
|
||||
function SolutionSideNavPanelContent({ title, onClose, categories, items }) {
|
||||
const { euiTheme } = useEuiTheme();
|
||||
const titleClasses = classNames(SolutionSideNavTitleStyles(euiTheme));
|
||||
return (
|
||||
<EuiFlexGroup direction="column" gutterSize="m" alignItems="flexStart">
|
||||
<EuiFlexItem>
|
||||
<EuiTitle size="xs" className={titleClasses}>
|
||||
<strong>{title}</strong>
|
||||
</EuiTitle>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem style={{ width: '100%' }}>
|
||||
{categories ? (
|
||||
<SolutionSideNavPanelCategories
|
||||
categories={categories}
|
||||
items={items}
|
||||
onClose={onClose}
|
||||
/>
|
||||
) : (
|
||||
<SolutionSideNavPanelItems items={items} onClose={onClose} />
|
||||
)}
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
interface SolutionSideNavPanelCategoriesProps {
|
||||
categories: LinkCategories;
|
||||
items: SolutionSideNavItem[];
|
||||
|
|
|
@ -20,9 +20,5 @@ export const TelemetryContextProvider: FC<TelemetryProviderProps> = ({ children,
|
|||
};
|
||||
|
||||
export const useTelemetryContext = () => {
|
||||
const context = useContext(TelemetryContext);
|
||||
if (!context) {
|
||||
throw new Error('No TelemetryContext found.');
|
||||
}
|
||||
return context;
|
||||
return useContext(TelemetryContext) ?? {};
|
||||
};
|
||||
|
|
|
@ -5,14 +5,17 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import {
|
||||
SecurityPageName,
|
||||
LinkCategoryType,
|
||||
type LinkCategory,
|
||||
type SeparatorLinkCategory,
|
||||
} from '@kbn/security-solution-navigation';
|
||||
import { ExternalPageName } from '../links/constants';
|
||||
import { ExternalPageName } from './links/constants';
|
||||
import type { ProjectPageName } from './links/types';
|
||||
|
||||
export const CATEGORIES: SeparatorLinkCategory[] = [
|
||||
export const CATEGORIES: Array<SeparatorLinkCategory<ProjectPageName>> = [
|
||||
{
|
||||
type: LinkCategoryType.separator,
|
||||
linkIds: [ExternalPageName.discover, SecurityPageName.dashboards],
|
||||
|
@ -43,3 +46,24 @@ export const CATEGORIES: SeparatorLinkCategory[] = [
|
|||
linkIds: [SecurityPageName.mlLanding],
|
||||
},
|
||||
];
|
||||
|
||||
export const FOOTER_CATEGORIES: Array<LinkCategory<ProjectPageName>> = [
|
||||
{
|
||||
type: LinkCategoryType.separator,
|
||||
linkIds: [SecurityPageName.landing, ExternalPageName.devTools],
|
||||
},
|
||||
{
|
||||
type: LinkCategoryType.accordion,
|
||||
label: i18n.translate('xpack.securitySolutionServerless.nav.projectSettings.title', {
|
||||
defaultMessage: 'Project settings',
|
||||
}),
|
||||
iconType: 'gear',
|
||||
linkIds: [
|
||||
ExternalPageName.management,
|
||||
ExternalPageName.integrationsSecurity,
|
||||
ExternalPageName.cloudUsersAndRoles,
|
||||
ExternalPageName.cloudPerformance,
|
||||
ExternalPageName.cloudBilling,
|
||||
],
|
||||
},
|
||||
];
|
|
@ -1,47 +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 React, { Suspense } from 'react';
|
||||
import { EuiLoadingSpinner } from '@elastic/eui';
|
||||
import type { NavigationTreeDefinition } from '@kbn/shared-ux-chrome-navigation';
|
||||
import type { SideNavComponent } from '@kbn/core-chrome-browser';
|
||||
import type { Services } from '../common/services';
|
||||
|
||||
const SecurityDefaultNavigationLazy = React.lazy(() =>
|
||||
import('@kbn/shared-ux-chrome-navigation').then(
|
||||
({ DefaultNavigation, NavigationKibanaProvider }) => ({
|
||||
default: React.memo<{
|
||||
navigationTree: NavigationTreeDefinition;
|
||||
services: Services;
|
||||
}>(function SecurityDefaultNavigation({ navigationTree, services }) {
|
||||
return (
|
||||
<NavigationKibanaProvider
|
||||
core={services}
|
||||
serverless={services.serverless}
|
||||
cloud={services.cloud}
|
||||
>
|
||||
<DefaultNavigation
|
||||
navigationTree={navigationTree}
|
||||
dataTestSubj="securitySolutionSideNav"
|
||||
/>
|
||||
</NavigationKibanaProvider>
|
||||
);
|
||||
}),
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
export const getDefaultNavigationComponent = (
|
||||
navigationTree: NavigationTreeDefinition,
|
||||
services: Services
|
||||
): SideNavComponent =>
|
||||
function SecuritySideNavComponent() {
|
||||
return (
|
||||
<Suspense fallback={<EuiLoadingSpinner size="m" />}>
|
||||
<SecurityDefaultNavigationLazy navigationTree={navigationTree} services={services} />
|
||||
</Suspense>
|
||||
);
|
||||
};
|
|
@ -9,10 +9,11 @@ import { APP_PATH } from '@kbn/security-solution-plugin/common';
|
|||
import type { CoreSetup } from '@kbn/core/public';
|
||||
import type { SecuritySolutionServerlessPluginSetupDeps } from '../types';
|
||||
import type { Services } from '../common/services';
|
||||
import { withServicesProvider } from '../common/services';
|
||||
import { subscribeBreadcrumbs } from './breadcrumbs';
|
||||
import { ProjectNavigationTree } from './navigation_tree';
|
||||
import { getSecuritySideNavComponent } from './side_navigation';
|
||||
import { getDefaultNavigationComponent } from './default_navigation';
|
||||
import { SecuritySideNavComponent } from './project_navigation';
|
||||
import { projectAppLinksSwitcher } from './links/app_links';
|
||||
import { formatProjectDeepLinks } from './links/deep_links';
|
||||
|
||||
|
@ -28,15 +29,14 @@ export const startNavigation = (services: Services) => {
|
|||
const { serverless, management } = services;
|
||||
serverless.setProjectHome(APP_PATH);
|
||||
|
||||
management.setupCardsNavigation({ enabled: true });
|
||||
|
||||
const projectNavigationTree = new ProjectNavigationTree(services);
|
||||
|
||||
if (services.experimentalFeatures.platformNavEnabled) {
|
||||
projectNavigationTree.getNavigationTree$().subscribe((navigationTree) => {
|
||||
serverless.setSideNavComponent(getDefaultNavigationComponent(navigationTree, services));
|
||||
});
|
||||
const SideNavComponentWithServices = withServicesProvider(SecuritySideNavComponent, services);
|
||||
serverless.setSideNavComponent(SideNavComponentWithServices);
|
||||
} else {
|
||||
management.setupCardsNavigation({ enabled: true });
|
||||
|
||||
projectNavigationTree.getChromeNavigationTree$().subscribe((chromeNavigationTree) => {
|
||||
serverless.setNavigation({ navigationTree: chromeNavigationTree });
|
||||
});
|
||||
|
|
|
@ -31,10 +31,6 @@ export const projectSettingsNavLinks: ProjectNavigationLink[] = [
|
|||
id: ExternalPageName.cloudUsersAndRoles,
|
||||
title: i18n.CLOUD_USERS_ROLES_TITLE,
|
||||
},
|
||||
{
|
||||
id: ExternalPageName.cloudPerformance,
|
||||
title: i18n.CLOUD_PERFORMANCE_TITLE,
|
||||
},
|
||||
{
|
||||
id: ExternalPageName.cloudBilling,
|
||||
title: i18n.CLOUD_BILLING_TITLE,
|
||||
|
|
|
@ -5,7 +5,8 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { APP_UI_ID } from '@kbn/security-solution-plugin/common';
|
||||
import { APP_UI_ID, SecurityPageName } from '@kbn/security-solution-plugin/common';
|
||||
import { ExternalPageName } from './constants';
|
||||
import type { GetCloudUrl, ProjectPageName } from './types';
|
||||
|
||||
export const getNavLinkIdFromProjectPageName = (projectNavLinkId: ProjectPageName): string => {
|
||||
|
@ -42,3 +43,16 @@ export const getCloudUrl: GetCloudUrl = (cloudUrlKey, cloud) => {
|
|||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Defines the navigation items that should be in the footer of the side navigation.
|
||||
* @todo Make it a new property in the `NavigationLink` type `position?: 'top' | 'bottom' (default: 'top')`
|
||||
*/
|
||||
export const isBottomNavItemId = (id: string) =>
|
||||
id === SecurityPageName.landing ||
|
||||
id === ExternalPageName.devTools ||
|
||||
id === ExternalPageName.management ||
|
||||
id === ExternalPageName.integrationsSecurity ||
|
||||
id === ExternalPageName.cloudUsersAndRoles ||
|
||||
id === ExternalPageName.cloudPerformance ||
|
||||
id === ExternalPageName.cloudBilling;
|
||||
|
|
|
@ -7,16 +7,10 @@
|
|||
|
||||
import type { Observable } from 'rxjs';
|
||||
import { map } from 'rxjs';
|
||||
import type { NavigationTreeDefinition } from '@kbn/shared-ux-chrome-navigation';
|
||||
import type { ChromeProjectNavigationNode } from '@kbn/core-chrome-browser';
|
||||
import type { LinkCategory } from '@kbn/security-solution-navigation';
|
||||
import type { Services } from '../../common/services';
|
||||
import type { ProjectNavLinks, ProjectPageName } from '../links/types';
|
||||
import type { ProjectNavLinks } from '../links/types';
|
||||
import { getFormatChromeProjectNavNodes } from './chrome_navigation_tree';
|
||||
import { formatNavigationTree } from './navigation_tree';
|
||||
import { CATEGORIES } from '../side_navigation/categories';
|
||||
|
||||
const projectCategories = CATEGORIES as Array<LinkCategory<ProjectPageName>>;
|
||||
|
||||
/**
|
||||
* This class is temporary until we can remove the chrome navigation tree and use only the formatNavigationTree
|
||||
|
@ -29,12 +23,6 @@ export class ProjectNavigationTree {
|
|||
this.projectNavLinks$ = getProjectNavLinks$();
|
||||
}
|
||||
|
||||
public getNavigationTree$(): Observable<NavigationTreeDefinition> {
|
||||
return this.projectNavLinks$.pipe(
|
||||
map((projectNavLinks) => formatNavigationTree(projectNavLinks, projectCategories))
|
||||
);
|
||||
}
|
||||
|
||||
public getChromeNavigationTree$(): Observable<ChromeProjectNavigationNode[]> {
|
||||
const formatChromeProjectNavNodes = getFormatChromeProjectNavNodes(this.services);
|
||||
return this.projectNavLinks$.pipe(
|
||||
|
|
|
@ -48,11 +48,12 @@ describe('formatNavigationTree', () => {
|
|||
});
|
||||
|
||||
it('should format flat nav nodes', async () => {
|
||||
const navigationTree = formatNavigationTree([link1]);
|
||||
const navigationTree = formatNavigationTree([link1], [], []);
|
||||
const securityNode = navigationTree.body?.[0] as GroupDefinition;
|
||||
|
||||
expect(securityNode?.children).toEqual([
|
||||
{
|
||||
id: link1.id,
|
||||
link: chromeNavLink1.id,
|
||||
title: link1.title,
|
||||
},
|
||||
|
@ -65,15 +66,17 @@ describe('formatNavigationTree', () => {
|
|||
type: LinkCategoryType.title,
|
||||
linkIds: [link1Id],
|
||||
};
|
||||
const navigationTree = formatNavigationTree([link1], [category]);
|
||||
const navigationTree = formatNavigationTree([link1], [category], []);
|
||||
const securityNode = navigationTree.body?.[0] as GroupDefinition;
|
||||
|
||||
expect(securityNode?.children).toEqual([
|
||||
{
|
||||
title: category.label,
|
||||
id: expect.any(String),
|
||||
breadcrumbStatus: 'hidden',
|
||||
children: [
|
||||
{
|
||||
id: link1.id,
|
||||
link: chromeNavLink1.id,
|
||||
title: link1.title,
|
||||
},
|
||||
|
@ -88,17 +91,24 @@ describe('formatNavigationTree', () => {
|
|||
type: LinkCategoryType.separator,
|
||||
linkIds: [link1Id, link2Id],
|
||||
};
|
||||
const navigationTree = formatNavigationTree([link1, link2], [category]);
|
||||
const navigationTree = formatNavigationTree([link1, link2], [category], []);
|
||||
const securityNode = navigationTree.body?.[0] as GroupDefinition;
|
||||
|
||||
expect(securityNode?.children).toEqual([
|
||||
{
|
||||
link: chromeNavLink1.id,
|
||||
title: link1.title,
|
||||
},
|
||||
{
|
||||
link: chromeNavLink2.id,
|
||||
title: link2.title,
|
||||
breadcrumbStatus: 'hidden',
|
||||
children: [
|
||||
{
|
||||
id: link1.id,
|
||||
link: chromeNavLink1.id,
|
||||
title: link1.title,
|
||||
},
|
||||
{
|
||||
id: link2.id,
|
||||
link: chromeNavLink2.id,
|
||||
title: link2.title,
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
@ -109,15 +119,17 @@ describe('formatNavigationTree', () => {
|
|||
type: LinkCategoryType.title,
|
||||
linkIds: [link1Id, link2Id],
|
||||
};
|
||||
const navigationTree = formatNavigationTree([link1], [category]);
|
||||
const navigationTree = formatNavigationTree([link1], [category], []);
|
||||
const securityNode = navigationTree.body?.[0] as GroupDefinition;
|
||||
|
||||
expect(securityNode?.children).toEqual([
|
||||
{
|
||||
title: category.label,
|
||||
id: expect.any(String),
|
||||
breadcrumbStatus: 'hidden',
|
||||
children: [
|
||||
{
|
||||
id: link1.id,
|
||||
link: chromeNavLink1.id,
|
||||
title: link1.title,
|
||||
},
|
||||
|
@ -132,15 +144,17 @@ describe('formatNavigationTree', () => {
|
|||
type: LinkCategoryType.title,
|
||||
linkIds: [link1Id],
|
||||
};
|
||||
const navigationTree = formatNavigationTree([link1, link2], [category]);
|
||||
const navigationTree = formatNavigationTree([link1, link2], [category], []);
|
||||
const securityNode = navigationTree.body?.[0] as GroupDefinition;
|
||||
|
||||
expect(securityNode?.children).toEqual([
|
||||
{
|
||||
title: category.label,
|
||||
id: expect.any(String),
|
||||
breadcrumbStatus: 'hidden',
|
||||
children: [
|
||||
{
|
||||
id: link1.id,
|
||||
link: chromeNavLink1.id,
|
||||
title: link1.title,
|
||||
},
|
||||
|
@ -150,11 +164,12 @@ describe('formatNavigationTree', () => {
|
|||
});
|
||||
|
||||
it('should format external chrome nav nodes', async () => {
|
||||
const navigationTree = formatNavigationTree([link3]);
|
||||
const navigationTree = formatNavigationTree([link3], [], []);
|
||||
const securityNode = navigationTree.body?.[0] as GroupDefinition;
|
||||
|
||||
expect(securityNode?.children).toEqual([
|
||||
{
|
||||
id: link3.id,
|
||||
link: chromeNavLink3.id,
|
||||
title: link3.title,
|
||||
},
|
||||
|
@ -162,20 +177,25 @@ describe('formatNavigationTree', () => {
|
|||
});
|
||||
|
||||
it('should set nested links', async () => {
|
||||
const navigationTree = formatNavigationTree([
|
||||
{ ...link1, links: [{ ...link2, links: [link3] }] },
|
||||
]);
|
||||
const navigationTree = formatNavigationTree(
|
||||
[{ ...link1, links: [{ ...link2, links: [link3] }] }],
|
||||
[],
|
||||
[]
|
||||
);
|
||||
const securityNode = navigationTree.body?.[0] as GroupDefinition;
|
||||
|
||||
expect(securityNode?.children).toEqual([
|
||||
{
|
||||
id: link1.id,
|
||||
link: chromeNavLink1.id,
|
||||
title: link1.title,
|
||||
children: [
|
||||
{
|
||||
id: link2.id,
|
||||
link: chromeNavLink2.id,
|
||||
title: link2.title,
|
||||
children: [{ link: chromeNavLink3.id, title: link3.title }],
|
||||
children: [{ id: link3.id, link: chromeNavLink3.id, title: link3.title }],
|
||||
renderAs: 'panelOpener',
|
||||
},
|
||||
],
|
||||
},
|
||||
|
@ -188,19 +208,22 @@ describe('formatNavigationTree', () => {
|
|||
id: `${APP_UI_ID}:${SecurityPageName.usersEvents}`, // userEvents link is blacklisted
|
||||
};
|
||||
|
||||
const navigationTree = formatNavigationTree([
|
||||
{ ...link1, id: SecurityPageName.usersEvents },
|
||||
link2,
|
||||
]);
|
||||
const navigationTree = formatNavigationTree(
|
||||
[{ ...link1, id: SecurityPageName.usersEvents }, link2],
|
||||
[],
|
||||
[]
|
||||
);
|
||||
const securityNode = navigationTree.body?.[0] as GroupDefinition;
|
||||
|
||||
expect(securityNode?.children).toEqual([
|
||||
{
|
||||
id: SecurityPageName.usersEvents,
|
||||
link: chromeNavLinkTest.id,
|
||||
title: link1.title,
|
||||
breadcrumbStatus: 'hidden',
|
||||
},
|
||||
{
|
||||
id: link2.id,
|
||||
link: chromeNavLink2.id,
|
||||
title: link2.title,
|
||||
},
|
||||
|
|
|
@ -4,118 +4,66 @@
|
|||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import { partition } from 'lodash/fp';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import type { NavigationTreeDefinition } from '@kbn/shared-ux-chrome-navigation';
|
||||
import type {
|
||||
NavigationTreeDefinition,
|
||||
RootNavigationItemDefinition,
|
||||
} from '@kbn/shared-ux-chrome-navigation';
|
||||
import type { AppDeepLinkId, NodeDefinition } from '@kbn/core-chrome-browser';
|
||||
import type { LinkCategory } from '@kbn/security-solution-navigation';
|
||||
import {
|
||||
SecurityPageName,
|
||||
isSeparatorLinkCategory,
|
||||
isTitleLinkCategory,
|
||||
isAccordionLinkCategory,
|
||||
} from '@kbn/security-solution-navigation';
|
||||
import type { ProjectNavigationLink, ProjectPageName } from '../links/types';
|
||||
import { getNavLinkIdFromProjectPageName } from '../links/util';
|
||||
import { getNavLinkIdFromProjectPageName, isBottomNavItemId, isCloudLink } from '../links/util';
|
||||
import { isBreadcrumbHidden } from './utils';
|
||||
import { ExternalPageName } from '../links/constants';
|
||||
|
||||
const SECURITY_TITLE = i18n.translate('xpack.securitySolutionServerless.nav.solution.title', {
|
||||
defaultMessage: 'Security',
|
||||
});
|
||||
const GET_STARTED_TITLE = i18n.translate('xpack.securitySolutionServerless.nav.getStarted.title', {
|
||||
defaultMessage: 'Get Started',
|
||||
});
|
||||
const DEV_TOOLS_TITLE = i18n.translate('xpack.securitySolutionServerless.nav.devTools.title', {
|
||||
defaultMessage: 'Developer tools',
|
||||
});
|
||||
const PROJECT_SETTINGS_TITLE = i18n.translate(
|
||||
'xpack.securitySolutionServerless.nav.projectSettings.title',
|
||||
const SECURITY_PROJECT_TITLE = i18n.translate(
|
||||
'xpack.securitySolutionServerless.nav.solution.title',
|
||||
{
|
||||
defaultMessage: 'Project settings',
|
||||
defaultMessage: 'Security',
|
||||
}
|
||||
);
|
||||
|
||||
export const formatNavigationTree = (
|
||||
projectNavLinks: ProjectNavigationLink[],
|
||||
categories?: Readonly<Array<LinkCategory<ProjectPageName>>>
|
||||
bodyCategories: Readonly<Array<LinkCategory<ProjectPageName>>>,
|
||||
footerCategories: Readonly<Array<LinkCategory<ProjectPageName>>>
|
||||
): NavigationTreeDefinition => {
|
||||
const children = formatNodesFromLinks(projectNavLinks, categories);
|
||||
const [bodyNavItems, footerNavItems] = partition(
|
||||
({ id }) => !isBottomNavItemId(id),
|
||||
projectNavLinks
|
||||
);
|
||||
|
||||
const bodyChildren = addMainLinksPanelOpenerProp(
|
||||
formatNodesFromLinks(bodyNavItems, bodyCategories)
|
||||
);
|
||||
return {
|
||||
body: [
|
||||
children
|
||||
? {
|
||||
type: 'navGroup',
|
||||
id: 'security_project_nav',
|
||||
title: SECURITY_TITLE,
|
||||
icon: 'logoSecurity',
|
||||
breadcrumbStatus: 'hidden',
|
||||
defaultIsCollapsed: false,
|
||||
children,
|
||||
}
|
||||
: {
|
||||
type: 'navItem',
|
||||
id: 'security_project_nav',
|
||||
title: SECURITY_TITLE,
|
||||
icon: 'logoSecurity',
|
||||
breadcrumbStatus: 'hidden',
|
||||
},
|
||||
],
|
||||
footer: [
|
||||
{
|
||||
type: 'navItem',
|
||||
id: 'getStarted',
|
||||
title: GET_STARTED_TITLE,
|
||||
link: getNavLinkIdFromProjectPageName(SecurityPageName.landing) as AppDeepLinkId,
|
||||
icon: 'launch',
|
||||
},
|
||||
{
|
||||
type: 'navItem',
|
||||
id: 'devTools',
|
||||
title: DEV_TOOLS_TITLE,
|
||||
link: 'dev_tools',
|
||||
icon: 'editorCodeBlock',
|
||||
},
|
||||
{
|
||||
type: 'navGroup',
|
||||
id: 'project_settings_project_nav',
|
||||
title: PROJECT_SETTINGS_TITLE,
|
||||
icon: 'gear',
|
||||
id: 'security_project_nav',
|
||||
title: SECURITY_PROJECT_TITLE,
|
||||
icon: 'logoSecurity',
|
||||
breadcrumbStatus: 'hidden',
|
||||
children: [
|
||||
{
|
||||
id: 'settings',
|
||||
children: [
|
||||
{
|
||||
link: 'management',
|
||||
title: 'Management',
|
||||
},
|
||||
{
|
||||
link: 'integrations',
|
||||
},
|
||||
{
|
||||
link: 'fleet',
|
||||
},
|
||||
{
|
||||
id: 'cloudLinkUserAndRoles',
|
||||
cloudLink: 'userAndRoles',
|
||||
},
|
||||
{
|
||||
id: 'cloudLinkBilling',
|
||||
cloudLink: 'billingAndSub',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
defaultIsCollapsed: false,
|
||||
children: bodyChildren,
|
||||
},
|
||||
],
|
||||
footer: formatFooterNodesFromLinks(footerNavItems, footerCategories),
|
||||
};
|
||||
};
|
||||
|
||||
// Body
|
||||
|
||||
const formatNodesFromLinks = (
|
||||
projectNavLinks: ProjectNavigationLink[],
|
||||
parentCategories?: Readonly<Array<LinkCategory<ProjectPageName>>>
|
||||
): NodeDefinition[] | undefined => {
|
||||
if (projectNavLinks.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
): NodeDefinition[] => {
|
||||
const nodes: NodeDefinition[] = [];
|
||||
if (parentCategories?.length) {
|
||||
parentCategories.forEach((category) => {
|
||||
|
@ -124,10 +72,7 @@ const formatNodesFromLinks = (
|
|||
} else {
|
||||
nodes.push(...formatNodesFromLinksWithoutCategory(projectNavLinks));
|
||||
}
|
||||
if (nodes.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
return nodes as NodeDefinition[];
|
||||
return nodes;
|
||||
};
|
||||
|
||||
const formatNodesFromLinksWithCategory = (
|
||||
|
@ -137,7 +82,8 @@ const formatNodesFromLinksWithCategory = (
|
|||
if (!category?.linkIds) {
|
||||
return [];
|
||||
}
|
||||
if (isTitleLinkCategory(category)) {
|
||||
|
||||
if (category.linkIds) {
|
||||
const children = category.linkIds.reduce<NodeDefinition[]>((acc, linkId) => {
|
||||
const projectNavLink = projectNavLinks.find(({ id }) => id === linkId);
|
||||
if (projectNavLink != null) {
|
||||
|
@ -145,48 +91,135 @@ const formatNodesFromLinksWithCategory = (
|
|||
}
|
||||
return acc;
|
||||
}, []);
|
||||
if (children.length === 0) {
|
||||
if (!children.length) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const id = isTitleLinkCategory(category) ? getCategoryIdFromLabel(category.label) : undefined;
|
||||
|
||||
return [
|
||||
{
|
||||
id: `category-${category.label.toLowerCase().replace(' ', '_')}`,
|
||||
title: category.label,
|
||||
children: children as NodeDefinition[],
|
||||
id,
|
||||
...(isTitleLinkCategory(category) && { title: category.label }),
|
||||
breadcrumbStatus: 'hidden',
|
||||
children,
|
||||
},
|
||||
];
|
||||
} else if (isSeparatorLinkCategory(category)) {
|
||||
// TODO: Add separator support when implemented in the shared-ux navigation
|
||||
const categoryProjectNavLinks = category.linkIds.reduce<ProjectNavigationLink[]>(
|
||||
(acc, linkId) => {
|
||||
const projectNavLink = projectNavLinks.find(({ id }) => id === linkId);
|
||||
if (projectNavLink != null) {
|
||||
acc.push(projectNavLink);
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
[]
|
||||
);
|
||||
return formatNodesFromLinksWithoutCategory(categoryProjectNavLinks);
|
||||
}
|
||||
return [];
|
||||
};
|
||||
|
||||
const formatNodesFromLinksWithoutCategory = (projectNavLinks: ProjectNavigationLink[]) =>
|
||||
projectNavLinks.map((projectNavLink) =>
|
||||
createNodeFromProjectNavLink(projectNavLink)
|
||||
) as NodeDefinition[];
|
||||
const formatNodesFromLinksWithoutCategory = (
|
||||
projectNavLinks: ProjectNavigationLink[]
|
||||
): NodeDefinition[] =>
|
||||
projectNavLinks.map((projectNavLink) => createNodeFromProjectNavLink(projectNavLink));
|
||||
|
||||
const createNodeFromProjectNavLink = (projectNavLink: ProjectNavigationLink): NodeDefinition => {
|
||||
const { id, title, links, categories } = projectNavLink;
|
||||
const { id, title, links, categories, disabled } = projectNavLink;
|
||||
const link = getNavLinkIdFromProjectPageName(id);
|
||||
const node: NodeDefinition = {
|
||||
id,
|
||||
link: link as AppDeepLinkId,
|
||||
title,
|
||||
...(isBreadcrumbHidden(id) && { breadcrumbStatus: 'hidden' }),
|
||||
...(disabled && { sideNavStatus: 'hidden' }),
|
||||
};
|
||||
if (links?.length) {
|
||||
node.children = formatNodesFromLinks(links, categories);
|
||||
}
|
||||
return node;
|
||||
};
|
||||
|
||||
// Footer
|
||||
|
||||
const formatFooterNodesFromLinks = (
|
||||
projectNavLinks: ProjectNavigationLink[],
|
||||
parentCategories?: Readonly<Array<LinkCategory<ProjectPageName>>>
|
||||
): RootNavigationItemDefinition[] => {
|
||||
const nodes: RootNavigationItemDefinition[] = [];
|
||||
if (parentCategories?.length) {
|
||||
parentCategories.forEach((category) => {
|
||||
if (isSeparatorLinkCategory(category)) {
|
||||
nodes.push(
|
||||
...category.linkIds.reduce<RootNavigationItemDefinition[]>((acc, linkId) => {
|
||||
const projectNavLink = projectNavLinks.find(({ id }) => id === linkId);
|
||||
if (projectNavLink != null) {
|
||||
acc.push({
|
||||
type: 'navItem',
|
||||
link: getNavLinkIdFromProjectPageName(projectNavLink.id) as AppDeepLinkId,
|
||||
title: projectNavLink.title,
|
||||
icon: projectNavLink.sideNavIcon,
|
||||
});
|
||||
}
|
||||
return acc;
|
||||
}, [])
|
||||
);
|
||||
}
|
||||
if (isAccordionLinkCategory(category)) {
|
||||
nodes.push({
|
||||
type: 'navGroup',
|
||||
id: getCategoryIdFromLabel(category.label),
|
||||
title: category.label,
|
||||
icon: category.iconType,
|
||||
breadcrumbStatus: 'hidden',
|
||||
defaultIsCollapsed: true,
|
||||
children:
|
||||
category.linkIds?.reduce<NodeDefinition[]>((acc, linkId) => {
|
||||
const projectNavLink = projectNavLinks.find(({ id }) => id === linkId);
|
||||
if (projectNavLink != null) {
|
||||
acc.push({
|
||||
title: projectNavLink.title,
|
||||
...(isCloudLink(projectNavLink.id)
|
||||
? {
|
||||
cloudLink: getCloudLink(projectNavLink.id),
|
||||
openInNewTab: true,
|
||||
}
|
||||
: {
|
||||
link: getNavLinkIdFromProjectPageName(projectNavLink.id) as AppDeepLinkId,
|
||||
}),
|
||||
});
|
||||
}
|
||||
return acc;
|
||||
}, []) ?? [],
|
||||
});
|
||||
}
|
||||
}, []);
|
||||
}
|
||||
return nodes;
|
||||
};
|
||||
|
||||
// Utils
|
||||
|
||||
const getCategoryIdFromLabel = (label: string): string =>
|
||||
`category-${label.toLowerCase().replace(' ', '_')}`;
|
||||
|
||||
/**
|
||||
* Adds the `renderAs: 'panelOpener'` prop to the main links that have children
|
||||
* This function expects all main links to be in nested groups to add the separation between them.
|
||||
* If these "separator" groups change this function will need to be updated.
|
||||
*/
|
||||
const addMainLinksPanelOpenerProp = (nodes: NodeDefinition[]): NodeDefinition[] =>
|
||||
nodes.map((node): NodeDefinition => {
|
||||
if (node.children?.length) {
|
||||
return {
|
||||
...node,
|
||||
children: node.children.map((child) => ({
|
||||
...child,
|
||||
...(child.children && { renderAs: 'panelOpener' }),
|
||||
})),
|
||||
};
|
||||
}
|
||||
return node;
|
||||
});
|
||||
|
||||
/** Returns the cloud link entry the default navigation expects */
|
||||
const getCloudLink = (id: ProjectPageName) => {
|
||||
switch (id) {
|
||||
case ExternalPageName.cloudUsersAndRoles:
|
||||
return 'userAndRoles';
|
||||
case ExternalPageName.cloudBilling:
|
||||
return 'billingAndSub';
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { Suspense } from 'react';
|
||||
import { EuiLoadingSpinner } from '@elastic/eui';
|
||||
|
||||
const SecuritySideNavComponentLazy = React.lazy(() => import('./project_navigation'));
|
||||
|
||||
export const SecuritySideNavComponent = () => (
|
||||
<Suspense fallback={<EuiLoadingSpinner size="s" />}>
|
||||
<SecuritySideNavComponentLazy />
|
||||
</Suspense>
|
||||
);
|
|
@ -0,0 +1,80 @@
|
|||
/*
|
||||
* 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, { useCallback, useMemo } from 'react';
|
||||
import { DefaultNavigation, NavigationKibanaProvider } from '@kbn/shared-ux-chrome-navigation';
|
||||
import type {
|
||||
ContentProvider,
|
||||
PanelComponentProps,
|
||||
} from '@kbn/shared-ux-chrome-navigation/src/ui/components/panel/types';
|
||||
import { SolutionSideNavPanelContent } from '@kbn/security-solution-side-nav/panel';
|
||||
import useObservable from 'react-use/lib/useObservable';
|
||||
import { useKibana } from '../../common/services';
|
||||
import type { ProjectNavigationLink, ProjectPageName } from '../links/types';
|
||||
import { useFormattedSideNavItems } from '../side_navigation/use_side_nav_items';
|
||||
import { CATEGORIES, FOOTER_CATEGORIES } from '../categories';
|
||||
import { formatNavigationTree } from '../navigation_tree/navigation_tree';
|
||||
|
||||
const getPanelContentProvider = (
|
||||
projectNavLinks: ProjectNavigationLink[]
|
||||
): React.FC<PanelComponentProps> =>
|
||||
React.memo(function PanelContentProvider({ selectedNode: { path }, closePanel }) {
|
||||
const linkId = path[path.length - 1] as ProjectPageName;
|
||||
const currentPanelItem = projectNavLinks.find((item) => item.id === linkId);
|
||||
|
||||
const { title = '', links = [], categories } = currentPanelItem ?? {};
|
||||
const items = useFormattedSideNavItems(links);
|
||||
|
||||
if (items.length === 0) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<SolutionSideNavPanelContent
|
||||
title={title}
|
||||
items={items}
|
||||
categories={categories}
|
||||
onClose={closePanel}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
const usePanelContentProvider = (projectNavLinks: ProjectNavigationLink[]): ContentProvider => {
|
||||
return useCallback(
|
||||
() => ({
|
||||
content: getPanelContentProvider(projectNavLinks),
|
||||
}),
|
||||
[projectNavLinks]
|
||||
);
|
||||
};
|
||||
|
||||
export const SecuritySideNavComponent = React.memo(function SecuritySideNavComponent() {
|
||||
const services = useKibana().services;
|
||||
const projectNavLinks = useObservable(services.getProjectNavLinks$(), []);
|
||||
|
||||
const navigationTree = useMemo(
|
||||
() => formatNavigationTree(projectNavLinks, CATEGORIES, FOOTER_CATEGORIES),
|
||||
[projectNavLinks]
|
||||
);
|
||||
|
||||
const panelContentProvider = usePanelContentProvider(projectNavLinks);
|
||||
|
||||
return (
|
||||
<NavigationKibanaProvider
|
||||
core={services}
|
||||
serverless={services.serverless}
|
||||
cloud={services.cloud}
|
||||
>
|
||||
<DefaultNavigation
|
||||
dataTestSubj="securitySolutionSideNav"
|
||||
navigationTree={navigationTree}
|
||||
panelContentProvider={panelContentProvider}
|
||||
/>
|
||||
</NavigationKibanaProvider>
|
||||
);
|
||||
});
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default SecuritySideNavComponent;
|
|
@ -20,7 +20,7 @@ import { useObservable } from 'react-use';
|
|||
import { css } from '@emotion/react';
|
||||
import { partition } from 'lodash/fp';
|
||||
import { useSideNavItems } from './use_side_nav_items';
|
||||
import { CATEGORIES } from './categories';
|
||||
import { CATEGORIES, FOOTER_CATEGORIES } from '../categories';
|
||||
import { getProjectPageNameFromNavLinkId } from '../links/util';
|
||||
import { useKibana } from '../../common/services';
|
||||
import { SideNavigationFooter } from './side_navigation_footer';
|
||||
|
@ -39,12 +39,7 @@ export const SecuritySideNavigation: SideNavComponent = React.memo(function Secu
|
|||
const { chrome } = useKibana().services;
|
||||
const { euiTheme } = useEuiTheme();
|
||||
const hasHeaderBanner = useObservable(chrome.hasHeaderBanner$());
|
||||
|
||||
/**
|
||||
* TODO: Uncomment this when we have the getIsSideNavCollapsed API available
|
||||
* const isCollapsed = useObservable(chrome.getIsSideNavCollapsed$());
|
||||
*/
|
||||
const isCollapsed = false;
|
||||
const isCollapsed = useObservable(chrome.getIsSideNavCollapsed$());
|
||||
|
||||
const items = useSideNavItems();
|
||||
|
||||
|
@ -70,7 +65,7 @@ export const SecuritySideNavigation: SideNavComponent = React.memo(function Secu
|
|||
padding-right: ${euiTheme.size.s};
|
||||
`;
|
||||
|
||||
const collapsedNavItems = useMemo(() => {
|
||||
const collapsedBodyItems = useMemo(() => {
|
||||
return CATEGORIES.reduce<EuiCollapsibleNavItemProps[]>((links, category) => {
|
||||
const categoryLinks = items.filter((item) => category.linkIds.includes(item.id));
|
||||
links.push(...categoryLinks.map((link) => getEuiNavItemFromSideNavItem(link, selectedId)));
|
||||
|
@ -93,7 +88,7 @@ export const SecuritySideNavigation: SideNavComponent = React.memo(function Secu
|
|||
icon="logoSecurity"
|
||||
iconProps={{ size: 'm' }}
|
||||
data-test-subj="securitySolutionNavHeading"
|
||||
items={isCollapsed ? collapsedNavItems : undefined}
|
||||
items={isCollapsed ? collapsedBodyItems : undefined}
|
||||
/>
|
||||
{!isCollapsed && (
|
||||
<div css={bodyStyle}>
|
||||
|
@ -107,7 +102,11 @@ export const SecuritySideNavigation: SideNavComponent = React.memo(function Secu
|
|||
)}
|
||||
</EuiCollapsibleNavBeta.Body>
|
||||
<EuiCollapsibleNavBeta.Footer>
|
||||
<SideNavigationFooter activeNodeId={activeNodeId} items={footerItems} />
|
||||
<SideNavigationFooter
|
||||
activeNodeId={activeNodeId}
|
||||
items={footerItems}
|
||||
categories={FOOTER_CATEGORIES}
|
||||
/>
|
||||
</EuiCollapsibleNavBeta.Footer>
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -12,6 +12,7 @@ import { SideNavigationFooter } from './side_navigation_footer';
|
|||
import { ExternalPageName } from '../links/constants';
|
||||
import { I18nProvider } from '@kbn/i18n-react';
|
||||
import type { ProjectSideNavItem } from './types';
|
||||
import { FOOTER_CATEGORIES } from '../categories';
|
||||
|
||||
jest.mock('../../common/services');
|
||||
|
||||
|
@ -54,9 +55,12 @@ describe('SideNavigationFooter', () => {
|
|||
});
|
||||
|
||||
it('should render all the items', () => {
|
||||
const component = render(<SideNavigationFooter items={items} activeNodeId={''} />, {
|
||||
wrapper: I18nProvider,
|
||||
});
|
||||
const component = render(
|
||||
<SideNavigationFooter items={items} activeNodeId={''} categories={FOOTER_CATEGORIES} />,
|
||||
{
|
||||
wrapper: I18nProvider,
|
||||
}
|
||||
);
|
||||
|
||||
items.forEach((item) => {
|
||||
expect(component.queryByTestId(`solutionSideNavItemLink-${item.id}`)).toBeInTheDocument();
|
||||
|
@ -64,9 +68,16 @@ describe('SideNavigationFooter', () => {
|
|||
});
|
||||
|
||||
it('should highlight the active node', () => {
|
||||
const component = render(<SideNavigationFooter items={items} activeNodeId={'dev_tools'} />, {
|
||||
wrapper: I18nProvider,
|
||||
});
|
||||
const component = render(
|
||||
<SideNavigationFooter
|
||||
items={items}
|
||||
activeNodeId={'dev_tools'}
|
||||
categories={FOOTER_CATEGORIES}
|
||||
/>,
|
||||
{
|
||||
wrapper: I18nProvider,
|
||||
}
|
||||
);
|
||||
|
||||
items.forEach((item) => {
|
||||
const isSelected = component
|
||||
|
@ -82,9 +93,16 @@ describe('SideNavigationFooter', () => {
|
|||
});
|
||||
|
||||
it('should highlight the active node inside the collapsible', () => {
|
||||
const component = render(<SideNavigationFooter items={items} activeNodeId={'management'} />, {
|
||||
wrapper: I18nProvider,
|
||||
});
|
||||
const component = render(
|
||||
<SideNavigationFooter
|
||||
items={items}
|
||||
activeNodeId={'management'}
|
||||
categories={FOOTER_CATEGORIES}
|
||||
/>,
|
||||
{
|
||||
wrapper: I18nProvider,
|
||||
}
|
||||
);
|
||||
|
||||
items.forEach((item) => {
|
||||
const isSelected = component
|
||||
|
@ -100,9 +118,12 @@ describe('SideNavigationFooter', () => {
|
|||
});
|
||||
|
||||
it('should render closed collapsible if it has no active node', () => {
|
||||
const component = render(<SideNavigationFooter items={items} activeNodeId={''} />, {
|
||||
wrapper: I18nProvider,
|
||||
});
|
||||
const component = render(
|
||||
<SideNavigationFooter items={items} activeNodeId={''} categories={FOOTER_CATEGORIES} />,
|
||||
{
|
||||
wrapper: I18nProvider,
|
||||
}
|
||||
);
|
||||
|
||||
const isOpen = component
|
||||
.queryByTestId('navFooterCollapsible-project-settings')
|
||||
|
@ -112,9 +133,16 @@ describe('SideNavigationFooter', () => {
|
|||
});
|
||||
|
||||
it('should open collapsible if it has an active node', () => {
|
||||
const component = render(<SideNavigationFooter items={items} activeNodeId={'management'} />, {
|
||||
wrapper: I18nProvider,
|
||||
});
|
||||
const component = render(
|
||||
<SideNavigationFooter
|
||||
items={items}
|
||||
activeNodeId={'management'}
|
||||
categories={FOOTER_CATEGORIES}
|
||||
/>,
|
||||
{
|
||||
wrapper: I18nProvider,
|
||||
}
|
||||
);
|
||||
|
||||
const isOpen = component
|
||||
.queryByTestId('navFooterCollapsible-project-settings')
|
||||
|
|
|
@ -8,50 +8,32 @@
|
|||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import type { EuiCollapsibleNavSubItemProps, IconType } from '@elastic/eui';
|
||||
import { EuiCollapsibleNavItem } from '@elastic/eui';
|
||||
import { SecurityPageName } from '@kbn/security-solution-navigation';
|
||||
import { ExternalPageName } from '../links/constants';
|
||||
import {
|
||||
isAccordionLinkCategory,
|
||||
isSeparatorLinkCategory,
|
||||
type LinkCategory,
|
||||
} from '@kbn/security-solution-navigation';
|
||||
import { getNavLinkIdFromProjectPageName } from '../links/util';
|
||||
import type { ProjectSideNavItem } from './types';
|
||||
|
||||
interface FooterCategory {
|
||||
type: 'standalone' | 'collapsible';
|
||||
title?: string;
|
||||
icon?: IconType;
|
||||
linkIds: string[];
|
||||
}
|
||||
|
||||
const categories: FooterCategory[] = [
|
||||
{ type: 'standalone', linkIds: [SecurityPageName.landing, ExternalPageName.devTools] },
|
||||
{
|
||||
type: 'collapsible',
|
||||
title: 'Project Settings',
|
||||
icon: 'gear',
|
||||
linkIds: [
|
||||
ExternalPageName.management,
|
||||
ExternalPageName.integrationsSecurity,
|
||||
ExternalPageName.cloudUsersAndRoles,
|
||||
ExternalPageName.cloudPerformance,
|
||||
ExternalPageName.cloudBilling,
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export const SideNavigationFooter: React.FC<{
|
||||
activeNodeId: string;
|
||||
items: ProjectSideNavItem[];
|
||||
}> = ({ activeNodeId, items }) => {
|
||||
categories: LinkCategory[];
|
||||
}> = ({ activeNodeId, items, categories }) => {
|
||||
return (
|
||||
<>
|
||||
{categories.map((category, index) => {
|
||||
const categoryItems = category.linkIds.reduce<ProjectSideNavItem[]>((acc, linkId) => {
|
||||
const item = items.find(({ id }) => id === linkId);
|
||||
if (item) {
|
||||
acc.push(item);
|
||||
}
|
||||
return acc;
|
||||
}, []);
|
||||
const categoryItems =
|
||||
category.linkIds?.reduce<ProjectSideNavItem[]>((acc, linkId) => {
|
||||
const item = items.find(({ id }) => id === linkId);
|
||||
if (item) {
|
||||
acc.push(item);
|
||||
}
|
||||
return acc;
|
||||
}, []) ?? [];
|
||||
|
||||
if (category.type === 'standalone') {
|
||||
if (isSeparatorLinkCategory(category)) {
|
||||
return (
|
||||
<SideNavigationFooterStandalone
|
||||
key={index}
|
||||
|
@ -60,14 +42,14 @@ export const SideNavigationFooter: React.FC<{
|
|||
/>
|
||||
);
|
||||
}
|
||||
if (category.type === 'collapsible') {
|
||||
if (isAccordionLinkCategory(category)) {
|
||||
return (
|
||||
<SideNavigationFooterCollapsible
|
||||
key={index}
|
||||
title={category.title ?? ''}
|
||||
title={category.label ?? ''}
|
||||
items={categoryItems}
|
||||
activeNodeId={activeNodeId}
|
||||
icon={category.icon}
|
||||
icon={category.iconType}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -6,27 +6,18 @@
|
|||
*/
|
||||
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { SecurityPageName, type NavigationLink } from '@kbn/security-solution-navigation';
|
||||
import { type NavigationLink } from '@kbn/security-solution-navigation';
|
||||
import { useGetLinkProps } from '@kbn/security-solution-navigation/links';
|
||||
import { SolutionSideNavItemPosition } from '@kbn/security-solution-side-nav';
|
||||
import { useNavLinks } from '../../common/hooks/use_nav_links';
|
||||
import { ExternalPageName } from '../links/constants';
|
||||
import type { ProjectSideNavItem } from './types';
|
||||
import type { ProjectPageName } from '../links/types';
|
||||
import type { ProjectNavigationLink, ProjectPageName } from '../links/types';
|
||||
import { isBottomNavItemId } from '../links/util';
|
||||
|
||||
type GetLinkProps = (link: NavigationLink) => {
|
||||
href: string & Partial<ProjectSideNavItem>;
|
||||
};
|
||||
|
||||
const isBottomNavItem = (id: string) =>
|
||||
id === SecurityPageName.landing ||
|
||||
id === ExternalPageName.devTools ||
|
||||
id === ExternalPageName.management ||
|
||||
id === ExternalPageName.integrationsSecurity ||
|
||||
id === ExternalPageName.cloudUsersAndRoles ||
|
||||
id === ExternalPageName.cloudPerformance ||
|
||||
id === ExternalPageName.cloudBilling;
|
||||
|
||||
/**
|
||||
* Formats generic navigation links into the shape expected by the `SolutionSideNav`
|
||||
*/
|
||||
|
@ -52,7 +43,7 @@ const formatLink = (
|
|||
id: navLink.id,
|
||||
label: navLink.title,
|
||||
iconType: navLink.sideNavIcon,
|
||||
position: isBottomNavItem(navLink.id)
|
||||
position: isBottomNavItemId(navLink.id)
|
||||
? SolutionSideNavItemPosition.bottom
|
||||
: SolutionSideNavItemPosition.top,
|
||||
...getLinkProps(navLink),
|
||||
|
@ -66,6 +57,15 @@ const formatLink = (
|
|||
*/
|
||||
export const useSideNavItems = (): ProjectSideNavItem[] => {
|
||||
const navLinks = useNavLinks();
|
||||
return useFormattedSideNavItems(navLinks);
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns all the formatted SideNavItems, including external links
|
||||
*/
|
||||
export const useFormattedSideNavItems = (
|
||||
navLinks: ProjectNavigationLink[]
|
||||
): ProjectSideNavItem[] => {
|
||||
const getKibanaLinkProps = useGetLinkProps();
|
||||
|
||||
const getLinkProps = useCallback<GetLinkProps>(
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue