Skip to content

Commit

Permalink
Adding flyout to create inference endpoint from Index Management (ela…
Browse files Browse the repository at this point in the history
…stic#205184)

## Summary

### Current Behavior
When adding the `semantic_text` field, users can choose from the
preconfigured `elser` or `e5` endpoints. If they wish to use a
third-party endpoint or create a new `elser` or `e5` endpoint, they need
to use the API or the inference management UI.

### Planned Improvement
To streamline this process, we plan to integrate the functionality for
creating inference endpoints directly within the `semantic_text`
inference selection popover. This enhancement will allow users to create
inference endpoints without leaving the index management interface,
making it more convenient to add fields within the index management
mapping tab.


This PR includes:
- Adding inference endpoint from the `mapping` tab of `index
management`.


### Recording


https://github.com/user-attachments/assets/2f94cc93-9829-444d-a60f-0e721a4a751b



### Checklist

Check the PR satisfies the following conditions. 

Reviewers should verify this PR satisfies this list as well.

- [X] Any text added follows [EUI's writing
guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses
sentence case text and includes [i18n
support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)
- [X] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios
- [X] [Flaky Test
Runner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1) was
used on any tests changed
- [X] The PR description includes the appropriate Release Notes section,
and the correct `release_note:*` label is applied per the
[guidelines](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)

---------

Co-authored-by: kibanamachine <[email protected]>
  • Loading branch information
Samiul-TheSoccerFan and kibanamachine authored Jan 24, 2025
1 parent d4cc532 commit fc79f62
Show file tree
Hide file tree
Showing 23 changed files with 679 additions and 680 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down
Original file line number Diff line number Diff line change
@@ -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 (
<I18nProvider>
<QueryClientProvider client={queryClient}>
<Form form={form}>{children}</Form>
</QueryClientProvider>
</I18nProvider>
);
};

describe('InferenceFlyout', () => {
beforeEach(async () => {
jest.clearAllMocks();

await act(async () => {
render(
<MockFormProvider>
<InferenceFlyoutWrapper
onFlyoutClose={mockOnClose}
onSubmitSuccess={mockOnSubmitSuccess}
isEdit={false}
http={httpMock}
toasts={notificationsMock.toasts}
addInferenceEndpoint={mockAddEndpoint}
/>
</MockFormProvider>
);
});
});

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();
});
});
Original file line number Diff line number Diff line change
@@ -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<void>;
http: HttpSetup;
toasts: IToasts;
onSubmitSuccess?: (inferenceId: string) => void;
isEdit?: boolean;
}

export const InferenceFlyoutWrapper: React.FC<InferenceFlyoutWrapperProps> = ({
onFlyoutClose,
addInferenceEndpoint,
http,
toasts,
onSubmitSuccess,
isEdit,
}) => {
const inferenceCreationFlyoutId = useGeneratedHtmlId({
prefix: 'InferenceFlyoutId',
});
const closeFlyout = () => onFlyoutClose(false);
const [isLoading, setIsLoading] = useState<boolean>(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 (
<EuiFlyout
ownFocus
onClose={closeFlyout}
aria-labelledby={inferenceCreationFlyoutId}
data-test-subj="inference-flyout"
>
<EuiFlyoutHeader hasBorder data-test-subj="inference-flyout-header">
<EuiTitle size="m">
<h2 id={inferenceCreationFlyoutId}>{LABELS.ENDPOINT_TITLE}</h2>
</EuiTitle>
</EuiFlyoutHeader>
<EuiFlyoutBody>
<Form form={form}>
<InferenceServiceFormFields http={http} toasts={toasts} isEdit={isEdit} />
<EuiSpacer size="m" />
<EuiFlexGroup justifyContent="flexStart">
<EuiFlexItem grow={false}>
<EuiButton
fill
color="success"
size="m"
isLoading={form.isSubmitting || isLoading}
disabled={(!form.isValid && form.isSubmitted) || isLoading}
data-test-subj="inference-endpoint-submit-button"
onClick={handleSubmit}
>
{LABELS.SAVE}
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
</Form>
</EuiFlyoutBody>
<EuiFlyoutFooter>
<EuiFlexGroup justifyContent="spaceBetween">
<EuiFlexItem grow={false}>
<EuiButtonEmpty
data-test-subj="inference-flyout-close-button"
onClick={closeFlyout}
flush="left"
>
{LABELS.CANCEL}
</EuiButtonEmpty>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlyoutFooter>
</EuiFlyout>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -6,119 +6,14 @@
*/

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';
import { Form, useForm } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib';
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(() => ({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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',
});
Original file line number Diff line number Diff line change
Expand Up @@ -53,3 +53,8 @@ export interface Secrets {
}

export const INFERENCE_ENDPOINT_INTERNAL_API_VERSION = '1';

export interface InferenceEndpoint {
config: Config;
secrets: Secrets;
}
Loading

0 comments on commit fc79f62

Please sign in to comment.