Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}) => (
<div data-test-subj="inference-flyout-wrapper">
<button data-test-subj="mock-flyout-close" onClick={onFlyoutClose}>
Close Flyout
</button>
<button data-test-subj="mock-flyout-submit" onClick={() => onSubmitSuccess('new-endpoint-id')}>
Submit
</button>
</div>
);

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;
}) => (
<div data-test-subj="inference-flyout-wrapper">
<button data-test-subj="mock-flyout-close" onClick={onFlyoutClose}>
Close Flyout
</button>
<button
data-test-subj="mock-flyout-submit"
onClick={() => onSubmitSuccess('new-endpoint-id')}
>
Submit
</button>
</div>
);

return {
__esModule: true,
default: MockInferenceFlyoutWrapper,
SERVICE_PROVIDERS,
};
});

jest.mock('../../../public/application/services/api', () => ({
...jest.requireActual('../../../public/application/services/api'),
Expand Down Expand Up @@ -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[],
});

Expand All @@ -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[],
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,10 @@ type SelectInferenceIdContentProps = SelectInferenceIdProps & {
value: string;
};

interface EndpointOptionData {
description: string;
}

export const SelectInferenceId: React.FC<SelectInferenceIdProps> = ({
'data-test-subj': dataTestSubj,
}: SelectInferenceIdProps) => {
Expand Down Expand Up @@ -116,13 +120,15 @@ const SelectInferenceIdContent: React.FC<SelectInferenceIdContentProps> = ({
* 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<EndpointOptionData>[] = useMemo(() => {
const selectableOptions: EuiSelectableOption<EndpointOptionData>[] =
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 && (
<EuiBadge color="hollow" iconType="lock">
Expand Down Expand Up @@ -150,16 +156,29 @@ const SelectInferenceIdContent: React.FC<SelectInferenceIdContentProps> = ({
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;
}, [compatibleEndpoints, value]);

const selectedOptionLabel = options.find((option) => option.checked)?.label;

const renderEndpointOption = useCallback((option: EuiSelectableOption<EndpointOptionData>) => {
return (
<>
<EuiText size="s">{option.label}</EuiText>
<EuiText size="xs" color="subdued" className="eui-displayBlock">
<small>{option.description}</small>
</EuiText>
Comment thread
damian-polewski marked this conversation as resolved.
</>
);
}, []);

/**
* Auto-select default inference endpoint when:
* - No endpoint is currently selected (!value)
Expand Down Expand Up @@ -282,7 +301,8 @@ const SelectInferenceIdContent: React.FC<SelectInferenceIdContentProps> = ({
}
)}
>
<EuiSelectable
<EuiSelectable<EndpointOptionData>
id="inferenceEndpointsSelectable"
aria-label={i18n.translate(
'xpack.idxMgmt.mappingsEditor.parameters.inferenceId.popover.selectable.ariaLabel',
{
Expand All @@ -307,6 +327,11 @@ const SelectInferenceIdContent: React.FC<SelectInferenceIdContentProps> = ({
onChange={(newOptions) => {
setValue(newOptions.find((option) => option.checked)?.label || '');
}}
renderOption={renderEndpointOption}
listProps={{
isVirtualized: false,
}}
height={euiTheme.base * 15}
>
{(list, search) => (
<>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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'),
Expand Down Expand Up @@ -299,7 +301,8 @@ describe('IndexActionsContextMenu', () => {
renderWithProviders(<IndexActionsContextMenu {...closed} />);

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);

Expand Down Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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];
Expand All @@ -70,10 +77,12 @@ export const useCompatibleInferenceEndpoints = (
defaultInferenceId = endpoint.inference_id;
}
}

endpointDefinitions.push({
inference_id: endpoint.inference_id,
requiredLicense,
accessible,
description,
});
});
return {
Expand Down
Loading