diff --git a/package.json b/package.json index 3e394328975e6..7fa8c79b598e9 100644 --- a/package.json +++ b/package.json @@ -170,6 +170,7 @@ "@kbn/ai-assistant": "link:x-pack/platform/packages/shared/kbn-ai-assistant", "@kbn/ai-assistant-common": "link:x-pack/platform/packages/shared/ai-assistant/common", "@kbn/ai-assistant-cta": "link:x-pack/platform/packages/shared/ai-assistant/ai-assistant-cta", + "@kbn/ai-assistant-default-llm-setting": "link:x-pack/platform/packages/shared/ai-assistant-default-llm-setting", "@kbn/ai-assistant-icon": "link:x-pack/platform/packages/shared/ai-assistant/icon", "@kbn/ai-assistant-management-plugin": "link:src/platform/plugins/shared/ai_assistant_management/selection", "@kbn/ai-security-labs-content": "link:x-pack/solutions/security/packages/ai-security-labs-content", 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 ed98ea65a7ed2..07a6f89aa35c0 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 @@ -123,6 +123,9 @@ export const OBSERVABILITY_AI_ASSISTANT_SIMULATED_FUNCTION_CALLING = 'observability:aiAssistantSimulatedFunctionCalling'; export const OBSERVABILITY_AI_ASSISTANT_SEARCH_CONNECTOR_INDEX_PATTERN = 'observability:aiAssistantSearchConnectorIndexPattern'; +export const GEN_AI_SETTINGS_DEFAULT_AI_CONNECTOR = 'genAiSettings:defaultAIConnector'; +export const GEN_AI_SETTINGS_DEFAULT_AI_CONNECTOR_DEFAULT_ONLY = + 'genAiSettings:defaultAIConnectorOnly'; export const AI_ANONYMIZATION_SETTINGS = 'ai:anonymizationSettings'; export const OBSERVABILITY_SEARCH_EXCLUDED_DATA_TIERS = 'observability:searchExcludedDataTiers'; export const OBSERVABILITY_ENABLE_STREAMS_UI = 'observability:enableStreamsUI'; diff --git a/src/platform/plugins/private/kibana_usage_collection/server/collectors/management/schema.ts b/src/platform/plugins/private/kibana_usage_collection/server/collectors/management/schema.ts index 4069efafadeff..24c2d547fff1c 100644 --- a/src/platform/plugins/private/kibana_usage_collection/server/collectors/management/schema.ts +++ b/src/platform/plugins/private/kibana_usage_collection/server/collectors/management/schema.ts @@ -651,6 +651,18 @@ export const stackManagementSchema: MakeSchemaFrom = { type: 'keyword', _meta: { description: 'Non-default value of setting.' }, }, + 'genAiSettings:defaultAIConnector': { + type: 'keyword', + _meta: { + description: 'Default AI connector', + }, + }, + 'genAiSettings:defaultAIConnectorOnly': { + type: 'boolean', + _meta: { + description: 'Restrict to default AI connector only', + }, + }, 'observability:searchExcludedDataTiers': { type: 'array', items: { diff --git a/src/platform/plugins/private/kibana_usage_collection/server/collectors/management/types.ts b/src/platform/plugins/private/kibana_usage_collection/server/collectors/management/types.ts index 124b70dcb7d55..bfa7400bfccdf 100644 --- a/src/platform/plugins/private/kibana_usage_collection/server/collectors/management/types.ts +++ b/src/platform/plugins/private/kibana_usage_collection/server/collectors/management/types.ts @@ -170,6 +170,8 @@ export interface UsageStats { 'observability:apmEnableTransactionProfiling': boolean; 'devTools:enablePersistentConsole': boolean; 'aiAssistant:preferredAIAssistantType': string; + 'genAiSettings:defaultAIConnector': string; + 'genAiSettings:defaultAIConnectorOnly': boolean; 'securitySolution:excludedDataTiersForRuleExecution': string[]; 'securitySolution:maxUnassociatedNotes': number; 'observability:searchExcludedDataTiers': string[]; diff --git a/src/platform/plugins/shared/ai_assistant_management/selection/common/constants.ts b/src/platform/plugins/shared/ai_assistant_management/selection/common/constants.ts new file mode 100644 index 0000000000000..05c3a71c09a00 --- /dev/null +++ b/src/platform/plugins/shared/ai_assistant_management/selection/common/constants.ts @@ -0,0 +1,10 @@ +/* + * 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". + */ + +export const NO_DEFAULT_CONNECTOR = 'NO_DEFAULT_CONNECTOR'; 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 index 5d00c575cb7e9..ad3f8b117da71 100644 --- 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 @@ -15,6 +15,11 @@ 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'; +import { + GEN_AI_SETTINGS_DEFAULT_AI_CONNECTOR, + GEN_AI_SETTINGS_DEFAULT_AI_CONNECTOR_DEFAULT_ONLY, +} from '@kbn/management-settings-ids'; +import { NO_DEFAULT_CONNECTOR } from '../common/constants'; describe('plugin', () => { beforeEach(() => { @@ -73,7 +78,7 @@ describe('plugin', () => { }, } as unknown as AIAssistantManagementSelectionPluginServerDependenciesSetup); - expect(coreSetup.uiSettings.register).toHaveBeenCalledTimes(1); + expect(coreSetup.uiSettings.register).toHaveBeenCalledTimes(3); expect(coreSetup.uiSettings.register).toHaveBeenCalledWith({ [PREFERRED_AI_ASSISTANT_TYPE_SETTING_KEY]: { @@ -81,6 +86,25 @@ describe('plugin', () => { value: AIAssistantType.Observability, }, }); + + expect(coreSetup.uiSettings.register).toHaveBeenCalledWith( + expect.objectContaining({ + [GEN_AI_SETTINGS_DEFAULT_AI_CONNECTOR]: expect.objectContaining({ + readonlyMode: 'ui', + readonly: false, + value: NO_DEFAULT_CONNECTOR, + }), + }) + ); + expect(coreSetup.uiSettings.register).toHaveBeenCalledWith( + expect.objectContaining({ + [GEN_AI_SETTINGS_DEFAULT_AI_CONNECTOR_DEFAULT_ONLY]: expect.objectContaining({ + readonlyMode: 'ui', + readonly: false, + value: false, + }), + }) + ); }); it('registers correct uiSettings for serverless security', () => { @@ -99,7 +123,7 @@ describe('plugin', () => { }, } as unknown as AIAssistantManagementSelectionPluginServerDependenciesSetup); - expect(coreSetup.uiSettings.register).toHaveBeenCalledTimes(1); + expect(coreSetup.uiSettings.register).toHaveBeenCalledTimes(3); expect(coreSetup.uiSettings.register).toHaveBeenCalledWith({ [PREFERRED_AI_ASSISTANT_TYPE_SETTING_KEY]: { @@ -107,6 +131,25 @@ describe('plugin', () => { value: AIAssistantType.Security, }, }); + + expect(coreSetup.uiSettings.register).toHaveBeenCalledWith( + expect.objectContaining({ + [GEN_AI_SETTINGS_DEFAULT_AI_CONNECTOR]: expect.objectContaining({ + readonlyMode: 'ui', + readonly: false, + value: NO_DEFAULT_CONNECTOR, + }), + }) + ); + expect(coreSetup.uiSettings.register).toHaveBeenCalledWith( + expect.objectContaining({ + [GEN_AI_SETTINGS_DEFAULT_AI_CONNECTOR_DEFAULT_ONLY]: expect.objectContaining({ + readonlyMode: 'ui', + readonly: false, + value: false, + }), + }) + ); }); it('registers correct uiSettings for serverless search', () => { @@ -125,13 +168,31 @@ describe('plugin', () => { }, } as unknown as AIAssistantManagementSelectionPluginServerDependenciesSetup); - expect(coreSetup.uiSettings.register).toHaveBeenCalledTimes(1); + expect(coreSetup.uiSettings.register).toHaveBeenCalledTimes(3); expect(coreSetup.uiSettings.register).toHaveBeenCalledWith({ [PREFERRED_AI_ASSISTANT_TYPE_SETTING_KEY]: { ...classicSetting, value: AIAssistantType.Default, }, }); + expect(coreSetup.uiSettings.register).toHaveBeenCalledWith( + expect.objectContaining({ + [GEN_AI_SETTINGS_DEFAULT_AI_CONNECTOR]: expect.objectContaining({ + readonlyMode: 'ui', + readonly: false, + value: NO_DEFAULT_CONNECTOR, + }), + }) + ); + expect(coreSetup.uiSettings.register).toHaveBeenCalledWith( + expect.objectContaining({ + [GEN_AI_SETTINGS_DEFAULT_AI_CONNECTOR_DEFAULT_ONLY]: expect.objectContaining({ + readonlyMode: 'ui', + readonly: false, + value: false, + }), + }) + ); }); }); @@ -177,13 +238,32 @@ describe('plugin', () => { aiAssistantManagementSelectionPlugin.setup(coreSetup, setupDeps); - expect(coreSetup.uiSettings.register).toHaveBeenCalledTimes(1); + expect(coreSetup.uiSettings.register).toHaveBeenCalledTimes(3); expect(coreSetup.uiSettings.register).toHaveBeenCalledWith({ [PREFERRED_AI_ASSISTANT_TYPE_SETTING_KEY]: { ...classicSetting, value: AIAssistantType.Observability, }, }); + + expect(coreSetup.uiSettings.register).toHaveBeenCalledWith( + expect.objectContaining({ + [GEN_AI_SETTINGS_DEFAULT_AI_CONNECTOR]: expect.objectContaining({ + readonlyMode: 'ui', + readonly: false, + value: NO_DEFAULT_CONNECTOR, + }), + }) + ); + expect(coreSetup.uiSettings.register).toHaveBeenCalledWith( + expect.objectContaining({ + [GEN_AI_SETTINGS_DEFAULT_AI_CONNECTOR_DEFAULT_ONLY]: expect.objectContaining({ + readonlyMode: 'ui', + readonly: false, + value: false, + }), + }) + ); }); }); }); 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 742584ceebafe..0b6b9ad2a9c76 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 @@ -17,6 +17,11 @@ import { DEFAULT_APP_CATEGORIES, } from '@kbn/core/server'; import { KibanaFeatureScope } from '@kbn/features-plugin/common'; +import { + GEN_AI_SETTINGS_DEFAULT_AI_CONNECTOR, + GEN_AI_SETTINGS_DEFAULT_AI_CONNECTOR_DEFAULT_ONLY, +} from '@kbn/management-settings-ids'; +import { schema } from '@kbn/config-schema'; import type { AIAssistantManagementSelectionConfig } from './config'; import type { AIAssistantManagementSelectionPluginServerDependenciesSetup, @@ -25,6 +30,7 @@ import type { AIAssistantManagementSelectionPluginServerStart, } from './types'; import { PREFERRED_AI_ASSISTANT_TYPE_SETTING_KEY } from '../common/ui_setting_keys'; +import { NO_DEFAULT_CONNECTOR } from '../common/constants'; import { classicSetting } from './src/settings/classic_setting'; import { observabilitySolutionSetting } from './src/settings/observability_setting'; import { securitySolutionSetting } from './src/settings/security_setting'; @@ -49,18 +55,6 @@ export class AIAssistantManagementSelectionPlugin core: CoreSetup, plugins: AIAssistantManagementSelectionPluginServerDependenciesSetup ) { - core.capabilities.registerProvider(() => { - return { - management: { - kibana: { - aiAssistantManagementSelection: true, - observabilityAiAssistantManagement: true, - securityAiAssistantManagement: true, - }, - }, - }; - }); - plugins.features?.registerKibanaFeature({ id: 'aiAssistantManagementSelection', name: i18n.translate('aiAssistantManagementSelection.featureRegistry.featureName', { @@ -119,6 +113,36 @@ export class AIAssistantManagementSelectionPlugin core: CoreSetup, plugins: AIAssistantManagementSelectionPluginServerDependenciesSetup ) { + core.uiSettings.register({ + [GEN_AI_SETTINGS_DEFAULT_AI_CONNECTOR]: { + readonlyMode: 'ui', + readonly: false, + schema: schema.string(), + value: NO_DEFAULT_CONNECTOR, + }, + }); + + core.uiSettings.register({ + [GEN_AI_SETTINGS_DEFAULT_AI_CONNECTOR_DEFAULT_ONLY]: { + readonlyMode: 'ui', + readonly: false, + schema: schema.boolean(), + value: false, + }, + }); + + core.capabilities.registerProvider(() => { + return { + management: { + kibana: { + aiAssistantManagementSelection: true, + observabilityAiAssistantManagement: true, + securityAiAssistantManagement: true, + }, + }, + }; + }); + const { cloud } = plugins; const serverlessProjectType = cloud?.serverless.projectType; 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 457f0ef42c6f8..b8f85b0e06e84 100644 --- a/src/platform/plugins/shared/ai_assistant_management/selection/tsconfig.json +++ b/src/platform/plugins/shared/ai_assistant_management/selection/tsconfig.json @@ -21,7 +21,8 @@ "@kbn/doc-links", "@kbn/core-ui-settings-common", "@kbn/cloud-plugin", - "@kbn/licensing-plugin" + "@kbn/licensing-plugin", + "@kbn/management-settings-ids" ], "exclude": ["target/**/*"] } diff --git a/src/platform/plugins/shared/telemetry/schema/oss_platform.json b/src/platform/plugins/shared/telemetry/schema/oss_platform.json index fbc55489f7c58..432339db48d3e 100644 --- a/src/platform/plugins/shared/telemetry/schema/oss_platform.json +++ b/src/platform/plugins/shared/telemetry/schema/oss_platform.json @@ -11545,6 +11545,18 @@ "description": "Non-default value of setting." } }, + "genAiSettings:defaultAIConnector": { + "type": "keyword", + "_meta": { + "description": "Default AI connector" + } + }, + "genAiSettings:defaultAIConnectorOnly": { + "type": "boolean", + "_meta": { + "description": "Restrict to default AI connector only" + } + }, "observability:searchExcludedDataTiers": { "type": "array", "items": { diff --git a/tsconfig.base.json b/tsconfig.base.json index 3a4702cf39f9f..f97e660ca9803 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -20,6 +20,8 @@ "@kbn/ai-assistant-common/*": ["x-pack/platform/packages/shared/ai-assistant/common/*"], "@kbn/ai-assistant-cta": ["x-pack/platform/packages/shared/ai-assistant/ai-assistant-cta"], "@kbn/ai-assistant-cta/*": ["x-pack/platform/packages/shared/ai-assistant/ai-assistant-cta/*"], + "@kbn/ai-assistant-default-llm-setting": ["x-pack/platform/packages/shared/ai-assistant-default-llm-setting"], + "@kbn/ai-assistant-default-llm-setting/*": ["x-pack/platform/packages/shared/ai-assistant-default-llm-setting/*"], "@kbn/ai-assistant-icon": ["x-pack/platform/packages/shared/ai-assistant/icon"], "@kbn/ai-assistant-icon/*": ["x-pack/platform/packages/shared/ai-assistant/icon/*"], "@kbn/ai-assistant-management-plugin": ["src/platform/plugins/shared/ai_assistant_management/selection"], diff --git a/x-pack/platform/packages/shared/ai-assistant-default-llm-setting/README.md b/x-pack/platform/packages/shared/ai-assistant-default-llm-setting/README.md new file mode 100644 index 0000000000000..96f21ba8254af --- /dev/null +++ b/x-pack/platform/packages/shared/ai-assistant-default-llm-setting/README.md @@ -0,0 +1,3 @@ +# @kbn/ai-assistant-default-llm-setting + +UI components for setting to configure default LLM. \ No newline at end of file diff --git a/x-pack/platform/packages/shared/ai-assistant-default-llm-setting/index.ts b/x-pack/platform/packages/shared/ai-assistant-default-llm-setting/index.ts new file mode 100644 index 0000000000000..4b39a28a2bf0d --- /dev/null +++ b/x-pack/platform/packages/shared/ai-assistant-default-llm-setting/index.ts @@ -0,0 +1,12 @@ +/* + * 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. + */ + +export { DefaultAIConnector } from './src/components/default_ai_connector'; +export { + DefaultAiConnectorSettingsContextProvider, + useDefaultAiConnectorSettingContext, +} from './src/context/default_ai_connector_context'; diff --git a/x-pack/platform/packages/shared/ai-assistant-default-llm-setting/jest.config.js b/x-pack/platform/packages/shared/ai-assistant-default-llm-setting/jest.config.js new file mode 100644 index 0000000000000..0f431f0b56e22 --- /dev/null +++ b/x-pack/platform/packages/shared/ai-assistant-default-llm-setting/jest.config.js @@ -0,0 +1,18 @@ +/* + * 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. + */ + +module.exports = { + coverageDirectory: '/target/kibana-coverage/jest/x-pack/packages/kbn_ai_assistant_src', + coverageReporters: ['text', 'html'], + collectCoverageFrom: [ + '/x-pack/platform/packages/shared/kbn-ai-assistant/src/**/*.{ts,tsx}', + '!/x-pack/platform/packages/shared/kbn-ai-assistant/src/*.test.{ts,tsx}', + ], + preset: '@kbn/test', + rootDir: '../../../../..', + roots: ['/x-pack/platform/packages/shared/ai-assistant-default-llm-setting'], +}; diff --git a/x-pack/platform/packages/shared/ai-assistant-default-llm-setting/kibana.jsonc b/x-pack/platform/packages/shared/ai-assistant-default-llm-setting/kibana.jsonc new file mode 100644 index 0000000000000..15a8f7be47529 --- /dev/null +++ b/x-pack/platform/packages/shared/ai-assistant-default-llm-setting/kibana.jsonc @@ -0,0 +1,7 @@ +{ + "type": "shared-browser", + "id": "@kbn/ai-assistant-default-llm-setting", + "owner": ["@elastic/security-generative-ai"], + "group": "platform", + "visibility": "shared" +} diff --git a/x-pack/platform/packages/shared/ai-assistant-default-llm-setting/package.json b/x-pack/platform/packages/shared/ai-assistant-default-llm-setting/package.json new file mode 100644 index 0000000000000..17ae27d19ff1c --- /dev/null +++ b/x-pack/platform/packages/shared/ai-assistant-default-llm-setting/package.json @@ -0,0 +1,7 @@ +{ + "name": "@kbn/ai-assistant-default-llm-setting", + "description": "Default LLM settings for AI Assistant", + "private": true, + "version": "1.0.0", + "license": "Elastic License 2.0" +} diff --git a/x-pack/platform/packages/shared/ai-assistant-default-llm-setting/setup_tests.ts b/x-pack/platform/packages/shared/ai-assistant-default-llm-setting/setup_tests.ts new file mode 100644 index 0000000000000..72e0edd0d07f7 --- /dev/null +++ b/x-pack/platform/packages/shared/ai-assistant-default-llm-setting/setup_tests.ts @@ -0,0 +1,9 @@ +/* + * 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. + */ + +// eslint-disable-next-line import/no-extraneous-dependencies +import '@testing-library/jest-dom'; diff --git a/x-pack/platform/packages/shared/ai-assistant-default-llm-setting/src/components/default_ai_connector.test.tsx b/x-pack/platform/packages/shared/ai-assistant-default-llm-setting/src/components/default_ai_connector.test.tsx new file mode 100644 index 0000000000000..52ef80b816bc4 --- /dev/null +++ b/x-pack/platform/packages/shared/ai-assistant-default-llm-setting/src/components/default_ai_connector.test.tsx @@ -0,0 +1,226 @@ +/* + * 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 '@testing-library/jest-dom'; +import { act, render, screen } from '@testing-library/react'; +import { + AI_ASSISTANT_DEFAULT_LLM_SETTING_ENABLED, + DefaultAIConnector, +} from './default_ai_connector'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { I18nProvider } from '@kbn/i18n-react'; +import userEvent from '@testing-library/user-event'; +import { FieldDefinition, UnsavedFieldChange } from '@kbn/management-settings-types'; +import { UiSettingsType } from '@kbn/core-ui-settings-common'; +import { IToasts } from '@kbn/core-notifications-browser'; +import { ApplicationStart } from '@kbn/core-application-browser'; +import { DocLinksStart } from '@kbn/core-doc-links-browser'; +import React from 'react'; +import { DefaultAiConnectorSettingsContextProvider } from '../context/default_ai_connector_context'; +import { FeatureFlagsStart } from '@kbn/core/public'; + +const mockConnectors = { + loading: false, + reload: jest.fn(), + connectors: [ + { + actionTypeId: 'pre-configured.1', + id: 'pre-configured1', + isDeprecated: false, + isPreconfigured: true, + isSystemAction: false, + name: 'Pre configured Connector', + referencedByCount: 0, + }, + { + actionTypeId: 'custom.1', + id: 'custom1', + isDeprecated: false, + isPreconfigured: false, + isSystemAction: false, + name: 'Custom Connector 1', + referencedByCount: 0, + }, + ], +}; + +function setupTest({ + fields, + unsavedChanges, + enabled = true, +}: { + fields: Record< + string, + FieldDefinition< + UiSettingsType, + string | number | boolean | (string | number)[] | null | undefined + > + >; + unsavedChanges: Record>; + enabled?: boolean; +}) { + const queryClient = new QueryClient(); + const handleFieldChange = jest.fn(); + + const settings = { + handleFieldChange, + fields, + unsavedChanges, + }; + + const utils = render( + <> + + , + { + wrapper: ({ children }) => ( + + + { + if (flag === AI_ASSISTANT_DEFAULT_LLM_SETTING_ENABLED && enabled) { + return true; + } + return false; + }), + } as unknown as FeatureFlagsStart + } + toast={{} as IToasts} + > + {children} + + + + ), + } + ); + + return { + ...utils, + handleFieldChange, + }; +} + +describe('DefaultAIConnector', () => { + describe('rendering', () => { + it('renders all component elements correctly', () => { + const { container } = setupTest({ + fields: {}, + unsavedChanges: {}, + }); + + expect(screen.getByText('genAiSettings:defaultAIConnector')).toBeInTheDocument(); + expect(screen.getByText('Disallow all other connectors')).toBeInTheDocument(); + expect(screen.getByTestId('defaultAiConnectorComboBox')).toBeInTheDocument(); + expect(screen.getByTestId('defaultAiConnectorCheckbox')).toBeInTheDocument(); + + expect(screen.getByTestId('comboBoxSearchInput')).toHaveAttribute( + 'value', + 'No default connector' + ); + expect(container.querySelector('[class$="square-unselected"]')).not.toBeNull(); + }); + + it('does not render when feature flag is off', () => { + setupTest({ + fields: {}, + unsavedChanges: {}, + enabled: false, + }); + + expect(screen.queryByText('genAiSettings:defaultAIConnector')).not.toBeInTheDocument(); + expect(screen.queryByText('Disallow all other connectors')).not.toBeInTheDocument(); + expect(screen.queryByTestId('defaultAiConnectorComboBox')).not.toBeInTheDocument(); + expect(screen.queryByTestId('defaultAiConnectorCheckbox')).not.toBeInTheDocument(); + }); + }); + + describe('combobox interaction', () => { + it('shows connector options when clicked', async () => { + setupTest({ + fields: {}, + unsavedChanges: {}, + }); + + act(() => { + screen.getByTestId('comboBoxSearchInput').click(); + }); + + await userEvent.click(screen.getByTestId('comboBoxSearchInput')); + + expect(screen.getByText('Pre configured Connector')).toBeVisible(); + expect(screen.getByText('Custom Connector 1')).toBeVisible(); + + expect( + // eslint-disable-next-line no-bitwise + screen + .getByText('Pre-configured') + .compareDocumentPosition(screen.getByText('Pre configured Connector')) & + Node.DOCUMENT_POSITION_FOLLOWING + ).toBeTruthy(); + expect( + // eslint-disable-next-line no-bitwise + screen + .getByText('Custom connectors') + .compareDocumentPosition(screen.getByText('Custom Connector 1')) & + Node.DOCUMENT_POSITION_FOLLOWING + ).toBeTruthy(); + + expect( + // eslint-disable-next-line no-bitwise + screen + .getByText('Pre configured Connector') + .compareDocumentPosition(screen.getByText('Custom connectors')) & + Node.DOCUMENT_POSITION_FOLLOWING + ).toBeTruthy(); + }); + + it('updates selection when connector is chosen', async () => { + const { handleFieldChange } = setupTest({ + fields: {}, + unsavedChanges: {}, + }); + + act(() => { + screen.getByTestId('comboBoxSearchInput').click(); + }); + + await userEvent.click(screen.getByTestId('comboBoxSearchInput')); + await userEvent.click(screen.getByText('Custom Connector 1')); + + expect(handleFieldChange).toHaveBeenCalledWith('genAiSettings:defaultAIConnector', { + type: 'string', + unsavedValue: 'custom1', + }); + }); + }); + + describe('checkbox interaction', () => { + it('updates checkbox state when clicked', async () => { + const { handleFieldChange, container } = setupTest({ + fields: {}, + unsavedChanges: {}, + }); + + expect(container.querySelector('[class$="square-unselected"]')).not.toBeNull(); + + await userEvent.click(screen.getByTestId('defaultAiConnectorCheckbox')); + + expect(handleFieldChange).toHaveBeenCalledWith('genAiSettings:defaultAIConnectorOnly', { + type: 'boolean', + unsavedValue: true, + }); + }); + }); +}); diff --git a/x-pack/platform/packages/shared/ai-assistant-default-llm-setting/src/components/default_ai_connector.tsx b/x-pack/platform/packages/shared/ai-assistant-default-llm-setting/src/components/default_ai_connector.tsx new file mode 100644 index 0000000000000..0c54697b68896 --- /dev/null +++ b/x-pack/platform/packages/shared/ai-assistant-default-llm-setting/src/components/default_ai_connector.tsx @@ -0,0 +1,404 @@ +/* + * 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 { EuiComboBoxOptionOption } from '@elastic/eui'; +import { + EuiCheckbox, + EuiComboBox, + EuiDescribedFormGroup, + EuiFlexGroup, + EuiFlexItem, + EuiFormRow, + EuiIconTip, + EuiLink, + EuiTitle, +} from '@elastic/eui'; +import React, { useMemo } from 'react'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { + GEN_AI_SETTINGS_DEFAULT_AI_CONNECTOR, + GEN_AI_SETTINGS_DEFAULT_AI_CONNECTOR_DEFAULT_ONLY, +} from '@kbn/management-settings-ids'; +import type { + FieldDefinition, + OnFieldChangeFn, + UnsavedFieldChange, +} from '@kbn/management-settings-types'; +import type { UiSettingsType } from '@kbn/core/public'; +import { i18n } from '@kbn/i18n'; +import { NO_DEFAULT_CONNECTOR } from '../lib/constants'; +import { useDefaultAiConnectorSettingContext } from '../context/default_ai_connector_context'; + +export const AI_ASSISTANT_DEFAULT_LLM_SETTING_ENABLED = + 'aiAssistant.defaultLlmSettingEnabled' as const; + +interface ConnectorData { + connectors?: Array<{ + id: string; + name: string; + isPreconfigured: boolean; + actionTypeId: string; + config?: Record; + }>; + loading: boolean; +} + +const hasElasticManagedLlm = (connectors: ConnectorData['connectors'] | undefined) => { + if (!Array.isArray(connectors) || connectors.length === 0) { + return false; + } + + return connectors.find( + (connector) => + connector.actionTypeId === '.inference' && + connector.isPreconfigured && + connector.config?.provider === 'elastic' + ); +}; + +interface Props { + settings: { + unsavedChanges: Record>; + handleFieldChange: OnFieldChangeFn; + fields: Record< + string, + FieldDefinition< + UiSettingsType, + string | number | boolean | (string | number)[] | null | undefined + > + >; + }; + connectors: ConnectorData; +} + +const NoDefaultOption: EuiComboBoxOptionOption = { + label: i18n.translate( + 'xpack.gen_ai_settings.settings.defaultLLm.select.option.noDefaultConnector', + { defaultMessage: 'No default connector' } + ), + value: NO_DEFAULT_CONNECTOR, +}; + +const getOptions = (connectors: ConnectorData): EuiComboBoxOptionOption[] => { + const preconfigured = + connectors.connectors + ?.filter((connector) => connector.isPreconfigured) + .map((connector) => ({ + label: connector.name, + value: connector.id, + })) ?? []; + + const custom = + connectors.connectors + ?.filter((connector) => !connector.isPreconfigured) + .map((connector) => ({ + label: connector.name, + value: connector.id, + })) ?? []; + + return [ + NoDefaultOption, + { + label: i18n.translate( + 'xpack.gen_ai_settings.settings.defaultLLm.select.group.preconfigured.label', + { defaultMessage: 'Pre-configured' } + ), + value: 'preconfigured', + options: preconfigured, + }, + { + label: i18n.translate('xpack.gen_ai_settings.settings.defaultLLm.select.group.custom.label', { + defaultMessage: 'Custom connectors', + }), + value: 'custom', + options: custom, + }, + ]; +}; + +const getOptionsByValues = ( + value: string, + options: EuiComboBoxOptionOption[] +): EuiComboBoxOptionOption[] => { + const getOptionsByValuesHelper = ( + option: EuiComboBoxOptionOption + ): EuiComboBoxOptionOption[] => { + if (option.options === undefined && option.value === value) { + // If the option has no sub-options and its value is in the selected values, include it + return [option]; + } + if (option.options) { + // If the option has sub-options, recursively get their options + return option.options.flatMap(getOptionsByValuesHelper); + } + return []; + }; + + return options.flatMap(getOptionsByValuesHelper); +}; + +export const DefaultAIConnector: React.FC = ({ connectors, settings }) => { + const { toast, application, docLinks, featureFlags } = useDefaultAiConnectorSettingContext(); + const options = useMemo(() => getOptions(connectors), [connectors]); + const { handleFieldChange, fields, unsavedChanges } = settings; + + const onChangeDefaultLlm = (selectedOptions: EuiComboBoxOptionOption[]) => { + const values = selectedOptions.map((option) => option.value); + if (values.length > 1) { + toast?.addDanger({ + title: i18n.translate( + 'xpack.observabilityAiAssistantManagement.defaultLlm.onChange.error.multipleSelected.title', + { + defaultMessage: 'An error occurred while changing the setting', + } + ), + text: i18n.translate( + 'xpack.observabilityAiAssistantManagement.defaultLlm.onChange.error.multipleSelected.text', + { + defaultMessage: 'Only one default AI connector can be selected', + } + ), + }); + throw new Error('Only one default AI connector can be selected'); + } + const value = values[0] ?? NO_DEFAULT_CONNECTOR; + + if (value === fields[GEN_AI_SETTINGS_DEFAULT_AI_CONNECTOR]?.savedValue) { + handleFieldChange(GEN_AI_SETTINGS_DEFAULT_AI_CONNECTOR); + return; + } + + handleFieldChange(GEN_AI_SETTINGS_DEFAULT_AI_CONNECTOR, { + type: 'string', + unsavedValue: value, + }); + }; + + const onChangeDefaultOnly = (checked: boolean) => { + if (checked === fields[GEN_AI_SETTINGS_DEFAULT_AI_CONNECTOR_DEFAULT_ONLY]?.savedValue) { + handleFieldChange(GEN_AI_SETTINGS_DEFAULT_AI_CONNECTOR_DEFAULT_ONLY); + return; + } + + handleFieldChange(GEN_AI_SETTINGS_DEFAULT_AI_CONNECTOR_DEFAULT_ONLY, { + type: 'boolean', + unsavedValue: checked, + }); + }; + + const defaultLlmValues = getDefaultLlmValue(unsavedChanges, fields); + + const selectedOptions = useMemo( + () => getOptionsByValues(defaultLlmValues, options), + [defaultLlmValues, options] + ); + + const defaultLlmOnlyValue = getDefaultLlmOnlyValue(unsavedChanges, fields); + + const elasticManagedLlmExists = hasElasticManagedLlm(connectors.connectors); + + const connectorDescription = useMemo(() => { + if (!elasticManagedLlmExists) { + return ( +

+ + + + ), + }} + /> +

