[AI4DSOC] Add navigation (#214382)

This commit is contained in:
Tomasz Ciecierski 2025-03-17 18:35:08 +01:00 committed by GitHub
parent e5e42a87ea
commit 7083930b87
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
24 changed files with 644 additions and 28 deletions

View file

@ -1 +1,5 @@
# Security Search AI Lake tier config
## Disable plugins
xpack.osquery.enabled: false

View file

@ -85,4 +85,5 @@ export enum SecurityPageName {
entityAnalyticsEntityStoreManagement = 'entity_analytics-entity_store_management',
coverageOverview = 'coverage-overview',
notes = 'notes',
alertSummary = 'alert_summary',
}

View file

@ -8,6 +8,8 @@
export enum ProductFeatureSecurityKey {
/** Enables Advanced Insights (Entity Risk, GenAI) */
advancedInsights = 'advanced_insights',
/** Enables Alerts Summary page for AI SOC */
alertsSummary = 'alerts_summary',
/**
* Enables Investigation guide in Timeline
*/

View file

@ -42,6 +42,18 @@ export const securityDefaultProductFeaturesConfig: DefaultSecurityProductFeature
},
},
},
[ProductFeatureSecurityKey.alertsSummary]: {
privileges: {
all: {
ui: ['alerts_summary'],
api: [`${APP_ID}-alert-summary`],
},
read: {
ui: ['alerts_summary_read'],
api: [`${APP_ID}-alert-summary`],
},
},
},
[ProductFeatureSecurityKey.investigationGuideInteractions]: {
privileges: {
all: {

View file

@ -96,6 +96,7 @@ export const DETECTION_RESPONSE_PATH = '/detection_response' as const;
export const DETECTIONS_PATH = '/detections' as const;
export const ALERTS_PATH = '/alerts' as const;
export const ALERT_DETAILS_REDIRECT_PATH = `${ALERTS_PATH}/redirect` as const;
export const ALERT_SUMMARY_PATH = `/alert_summary` as const;
export const RULES_PATH = '/rules' as const;
export const RULES_LANDING_PATH = `${RULES_PATH}/landing` as const;
export const RULES_ADD_PATH = `${RULES_PATH}/add_rules` as const;

View file

@ -10,7 +10,7 @@ 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';
import { alertsLink, alertSummaryLink } from './detections/links';
import { links as rulesLinks } from './rules/links';
import { links as timelinesLinks } from './timelines/links';
import { links as casesLinks } from './cases/links';
@ -26,7 +26,8 @@ export { solutionAppLinksSwitcher } from './app/solution_navigation/links/app_li
export const appLinks: AppLinkItems = Object.freeze([
dashboardsLinks,
alertsLinks,
alertsLink,
alertSummaryLink,
attackDiscoveryLinks,
findingsLinks,
casesLinks,
@ -47,7 +48,8 @@ export const getFilteredLinks = async (
return Object.freeze([
dashboardsLinks,
alertsLinks,
alertsLink,
alertSummaryLink,
attackDiscoveryLinks,
findingsLinks,
casesLinks,

View file

@ -5,11 +5,16 @@
* 2.0.
*/
import { i18n } from '@kbn/i18n';
import { ALERTS_PATH, SecurityPageName, SECURITY_FEATURE_ID } from '../../common/constants';
import {
ALERTS_PATH,
SecurityPageName,
SECURITY_FEATURE_ID,
ALERT_SUMMARY_PATH,
} from '../../common/constants';
import { ALERTS } from '../app/translations';
import type { LinkItem } from '../common/links/types';
export const links: LinkItem = {
export const alertsLink: LinkItem = {
id: SecurityPageName.alerts,
title: ALERTS,
path: ALERTS_PATH,
@ -21,3 +26,17 @@ export const links: LinkItem = {
}),
],
};
export const alertSummaryLink: LinkItem = {
id: SecurityPageName.alertSummary,
path: ALERT_SUMMARY_PATH,
title: 'Alert summary',
capabilities: [[`${SECURITY_FEATURE_ID}.show`, `${SECURITY_FEATURE_ID}.alerts_summary`]],
globalNavPosition: 3,
globalSearchKeywords: [
i18n.translate('xpack.securitySolution.appLinks.alertSummary', {
defaultMessage: 'Alert summary',
}),
],
hideTimeline: true,
};

View file

@ -7,9 +7,11 @@
import type { Capabilities } from '@kbn/core/public';
import { SECURITY_FEATURE_ID } from '../common/constants';
export function hasAccessToSecuritySolution(capabilities: Capabilities) {
return (
export function hasAccessToSecuritySolution(capabilities: Capabilities): boolean {
return Boolean(
// Using `siemV2`
capabilities[SECURITY_FEATURE_ID]?.show === true
capabilities[SECURITY_FEATURE_ID]?.show ||
capabilities.securitySolutionCasesV2?.read_cases ||
capabilities.securitySolutionAttackDiscovery?.['attack-discovery']
);
}

View file

@ -435,16 +435,11 @@ export class Plugin implements IPlugin<PluginSetup, PluginStart, SetupPlugins, S
const { capabilities } = core.application;
const { upsellingService, isSolutionNavigationEnabled$ } = this.contract;
// When the user does not have access to SIEM (main Security feature) nor Security Cases feature, the plugin must be inaccessible.
if (
!hasAccessToSecuritySolution(capabilities) &&
!capabilities.securitySolutionCasesV2?.read_cases
) {
this.appUpdater$.next(() => ({
status: AppStatus.inaccessible,
visibleIn: [],
}));
// no need to register the links updater when the plugin is inaccessible
// When the user does not have any of the capabilities required to access security solution, the plugin should be inaccessible
// This is necessary to hide security solution from the selectable solutions in the spaces UI
if (!hasAccessToSecuritySolution(capabilities)) {
this.appUpdater$.next(() => ({ status: AppStatus.inaccessible, visibleIn: [] }));
// no need to register the links updater when the plugin is inaccessible. return early
return;
}

View file

@ -329,7 +329,7 @@ export class Plugin implements ISecuritySolutionPlugin {
licensing: plugins.licensing,
scheduleNotificationResponseActionsService: getScheduleNotificationResponseActionsService({
endpointAppContextService: this.endpointAppContextService,
osqueryCreateActionService: plugins.osquery.createActionService,
osqueryCreateActionService: plugins.osquery?.createActionService,
}),
};

View file

@ -5,8 +5,17 @@
* 2.0.
*/
import { ALL_PRODUCT_FEATURE_KEYS } from '@kbn/security-solution-features/keys';
import type { ProductFeatureKeyType } from '@kbn/security-solution-features/keys';
import {
ALL_PRODUCT_FEATURE_KEYS,
ProductFeatureSecurityKey,
} from '@kbn/security-solution-features/keys';
// Just copying all feature keys for now.
// We may need a different set of keys in the future if we create serverless-specific productFeatures
export const DEFAULT_PRODUCT_FEATURES = [...ALL_PRODUCT_FEATURE_KEYS];
// List of product features that are disabled in different offering (eg. Serverless).
const DISABLED_PRODUCT_FEATURES: ProductFeatureKeyType[] = [
ProductFeatureSecurityKey.alertsSummary,
];
export const DEFAULT_PRODUCT_FEATURES = ALL_PRODUCT_FEATURE_KEYS.filter(
(key) => !DISABLED_PRODUCT_FEATURES.includes(key as ProductFeatureKeyType)
);

View file

@ -16,7 +16,11 @@ type PliProductFeatures = Readonly<
export const PLI_PRODUCT_FEATURES: PliProductFeatures = {
[ProductLine.aiSoc]: {
search_ai_lake: [ProductFeatureKey.attackDiscovery, ProductFeatureKey.assistant],
search_ai_lake: [
ProductFeatureKey.attackDiscovery,
ProductFeatureKey.assistant,
ProductFeatureKey.alertsSummary,
],
essentials: [ProductFeatureKey.attackDiscovery, ProductFeatureKey.assistant],
complete: [ProductFeatureKey.attackDiscovery, ProductFeatureKey.assistant],
},

View file

@ -0,0 +1,33 @@
/*
* 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 AiForTheSoc: React.FC<SVGProps<SVGSVGElement>> = (props) => (
<svg fill="none" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg" {...props}>
<path
fill="#F04E98"
fillRule="evenodd"
d="M5.625 4.38V0h12.5v10.465c0 2.446-3.986 4.048-5.635 4.535V4.38H5.625Z"
clipRule="evenodd"
/>
<path
fill="#FACB3D"
fillRule="evenodd"
d="M1.875 12.546V6.25h8.75V20s-8.75-3.769-8.75-7.454Z"
clipRule="evenodd"
/>
<path
fill="#0B64DD"
fillRule="evenodd"
d="M5.625 6.25h5V15c-1.865-.713-5-2.348-5-4.402V6.25Z"
clipRule="evenodd"
/>
</svg>
);
// eslint-disable-next-line import/no-default-export
export default AiForTheSoc;

View file

@ -47,3 +47,6 @@ export const IconRapidBarGraphLazy = withSuspenseIcon(
export const IconFilebeatChartLazy = withSuspenseIcon(
React.lazy(() => import('./icons/filebeat_chart'))
);
export const IconAiForTheSocLazy = withSuspenseIcon(
React.lazy(() => import('./icons/ai_for_the_soc'))
);

View file

@ -0,0 +1,137 @@
/*
* 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 { applyAiSocNavigation, aiGroup } from './ai_soc_navigation';
import { alertSummaryLink } from './links';
import { ProductLine, ProductTier } from '../../../common/product';
import * as utils from './utils'; // We'll spy on the named export from here
import type { WritableDraft } from 'immer/dist/internal';
import type {
AppDeepLinkId,
GroupDefinition,
NavigationTreeDefinition,
NodeDefinition,
} from '@kbn/core-chrome-browser';
const nonAiProduct = { product_line: ProductLine.security, product_tier: ProductTier.essentials };
const aiProduct = { product_line: ProductLine.aiSoc, product_tier: ProductTier.essentials };
const getSampleDraft = (): WritableDraft<NavigationTreeDefinition<AppDeepLinkId>> => ({
body: [
{
type: 'navGroup',
id: 'security_solution_nav',
title: 'Security',
icon: 'logoSecurity',
breadcrumbStatus: 'hidden',
defaultIsCollapsed: false,
children: [
{
breadcrumbStatus: 'hidden',
children: [
{
id: 'discover:',
link: 'discover',
title: 'Discover',
},
{
id: 'dashboards',
link: 'securitySolutionUI:dashboards',
title: 'Dashboards',
children: [
{
id: 'overview',
link: 'securitySolutionUI:overview',
title: 'Overview',
},
{
id: 'detection_response',
link: 'securitySolutionUI:detection_response',
title: 'Detection & Response',
},
{
id: 'entity_analytics',
link: 'securitySolutionUI:entity_analytics',
title: 'Entity Analytics',
},
{
id: 'data_quality',
link: 'securitySolutionUI:data_quality',
title: 'Data Quality',
},
],
renderAs: 'panelOpener',
},
],
},
],
isCollapsible: false,
},
],
});
describe('applyAiSocNavigation', () => {
let draft: WritableDraft<NavigationTreeDefinition<AppDeepLinkId>>;
beforeEach(() => {
draft = getSampleDraft();
});
describe('when productTypes does NOT include aiSoc', () => {
it('should not modify the navigation tree', () => {
const productTypes = [nonAiProduct];
const originalDraft = JSON.parse(JSON.stringify(draft));
applyAiSocNavigation(draft, productTypes);
// Should remain unchanged
expect(draft).toEqual(originalDraft);
});
});
describe('when productTypes includes aiSoc', () => {
let filterSpy: jest.SpyInstance;
beforeEach(() => {
// Spy on filterFromWhitelist so we can control the filter result
filterSpy = jest
.spyOn(utils, 'filterFromWhitelist')
.mockImplementation((nodes: Array<NodeDefinition<AppDeepLinkId>>, _whitelist: string[]) => {
// Simulate that the filter keeps only alertSummaryLink
return [alertSummaryLink];
});
});
afterEach(() => {
filterSpy.mockRestore();
});
it('should modify the navigation tree correctly', () => {
const productTypes = [aiProduct];
applyAiSocNavigation(draft, productTypes);
// The final draft.body should be replaced by one navGroup from aiGroup
// that has only the filtered children (we forced it to return [alertSummaryLink]).
expect(draft.body).toEqual([
{
...aiGroup,
children: [alertSummaryLink],
},
]);
// Check that filterFromWhitelist was called with the original children plus alertSummaryLink
const securityGroup = getSampleDraft().body[0] as WritableDraft<
GroupDefinition<AppDeepLinkId, string, string>
>;
const originalChildren = securityGroup.children;
const expectedChildrenForFiltering = [...originalChildren, alertSummaryLink];
expect(filterSpy).toHaveBeenCalledWith(expectedChildrenForFiltering, expect.any(Array));
});
});
});

View file

@ -0,0 +1,74 @@
/*
* 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 {
AppDeepLinkId,
GroupDefinition,
NavigationTreeDefinition,
} from '@kbn/core-chrome-browser';
import type { WritableDraft } from 'immer/dist/internal';
import { ExternalPageName, SecurityPageName } from '@kbn/security-solution-navigation';
import { alertSummaryLink } from './links';
import { AiForTheSocIcon } from './icons';
import { filterFromWhitelist } from './utils';
import { type SecurityProductTypes } from '../../../common/config';
import { ProductLine } from '../../../common/product';
const shouldUseAINavigation = (productTypes: SecurityProductTypes) => {
return productTypes.some((productType) => productType.product_line === ProductLine.aiSoc);
};
export const aiGroup: GroupDefinition<AppDeepLinkId, string, string> = {
type: 'navGroup',
id: 'security_solution_ai_nav',
title: 'AI for SOC',
icon: AiForTheSocIcon,
breadcrumbStatus: 'hidden',
defaultIsCollapsed: false,
isCollapsible: false,
children: [],
};
// Elements we want to show in AI for SOC navigation
// This is a temporary solution until we figure out a way to handle Upselling with new Tier
const whitelist = [
SecurityPageName.case,
SecurityPageName.caseCreate,
SecurityPageName.caseConfigure,
SecurityPageName.alertSummary,
SecurityPageName.attackDiscovery,
ExternalPageName.discover,
SecurityPageName.mlLanding,
];
// Apply AI for SOC navigation tree changes.
// The navigation tree received by parameter is generated at: x-pack/solutions/security/plugins/security_solution/public/app/solution_navigation/navigation_tree.ts
// An example of static navigation tree: x-pack/solutions/observability/plugins/observability/public/navigation_tree.ts
// !! This is a temporary solution until the "classic" navigation is deprecated and the "generated" navigationTree is replaced by a static navigationTree (probably multiple of them).
export const applyAiSocNavigation = (
draft: WritableDraft<NavigationTreeDefinition<AppDeepLinkId>>,
productTypes: SecurityProductTypes
): void => {
if (!shouldUseAINavigation(productTypes)) {
return;
}
const securityGroup = draft.body[0] as WritableDraft<
GroupDefinition<AppDeepLinkId, string, string>
>;
// hardcode elements existing only in AI for SOC group
securityGroup.children.push(alertSummaryLink);
// Overwrite the children with only the elements available for AI for SOC navigation
// Temporary solution until we have clarity how to proceed with Upselling in the new Tier
// (eg. Threat Intelligence couldn't be hidden)
securityGroup.children = filterFromWhitelist(securityGroup.children, whitelist);
draft.body = [{ ...aiGroup, children: securityGroup.children }];
};

View file

@ -0,0 +1,15 @@
/*
* 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 { EuiIconProps } from '@elastic/eui';
import { EuiIcon } from '@elastic/eui';
import React from 'react';
import { IconAiForTheSocLazy } from '../../common/lazy_icons';
export const AiForTheSocIcon = ({ size = 'm', ...rest }: Omit<EuiIconProps, 'type'>) => {
return <EuiIcon {...{ type: IconAiForTheSocLazy, size, ...rest }} />;
};

View file

@ -0,0 +1,16 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { SecurityPageName } from '@kbn/security-solution-navigation';
import type { AppDeepLinkId, NodeDefinition } from '@kbn/core-chrome-browser';
import { ALERT_SUMMARY } from './translations';
export const alertSummaryLink: NodeDefinition<AppDeepLinkId, string, string> = {
id: SecurityPageName.alertSummary,
link: 'securitySolutionUI:alert_summary',
title: ALERT_SUMMARY,
};

View file

@ -0,0 +1,15 @@
/*
* 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';
export const ALERT_SUMMARY = i18n.translate(
'xpack.securitySolutionServerless.navigation.aiSoc.alertSummary',
{
defaultMessage: 'Alert summary',
}
);

View file

@ -0,0 +1,235 @@
/*
* 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 { filterFromWhitelist } from './utils';
import type { AppDeepLinkId, NodeDefinition } from '@kbn/core-chrome-browser';
describe('AI SOC utils', () => {
describe('filterFromWhitelist', () => {
const nodes: Array<NodeDefinition<AppDeepLinkId>> = [
{
breadcrumbStatus: 'hidden',
children: [
{
id: 'attack_discovery',
link: 'securitySolutionUI:attack_discovery',
title: 'Attack discovery',
},
{
id: 'cases',
link: 'securitySolutionUI:cases',
title: 'Cases',
children: [
{
id: 'cases_create',
link: 'securitySolutionUI:cases_create',
title: 'Create',
sideNavStatus: 'hidden',
},
{
id: 'cases_configure',
link: 'securitySolutionUI:cases_configure',
title: 'Settings',
sideNavStatus: 'hidden',
},
],
renderAs: 'panelOpener',
},
],
},
];
it('should filter nodes based on whitelist of IDs', () => {
const idsToAttach = ['cases', 'attack_discovery'];
const result = filterFromWhitelist(nodes, idsToAttach);
expect(result).toEqual([
{
id: 'attack_discovery',
link: 'securitySolutionUI:attack_discovery',
title: 'Attack discovery',
},
{
id: 'cases',
link: 'securitySolutionUI:cases',
title: 'Cases',
children: [
{
id: 'cases_create',
link: 'securitySolutionUI:cases_create',
title: 'Create',
sideNavStatus: 'hidden',
},
{
id: 'cases_configure',
link: 'securitySolutionUI:cases_configure',
title: 'Settings',
sideNavStatus: 'hidden',
},
],
renderAs: 'panelOpener',
},
]);
});
it('should handle empty nodes array', () => {
const result = filterFromWhitelist([], ['cases']);
expect(result).toEqual([]);
});
it('should handle empty idsToAttach array', () => {
const result = filterFromWhitelist(nodes, []);
expect(result).toEqual([]);
});
});
describe('filterFromWhitelist - Larger Dataset', () => {
let bigNodes: Array<NodeDefinition<AppDeepLinkId>>;
beforeEach(() => {
bigNodes = [
{
breadcrumbStatus: 'hidden',
children: [
{
id: 'discover:',
link: 'discover',
title: 'Discover',
},
],
},
{
breadcrumbStatus: 'hidden',
children: [
{
id: 'attack_discovery',
link: 'securitySolutionUI:attack_discovery',
title: 'Attack discovery',
},
{
id: 'cases',
link: 'securitySolutionUI:cases',
title: 'Cases',
children: [
{
id: 'cases_create',
link: 'securitySolutionUI:cases_create',
title: 'Create',
sideNavStatus: 'hidden',
},
{
id: 'cases_configure',
link: 'securitySolutionUI:cases_configure',
title: 'Settings',
sideNavStatus: 'hidden',
},
],
renderAs: 'panelOpener',
},
],
},
{
breadcrumbStatus: 'hidden',
children: [
{
id: 'threat_intelligence',
link: 'securitySolutionUI:threat_intelligence',
title: 'Intelligence',
},
],
},
{
breadcrumbStatus: 'hidden',
children: [
{
id: 'assets',
link: 'securitySolutionUI:assets',
title: 'Assets',
children: [
{
id: 'fleet:',
link: 'fleet',
title: 'Fleet',
children: [
{
id: 'fleet:agents',
link: 'fleet:agents',
title: 'Agents',
},
{
id: 'fleet:policies',
link: 'fleet:policies',
title: 'Policies',
},
{
id: 'fleet:enrollment_tokens',
link: 'fleet:enrollment_tokens',
title: 'Enrollment tokens',
},
{
id: 'fleet:uninstall_tokens',
link: 'fleet:uninstall_tokens',
title: 'Uninstall tokens',
},
{
id: 'fleet:data_streams',
link: 'fleet:data_streams',
title: 'Data streams',
},
{
id: 'fleet:settings',
link: 'fleet:settings',
title: 'Settings',
},
],
},
],
renderAs: 'panelOpener',
},
],
},
{
id: 'management:securityAiAssistantManagement',
link: 'management:securityAiAssistantManagement',
title: 'Knowledge sources',
},
];
});
it('should whitelist multiple nodes from deep in the structure', () => {
const result = filterFromWhitelist(bigNodes, ['fleet:policies', 'cases_create']);
expect(result).toHaveLength(2);
expect(result.map((n) => n.id).sort()).toEqual(['cases_create', 'fleet:policies'].sort());
});
it('should preserve entire whitelisted node with children if matched directly', () => {
const result = filterFromWhitelist(bigNodes, ['assets']);
expect(result).toHaveLength(1);
expect(result[0].id).toEqual('assets');
expect(result[0].children).toBeDefined();
expect(result[0].children![0].id).toEqual('fleet:');
});
it('should handle no matches found in nested structure', () => {
const result = filterFromWhitelist(bigNodes, ['random1', 'random2']);
expect(result).toEqual([]);
});
it('should handle partial matches plus top-level matches', () => {
const result = filterFromWhitelist(bigNodes, ['threat_intelligence', 'fleet:data_streams']);
expect(result).toHaveLength(2);
expect(result.map((r) => r.id).sort()).toEqual(
['fleet:data_streams', 'threat_intelligence'].sort()
);
});
it('should work if we match the top-level `discover:` node plus a nested fleet child', () => {
const result = filterFromWhitelist(bigNodes, ['discover:', 'fleet:agents']);
expect(result).toHaveLength(2);
expect(result.map((n) => n.id).sort()).toEqual(['discover:', 'fleet:agents'].sort());
});
});
});

View file

@ -0,0 +1,29 @@
/*
* 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 { AppDeepLinkId, NodeDefinition } from '@kbn/core-chrome-browser';
// Filter nodes from a whitelist of IDs, handling nested structures
export const filterFromWhitelist = (
nodes: Array<NodeDefinition<AppDeepLinkId>>,
idsToAttach: string[]
): Array<NodeDefinition<AppDeepLinkId>> => {
const attachedNodes: Array<NodeDefinition<AppDeepLinkId>> = [];
const stack: Array<NodeDefinition<AppDeepLinkId>> = [...nodes];
while (stack.length > 0) {
const node = stack.pop();
if (node) {
if (idsToAttach.includes(node.id as string)) {
attachedNodes.unshift(node);
} else if (node.children) {
stack.unshift(...node.children);
}
}
}
return attachedNodes;
};

View file

@ -6,15 +6,16 @@
*/
import { APP_PATH } from '@kbn/security-solution-plugin/common';
import type { SecurityProductTypes } from '../../common/config';
import type { Services } from '../common/services';
import { subscribeBreadcrumbs } from './breadcrumbs';
import { initSideNavigation } from './side_navigation';
import { enableManagementCardsLanding } from './management_cards';
export const startNavigation = (services: Services) => {
export const startNavigation = (services: Services, productTypes: SecurityProductTypes) => {
services.serverless.setProjectHome(APP_PATH);
initSideNavigation(services);
initSideNavigation(services, productTypes);
enableManagementCardsLanding(services);
subscribeBreadcrumbs(services);
};

View file

@ -9,14 +9,19 @@ import { i18n } from '@kbn/i18n';
import type { AppDeepLinkId, GroupDefinition, NodeDefinition } from '@kbn/core-chrome-browser';
import produce from 'immer';
import { map } from 'rxjs';
import type { SecurityProductTypes } from '../../common/config';
import { type Services } from '../common/services';
import { applyAiSocNavigation } from './ai_soc/ai_soc_navigation';
const PROJECT_SETTINGS_TITLE = i18n.translate(
'xpack.securitySolutionServerless.navLinks.projectSettings.title',
{ defaultMessage: 'Project Settings' }
);
export const initSideNavigation = async (services: Services) => {
export const initSideNavigation = async (
services: Services,
productTypes: SecurityProductTypes
) => {
services.securitySolution.setIsSolutionNavigationEnabled(true);
const { navigationTree$, panelContentProvider } =
@ -41,6 +46,8 @@ export const initSideNavigation = async (services: Services) => {
footerGroup.title = PROJECT_SETTINGS_TITLE;
footerGroup.children.push({ cloudLink: 'billingAndSub', openInNewTab: true });
}
applyAiSocNavigation(draft, productTypes);
})
)
);

View file

@ -75,7 +75,7 @@ export class SecuritySolutionServerlessPlugin
});
setOnboardingSettings(services);
startNavigation(services);
startNavigation(services, productTypes);
return {};
}