diff --git a/x-pack/platform/packages/shared/kbn-inference-endpoint-ui-common/index.ts b/x-pack/platform/packages/shared/kbn-inference-endpoint-ui-common/index.ts
index 84ee2608c546b..b819501b35c88 100644
--- a/x-pack/platform/packages/shared/kbn-inference-endpoint-ui-common/index.ts
+++ b/x-pack/platform/packages/shared/kbn-inference-endpoint-ui-common/index.ts
@@ -6,6 +6,9 @@
*/
export { InferenceServiceFormFields } from './src/components/inference_service_form_fields';
+// default export required for React.Lazy
+// eslint-disable-next-line import/no-default-export
+export { InferenceFlyoutWrapper as default } from './src/components/inference_flyout_wrapper';
export { useProviders } from './src/hooks/use_providers';
export { SERVICE_PROVIDERS } from './src/components/providers/render_service_provider/service_provider';
diff --git a/x-pack/platform/packages/shared/kbn-inference-endpoint-ui-common/src/components/inference_flyout_wrapper.test.tsx b/x-pack/platform/packages/shared/kbn-inference-endpoint-ui-common/src/components/inference_flyout_wrapper.test.tsx
new file mode 100644
index 0000000000000..8241715df02fd
--- /dev/null
+++ b/x-pack/platform/packages/shared/kbn-inference-endpoint-ui-common/src/components/inference_flyout_wrapper.test.tsx
@@ -0,0 +1,90 @@
+/*
+ * 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 { Form, useForm } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib';
+import { I18nProvider } from '@kbn/i18n-react';
+import userEvent from '@testing-library/user-event';
+import { render, screen } from '@testing-library/react';
+import { act } from 'react-dom/test-utils';
+import React from 'react';
+import { httpServiceMock } from '@kbn/core-http-browser-mocks';
+import { notificationServiceMock } from '@kbn/core-notifications-browser-mocks';
+
+import { InferenceFlyoutWrapper } from './inference_flyout_wrapper';
+import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
+import { mockProviders } from '../utils/mock_providers';
+
+const mockAddEndpoint = jest.fn();
+const mockOnSubmitSuccess = jest.fn();
+const mockOnClose = jest.fn();
+const httpMock = httpServiceMock.createStartContract();
+const notificationsMock = notificationServiceMock.createStartContract();
+
+jest.mock('../hooks/use_providers', () => ({
+ useProviders: jest.fn(() => ({
+ data: mockProviders,
+ })),
+}));
+
+const MockFormProvider = ({ children }: { children: React.ReactElement }) => {
+ const { form } = useForm();
+ const queryClient = new QueryClient();
+ return (
+
+
+
+
+
+ );
+};
+
+describe('InferenceFlyout', () => {
+ beforeEach(async () => {
+ jest.clearAllMocks();
+
+ await act(async () => {
+ render(
+
+
+
+ );
+ });
+ });
+
+ it('renders', async () => {
+ expect(screen.getByTestId('inference-flyout')).toBeInTheDocument();
+ expect(screen.getByTestId('inference-flyout-header')).toBeInTheDocument();
+ expect(screen.getByTestId('inference-flyout-close-button')).toBeInTheDocument();
+ });
+
+ it('invalidates form if no provider is selected', async () => {
+ await userEvent.click(screen.getByTestId('inference-endpoint-submit-button'));
+ expect(screen.getByText('Provider is required.')).toBeInTheDocument();
+ expect(mockAddEndpoint).not.toHaveBeenCalled();
+ expect(screen.getByTestId('inference-endpoint-submit-button')).toBeDisabled();
+ });
+
+ it('submit form', async () => {
+ await userEvent.click(screen.getByTestId('provider-select'));
+ await userEvent.click(screen.getByText('Elasticsearch'));
+ await userEvent.click(screen.getByTestId('inference-endpoint-submit-button'));
+
+ expect(mockAddEndpoint).toHaveBeenCalled();
+ });
+
+ it('closes flyout', async () => {
+ await userEvent.click(screen.getByTestId('inference-flyout-close-button'));
+ expect(mockOnClose).toBeCalled();
+ });
+});
diff --git a/x-pack/platform/packages/shared/kbn-inference-endpoint-ui-common/src/components/inference_flyout_wrapper.tsx b/x-pack/platform/packages/shared/kbn-inference-endpoint-ui-common/src/components/inference_flyout_wrapper.tsx
new file mode 100644
index 0000000000000..c2c195637d216
--- /dev/null
+++ b/x-pack/platform/packages/shared/kbn-inference-endpoint-ui-common/src/components/inference_flyout_wrapper.tsx
@@ -0,0 +1,126 @@
+/*
+ * 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 {
+ EuiButton,
+ EuiButtonEmpty,
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiFlyout,
+ EuiFlyoutBody,
+ EuiFlyoutFooter,
+ EuiFlyoutHeader,
+ EuiSpacer,
+ EuiTitle,
+ useGeneratedHtmlId,
+} from '@elastic/eui';
+import React, { useCallback, useState } from 'react';
+
+import { HttpSetup, IToasts } from '@kbn/core/public';
+import { Form, useForm } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib';
+import * as LABELS from '../translations';
+import { InferenceEndpoint } from '../types/types';
+import { InferenceServiceFormFields } from './inference_service_form_fields';
+
+interface InferenceFlyoutWrapperProps {
+ onFlyoutClose: (state: boolean) => void;
+ addInferenceEndpoint: (
+ inferenceEndpoint: InferenceEndpoint,
+ onSuccess: (inferenceId: string) => void,
+ onError: () => void
+ ) => Promise;
+ http: HttpSetup;
+ toasts: IToasts;
+ onSubmitSuccess?: (inferenceId: string) => void;
+ isEdit?: boolean;
+}
+
+export const InferenceFlyoutWrapper: React.FC = ({
+ onFlyoutClose,
+ addInferenceEndpoint,
+ http,
+ toasts,
+ onSubmitSuccess,
+ isEdit,
+}) => {
+ const inferenceCreationFlyoutId = useGeneratedHtmlId({
+ prefix: 'InferenceFlyoutId',
+ });
+ const closeFlyout = () => onFlyoutClose(false);
+ const [isLoading, setIsLoading] = useState(false);
+ const onSuccess = useCallback(
+ (inferenceId: string) => {
+ setIsLoading(false);
+ onSubmitSuccess?.(inferenceId);
+ },
+ [onSubmitSuccess]
+ );
+ const onError = useCallback(() => {
+ setIsLoading(false);
+ }, []);
+
+ const { form } = useForm();
+ const handleSubmit = useCallback(async () => {
+ setIsLoading(true);
+ const { isValid, data } = await form.submit();
+
+ if (isValid) {
+ addInferenceEndpoint(data as InferenceEndpoint, onSuccess, onError);
+ } else {
+ setIsLoading(false);
+ }
+ }, [addInferenceEndpoint, form, onError, onSuccess]);
+
+ return (
+
+
+
+ {LABELS.ENDPOINT_TITLE}
+
+
+
+
+
+
+
+
+
+ {LABELS.CANCEL}
+
+
+
+
+
+ );
+};
diff --git a/x-pack/platform/packages/shared/kbn-inference-endpoint-ui-common/src/components/inference_service_form_fields.test.tsx b/x-pack/platform/packages/shared/kbn-inference-endpoint-ui-common/src/components/inference_service_form_fields.test.tsx
index edf80bde790d8..a9cadc5c16186 100644
--- a/x-pack/platform/packages/shared/kbn-inference-endpoint-ui-common/src/components/inference_service_form_fields.test.tsx
+++ b/x-pack/platform/packages/shared/kbn-inference-endpoint-ui-common/src/components/inference_service_form_fields.test.tsx
@@ -6,7 +6,6 @@
*/
import { InferenceServiceFormFields } from './inference_service_form_fields';
-import { FieldType, InferenceProvider } from '../types/types';
import React from 'react';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
@@ -14,111 +13,7 @@ import { Form, useForm } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_l
import { I18nProvider } from '@kbn/i18n-react';
import { httpServiceMock } from '@kbn/core-http-browser-mocks';
import { notificationServiceMock } from '@kbn/core-notifications-browser-mocks';
-
-const mockProviders = [
- {
- service: 'hugging_face',
- name: 'Hugging Face',
- task_types: ['text_embedding', 'sparse_embedding'],
- configurations: {
- api_key: {
- default_value: null,
- description: `API Key for the provider you're connecting to.`,
- label: 'API Key',
- required: true,
- sensitive: true,
- updatable: true,
- type: FieldType.STRING,
- supported_task_types: ['text_embedding', 'sparse_embedding'],
- },
- 'rate_limit.requests_per_minute': {
- default_value: null,
- description: 'Minimize the number of rate limit errors.',
- label: 'Rate Limit',
- required: false,
- sensitive: false,
- updatable: true,
- type: FieldType.INTEGER,
- supported_task_types: ['text_embedding', 'sparse_embedding'],
- },
- url: {
- default_value: 'https://api.openai.com/v1/embeddings',
- description: 'The URL endpoint to use for the requests.',
- label: 'URL',
- required: true,
- sensitive: false,
- updatable: true,
- type: FieldType.STRING,
- supported_task_types: ['text_embedding', 'sparse_embedding'],
- },
- },
- },
- {
- service: 'cohere',
- name: 'Cohere',
- task_types: ['text_embedding', 'rerank', 'completion'],
- configurations: {
- api_key: {
- default_value: null,
- description: `API Key for the provider you're connecting to.`,
- label: 'API Key',
- required: true,
- sensitive: true,
- updatable: true,
- type: FieldType.STRING,
- supported_task_types: ['text_embedding', 'rerank', 'completion'],
- },
- 'rate_limit.requests_per_minute': {
- default_value: null,
- description: 'Minimize the number of rate limit errors.',
- label: 'Rate Limit',
- required: false,
- sensitive: false,
- updatable: true,
- type: FieldType.INTEGER,
- supported_task_types: ['text_embedding', 'completion'],
- },
- },
- },
- {
- service: 'anthropic',
- name: 'Anthropic',
- task_types: ['completion'],
- configurations: {
- api_key: {
- default_value: null,
- description: `API Key for the provider you're connecting to.`,
- label: 'API Key',
- required: true,
- sensitive: true,
- updatable: true,
- type: FieldType.STRING,
- supported_task_types: ['completion'],
- },
- 'rate_limit.requests_per_minute': {
- default_value: null,
- description:
- 'By default, the anthropic service sets the number of requests allowed per minute to 50.',
- label: 'Rate Limit',
- required: false,
- sensitive: false,
- updatable: true,
- type: FieldType.INTEGER,
- supported_task_types: ['completion'],
- },
- model_id: {
- default_value: null,
- description: 'The name of the model to use for the inference task.',
- label: 'Model ID',
- required: true,
- sensitive: false,
- updatable: true,
- type: FieldType.STRING,
- supported_task_types: ['completion'],
- },
- },
- },
-] as InferenceProvider[];
+import { mockProviders } from '../utils/mock_providers';
jest.mock('../hooks/use_providers', () => ({
useProviders: jest.fn(() => ({
diff --git a/x-pack/platform/packages/shared/kbn-inference-endpoint-ui-common/src/translations.ts b/x-pack/platform/packages/shared/kbn-inference-endpoint-ui-common/src/translations.ts
index 3c9bab9ecb6fe..867625b993bc5 100644
--- a/x-pack/platform/packages/shared/kbn-inference-endpoint-ui-common/src/translations.ts
+++ b/x-pack/platform/packages/shared/kbn-inference-endpoint-ui-common/src/translations.ts
@@ -134,3 +134,18 @@ export const GET_PROVIDERS_FAILED = i18n.translate(
defaultMessage: 'Unable to find providers',
}
);
+
+export const ENDPOINT_TITLE = i18n.translate(
+ 'xpack.inferenceEndpointUICommon.components.EndpointTitle',
+ {
+ defaultMessage: 'Inference Endpoint',
+ }
+);
+
+export const CANCEL = i18n.translate('xpack.inferenceEndpointUICommon.components.cancelBtnLabel', {
+ defaultMessage: 'Cancel',
+});
+
+export const SAVE = i18n.translate('xpack.inferenceEndpointUICommon.components.saveBtnLabel', {
+ defaultMessage: 'Save',
+});
diff --git a/x-pack/platform/packages/shared/kbn-inference-endpoint-ui-common/src/types/types.ts b/x-pack/platform/packages/shared/kbn-inference-endpoint-ui-common/src/types/types.ts
index 7e9533cf047dc..17f163cabdb5d 100644
--- a/x-pack/platform/packages/shared/kbn-inference-endpoint-ui-common/src/types/types.ts
+++ b/x-pack/platform/packages/shared/kbn-inference-endpoint-ui-common/src/types/types.ts
@@ -53,3 +53,8 @@ export interface Secrets {
}
export const INFERENCE_ENDPOINT_INTERNAL_API_VERSION = '1';
+
+export interface InferenceEndpoint {
+ config: Config;
+ secrets: Secrets;
+}
diff --git a/x-pack/platform/packages/shared/kbn-inference-endpoint-ui-common/src/utils/mock_providers.ts b/x-pack/platform/packages/shared/kbn-inference-endpoint-ui-common/src/utils/mock_providers.ts
new file mode 100644
index 0000000000000..032d678c55085
--- /dev/null
+++ b/x-pack/platform/packages/shared/kbn-inference-endpoint-ui-common/src/utils/mock_providers.ts
@@ -0,0 +1,151 @@
+/*
+ * 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 { FieldType, InferenceProvider } from '../types/types';
+
+export const mockProviders: InferenceProvider[] = [
+ {
+ service: 'hugging_face',
+ name: 'Hugging Face',
+ task_types: ['text_embedding', 'sparse_embedding'],
+ configurations: {
+ api_key: {
+ default_value: null,
+ description: `API Key for the provider you're connecting to.`,
+ label: 'API Key',
+ required: true,
+ sensitive: true,
+ updatable: true,
+ type: FieldType.STRING,
+ supported_task_types: ['text_embedding', 'sparse_embedding'],
+ },
+ 'rate_limit.requests_per_minute': {
+ default_value: null,
+ description: 'Minimize the number of rate limit errors.',
+ label: 'Rate Limit',
+ required: false,
+ sensitive: false,
+ updatable: true,
+ type: FieldType.INTEGER,
+ supported_task_types: ['text_embedding', 'sparse_embedding'],
+ },
+ url: {
+ default_value: 'https://api.openai.com/v1/embeddings',
+ description: 'The URL endpoint to use for the requests.',
+ label: 'URL',
+ required: true,
+ sensitive: false,
+ updatable: true,
+ type: FieldType.STRING,
+ supported_task_types: ['text_embedding', 'sparse_embedding'],
+ },
+ },
+ },
+ {
+ service: 'cohere',
+ name: 'Cohere',
+ task_types: ['text_embedding', 'rerank', 'completion'],
+ configurations: {
+ api_key: {
+ default_value: null,
+ description: `API Key for the provider you're connecting to.`,
+ label: 'API Key',
+ required: true,
+ sensitive: true,
+ updatable: true,
+ type: FieldType.STRING,
+ supported_task_types: ['text_embedding', 'rerank', 'completion'],
+ },
+ 'rate_limit.requests_per_minute': {
+ default_value: null,
+ description: 'Minimize the number of rate limit errors.',
+ label: 'Rate Limit',
+ required: false,
+ sensitive: false,
+ updatable: true,
+ type: FieldType.INTEGER,
+ supported_task_types: ['text_embedding', 'rerank', 'completion'],
+ },
+ },
+ },
+ {
+ service: 'anthropic',
+ name: 'Anthropic',
+ task_types: ['completion'],
+ configurations: {
+ api_key: {
+ default_value: null,
+ description: `API Key for the provider you're connecting to.`,
+ label: 'API Key',
+ required: true,
+ sensitive: true,
+ updatable: true,
+ type: FieldType.STRING,
+ supported_task_types: ['completion'],
+ },
+ 'rate_limit.requests_per_minute': {
+ default_value: null,
+ description:
+ 'By default, the anthropic service sets the number of requests allowed per minute to 50.',
+ label: 'Rate Limit',
+ required: false,
+ sensitive: false,
+ updatable: true,
+ type: FieldType.INTEGER,
+ supported_task_types: ['completion'],
+ },
+ model_id: {
+ default_value: null,
+ description: 'The name of the model to use for the inference task.',
+ label: 'Model ID',
+ required: true,
+ sensitive: false,
+ updatable: true,
+ type: FieldType.STRING,
+ supported_task_types: ['completion'],
+ },
+ },
+ },
+ {
+ service: 'elasticsearch',
+ name: 'Elasticsearch',
+ task_types: ['sparse_embedding', 'text_embedding', 'rerank'],
+ configurations: {
+ num_allocations: {
+ default_value: 1,
+ description:
+ 'The total number of allocations this model is assigned across machine learning nodes.',
+ label: 'Number Allocations',
+ required: true,
+ sensitive: false,
+ updatable: true,
+ type: FieldType.INTEGER,
+ supported_task_types: ['sparse_embedding', 'text_embedding', 'rerank'],
+ },
+ num_threads: {
+ default_value: 2,
+ description: 'Sets the number of threads used by each model allocation during inference.',
+ label: 'Number Threads',
+ required: true,
+ sensitive: false,
+ updatable: true,
+ type: FieldType.INTEGER,
+ supported_task_types: ['sparse_embedding', 'text_embedding', 'rerank'],
+ },
+ model_id: {
+ default_value: '.elser_model_2',
+ description: 'The name of the model to use for the inference task.',
+ label: 'Model ID',
+ required: true,
+ sensitive: false,
+ updatable: true,
+ type: FieldType.STRING,
+ supported_task_types: ['sparse_embedding', 'text_embedding', 'rerank'],
+ },
+ },
+ },
+];
diff --git a/x-pack/platform/plugins/private/translations/translations/fr-FR.json b/x-pack/platform/plugins/private/translations/translations/fr-FR.json
index ec3840dd19284..59bb6e6404e60 100644
--- a/x-pack/platform/plugins/private/translations/translations/fr-FR.json
+++ b/x-pack/platform/plugins/private/translations/translations/fr-FR.json
@@ -20773,7 +20773,6 @@
"xpack.idxMgmt.mappingsEditor.createField.addMultiFieldButtonLabel": "Ajouter un champ multiple",
"xpack.idxMgmt.mappingsEditor.createField.cancelButtonLabel": "Annuler",
"xpack.idxMgmt.mappingsEditor.createField.modelDeployedNotification": "Le modèle {modelName} a été déployé sur votre nœud de machine learning.",
- "xpack.idxMgmt.mappingsEditor.createField.modelDeploymentErrorTitle": "Échec du déploiement du modèle",
"xpack.idxMgmt.mappingsEditor.createField.modelDeploymentNotification": "Le modèle {modelName} est en cours de déploiement sur votre nœud de machine learning.",
"xpack.idxMgmt.mappingsEditor.createField.modelDeploymentStartedNotification": "Le déploiement du modèle a commencé",
"xpack.idxMgmt.mappingsEditor.customButtonLabel": "Utiliser un analyseur personnalisé",
@@ -21133,7 +21132,6 @@
"xpack.idxMgmt.mappingsEditor.parameters.dimsHelpTextDescription": "Nombre de dimensions dans le vecteur.",
"xpack.idxMgmt.mappingsEditor.parameters.geoPointNullValueHelpText": "Vous pouvez exprimer les points géographiques sous la forme d'un objet, d'une chaîne, d'un geohash, d'un tableau ou d'un POINT {docsLink}.",
"xpack.idxMgmt.mappingsEditor.parameters.inferenceId.popover.alreadyExistsLabel": "Aucun point de terminaison d'inférence sélectionné",
- "xpack.idxMgmt.mappingsEditor.parameters.inferenceId.popover.defaultLabel": "Le point de terminaison d'inférence {inferenceId} existe déjà",
"xpack.idxMgmt.mappingsEditor.parameters.inferenceId.popover.manageInferenceEndpointButton": "Gérer les points de terminaison d'inférence",
"xpack.idxMgmt.mappingsEditor.parameters.inferenceId.popover.selectable.ariaLabel": "Points de terminaison existants",
"xpack.idxMgmt.mappingsEditor.parameters.inferenceId.popover.selectable.Label": "Points de terminaison existants",
@@ -21223,7 +21221,6 @@
"xpack.idxMgmt.mappingsEditor.searchQuoteAnalyzerFieldLabel": "Analyseur de termes de recherche",
"xpack.idxMgmt.mappingsEditor.searchResult.emptyPrompt.clearSearchButtonLabel": "Effacer la recherche",
"xpack.idxMgmt.mappingsEditor.searchResult.emptyPromptTitle": "Aucun champ ne correspond à votre recherche",
- "xpack.idxMgmt.mappingsEditor.semanticText.inferenceError": "Aucun modèle d'inférence trouvé pour l'ID d'inférence {inferenceId}",
"xpack.idxMgmt.mappingsEditor.semanticTextNameFieldLabel": "Nom du nouveau champ",
"xpack.idxMgmt.mappingsEditor.setSimilarityFieldDescription": "Algorithme de notation ou similarité à utiliser.",
"xpack.idxMgmt.mappingsEditor.setSimilarityFieldTitle": "Définir la similarité",
diff --git a/x-pack/platform/plugins/private/translations/translations/ja-JP.json b/x-pack/platform/plugins/private/translations/translations/ja-JP.json
index 3531e85feb65c..f228052f48391 100644
--- a/x-pack/platform/plugins/private/translations/translations/ja-JP.json
+++ b/x-pack/platform/plugins/private/translations/translations/ja-JP.json
@@ -20633,7 +20633,6 @@
"xpack.idxMgmt.mappingsEditor.createField.addMultiFieldButtonLabel": "マルチフィールドの追加",
"xpack.idxMgmt.mappingsEditor.createField.cancelButtonLabel": "キャンセル",
"xpack.idxMgmt.mappingsEditor.createField.modelDeployedNotification": "モデル\"{modelName}\"が機械学習ノードにデプロイされました。",
- "xpack.idxMgmt.mappingsEditor.createField.modelDeploymentErrorTitle": "モデルのデプロイが失敗しました",
"xpack.idxMgmt.mappingsEditor.createField.modelDeploymentNotification": "モデル\"{modelName}\"は機械学習ノードにデプロイ中です。",
"xpack.idxMgmt.mappingsEditor.createField.modelDeploymentStartedNotification": "モデルのデプロイが開始しました",
"xpack.idxMgmt.mappingsEditor.customButtonLabel": "カスタムアナライザーの使用",
@@ -20993,7 +20992,6 @@
"xpack.idxMgmt.mappingsEditor.parameters.dimsHelpTextDescription": "ベルトルでの次元数。",
"xpack.idxMgmt.mappingsEditor.parameters.geoPointNullValueHelpText": "地点は、オブジェクト、文字列、ジオハッシュ、配列または{docsLink} POINTとして表現できます。",
"xpack.idxMgmt.mappingsEditor.parameters.inferenceId.popover.alreadyExistsLabel": "推論エンドポイントが選択されていません",
- "xpack.idxMgmt.mappingsEditor.parameters.inferenceId.popover.defaultLabel": "推論エンドポイント{inferenceId}はすでに存在します",
"xpack.idxMgmt.mappingsEditor.parameters.inferenceId.popover.manageInferenceEndpointButton": "推論エンドポイントを管理",
"xpack.idxMgmt.mappingsEditor.parameters.inferenceId.popover.selectable.ariaLabel": "既存のエンドポイント",
"xpack.idxMgmt.mappingsEditor.parameters.inferenceId.popover.selectable.Label": "既存のエンドポイント",
@@ -21083,7 +21081,6 @@
"xpack.idxMgmt.mappingsEditor.searchQuoteAnalyzerFieldLabel": "検索見積もりアナライザー",
"xpack.idxMgmt.mappingsEditor.searchResult.emptyPrompt.clearSearchButtonLabel": "検索のクリア",
"xpack.idxMgmt.mappingsEditor.searchResult.emptyPromptTitle": "検索にマッチするフィールドがありません",
- "xpack.idxMgmt.mappingsEditor.semanticText.inferenceError": "推論ID {inferenceId}の推論モデルが見つかりません",
"xpack.idxMgmt.mappingsEditor.semanticTextNameFieldLabel": "新しいフィールド名",
"xpack.idxMgmt.mappingsEditor.setSimilarityFieldDescription": "使用するためのスコアリングアルゴリズムや類似性。",
"xpack.idxMgmt.mappingsEditor.setSimilarityFieldTitle": "類似性の設定",
diff --git a/x-pack/platform/plugins/private/translations/translations/zh-CN.json b/x-pack/platform/plugins/private/translations/translations/zh-CN.json
index 43226ddc03bf0..4618a932f3e1a 100644
--- a/x-pack/platform/plugins/private/translations/translations/zh-CN.json
+++ b/x-pack/platform/plugins/private/translations/translations/zh-CN.json
@@ -20302,7 +20302,6 @@
"xpack.idxMgmt.mappingsEditor.createField.addMultiFieldButtonLabel": "添加多字段",
"xpack.idxMgmt.mappingsEditor.createField.cancelButtonLabel": "取消",
"xpack.idxMgmt.mappingsEditor.createField.modelDeployedNotification": "模型 {modelName} 已部署到您的 Machine Learning 节点。",
- "xpack.idxMgmt.mappingsEditor.createField.modelDeploymentErrorTitle": "模型部署失败",
"xpack.idxMgmt.mappingsEditor.createField.modelDeploymentNotification": "模型 {modelName} 正部署到您的 Machine Learning 节点。",
"xpack.idxMgmt.mappingsEditor.createField.modelDeploymentStartedNotification": "模型部署已开始",
"xpack.idxMgmt.mappingsEditor.customButtonLabel": "使用定制分析器",
@@ -20658,7 +20657,6 @@
"xpack.idxMgmt.mappingsEditor.parameters.dimsHelpTextDescription": "向量中的维度数。",
"xpack.idxMgmt.mappingsEditor.parameters.geoPointNullValueHelpText": "地理坐标点可表示为对象、字符串、geohash、数组或 {docsLink} POINT。",
"xpack.idxMgmt.mappingsEditor.parameters.inferenceId.popover.alreadyExistsLabel": "未选择推理终端",
- "xpack.idxMgmt.mappingsEditor.parameters.inferenceId.popover.defaultLabel": "推理终端 {inferenceId} 已存在",
"xpack.idxMgmt.mappingsEditor.parameters.inferenceId.popover.manageInferenceEndpointButton": "管理推理终端",
"xpack.idxMgmt.mappingsEditor.parameters.inferenceId.popover.selectable.ariaLabel": "现有终端",
"xpack.idxMgmt.mappingsEditor.parameters.inferenceId.popover.selectable.Label": "现有终端",
@@ -20748,7 +20746,6 @@
"xpack.idxMgmt.mappingsEditor.searchQuoteAnalyzerFieldLabel": "搜索引号分析器",
"xpack.idxMgmt.mappingsEditor.searchResult.emptyPrompt.clearSearchButtonLabel": "清除搜索",
"xpack.idxMgmt.mappingsEditor.searchResult.emptyPromptTitle": "没有字段匹配您的搜索",
- "xpack.idxMgmt.mappingsEditor.semanticText.inferenceError": "找不到推理 ID 为 {inferenceId} 的推理模型",
"xpack.idxMgmt.mappingsEditor.semanticTextNameFieldLabel": "新字段名称",
"xpack.idxMgmt.mappingsEditor.setSimilarityFieldDescription": "要使用的评分算法或相似度。",
"xpack.idxMgmt.mappingsEditor.setSimilarityFieldTitle": "设置相似度",
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 81272dc1c4c8d..6861e3c6cd3ee 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
@@ -18,7 +18,6 @@ import {
import React from 'react';
import { InferenceAPIConfigResponse } from '@kbn/ml-trained-models-utils';
-const createInferenceEndpointMock = jest.fn();
const mockDispatch = jest.fn();
const INFERENCE_LOCATOR = 'SEARCH_INFERENCE_ENDPOINTS';
const createMockLocator = (id: string) => ({
@@ -36,6 +35,11 @@ jest.mock('../../../public/application/app_context', () => ({
},
},
},
+ services: {
+ notificationService: {
+ toasts: {},
+ },
+ },
docLinks: {
links: {
inferenceManagement: {
@@ -44,14 +48,6 @@ jest.mock('../../../public/application/app_context', () => ({
},
},
plugins: {
- ml: {
- mlApi: {
- trainedModels: {
- getTrainedModels: jest.fn().mockResolvedValue([]),
- getTrainedModelStats: jest.fn().mockResolvedValue([]),
- },
- },
- },
share: {
url: {
locators: {
@@ -120,7 +116,6 @@ describe('SelectInferenceId', () => {
beforeAll(async () => {
const defaultProps: SelectInferenceIdProps = {
'data-test-subj': 'data-inference-endpoint-list',
- createInferenceEndpoint: createInferenceEndpointMock,
};
const setup = registerTestBed(getTestForm(SelectInferenceId), {
defaultProps,
@@ -142,6 +137,7 @@ describe('SelectInferenceId', () => {
find('inferenceIdButton').simulate('click');
expect(exists('learn-how-to-create-inference-endpoints')).toBe(true);
expect(exists('manageInferenceEndpointButton')).toBe(true);
+ expect(exists('createInferenceEndpointButton')).toBe(true);
});
it('should display the inference endpoints in the combo', () => {
diff --git a/x-pack/platform/plugins/shared/index_management/public/application/app_context.tsx b/x-pack/platform/plugins/shared/index_management/public/application/app_context.tsx
index 5fc1363050026..fbadd9a5df565 100644
--- a/x-pack/platform/plugins/shared/index_management/public/application/app_context.tsx
+++ b/x-pack/platform/plugins/shared/index_management/public/application/app_context.tsx
@@ -29,6 +29,7 @@ import type { ConsolePluginStart } from '@kbn/console-plugin/public';
import { EuiBreadcrumb } from '@elastic/eui';
import { LicensingPluginStart } from '@kbn/licensing-plugin/public';
+import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { ExtensionsService } from '../services';
import { HttpService, NotificationService, UiMetricService } from './services';
import { IndexManagementBreadcrumb } from './services/breadcrumbs';
@@ -90,6 +91,8 @@ export interface AppDependencies {
};
}
+const queryClient = new QueryClient({});
+
export const AppContextProvider = ({
children,
value,
@@ -97,7 +100,11 @@ export const AppContextProvider = ({
value: AppDependencies;
children: React.ReactNode;
}) => {
- return {children};
+ return (
+
+ {children}
+
+ );
};
export const AppContextConsumer = AppContext.Consumer;
diff --git a/x-pack/platform/plugins/shared/index_management/public/application/components/mappings_editor/__jest__/client_integration/mappings_editor.test.tsx b/x-pack/platform/plugins/shared/index_management/public/application/components/mappings_editor/__jest__/client_integration/mappings_editor.test.tsx
index 1a99d74e6bb13..a77748379c0b9 100644
--- a/x-pack/platform/plugins/shared/index_management/public/application/components/mappings_editor/__jest__/client_integration/mappings_editor.test.tsx
+++ b/x-pack/platform/plugins/shared/index_management/public/application/components/mappings_editor/__jest__/client_integration/mappings_editor.test.tsx
@@ -29,7 +29,10 @@ describe('Mappings editor: core', () => {
let getMappingsEditorData = getMappingsEditorDataFactory(onChangeHandler);
let testBed: MappingsEditorTestBed;
const appDependencies = {
- core: { application: {} },
+ core: { application: {}, http: {} },
+ services: {
+ notificationService: { toasts: {} },
+ },
docLinks: {
links: {
inferenceManagement: {
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 813cc1023c06d..e9ed4d5457090 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
@@ -22,28 +22,19 @@ import {
EuiTitle,
EuiIcon,
EuiLink,
+ EuiLoadingSpinner,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
-import React, { useEffect, useState, useCallback, useMemo } from 'react';
+import React, { useState, useCallback, useMemo, lazy, Suspense } from 'react';
-import { SUPPORTED_PYTORCH_TASKS, TRAINED_MODEL_TYPE } from '@kbn/ml-trained-models-utils';
-import { InferenceTaskType } from '@elastic/elasticsearch/lib/api/types';
-import { ModelConfig } from '@kbn/inference_integration_flyout/types';
-import { InferenceFlyoutWrapper } from '@kbn/inference_integration_flyout/components/inference_flyout_wrapper';
-import { TrainedModelConfigResponse } from '@kbn/ml-plugin/common/types/trained_models';
+import { useAddEndpoint } from '../../../../../hooks/use_add_endpoint';
import { getFieldConfig } from '../../../lib';
import { useAppContext } from '../../../../../app_context';
import { useLoadInferenceEndpoints } from '../../../../../services/api';
-import { useMLModelNotificationToasts } from '../../../../../../hooks/use_ml_model_status_toasts';
-import { CustomInferenceEndpointConfig } from '../../../types';
import { UseField } from '../../../shared_imports';
+const InferenceFlyoutWrapper = lazy(() => import('@kbn/inference-endpoint-ui-common'));
export interface SelectInferenceIdProps {
- createInferenceEndpoint: (
- trainedModelId: string,
- inferenceId: string,
- modelConfig: CustomInferenceEndpointConfig
- ) => Promise;
'data-test-subj'?: string;
}
@@ -53,7 +44,6 @@ type SelectInferenceIdContentProps = SelectInferenceIdProps & {
};
export const SelectInferenceId: React.FC = ({
- createInferenceEndpoint,
'data-test-subj': dataTestSubj,
}: SelectInferenceIdProps) => {
const config = getFieldConfig('inference_id');
@@ -62,7 +52,6 @@ export const SelectInferenceId: React.FC = ({
{(field) => {
return (
= ({
};
const SelectInferenceIdContent: React.FC = ({
- createInferenceEndpoint,
'data-test-subj': dataTestSubj,
setValue,
value,
}) => {
const {
- core: { application },
+ core: { application, http },
+ services: {
+ notificationService: { toasts },
+ },
docLinks,
- plugins: { ml, share },
+ plugins: { share },
} = useAppContext();
+ const { addInferenceEndpoint } = useAddEndpoint();
const config = getFieldConfig('inference_id');
const inferenceEndpointsPageLink = share?.url.locators
@@ -91,35 +83,21 @@ const SelectInferenceIdContent: React.FC = ({
?.useUrl({});
const [isInferenceFlyoutVisible, setIsInferenceFlyoutVisible] = useState(false);
- const [availableTrainedModels, setAvailableTrainedModels] = useState<
- TrainedModelConfigResponse[]
- >([]);
const onFlyoutClose = useCallback(() => {
setIsInferenceFlyoutVisible(!isInferenceFlyoutVisible);
}, [isInferenceFlyoutVisible]);
- useEffect(() => {
- const fetchAvailableTrainedModels = async () => {
- setAvailableTrainedModels((await ml?.mlApi?.trainedModels?.getTrainedModels()) ?? []);
- };
- fetchAvailableTrainedModels();
- }, [ml]);
- const trainedModels = useMemo(() => {
- const availableTrainedModelsList = availableTrainedModels
- .filter(
- (model: TrainedModelConfigResponse) =>
- model.model_type === TRAINED_MODEL_TYPE.PYTORCH &&
- (model?.inference_config
- ? Object.keys(model.inference_config).includes(SUPPORTED_PYTORCH_TASKS.TEXT_EMBEDDING)
- : {})
- )
- .map((model: TrainedModelConfigResponse) => model.model_id);
+ const { isLoading, data: endpoints, resendRequest } = useLoadInferenceEndpoints();
- return availableTrainedModelsList;
- }, [availableTrainedModels]);
- const [isSaveInferenceLoading, setIsSaveInferenceLoading] = useState(false);
+ const onSubmitSuccess = useCallback(
+ (newEndpointId: string) => {
+ resendRequest();
+ setValue(newEndpointId);
- const { isLoading, data: endpoints, resendRequest } = useLoadInferenceEndpoints();
+ setIsInferenceFlyoutVisible(!isInferenceFlyoutVisible);
+ },
+ [isInferenceFlyoutVisible, resendRequest, setValue]
+ );
const options: EuiSelectableOption[] = useMemo(() => {
const filteredEndpoints = endpoints?.filter(
@@ -152,52 +130,7 @@ const SelectInferenceIdContent: React.FC = ({
return newOptions;
}, [endpoints, value]);
- const { showErrorToasts } = useMLModelNotificationToasts();
-
- const onSaveInferenceCallback = useCallback(
- async (inferenceId: string, taskType: InferenceTaskType, modelConfig: ModelConfig) => {
- try {
- const trainedModelId = modelConfig.service_settings.model_id || '';
- const customModelConfig = {
- taskType,
- modelConfig,
- };
- setIsSaveInferenceLoading(true);
- await createInferenceEndpoint(trainedModelId, inferenceId, customModelConfig);
- resendRequest();
- setValue(inferenceId);
- setIsInferenceFlyoutVisible(!isInferenceFlyoutVisible);
- setIsSaveInferenceLoading(false);
- } catch (error) {
- showErrorToasts(error);
- setIsSaveInferenceLoading(false);
- }
- },
- [createInferenceEndpoint, setValue, isInferenceFlyoutVisible, showErrorToasts, resendRequest]
- );
const [isInferencePopoverVisible, setIsInferencePopoverVisible] = useState(false);
- const [inferenceEndpointError, setInferenceEndpointError] = useState(
- undefined
- );
- const onInferenceEndpointChange = useCallback(
- async (inferenceId: string) => {
- const modelsExist = options.some((i) => i.label === inferenceId);
- if (modelsExist) {
- setInferenceEndpointError(
- i18n.translate(
- 'xpack.idxMgmt.mappingsEditor.parameters.inferenceId.popover.defaultLabel',
- {
- defaultMessage: 'Inference endpoint {inferenceId} already exists',
- values: { inferenceId },
- }
- )
- );
- } else {
- setInferenceEndpointError(undefined);
- }
- },
- [options]
- );
const selectedOptionLabel = options.find((option) => option.checked)?.label;
@@ -234,8 +167,26 @@ const SelectInferenceIdContent: React.FC = ({
panelPaddingSize="m"
closePopover={() => setIsInferencePopoverVisible(!isInferencePopoverVisible)}
>
- {inferenceEndpointsPageLink && (
-
+
+ {
+ e.preventDefault();
+ setIsInferenceFlyoutVisible(true);
+ setIsInferencePopoverVisible(!isInferencePopoverVisible);
+ }}
+ >
+ {i18n.translate(
+ 'xpack.idxMgmt.mappingsEditor.parameters.inferenceId.popover.createInferenceEndpointButton',
+ {
+ defaultMessage: 'Add inference endpoint',
+ }
+ )}
+
+ {inferenceEndpointsPageLink && (
= ({
}
)}
-
- )}
+ )}
+
@@ -327,22 +278,18 @@ const SelectInferenceIdContent: React.FC = ({
{inferencePopover()}
- {isInferenceFlyoutVisible && (
-
- )}
+ {isInferenceFlyoutVisible ? (
+ }>
+
+
+ ) : null}
;
@@ -77,8 +76,9 @@ export const CreateField = React.memo(function CreateFieldComponent({
semanticTextInfo,
createFieldFormRef,
}: Props) {
- const { isSemanticTextEnabled, ml, setErrorsInTrainedModelDeployment } = semanticTextInfo ?? {};
+ const { isSemanticTextEnabled } = semanticTextInfo ?? {};
const dispatch = useDispatch();
+ const { fields, mappingViewFields } = useMappingsState();
const fieldTypeInputRef = useRef(null);
const { form } = useForm({
@@ -106,18 +106,40 @@ export const CreateField = React.memo(function CreateFieldComponent({
}
};
- const { createInferenceEndpoint } = useSemanticText({
- form,
- setErrorsInTrainedModelDeployment,
- ml,
- });
-
const isSemanticText = form.getFormData().type === 'semantic_text';
useEffect(() => {
if (createFieldFormRef?.current) createFieldFormRef?.current.focus();
}, [createFieldFormRef]);
+ useEffect(() => {
+ if (isSemanticText) {
+ const allSemanticFields = {
+ byId: {
+ ...fields.byId,
+ ...mappingViewFields.byId,
+ },
+ rootLevelFields: [],
+ aliases: {},
+ maxNestedDepth: 0,
+ };
+ const defaultName = getFieldByPathName(allSemanticFields, 'semantic_text')
+ ? ''
+ : 'semantic_text';
+ const referenceField =
+ Object.values(allSemanticFields.byId)
+ .find((field) => field.source.type === 'text' && !field.isMultiField)
+ ?.path.join('.') || '';
+ if (!form.getFormData().name) {
+ form.setFieldValue('name', defaultName);
+ }
+ if (!form.getFormData().reference_field) {
+ form.setFieldValue('reference_field', referenceField);
+ }
+ }
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [isSemanticText]);
+
const submitForm = async (
e?: React.FormEvent,
exitAfter: boolean = false,
@@ -287,9 +309,7 @@ export const CreateField = React.memo(function CreateFieldComponent({
{renderRequiredParametersForm()}
- {isSemanticText && (
-
- )}
+ {isSemanticText && }
{renderFormActions()}
diff --git a/x-pack/platform/plugins/shared/index_management/public/application/components/mappings_editor/components/document_fields/fields/create_field/semantic_text/use_semantic_text.test.ts b/x-pack/platform/plugins/shared/index_management/public/application/components/mappings_editor/components/document_fields/fields/create_field/semantic_text/use_semantic_text.test.ts
deleted file mode 100644
index e0ce2db2446ee..0000000000000
--- a/x-pack/platform/plugins/shared/index_management/public/application/components/mappings_editor/components/document_fields/fields/create_field/semantic_text/use_semantic_text.test.ts
+++ /dev/null
@@ -1,273 +0,0 @@
-/*
- * 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 { renderHook } from '@testing-library/react';
-import { CustomInferenceEndpointConfig, SemanticTextField } from '../../../../../types';
-import { useSemanticText } from './use_semantic_text';
-import { act } from 'react-dom/test-utils';
-
-jest.mock('../../../../../../../../hooks/use_details_page_mappings_model_management', () => ({
- useDetailsPageMappingsModelManagement: () => ({
- fetchInferenceToModelIdMap: () => ({
- '.preconfigured_elser': {
- isDeployed: false,
- isDeployable: true,
- trainedModelId: '.elser_model_2',
- },
- '.preconfigured_e5': {
- isDeployed: false,
- isDeployable: true,
- trainedModelId: '.multilingual-e5-small',
- },
- openai: {
- isDeployed: false,
- isDeployable: false,
- trainedModelId: '',
- },
- my_elser_endpoint: {
- isDeployed: false,
- isDeployable: true,
- trainedModelId: '.elser_model_2',
- },
- }),
- }),
-}));
-
-const mlMock: any = {
- mlApi: {
- inferenceModels: {
- createInferenceEndpoint: jest.fn().mockResolvedValue({}),
- },
- },
-};
-
-const mockField: Record = {
- elser_model_2: {
- name: 'name',
- type: 'semantic_text',
- inference_id: '.preconfigured_elser',
- reference_field: 'title',
- },
- e5: {
- name: 'name',
- type: 'semantic_text',
- inference_id: '.preconfigured_e5',
- reference_field: 'title',
- },
- openai: {
- name: 'name',
- type: 'semantic_text',
- inference_id: 'openai',
- reference_field: 'title',
- },
- my_elser_endpoint: {
- name: 'name',
- type: 'semantic_text',
- inference_id: 'my_elser_endpoint',
- reference_field: 'title',
- },
-};
-
-const mockConfig: Record = {
- openai: {
- taskType: 'text_embedding',
- modelConfig: {
- service: 'openai',
- service_settings: {
- api_key: 'test',
- model_id: 'text-embedding-ada-002',
- },
- },
- },
- elser: {
- taskType: 'sparse_embedding',
- modelConfig: {
- service: 'elser',
- service_settings: {
- num_allocations: 1,
- num_threads: 1,
- },
- },
- },
-};
-
-const mockDispatch = jest.fn();
-
-jest.mock('../../../../../mappings_state_context', () => ({
- useMappingsState: jest.fn().mockReturnValue({
- inferenceToModelIdMap: {
- '.preconfigured_elser': {
- isDeployed: false,
- isDeployable: true,
- trainedModelId: '.elser_model_2',
- },
- '.preconfigured_e5': {
- isDeployed: false,
- isDeployable: true,
- trainedModelId: '.multilingual-e5-small',
- },
- openai: {
- isDeployed: false,
- isDeployable: false,
- trainedModelId: '',
- },
- my_elser_endpoint: {
- isDeployed: false,
- isDeployable: true,
- trainedModelId: '.elser_model_2',
- },
- },
- fields: {
- byId: {},
- },
- mappingViewFields: { byId: {} },
- }),
- useDispatch: () => mockDispatch,
-}));
-
-jest.mock('../../../../../../component_templates/component_templates_context', () => ({
- useComponentTemplatesContext: jest.fn().mockReturnValue({
- toasts: {
- addError: jest.fn(),
- addSuccess: jest.fn(),
- },
- }),
-}));
-
-jest.mock('../../../../../../../services/api', () => ({
- getInferenceEndpoints: jest.fn().mockResolvedValue({
- data: [
- {
- inference_id: '.preconfigured_e5',
- task_type: 'text_embedding',
- service: 'elasticsearch',
- service_settings: {
- num_allocations: 1,
- num_threads: 1,
- model_id: '.multilingual-e5-small',
- },
- task_settings: {},
- },
- ],
- }),
-}));
-
-describe('useSemanticText', () => {
- let mockForm: any;
-
- beforeEach(() => {
- jest.clearAllMocks();
- mockForm = {
- form: {
- getFormData: jest.fn().mockReturnValue({
- referenceField: 'title',
- name: 'sem',
- type: 'semantic_text',
- inferenceId: 'e5',
- }),
- setFieldValue: jest.fn(),
- },
- thirdPartyModel: {
- getFormData: jest.fn().mockReturnValue({
- referenceField: 'title',
- name: 'semantic_text_openai_endpoint',
- type: 'semantic_text',
- inferenceId: 'openai',
- }),
- setFieldValue: jest.fn(),
- },
- elasticModelEndpointCreatedfromFlyout: {
- getFormData: jest.fn().mockReturnValue({
- referenceField: 'title',
- name: 'semantic_text_elserServiceType_endpoint',
- type: 'semantic_text',
- inferenceId: 'my_elser_endpoint',
- }),
- setFieldValue: jest.fn(),
- },
- };
- });
- it('should handle semantic text with third party model correctly', async () => {
- const { result } = renderHook(() =>
- useSemanticText({
- form: mockForm.thirdPartyModel,
- setErrorsInTrainedModelDeployment: jest.fn(),
- ml: mlMock,
- })
- );
- await act(async () => {
- result.current.handleSemanticText(mockField.openai, mockConfig.openai);
- });
- expect(mockDispatch).toHaveBeenCalledWith({
- type: 'field.add',
- value: mockField.openai,
- });
- expect(mlMock.mlApi.inferenceModels.createInferenceEndpoint).toHaveBeenCalledWith(
- 'openai',
- 'text_embedding',
- mockConfig.openai.modelConfig
- );
- });
-
- it('should handle semantic text correctly', async () => {
- const { result } = renderHook(() =>
- useSemanticText({
- form: mockForm.form,
- setErrorsInTrainedModelDeployment: jest.fn(),
- ml: mlMock,
- })
- );
-
- await act(async () => {
- result.current.handleSemanticText(mockField.elser_model_2);
- });
-
- expect(mockDispatch).toHaveBeenCalledWith({
- type: 'field.add',
- value: mockField.elser_model_2,
- });
- });
- it('does not call create inference endpoint api, if default endpoint already exists', async () => {
- const { result } = renderHook(() =>
- useSemanticText({
- form: mockForm.form,
- setErrorsInTrainedModelDeployment: jest.fn(),
- ml: mlMock,
- })
- );
-
- await act(async () => {
- result.current.handleSemanticText(mockField.e5);
- });
-
- expect(mockDispatch).toHaveBeenCalledWith({
- type: 'field.add',
- value: mockField.e5,
- });
-
- expect(mlMock.mlApi.inferenceModels.createInferenceEndpoint).not.toBeCalled();
- });
-
- it('handles errors correctly', async () => {
- const mockError = new Error('Test error');
- mlMock.mlApi?.inferenceModels.createInferenceEndpoint.mockImplementationOnce(() => {
- throw mockError;
- });
-
- const setErrorsInTrainedModelDeployment = jest.fn();
-
- const { result } = renderHook(() =>
- useSemanticText({ form: mockForm.form, setErrorsInTrainedModelDeployment, ml: mlMock })
- );
-
- await act(async () => {
- result.current.handleSemanticText(mockField.elser_model_2);
- });
-
- expect(setErrorsInTrainedModelDeployment).toHaveBeenCalledWith(expect.any(Function));
- });
-});
diff --git a/x-pack/platform/plugins/shared/index_management/public/application/components/mappings_editor/components/document_fields/fields/create_field/semantic_text/use_semantic_text.ts b/x-pack/platform/plugins/shared/index_management/public/application/components/mappings_editor/components/document_fields/fields/create_field/semantic_text/use_semantic_text.ts
deleted file mode 100644
index a7b380fd120cd..0000000000000
--- a/x-pack/platform/plugins/shared/index_management/public/application/components/mappings_editor/components/document_fields/fields/create_field/semantic_text/use_semantic_text.ts
+++ /dev/null
@@ -1,158 +0,0 @@
-/*
- * 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 { useCallback } from 'react';
-import { MlPluginStart } from '@kbn/ml-plugin/public';
-import React, { useEffect } from 'react';
-import { InferenceTaskType } from '@elastic/elasticsearch/lib/api/types';
-import { ElserModels } from '@kbn/ml-trained-models-utils';
-import { i18n } from '@kbn/i18n';
-import { useDetailsPageMappingsModelManagement } from '../../../../../../../../hooks/use_details_page_mappings_model_management';
-import { useDispatch, useMappingsState } from '../../../../../mappings_state_context';
-import { FormHook } from '../../../../../shared_imports';
-import { CustomInferenceEndpointConfig, Field, SemanticTextField } from '../../../../../types';
-import { useMLModelNotificationToasts } from '../../../../../../../../hooks/use_ml_model_status_toasts';
-
-import { getInferenceEndpoints } from '../../../../../../../services/api';
-import { getFieldByPathName } from '../../../../../lib/utils';
-
-interface UseSemanticTextProps {
- form: FormHook;
- ml?: MlPluginStart;
- setErrorsInTrainedModelDeployment?: React.Dispatch<
- React.SetStateAction>
- >;
-}
-interface DefaultInferenceEndpointConfig {
- taskType: InferenceTaskType;
- service: string;
-}
-
-export function useSemanticText(props: UseSemanticTextProps) {
- const { form, setErrorsInTrainedModelDeployment, ml } = props;
- const { fields, mappingViewFields } = useMappingsState();
- const { fetchInferenceToModelIdMap } = useDetailsPageMappingsModelManagement();
- const dispatch = useDispatch();
- const { showSuccessToasts, showErrorToasts } = useMLModelNotificationToasts();
-
- const fieldTypeValue = form.getFormData()?.type;
- useEffect(() => {
- if (fieldTypeValue === 'semantic_text') {
- const allFields = {
- byId: {
- ...fields.byId,
- ...mappingViewFields.byId,
- },
- rootLevelFields: [],
- aliases: {},
- maxNestedDepth: 0,
- };
- const defaultName = getFieldByPathName(allFields, 'semantic_text') ? '' : 'semantic_text';
- const referenceField =
- Object.values(allFields.byId)
- .find((field) => field.source.type === 'text' && !field.isMultiField)
- ?.path.join('.') || '';
- if (!form.getFormData().name) {
- form.setFieldValue('name', defaultName);
- }
- if (!form.getFormData().reference_field) {
- form.setFieldValue('reference_field', referenceField);
- }
- }
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [fieldTypeValue]);
-
- const createInferenceEndpoint = useCallback(
- async (
- trainedModelId: string,
- inferenceId: string,
- customInferenceEndpointConfig?: CustomInferenceEndpointConfig
- ) => {
- const isElser = ElserModels.includes(trainedModelId);
- const defaultInferenceEndpointConfig: DefaultInferenceEndpointConfig = {
- service: isElser ? 'elser' : 'elasticsearch',
- taskType: isElser ? 'sparse_embedding' : 'text_embedding',
- };
-
- const modelConfig = customInferenceEndpointConfig
- ? customInferenceEndpointConfig.modelConfig
- : {
- service: defaultInferenceEndpointConfig.service,
- service_settings: {
- adaptive_allocations: { enabled: true },
- num_threads: 1,
- model_id: trainedModelId,
- },
- };
- const taskType: InferenceTaskType =
- customInferenceEndpointConfig?.taskType ?? defaultInferenceEndpointConfig.taskType;
-
- await ml?.mlApi?.inferenceModels?.createInferenceEndpoint(inferenceId, taskType, modelConfig);
- },
- [ml?.mlApi?.inferenceModels]
- );
-
- const handleSemanticText = async (
- data: SemanticTextField,
- customInferenceEndpointConfig?: CustomInferenceEndpointConfig
- ) => {
- const modelIdMap = await fetchInferenceToModelIdMap();
- const inferenceId = data.inference_id;
- const inferenceData = modelIdMap?.[inferenceId];
- if (!inferenceData) {
- throw new Error(
- i18n.translate('xpack.idxMgmt.mappingsEditor.semanticText.inferenceError', {
- defaultMessage: 'No inference model found for inference ID {inferenceId}',
- values: { inferenceId },
- })
- );
- }
-
- const { trainedModelId } = inferenceData;
- dispatch({ type: 'field.add', value: data });
- const inferenceEndpoints = await getInferenceEndpoints();
- const hasInferenceEndpoint = inferenceEndpoints.data?.some(
- (inference) => inference.inference_id === inferenceId
- );
- // if inference endpoint exists already, do not create new inference endpoint
- if (hasInferenceEndpoint) {
- return;
- }
- try {
- // Only show toast if it's an internal Elastic model that hasn't been deployed yet
- await createInferenceEndpoint(
- trainedModelId,
- data.inference_id,
- customInferenceEndpointConfig
- );
- if (trainedModelId) {
- if (inferenceData.isDeployable && !inferenceData.isDeployed) {
- showSuccessToasts(trainedModelId);
- }
- // clear error because we've succeeded here
- setErrorsInTrainedModelDeployment?.((prevItems) => ({
- ...prevItems,
- [data.inference_id]: undefined,
- }));
- }
- } catch (error) {
- // trainedModelId is empty string when it's a third party model
- if (trainedModelId) {
- setErrorsInTrainedModelDeployment?.((prevItems) => ({
- ...prevItems,
- [data.inference_id]: error,
- }));
- }
- showErrorToasts(error);
- }
- };
-
- return {
- createInferenceEndpoint,
- handleSemanticText,
- };
-}
diff --git a/x-pack/platform/plugins/shared/index_management/public/application/hooks/use_add_endpoint.test.tsx b/x-pack/platform/plugins/shared/index_management/public/application/hooks/use_add_endpoint.test.tsx
new file mode 100644
index 0000000000000..f5f425173c2d1
--- /dev/null
+++ b/x-pack/platform/plugins/shared/index_management/public/application/hooks/use_add_endpoint.test.tsx
@@ -0,0 +1,92 @@
+/*
+ * 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 } from '@testing-library/react';
+import { useAddEndpoint } from './use_add_endpoint';
+import { useMLModelNotificationToasts } from '../../hooks/use_ml_model_status_toasts';
+import { createInferenceEndpoint } from '../services';
+
+jest.mock('../services', () => ({
+ createInferenceEndpoint: jest.fn(),
+}));
+
+jest.mock('../../hooks/use_ml_model_status_toasts', () => ({
+ useMLModelNotificationToasts: jest.fn(),
+}));
+
+const mockOnSuccess = jest.fn();
+const mockOnError = jest.fn();
+const mockShowErrorToast = jest.fn();
+const mockShowSuccessToast = jest.fn();
+
+describe('useAddEndpoint', () => {
+ const mockConfig: any = {
+ provider: 'elasticsearch',
+ taskType: 'text_embedding',
+ inferenceId: 'es-endpoint-1',
+ providerConfig: {
+ num_allocations: 1,
+ num_threads: 2,
+ model_id: '.multilingual-e5-small',
+ },
+ };
+ const mockSecrets: any = { providerSecrets: {} };
+
+ const mockInferenceEndpoint = {
+ config: mockConfig,
+ secrets: mockSecrets,
+ };
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ (useMLModelNotificationToasts as jest.Mock).mockReturnValue({
+ showInferenceCreationErrorToasts: mockShowErrorToast,
+ showInferenceSuccessToast: mockShowSuccessToast,
+ });
+ });
+
+ it('calls onSuccess and shows success toast on successful endpoint creation', async () => {
+ (createInferenceEndpoint as jest.Mock).mockResolvedValueOnce({ error: null });
+
+ const { result } = renderHook(() => useAddEndpoint());
+
+ await act(async () => {
+ await result.current.addInferenceEndpoint(mockInferenceEndpoint, mockOnSuccess, mockOnError);
+ });
+
+ expect(createInferenceEndpoint).toHaveBeenCalledWith(
+ 'text_embedding',
+ 'es-endpoint-1',
+ mockInferenceEndpoint
+ );
+ expect(mockShowSuccessToast).toHaveBeenCalledTimes(1);
+ expect(mockShowErrorToast).not.toHaveBeenCalled();
+ expect(mockOnSuccess).toHaveBeenCalledTimes(1);
+ expect(mockOnError).not.toHaveBeenCalled();
+ });
+
+ it('calls onError and shows error toast on endpoint creation failure', async () => {
+ const mockError = new Error('Endpoint creation failed');
+ (createInferenceEndpoint as jest.Mock).mockResolvedValueOnce({ error: mockError });
+
+ const { result } = renderHook(() => useAddEndpoint());
+
+ await act(async () => {
+ await result.current.addInferenceEndpoint(mockInferenceEndpoint, mockOnSuccess, mockOnError);
+ });
+
+ expect(createInferenceEndpoint).toHaveBeenCalledWith(
+ 'text_embedding',
+ 'es-endpoint-1',
+ mockInferenceEndpoint
+ );
+ expect(mockShowErrorToast).toHaveBeenCalledWith('Endpoint creation failed');
+ expect(mockShowSuccessToast).not.toHaveBeenCalled();
+ expect(mockOnError).toHaveBeenCalledTimes(1);
+ expect(mockOnSuccess).not.toHaveBeenCalled();
+ });
+});
diff --git a/x-pack/platform/plugins/shared/index_management/public/application/hooks/use_add_endpoint.ts b/x-pack/platform/plugins/shared/index_management/public/application/hooks/use_add_endpoint.ts
new file mode 100644
index 0000000000000..f5b2f98610775
--- /dev/null
+++ b/x-pack/platform/plugins/shared/index_management/public/application/hooks/use_add_endpoint.ts
@@ -0,0 +1,48 @@
+/*
+ * 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 { useCallback } from 'react';
+
+import { InferenceEndpoint } from '@kbn/inference-endpoint-ui-common';
+import { useMLModelNotificationToasts } from '../../hooks/use_ml_model_status_toasts';
+import { createInferenceEndpoint } from '../services';
+
+export const useAddEndpoint = () => {
+ const { showInferenceCreationErrorToasts, showInferenceSuccessToast } =
+ useMLModelNotificationToasts();
+
+ const addInferenceEndpoint = useCallback(
+ async (
+ inferenceEndpoint: InferenceEndpoint,
+ onSuccess?: (inferenceId: string) => void,
+ onError?: () => void
+ ) => {
+ const { error } = await createInferenceEndpoint(
+ inferenceEndpoint.config.taskType,
+ inferenceEndpoint.config.inferenceId,
+ inferenceEndpoint
+ );
+
+ if (error) {
+ showInferenceCreationErrorToasts(error?.message);
+ if (onError) {
+ onError();
+ }
+ } else {
+ showInferenceSuccessToast();
+ if (onSuccess) {
+ onSuccess(inferenceEndpoint.config.inferenceId);
+ }
+ }
+ },
+ [showInferenceCreationErrorToasts, showInferenceSuccessToast]
+ );
+
+ return {
+ addInferenceEndpoint,
+ };
+};
diff --git a/x-pack/platform/plugins/shared/index_management/public/application/services/api.ts b/x-pack/platform/plugins/shared/index_management/public/application/services/api.ts
index 8fb3524a65a0f..5939163a4fb1f 100644
--- a/x-pack/platform/plugins/shared/index_management/public/application/services/api.ts
+++ b/x-pack/platform/plugins/shared/index_management/public/application/services/api.ts
@@ -9,7 +9,8 @@ import { METRIC_TYPE } from '@kbn/analytics';
import type { SerializedEnrichPolicy } from '@kbn/index-management-shared-types';
import { IndicesStatsResponse } from '@elastic/elasticsearch/lib/api/types';
import { InferenceAPIConfigResponse } from '@kbn/ml-trained-models-utils';
-import { MappingTypeMapping } from '@elastic/elasticsearch/lib/api/types';
+import { MappingTypeMapping } from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
+import { InferenceEndpoint } from '@kbn/inference-endpoint-ui-common';
import {
API_BASE_PATH,
INTERNAL_API_BASE_PATH,
@@ -460,6 +461,18 @@ export function getInferenceEndpoints() {
});
}
+export function createInferenceEndpoint(
+ taskType: string,
+ inferenceId: string,
+ inferenceEndpoint: InferenceEndpoint
+) {
+ return sendRequest({
+ path: `/internal/inference_endpoint/endpoints/${taskType}/${inferenceId}`,
+ method: 'put',
+ body: JSON.stringify(inferenceEndpoint),
+ });
+}
+
export function useLoadInferenceEndpoints() {
return useRequest({
path: `${API_BASE_PATH}/inference/all`,
diff --git a/x-pack/platform/plugins/shared/index_management/public/application/services/index.ts b/x-pack/platform/plugins/shared/index_management/public/application/services/index.ts
index 09d9065b2b729..5ad0cadede763 100644
--- a/x-pack/platform/plugins/shared/index_management/public/application/services/index.ts
+++ b/x-pack/platform/plugins/shared/index_management/public/application/services/index.ts
@@ -28,6 +28,7 @@ export {
useLoadIndexSettings,
createIndex,
useLoadInferenceEndpoints,
+ createInferenceEndpoint,
} from './api';
export { sortTable } from './sort_table';
diff --git a/x-pack/platform/plugins/shared/index_management/public/hooks/use_ml_model_status_toasts.ts b/x-pack/platform/plugins/shared/index_management/public/hooks/use_ml_model_status_toasts.ts
index 7b553f37498d5..2b9efa7a32f30 100644
--- a/x-pack/platform/plugins/shared/index_management/public/hooks/use_ml_model_status_toasts.ts
+++ b/x-pack/platform/plugins/shared/index_management/public/hooks/use_ml_model_status_toasts.ts
@@ -46,10 +46,36 @@ export function useMLModelNotificationToasts() {
const showErrorToasts = (error: ErrorType) => {
const errorObj = extractErrorProperties(error);
return toasts.addError(new MLRequestFailure(errorObj, error), {
- title: i18n.translate('xpack.idxMgmt.mappingsEditor.createField.modelDeploymentErrorTitle', {
+ title: i18n.translate('xpack.idxMgmt.mappingsEditor.createField.inferenceErrorTitle', {
defaultMessage: 'Model deployment failed',
}),
});
};
- return { showSuccessToasts, showErrorToasts, showSuccessfullyDeployedToast };
+
+ const showInferenceCreationErrorToasts = (error: ErrorType) => {
+ const errorObj = extractErrorProperties(error);
+ return toasts.addError(new MLRequestFailure(errorObj, error), {
+ title: i18n.translate(
+ 'xpack.idxMgmt.mappingsEditor.createField.inferenceCreationErrorTitle',
+ {
+ defaultMessage: 'Endpoint creation failed',
+ }
+ ),
+ });
+ };
+
+ const showInferenceSuccessToast = () => {
+ return toasts.addSuccess({
+ title: i18n.translate('xpack.idxMgmt.mappingsEditor.createField.endpointAddedSuccess', {
+ defaultMessage: 'Inference endpoint added',
+ }),
+ });
+ };
+ return {
+ showSuccessToasts,
+ showErrorToasts,
+ showSuccessfullyDeployedToast,
+ showInferenceCreationErrorToasts,
+ showInferenceSuccessToast,
+ };
}
diff --git a/x-pack/platform/plugins/shared/index_management/tsconfig.json b/x-pack/platform/plugins/shared/index_management/tsconfig.json
index 41514049a13a8..d88eae7dbad66 100644
--- a/x-pack/platform/plugins/shared/index_management/tsconfig.json
+++ b/x-pack/platform/plugins/shared/index_management/tsconfig.json
@@ -55,6 +55,7 @@
"@kbn/unsaved-changes-prompt",
"@kbn/shared-ux-table-persist",
"@kbn/core-application-browser",
+ "@kbn/inference-endpoint-ui-common",
],
"exclude": ["target/**/*"]
}