Modify the Inference Endpoints management page based on proposed recommendations (#188783)

This PR resolves these tickets:
https://github.com/elastic/search-team/issues/7930 and
https://github.com/elastic/search-team/issues/7933.

### Empty State

![Screenshot 2024-07-24 at 3 19
21 PM](https://github.com/user-attachments/assets/b85a1d0d-a5ad-4cbd-bd26-c15c98e286dc)




### Tabular Page

![Screenshot 2024-07-24 at 3 20
34 PM](https://github.com/user-attachments/assets/a576f411-ef45-4916-92d7-d3542f42220b)

---------

Co-authored-by: Liam Thompson <32779855+leemthompo@users.noreply.github.com>
Co-authored-by: István Zoltán Szabó <istvan.szabo@elastic.co>
This commit is contained in:
Saikat Sarkar 2024-07-25 13:09:18 -06:00 committed by GitHub
parent 7aec3f775c
commit e831e837fb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 238 additions and 314 deletions

View file

@ -8,14 +8,16 @@
import { DocLinks } from '@kbn/doc-links';
class InferenceEndpointsDocLinks {
public nlpImportModel: string = '';
public supportedNlpModels: string = '';
public createInferenceEndpoint: string = '';
public semanticSearchElser: string = '';
public semanticSearchE5: string = '';
constructor() {}
setDocLinks(newDocLinks: DocLinks) {
this.nlpImportModel = newDocLinks.ml.nlpImportModel;
this.supportedNlpModels = newDocLinks.ml.supportedNlpModels;
this.createInferenceEndpoint = newDocLinks.enterpriseSearch.inferenceApiCreate;
this.semanticSearchElser = newDocLinks.enterpriseSearch.elser;
this.semanticSearchE5 = newDocLinks.enterpriseSearch.e5Model;
}
}

View file

@ -21,14 +21,7 @@ export const CANCEL = i18n.translate('xpack.searchInferenceEndpoints.cancel', {
export const MANAGE_INFERENCE_ENDPOINTS_LABEL = i18n.translate(
'xpack.searchInferenceEndpoints.allInferenceEndpoints.description',
{
defaultMessage: 'Manage your inference endpoints.',
}
);
export const ADD_ENDPOINT_LABEL = i18n.translate(
'xpack.searchInferenceEndpoints.newInferenceEndpointButtonLabel',
{
defaultMessage: 'Add endpoint',
defaultMessage: 'View and manage your deployed inference endpoints.',
}
);
@ -36,14 +29,14 @@ export const CREATE_FIRST_INFERENCE_ENDPOINT_DESCRIPTION = i18n.translate(
'xpack.searchInferenceEndpoints.addEmptyPrompt.createFirstInferenceEndpointDescription',
{
defaultMessage:
'Connect to your third-party model provider to create an inference endpoint for semantic search.',
"Inference endpoints enable you to perform inference tasks using NLP models provided by third-party services or Elastic's built-in models like ELSER and E5. Set up tasks such as text embedding, completions, reranking, and more by using the Create Inference API.",
}
);
export const START_WITH_PREPARED_ENDPOINTS_LABEL = i18n.translate(
'xpack.searchInferenceEndpoints.addEmptyPrompt.startWithPreparedEndpointsLabel',
{
defaultMessage: 'Get started quickly with our prepared endpoints:',
defaultMessage: 'Learn more about built-in NLP models:',
}
);
@ -54,23 +47,50 @@ export const ELSER_TITLE = i18n.translate(
}
);
export const LEARN_HOW_TO_CREATE_INFERENCE_ENDPOINTS_LINK = i18n.translate(
'xpack.searchInferenceEndpoints.addEmptyPrompt.learnHowToCreateInferenceEndpoints',
{
defaultMessage: 'Learn how to create inference endpoints',
}
);
export const SEMANTIC_SEARCH_WITH_ELSER_LINK = i18n.translate(
'xpack.searchInferenceEndpoints.addEmptyPrompt.semanticSearchWithElser',
{
defaultMessage: 'Semantic search with ELSER',
}
);
export const SEMANTIC_SEARCH_WITH_E5_LINK = i18n.translate(
'xpack.searchInferenceEndpoints.addEmptyPrompt.semanticSearchWithE5',
{
defaultMessage: 'Semantic search with E5 Multilingual',
}
);
export const VIEW_YOUR_MODELS_LINK = i18n.translate(
'xpack.searchInferenceEndpoints.addEmptyPrompt.viewYourModels',
{
defaultMessage: 'View your models',
}
);
export const ELSER_DESCRIPTION = i18n.translate(
'xpack.searchInferenceEndpoints.addEmptyPrompt.elserDescription',
{
defaultMessage:
'ELSER is a sparse vector NLP model trained by Elastic for semantic search. Recommended for English language.',
defaultMessage: "ELSER is Elastic's sparse vector NLP model for semantic search in English.",
}
);
export const E5_TITLE = i18n.translate('xpack.searchInferenceEndpoints.addEmptyPrompt.e5Title', {
defaultMessage: 'Multilingual E5',
defaultMessage: 'E5 Multilingual',
});
export const E5_DESCRIPTION = i18n.translate(
'xpack.searchInferenceEndpoints.addEmptyPrompt.e5Description',
{
defaultMessage:
'E5 is a dense vector NLP model that enables you to perform multi-lingual semantic search.',
'E5 is a third-party NLP model that enables you to perform multilingual semantic search by using dense vector representations.',
}
);

View file

@ -9,6 +9,16 @@ import { render, screen } from '@testing-library/react';
import React from 'react';
import { EndpointInfo } from './endpoint_info';
jest.mock('@kbn/ml-trained-models-utils', () => ({
...jest.requireActual('@kbn/ml-trained-models-utils'),
ELASTIC_MODEL_DEFINITIONS: {
'model-with-mit-license': {
license: 'MIT',
licenseUrl: 'https://abc.com',
},
},
}));
describe('RenderEndpoint component tests', () => {
describe('with cohere service', () => {
const mockEndpoint = {
@ -254,4 +264,40 @@ describe('RenderEndpoint component tests', () => {
expect(screen.queryByText('Rate limit:')).not.toBeInTheDocument();
});
});
describe('for MIT licensed models', () => {
const mockEndpointWithMitLicensedModel = {
model_id: 'model-123',
service: 'elasticsearch',
service_settings: {
num_allocations: 5,
num_threads: 10,
model_id: 'model-with-mit-license',
},
} as any;
it('renders the MIT license badge if the model is eligible', () => {
render(<EndpointInfo endpoint={mockEndpointWithMitLicensedModel} />);
const mitBadge = screen.getByTestId('mit-license-badge');
expect(mitBadge).toBeInTheDocument();
expect(mitBadge).toHaveAttribute('href', 'https://abc.com');
});
it('does not render the MIT license badge if the model is not eligible', () => {
const mockEndpointWithNonMitLicensedModel = {
model_id: 'model-123',
service: 'elasticsearch',
service_settings: {
num_allocations: 5,
num_threads: 10,
model_id: 'model-without-mit-license',
},
} as any;
render(<EndpointInfo endpoint={mockEndpointWithNonMitLicensedModel} />);
expect(screen.queryByTestId('mit-license-badge')).not.toBeInTheDocument();
});
});
});

View file

@ -6,8 +6,11 @@
*/
import React from 'react';
import { InferenceAPIConfigResponse } from '@kbn/ml-trained-models-utils';
import { EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui';
import {
InferenceAPIConfigResponse,
ELASTIC_MODEL_DEFINITIONS,
} from '@kbn/ml-trained-models-utils';
import { EuiFlexGroup, EuiFlexItem, EuiText, EuiBadge } from '@elastic/eui';
import { ServiceProviderKeys } from '../../types';
import { ModelBadge } from './model_badge';
import * as i18n from './translations';
@ -38,13 +41,31 @@ export const EndpointModelInfo: React.FC<EndpointInfoProps> = ({ endpoint }) =>
? serviceSettings.model
: undefined;
const isEligibleForMITBadge = modelId && ELASTIC_MODEL_DEFINITIONS[modelId]?.license === 'MIT';
return (
<EuiFlexGroup gutterSize="s" alignItems="center">
<EuiFlexGroup gutterSize="s" wrap alignItems="center">
{modelId && (
<EuiFlexItem grow={false}>
<ModelBadge model={modelId} />
</EuiFlexItem>
)}
{isEligibleForMITBadge && (
<EuiFlexItem grow={false}>
<EuiBadge
color="hollow"
iconType="popout"
iconSide="right"
href={ELASTIC_MODEL_DEFINITIONS[modelId].licenseUrl ?? ''}
target="_blank"
data-test-subj={'mit-license-badge'}
>
{i18n.MIT_LICENSE}
</EuiBadge>
</EuiFlexItem>
)}
<EuiFlexItem grow={false}>
<EuiText color="subdued" size="xs">
{endpointModelAtrributes(endpoint)}

View file

@ -18,3 +18,10 @@ export const ALLOCATIONS = (numAllocations: number) =>
defaultMessage: 'Allocations: {numAllocations}',
values: { numAllocations },
});
export const MIT_LICENSE = i18n.translate(
'xpack.searchInferenceEndpoints.elasticsearch.mitLicense',
{
defaultMessage: 'License: MIT',
}
);

View file

@ -57,7 +57,7 @@ export const useTableColumns = () => {
return null;
},
sortable: false,
width: '265px',
width: '185px',
},
{
field: 'type',
@ -70,7 +70,7 @@ export const useTableColumns = () => {
return null;
},
sortable: false,
width: '265px',
width: '185px',
},
actions,
];

View file

@ -6,28 +6,42 @@
*/
import React from 'react';
import { fireEvent, screen } from '@testing-library/react';
import { screen } from '@testing-library/react';
import { AddEmptyPrompt } from './add_empty_prompt';
import { renderReactTestingLibraryWithI18n as render } from '@kbn/test-jest-helpers';
import '@testing-library/jest-dom';
const setIsInferenceFlyoutVisibleMock = jest.fn();
describe('When empty prompt is loaded', () => {
beforeEach(() => {
render(<AddEmptyPrompt setIsInferenceFlyoutVisible={setIsInferenceFlyoutVisibleMock} />);
render(<AddEmptyPrompt />);
});
it('should display the description for creation of the first inference endpoint', () => {
expect(
screen.getByText(
/Connect to your third-party model provider to create an inference endpoint for semantic search./
/Inference endpoints enable you to perform inference tasks using NLP models provided by third-party services/
)
).toBeInTheDocument();
});
it('calls setIsInferenceFlyoutVisible when the addInferenceEndpoint button is clicked', async () => {
fireEvent.click(screen.getByTestId('addEndpointButtonForEmptyPrompt'));
expect(setIsInferenceFlyoutVisibleMock).toHaveBeenCalled();
it('should have a learn-more link', () => {
const learnMoreLink = screen.getByTestId('learn-how-to-create-inference-endpoints');
expect(learnMoreLink).toBeInTheDocument();
});
it('should have a view-your-models link', () => {
const learnMoreLink = screen.getByTestId('view-your-models');
expect(learnMoreLink).toBeInTheDocument();
});
it('should have a semantic-search-with-elser link', () => {
const learnMoreLink = screen.getByTestId('semantic-search-with-elser');
expect(learnMoreLink).toBeInTheDocument();
});
it('should have a semantic-search-with-e5 link', () => {
const learnMoreLink = screen.getByTestId('semantic-search-with-e5');
expect(learnMoreLink).toBeInTheDocument();
});
});

View file

@ -8,27 +8,27 @@
import React from 'react';
import {
EuiButton,
EuiPageTemplate,
EuiFlexGroup,
EuiFlexItem,
EuiImage,
EuiSpacer,
EuiLink,
} from '@elastic/eui';
import { docLinks } from '../../../common/doc_links';
import * as i18n from '../../../common/translations';
import inferenceEndpoint from '../../assets/images/inference_endpoint.svg';
import { EndpointPrompt } from './endpoint_prompt';
import { useTrainedModelPageUrl } from '../../hooks/use_trained_model_page_url';
import './add_empty_prompt.scss';
interface AddEmptyPromptProps {
setIsInferenceFlyoutVisible: (value: boolean) => void;
}
export const AddEmptyPrompt: React.FC = () => {
const trainedModelPageUrl = useTrainedModelPageUrl();
export const AddEmptyPrompt: React.FC<AddEmptyPromptProps> = ({ setIsInferenceFlyoutVisible }) => {
return (
<EuiPageTemplate.EmptyPrompt
layout="horizontal"
@ -42,18 +42,19 @@ export const AddEmptyPrompt: React.FC<AddEmptyPromptProps> = ({ setIsInferenceFl
<EuiFlexItem data-test-subj="createFirstInferenceEndpointDescription">
{i18n.CREATE_FIRST_INFERENCE_ENDPOINT_DESCRIPTION}
</EuiFlexItem>
<EuiFlexItem grow={false}>
<div>
<EuiButton
color="primary"
fill
iconType="plusInCircle"
data-test-subj="addEndpointButtonForEmptyPrompt"
onClick={() => setIsInferenceFlyoutVisible(true)}
>
{i18n.ADD_ENDPOINT_LABEL}
</EuiButton>
</div>
<EuiFlexItem>
<EuiLink
href={docLinks.createInferenceEndpoint}
target="_blank"
data-test-subj="learn-how-to-create-inference-endpoints"
>
{i18n.LEARN_HOW_TO_CREATE_INFERENCE_ENDPOINTS_LINK}
</EuiLink>
</EuiFlexItem>
<EuiFlexItem>
<EuiLink href={trainedModelPageUrl} target="_blank" data-test-subj="view-your-models">
{i18n.VIEW_YOUR_MODELS_LINK}
</EuiLink>
</EuiFlexItem>
</EuiFlexGroup>
}
@ -66,31 +67,31 @@ export const AddEmptyPrompt: React.FC<AddEmptyPromptProps> = ({ setIsInferenceFl
<EuiFlexGroup>
<EuiFlexItem>
<EndpointPrompt
setIsInferenceFlyoutVisible={setIsInferenceFlyoutVisible}
title={i18n.ELSER_TITLE}
description={i18n.ELSER_DESCRIPTION}
footer={
<EuiButton
iconType="plusInCircle"
onClick={() => setIsInferenceFlyoutVisible(true)}
<EuiLink
href={docLinks.semanticSearchElser}
target="_blank"
data-test-subj="semantic-search-with-elser"
>
{i18n.ADD_ENDPOINT_LABEL}
</EuiButton>
{i18n.SEMANTIC_SEARCH_WITH_ELSER_LINK}
</EuiLink>
}
/>
</EuiFlexItem>
<EuiFlexItem>
<EndpointPrompt
setIsInferenceFlyoutVisible={setIsInferenceFlyoutVisible}
title={i18n.E5_TITLE}
description={i18n.E5_DESCRIPTION}
footer={
<EuiButton
iconType="plusInCircle"
onClick={() => setIsInferenceFlyoutVisible(true)}
<EuiLink
href={docLinks.semanticSearchE5}
target="_blank"
data-test-subj="semantic-search-with-e5"
>
{i18n.ADD_ENDPOINT_LABEL}
</EuiButton>
{i18n.SEMANTIC_SEARCH_WITH_E5_LINK}
</EuiLink>
}
/>
</EuiFlexItem>

View file

@ -9,18 +9,12 @@ import React from 'react';
import { EuiCard } from '@elastic/eui';
interface EndpointPromptProps {
setIsInferenceFlyoutVisible: (value: boolean) => void;
title: string;
description: string;
footer: React.ReactElement;
}
export const EndpointPrompt: React.FC<EndpointPromptProps> = ({
setIsInferenceFlyoutVisible,
title,
description,
footer,
}) => (
export const EndpointPrompt: React.FC<EndpointPromptProps> = ({ title, description, footer }) => (
<EuiCard
display="plain"
textAlign="left"

View file

@ -1,18 +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 React from 'react';
import { AddEmptyPrompt } from './empty_prompt/add_empty_prompt';
interface EmptyPromptPageProps {
setIsInferenceFlyoutVisible: (value: boolean) => void;
}
export const EmptyPromptPage: React.FC<EmptyPromptPageProps> = ({
setIsInferenceFlyoutVisible,
}) => <AddEmptyPrompt setIsInferenceFlyoutVisible={setIsInferenceFlyoutVisible} />;

View file

@ -5,39 +5,28 @@
* 2.0.
*/
import React, { useState } from 'react';
import React from 'react';
import { EuiPageTemplate } from '@elastic/eui';
import { useQueryInferenceEndpoints } from '../hooks/use_inference_endpoints';
import { TabularPage } from './all_inference_endpoints/tabular_page';
import { EmptyPromptPage } from './empty_prompt_page';
import { AddEmptyPrompt } from './empty_prompt/add_empty_prompt';
import { InferenceEndpointsHeader } from './inference_endpoints_header';
import { InferenceFlyoutWrapperComponent } from './inference_flyout_wrapper_component';
export const InferenceEndpoints: React.FC = () => {
const { inferenceEndpoints } = useQueryInferenceEndpoints();
const [isInferenceFlyoutVisible, setIsInferenceFlyoutVisible] = useState<boolean>(false);
return (
<>
{inferenceEndpoints.length > 0 && (
<InferenceEndpointsHeader setIsInferenceFlyoutVisible={setIsInferenceFlyoutVisible} />
)}
{inferenceEndpoints.length > 0 && <InferenceEndpointsHeader />}
<EuiPageTemplate.Section className="eui-yScroll">
{inferenceEndpoints.length === 0 ? (
<EmptyPromptPage setIsInferenceFlyoutVisible={setIsInferenceFlyoutVisible} />
<AddEmptyPrompt />
) : (
<TabularPage inferenceEndpoints={inferenceEndpoints} />
)}
</EuiPageTemplate.Section>
{isInferenceFlyoutVisible && (
<InferenceFlyoutWrapperComponent
isInferenceFlyoutVisible={isInferenceFlyoutVisible}
setIsInferenceFlyoutVisible={setIsInferenceFlyoutVisible}
inferenceEndpoints={inferenceEndpoints}
/>
)}
</>
);
};

View file

@ -5,34 +5,40 @@
* 2.0.
*/
import { EuiButton, EuiPageTemplate } from '@elastic/eui';
import { EuiPageTemplate, EuiLink, EuiText, EuiSpacer } from '@elastic/eui';
import React from 'react';
import * as i18n from '../../common/translations';
import { docLinks } from '../../common/doc_links';
import { useTrainedModelPageUrl } from '../hooks/use_trained_model_page_url';
interface InferenceEndpointsHeaderProps {
setIsInferenceFlyoutVisible: (isVisible: boolean) => void;
}
export const InferenceEndpointsHeader: React.FC = () => {
const trainedModelPageUrl = useTrainedModelPageUrl();
export const InferenceEndpointsHeader: React.FC<InferenceEndpointsHeaderProps> = ({
setIsInferenceFlyoutVisible,
}) => (
<EuiPageTemplate.Header
css={{ '.euiPageHeaderContent > .euiFlexGroup': { flexWrap: 'wrap' } }}
data-test-subj="allInferenceEndpointsPage"
pageTitle={i18n.INFERENCE_ENDPOINT_LABEL}
description={i18n.MANAGE_INFERENCE_ENDPOINTS_LABEL}
bottomBorder={true}
rightSideItems={[
<EuiButton
key="newInferenceEndpoint"
color="primary"
iconType="plusInCircle"
data-test-subj="addEndpointButtonForAllInferenceEndpoints"
fill
onClick={() => setIsInferenceFlyoutVisible(true)}
>
{i18n.ADD_ENDPOINT_LABEL}
</EuiButton>,
]}
/>
);
return (
<EuiPageTemplate.Header
data-test-subj="allInferenceEndpointsPage"
pageTitle={i18n.INFERENCE_ENDPOINT_LABEL}
description={
<EuiText>
<p>
{i18n.MANAGE_INFERENCE_ENDPOINTS_LABEL}
<EuiSpacer size="s" />
<EuiLink
href={docLinks.createInferenceEndpoint}
target="_blank"
data-test-subj="learn-how-to-create-inference-endpoints"
>
{i18n.LEARN_HOW_TO_CREATE_INFERENCE_ENDPOINTS_LINK}
</EuiLink>
</p>
</EuiText>
}
bottomBorder={true}
rightSideItems={[
<EuiLink href={trainedModelPageUrl} target="_blank" data-test-subj="view-your-models">
{i18n.VIEW_YOUR_MODELS_LINK}
</EuiLink>,
]}
/>
);
};

View file

@ -1,187 +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 { EuiFlexItem, EuiCallOut, EuiText, EuiSpacer } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import { InferenceFlyoutWrapper } from '@kbn/inference_integration_flyout/components/inference_flyout_wrapper';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { InferenceTaskType } from '@elastic/elasticsearch/lib/api/types';
import { ModelConfig } from '@kbn/inference_integration_flyout/types';
import { extractErrorProperties } from '@kbn/ml-error-utils';
import { TrainedModelConfigResponse } from '@kbn/ml-plugin/common/types/trained_models';
import { SUPPORTED_PYTORCH_TASKS, TRAINED_MODEL_TYPE } from '@kbn/ml-trained-models-utils';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { InferenceAPIConfigResponse } from '@kbn/ml-trained-models-utils';
import * as i18n from '../../common/translations';
import { INFERENCE_ENDPOINTS_QUERY_KEY } from '../../common/constants';
import { useKibana } from '../hooks/use_kibana';
import { docLinks } from '../../common/doc_links';
interface InferenceFlyoutWrapperComponentProps {
inferenceEndpoints: InferenceAPIConfigResponse[];
isInferenceFlyoutVisible: boolean;
setIsInferenceFlyoutVisible: (isVisible: boolean) => void;
}
export const InferenceFlyoutWrapperComponent: React.FC<InferenceFlyoutWrapperComponentProps> = ({
inferenceEndpoints,
isInferenceFlyoutVisible,
setIsInferenceFlyoutVisible,
}) => {
const [inferenceAddError, setInferenceAddError] = useState<string | undefined>(undefined);
const [isCreateInferenceApiLoading, setIsCreateInferenceApiLoading] = useState(false);
const [availableTrainedModels, setAvailableTrainedModels] = useState<
TrainedModelConfigResponse[]
>([]);
const [inferenceEndpointError, setInferenceEndpointError] = useState<string | undefined>(
undefined
);
const queryClient = useQueryClient();
const {
services: { ml, notifications },
} = useKibana();
const toasts = notifications?.toasts;
const createInferenceEndpointMutation = useMutation(
async ({
inferenceId,
taskType,
modelConfig,
}: {
inferenceId: string;
taskType: InferenceTaskType;
modelConfig: ModelConfig;
}) => {
if (!ml) {
throw new Error(i18n.UNABLE_TO_CREATE_INFERENCE_ENDPOINT);
}
await ml?.mlApi?.inferenceModels?.createInferenceEndpoint(inferenceId, taskType, modelConfig);
toasts?.addSuccess({
title: i18n.ENDPOINT_ADDED_SUCCESS,
text: i18n.ENDPOINT_ADDED_SUCCESS_DESCRIPTION(inferenceId),
});
},
{
onSuccess: () => {
queryClient.invalidateQueries([INFERENCE_ENDPOINTS_QUERY_KEY]);
},
}
);
const onInferenceEndpointChange = useCallback(
async (inferenceId: string) => {
const modelsExist = inferenceEndpoints.some((i) => i.model_id === inferenceId);
if (modelsExist) {
setInferenceEndpointError(i18n.INFERENCE_ENDPOINT_ALREADY_EXISTS);
} else {
setInferenceEndpointError(undefined);
}
},
[inferenceEndpoints]
);
const onSaveInferenceCallback = useCallback(
async (inferenceId: string, taskType: InferenceTaskType, modelConfig: ModelConfig) => {
setIsCreateInferenceApiLoading(true);
createInferenceEndpointMutation
.mutateAsync({ inferenceId, taskType, modelConfig })
.catch((error) => {
const errorObj = extractErrorProperties(error);
notifications?.toasts?.addError(errorObj.message ? new Error(error.message) : error, {
title: i18n.ENDPOINT_CREATION_FAILED,
});
})
.finally(() => {
setIsCreateInferenceApiLoading(false);
});
setIsInferenceFlyoutVisible(!isInferenceFlyoutVisible);
},
[
createInferenceEndpointMutation,
isInferenceFlyoutVisible,
setIsInferenceFlyoutVisible,
notifications,
]
);
const onFlyoutClose = useCallback(() => {
setInferenceAddError(undefined);
setIsInferenceFlyoutVisible(!isInferenceFlyoutVisible);
}, [isInferenceFlyoutVisible, setIsInferenceFlyoutVisible]);
useEffect(() => {
const fetchAvailableTrainedModels = async () => {
let models;
try {
models = await ml?.mlApi?.trainedModels?.getTrainedModels();
} catch (error) {
const errorObj = extractErrorProperties(error);
if (errorObj.statusCode === 403) {
setInferenceAddError(i18n.FORBIDDEN_TO_ACCESS_TRAINED_MODELS);
} else {
setInferenceAddError(errorObj.message);
}
} finally {
setAvailableTrainedModels(models || []);
}
};
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);
return availableTrainedModelsList;
}, [availableTrainedModels]);
return (
<InferenceFlyoutWrapper
errorCallout={
inferenceAddError && (
<EuiFlexItem grow={false}>
<EuiCallOut color="danger" iconType="error" title={i18n.ERROR_TITLE}>
<EuiText>
<FormattedMessage
id="xpack.searchInferenceEndpoints.inferenceEndpoints.inferenceId.errorDescription"
defaultMessage="Error adding inference endpoint: {errorMessage}"
values={{ errorMessage: inferenceAddError }}
/>
</EuiText>
</EuiCallOut>
<EuiSpacer />
</EuiFlexItem>
)
}
onInferenceEndpointChange={onInferenceEndpointChange}
inferenceEndpointError={inferenceEndpointError}
trainedModels={trainedModels}
onSaveInferenceEndpoint={onSaveInferenceCallback}
onFlyoutClose={onFlyoutClose}
isInferenceFlyoutVisible={isInferenceFlyoutVisible}
supportedNlpModels={docLinks.supportedNlpModels}
nlpImportModel={docLinks.nlpImportModel}
isCreateInferenceApiLoading={isCreateInferenceApiLoading}
setInferenceEndpointError={setInferenceEndpointError}
/>
);
};

View file

@ -0,0 +1,30 @@
/*
* 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 { useEffect, useState } from 'react';
import { useKibana } from './use_kibana';
export const useTrainedModelPageUrl = () => {
const {
services: { ml },
} = useKibana();
const [trainedModelPageUrl, setTrainedModelPageUrl] = useState<string | undefined>(undefined);
useEffect(() => {
const fetchMlTrainedModelPageUrl = async () => {
const url = await ml?.locator?.getUrl({
page: 'trained_models',
});
setTrainedModelPageUrl(url);
};
fetchMlTrainedModelPageUrl();
}, [ml]);
return trainedModelPageUrl;
};

View file

@ -16,7 +16,6 @@
"@kbn/i18n",
"@kbn/i18n-react",
"@kbn/kibana-react-plugin",
"@kbn/inference_integration_flyout",
"@kbn/ml-plugin",
"@kbn/ml-trained-models-utils",
"@kbn/shared-ux-router",