Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding flyout to create inference endpoint from Index Management #205184

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
908ef63
update codeowners for infernece endpoint ui common package
Samiul-TheSoccerFan Dec 26, 2024
b930e9c
Adding flyout to add inference endpoint from index management for sem…
Samiul-TheSoccerFan Dec 27, 2024
91938b4
Remove redundant code and added unit tests
Samiul-TheSoccerFan Dec 27, 2024
765affd
[CI] Auto-commit changed files from 'node scripts/generate codeowners'
kibanamachine Dec 27, 2024
bf2c61f
[CI] Auto-commit changed files from 'node scripts/notice'
kibanamachine Dec 27, 2024
7135c3a
removed unnecessary interface declaration
Samiul-TheSoccerFan Jan 7, 2025
6a02208
Removing server side code from mapping
Samiul-TheSoccerFan Jan 8, 2025
c6e2e09
[CI] Auto-commit changed files from 'node scripts/notice'
kibanamachine Jan 8, 2025
91871fc
moving infernece flyout into shared package and removing redundant code
Samiul-TheSoccerFan Jan 21, 2025
0c8345e
select the new added inference endpoint
Samiul-TheSoccerFan Jan 21, 2025
6742707
remove unnecessary new line
Samiul-TheSoccerFan Jan 21, 2025
232fbc9
using endpoint creation from search infernece endpoint
Samiul-TheSoccerFan Jan 21, 2025
f307113
[CI] Auto-commit changed files from 'node scripts/yarn_deduplicate'
kibanamachine Jan 21, 2025
7ec0823
Loading flyout as async
Samiul-TheSoccerFan Jan 22, 2025
32a9231
Remove commented code
Samiul-TheSoccerFan Jan 22, 2025
1923fe1
fix conflicts and type errors
Samiul-TheSoccerFan Jan 23, 2025
c8bdda9
fix codeowners file
Samiul-TheSoccerFan Jan 23, 2025
e85afed
removing unnecessary tests
Samiul-TheSoccerFan Jan 23, 2025
adfea8f
Fixing type issues
Samiul-TheSoccerFan Jan 24, 2025
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 @@ -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
Loading