diff --git a/x-pack/platform/plugins/shared/index_management/__jest__/client_integration/index_details_page/select_inference_id.helpers.tsx b/x-pack/platform/plugins/shared/index_management/__jest__/client_integration/index_details_page/select_inference_id.helpers.tsx index 4e245f2e92736..c717a848fdf58 100644 --- a/x-pack/platform/plugins/shared/index_management/__jest__/client_integration/index_details_page/select_inference_id.helpers.tsx +++ b/x-pack/platform/plugins/shared/index_management/__jest__/client_integration/index_details_page/select_inference_id.helpers.tsx @@ -26,10 +26,30 @@ export const createMockLocator = () => ({ export const mockResendRequest = jest.fn(); export const DEFAULT_ENDPOINTS: InferenceAPIConfigResponse[] = [ - { inference_id: '.preconfigured-elser', task_type: 'sparse_embedding' }, - { inference_id: '.preconfigured-e5', task_type: 'text_embedding' }, - { inference_id: 'endpoint-1', task_type: 'text_embedding' }, - { inference_id: 'endpoint-2', task_type: 'sparse_embedding' }, + { + inference_id: '.preconfigured-elser', + task_type: 'sparse_embedding', + service: 'elastic', + service_settings: { model_id: 'elser' }, + }, + { + inference_id: '.preconfigured-e5', + task_type: 'text_embedding', + service: 'elastic', + service_settings: { model_id: 'e5' }, + }, + { + inference_id: 'endpoint-1', + task_type: 'text_embedding', + service: 'openai', + service_settings: { model_id: 'text-embedding-3-large' }, + }, + { + inference_id: 'endpoint-2', + task_type: 'sparse_embedding', + service: 'elastic', + service_settings: { model_id: 'elser' }, + }, ] as InferenceAPIConfigResponse[]; export const defaultProps: SelectInferenceIdProps = { diff --git a/x-pack/platform/plugins/shared/index_management/__jest__/client_integration/index_details_page/select_inference_id.test.tsx b/x-pack/platform/plugins/shared/index_management/__jest__/client_integration/index_details_page/select_inference_id.test.tsx index fb0d0c8538697..d0de0e7503b3b 100644 --- a/x-pack/platform/plugins/shared/index_management/__jest__/client_integration/index_details_page/select_inference_id.test.tsx +++ b/x-pack/platform/plugins/shared/index_management/__jest__/client_integration/index_details_page/select_inference_id.test.tsx @@ -94,31 +94,42 @@ jest.mock('../../../public/application/components/mappings_editor/mappings_state useDispatch: () => mockDispatch, })); -const MockInferenceFlyoutWrapper = ({ - onFlyoutClose, - onSubmitSuccess, -}: { - onFlyoutClose: () => void; - onSubmitSuccess: (id: string) => void; - http?: unknown; - toasts?: unknown; - isEdit?: boolean; - enforceAdaptiveAllocations?: boolean; -}) => ( -
- - -
-); - -jest.mock('@kbn/inference-endpoint-ui-common', () => ({ - __esModule: true, - default: MockInferenceFlyoutWrapper, -})); +jest.mock('@kbn/inference-endpoint-ui-common', () => { + const SERVICE_PROVIDERS = { + elastic: { name: 'Elastic' }, + openai: { name: 'OpenAI' }, + }; + + const MockInferenceFlyoutWrapper = ({ + onFlyoutClose, + onSubmitSuccess, + }: { + onFlyoutClose: () => void; + onSubmitSuccess: (id: string) => void; + http?: unknown; + toasts?: unknown; + isEdit?: boolean; + enforceAdaptiveAllocations?: boolean; + }) => ( +
+ + +
+ ); + + return { + __esModule: true, + default: MockInferenceFlyoutWrapper, + SERVICE_PROVIDERS, + }; +}); jest.mock('../../../public/application/services/api', () => ({ ...jest.requireActual('../../../public/application/services/api'), @@ -414,9 +425,24 @@ describe('SelectInferenceId', () => { it('SHOULD prioritize .elser-2-elastic over other endpoints IF has enterprise license', async () => { setupInferenceEndpointsMocks({ data: [ - { inference_id: '.elser-2-elastic', task_type: 'sparse_embedding' }, - { inference_id: '.preconfigured-elser', task_type: 'sparse_embedding' }, - { inference_id: 'endpoint-1', task_type: 'text_embedding' }, + { + inference_id: '.elser-2-elastic', + task_type: 'sparse_embedding', + service: 'elastic', + service_settings: { model_id: 'elser-2-elastic' }, + }, + { + inference_id: '.preconfigured-elser', + task_type: 'sparse_embedding', + service: 'elastic', + service_settings: { model_id: 'elser' }, + }, + { + inference_id: 'endpoint-1', + task_type: 'text_embedding', + service: 'openai', + service_settings: { model_id: 'text-embedding-3-large' }, + }, ] as InferenceAPIConfigResponse[], }); @@ -432,9 +458,24 @@ describe('SelectInferenceId', () => { setupInferenceEndpointsMocks({ data: [ - { inference_id: '.elser-2-elastic', task_type: 'sparse_embedding' }, - { inference_id: '.preconfigured-elser', task_type: 'sparse_embedding' }, - { inference_id: 'endpoint-1', task_type: 'text_embedding' }, + { + inference_id: '.elser-2-elastic', + task_type: 'sparse_embedding', + service: 'elastic', + service_settings: { model_id: 'elser-2-elastic' }, + }, + { + inference_id: '.preconfigured-elser', + task_type: 'sparse_embedding', + service: 'elastic', + service_settings: { model_id: 'elser' }, + }, + { + inference_id: 'endpoint-1', + task_type: 'text_embedding', + service: 'openai', + service_settings: { model_id: 'text-embedding-3-large' }, + }, ] as InferenceAPIConfigResponse[], }); diff --git a/x-pack/platform/plugins/shared/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/select_inference_id.tsx b/x-pack/platform/plugins/shared/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/select_inference_id.tsx index 0aa05b25e09c3..8ee72e09b0734 100644 --- a/x-pack/platform/plugins/shared/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/select_inference_id.tsx +++ b/x-pack/platform/plugins/shared/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/select_inference_id.tsx @@ -45,6 +45,10 @@ type SelectInferenceIdContentProps = SelectInferenceIdProps & { value: string; }; +interface EndpointOptionData { + description: string; +} + export const SelectInferenceId: React.FC = ({ 'data-test-subj': dataTestSubj, }: SelectInferenceIdProps) => { @@ -116,13 +120,15 @@ const SelectInferenceIdContent: React.FC = ({ * Only includes endpoints compatible with semantic_text (text_embedding and sparse_embedding). * Includes optimistic updates for newly created endpoints that may not be in the list yet. */ - const options: EuiSelectableOption[] = useMemo(() => { - const selectableOptions: EuiSelectableOption[] = + const options: EuiSelectableOption[] = useMemo(() => { + const selectableOptions: EuiSelectableOption[] = compatibleEndpoints?.endpointDefinitions?.map((endpoint) => { return { + key: endpoint.inference_id, label: endpoint.inference_id, 'data-test-subj': `custom-inference_${endpoint.inference_id}`, checked: value === endpoint.inference_id ? 'on' : undefined, + description: endpoint.description, disabled: !endpoint.accessible, append: !endpoint.accessible && endpoint.requiredLicense && ( @@ -150,9 +156,11 @@ const SelectInferenceIdContent: React.FC = ({ const isValueInOptions = selectableOptions.some((option) => option.label === value); if (value && !isValueInOptions) { selectableOptions.push({ + key: value, label: value, checked: 'on', 'data-test-subj': `custom-inference_${value}`, + description: '', }); } return selectableOptions; @@ -160,6 +168,17 @@ const SelectInferenceIdContent: React.FC = ({ const selectedOptionLabel = options.find((option) => option.checked)?.label; + const renderEndpointOption = useCallback((option: EuiSelectableOption) => { + return ( + <> + {option.label} + + {option.description} + + + ); + }, []); + /** * Auto-select default inference endpoint when: * - No endpoint is currently selected (!value) @@ -282,7 +301,8 @@ const SelectInferenceIdContent: React.FC = ({ } )} > - + id="inferenceEndpointsSelectable" aria-label={i18n.translate( 'xpack.idxMgmt.mappingsEditor.parameters.inferenceId.popover.selectable.ariaLabel', { @@ -307,6 +327,11 @@ const SelectInferenceIdContent: React.FC = ({ onChange={(newOptions) => { setValue(newOptions.find((option) => option.checked)?.label || ''); }} + renderOption={renderEndpointOption} + listProps={{ + isVirtualized: false, + }} + height={euiTheme.base * 15} > {(list, search) => ( <> diff --git a/x-pack/platform/plugins/shared/index_management/public/application/sections/home/index_list/index_actions_context_menu/index_actions_context_menu.test.tsx b/x-pack/platform/plugins/shared/index_management/public/application/sections/home/index_list/index_actions_context_menu/index_actions_context_menu.test.tsx index 3732d09b5651e..de8100450854d 100644 --- a/x-pack/platform/plugins/shared/index_management/public/application/sections/home/index_list/index_actions_context_menu/index_actions_context_menu.test.tsx +++ b/x-pack/platform/plugins/shared/index_management/public/application/sections/home/index_list/index_actions_context_menu/index_actions_context_menu.test.tsx @@ -17,7 +17,9 @@ import type { Index } from '@kbn/index-management-shared-types'; import { notificationService } from '../../../../services/notification'; import { navigateToIndexDetailsPage, getIndexDetailsLink } from '../../../../services/routing'; -const user = userEvent.setup(); +// EUI context menus keep inactive panels mounted with `pointer-events: none`, +// which can cause user-event to throw when interacting with menu items. +const user = userEvent.setup({ pointerEventsCheck: 0, delay: null }); jest.mock('../../../../services/routing', () => ({ ...jest.requireActual('../../../../services/routing'), @@ -299,7 +301,8 @@ describe('IndexActionsContextMenu', () => { renderWithProviders(); await openContextMenu(); - const openBtn = await screen.findByTestId('openIndexMenuButton'); + const menu = await screen.findByTestId('indexContextMenu'); + const openBtn = await within(menu).findByTestId('openIndexMenuButton'); await user.click(openBtn); @@ -406,15 +409,18 @@ describe('IndexActionsContextMenu', () => { ); await openContextMenu(); - const overviewBtn = await screen.findByText(/show index overview/i); + const menu = await screen.findByTestId('indexContextMenu'); + const overviewBtn = await within(menu).findByText(/show index overview/i); await user.click(overviewBtn); await openContextMenu(); - const settingsBtn = await screen.findByText(/show index settings/i); + const menu2 = await screen.findByTestId('indexContextMenu'); + const settingsBtn = await within(menu2).findByText(/show index settings/i); await user.click(settingsBtn); await openContextMenu(); - const mappingBtn = await screen.findByText(/show index mapping/i); + const menu3 = await screen.findByTestId('indexContextMenu'); + const mappingBtn = await within(menu3).findByText(/show index mapping/i); await user.click(mappingBtn); expect(navigateToIndexDetailsPage).toHaveBeenCalledTimes(3); diff --git a/x-pack/platform/plugins/shared/index_management/public/hooks/use_compatible_inference_endpoints.ts b/x-pack/platform/plugins/shared/index_management/public/hooks/use_compatible_inference_endpoints.ts index fcb9d8e01746f..26b6336a85449 100644 --- a/x-pack/platform/plugins/shared/index_management/public/hooks/use_compatible_inference_endpoints.ts +++ b/x-pack/platform/plugins/shared/index_management/public/hooks/use_compatible_inference_endpoints.ts @@ -9,6 +9,7 @@ import { useMemo } from 'react'; import { defaultInferenceEndpoints } from '@kbn/inference-common'; import type { LicenseType } from '@kbn/licensing-types'; import type { InferenceAPIConfigResponse } from '@kbn/ml-trained-models-utils'; +import { SERVICE_PROVIDERS } from '@kbn/inference-endpoint-ui-common'; import { useLicense } from './use_license'; const COMPATIBLE_TASK_TYPES = ['text_embedding', 'sparse_embedding'] as const; @@ -28,6 +29,7 @@ interface EndpointDefinition { requiredLicense: string | undefined; /** Whether the endpoint is accessible to the current license. Defaults to true if no license requirement is specified. */ accessible: boolean; + description: string; } interface CompatibleEndpointsData { defaultInferenceId: string | undefined; @@ -56,6 +58,11 @@ export const useCompatibleInferenceEndpoints = ( if (!COMPATIBLE_TASK_TYPES.includes(endpoint.task_type as CompatibleTaskType)) { return; } + const provider = SERVICE_PROVIDERS[endpoint.service]; + const modelId = endpoint.service_settings.model_id ?? endpoint.service_settings.model; + const service = provider?.name ?? endpoint.service; + const description = modelId ? `${service} - ${modelId}` : service; + const isElserInEis = endpoint.inference_id === defaultInferenceEndpoints.ELSER_IN_EIS_INFERENCE_ID; const requiredLicense = INFERENCE_ENDPOINT_LICENSE_MAP[endpoint.inference_id]; @@ -70,10 +77,12 @@ export const useCompatibleInferenceEndpoints = ( defaultInferenceId = endpoint.inference_id; } } + endpointDefinitions.push({ inference_id: endpoint.inference_id, requiredLicense, accessible, + description, }); }); return {