diff --git a/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/mock/test_providers/test_providers.tsx b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/mock/test_providers/test_providers.tsx index 795542e3ec220..754187fae46d2 100644 --- a/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/mock/test_providers/test_providers.tsx +++ b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/mock/test_providers/test_providers.tsx @@ -103,6 +103,8 @@ export const TestProvidersComponent: React.FC = ({ settings: { client: { get: jest.fn(), + get$: jest.fn().mockReturnValue(of(undefined)), + getUpdate$: jest.fn().mockReturnValue(of()), }, } as unknown as SettingsStart, } as AssistantProviderProps; diff --git a/x-pack/platform/plugins/shared/agent_builder/public/application/components/conversations/conversation_input/input_actions/connector_selector/connector_selector.test.tsx b/x-pack/platform/plugins/shared/agent_builder/public/application/components/conversations/conversation_input/input_actions/connector_selector/connector_selector.test.tsx new file mode 100644 index 0000000000000..73e5cfc195f85 --- /dev/null +++ b/x-pack/platform/plugins/shared/agent_builder/public/application/components/conversations/conversation_input/input_actions/connector_selector/connector_selector.test.tsx @@ -0,0 +1,302 @@ +/* + * 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 from 'react'; +import { render, screen, act } from '@testing-library/react'; +import { __IntlProvider as IntlProvider } from '@kbn/i18n-react'; +import type { AIConnector } from '@kbn/elastic-assistant'; + +jest.mock('@kbn/inference-connectors', () => ({ + useLoadConnectors: jest.fn(), +})); + +jest.mock('../../../../../hooks/use_kibana', () => ({ + useKibana: jest.fn(), +})); + +jest.mock('../../../../../context/send_message/send_message_context', () => ({ + useSendMessage: jest.fn(), +})); + +jest.mock('../../../../../hooks/chat/use_default_connector', () => ({ + useDefaultConnector: jest.fn(), +})); + +jest.mock('../../../../../hooks/use_navigation', () => ({ + useNavigation: () => ({ manageConnectorsUrl: '/manage' }), +})); + +jest.mock('../../../../../hooks/use_ui_privileges', () => ({ + useUiPrivileges: () => ({ write: true }), +})); + +jest.mock('../../../../../../../common/recommended_connectors', () => ({ + isRecommendedConnector: () => false, +})); + +jest.mock('../input_actions.styles', () => ({ + getMaxListHeight: () => 200, + selectorPopoverPanelStyles: undefined, + useSelectorListStyles: () => undefined, +})); + +jest.mock('../input_popover_button', () => ({ + InputPopoverButton: ({ + disabled, + children, + onClick, + }: { + disabled?: boolean; + children: React.ReactNode; + onClick: () => void; + }) => ( + + ), +})); + +jest.mock('../option_text', () => ({ + OptionText: ({ children }: { children: React.ReactNode }) => {children}, +})); + +jest.mock('./connector_icon', () => ({ + ConnectorIcon: () => null, +})); + +import { useLoadConnectors } from '@kbn/inference-connectors'; +import { useKibana } from '../../../../../hooks/use_kibana'; +import { useSendMessage } from '../../../../../context/send_message/send_message_context'; +import { useDefaultConnector } from '../../../../../hooks/chat/use_default_connector'; +import { ConnectorSelector } from './connector_selector'; + +const mockUseLoadConnectors = useLoadConnectors as jest.MockedFunction; +const mockUseKibana = useKibana as jest.MockedFunction; +const mockUseSendMessage = useSendMessage as jest.MockedFunction; +const mockUseDefaultConnector = useDefaultConnector as jest.MockedFunction< + typeof useDefaultConnector +>; + +const mkConnector = (id: string, isPreconfigured = true): AIConnector => + ({ + id, + name: id, + isPreconfigured, + isMissingSecrets: false, + actionTypeId: '.gen-ai', + secrets: {}, + isDeprecated: false, + isSystemAction: false, + config: {}, + isConnectorTypeDeprecated: false, + } as AIConnector); + +interface RenderOptions { + connectors?: AIConnector[]; + isLoading?: boolean; + selectedConnector?: string; + defaultConnectorId?: string; + defaultConnectorOnly?: boolean; + initialConnectorId?: string; +} + +const setup = ({ + connectors = [], + isLoading = false, + selectedConnector, + defaultConnectorId, + defaultConnectorOnly = false, + initialConnectorId, +}: RenderOptions = {}) => { + mockUseKibana.mockReturnValue({ + services: { + http: {} as any, + settings: {} as any, + }, + } as any); + + mockUseLoadConnectors.mockReturnValue({ + data: connectors, + isLoading, + } as any); + + // Default: pick defaultConnectorId if it's in the list, otherwise fall back to the first connector. + mockUseDefaultConnector.mockImplementation(({ connectors: cs, defaultConnectorId: did }: any) => { + if (initialConnectorId !== undefined) return initialConnectorId; + if (did && cs.some((c: AIConnector) => c.id === did)) return did; + return cs[0]?.id; + }); + + const selectConnector = jest.fn(); + + mockUseSendMessage.mockReturnValue({ + connectorSelection: { + selectedConnector, + selectConnector, + defaultConnectorId, + defaultConnectorOnly, + }, + } as any); + + const utils = render( + + + + ); + return { + ...utils, + selectConnector, + // Helper to re-render with a new send-message context (simulates admin changing a setting). + updateContext: (next: Partial) => { + mockUseSendMessage.mockReturnValue({ + connectorSelection: { + selectedConnector: next.selectedConnector ?? selectedConnector, + selectConnector, + defaultConnectorId: + 'defaultConnectorId' in next ? next.defaultConnectorId : defaultConnectorId, + defaultConnectorOnly: next.defaultConnectorOnly ?? defaultConnectorOnly, + }, + } as any); + act(() => { + utils.rerender( + + + + ); + }); + }, + }; +}; + +describe('ConnectorSelector sync effect', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + it('picks the initial connector when the user has no preference', () => { + const connectors = [mkConnector('A'), mkConnector('B')]; + const { selectConnector } = setup({ + connectors, + selectedConnector: undefined, + defaultConnectorId: 'A', + }); + expect(selectConnector).toHaveBeenCalledWith('A'); + }); + + it('falls back to the initial connector when the stored pick is no longer in the list', () => { + const connectors = [mkConnector('A')]; + const { selectConnector } = setup({ + connectors, + selectedConnector: 'missing-from-list', + defaultConnectorId: 'A', + }); + expect(selectConnector).toHaveBeenCalledWith('A'); + }); + + it('does not override a valid stored pick on mount even when a default is configured', () => { + const connectors = [mkConnector('A'), mkConnector('saved')]; + const { selectConnector } = setup({ + connectors, + selectedConnector: 'saved', + defaultConnectorId: 'A', + }); + expect(selectConnector).not.toHaveBeenCalled(); + }); + + it('switches to the new default when an admin changes the default during the session', () => { + const connectors = [mkConnector('A'), mkConnector('B'), mkConnector('saved')]; + const { selectConnector, updateContext } = setup({ + connectors, + selectedConnector: 'saved', + defaultConnectorId: 'A', + }); + expect(selectConnector).not.toHaveBeenCalled(); + + updateContext({ defaultConnectorId: 'B' }); + + expect(selectConnector).toHaveBeenCalledWith('B'); + }); + + it('switches to the default when an admin sets a default for the first time', () => { + const connectors = [mkConnector('A'), mkConnector('saved')]; + const { selectConnector, updateContext } = setup({ + connectors, + selectedConnector: 'saved', + defaultConnectorId: undefined, + }); + expect(selectConnector).not.toHaveBeenCalled(); + + updateContext({ defaultConnectorId: 'A' }); + + expect(selectConnector).toHaveBeenCalledWith('A'); + }); + + it('keeps the current pick when the admin unsets the default', () => { + const connectors = [mkConnector('A'), mkConnector('saved')]; + const { selectConnector, updateContext } = setup({ + connectors, + selectedConnector: 'saved', + defaultConnectorId: 'A', + }); + + updateContext({ defaultConnectorId: undefined }); + + expect(selectConnector).not.toHaveBeenCalled(); + }); + + it('does not act while connectors are still loading', () => { + const { selectConnector } = setup({ + connectors: [], + isLoading: true, + selectedConnector: undefined, + defaultConnectorId: 'A', + }); + expect(selectConnector).not.toHaveBeenCalled(); + }); + + describe('defaultConnectorOnly', () => { + it('forces the chat to the default when turned on', () => { + const connectors = [mkConnector('A'), mkConnector('saved')]; + const { selectConnector } = setup({ + connectors, + selectedConnector: 'saved', + defaultConnectorId: 'A', + defaultConnectorOnly: true, + }); + expect(selectConnector).toHaveBeenCalledWith('A'); + }); + + it('disables the popover button when turned on', () => { + const connectors = [mkConnector('A')]; + setup({ + connectors, + selectedConnector: 'A', + defaultConnectorId: 'A', + defaultConnectorOnly: true, + }); + const button = screen.getByTestId('agentBuilderConnectorSelectorButton'); + expect(button).toBeDisabled(); + }); + + it('leaves the popover button enabled when turned off', () => { + const connectors = [mkConnector('A')]; + setup({ + connectors, + selectedConnector: 'A', + defaultConnectorId: 'A', + defaultConnectorOnly: false, + }); + const button = screen.getByTestId('agentBuilderConnectorSelectorButton'); + expect(button).not.toBeDisabled(); + }); + }); +}); diff --git a/x-pack/platform/plugins/shared/agent_builder/public/application/components/conversations/conversation_input/input_actions/connector_selector/connector_selector.tsx b/x-pack/platform/plugins/shared/agent_builder/public/application/components/conversations/conversation_input/input_actions/connector_selector/connector_selector.tsx index 1f5872252a30f..db18ebdf51b77 100644 --- a/x-pack/platform/plugins/shared/agent_builder/public/application/components/conversations/conversation_input/input_actions/connector_selector/connector_selector.tsx +++ b/x-pack/platform/plugins/shared/agent_builder/public/application/components/conversations/conversation_input/input_actions/connector_selector/connector_selector.tsx @@ -18,7 +18,7 @@ import { import { useLoadConnectors } from '@kbn/inference-connectors'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; -import React, { useEffect, useMemo, useState } from 'react'; +import React, { useEffect, useMemo, useRef, useState } from 'react'; import { useUiPrivileges } from '../../../../../hooks/use_ui_privileges'; import { useNavigation } from '../../../../../hooks/use_navigation'; import { useSendMessage } from '../../../../../context/send_message/send_message_context'; @@ -154,6 +154,7 @@ export const ConnectorSelector: React.FC<{}> = () => { selectConnector: onSelectConnector, selectedConnector: selectedConnectorId, defaultConnectorId, + defaultConnectorOnly, }, } = useSendMessage(); const [isPopoverOpen, setIsPopoverOpen] = useState(false); @@ -233,19 +234,59 @@ export const ConnectorSelector: React.FC<{}> = () => { const selectedConnector = connectors.find((c) => c.id === selectedConnectorId); + // Track the previously-observed default so we can detect admin-initiated changes. + // Seeded with the current value on first render and updated on every effect run + // (including early returns) so the ref stays aligned with the observable even + // while connectors are still loading. That way, once we proceed past the early + // return, `previousDefault` reflects the last observed value — not a mount-time + // baseline — and the first real emission is not mistaken for a change. + const previousDefaultRef = useRef(defaultConnectorId); + useEffect(() => { - if (!isLoading && initialConnectorId) { - // No user preference set - if (!selectedConnectorId) { - onSelectConnector(initialConnectorId); - } - // User preference is set but connector is not available in the list. - // Scenario: the connector was deleted or admin changed GenAI settings - else if (selectedConnectorId && !selectedConnector) { - onSelectConnector(initialConnectorId); + const previousDefault = previousDefaultRef.current; + previousDefaultRef.current = defaultConnectorId; + + if (isLoading || !initialConnectorId) return; + + // Admin enforces "only allow the default model" — always follow the default. + if (defaultConnectorOnly && defaultConnectorId) { + if (selectedConnectorId !== defaultConnectorId) { + onSelectConnector(defaultConnectorId); } + return; + } + + // No user preference set yet. + if (!selectedConnectorId) { + onSelectConnector(initialConnectorId); + return; + } + + // Stored preference is no longer in the list (connector deleted or filtered out). + if (!selectedConnector) { + onSelectConnector(initialConnectorId); + return; } - }, [selectedConnectorId, selectedConnector, isLoading, initialConnectorId, onSelectConnector]); + + // Admin-initiated change of the default-model setting to a valid connector. + if ( + defaultConnectorId && + defaultConnectorId !== previousDefault && + defaultConnectorId !== selectedConnectorId && + connectors.some((c) => c.id === defaultConnectorId) + ) { + onSelectConnector(defaultConnectorId); + } + }, [ + selectedConnectorId, + selectedConnector, + isLoading, + initialConnectorId, + defaultConnectorId, + defaultConnectorOnly, + connectors, + onSelectConnector, + ]); const selectorListStyles = useSelectorListStyles({ listId: connectorListId }); const listItemsHeight = connectorOptions.length * CONNECTOR_OPTION_ROW_HEIGHT; @@ -259,7 +300,7 @@ export const ConnectorSelector: React.FC<{}> = () => { } diff --git a/x-pack/platform/plugins/shared/agent_builder/public/application/context/send_message/send_message_context.tsx b/x-pack/platform/plugins/shared/agent_builder/public/application/context/send_message/send_message_context.tsx index a73cd057d11e4..fafe7c27e2f57 100644 --- a/x-pack/platform/plugins/shared/agent_builder/public/application/context/send_message/send_message_context.tsx +++ b/x-pack/platform/plugins/shared/agent_builder/public/application/context/send_message/send_message_context.tsx @@ -31,6 +31,7 @@ interface SendMessageState { selectedConnector: string | undefined; selectConnector: (connectorId: string) => void; defaultConnectorId?: string; + defaultConnectorOnly: boolean; }; } @@ -88,6 +89,7 @@ export const SendMessageProvider = ({ children }: { children: React.ReactNode }) selectedConnector: connectorSelection.selectedConnector, selectConnector: connectorSelection.selectConnector, defaultConnectorId: connectorSelection.defaultConnectorId, + defaultConnectorOnly: connectorSelection.defaultConnectorOnly, }, }} > diff --git a/x-pack/platform/plugins/shared/agent_builder/public/application/hooks/chat/use_connector_selection.test.ts b/x-pack/platform/plugins/shared/agent_builder/public/application/hooks/chat/use_connector_selection.test.ts index 0d2d81f8c2ff8..ac826ca219b6b 100644 --- a/x-pack/platform/plugins/shared/agent_builder/public/application/hooks/chat/use_connector_selection.test.ts +++ b/x-pack/platform/plugins/shared/agent_builder/public/application/hooks/chat/use_connector_selection.test.ts @@ -6,6 +6,7 @@ */ import { renderHook, act } from '@testing-library/react'; +import { BehaviorSubject } from 'rxjs'; import { useConnectorSelection } from './use_connector_selection'; jest.mock('../use_kibana', () => ({ @@ -24,13 +25,27 @@ import useLocalStorage from 'react-use/lib/useLocalStorage'; const mockUseKibana = useKibana as jest.MockedFunction; const mockUseLocalStorage = useLocalStorage as jest.MockedFunction; +import { + GEN_AI_SETTINGS_DEFAULT_AI_CONNECTOR, + GEN_AI_SETTINGS_DEFAULT_AI_CONNECTOR_DEFAULT_ONLY, +} from '@kbn/management-settings-ids'; + describe('useConnectorSelection', () => { - let mockSettingsGet: jest.Mock; + let defaultConnector$: BehaviorSubject; + let defaultConnectorOnly$: BehaviorSubject; let localStorageState: { [key: string]: string | undefined }; let mockSetLocalStorage: jest.Mock; + const buildGet$ = () => + jest.fn((key: string) => { + if (key === GEN_AI_SETTINGS_DEFAULT_AI_CONNECTOR) return defaultConnector$; + if (key === GEN_AI_SETTINGS_DEFAULT_AI_CONNECTOR_DEFAULT_ONLY) return defaultConnectorOnly$; + return new BehaviorSubject(undefined); + }); + beforeEach(() => { - mockSettingsGet = jest.fn(); + defaultConnector$ = new BehaviorSubject(undefined); + defaultConnectorOnly$ = new BehaviorSubject(false); localStorageState = {}; mockSetLocalStorage = jest.fn((newValue: string) => { localStorageState[storageKeys.lastUsedConnector] = newValue; @@ -47,7 +62,7 @@ describe('useConnectorSelection', () => { services: { settings: { client: { - get: mockSettingsGet, + get$: buildGet$(), }, }, }, @@ -60,8 +75,6 @@ describe('useConnectorSelection', () => { describe('selectedConnector', () => { it('should return undefined when no connector is selected and no default is set', () => { - mockSettingsGet.mockReturnValue(undefined); - const { result } = renderHook(() => useConnectorSelection()); expect(result.current.selectedConnector).toBeUndefined(); @@ -69,7 +82,6 @@ describe('useConnectorSelection', () => { }); it('should return the connector from localStorage when set', () => { - mockSettingsGet.mockReturnValue(undefined); localStorageState[storageKeys.lastUsedConnector] = 'connector-1'; const { result } = renderHook(() => useConnectorSelection()); @@ -78,7 +90,16 @@ describe('useConnectorSelection', () => { }); it('should return defaultConnectorId from settings', () => { - mockSettingsGet.mockReturnValue('default-connector'); + defaultConnector$ = new BehaviorSubject('default-connector'); + mockUseKibana.mockReturnValue({ + services: { + settings: { + client: { + get$: buildGet$(), + }, + }, + }, + } as any); const { result } = renderHook(() => useConnectorSelection()); @@ -86,10 +107,57 @@ describe('useConnectorSelection', () => { }); }); + describe('defaultConnectorOnly', () => { + it('defaults to false when settings is undefined', () => { + mockUseKibana.mockReturnValue({ + services: { + settings: undefined, + }, + } as any); + + const { result } = renderHook(() => useConnectorSelection()); + + expect(result.current.defaultConnectorOnly).toBe(false); + }); + + it('reads the current value of the defaultConnectorOnly setting', () => { + defaultConnectorOnly$ = new BehaviorSubject(true); + mockUseKibana.mockReturnValue({ + services: { + settings: { + client: { + get$: buildGet$(), + }, + }, + }, + } as any); + + const { result } = renderHook(() => useConnectorSelection()); + + expect(result.current.defaultConnectorOnly).toBe(true); + }); + + it('reactively updates when the defaultConnectorOnly setting changes', () => { + const { result } = renderHook(() => useConnectorSelection()); + + expect(result.current.defaultConnectorOnly).toBe(false); + + act(() => { + defaultConnectorOnly$.next(true); + }); + + expect(result.current.defaultConnectorOnly).toBe(true); + + act(() => { + defaultConnectorOnly$.next(false); + }); + + expect(result.current.defaultConnectorOnly).toBe(false); + }); + }); + describe('selectConnector', () => { it('should update localStorage when a connector is selected', () => { - mockSettingsGet.mockReturnValue(undefined); - const { result } = renderHook(() => useConnectorSelection()); act(() => { @@ -101,8 +169,6 @@ describe('useConnectorSelection', () => { }); it('should update selected connector when selectConnector is called', () => { - mockSettingsGet.mockReturnValue(undefined); - const { result, rerender } = renderHook(() => useConnectorSelection()); act(() => { @@ -116,8 +182,6 @@ describe('useConnectorSelection', () => { }); it('should allow selecting different connectors sequentially', () => { - mockSettingsGet.mockReturnValue(undefined); - const { result, rerender } = renderHook(() => useConnectorSelection()); act(() => { @@ -155,7 +219,16 @@ describe('useConnectorSelection', () => { }); it('should maintain selection stability across re-renders', () => { - mockSettingsGet.mockReturnValue('default-connector'); + defaultConnector$ = new BehaviorSubject('default-connector'); + mockUseKibana.mockReturnValue({ + services: { + settings: { + client: { + get$: buildGet$(), + }, + }, + }, + } as any); localStorageState[storageKeys.lastUsedConnector] = 'connector-2'; const { result, rerender } = renderHook(() => useConnectorSelection()); @@ -172,14 +245,24 @@ describe('useConnectorSelection', () => { }); it('should update when default connector changes in settings', () => { - mockSettingsGet.mockReturnValue('default-connector-1'); + defaultConnector$ = new BehaviorSubject('default-connector-1'); + mockUseKibana.mockReturnValue({ + services: { + settings: { + client: { + get$: buildGet$(), + }, + }, + }, + } as any); - const { result, rerender } = renderHook(() => useConnectorSelection()); + const { result } = renderHook(() => useConnectorSelection()); expect(result.current.defaultConnectorId).toBe('default-connector-1'); - mockSettingsGet.mockReturnValue('default-connector-2'); - rerender(); + act(() => { + defaultConnector$.next('default-connector-2'); + }); expect(result.current.defaultConnectorId).toBe('default-connector-2'); }); diff --git a/x-pack/platform/plugins/shared/agent_builder/public/application/hooks/chat/use_connector_selection.ts b/x-pack/platform/plugins/shared/agent_builder/public/application/hooks/chat/use_connector_selection.ts index e3d16dd26e17a..8852554458ba8 100644 --- a/x-pack/platform/plugins/shared/agent_builder/public/application/hooks/chat/use_connector_selection.ts +++ b/x-pack/platform/plugins/shared/agent_builder/public/application/hooks/chat/use_connector_selection.ts @@ -5,9 +5,14 @@ * 2.0. */ -import { useCallback } from 'react'; +import { useCallback, useMemo } from 'react'; +import { EMPTY } from 'rxjs'; import useLocalStorage from 'react-use/lib/useLocalStorage'; -import { GEN_AI_SETTINGS_DEFAULT_AI_CONNECTOR } from '@kbn/management-settings-ids'; +import useObservable from 'react-use/lib/useObservable'; +import { + GEN_AI_SETTINGS_DEFAULT_AI_CONNECTOR, + GEN_AI_SETTINGS_DEFAULT_AI_CONNECTOR_DEFAULT_ONLY, +} from '@kbn/management-settings-ids'; import { useKibana } from '../use_kibana'; import { storageKeys } from '../../storage_keys'; @@ -15,6 +20,7 @@ export interface UseConnectorSelectionResult { selectedConnector?: string; selectConnector: (connectorId: string) => void; defaultConnectorId?: string; + defaultConnectorOnly: boolean; } export function useConnectorSelection(): UseConnectorSelectionResult { @@ -26,10 +32,18 @@ export function useConnectorSelection(): UseConnectorSelectionResult { storageKeys.lastUsedConnector ); - const defaultConnectorId = settings?.client.get( - GEN_AI_SETTINGS_DEFAULT_AI_CONNECTOR, - undefined + const defaultConnector$ = useMemo( + () => settings?.client.get$(GEN_AI_SETTINGS_DEFAULT_AI_CONNECTOR) ?? EMPTY, + [settings] ); + const defaultConnectorId = useObservable(defaultConnector$); + + const defaultConnectorOnly$ = useMemo( + () => + settings?.client.get$(GEN_AI_SETTINGS_DEFAULT_AI_CONNECTOR_DEFAULT_ONLY) ?? EMPTY, + [settings] + ); + const defaultConnectorOnly = useObservable(defaultConnectorOnly$, false) ?? false; const selectConnector = useCallback( (connectorId: string) => { @@ -42,5 +56,6 @@ export function useConnectorSelection(): UseConnectorSelectionResult { selectedConnector, selectConnector, defaultConnectorId, + defaultConnectorOnly, }; } diff --git a/x-pack/platform/plugins/shared/search_inference_endpoints/public/hooks/use_default_model_settings.ts b/x-pack/platform/plugins/shared/search_inference_endpoints/public/hooks/use_default_model_settings.ts index 17631f2419ad8..23e97df0fce79 100644 --- a/x-pack/platform/plugins/shared/search_inference_endpoints/public/hooks/use_default_model_settings.ts +++ b/x-pack/platform/plugins/shared/search_inference_endpoints/public/hooks/use_default_model_settings.ts @@ -31,26 +31,26 @@ export interface UseDefaultModelSettingsReturn { export const useDefaultModelSettings = (): UseDefaultModelSettingsReturn => { const { services } = useKibana(); - const uiSettings = services.uiSettings; + const settingsClient = services.settings.client; const notifications = services.notifications; const getSavedState = useCallback((): DefaultModelSettingsState => { - const defaultModelId = uiSettings.get( + const defaultModelId = settingsClient.get( GEN_AI_SETTINGS_DEFAULT_AI_CONNECTOR, NO_DEFAULT_MODEL ); - const disallowOtherModels = uiSettings.get( + const disallowOtherModels = settingsClient.get( GEN_AI_SETTINGS_DEFAULT_AI_CONNECTOR_DEFAULT_ONLY, false ); return { defaultModelId, disallowOtherModels }; - }, [uiSettings]); + }, [settingsClient]); const [savedState, setSavedState] = useState(getSavedState); const [state, setState] = useState(getSavedState); useEffect(() => { - const subscription = uiSettings.getUpdate$().subscribe(({ key }) => { + const subscription = settingsClient.getUpdate$().subscribe(({ key }) => { if ( key === GEN_AI_SETTINGS_DEFAULT_AI_CONNECTOR || key === GEN_AI_SETTINGS_DEFAULT_AI_CONNECTOR_DEFAULT_ONLY @@ -60,7 +60,7 @@ export const useDefaultModelSettings = (): UseDefaultModelSettingsReturn => { } }); return () => subscription.unsubscribe(); - }, [uiSettings, getSavedState]); + }, [settingsClient, getSavedState]); const isDirty = useMemo( () => @@ -82,10 +82,10 @@ export const useDefaultModelSettings = (): UseDefaultModelSettingsReturn => { const save = useCallback(async () => { try { if (state.defaultModelId !== savedState.defaultModelId) { - await uiSettings.set(GEN_AI_SETTINGS_DEFAULT_AI_CONNECTOR, state.defaultModelId); + await settingsClient.set(GEN_AI_SETTINGS_DEFAULT_AI_CONNECTOR, state.defaultModelId); } if (state.disallowOtherModels !== savedState.disallowOtherModels) { - await uiSettings.set( + await settingsClient.set( GEN_AI_SETTINGS_DEFAULT_AI_CONNECTOR_DEFAULT_ONLY, state.disallowOtherModels ); @@ -106,7 +106,7 @@ export const useDefaultModelSettings = (): UseDefaultModelSettingsReturn => { text: (e as Error)?.message ?? 'Unknown error', }); } - }, [state, savedState, uiSettings, getSavedState, notifications]); + }, [state, savedState, settingsClient, getSavedState, notifications]); const reset = useCallback(() => { setState(savedState); diff --git a/x-pack/solutions/security/packages/ecs-data-quality-dashboard/impl/data_quality_panel/mock/test_providers/test_providers.tsx b/x-pack/solutions/security/packages/ecs-data-quality-dashboard/impl/data_quality_panel/mock/test_providers/test_providers.tsx index 6238657ee80b1..40d84f91ea5d7 100644 --- a/x-pack/solutions/security/packages/ecs-data-quality-dashboard/impl/data_quality_panel/mock/test_providers/test_providers.tsx +++ b/x-pack/solutions/security/packages/ecs-data-quality-dashboard/impl/data_quality_panel/mock/test_providers/test_providers.tsx @@ -102,6 +102,8 @@ const TestExternalProvidersComponent: React.FC = ({ settings: { client: { get: jest.fn(), + get$: jest.fn().mockReturnValue(of(undefined)), + getUpdate$: jest.fn().mockReturnValue(of()), }, } as unknown as SettingsStart, }; diff --git a/x-pack/solutions/security/plugins/security_solution/public/common/mock/mock_assistant_provider.tsx b/x-pack/solutions/security/plugins/security_solution/public/common/mock/mock_assistant_provider.tsx index fdaf0122de7a4..1a117fc25e386 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/common/mock/mock_assistant_provider.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/common/mock/mock_assistant_provider.tsx @@ -74,6 +74,8 @@ export const MockAssistantProviderComponent: React.FC = ({ settings: { client: { get: jest.fn(), + get$: jest.fn().mockReturnValue(of(undefined)), + getUpdate$: jest.fn().mockReturnValue(of()), }, } as unknown as SettingsStart, }); diff --git a/x-pack/solutions/security/plugins/security_solution/public/detections/components/attacks/content.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/detections/components/attacks/content.test.tsx index 011ba36478db3..11a1a9c2db571 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detections/components/attacks/content.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detections/components/attacks/content.test.tsx @@ -7,6 +7,7 @@ import React from 'react'; import { render, screen, waitFor, fireEvent } from '@testing-library/react'; +import { of } from 'rxjs'; import type { DataView } from '@kbn/data-views-plugin/common'; import { createStubDataView } from '@kbn/data-views-plugin/common/data_views/data_view.stub'; @@ -52,7 +53,13 @@ describe('AttacksPageContent', () => { beforeEach(() => { (useKibana as jest.Mock).mockReturnValue({ services: { - settings: {}, + settings: { + client: { + get: jest.fn(), + get$: jest.fn().mockReturnValue(of(undefined)), + getUpdate$: jest.fn().mockReturnValue(of()), + }, + }, telemetry: { reportEvent, },