[Security Solution][Serverless] Unified Management nav (#168946)

## Summary

needed for: https://github.com/elastic/kibana/issues/166545

This PR makes the generic Management landing page available in Security
Solution project navigation. Unifying the experience with the rest of
Solutions

- Security Solution specific `Project Settings` landing page removed.
  - `SecurityPageName.projectSettings` removed.
  - `Project Settings` landing page component and route were removed.
  - Unused icons cleaned. 
  - Link to Management application added in the SideNav.
  - Management app "cards landing page" enabled.
- Cleaned the redirect logic in the Management app, this API was created
only for Security and is not used anymore.
- `developerSettings` config is not needed anymore and has been removed.
- `Entity risk score`, `Maps`, and `Visualize library` links are
configured and available, but they are not displayed in the side
navigation. They will be added as cards on the Management landing page
in a follow-up.
- Unified Navigation design implemented as a backup plan for Serverless
public preview.
- Shared-ux `DefaultNavigation` implementation is still under the
`platformNavEnabled` experimental flag.

## Screenshot

<img width="1717" alt="screenshot"
src="e3b4db3f-4d73-4890-af06-64c706317ff6">

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Sergi Massaneda 2023-10-18 12:20:00 +02:00 committed by GitHub
parent 6d88fb5f54
commit 9a1ae4b03a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
41 changed files with 584 additions and 997 deletions

View file

@ -39,13 +39,3 @@ If card needs to be hidden from the navigation you can specify that by using the
```
More specifics about the `setupCardsNavigation` can be found in `packages/kbn-management/cards_navigation/readme.mdx`.
## Landing page redirect
If the consumer wants to have a separate landing page for the management section, they can use the `setLandingPageRedirect`
method to specify the path to the landing page:
```
management.setLandingPageRedirect('/app/security/management');
```

View file

@ -24,24 +24,9 @@ export const ManagementLandingPage = ({
setBreadcrumbs,
onAppMounted,
}: ManagementLandingPageProps) => {
const {
appBasePath,
sections,
kibanaVersion,
cardsNavigationConfig,
landingPageRedirect,
navigateToUrl,
basePath,
} = useAppContext();
const { appBasePath, sections, kibanaVersion, cardsNavigationConfig } = useAppContext();
setBreadcrumbs();
// Redirect the user to the configured landing page if there is one
useEffect(() => {
if (landingPageRedirect) {
navigateToUrl(basePath.prepend(landingPageRedirect));
}
}, [landingPageRedirect, navigateToUrl, basePath]);
useEffect(() => {
onAppMounted('');
}, [onAppMounted]);

View file

@ -43,7 +43,6 @@ export interface ManagementAppDependencies {
setBreadcrumbs: (newBreadcrumbs: ChromeBreadcrumb[]) => void;
isSidebarEnabled$: BehaviorSubject<boolean>;
cardsNavigationConfig$: BehaviorSubject<NavigationCardsSubject>;
landingPageRedirect$: BehaviorSubject<string | undefined>;
}
export const ManagementApp = ({
@ -52,13 +51,11 @@ export const ManagementApp = ({
theme$,
appBasePath,
}: ManagementAppProps) => {
const { setBreadcrumbs, isSidebarEnabled$, cardsNavigationConfig$, landingPageRedirect$ } =
dependencies;
const { setBreadcrumbs, isSidebarEnabled$, cardsNavigationConfig$ } = dependencies;
const [selectedId, setSelectedId] = useState<string>('');
const [sections, setSections] = useState<ManagementSection[]>();
const isSidebarEnabled = useObservable(isSidebarEnabled$);
const cardsNavigationConfig = useObservable(cardsNavigationConfig$);
const landingPageRedirect = useObservable(landingPageRedirect$);
const onAppMounted = useCallback((id: string) => {
setSelectedId(id);
@ -114,9 +111,6 @@ export const ManagementApp = ({
sections,
cardsNavigationConfig,
kibanaVersion: dependencies.kibanaVersion,
landingPageRedirect,
navigateToUrl: dependencies.coreStart.application.navigateToUrl,
basePath: dependencies.coreStart.http.basePath,
};
return (

View file

@ -44,7 +44,6 @@ const createSetupContract = (): ManagementSetup => ({
const createStartContract = (): ManagementStart => ({
setIsSidebarEnabled: jest.fn(),
setupCardsNavigation: jest.fn(),
setLandingPageRedirect: jest.fn(),
});
export const managementPluginMock = {

View file

@ -90,7 +90,6 @@ export class ManagementPlugin
private hasAnyEnabledApps = true;
private isSidebarEnabled$ = new BehaviorSubject<boolean>(true);
private landingPageRedirect$ = new BehaviorSubject<string | undefined>(undefined);
private cardsNavigationConfig$ = new BehaviorSubject<NavigationCardsSubject>({
enabled: false,
hideLinksTo: [],
@ -151,7 +150,6 @@ export class ManagementPlugin
},
isSidebarEnabled$: managementPlugin.isSidebarEnabled$,
cardsNavigationConfig$: managementPlugin.cardsNavigationConfig$,
landingPageRedirect$: managementPlugin.landingPageRedirect$,
});
},
});
@ -209,8 +207,6 @@ export class ManagementPlugin
this.isSidebarEnabled$.next(isSidebarEnabled),
setupCardsNavigation: ({ enabled, hideLinksTo }) =>
this.cardsNavigationConfig$.next({ enabled, hideLinksTo }),
setLandingPageRedirect: (landingPageRedirect: string) =>
this.landingPageRedirect$.next(landingPageRedirect),
};
}
}

View file

@ -12,8 +12,6 @@ import type { LocatorPublic } from '@kbn/share-plugin/common';
import { ChromeBreadcrumb, CoreTheme } from '@kbn/core/public';
import type { AppId } from '@kbn/management-cards-navigation';
import { AppNavLinkStatus } from '@kbn/core/public';
import type { ApplicationStart } from '@kbn/core-application-browser';
import type { HttpStart } from '@kbn/core-http-browser';
import { ManagementSection, RegisterManagementSectionArgs } from './utils';
import type { ManagementAppLocatorParams } from '../common/locator';
@ -33,7 +31,6 @@ export interface DefinedSections {
export interface ManagementStart {
setIsSidebarEnabled: (enabled: boolean) => void;
setLandingPageRedirect: (landingPageRedirect: string) => void;
setupCardsNavigation: ({ enabled, hideLinksTo }: NavigationCardsSubject) => void;
}
@ -95,9 +92,6 @@ export interface AppDependencies {
kibanaVersion: string;
sections: ManagementSection[];
cardsNavigationConfig?: NavigationCardsSubject;
landingPageRedirect: string | undefined;
navigateToUrl: ApplicationStart['navigateToUrl'];
basePath: HttpStart['basePath'];
}
export interface ConfigSchema {

View file

@ -24,8 +24,6 @@
"@kbn/shared-ux-link-redirect-app",
"@kbn/test-jest-helpers",
"@kbn/config-schema",
"@kbn/core-application-browser",
"@kbn/core-http-browser",
"@kbn/serverless",
"@kbn/management-settings-application",
"@kbn/react-kibana-context-render",

View file

@ -59,7 +59,6 @@ export enum SecurityPageName {
noPage = '',
overview = 'overview',
policies = 'policy',
projectSettings = 'project_settings',
responseActionsHistory = 'response_actions_history',
rules = 'rules',
rulesAdd = 'rules-add',

View file

@ -33,28 +33,8 @@ export const productTypes = schema.arrayOf<SecurityProductType>(productType, {
});
export type SecurityProductTypes = TypeOf<typeof productTypes>;
/**
* Developer only options that can be set in `serverless.security.dev.yml`
*/
export const developerConfigSchema = schema.object({
/**
* Disables the redirect in the UI for kibana management pages (ex. users, roles, etc).
*
* NOTE: you likely will also need to add the following to your `serverless.security.dev.yml`
* file if wanting to access the user, roles and role mapping pages via URL
*
* xpack.security.ui.userManagementEnabled: true
* xpack.security.ui.roleManagementEnabled: true
* xpack.security.ui.roleMappingManagementEnabled: true
*/
disableManagementUrlRedirect: schema.boolean({ defaultValue: false }),
});
export type DeveloperConfig = TypeOf<typeof developerConfigSchema>;
export const configSchema = schema.object({
enabled: schema.boolean({ defaultValue: false }),
developer: developerConfigSchema,
productTypes,
/**
* For internal use. A list of string values (comma delimited) that will enable experimental

View file

@ -1,68 +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 IconIndexManagement: React.FC<SVGProps<SVGSVGElement>> = ({ ...props }) => (
<svg
width="32"
height="32"
viewBox="0 0 32 32"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<g id="Group 13">
<path
id="Fill 1"
fillRule="evenodd"
clipRule="evenodd"
d="M17 18V16H15V18H3V24H5V20H15V24H17V20H27V24H29V18H17Z"
fill="#535766"
/>
<g id="Group 12">
<path
id="Fill 2"
fillRule="evenodd"
clipRule="evenodd"
d="M4 28C3.448 28 3 28.448 3 29C3 29.552 3.448 30 4 30C4.552 30 5 29.552 5 29C5 28.448 4.552 28 4 28ZM4 32C2.346 32 1 30.654 1 29C1 27.346 2.346 26 4 26C5.654 26 7 27.346 7 29C7 30.654 5.654 32 4 32Z"
fill="#00BFB3"
/>
<path
id="Fill 4"
fillRule="evenodd"
clipRule="evenodd"
d="M16 28C15.448 28 15 28.448 15 29C15 29.552 15.448 30 16 30C16.552 30 17 29.552 17 29C17 28.448 16.552 28 16 28ZM16 32C14.346 32 13 30.654 13 29C13 27.346 14.346 26 16 26C17.654 26 19 27.346 19 29C19 30.654 17.654 32 16 32Z"
fill="#00BFB3"
/>
<path
id="Fill 6"
fillRule="evenodd"
clipRule="evenodd"
d="M28 28C27.448 28 27 28.448 27 29C27 29.552 27.448 30 28 30C28.552 30 29 29.552 29 29C29 28.448 28.552 28 28 28ZM28 32C26.346 32 25 30.654 25 29C25 27.346 26.346 26 28 26C29.654 26 31 27.346 31 29C31 30.654 29.654 32 28 32Z"
fill="#00BFB3"
/>
<path
id="Fill 8"
fillRule="evenodd"
clipRule="evenodd"
d="M16 10C14.346 10 13 8.654 13 7C13 5.346 14.346 4 16 4C17.654 4 19 5.346 19 7C19 8.654 17.654 10 16 10ZM23 8V6H20.898C20.77 5.363 20.515 4.771 20.167 4.247L21.657 2.757L20.243 1.343L18.753 2.833C18.229 2.485 17.637 2.23 17 2.102V0H15V2.102C14.363 2.23 13.771 2.485 13.247 2.833L11.757 1.343L10.343 2.757L11.833 4.247C11.485 4.771 11.23 5.363 11.102 6H9V8H11.102C11.23 8.637 11.485 9.229 11.833 9.753L10.343 11.243L11.757 12.657L13.247 11.167C13.771 11.515 14.363 11.77 15 11.898V14H17V11.898C17.637 11.77 18.229 11.515 18.753 11.167L20.243 12.657L21.657 11.243L20.167 9.753C20.515 9.229 20.77 8.637 20.898 8H23Z"
fill="#00BFB3"
/>
<path
id="Fill 10"
fillRule="evenodd"
clipRule="evenodd"
d="M16 8C15.729 8 15.479 7.899 15.29 7.71C15.1 7.52 15 7.27 15 7C15 6.93 15.01 6.87 15.02 6.8C15.03 6.74 15.05 6.68 15.08 6.62C15.1 6.56 15.13 6.5 15.17 6.439C15.2 6.39 15.25 6.34 15.29 6.29C15.38 6.2 15.49 6.13 15.62 6.08C15.979 5.93 16.43 6.01 16.71 6.29L16.83 6.439C16.87 6.5 16.899 6.56 16.92 6.62C16.95 6.68 16.97 6.74 16.979 6.8C16.99 6.87 17 6.93 17 7C17 7.27 16.899 7.52 16.71 7.71C16.52 7.899 16.27 8 16 8Z"
fill="#535766"
/>
</g>
</g>
</svg>
);
// eslint-disable-next-line import/no-default-export
export default IconIndexManagement;

View file

@ -1,33 +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 IconMapServices: React.FC<SVGProps<SVGSVGElement>> = ({ ...props }) => (
<svg
width="32"
height="32"
viewBox="0 0 32 32"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path d="M3 22H6V24H1V1H24V6H22V3H3V22Z" fill="#00BFB3" />
<path
fillRule="evenodd"
clipRule="evenodd"
d="M31 31V8H8V31H31ZM10 29V10H29V29H10Z"
fill="#535766"
/>
<path
d="M30.8227 21.1314L29.1773 22.2683C25.3207 16.6863 18.6562 14.1699 16.5129 17.6167C15.6822 18.9527 15.8443 19.9862 16.9432 22.2789C16.9911 22.3787 16.9911 22.3787 17.0395 22.4794C18.7858 26.1116 18.9092 28.0826 16.3826 30.6952L14.9449 29.3048C16.7327 27.4562 16.6607 26.3072 15.237 23.346C15.1885 23.2451 15.1885 23.2451 15.1397 23.1433C13.771 20.2879 13.5144 18.6515 14.8145 16.5606C18.0019 11.4346 26.2837 14.5616 30.8227 21.1314Z"
fill="#535766"
/>
</svg>
);
// eslint-disable-next-line import/no-default-export
export default IconMapServices;

View file

@ -1,37 +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 IconReporting: React.FC<SVGProps<SVGSVGElement>> = ({ ...props }) => (
<svg
width="32"
height="32"
viewBox="0 0 32 32"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path
id="Stroke 1"
d="M20.833 7V5H21C22.6563 5 24 6.34372 24 8V28C24 29.6563 22.6563 31 21 31H3C1.34372 31 0 29.6563 0 28V8C0 6.34372 1.34372 5 3 5H3.167V7H3C2.44828 7 2 7.44828 2 8V28C2 28.5517 2.44828 29 3 29H21C21.5517 29 22 28.5517 22 28V8C22 7.44828 21.5517 7 21 7H20.833Z"
fill="#535766"
/>
<path
id="Stroke 3"
fillRule="evenodd"
clipRule="evenodd"
d="M19 9V3H15.874C15.4299 1.27477 13.8638 0 12 0C10.1362 0 8.57006 1.27477 8.12602 3H5V9H19ZM14 5H17V7H7V5H10V4C10 2.89543 10.8954 2 12 2C13.1046 2 14 2.89543 14 4V5Z"
fill="#535766"
/>
<path id="Stroke 5" d="M6 15V13H18V15H6Z" fill="#00BFB3" />
<path id="Stroke 7" d="M6 20V18H18V20H6Z" fill="#00BFB3" />
<path id="Stroke 9" d="M6 25V23H18V25H6Z" fill="#00BFB3" />
</svg>
);
// eslint-disable-next-line import/no-default-export
export default IconReporting;

View file

@ -1,57 +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 IconUsersRoles: React.FC<SVGProps<SVGSVGElement>> = ({ ...props }) => (
<svg
width="32"
height="32"
viewBox="0 0 32 32"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path
id="Fill 1"
fillRule="evenodd"
clipRule="evenodd"
d="M16 4C15.448 4 15 3.552 15 3C15 2.448 15.448 2 16 2C16.552 2 17 2.448 17 3C17 3.552 16.552 4 16 4ZM18.911 2.308C18.597 0.987 17.415 0 16 0C14.346 0 13 1.346 13 3C13 4.654 14.346 6 16 6C17.176 6 18.186 5.313 18.677 4.326C22.501 5.203 25.711 7.91 27.166 11.597L29.027 10.862C27.302 6.491 23.47 3.274 18.911 2.308Z"
fill="#535766"
/>
<path
id="Fill 3"
fillRule="evenodd"
clipRule="evenodd"
d="M3 17C2.448 17 2 16.552 2 16C2 15.448 2.448 15 3 15C3.552 15 4 15.448 4 16C4 16.552 3.552 17 3 17ZM11.597 4.83402L10.862 2.97302C6.491 4.69802 3.274 8.53002 2.308 13.089C0.987 13.403 0 14.585 0 16C0 17.654 1.346 19 3 19C4.654 19 6 17.654 6 16C6 14.824 5.313 13.814 4.326 13.323C5.203 9.49902 7.91 6.28902 11.597 4.83402Z"
fill="#535766"
/>
<path
id="Fill 5"
fillRule="evenodd"
clipRule="evenodd"
d="M16.0001 30C15.4481 30 15.0001 29.552 15.0001 29C15.0001 28.448 15.4481 28 16.0001 28C16.5521 28 17.0001 28.448 17.0001 29C17.0001 29.552 16.5521 30 16.0001 30ZM16.0001 26C14.8241 26 13.8141 26.687 13.3231 27.674C9.49915 26.797 6.28914 24.09 4.83414 20.403L2.97314 21.138C4.69814 25.509 8.53014 28.726 13.0891 29.692C13.4031 31.013 14.5851 32 16.0001 32C17.6541 32 19.0001 30.654 19.0001 29C19.0001 27.346 17.6541 26 16.0001 26Z"
fill="#535766"
/>
<path
id="Fill 7"
fillRule="evenodd"
clipRule="evenodd"
d="M28.9998 17C28.4478 17 27.9998 16.552 27.9998 16C27.9998 15.448 28.4478 15 28.9998 15C29.5518 15 29.9998 15.448 29.9998 16C29.9998 16.552 29.5518 17 28.9998 17ZM31.9998 16C31.9998 14.346 30.6538 13 28.9998 13C27.3458 13 25.9998 14.346 25.9998 16C25.9998 17.176 26.6868 18.186 27.6738 18.677C26.7978 22.501 24.0898 25.711 20.4028 27.166L21.1378 29.027C25.5088 27.302 28.7258 23.47 29.6918 18.911C31.0128 18.597 31.9998 17.415 31.9998 16Z"
fill="#535766"
/>
<path
id="Fill 9"
fillRule="evenodd"
clipRule="evenodd"
d="M13 20C13 18.346 14.346 17 16 17C17.654 17 19 18.346 19 20H13ZM14 13C14 11.897 14.897 11 16 11C17.103 11 18 11.897 18 13C18 14.103 17.103 15 16 15C14.897 15 14 14.103 14 13ZM18.794 15.855C19.536 15.129 20 14.119 20 13C20 10.794 18.206 9 16 9C13.794 9 12 10.794 12 13C12 14.119 12.464 15.129 13.206 15.855C11.876 16.755 11 18.277 11 20V22H21V20C21 18.277 20.124 16.755 18.794 15.855Z"
fill="#00BFB3"
/>
</svg>
);
// eslint-disable-next-line import/no-default-export
export default IconUsersRoles;

View file

@ -19,18 +19,11 @@ const withSuspenseIcon = <T extends object = {}>(Component: React.ComponentType<
export const IconLensLazy = withSuspenseIcon(React.lazy(() => import('./icons/lens')));
export const IconEndpointLazy = withSuspenseIcon(React.lazy(() => import('./icons/endpoint')));
export const IconIndexManagementLazy = withSuspenseIcon(
React.lazy(() => import('./icons/index_management'))
);
export const IconFleetLazy = withSuspenseIcon(React.lazy(() => import('./icons/fleet')));
export const IconEcctlLazy = withSuspenseIcon(React.lazy(() => import('./icons/ecctl')));
export const IconMapServicesLazy = withSuspenseIcon(
React.lazy(() => import('./icons/map_services'))
);
export const IconTimelineLazy = withSuspenseIcon(React.lazy(() => import('./icons/timeline')));
export const IconOsqueryLazy = withSuspenseIcon(React.lazy(() => import('./icons/osquery')));
export const IconUsersRolesLazy = withSuspenseIcon(React.lazy(() => import('./icons/users_roles')));
export const IconReportingLazy = withSuspenseIcon(React.lazy(() => import('./icons/reporting')));
export const IconVisualizationLazy = withSuspenseIcon(
React.lazy(() => import('./icons/visualization'))
);

View file

@ -21,12 +21,7 @@ export const createServices = (
experimentalFeatures: ExperimentalFeatures
): Services => {
const { securitySolution, cloud } = pluginsStart;
const projectNavLinks$ = createProjectNavLinks$(
securitySolution.getNavLinks$(),
core,
cloud,
experimentalFeatures
);
const projectNavLinks$ = createProjectNavLinks$(securitySolution.getNavLinks$(), core, cloud);
return {
...core,
...pluginsStart,

View file

@ -6,7 +6,6 @@
*/
import React from 'react';
import { SecurityPageName } from '@kbn/security-solution-plugin/common';
import {
EuiFlexGroup,
EuiFlexItem,
@ -43,7 +42,6 @@ const ChangePlanLinkComponent = ({ productTier }: { productTier: ProductTier | u
<ProductTierBadge productTier={productTier} />
<EuiLink
className="eui-alignMiddle"
id={SecurityPageName.projectSettings}
css={css`
color: ${euiTheme.colors.primaryText};
padding-left: ${euiTheme.size.m};

View file

@ -5,36 +5,26 @@
* 2.0.
*/
import { APP_PATH, SecurityPageName } from '@kbn/security-solution-plugin/common';
import { APP_PATH } from '@kbn/security-solution-plugin/common';
import type { CoreSetup } from '@kbn/core/public';
import type {
SecuritySolutionServerlessPluginSetupDeps,
ServerlessSecurityPublicConfig,
} from '../types';
import type { SecuritySolutionServerlessPluginSetupDeps } from '../types';
import type { Services } from '../common/services';
import { subscribeBreadcrumbs } from './breadcrumbs';
import { SecurityPagePath } from './links/constants';
import { ProjectNavigationTree } from './navigation_tree';
import { getSecuritySideNavComponent } from './side_navigation';
import { getDefaultNavigationComponent } from './default_navigation';
import { getProjectAppLinksSwitcher } from './links/app_links';
import { projectAppLinksSwitcher } from './links/app_links';
import { formatProjectDeepLinks } from './links/deep_links';
import type { ExperimentalFeatures } from '../../common/experimental_features';
const SECURITY_PROJECT_SETTINGS_PATH = `${APP_PATH}${
SecurityPagePath[SecurityPageName.projectSettings]
}`;
export const setupNavigation = (
_core: CoreSetup,
{ securitySolution }: SecuritySolutionServerlessPluginSetupDeps,
experimentalFeatures: ExperimentalFeatures
{ securitySolution }: SecuritySolutionServerlessPluginSetupDeps
) => {
securitySolution.setAppLinksSwitcher(getProjectAppLinksSwitcher(experimentalFeatures));
securitySolution.setAppLinksSwitcher(projectAppLinksSwitcher);
securitySolution.setDeepLinksFormatter(formatProjectDeepLinks);
};
export const startNavigation = (services: Services, config: ServerlessSecurityPublicConfig) => {
export const startNavigation = (services: Services) => {
const { serverless, management } = services;
serverless.setProjectHome(APP_PATH);
@ -45,9 +35,8 @@ export const startNavigation = (services: Services, config: ServerlessSecurityPu
serverless.setSideNavComponent(getDefaultNavigationComponent(navigationTree, services));
});
} else {
if (!config.developer.disableManagementUrlRedirect) {
management.setLandingPageRedirect(SECURITY_PROJECT_SETTINGS_PATH);
}
management.setupCardsNavigation({ enabled: true });
projectNavigationTree.getChromeNavigationTree$().subscribe((chromeNavigationTree) => {
serverless.setNavigation({ navigationTree: chromeNavigationTree });
});

View file

@ -14,39 +14,33 @@ import { cloneDeep, remove } from 'lodash';
import { createInvestigationsLinkFromTimeline } from './sections/investigations_links';
import { mlAppLink } from './sections/ml_links';
import { createAssetsLinkFromManage } from './sections/assets_links';
import { createProjectSettingsLinkFromManage } from './sections/project_settings_links';
import type { ExperimentalFeatures } from '../../../common/experimental_features';
import { createProjectSettingsLinksFromManage } from './sections/project_settings_links';
// This function is called by the security_solution plugin to alter the app links
// that will be registered to the Security Solution application on Serverless projects.
// The capabilities filtering is done after this function is called by the security_solution plugin.
export const getProjectAppLinksSwitcher =
(experimentalFeatures: ExperimentalFeatures): AppLinksSwitcher =>
(appLinks) => {
const projectAppLinks = cloneDeep(appLinks) as LinkItem[];
export const projectAppLinksSwitcher: AppLinksSwitcher = (appLinks) => {
const projectAppLinks = cloneDeep(appLinks) as LinkItem[];
// Remove timeline link
const [timelineLinkItem] = remove(projectAppLinks, { id: SecurityPageName.timelines });
if (timelineLinkItem) {
// Add investigations link
projectAppLinks.push(createInvestigationsLinkFromTimeline(timelineLinkItem));
}
// Remove timeline link
const [timelineLinkItem] = remove(projectAppLinks, { id: SecurityPageName.timelines });
if (timelineLinkItem) {
// Add investigations link
projectAppLinks.push(createInvestigationsLinkFromTimeline(timelineLinkItem));
}
// Remove manage link
const [manageLinkItem] = remove(projectAppLinks, { id: SecurityPageName.administration });
// Remove manage link
const [manageLinkItem] = remove(projectAppLinks, { id: SecurityPageName.administration });
if (manageLinkItem) {
// Add assets link
projectAppLinks.push(createAssetsLinkFromManage(manageLinkItem));
}
if (manageLinkItem) {
// Add assets link
projectAppLinks.push(createAssetsLinkFromManage(manageLinkItem));
// Add entity analytics link if exists
projectAppLinks.push(...createProjectSettingsLinksFromManage(manageLinkItem));
}
// Add ML link
projectAppLinks.push(mlAppLink);
// Add ML link
projectAppLinks.push(mlAppLink);
if (!experimentalFeatures.platformNavEnabled && manageLinkItem) {
// Add project settings link
projectAppLinks.push(createProjectSettingsLinkFromManage(manageLinkItem));
}
return projectAppLinks;
};
return projectAppLinks;
};

View file

@ -13,7 +13,6 @@ export const SecurityPagePath = {
[SecurityPageName.mlLanding]: '/ml',
[SecurityPageName.assets]: '/assets',
[SecurityPageName.cloudDefend]: '/cloud_defend',
[SecurityPageName.projectSettings]: '/project_settings',
} as const;
/**
@ -74,6 +73,7 @@ export enum ExternalPageName {
integrationsSecurity = 'integrations:/browse/security',
// Management
// Ref: packages/default-nav/management/default_navigation.ts
management = 'management:',
managementIngestPipelines = 'management:ingest_pipelines',
managementPipelines = 'management:pipelines',
managementIndexManagement = 'management:index_management',
@ -97,4 +97,5 @@ export enum ExternalPageName {
// cloudUrlKey Ref: x-pack/plugins/security_solution_serverless/public/navigation/links/util.ts
cloudUsersAndRoles = 'cloud:usersAndRoles',
cloudBilling = 'cloud:billing',
cloudPerformance = 'cloud:performance',
}

View file

@ -9,19 +9,14 @@ import { APP_UI_ID } from '@kbn/security-solution-plugin/common';
import type { NavigationLink } from '@kbn/security-solution-navigation';
import { SecurityPageName } from '@kbn/security-solution-navigation';
import { createProjectNavLinks$ } from './nav_links';
import type { Observable } from 'rxjs';
import { BehaviorSubject, firstValueFrom, take } from 'rxjs';
import { mockServices } from '../../common/services/__mocks__/services.mock';
import { mlNavCategories, mlNavLinks } from './sections/ml_links';
import { assetsNavLinks } from './sections/assets_links';
import { ExternalPageName } from './constants';
import type { ProjectNavigationLink } from './types';
import { investigationsNavLinks } from './sections/investigations_links';
import {
projectSettingsNavCategories,
projectSettingsNavLinks,
} from './sections/project_settings_links';
import { isCloudLink } from './util';
import type { ExperimentalFeatures } from '../../../common/experimental_features';
const mockCloudStart = mockServices.cloud;
const mockChromeNavLinks = jest.fn((): ChromeNavLink[] => []);
@ -40,7 +35,6 @@ const testServices = {
},
},
};
const experimentalFeatures = { platformNavEnabled: false } as ExperimentalFeatures;
const link1Id = 'link-1' as SecurityPageName;
const link2Id = 'link-2' as SecurityPageName;
@ -52,15 +46,6 @@ const linkMlLanding: NavigationLink<SecurityPageName> = {
title: 'ML Landing',
links: [],
};
const projectLinkDevTools: ProjectNavigationLink = {
id: ExternalPageName.devTools,
title: 'Dev tools',
};
const projectLinkDiscover: ProjectNavigationLink = {
id: ExternalPageName.discover,
title: 'Discover',
};
const chromeNavLink1: ChromeNavLink = {
id: `${APP_UI_ID}:${link1.id}`,
@ -77,6 +62,22 @@ const devToolsChromeNavLink: ChromeNavLink = {
baseUrl: '',
};
const createTestProjectNavLinks = async (
testSecurityNavLinks$: Observable<Array<NavigationLink<SecurityPageName>>>,
{ filterCloudLinks = true }: { filterCloudLinks?: boolean } = {}
) => {
const projectNavLinks$ = createProjectNavLinks$(
testSecurityNavLinks$,
testServices,
mockCloudStart
);
const value = await firstValueFrom(projectNavLinks$.pipe(take(1)));
if (filterCloudLinks) {
return value.filter((link) => !isCloudLink(link.id));
}
return value;
};
describe('getProjectNavLinks', () => {
beforeEach(() => {
jest.clearAllMocks();
@ -86,47 +87,19 @@ describe('getProjectNavLinks', () => {
);
});
it('should return security nav links with all external links filtered', async () => {
it('should return security nav links with all external (non cloud) links filtered', async () => {
mockChromeNavLinksHas.mockReturnValue(false); // no external links exist
const testSecurityNavLinks$ = new BehaviorSubject([link1, link2]);
const projectNavLinks$ = createProjectNavLinks$(
testSecurityNavLinks$,
testServices,
mockCloudStart,
experimentalFeatures
);
const value = await firstValueFrom(projectNavLinks$.pipe(take(1)));
const value = await createTestProjectNavLinks(testSecurityNavLinks$);
expect(value).toEqual([link1, link2]);
});
it('should add devTools nav link if chrome nav link exists', async () => {
mockChromeNavLinks.mockReturnValue([devToolsChromeNavLink]);
const testSecurityNavLinks$ = new BehaviorSubject([link1]);
const projectNavLinks$ = createProjectNavLinks$(
testSecurityNavLinks$,
testServices,
mockCloudStart,
experimentalFeatures
);
const value = await firstValueFrom(projectNavLinks$.pipe(take(1)));
expect(value).toEqual([link1, projectLinkDevTools]);
});
it('should filter all external links not configured in chrome links', async () => {
mockChromeNavLinks.mockReturnValue([chromeNavLink1]);
const testSecurityNavLinks$ = new BehaviorSubject([link1, link2, linkMlLanding]);
const projectNavLinks$ = createProjectNavLinks$(
testSecurityNavLinks$,
testServices,
mockCloudStart,
experimentalFeatures
);
const value = await firstValueFrom(projectNavLinks$.pipe(take(1)));
const value = await createTestProjectNavLinks(testSecurityNavLinks$);
expect(value).toEqual([
link1,
link2,
@ -134,25 +107,26 @@ describe('getProjectNavLinks', () => {
]);
});
it('should add devTools nav link if chrome nav link exists', async () => {
mockChromeNavLinks.mockReturnValue([devToolsChromeNavLink]);
const testSecurityNavLinks$ = new BehaviorSubject([link1]);
const value = await createTestProjectNavLinks(testSecurityNavLinks$);
expect(value).toEqual([link1, expect.objectContaining({ id: ExternalPageName.devTools })]);
});
it('should add machineLearning links', async () => {
mockChromeNavLinksHas.mockReturnValue(true); // all links exist
const testSecurityNavLinks$ = new BehaviorSubject([link1, link2, linkMlLanding]);
const projectNavLinks$ = createProjectNavLinks$(
testSecurityNavLinks$,
testServices,
mockCloudStart,
experimentalFeatures
const value = await createTestProjectNavLinks(testSecurityNavLinks$);
expect(value).toEqual(
expect.arrayContaining([
link1,
link2,
{ ...linkMlLanding, categories: mlNavCategories, links: mlNavLinks },
])
);
const value = await firstValueFrom(projectNavLinks$.pipe(take(1)));
expect(value).toEqual([
link1,
link2,
{ ...linkMlLanding, categories: mlNavCategories, links: mlNavLinks },
projectLinkDiscover,
projectLinkDevTools,
]);
});
it('should add assets links', async () => {
@ -164,20 +138,10 @@ describe('getProjectNavLinks', () => {
};
const testSecurityNavLinks$ = new BehaviorSubject([link1, linkAssets]);
const projectNavLinks$ = createProjectNavLinks$(
testSecurityNavLinks$,
testServices,
mockCloudStart,
experimentalFeatures
const value = await createTestProjectNavLinks(testSecurityNavLinks$);
expect(value).toEqual(
expect.arrayContaining([link1, { ...linkAssets, links: [...assetsNavLinks, link2] }])
);
const value = await firstValueFrom(projectNavLinks$.pipe(take(1)));
expect(value).toEqual([
link1,
{ ...linkAssets, links: [...assetsNavLinks, link2] },
projectLinkDiscover,
projectLinkDevTools,
]);
});
it('should add investigations links', async () => {
@ -189,77 +153,41 @@ describe('getProjectNavLinks', () => {
};
const testSecurityNavLinks$ = new BehaviorSubject([link1, linkInvestigations]);
const projectNavLinks$ = createProjectNavLinks$(
testSecurityNavLinks$,
testServices,
mockCloudStart,
experimentalFeatures
const value = await createTestProjectNavLinks(testSecurityNavLinks$);
expect(value).toEqual(
expect.arrayContaining([
link1,
{ ...linkInvestigations, links: [link2, ...investigationsNavLinks] },
])
);
const value = await firstValueFrom(projectNavLinks$.pipe(take(1)));
expect(value).toEqual([
link1,
{ ...linkInvestigations, links: [link2, ...investigationsNavLinks] },
projectLinkDiscover,
projectLinkDevTools,
]);
});
it('should add project settings links', async () => {
mockChromeNavLinksHas.mockReturnValue(true); // all links exist
const linkProjectSettings: NavigationLink<SecurityPageName> = {
id: SecurityPageName.projectSettings,
title: 'Project settings',
links: [link2],
};
const testSecurityNavLinks$ = new BehaviorSubject([link1, linkProjectSettings]);
const testSecurityNavLinks$ = new BehaviorSubject([link1]);
const projectNavLinks$ = createProjectNavLinks$(
testSecurityNavLinks$,
testServices,
mockCloudStart,
experimentalFeatures
const value = await createTestProjectNavLinks(testSecurityNavLinks$, {
filterCloudLinks: false,
});
expect(value).toEqual(
expect.arrayContaining([
link1,
expect.objectContaining({ id: ExternalPageName.management }),
expect.objectContaining({ id: ExternalPageName.integrationsSecurity }),
expect.objectContaining({ id: ExternalPageName.cloudUsersAndRoles }),
expect.objectContaining({ id: ExternalPageName.cloudBilling }),
])
);
const value = await firstValueFrom(projectNavLinks$.pipe(take(1)));
const expectedProjectSettingsNavLinks = projectSettingsNavLinks.map(
(link) => expect.objectContaining(link) // ignore externalUrl property in cloud links, tested separately
);
expect(value).toEqual([
link1,
{
...linkProjectSettings,
categories: projectSettingsNavCategories,
links: [...expectedProjectSettingsNavLinks, link2],
},
projectLinkDiscover,
projectLinkDevTools,
]);
});
it('should process cloud links', async () => {
mockChromeNavLinksHas.mockReturnValue(true); // all links exist
const linkProjectSettings: NavigationLink<SecurityPageName> = {
id: SecurityPageName.projectSettings,
title: 'Project settings',
links: [link2],
};
const testSecurityNavLinks$ = new BehaviorSubject([link1, linkProjectSettings]);
const testSecurityNavLinks$ = new BehaviorSubject([link1]);
const projectNavLinks$ = createProjectNavLinks$(
testSecurityNavLinks$,
testServices,
mockCloudStart,
experimentalFeatures
);
const value = await firstValueFrom(projectNavLinks$.pipe(take(1)));
const cloudLinks =
value
.find((link) => link.id === SecurityPageName.projectSettings)
?.links?.filter((link) => isCloudLink(link.id)) ?? [];
const value = await createTestProjectNavLinks(testSecurityNavLinks$, {
filterCloudLinks: false,
});
const cloudLinks = value.filter(({ id }) => isCloudLink(id));
expect(cloudLinks.length > 0).toBe(true);
expect(cloudLinks.every((cloudLink) => cloudLink.externalUrl)).toBe(true);

View file

@ -10,26 +10,19 @@ import type { ChromeNavLinks, CoreStart } from '@kbn/core/public';
import { SecurityPageName, type NavigationLink } from '@kbn/security-solution-navigation';
import { isSecurityId } from '@kbn/security-solution-navigation/links';
import type { CloudStart } from '@kbn/cloud-plugin/public';
import { remove } from 'lodash';
import { assetsNavLinks } from './sections/assets_links';
import { mlNavCategories, mlNavLinks } from './sections/ml_links';
import {
projectSettingsNavCategories,
projectSettingsNavLinks,
} from './sections/project_settings_links';
import { projectSettingsNavLinks } from './sections/project_settings_links';
import { devToolsNavLink } from './sections/dev_tools_links';
import { discoverNavLink } from './sections/discover_links';
import type { ProjectNavigationLink } from './types';
import { getCloudLinkKey, getCloudUrl, getNavLinkIdFromProjectPageName, isCloudLink } from './util';
import { investigationsNavLinks } from './sections/investigations_links';
import { ExternalPageName } from './constants';
import type { ExperimentalFeatures } from '../../../common/experimental_features';
export const createProjectNavLinks$ = (
securityNavLinks$: Observable<Array<NavigationLink<SecurityPageName>>>,
core: CoreStart,
cloud: CloudStart,
experimentalFeatures: ExperimentalFeatures
cloud: CloudStart
): Observable<ProjectNavigationLink[]> => {
const { chrome } = core;
return combineLatest([securityNavLinks$, chrome.navLinks.getNavLinks$()]).pipe(
@ -38,9 +31,7 @@ export const createProjectNavLinks$ = (
([securityNavLinks, chromeNavLinks]) =>
securityNavLinks.length === 0 || chromeNavLinks.length === 0 // skip if not initialized
),
map(([securityNavLinks]) =>
processNavLinks(securityNavLinks, chrome.navLinks, cloud, experimentalFeatures)
)
map(([securityNavLinks]) => processNavLinks(securityNavLinks, chrome.navLinks, cloud))
);
};
@ -51,8 +42,7 @@ export const createProjectNavLinks$ = (
const processNavLinks = (
securityNavLinks: Array<NavigationLink<SecurityPageName>>,
chromeNavLinks: ChromeNavLinks,
cloud: CloudStart,
experimentalFeatures: ExperimentalFeatures
cloud: CloudStart
): ProjectNavigationLink[] => {
const projectNavLinks: ProjectNavigationLink[] = [...securityNavLinks];
@ -91,27 +81,9 @@ const processNavLinks = (
};
}
// Project Settings, adding all external sub-links
const projectSettingsLinkIndex = projectNavLinks.findIndex(
({ id }) => id === SecurityPageName.projectSettings
);
if (projectSettingsLinkIndex !== -1) {
const projectSettingsNavLink = projectNavLinks[projectSettingsLinkIndex];
projectNavLinks[projectSettingsLinkIndex] = {
...projectSettingsNavLink,
categories: projectSettingsNavCategories,
links: [...projectSettingsNavLinks, ...(projectSettingsNavLink.links ?? [])],
};
}
// Dev Tools. just pushing it
projectNavLinks.push(devToolsNavLink);
if (experimentalFeatures.platformNavEnabled) {
remove(projectNavLinks, { id: SecurityPageName.landing });
remove(projectNavLinks, { id: ExternalPageName.devTools });
remove(projectNavLinks, { id: SecurityPageName.projectSettings });
}
projectNavLinks.push(...projectSettingsNavLinks);
return processCloudLinks(filterDisabled(projectNavLinks, chromeNavLinks), cloud);
};

View file

@ -12,4 +12,5 @@ import { DEV_TOOLS_TITLE } from './dev_tools_translations';
export const devToolsNavLink: ProjectNavigationLink = {
id: ExternalPageName.devTools,
title: DEV_TOOLS_TITLE,
sideNavIcon: 'editorCodeBlock',
};

View file

@ -10,6 +10,6 @@ import { i18n } from '@kbn/i18n';
export const DEV_TOOLS_TITLE = i18n.translate(
'xpack.securitySolutionServerless.navLinks.devTools.title',
{
defaultMessage: 'Dev tools',
defaultMessage: 'Developer tools',
}
);

View file

@ -5,194 +5,48 @@
* 2.0.
*/
import { LinkCategoryType, SecurityPageName } from '@kbn/security-solution-navigation';
import { SERVER_APP_ID } from '@kbn/security-solution-plugin/common';
import type { LinkItem } from '@kbn/security-solution-plugin/public';
import { ExternalPageName, SecurityPagePath } from '../constants';
import type { ProjectLinkCategory, ProjectNavigationLink } from '../types';
import {
IconMapServicesLazy,
IconIndexManagementLazy,
IconUsersRolesLazy,
IconReportingLazy,
IconVisualizationLazy,
} from '../../../common/lazy_icons';
import { SecurityPageName } from '@kbn/security-solution-navigation';
import { ExternalPageName } from '../constants';
import type { ProjectNavigationLink } from '../types';
import * as i18n from './project_settings_translations';
// appLinks configures the Security Solution pages links
const projectSettingsAppLink: LinkItem = {
id: SecurityPageName.projectSettings,
title: i18n.PROJECT_SETTINGS_TITLE,
path: SecurityPagePath[SecurityPageName.projectSettings],
capabilities: [`${SERVER_APP_ID}.show`],
hideTimeline: true,
skipUrlState: true,
links: [], // endpoints and cloudDefend links are added in createAssetsLinkFromManage
};
export const createProjectSettingsLinkFromManage = (manageLink: LinkItem): LinkItem => {
const projectSettingsSubLinks = [];
export const createProjectSettingsLinksFromManage = (manageLink: LinkItem): LinkItem[] => {
const entityAnalyticsLink = manageLink.links?.find(
({ id }) => id === SecurityPageName.entityAnalyticsManagement
);
if (entityAnalyticsLink) {
projectSettingsSubLinks.push(entityAnalyticsLink);
}
return {
...projectSettingsAppLink,
links: projectSettingsSubLinks,
};
return entityAnalyticsLink ? [{ ...entityAnalyticsLink, sideNavDisabled: true }] : [];
};
export const projectSettingsNavCategories: ProjectLinkCategory[] = [
{
type: LinkCategoryType.separator,
linkIds: [
ExternalPageName.cloudUsersAndRoles,
ExternalPageName.cloudBilling,
SecurityPageName.entityAnalyticsManagement,
],
},
{
type: LinkCategoryType.separator,
linkIds: [
ExternalPageName.integrationsSecurity,
ExternalPageName.maps,
ExternalPageName.visualize,
],
},
{
type: LinkCategoryType.accordion,
label: i18n.MANAGEMENT_CATEGORY_TITLE,
categories: [
{
label: i18n.DATA_CATEGORY_TITLE,
linkIds: [
ExternalPageName.managementIndexManagement,
ExternalPageName.managementTransforms,
ExternalPageName.managementIngestPipelines,
ExternalPageName.managementDataViews,
ExternalPageName.managementJobsListLink,
ExternalPageName.managementPipelines,
],
},
{
label: i18n.ALERTS_INSIGHTS_CATEGORY_TITLE,
linkIds: [
ExternalPageName.managementCases,
ExternalPageName.managementTriggersActionsConnectors,
ExternalPageName.managementMaintenanceWindows,
],
},
{
label: i18n.CONTENT_CATEGORY_TITLE,
linkIds: [
ExternalPageName.managementObjects,
ExternalPageName.managementFiles,
ExternalPageName.managementReporting,
ExternalPageName.managementTags,
],
},
{
label: i18n.OTHER_CATEGORY_TITLE,
linkIds: [ExternalPageName.managementApiKeys, ExternalPageName.managementSettings],
},
],
},
];
// navLinks define the navigation links for the Security Solution pages and External pages as well
export const projectSettingsNavLinks: ProjectNavigationLink[] = [
{
id: ExternalPageName.cloudUsersAndRoles,
title: i18n.CLOUD_USERS_ROLES_TITLE,
description: i18n.CLOUD_USERS_ROLES_DESCRIPTION,
landingIcon: IconUsersRolesLazy,
},
{
id: ExternalPageName.cloudBilling,
title: i18n.CLOUD_BILLING_TITLE,
description: i18n.CLOUD_BILLING_DESCRIPTION,
landingIcon: IconReportingLazy,
id: ExternalPageName.management,
title: i18n.MANAGEMENT_TITLE,
},
{
id: ExternalPageName.integrationsSecurity,
title: i18n.INTEGRATIONS_TITLE,
description: i18n.INTEGRATIONS_DESCRIPTION,
landingIcon: IconIndexManagementLazy,
},
{
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,
},
{
id: ExternalPageName.maps,
title: i18n.MAPS_TITLE,
description: i18n.MAPS_DESCRIPTION,
landingIcon: IconMapServicesLazy,
title: i18n.CLOUD_MAPS_TITLE,
disabled: true, // the link will be available in the navigationTree (breadcrumbs) but not appear in the sideNav
},
{
id: ExternalPageName.visualize,
title: i18n.VISUALIZE_TITLE,
description: i18n.VISUALIZE_DESCRIPTION,
landingIcon: IconVisualizationLazy,
},
{
id: ExternalPageName.managementIndexManagement,
title: i18n.MANAGEMENT_INDEX_MANAGEMENT_TITLE,
},
{
id: ExternalPageName.managementTransforms,
title: i18n.MANAGEMENT_TRANSFORMS_TITLE,
},
{
id: ExternalPageName.managementMaintenanceWindows,
title: i18n.MANAGEMENT_MAINTENANCE_WINDOWS_TITLE,
},
{
id: ExternalPageName.managementIngestPipelines,
title: i18n.MANAGEMENT_INGEST_PIPELINES_TITLE,
},
{
id: ExternalPageName.managementDataViews,
title: i18n.MANAGEMENT_DATA_VIEWS_TITLE,
},
{
id: ExternalPageName.managementJobsListLink,
title: i18n.MANAGEMENT_ML_TITLE,
},
{
id: ExternalPageName.managementPipelines,
title: i18n.MANAGEMENT_LOGSTASH_PIPELINES_TITLE,
},
{
id: ExternalPageName.managementCases,
title: i18n.MANAGEMENT_CASES_TITLE,
},
{
id: ExternalPageName.managementTriggersActionsConnectors,
title: i18n.MANAGEMENT_CONNECTORS_TITLE,
},
{
id: ExternalPageName.managementReporting,
title: i18n.MANAGEMENT_REPORTING_TITLE,
},
{
id: ExternalPageName.managementObjects,
title: i18n.MANAGEMENT_SAVED_OBJECTS_TITLE,
},
{
id: ExternalPageName.managementApiKeys,
title: i18n.MANAGEMENT_API_KEYS_TITLE,
},
{
id: ExternalPageName.managementTags,
title: i18n.MANAGEMENT_TAGS_TITLE,
},
{
id: ExternalPageName.managementFiles,
title: i18n.MANAGEMENT_FILES_TITLE,
},
{
id: ExternalPageName.managementSettings,
title: i18n.MANAGEMENT_SETTINGS_TITLE,
title: i18n.CLOUD_VISUALIZE_TITLE,
disabled: true, // the link will be available in the navigationTree (breadcrumbs) but not appear in the sideNav
},
];

View file

@ -7,195 +7,46 @@
import { i18n } from '@kbn/i18n';
export const PROJECT_SETTINGS_TITLE = i18n.translate(
'xpack.securitySolutionServerless.navLinks.projectSettings.title',
export const MANAGEMENT_TITLE = i18n.translate(
'xpack.securitySolutionServerless.navLinks.management.title',
{
defaultMessage: 'Project settings',
defaultMessage: 'Management',
}
);
export const INTEGRATIONS_TITLE = i18n.translate(
'xpack.securitySolutionServerless.navLinks.projectSettings.integrations.title',
{
defaultMessage: 'Integrations',
}
);
export const INTEGRATIONS_DESCRIPTION = i18n.translate(
'xpack.securitySolutionServerless.navLinks.projectSettings.integrations.description',
{
defaultMessage: 'Security integrations',
}
);
export const CLOUD_USERS_ROLES_TITLE = i18n.translate(
'xpack.securitySolutionServerless.navLinks.projectSettings.usersAndRoles.title',
{
defaultMessage: 'Users & roles',
defaultMessage: 'Users and roles',
}
);
export const CLOUD_USERS_ROLES_DESCRIPTION = i18n.translate(
'xpack.securitySolutionServerless.navLinks.projectSettings.usersAndRoles.description',
export const CLOUD_PERFORMANCE_TITLE = i18n.translate(
'xpack.securitySolutionServerless.navLinks.projectSettings.performance.title',
{
defaultMessage: 'Users and roles management',
defaultMessage: 'Performance',
}
);
export const CLOUD_BILLING_TITLE = i18n.translate(
'xpack.securitySolutionServerless.navLinks.projectSettings.billing.title',
{
defaultMessage: 'Billing & consumptions',
}
);
export const CLOUD_BILLING_DESCRIPTION = i18n.translate(
'xpack.securitySolutionServerless.navLinks.projectSettings.billing.description',
{
defaultMessage: 'Billing & consumption page',
defaultMessage: 'Billing and subscription',
}
);
export const MANAGEMENT_INDEX_MANAGEMENT_TITLE = i18n.translate(
'xpack.securitySolutionServerless.navLinks.projectSettings.management.indexManagement.title',
{
defaultMessage: 'Index management',
}
);
export const MANAGEMENT_TRANSFORMS_TITLE = i18n.translate(
'xpack.securitySolutionServerless.navLinks.projectSettings.management.transforms.title',
{
defaultMessage: 'Transforms',
}
);
export const MANAGEMENT_INGEST_PIPELINES_TITLE = i18n.translate(
'xpack.securitySolutionServerless.navLinks.projectSettings.management.ingestPipelines.title',
{
defaultMessage: 'Ingest pipelines',
}
);
export const MANAGEMENT_DATA_VIEWS_TITLE = i18n.translate(
'xpack.securitySolutionServerless.navLinks.projectSettings.management.dataViews.title',
{
defaultMessage: 'Data views',
}
);
export const MANAGEMENT_ML_TITLE = i18n.translate(
'xpack.securitySolutionServerless.navLinks.projectSettings.management.ml.title',
{
defaultMessage: 'Machine learning',
}
);
export const MANAGEMENT_LOGSTASH_PIPELINES_TITLE = i18n.translate(
'xpack.securitySolutionServerless.navLinks.projectSettings.management.logstashPipelines.title',
{
defaultMessage: 'Logstash pipelines',
}
);
export const MANAGEMENT_CASES_TITLE = i18n.translate(
'xpack.securitySolutionServerless.navLinks.projectSettings.management.cases.title',
{
defaultMessage: 'Cases',
}
);
export const MANAGEMENT_CONNECTORS_TITLE = i18n.translate(
'xpack.securitySolutionServerless.navLinks.projectSettings.management.connectors.title',
{
defaultMessage: 'Connectors',
}
);
export const MANAGEMENT_SAVED_OBJECTS_TITLE = i18n.translate(
'xpack.securitySolutionServerless.navLinks.projectSettings.management.savedObjects.title',
{
defaultMessage: 'Saved objects',
}
);
export const MANAGEMENT_TAGS_TITLE = i18n.translate(
'xpack.securitySolutionServerless.navLinks.projectSettings.management.tags.title',
{
defaultMessage: 'Tags',
}
);
export const MANAGEMENT_SETTINGS_TITLE = i18n.translate(
'xpack.securitySolutionServerless.navLinks.projectSettings.management.settings.title',
{
defaultMessage: 'Advanced settings',
}
);
export const MANAGEMENT_MAINTENANCE_WINDOWS_TITLE = i18n.translate(
'xpack.securitySolutionServerless.navLinks.projectSettings.management.maintenanceWindows.title',
{
defaultMessage: 'Maintenance windows',
}
);
export const MANAGEMENT_REPORTING_TITLE = i18n.translate(
'xpack.securitySolutionServerless.navLinks.projectSettings.management.reporting.title',
{
defaultMessage: 'Reporting',
}
);
export const MANAGEMENT_API_KEYS_TITLE = i18n.translate(
'xpack.securitySolutionServerless.navLinks.projectSettings.management.apiKeys.title',
{
defaultMessage: 'Api keys',
}
);
export const MANAGEMENT_FILES_TITLE = i18n.translate(
'xpack.securitySolutionServerless.navLinks.projectSettings.management.files.title',
{
defaultMessage: 'Files',
}
);
export const MANAGEMENT_CATEGORY_TITLE = i18n.translate(
'xpack.securitySolutionServerless.navLinks.projectSettings.category.management',
{
defaultMessage: 'MANAGEMENT',
}
);
export const DATA_CATEGORY_TITLE = i18n.translate(
'xpack.securitySolutionServerless.navLinks.projectSettings.subCategory.data',
{
defaultMessage: 'DATA',
}
);
export const ALERTS_INSIGHTS_CATEGORY_TITLE = i18n.translate(
'xpack.securitySolutionServerless.navLinks.projectSettings.subCategory.alertsAndInsights',
{
defaultMessage: 'ALERTS AND INSIGHTS',
}
);
export const CONTENT_CATEGORY_TITLE = i18n.translate(
'xpack.securitySolutionServerless.navLinks.projectSettings.subCategory.content',
{
defaultMessage: 'CONTENT',
}
);
export const OTHER_CATEGORY_TITLE = i18n.translate(
'xpack.securitySolutionServerless.navLinks.projectSettings.subCategory.other',
{
defaultMessage: 'OTHER',
}
);
export const MAPS_TITLE = i18n.translate(
export const CLOUD_MAPS_TITLE = i18n.translate(
'xpack.securitySolutionServerless.navLinks.projectSettings.maps.title',
{
defaultMessage: 'Maps',
}
);
export const MAPS_DESCRIPTION = i18n.translate(
'xpack.securitySolutionServerless.navLinks.projectSettings.maps.description',
{
defaultMessage: 'Plot geographic data',
}
);
export const VISUALIZE_TITLE = i18n.translate(
export const CLOUD_VISUALIZE_TITLE = i18n.translate(
'xpack.securitySolutionServerless.navLinks.projectSettings.visualize.title',
{
defaultMessage: 'Visualize library',
}
);
export const VISUALIZE_DESCRIPTION = i18n.translate(
'xpack.securitySolutionServerless.navLinks.projectSettings.visualize.description',
{
defaultMessage: 'Visualize library page',
}
);

View file

@ -6,6 +6,7 @@
*/
import { SecurityPageName } from '@kbn/security-solution-navigation';
import { ExternalPageName } from '../links/constants';
import type { ProjectPageName } from '../links/types';
// We need to hide breadcrumbs for some pages (tabs) because they appear duplicated.
@ -31,4 +32,5 @@ const HIDDEN_BREADCRUMBS = new Set<ProjectPageName>([
export const isBreadcrumbHidden = (id: ProjectPageName): boolean =>
HIDDEN_BREADCRUMBS.has(id) ||
id.startsWith('management:'); /* management sub-pages set their breadcrumbs themselves */
/* management sub-pages set their breadcrumbs themselves, the main Management breadcrumb is configured with our navigationTree definition */
(id.startsWith(ExternalPageName.management) && id !== ExternalPageName.management);

View file

@ -26,17 +26,27 @@ jest.mock('@kbn/security-solution-side-nav', () => ({
SolutionSideNav: (props: unknown) => mockSolutionSideNav(props),
}));
const mockSideNavigationFooter = jest.fn((_props: unknown) => (
<div data-test-subj="solutionSideNavFooter" />
));
jest.mock('./side_navigation_footer', () => ({
...jest.requireActual('./side_navigation_footer'),
SideNavigationFooter: (props: unknown) => mockSideNavigationFooter(props),
}));
const sideNavItems = [
{
id: SecurityPageName.dashboards,
label: 'Dashboards',
href: '/dashboards',
position: 'top',
onClick: jest.fn(),
},
{
id: SecurityPageName.alerts,
label: 'Alerts',
href: '/alerts',
position: 'top',
onClick: jest.fn(),
},
{
@ -76,17 +86,37 @@ describe('SecuritySideNavigation', () => {
expect(component.queryByTestId('solutionSideNav')).toBeInTheDocument();
});
it('should pass item props to the SolutionSideNav component', () => {
it('should render the SideNav footer when items received', () => {
const component = render(<SecuritySideNavigation activeNodes={[]} />, {
wrapper: I18nProvider,
});
expect(component.queryByTestId('solutionSideNavFooter')).toBeInTheDocument();
});
it('should pass only top items to the SolutionSideNav component', () => {
render(<SecuritySideNavigation activeNodes={[]} />, { wrapper: I18nProvider });
expect(mockSolutionSideNav).toHaveBeenCalledWith(
expect.objectContaining({
items: sideNavItems,
items: [
expect.objectContaining({ id: SecurityPageName.dashboards }),
expect.objectContaining({ id: SecurityPageName.alerts }),
],
})
);
});
it('should have empty selectedId the SolutionSideNav component', () => {
it('should pass only bottom items to the SideNavigationFooter component', () => {
render(<SecuritySideNavigation activeNodes={[]} />, { wrapper: I18nProvider });
expect(mockSideNavigationFooter).toHaveBeenCalledWith(
expect.objectContaining({
items: [expect.objectContaining({ id: SecurityPageName.administration })],
})
);
});
it('should set empty selectedId', () => {
render(<SecuritySideNavigation activeNodes={[]} />, { wrapper: I18nProvider });
expect(mockSolutionSideNav).toHaveBeenCalledWith(
@ -94,9 +124,14 @@ describe('SecuritySideNavigation', () => {
selectedId: '',
})
);
expect(mockSideNavigationFooter).toHaveBeenCalledWith(
expect.objectContaining({
activeNodeId: '',
})
);
});
it('should have root external selectedId the SolutionSideNav component', () => {
it('should set root external selectedId', () => {
const activeNodes = [[{ id: 'dev_tools' }]] as ChromeProjectNavigationNode[][];
render(<SecuritySideNavigation activeNodes={activeNodes} />, { wrapper: I18nProvider });
@ -105,9 +140,14 @@ describe('SecuritySideNavigation', () => {
selectedId: ExternalPageName.devTools,
})
);
expect(mockSideNavigationFooter).toHaveBeenCalledWith(
expect.objectContaining({
activeNodeId: 'dev_tools',
})
);
});
it('should have external page selectedId the SolutionSideNav component', () => {
it('should set external page selectedId', () => {
const activeNodes = [[{ id: `ml:overview` }]] as ChromeProjectNavigationNode[][];
render(<SecuritySideNavigation activeNodes={activeNodes} />, { wrapper: I18nProvider });
@ -116,9 +156,14 @@ describe('SecuritySideNavigation', () => {
selectedId: ExternalPageName.mlOverview,
})
);
expect(mockSideNavigationFooter).toHaveBeenCalledWith(
expect.objectContaining({
activeNodeId: 'ml:overview',
})
);
});
it('should internal selectedId the SolutionSideNav component', () => {
it('should set internal selectedId', () => {
const activeNodes = [
[{ id: `${APP_UI_ID}:${SecurityPageName.alerts}` }],
] as ChromeProjectNavigationNode[][];
@ -129,5 +174,10 @@ describe('SecuritySideNavigation', () => {
selectedId: SecurityPageName.alerts,
})
);
expect(mockSideNavigationFooter).toHaveBeenCalledWith(
expect.objectContaining({
activeNodeId: 'securitySolutionUI:alerts',
})
);
});
});

View file

@ -6,25 +6,51 @@
*/
import React, { useMemo } from 'react';
import { EuiLoadingSpinner, useEuiTheme } from '@elastic/eui';
import type { EuiCollapsibleNavItemProps } from '@elastic/eui';
import {
EuiCollapsibleNavBeta,
EuiCollapsibleNavItem,
EuiLoadingSpinner,
useEuiTheme,
} from '@elastic/eui';
import type { SideNavComponent } from '@kbn/core-chrome-browser';
import { SolutionNav } from '@kbn/shared-ux-page-solution-nav';
import { SolutionSideNav } from '@kbn/security-solution-side-nav';
import type { SolutionSideNavItem } from '@kbn/security-solution-side-nav';
import { SolutionSideNav, SolutionSideNavItemPosition } from '@kbn/security-solution-side-nav';
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 { getProjectPageNameFromNavLinkId } from '../links/util';
import { useKibana } from '../../common/services';
import { SideNavigationFooter } from './side_navigation_footer';
const getEuiNavItemFromSideNavItem = (sideNavItem: SolutionSideNavItem, selectedId: string) => ({
id: sideNavItem.id,
title: sideNavItem.label,
isSelected: sideNavItem.id === selectedId,
href: sideNavItem.href,
onClick: sideNavItem.onClick,
});
export const SecuritySideNavigation: SideNavComponent = React.memo(function SecuritySideNavigation({
activeNodes: [activeChromeNodes],
}) {
const { hasHeaderBanner$ } = useKibana().services.chrome;
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 items = useSideNavItems();
const hasHeaderBanner = useObservable(hasHeaderBanner$());
const isLoading = items.length === 0;
// we only care about the first node to highlight a left nav main item
const activeNodeId = activeChromeNodes?.[0].id ?? '';
const panelTopOffset = useMemo(
() =>
@ -34,30 +60,56 @@ export const SecuritySideNavigation: SideNavComponent = React.memo(function Secu
[hasHeaderBanner, euiTheme]
);
const selectedId = useMemo(() => {
const mainNode = activeChromeNodes?.[0]; // we only care about the first node to highlight a left nav main item
return mainNode ? getProjectPageNameFromNavLinkId(mainNode.id) : '';
}, [activeChromeNodes]);
const selectedId = useMemo(
() => (activeNodeId ? getProjectPageNameFromNavLinkId(activeNodeId) : ''),
[activeNodeId]
);
const bodyStyle = css`
padding-left: calc(${euiTheme.size.xl} + ${euiTheme.size.s});
padding-right: ${euiTheme.size.s};
`;
const collapsedNavItems = 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)));
return links;
}, []);
}, [items, selectedId]);
const [bodyItems, footerItems] = useMemo(
() => partition((item) => item.position === SolutionSideNavItemPosition.top, items),
[items]
);
return isLoading ? (
<EuiLoadingSpinner size="m" data-test-subj="sideNavLoader" />
) : (
<SolutionNav
canBeCollapsed={false}
name={'Security'}
icon={'logoSecurity'}
closeFlyoutButtonPosition={'inside'}
headingProps={{
'data-test-subj': 'securitySolutionNavHeading',
}}
>
<SolutionSideNav
items={items}
categories={CATEGORIES}
selectedId={selectedId}
panelTopOffset={panelTopOffset}
/>
</SolutionNav>
<>
<EuiCollapsibleNavBeta.Body>
<EuiCollapsibleNavItem
title="Security"
icon="logoSecurity"
iconProps={{ size: 'm' }}
data-test-subj="securitySolutionNavHeading"
items={isCollapsed ? collapsedNavItems : undefined}
/>
{!isCollapsed && (
<div css={bodyStyle}>
<SolutionSideNav
items={bodyItems}
categories={CATEGORIES}
selectedId={selectedId}
panelTopOffset={panelTopOffset}
/>
</div>
)}
</EuiCollapsibleNavBeta.Body>
<EuiCollapsibleNavBeta.Footer>
<SideNavigationFooter activeNodeId={activeNodeId} items={footerItems} />
</EuiCollapsibleNavBeta.Footer>
</>
);
});

View file

@ -0,0 +1,125 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React from 'react';
import { render } from '@testing-library/react';
import { SecurityPageName } from '@kbn/security-solution-navigation';
import { SideNavigationFooter } from './side_navigation_footer';
import { ExternalPageName } from '../links/constants';
import { I18nProvider } from '@kbn/i18n-react';
import type { ProjectSideNavItem } from './types';
jest.mock('../../common/services');
const items: ProjectSideNavItem[] = [
{
id: SecurityPageName.landing,
label: 'Get Started',
href: '/landing',
},
{
id: ExternalPageName.devTools,
label: 'Developer tools',
href: '/dev_tools',
},
{
id: ExternalPageName.management,
label: 'Management',
href: '/management',
},
{
id: ExternalPageName.integrationsSecurity,
label: 'Integrations',
href: '/integrations',
},
{
id: ExternalPageName.cloudUsersAndRoles,
label: 'Users and roles',
href: '/cloud/users_and_roles',
},
{
id: ExternalPageName.cloudBilling,
label: 'Billing and subscription',
href: '/cloud/billing',
},
];
describe('SideNavigationFooter', () => {
beforeEach(() => {
jest.clearAllMocks();
});
it('should render all the items', () => {
const component = render(<SideNavigationFooter items={items} activeNodeId={''} />, {
wrapper: I18nProvider,
});
items.forEach((item) => {
expect(component.queryByTestId(`solutionSideNavItemLink-${item.id}`)).toBeInTheDocument();
});
});
it('should highlight the active node', () => {
const component = render(<SideNavigationFooter items={items} activeNodeId={'dev_tools'} />, {
wrapper: I18nProvider,
});
items.forEach((item) => {
const isSelected = component
.queryByTestId(`solutionSideNavItemLink-${item.id}`)
?.className.includes('isSelected');
if (item.id === ExternalPageName.devTools) {
expect(isSelected).toBe(true);
} else {
expect(isSelected).toBe(false);
}
});
});
it('should highlight the active node inside the collapsible', () => {
const component = render(<SideNavigationFooter items={items} activeNodeId={'management'} />, {
wrapper: I18nProvider,
});
items.forEach((item) => {
const isSelected = component
.queryByTestId(`solutionSideNavItemLink-${item.id}`)
?.className.includes('isSelected');
if (item.id === ExternalPageName.management) {
expect(isSelected).toBe(true);
} else {
expect(isSelected).toBe(false);
}
});
});
it('should render closed collapsible if it has no active node', () => {
const component = render(<SideNavigationFooter items={items} activeNodeId={''} />, {
wrapper: I18nProvider,
});
const isOpen = component
.queryByTestId('navFooterCollapsible-project-settings')
?.className.includes('euiAccordion-isOpen');
expect(isOpen).toBe(false);
});
it('should open collapsible if it has an active node', () => {
const component = render(<SideNavigationFooter items={items} activeNodeId={'management'} />, {
wrapper: I18nProvider,
});
const isOpen = component
.queryByTestId('navFooterCollapsible-project-settings')
?.className.includes('euiAccordion-isOpen');
expect(isOpen).toBe(true);
});
});

View file

@ -0,0 +1,153 @@
/*
* 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, { 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 { 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 }) => {
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;
}, []);
if (category.type === 'standalone') {
return (
<SideNavigationFooterStandalone
key={index}
items={categoryItems}
activeNodeId={activeNodeId}
/>
);
}
if (category.type === 'collapsible') {
return (
<SideNavigationFooterCollapsible
key={index}
title={category.title ?? ''}
items={categoryItems}
activeNodeId={activeNodeId}
icon={category.icon}
/>
);
}
return null;
})}
</>
);
};
const SideNavigationFooterStandalone: React.FC<{
items: ProjectSideNavItem[];
activeNodeId: string;
}> = ({ items, activeNodeId }) => (
<>
{items.map((item) => (
<EuiCollapsibleNavItem
key={item.id}
id={item.id}
title={item.label}
icon={item.iconType}
iconProps={{ size: 'm' }}
data-test-subj={`solutionSideNavItemLink-${item.id}`}
href={item.href}
onClick={item.onClick}
isSelected={getNavLinkIdFromProjectPageName(item.id) === activeNodeId}
linkProps={{ external: item.openInNewTab }}
/>
))}
</>
);
const SideNavigationFooterCollapsible: React.FC<{
title: string;
items: ProjectSideNavItem[];
activeNodeId: string;
icon?: IconType;
}> = ({ title, icon, items, activeNodeId }) => {
const hasSelected = useMemo(
() => items.some(({ id }) => getNavLinkIdFromProjectPageName(id) === activeNodeId),
[activeNodeId, items]
);
const [isOpen, setIsOpen] = useState(hasSelected);
const categoryId = useMemo(() => (title ?? '').toLowerCase().replace(' ', '-'), [title]);
useEffect(() => {
setIsOpen((open) => (!open ? hasSelected : true));
}, [hasSelected]);
return (
<EuiCollapsibleNavItem
key={categoryId}
data-test-subj={`navFooterCollapsible-${categoryId}`}
title={title}
icon={icon}
iconProps={{ size: 'm' }}
accordionProps={{
forceState: isOpen ? 'open' : 'closed',
initialIsOpen: isOpen,
onToggle: (open) => {
setIsOpen(open);
},
}}
items={items.map((item) => formatCollapsibleItem(item, activeNodeId))}
/>
);
};
const formatCollapsibleItem = (
sideNavItem: ProjectSideNavItem,
activeNodeId: string
): EuiCollapsibleNavSubItemProps => {
return {
'data-test-subj': `solutionSideNavItemLink-${sideNavItem.id}`,
id: sideNavItem.id,
title: sideNavItem.label,
isSelected: getNavLinkIdFromProjectPageName(sideNavItem.id) === activeNodeId,
href: sideNavItem.href,
...(sideNavItem.openInNewTab && { target: '_blank' }),
onClick: sideNavItem.onClick,
icon: sideNavItem.iconType,
iconProps: { size: 's' },
};
};

View file

@ -0,0 +1,11 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import type { SolutionSideNavItem } from '@kbn/security-solution-side-nav';
import type { ProjectPageName } from '../links/types';
export type ProjectSideNavItem = SolutionSideNavItem<ProjectPageName>;

View file

@ -104,7 +104,6 @@ describe('useSideNavItems', () => {
position: 'bottom',
onClick: expect.any(Function),
iconType: 'launch',
appendSeparator: true,
},
]);
});
@ -129,7 +128,7 @@ describe('useSideNavItems', () => {
label: 'Users & Roles',
openInNewTab: true,
iconType: 'someicon',
position: 'top',
position: 'bottom',
},
]);
});

View file

@ -8,28 +8,33 @@
import { useCallback, useMemo } from 'react';
import { SecurityPageName, type NavigationLink } from '@kbn/security-solution-navigation';
import { useGetLinkProps } from '@kbn/security-solution-navigation/links';
import {
SolutionSideNavItemPosition,
type SolutionSideNavItem,
} from '@kbn/security-solution-side-nav';
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';
type GetLinkProps = (link: NavigationLink) => {
href: string & Partial<SolutionSideNavItem>;
href: string & Partial<ProjectSideNavItem>;
};
const isBottomNavItem = (id: string) =>
id === SecurityPageName.landing ||
id === SecurityPageName.projectSettings ||
id === ExternalPageName.devTools;
const isGetStartedNavItem = (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`
*/
const formatLink = (navLink: NavigationLink, getLinkProps: GetLinkProps): SolutionSideNavItem => {
const items = navLink.links?.reduce<SolutionSideNavItem[]>((acc, current) => {
const formatLink = (
navLink: NavigationLink<ProjectPageName>,
getLinkProps: GetLinkProps
): ProjectSideNavItem => {
const items = navLink.links?.reduce<ProjectSideNavItem[]>((acc, current) => {
if (!current.disabled) {
acc.push({
id: current.id,
@ -56,25 +61,10 @@ const formatLink = (navLink: NavigationLink, getLinkProps: GetLinkProps): Soluti
};
};
/**
* 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(navLink),
appendSeparator: true,
});
/**
* Returns all the formatted SideNavItems, including external links
*/
export const useSideNavItems = (): SolutionSideNavItem[] => {
export const useSideNavItems = (): ProjectSideNavItem[] => {
const navLinks = useNavLinks();
const getKibanaLinkProps = useGetLinkProps();
@ -94,13 +84,8 @@ export const useSideNavItems = (): SolutionSideNavItem[] => {
return useMemo(
() =>
navLinks.reduce<SolutionSideNavItem[]>((items, navLink) => {
if (navLink.disabled) {
return items;
}
if (isGetStartedNavItem(navLink.id)) {
items.push(formatGetStartedLink(navLink, getLinkProps));
} else {
navLinks.reduce<ProjectSideNavItem[]>((items, navLink) => {
if (!navLink.disabled) {
items.push(formatLink(navLink, getLinkProps));
}
return items;

View file

@ -1,61 +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 from 'react';
import { EuiHorizontalRule, EuiPageHeader, EuiSpacer } from '@elastic/eui';
import {
LandingLinksIconsCategories,
LandingLinksIconsCategoriesGroups,
} from '@kbn/security-solution-navigation/landing_links';
import type { AccordionLinkCategory, NavigationLink } from '@kbn/security-solution-navigation';
import {
isAccordionLinkCategory,
isSeparatorLinkCategory,
SecurityPageName,
} from '@kbn/security-solution-navigation';
import { KibanaPageTemplate } from '@kbn/shared-ux-page-kibana-template';
import { TrackApplicationView } from '@kbn/usage-collection-plugin/public';
import { useNavLink } from '../common/hooks/use_nav_links';
export const ProjectSettingsRoute: React.FC = () => {
const projectSettingsLink = useNavLink(SecurityPageName.projectSettings);
const { links = [], categories = [], title } = projectSettingsLink ?? {};
const iconLinks = categories.reduce<NavigationLink[]>((acc, category) => {
if (isSeparatorLinkCategory(category)) {
const categoryLinks = links.filter(({ id }) => category.linkIds.includes(id));
acc.push(...categoryLinks);
}
return acc;
}, []);
const accordionCategories = (categories.filter((category) => isAccordionLinkCategory(category)) ??
[]) as AccordionLinkCategory[];
const separatorCategories = (categories.filter((category) => isSeparatorLinkCategory(category)) ??
[]) as AccordionLinkCategory[];
return (
<KibanaPageTemplate restrictWidth={false} contentBorder={false} grow={true}>
<KibanaPageTemplate.Section>
<TrackApplicationView viewId={SecurityPageName.projectSettings}>
<EuiPageHeader pageTitle={title} />
<EuiSpacer size="l" />
<EuiSpacer size="xl" />
<LandingLinksIconsCategories links={iconLinks} categories={separatorCategories} />
<EuiSpacer size="l" />
<EuiHorizontalRule />
<LandingLinksIconsCategoriesGroups links={links} categories={accordionCategories} />
</TrackApplicationView>
</KibanaPageTemplate.Section>
</KibanaPageTemplate>
);
};
// eslint-disable-next-line import/no-default-export
export default ProjectSettingsRoute;

View file

@ -29,9 +29,6 @@ const AssetsPage = withSuspense(AssetsPageLazy);
const MachineLearningPageLazy = lazy(() => import('./machine_learning'));
const MachineLearningPage = withSuspense(MachineLearningPageLazy);
const ProjectSettingsPageLazy = lazy(() => import('./project_settings'));
const ProjectSettingsPage = withSuspense(ProjectSettingsPageLazy);
// Sets the project specific routes for Serverless as extra routes in the Security Solution plugin
export const setRoutes = (services: Services) => {
const projectRoutes: RouteProps[] = [
@ -47,10 +44,6 @@ export const setRoutes = (services: Services) => {
path: SecurityPagePath[SecurityPageName.mlLanding],
component: withServicesProvider(MachineLearningPage, services),
},
{
path: SecurityPagePath[SecurityPageName.projectSettings],
component: withServicesProvider(ProjectSettingsPage, services),
},
];
services.securitySolution.setExtraRoutes(projectRoutes);
};

View file

@ -53,7 +53,7 @@ export class SecuritySolutionServerlessPlugin
securitySolution.experimentalFeatures
).features;
setupNavigation(core, setupDeps, this.experimentalFeatures);
setupNavigation(core, setupDeps);
return {};
}
@ -73,7 +73,7 @@ export class SecuritySolutionServerlessPlugin
dashboardsLandingCallout: getDashboardsLandingCallout(services),
});
startNavigation(services, this.config);
startNavigation(services);
setRoutes(services);
return {};

View file

@ -38,5 +38,5 @@ export interface SecuritySolutionServerlessPluginStartDeps {
export type ServerlessSecurityPublicConfig = Pick<
ServerlessSecurityConfigSchema,
'productTypes' | 'developer' | 'enableExperimental'
'productTypes' | 'enableExperimental'
>;

View file

@ -8,13 +8,12 @@
import { schema, type TypeOf } from '@kbn/config-schema';
import type { PluginConfigDescriptor, PluginInitializerContext } from '@kbn/core/server';
import type { SecuritySolutionPluginSetup } from '@kbn/security-solution-plugin/server/plugin_contract';
import { developerConfigSchema, productTypes } from '../common/config';
import { productTypes } from '../common/config';
import type { ExperimentalFeatures } from '../common/experimental_features';
import { parseExperimentalConfigValue } from '../common/experimental_features';
export const configSchema = schema.object({
enabled: schema.boolean({ defaultValue: false }),
developer: developerConfigSchema,
productTypes,
/**
* For internal use. A list of string values (comma delimited) that will enable experimental
@ -38,7 +37,6 @@ export const config: PluginConfigDescriptor<ServerlessSecuritySchema> = {
exposeToBrowser: {
enableExperimental: true,
productTypes: true,
developer: true,
},
schema: configSchema,
deprecations: ({ renameFromRoot }) => [

View file

@ -20,7 +20,6 @@
"@kbn/security-solution-ess",
"@kbn/security-solution-plugin",
"@kbn/serverless",
"@kbn/shared-ux-page-solution-nav",
"@kbn/security-solution-side-nav",
"@kbn/security-solution-navigation",
"@kbn/security-solution-upselling",

View file

@ -1,34 +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 { FtrProviderContext } from '../../../ftr_provider_context';
export default function ({ getPageObject }: FtrProviderContext) {
const PageObject = getPageObject('common');
const svlCommonPage = getPageObject('svlCommonPage');
describe('Management', function () {
before(async () => {
await svlCommonPage.login();
});
after(async () => {
await svlCommonPage.forceLogout();
});
it('redirects from common management url to security specific page', async () => {
const SUB_URL = '';
await PageObject.navigateToUrl('management', SUB_URL, {
ensureCurrentUrl: false,
shouldLoginIfPrompted: false,
shouldUseHashForSubUrl: false,
});
await PageObject.waitUntilUrlIncludes('/security/project_settings');
});
});
}

View file

@ -11,7 +11,6 @@ export default function ({ loadTestFile }: FtrProviderContext) {
describe('serverless security UI', function () {
loadTestFile(require.resolve('./ftr/landing_page'));
loadTestFile(require.resolve('./ftr/navigation'));
loadTestFile(require.resolve('./ftr/management'));
loadTestFile(require.resolve('./ftr/cases/attachment_framework'));
loadTestFile(require.resolve('./ftr/cases/view_case'));
loadTestFile(require.resolve('./ftr/cases/create_case_form'));