From 226b69d15da3ad102bf43dc7d4e0e579e593703d Mon Sep 17 00:00:00 2001 From: Kenneth Kreindler <42113355+KDKHD@users.noreply.github.com> Date: Tue, 9 Sep 2025 09:09:02 +0100 Subject: [PATCH 1/2] Feature/default llm setting ai settings (#233874) ## Summary Summarize your PR. If it involves visual changes include a screenshot or gif. Follow-up to this change: https://github.com/elastic/kibana/pull/231940 The PR linked above could not be backported to 8.18, 9.19, 9.1 because the GenAi settings page where the setting was added to does not exist in the versions just listed. As this setting is needed on these version, this PR is required. This PR adds the Default LLM setting to the Security, Obs & ES AI settings pages. image image image image ### How to test: - Enable the feature flag. Add `feature_flags.overrides.aiAssistant.defaultLlmSettingEnabled: true` to kibana.dev.yml - Start Kibana and go to http://localhost:5601/app/management/kibana/securityAiAssistantManagement?tab=connectors or http://localhost:5601/app/management/kibana/observabilityAiAssistantManagement - You should see the `genAiSettings:defaultAIConnector` setting. Changing the setting won't do anything just yet as changes within the assistants still need to be configured. TODO: When the feature flag is lifted, we should remove the following Kibana advanced setting as this one will rpelace it: image ### Checklist Check the PR satisfies following conditions. Reviewers should verify this PR satisfies this list as well. - [X] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/src/platform/packages/shared/kbn-i18n/README.md) - [X] [Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html) was added for features that require explanation or tutorials - [X] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios - [X] If a plugin configuration key changed, check if it needs to be allowlisted in the cloud and added to the [docker list](https://github.com/elastic/kibana/blob/main/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker) - [X] This was checked for breaking HTTP API changes, and any breaking changes have been approved by the breaking-change committee. The `release_note:breaking` label should be applied in these situations. - [X] [Flaky Test Runner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1) was used on any tests changed - [X] The PR description includes the appropriate Release Notes section, and the correct `release_note:*` label is applied per the [guidelines](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process) - [X] Review the [backport guidelines](https://docs.google.com/document/d/1VyN5k91e5OVumlc0Gb9RPa3h1ewuPE705nRtioPiTvY/edit?usp=sharing) and apply applicable `backport:*` labels. ### Identify risks Does this PR introduce any risks? For example, consider risks like hard to test bugs, performance regression, potential of data loss. Describe the risk, its severity, and mitigation for each identified risk. Invite stakeholders and evaluate how to proceed before merging. - [ ] [See some risk examples](https://github.com/elastic/kibana/blob/main/RISK_MATRIX.mdx) - [ ] ... --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Elastic Machine (cherry picked from commit 6141299372af19ca322fb9d90c05d8b775954e48) # Conflicts: # package.json # tsconfig.base.json # x-pack/platform/packages/shared/kbn-elastic-assistant/tsconfig.json # x-pack/solutions/observability/plugins/observability_ai_assistant_management/public/routes/components/settings_tab/ui_settings.tsx # yarn.lock --- package.json | 1 + .../settings/setting_ids/index.ts | 3 + .../server/collectors/management/schema.ts | 12 + .../server/collectors/management/types.ts | 2 + .../selection/common/constants.ts | 10 + .../selection/server/plugin.test.ts | 88 +++- .../selection/server/plugin.ts | 48 ++- .../selection/tsconfig.json | 3 +- .../shared/telemetry/schema/oss_platform.json | 12 + tsconfig.base.json | 2 + .../README.md | 3 + .../ai-assistant-default-llm-setting/index.ts | 12 + .../jest.config.js | 18 + .../kibana.jsonc | 7 + .../package.json | 7 + .../setup_tests.ts | 9 + .../components/default_ai_connector.test.tsx | 226 ++++++++++ .../src/components/default_ai_connector.tsx | 404 ++++++++++++++++++ .../context/default_ai_connector_context.tsx | 75 ++++ .../src/lib/constants.ts | 8 + .../tsconfig.json | 32 ++ .../assistant_settings_management.test.tsx | 2 + .../assistant_settings_management.tsx | 8 +- ...onfigurations_settings_management.test.tsx | 2 + ...ake_configurations_settings_management.tsx | 10 +- .../index.tsx | 11 +- .../bottom_bar_actions.test.tsx | 52 +++ .../bottom_bar_actions/bottom_bar_actions.tsx | 103 +++++ .../context/settings_context.test.tsx | 175 ++++++++ .../context/settings_context.tsx | 172 ++++++++ .../connector_settings_management/index.tsx | 114 ++++- .../translations.ts | 14 + .../settings_tab/settings_tab.test.tsx | 22 +- .../components/settings_tab/ui_settings.tsx | 35 +- .../tsconfig.json | 4 +- .../management_settings.test.tsx | 7 + .../stack_management/management_settings.tsx | 36 +- .../configurations/tabs/ai_settings.test.tsx | 14 + .../configurations/tabs/ai_settings.tsx | 37 +- .../plugins/security_solution/tsconfig.json | 1 + yarn.lock | 5 +- 41 files changed, 1726 insertions(+), 80 deletions(-) create mode 100644 src/platform/plugins/shared/ai_assistant_management/selection/common/constants.ts create mode 100644 x-pack/platform/packages/shared/ai-assistant-default-llm-setting/README.md create mode 100644 x-pack/platform/packages/shared/ai-assistant-default-llm-setting/index.ts create mode 100644 x-pack/platform/packages/shared/ai-assistant-default-llm-setting/jest.config.js create mode 100644 x-pack/platform/packages/shared/ai-assistant-default-llm-setting/kibana.jsonc create mode 100644 x-pack/platform/packages/shared/ai-assistant-default-llm-setting/package.json create mode 100644 x-pack/platform/packages/shared/ai-assistant-default-llm-setting/setup_tests.ts create mode 100644 x-pack/platform/packages/shared/ai-assistant-default-llm-setting/src/components/default_ai_connector.test.tsx create mode 100644 x-pack/platform/packages/shared/ai-assistant-default-llm-setting/src/components/default_ai_connector.tsx create mode 100644 x-pack/platform/packages/shared/ai-assistant-default-llm-setting/src/context/default_ai_connector_context.tsx create mode 100644 x-pack/platform/packages/shared/ai-assistant-default-llm-setting/src/lib/constants.ts create mode 100644 x-pack/platform/packages/shared/ai-assistant-default-llm-setting/tsconfig.json create mode 100644 x-pack/platform/packages/shared/kbn-elastic-assistant/impl/connectorland/connector_settings_management/bottom_bar_actions/bottom_bar_actions.test.tsx create mode 100644 x-pack/platform/packages/shared/kbn-elastic-assistant/impl/connectorland/connector_settings_management/bottom_bar_actions/bottom_bar_actions.tsx create mode 100644 x-pack/platform/packages/shared/kbn-elastic-assistant/impl/connectorland/connector_settings_management/context/settings_context.test.tsx create mode 100644 x-pack/platform/packages/shared/kbn-elastic-assistant/impl/connectorland/connector_settings_management/context/settings_context.tsx diff --git a/package.json b/package.json index e8e89884af18e..392cdc61ecee3 100644 --- a/package.json +++ b/package.json @@ -164,6 +164,7 @@ "@kbn/advanced-settings-plugin": "link:src/platform/plugins/private/advanced_settings", "@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-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 74d7c47938f83..8ab94b821b59f 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 @@ -127,6 +127,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 AI_ASSISTANT_PREFERRED_AI_ASSISTANT_TYPE = 'aiAssistant:preferredAIAssistantType'; 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 13c84560199b0..b780ee6653c24 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 @@ -650,6 +650,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 45e820c6be67c..3d8b090f11122 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 @@ -169,6 +169,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 93e64bd08f4d7..cfccdb72672a6 100644 --- a/src/platform/plugins/shared/telemetry/schema/oss_platform.json +++ b/src/platform/plugins/shared/telemetry/schema/oss_platform.json @@ -11405,6 +11405,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 c55ee38659e48..20350b5b83dfc 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -18,6 +18,8 @@ "@kbn/ai-assistant/*": ["x-pack/platform/packages/shared/kbn-ai-assistant/*"], "@kbn/ai-assistant-common": ["x-pack/platform/packages/shared/ai-assistant/common"], "@kbn/ai-assistant-common/*": ["x-pack/platform/packages/shared/ai-assistant/common/*"], + "@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/solutions/observability/plugins/observability_ai_assistant_management/public/routes/components/settings_tab/settings_tab.test.tsx b/x-pack/solutions/observability/plugins/observability_ai_assistant_management/public/routes/components/settings_tab/settings_tab.test.tsx index 1c55a275c46d0..ea57721dcab24 100644 --- a/x-pack/solutions/observability/plugins/observability_ai_assistant_management/public/routes/components/settings_tab/settings_tab.test.tsx +++ b/x-pack/solutions/observability/plugins/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/solutions/observability/plugins/observability_ai_assistant_management/public/routes/components/settings_tab/ui_settings.tsx b/x-pack/solutions/observability/plugins/observability_ai_assistant_management/public/routes/components/settings_tab/ui_settings.tsx index b98a2719df70e..9c791afa511cf 100644 --- a/x-pack/solutions/observability/plugins/observability_ai_assistant_management/public/routes/components/settings_tab/ui_settings.tsx +++ b/x-pack/solutions/observability/plugins/observability_ai_assistant_management/public/routes/components/settings_tab/ui_settings.tsx @@ -19,17 +19,21 @@ 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 { useAppContext } from '../../../hooks/use_app_context'; import { useKibana } from '../../../hooks/use_kibana'; 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, @@ -38,8 +42,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; @@ -88,6 +97,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 e324a53de22b2..e1a170da4d6d3 100644 --- a/x-pack/solutions/security/plugins/security_solution/tsconfig.json +++ b/x-pack/solutions/security/plugins/security_solution/tsconfig.json @@ -259,5 +259,6 @@ "@kbn/elastic-assistant-shared-state", "@kbn/elastic-assistant-shared-state-plugin", "@kbn/core-metrics-server", + "@kbn/ai-assistant-default-llm-setting", ] } diff --git a/yarn.lock b/yarn.lock index a45ab62f5aef3..a44e7521eb26f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3758,10 +3758,13 @@ version "0.0.0" uid "" -"@kbn/ai-assistant-icon@link:x-pack/platform/packages/shared/ai-assistant/icon": +"@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" + "@kbn/ai-assistant-management-plugin@link:src/platform/plugins/shared/ai_assistant_management/selection": version "0.0.0" uid "" From 2812f6d306cfc47a0367fc0841b04b7a0302746e Mon Sep 17 00:00:00 2001 From: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Date: Tue, 9 Sep 2025 09:17:23 +0000 Subject: [PATCH 2/2] [CI] Auto-commit changed files from 'node scripts/yarn_deduplicate' --- .../packages/shared/kbn-elastic-assistant/tsconfig.json | 6 ++++++ 1 file changed, 6 insertions(+) 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 5a5227c93de83..b6ab21779f9f4 100644 --- a/x-pack/platform/packages/shared/kbn-elastic-assistant/tsconfig.json +++ b/x-pack/platform/packages/shared/kbn-elastic-assistant/tsconfig.json @@ -44,5 +44,11 @@ "@kbn/alerts-ui-shared", "@kbn/deeplinks-security", "@kbn/inference-common", + "@kbn/core-ui-settings-browser", + "@kbn/ai-assistant-default-llm-setting", + "@kbn/management-settings-types", + "@kbn/management-settings-utilities", + "@kbn/management-settings-field-definition", + "@kbn/management-settings-ids", ] }