+ ); + } + + return ( +

+ + + + ), + manageConnectors: ( + + + + ), + elasticManagedLlm: ( + + + + ), + }} + /> +

+ ); + }, [elasticManagedLlmExists, application, docLinks]); + + if (!featureFlags.getBooleanValue(AI_ASSISTANT_DEFAULT_LLM_SETTING_ENABLED, false)) { + return null; + } + + return ( + <> + + + +

+ +

+
+
+ + } + description={connectorDescription} + > + + + + + + + + + + + + } + checked={defaultLlmOnlyValue} + onChange={(e) => onChangeDefaultOnly(e.target.checked)} + /> + + + + + + + + + + +
+ + ); +}; + +/** + * Gets current value for the default LLM connector. First checks for unsaved changes, then saved, then default. + */ +function getDefaultLlmValue( + unsavedChanges: Record>, + fields: Record> +) { + const defaultLlmUnsavedValue = unsavedChanges[GEN_AI_SETTINGS_DEFAULT_AI_CONNECTOR] + ?.unsavedValue as string | undefined; + const defaultLlmSavedValue = fields[GEN_AI_SETTINGS_DEFAULT_AI_CONNECTOR]?.savedValue as + | string + | undefined; + const defaultLlmDefaultValue = fields[GEN_AI_SETTINGS_DEFAULT_AI_CONNECTOR]?.defaultValue as + | string + | undefined; + + const defaultLlmValue = + defaultLlmUnsavedValue ?? + defaultLlmSavedValue ?? + defaultLlmDefaultValue ?? + NO_DEFAULT_CONNECTOR; + return defaultLlmValue; +} + +/** + * Gets current value for the default LLM only setting. First checks for unsaved changes, then saved, then default. + */ +function getDefaultLlmOnlyValue( + unsavedChanges: Record>, + fields: Record> +): boolean { + const defaultLlmOnlyUnsavedValue = unsavedChanges[ + GEN_AI_SETTINGS_DEFAULT_AI_CONNECTOR_DEFAULT_ONLY + ]?.unsavedValue as boolean | undefined; + const defaultLlmOnlySavedValue = fields[GEN_AI_SETTINGS_DEFAULT_AI_CONNECTOR_DEFAULT_ONLY] + ?.savedValue as boolean | undefined; + const defaultLlmOnlyDefaultValue = fields[GEN_AI_SETTINGS_DEFAULT_AI_CONNECTOR_DEFAULT_ONLY] + ?.defaultValue as boolean | undefined; + + const defaultLlmOnlyValue = + defaultLlmOnlyUnsavedValue ?? defaultLlmOnlySavedValue ?? defaultLlmOnlyDefaultValue ?? false; + return defaultLlmOnlyValue; +} diff --git a/x-pack/platform/packages/shared/ai-assistant-default-llm-setting/src/context/default_ai_connector_context.tsx b/x-pack/platform/packages/shared/ai-assistant-default-llm-setting/src/context/default_ai_connector_context.tsx new file mode 100644 index 0000000000000..b8a5ca9c5def6 --- /dev/null +++ b/x-pack/platform/packages/shared/ai-assistant-default-llm-setting/src/context/default_ai_connector_context.tsx @@ -0,0 +1,75 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { createContext, useContext } from 'react'; +import type { ApplicationStart, DocLinksStart, FeatureFlagsStart, IToasts } from '@kbn/core/public'; + +type DefaultAiConnectorSettingContext = ReturnType; + +const DefaultAiConnectorSettingContext = createContext( + null +); + +export const useDefaultAiConnectorSettingContext = () => { + const context = useContext(DefaultAiConnectorSettingContext); + if (!context) { + throw new Error( + 'useDefaultAiConnectorContext must be inside of a SettingsContextProvider.Provider.' + ); + } + return context; +}; + +export const DefaultAiConnectorSettingsContextProvider = ({ + children, + toast, + application, + docLinks, + featureFlags, +}: { + children: React.ReactNode; + toast: IToasts | undefined; + application: ApplicationStart; + docLinks: DocLinksStart; + featureFlags: FeatureFlagsStart; +}) => { + const value = DefaultAiConnector({ + toast, + application, + docLinks, + featureFlags, + }); + return ( + + {children} + + ); +}; + +const DefaultAiConnector = ({ + toast, + application, + docLinks, + featureFlags, +}: { + toast: IToasts | undefined; + application: ApplicationStart; + docLinks: DocLinksStart; + featureFlags: FeatureFlagsStart; +}) => { + return { + toast, + application, + docLinks, + featureFlags, + }; +}; + +export { + DefaultAiConnectorSettingContext as SettingsContext, + useDefaultAiConnectorSettingContext as useSettingsContext, +}; diff --git a/x-pack/platform/packages/shared/ai-assistant-default-llm-setting/src/lib/constants.ts b/x-pack/platform/packages/shared/ai-assistant-default-llm-setting/src/lib/constants.ts new file mode 100644 index 0000000000000..1cfad15d32c6f --- /dev/null +++ b/x-pack/platform/packages/shared/ai-assistant-default-llm-setting/src/lib/constants.ts @@ -0,0 +1,8 @@ +/* + * 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. + */ + +export const NO_DEFAULT_CONNECTOR = 'NO_DEFAULT_CONNECTOR'; diff --git a/x-pack/platform/packages/shared/ai-assistant-default-llm-setting/tsconfig.json b/x-pack/platform/packages/shared/ai-assistant-default-llm-setting/tsconfig.json new file mode 100644 index 0000000000000..88a03c24103dc --- /dev/null +++ b/x-pack/platform/packages/shared/ai-assistant-default-llm-setting/tsconfig.json @@ -0,0 +1,32 @@ +{ + "extends": "../../../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "target/types", + "types": [ + "jest", + "node", + "react", + "@emotion/react/types/css-prop" + ] + }, + "include": [ + "**/*.ts", + "**/*.tsx", + ".storybook/**/*.ts", + ".storybook/**/*.tsx", + ], + "exclude": [ + "target/**/*" + ], + "kbn_references": [ + "@kbn/i18n-react", + "@kbn/management-settings-types", + "@kbn/core-ui-settings-common", + "@kbn/core-notifications-browser", + "@kbn/core-application-browser", + "@kbn/core-doc-links-browser", + "@kbn/management-settings-ids", + "@kbn/core", + "@kbn/i18n", + ] +} diff --git a/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/settings/assistant_settings_management.test.tsx b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/settings/assistant_settings_management.test.tsx index 077e0bb38edfb..756b407251db9 100644 --- a/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/settings/assistant_settings_management.test.tsx +++ b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/settings/assistant_settings_management.test.tsx @@ -25,6 +25,7 @@ import { SYSTEM_PROMPTS_TAB, } from './const'; import { DataViewsContract } from '@kbn/data-views-plugin/public'; +import { SettingsStart } from '@kbn/core-ui-settings-browser'; const mockContext = { basePromptContexts: MOCK_QUICK_PROMPTS, @@ -48,6 +49,7 @@ const testProps = { dataViews: mockDataViews, onTabChange, currentTab: CONNECTORS_TAB, + settings: {} as SettingsStart, }; jest.mock('../../assistant_context'); diff --git a/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/settings/assistant_settings_management.tsx b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/settings/assistant_settings_management.tsx index 2d1b458eeebe3..fd1c78140f6df 100644 --- a/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/settings/assistant_settings_management.tsx +++ b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/settings/assistant_settings_management.tsx @@ -9,6 +9,7 @@ import React, { useEffect, useMemo } from 'react'; import { EuiAvatar, EuiPageTemplate, EuiTitle, useEuiShadow, useEuiTheme } from '@elastic/eui'; import { css } from '@emotion/react'; import { DataViewsContract } from '@kbn/data-views-plugin/public'; +import { SettingsStart } from '@kbn/core-ui-settings-browser'; import * as i18n from './translations'; import { useAssistantContext } from '../../assistant_context'; import { useLoadConnectors } from '../../connectorland/use_load_connectors'; @@ -34,6 +35,7 @@ import { ManagementSettingsTabs } from './types'; interface Props { dataViews: DataViewsContract; + settings: SettingsStart; onTabChange?: (tabId: string) => void; currentTab: ManagementSettingsTabs; } @@ -43,7 +45,7 @@ interface Props { * anonymization, knowledge base, and evaluation via the `isModelEvaluationEnabled` feature flag. */ export const AssistantSettingsManagement: React.FC = React.memo( - ({ dataViews, onTabChange, currentTab: selectedSettingsTab }) => { + ({ dataViews, onTabChange, currentTab: selectedSettingsTab, settings }) => { const { assistantFeatures: { assistantModelEvaluation: modelEvaluatorEnabled }, http, @@ -154,7 +156,9 @@ export const AssistantSettingsManagement: React.FC = React.memo( `} data-test-subj={`tab-${selectedSettingsTab}`} > - {selectedSettingsTab === CONNECTORS_TAB && } + {selectedSettingsTab === CONNECTORS_TAB && ( + + )} {selectedSettingsTab === CONVERSATIONS_TAB && ( void; currentTab: ManagementSettingsTabs; + settings: SettingsStart; } /** @@ -49,7 +51,7 @@ interface Props { * anonymization, knowledge base, and evaluation via the `isModelEvaluationEnabled` feature flag. */ export const SearchAILakeConfigurationsSettingsManagement: React.FC = React.memo( - ({ dataViews, onTabChange, currentTab }) => { + ({ dataViews, onTabChange, currentTab, settings }) => { const { assistantFeatures: { assistantModelEvaluation: modelEvaluatorEnabled }, http, @@ -122,7 +124,9 @@ export const SearchAILakeConfigurationsSettingsManagement: React.FC = Rea const renderTabBody = useCallback(() => { switch (currentTab) { case CONNECTORS_TAB: - return ; + return ( + + ); case SYSTEM_PROMPTS_TAB: return ( = Rea /> ); } - }, [connectors, currentTab, dataViews, defaultConnector, modelEvaluatorEnabled]); + }, [connectors, currentTab, dataViews, defaultConnector, modelEvaluatorEnabled, settings]); return ( { +interface Props { + connectors: AIConnector[] | undefined; + settings: SettingsStart; +} + +export const AIForSOCConnectorSettingsManagement = ({ connectors, settings }: Props) => { return ( <> - + diff --git a/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/connectorland/connector_settings_management/bottom_bar_actions/bottom_bar_actions.test.tsx b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/connectorland/connector_settings_management/bottom_bar_actions/bottom_bar_actions.test.tsx new file mode 100644 index 0000000000000..bc23ef4e63716 --- /dev/null +++ b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/connectorland/connector_settings_management/bottom_bar_actions/bottom_bar_actions.test.tsx @@ -0,0 +1,52 @@ +/* + * 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 { render, screen } from '@testing-library/react'; +import { BottomBarActions } from './bottom_bar_actions'; +import React from 'react'; +import { __IntlProvider as IntlProvider } from '@kbn/i18n-react'; + +describe('bottom_bar_actions', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + function Providers({ children }: { children: React.ReactNode }) { + return ( + + {children} + + ); + } + + it('renders correctly', () => { + const onDiscardChanges = jest.fn(); + const onSave = jest.fn(); + render( + , + { wrapper: Providers } + ); + + expect(screen.getByTestId('genAiSettingsBottomBar')).toBeInTheDocument(); + expect(screen.getByText('5 unsaved changes')).toBeInTheDocument(); + expect(screen.getByText('Save Changes')).toBeInTheDocument(); + expect(screen.getByText('Discard changes')).toBeInTheDocument(); + + expect(onDiscardChanges).not.toHaveBeenCalled(); + screen.getByText('Discard changes').click(); + expect(onDiscardChanges).toHaveBeenCalled(); + + expect(onSave).not.toHaveBeenCalled(); + }); +}); diff --git a/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/connectorland/connector_settings_management/bottom_bar_actions/bottom_bar_actions.tsx b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/connectorland/connector_settings_management/bottom_bar_actions/bottom_bar_actions.tsx new file mode 100644 index 0000000000000..1b95a6afb31cf --- /dev/null +++ b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/connectorland/connector_settings_management/bottom_bar_actions/bottom_bar_actions.tsx @@ -0,0 +1,103 @@ +/* + * 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 { + EuiBottomBar, + EuiButton, + EuiButtonEmpty, + EuiFlexGroup, + EuiFlexItem, + EuiHealth, + EuiText, + EuiToolTip, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import React from 'react'; +import { FormattedMessage } from '@kbn/i18n-react'; + +interface Props { + unsavedChangesCount: number; + isLoading: boolean; + onDiscardChanges: () => void; + onSave: () => void; + saveLabel: string; + appTestSubj: string; + areChangesInvalid?: boolean; +} + +export const BottomBarActions = ({ + isLoading, + onDiscardChanges, + onSave, + unsavedChangesCount, + saveLabel, + appTestSubj, + areChangesInvalid = false, +}: Props) => { + return ( + + + + + + + + + + + + + + + + + + + {saveLabel} + + + + + + + + ); +}; diff --git a/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/connectorland/connector_settings_management/context/settings_context.test.tsx b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/connectorland/connector_settings_management/context/settings_context.test.tsx new file mode 100644 index 0000000000000..bf998ca41fbe2 --- /dev/null +++ b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/connectorland/connector_settings_management/context/settings_context.test.tsx @@ -0,0 +1,175 @@ +/* + * 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 { act, renderHook, waitFor } from '@testing-library/react'; +import { SettingsContextProvider, useSettingsContext } from './settings_context'; +import React from 'react'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import type { PublicUiSettingsParams, UserProvidedValues } from '@kbn/core/public'; +import { Subject } from 'rxjs'; +import { SettingsStart } from '@kbn/core-ui-settings-browser'; + +describe('settings_context', () => { + const setupSettingsContext = () => { + const queryClient = new QueryClient(); + const set = jest.fn().mockResolvedValue(undefined); + + const rendered = renderHook(() => useSettingsContext(), { + wrapper: ({ children }) => { + return ( + + new Subject(), + isOverridden: () => false, + isCustom: () => false, + set, + getAll: jest.fn().mockReturnValue({ + 'genAiSettings:defaultAIConnector': { + readonlyMode: 'ui', + value: 'NO_DEFAULT_CONNECTOR', + userValue: 'pmeClaudeV37SonnetUsEast1', + }, + 'genAiSettings:defaultAIConnectorOnly': { + readonlyMode: 'ui', + value: false, + userValue: true, + }, + } as Record), + }, + } as unknown as SettingsStart + } + > + {children} + + + ); + }, + }); + + return { result: rendered.result, set }; + }; + + it('should provide the correct initial state', async () => { + const { result } = setupSettingsContext(); + + await waitFor(() => { + expect(result.current.fields).toEqual( + expect.objectContaining({ + 'genAiSettings:defaultAIConnector': expect.anything(), + 'genAiSettings:defaultAIConnectorOnly': expect.anything(), + }) + ); + }); + + expect(result.current.unsavedChanges).toEqual({}); + expect(result.current.handleFieldChange).toBeInstanceOf(Function); + expect(result.current.saveAll).toBeInstanceOf(Function); + expect(result.current.cleanUnsavedChanges).toBeInstanceOf(Function); + expect(result.current.saveSingleSetting).toBeInstanceOf(Function); + }); + + it('should handle updating unsaved changes', async () => { + const { result } = setupSettingsContext(); + + await waitFor(() => { + expect(result.current.fields).toBeDefined(); + }); + + expect(result.current.unsavedChanges).toEqual({}); + + act(() => { + result.current.handleFieldChange('test', { + type: 'string', + unsavedValue: 'testValue', + }); + }); + + expect(result.current.unsavedChanges).toEqual({ + test: { + type: 'string', + unsavedValue: 'testValue', + }, + }); + }); + + it('should save unsaved changes', async () => { + const { result, set } = setupSettingsContext(); + + await waitFor(() => { + expect(result.current.fields).toBeDefined(); + }); + + act(() => { + result.current.handleFieldChange('test', { + type: 'string', + unsavedValue: 'testValue', + }); + }); + + expect(set).toHaveBeenCalledTimes(0); + + await act(async () => { + await result.current.saveAll(); + }); + + expect(set).toHaveBeenCalledTimes(1); + + await waitFor(() => { + expect(result.current.unsavedChanges).toEqual({}); + }); + }); + + it('should save single setting', async () => { + const { result, set } = setupSettingsContext(); + + await waitFor(() => { + expect(result.current.fields).toBeDefined(); + }); + + expect(set).toHaveBeenCalledTimes(0); + + await act(async () => { + await result.current.saveSingleSetting({ + id: 'foo', + change: 'bar', + }); + }); + + expect(set).toHaveBeenCalledTimes(1); + }); + + it('should revert unsaved changes', async () => { + const { result } = setupSettingsContext(); + + await waitFor(() => { + expect(result.current.fields).toBeDefined(); + }); + + act(() => { + result.current.handleFieldChange('test', { + type: 'string', + unsavedValue: 'testValue', + }); + }); + + expect(result.current.unsavedChanges).toEqual({ + test: { + type: 'string', + unsavedValue: 'testValue', + }, + }); + + act(() => { + result.current.cleanUnsavedChanges(); + }); + + expect(result.current.unsavedChanges).toEqual({}); + }); +}); diff --git a/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/connectorland/connector_settings_management/context/settings_context.tsx b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/connectorland/connector_settings_management/context/settings_context.tsx new file mode 100644 index 0000000000000..c5612ba8ca528 --- /dev/null +++ b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/connectorland/connector_settings_management/context/settings_context.tsx @@ -0,0 +1,172 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { createContext, useContext } from 'react'; +import type { + FieldDefinition, + OnFieldChangeFn, + UiSettingMetadata, + UnsavedFieldChange, +} from '@kbn/management-settings-types'; +import { isEmpty } from 'lodash'; +import type { IUiSettingsClient, UiSettingsType } from '@kbn/core/public'; +import { normalizeSettings } from '@kbn/management-settings-utilities'; +import { getFieldDefinition } from '@kbn/management-settings-field-definition'; +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; +import { + GEN_AI_SETTINGS_DEFAULT_AI_CONNECTOR, + GEN_AI_SETTINGS_DEFAULT_AI_CONNECTOR_DEFAULT_ONLY, +} from '@kbn/management-settings-ids'; +import { SettingsStart } from '@kbn/core-ui-settings-browser'; + +type SettingsContext = ReturnType; + +const SettingsContext = createContext(null); + +const useSettingsContext = () => { + const context = useContext(SettingsContext); + if (!context) { + throw new Error('useSettingsContext must be inside of a SettingsContextProvider.Provider.'); + } + return context; +}; + +const SETTING_KEYS = [ + GEN_AI_SETTINGS_DEFAULT_AI_CONNECTOR, + GEN_AI_SETTINGS_DEFAULT_AI_CONNECTOR_DEFAULT_ONLY, +]; + +export const SettingsContextProvider = ({ + children, + settings, +}: { + children: React.ReactNode; + settings: SettingsStart; +}) => { + const value = Settings({ settingsKeys: SETTING_KEYS, settings }); + return {children}; +}; + +function combineErrors(errors: Error[]): Error { + const message = errors.map((err) => err.message || String(err)).join('; '); + return new Error(message); +} + +function getSettingsFields({ + settingsKeys, + uiSettings, +}: { + settingsKeys: string[]; + uiSettings?: IUiSettingsClient; +}) { + if (!uiSettings) { + return {}; + } + + const uiSettingsDefinition = uiSettings.getAll(); + const normalizedSettings = normalizeSettings(uiSettingsDefinition); + + return settingsKeys.reduce>((acc, key) => { + const setting: UiSettingMetadata = normalizedSettings[key]; + if (setting) { + const field = getFieldDefinition({ + id: key, + setting, + params: { isCustom: uiSettings.isCustom(key), isOverridden: uiSettings.isOverridden(key) }, + }); + acc[key] = field; + } + return acc; + }, {}); +} + +const Settings = ({ + settingsKeys, + settings, +}: { + settingsKeys: string[]; + settings: SettingsStart; +}) => { + const [unsavedChanges, setUnsavedChanges] = React.useState>( + {} + ); + + const queryClient = useQueryClient(); + + const fieldsQuery = useQuery({ + queryKey: ['settingsFields', settingsKeys], + queryFn: async () => { + return getSettingsFields({ settingsKeys, uiSettings: settings?.client }); + }, + refetchOnWindowFocus: true, + }); + + const saveSingleSettingMutation = useMutation({ + mutationFn: async ({ + id, + change, + }: { + id: string; + change: UnsavedFieldChange['unsavedValue']; + }) => { + await settings.client.set(id, change); + queryClient.invalidateQueries({ queryKey: ['settingsFields', settingsKeys] }); + }, + }); + + const saveAllMutation = useMutation({ + mutationFn: async () => { + if (settings && !isEmpty(unsavedChanges)) { + const updateErrors: Error[] = []; + const subscription = settings.client.getUpdateErrors$().subscribe((error) => { + updateErrors.push(error); + }); + try { + await Promise.all( + Object.entries(unsavedChanges).map(([key, value]) => { + return settings.client.set(key, value.unsavedValue); + }) + ); + queryClient.invalidateQueries({ queryKey: ['settingsFields', settingsKeys] }); + cleanUnsavedChanges(); + if (updateErrors.length > 0) { + throw combineErrors(updateErrors); + } + } finally { + if (subscription) { + subscription.unsubscribe(); + } + } + } + }, + }); + + const handleFieldChange: OnFieldChangeFn = (id, change) => { + if (!change) { + const { [id]: unsavedChange, ...rest } = unsavedChanges; + setUnsavedChanges(rest); + return; + } + setUnsavedChanges((changes) => ({ ...changes, [id]: change })); + }; + + function cleanUnsavedChanges() { + setUnsavedChanges({}); + } + + return { + fields: fieldsQuery.data ?? {}, + unsavedChanges, + handleFieldChange, + saveAll: saveAllMutation.mutateAsync, + isSaving: saveAllMutation.isLoading || saveSingleSettingMutation.isLoading, + cleanUnsavedChanges, + saveSingleSetting: saveSingleSettingMutation.mutateAsync, + }; +}; + +export { SettingsContext, useSettingsContext }; diff --git a/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/connectorland/connector_settings_management/index.tsx b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/connectorland/connector_settings_management/index.tsx index afa8bd48567a2..fc0e02a6d3836 100644 --- a/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/connectorland/connector_settings_management/index.tsx +++ b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/connectorland/connector_settings_management/index.tsx @@ -7,20 +7,29 @@ import { EuiButton, + EuiDescribedFormGroup, EuiFlexGroup, EuiFlexItem, + EuiFormRow, EuiPanel, - EuiSpacer, - EuiText, EuiTitle, } from '@elastic/eui'; -import { css } from '@emotion/react'; import React, { useCallback } from 'react'; +import { DefaultAIConnector } from '@kbn/ai-assistant-default-llm-setting'; +import { isEmpty } from 'lodash'; +import { SettingsStart } from '@kbn/core-ui-settings-browser'; import { useAssistantContext } from '../../assistant_context'; - import * as i18n from './translations'; +import { SettingsContextProvider, useSettingsContext } from './context/settings_context'; +import { BottomBarActions } from './bottom_bar_actions/bottom_bar_actions'; +import { AIConnector } from '../connector_selector'; + +interface Props { + connectors: AIConnector[] | undefined; + settings: SettingsStart; +} -const ConnectorsSettingsManagementComponent: React.FC = () => { +const ConnectorsSettingsManagementComponent: React.FC = ({ connectors, settings }) => { const { navigateToApp } = useAssistantContext(); const onClick = useCallback( @@ -32,25 +41,84 @@ const ConnectorsSettingsManagementComponent: React.FC = () => { ); return ( - - -

{i18n.CONNECTOR_SETTINGS_MANAGEMENT_TITLE}

-
- - - + + + + +

+ {i18n.CONNECTOR_SETTINGS_MANAGEMENT_TITLE} +

+
+
+
+ } + description={i18n.CONNECTOR_SETTINGS_MANAGEMENT_DESCRIPTION} > - {i18n.CONNECTOR_SETTINGS_MANAGEMENT_DESCRIPTION} - - - - {i18n.CONNECTOR_MANAGEMENT_BUTTON_TITLE} - -
- + + + + {i18n.CONNECTOR_MANAGEMENT_BUTTON_TITLE} + + + + + + + + + + ); +}; + +export const DefaultAIConnectorHoc: React.FC> = ({ connectors }) => { + const { fields, handleFieldChange, unsavedChanges } = useSettingsContext(); + + return ( + + ); +}; + +export const BottomBarActionsHoc = () => { + const { unsavedChanges, cleanUnsavedChanges, isSaving, saveAll } = useSettingsContext(); + const { toasts } = useAssistantContext(); + if (isEmpty(unsavedChanges)) { + return null; + } + + async function handleSave() { + try { + await saveAll(); + } catch (e) { + const error = e as Error; + + toasts?.addDanger({ + title: i18n.BOTTOM_BAR_ACTIONS_SAVE_ERROR, + text: error.message, + }); + throw error; + } + } + + return ( + ); }; diff --git a/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/connectorland/connector_settings_management/translations.ts b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/connectorland/connector_settings_management/translations.ts index 505a78fcf44f7..5d0aedb348a4c 100644 --- a/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/connectorland/connector_settings_management/translations.ts +++ b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/connectorland/connector_settings_management/translations.ts @@ -28,3 +28,17 @@ export const CONNECTOR_MANAGEMENT_BUTTON_TITLE = i18n.translate( defaultMessage: 'Manage Connectors', } ); + +export const BOTTOM_BAR_ACTIONS_SAVE_LABEL = i18n.translate( + 'xpack.elasticAssistant.settings.bottomBar.action.saveButton', + { + defaultMessage: 'Save changes', + } +); + +export const BOTTOM_BAR_ACTIONS_SAVE_ERROR = i18n.translate( + 'xpack.elasticAssistant.settings.bottomBar.action.save.error', + { + defaultMessage: 'An error occurred while saving the settings', + } +); diff --git a/x-pack/platform/packages/shared/kbn-elastic-assistant/tsconfig.json b/x-pack/platform/packages/shared/kbn-elastic-assistant/tsconfig.json index 2be1fd05e9cf1..842a80ea28192 100644 --- a/x-pack/platform/packages/shared/kbn-elastic-assistant/tsconfig.json +++ b/x-pack/platform/packages/shared/kbn-elastic-assistant/tsconfig.json @@ -46,5 +46,11 @@ "@kbn/deeplinks-security", "@kbn/inference-common", "@kbn/core-doc-links-browser-mocks", + "@kbn/management-settings-types", + "@kbn/management-settings-utilities", + "@kbn/management-settings-field-definition", + "@kbn/management-settings-ids", + "@kbn/ai-assistant-default-llm-setting", + "@kbn/core-ui-settings-browser", ] } diff --git a/x-pack/platform/plugins/private/observability_ai_assistant_management/public/routes/components/settings_tab/settings_tab.test.tsx b/x-pack/platform/plugins/private/observability_ai_assistant_management/public/routes/components/settings_tab/settings_tab.test.tsx index 1c55a275c46d0..ea57721dcab24 100644 --- a/x-pack/platform/plugins/private/observability_ai_assistant_management/public/routes/components/settings_tab/settings_tab.test.tsx +++ b/x-pack/platform/plugins/private/observability_ai_assistant_management/public/routes/components/settings_tab/settings_tab.test.tsx @@ -46,6 +46,14 @@ describe('SettingsTab', () => { }); useKibanaMock.mockReturnValue({ services: { + featureFlags: { + getBooleanValue: jest.fn().mockImplementation((flag) => { + if (flag === 'aiAssistant.defaultLlmSettingEnabled') { + return true; + } + return false; + }), + }, application: { getUrlForApp: getUrlForAppMock, capabilities: { @@ -75,7 +83,17 @@ describe('SettingsTab', () => { installProductDoc: jest.fn().mockResolvedValue({}), uninstallProductDoc: jest.fn().mockResolvedValue({}), }); - useGenAIConnectorsMock.mockReturnValue({ connectors: [{ id: 'test-connector' }] }); + useGenAIConnectorsMock.mockReturnValue({ + connectors: [ + { + id: 'test-connector', + name: 'Test Connector', + isPreconfigured: false, + actionTypeId: 'test-action-type', + }, + ], + loading: false, + }); useInferenceEndpointsMock.mockReturnValue({ inferenceEndpoints: [{ id: 'test-endpoint', inference_id: 'test-inference-id' }], isLoading: false, @@ -120,7 +138,7 @@ describe('SettingsTab', () => { }); it('should not show knowledge base model section when no connectors exist', () => { - useGenAIConnectorsMock.mockReturnValue({ connectors: [] }); + useGenAIConnectorsMock.mockReturnValue({ connectors: [], loading: false }); const { queryByTestId } = render(, { coreStart: { diff --git a/x-pack/platform/plugins/private/observability_ai_assistant_management/public/routes/components/settings_tab/ui_settings.tsx b/x-pack/platform/plugins/private/observability_ai_assistant_management/public/routes/components/settings_tab/ui_settings.tsx index b746ce4edc9fb..8ca723396f705 100644 --- a/x-pack/platform/plugins/private/observability_ai_assistant_management/public/routes/components/settings_tab/ui_settings.tsx +++ b/x-pack/platform/plugins/private/observability_ai_assistant_management/public/routes/components/settings_tab/ui_settings.tsx @@ -18,19 +18,23 @@ import { isEmpty } from 'lodash'; import { i18n } from '@kbn/i18n'; import { LogSourcesSettingSynchronisationInfo } from '@kbn/logs-data-access-plugin/public'; import { UseKnowledgeBaseResult } from '@kbn/ai-assistant'; +import { + GEN_AI_SETTINGS_DEFAULT_AI_CONNECTOR, + GEN_AI_SETTINGS_DEFAULT_AI_CONNECTOR_DEFAULT_ONLY, +} from '@kbn/management-settings-ids'; +import { DefaultAIConnector } from '@kbn/ai-assistant-default-llm-setting'; +import { useGenAIConnectors } from '@kbn/ai-assistant/src/hooks'; +import { DefaultAiConnectorSettingsContextProvider } from '@kbn/ai-assistant-default-llm-setting/src/context/default_ai_connector_context'; import { useEditableSettings } from '../../../hooks/use_editable_settings'; import { useAppContext } from '../../../hooks/use_app_context'; import { useKibana } from '../../../hooks/use_kibana'; import { BottomBarActions } from '../bottom_bar_actions/bottom_bar_actions'; export function UISettings({ knowledgeBase }: { knowledgeBase: UseKnowledgeBaseResult }) { - const { - docLinks, - settings, - notifications, - application: { capabilities, getUrlForApp }, - } = useKibana().services; + const { docLinks, settings, notifications, application, featureFlags } = useKibana().services; + const { capabilities, getUrlForApp } = application; const { config } = useAppContext(); + const connectors = useGenAIConnectors(); const settingsKeys = [ aiAnonymizationSettings, @@ -39,8 +43,13 @@ export function UISettings({ knowledgeBase }: { knowledgeBase: UseKnowledgeBaseR ...(config.visibilityEnabled ? [aiAssistantPreferredAIAssistantType] : []), ]; + const customComponentSettingsKeys = [ + GEN_AI_SETTINGS_DEFAULT_AI_CONNECTOR, + GEN_AI_SETTINGS_DEFAULT_AI_CONNECTOR_DEFAULT_ONLY, + ]; + const { fields, handleFieldChange, unsavedChanges, saveAll, isSaving, cleanUnsavedChanges } = - useEditableSettings(settingsKeys); + useEditableSettings([...settingsKeys, ...customComponentSettingsKeys]); const canEditAdvancedSettings = capabilities.advancedSettings?.save; @@ -89,6 +98,18 @@ export function UISettings({ knowledgeBase }: { knowledgeBase: UseKnowledgeBaseR ); })} + + + + {config.logSourcesEnabled && ( { getCurrent: jest.fn().mockResolvedValue({ data: { color: 'blue', initials: 'P' } }), }, }, + notifications: { + toasts: { + addError: jest.fn(), + addSuccess: jest.fn(), + addWarning: jest.fn(), + }, + }, }, }); diff --git a/x-pack/solutions/security/plugins/security_solution/public/assistant/stack_management/management_settings.tsx b/x-pack/solutions/security/plugins/security_solution/public/assistant/stack_management/management_settings.tsx index 08f14861860a8..da5a75e4c6dbd 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/assistant/stack_management/management_settings.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/assistant/stack_management/management_settings.tsx @@ -14,21 +14,27 @@ import { CONVERSATIONS_TAB } from '@kbn/elastic-assistant/impl/assistant/setting import type { ManagementSettingsTabs } from '@kbn/elastic-assistant/impl/assistant/settings/types'; import { AssistantSpaceIdProvider } from '@kbn/elastic-assistant/impl/assistant/use_space_aware_context'; +import { DefaultAiConnectorSettingsContextProvider } from '@kbn/ai-assistant-default-llm-setting/src/context/default_ai_connector_context'; import { useKibana } from '../../common/lib/kibana'; import { useSpaceId } from '../../common/hooks/use_space_id'; export const ManagementSettings = React.memo(() => { const { - application: { - navigateToApp, - capabilities: { - securitySolutionAssistant: { 'ai-assistant': securityAIAssistantEnabled }, - }, - }, + application, data: { dataViews }, chrome: { docTitle, setBreadcrumbs }, serverless, + settings, + docLinks, + featureFlags, + notifications, } = useKibana().services; + const { + navigateToApp, + capabilities: { + securitySolutionAssistant: { 'ai-assistant': securityAIAssistantEnabled }, + }, + } = application; const spaceId = useSpaceId(); docTitle.change(SECURITY_AI_SETTINGS); @@ -98,11 +104,19 @@ export const ManagementSettings = React.memo(() => { return spaceId ? ( - + + + ) : null; }); diff --git a/x-pack/solutions/security/plugins/security_solution/public/configurations/tabs/ai_settings.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/configurations/tabs/ai_settings.test.tsx index 002a91d997220..fcbe740810c56 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/configurations/tabs/ai_settings.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/configurations/tabs/ai_settings.test.tsx @@ -31,6 +31,13 @@ describe('AISettings', () => { securitySolutionAssistant: { 'ai-assistant': true }, }, }, + notifications: { + toasts: { + addError: jest.fn(), + addSuccess: jest.fn(), + addWarning: jest.fn(), + }, + }, data: { dataViews: {} }, }, }); @@ -76,6 +83,13 @@ describe('AISettings', () => { securitySolutionAssistant: { 'ai-assistant': false }, }, }, + notifications: { + toasts: { + addError: jest.fn(), + addSuccess: jest.fn(), + addWarning: jest.fn(), + }, + }, data: { dataViews: {} }, }, }); diff --git a/x-pack/solutions/security/plugins/security_solution/public/configurations/tabs/ai_settings.tsx b/x-pack/solutions/security/plugins/security_solution/public/configurations/tabs/ai_settings.tsx index c6963e4f19ceb..0822ff12d7b1f 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/configurations/tabs/ai_settings.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/configurations/tabs/ai_settings.tsx @@ -13,6 +13,7 @@ import { AssistantSpaceIdProvider, } from '@kbn/elastic-assistant'; import { useSearchParams } from 'react-router-dom-v5-compat'; +import { DefaultAiConnectorSettingsContextProvider } from '@kbn/ai-assistant-default-llm-setting'; import { SecurityPageName } from '../../../common/constants'; import { useKibana, useNavigation } from '../../common/lib/kibana'; import { useSpaceId } from '../../common/hooks/use_space_id'; @@ -20,14 +21,20 @@ import { useSpaceId } from '../../common/hooks/use_space_id'; export const AISettings: React.FC = () => { const { navigateTo } = useNavigation(); const { - application: { - navigateToApp, - capabilities: { - securitySolutionAssistant: { 'ai-assistant': securityAIAssistantEnabled }, - }, - }, + application, data: { dataViews }, + settings, + docLinks, + notifications: { toasts }, + featureFlags, } = useKibana().services; + + const { + navigateToApp, + capabilities: { + securitySolutionAssistant: { 'ai-assistant': securityAIAssistantEnabled }, + }, + } = application; const spaceId = useSpaceId(); const onTabChange = useCallback( (tab: string) => { @@ -49,11 +56,19 @@ export const AISettings: React.FC = () => { } return spaceId ? ( - + + + ) : null; }; diff --git a/x-pack/solutions/security/plugins/security_solution/tsconfig.json b/x-pack/solutions/security/plugins/security_solution/tsconfig.json index 66db280d083cd..286417854fc22 100644 --- a/x-pack/solutions/security/plugins/security_solution/tsconfig.json +++ b/x-pack/solutions/security/plugins/security_solution/tsconfig.json @@ -257,5 +257,6 @@ "@kbn/elastic-assistant-shared-state-plugin", "@kbn/spaces-utils", "@kbn/core-metrics-server", + "@kbn/ai-assistant-default-llm-setting", ] } diff --git a/yarn.lock b/yarn.lock index 0a66d5d085485..76037ff7b4c95 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3901,6 +3901,10 @@ version "0.0.0" uid "" +"@kbn/ai-assistant-default-llm-setting@link:x-pack/platform/packages/shared/ai-assistant-default-llm-setting": + version "0.0.0" + uid "" + "@kbn/ai-assistant-icon@link:x-pack/platform/packages/shared/ai-assistant/icon": version "0.0.0" uid ""