diff --git a/config/serverless.security.yml b/config/serverless.security.yml
index 3e3a1eb782a33..a607ace5d629d 100644
--- a/config/serverless.security.yml
+++ b/config/serverless.security.yml
@@ -211,3 +211,6 @@ xpack.alerting.rules.run.ruleTypeOverrides:
# These features are disabled in Serverless until fully tested
xpack.securitySolution.enableExperimental:
- privilegedUserMonitoringDisabled
+
+# AI Assistant config
+aiAssistantManagementSelection.preferredAIAssistantType: 'security'
\ No newline at end of file
diff --git a/src/platform/packages/shared/kbn-management/settings/setting_ids/index.ts b/src/platform/packages/shared/kbn-management/settings/setting_ids/index.ts
index 4769049a09c8f..0b5ff35a3422c 100644
--- a/src/platform/packages/shared/kbn-management/settings/setting_ids/index.ts
+++ b/src/platform/packages/shared/kbn-management/settings/setting_ids/index.ts
@@ -169,6 +169,7 @@ export const SECURITY_SOLUTION_ENABLE_ASSET_INVENTORY_SETTING =
'securitySolution:enableAssetInventory' as const;
export const SECURITY_SOLUTION_ENABLE_CLOUD_CONNECTOR_SETTING =
'securitySolution:enableCloudConnector' as const;
+export const AI_ASSISTANT_PREFERRED_AI_ASSISTANT_TYPE = 'aiAssistant:preferredAIAssistantType';
// Timelion settings
export const TIMELION_ES_DEFAULT_INDEX_ID = 'timelion:es.default_index';
diff --git a/src/platform/packages/shared/serverless/settings/observability_project/index.ts b/src/platform/packages/shared/serverless/settings/observability_project/index.ts
index e12d28de1c81b..297aeb3d87a53 100644
--- a/src/platform/packages/shared/serverless/settings/observability_project/index.ts
+++ b/src/platform/packages/shared/serverless/settings/observability_project/index.ts
@@ -29,4 +29,5 @@ export const OBSERVABILITY_PROJECT_SETTINGS = [
export const OBSERVABILITY_AI_ASSISTANT_PROJECT_SETTINGS = [
settings.OBSERVABILITY_AI_ASSISTANT_SIMULATED_FUNCTION_CALLING,
settings.OBSERVABILITY_AI_ASSISTANT_SEARCH_CONNECTOR_INDEX_PATTERN,
+ settings.AI_ASSISTANT_PREFERRED_AI_ASSISTANT_TYPE,
];
diff --git a/src/platform/packages/shared/serverless/settings/security_project/index.ts b/src/platform/packages/shared/serverless/settings/security_project/index.ts
index 6dacb7940a3aa..2ccbd97dac10e 100644
--- a/src/platform/packages/shared/serverless/settings/security_project/index.ts
+++ b/src/platform/packages/shared/serverless/settings/security_project/index.ts
@@ -26,4 +26,5 @@ export const SECURITY_PROJECT_SETTINGS = [
settings.SECURITY_SOLUTION_ENABLE_GRAPH_VISUALIZATION_SETTING,
settings.SECURITY_SOLUTION_ENABLE_ASSET_INVENTORY_SETTING,
settings.SECURITY_SOLUTION_ENABLE_CLOUD_CONNECTOR_SETTING,
+ settings.AI_ASSISTANT_PREFERRED_AI_ASSISTANT_TYPE,
];
diff --git a/src/platform/plugins/shared/ai_assistant_management/selection/common/ai_assistant_type.ts b/src/platform/plugins/shared/ai_assistant_management/selection/common/ai_assistant_type.ts
index 7f50f99277a75..d0947535299bb 100644
--- a/src/platform/plugins/shared/ai_assistant_management/selection/common/ai_assistant_type.ts
+++ b/src/platform/plugins/shared/ai_assistant_management/selection/common/ai_assistant_type.ts
@@ -9,6 +9,7 @@
export enum AIAssistantType {
Observability = 'observability',
+ Security = 'security',
Default = 'default',
Never = 'never',
}
diff --git a/src/platform/plugins/shared/ai_assistant_management/selection/kibana.jsonc b/src/platform/plugins/shared/ai_assistant_management/selection/kibana.jsonc
index d1509c7d34262..5508eb3d5e1c3 100644
--- a/src/platform/plugins/shared/ai_assistant_management/selection/kibana.jsonc
+++ b/src/platform/plugins/shared/ai_assistant_management/selection/kibana.jsonc
@@ -13,12 +13,13 @@
"aiAssistantManagementSelection"
],
"requiredPlugins": [
- "management"
+ "management",
],
"optionalPlugins": [
"home",
"serverless",
- "features"
+ "features",
+ "cloud"
],
"requiredBundles": [
"kibanaReact"
diff --git a/src/platform/plugins/shared/ai_assistant_management/selection/public/plugin.test.ts b/src/platform/plugins/shared/ai_assistant_management/selection/public/plugin.test.ts
new file mode 100644
index 0000000000000..c288feb8cbde0
--- /dev/null
+++ b/src/platform/plugins/shared/ai_assistant_management/selection/public/plugin.test.ts
@@ -0,0 +1,46 @@
+/*
+ * 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", the "GNU Affero General Public License v3.0 only", and the "Server Side
+ * Public License v 1"; you may not use this file except in compliance with, at
+ * your election, the "Elastic License 2.0", the "GNU Affero General Public
+ * License v3.0 only", or the "Server Side Public License, v 1".
+ */
+
+import { CoreStart, PluginInitializerContext } from '@kbn/core/public';
+import { AIAssistantManagementPlugin } from './plugin';
+import { AIAssistantType } from '../common/ai_assistant_type';
+import { PREFERRED_AI_ASSISTANT_TYPE_SETTING_KEY } from '../common/ui_setting_keys';
+
+describe('AI Assistant Management Selection Plugin', () => {
+ it('uses the correct setting key to get the correct value from uiSettings', async () => {
+ const plugin = new AIAssistantManagementPlugin({
+ config: {
+ get: jest.fn(),
+ },
+ env: { packageInfo: { buildFlavor: 'traditional', branch: 'main' } },
+ } as unknown as PluginInitializerContext);
+
+ const coreStart = {
+ uiSettings: {
+ get: jest.fn((key: string) => {
+ if (key === PREFERRED_AI_ASSISTANT_TYPE_SETTING_KEY) {
+ return AIAssistantType.Default;
+ }
+ }),
+ },
+ } as unknown as CoreStart;
+
+ const result = plugin.start(coreStart);
+
+ const collected: any[] = [];
+ const subscription = result.aiAssistantType$.subscribe((value) => {
+ collected.push(value);
+ });
+ subscription.unsubscribe();
+
+ const allCalls = (coreStart.uiSettings.get as jest.Mock).mock.calls;
+ expect(allCalls).toEqual([['aiAssistant:preferredAIAssistantType']]);
+ expect(collected).toEqual([AIAssistantType.Default]);
+ });
+});
diff --git a/src/platform/plugins/shared/ai_assistant_management/selection/server/config.ts b/src/platform/plugins/shared/ai_assistant_management/selection/server/config.ts
index c8aaee1a80626..9e6b64c2492c2 100644
--- a/src/platform/plugins/shared/ai_assistant_management/selection/server/config.ts
+++ b/src/platform/plugins/shared/ai_assistant_management/selection/server/config.ts
@@ -18,6 +18,7 @@ const configSchema = schema.object({
schema.literal(AIAssistantType.Default),
schema.literal(AIAssistantType.Never),
schema.literal(AIAssistantType.Observability),
+ schema.literal(AIAssistantType.Security),
],
{ defaultValue: AIAssistantType.Default }
),
diff --git a/src/platform/plugins/shared/ai_assistant_management/selection/server/plugin.test.ts b/src/platform/plugins/shared/ai_assistant_management/selection/server/plugin.test.ts
new file mode 100644
index 0000000000000..5d00c575cb7e9
--- /dev/null
+++ b/src/platform/plugins/shared/ai_assistant_management/selection/server/plugin.test.ts
@@ -0,0 +1,189 @@
+/*
+ * 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", the "GNU Affero General Public License v3.0 only", and the "Server Side
+ * Public License v 1"; you may not use this file except in compliance with, at
+ * your election, the "Elastic License 2.0", the "GNU Affero General Public
+ * License v3.0 only", or the "Server Side Public License, v 1".
+ */
+
+import { PluginInitializerContext, CoreSetup } from '@kbn/core/server';
+import type { AIAssistantManagementSelectionPluginServerDependenciesSetup } from './types';
+import { AIAssistantType } from '../common/ai_assistant_type';
+import { PREFERRED_AI_ASSISTANT_TYPE_SETTING_KEY } from '../common/ui_setting_keys';
+import { classicSetting } from './src/settings/classic_setting';
+import { observabilitySolutionSetting } from './src/settings/observability_setting';
+import { securitySolutionSetting } from './src/settings/security_setting';
+import { AIAssistantManagementSelectionPlugin } from './plugin';
+
+describe('plugin', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+ describe('stateless', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+ const initializerContext = {
+ env: {
+ packageInfo: {
+ buildFlavor: 'serverless',
+ },
+ },
+ config: {
+ get: jest.fn(),
+ },
+ } as unknown as PluginInitializerContext;
+
+ const coreSetup = {
+ uiSettings: {
+ register: jest.fn(),
+ },
+ capabilities: {
+ registerProvider: jest.fn(),
+ },
+ } as unknown as CoreSetup;
+
+ const setupDeps = {
+ management: {
+ sections: {
+ getSection: jest.fn(),
+ },
+ },
+ serverless: {
+ uiSettings: {
+ register: jest.fn(),
+ },
+ },
+ };
+
+ it('registers correct uiSettings for serverless oblt', () => {
+ (initializerContext.config.get as jest.Mock).mockReturnValue({
+ preferredAIAssistantType: AIAssistantType.Observability,
+ });
+ const aiAssistantManagementSelectionPlugin = new AIAssistantManagementSelectionPlugin(
+ initializerContext
+ );
+ aiAssistantManagementSelectionPlugin.setup(coreSetup, {
+ ...setupDeps,
+ cloud: {
+ serverless: {
+ projectType: 'observability',
+ },
+ },
+ } as unknown as AIAssistantManagementSelectionPluginServerDependenciesSetup);
+
+ expect(coreSetup.uiSettings.register).toHaveBeenCalledTimes(1);
+
+ expect(coreSetup.uiSettings.register).toHaveBeenCalledWith({
+ [PREFERRED_AI_ASSISTANT_TYPE_SETTING_KEY]: {
+ ...observabilitySolutionSetting,
+ value: AIAssistantType.Observability,
+ },
+ });
+ });
+
+ it('registers correct uiSettings for serverless security', () => {
+ (initializerContext.config.get as jest.Mock).mockReturnValue({
+ preferredAIAssistantType: AIAssistantType.Security,
+ });
+ const aiAssistantManagementSelectionPlugin = new AIAssistantManagementSelectionPlugin(
+ initializerContext
+ );
+ aiAssistantManagementSelectionPlugin.setup(coreSetup, {
+ ...setupDeps,
+ cloud: {
+ serverless: {
+ projectType: 'security',
+ },
+ },
+ } as unknown as AIAssistantManagementSelectionPluginServerDependenciesSetup);
+
+ expect(coreSetup.uiSettings.register).toHaveBeenCalledTimes(1);
+
+ expect(coreSetup.uiSettings.register).toHaveBeenCalledWith({
+ [PREFERRED_AI_ASSISTANT_TYPE_SETTING_KEY]: {
+ ...securitySolutionSetting,
+ value: AIAssistantType.Security,
+ },
+ });
+ });
+
+ it('registers correct uiSettings for serverless search', () => {
+ (initializerContext.config.get as jest.Mock).mockReturnValue({
+ preferredAIAssistantType: undefined,
+ });
+ const aiAssistantManagementSelectionPlugin = new AIAssistantManagementSelectionPlugin(
+ initializerContext
+ );
+ aiAssistantManagementSelectionPlugin.setup(coreSetup, {
+ ...setupDeps,
+ cloud: {
+ serverless: {
+ projectType: 'search',
+ },
+ },
+ } as unknown as AIAssistantManagementSelectionPluginServerDependenciesSetup);
+
+ expect(coreSetup.uiSettings.register).toHaveBeenCalledTimes(1);
+ expect(coreSetup.uiSettings.register).toHaveBeenCalledWith({
+ [PREFERRED_AI_ASSISTANT_TYPE_SETTING_KEY]: {
+ ...classicSetting,
+ value: AIAssistantType.Default,
+ },
+ });
+ });
+ });
+
+ describe('stateful', () => {
+ it('uses the correct setting key to get the correct value from uiSettings', async () => {
+ const initializerContext = {
+ env: {
+ packageInfo: {
+ buildFlavor: 'classic',
+ },
+ },
+ config: {
+ get: jest.fn().mockReturnValue({
+ preferredAIAssistantType: AIAssistantType.Observability,
+ }),
+ },
+ } as unknown as PluginInitializerContext;
+ const aiAssistantManagementSelectionPlugin = new AIAssistantManagementSelectionPlugin(
+ initializerContext
+ );
+
+ const coreSetup = {
+ uiSettings: {
+ register: jest.fn(),
+ },
+ capabilities: {
+ registerProvider: jest.fn(),
+ },
+ } as unknown as CoreSetup;
+
+ const setupDeps = {
+ management: {
+ sections: {
+ getSection: jest.fn(),
+ },
+ },
+ serverless: {
+ uiSettings: {
+ register: jest.fn(),
+ },
+ },
+ } as unknown as AIAssistantManagementSelectionPluginServerDependenciesSetup;
+
+ aiAssistantManagementSelectionPlugin.setup(coreSetup, setupDeps);
+
+ expect(coreSetup.uiSettings.register).toHaveBeenCalledTimes(1);
+ expect(coreSetup.uiSettings.register).toHaveBeenCalledWith({
+ [PREFERRED_AI_ASSISTANT_TYPE_SETTING_KEY]: {
+ ...classicSetting,
+ value: AIAssistantType.Observability,
+ },
+ });
+ });
+ });
+});
diff --git a/src/platform/plugins/shared/ai_assistant_management/selection/server/plugin.ts b/src/platform/plugins/shared/ai_assistant_management/selection/server/plugin.ts
index 67a4e000ed78d..742584ceebafe 100644
--- a/src/platform/plugins/shared/ai_assistant_management/selection/server/plugin.ts
+++ b/src/platform/plugins/shared/ai_assistant_management/selection/server/plugin.ts
@@ -16,7 +16,6 @@ import {
Plugin,
DEFAULT_APP_CATEGORIES,
} from '@kbn/core/server';
-import { schema } from '@kbn/config-schema';
import { KibanaFeatureScope } from '@kbn/features-plugin/common';
import type { AIAssistantManagementSelectionConfig } from './config';
import type {
@@ -25,8 +24,11 @@ import type {
AIAssistantManagementSelectionPluginServerSetup,
AIAssistantManagementSelectionPluginServerStart,
} from './types';
-import { AIAssistantType } from '../common/ai_assistant_type';
import { PREFERRED_AI_ASSISTANT_TYPE_SETTING_KEY } from '../common/ui_setting_keys';
+import { classicSetting } from './src/settings/classic_setting';
+import { observabilitySolutionSetting } from './src/settings/observability_setting';
+import { securitySolutionSetting } from './src/settings/security_setting';
+import { AIAssistantType } from '../common/ai_assistant_type';
export class AIAssistantManagementSelectionPlugin
implements
@@ -47,52 +49,6 @@ export class AIAssistantManagementSelectionPlugin
core: CoreSetup,
plugins: AIAssistantManagementSelectionPluginServerDependenciesSetup
) {
- core.uiSettings.register({
- [PREFERRED_AI_ASSISTANT_TYPE_SETTING_KEY]: {
- name: i18n.translate('aiAssistantManagementSelection.preferredAIAssistantTypeSettingName', {
- defaultMessage: 'AI Assistant for Observability and Search visibility',
- }),
- category: [DEFAULT_APP_CATEGORIES.observability.id],
- value: this.config.preferredAIAssistantType,
- description: i18n.translate(
- 'aiAssistantManagementSelection.preferredAIAssistantTypeSettingDescription',
- {
- defaultMessage:
- '[technical preview] Whether to show the AI Assistant menu item in Observability and Search, everywhere, or nowhere.',
- values: {
- em: (chunks) => `${chunks}`,
- },
- }
- ),
- schema: schema.oneOf(
- [
- schema.literal(AIAssistantType.Default),
- schema.literal(AIAssistantType.Observability),
- schema.literal(AIAssistantType.Never),
- ],
- { defaultValue: this.config.preferredAIAssistantType }
- ),
- options: [AIAssistantType.Default, AIAssistantType.Observability, AIAssistantType.Never],
- type: 'select',
- optionLabels: {
- [AIAssistantType.Default]: i18n.translate(
- 'aiAssistantManagementSelection.preferredAIAssistantTypeSettingValueDefault',
- { defaultMessage: 'Observability and Search only (default)' }
- ),
- [AIAssistantType.Observability]: i18n.translate(
- 'aiAssistantManagementSelection.preferredAIAssistantTypeSettingValueObservability',
- { defaultMessage: 'Everywhere' }
- ),
- [AIAssistantType.Never]: i18n.translate(
- 'aiAssistantManagementSelection.preferredAIAssistantTypeSettingValueNever',
- { defaultMessage: 'Nowhere' }
- ),
- },
- requiresPageReload: true,
- solution: 'oblt',
- },
- });
-
core.capabilities.registerProvider(() => {
return {
management: {
@@ -154,9 +110,48 @@ export class AIAssistantManagementSelectionPlugin
},
});
+ this.registerUiSettings(core, plugins);
+
return {};
}
+ private registerUiSettings(
+ core: CoreSetup,
+ plugins: AIAssistantManagementSelectionPluginServerDependenciesSetup
+ ) {
+ const { cloud } = plugins;
+ const serverlessProjectType = cloud?.serverless.projectType;
+
+ switch (serverlessProjectType) {
+ case 'observability':
+ core.uiSettings.register({
+ [PREFERRED_AI_ASSISTANT_TYPE_SETTING_KEY]: {
+ ...observabilitySolutionSetting,
+ value: this.config.preferredAIAssistantType,
+ },
+ });
+ return;
+ case 'security':
+ core.uiSettings.register({
+ [PREFERRED_AI_ASSISTANT_TYPE_SETTING_KEY]: {
+ ...securitySolutionSetting,
+ value: this.config.preferredAIAssistantType,
+ },
+ });
+ return;
+ // TODO: Add another case for search with the correct copy of the setting.
+ // see: https://github.com/elastic/kibana/issues/227695
+ default:
+ // This case is hit when in stateful Kibana
+ return core.uiSettings.register({
+ [PREFERRED_AI_ASSISTANT_TYPE_SETTING_KEY]: {
+ ...classicSetting,
+ value: this.config.preferredAIAssistantType ?? AIAssistantType.Default,
+ },
+ });
+ }
+ }
+
public start(core: CoreStart) {
return {};
}
diff --git a/src/platform/plugins/shared/ai_assistant_management/selection/server/src/settings/classic_setting.ts b/src/platform/plugins/shared/ai_assistant_management/selection/server/src/settings/classic_setting.ts
new file mode 100644
index 0000000000000..bf5f9105102b0
--- /dev/null
+++ b/src/platform/plugins/shared/ai_assistant_management/selection/server/src/settings/classic_setting.ts
@@ -0,0 +1,56 @@
+/*
+ * 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", the "GNU Affero General Public License v3.0 only", and the "Server Side
+ * Public License v 1"; you may not use this file except in compliance with, at
+ * your election, the "Elastic License 2.0", the "GNU Affero General Public
+ * License v3.0 only", or the "Server Side Public License, v 1".
+ */
+
+import { i18n } from '@kbn/i18n';
+
+import { schema } from '@kbn/config-schema';
+import { UiSettingsParams } from '@kbn/core-ui-settings-common';
+import { AIAssistantType } from '../../../common/ai_assistant_type';
+import {
+ ONLY_IN_THEIR_SOLUTIONS,
+ OBSERVABILITY_IN_OTHER_APPS,
+ SECURITY_IN_OTHER_APPS,
+ HIDE_ALL_ASSISTANTS,
+ TITLE,
+} from './translations';
+
+// Define the classicSetting with proper typing
+export const classicSetting: Omit, 'value'> = {
+ name: TITLE,
+ description: i18n.translate(
+ 'aiAssistantManagementSelection.preferredAIAssistantTypeSettingDescription',
+ {
+ defaultMessage:
+ 'Choose where and which AI Assistants are available. You can limit the AI Assistants to their own solutions, show either the Observability and Search AI Assistants or the Security AI Assistant in other Kibana apps, or hide AI Assistants entirely.',
+ }
+ ),
+ schema: schema.oneOf(
+ [
+ schema.literal(AIAssistantType.Default),
+ schema.literal(AIAssistantType.Observability),
+ schema.literal(AIAssistantType.Security),
+ schema.literal(AIAssistantType.Never),
+ ],
+ { defaultValue: AIAssistantType.Default }
+ ),
+ options: [
+ AIAssistantType.Default,
+ AIAssistantType.Observability,
+ AIAssistantType.Security,
+ AIAssistantType.Never,
+ ],
+ type: 'select' as const,
+ optionLabels: {
+ [AIAssistantType.Default]: ONLY_IN_THEIR_SOLUTIONS,
+ [AIAssistantType.Observability]: OBSERVABILITY_IN_OTHER_APPS,
+ [AIAssistantType.Security]: SECURITY_IN_OTHER_APPS,
+ [AIAssistantType.Never]: HIDE_ALL_ASSISTANTS,
+ },
+ requiresPageReload: true,
+};
diff --git a/src/platform/plugins/shared/ai_assistant_management/selection/server/src/settings/observability_setting.ts b/src/platform/plugins/shared/ai_assistant_management/selection/server/src/settings/observability_setting.ts
new file mode 100644
index 0000000000000..caecf5d3099a5
--- /dev/null
+++ b/src/platform/plugins/shared/ai_assistant_management/selection/server/src/settings/observability_setting.ts
@@ -0,0 +1,42 @@
+/*
+ * 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", the "GNU Affero General Public License v3.0 only", and the "Server Side
+ * Public License v 1"; you may not use this file except in compliance with, at
+ * your election, the "Elastic License 2.0", the "GNU Affero General Public
+ * License v3.0 only", or the "Server Side Public License, v 1".
+ */
+
+import { i18n } from '@kbn/i18n';
+
+import { schema } from '@kbn/config-schema';
+import { UiSettingsParams } from '@kbn/core-ui-settings-common';
+import { AIAssistantType } from '../../../common/ai_assistant_type';
+import { SHOW_OBSERVABILITY, HIDE_ASSISTANT, TITLE } from './translations';
+
+// Define the classicSetting with proper typing
+export const observabilitySolutionSetting: Omit<
+ UiSettingsParams,
+ 'value'
+> = {
+ name: TITLE,
+ description: i18n.translate(
+ 'aiAssistantManagementSelection.observabilitySolutionSetting.preferredAIAssistantTypeSettingDescription',
+ {
+ defaultMessage:
+ 'Choose if the Observability AI Assistant is available. Show the Observability AI Assistant, or hide the Assistant entirely.',
+ }
+ ),
+ schema: schema.oneOf(
+ [schema.literal(AIAssistantType.Observability), schema.literal(AIAssistantType.Never)],
+ { defaultValue: AIAssistantType.Observability }
+ ),
+ options: [AIAssistantType.Observability, AIAssistantType.Never],
+ type: 'select' as const,
+ optionLabels: {
+ [AIAssistantType.Observability]: SHOW_OBSERVABILITY,
+ [AIAssistantType.Never]: HIDE_ASSISTANT,
+ },
+ requiresPageReload: true,
+ solution: 'oblt',
+};
diff --git a/src/platform/plugins/shared/ai_assistant_management/selection/server/src/settings/security_setting.ts b/src/platform/plugins/shared/ai_assistant_management/selection/server/src/settings/security_setting.ts
new file mode 100644
index 0000000000000..03b4553b02860
--- /dev/null
+++ b/src/platform/plugins/shared/ai_assistant_management/selection/server/src/settings/security_setting.ts
@@ -0,0 +1,42 @@
+/*
+ * 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", the "GNU Affero General Public License v3.0 only", and the "Server Side
+ * Public License v 1"; you may not use this file except in compliance with, at
+ * your election, the "Elastic License 2.0", the "GNU Affero General Public
+ * License v3.0 only", or the "Server Side Public License, v 1".
+ */
+
+import { i18n } from '@kbn/i18n';
+
+import { schema } from '@kbn/config-schema';
+import { UiSettingsParams } from '@kbn/core-ui-settings-common';
+import { AIAssistantType } from '../../../common/ai_assistant_type';
+import { SHOW_SECURITY, HIDE_ASSISTANT, TITLE } from './translations';
+
+// Define the securitySolutionSetting with proper typing
+export const securitySolutionSetting: Omit<
+ UiSettingsParams,
+ 'value'
+> = {
+ name: TITLE,
+ description: i18n.translate(
+ 'aiAssistantManagementSelection.securitySolutionSetting.preferredAIAssistantTypeSettingDescription',
+ {
+ defaultMessage:
+ 'Choose if the Security AI Assistant is available. Show the Security AI Assistant, or hide the Assistant entirely.',
+ }
+ ),
+ schema: schema.oneOf(
+ [schema.literal(AIAssistantType.Security), schema.literal(AIAssistantType.Never)],
+ { defaultValue: AIAssistantType.Security }
+ ),
+ options: [AIAssistantType.Security, AIAssistantType.Never],
+ type: 'select' as const,
+ optionLabels: {
+ [AIAssistantType.Security]: SHOW_SECURITY,
+ [AIAssistantType.Never]: HIDE_ASSISTANT,
+ },
+ requiresPageReload: true,
+ solution: 'security',
+};
diff --git a/src/platform/plugins/shared/ai_assistant_management/selection/server/src/settings/translations.ts b/src/platform/plugins/shared/ai_assistant_management/selection/server/src/settings/translations.ts
new file mode 100644
index 0000000000000..26634464ea1e4
--- /dev/null
+++ b/src/platform/plugins/shared/ai_assistant_management/selection/server/src/settings/translations.ts
@@ -0,0 +1,49 @@
+/*
+ * 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", the "GNU Affero General Public License v3.0 only", and the "Server Side
+ * Public License v 1"; you may not use this file except in compliance with, at
+ * your election, the "Elastic License 2.0", the "GNU Affero General Public
+ * License v3.0 only", or the "Server Side Public License, v 1".
+ */
+
+import { i18n } from '@kbn/i18n';
+
+export const TITLE = i18n.translate(
+ 'aiAssistantManagementSelection.preferredAIAssistantTypeSettingName',
+ {
+ defaultMessage: 'AI Assistant visibility',
+ }
+);
+
+export const ONLY_IN_THEIR_SOLUTIONS = i18n.translate(
+ 'aiAssistantManagementSelection.preferredAIAssistantTypeSettingValueDefault',
+ { defaultMessage: 'Only in their solutions' }
+);
+export const OBSERVABILITY_IN_OTHER_APPS = i18n.translate(
+ 'aiAssistantManagementSelection.preferredAIAssistantTypeSettingValueObservability',
+ { defaultMessage: 'Observability and Search AI Assistants in other apps' }
+);
+export const SECURITY_IN_OTHER_APPS = i18n.translate(
+ 'aiAssistantManagementSelection.preferredAIAssistantTypeSettingValueSecurity',
+ { defaultMessage: 'Security AI Assistant in other apps' }
+);
+export const HIDE_ALL_ASSISTANTS = i18n.translate(
+ 'aiAssistantManagementSelection.preferredAIAssistantTypeSettingValueNever',
+ { defaultMessage: 'Hide all assistants' }
+);
+
+export const SHOW_SECURITY = i18n.translate(
+ 'aiAssistantManagementSelection.preferredAIAssistantTypeSettingValueNever',
+ { defaultMessage: 'Show Security AI Assistant' }
+);
+
+export const SHOW_OBSERVABILITY = i18n.translate(
+ 'aiAssistantManagementSelection.preferredAIAssistantTypeSettingValueNever',
+ { defaultMessage: 'Show Observability AI Assistant' }
+);
+
+export const HIDE_ASSISTANT = i18n.translate(
+ 'aiAssistantManagementSelection.preferredAIAssistantTypeSettingValueNever',
+ { defaultMessage: 'Hide AI Assistant' }
+);
diff --git a/src/platform/plugins/shared/ai_assistant_management/selection/server/types.ts b/src/platform/plugins/shared/ai_assistant_management/selection/server/types.ts
index 4667ad9ab4fbc..d581b8be26540 100644
--- a/src/platform/plugins/shared/ai_assistant_management/selection/server/types.ts
+++ b/src/platform/plugins/shared/ai_assistant_management/selection/server/types.ts
@@ -7,6 +7,7 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
+import { CloudSetup } from '@kbn/cloud-plugin/server';
import type { FeaturesPluginSetup } from '@kbn/features-plugin/server';
// eslint-disable-next-line @typescript-eslint/no-empty-interface
@@ -14,6 +15,7 @@ export interface AIAssistantManagementSelectionPluginServerDependenciesStart {}
export interface AIAssistantManagementSelectionPluginServerDependenciesSetup {
features?: FeaturesPluginSetup;
+ cloud?: CloudSetup;
}
// eslint-disable-next-line @typescript-eslint/no-empty-interface
diff --git a/src/platform/plugins/shared/ai_assistant_management/selection/tsconfig.json b/src/platform/plugins/shared/ai_assistant_management/selection/tsconfig.json
index 0fc6cec454817..7d7827387699d 100644
--- a/src/platform/plugins/shared/ai_assistant_management/selection/tsconfig.json
+++ b/src/platform/plugins/shared/ai_assistant_management/selection/tsconfig.json
@@ -18,7 +18,9 @@
"@kbn/core-plugins-server",
"@kbn/features-plugin",
"@kbn/config",
- "@kbn/doc-links"
+ "@kbn/doc-links",
+ "@kbn/core-ui-settings-common",
+ "@kbn/cloud-plugin"
],
"exclude": ["target/**/*"]
}
diff --git a/src/platform/test/plugin_functional/test_suites/core_plugins/rendering.ts b/src/platform/test/plugin_functional/test_suites/core_plugins/rendering.ts
index 746b7f7e8c110..272a87501f8cb 100644
--- a/src/platform/test/plugin_functional/test_suites/core_plugins/rendering.ts
+++ b/src/platform/test/plugin_functional/test_suites/core_plugins/rendering.ts
@@ -380,7 +380,7 @@ export default function ({ getService }: PluginFunctionalProviderContext) {
'xpack.observabilityAiAssistantManagement.spacesEnabled (boolean?)',
'xpack.observabilityAiAssistantManagement.visibilityEnabled (boolean?)',
'share.new_version.enabled (boolean?)',
- 'aiAssistantManagementSelection.preferredAIAssistantType (default?|never?|observability?)',
+ 'aiAssistantManagementSelection.preferredAIAssistantType (default?|never?|observability?|security?)',
/**
* Rule form V2 feature flags
*/
diff --git a/x-pack/platform/plugins/private/translations/translations/fr-FR.json b/x-pack/platform/plugins/private/translations/translations/fr-FR.json
index 9f5ff85500a7c..0d36a376b57b6 100644
--- a/x-pack/platform/plugins/private/translations/translations/fr-FR.json
+++ b/x-pack/platform/plugins/private/translations/translations/fr-FR.json
@@ -96,7 +96,6 @@
"aiAssistantManagementSelection.breadcrumb.index": "Assistant d'IA",
"aiAssistantManagementSelection.featureRegistry.featureName": "Assistant d'IA",
"aiAssistantManagementSelection.managementSectionLabel": "Assistants d'IA",
- "aiAssistantManagementSelection.preferredAIAssistantTypeSettingDescription": "[version d'évaluation technique] Afficher ou non l'élément de menu Assistant d’IA dans Observability et Search, partout ou nulle part.",
"aiAssistantManagementSelection.preferredAIAssistantTypeSettingName": "Assistant d’IA de visibilité pour Observability et Search",
"aiAssistantManagementSelection.preferredAIAssistantTypeSettingValueDefault": "Observability et Search uniquement (par défaut)",
"aiAssistantManagementSelection.preferredAIAssistantTypeSettingValueNever": "Nulle part",
diff --git a/x-pack/platform/plugins/private/translations/translations/ja-JP.json b/x-pack/platform/plugins/private/translations/translations/ja-JP.json
index 3fb2ba5d518f6..c6136c351e5ad 100644
--- a/x-pack/platform/plugins/private/translations/translations/ja-JP.json
+++ b/x-pack/platform/plugins/private/translations/translations/ja-JP.json
@@ -96,7 +96,6 @@
"aiAssistantManagementSelection.breadcrumb.index": "AI Assistant",
"aiAssistantManagementSelection.featureRegistry.featureName": "AI Assistant",
"aiAssistantManagementSelection.managementSectionLabel": "AI Assistant",
- "aiAssistantManagementSelection.preferredAIAssistantTypeSettingDescription": "[テクニカルプレビュー]オブザーバビリティとSearchのAI Assistantメニュー項目を、どの場所でも表示するか、どの場所でも表示しないか。",
"aiAssistantManagementSelection.preferredAIAssistantTypeSettingName": "AI Assistant for Observability and Searchの表示",
"aiAssistantManagementSelection.preferredAIAssistantTypeSettingValueDefault": "Observability and Searchのみ(デフォルト)",
"aiAssistantManagementSelection.preferredAIAssistantTypeSettingValueNever": "なし",
diff --git a/x-pack/platform/plugins/private/translations/translations/zh-CN.json b/x-pack/platform/plugins/private/translations/translations/zh-CN.json
index 9e095727fd7e9..b13c338b97763 100644
--- a/x-pack/platform/plugins/private/translations/translations/zh-CN.json
+++ b/x-pack/platform/plugins/private/translations/translations/zh-CN.json
@@ -96,7 +96,6 @@
"aiAssistantManagementSelection.breadcrumb.index": "AI 助手",
"aiAssistantManagementSelection.featureRegistry.featureName": "AI 助手",
"aiAssistantManagementSelection.managementSectionLabel": "AI 助手",
- "aiAssistantManagementSelection.preferredAIAssistantTypeSettingDescription": "[技术预览] Observability 和 Search 中是否显示 AI 助手菜单项:随处显示或者从不显示。",
"aiAssistantManagementSelection.preferredAIAssistantTypeSettingName": "适用于 Observability 和 Search 的 AI 助手可见性",
"aiAssistantManagementSelection.preferredAIAssistantTypeSettingValueDefault": "仅限 Observability 和 Search(默认)",
"aiAssistantManagementSelection.preferredAIAssistantTypeSettingValueNever": "从不显示",
diff --git a/x-pack/solutions/observability/plugins/observability_ai_assistant_app/public/hooks/is_nav_control_visible.test.tsx b/x-pack/solutions/observability/plugins/observability_ai_assistant_app/public/hooks/is_nav_control_visible.test.tsx
new file mode 100644
index 0000000000000..fb4a5e1a7b685
--- /dev/null
+++ b/x-pack/solutions/observability/plugins/observability_ai_assistant_app/public/hooks/is_nav_control_visible.test.tsx
@@ -0,0 +1,237 @@
+/*
+ * 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 { renderHook } from '@testing-library/react';
+import { useIsNavControlVisible } from './is_nav_control_visible';
+import { CoreStart } from '@kbn/core/public';
+import { ObservabilityAIAssistantAppPluginStartDependencies } from '../types';
+import { of } from 'rxjs';
+import { AIAssistantType } from '@kbn/ai-assistant-management-plugin/public';
+
+describe('isNavControlVisible', () => {
+ describe('with solution:oblt', () => {
+ it('returns true when the current app is discover and the ai assistant type is observability', () => {
+ const coreStart = {
+ application: {
+ currentAppId$: of('discover'),
+ applications$: of(
+ new Map([['discover', { id: 'discover', category: { id: 'kibana' } }]])
+ ),
+ },
+ } as unknown as CoreStart;
+
+ const pluginsStart = {
+ aiAssistantManagementSelection: {
+ aiAssistantType$: of(AIAssistantType.Observability),
+ },
+ spaces: {
+ getActiveSpace$: () => of({ solution: 'oblt' }),
+ },
+ } as unknown as ObservabilityAIAssistantAppPluginStartDependencies;
+
+ const { result } = renderHook(() => useIsNavControlVisible({ coreStart, pluginsStart }));
+
+ expect(result.current.isVisible).toBe(true);
+ });
+
+ it('returns true when the current app is discover and the ai assistant type is default', () => {
+ const coreStart = {
+ application: {
+ currentAppId$: of('discover'),
+ applications$: of(
+ new Map([['discover', { id: 'discover', category: { id: 'kibana' } }]])
+ ),
+ },
+ } as unknown as CoreStart;
+
+ const pluginsStart = {
+ aiAssistantManagementSelection: {
+ aiAssistantType$: of(AIAssistantType.Default),
+ },
+ spaces: {
+ getActiveSpace$: () => of({ solution: 'oblt' }),
+ },
+ } as unknown as ObservabilityAIAssistantAppPluginStartDependencies;
+
+ const { result } = renderHook(() => useIsNavControlVisible({ coreStart, pluginsStart }));
+
+ expect(result.current.isVisible).toBe(true);
+ });
+
+ it('returns true when the current app is observability and the ai assistant type is default', () => {
+ const coreStart = {
+ application: {
+ currentAppId$: of('observability'),
+ applications$: of(
+ new Map([['observability', { id: 'observability', category: { id: 'observability' } }]])
+ ),
+ },
+ } as unknown as CoreStart;
+
+ const pluginsStart = {
+ aiAssistantManagementSelection: {
+ aiAssistantType$: of(AIAssistantType.Default),
+ },
+ spaces: {
+ getActiveSpace$: () => of({ solution: 'oblt' }),
+ },
+ } as unknown as ObservabilityAIAssistantAppPluginStartDependencies;
+
+ const { result } = renderHook(() => useIsNavControlVisible({ coreStart, pluginsStart }));
+
+ expect(result.current.isVisible).toBe(true);
+ });
+
+ it('returns true when the current app is search and the ai assistant type is default', () => {
+ const coreStart = {
+ application: {
+ currentAppId$: of('search'),
+ applications$: of(
+ new Map([['search', { id: 'search', category: { id: 'enterpriseSearch' } }]])
+ ),
+ },
+ } as unknown as CoreStart;
+
+ const pluginsStart = {
+ aiAssistantManagementSelection: {
+ aiAssistantType$: of(AIAssistantType.Default),
+ },
+ spaces: {
+ getActiveSpace$: () => of({ solution: 'oblt' }),
+ },
+ } as unknown as ObservabilityAIAssistantAppPluginStartDependencies;
+
+ const { result } = renderHook(() => useIsNavControlVisible({ coreStart, pluginsStart }));
+
+ expect(result.current.isVisible).toBe(true);
+ });
+
+ it('returns false when the current app is security and the ai assistant type is observability', () => {
+ const coreStart = {
+ application: {
+ currentAppId$: of('security'),
+ applications$: of(
+ new Map([['security', { id: 'security', category: { id: 'securitySolution' } }]])
+ ),
+ },
+ } as unknown as CoreStart;
+
+ const pluginsStart = {
+ aiAssistantManagementSelection: {
+ aiAssistantType$: of(AIAssistantType.Observability),
+ },
+ spaces: {
+ getActiveSpace$: () => of({ solution: 'oblt' }),
+ },
+ } as unknown as ObservabilityAIAssistantAppPluginStartDependencies;
+
+ const { result } = renderHook(() => useIsNavControlVisible({ coreStart, pluginsStart }));
+
+ expect(result.current.isVisible).toBe(false);
+ });
+
+ it('returns false when the ai assistant type is never', () => {
+ const coreStart = {
+ application: {
+ currentAppId$: of('observability'),
+ applications$: of(
+ new Map([['observability', { id: 'observability', category: { id: 'observability' } }]])
+ ),
+ },
+ } as unknown as CoreStart;
+
+ const pluginsStart = {
+ aiAssistantManagementSelection: {
+ aiAssistantType$: of(AIAssistantType.Never),
+ },
+ spaces: {
+ getActiveSpace$: () => of({ solution: 'oblt' }),
+ },
+ } as unknown as ObservabilityAIAssistantAppPluginStartDependencies;
+
+ const { result } = renderHook(() => useIsNavControlVisible({ coreStart, pluginsStart }));
+
+ expect(result.current.isVisible).toBe(false);
+ });
+ });
+
+ describe('with solution:es', () => {
+ it('returns true when the current space is es and the ai assistant type is observability', () => {
+ const coreStart = {
+ application: {
+ currentAppId$: of('discover'),
+ applications$: of(
+ new Map([['discover', { id: 'discover', category: { id: 'kibana' } }]])
+ ),
+ },
+ } as unknown as CoreStart;
+
+ const pluginsStart = {
+ aiAssistantManagementSelection: {
+ aiAssistantType$: of(AIAssistantType.Observability),
+ },
+ spaces: {
+ getActiveSpace$: () => of({ solution: 'es' }),
+ },
+ } as unknown as ObservabilityAIAssistantAppPluginStartDependencies;
+
+ const { result } = renderHook(() => useIsNavControlVisible({ coreStart, pluginsStart }));
+
+ expect(result.current.isVisible).toBe(true);
+ });
+
+ it('returns false when the current space is es and the ai assistant type is never', () => {
+ const coreStart = {
+ application: {
+ currentAppId$: of('discover'),
+ applications$: of(
+ new Map([['discover', { id: 'discover', category: { id: 'kibana' } }]])
+ ),
+ },
+ } as unknown as CoreStart;
+
+ const pluginsStart = {
+ aiAssistantManagementSelection: {
+ aiAssistantType$: of(AIAssistantType.Never),
+ },
+ spaces: {
+ getActiveSpace$: () => of({ solution: 'es' }),
+ },
+ } as unknown as ObservabilityAIAssistantAppPluginStartDependencies;
+
+ const { result } = renderHook(() => useIsNavControlVisible({ coreStart, pluginsStart }));
+
+ expect(result.current.isVisible).toBe(false);
+ });
+ });
+
+ describe('with classic', () => {
+ it('returns false when the ai assistant type is default', () => {
+ const coreStart = {
+ application: {
+ currentAppId$: of('discover'),
+ applications$: of(
+ new Map([['discover', { id: 'discover', category: { id: 'kibana' } }]])
+ ),
+ },
+ } as unknown as CoreStart;
+
+ const pluginsStart = {
+ aiAssistantManagementSelection: {
+ aiAssistantType$: of(AIAssistantType.Default),
+ },
+ spaces: {
+ getActiveSpace$: () => of({ solution: 'classic' }),
+ },
+ } as unknown as ObservabilityAIAssistantAppPluginStartDependencies;
+
+ const { result } = renderHook(() => useIsNavControlVisible({ coreStart, pluginsStart }));
+
+ expect(result.current.isVisible).toBe(false);
+ });
+ });
+});
diff --git a/x-pack/solutions/observability/plugins/observability_ai_assistant_app/public/hooks/is_nav_control_visible.tsx b/x-pack/solutions/observability/plugins/observability_ai_assistant_app/public/hooks/is_nav_control_visible.tsx
index ce295737315d9..e8fb82fb22ce0 100644
--- a/x-pack/solutions/observability/plugins/observability_ai_assistant_app/public/hooks/is_nav_control_visible.tsx
+++ b/x-pack/solutions/observability/plugins/observability_ai_assistant_app/public/hooks/is_nav_control_visible.tsx
@@ -30,7 +30,11 @@ function getVisibility(
const categoryId =
(appId && applications.get(appId)?.category?.id) || DEFAULT_APP_CATEGORIES.kibana.id;
- if (preferredAssistantType === AIAssistantType.Observability || space.solution === 'es') {
+ if (
+ preferredAssistantType === AIAssistantType.Observability ||
+ space.solution === 'es' ||
+ space.solution === 'oblt'
+ ) {
return categoryId !== DEFAULT_APP_CATEGORIES.security.id;
}
diff --git a/x-pack/solutions/security/plugins/elastic_assistant/public/src/context/assistant_context/assistant_provider.tsx b/x-pack/solutions/security/plugins/elastic_assistant/public/src/context/assistant_context/assistant_provider.tsx
index 3f9ca479e2915..dabff2d8e0d63 100644
--- a/x-pack/solutions/security/plugins/elastic_assistant/public/src/context/assistant_context/assistant_provider.tsx
+++ b/x-pack/solutions/security/plugins/elastic_assistant/public/src/context/assistant_context/assistant_provider.tsx
@@ -46,6 +46,7 @@ export function AssistantProvider({ children }: { children: React.ReactElement }
const inferenceEnabled = useInferenceEnabled();
const basePath = useBasePath();
+ const { isVisible } = useIsNavControlVisible();
const assistantAvailability = useAssistantAvailability();
const assistantTelemetry = useAssistantTelemetry();
@@ -113,8 +114,6 @@ export function AssistantProvider({ children }: { children: React.ReactElement }
);
}, [assistantContextValue, elasticAssistantSharedState.assistantContextValue]);
- const { isVisible } = useIsNavControlVisible();
-
if (!isVisible) {
return null;
}
diff --git a/x-pack/solutions/security/plugins/elastic_assistant/public/src/hooks/assistant_availability/use_assistant_availability.test.ts b/x-pack/solutions/security/plugins/elastic_assistant/public/src/hooks/assistant_availability/use_assistant_availability.test.ts
index a5b22556720f8..0be2ab01e3784 100644
--- a/x-pack/solutions/security/plugins/elastic_assistant/public/src/hooks/assistant_availability/use_assistant_availability.test.ts
+++ b/x-pack/solutions/security/plugins/elastic_assistant/public/src/hooks/assistant_availability/use_assistant_availability.test.ts
@@ -12,15 +12,24 @@ import { LicenseService } from '../licence/license_service';
import { renderHook } from '@testing-library/react';
import { SECURITY_FEATURE_ID } from '../../../../common/constants';
import { ASSISTANT_FEATURE_ID } from '@kbn/security-solution-features/constants';
+import { useIsNavControlVisible } from '../is_nav_control_visible/use_is_nav_control_visible';
+
jest.mock('../licence/use_licence');
jest.mock('../../context/typed_kibana_context/typed_kibana_context');
+jest.mock('../is_nav_control_visible/use_is_nav_control_visible');
const mockUseLicense = useLicense as jest.MockedFunction;
const mockUseKibana = useKibana as jest.MockedFunction;
+const mockUseIsNavControlVisible = useIsNavControlVisible as jest.MockedFunction<
+ typeof useIsNavControlVisible
+>;
describe('useAssistantAvailability', () => {
beforeEach(() => {
jest.resetAllMocks();
+ mockUseIsNavControlVisible.mockReturnValue({
+ isVisible: true,
+ });
});
it('returns correct values when all privileges are available', () => {
@@ -48,6 +57,9 @@ describe('useAssistantAvailability', () => {
},
},
},
+ aiAssistantManagementSelection: {
+ aiAssistantManagementSelection$: jest.fn(),
+ },
featureFlags: {
getBooleanValue: jest.fn().mockReturnValue(true),
},
@@ -68,6 +80,58 @@ describe('useAssistantAvailability', () => {
});
});
+ it('returns correct values when all privileges are available but assistant his hidden', () => {
+ mockUseLicense.mockReturnValue({
+ isEnterprise: jest.fn().mockReturnValue(true),
+ } as unknown as LicenseService);
+
+ mockUseIsNavControlVisible.mockReturnValue({
+ isVisible: false,
+ });
+
+ mockUseKibana.mockReturnValue({
+ services: {
+ application: {
+ capabilities: {
+ [ASSISTANT_FEATURE_ID]: {
+ 'ai-assistant': true,
+ updateAIAssistantAnonymization: true,
+ manageGlobalKnowledgeBaseAIAssistant: true,
+ },
+ [SECURITY_FEATURE_ID]: {
+ configurations: true,
+ },
+ actions: {
+ show: true,
+ execute: true,
+ save: true,
+ delete: true,
+ },
+ },
+ },
+ aiAssistantManagementSelection: {
+ aiAssistantManagementSelection$: jest.fn(),
+ },
+ featureFlags: {
+ getBooleanValue: jest.fn().mockReturnValue(true),
+ },
+ },
+ } as unknown as ReturnType);
+
+ const { result } = renderHook(() => useAssistantAvailability());
+
+ expect(result.current).toEqual({
+ hasSearchAILakeConfigurations: true,
+ hasAssistantPrivilege: true,
+ hasConnectorsAllPrivilege: true,
+ hasConnectorsReadPrivilege: true,
+ isAssistantEnabled: false,
+ isStarterPromptsEnabled: true,
+ hasUpdateAIAssistantAnonymization: true,
+ hasManageGlobalKnowledgeBase: true,
+ });
+ });
+
it('returns correct values when no privileges are available', () => {
mockUseLicense.mockReturnValue({
isEnterprise: jest.fn().mockReturnValue(false),
diff --git a/x-pack/solutions/security/plugins/elastic_assistant/public/src/hooks/assistant_availability/use_assistant_availability.ts b/x-pack/solutions/security/plugins/elastic_assistant/public/src/hooks/assistant_availability/use_assistant_availability.ts
index ced9f5101f8a7..f20598af543f1 100644
--- a/x-pack/solutions/security/plugins/elastic_assistant/public/src/hooks/assistant_availability/use_assistant_availability.ts
+++ b/x-pack/solutions/security/plugins/elastic_assistant/public/src/hooks/assistant_availability/use_assistant_availability.ts
@@ -11,9 +11,11 @@ import { SECURITY_FEATURE_ID } from '../../../../common/constants';
import { useKibana } from '../../context/typed_kibana_context/typed_kibana_context';
import { useLicense } from '../licence/use_licence';
+import { useIsNavControlVisible } from '../is_nav_control_visible/use_is_nav_control_visible';
export const STARTER_PROMPTS_FEATURE_FLAG = 'elasticAssistant.starterPromptsEnabled' as const;
export const useAssistantAvailability = (): UseAssistantAvailability => {
+ const { isVisible } = useIsNavControlVisible();
const isEnterprise = useLicense().isEnterprise();
const {
application: { capabilities },
@@ -45,7 +47,7 @@ export const useAssistantAvailability = (): UseAssistantAvailability => {
hasConnectorsAllPrivilege,
hasConnectorsReadPrivilege,
isStarterPromptsEnabled,
- isAssistantEnabled: isEnterprise,
+ isAssistantEnabled: isEnterprise && isVisible,
hasUpdateAIAssistantAnonymization,
hasManageGlobalKnowledgeBase,
};
diff --git a/x-pack/solutions/security/plugins/elastic_assistant/public/src/hooks/is_nav_control_visible/use_is_nav_control_visible.test.ts b/x-pack/solutions/security/plugins/elastic_assistant/public/src/hooks/is_nav_control_visible/use_is_nav_control_visible.test.ts
new file mode 100644
index 0000000000000..5858d8ac45323
--- /dev/null
+++ b/x-pack/solutions/security/plugins/elastic_assistant/public/src/hooks/is_nav_control_visible/use_is_nav_control_visible.test.ts
@@ -0,0 +1,118 @@
+/*
+ * 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 { renderHook } from '@testing-library/react';
+import { useIsNavControlVisible } from './use_is_nav_control_visible';
+import { of } from 'rxjs';
+import { AIAssistantType } from '@kbn/ai-assistant-management-plugin/public';
+import { useKibana } from '../../context/typed_kibana_context/typed_kibana_context';
+
+jest.mock('../../context/typed_kibana_context/typed_kibana_context', () => {
+ return {
+ useKibana: jest.fn(),
+ };
+});
+
+describe('isNavControlVisible', () => {
+ it('returns true when the current app is security and the ai assistant type is default', () => {
+ (useKibana as jest.Mock).mockReturnValue({
+ services: {
+ application: {
+ currentAppId$: of('security'),
+ applications$: of(
+ new Map([
+ ['security', { id: 'security', category: { id: 'securitySolution' } }],
+ ['observability', { id: 'observability', category: { id: 'observability' } }],
+ ])
+ ),
+ },
+ aiAssistantManagementSelection: {
+ aiAssistantType$: of(AIAssistantType.Default),
+ },
+ },
+ });
+
+ const { result } = renderHook(() => useIsNavControlVisible());
+ expect(result.current.isVisible).toEqual(true);
+ });
+
+ it('returns false when the current app is observability and the ai assistant type is default', () => {
+ (useKibana as jest.Mock).mockReturnValue({
+ services: {
+ application: {
+ currentAppId$: of('observability'),
+ applications$: of(
+ new Map([['observability', { id: 'observability', category: { id: 'observability' } }]])
+ ),
+ },
+ aiAssistantManagementSelection: {
+ aiAssistantType$: of(AIAssistantType.Default),
+ },
+ },
+ });
+
+ const { result } = renderHook(() => useIsNavControlVisible());
+ expect(result.current.isVisible).toEqual(false);
+ });
+
+ it('returns false when the current app is search and the ai assistant type is default', () => {
+ (useKibana as jest.Mock).mockReturnValue({
+ services: {
+ application: {
+ currentAppId$: of('search'),
+ applications$: of(
+ new Map([['search', { id: 'search', category: { id: 'enterpriseSearch' } }]])
+ ),
+ },
+ aiAssistantManagementSelection: {
+ aiAssistantType$: of(AIAssistantType.Security),
+ },
+ },
+ });
+
+ const { result } = renderHook(() => useIsNavControlVisible());
+ expect(result.current.isVisible).toEqual(false);
+ });
+
+ it('returns false when the current app is discover and the ai assistant type is security', () => {
+ (useKibana as jest.Mock).mockReturnValue({
+ services: {
+ application: {
+ currentAppId$: of('discover'),
+ applications$: of(
+ new Map([['discover', { id: 'discover', category: { id: 'kibana' } }]])
+ ),
+ },
+ aiAssistantManagementSelection: {
+ aiAssistantType$: of(AIAssistantType.Security),
+ },
+ },
+ });
+
+ const { result } = renderHook(() => useIsNavControlVisible());
+ expect(result.current.isVisible).toEqual(true);
+ });
+
+ it('returns false when the current app is discover and the ai assistant type is observability', () => {
+ (useKibana as jest.Mock).mockReturnValue({
+ services: {
+ application: {
+ currentAppId$: of('discover'),
+ applications$: of(
+ new Map([['discover', { id: 'discover', category: { id: 'kibana' } }]])
+ ),
+ },
+ aiAssistantManagementSelection: {
+ aiAssistantType$: of(AIAssistantType.Observability),
+ },
+ },
+ });
+
+ const { result } = renderHook(() => useIsNavControlVisible());
+ expect(result.current.isVisible).toEqual(false);
+ });
+});
diff --git a/x-pack/solutions/security/plugins/elastic_assistant/public/src/hooks/is_nav_control_visible/use_is_nav_control_visible.ts b/x-pack/solutions/security/plugins/elastic_assistant/public/src/hooks/is_nav_control_visible/use_is_nav_control_visible.ts
index b21becbefed76..42164fecff600 100644
--- a/x-pack/solutions/security/plugins/elastic_assistant/public/src/hooks/is_nav_control_visible/use_is_nav_control_visible.ts
+++ b/x-pack/solutions/security/plugins/elastic_assistant/public/src/hooks/is_nav_control_visible/use_is_nav_control_visible.ts
@@ -16,12 +16,20 @@ function getVisibility(
applications: ReadonlyMap,
preferredAssistantType: AIAssistantType
) {
- // The "Global assistant" stack management setting for the security assistant still needs to be developed.
- // In the meantime, while testing, show the Security assistant everywhere except in Observability.
+ if (preferredAssistantType === AIAssistantType.Never) {
+ return false;
+ }
const categoryId =
(appId && applications.get(appId)?.category?.id) || DEFAULT_APP_CATEGORIES.kibana.id;
+ if (preferredAssistantType === AIAssistantType.Security) {
+ return (
+ DEFAULT_APP_CATEGORIES.observability.id !== categoryId &&
+ DEFAULT_APP_CATEGORIES.enterpriseSearch.id !== categoryId
+ );
+ }
+
return DEFAULT_APP_CATEGORIES.security.id === categoryId;
}
diff --git a/x-pack/solutions/security/plugins/security_solution/public/assistant/migrate_conversations_from_local_storage/use_migrate_conversation_from_local_storage.ts b/x-pack/solutions/security/plugins/security_solution/public/assistant/migrate_conversations_from_local_storage/use_migrate_conversation_from_local_storage.ts
index faaaa3dafe979..2308f6168e671 100644
--- a/x-pack/solutions/security/plugins/security_solution/public/assistant/migrate_conversations_from_local_storage/use_migrate_conversation_from_local_storage.ts
+++ b/x-pack/solutions/security/plugins/security_solution/public/assistant/migrate_conversations_from_local_storage/use_migrate_conversation_from_local_storage.ts
@@ -43,3 +43,8 @@ export const useMigrateConversationsFromLocalStorage = () => {
storage,
]);
};
+
+export const MigrateConversationsFromLocalStorage = () => {
+ useMigrateConversationsFromLocalStorage();
+ return null;
+};
diff --git a/x-pack/solutions/security/plugins/security_solution/public/assistant/provider.tsx b/x-pack/solutions/security/plugins/security_solution/public/assistant/provider.tsx
index 7b4414dde23cc..b48fb45852081 100644
--- a/x-pack/solutions/security/plugins/security_solution/public/assistant/provider.tsx
+++ b/x-pack/solutions/security/plugins/security_solution/public/assistant/provider.tsx
@@ -11,13 +11,12 @@ import { AssistantProvider as ElasticAssistantProvider } from '@kbn/elastic-assi
import { isEmpty } from 'lodash/fp';
import useObservable from 'react-use/lib/useObservable';
import { useKibana } from '../common/lib/kibana';
-import { useAssistantAvailability } from './use_assistant_availability';
import { licenseService } from '../common/hooks/use_license';
import { useFindPromptContexts } from './content/prompt_contexts/use_find_prompt_contexts';
import { CommentActionsPortal } from './comment_actions/comment_actions_portal';
import { AugmentMessageCodeBlocksPortal } from './use_augment_message_code_blocks/augment_message_code_blocks_portal';
import { useElasticAssistantSharedStateSignalIndex } from './use_elastic_assistant_shared_state_signal_index/use_elastic_assistant_shared_state_signal_index';
-import { useMigrateConversationsFromLocalStorage } from './migrate_conversations_from_local_storage/use_migrate_conversation_from_local_storage';
+import { MigrateConversationsFromLocalStorage } from './migrate_conversations_from_local_storage/use_migrate_conversation_from_local_storage';
/**
* This component configures the Elastic AI Assistant context provider for the Security Solution app.
@@ -29,17 +28,15 @@ export const AssistantProvider: FC> = ({ children })
elasticAssistantSharedState.assistantContextValue.getAssistantContextValue$()
);
- const assistantAvailability = useAssistantAvailability();
const hasEnterpriseLicence = licenseService.isEnterprise();
- useMigrateConversationsFromLocalStorage();
useElasticAssistantSharedStateSignalIndex();
const promptContexts = useFindPromptContexts({
context: {
isAssistantEnabled:
hasEnterpriseLicence &&
- assistantAvailability.isAssistantEnabled &&
- assistantAvailability.hasAssistantPrivilege,
+ (assistantContextValue?.assistantAvailability.isAssistantEnabled ?? false) &&
+ (assistantContextValue?.assistantAvailability.hasAssistantPrivilege ?? false),
httpFetch: http.fetch,
toasts: notifications.toasts,
},
@@ -66,6 +63,7 @@ export const AssistantProvider: FC> = ({ children })
+
{children}
);
diff --git a/x-pack/solutions/security/plugins/security_solution/public/assistant/use_assistant_availability/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/assistant/use_assistant_availability/index.tsx
index e08aa8579c2ae..00118f18473c6 100644
--- a/x-pack/solutions/security/plugins/security_solution/public/assistant/use_assistant_availability/index.tsx
+++ b/x-pack/solutions/security/plugins/security_solution/public/assistant/use_assistant_availability/index.tsx
@@ -4,10 +4,7 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
-
-import { useLicense } from '../../common/hooks/use_license';
-import { useKibana } from '../../common/lib/kibana';
-import { ASSISTANT_FEATURE_ID, SECURITY_FEATURE_ID } from '../../../common/constants';
+import { useAssistantContext } from '@kbn/elastic-assistant';
export interface UseAssistantAvailability {
// True when searchAiLake configurations is available
@@ -27,32 +24,6 @@ export interface UseAssistantAvailability {
}
export const useAssistantAvailability = (): UseAssistantAvailability => {
- const isEnterprise = useLicense().isEnterprise();
- const capabilities = useKibana().services.application.capabilities;
- const hasAssistantPrivilege = capabilities[ASSISTANT_FEATURE_ID]?.['ai-assistant'] === true;
- const hasUpdateAIAssistantAnonymization =
- capabilities[ASSISTANT_FEATURE_ID]?.updateAIAssistantAnonymization === true;
- const hasManageGlobalKnowledgeBase =
- capabilities[ASSISTANT_FEATURE_ID]?.manageGlobalKnowledgeBaseAIAssistant === true;
- const hasSearchAILakeConfigurations = capabilities[SECURITY_FEATURE_ID]?.configurations === true;
-
- // Connectors & Actions capabilities as defined in x-pack/plugins/actions/server/feature.ts
- // `READ` ui capabilities defined as: { ui: ['show', 'execute'] }
- const hasConnectorsReadPrivilege =
- capabilities.actions?.show === true && capabilities.actions?.execute === true;
- // `ALL` ui capabilities defined as: { ui: ['show', 'execute', 'save', 'delete'] }
- const hasConnectorsAllPrivilege =
- hasConnectorsReadPrivilege &&
- capabilities.actions?.delete === true &&
- capabilities.actions?.save === true;
-
- return {
- hasSearchAILakeConfigurations,
- hasAssistantPrivilege,
- hasConnectorsAllPrivilege,
- hasConnectorsReadPrivilege,
- isAssistantEnabled: isEnterprise,
- hasUpdateAIAssistantAnonymization,
- hasManageGlobalKnowledgeBase,
- };
+ const { assistantAvailability } = useAssistantContext();
+ return assistantAvailability;
};
diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/common/components/rule_execution_status/rule_status_failed_callout.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/common/components/rule_execution_status/rule_status_failed_callout.test.tsx
index c57ad6dd4d2cb..3b17180d713b5 100644
--- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/common/components/rule_execution_status/rule_status_failed_callout.test.tsx
+++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/common/components/rule_execution_status/rule_status_failed_callout.test.tsx
@@ -47,15 +47,6 @@ const ContextWrapper: FC> = ({ children }) => {
};
describe('RuleStatusFailedCallOut', () => {
- const renderWith = (status: RuleExecutionStatus | null | undefined) =>
- render(
-
- );
const renderWithAssistant = (status: RuleExecutionStatus | null | undefined) =>
render(
@@ -64,31 +55,31 @@ describe('RuleStatusFailedCallOut', () => {
date={DATE}
message={MESSAGE}
ruleNameForChat="ruleNameForChat"
- />{' '}
+ />
);
it('is hidden if status is undefined', () => {
- const result = renderWith(undefined);
+ const result = renderWithAssistant(undefined);
expect(result.queryByTestId(TEST_ID)).toBe(null);
});
it('is hidden if status is null', () => {
- const result = renderWith(null);
+ const result = renderWithAssistant(null);
expect(result.queryByTestId(TEST_ID)).toBe(null);
});
it('is hidden if status is "going to run"', () => {
- const result = renderWith(RuleExecutionStatusEnum['going to run']);
+ const result = renderWithAssistant(RuleExecutionStatusEnum['going to run']);
expect(result.queryByTestId(TEST_ID)).toBe(null);
});
it('is hidden if status is "running"', () => {
- const result = renderWith(RuleExecutionStatusEnum.running);
+ const result = renderWithAssistant(RuleExecutionStatusEnum.running);
expect(result.queryByTestId(TEST_ID)).toBe(null);
});
it('is hidden if status is "succeeded"', () => {
- const result = renderWith(RuleExecutionStatusEnum.succeeded);
+ const result = renderWithAssistant(RuleExecutionStatusEnum.succeeded);
expect(result.queryByTestId(TEST_ID)).toBe(null);
});
diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/rules_table_toolbar.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/rules_table_toolbar.tsx
index d16ca060a4e0f..0e2fa2c3f1c06 100644
--- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/rules_table_toolbar.tsx
+++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/rules_table_toolbar.tsx
@@ -103,7 +103,7 @@ export const RulesTableToolbar = React.memo(() => {
- {hasAssistantPrivilege && selectedRules.length > 0 && (
+ {hasAssistantPrivilege && selectedRules.length > 0 && isAssistantEnabled && (
): J
({ eui: euiDarkVars, darkMode: true })}>
-
- {children}
-
+
+
+ {children}
+
+
diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/hooks/use_assistant.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/hooks/use_assistant.test.tsx
index 66f16c9280923..3180f40aaf6da 100644
--- a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/hooks/use_assistant.test.tsx
+++ b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/hooks/use_assistant.test.tsx
@@ -79,6 +79,22 @@ describe('useAssistant', () => {
expect(hookResult.result.current.promptContextId).toEqual('123');
});
+ it(`should return showAssistant false if isAssistantEnabled is false`, () => {
+ jest.mocked(useAssistantAvailability).mockReturnValue({
+ hasSearchAILakeConfigurations: false,
+ hasAssistantPrivilege: true,
+ hasConnectorsAllPrivilege: true,
+ hasConnectorsReadPrivilege: true,
+ hasUpdateAIAssistantAnonymization: true,
+ hasManageGlobalKnowledgeBase: true,
+ isAssistantEnabled: false,
+ });
+
+ hookResult = renderUseAssistant();
+
+ expect(hookResult.result.current.showAssistant).toEqual(false);
+ });
+
it(`should return showAssistant false if hasAssistantPrivilege is false`, () => {
jest.mocked(useAssistantAvailability).mockReturnValue({
hasSearchAILakeConfigurations: false,
diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/hooks/use_assistant.ts b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/hooks/use_assistant.ts
index 7a3c137a396f9..0671aeff35c5e 100644
--- a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/hooks/use_assistant.ts
+++ b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/hooks/use_assistant.ts
@@ -107,7 +107,7 @@ export const useAssistant = ({
);
return {
- showAssistant: hasAssistantPrivilege && promptContextId !== null,
+ showAssistant: isAssistantEnabled && hasAssistantPrivilege && promptContextId !== null,
showAssistantOverlay,
promptContextId: promptContextId || '',
};