mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
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  ### Tabular Page  --------- 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:
parent
7aec3f775c
commit
e831e837fb
15 changed files with 238 additions and 314 deletions
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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.',
|
||||
}
|
||||
);
|
||||
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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)}
|
||||
|
|
|
@ -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',
|
||||
}
|
||||
);
|
||||
|
|
|
@ -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,
|
||||
];
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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} />;
|
|
@ -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}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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>,
|
||||
]}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -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;
|
||||
};
|
|
@ -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",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue