Skip to content

Commit

Permalink
Add 'inventory' item to Security navigation menu (#204373)
Browse files Browse the repository at this point in the history
## Summary

Add 'inventory' item to Security navigation menu (either when the
sidebar is expanded and the full navigation menu is shown or when the
sidebar is collapsed and only the Security menu is visible).

### Changeset details

- Render 'inventory' item and enable `/app/security/asset_inventory`
route both conditionally based on feature flag
- Async loading/rendering of AssetInventory main page from within
SecuritySolution plugin
  - Delete unnecessary boilerplate existing in AssetInventory

### Out of scope
- AssetInventory nav sub-menu is skipped until more concrete
requirements are defined on what to do with them

### How to test

Activate the feature flag by adding this line to your local
`kibana.dev.yml`:

```yml
xpack.securitySolution.enableExperimental: ['assetInventoryStoreEnabled']
```

### Screenshots

<details><summary>Full menu (expanded mode)</summary>
<img width="240" alt="Screenshot 2024-12-16 at 13 12 45"
src="https://github.com/user-attachments/assets/f0939f38-5be6-481b-ace1-07f46f3622ae"
/>
</details> 

<details><summary>Only Security menu (collapsed mode)</summary>
<img width="256" alt="Screenshot 2024-12-16 at 13 12 33"
src="https://github.com/user-attachments/assets/b0bd62f0-5cea-4b7b-a731-3a53be362192"
/>
</details> 

<details><summary>AssetInventory loaded async from within Security
Solution</summary>
<img width="1640" alt="Screenshot 2024-12-16 at 17 23 01"
src="https://github.com/user-attachments/assets/b84716c9-6b18-4225-bf71-62c8ef07b302"
/>
</details> 

### Checklist

- [x] If a plugin configuration key changed, check if it needs to be
allowlisted in the cloud and added to the [docker
list](https://github.com/elastic/kibana/blob/main/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker)
- [x] The PR description includes the appropriate Release Notes section,
and the correct `release_note:*` label is applied per the
[guidelines](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)

### Risks

No risks. Navigation item will be added if and only if feature flag is
enabled, which shouldn't happen for end users until development is
completed.
  • Loading branch information
albertoblaz authored Dec 23, 2024
1 parent 71303af commit 6d5be74
Show file tree
Hide file tree
Showing 22 changed files with 191 additions and 68 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export enum SecurityPageName {
administration = 'administration',
alerts = 'alerts',
assets = 'assets',
assetInventory = 'asset_inventory',
attackDiscovery = 'attack_discovery',
blocklist = 'blocklist',
/*
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -6,33 +6,26 @@
*/
import React from 'react';
import { FormattedMessage, I18nProvider } from '@kbn/i18n-react';
import { BrowserRouter as Router } from '@kbn/shared-ux-router';
import { EuiPageTemplate, EuiTitle } from '@elastic/eui';
import type { CoreStart } from '@kbn/core/public';

interface AssetInventoryAppDeps {
basename: string;
notifications: CoreStart['notifications'];
http: CoreStart['http'];
}

export const AssetInventoryApp = ({ basename }: AssetInventoryAppDeps) => {
const AssetInventoryApp = () => {
return (
<Router basename={basename}>
<I18nProvider>
<>
<EuiPageTemplate restrictWidth="1000px">
<EuiPageTemplate.Header>
<EuiTitle size="l">
<h1>
<FormattedMessage id="assetInventory.helloWorldText" defaultMessage="Inventory" />
</h1>
</EuiTitle>
</EuiPageTemplate.Header>
<EuiPageTemplate.Section />
</EuiPageTemplate>
</>
</I18nProvider>
</Router>
<I18nProvider>
<>
<EuiPageTemplate restrictWidth="1000px">
<EuiPageTemplate.Header>
<EuiTitle size="l">
<h1>
<FormattedMessage id="assetInventory.allAssets" defaultMessage="All Assets" />
</h1>
</EuiTitle>
</EuiPageTemplate.Header>
<EuiPageTemplate.Section />
</EuiPageTemplate>
</>
</I18nProvider>
);
};

// we need to use default exports to import it via React.lazy
export default AssetInventoryApp; // eslint-disable-line import/no-default-export
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import React, { lazy, Suspense } from 'react';
import { EuiLoadingSpinner } from '@elastic/eui';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { AppPluginStartDependencies } from '../types';

// Initializing react-query
const queryClient = new QueryClient({
defaultOptions: {
queries: {
refetchOnWindowFocus: false,
refetchOnMount: false,
refetchOnReconnect: false,
},
},
});

const AssetInventoryLazy = lazy(() => import('../components/app'));

export const getAssetInventoryLazy = (props: AppPluginStartDependencies) => {
return (
<QueryClientProvider client={queryClient}>
<Suspense fallback={<EuiLoadingSpinner />}>
<AssetInventoryLazy {...props} />
</Suspense>
</QueryClientProvider>
);
};
15 changes: 5 additions & 10 deletions x-pack/solutions/security/plugins/asset_inventory/public/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,29 +4,24 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import type { AppMountParameters, CoreSetup, CoreStart, Plugin } from '@kbn/core/public';
import type { CoreSetup, CoreStart, Plugin } from '@kbn/core/public';
import type {
AssetInventoryPluginSetup,
AssetInventoryPluginStart,
AppPluginStartDependencies,
} from './types';
import { getAssetInventoryLazy } from './methods';

export class AssetInventoryPlugin
implements Plugin<AssetInventoryPluginSetup, AssetInventoryPluginStart>
{
public setup(core: CoreSetup): AssetInventoryPluginSetup {
return {};
}
public start(
coreStart: CoreStart,
depsStart: AppPluginStartDependencies
): AssetInventoryPluginStart {
public start(coreStart: CoreStart): AssetInventoryPluginStart {
return {
getAssetInventoryPage: async (params: AppMountParameters) => {
// Load application bundle
const { renderApp } = await import('./application');
// Render the application
return renderApp(coreStart, depsStart as AppPluginStartDependencies, params);
getAssetInventoryPage: (assetInventoryDeps: AppPluginStartDependencies) => {
return getAssetInventoryLazy(assetInventoryDeps);
},
};
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,9 @@
// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface AssetInventoryPluginSetup {}

// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface AssetInventoryPluginStart {}
export interface AssetInventoryPluginStart {
getAssetInventoryPage: (assetInventoryStartDeps: AppPluginStartDependencies) => JSX.Element;
}

// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface AppPluginStartDependencies {}
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,5 @@
"../../../../../typings/**/*"
],
"exclude": ["target/**/*"],
"kbn_references": [
"@kbn/core",
"@kbn/i18n-react",
"@kbn/shared-ux-router",
"@kbn/securitysolution-es-utils"
]
"kbn_references": ["@kbn/core", "@kbn/i18n-react", "@kbn/securitysolution-es-utils"]
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export { SecurityPageName } from '@kbn/security-solution-navigation';
*/
export const APP_ID = 'securitySolution' as const;
export const APP_UI_ID = 'securitySolutionUI' as const;
export const ASSET_INVENTORY_FEATURE_ID = 'securitySolutionAssetInventory' as const;
export const ASSISTANT_FEATURE_ID = 'securitySolutionAssistant' as const;
export const ATTACK_DISCOVERY_FEATURE_ID = 'securitySolutionAttackDiscovery' as const;
export const CASES_FEATURE_ID = 'securitySolutionCasesV2' as const;
Expand Down Expand Up @@ -102,6 +103,7 @@ export const EXCEPTIONS_PATH = '/exceptions' as const;
export const EXCEPTION_LIST_DETAIL_PATH = `${EXCEPTIONS_PATH}/details/:detailName` as const;
export const HOSTS_PATH = '/hosts' as const;
export const ATTACK_DISCOVERY_PATH = '/attack_discovery' as const;
export const ASSET_INVENTORY_PATH = '/asset_inventory' as const;
export const USERS_PATH = '/users' as const;
export const KUBERNETES_PATH = '/kubernetes' as const;
export const NETWORK_PATH = '/network' as const;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
],
"requiredPlugins": [
"actions",
"assetInventory",
"alerting",
"cases",
"cloud",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ export const CATEGORIES: Array<SeparatorLinkCategory<SolutionPageName>> = [
SecurityPageName.exploreLanding,
],
},
{
type: LinkCategoryType.separator,
linkIds: [SecurityPageName.assetInventory],
},
{
type: LinkCategoryType.separator,
linkIds: [SecurityPageName.assets],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,10 @@ export const ATTACK_DISCOVERY = i18n.translate(
}
);

export const INVENTORY = i18n.translate('xpack.securitySolution.navigation.inventory', {
defaultMessage: 'Inventory',
});

export const TIMELINES = i18n.translate('xpack.securitySolution.navigation.timelines', {
defaultMessage: 'Timelines',
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import type { CoreStart } from '@kbn/core/public';

import { links as attackDiscoveryLinks } from './attack_discovery/links';
import { links as assetInventoryLinks } from './asset_inventory/links';
import type { AppLinkItems } from './common/links/types';
import { indicatorsLinks } from './threat_intelligence/links';
import { links as alertsLinks } from './detections/links';
Expand All @@ -32,6 +33,7 @@ export const appLinks: AppLinkItems = Object.freeze([
timelinesLinks,
indicatorsLinks,
exploreLinks,
assetInventoryLinks,
rulesLinks,
onboardingLinks,
managementLinks,
Expand All @@ -52,6 +54,7 @@ export const getFilteredLinks = async (
timelinesLinks,
indicatorsLinks,
exploreLinks,
assetInventoryLinks,
rulesLinks,
onboardingLinks,
managementFilteredLinks,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/*
* 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 { SecuritySubPlugin } from '../app/types';
import { routes } from './routes';

export class AssetInventory {
public setup() {}

public start(): SecuritySubPlugin {
return {
routes,
};
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import { i18n } from '@kbn/i18n';

import { INVENTORY } from '../app/translations';
import { ASSET_INVENTORY_PATH, SecurityPageName, SERVER_APP_ID } from '../../common/constants';
import type { LinkItem } from '../common/links/types';

export const links: LinkItem = {
capabilities: [`${SERVER_APP_ID}.show`],
globalNavPosition: 10,
globalSearchKeywords: [
i18n.translate('xpack.securitySolution.appLinks.inventory', {
defaultMessage: 'Inventory',
}),
],
experimentalKey: 'assetInventoryStoreEnabled',
id: SecurityPageName.assetInventory,
path: ASSET_INVENTORY_PATH,
title: INVENTORY,
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/*
* 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 { SecuritySolutionPageWrapper } from '../../common/components/page_wrapper';
import { useKibana } from '../../common/lib/kibana';
import { SecurityPageName } from '../../../common/constants';
import { SpyRoute } from '../../common/utils/route/spy_routes';

export const AssetInventoryContainer = React.memo(() => {
const { assetInventory } = useKibana().services;

return (
<SecuritySolutionPageWrapper noPadding>
{assetInventory.getAssetInventoryPage({})}
<SpyRoute pageName={SecurityPageName.assetInventory} />
</SecuritySolutionPageWrapper>
);
});

AssetInventoryContainer.displayName = 'AssetInventoryContainer';
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/*
* 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 type { SecuritySubPluginRoutes } from '../app/types';
import { SecurityPageName } from '../app/types';
import { ASSET_INVENTORY_PATH } from '../../common/constants';
import { PluginTemplateWrapper } from '../common/components/plugin_template_wrapper';
import { SecurityRoutePageWrapper } from '../common/components/security_route_page_wrapper';
import { ExperimentalFeaturesService } from '../common/experimental_features_service';
import { AssetInventoryContainer } from './pages';

export const AssetInventoryRoutes = () => (
<PluginTemplateWrapper>
<SecurityRoutePageWrapper pageName={SecurityPageName.assetInventory}>
<AssetInventoryContainer />
</SecurityRoutePageWrapper>
</PluginTemplateWrapper>
);

export const routes: SecuritySubPluginRoutes = [
{
path: ExperimentalFeaturesService.get().assetInventoryStoreEnabled ? ASSET_INVENTORY_PATH : [],
component: AssetInventoryRoutes,
},
];
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,8 @@ export const CATEGORIES: SeparatorLinkCategory[] = [
SecurityPageName.exploreLanding,
],
},
{
type: LinkCategoryType.separator,
linkIds: [SecurityPageName.assetInventory],
},
];
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
* By loading these later we can reduce the initial bundle size and allow users to delay loading these dependencies until they are needed.
*/

import { AssetInventory } from './asset_inventory';
import { AttackDiscovery } from './attack_discovery';
import { Cases } from './cases';
import { Detections } from './detections';
Expand All @@ -35,6 +36,7 @@ import { SiemMigrations } from './siem_migrations';
* The classes used to instantiate the sub plugins. These are grouped into a single object for the sake of bundling them in a single dynamic import.
*/
const subPluginClasses = {
AssetInventory,
AttackDiscovery,
Detections,
Cases,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ export const links: LinkItem = {
path: MANAGE_PATH,
skipUrlState: true,
hideTimeline: true,
globalNavPosition: 10,
globalNavPosition: 11,
capabilities: [`${SERVER_APP_ID}.show`],
globalSearchKeywords: [
i18n.translate('xpack.securitySolution.appLinks.manage', {
Expand Down
Loading

0 comments on commit 6d5be74

Please sign in to comment.