diff --git a/packages/kbn-optimizer/limits.yml b/packages/kbn-optimizer/limits.yml index f47843b187235..f14456925a924 100644 --- a/packages/kbn-optimizer/limits.yml +++ b/packages/kbn-optimizer/limits.yml @@ -165,7 +165,7 @@ pageLoadAssetSize: searchSynonyms: 6371 security: 79627 securitySolution: 171101 - securitySolutionEss: 38689 + securitySolutionEss: 42592 securitySolutionServerless: 52082 serverless: 7412 serverlessObservability: 19300 diff --git a/src/platform/plugins/shared/navigation/common/index.ts b/src/platform/plugins/shared/navigation/common/index.ts index b50b74f3f972f..a57db6d686c0d 100644 --- a/src/platform/plugins/shared/navigation/common/index.ts +++ b/src/platform/plugins/shared/navigation/common/index.ts @@ -7,4 +7,8 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -export { DEFAULT_ROUTES, DEFAULT_ROUTE_UI_SETTING_ID } from './constants'; +export { + DEFAULT_ROUTES, + DEFAULT_ROUTE_UI_SETTING_ID, + AGENT_BUILDER_NAV_AT_TOP_FLAG, +} from './constants'; diff --git a/src/platform/plugins/shared/navigation/public/index.ts b/src/platform/plugins/shared/navigation/public/index.ts index 053babb680f50..adea31e72b489 100644 --- a/src/platform/plugins/shared/navigation/public/index.ts +++ b/src/platform/plugins/shared/navigation/public/index.ts @@ -22,6 +22,8 @@ export type { AddSolutionNavigationArg, } from './types'; +export { AGENT_BUILDER_NAV_AT_TOP_FLAG } from '../common'; + // Export plugin after all other imports import { NavigationPublicPlugin } from './plugin'; export { NavigationPublicPlugin as Plugin }; diff --git a/x-pack/solutions/security/plugins/security_solution_ess/public/navigation/navigation_tree.ts b/x-pack/solutions/security/plugins/security_solution_ess/public/navigation/navigation_tree.ts index 28524bc9fd5aa..87904266919cb 100644 --- a/x-pack/solutions/security/plugins/security_solution_ess/public/navigation/navigation_tree.ts +++ b/x-pack/solutions/security/plugins/security_solution_ess/public/navigation/navigation_tree.ts @@ -14,6 +14,7 @@ import { import { i18nStrings, securityLink } from '@kbn/security-solution-navigation/links'; import { defaultNavigationTree } from '@kbn/security-solution-navigation/navigation_tree'; import { STACK_MANAGEMENT_NAV_ID, DATA_MANAGEMENT_NAV_ID } from '@kbn/deeplinks-management'; +import { AGENT_BUILDER_NAV_AT_TOP_FLAG } from '@kbn/navigation-plugin/public'; import { type Services } from '../common/services'; import { SOLUTION_NAME } from './translations'; @@ -21,6 +22,15 @@ export const createNavigationTree = ( services: Services, chatExperience: AIChatExperience = AIChatExperience.Classic ): NavigationTreeDefinition => { + const showAgentBuilder = chatExperience === AIChatExperience.Agent; + const agentBuilderNavAtTop = services.featureFlags.getBooleanValue( + AGENT_BUILDER_NAV_AT_TOP_FLAG, + false + ); + const agentBuilderLink = { + icon: 'productAgent', + link: 'agent_builder' as AppDeepLinkId, + }; const showAlertingV2 = Boolean(services.application.capabilities.alertingVTwo); return { body: [ @@ -31,6 +41,7 @@ export const createNavigationTree = ( renderAs: 'home', title: SOLUTION_NAME, }, + ...(showAgentBuilder && agentBuilderNavAtTop ? [agentBuilderLink] : []), { link: 'inbox' as AppDeepLinkId, icon: 'email', @@ -51,14 +62,8 @@ export const createNavigationTree = ( { link: 'workflows', }, - ...(chatExperience === AIChatExperience.Agent - ? [ - { - icon: 'productAgent', - link: 'agent_builder' as AppDeepLinkId, - }, - ] - : []), + // TODO: remove this item when agentBuilderNavAtTop is enabled by default and the Agent Builder link is always at the top of the nav + ...(showAgentBuilder && !agentBuilderNavAtTop ? [agentBuilderLink] : []), { id: SecurityPageName.attackDiscovery, icon: 'bolt', diff --git a/x-pack/solutions/security/plugins/security_solution_serverless/kibana.jsonc b/x-pack/solutions/security/plugins/security_solution_serverless/kibana.jsonc index a3ef47321a2ee..f27eebe746d39 100644 --- a/x-pack/solutions/security/plugins/security_solution_serverless/kibana.jsonc +++ b/x-pack/solutions/security/plugins/security_solution_serverless/kibana.jsonc @@ -1,9 +1,7 @@ { "type": "plugin", "id": "@kbn/security-solution-serverless", - "owner": [ - "@elastic/security-solution" - ], + "owner": ["@elastic/security-solution"], "group": "security", "visibility": "private", "description": "Serverless customizations for security.", @@ -11,10 +9,7 @@ "id": "securitySolutionServerless", "browser": true, "server": true, - "configPath": [ - "xpack", - "securitySolutionServerless" - ], + "configPath": ["xpack", "securitySolutionServerless"], "requiredPlugins": [ "kibanaReact", "management", @@ -25,13 +20,9 @@ "cloud", "fleet", "actions", - "discover" + "discover", + "navigation" ], - "optionalPlugins": [ - "securitySolutionEss", - "automaticImport", - "usageApi", - "workflowsManagement" - ] + "optionalPlugins": ["securitySolutionEss", "automaticImport", "usageApi", "workflowsManagement"] } -} \ No newline at end of file +} diff --git a/x-pack/solutions/security/plugins/security_solution_serverless/moon.yml b/x-pack/solutions/security/plugins/security_solution_serverless/moon.yml index 54e0b57292bb1..f991f0bcb40d9 100644 --- a/x-pack/solutions/security/plugins/security_solution_serverless/moon.yml +++ b/x-pack/solutions/security/plugins/security_solution_serverless/moon.yml @@ -56,6 +56,7 @@ dependsOn: - '@kbn/workflows' - '@kbn/usage-api-plugin' - '@kbn/workflows-management-plugin' + - '@kbn/navigation-plugin' tags: - plugin - prod diff --git a/x-pack/solutions/security/plugins/security_solution_serverless/public/navigation/ai_navigation/ai_navigation_tree.test.ts b/x-pack/solutions/security/plugins/security_solution_serverless/public/navigation/ai_navigation/ai_navigation_tree.test.ts index 6f645186ffd7c..b572a7369e935 100644 --- a/x-pack/solutions/security/plugins/security_solution_serverless/public/navigation/ai_navigation/ai_navigation_tree.test.ts +++ b/x-pack/solutions/security/plugins/security_solution_serverless/public/navigation/ai_navigation/ai_navigation_tree.test.ts @@ -9,8 +9,8 @@ import { AIChatExperience } from '@kbn/ai-assistant-common'; import { createAiNavigationTree } from './ai_navigation_tree'; describe('createAiNavigationTree', () => { - it('returns the Workflows link between Agents and Value report when enabled', () => { - const navigationTree = createAiNavigationTree(AIChatExperience.Agent, true); + it('returns the Workflows link between Agents and Value report when `workflows` is enabled', () => { + const navigationTree = createAiNavigationTree(AIChatExperience.Agent, true, false, false); const primaryNavSection = navigationTree.body[4]; const children = 'children' in primaryNavSection ? primaryNavSection.children : []; @@ -25,8 +25,8 @@ describe('createAiNavigationTree', () => { expect(workflowsIndex).toBe((agentsIndex ?? 0) + 1); }); - it('does not include the Workflows link when disabled', () => { - const navigationTree = createAiNavigationTree(AIChatExperience.Agent, false); + it('does not include the Workflows link when `workflows` is disabled', () => { + const navigationTree = createAiNavigationTree(AIChatExperience.Agent, false, false, false); const primaryNavSection = navigationTree.body[4]; const children = 'children' in primaryNavSection ? primaryNavSection.children : []; @@ -37,4 +37,25 @@ describe('createAiNavigationTree', () => { expect(workflowsIndex).toBe(-1); }); + + it('places the Agent Builder link after `home` when `agentBuilderNavAtTop` is enabled', () => { + const navigationTree = createAiNavigationTree(AIChatExperience.Agent, true, false, true); + + const secondNavItem = navigationTree.body[1]; + + expect('link' in secondNavItem && secondNavItem.link).toBe('agent_builder'); + }); + + it('places the Agent Builder later in the nav when `agentBuilderNavAtTop` is disabled', () => { + const navigationTree = createAiNavigationTree(AIChatExperience.Agent, true, false, false); + + const primaryNavSection = navigationTree.body[4]; + const children = 'children' in primaryNavSection ? primaryNavSection.children : []; + + const agentBuilderIndex = children?.findIndex( + (item) => 'link' in item && item.link === 'agent_builder' + ); + + expect(agentBuilderIndex).toBeGreaterThan(0); + }); }); diff --git a/x-pack/solutions/security/plugins/security_solution_serverless/public/navigation/ai_navigation/ai_navigation_tree.ts b/x-pack/solutions/security/plugins/security_solution_serverless/public/navigation/ai_navigation/ai_navigation_tree.ts index b569d68b88ed8..b4a312e633197 100644 --- a/x-pack/solutions/security/plugins/security_solution_serverless/public/navigation/ai_navigation/ai_navigation_tree.ts +++ b/x-pack/solutions/security/plugins/security_solution_serverless/public/navigation/ai_navigation/ai_navigation_tree.ts @@ -23,7 +23,8 @@ const SOLUTION_NAME = i18n.translate( export const createAiNavigationTree = ( chatExperience: AIChatExperience = AIChatExperience.Classic, workflowsUiEnabled: boolean = false, - showAlertingV2: boolean = false + showAlertingV2: boolean = false, + showAgentBuilderNavAtTop: boolean = false ): NavigationTreeDefinition => ({ body: [ { @@ -33,6 +34,14 @@ export const createAiNavigationTree = ( icon: AiNavigationIcon, renderAs: 'home', }, + ...(chatExperience === AIChatExperience.Agent && showAgentBuilderNavAtTop + ? [ + { + icon: 'productAgent', + link: 'agent_builder' as AppDeepLinkId, + }, + ] + : []), { id: SecurityPageName.alertSummary, link: securityLink(SecurityPageName.alertSummary), @@ -83,7 +92,8 @@ export const createAiNavigationTree = ( link: 'discover' as AppDeepLinkId, icon: 'productDiscover', }, - ...(chatExperience === AIChatExperience.Agent + // TODO: remove this item when agentBuilderNavAtTop is enabled by default and the Agent Builder link is always at the top of the nav + ...(chatExperience === AIChatExperience.Agent && !showAgentBuilderNavAtTop ? [ { icon: 'productAgent', diff --git a/x-pack/solutions/security/plugins/security_solution_serverless/public/navigation/navigation.test.ts b/x-pack/solutions/security/plugins/security_solution_serverless/public/navigation/navigation.test.ts index 1594474b0a08b..21eb349fc2978 100644 --- a/x-pack/solutions/security/plugins/security_solution_serverless/public/navigation/navigation.test.ts +++ b/x-pack/solutions/security/plugins/security_solution_serverless/public/navigation/navigation.test.ts @@ -73,6 +73,7 @@ describe('Security Side Nav', () => { expect(mockedCreateAiNavigationTree).toHaveBeenCalledWith( AIChatExperience.Classic, false, + false, false ); expect(mockedCreateNavigationTree).not.toHaveBeenCalled(); @@ -119,6 +120,7 @@ describe('Security Side Nav', () => { expect(mockedCreateAiNavigationTree).toHaveBeenCalledWith( AIChatExperience.Classic, true, + false, false ); }); diff --git a/x-pack/solutions/security/plugins/security_solution_serverless/public/navigation/navigation.ts b/x-pack/solutions/security/plugins/security_solution_serverless/public/navigation/navigation.ts index b75cb72b500fb..1c92482ea01a9 100644 --- a/x-pack/solutions/security/plugins/security_solution_serverless/public/navigation/navigation.ts +++ b/x-pack/solutions/security/plugins/security_solution_serverless/public/navigation/navigation.ts @@ -10,6 +10,7 @@ import { firstValueFrom } from 'rxjs'; import { AI_CHAT_EXPERIENCE_TYPE } from '@kbn/management-settings-ids'; import type { AIChatExperience } from '@kbn/ai-assistant-common'; import { WORKFLOWS_UI_SETTING_ID } from '@kbn/workflows/common/constants'; +import { AGENT_BUILDER_NAV_AT_TOP_FLAG } from '@kbn/navigation-plugin/public'; import { ProductLine } from '../../common/product'; import type { SecurityProductTypes } from '../../common/config'; import { type Services } from '../common/services'; @@ -24,6 +25,11 @@ export const registerSolutionNavigation = async ( (productType) => productType.product_line === ProductLine.aiSoc ); + const agentBuilderNavAtTop = services.featureFlags.getBooleanValue( + AGENT_BUILDER_NAV_AT_TOP_FLAG, + false + ); + // Do not pass a defaultOverride: when userValue is unset, get() must use the registered // default (e.g. Agent for security spaces from aiAssistantManagementSelection), not Classic. const chatExperience$ = services.settings.client.get$(AI_CHAT_EXPERIENCE_TYPE); @@ -39,7 +45,12 @@ export const registerSolutionNavigation = async ( const showAlertingV2 = Boolean(services.application.capabilities.alertingVTwo); const navigationTree = shouldUseAINavigation - ? createAiNavigationTree(initialChatExperience, workflowsUiEnabled, showAlertingV2) + ? createAiNavigationTree( + initialChatExperience, + workflowsUiEnabled, + showAlertingV2, + agentBuilderNavAtTop + ) : await createNavigationTree(services, initialChatExperience); services.securitySolution.setSolutionNavigationTree(navigationTree); diff --git a/x-pack/solutions/security/plugins/security_solution_serverless/public/navigation/navigation_tree.ts b/x-pack/solutions/security/plugins/security_solution_serverless/public/navigation/navigation_tree.ts index ebfa427f1cfa6..7feb16835c76a 100644 --- a/x-pack/solutions/security/plugins/security_solution_serverless/public/navigation/navigation_tree.ts +++ b/x-pack/solutions/security/plugins/security_solution_serverless/public/navigation/navigation_tree.ts @@ -15,6 +15,7 @@ import { } from '@kbn/security-solution-navigation'; import { i18nStrings, securityLink } from '@kbn/security-solution-navigation/links'; import { defaultNavigationTree } from '@kbn/security-solution-navigation/navigation_tree'; +import { AGENT_BUILDER_NAV_AT_TOP_FLAG } from '@kbn/navigation-plugin/public'; import { type Services } from '../common/services'; import { createManagementFooterItemsTree } from './management_footer_items'; @@ -29,6 +30,17 @@ export const createNavigationTree = async ( chatExperience: AIChatExperience = AIChatExperience.Classic ): Promise => { const showAlertingV2 = Boolean(services.application.capabilities.alertingVTwo); + + const showAgentBuilder = chatExperience === AIChatExperience.Agent; + const agentBuilderNavAtTop = services.featureFlags.getBooleanValue( + AGENT_BUILDER_NAV_AT_TOP_FLAG, + false + ); + const agentBuilderLink = { + icon: 'productAgent', + link: 'agent_builder' as AppDeepLinkId, + }; + return { body: [ { @@ -38,6 +50,7 @@ export const createNavigationTree = async ( icon: 'logoSecurity', renderAs: 'home', }, + ...(showAgentBuilder && agentBuilderNavAtTop ? [agentBuilderLink] : []), { link: 'inbox' as AppDeepLinkId, icon: 'email', @@ -58,14 +71,8 @@ export const createNavigationTree = async ( { link: 'workflows', }, - ...(chatExperience === AIChatExperience.Agent - ? [ - { - icon: 'productAgent', - link: 'agent_builder' as AppDeepLinkId, - }, - ] - : []), + // TODO: remove this item when agentBuilderNavAtTop is enabled by default and the Agent Builder link is always at the top of the nav + ...(showAgentBuilder && !agentBuilderNavAtTop ? [agentBuilderLink] : []), { id: SecurityPageName.attackDiscovery, icon: 'bolt', diff --git a/x-pack/solutions/security/plugins/security_solution_serverless/tsconfig.json b/x-pack/solutions/security/plugins/security_solution_serverless/tsconfig.json index 37bb3f3e050f1..b5edf184622e2 100644 --- a/x-pack/solutions/security/plugins/security_solution_serverless/tsconfig.json +++ b/x-pack/solutions/security/plugins/security_solution_serverless/tsconfig.json @@ -52,5 +52,6 @@ "@kbn/workflows", "@kbn/usage-api-plugin", "@kbn/workflows-management-plugin", + "@kbn/navigation-plugin", ] }